From 9631d42e90fb5e73f3dd3188c271081760381737 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Fri, 7 Aug 2026 14:34:08 +0200 Subject: [PATCH 1/7] fix(bundler): add exposed modules to the federation tsconfig updateFederationTsConfig filtered out every entry point whose fileName starts with '.', which is exactly how core hands over exposes -- their `file` is passed through verbatim from federation.config and is workspace-root-relative. Only shared mappings (absolute) ever made it into `include`, so exposed modules had to be listed by hand or the angular-compiler plugin failed with "File 'x' not found in TypeScript compilation". The extraction refactor in b044cf5 dropped the half of createFederationTsConfig that assigned those local entry points to `files`. Restore the behaviour by resolving relative fileNames against the workspace root instead of discarding them, and append them to `include` rather than overwriting `files` -- the config is now the user's own tsconfig, not a generated copy. The optimizedMappings gate moves into the function: mappings are still only added once ignoreUnusedDeps has pruned them, but exposes are added unconditionally. Refs #113 --- src/tools/esbuild/angular-bundler.spec.ts | 14 +++- src/tools/esbuild/angular-bundler.ts | 4 +- .../create-federation-tsconfig.spec.ts | 69 +++++++++++++++---- .../esbuild/create-federation-tsconfig.ts | 21 ++++-- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/tools/esbuild/angular-bundler.spec.ts b/src/tools/esbuild/angular-bundler.spec.ts index b86c389..617279e 100644 --- a/src/tools/esbuild/angular-bundler.spec.ts +++ b/src/tools/esbuild/angular-bundler.spec.ts @@ -91,10 +91,22 @@ describe('createAngularEsbuildContext', () => { expect(updateFederationTsConfig).toHaveBeenCalledWith( workspaceRoot, 'apps/example/tsconfig.app.json', - expect.anything() + expect.anything(), + true ); expect(lastBuildOptions().tsconfig).toBe( path.join(workspaceRoot, 'apps/example/tsconfig.app.json') ); }); + + it('still updates the tsconfig when mappings are not optimized, so exposes land in the program', async () => { + await createAngularEsbuildContext(makeOptions({ optimizedMappings: false })); + + expect(updateFederationTsConfig).toHaveBeenCalledWith( + workspaceRoot, + 'apps/example/tsconfig.app.json', + expect.anything(), + false + ); + }); }); diff --git a/src/tools/esbuild/angular-bundler.ts b/src/tools/esbuild/angular-bundler.ts index d792a2f..770f32f 100644 --- a/src/tools/esbuild/angular-bundler.ts +++ b/src/tools/esbuild/angular-bundler.ts @@ -78,9 +78,7 @@ export async function createAngularEsbuildContext(options: NormalizedContextOpti } } - if (optimizedMappings) { - updateFederationTsConfig(workspaceRoot, tsConfigPath, entryPoints); - } + updateFederationTsConfig(workspaceRoot, tsConfigPath, entryPoints, optimizedMappings); tsConfigPath = path.join(workspaceRoot, tsConfigPath); diff --git a/src/tools/esbuild/create-federation-tsconfig.spec.ts b/src/tools/esbuild/create-federation-tsconfig.spec.ts index b228695..f2b10fe 100644 --- a/src/tools/esbuild/create-federation-tsconfig.spec.ts +++ b/src/tools/esbuild/create-federation-tsconfig.spec.ts @@ -18,35 +18,76 @@ describe('updateFederationTsConfig', () => { vi.mocked(fs.writeFileSync).mockReset(); }); - it('returns early without touching fs when all entry points are local', () => { - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('./local-a'), entry('./local-b')]); + it('returns early without touching fs when there are no entry points', () => { + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [], true); expect(fs.readFileSync).not.toHaveBeenCalled(); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); - it('appends non-local entry points relative to the tsconfig dir, skipping locals', () => { + it('resolves workspace-root-relative exposes against the workspace root', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: [] }) as never); + + updateFederationTsConfig( + '/ws', + 'projects/mfe1/tsconfig.fed.json', + [entry('./projects/mfe1/src/bootstrap.ts')], + true + ); + + const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); + expect(written.include).toEqual(['src/bootstrap.ts']); + }); + + it('appends absolute mapping entry points relative to the tsconfig dir', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['existing.ts'] }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [ - entry('/ws/src/a.ts'), - entry('./skip.ts'), - ]); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); expect(fs.writeFileSync).toHaveBeenCalledTimes(1); const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); expect(written.include).toEqual(['existing.ts', 'src/a.ts']); }); + it('skips mapping entry points but keeps exposes when mappings are not optimized', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: [] }) as never); + + updateFederationTsConfig( + '/ws', + 'tsconfig.fed.json', + [entry('/ws/libs/unused/src/index.ts'), entry('./src/bootstrap.ts')], + false + ); + + const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); + expect(written.include).toEqual(['src/bootstrap.ts']); + }); + + it('returns early without touching fs when only mappings are present and unoptimized', () => { + updateFederationTsConfig( + '/ws', + 'tsconfig.fed.json', + [entry('/ws/libs/unused/src/index.ts')], + false + ); + + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + it('does not duplicate an include that is already present', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['src/a.ts'] }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [ - entry('/ws/src/a.ts'), - entry('/ws/src/b.ts'), - ]); + updateFederationTsConfig( + '/ws', + 'tsconfig.fed.json', + [entry('/ws/src/a.ts'), entry('/ws/src/b.ts')], + true + ); const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); expect(written.include).toEqual(['src/a.ts', 'src/b.ts']); @@ -56,7 +97,7 @@ describe('updateFederationTsConfig', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ compilerOptions: {} }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')]); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); expect(written.include).toEqual(['src/a.ts']); @@ -70,7 +111,7 @@ describe('updateFederationTsConfig', () => { .spyOn(path, 'relative') .mockReturnValue('..\\libs\\shared\\src\\index.ts'); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/libs/shared/src/index.ts')]); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/libs/shared/src/index.ts')], true); const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); expect(written.include).toEqual(['../libs/shared/src/index.ts']); @@ -82,7 +123,7 @@ describe('updateFederationTsConfig', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['src/a.ts'] }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')]); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); diff --git a/src/tools/esbuild/create-federation-tsconfig.ts b/src/tools/esbuild/create-federation-tsconfig.ts index 9a59432..e3763bc 100644 --- a/src/tools/esbuild/create-federation-tsconfig.ts +++ b/src/tools/esbuild/create-federation-tsconfig.ts @@ -4,21 +4,28 @@ import fs from 'fs'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'util'; -/** - * Updates the federation tsconfig to include optimized mapping entry points. - * Only modifies the file when there are non-local entry points to add. - */ +// Adds the federation entry points to the federation tsconfig, so the angular-compiler +// plugin finds them in the TypeScript program. export function updateFederationTsConfig( workspaceRoot: string, tsConfigPath: string, - entryPoints: EntryPoint[] + entryPoints: EntryPoint[], + optimizedMappings: boolean ): void { const fullTsConfigPath = path.join(workspaceRoot, tsConfigPath); const tsconfigDir = path.dirname(fullTsConfigPath); + // Core hands exposes over workspace-root-relative and shared mappings absolute. + // Unpruned, the mappings carry every path mapping in the workspace, used or not. const filtered = entryPoints - .filter(ep => !ep.fileName.startsWith('.')) - .map(ep => path.relative(tsconfigDir, ep.fileName).replace(/\\/g, '/')); + .filter(ep => optimizedMappings || !path.isAbsolute(ep.fileName)) + .map(ep => { + const fileName = path.isAbsolute(ep.fileName) + ? ep.fileName + : path.join(workspaceRoot, ep.fileName); + + return path.relative(tsconfigDir, fileName).replace(/\\/g, '/'); + }); if (filtered.length === 0) { return; From 5c329de3d47e8098a12ecb546476ce0f6e245774 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Fri, 7 Aug 2026 14:52:00 +0200 Subject: [PATCH 2/7] feat(schematics): scaffold tsconfig.federation.json and wire it up The init schematic never created a federation tsconfig and never set `tsConfig` on the generated build/serve targets, so the federation build fell back to the app tsconfig and then wrote its entry points into it. A fresh clone also hit ENOENT because the builder updates that file but never creates it (#101). Generate `/tsconfig.federation.json` extending the app tsconfig -- it drives esbuild's module resolution, so it needs the workspace paths -- with an empty program the builder fills in per build. Point both the build and serve targets at it. Refs #101, #113 --- src/schematics/init/schematic.ts | 5 +- .../generate-federation-tsconfig.spec.ts | 123 ++++++++++++++++++ .../steps/generate-federation-tsconfig.ts | 48 +++++++ .../init/steps/update-workspace-config.ts | 5 +- 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/schematics/init/steps/generate-federation-tsconfig.spec.ts create mode 100644 src/schematics/init/steps/generate-federation-tsconfig.ts diff --git a/src/schematics/init/schematic.ts b/src/schematics/init/schematic.ts index 61946cc..9503272 100644 --- a/src/schematics/init/schematic.ts +++ b/src/schematics/init/schematic.ts @@ -12,6 +12,7 @@ import { updatePolyfills } from './steps/update-polyfills.js'; import { generateRemoteMap } from './steps/generate-remote-map.js'; import { generateFederationConfig } from './steps/generate-federation-config.js'; import { updateWorkspaceConfig } from './steps/update-workspace-config.js'; +import { generateFederationTsConfig } from './steps/generate-federation-tsconfig.js'; import { addDependencies } from './steps/add-dependencies.js'; import { makeMainAsync } from './steps/make-main-async.js'; import { makeServerAsync } from './steps/make-server-async.js'; @@ -69,7 +70,9 @@ export default function config(options: NfSchematicSchema): Rule { const ssr = isSsrProject(normalized); const server = ssr ? getSsrFilePath(normalized) : ''; - updateWorkspaceConfig(tree, normalized, workspace, workspaceFileName, ssr); + const federationTsConfig = generateFederationTsConfig(tree, normalized); + + updateWorkspaceConfig(tree, normalized, workspace, workspaceFileName, ssr, federationTsConfig); addDependencies(tree, context, ssr); diff --git a/src/schematics/init/steps/generate-federation-tsconfig.spec.ts b/src/schematics/init/steps/generate-federation-tsconfig.spec.ts new file mode 100644 index 0000000..cd4564a --- /dev/null +++ b/src/schematics/init/steps/generate-federation-tsconfig.spec.ts @@ -0,0 +1,123 @@ +import { EmptyTree, type Tree } from '@angular-devkit/schematics'; + +import { generateFederationTsConfig } from './generate-federation-tsconfig.js'; +import type { NormalizedOptions } from './normalize-options.js'; + +function makeOptions(overrides: Partial = {}): NormalizedOptions { + return { + polyfills: [] as unknown as string, + projectName: 'mfe1', + projectRoot: 'projects/mfe1', + projectSourceRoot: 'projects/mfe1/src', + manifestPath: '', + manifestRelPath: '', + main: 'projects/mfe1/src/main.ts', + port: 4200, + projectConfig: { + architect: { + build: { + builder: '@angular/build:application', + options: { tsConfig: 'projects/mfe1/tsconfig.app.json' }, + }, + }, + }, + ...overrides, + }; +} + +function read(tree: Tree, path: string) { + return JSON.parse(tree.read(path)!.toString('utf8')); +} + +describe('generateFederationTsConfig', () => { + let tree: Tree; + + beforeEach(() => { + tree = new EmptyTree(); + }); + + it('creates a federation tsconfig extending the app tsconfig', () => { + const result = generateFederationTsConfig(tree, makeOptions()); + + expect(result).toBe('projects/mfe1/tsconfig.federation.json'); + expect(read(tree, result)).toEqual({ + extends: './tsconfig.app.json', + files: [], + include: ['src/**/*.d.ts'], + }); + }); + + it('derives the include glob from the project source root', () => { + const result = generateFederationTsConfig( + tree, + makeOptions({ projectSourceRoot: 'projects/mfe1/app-src' }) + ); + + expect(read(tree, result).include).toEqual(['app-src/**/*.d.ts']); + }); + + it('points extends at a tsconfig that lives outside the project root', () => { + const result = generateFederationTsConfig( + tree, + makeOptions({ + projectConfig: { + architect: { + build: { + builder: '@angular/build:application', + options: { tsConfig: 'tsconfig.app.json' }, + }, + }, + }, + }) + ); + + expect(read(tree, result).extends).toBe('../../tsconfig.app.json'); + }); + + it('leaves an existing federation tsconfig untouched', () => { + tree.create('projects/mfe1/tsconfig.federation.json', '{ "files": ["src/bootstrap.ts"] }'); + + const result = generateFederationTsConfig(tree, makeOptions()); + + expect(read(tree, result)).toEqual({ files: ['src/bootstrap.ts'] }); + }); + + it('does nothing when the project is already on the federation builder', () => { + const options = makeOptions(); + options.projectConfig.architect.build.builder = '@angular-architects/native-federation:build'; + + const result = generateFederationTsConfig(tree, options); + + expect(tree.exists(result)).toBe(false); + }); + + // esbuild is where a previous run parked the original build target. + it('falls back to the esbuild target tsConfig', () => { + const result = generateFederationTsConfig( + tree, + makeOptions({ + projectConfig: { + architect: { + build: { builder: '@angular/build:application', options: {} }, + esbuild: { options: { tsConfig: 'projects/mfe1/tsconfig.app.json' } }, + }, + }, + }) + ); + + expect(read(tree, result).extends).toBe('./tsconfig.app.json'); + }); + + it('throws when no tsConfig can be found', () => { + expect(() => + generateFederationTsConfig( + tree, + makeOptions({ + projectConfig: { + architect: { build: { builder: '@angular/build:application', options: {} } }, + }, + }) + ) + ).toThrow('has no tsConfig'); + }); +}); diff --git a/src/schematics/init/steps/generate-federation-tsconfig.ts b/src/schematics/init/steps/generate-federation-tsconfig.ts new file mode 100644 index 0000000..d998d6b --- /dev/null +++ b/src/schematics/init/steps/generate-federation-tsconfig.ts @@ -0,0 +1,48 @@ +import type { Tree } from '@angular-devkit/schematics'; +import type { NormalizedOptions } from './normalize-options.js'; +import * as path from 'path'; + +const NF_BUILDER = '@angular-architects/native-federation:build'; + +function toPosix(p: string): string { + return p.replace(/\\/g, '/'); +} + +// The federation build compiles exposes and shared mappings, not the app entry, so it +// starts from an empty program that the builder fills in per build. It extends the app +// tsconfig because it also drives esbuild's module resolution and so needs its paths. +export function generateFederationTsConfig(tree: Tree, options: NormalizedOptions): string { + const { projectConfig, projectRoot, projectSourceRoot } = options; + + const federationTsConfig = toPosix(path.join(projectRoot, 'tsconfig.federation.json')); + + if (projectConfig.architect.build.builder === NF_BUILDER || tree.exists(federationTsConfig)) { + return federationTsConfig; + } + + const appTsConfig = + projectConfig.architect.build.options?.tsConfig ?? + projectConfig.architect.esbuild?.options?.tsConfig; + + if (!appTsConfig) { + throw new Error(`The build target of ${options.projectName} has no tsConfig!`); + } + + const extendsPath = toPosix(path.relative(projectRoot, appTsConfig)); + const sourceDir = toPosix(path.relative(projectRoot, projectSourceRoot)); + + tree.create( + federationTsConfig, + JSON.stringify( + { + extends: extendsPath.startsWith('.') ? extendsPath : `./${extendsPath}`, + files: [], + include: [`${sourceDir}/**/*.d.ts`], + }, + null, + 2 + ) + ); + + return federationTsConfig; +} diff --git a/src/schematics/init/steps/update-workspace-config.ts b/src/schematics/init/steps/update-workspace-config.ts index 36624fb..582415d 100644 --- a/src/schematics/init/steps/update-workspace-config.ts +++ b/src/schematics/init/steps/update-workspace-config.ts @@ -6,7 +6,8 @@ export function updateWorkspaceConfig( options: NormalizedOptions, workspace: any, workspaceFileName: string, - ssr: boolean + ssr: boolean, + federationTsConfig: string ) { const { projectConfig, projectName, port } = options; @@ -43,6 +44,7 @@ export function updateWorkspaceConfig( builder: '@angular-architects/native-federation:build', options: { cacheExternalArtifacts: true, + tsConfig: federationTsConfig, }, configurations: { production: { @@ -96,6 +98,7 @@ export function updateWorkspaceConfig( builder: '@angular-architects/native-federation:build', options: { target: `${projectName}:serve-original:development`, + tsConfig: federationTsConfig, rebuildDelay: 500, cacheExternalArtifacts: true, dev: true, From 6cbeaad2be79248dee9d9e6217137e4f99ac89c0 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Fri, 7 Aug 2026 15:34:51 +0200 Subject: [PATCH 3/7] fix(bundler): only manage a tsconfig the NF target declares itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The federation tsconfig is now the builder's to rewrite only when the target points at one. Without `tsConfig` the builder falls back to the Angular target's own tsconfig, whose include already covers the exposes; rewriting that stripped its comments for no gain. `files` replaces `include` as the key the builder owns: it is an explicit file list, is not filtered by the inherited `exclude`, and rewriting it wholesale prunes entry points that were renamed or removed. The schematic seeds it from the component it exposes, so the first build is a no-op, and a build without entry points of its own falls back to main.ts rather than handing the compiler an empty program. Drops the `optimizedMappings` filter. With `ignoreUnusedDeps: false` core hands over every tsconfig path mapping, used or not, and all of them are bundled — leaving them out of the program failed the build with "not found in TypeScript compilation". update22 backfills the tsconfig for projects federated earlier; its collection version moves to 22.1.1 so `ng update` reaches them. --- migration-collection.json | 4 +- src/builders/build/builder.ts | 14 +- src/builders/build/schema.d.ts | 13 ++ src/builders/build/schema.json | 2 +- src/builders/remote/builder.ts | 16 ++- src/builders/remote/schema.json | 2 +- src/schematics/init/schematic.ts | 11 +- .../generate-federation-tsconfig.spec.ts | 34 +++-- .../steps/generate-federation-tsconfig.ts | 71 +++++++--- src/schematics/update22/schematic.spec.ts | 124 ++++++++++++++++++ src/schematics/update22/schematic.ts | 83 +++++++++++- src/tools/esbuild/angular-bundler.spec.ts | 28 ++-- src/tools/esbuild/angular-bundler.ts | 13 +- .../create-federation-tsconfig.spec.ts | 111 ++++++++-------- .../esbuild/create-federation-tsconfig.ts | 49 +++---- 15 files changed, 446 insertions(+), 129 deletions(-) create mode 100644 src/schematics/update22/schematic.spec.ts diff --git a/migration-collection.json b/migration-collection.json index b26a6c6..3697745 100644 --- a/migration-collection.json +++ b/migration-collection.json @@ -10,10 +10,10 @@ "description": "migrating to v18" }, "update22": { - "version": "22.0.0", + "version": "22.1.1", "factory": "./src/schematics/update22/schematic", "schema": "./src/schematics/update22/schema.json", - "description": "migrating native-federation to the v22 ESM standard" + "description": "migrating native-federation to the v22 ESM standard and generating a tsconfig.federation.json per federated project" } } } diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index 21aa119..2efaa52 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -216,11 +216,20 @@ export async function* runBuilder( ? nfBuilderOptions.tsConfig : ngBuilderOptions.tsConfig; + const entryPoints: string[] | undefined = + nfBuilderOptions.entryPoints && nfBuilderOptions.entryPoints.length > 0 + ? nfBuilderOptions.entryPoints + : [path.join(path.dirname(federationTsConfig), "src/main.ts")]; + const adapter = createAngularBuildAdapter( { ...ngBuilderOptions, plugins: nfBuilderOptions.plugins, instrumentForCoverage: nfBuilderOptions.instrumentForCoverage, + // Deliberately not `federationTsConfig`: only a tsconfig the target declares itself is + // the builder's to rewrite, never the Angular target's own one it falls back to. + managedTsConfig: nfBuilderOptions.tsConfig, + fallbackEntryPoints: entryPoints, }, context, ); @@ -265,11 +274,6 @@ export async function* runBuilder( ? browserOutputPath : path.join(outputOptions.base, outputOptions.browser, localeFilter[0]!); - const entryPoints: string[] | undefined = - nfBuilderOptions.entryPoints && nfBuilderOptions.entryPoints.length > 0 - ? nfBuilderOptions.entryPoints - : [path.join(path.dirname(federationTsConfig), "src/main.ts")]; - const cachePath = getDefaultCachePath(context.workspaceRoot); const normalized = await normalizeFederationOptions( diff --git a/src/builders/build/schema.d.ts b/src/builders/build/schema.d.ts index 374711e..9df0d82 100644 --- a/src/builders/build/schema.d.ts +++ b/src/builders/build/schema.d.ts @@ -32,4 +32,17 @@ export type NfInternalOptions = { * Used exclusively for tests and shouldn't be used for other kinds of builds. */ instrumentForCoverage?: (filename: string) => boolean; + + /** + * The federation tsconfig, set only when the NF target declares one. That file is the + * builder's to manage (see tools/esbuild/create-federation-tsconfig.ts); when it is absent + * the builder falls back to the Angular target's own tsconfig, which must not be rewritten. + */ + managedTsConfig?: string; + + /** + * Roots keeping the federation program non-empty when a build has no entry points of its + * own — core's reachability entry points, which default to the project's main.ts. + */ + fallbackEntryPoints?: string[]; }; diff --git a/src/builders/build/schema.json b/src/builders/build/schema.json index 1daeea7..4ac94a6 100644 --- a/src/builders/build/schema.json +++ b/src/builders/build/schema.json @@ -61,7 +61,7 @@ }, "tsConfig": { "type": "string", - "description": "A specific tsconfig file for the nf remotes and exposed modules. It also drives esbuild's module resolution, so it must declare or extend the workspace baseUrl/paths." + "description": "A specific tsconfig file for the nf remotes and exposed modules. It also drives esbuild's module resolution, so it must declare or extend the workspace baseUrl/paths. The builder owns this file's `files` array and rewrites it on every build; comments are not preserved. Leave it unset to compile against the Angular target's own tsconfig, which the builder never rewrites." }, "cacheExternalArtifacts": { "type": "boolean", diff --git a/src/builders/remote/builder.ts b/src/builders/remote/builder.ts index 37a5d19..799585c 100644 --- a/src/builders/remote/builder.ts +++ b/src/builders/remote/builder.ts @@ -64,10 +64,6 @@ export async function* runRemoteBuilder( context ); - const adapter = createAngularBuildAdapter(ngBuilderOptions, context); - setBuildAdapter(adapter); - setLogLevel(nfBuilderOptions.verbose ? 'verbose' : 'info'); - // Unlike the regular build builder, remote never bundles a main.ts / polyfills. // Entry points come from the schema override or, when omitted, from the // `exposes` map in federation.config.{mjs,js} (resolved by normalizeFederationOptions). @@ -75,6 +71,18 @@ export async function* runRemoteBuilder( ? nfBuilderOptions.entryPoints : undefined; + const adapter = createAngularBuildAdapter( + { + ...ngBuilderOptions, + // Required by the schema, so the tsconfig is always the builder's to manage. + managedTsConfig: federationTsConfig, + fallbackEntryPoints: entryPoints, + }, + context + ); + setBuildAdapter(adapter); + setLogLevel(nfBuilderOptions.verbose ? 'verbose' : 'info'); + const cachePath = getDefaultCachePath(context.workspaceRoot); const normalized = await normalizeFederationOptions( diff --git a/src/builders/remote/schema.json b/src/builders/remote/schema.json index 5f83a36..d3441fe 100644 --- a/src/builders/remote/schema.json +++ b/src/builders/remote/schema.json @@ -8,7 +8,7 @@ "properties": { "tsConfig": { "type": "string", - "description": "Path to the tsconfig used to compile the exposed modules and shared mappings. It also drives esbuild's module resolution, so it must declare or extend the workspace baseUrl/paths." + "description": "Path to the tsconfig used to compile the exposed modules and shared mappings. It also drives esbuild's module resolution, so it must declare or extend the workspace baseUrl/paths. The builder owns this file's `files` array and rewrites it on every build; comments are not preserved." }, "dev": { "type": "boolean", diff --git a/src/schematics/init/schematic.ts b/src/schematics/init/schematic.ts index 9503272..f9d27fc 100644 --- a/src/schematics/init/schematic.ts +++ b/src/schematics/init/schematic.ts @@ -70,7 +70,16 @@ export default function config(options: NfSchematicSchema): Rule { const ssr = isSsrProject(normalized); const server = ssr ? getSsrFilePath(normalized) : ''; - const federationTsConfig = generateFederationTsConfig(tree, normalized); + // Seed the federation program with what the generated config exposes, so the first build + // finds the tsconfig already correct. Where the exposes are unknown (a host, a config we + // did not write, or a project without a recognisable app component) main.ts stands in — + // the same fallback the builder applies. + const exposesAppComponent = + !exists && options.type === 'remote' && appComponent !== 'update-this.ts'; + + const federationTsConfig = generateFederationTsConfig(tree, normalized, [ + exposesAppComponent ? appComponent : main, + ]); updateWorkspaceConfig(tree, normalized, workspace, workspaceFileName, ssr, federationTsConfig); diff --git a/src/schematics/init/steps/generate-federation-tsconfig.spec.ts b/src/schematics/init/steps/generate-federation-tsconfig.spec.ts index cd4564a..7e76fee 100644 --- a/src/schematics/init/steps/generate-federation-tsconfig.spec.ts +++ b/src/schematics/init/steps/generate-federation-tsconfig.spec.ts @@ -3,6 +3,8 @@ import { EmptyTree, type Tree } from '@angular-devkit/schematics'; import { generateFederationTsConfig } from './generate-federation-tsconfig.js'; import type { NormalizedOptions } from './normalize-options.js'; +const EXPOSED = ['projects/mfe1/src/app/app.ts']; + function makeOptions(overrides: Partial = {}): NormalizedOptions { return { polyfills: [] as unknown as string, @@ -37,20 +39,33 @@ describe('generateFederationTsConfig', () => { }); it('creates a federation tsconfig extending the app tsconfig', () => { - const result = generateFederationTsConfig(tree, makeOptions()); + const result = generateFederationTsConfig(tree, makeOptions(), EXPOSED); expect(result).toBe('projects/mfe1/tsconfig.federation.json'); expect(read(tree, result)).toEqual({ extends: './tsconfig.app.json', - files: [], + files: ['src/app/app.ts'], include: ['src/**/*.d.ts'], }); }); + // An empty `files` list is a TypeScript error (TS18002) unless the config also extends + // another one, so neither key may be dropped from the generated shape. + it('always emits both extends and a non-empty files list', () => { + const result = generateFederationTsConfig(tree, makeOptions(), [ + 'projects/mfe1/src/main.ts', + ]); + + const tsconfig = read(tree, result); + expect(tsconfig.extends).toBeTruthy(); + expect(tsconfig.files).toEqual(['src/main.ts']); + }); + it('derives the include glob from the project source root', () => { const result = generateFederationTsConfig( tree, - makeOptions({ projectSourceRoot: 'projects/mfe1/app-src' }) + makeOptions({ projectSourceRoot: 'projects/mfe1/app-src' }), + EXPOSED ); expect(read(tree, result).include).toEqual(['app-src/**/*.d.ts']); @@ -68,7 +83,8 @@ describe('generateFederationTsConfig', () => { }, }, }, - }) + }), + EXPOSED ); expect(read(tree, result).extends).toBe('../../tsconfig.app.json'); @@ -77,7 +93,7 @@ describe('generateFederationTsConfig', () => { it('leaves an existing federation tsconfig untouched', () => { tree.create('projects/mfe1/tsconfig.federation.json', '{ "files": ["src/bootstrap.ts"] }'); - const result = generateFederationTsConfig(tree, makeOptions()); + const result = generateFederationTsConfig(tree, makeOptions(), EXPOSED); expect(read(tree, result)).toEqual({ files: ['src/bootstrap.ts'] }); }); @@ -86,7 +102,7 @@ describe('generateFederationTsConfig', () => { const options = makeOptions(); options.projectConfig.architect.build.builder = '@angular-architects/native-federation:build'; - const result = generateFederationTsConfig(tree, options); + const result = generateFederationTsConfig(tree, options, EXPOSED); expect(tree.exists(result)).toBe(false); }); @@ -102,7 +118,8 @@ describe('generateFederationTsConfig', () => { esbuild: { options: { tsConfig: 'projects/mfe1/tsconfig.app.json' } }, }, }, - }) + }), + EXPOSED ); expect(read(tree, result).extends).toBe('./tsconfig.app.json'); @@ -116,7 +133,8 @@ describe('generateFederationTsConfig', () => { projectConfig: { architect: { build: { builder: '@angular/build:application', options: {} } }, }, - }) + }), + EXPOSED ) ).toThrow('has no tsConfig'); }); diff --git a/src/schematics/init/steps/generate-federation-tsconfig.ts b/src/schematics/init/steps/generate-federation-tsconfig.ts index d998d6b..d7e217e 100644 --- a/src/schematics/init/steps/generate-federation-tsconfig.ts +++ b/src/schematics/init/steps/generate-federation-tsconfig.ts @@ -8,25 +8,33 @@ function toPosix(p: string): string { return p.replace(/\\/g, '/'); } -// The federation build compiles exposes and shared mappings, not the app entry, so it -// starts from an empty program that the builder fills in per build. It extends the app -// tsconfig because it also drives esbuild's module resolution and so needs its paths. -export function generateFederationTsConfig(tree: Tree, options: NormalizedOptions): string { - const { projectConfig, projectRoot, projectSourceRoot } = options; +export function federationTsConfigPath(projectRoot: string): string { + return toPosix(path.join(projectRoot, 'tsconfig.federation.json')); +} - const federationTsConfig = toPosix(path.join(projectRoot, 'tsconfig.federation.json')); +export interface FederationTsConfigOptions { + projectRoot: string; + projectSourceRoot: string; + /** Workspace-relative path of the tsconfig to extend, usually the app's. */ + appTsConfig: string; + /** Workspace-relative entry points seeding the program. */ + entryPoints: string[]; +} - if (projectConfig.architect.build.builder === NF_BUILDER || tree.exists(federationTsConfig)) { - return federationTsConfig; - } +/** + * Writes the tsconfig the federation build compiles against. It covers the exposes and shared + * mappings rather than the app entry, so `files` is a plain list of entry points that the + * builder rewrites per build (see tools/esbuild/create-federation-tsconfig.ts) and `include` + * only picks up ambient declarations. It extends the app tsconfig because it also drives + * esbuild's module resolution and so needs its paths. + * + * Both `extends` and `files` have to stay present: TypeScript reports an empty `files` list + * (TS18002) unless the config also extends another one. + */ +export function writeFederationTsConfig(tree: Tree, options: FederationTsConfigOptions): string { + const { projectRoot, projectSourceRoot, appTsConfig, entryPoints } = options; - const appTsConfig = - projectConfig.architect.build.options?.tsConfig ?? - projectConfig.architect.esbuild?.options?.tsConfig; - - if (!appTsConfig) { - throw new Error(`The build target of ${options.projectName} has no tsConfig!`); - } + const federationTsConfig = federationTsConfigPath(projectRoot); const extendsPath = toPosix(path.relative(projectRoot, appTsConfig)); const sourceDir = toPosix(path.relative(projectRoot, projectSourceRoot)); @@ -36,7 +44,7 @@ export function generateFederationTsConfig(tree: Tree, options: NormalizedOption JSON.stringify( { extends: extendsPath.startsWith('.') ? extendsPath : `./${extendsPath}`, - files: [], + files: entryPoints.map(entry => toPosix(path.relative(projectRoot, entry))), include: [`${sourceDir}/**/*.d.ts`], }, null, @@ -46,3 +54,32 @@ export function generateFederationTsConfig(tree: Tree, options: NormalizedOption return federationTsConfig; } + +export function generateFederationTsConfig( + tree: Tree, + options: NormalizedOptions, + entryPoints: string[] +): string { + const { projectConfig, projectRoot, projectSourceRoot } = options; + + const federationTsConfig = federationTsConfigPath(projectRoot); + + if (projectConfig.architect.build.builder === NF_BUILDER || tree.exists(federationTsConfig)) { + return federationTsConfig; + } + + const appTsConfig = + projectConfig.architect.build.options?.tsConfig ?? + projectConfig.architect.esbuild?.options?.tsConfig; + + if (!appTsConfig) { + throw new Error(`The build target of ${options.projectName} has no tsConfig!`); + } + + return writeFederationTsConfig(tree, { + projectRoot, + projectSourceRoot, + appTsConfig, + entryPoints, + }); +} diff --git a/src/schematics/update22/schematic.spec.ts b/src/schematics/update22/schematic.spec.ts new file mode 100644 index 0000000..0e62cfe --- /dev/null +++ b/src/schematics/update22/schematic.spec.ts @@ -0,0 +1,124 @@ +import { EmptyTree, type Tree } from '@angular-devkit/schematics'; + +import update22 from './schematic.js'; + +const NF_BUILDER = '@angular-architects/native-federation:build'; + +// A project as the init schematic left it before v22.1.1: NF build/serve targets with no +// tsConfig, and the original application builder parked under `esbuild`. +function makeWorkspace(overrides: Record = {}) { + return { + projects: { + mfe1: { + root: 'projects/mfe1', + sourceRoot: 'projects/mfe1/src', + architect: { + build: { builder: NF_BUILDER, options: { cacheExternalArtifacts: true } }, + esbuild: { + builder: '@angular/build:application', + options: { + browser: 'projects/mfe1/src/main.ts', + tsConfig: 'projects/mfe1/tsconfig.app.json', + }, + }, + serve: { builder: NF_BUILDER, options: { target: 'mfe1:serve-original:development' } }, + }, + ...overrides, + }, + }, + }; +} + +function seed(tree: Tree, workspace: unknown) { + tree.create('angular.json', JSON.stringify(workspace)); + return tree; +} + +function readJson(tree: Tree, path: string) { + return JSON.parse(tree.read(path)!.toString('utf8')); +} + +function architect(tree: Tree, project = 'mfe1') { + return readJson(tree, 'angular.json').projects[project].architect; +} + +describe('update22 — federation tsconfig', () => { + let tree: Tree; + + beforeEach(() => { + tree = new EmptyTree(); + }); + + it('generates the federation tsconfig and wires every NF target to it', async () => { + seed(tree, makeWorkspace()); + + await update22()(tree, {} as never); + + expect(readJson(tree, 'projects/mfe1/tsconfig.federation.json')).toEqual({ + extends: './tsconfig.app.json', + files: ['src/main.ts'], + include: ['src/**/*.d.ts'], + }); + + const targets = architect(tree); + expect(targets.build.options.tsConfig).toBe('projects/mfe1/tsconfig.federation.json'); + expect(targets.serve.options.tsConfig).toBe('projects/mfe1/tsconfig.federation.json'); + // Untouched: the app build keeps compiling against its own tsconfig. + expect(targets.esbuild.options.tsConfig).toBe('projects/mfe1/tsconfig.app.json'); + }); + + it('is idempotent', async () => { + seed(tree, makeWorkspace()); + + await update22()(tree, {} as never); + const afterFirst = readJson(tree, 'angular.json'); + + // The second run must not throw on the tsconfig it already created. + await update22()(tree, {} as never); + + expect(readJson(tree, 'angular.json')).toEqual(afterFirst); + }); + + it('keeps a tsConfig the target already declares', async () => { + const workspace = makeWorkspace(); + workspace.projects.mfe1.architect.build.options.tsConfig = 'projects/mfe1/custom.json'; + seed(tree, workspace); + + await update22()(tree, {} as never); + + expect(architect(tree).build.options.tsConfig).toBe('projects/mfe1/custom.json'); + }); + + it('leaves an existing federation tsconfig alone but still wires it up', async () => { + seed(tree, makeWorkspace()); + tree.create('projects/mfe1/tsconfig.federation.json', '{ "files": ["src/bootstrap.ts"] }'); + + await update22()(tree, {} as never); + + expect(readJson(tree, 'projects/mfe1/tsconfig.federation.json')).toEqual({ + files: ['src/bootstrap.ts'], + }); + expect(architect(tree).build.options.tsConfig).toBe('projects/mfe1/tsconfig.federation.json'); + }); + + it('skips projects that are not federated', async () => { + seed(tree, { + projects: { + app: { + root: 'projects/app', + sourceRoot: 'projects/app/src', + architect: { + build: { + builder: '@angular/build:application', + options: { tsConfig: 'projects/app/tsconfig.app.json' }, + }, + }, + }, + }, + }); + + await update22()(tree, {} as never); + + expect(tree.exists('projects/app/tsconfig.federation.json')).toBe(false); + }); +}); diff --git a/src/schematics/update22/schematic.ts b/src/schematics/update22/schematic.ts index e09b7ae..25832ef 100644 --- a/src/schematics/update22/schematic.ts +++ b/src/schematics/update22/schematic.ts @@ -1,5 +1,9 @@ import type { Rule, Tree } from "@angular-devkit/schematics"; import { getWorkspaceFileName } from "../init/schematic.js"; +import { + federationTsConfigPath, + writeFederationTsConfig, +} from "../init/steps/generate-federation-tsconfig.js"; import * as path from "path"; @@ -9,7 +13,9 @@ const BETA_PACKAGE = "@angular-architects/native-federation-v4"; const NF_BUILDER = `${NF_PACKAGE}:build`; const BETA_BUILDER = `${BETA_PACKAGE}:build`; -// `ng update` migration for v22: brings every project onto the ESM standard. +// `ng update` migration for v22: brings every project onto the ESM standard and onto its own +// federation tsconfig. Every step re-runs safely, so the collection entry can be bumped to a +// later version to reach projects that already ran an earlier one. export default function update22(): Rule { return async function (tree: Tree) { const workspaceFileName = getWorkspaceFileName(tree); @@ -18,11 +24,86 @@ export default function update22(): Rule { ); normalizeBuilderReferences(tree, workspace, workspaceFileName); + generateFederationTsConfigs(tree, workspace, workspaceFileName); migrateFederationConfigs(tree, workspace); normalizeMainTsImports(tree, workspace); }; } +/** + * Give every federated project its own tsconfig.federation.json. Without one the builder + * compiles the federation artifacts against the Angular target's tsconfig — the whole app, + * for every build — and would have to rewrite that file to add the exposes to its program. + */ +function generateFederationTsConfigs( + tree: Tree, + workspace: any, + workspaceFileName: string, +): void { + let modified = false; + + for (const projectName of Object.keys(workspace.projects ?? {})) { + const project = workspace.projects[projectName]; + const architect = project?.architect ?? {}; + + const targets = Object.values(architect).filter( + (target: any) => + target?.builder === NF_BUILDER && !target?.options?.tsConfig, + ) as { options?: Record }[]; + + if (targets.length === 0) { + continue; + } + + const projectRoot: string = (project.root ?? "").replace(/\\/g, "/"); + const federationTsConfig = federationTsConfigPath(projectRoot); + + if (!tree.exists(federationTsConfig)) { + // `esbuild` is where the init schematic parked the original build target. + const original = architect.esbuild ?? architect.build; + const appTsConfig = original?.options?.tsConfig; + + if (!appTsConfig) { + console.warn( + `Skipping ${projectName}: its build target has no tsConfig, so ` + + `${federationTsConfig} cannot be generated.`, + ); + continue; + } + + const projectSourceRoot: string = ( + project.sourceRoot ?? path.join(projectRoot, "src") + ).replace(/\\/g, "/"); + + writeFederationTsConfig(tree, { + projectRoot, + projectSourceRoot, + appTsConfig, + // The exposes live in federation.config.mjs, which is not ours to parse; main.ts + // keeps the program non-empty until the first build fills in the real entries. + entryPoints: [ + original.options.browser ?? + original.options.main ?? + path.join(projectSourceRoot, "main.ts"), + ], + }); + + console.log(`Generated ${federationTsConfig}`); + } + + for (const target of targets) { + target.options ??= {}; + target.options.tsConfig = federationTsConfig; + } + + modified = true; + } + + if (modified) { + tree.overwrite(workspaceFileName, JSON.stringify(workspace, null, "\t")); + } +} + // Rename the beta builder back and ensure NF targets carry entryPoints/projectName. function normalizeBuilderReferences( tree: Tree, diff --git a/src/tools/esbuild/angular-bundler.spec.ts b/src/tools/esbuild/angular-bundler.spec.ts index 617279e..f9831ff 100644 --- a/src/tools/esbuild/angular-bundler.spec.ts +++ b/src/tools/esbuild/angular-bundler.spec.ts @@ -84,29 +84,35 @@ describe('createAngularEsbuildContext', () => { expect(pluginOptions.tsconfig).toBe(expected); }); - it('joins the workspace root once when mappings are optimized', async () => { - await createAngularEsbuildContext(makeOptions({ optimizedMappings: true })); + it('updates the tsconfig the NF target declared, passing the fallback entry points', async () => { + await createAngularEsbuildContext( + makeOptions({ + builderOptions: { + optimization: false, + sourceMap: false, + managedTsConfig: 'apps/example/tsconfig.federation.json', + fallbackEntryPoints: ['apps/example/src/main.ts'], + }, + } as unknown as Partial) + ); // updateFederationTsConfig joins the workspace root itself expect(updateFederationTsConfig).toHaveBeenCalledWith( workspaceRoot, 'apps/example/tsconfig.app.json', expect.anything(), - true + ['apps/example/src/main.ts'] ); expect(lastBuildOptions().tsconfig).toBe( path.join(workspaceRoot, 'apps/example/tsconfig.app.json') ); }); - it('still updates the tsconfig when mappings are not optimized, so exposes land in the program', async () => { - await createAngularEsbuildContext(makeOptions({ optimizedMappings: false })); + // Without `tsConfig` on the NF target the builder falls back to the Angular target's own + // tsconfig, which is the user's file and must be left alone. + it('leaves the tsconfig alone when the NF target declared none', async () => { + await createAngularEsbuildContext(makeOptions()); - expect(updateFederationTsConfig).toHaveBeenCalledWith( - workspaceRoot, - 'apps/example/tsconfig.app.json', - expect.anything(), - false - ); + expect(updateFederationTsConfig).not.toHaveBeenCalled(); }); }); diff --git a/src/tools/esbuild/angular-bundler.ts b/src/tools/esbuild/angular-bundler.ts index 770f32f..f3a427e 100644 --- a/src/tools/esbuild/angular-bundler.ts +++ b/src/tools/esbuild/angular-bundler.ts @@ -33,7 +33,6 @@ export async function createAngularEsbuildContext(options: NormalizedContextOpti hash, chunks, platform, - optimizedMappings, } = options; let tsConfigPath = options.tsConfigPath; @@ -78,7 +77,17 @@ export async function createAngularEsbuildContext(options: NormalizedContextOpti } } - updateFederationTsConfig(workspaceRoot, tsConfigPath, entryPoints, optimizedMappings); + // Only a tsconfig the NF target explicitly points at is ours to rewrite. Without one the + // builder falls back to the Angular target's own tsconfig, whose include already covers the + // exposes — rewriting that would strip its comments to no effect. + if (builderOptions.managedTsConfig) { + updateFederationTsConfig( + workspaceRoot, + tsConfigPath, + entryPoints, + builderOptions.fallbackEntryPoints + ); + } tsConfigPath = path.join(workspaceRoot, tsConfigPath); diff --git a/src/tools/esbuild/create-federation-tsconfig.spec.ts b/src/tools/esbuild/create-federation-tsconfig.spec.ts index f2b10fe..bebd999 100644 --- a/src/tools/esbuild/create-federation-tsconfig.spec.ts +++ b/src/tools/esbuild/create-federation-tsconfig.spec.ts @@ -11,6 +11,10 @@ function entry(fileName: string): EntryPoint { return { fileName, outName: 'out.js' } as EntryPoint; } +function written() { + return JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); +} + describe('updateFederationTsConfig', () => { afterEach(() => { vi.mocked(fs.existsSync).mockReset(); @@ -18,8 +22,8 @@ describe('updateFederationTsConfig', () => { vi.mocked(fs.writeFileSync).mockReset(); }); - it('returns early without touching fs when there are no entry points', () => { - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [], true); + it('returns early without touching fs when there is nothing to compile', () => { + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [], []); expect(fs.readFileSync).not.toHaveBeenCalled(); expect(fs.writeFileSync).not.toHaveBeenCalled(); @@ -27,103 +31,102 @@ describe('updateFederationTsConfig', () => { it('resolves workspace-root-relative exposes against the workspace root', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: [] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); - updateFederationTsConfig( - '/ws', - 'projects/mfe1/tsconfig.fed.json', - [entry('./projects/mfe1/src/bootstrap.ts')], - true - ); + updateFederationTsConfig('/ws', 'projects/mfe1/tsconfig.fed.json', [ + entry('./projects/mfe1/src/bootstrap.ts'), + ]); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['src/bootstrap.ts']); + expect(written().files).toEqual(['src/bootstrap.ts']); }); - it('appends absolute mapping entry points relative to the tsconfig dir', () => { + it('resolves absolute mapping entry points relative to the tsconfig dir', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['existing.ts'] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')]); expect(fs.writeFileSync).toHaveBeenCalledTimes(1); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['existing.ts', 'src/a.ts']); + expect(written().files).toEqual(['src/a.ts']); }); - it('skips mapping entry points but keeps exposes when mappings are not optimized', () => { + // Regression: with `ignoreUnusedDeps: false` core hands over every tsconfig path mapping, + // used or not. They are all bundled, so they all have to be in the program. + it('keeps mapping entry points alongside exposes', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: [] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); - updateFederationTsConfig( - '/ws', - 'tsconfig.fed.json', - [entry('/ws/libs/unused/src/index.ts'), entry('./src/bootstrap.ts')], - false - ); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [ + entry('/ws/libs/unused/src/index.ts'), + entry('./src/bootstrap.ts'), + ]); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['src/bootstrap.ts']); + expect(written().files).toEqual(['libs/unused/src/index.ts', 'src/bootstrap.ts']); }); - it('returns early without touching fs when only mappings are present and unoptimized', () => { - updateFederationTsConfig( - '/ws', - 'tsconfig.fed.json', - [entry('/ws/libs/unused/src/index.ts')], - false + it('replaces the previous files, dropping entry points that are gone', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue( + JSON5.stringify({ files: ['src/renamed-away.ts'], include: ['src/**/*.d.ts'] }) as never ); - expect(fs.readFileSync).not.toHaveBeenCalled(); - expect(fs.writeFileSync).not.toHaveBeenCalled(); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('./src/a.ts')]); + + expect(written()).toEqual({ files: ['src/a.ts'], include: ['src/**/*.d.ts'] }); }); - it('does not duplicate an include that is already present', () => { + it('deduplicates entry points resolving to the same file', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['src/a.ts'] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); - updateFederationTsConfig( - '/ws', - 'tsconfig.fed.json', - [entry('/ws/src/a.ts'), entry('/ws/src/b.ts')], - true - ); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [ + entry('/ws/src/a.ts'), + entry('./src/a.ts'), + ]); + + expect(written().files).toEqual(['src/a.ts']); + }); + + it('falls back to the given entry points when the build has none of its own', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); + + updateFederationTsConfig('/ws', 'projects/host/tsconfig.fed.json', [], [ + 'projects/host/src/main.ts', + ]); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['src/a.ts', 'src/b.ts']); + expect(written().files).toEqual(['src/main.ts']); }); - it('creates the include array when the tsconfig has none', () => { + it('creates the files array when the tsconfig has none', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ compilerOptions: {} }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')]); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['src/a.ts']); + expect(written().files).toEqual(['src/a.ts']); }); it('normalizes OS-specific backslash separators to forward slashes', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: [] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); // Simulate Windows: path.relative returns single-backslash separators. const relativeSpy = vi .spyOn(path, 'relative') .mockReturnValue('..\\libs\\shared\\src\\index.ts'); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/libs/shared/src/index.ts')], true); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/libs/shared/src/index.ts')]); - const written = JSON.parse(String(vi.mocked(fs.writeFileSync).mock.calls[0]![1])); - expect(written.include).toEqual(['../libs/shared/src/index.ts']); + expect(written().files).toEqual(['../libs/shared/src/index.ts']); relativeSpy.mockRestore(); }); it('does not write when the resulting config is unchanged', () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ include: ['src/a.ts'] }) as never); + vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: ['src/a.ts'] }) as never); - updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')], true); + updateFederationTsConfig('/ws', 'tsconfig.fed.json', [entry('/ws/src/a.ts')]); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); diff --git a/src/tools/esbuild/create-federation-tsconfig.ts b/src/tools/esbuild/create-federation-tsconfig.ts index e3763bc..9d65dc1 100644 --- a/src/tools/esbuild/create-federation-tsconfig.ts +++ b/src/tools/esbuild/create-federation-tsconfig.ts @@ -4,45 +4,50 @@ import fs from 'fs'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'util'; -// Adds the federation entry points to the federation tsconfig, so the angular-compiler -// plugin finds them in the TypeScript program. +/** + * Puts the federation entry points into the federation tsconfig's `files`, so the + * angular-compiler plugin finds them in the TypeScript program. + * + * The two keys are owned by different sides: the schematic writes `extends` and `include` + * (see schematics/init/steps/generate-federation-tsconfig.ts), this writes `files`. Because + * `files` is replaced rather than appended to, an expose that was renamed or removed leaves + * nothing behind. + * + * Only ever call this for a tsconfig the NF target explicitly points at — it is rewritten + * as plain JSON, which drops any comments the file had. + */ export function updateFederationTsConfig( workspaceRoot: string, tsConfigPath: string, entryPoints: EntryPoint[], - optimizedMappings: boolean + fallbackEntryPoints: string[] = [] ): void { const fullTsConfigPath = path.join(workspaceRoot, tsConfigPath); const tsconfigDir = path.dirname(fullTsConfigPath); // Core hands exposes over workspace-root-relative and shared mappings absolute. - // Unpruned, the mappings carry every path mapping in the workspace, used or not. - const filtered = entryPoints - .filter(ep => optimizedMappings || !path.isAbsolute(ep.fileName)) - .map(ep => { - const fileName = path.isAbsolute(ep.fileName) - ? ep.fileName - : path.join(workspaceRoot, ep.fileName); + const toTsConfigRelative = (fileName: string) => { + const absolute = path.isAbsolute(fileName) ? fileName : path.join(workspaceRoot, fileName); - return path.relative(tsconfigDir, fileName).replace(/\\/g, '/'); - }); + return path.relative(tsconfigDir, absolute).replace(/\\/g, '/'); + }; - if (filtered.length === 0) { + const resolved = entryPoints.map(ep => toTsConfigRelative(ep.fileName)); + + // A host without exposes or shared mappings has no entry points of its own; the app's + // main.ts keeps the program from being empty. + const files = [ + ...new Set(resolved.length > 0 ? resolved : fallbackEntryPoints.map(toTsConfigRelative)), + ]; + + if (files.length === 0) { return; } const tsconfigAsString = fs.readFileSync(fullTsConfigPath, 'utf-8'); const tsconfig = JSON5.parse(tsconfigAsString); - if (!tsconfig.include) { - tsconfig.include = []; - } - - for (const ep of filtered) { - if (!tsconfig.include.includes(ep)) { - tsconfig.include.push(ep); - } - } + tsconfig.files = files; const content = JSON5.stringify(tsconfig, null, 2); From e19bb4d8fdff358f423b509652c6d77407dbba30 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 11 Aug 2026 08:11:10 +0200 Subject: [PATCH 4/7] docs(builders): document entryPoints as a fallback in both schemas Exposes from federation.config take precedence over the option in both places core consults it, so the schema descriptions now say so and point at the federation tsconfig's include for adding files to the program. Also types the array's items as string, matching schema.d.ts. --- src/builders/build/schema.json | 4 +++- src/builders/remote/builder.ts | 7 ++++--- src/builders/remote/schema.json | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/builders/build/schema.json b/src/builders/build/schema.json index ba1eab1..f55cb85 100644 --- a/src/builders/build/schema.json +++ b/src/builders/build/schema.json @@ -23,7 +23,9 @@ "default": 0 }, "entryPoints": { - "type": "array" + "type": "array", + "items": { "type": "string" }, + "description": "Fallback entry points, used only when the project has nothing federated of its own (no exposes, no shared mappings). They seed the federation tsconfig's 'files' and the unused-dependency scan. Exposes from federation.config always take precedence, so this cannot override or narrow them; to add extra files to the TypeScript program, use 'include' in the federation tsconfig instead. Defaults to 'src/main.ts' resolved next to the federation tsconfig." }, "rebuildDelay": { "type": "number", diff --git a/src/builders/remote/builder.ts b/src/builders/remote/builder.ts index 799585c..e4c5122 100644 --- a/src/builders/remote/builder.ts +++ b/src/builders/remote/builder.ts @@ -64,9 +64,10 @@ export async function* runRemoteBuilder( context ); - // Unlike the regular build builder, remote never bundles a main.ts / polyfills. - // Entry points come from the schema override or, when omitted, from the - // `exposes` map in federation.config.{mjs,js} (resolved by normalizeFederationOptions). + // Unlike the regular build builder, remote never bundles a main.ts / polyfills. Entry points + // come from the `exposes` map in federation.config.{mjs,js}; the schema option is only a + // fallback for when there are none, so passing `undefined` when it is omitted keeps core + // from treating an empty list as a deliberate one. const entryPoints: string[] | undefined = nfBuilderOptions.entryPoints?.length ? nfBuilderOptions.entryPoints : undefined; diff --git a/src/builders/remote/schema.json b/src/builders/remote/schema.json index d3441fe..4ea8a8f 100644 --- a/src/builders/remote/schema.json +++ b/src/builders/remote/schema.json @@ -20,7 +20,9 @@ "default": false }, "entryPoints": { - "type": "array" + "type": "array", + "items": { "type": "string" }, + "description": "Fallback entry points, used only when the project has nothing federated of its own (no exposes, no shared mappings). They seed the federation tsconfig's 'files' and the unused-dependency scan. Exposes from federation.config always take precedence, so this cannot override or narrow them; to add extra files to the TypeScript program, use 'include' in the federation tsconfig instead. Unset by default — a remote's exposes are normally all it bundles." }, "rebuildDelay": { "type": "number", From f2b65b0bbeea50cfc00379d4002d0830dbe07241 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 11 Aug 2026 08:30:16 +0200 Subject: [PATCH 5/7] refactor(bundler): make the tsconfig ownership flag a boolean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit managedTsConfig carried a path that was never read — the bundler only tested it for truthiness and rewrote tsConfigPath, which the path always equalled. manageTsConfig says what the flag actually decides. Corrects the rationale recorded with it: the fallback tsconfig is off limits not because its include already covers the exposes, but because `files` there is Angular's. Projects scaffolded with the older files/include shape keep main.ts in `files` and only .d.ts in `include`, so replacing files would drop the app's own entry point from the program. --- src/builders/build/builder.ts | 16 +++++++++------- src/builders/build/schema.d.ts | 9 +++++---- src/builders/remote/builder.ts | 2 +- src/tools/esbuild/angular-bundler.spec.ts | 2 +- src/tools/esbuild/angular-bundler.ts | 9 +++++---- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index 2efaa52..130852a 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -211,10 +211,12 @@ export async function* runBuilder( ngBuilderOptions.outputPath = nfBuilderOptions.outputPath; } - const federationTsConfig = - !!nfBuilderOptions.tsConfig && nfBuilderOptions.tsConfig.length > 0 - ? nfBuilderOptions.tsConfig - : ngBuilderOptions.tsConfig; + const declaresTsConfig = + !!nfBuilderOptions.tsConfig && nfBuilderOptions.tsConfig.length > 0; + + const federationTsConfig = declaresTsConfig + ? nfBuilderOptions.tsConfig! + : ngBuilderOptions.tsConfig; const entryPoints: string[] | undefined = nfBuilderOptions.entryPoints && nfBuilderOptions.entryPoints.length > 0 @@ -226,9 +228,9 @@ export async function* runBuilder( ...ngBuilderOptions, plugins: nfBuilderOptions.plugins, instrumentForCoverage: nfBuilderOptions.instrumentForCoverage, - // Deliberately not `federationTsConfig`: only a tsconfig the target declares itself is - // the builder's to rewrite, never the Angular target's own one it falls back to. - managedTsConfig: nfBuilderOptions.tsConfig, + // Only a tsconfig the target declares itself is the builder's to rewrite, never the + // Angular target's own one it falls back to. + manageTsConfig: declaresTsConfig, fallbackEntryPoints: entryPoints, }, context, diff --git a/src/builders/build/schema.d.ts b/src/builders/build/schema.d.ts index 9df0d82..34ec17f 100644 --- a/src/builders/build/schema.d.ts +++ b/src/builders/build/schema.d.ts @@ -34,11 +34,12 @@ export type NfInternalOptions = { instrumentForCoverage?: (filename: string) => boolean; /** - * The federation tsconfig, set only when the NF target declares one. That file is the - * builder's to manage (see tools/esbuild/create-federation-tsconfig.ts); when it is absent - * the builder falls back to the Angular target's own tsconfig, which must not be rewritten. + * Whether the tsconfig the federation build resolved to is the builder's to rewrite (see + * tools/esbuild/create-federation-tsconfig.ts). True only when the NF target declares a + * `tsConfig` of its own; without one the build falls back to the Angular target's tsconfig, + * where `files` is Angular's — replacing it would drop main.ts from the app's own program. */ - managedTsConfig?: string; + manageTsConfig?: boolean; /** * Roots keeping the federation program non-empty when a build has no entry points of its diff --git a/src/builders/remote/builder.ts b/src/builders/remote/builder.ts index e4c5122..1a7e8e8 100644 --- a/src/builders/remote/builder.ts +++ b/src/builders/remote/builder.ts @@ -76,7 +76,7 @@ export async function* runRemoteBuilder( { ...ngBuilderOptions, // Required by the schema, so the tsconfig is always the builder's to manage. - managedTsConfig: federationTsConfig, + manageTsConfig: true, fallbackEntryPoints: entryPoints, }, context diff --git a/src/tools/esbuild/angular-bundler.spec.ts b/src/tools/esbuild/angular-bundler.spec.ts index f9831ff..c8dcae7 100644 --- a/src/tools/esbuild/angular-bundler.spec.ts +++ b/src/tools/esbuild/angular-bundler.spec.ts @@ -90,7 +90,7 @@ describe('createAngularEsbuildContext', () => { builderOptions: { optimization: false, sourceMap: false, - managedTsConfig: 'apps/example/tsconfig.federation.json', + manageTsConfig: true, fallbackEntryPoints: ['apps/example/src/main.ts'], }, } as unknown as Partial) diff --git a/src/tools/esbuild/angular-bundler.ts b/src/tools/esbuild/angular-bundler.ts index f3a427e..9a8d24c 100644 --- a/src/tools/esbuild/angular-bundler.ts +++ b/src/tools/esbuild/angular-bundler.ts @@ -77,10 +77,11 @@ export async function createAngularEsbuildContext(options: NormalizedContextOpti } } - // Only a tsconfig the NF target explicitly points at is ours to rewrite. Without one the - // builder falls back to the Angular target's own tsconfig, whose include already covers the - // exposes — rewriting that would strip its comments to no effect. - if (builderOptions.managedTsConfig) { + // Only a tsconfig the NF target explicitly points at is ours to rewrite. Without one this is + // the Angular target's own tsconfig, where `files` belongs to Angular — replacing it there + // drops main.ts from the app's program on any project scaffolded with the older + // `files: ["src/main.ts"]` / `include: ["src/**/*.d.ts"]` shape. + if (builderOptions.manageTsConfig) { updateFederationTsConfig( workspaceRoot, tsConfigPath, From c2937ed7f7f2ad69f61eda838252f9434998fb0f Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 11 Aug 2026 08:35:21 +0200 Subject: [PATCH 6/7] fix(bundler): fail with the path when the managed tsconfig is missing A tsconfig the NF target declares is read to have its files replaced, so an absent one surfaced as a bare ENOENT from inside the bundler, naming a path with no context. Throw before the read instead. The check sits after the nothing-to-compile return, so a build with no entry points of its own stays a no-op rather than failing on a file it never needed. --- .../esbuild/create-federation-tsconfig.spec.ts | 13 +++++++++++++ src/tools/esbuild/create-federation-tsconfig.ts | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/src/tools/esbuild/create-federation-tsconfig.spec.ts b/src/tools/esbuild/create-federation-tsconfig.spec.ts index bebd999..14a522c 100644 --- a/src/tools/esbuild/create-federation-tsconfig.spec.ts +++ b/src/tools/esbuild/create-federation-tsconfig.spec.ts @@ -29,6 +29,19 @@ describe('updateFederationTsConfig', () => { expect(fs.writeFileSync).not.toHaveBeenCalled(); }); + it('throws naming the tsconfig when the file the target points at is missing', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + expect(() => + updateFederationTsConfig('/ws', 'projects/mfe1/tsconfig.fed.json', [ + entry('./projects/mfe1/src/bootstrap.ts'), + ]) + ).toThrow(/"projects\/mfe1\/tsconfig\.fed\.json" does not exist/); + + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + it('resolves workspace-root-relative exposes against the workspace root', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON5.stringify({ files: [] }) as never); diff --git a/src/tools/esbuild/create-federation-tsconfig.ts b/src/tools/esbuild/create-federation-tsconfig.ts index 9d65dc1..8413139 100644 --- a/src/tools/esbuild/create-federation-tsconfig.ts +++ b/src/tools/esbuild/create-federation-tsconfig.ts @@ -44,6 +44,13 @@ export function updateFederationTsConfig( return; } + if (!fs.existsSync(fullTsConfigPath)) { + throw new Error( + `The federation tsconfig "${tsConfigPath}" does not exist, so the exposed modules and ` + + `shared mappings cannot be added to the TypeScript program.` + ); + } + const tsconfigAsString = fs.readFileSync(fullTsConfigPath, 'utf-8'); const tsconfig = JSON5.parse(tsconfigAsString); From ee2269d2e202bc597ab521498f42971f1ab38ad0 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 11 Aug 2026 08:39:57 +0200 Subject: [PATCH 7/7] refactor(bundler): rename create-federation-tsconfig to update-federation-tsconfig The module has not created anything since the schematic took over scaffolding; it replaces `files` in a tsconfig that must already exist. The file name now matches its export and contrasts with the schematic's generate- step. --- src/builders/build/builder.ts | 2 -- src/builders/build/schema.d.ts | 2 +- src/schematics/init/steps/generate-federation-tsconfig.ts | 2 +- src/tools/esbuild/angular-bundler.spec.ts | 4 ++-- src/tools/esbuild/angular-bundler.ts | 2 +- ...on-tsconfig.spec.ts => update-federation-tsconfig.spec.ts} | 2 +- ...e-federation-tsconfig.ts => update-federation-tsconfig.ts} | 0 7 files changed, 6 insertions(+), 8 deletions(-) rename src/tools/esbuild/{create-federation-tsconfig.spec.ts => update-federation-tsconfig.spec.ts} (98%) rename src/tools/esbuild/{create-federation-tsconfig.ts => update-federation-tsconfig.ts} (100%) diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index 130852a..4180e54 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -228,8 +228,6 @@ export async function* runBuilder( ...ngBuilderOptions, plugins: nfBuilderOptions.plugins, instrumentForCoverage: nfBuilderOptions.instrumentForCoverage, - // Only a tsconfig the target declares itself is the builder's to rewrite, never the - // Angular target's own one it falls back to. manageTsConfig: declaresTsConfig, fallbackEntryPoints: entryPoints, }, diff --git a/src/builders/build/schema.d.ts b/src/builders/build/schema.d.ts index 34ec17f..c8c8be7 100644 --- a/src/builders/build/schema.d.ts +++ b/src/builders/build/schema.d.ts @@ -35,7 +35,7 @@ export type NfInternalOptions = { /** * Whether the tsconfig the federation build resolved to is the builder's to rewrite (see - * tools/esbuild/create-federation-tsconfig.ts). True only when the NF target declares a + * tools/esbuild/update-federation-tsconfig.ts). True only when the NF target declares a * `tsConfig` of its own; without one the build falls back to the Angular target's tsconfig, * where `files` is Angular's — replacing it would drop main.ts from the app's own program. */ diff --git a/src/schematics/init/steps/generate-federation-tsconfig.ts b/src/schematics/init/steps/generate-federation-tsconfig.ts index d7e217e..31a6ebb 100644 --- a/src/schematics/init/steps/generate-federation-tsconfig.ts +++ b/src/schematics/init/steps/generate-federation-tsconfig.ts @@ -24,7 +24,7 @@ export interface FederationTsConfigOptions { /** * Writes the tsconfig the federation build compiles against. It covers the exposes and shared * mappings rather than the app entry, so `files` is a plain list of entry points that the - * builder rewrites per build (see tools/esbuild/create-federation-tsconfig.ts) and `include` + * builder rewrites per build (see tools/esbuild/update-federation-tsconfig.ts) and `include` * only picks up ambient declarations. It extends the app tsconfig because it also drives * esbuild's module resolution and so needs its paths. * diff --git a/src/tools/esbuild/angular-bundler.spec.ts b/src/tools/esbuild/angular-bundler.spec.ts index c8dcae7..6d90931 100644 --- a/src/tools/esbuild/angular-bundler.spec.ts +++ b/src/tools/esbuild/angular-bundler.spec.ts @@ -4,7 +4,7 @@ import type { CompilerPluginOptions } from '@angular/build/private'; import { createAngularEsbuildContext } from './angular-bundler.js'; import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin.js'; -import { updateFederationTsConfig } from './create-federation-tsconfig.js'; +import { updateFederationTsConfig } from './update-federation-tsconfig.js'; import type { NormalizedContextOptions } from '../../utils/normalize-context-options.js'; vi.mock('esbuild', () => ({ context: vi.fn().mockResolvedValue({ rebuild: vi.fn() }) })); @@ -23,7 +23,7 @@ vi.mock('./create-awaitable-compiler-plugin.js', () => ({ .mockReturnValue([{ name: 'angular-compiler', setup: vi.fn() }, Promise.resolve()]), })); -vi.mock('./create-federation-tsconfig.js', () => ({ updateFederationTsConfig: vi.fn() })); +vi.mock('./update-federation-tsconfig.js', () => ({ updateFederationTsConfig: vi.fn() })); vi.mock('@chialab/esbuild-plugin-commonjs', () => ({ default: () => ({ name: 'commonjs', setup: vi.fn() }), diff --git a/src/tools/esbuild/angular-bundler.ts b/src/tools/esbuild/angular-bundler.ts index 9a8d24c..759dab4 100644 --- a/src/tools/esbuild/angular-bundler.ts +++ b/src/tools/esbuild/angular-bundler.ts @@ -16,7 +16,7 @@ import { normalizeOptimization, normalizeSourceMaps } from '../../utils/normaliz import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin.js'; import type { NormalizedContextOptions } from '../../utils/normalize-context-options.js'; -import { updateFederationTsConfig } from './create-federation-tsconfig.js'; +import { updateFederationTsConfig } from './update-federation-tsconfig.js'; export async function createAngularEsbuildContext(options: NormalizedContextOptions): Promise<{ ctx: esbuild.BuildContext; diff --git a/src/tools/esbuild/create-federation-tsconfig.spec.ts b/src/tools/esbuild/update-federation-tsconfig.spec.ts similarity index 98% rename from src/tools/esbuild/create-federation-tsconfig.spec.ts rename to src/tools/esbuild/update-federation-tsconfig.spec.ts index 14a522c..83aaa11 100644 --- a/src/tools/esbuild/create-federation-tsconfig.spec.ts +++ b/src/tools/esbuild/update-federation-tsconfig.spec.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import JSON5 from 'json5'; -import { updateFederationTsConfig } from './create-federation-tsconfig.js'; +import { updateFederationTsConfig } from './update-federation-tsconfig.js'; import type { EntryPoint } from '@softarc/native-federation'; vi.mock('fs'); diff --git a/src/tools/esbuild/create-federation-tsconfig.ts b/src/tools/esbuild/update-federation-tsconfig.ts similarity index 100% rename from src/tools/esbuild/create-federation-tsconfig.ts rename to src/tools/esbuild/update-federation-tsconfig.ts