From 9f7634d4dbc44e1be3e4f6a39614225a44b13875 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 28 Oct 2026 15:13:04 +0600 Subject: [PATCH 1/2] fix(@schematics/angular): migrate `serviceWorker: true` to a config path in use-application-builder The browser builder accepts `serviceWorker: true` without `ngswConfigPath` and falls back to `/ngsw-config.json`, but the application builder only accepts a path or `false`. The migration kept `true` when `ngswConfigPath` was not set in the same options object, so the first `ng build` after migrating failed schema validation. Replace `true` with the `ngswConfigPath` of the same options, then the one in the base options, then the browser builder default. --- .../use-application-builder/migration.ts | 14 ++++++-- .../use-application-builder/migration_spec.ts | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/schematics/angular/migrations/use-application-builder/migration.ts b/packages/schematics/angular/migrations/use-application-builder/migration.ts index dcf0526a3945..1edfa0933cc3 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration.ts @@ -39,6 +39,7 @@ import { function* updateBuildTarget( projectName: string, + projectRoot: string, buildTarget: TargetDefinition, serverTarget: TargetDefinition | undefined, tree: Tree, @@ -46,6 +47,7 @@ function* updateBuildTarget( ): Iterable { // Update builder target and options buildTarget.builder = Builders.Application; + const baseNgswConfigPath = buildTarget.options?.['ngswConfigPath']; for (const [, options] of allTargetOptions(buildTarget, false)) { if (options['index'] === '') { @@ -57,7 +59,13 @@ function* updateBuildTarget( if (serverTarget && typeof options['browser'] === 'string') { options['server'] = dirname(options['browser']) + '/main.server.ts'; } - options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker']; + if (options['serviceWorker'] === true) { + // The application builder only accepts a path, while the browser builder defaults it in code. + options['serviceWorker'] = + options['ngswConfigPath'] ?? baseNgswConfigPath ?? join(projectRoot, 'ngsw-config.json'); + } else { + options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker']; + } if (typeof options['polyfills'] === 'string') { options['polyfills'] = [options['polyfills']]; @@ -197,7 +205,9 @@ function updateProjects(tree: Tree, context: SchematicContext) { const serverTarget = project.targets.get('server'); - rules.push(...updateBuildTarget(name, buildTarget, serverTarget, tree, context)); + rules.push( + ...updateBuildTarget(name, project.root, buildTarget, serverTarget, tree, context), + ); // Delete all redundant targets for (const [key, target] of project.targets) { diff --git a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts index 05259962dc5d..73722c197b05 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts @@ -113,6 +113,40 @@ describe(`Migration to use the application builder`, () => { }); }); + it(`should replace 'serviceWorker: true' with the default 'ngsw-config.json' path`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const workspaceContent = tree.readJson('/angular.json') as Record; + workspaceContent['projects']['app']['architect']['build']['options']['serviceWorker'] = true; + tree.overwrite('/angular.json', JSON.stringify(workspaceContent)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { serviceWorker, ngswConfigPath } = app.architect['build'].options; + expect(serviceWorker).toBe('/project/app/ngsw-config.json'); + expect(ngswConfigPath).toBeUndefined(); + }); + + it(`should replace 'serviceWorker: true' in a configuration with the base 'ngswConfigPath'`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const workspaceContent = tree.readJson('/angular.json') as Record; + const build = workspaceContent['projects']['app']['architect']['build']; + build['options']['ngswConfigPath'] = 'src/ngsw-config.json'; + build['configurations'] = { production: { serviceWorker: true } }; + tree.overwrite('/angular.json', JSON.stringify(workspaceContent)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + expect(app.architect['build'].configurations.production.serviceWorker).toBe( + 'src/ngsw-config.json', + ); + }); + it(`should remove 'builderMode' from karma options`, async () => { addWorkspaceTarget(tree, 'test', { 'builder': Builders.Karma, From 760065456cfd2224be09c6a9e92c3b050737bedb Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 28 Oct 2026 18:58:57 +0600 Subject: [PATCH 2/2] fixup! fix(@schematics/angular): migrate `serviceWorker: true` to a config path in use-application-builder --- .../use-application-builder/migration.ts | 10 ++++- .../use-application-builder/migration_spec.ts | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/schematics/angular/migrations/use-application-builder/migration.ts b/packages/schematics/angular/migrations/use-application-builder/migration.ts index 1edfa0933cc3..510d11bdc345 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration.ts @@ -47,6 +47,7 @@ function* updateBuildTarget( ): Iterable { // Update builder target and options buildTarget.builder = Builders.Application; + const baseServiceWorker = buildTarget.options?.['serviceWorker']; const baseNgswConfigPath = buildTarget.options?.['ngswConfigPath']; for (const [, options] of allTargetOptions(buildTarget, false)) { @@ -63,8 +64,13 @@ function* updateBuildTarget( // The application builder only accepts a path, while the browser builder defaults it in code. options['serviceWorker'] = options['ngswConfigPath'] ?? baseNgswConfigPath ?? join(projectRoot, 'ngsw-config.json'); - } else { - options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker']; + } else if ( + options['serviceWorker'] === undefined && + baseServiceWorker === true && + typeof options['ngswConfigPath'] === 'string' + ) { + // A configuration that only changes the config path of the service worker enabled in the base options. + options['serviceWorker'] = options['ngswConfigPath']; } if (typeof options['polyfills'] === 'string') { diff --git a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts index 73722c197b05..3eded0bd8137 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts @@ -145,6 +145,44 @@ describe(`Migration to use the application builder`, () => { expect(app.architect['build'].configurations.production.serviceWorker).toBe( 'src/ngsw-config.json', ); + expect(app.architect['build'].options.serviceWorker).toBeUndefined(); + }); + + it(`should not enable the service worker when 'serviceWorker' is false`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const workspaceContent = tree.readJson('/angular.json') as Record; + const build = workspaceContent['projects']['app']['architect']['build']; + build['options']['serviceWorker'] = false; + build['options']['ngswConfigPath'] = 'src/ngsw-config.json'; + tree.overwrite('/angular.json', JSON.stringify(workspaceContent)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { serviceWorker, ngswConfigPath } = app.architect['build'].options; + expect(serviceWorker).toBeFalse(); + expect(ngswConfigPath).toBeUndefined(); + }); + + it(`should use a configuration 'ngswConfigPath' when the base options enable the service worker`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const workspaceContent = tree.readJson('/angular.json') as Record; + const build = workspaceContent['projects']['app']['architect']['build']; + build['options']['serviceWorker'] = true; + build['configurations'] = { production: { ngswConfigPath: 'src/ngsw-config.prod.json' } }; + tree.overwrite('/angular.json', JSON.stringify(workspaceContent)); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + expect(app.architect['build'].options.serviceWorker).toBe('/project/app/ngsw-config.json'); + expect(app.architect['build'].configurations.production.serviceWorker).toBe( + 'src/ngsw-config.prod.json', + ); }); it(`should remove 'builderMode' from karma options`, async () => {