Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8cdcbc9
fix: Enhanced package.json export support
Aukevanoost Dec 6, 2025
926d985
fix: Exhaustive search for packages
Aukevanoost Dec 6, 2025
cd8de01
fix: Correct federation export and added cache breaker when switching…
Aukevanoost Dec 6, 2025
f01eefe
fix(1006): Support for windows paths
Aukevanoost Dec 3, 2025
3b40c04
fix: Switch to env separator
Aukevanoost Dec 4, 2025
d566d5a
chore(nf): update to 21.0.2
manfredsteyer Dec 4, 2025
4718499
fix(native-federation): resolveGlob is now false by default to preven…
Aukevanoost Dec 12, 2025
1c0ae53
chore: format files
Aukevanoost Dec 12, 2025
8d15538
fix(native-federation): Bug where wrong variable was used as export
Aukevanoost Dec 12, 2025
2913d6f
fix(native-federation): Allows to opt-out of removeUnusedDeps
Aukevanoost Dec 12, 2025
4a3c025
chore: Removed redundant double-negations
Aukevanoost Dec 12, 2025
50c25ce
fix(native-federation): Stable isESM check
Aukevanoost Dec 12, 2025
9140eb9
fix(native-federation): Added promise to wait for dispose function
Aukevanoost Dec 10, 2025
ecba53f
fix(native-federation): callback is now synchronous.
Aukevanoost Dec 10, 2025
f21e000
fix: Bumped target to es2022
Aukevanoost Dec 11, 2025
3d315d0
fix(native-federation): Added typing to wrapped plugin
Aukevanoost Dec 11, 2025
5c29743
chore(nf): update version to 21.0.3
manfredsteyer Dec 14, 2025
3232c8a
fix(native-federation): Allow for agressive wildcard filter with seco…
Aukevanoost Dec 15, 2025
2869ff2
fix: Added choice between package level isolation or separate seconda…
Aukevanoost Dec 15, 2025
22984fc
Merge branch 'main' into fix/secondary-entry-points-II
Aukevanoost Dec 17, 2025
a811e73
fix(native-federation): Updated docs
Aukevanoost Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions libs/native-federation-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ The method `federationBuilder.build` bundles the shared and exposed parts of you

The `withNativeFederation` function sets up a configuration for your applications. This is an example configuration for a host:

The `shareAll` helper shares all your dependencies defined in your `package.json`. The `package.json` is look up as described above:

```typescript
// shell/federation.config.js

Expand All @@ -163,6 +165,152 @@ module.exports = withNativeFederation({
});
```

The options passed to shareAll are applied to all dependencies found in your `package.json`.

This might come in handy in an mono repo scenario and when doing some experiments/ trouble shooting.

> Since v21.1 it's also possible to add overrides to the shareAll for specific packages.

```typescript
// shell/federation.config.js

const { withNativeFederation, shareAll } = require('@softarc/native-federation/build');

module.exports = withNativeFederation({
name: 'host',

shared: {
...shareAll(
{
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
{
overrides: {
'package-a/themes/xyz': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: { skip: '@package-a/themes/xyz/*' },
build: 'package',
},
'package-b': {
singleton: false,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: { skip: 'package-b/icons/*' },
build: 'package',
},
},
},
),
},
});
```

### Share Helper

The helper function share adds some additional options for the shared dependencies:

```typescript
shared: share({
"package-a": {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: true
},
[...]
})
```

The added options are `requireVersion: 'auto'` and `includeSecondaries`.

#### requireVersion: 'auto'

If you set `requireVersion` to `'auto'`, the helper takes the version defined in your `package.json`.

This helps to solve issues with not (fully) met peer dependencies and secondary entry points (see Pitfalls section below).

By default, it takes the `package.json` that is closest to the caller (normally the `webpack.config.js`). However, you can pass the path to an other `package.json` using the second optional parameter. Also, you need to define the shared libray within the node dependencies in your `package.json`.

Instead of setting requireVersion to auto time and again, you can also skip this option and call `setInferVersion(true)` before:

```typescript
setInferVersion(true);
```

#### includeSecondaries

If set to `true`, all secondary entry points are added too. In the case of `@angular/common` this is also `@angular/common/http`, `@angular/common/http/testing`, `@angular/common/testing`, `@angular/common/http/upgrade`, and `@angular/common/locales`. This exhaustive list shows that using this option for `@angular/common` is not the best idea because normally, you don't need most of them.

> `includeSecondaries` is true by default.

However, this option can come in handy for quick experiments or if you want to quickly share a package like `@angular/material` that comes with a myriad of secondary entry points.

Even if you share too much, Native Federation will only load the needed ones at runtime. However, please keep in mind that shared packages can not be tree-shaken.

To skip some secondary entry points, you can assign a configuration option instead of `true`:

```typescript
shared: share({
"@angular/common": {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: {
skip: ['@angular/common/http/testing']
}
},
[...]
})
```

### includeSecondaries

Since v21 it's also possible to resolve Glob exports by enabling the `globResolve` property:

```typescript
shared: share({
"package-a": {
singleton: true,
strictVersion: true,
requiredVersion: "auto",
includeSecondaries: {resolveGlob: true}
},
[...]
})
```

This is disabled by default since it will create a bundle of every valid exported file it finds, **Only use this feature in combination with `ignoreUnusedDeps` flag**. If you want to specifically skip certain parts of the glob export, you can also use the wildcard in the skip section:

```typescript
shared: share({
"package-a/themes/xyz": {
singleton: true,
strictVersion: true,
requiredVersion: "auto",
includeSecondaries: {skip: "package-a/themes/xyz/*", resolveGlob: true}
},
[...]
})
```

Finally, it's also possible to break out of the "removeUnusedDep" for a specific external if desired, for example when sharing a whole suite of external modules. This can be handy when you want to avoid the chance of cross-version secondary entrypoints being used by the different micro frontends. E.g. mfe1 uses @angular/core v20.1.0 and mfe2 uses @angular/core/rxjs-interop v20.0.8, then you might want to use consistent use of v20.1.0 so rxjs-interop should be exported by mfe1. The "keepAll" prop allows you to enforce this:

```typescript
shared: share({
"@angular/core": {
singleton: true,
strictVersion: true,
requiredVersion: "auto",
includeSecondaries: {keepAll: true}
},
[...]
})
```

The API for configuring and using Native Federation is very similar to the one provided by our Module Federation plugin [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/native-federation). Hence, most the articles on it are also valid for Native Federation.

### Sharing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface NormalizedSharedConfig {
version?: string;
includeSecondaries?: boolean;
platform: 'browser' | 'node';
build: 'default' | 'separate';
build: 'default' | 'separate' | 'package';
packageInfo?: {
entryPoint: string;
version: string;
Expand Down
76 changes: 57 additions & 19 deletions libs/native-federation-core/src/lib/config/share-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export const DEFAULT_SECONDARIES_SKIP_LIST = [
'@angular/common/upgrade',
];

type IncludeSecondariesOptions = { skip: string | string[] } | boolean;
type IncludeSecondariesOptions =
| { skip: string | string[]; resolveGlob?: boolean; keepAll?: boolean }
| boolean;
type CustomSharedConfig = SharedConfig & {
includeSecondaries?: IncludeSecondariesOptions;
};
Expand Down Expand Up @@ -124,9 +126,13 @@ function _findSecondaries(
const secondaryLibName = s
.replace(/\\/g, '/')
.replace(/^.*node_modules[/]/, '');
if (excludes.includes(secondaryLibName)) {
continue;
}

const inCustomSkipList = excludes.some(
(e) =>
e === secondaryLibName ||
(e.endsWith('*') && secondaryLibName.startsWith(e.slice(0, -1))),
);
if (inCustomSkipList) continue;

if (isInSkipList(secondaryLibName, preparedSkipList)) {
continue;
Expand Down Expand Up @@ -159,12 +165,14 @@ function getSecondaries(
): Record<string, SharedConfig> | null {
let exclude = [...DEFAULT_SECONDARIES_SKIP_LIST];

let resolveGlob = false;
if (typeof includeSecondaries === 'object') {
if (Array.isArray(includeSecondaries.skip)) {
exclude = includeSecondaries.skip;
} else if (typeof includeSecondaries.skip === 'string') {
exclude = [includeSecondaries.skip];
}
resolveGlob = !!includeSecondaries.resolveGlob;
}

// const libPath = path.join(path.dirname(packagePath), 'node_modules', key);
Expand All @@ -179,6 +187,7 @@ function getSecondaries(
exclude,
shareObject,
preparedSkipList,
resolveGlob,
);
if (configured) {
return configured;
Expand All @@ -200,6 +209,7 @@ function readConfiguredSecondaries(
exclude: string[],
shareObject: SharedConfig,
preparedSkipList: PreparedSkipList,
resolveGlob: boolean,
): Record<string, SharedConfig> | null {
const libPackageJson = path.join(libPath, 'package.json');

Expand Down Expand Up @@ -237,9 +247,12 @@ function readConfiguredSecondaries(
for (const key of keys) {
const secondaryName = path.join(parent, key).replace(/\\/g, '/');

if (exclude.includes(secondaryName)) {
continue;
}
const inCustomSkipList = exclude.some(
(e) =>
e === secondaryName ||
(e.endsWith('*') && secondaryName.startsWith(e.slice(0, -1))),
);
if (inCustomSkipList) continue;

if (isInSkipList(secondaryName, preparedSkipList)) {
continue;
Expand All @@ -260,13 +273,14 @@ function readConfiguredSecondaries(
continue;
}

const items = resolveSecondaries(
const items = resolveGlobSecondaries(
key,
libPath,
parent,
secondaryName,
entry,
{ discovered: discoveredFiles, skip: exclude },
resolveGlob,
);
items.forEach((e) =>
discoveredFiles.add(typeof e === 'string' ? e : e.value),
Expand All @@ -293,16 +307,18 @@ function readConfiguredSecondaries(
return result;
}

function resolveSecondaries(
function resolveGlobSecondaries(
key: string,
libPath: string,
parent: string,
secondaryName: string,
entry: string,
excludes: { discovered: Set<string>; skip: string[] },
resolveGlob: boolean,
): Array<string | KeyValuePair> {
let items: Array<string | KeyValuePair> = [];
if (key.includes('*')) {
if (!resolveGlob) return items;
const expanded = resolveWildcardKeys(key, entry, libPath);
items = expanded
.map((e) => ({
Expand Down Expand Up @@ -363,12 +379,14 @@ function getDefaultEntry(

export function shareAll(
config: CustomSharedConfig = {},
skip: SkipList = DEFAULT_SKIP_LIST,
projectPath = '',
opts: {
skipList?: SkipList;
projectPath?: string;
overrides?: Config;
} = {},
): Config | null {
// let workspacePath: string | undefined = undefined;

projectPath = inferProjectPath(projectPath);
const projectPath = inferProjectPath(opts.projectPath);

// workspacePath = getConfigContext().workspaceRoot ?? '';

Expand All @@ -377,31 +395,50 @@ export function shareAll(
// }

const versionMaps = getVersionMaps(projectPath, projectPath);
const share: Record<string, unknown> = {};
const preparedSkipList = prepareSkipList(skip);
const sharedExternals: Config = {};
const preparedSkipList = prepareSkipList(opts.skipList ?? DEFAULT_SKIP_LIST);

for (const versions of versionMaps) {
for (const key in versions) {
if (isInSkipList(key, preparedSkipList)) {
continue;
}
if (
!!opts.overrides &&
Object.keys(opts.overrides).some((o) => key.startsWith(o))
) {
continue;
}

const inferVersion =
!config.requiredVersion || config.requiredVersion === 'auto';
const requiredVersion = inferVersion
? versions[key]
: config.requiredVersion;

if (!share[key]) {
share[key] = { ...config, requiredVersion };
if (!sharedExternals[key]) {
sharedExternals[key] = { ...config, requiredVersion };
}
}
}

return module.exports.share(share, projectPath, skip);
return {
...share(
sharedExternals,
opts.projectPath,
opts.skipList ?? DEFAULT_SKIP_LIST,
),
...(!opts.overrides
? {}
: share(
opts.overrides,
opts.projectPath,
opts.skipList ?? DEFAULT_SKIP_LIST,
)),
};
}

function inferProjectPath(projectPath: string) {
function inferProjectPath(projectPath: string | undefined) {
if (!projectPath && getConfigContext().packageJson) {
projectPath = path.dirname(getConfigContext().packageJson || '');
}
Expand Down Expand Up @@ -570,6 +607,7 @@ export function share(
if (shareObject.includeSecondaries) {
includeSecondaries = shareObject.includeSecondaries;
delete shareObject.includeSecondaries;
if (includeSecondaries?.keepAll) shareObject.includeSecondaries = true;
}

result[key] = shareObject;
Expand Down
Loading