From d88d4d6eebab1c50bc9b714620f2edb35cd46da2 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 7 Oct 2026 16:00:05 -0700 Subject: [PATCH] fix(schematics): require only exported devkit paths from the shipped schematics @angular-devkit/schematics 22.3 adds an exports map that allows ./tasks but not ./tasks/index.js. The shipped setup, deploy builder and v21 migration bundles required tasks/index.js, so `ng update` crashed after installing whenever it ran a 22.3 CLI, and the v21 migration never ran. The source keeps the index.js spelling, which the ESM spec run needs while the repo's devkit 21 has no exports map. An esbuild alias rewrites it in the bundles, and the build now fails when a bundle requires a package subpath that is not in exportedSubpaths, naming the importers. typescript stays external so the v21 migration uses the compiler from the user's workspace at ng update time, rather than adding about 3.5 MB to the package. --- tools/build.ts | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tools/build.ts b/tools/build.ts index 46a1ed370..53222cda2 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -384,8 +384,24 @@ const schematicEntryPoints = [ ['update', 'v21', 'index'], ]; +const externalPackages = [ + "@angular-devkit/schematics", + "@angular-devkit/architect", + "@angular-devkit/core", + "rxjs", + "@schematics/angular", + "jsonc-parser", + "firebase-tools", + "typescript" +]; + +/* Allowed package subpaths for bundles. Each must be in that package's `exports` map, or the + * schematics cannot load with any version of the package that has one. */ +const exportedSubpaths = ['@angular-devkit/schematics/tasks', '@schematics/angular/utility', 'rxjs/operators']; +const isPackageSubpath = (path: string) => externalPackages.some(name => path.startsWith(`${name}/`)); + async function compileSchematics() { - await esbuild.build({ + const { metafile } = await esbuild.build({ entryPoints: schematicEntryPoints.map(segments => `${src('schematics', ...segments)}.ts`), format: "cjs", // turns out schematics don't support ESM, need to use webpack or shim these @@ -396,20 +412,22 @@ async function compileSchematics() { minify: true, platform: "node", target: "es2016", - external: [ - "@angular-devkit/schematics", - "@angular-devkit/architect", - "@angular-devkit/core", - "rxjs", - "@schematics/angular", - "jsonc-parser", - "firebase-tools", - // The v21 migration parses user source with the TypeScript compiler; resolve it from - // the workspace at ng-update time instead of bundling ~3.5MB into the package. - "typescript" - ], + external: externalPackages, + alias: { "@angular-devkit/schematics/tasks/index.js": "@angular-devkit/schematics/tasks" }, outdir: dest('schematics'), + metafile: true, }); + const importersBySubpath = new Map(); + for (const [importer, { imports }] of Object.entries(metafile.inputs)) { + for (const { path } of imports.filter(entry => entry.external && isPackageSubpath(entry.path))) { + importersBySubpath.set(path, [...(importersBySubpath.get(path) ?? []), importer]); + } + } + const unlistedSubpaths = [...importersBySubpath.keys()].filter(subpath => !exportedSubpaths.includes(subpath)); + if (unlistedSubpaths.length) { + const required = unlistedSubpaths.map(subpath => `\n ${subpath}, imported by ${importersBySubpath.get(subpath)?.join(', ')}`); + throw new Error(`The schematics require package paths that exportedSubpaths does not list:${required.join('')}\nAdd an alias in compileSchematics to a path the package exports, or, if the exports map in the newest published version lists the path (npm view @next exports), add it to exportedSubpaths.`); + } await Promise.all([ copy(src('schematics', 'versions.json'), dest('schematics', 'versions.json')), copy(src('schematics', 'builders.json'), dest('schematics', 'builders.json')),