From 4bc59efdfa975e186ca4a46ff5b2c14b5b933f06 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Mon, 3 Aug 2026 15:46:44 -0700 Subject: [PATCH 1/2] fix(schematics): restore ng deploy under the CommonJS schematics bundle `ng deploy` threw at module load in 21.0.0-rc.0, before any user code ran: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string or an instance of URL. Received undefined at fileURLToPath (node:internal/url) The schematics are bundled by esbuild with `format: "cjs"`, and esbuild rewrites `import.meta` to an empty object in CommonJS output. The shipped bundle therefore read `undefined.url`, so both `deploy/actions.js` and `deploy/builder.js` failed to load. Every other shipped entry point (ng add, both ng update migrations, the setup schematic) was unaffected. The shim was introduced when `versions.json` moved from a compile-time import to a runtime read. That move fixed a real bug of its own: because esbuild bundles before the build copies and rewrites `versions.json`, the compile-time import inlined the unreplaced `0.0.0` placeholders, and 20.0.1 generates a Cloud Functions manifest pinning `0.0.0` that cannot install. So the runtime read has to stay. `typeof` on an undeclared identifier is the one form that does not throw under ESM, so a single expression works under both loaders, and the CommonJS branch comes first because `import.meta` is the substituted empty object there. The alternatives were built and run, not assumed: - plain `__dirname` breaks `npm run test:node-esm`, which genuinely loads the compiled specs as ESM - `require('../versions.json')` reintroduces the `0.0.0` bug above - an esbuild define/banner works today but fails with "require is not defined in ES module scope" the moment `format: "esm"` is enabled, which tools/build.ts already has staged in a comment Verified against the built package: all seven shipped entry points now load via both `require()` and `await import()`, the builder exposes the Architect builder symbols, and the runtime `versions.json` read resolves correctly. `ng lint` also drops its only warning, which sat on the replaced line. This is v21-only. v20 has no `import.meta` shim and must not take this change. --- src/schematics/deploy/actions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 9a2fb7cde..cecf255bf 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -13,8 +13,9 @@ import * as winston from 'winston'; import { BuildTarget, CloudRunOptions, DeployBuilderSchema, FSHost, FirebaseTools } from '../interfaces'; import { DEFAULT_FUNCTION_NAME, defaultFunction, defaultPackage, dockerfile, functionGen2 } from './functions-templates.js'; -// @ts-ignore -const __dirname = dirname(fileURLToPath(import.meta.url)); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore `import.meta` is rejected by the --module es2015 pass of `npm run build:jasmine`. +const moduleDirectory = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url)); const { copySync, removeSync, readJsonSync } = fsExtra; @@ -128,7 +129,7 @@ const findPackageVersion = (packageManager: string, name: string) => { const getPackageJson = (context: BuilderContext, workspaceRoot: string, options: DeployBuilderOptions, main?: string) => { const dependencies: Record = {}; const devDependencies: Record = {}; - const { firebaseFunctionsDependencies } = readJsonSync(join(__dirname, '..', 'versions.json')); + const { firebaseFunctionsDependencies } = readJsonSync(join(moduleDirectory, '..', 'versions.json')); if (options.ssr !== 'cloud-run') { Object.keys(firebaseFunctionsDependencies).forEach(name => { const { version, dev } = firebaseFunctionsDependencies[name]; From 5c7aba4eccb25da77bcd53d3549b0d3e20f599fe Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Mon, 3 Aug 2026 15:46:54 -0700 Subject: [PATCH 2/2] build: load every compiled schematic before publishing The load failure fixed in the previous commit reached a published release because nothing in the build or the test suite ever loads what actually ships. The jasmine suite runs against the TypeScript output, which is a different module format from the CommonJS bundle in the package, so a bundle can be completely unloadable while every test passes. Requiring each compiled entry point at the end of the schematics build closes that gap. Reverting the previous commit now fails the build with the real error: Compiled schematics failed to load: deploy/actions.js: TypeError [ERR_INVALID_ARG_TYPE] ... deploy/builder.js: TypeError [ERR_INVALID_ARG_TYPE] ... It catches the whole class, not just this instance: an unresolvable import, a bad top-level require, or anything else that throws at module load. --- tools/build.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tools/build.ts b/tools/build.ts index 0d8dd38c4..ee3e6a64c 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -357,6 +357,35 @@ async function compileSchematics() { copy(src('schematics', 'setup', 'schema.json'), dest('schematics', 'setup', 'schema.json')), ]); await replaceSchematicVersions(); + await loadCompiledSchematics(); +} + +/** + * Loads every compiled schematic entry point, so a bundle that cannot even be required fails the + * build instead of shipping. + */ +async function loadCompiledSchematics() { + const entryPoints = [ + join('update', 'index.js'), + join('deploy', 'actions.js'), + join('deploy', 'builder.js'), + join('add', 'index.js'), + join('setup', 'index.js'), + join('update', 'v7', 'index.js'), + join('update', 'v21', 'index.js'), + ]; + const failures: string[] = []; + for (const entryPoint of entryPoints) { + const path = dest('schematics', entryPoint); + try { + require(path); + } catch (error) { + failures.push(` ${entryPoint}: ${error}`); + } + } + if (failures.length) { + throw new Error(`Compiled schematics failed to load:\n${failures.join('\n')}`); + } } async function buildLibrary() {