From 28eccaee52449725f5b83e390ed98e31737c75cc Mon Sep 17 00:00:00 2001 From: Jeroen Akkerman Date: Wed, 29 Jul 2026 13:46:49 +0200 Subject: [PATCH] fix: Prevented double -v4 substitution in the update-v4 schematic --- .../schematics/update-v4/schematic.spec.ts | 75 +++++++++++++++++++ .../src/schematics/update-v4/schematic.ts | 7 +- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 packages/angular/src/schematics/update-v4/schematic.spec.ts diff --git a/packages/angular/src/schematics/update-v4/schematic.spec.ts b/packages/angular/src/schematics/update-v4/schematic.spec.ts new file mode 100644 index 0000000..ebbfccc --- /dev/null +++ b/packages/angular/src/schematics/update-v4/schematic.spec.ts @@ -0,0 +1,75 @@ +import { HostTree, type SchematicContext } from '@angular-devkit/schematics'; + +import updateV4 from './schematic.js'; + +const V4_CONFIG_IMPORT = + "import { withNativeFederation, shareAll } from '@angular-architects/native-federation-v4/config';"; + +describe('update-v4 schematic', () => { + let tree: HostTree; + + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + tree = new HostTree(); + tree.create( + 'angular.json', + JSON.stringify({ + projects: { + shell: { root: '', sourceRoot: 'src', architect: {} }, + }, + }) + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + async function runSchematic(): Promise { + await updateV4({ project: 'shell' })(tree, {} as SchematicContext); + } + + it('migrates a CJS federation.config.js to ESM with a single -v4 suffix', async () => { + tree.create( + 'federation.config.js', + [ + "const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');", + '', + 'module.exports = withNativeFederation({', + " name: 'shell',", + ' shared: {', + ' ...shareAll({ singleton: true }),', + ' },', + '});', + '', + ].join('\n') + ); + + await runSchematic(); + + expect(tree.exists('federation.config.mjs')).toBe(true); + const content = tree.readText('federation.config.mjs'); + expect(content).toContain(V4_CONFIG_IMPORT); + expect(content).not.toContain('native-federation-v4-v4'); + expect(content).toContain('export default withNativeFederation({'); + }); + + it('leaves an already-updated v4 require path untouched', async () => { + tree.create( + 'federation.config.js', + [ + "const { withNativeFederation, shareAll } = require('@angular-architects/native-federation-v4/config');", + '', + 'module.exports = withNativeFederation({});', + '', + ].join('\n') + ); + + await runSchematic(); + + expect(tree.exists('federation.config.mjs')).toBe(true); + const content = tree.readText('federation.config.mjs'); + expect(content).toContain(V4_CONFIG_IMPORT); + expect(content).not.toContain('native-federation-v4-v4'); + }); +}); diff --git a/packages/angular/src/schematics/update-v4/schematic.ts b/packages/angular/src/schematics/update-v4/schematic.ts index 6cd67d8..25d2f93 100644 --- a/packages/angular/src/schematics/update-v4/schematic.ts +++ b/packages/angular/src/schematics/update-v4/schematic.ts @@ -86,7 +86,10 @@ function migrateFederationConfigs(tree: Tree, workspace: any, options: UpdateV4S const requireRegex = /const\s+(\{[^}]+\})\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)\s*;?/g; const imports: string[] = []; content = content.replace(requireRegex, (_match, bindings: string, modulePath: string) => { - const updatedPath = modulePath.replace(V3_PACKAGE, V4_PACKAGE); + const updatedPath = modulePath.replace( + new RegExp(escapeRegExp(V3_PACKAGE) + '(?!-v4)'), + V4_PACKAGE + ); imports.push(`import ${bindings} from '${updatedPath}';`); return ''; // Remove the require line; import will be prepended }); @@ -105,7 +108,7 @@ function migrateFederationConfigs(tree: Tree, workspace: any, options: UpdateV4S new RegExp(escapeRegExp(V3_PACKAGE + '/config'), 'g'), V4_PACKAGE + '/config' ); - content = content.replace(new RegExp(escapeRegExp(V3_PACKAGE) + '(?!/)', 'g'), V4_PACKAGE); + content = content.replace(new RegExp(escapeRegExp(V3_PACKAGE) + '(?!/|-v4)', 'g'), V4_PACKAGE); if (content !== originalContent) { tree.delete(configPath);