From c07dd7fbf51b72814dc2dbd60aa9063174a43efc Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 23 Oct 2026 02:38:30 -0700 Subject: [PATCH 1/2] build!: make firebase a peer dependency and add the v22 migration Through 21, @angular/fire listed firebase under dependencies, so an app that asked for a different version got a second copy nested under @angular/fire, and the two copies rejected each other's objects at runtime (#3684, #3681, #3682 and six more). As a peer dependency there is exactly one copy: npm refuses an incompatible range with ERESOLVE before installing anything, and pnpm and yarn install one copy with a warning. ng update rewrites a firebase entry the app already declares but adds none to an app without one, so migration-v22 calls alignFirebaseVersion and reinstalls only when it changed something. The comment and the two messages in alignFirebaseVersion no longer say that @angular/fire bundles its own firebase, and the firebaseVersionRange spec reads the peer entry. The duplicate-firebase warning from ng add stays: two copies still appear when another package bundles its own firebase. docs/version-22-upgrade.md starts with this change, naming the canary tag until a release candidate exists. BREAKING CHANGE: firebase is a peer dependency. Apps must declare firebase ^12.18.0 in their own package.json. ng update @angular/fire adds it when missing. A firebase outside that range now fails to install on npm (ERESOLVE) instead of installing a second copy. --- README.md | 2 + docs/version-22-upgrade.md | 58 +++++++++++++++++++++ src/ng-package.json | 2 +- src/package.json | 2 +- src/schematics/common.jasmine.ts | 4 +- src/schematics/common.ts | 24 ++++----- src/schematics/migration.json | 7 ++- src/schematics/tsconfig.json | 1 + src/schematics/update/v22/index.jasmine.ts | 59 ++++++++++++++++++++++ src/schematics/update/v22/index.ts | 14 +++++ tools/build.ts | 1 + 11 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 docs/version-22-upgrade.md create mode 100644 src/schematics/update/v22/index.jasmine.ts create mode 100644 src/schematics/update/v22/index.ts diff --git a/README.md b/README.md index f083fb49d..788b8d2ab 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ export class App { [Upgrading from AngularFire 20? See the v21 upgrade guide.](docs/version-21-upgrade.md) +[Upgrading from AngularFire 21? See the v22 upgrade guide.](docs/version-22-upgrade.md) + ### Sample app The [`sample`](sample) folder contains a kitchen sink application that demonstrates use of the "modular" API, in a zoneless server-rendered application, with all the bells and whistles. diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md new file mode 100644 index 000000000..d4c779541 --- /dev/null +++ b/docs/version-22-upgrade.md @@ -0,0 +1,58 @@ +# Upgrading to the AngularFire 22 canary + +AngularFire 22 targets **Angular 22** and the **Firebase JS SDK v12**. Most of the upgrade is handled for you by `ng update`. + +## Run the update + +AngularFire 22 is published under the `canary` tag, a build of every merge to `main`, until a release candidate is published under `next`. From an app on Angular 21 and AngularFire 21, run: + +```bash +ng update @angular/core@22 @angular/cli@22 @angular/fire@canary +``` + +Include `@angular/fire@canary` in the same command as Angular. On its own, `ng update @angular/core@22 @angular/cli@22` refuses to run while AngularFire 21 is installed, because AngularFire 21 only accepts Angular 21. + +The AngularFire update runs a migration that: + +- **Adds** `firebase` **to your** `package.json` **if it is missing**, as `"firebase": "^12.18.0"`, and reinstalls. See the next section for why. +- **Aligns an existing** `firebase` **entry to** `^12.18.0` when it requests a version outside that range. + +## Your app now owns the Firebase SDK + +Through AngularFire 21, `@angular/fire` listed `firebase` under `dependencies`, so npm could install a second copy of the SDK under `node_modules/` whenever your app asked for a different version. The two copies reject each other's objects at runtime, with errors such as `Expected type '_Query', but it was: a custom _CollectionReference object` or `toDate is not a function`. + +AngularFire 22 lists `firebase` under `peerDependencies` instead. So your app should declare `firebase` in its `package.json` (on Yarn it must, since Yarn installs no peer dependencies). This gives you exactly one copy of `firebase`, and an incompatible version is refused at install time instead of failing at runtime. + +`ng update @angular/fire@canary` updates an older `firebase` entry in your `package.json` to the current 12.x release. If your app doesn't list `firebase` in `package.json` the migration adds `"firebase": "^12.18.0"` and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. + +### If you install with `npm install` instead of `ng update` + +Add the dependency yourself: + +```bash +npm install @angular/fire@canary firebase@^12.18.0 +``` + +npm 7 and later install a missing peer dependency automatically, so `npm install @angular/fire@canary` alone also works, but it does not write `firebase` into your `package.json`. Declare it so that a later install cannot change which version you get. + +### If npm stops with `ERESOLVE` + +An app that requests a `firebase` version outside the range AngularFire 22 supports now fails to install with an `ERESOLVE` error. Update your `firebase` entry to a version inside the range given in the error message, then install again: + +```bash +npm install firebase@^12.18.0 +``` + +Do not use `--force` or `--legacy-peer-deps` to get past this message. Both install the older SDK next to AngularFire 22 and the runtime errors described above come back. If your project has `legacy-peer-deps=true` in its `.npmrc`, remove it before upgrading. + +### Yarn + +Yarn doesn't install peer dependencies. Add `firebase` to your own `package.json` as shown above, or run `ng update @angular/fire@canary`, which adds it. + +An older version installs anyway with a warning (`has incorrect peer dependency "firebase@^12.18.0"` on Yarn 1, `YN0060: INCOMPATIBLE_PEER_DEPENDENCY` on Yarn 2+), so treat that warning as an upgrade step you still have to do. + +### pnpm + +pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with `unmet peer firebase@^12.18.0: found 11.x`, and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile. + +Set `strict-peer-dependencies=true` in `.npmrc` to make pnpm refuse the mismatch outright. diff --git a/src/ng-package.json b/src/ng-package.json index 0eb72fb12..9e2805084 100644 --- a/src/ng-package.json +++ b/src/ng-package.json @@ -7,6 +7,6 @@ "allowedNonPeerDependencies": [ "@angular-devkit/architect", "@angular-devkit/core", "@angular-devkit/schematics", "@schematics/angular", - "jsonc-parser", "firebase", "rxfire" + "jsonc-parser", "rxfire" ] } diff --git a/src/package.json b/src/package.json index 9f2537957..4f141d993 100644 --- a/src/package.json +++ b/src/package.json @@ -31,6 +31,7 @@ "@angular/common": "^22.0.0", "@angular/core": "^22.0.0", "@angular/platform-browser": "^22.0.0", + "firebase": "^12.18.0", "rxjs": "~7.8.0", "firebase-tools": "^14.0.0 || ^15.0.0", "typescript": ">=6.0 <6.1" @@ -40,7 +41,6 @@ "typescript": { "optional": true } }, "dependencies": { - "firebase": "^12.18.0", "rxfire": "^6.2.0", "@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0", "@angular-devkit/core": "^22.0.0", diff --git a/src/schematics/common.jasmine.ts b/src/schematics/common.jasmine.ts index c9209cb71..2a55eaa64 100644 --- a/src/schematics/common.jasmine.ts +++ b/src/schematics/common.jasmine.ts @@ -214,9 +214,9 @@ describe('alignFirebaseVersion', () => { expect(sectionIn(tree, 'devDependencies').firebase).toBe(firebaseVersionRange); }); - it('matches the firebase range the library actually ships with', () => { + it('matches the firebase range the library declares as its peer dependency', () => { const libraryManifest = JSON.parse(readFileSync('src/package.json', 'utf8')); - expect(libraryManifest.dependencies.firebase).toBe(firebaseVersionRange); + expect(libraryManifest.peerDependencies.firebase).toBe(firebaseVersionRange); }); it('is a no-op when run a second time', () => { diff --git a/src/schematics/common.ts b/src/schematics/common.ts index 4630bcfe9..6df7c8bb1 100644 --- a/src/schematics/common.ts +++ b/src/schematics/common.ts @@ -72,22 +72,20 @@ export const addDependencies = ( overwriteIfExists(host, 'package.json', stringifyFormatted(packageJson)); }; -/* Must stay identical to `dependencies.firebase` in `src/package.json`: if the two drift, the - * alignment below can pin workspaces outside the range the library actually installs against, - * re-creating duplicate-SDK trees. */ +/* Must stay identical to `peerDependencies.firebase` in `src/package.json`: if the two drift, the + * alignment below writes a range the peer dependency then rejects. */ export const firebaseVersionRange = '^12.18.0'; /** * Aligns the workspace's `firebase` entry with the range `@angular/fire` requires. * - * `@angular/fire` bundles its own `firebase` dependency, so a workspace pinned to an older - * major never conflicts at install time; npm silently nests a second SDK copy under - * `@angular/fire`, and the two copies reject each other's objects at runtime (#3684, #3681, - * #3682). Rewriting the workspace's range before the install runs is the only point where - * that class of breakage can be headed off. + * `firebase` is a peer dependency of `@angular/fire`. Depending on the package manager and its + * settings, a workspace range outside it is refused at install time or installs a `firebase` + * the library was not built against, and a workspace that never declared `firebase` gets no + * entry written. Rewriting the entry before the install runs heads both off. * * Returns whether `package.json` was modified, so callers can skip scheduling an install - * when nothing changed (`ng update` re-runs the v21 migration on rc-to-stable updates). + * when nothing changed (`ng update` re-runs a migration on rc-to-stable updates). */ export const alignFirebaseVersion = ( host: Tree, @@ -119,8 +117,8 @@ export const alignFirebaseVersion = ( } catch (_) { context.logger.warn( `⚠️ The firebase version in your package.json (${declaredFirebaseVersion}) is not a semver ` + - `range, so it was left as-is; make sure it resolves inside ${firebaseVersionRange}, the ` + - 'range @angular/fire requires; a version outside it can leave the install with two copies of the firebase SDK.' + `range, so it was left as-is. Make sure it resolves inside ${firebaseVersionRange}, the ` + + 'range @angular/fire requires. A version outside it either fails to install or installs a firebase that @angular/fire was not built against.' ); continue; } @@ -128,8 +126,8 @@ export const alignFirebaseVersion = ( packageJson[section].firebase = firebaseVersionRange; context.logger.info( `Updated the firebase version in your package.json from ${declaredFirebaseVersion} to ` + - `${firebaseVersionRange}, the range @angular/fire requires; a workspace range outside it ` + - 'can leave the install with a second copy of the firebase SDK, which fails at runtime.' + `${firebaseVersionRange}, the range @angular/fire requires. A range outside it either fails ` + + 'to install or installs a firebase that @angular/fire was not built against.' ); changed = true; } diff --git a/src/schematics/migration.json b/src/schematics/migration.json index 2a2b926b0..4b99835c5 100644 --- a/src/schematics/migration.json +++ b/src/schematics/migration.json @@ -8,8 +8,13 @@ }, "migration-v21": { "version": "21.0.0", - "description": "Align the workspace's firebase dependency with the range @angular/fire 21 requires, and rewrite Vertex AI imports to Firebase AI Logic (getVertexAI callers keep the same backend, and a call that passed no location moves from us-central1 to global)", + "description": "Align the workspace's firebase dependency with the range @angular/fire requires, and rewrite Vertex AI imports to Firebase AI Logic (getVertexAI callers keep the same backend, and a call that passed no location moves from us-central1 to global)", "factory": "./update/v21#ngUpdate" + }, + "migration-v22": { + "version": "22.0.0", + "description": "Add firebase to the workspace's package.json when it is missing, now that @angular/fire lists it as a peer dependency, and align an existing entry with the range @angular/fire requires", + "factory": "./update/v22#ngUpdate" } } } \ No newline at end of file diff --git a/src/schematics/tsconfig.json b/src/schematics/tsconfig.json index 5c2536e7d..ca9d80392 100644 --- a/src/schematics/tsconfig.json +++ b/src/schematics/tsconfig.json @@ -29,5 +29,6 @@ "setup/index.ts", "update/v7/index.ts", "update/v21/index.ts", + "update/v22/index.ts", ] } \ No newline at end of file diff --git a/src/schematics/update/v22/index.jasmine.ts b/src/schematics/update/v22/index.jasmine.ts new file mode 100644 index 000000000..60a171eab --- /dev/null +++ b/src/schematics/update/v22/index.jasmine.ts @@ -0,0 +1,59 @@ +import { logging } from '@angular-devkit/core'; +import { HostTree, SchematicContext } from '@angular-devkit/schematics'; +import { firebaseVersionRange } from '../../common.js'; +import { ngUpdate } from './index.js'; +import 'jasmine'; + +const contextWithTaskSpy = () => { + const addTask = jasmine.createSpy('addTask'); + const context = { + logger: new logging.Logger('test'), + addTask, + } as unknown as SchematicContext; + return { context, addTask }; +}; + +const treeWithDependencies = (dependencies: Record) => { + const tree = new HostTree(); + tree.create('package.json', JSON.stringify({ name: 'test-app', dependencies }, null, 2)); + return tree; +}; + +const dependenciesIn = (tree: HostTree) => JSON.parse(tree.readText('package.json')).dependencies; + +describe('migration-v22 ngUpdate', () => { + + it('adds firebase when the workspace never declared it and schedules an install', () => { + const { context, addTask } = contextWithTaskSpy(); + const tree = treeWithDependencies({ '@angular/fire': '^22.0.0' }); + ngUpdate()(tree, context); + expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange); + expect(addTask).toHaveBeenCalledTimes(1); + }); + + it('aligns a firebase range outside the peer range and schedules an install', () => { + const { context, addTask } = contextWithTaskSpy(); + const tree = treeWithDependencies({ firebase: '^11.0.0' }); + ngUpdate()(tree, context); + expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange); + expect(addTask).toHaveBeenCalledTimes(1); + }); + + it('schedules nothing when the workspace is already aligned', () => { + const { context, addTask } = contextWithTaskSpy(); + const tree = treeWithDependencies({ firebase: firebaseVersionRange }); + ngUpdate()(tree, context); + expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('is a no-op on the second run', () => { + const { context, addTask } = contextWithTaskSpy(); + const tree = treeWithDependencies({}); + ngUpdate()(tree, context); + ngUpdate()(tree, context); + expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange); + expect(addTask).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/src/schematics/update/v22/index.ts b/src/schematics/update/v22/index.ts new file mode 100644 index 000000000..0b0dd4b3a --- /dev/null +++ b/src/schematics/update/v22/index.ts @@ -0,0 +1,14 @@ +import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks/index.js'; +import { alignFirebaseVersion } from '../../common.js'; + +/* `ng update` runs this migration again on every prerelease step of the same release (rc.0 to + * rc.1, rc.1 to 22.0.0), so it must change nothing when the workspace is already aligned. */ +export const ngUpdate = (): Rule => (host: Tree, context: SchematicContext) => { + /* `ng update` updates a `firebase` entry the workspace already has, but adds none to a workspace + * that has no `firebase` entry at all. Under a peer dependency that workspace needs one. */ + if (alignFirebaseVersion(host, context)) { + context.addTask(new NodePackageInstallTask()); + } + return host; +}; diff --git a/tools/build.ts b/tools/build.ts index c512f867e..01ceeea36 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -383,6 +383,7 @@ const schematicEntryPoints = [ ['setup', 'index'], ['update', 'v7', 'index'], ['update', 'v21', 'index'], + ['update', 'v22', 'index'], ]; const externalPackages = [ From 10385539b193c94b3bc3cee11a4d893c9642f4d0 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 23 Oct 2026 13:41:36 -0700 Subject: [PATCH 2/2] docs: fix the v22 guide's migration range and pnpm warning text The upgrade guide said the migration updates an older firebase entry "to the current 12.x release". It writes ^12.18.0, and only when the existing entry falls outside that range, so say so. The pnpm paragraph quoted the unmet-peer warning as pnpm 9 and 10 print it. pnpm 11 changed the wording to "Issues with peer dependencies found. Run "pnpm peers check" to list them." and pnpm 12 keeps that, checked against this branch's tarball on 9.15.4, 10.34.8, 11.28.8 and 12.15.0. On all four the warning prints on the first resolution only, never on a lockfile-backed install, which the paragraph already said. --- docs/version-22-upgrade.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index d4c779541..159a1673c 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -23,7 +23,7 @@ Through AngularFire 21, `@angular/fire` listed `firebase` under `dependencies`, AngularFire 22 lists `firebase` under `peerDependencies` instead. So your app should declare `firebase` in its `package.json` (on Yarn it must, since Yarn installs no peer dependencies). This gives you exactly one copy of `firebase`, and an incompatible version is refused at install time instead of failing at runtime. -`ng update @angular/fire@canary` updates an older `firebase` entry in your `package.json` to the current 12.x release. If your app doesn't list `firebase` in `package.json` the migration adds `"firebase": "^12.18.0"` and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. +`ng update @angular/fire@canary` updates an older `firebase` entry in your `package.json` to `^12.18.0` when it requests a version outside that range. If your app doesn't list `firebase` in `package.json` the migration adds `"firebase": "^12.18.0"` and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. ### If you install with `npm install` instead of `ng update` @@ -53,6 +53,6 @@ An older version installs anyway with a warning (`has incorrect peer dependency ### pnpm -pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with `unmet peer firebase@^12.18.0: found 11.x`, and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile. +pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with a warning (`unmet peer firebase@^12.18.0: found 11.x` on pnpm 9 and 10, `Issues with peer dependencies found. Run "pnpm peers check" to list them.` on pnpm 11 and 12), and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile. Set `strict-peer-dependencies=true` in `.npmrc` to make pnpm refuse the mismatch outright.