From 5ca16fe5538d0aa4074baf38cddd06034654bb49 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 23 Sep 2026 15:56:08 +0200 Subject: [PATCH 1/5] fix: infer the platform from a shared builder passed without get() Core 4.7.0 lets `shared` take a builder such as `fromPackageJson(...)` directly. The platform was inferred from `Object.keys(cfg.shared)`, which then lists the builder's methods, so an SSR app got 'browser' without a warning. The builder is now resolved once before the inference and core receives the resolved object. The config helpers now declare their return types instead of inferring them from core. --- src/config/share-utils.spec.ts | 13 +++++++++++++ src/config/share-utils.ts | 34 ++++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/config/share-utils.spec.ts b/src/config/share-utils.spec.ts index 2fb6507..e7058a5 100644 --- a/src/config/share-utils.spec.ts +++ b/src/config/share-utils.spec.ts @@ -132,6 +132,19 @@ describe('withNativeFederation', () => { expect(mockCoreWithNativeFederation.mock.calls[0]![0].platform).toBe('browser'); }); + it('infers the platform from a shared builder passed without get()', () => { + const resolved = { '@angular/ssr': {} }; + const builder = { get: vi.fn(() => resolved) }; + + withNativeFederation({ shared: builder } as never); + + const passed = mockCoreWithNativeFederation.mock.calls[0]![0]; + expect(passed.platform).toBe('node'); + // resolved once here, so core receives the plain object rather than calling get() again + expect(passed.shared).toBe(resolved); + expect(builder.get).toHaveBeenCalledTimes(1); + }); + it('does not override an explicitly configured platform', () => { withNativeFederation({ platform: 'node', shared: { '@angular/core': {} } } as never); diff --git a/src/config/share-utils.ts b/src/config/share-utils.ts index 2a194c2..757e635 100644 --- a/src/config/share-utils.ts +++ b/src/config/share-utils.ts @@ -1,8 +1,11 @@ import type { + ConfigBuilder, + FederationConfig, + PackageJsonExternalsBuilder, + ResolvedSharedExternalsConfig, ShareAllExternalsOptions, ShareExternalsOptions, SkipList, - FederationConfig, } from "@softarc/native-federation/domain"; import { share as coreShare, @@ -11,7 +14,10 @@ import { withNativeFederation as coreWithNativeFederation, } from "@softarc/native-federation/config"; import { NG_SKIP_LIST } from "./angular-skip-list.js"; -import type { NormalizedSharedExternalsConfig } from "@softarc/native-federation/internal"; +import type { + NormalizedFederationConfig, + NormalizedSharedExternalsConfig, +} from "@softarc/native-federation/internal"; import { existsSync, readFileSync } from "node:fs"; import * as path from "node:path"; import { cwd } from "node:process"; @@ -23,7 +29,7 @@ export function shareAll( projectPath?: string; overrides?: ShareExternalsOptions; } = {}, -) { +): ResolvedSharedExternalsConfig { if (!opts.skipList) opts.skipList = NG_SKIP_LIST; return coreShareAll(config, opts); } @@ -31,8 +37,8 @@ export function shareAll( export function share( configuredShareObjects: ShareExternalsOptions, projectPath = "", - skipList = NG_SKIP_LIST, -) { + skipList: SkipList = NG_SKIP_LIST, +): ResolvedSharedExternalsConfig { return coreShare(configuredShareObjects, projectPath, skipList); } @@ -45,13 +51,17 @@ export function share( export function fromPackageJson( baseCfg: ShareAllExternalsOptions, projectPath = "", -) { +): PackageJsonExternalsBuilder { return coreFromPackageJson(baseCfg, projectPath).skip(NG_SKIP_LIST); } -export function withNativeFederation(cfg: FederationConfig) { - if (!cfg.platform) +export function withNativeFederation( + cfg: FederationConfig, +): NormalizedFederationConfig { + if (!cfg.platform) { + cfg.shared = fromBuilder(cfg.shared); cfg.platform = getDefaultPlatform(Object.keys(cfg.shared ?? {})); + } const normalized = coreWithNativeFederation(cfg); @@ -148,6 +158,14 @@ export function autoShareScope(opts: PackageShareScopeOptions = {}): string { return `${prefix}${major}.${minor}.${patch}`; } +function fromBuilder( + value: T | ConfigBuilder | undefined, +): T | undefined { + return typeof (value as ConfigBuilder | undefined)?.get === "function" + ? (value as ConfigBuilder).get() + : (value as T | undefined); +} + function removeNgLocales( shared: NormalizedSharedExternalsConfig, ): NormalizedSharedExternalsConfig { From d84fc60bd1b884d557e99b1b3f81433036faa295 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 23 Sep 2026 15:56:08 +0200 Subject: [PATCH 2/5] feat(config): export the shared and mapping config types `/config` now also exports `ExternalConfig`, `SharedExternalsConfig` and `SharedMappingEntry` next to `FederationConfig`. The builder types stay core's: they reach users through the signatures without being part of this package's API. --- src/config.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index e0bb025..a5ba010 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,9 +8,12 @@ export { } from './config/share-utils.js'; // Nothing Angular-specific to add: the skip list NG_SKIP_LIST seeds applies to npm // packages, not to workspace path mappings. -export { - mappingsFromWorkspace, - type FederationConfig, +export { mappingsFromWorkspace } from '@softarc/native-federation/config'; +export type { + ExternalConfig, + FederationConfig, + SharedExternalsConfig, + SharedMappingEntry, } from '@softarc/native-federation/config'; export { NG_SKIP_LIST } from './config/angular-skip-list.js'; export { shareAngularLocales } from './config/angular-locales.js'; From bba096184f97d0275f3d181e95118384deab77ad Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 23 Sep 2026 15:56:08 +0200 Subject: [PATCH 3/5] feat(schematics): generate the shared config with fromPackageJson `init` now writes `fromPackageJson(...).override(...)` instead of spreading `shareAll(...)` with `overrides`. The resolved config is identical; the builder no longer needs `.get()` since core 4.7.0. --- .../init/files/federation.config.mjs__tmpl__ | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/schematics/init/files/federation.config.mjs__tmpl__ b/src/schematics/init/files/federation.config.mjs__tmpl__ index 83f50ed..f59b75c 100644 --- a/src/schematics/init/files/federation.config.mjs__tmpl__ +++ b/src/schematics/init/files/federation.config.mjs__tmpl__ @@ -1,4 +1,4 @@ -import { withNativeFederation, shareAll } from '@angular-architects/native-federation/config'; +import { withNativeFederation, fromPackageJson } from '@angular-architects/native-federation/config'; export default withNativeFederation({ name: '<%=project%>', @@ -9,18 +9,12 @@ export default withNativeFederation({ './Component': './<%=appComponentPath%>', }, <% } %> - shared: { - ...shareAll( - { singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package' }, - { - overrides: { - // includeSecondaries is an opt-out of ignoreUnusedDeps, so all of - // @angular/core is shared to prevent mismatches. - '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package', includeSecondaries: { keepAll: true } }, - }, - }, - ), - }, + shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package' }) + // includeSecondaries is an opt-out of ignoreUnusedDeps, so all of + // @angular/core is shared to prevent mismatches. + .override({ + '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package', includeSecondaries: { keepAll: true } }, + }), skip: [ 'rxjs/ajax', From 88573e94b2951ed85292d4dc44a2b1ec1e2fa9a8 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 23 Sep 2026 15:56:08 +0200 Subject: [PATCH 4/5] docs: drop the trailing get() from the builder examples Also lists `fromPackageJson().filter()`, and notes that wildcard mappings need `includeSecondaries: { resolveGlob: true }` when `ignoreUnusedDeps` is off (native-federation/native-federation-core#147). --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c60da6f..ae04f12 100644 --- a/README.md +++ b/README.md @@ -527,8 +527,7 @@ export default withNativeFederation({ requiredVersion: "auto", }) .skip(["rxjs/ajax", "rxjs/fetch"]) - .override({ "large-lib": { singleton: false } }) - .get(), + .override({ "large-lib": { singleton: false } }), }); ``` @@ -536,10 +535,11 @@ The builder exposes: | Method | Purpose | | ------------------------ | -------------------------------------------------------------------- | +| `.filter(patterns)` | Narrow the shared dependencies to those matching the patterns. | | `.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. | +| `.get()` | Resolve the builder into the shared config object (optional). | 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. @@ -572,8 +572,7 @@ export default withNativeFederation({ strictVersion: true, }) .filter(["@my-org/ui/*", "@my-org/auth-lib"]) - .patch(["@my-org/ui/*"], { singleton: false }) - .get(), + .patch(["@my-org/ui/*"], { singleton: false }), }); ``` @@ -581,9 +580,11 @@ export default withNativeFederation({ | ----------------------- | ------------------------------------------------------------------------------ | | `.filter(patterns)` | Narrow the selection. Omit it to select every mapped path. | | `.patch(patterns, cfg)` | Merge a partial config into the matching mappings; never widens the selection. | -| `.get()` | Resolve the builder into the `sharedMappings` array. | +| `.get()` | Resolve the builder into the `sharedMappings` array (optional). | -Requires `@softarc/native-federation` ≥ `4.4.0`. See the [core README](https://github.com/native-federation/native-federation-core#configuring-shared-mappings) for which `ExternalConfig` properties a mapping honours, how `includeSecondaries: { keepAll: true, resolveGlob: true }` keeps mappings nothing imports, and why only barrel imports can be shared as a mapped path. +Since `@softarc/native-federation` `4.7.0`, `shared` and `sharedMappings` accept the builders directly, so the trailing `.get()` can be dropped. Add `// @ts-check` at the top of `federation.config.mjs` to have your editor check the config against the exported `FederationConfig` type. + +Requires `@softarc/native-federation` ≥ `4.4.0`. See the [core README](https://github.com/native-federation/native-federation-core#configuring-shared-mappings) for which `ExternalConfig` properties a mapping honours, how `includeSecondaries: { keepAll: true, resolveGlob: true }` keeps mappings nothing imports, and why only barrel imports can be shared as a mapped path. Note that with `ignoreUnusedDeps: false` a wildcard mapping (`@my-org/ui/*`) is dropped unless it sets `includeSecondaries: { resolveGlob: true }`: without the pruning scan nothing expands the wildcard into entry points. ### SSR and Hydration From f4cb3123b99172626e0cada97072e8783839b86d Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 23 Sep 2026 16:04:11 +0200 Subject: [PATCH 5/5] feat(schematics): patch @angular/core instead of overriding it `.patch()` merges `includeSecondaries: { keepAll: true }` into the entry `fromPackageJson` already built, so the base options are not repeated. It also stops `@angular/core` from carrying a `version` taken from the declared range (`^22.0.0` -> `22.0.0`), which its secondaries inherited over the installed version. --- src/schematics/init/files/federation.config.mjs__tmpl__ | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/schematics/init/files/federation.config.mjs__tmpl__ b/src/schematics/init/files/federation.config.mjs__tmpl__ index f59b75c..c3a61ba 100644 --- a/src/schematics/init/files/federation.config.mjs__tmpl__ +++ b/src/schematics/init/files/federation.config.mjs__tmpl__ @@ -12,9 +12,7 @@ export default withNativeFederation({ shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package' }) // includeSecondaries is an opt-out of ignoreUnusedDeps, so all of // @angular/core is shared to prevent mismatches. - .override({ - '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto', build: 'package', includeSecondaries: { keepAll: true } }, - }), + .patch(['@angular/core'], { includeSecondaries: { keepAll: true } }), skip: [ 'rxjs/ajax',