From 702a0cb0ba6bb1dad4657124a7dc500053916b4e Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 8 Jul 2026 16:39:34 -0700 Subject: [PATCH 1/3] feat(schematics): generate .firebaserc and Firestore starter files during ng add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/schematics/interfaces.ts | 6 + .../setup/firebaseConfigs.jasmine.ts | 168 ++++++++++++++++++ src/schematics/setup/firebaseConfigs.ts | 119 +++++++++++++ src/schematics/setup/index.ts | 16 +- 4 files changed, 307 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..5ed364f36 --- /dev/null +++ b/src/schematics/setup/firebaseConfigs.jasmine.ts @@ -0,0 +1,168 @@ +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(); + }); + +}); diff --git a/src/schematics/setup/firebaseConfigs.ts b/src/schematics/setup/firebaseConfigs.ts new file mode 100644 index 000000000..078601c7e --- /dev/null +++ b/src/schematics/setup/firebaseConfigs.ts @@ -0,0 +1,119 @@ +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. + */ +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; } + // Re-read first: firebaseTools.init calls may have rewritten firebase.json on disk. + let firebaseJson: FirebaseJSON; + try { + firebaseJson = JSON.parse(readFileSync(join(projectRoot, 'firebase.json')).toString()); + } catch (e) { + context.logger.warn( + `Could not read firebase.json to add the firestore section (${e.message}). Add ` + + '{ "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" } } to it manually.' + ); + return; + } + if (!firebaseJson.firestore) { + firebaseJson.firestore = { rules: 'firestore.rules', indexes: 'firestore.indexes.json' }; + writeFileSync(join(projectRoot, 'firebase.json'), stringifyFormatted(firebaseJson)); + } +}; diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 3d40ed132..91cbb7782 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,9 @@ export const ngAddSetupProject = ( const [ defaultProjectName ] = getFirebaseProjectNameFromHost(host, ngProjectName); const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot, account: user.email }); - + + createFirestoreStarterFiles(host, context, features, firebaseJson); + let firebaseApp: FirebaseApp|undefined; let sdkConfig: Record|undefined; @@ -139,9 +146,14 @@ export const ngAddSetupProject = ( setupTanstackDependencies(host, context); setupConfig.dataConnectConfig = dataConnectConfig; } - + } + // 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 3a5b2bbaa7c17eb60bc1238435e5ffa093fe9941 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 15 Jul 2026 17:27:20 -0700 Subject: [PATCH 2/3] fix(schematics): move firestore starter files after init, widen error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../setup/firebaseConfigs.jasmine.ts | 8 +++++ src/schematics/setup/firebaseConfigs.ts | 29 ++++++++++++------- src/schematics/setup/index.ts | 16 ++++++++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/schematics/setup/firebaseConfigs.jasmine.ts b/src/schematics/setup/firebaseConfigs.jasmine.ts index 5ed364f36..88f185e52 100644 --- a/src/schematics/setup/firebaseConfigs.jasmine.ts +++ b/src/schematics/setup/firebaseConfigs.jasmine.ts @@ -165,4 +165,12 @@ describe('addFirestoreToFirebaseJson', () => { 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 index 078601c7e..b08838c83 100644 --- a/src/schematics/setup/firebaseConfigs.ts +++ b/src/schematics/setup/firebaseConfigs.ts @@ -62,6 +62,12 @@ export const setDefaultProjectInFirebaseRc = (projectRoot: string, projectId: st * 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, @@ -101,19 +107,22 @@ export const addFirestoreToFirebaseJson = ( features: FEATURES[], ) => { if (!features.includes(FEATURES.Firestore)) { return; } - // Re-read first: firebaseTools.init calls may have rewritten firebase.json on disk. - let firebaseJson: FirebaseJSON; + // 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 { - firebaseJson = JSON.parse(readFileSync(join(projectRoot, 'firebase.json')).toString()); + 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 read firebase.json to add the firestore section (${e.message}). Add ` + - '{ "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" } } to it manually.' + `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.' ); - return; - } - if (!firebaseJson.firestore) { - firebaseJson.firestore = { rules: 'firestore.rules', indexes: 'firestore.indexes.json' }; - writeFileSync(join(projectRoot, 'firebase.json'), stringifyFormatted(firebaseJson)); } }; diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 91cbb7782..63986c25e 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -90,8 +90,6 @@ export const ngAddSetupProject = ( const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot, account: user.email }); - createFirestoreStarterFiles(host, context, features, firebaseJson); - let firebaseApp: FirebaseApp|undefined; let sdkConfig: Record|undefined; @@ -149,6 +147,20 @@ export const ngAddSetupProject = ( } + // Re-read firebase.json after the last firebaseTools.init call — init rewrites it on disk + // mid-run, staging files against a stale pre-init copy risks the same Tree/disk collision + // .firebaserc hit earlier in this file (a Tree-staged file colliding with one firebase-tools + // already wrote to disk, which crashes the schematic when the Tree commits). Right now no + // init call adds a firestore section, so this is defensive against a future one that might. + const firebaseJsonAfterInit: FirebaseJSON = JSON.parse( + readFileSync(join(projectRoot, "firebase.json")).toString() + ); + // Must run before addFirestoreToFirebaseJson below: that call is what adds the firestore + // section on a normal run. If it ran first, this read would see the section it just added + // and skip creating the files — leaving firebase.json pointing at rules/indexes files that + // were never created. + 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); From 28de572fc22010c0976880cd6e37e30a54c98f27 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 15 Jul 2026 19:13:40 -0700 Subject: [PATCH 3/3] fix(schematics): warn instead of crashing when the post-init firebase.json read fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/setup/index.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 63986c25e..f86d1e683 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -147,18 +147,19 @@ export const ngAddSetupProject = ( } - // Re-read firebase.json after the last firebaseTools.init call — init rewrites it on disk - // mid-run, staging files against a stale pre-init copy risks the same Tree/disk collision - // .firebaserc hit earlier in this file (a Tree-staged file colliding with one firebase-tools - // already wrote to disk, which crashes the schematic when the Tree commits). Right now no - // init call adds a firestore section, so this is defensive against a future one that might. - const firebaseJsonAfterInit: FirebaseJSON = JSON.parse( - readFileSync(join(projectRoot, "firebase.json")).toString() - ); - // Must run before addFirestoreToFirebaseJson below: that call is what adds the firestore - // section on a normal run. If it ran first, this read would see the section it just added - // and skip creating the files — leaving firebase.json pointing at rules/indexes files that - // were never created. + // 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