Skip to content

Commit 5f17245

Browse files
build(schematics): keep only architect declared, bundle jsonc-parser (#3808)
* build(schematics): keep only architect declared, bundle jsonc-parser The published manifest declared five build-time packages that only the schematics and the ng deploy builder use. The Angular CLI runs schematics installed under node_modules/@angular/ through its own loader, which supplies @angular-devkit/* and @schematics/* from the CLI's copy, so declaring @angular-devkit/schematics, @angular-devkit/core and @schematics/angular never changed which copy ran. They leave the manifest, and jsonc-parser, the one package the schematics had to resolve on their own, is bundled. @angular-devkit/architect stays: the architect host loads the deploy builder with a plain require, outside that loader. The deploy bundles stop importing the schematic packages (the .firebaserc helper moves out of utils.ts, SchematicsException becomes Error), and the build now fails if a deploy bundle requires a package the manifest does not declare. On a build of this commit, ng add and the deploy builder load under Yarn Plug'n'Play and pnpm hoist=false, and the full ng add command works through a registry on npm. The same change built before #3803 merged ran the wider set of yarn and pnpm layouts and the ng update migration. A production install of a CLI app lists 112 packages instead of 136. Fixes #3694 Refs #3746 * docs: note the build-time packages @angular/fire no longer installs
1 parent 80ed66b commit 5f17245

9 files changed

Lines changed: 91 additions & 47 deletions

File tree

‎docs/version-22-upgrade.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,9 @@ An older version installs anyway with a warning (`has incorrect peer dependency
5656
pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with a warning (`unmet peer firebase@^12.18.0: found 11.x` on pnpm 9 and 10, `Issues with peer dependencies found. Run "pnpm peers check" to list them.` on pnpm 11 and 12), and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile.
5757

5858
Set `strict-peer-dependencies=true` in `.npmrc` to make pnpm refuse the mismatch outright.
59+
60+
## Fewer build-time packages
61+
62+
`@angular/fire` no longer installs `@angular-devkit/schematics`, `@angular-devkit/core` or `@schematics/angular`. The Angular CLI supplies them when it runs `ng add` and `ng update`, so nothing changes for an Angular CLI or Nx workspace.
63+
64+
If you run AngularFire's schematics with the standalone `schematics` command on pnpm with `hoist=false`, add those three packages to your own `package.json`.

‎src/ng-package.json‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
},
66
"dest": "../dist/packages-dist",
77
"allowedNonPeerDependencies": [
8-
"@angular-devkit/architect", "@angular-devkit/core",
9-
"@angular-devkit/schematics", "@schematics/angular",
10-
"jsonc-parser", "rxfire"
8+
"@angular-devkit/architect", "rxfire"
119
]
1210
}

‎src/package.json‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@
4343
"dependencies": {
4444
"rxfire": "^6.2.0",
4545
"@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0",
46-
"@angular-devkit/core": "^22.0.0",
47-
"@angular-devkit/schematics": "^22.0.0",
48-
"@schematics/angular": "^22.0.0",
49-
"jsonc-parser": "^3.0.0",
5046
"tslib": "^2.3.0"
5147
},
5248
"ng-update": {

‎src/schematics/deploy/actions.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
33
import { dirname, join } from 'path';
44
import { fileURLToPath } from 'url';
55
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
6-
import { SchematicsException } from '@angular-devkit/schematics';
76
import crossSpawn from 'cross-spawn';
87
import fsExtra from 'fs-extra';
98
import * as inquirer from 'inquirer';
@@ -82,7 +81,7 @@ const escapeRegExp = (str: string) => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&
8281
// being read as a flag rather than a path.
8382
export const assertSafeOutputPath = (outputPath: string, targetName: string): void => {
8483
if (/['"`\\$\n\r]/.test(outputPath) || outputPath.startsWith('-')) {
85-
throw new SchematicsException(
84+
throw new Error(
8685
`Unsafe outputPath ${JSON.stringify(outputPath)} for target '${targetName}' in angular.json.`
8786
);
8887
}
@@ -95,7 +94,7 @@ export const assertSafeOutputPath = (outputPath: string, targetName: string): vo
9594
// example one containing a dash) into an explicit error.
9695
export const assertSafeFunctionName = (functionName: string | undefined): void => {
9796
if (functionName !== undefined && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(functionName)) {
98-
throw new SchematicsException(
97+
throw new Error(
9998
`Unsafe functionName ${JSON.stringify(functionName)} in angular.json; expected a plain identifier.`
10099
);
101100
}
@@ -113,7 +112,7 @@ const NODE_IMAGE_TAG = /^(?!latest$)[\w][\w.-]{0,127}$/;
113112

114113
export const assertSafeNodeVersion = (version: string | number | undefined): void => {
115114
if (version !== undefined && !NODE_IMAGE_TAG.test(String(version))) {
116-
throw new SchematicsException(
115+
throw new Error(
117116
`Unsafe functionsNodeVersion ${JSON.stringify(version)} in angular.json; expected a node image tag, such as 22 or lts.`
118117
);
119118
}
@@ -185,7 +184,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'cnpm', 'bun']
185184

186185
export const assertSupportedPackageManager = (packageManager: string): string => {
187186
if (!SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
188-
throw new SchematicsException(
187+
throw new Error(
189188
`Unsupported package manager "${packageManager}" in angular.json (cli.packageManager). ` +
190189
`Expected one of: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}.`
191190
);
@@ -217,7 +216,7 @@ export const processHost = {
217216
const result = crossSpawn.sync(command, args, options);
218217
if (result.error) { throw result.error; }
219218
if (result.status !== 0) {
220-
throw new SchematicsException(
219+
throw new Error(
221220
`Command "${command}" exited with ${result.signal ? `signal ${result.signal}` : `code ${result.status}`}.`
222221
);
223222
}
@@ -510,7 +509,7 @@ export const deployToCloudRun = async (
510509
}
511510

512511
if (options.preview) {
513-
throw new SchematicsException('Cloud Run preview not supported.');
512+
throw new Error('Cloud Run preview not supported.');
514513
}
515514

516515
const deployArguments: string[] = [];

‎src/schematics/deploy/builder.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
22
import { getFirebaseTools } from '../firebaseTools';
3+
import { getFirebaseProjectNameFromFs } from '../firebaserc';
34
import { BuildTarget } from '../interfaces';
4-
import { getFirebaseProjectNameFromFs } from '../utils';
55
import deploy, { DeployBuilderOptions } from './actions';
66

77
// Call the createBuilder() function to create a builder. This mirrors

‎src/schematics/firebaserc.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { readFileSync } from "fs";
2+
import { join } from "path";
3+
import { FirebaseRc } from "./interfaces";
4+
5+
export const projectFromRc = (
6+
rc: FirebaseRc,
7+
target: string
8+
): [string | undefined, string | undefined] => {
9+
const defaultProject = rc.projects?.default;
10+
const project = Object.keys(rc.targets || {}).find(
11+
(project) => !!rc.targets?.[project]?.hosting?.[target]
12+
);
13+
const site = project && rc.targets?.[project]?.hosting?.[target]?.[0];
14+
return [project || defaultProject, site];
15+
};
16+
17+
export function getFirebaseProjectNameFromFs(
18+
root: string,
19+
target: string
20+
): [string | undefined, string | undefined] {
21+
const path = join(root, ".firebaserc");
22+
try {
23+
const buffer = readFileSync(path);
24+
const rc: FirebaseRc = JSON.parse(buffer.toString());
25+
return projectFromRc(rc, target);
26+
} catch {
27+
return [undefined, undefined];
28+
}
29+
}

‎src/schematics/utils.ts‎

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { NodePackageInstallTask } from "@angular-devkit/schematics/tasks/index.j
1111
import { addRootProvider } from "@schematics/angular/utility";
1212
import { parse } from "yaml";
1313
import { overwriteIfExists, safeReadJSON, stringifyFormatted } from "./common.js";
14+
import { projectFromRc } from "./firebaserc.js";
1415
import {
1516
ConnectorConfig,
1617
ConnectorYaml,
@@ -90,32 +91,6 @@ export function getFirebaseProjectNameFromHost(
9091
return projectFromRc(rc, target);
9192
}
9293

93-
export function getFirebaseProjectNameFromFs(
94-
root: string,
95-
target: string
96-
): [string | undefined, string | undefined] {
97-
const path = join(root, ".firebaserc");
98-
try {
99-
const buffer = readFileSync(path);
100-
const rc: FirebaseRc = JSON.parse(buffer.toString());
101-
return projectFromRc(rc, target);
102-
} catch (_) {
103-
return [undefined, undefined];
104-
}
105-
}
106-
107-
const projectFromRc = (
108-
rc: FirebaseRc,
109-
target: string
110-
): [string | undefined, string | undefined] => {
111-
const defaultProject = rc.projects?.default;
112-
const project = Object.keys(rc.targets || {}).find(
113-
(project) => !!rc.targets?.[project]?.hosting?.[target]
114-
);
115-
const site = project && rc.targets?.[project]?.hosting?.[target]?.[0];
116-
return [project || defaultProject, site];
117-
};
118-
11994
// TODO rewrite using typescript
12095
export function addFixesToServer(host: Tree) {
12196
const serverPath = `/server.ts`;

‎src/schematics/workspace.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
import { existsSync, readFileSync } from 'fs';
1515
import { dirname, join } from 'path';
16-
import { SchematicsException } from '@angular-devkit/schematics';
1716
import { parse as parseJsonWithComments } from 'jsonc-parser';
1817

1918
/**
@@ -102,7 +101,7 @@ export const workspaceRootFor = (startDirectory: string): string => {
102101
export const assertSafeDependencyName = (name: string, source: string): string => {
103102
if (typeof name !== 'string' || name.length === 0 || name.startsWith('-') ||
104103
/[\s;&|$`(){}<>!\\'"]/.test(name)) {
105-
throw new SchematicsException(`Invalid dependency name ${JSON.stringify(name)} ${source}.`);
104+
throw new Error(`Invalid dependency name ${JSON.stringify(name)} ${source}.`);
106105
}
107106
return name;
108107
};

‎tools/build.ts‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { spawn } from 'cross-spawn';
2-
import { copy, writeFile } from 'fs-extra';
3-
import { join, sep } from 'path';
2+
import { copy, readJson, writeFile } from 'fs-extra';
3+
import { join, resolve, sep } from 'path';
44
import { keys as tsKeys } from 'ts-transformer-keys';
55
import * as esbuild from "esbuild";
66
import type { ReplaceResult } from 'replace-in-file';
@@ -394,7 +394,6 @@ const externalPackages = [
394394
"@angular-devkit/core",
395395
"rxjs",
396396
"@schematics/angular",
397-
"jsonc-parser",
398397
"firebase-tools",
399398
"typescript"
400399
];
@@ -404,6 +403,17 @@ const externalPackages = [
404403
const exportedSubpaths = ['@angular-devkit/schematics/tasks', '@schematics/angular/utility', 'rxjs/operators'];
405404
const isPackageSubpath = (path: string) => externalPackages.some(name => path.startsWith(`${name}/`));
406405

406+
/* The Angular CLI runs schematics through a loader that supplies @angular-devkit/* and
407+
* @schematics/* from its own copy, so the published manifest does not declare them. The deploy
408+
* builder is loaded by @angular-devkit/architect without that loader, so every package its bundles
409+
* require must be declared in src/package.json. */
410+
const builderEntryPoints = schematicEntryPoints.filter(([first]) => first === 'deploy');
411+
// The installed @types/node predates module.isBuiltin, so the list is read untyped.
412+
const nodeModule: { builtinModules: string[] } = require('module');
413+
const isNodeBuiltin = (importPath: string) => nodeModule.builtinModules.includes(importPath.replace(/^node:/, ''));
414+
const packageNameOf = (importPath: string) =>
415+
importPath.split('/').slice(0, importPath.startsWith('@') ? 2 : 1).join('/');
416+
407417
async function compileSchematics() {
408418
const { metafile } = await esbuild.build({
409419
entryPoints: schematicEntryPoints.map(segments => `${src('schematics', ...segments)}.ts`),
@@ -417,7 +427,10 @@ async function compileSchematics() {
417427
platform: "node",
418428
target: "es2016",
419429
external: externalPackages,
420-
alias: { "@angular-devkit/schematics/tasks/index.js": "@angular-devkit/schematics/tasks" },
430+
alias: {
431+
"@angular-devkit/schematics/tasks/index.js": "@angular-devkit/schematics/tasks",
432+
"jsonc-parser": "jsonc-parser/lib/esm/main.js",
433+
},
421434
outdir: dest('schematics'),
422435
metafile: true,
423436
});
@@ -432,6 +445,35 @@ async function compileSchematics() {
432445
const required = unlistedSubpaths.map(subpath => `\n ${subpath}, imported by ${importersBySubpath.get(subpath)?.join(', ')}`);
433446
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 <package>@next exports), add it to exportedSubpaths.`);
434447
}
448+
/* Every package the deploy builder bundles require at run time must be a dependency or peer of
449+
* the published package (src/package.json), because nothing else resolves them for the builder. */
450+
const publishedManifest = await readJson(src('package.json'));
451+
const declaredPackages = new Set([
452+
...Object.keys(publishedManifest.dependencies ?? {}),
453+
...Object.keys(publishedManifest.peerDependencies ?? {}),
454+
]);
455+
const undeclaredInBuilder: string[] = [];
456+
for (const segments of builderEntryPoints) {
457+
/* Find the bundle esbuild wrote for this entry point. The metafile records entry points
458+
* relative to the working directory, so compare absolute paths. */
459+
const entryFile = `${src('schematics', ...segments)}.ts`;
460+
const outputs = Object.entries(metafile.outputs)
461+
.filter(([, output]) => output.entryPoint && resolve(output.entryPoint) === entryFile);
462+
if (outputs.length !== 1) {
463+
throw new Error(`Expected exactly one bundle for ${join(...segments)}, found ${outputs.length}. The builder dependency check cannot run.`);
464+
}
465+
/* The bundle's `imports` are the requires esbuild left in place (the externals). Each one must
466+
* be a Node builtin or belong to a declared package. */
467+
const [file, output] = outputs[0];
468+
for (const entry of output.imports) {
469+
if (entry.external && !isNodeBuiltin(entry.path) && !declaredPackages.has(packageNameOf(entry.path))) {
470+
undeclaredInBuilder.push(`\n ${file} requires ${entry.path}`);
471+
}
472+
}
473+
}
474+
if (undeclaredInBuilder.length) {
475+
throw new Error(`The deploy builder bundles require packages that src/package.json does not declare:${undeclaredInBuilder.join('')}\n@angular-devkit/architect loads the builder outside the Angular CLI's schematic loader, which supplies @angular-devkit/* and @schematics/* to schematics only. Declare the package in src/package.json, or remove the import from the builder's import graph (a module-level import in a shared file is enough to pull it in).`);
476+
}
435477
await Promise.all([
436478
copy(src('schematics', 'versions.json'), dest('schematics', 'versions.json')),
437479
copy(src('schematics', 'builders.json'), dest('schematics', 'builders.json')),

0 commit comments

Comments
 (0)