From 8a12246973d11735d764aa6ddecd5bf069181bb7 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 10 Jul 2026 15:43:53 -0700 Subject: [PATCH 01/13] fix(schematics): only emit FirebaseOptions keys in the generated app config (#3707) The Firebase CLI's apps.sdkconfig response includes management-API fields (projectNumber, version, locationId) alongside the web app config. The schematic only deleted locationId and inlined the rest into initializeApp(), so the generated app failed to compile with TS2769: 'projectNumber' does not exist in type 'FirebaseOptions'. Keep only the keys FirebaseOptions accepts, so future additions to the CLI response cannot break the generated config again. --- src/schematics/setup/index.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 06e29f789..3d40ed132 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -18,6 +18,12 @@ import { } from '../utils'; import { appPrompt, featuresPrompt, projectPrompt, userPrompt } from './prompts'; +// FirebaseOptions keys — apps.sdkconfig responses include management-API extras that initializeApp() rejects. +const firebaseOptionsKeys = [ + 'apiKey', 'authDomain', 'databaseURL', 'projectId', 'storageBucket', + 'messagingSenderId', 'appId', 'measurementId', 'recaptchaSiteKey', +]; + export interface SetupConfig extends DeployOptions { firebaseProject: FirebaseProject, firebaseApp?: FirebaseApp, @@ -92,8 +98,11 @@ export const ngAddSetupProject = ( firebaseApp = await appPrompt(firebaseProject, undefined, { projectRoot }); const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true, projectRoot }); - sdkConfig = result.sdkConfig; - delete sdkConfig.locationId; + sdkConfig = Object.fromEntries( + firebaseOptionsKeys + .filter(key => result.sdkConfig[key] !== undefined) + .map(key => [key, result.sdkConfig[key]]) + ); setupConfig.sdkConfig = sdkConfig; setupConfig.firebaseApp = firebaseApp; // set up data connect locally if data connect hasn't already been initialized. From 6abc0f74715959d8d0ca846afdae5a020ada00a9 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Tue, 4 Aug 2026 17:34:14 -0700 Subject: [PATCH 02/13] fix(schematics): use the exported logger/compat subpath in generated functions (#3730) The SSR Cloud Function generated by `ng deploy` crashed at cold start: Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/logger/compat' is not defined by "exports" in node_modules/firebase-functions/package.json Both generated templates required `firebase-functions/lib/logger/compat`. firebase-functions declares an exports map, and the internal `lib/` path is not in it. The exported subpath is `firebase-functions/logger/compat`, which maps to the same file. Verified by generating the function source and loading it against a real firebase-functions install: the shipped path throws, the exported path loads and emits structured Cloud Logging JSON, which is what the compat logger is there to do. This affects the deployed function rather than the build, so it fails at runtime in Cloud Functions rather than during `ng deploy`, and it is present in both the 20.x and 21.x lines. (cherry picked from commit a99f09b72aa941946d1412149c6c8b4e9dc51783) --- src/schematics/deploy/functions-templates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schematics/deploy/functions-templates.ts b/src/schematics/deploy/functions-templates.ts index 4005675c9..13cd9ab14 100644 --- a/src/schematics/deploy/functions-templates.ts +++ b/src/schematics/deploy/functions-templates.ts @@ -37,7 +37,7 @@ export const defaultFunction = ( ) => `const functions = require('firebase-functions'); // Increase readability in Cloud Logging -require("firebase-functions/lib/logger/compat"); +require("firebase-functions/logger/compat"); const expressApp = require('./${path}/main').app(); @@ -55,7 +55,7 @@ export const functionGen2 = ( ) => `const { onRequest } = require('firebase-functions/v2/https'); // Increase readability in Cloud Logging -require("firebase-functions/lib/logger/compat"); +require("firebase-functions/logger/compat"); const expressApp = require('./${path}/main').app(); From 20b342e47e47d5a210caee5ecca462c5f811411c Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Mon, 20 Jul 2026 16:56:23 -0700 Subject: [PATCH 03/13] fix(core): drop vestigial @angular/platform-browser-dynamic peer (#3718) Every platform-browser-dynamic 20.x/21.x patch release requires the exact @angular/core and @angular/common version matching its own patch. Declaring pbd as a required peer therefore makes npm pull in a pbd version whose required core/common usually differs from the app's installed core/common, failing `ng add @angular/fire` with ERESOLVE on Angular >= 20.1 apps. Nothing in the published package imports platform-browser-dynamic; the only usage in the repo is the Karma test bootstrap (src/test.ts), which is supplied by the root package.json and is unaffected. Removing a peer nothing imports is non-breaking for consumers. Considered marking the peer optional via peerDependenciesMeta instead (the manifest already uses that pattern for firebase-tools and platform-server); it would also avoid the ERESOLVE on npm, but it keeps advertising a dependency relationship that does not exist and relies on npm-specific resolver behavior. Deletion fixes every package manager. Refs #3667 (cherry picked from commit 8dfcc2c8b5c117d0a35d60bd3fb2cf55c5a84eb4) --- src/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/package.json b/src/package.json index 4f0bc9082..71657cddd 100644 --- a/src/package.json +++ b/src/package.json @@ -26,7 +26,6 @@ "@angular/common": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@angular/platform-browser-dynamic": "^20.0.0", "@angular/platform-server": "^20.0.0", "rxjs": "~7.8.0", "firebase-tools": "^14.0.0" From 4fcb83d532cb870b16c5cd1fc2fd89279aab1919 Mon Sep 17 00:00:00 2001 From: Munir I Said Date: Wed, 8 Jul 2026 00:14:29 +0300 Subject: [PATCH 04/13] Update firebaseTools schematics to reflect v14.0.0 firebase-tools requirement (#3662) (cherry picked from commit 8bf386e5587b65dfbe3aefeacb2514bcdee8044f) --- src/schematics/firebaseTools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schematics/firebaseTools.ts b/src/schematics/firebaseTools.ts index 2d8c394b1..3d8136763 100644 --- a/src/schematics/firebaseTools.ts +++ b/src/schematics/firebaseTools.ts @@ -45,7 +45,7 @@ export const getFirebaseTools = () => globalThis.firebaseTools ? const version = firebaseTools.cli.version(); console.log(`Using firebase-tools version ${version}`); if (semverCompare(version, '14.0.0') === -1) { - console.error('firebase-tools version 13.0.0+ is required, please upgrade and run again'); + console.error('firebase-tools version 14.0.0+ is required, please upgrade and run again'); return Promise.reject(); } return firebaseTools; From 454ec7ac6fbdf8b35ae5bf21590cfeda0ae83240 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 8 Jul 2026 14:18:22 -0700 Subject: [PATCH 05/13] fix(schematics): accept firebase-tools 15 in the peer range (#3702) The optional firebase-tools peerDependency was capped at ^14.0.0, which excludes the current major (15.x). The ng add version check already accepts anything >= 14.0.0 and the schematic runs fine on 15.x, so the narrow range only produces spurious peer-dependency warnings for users on the current CLI. (cherry picked from commit 45f138df324dc0c86c0e052115b532300ccf00bb) --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index 71657cddd..1cce5ce94 100644 --- a/src/package.json +++ b/src/package.json @@ -28,7 +28,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "rxjs": "~7.8.0", - "firebase-tools": "^14.0.0" + "firebase-tools": "^14.0.0 || ^15.0.0" }, "peerDependenciesMeta": { "firebase-tools": { "optional": true }, From 29bf65d31054845b2619e558e2f05b8e34a2d6b2 Mon Sep 17 00:00:00 2001 From: Maxim Wilman Date: Mon, 12 Jan 2026 20:53:53 +0100 Subject: [PATCH 06/13] Upgrade Java version from 11 to 21 in workflow (cherry picked from commit 7576b5fb5c3baf58bceb6b190653437403e0d747) --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebb9d9b3e..6786c6311 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -115,7 +115,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '11' + java-version: '21' - name: node_modules cache id: node_modules_cache uses: actions/cache@v3 @@ -183,7 +183,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '11' + java-version: '21' - name: Test headless run: npm run test:chrome-headless # Tests are flaky on Windows From d0aaa8496e7a3570e04c0b2efc5242dcd4e2be81 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 12 Aug 2026 12:26:57 -0700 Subject: [PATCH 07/13] fix(deploy): target Node 22 and the firebase-functions/v1 API in generated Cloud Functions (#3743) * fix(deploy): target Node 22 and the firebase-functions/v1 API in generated Cloud Functions The generated function declared engines.node 14, a runtime Cloud Functions decommissioned in early 2025, so it could not deploy at all. The gen 1 template also called functions.region() on the firebase-functions package root, which moved to the /v1 subpath in v6, so a deployed function crashed at cold start with TypeError: functions.region is not a function. Node 22 is the newest runtime Cloud Functions supports and satisfies the engines ranges of @angular/core and firebase-admin. Node 20 would deploy today, but its security support ended 2026-04-30. The same constant picks the Cloud Run base image, now node:22-slim. The docs example moves off the decommissioned functionsNodeVersion 12. * docs(site): update the functionsNodeVersion example in the site copy The site/ copy of the deploy guide still showed the decommissioned 12, flagged in review. Also fixes a spelling error on the same passage. * test(deploy): assert the generated template's require path and runtime Add two specs for what the earlier commit in this PR fixed: the gen 1 template requires firebase-functions/v1, and the default runtime is 22. Both specs fail when run against the old template. Also moves the /v1 comment above the export so it no longer reads like a parameter annotation. --- docs/deploy/getting-started.md | 2 +- site/src/get-started/deploying.md | 4 ++-- .../deploy/functions-templates.jasmine.ts | 20 +++++++++++++++++++ src/schematics/deploy/functions-templates.ts | 9 +++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/schematics/deploy/functions-templates.jasmine.ts diff --git a/docs/deploy/getting-started.md b/docs/deploy/getting-started.md index 5e3cf180d..1c701adc0 100644 --- a/docs/deploy/getting-started.md +++ b/docs/deploy/getting-started.md @@ -97,7 +97,7 @@ Setting `functionsNodeVersion` and `functionsRuntimeOptions` in your `angular.js "deploy": { "builder": "@angular/fire:deploy", "options": { - "functionsNodeVersion": 12, + "functionsNodeVersion": 22, "functionsRuntimeOptions": { "memory": "2GB", "timeoutSeconds": 10, diff --git a/site/src/get-started/deploying.md b/site/src/get-started/deploying.md index b0e9011b4..f5c4886a7 100644 --- a/site/src/get-started/deploying.md +++ b/site/src/get-started/deploying.md @@ -98,13 +98,13 @@ To customize the deployment flow, you can use the configuration files you're alr ### Configuring Cloud Functions -Setting `functionsNodeVersion` and `functionsRuntimeOptions` in your `angular.json` allow you to custimze the version of Node.js Cloud Functions is running and run-time settings like timeout, VPC connectors, and memory. +Setting `functionsNodeVersion` and `functionsRuntimeOptions` in your `angular.json` allow you to customize the version of Node.js Cloud Functions is running and run-time settings like timeout, VPC connectors, and memory. ```json "deploy": { "builder": "@angular/fire:deploy", "options": { - "functionsNodeVersion": 12, + "functionsNodeVersion": 22, "functionsRuntimeOptions": { "memory": "2GB", "timeoutSeconds": 10, diff --git a/src/schematics/deploy/functions-templates.jasmine.ts b/src/schematics/deploy/functions-templates.jasmine.ts new file mode 100644 index 000000000..b6bf4c148 --- /dev/null +++ b/src/schematics/deploy/functions-templates.jasmine.ts @@ -0,0 +1,20 @@ +import { DEFAULT_NODE_VERSION, defaultFunction, defaultPackage } from './functions-templates.js'; +import 'jasmine'; + +describe('functions templates', () => { + describe('defaultFunction', () => { + it('requires the firebase-functions/v1 subpath, where the v1 API lives on firebase-functions 6', () => { + const generated = defaultFunction('dist/app', {}, undefined); + expect(generated).toContain(`require('firebase-functions/v1')`); + expect(generated).not.toContain(`require('firebase-functions')`); + }); + }); + + describe('defaultPackage', () => { + it('defaults engines.node to a runtime Cloud Functions still accepts', () => { + const generated = defaultPackage({}, {}, {}); + expect(generated.engines.node).toBe(DEFAULT_NODE_VERSION.toString()); + expect(DEFAULT_NODE_VERSION).toBe(22); + }); + }); +}); diff --git a/src/schematics/deploy/functions-templates.ts b/src/schematics/deploy/functions-templates.ts index 13cd9ab14..b7af2a3b1 100644 --- a/src/schematics/deploy/functions-templates.ts +++ b/src/schematics/deploy/functions-templates.ts @@ -1,6 +1,9 @@ import { DeployBuilderOptions } from './actions'; -export const DEFAULT_NODE_VERSION = 14; +// Must be a runtime Cloud Functions still accepts, and must satisfy the engines of @angular/core +// (the function runs the Angular server) and of firebase-admin. The `dockerfile` template below +// also drops it into `FROM node:-slim`, so it picks the base image on the Cloud Run path. +export const DEFAULT_NODE_VERSION = 22; export const DEFAULT_FUNCTION_NAME = 'ssr'; const DEFAULT_FUNCTION_REGION = 'us-central1'; @@ -30,11 +33,13 @@ export const defaultPackage = ( private: true }); +// `.region()` lives on the /v1 subpath since firebase-functions 6. Requiring the package root +// instead still resolves, and fails only when the deployed function serves its first request. export const defaultFunction = ( path: string, options: DeployBuilderOptions, functionName: string|undefined, -) => `const functions = require('firebase-functions'); +) => `const functions = require('firebase-functions/v1'); // Increase readability in Cloud Logging require("firebase-functions/logger/compat"); From d47dd05bfe7fe9fc9011b52b32d963a91feecea7 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 12 Aug 2026 12:35:43 -0700 Subject: [PATCH 08/13] fix(deploy): declare firebase-admin in the generated Cloud Functions manifest (#3745) * fix(deploy): declare firebase-admin in the generated Cloud Functions manifest The root package.json had no firebase-admin entry, so the build resolved its version to undefined and JSON.stringify dropped it from the generated manifest. firebase-functions 6.x requires firebase-admin eagerly at module load, so the deployed function crashed at cold start wherever peers are not auto-installed (--legacy-peer-deps, yarn 1, pnpm). firebase-functions moves to ^6.1.1, the first release whose peer range accepts firebase-admin 13. * build: fail the build when a schematic dependency version is missing replaceSchematicVersions resolved versions from the root package.json with no check, and a missing entry was silently dropped from the shipped versions.json by JSON.stringify. That is how the generated Cloud Functions manifest lost firebase-admin (#3744). The build now throws, naming the package, and both loops share the guard. --- package-lock.json | 137 ++++++++++------------------------------------ package.json | 3 +- tools/build.ts | 13 ++++- 3 files changed, 41 insertions(+), 112 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45a587ed3..149335d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@angular/fire", - "version": "20.0.0", + "version": "20.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@angular/fire", - "version": "20.0.0", + "version": "20.0.2", "license": "MIT", "dependencies": { "@angular-devkit/architect": "~0.2000.0", @@ -21,7 +21,8 @@ "@schematics/angular": "^20.0.0", "esbuild": "^0.24.0", "firebase": "^11.8.0", - "firebase-functions": "^6.1.0", + "firebase-admin": "^13.0.0", + "firebase-functions": "^6.1.1", "fs-extra": "^8.0.1", "fuzzy": "^0.1.3", "husky": "^4.2.5", @@ -4775,8 +4776,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@firebase/ai": { "version": "1.3.0", @@ -5392,7 +5392,6 @@ "integrity": "sha512-ZxOdH8Wr01hBDvKCQfMWqwUcfNcN3JY19k1LtS1fTFhEyorYPLsbWN+VxIRL46pOYGHTPkU3Or5HbT/SLQM5nA==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", @@ -5410,7 +5409,6 @@ "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" @@ -5425,7 +5423,6 @@ "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=14.0.0" } @@ -5436,7 +5433,6 @@ "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=14" } @@ -5447,7 +5443,6 @@ "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", @@ -5475,7 +5470,6 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "optional": true, - "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -5791,7 +5785,6 @@ "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", "license": "MIT", "optional": true, - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -6658,7 +6651,6 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8.0.0" } @@ -7508,7 +7500,6 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10" } @@ -7562,8 +7553,7 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/connect": { "version": "3.4.38", @@ -7731,7 +7721,6 @@ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/ms": "*", "@types/node": "*" @@ -7759,8 +7748,7 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/mime": { "version": "1.3.5", @@ -7772,8 +7760,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/node": { "version": "12.12.41", @@ -8036,8 +8023,7 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/triple-beam": { "version": "1.3.5", @@ -8505,7 +8491,6 @@ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -8897,7 +8882,6 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -8924,7 +8908,6 @@ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "retry": "0.13.1" } @@ -8934,8 +8917,7 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/autoprefixer": { "version": "10.4.21", @@ -9139,7 +9121,6 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -9309,8 +9290,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", @@ -9855,7 +9835,6 @@ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -10772,7 +10751,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=0.4.0" } @@ -10969,7 +10947,6 @@ "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", @@ -10989,7 +10966,6 @@ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -11067,7 +11043,6 @@ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "once": "^1.4.0" } @@ -11867,7 +11842,6 @@ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=6" } @@ -11982,7 +11956,6 @@ "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" } @@ -12065,7 +12038,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "dependencies": { "strnum": "^1.1.1" }, @@ -12317,7 +12289,6 @@ "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.4.0.tgz", "integrity": "sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", @@ -12343,7 +12314,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz", "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -12467,7 +12437,6 @@ "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -12595,8 +12564,7 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -12621,7 +12589,6 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -12642,7 +12609,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -12652,7 +12618,6 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", @@ -13086,7 +13051,6 @@ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", "license": "Apache-2.0", - "peer": true, "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -13105,7 +13069,6 @@ "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@grpc/grpc-js": "^1.10.9", "@grpc/proto-loader": "^0.7.13", @@ -13130,7 +13093,6 @@ "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -13149,7 +13111,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -13159,7 +13120,6 @@ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -13194,7 +13154,6 @@ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", "license": "MIT", - "peer": true, "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -13424,8 +13383,7 @@ } ], "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/html-escaper": { "version": "2.0.2", @@ -14746,7 +14704,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -14795,7 +14752,6 @@ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", - "peer": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -14904,7 +14860,6 @@ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "license": "MIT", - "peer": true, "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -14927,7 +14882,6 @@ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", - "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -14939,7 +14893,6 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "license": "MIT", - "peer": true, "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" @@ -14950,7 +14903,6 @@ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", - "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -14962,7 +14914,6 @@ "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", "license": "MIT", - "peer": true, "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", @@ -14980,7 +14931,6 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "license": "MIT", - "peer": true, "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -15578,8 +15528,7 @@ "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "peer": true + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -15821,8 +15770,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -15835,43 +15783,37 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -15884,8 +15826,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.template": { "version": "4.5.0", @@ -16213,7 +16154,6 @@ "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", "license": "MIT", - "peer": true, "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" @@ -16224,7 +16164,6 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", - "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16236,8 +16175,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/magic-string": { "version": "0.30.17", @@ -16589,7 +16527,6 @@ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", "optional": true, - "peer": true, "bin": { "mime": "cli.js" }, @@ -17763,7 +17700,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -18145,7 +18081,6 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 6" } @@ -19056,7 +18991,6 @@ "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "protobufjs": "^7.2.5" }, @@ -19840,7 +19774,6 @@ "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", @@ -19856,7 +19789,6 @@ "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -21217,7 +21149,6 @@ "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "stubs": "^3.0.0" } @@ -21227,8 +21158,7 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/streamroller": { "version": "3.1.5", @@ -21435,16 +21365,14 @@ } ], "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/supports-color": { "version": "7.2.0", @@ -21588,7 +21516,6 @@ "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", @@ -21606,7 +21533,6 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "debug": "4" }, @@ -21620,7 +21546,6 @@ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@tootallnate/once": "2", "agent-base": "6", @@ -21636,7 +21561,6 @@ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -21655,7 +21579,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -21891,8 +21814,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tree-dump": { "version": "1.0.3", @@ -22647,7 +22569,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -22743,8 +22664,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/webpack": { "version": "5.99.8", @@ -23170,7 +23090,6 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index dbfd2b852..056d47bbb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "@schematics/angular": "^20.0.0", "esbuild": "^0.24.0", "firebase": "^11.8.0", - "firebase-functions": "^6.1.0", + "firebase-admin": "^13.0.0", + "firebase-functions": "^6.1.1", "fs-extra": "^8.0.1", "fuzzy": "^0.1.3", "husky": "^4.2.5", diff --git a/tools/build.ts b/tools/build.ts index c947ad765..bdd254d28 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -278,11 +278,20 @@ async function replaceSchematicVersions() { const root = await rootPackage; const packagesPath = dest('schematics', 'versions.json'); const dependencies = await import(packagesPath); + // A missing version is dropped by JSON.stringify, so the package silently vanishes from the + // manifests the schematics generate. Fail the build instead. + const resolveVersion = (name: string): string => { + const version = root.dependencies[name] || root.devDependencies[name]; + if (!version) { + throw new Error(`No version resolved for ${name} in the root package.json, so the schematics would generate manifests without it.`); + } + return version; + }; Object.keys(dependencies.peerDependencies).forEach(name => { - dependencies.peerDependencies[name].version = root.dependencies[name] || root.devDependencies[name]; + dependencies.peerDependencies[name].version = resolveVersion(name); }); Object.keys(dependencies.firebaseFunctionsDependencies).forEach(name => { - dependencies.firebaseFunctionsDependencies[name].version = root.dependencies[name] || root.devDependencies[name]; + dependencies.firebaseFunctionsDependencies[name].version = resolveVersion(name); }); return writeFile(packagesPath, JSON.stringify(dependencies, null, 2)); } From ed8ce70f7a3c2d64d5a970e7da9abf33471d5b56 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 12 Aug 2026 13:10:44 -0700 Subject: [PATCH 09/13] build: write real versions into the compiled v20 schematic bundles v20's deploy schematics statically import versions.json, so esbuild inlines the source placeholders and 20.0.1 shipped version:"0.0.0" in the generated Cloud Functions manifest (ETARGET at install). The build now rewrites per-package placeholder tokens inside the emitted bundles and fails if any token survives. v20-only: main reads versions.json at runtime and never inlines it. --- src/schematics/versions.json | 4 +- tools/build.ts | 92 +++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/schematics/versions.json b/src/schematics/versions.json index 9477d49a4..176a5b8ea 100644 --- a/src/schematics/versions.json +++ b/src/schematics/versions.json @@ -2,7 +2,7 @@ "peerDependencies": { }, "firebaseFunctionsDependencies": { - "firebase-admin": { "dev": false, "version": "0.0.0" }, - "firebase-functions": { "dev": false, "version": "0.0.0" } + "firebase-admin": { "dev": false, "version": "FIREBASE_ADMIN_VERSION" }, + "firebase-functions": { "dev": false, "version": "FIREBASE_FUNCTIONS_VERSION" } } } diff --git a/tools/build.ts b/tools/build.ts index bdd254d28..d4daf50a6 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -1,6 +1,7 @@ import { spawn } from 'cross-spawn'; +import { readFileSync, readdirSync } from 'fs'; import { copy, writeFile } from 'fs-extra'; -import { join } from 'path'; +import { join, sep } from 'path'; import { keys as tsKeys } from 'ts-transformer-keys'; import * as esbuild from "esbuild"; @@ -274,6 +275,82 @@ async function replacePackageCoreVersion() { }); } +/** The placeholder a package's version is written as in src/schematics/versions.json, e.g. + * firebase-admin becomes FIREBASE_ADMIN_VERSION. That file is hand-written, so a placeholder + * spelled any other way is not a compile error: it only fails the build in the checks below. */ +const versionPlaceholder = (name: string) => `${name.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_VERSION`; + +/** Every compiled schematic bundle, as a glob for replace-in-file. */ +const compiledSchematicsGlob = () => `${dest('schematics').split(sep).join('/')}/**/*.js`; + +/** Every compiled schematic bundle, as absolute paths. */ +const compiledSchematicFiles = (directory: string): string[] => + readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = join(directory, entry.name); + if (entry.isDirectory()) { return compiledSchematicFiles(entryPath); } + return entryPath.endsWith('.js') ? [entryPath] : []; + }); + +/** The schematics ship as esbuild bundles, so `import { firebaseFunctionsDependencies } from + * '../versions.json'` is inlined at compile time and the copied versions.json is never read at + * runtime. Rewriting only that copy leaves the placeholder baked into the emitted JavaScript. */ +async function writeSchematicVersionsIntoBundles(versions: Record) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const replace = require('replace-in-file'); + for (const [name, version] of Object.entries(versions)) { + if (!version) { + throw new Error(`No version resolved for ${name}, so the generated Cloud Functions manifest would not install. Add it to the root package.json.`); + } + const placeholder = versionPlaceholder(name); + const replacements = await replace({ + // Glob patterns need forward slashes even on Windows. + files: compiledSchematicsGlob(), + // (?![A-Z0-9_]) stops FIREBASE_ADMIN_VERSION from also matching the start of a misspelled + // FIREBASE_ADMIN_VERSIONS, which would replace part of it and leave "^13.0.0S" behind. + from: new RegExp(`${placeholder}(?![A-Z0-9_])`, 'g'), + to: version, + // When the glob matches no files at all, return an empty result instead of rejecting, so the + // failure comes out of the check below with a message that says where we looked. + allowEmptyPaths: true, + }); + if (!replacements.some(replacement => replacement.hasChanged)) { + throw new Error(`No ${placeholder} placeholder found in the compiled schematics. Check that src/schematics/versions.json spells it exactly that way, and that the build is reading ${compiledSchematicsGlob()}.`); + } + } +} + +/** A compiled bundle that still contains one or more of our placeholders. */ +interface BundleWithPlaceholders { + file: string; + placeholders: string[]; +} + +/** Fails the build if any of our version placeholders is still present in a compiled bundle. + * Checks only the placeholders named in `packageNames`, because a blanket search for anything + * ending in _VERSION also hits SEMVER_SPEC_VERSION and TERM_PROGRAM_VERSION from bundled + * libraries, which are not ours and are supposed to be there. */ +function assertNoVersionPlaceholdersRemain(packageNames: string[]) { + const files = compiledSchematicFiles(dest('schematics')); + if (files.length === 0) { + throw new Error(`No compiled schematics found under ${dest('schematics')}, so no version placeholder could have been replaced.`); + } + const ourPlaceholders = packageNames.map(versionPlaceholder); + const bundlesWithPlaceholders: BundleWithPlaceholders[] = files + .map(file => { + const contents = readFileSync(file, 'utf8'); + return { file, placeholders: ourPlaceholders.filter(placeholder => contents.includes(placeholder)) }; + }) + .filter(bundle => bundle.placeholders.length > 0); + if (bundlesWithPlaceholders.length > 0) { + const offendingBundles = bundlesWithPlaceholders + .map(bundle => ` ${bundle.file}: ${bundle.placeholders.join(', ')}`) + .join('\n'); + throw new Error(`Unreplaced version placeholders in the compiled schematics:\n${offendingBundles}\nThe generated manifests would ship a placeholder instead of a version.`); + } +} + +/** Resolves the versions src/schematics/versions.json asks for from the root package.json, then + * writes them both into the copied versions.json and into the compiled bundles that inlined them. */ async function replaceSchematicVersions() { const root = await rootPackage; const packagesPath = dest('schematics', 'versions.json'); @@ -287,12 +364,15 @@ async function replaceSchematicVersions() { } return version; }; - Object.keys(dependencies.peerDependencies).forEach(name => { - dependencies.peerDependencies[name].version = resolveVersion(name); - }); - Object.keys(dependencies.firebaseFunctionsDependencies).forEach(name => { - dependencies.firebaseFunctionsDependencies[name].version = resolveVersion(name); + const resolved: Record = {}; + [dependencies.peerDependencies, dependencies.firebaseFunctionsDependencies].forEach(block => { + Object.keys(block).forEach(name => { + block[name].version = resolveVersion(name); + resolved[name] = block[name].version; + }); }); + await writeSchematicVersionsIntoBundles(resolved); + assertNoVersionPlaceholdersRemain(Object.keys(resolved)); return writeFile(packagesPath, JSON.stringify(dependencies, null, 2)); } From f030f4a3f9ea2475f82350b6bd2542540f5533c7 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 12 Aug 2026 13:11:09 -0700 Subject: [PATCH 10/13] chore(release): 20.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 056d47bbb..b57ae332f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/fire", - "version": "20.0.0", + "version": "20.0.2", "description": "Angular + Firebase = ❤️", "private": true, "scripts": { From 878d738b46eb5cf5276ce945d7d2c1aab44c030b Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 15 Jul 2026 19:24:18 -0700 Subject: [PATCH 11/13] feat(schematics): generate .firebaserc and Firestore starter files during ng add (#3714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(schematics): generate .firebaserc and Firestore starter files during ng add ng add asks the user to pick a Firebase project but never records the choice, so every later firebase-tools command has no default project. It also leaves firebase.json empty and creates no security rules, so a Firestore-selected workspace cannot deploy rules at all. Now, after the project prompt, the schematic records the selection in .firebaserc (merging into an existing file rather than replacing it). When Firestore is selected it also generates firestore.rules and firestore.indexes.json — the same test-mode starter files that 'firebase init firestore' produces, with a warning that the rules expire in 30 days — and wires the firestore section into firebase.json. Existing rules files are left untouched. The new files go through the schematic Tree; firebase.json stays on the real filesystem because firebase-tools reads and rewrites it during the same run, and its firestore section is added only after those rewrites. * fix(schematics): move firestore starter files after init, widen error handling Tyler's review on #3714: createFirestoreStarterFiles staged the Tree before the DataConnect init calls, using a pre-init firebase.json snapshot. If a future init call ever added a firestore section mid-run, the stale snapshot would miss it and stage starter files on top of files firebase-tools already wrote to disk — the same Tree/disk collision the .firebaserc write hit earlier in this PR. Moved the call to run after init with a fresh re-read, and documented the more immediate invariant this enforces: it must run before addFirestoreToFirebaseJson, which is what adds the firestore section on a normal run. Also widened addFirestoreToFirebaseJson's try/catch to cover the firebase.json mutation and write, not just the read — a write failure (disk full, permissions) previously crashed with a raw Node error instead of the warn-and-continue the read path already gets. * fix(schematics): warn instead of crashing when the post-init firebase.json read fails The re-read added for the firestore starter files was unguarded, so a firebase.json that firebase-tools left unreadable would abort the whole ng add. Before the starter files moved after the init calls, this same failure degraded gracefully — the files were already staged, and addFirestoreToFirebaseJson warned and let setup finish. Restore that: warn and continue. createFirestoreStarterFiles independently checks the disk for each file it would create, so a missing snapshot costs the firestore-section check, not the collision safety it also relies on. Trimmed the surrounding comments to the two constraints a future edit could silently break — read after init, and run before addFirestoreToFirebaseJson — and moved the rationale here. The reason the read sits after the init calls at all: staging against a stale pre-init snapshot risks the Tree/disk collision the .firebaserc write hit earlier in this PR, where a Tree-staged file collides at commit time with one firebase-tools already wrote to disk and aborts the run. No init call adds a firestore section today, so that specific path is defensive against a future one. --- src/schematics/interfaces.ts | 6 + .../setup/firebaseConfigs.jasmine.ts | 176 ++++++++++++++++++ src/schematics/setup/firebaseConfigs.ts | 128 +++++++++++++ src/schematics/setup/index.ts | 29 ++- 4 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 src/schematics/setup/firebaseConfigs.jasmine.ts create mode 100644 src/schematics/setup/firebaseConfigs.ts diff --git a/src/schematics/interfaces.ts b/src/schematics/interfaces.ts index a21df2168..a3a14f3be 100644 --- a/src/schematics/interfaces.ts +++ b/src/schematics/interfaces.ts @@ -155,9 +155,15 @@ export interface DataConnectConfig { source?: string; } +export interface FirebaseFirestoreConfig { + rules?: string; + indexes?: string; +} + export interface FirebaseJSON { hosting?: FirebaseHostingConfig[] | FirebaseHostingConfig; functions?: FirebaseFunctionsConfig; + firestore?: FirebaseFirestoreConfig; dataconnect?: DataConnectConfig; } diff --git a/src/schematics/setup/firebaseConfigs.jasmine.ts b/src/schematics/setup/firebaseConfigs.jasmine.ts new file mode 100644 index 000000000..88f185e52 --- /dev/null +++ b/src/schematics/setup/firebaseConfigs.jasmine.ts @@ -0,0 +1,176 @@ +import { mkdtempSync, readFileSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { logging } from '@angular-devkit/core'; +import { HostTree, SchematicContext } from '@angular-devkit/schematics'; +import fsExtra from 'fs-extra'; +import { FEATURES } from '../interfaces.js'; +import { + addFirestoreToFirebaseJson, + createFirestoreStarterFiles, + setDefaultProjectInFirebaseRc, +} from './firebaseConfigs.js'; +import 'jasmine'; + +const context = { logger: new logging.Logger('test') } as unknown as SchematicContext; + +describe('setDefaultProjectInFirebaseRc', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = mkdtempSync(join(tmpdir(), 'angularfire-test-')); + }); + + afterEach(() => { + fsExtra.removeSync(projectRoot); + }); + + const firebaseRcOnDisk = () => + JSON.parse(readFileSync(join(projectRoot, '.firebaserc')).toString()); + + it('creates .firebaserc recording the selected project as default', () => { + setDefaultProjectInFirebaseRc(projectRoot, 'my-project'); + expect(firebaseRcOnDisk()).toEqual({ + projects: { default: 'my-project' }, + }); + }); + + it('merges into an existing .firebaserc, preserving targets and other aliases', () => { + writeFileSync(join(projectRoot, '.firebaserc'), JSON.stringify({ + projects: { default: 'old-project', staging: 'staging-project' }, + targets: { 'old-project': { hosting: { app: ['app-site'] } } }, + })); + setDefaultProjectInFirebaseRc(projectRoot, 'new-project'); + expect(firebaseRcOnDisk()).toEqual({ + projects: { default: 'new-project', staging: 'staging-project' }, + targets: { 'old-project': { hosting: { app: ['app-site'] } } }, + }); + }); + + it('names the file when an existing .firebaserc cannot be parsed', () => { + writeFileSync(join(projectRoot, '.firebaserc'), '{ not json'); + expect(() => setDefaultProjectInFirebaseRc(projectRoot, 'my-project')) + .toThrowError(/\.firebaserc/); + }); + +}); + +describe('createFirestoreStarterFiles', () => { + + it('creates test-mode rules and an empty index manifest when Firestore is selected', () => { + const tree = new HostTree(); + createFirestoreStarterFiles(tree, context, [FEATURES.Firestore]); + const rules = tree.readText('/firestore.rules'); + expect(rules).toContain("rules_version='2'"); + expect(rules).toContain('allow read, write: if request.time < timestamp.date('); + expect(JSON.parse(tree.readText('/firestore.indexes.json'))).toEqual({ + indexes: [], + fieldOverrides: [], + }); + }); + + it('sets the rules to expire roughly 30 days out', () => { + const tree = new HostTree(); + createFirestoreStarterFiles(tree, context, [FEATURES.Firestore]); + const match = /timestamp\.date\((\d+), (\d+), (\d+)\)/.exec(tree.readText('/firestore.rules')); + expect(match).not.toBeNull(); + const [, year, month, day] = match.map(Number); + const expiry = new Date(year, month - 1, day); + const daysOut = (expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24); + expect(daysOut).toBeGreaterThan(28); + expect(daysOut).toBeLessThan(31); + }); + + it('never overwrites an existing rules file', () => { + const tree = new HostTree(); + tree.create('/firestore.rules', 'my existing rules'); + createFirestoreStarterFiles(tree, context, [FEATURES.Firestore]); + expect(tree.readText('/firestore.rules')).toBe('my existing rules'); + }); + + it('still creates the index manifest when only the rules file exists', () => { + const tree = new HostTree(); + tree.create('/firestore.rules', 'my existing rules'); + createFirestoreStarterFiles(tree, context, [FEATURES.Firestore]); + expect(tree.exists('/firestore.indexes.json')).toBeTrue(); + }); + + it('does nothing when Firestore is not selected', () => { + const tree = new HostTree(); + createFirestoreStarterFiles(tree, context, [FEATURES.Authentication]); + expect(tree.exists('/firestore.rules')).toBeFalse(); + expect(tree.exists('/firestore.indexes.json')).toBeFalse(); + }); + + it('creates nothing when firebase.json already has a firestore section', () => { + const tree = new HostTree(); + createFirestoreStarterFiles(tree, context, [FEATURES.Firestore], { + firestore: { rules: 'custom.rules' }, + }); + expect(tree.exists('/firestore.rules')).toBeFalse(); + expect(tree.exists('/firestore.indexes.json')).toBeFalse(); + }); + +}); + +describe('addFirestoreToFirebaseJson', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = mkdtempSync(join(tmpdir(), 'angularfire-test-')); + }); + + afterEach(() => { + fsExtra.removeSync(projectRoot); + }); + + const firebaseJsonOnDisk = () => + JSON.parse(readFileSync(join(projectRoot, 'firebase.json')).toString()); + + it('adds the firestore section pointing at the starter files', () => { + writeFileSync(join(projectRoot, 'firebase.json'), '{}'); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Firestore]); + expect(firebaseJsonOnDisk().firestore).toEqual({ + rules: 'firestore.rules', + indexes: 'firestore.indexes.json', + }); + }); + + it('preserves sections other tools wrote to firebase.json', () => { + writeFileSync(join(projectRoot, 'firebase.json'), JSON.stringify({ + dataconnect: { source: 'dataconnect' }, + })); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Firestore]); + expect(firebaseJsonOnDisk().dataconnect).toEqual({ source: 'dataconnect' }); + expect(firebaseJsonOnDisk().firestore).toBeDefined(); + }); + + it('leaves an existing firestore section untouched', () => { + writeFileSync(join(projectRoot, 'firebase.json'), JSON.stringify({ + firestore: { rules: 'custom.rules' }, + })); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Firestore]); + expect(firebaseJsonOnDisk().firestore).toEqual({ rules: 'custom.rules' }); + }); + + it('does nothing when Firestore is not selected', () => { + writeFileSync(join(projectRoot, 'firebase.json'), '{}'); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Authentication]); + expect(firebaseJsonOnDisk().firestore).toBeUndefined(); + }); + + it('warns and skips when firebase.json cannot be read', () => { + const warnSpy = spyOn(context.logger, 'warn'); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Firestore]); + expect(warnSpy).toHaveBeenCalled(); + }); + + it('warns and skips when firebase.json parses to a non-object', () => { + writeFileSync(join(projectRoot, 'firebase.json'), 'null'); + const warnSpy = spyOn(context.logger, 'warn'); + addFirestoreToFirebaseJson(projectRoot, context, [FEATURES.Firestore]); + expect(warnSpy).toHaveBeenCalled(); + expect(readFileSync(join(projectRoot, 'firebase.json')).toString()).toBe('null'); + }); + +}); diff --git a/src/schematics/setup/firebaseConfigs.ts b/src/schematics/setup/firebaseConfigs.ts new file mode 100644 index 000000000..b08838c83 --- /dev/null +++ b/src/schematics/setup/firebaseConfigs.ts @@ -0,0 +1,128 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; +import { stringifyFormatted } from '../common.js'; +import { FEATURES, FirebaseJSON, FirebaseRc } from '../interfaces.js'; + +/** + * Builds the same test-mode starter rules `firebase init firestore` generates: anyone can read + * and write until a date 30 days from generation, after which all client requests are denied. + */ +const testModeFirestoreRules = () => { + const expiry = new Date(); + expiry.setDate(expiry.getDate() + 30); + return `rules_version='2' + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + // This rule allows anyone with your database reference to view, edit, + // and delete all data in your database. It is useful for getting + // started, but it is configured to expire after 30 days because it + // leaves your app open to attackers. At that time, all client + // requests to your database will be denied. + // + // Make sure to write security rules for your app before that time, or + // else all client requests to your database will be denied until you + // update your rules. + allow read, write: if request.time < timestamp.date(${expiry.getFullYear()}, ${expiry.getMonth() + 1}, ${expiry.getDate()}); + } + } +} +`; +}; + +/** + * Records the selected Firebase project as the workspace's default in `.firebaserc`, so + * firebase-tools commands the user runs after setup completes don't prompt for (or fail without) + * a project. Merges into an existing file — only `projects.default` is set; targets and other + * aliases are preserved. Written to the real filesystem (not the schematic Tree) because + * `firebaseTools.init` writes `.firebaserc` on disk during the same `ng add` run — a Tree-staged + * copy would collide with it when the schematic commits; call this after the last + * `firebaseTools.init` call for that reason. + */ +export const setDefaultProjectInFirebaseRc = (projectRoot: string, projectId: string) => { + const path = join(projectRoot, '.firebaserc'); + let rc: FirebaseRc = {}; + if (existsSync(path)) { + try { + rc = JSON.parse(readFileSync(path).toString()); + } catch (e) { + throw new SchematicsException(`Error when parsing ${path}: ${e.message}`); + } + } + rc.projects = { ...rc.projects, default: projectId }; + writeFileSync(path, stringifyFormatted(rc)); +}; + +/** + * When Firestore is selected, generates `firestore.rules` (test mode, with a logged warning + * about the 30-day expiry) and `firestore.indexes.json`. Existing files are never overwritten — + * a later `firebase deploy` would replace rules already deployed from elsewhere. A workspace + * whose firebase.json already has a `firestore` section is left entirely untouched: its section + * may point at differently-named files, and creating the default-named ones would only add + * orphans nothing references. + * + * `firebaseJsonConfig`, when passed, should be the post-init snapshot of firebase.json (read + * after the last `firebaseTools.init` call and before `addFirestoreToFirebaseJson` runs) — see + * the call site in `ngAddSetupProject` for why that order matters. Passing a stale pre-init + * snapshot risks staging the default-named starter files on top of files firebase-tools already + * wrote to disk, colliding when the Tree commits. + */ +export const createFirestoreStarterFiles = ( + host: Tree, + context: SchematicContext, + features: FEATURES[], + firebaseJsonConfig?: FirebaseJSON, +) => { + if (!features.includes(FEATURES.Firestore)) { return; } + if (firebaseJsonConfig?.firestore) { + context.logger.info('firebase.json already has a firestore section — leaving its rules and indexes untouched.'); + return; + } + if (host.exists('/firestore.rules')) { + context.logger.info('firestore.rules already exists — leaving it untouched.'); + } else { + host.create('/firestore.rules', testModeFirestoreRules()); + context.logger.warn( + 'Generated firestore.rules in test mode: anyone can read and write your database until the rules expire in 30 days. ' + + 'Write real security rules before then — https://firebase.google.com/docs/rules' + ); + } + if (!host.exists('/firestore.indexes.json')) { + // Must exist once firebase.json references it — `firebase deploy` errors on a missing indexes file. + host.create('/firestore.indexes.json', stringifyFormatted({ indexes: [], fieldOverrides: [] })); + } +}; + +/** + * When Firestore is selected, points the `firestore` section of `firebase.json` at the starter + * files so `firebase deploy` includes rules and indexes. Written to the real filesystem (not the + * schematic Tree) because firebase-tools reads and rewrites `firebase.json` during the same + * `ng add` run; call this after the last `firebaseTools.init` call for that reason. + */ +export const addFirestoreToFirebaseJson = ( + projectRoot: string, + context: SchematicContext, + features: FEATURES[], +) => { + if (!features.includes(FEATURES.Firestore)) { return; } + // firebaseTools.init calls may have rewritten firebase.json on disk, so re-read it, add the + // firestore section if absent, and write it back. The whole read-check-write is guarded: a + // failed read (missing or unparseable file), a parse that isn't a usable object (reading + // `.firestore` off `null`/`undefined` throws; assigning it on a non-object primitive throws + // too), or a failed write (disk full, permissions) all warn and leave the file for the user to + // finish, rather than crashing the schematic with a raw Node error. + try { + const firebaseJson: FirebaseJSON = JSON.parse(readFileSync(join(projectRoot, 'firebase.json')).toString()); + if (!firebaseJson.firestore) { + firebaseJson.firestore = { rules: 'firestore.rules', indexes: 'firestore.indexes.json' }; + writeFileSync(join(projectRoot, 'firebase.json'), stringifyFormatted(firebaseJson)); + } + } catch (e) { + context.logger.warn( + `Could not update firebase.json with the firestore section (${e.message}). Check firebase.json ` + + 'and add { "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" } } manually.' + ); + } +}; diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 3d40ed132..f86d1e683 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -16,6 +16,11 @@ import { parseDataConnectConfig, setupTanstackDependencies, } from '../utils'; +import { + addFirestoreToFirebaseJson, + createFirestoreStarterFiles, + setDefaultProjectInFirebaseRc, +} from './firebaseConfigs'; import { appPrompt, featuresPrompt, projectPrompt, userPrompt } from './prompts'; // FirebaseOptions keys — apps.sdkconfig responses include management-API extras that initializeApp() rejects. @@ -84,7 +89,7 @@ export const ngAddSetupProject = ( const [ defaultProjectName ] = getFirebaseProjectNameFromHost(host, ngProjectName); const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot, account: user.email }); - + let firebaseApp: FirebaseApp|undefined; let sdkConfig: Record|undefined; @@ -139,8 +144,28 @@ export const ngAddSetupProject = ( setupTanstackDependencies(host, context); setupConfig.dataConnectConfig = dataConnectConfig; } - + + } + + // Read after the init calls, never before — firebase-tools rewrites firebase.json on disk + // mid-run. A failed read isn't fatal: createFirestoreStarterFiles falls back to checking the + // disk for each file it would create. + let firebaseJsonAfterInit: FirebaseJSON | undefined; + try { + firebaseJsonAfterInit = JSON.parse( + readFileSync(join(projectRoot, "firebase.json")).toString() + ); + } catch (e) { + context.logger.warn(`Could not re-read firebase.json after setup (${e.message}).`); } + // Must run before addFirestoreToFirebaseJson: that call adds the firestore section, and if it + // ran first this snapshot would see it and skip creating the files it points at. + createFirestoreStarterFiles(host, context, features, firebaseJsonAfterInit); + + // Both write the real filesystem, after the last firebaseTools.init call — init writes + // .firebaserc and rewrites firebase.json on disk during the run. + setDefaultProjectInFirebaseRc(projectRoot, firebaseProject.projectId); + addFirestoreToFirebaseJson(projectRoot, context, features); return setupProject(host, context, features, setupConfig); } From 3d70ab5423e83c2c9cf645939abff64d4f22ed12 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Mon, 3 Aug 2026 14:33:56 -0700 Subject: [PATCH 12/13] fix(schematics): warn when ng add setup runs with no features selected (#3717) * fix(schematics): warn when ng add setup runs with no features selected Pressing Enter without toggling any checkbox at the features prompt silently exited with the CLI's stock "Nothing to be done." message, giving no indication that Space selects a feature. Print a warning explaining the checkbox controls and inviting a retry. * refactor(schematics): derive the empty-features warning from the prompt message The empty-features warning duplicated the checkbox prompt's message as a second string literal, so a reworded prompt would leave the warning pointing at text the user never sees. Export the message as a shared constant and reference it in both places, matching the shared-constant pattern used for the firebase-tools version message in 59c8a2f. --- src/schematics/setup/index.ts | 11 +++++++++-- src/schematics/setup/prompts.ts | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index f86d1e683..8a657e17a 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -21,7 +21,7 @@ import { createFirestoreStarterFiles, setDefaultProjectInFirebaseRc, } from './firebaseConfigs'; -import { appPrompt, featuresPrompt, projectPrompt, userPrompt } from './prompts'; +import { appPrompt, featuresPrompt, featuresPromptMessage, projectPrompt, userPrompt } from './prompts'; // FirebaseOptions keys — apps.sdkconfig responses include management-API extras that initializeApp() rejects. const firebaseOptionsKeys = [ @@ -67,7 +67,14 @@ export const ngAddSetupProject = ( const features = await featuresPrompt(); - if (features.length > 0) { + if (features.length === 0) { + context.logger.warn( + 'No features were selected, so there is nothing to set up. ' + + `At the "${featuresPromptMessage}" prompt, use the arrow keys to move, ` + + 'press Space to select each feature you want, then Enter to confirm. ' + + 'Re-run ng add @angular/fire to try again.' + ); + } else { const firebaseTools = await getFirebaseTools(); diff --git a/src/schematics/setup/prompts.ts b/src/schematics/setup/prompts.ts index 1bc8a85ca..b48d80d5a 100644 --- a/src/schematics/setup/prompts.ts +++ b/src/schematics/setup/prompts.ts @@ -79,12 +79,14 @@ type Prompt = (questions: { name: K, source: (...a const autocomplete: Prompt = (questions) => inquirer.prompt(questions); +export const featuresPromptMessage = 'What features would you like to setup?'; + export const featuresPrompt = async (): Promise => { const { features } = await inquirer.prompt({ type: 'checkbox', name: 'features', choices: featureOptions, - message: 'What features would you like to setup?', + message: featuresPromptMessage, default: [], }) as { features: FEATURES[] }; return features; From 1c4419288753d40fd7dd5c51b22cbf8d43f1f297 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 12 Aug 2026 16:14:14 -0700 Subject: [PATCH 13/13] chore(release): 20.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 149335d97..29cf309fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@angular/fire", - "version": "20.0.2", + "version": "20.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@angular/fire", - "version": "20.0.2", + "version": "20.1.0", "license": "MIT", "dependencies": { "@angular-devkit/architect": "~0.2000.0", diff --git a/package.json b/package.json index b57ae332f..d05fb8a29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/fire", - "version": "20.0.2", + "version": "20.1.0", "description": "Angular + Firebase = ❤️", "private": true, "scripts": {