From cc0b0eafc3484aace5bbb367472254ce5bedc41a Mon Sep 17 00:00:00 2001 From: rk Date: Wed, 21 Oct 2026 15:11:05 +0500 Subject: [PATCH 1/2] fix(@schematics/angular): migrate secondary entry point style options in use-library-builder migration read only `lib.entryFile` from secondary `ng-package.json` and then deleted it, so its `styleIncludePaths` and `sass` options were lost and next build failed with `Can't find stylesheet to import` library builder has single global `stylePreprocessorOptions`, so secondary optoins are now merged into it next to primary ones --- .../use-library-builder/migration.ts | 48 +++++++++++++++---- .../use-library-builder/migration_spec.ts | 36 ++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/schematics/angular/migrations/use-library-builder/migration.ts b/packages/schematics/angular/migrations/use-library-builder/migration.ts index 285cdf9f2332..042e55b242e8 100644 --- a/packages/schematics/angular/migrations/use-library-builder/migration.ts +++ b/packages/schematics/angular/migrations/use-library-builder/migration.ts @@ -60,6 +60,7 @@ interface SecondaryEntryPoint { subpath: string; entryPoint: string; configFile: string; + config: NgPackageConfig | undefined; } /** @@ -176,6 +177,7 @@ function findSecondaryEntryPoints( subpath, entryPoint: join(currentPath, entryFile), configFile: ngPackageJsonPath, + config, }); } else if ( current.subfiles.some((f) => f === 'package.json') && @@ -189,6 +191,7 @@ function findSecondaryEntryPoints( subpath, entryPoint: join(currentPath, entryFile), configFile: packageJsonPath, + config, }); } } @@ -217,6 +220,7 @@ function findSecondaryEntryPoints( * @param projectDir The directory containing the primary `ng-packagr` configuration file. * @param buildTarget The library's `build` target definition in `angular.json`. * @param ngPackageConfig The parsed `NgPackageConfig`, if available. + * @param secondaryEntryPoints The discovered secondary entry points of the library. * @param tree The virtual file system tree. */ function migrateTargetOptions( @@ -224,6 +228,7 @@ function migrateTargetOptions( projectDir: string, buildTarget: TargetDefinition, ngPackageConfig: NgPackageConfig | undefined, + secondaryEntryPoints: SecondaryEntryPoint[], tree: Tree, ): void { buildTarget.builder = Builders.BuildLibrary; @@ -281,27 +286,43 @@ function migrateTargetOptions( buildTarget.options['assets'] = [...existingAssets, ...migratedAssets]; } - if (ngPackageConfig?.lib?.styleIncludePaths?.length || ngPackageConfig?.lib?.sass) { + // The library builder applies style options to all entry points, so the options of + // secondary entry points are merged with the primary ones. + const styleConfigs = [ + { dir: projectDir, lib: ngPackageConfig?.lib }, + ...secondaryEntryPoints.map(({ configFile, config }) => ({ + dir: dirname(configFile), + lib: config?.lib, + })), + ]; + const migratedIncludePaths = styleConfigs.flatMap(({ dir, lib }) => + (lib?.styleIncludePaths ?? []).map((p) => join(dir, p)), + ); + const migratedSassOptions = styleConfigs.flatMap(({ lib }) => (lib?.sass ? [lib.sass] : [])); + + if (migratedIncludePaths.length || migratedSassOptions.length) { const stylePreprocessorOptions = buildTarget.options['stylePreprocessorOptions']; const existingPreprocessorOptions: JsonObject = stylePreprocessorOptions && isJsonObject(stylePreprocessorOptions) ? { ...stylePreprocessorOptions } : {}; - if (ngPackageConfig.lib.styleIncludePaths?.length) { + if (migratedIncludePaths.length) { const existingIncludePaths = Array.isArray(existingPreprocessorOptions['includePaths']) ? (existingPreprocessorOptions['includePaths'] as string[]) : []; - const migratedIncludePaths = ngPackageConfig.lib.styleIncludePaths.map((p) => - join(projectDir, p), - ); existingPreprocessorOptions['includePaths'] = [ - ...existingIncludePaths, - ...migratedIncludePaths, + ...new Set([...existingIncludePaths, ...migratedIncludePaths]), ]; } - if (ngPackageConfig.lib.sass) { - existingPreprocessorOptions['sass'] = ngPackageConfig.lib.sass; + if (migratedSassOptions.length) { + const sass: Record = {}; + for (const options of migratedSassOptions) { + for (const [key, value] of Object.entries(options)) { + sass[key] = [...new Set([...(sass[key] ?? []), ...(value ?? [])])]; + } + } + existingPreprocessorOptions['sass'] = sass; } buildTarget.options['stylePreprocessorOptions'] = existingPreprocessorOptions; @@ -432,7 +453,14 @@ function updateLibraryTarget( entryPointExports[exportKey] = toRelativeExportPath(secondary.entryPoint); } - migrateTargetOptions(normalizedProjectRoot, projectDir, buildTarget, ngPackageConfig, tree); + migrateTargetOptions( + normalizedProjectRoot, + projectDir, + buildTarget, + ngPackageConfig, + secondaryEntryPoints, + tree, + ); const packageJsonPath = join(normalizedProjectRoot, 'package.json'); diff --git a/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts b/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts index d54d5f62f61a..e72c8298c711 100644 --- a/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts +++ b/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts @@ -399,6 +399,42 @@ describe('Migration to use the library builder', () => { }); }); + it('should migrate styleIncludePaths and sass options from secondary entry points', async () => { + tree.overwrite( + '/projects/my-lib/ng-package.json', + JSON.stringify({ + lib: { + entryFile: 'src/public-api.ts', + styleIncludePaths: ['styles'], + sass: { silenceDeprecations: ['import'] }, + }, + }), + ); + tree.create( + '/projects/my-lib/testing/ng-package.json', + JSON.stringify({ + lib: { + entryFile: 'src/public-api.ts', + styleIncludePaths: ['../styles', 'styles'], + sass: { silenceDeprecations: ['import', 'global-builtin'] }, + }, + }), + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { 'my-lib': lib }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = newTree.readJson('/angular.json') as any; + + expect(lib.architect['build'].options.stylePreprocessorOptions).toEqual({ + includePaths: ['projects/my-lib/styles', 'projects/my-lib/testing/styles'], + sass: { + silenceDeprecations: ['import', 'global-builtin'], + }, + }); + }); + it('should throw an error when entry file is not configured and no default candidate exists', async () => { tree.overwrite('/projects/my-lib/ng-package.json', JSON.stringify({}, undefined, 2)); From 9674f744a0bcae9db57d1e61c9974dddf4eb02a3 Mon Sep 17 00:00:00 2001 From: rk Date: Wed, 21 Oct 2026 18:39:29 +0500 Subject: [PATCH 2/2] fixup! fix(@schematics/angular): migrate secondary entry point style options in use-library-builder Co-authored-by: Alan Agius --- .../use-library-builder/migration.ts | 71 ++++++++++--------- .../use-library-builder/migration_spec.ts | 32 +++++++++ 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/packages/schematics/angular/migrations/use-library-builder/migration.ts b/packages/schematics/angular/migrations/use-library-builder/migration.ts index 042e55b242e8..4cbd8634f248 100644 --- a/packages/schematics/angular/migrations/use-library-builder/migration.ts +++ b/packages/schematics/angular/migrations/use-library-builder/migration.ts @@ -220,7 +220,6 @@ function findSecondaryEntryPoints( * @param projectDir The directory containing the primary `ng-packagr` configuration file. * @param buildTarget The library's `build` target definition in `angular.json`. * @param ngPackageConfig The parsed `NgPackageConfig`, if available. - * @param secondaryEntryPoints The discovered secondary entry points of the library. * @param tree The virtual file system tree. */ function migrateTargetOptions( @@ -228,7 +227,6 @@ function migrateTargetOptions( projectDir: string, buildTarget: TargetDefinition, ngPackageConfig: NgPackageConfig | undefined, - secondaryEntryPoints: SecondaryEntryPoint[], tree: Tree, ): void { buildTarget.builder = Builders.BuildLibrary; @@ -286,40 +284,31 @@ function migrateTargetOptions( buildTarget.options['assets'] = [...existingAssets, ...migratedAssets]; } - // The library builder applies style options to all entry points, so the options of - // secondary entry points are merged with the primary ones. - const styleConfigs = [ - { dir: projectDir, lib: ngPackageConfig?.lib }, - ...secondaryEntryPoints.map(({ configFile, config }) => ({ - dir: dirname(configFile), - lib: config?.lib, - })), - ]; - const migratedIncludePaths = styleConfigs.flatMap(({ dir, lib }) => - (lib?.styleIncludePaths ?? []).map((p) => join(dir, p)), - ); - const migratedSassOptions = styleConfigs.flatMap(({ lib }) => (lib?.sass ? [lib.sass] : [])); - - if (migratedIncludePaths.length || migratedSassOptions.length) { + if (ngPackageConfig?.lib?.styleIncludePaths?.length || ngPackageConfig?.lib?.sass) { const stylePreprocessorOptions = buildTarget.options['stylePreprocessorOptions']; const existingPreprocessorOptions: JsonObject = stylePreprocessorOptions && isJsonObject(stylePreprocessorOptions) ? { ...stylePreprocessorOptions } : {}; - if (migratedIncludePaths.length) { + if (ngPackageConfig.lib.styleIncludePaths?.length) { const existingIncludePaths = Array.isArray(existingPreprocessorOptions['includePaths']) ? (existingPreprocessorOptions['includePaths'] as string[]) : []; + const migratedIncludePaths = ngPackageConfig.lib.styleIncludePaths.map((p) => + join(projectDir, p), + ); existingPreprocessorOptions['includePaths'] = [ ...new Set([...existingIncludePaths, ...migratedIncludePaths]), ]; } - if (migratedSassOptions.length) { - const sass: Record = {}; - for (const options of migratedSassOptions) { - for (const [key, value] of Object.entries(options)) { - sass[key] = [...new Set([...(sass[key] ?? []), ...(value ?? [])])]; + if (ngPackageConfig.lib.sass) { + const existingSass = existingPreprocessorOptions['sass']; + const sass: JsonObject = isJsonObject(existingSass) ? { ...existingSass } : {}; + for (const [key, value] of Object.entries(ngPackageConfig.lib.sass)) { + if (Array.isArray(value)) { + const existing = Array.isArray(sass[key]) ? (sass[key] as string[]) : []; + sass[key] = [...new Set([...existing, ...value])]; } } existingPreprocessorOptions['sass'] = sass; @@ -421,7 +410,7 @@ function updateLibraryTarget( return false; } - const ngPackageConfig = primaryProjectFile + let ngPackageConfig = primaryProjectFile ? parseNgPackageFile(tree, primaryProjectFile) : undefined; @@ -451,16 +440,34 @@ function updateLibraryTarget( ? secondary.subpath : `./${secondary.subpath}`; entryPointExports[exportKey] = toRelativeExportPath(secondary.entryPoint); + + if (secondary.config?.lib) { + ngPackageConfig ??= {}; + ngPackageConfig.lib ??= {}; + + if (secondary.config.lib.styleIncludePaths?.length) { + const secondaryDir = dirname(secondary.configFile); + ngPackageConfig.lib.styleIncludePaths ??= []; + for (const includePath of secondary.config.lib.styleIncludePaths) { + ngPackageConfig.lib.styleIncludePaths.push( + relative(projectDir, join(secondaryDir, includePath)), + ); + } + } + + if (secondary.config.lib.sass) { + const sass: Record = (ngPackageConfig.lib.sass ??= {}); + for (const [key, value] of Object.entries(secondary.config.lib.sass)) { + if (Array.isArray(value)) { + const existing = Array.isArray(sass[key]) ? (sass[key] as string[]) : []; + sass[key] = [...new Set([...existing, ...value])]; + } + } + } + } } - migrateTargetOptions( - normalizedProjectRoot, - projectDir, - buildTarget, - ngPackageConfig, - secondaryEntryPoints, - tree, - ); + migrateTargetOptions(normalizedProjectRoot, projectDir, buildTarget, ngPackageConfig, tree); const packageJsonPath = join(normalizedProjectRoot, 'package.json'); diff --git a/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts b/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts index e72c8298c711..98fbe65fd5f6 100644 --- a/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts +++ b/packages/schematics/angular/migrations/use-library-builder/migration_spec.ts @@ -399,6 +399,38 @@ describe('Migration to use the library builder', () => { }); }); + it('should merge existing stylePreprocessorOptions.sass when migrating sass options', async () => { + const angularJson = JSON.parse(tree.readContent('/angular.json')); + angularJson.projects['my-lib'].architect.build.options.stylePreprocessorOptions = { + sass: { silenceDeprecations: ['mixed-decls'] }, + }; + tree.overwrite('/angular.json', JSON.stringify(angularJson, undefined, 2)); + + tree.overwrite( + '/projects/my-lib/ng-package.json', + JSON.stringify( + { + lib: { + entryFile: 'src/public-api.ts', + sass: { silenceDeprecations: ['import'] }, + }, + }, + undefined, + 2, + ), + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { 'my-lib': lib }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = newTree.readJson('/angular.json') as any; + + expect(lib.architect['build'].options.stylePreprocessorOptions).toEqual({ + sass: { silenceDeprecations: ['mixed-decls', 'import'] }, + }); + }); + it('should migrate styleIncludePaths and sass options from secondary entry points', async () => { tree.overwrite( '/projects/my-lib/ng-package.json',