Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,27 @@ export default withNativeFederation({

When enabled, instead of listing each chunk as a separate shared dependency, chunks are grouped by bundle name in a dedicated `chunks` object. Each shared dependency gets a `bundle` property linking it to its chunk bundle. This results in a smaller `remoteEntry.json` and allows chunks to be skipped if the dependency is not used in the final import map.

#### Dense Externals

The `denseExternals` feature flag applies the same compaction to shared externals themselves:

```js
export default withNativeFederation({
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
features: {
denseExternals: true,
},
});
```

When enabled, each shared external is emitted as a single dense entry that carries its output filenames in an `entries` map, rather than one flat record per file. This further shrinks the `remoteEntry.json`. The Angular I18N build understands both the flat and dense shapes, so localization keeps translating shared bundles either way.

### Version-Pinned Share Scopes

A `shareScope` isolates shared dependencies into a named bucket, so packages are only shared between remotes that use the same scope. The `autoShareScope` helper derives that name from a dependency's declared version, letting you pin sharing to a version line without hardcoding the number.
Expand Down Expand Up @@ -467,6 +488,36 @@ export default withNativeFederation({

The version is read from `dependencies`, `devDependencies` or `peerDependencies` in your `package.json`. `autoShareScope` throws if the dependency isn't declared, or if the declared version lacks enough segments for the requested `level`.

### Building the Shared Config from `package.json`

As an alternative to `shareAll`, the `fromPackageJson` helper builds the shared config from your `package.json` and returns a fluent builder you can refine before handing it to `withNativeFederation`:

```js
import { withNativeFederation, fromPackageJson } from '@angular-architects/native-federation/config';

export default withNativeFederation({
shared: fromPackageJson({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
})
.skip(['rxjs/ajax', 'rxjs/fetch'])
.override({ 'large-lib': { singleton: false } })
.get(),
});
```

The builder exposes:

| Method | Purpose |
| ------ | ------- |
| `.skip(externals)` | Add packages to the skip list, on top of the ones seeded by default. |
| `.override(externals)` | Replace the sharing options for specific packages. |
| `.patch(externals, cfg)` | Merge a partial config into the given packages. |
| `.get()` | Resolve the builder into the shared config object. |

Unlike the core `fromPackageJson`, this adapter's version pre-seeds the Angular skip list (`NG_SKIP_LIST`) — the same list `shareAll` uses — so Angular-internal and localization packages are skipped for you out of the box.

### SSR and Hydration

We support Angular's SSR and (Incremental) Hydration. Please find [more information here](https://www.angulararchitects.io/blog/ssr-and-hydration-with-native-federation-for-angular/).
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"@angular-devkit/core": "~22.0.0",
"@angular-devkit/schematics": "~22.0.0",
"@chialab/esbuild-plugin-commonjs": "^0.19.0",
"@softarc/native-federation": "^4.0.0",
"@softarc/native-federation-orchestrator": "^4.2.2",
"@softarc/native-federation": "^4.3.0",
"@softarc/native-federation-orchestrator": "^4.5.0",
"es-module-shims": "^2.8.0",
"esbuild": "^0.28.0",
"mrmime": "^2.0.1"
Expand Down
615 changes: 311 additions & 304 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/builders/build/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ describe('translateFederationArtifacts', () => {
expect(cmd).toContain('["a.xlf","b.xlf"]');
});

it('collects dense shared entries when denseExternals is enabled', async () => {
const denseResult = {
shared: [{ entries: { esm: 'dep1.js', legacy: 'dep1.legacy.js' } }],
exposes: [{ outFileName: './cmp.js' }],
chunks: { c1: ['chunk1.js'] },
} as unknown as FederationInfo;

await translateFederationArtifacts(i18n, true, '/dist', denseResult);

const cmd = vi.mocked(execSync).mock.calls[0]![0] as string;
expect(cmd).toContain('-s "{dep1.js,dep1.legacy.js,./cmp.js,chunk1.js}"');
});

it('uses sourceLocale.code when the source locale is an object', async () => {
const objLocaleI18n: I18nConfig = {
sourceLocale: { code: 'en-US' },
Expand Down
4 changes: 3 additions & 1 deletion src/builders/build/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export async function translateFederationArtifacts(
const translationOutPath = path.join(outputPath, 'browser', '{{LOCALE}}');

const federationFiles = [
...federationResult.shared.map(s => s.outFileName),
...federationResult.shared.flatMap(s =>
'entries' in s ? Object.values(s.entries) : [s.outFileName]
),
...federationResult.exposes.map(e => e.outFileName),
...Object.values(federationResult.chunks ?? {}).flat(),
];
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
share,
shareAll,
fromPackageJson,
withNativeFederation,
autoShareScope,
type PackageShareScopeOptions,
Expand Down
25 changes: 25 additions & 0 deletions src/config/share-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
share,
shareAll,
fromPackageJson,
withNativeFederation,
getDefaultPlatform,
autoShareScope,
Expand All @@ -11,11 +12,14 @@ import { NG_SKIP_LIST } from './angular-skip-list.js';
const mockCoreShare = vi.fn((cfg: unknown) => cfg);
const mockCoreShareAll = vi.fn((cfg: unknown) => cfg);
const mockCoreWithNativeFederation = vi.fn();
const mockFromPackageJsonSkip = vi.fn(() => ({ get: () => 'resolved' }));
const mockCoreFromPackageJson = vi.fn(() => ({ skip: mockFromPackageJsonSkip }));

vi.mock('@softarc/native-federation/config', () => ({
DEFAULT_SKIP_LIST: [],
share: (...args: unknown[]) => mockCoreShare(...args),
shareAll: (...args: unknown[]) => mockCoreShareAll(...args),
fromPackageJson: (...args: unknown[]) => mockCoreFromPackageJson(...args),
withNativeFederation: (...args: unknown[]) => mockCoreWithNativeFederation(...args),
}));

Expand Down Expand Up @@ -63,6 +67,27 @@ describe('shareAll', () => {
});
});

describe('fromPackageJson', () => {
it('delegates to core with default projectPath and pre-seeds the Angular skip list', () => {
const baseCfg = { singleton: true } as never;

fromPackageJson(baseCfg);

expect(mockCoreFromPackageJson).toHaveBeenCalledWith(baseCfg, '');
expect(mockFromPackageJsonSkip).toHaveBeenCalledWith(NG_SKIP_LIST);
});

it('passes through an explicit projectPath', () => {
fromPackageJson({} as never, '/project');

expect(mockCoreFromPackageJson).toHaveBeenCalledWith({}, '/project');
});

it('returns the skip-seeded builder from core', () => {
expect(fromPackageJson({} as never)).toEqual({ get: expect.any(Function) });
});
});

describe('share', () => {
it('delegates with default projectPath and NG_SKIP_LIST', () => {
const config = { rxjs: { singleton: true } } as never;
Expand Down
14 changes: 14 additions & 0 deletions src/config/share-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import {
share as coreShare,
shareAll as coreShareAll,
fromPackageJson as coreFromPackageJson,
withNativeFederation as coreWithNativeFederation,
} from "@softarc/native-federation/config";
import { NG_SKIP_LIST } from "./angular-skip-list.js";
Expand Down Expand Up @@ -35,6 +36,19 @@ export function share(
return coreShare(configuredShareObjects, projectPath, skipList);
}

/**
* Angular-flavored {@link coreFromPackageJson}: builds a shared-externals config
* from `package.json` and pre-seeds the Angular skip list, so the returned
* fluent builder (`.skip()`, `.override()`, `.patch()`, `.get()`) starts from
* {@link NG_SKIP_LIST} instead of core's `DEFAULT_SKIP_LIST`.
*/
export function fromPackageJson(
baseCfg: ShareAllExternalsOptions,
projectPath = "",
) {
return coreFromPackageJson(baseCfg, projectPath).skip(NG_SKIP_LIST);
}

export function withNativeFederation(cfg: FederationConfig) {
if (!cfg.platform)
cfg.platform = getDefaultPlatform(Object.keys(cfg.shared ?? {}));
Expand Down
Loading