Skip to content

Commit 29ca012

Browse files
build!: make firebase a peer dependency and add the v22 migration (#3805)
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. The guide states the exact range the migration writes (^12.18.0) and quotes pnpm's unmet-peer warning as pnpm 9 and 10 print it and as pnpm 11 and 12 print it. 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. Fixes #3804 Refs #3684, #3681, #3682
1 parent f95d598 commit 29ca012

11 files changed

Lines changed: 156 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export class App {
7575

7676
[Upgrading from AngularFire 20? See the v21 upgrade guide.](docs/version-21-upgrade.md)
7777

78+
[Upgrading from AngularFire 21? See the v22 upgrade guide.](docs/version-22-upgrade.md)
79+
7880
### Sample app
7981

8082
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.

‎docs/version-22-upgrade.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Upgrading to the AngularFire 22 canary
2+
3+
AngularFire 22 targets **Angular 22** and the **Firebase JS SDK v12**. Most of the upgrade is handled for you by `ng update`.
4+
5+
## Run the update
6+
7+
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:
8+
9+
```bash
10+
ng update @angular/core@22 @angular/cli@22 @angular/fire@canary
11+
```
12+
13+
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.
14+
15+
The AngularFire update runs a migration that:
16+
17+
- **Adds** `firebase` **to your** `package.json` **if it is missing**, as `"firebase": "^12.18.0"`, and reinstalls. See the next section for why.
18+
- **Aligns an existing** `firebase` **entry to** `^12.18.0` when it requests a version outside that range.
19+
20+
## Your app now owns the Firebase SDK
21+
22+
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`.
23+
24+
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.
25+
26+
`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.
27+
28+
### If you install with `npm install` instead of `ng update`
29+
30+
Add the dependency yourself:
31+
32+
```bash
33+
npm install @angular/fire@canary firebase@^12.18.0
34+
```
35+
36+
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.
37+
38+
### If npm stops with `ERESOLVE`
39+
40+
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:
41+
42+
```bash
43+
npm install firebase@^12.18.0
44+
```
45+
46+
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.
47+
48+
### Yarn
49+
50+
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.
51+
52+
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.
53+
54+
### pnpm
55+
56+
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.
57+
58+
Set `strict-peer-dependencies=true` in `.npmrc` to make pnpm refuse the mismatch outright.

‎src/ng-package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
"allowedNonPeerDependencies": [
88
"@angular-devkit/architect", "@angular-devkit/core",
99
"@angular-devkit/schematics", "@schematics/angular",
10-
"jsonc-parser", "firebase", "rxfire"
10+
"jsonc-parser", "rxfire"
1111
]
1212
}

‎src/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@angular/common": "^22.0.0",
3232
"@angular/core": "^22.0.0",
3333
"@angular/platform-browser": "^22.0.0",
34+
"firebase": "^12.18.0",
3435
"rxjs": "~7.8.0",
3536
"firebase-tools": "^14.0.0 || ^15.0.0",
3637
"typescript": ">=6.0 <6.1"
@@ -40,7 +41,6 @@
4041
"typescript": { "optional": true }
4142
},
4243
"dependencies": {
43-
"firebase": "^12.18.0",
4444
"rxfire": "^6.2.0",
4545
"@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0",
4646
"@angular-devkit/core": "^22.0.0",

‎src/schematics/common.jasmine.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ describe('alignFirebaseVersion', () => {
214214
expect(sectionIn(tree, 'devDependencies').firebase).toBe(firebaseVersionRange);
215215
});
216216

217-
it('matches the firebase range the library actually ships with', () => {
217+
it('matches the firebase range the library declares as its peer dependency', () => {
218218
const libraryManifest = JSON.parse(readFileSync('src/package.json', 'utf8'));
219-
expect(libraryManifest.dependencies.firebase).toBe(firebaseVersionRange);
219+
expect(libraryManifest.peerDependencies.firebase).toBe(firebaseVersionRange);
220220
});
221221

222222
it('is a no-op when run a second time', () => {

‎src/schematics/common.ts‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,20 @@ export const addDependencies = (
7272
overwriteIfExists(host, 'package.json', stringifyFormatted(packageJson));
7373
};
7474

75-
/* Must stay identical to `dependencies.firebase` in `src/package.json`: if the two drift, the
76-
* alignment below can pin workspaces outside the range the library actually installs against,
77-
* re-creating duplicate-SDK trees. */
75+
/* Must stay identical to `peerDependencies.firebase` in `src/package.json`: if the two drift, the
76+
* alignment below writes a range the peer dependency then rejects. */
7877
export const firebaseVersionRange = '^12.18.0';
7978

8079
/**
8180
* Aligns the workspace's `firebase` entry with the range `@angular/fire` requires.
8281
*
83-
* `@angular/fire` bundles its own `firebase` dependency, so a workspace pinned to an older
84-
* major never conflicts at install time; npm silently nests a second SDK copy under
85-
* `@angular/fire`, and the two copies reject each other's objects at runtime (#3684, #3681,
86-
* #3682). Rewriting the workspace's range before the install runs is the only point where
87-
* that class of breakage can be headed off.
82+
* `firebase` is a peer dependency of `@angular/fire`. Depending on the package manager and its
83+
* settings, a workspace range outside it is refused at install time or installs a `firebase`
84+
* the library was not built against, and a workspace that never declared `firebase` gets no
85+
* entry written. Rewriting the entry before the install runs heads both off.
8886
*
8987
* Returns whether `package.json` was modified, so callers can skip scheduling an install
90-
* when nothing changed (`ng update` re-runs the v21 migration on rc-to-stable updates).
88+
* when nothing changed (`ng update` re-runs a migration on rc-to-stable updates).
9189
*/
9290
export const alignFirebaseVersion = (
9391
host: Tree,
@@ -119,17 +117,17 @@ export const alignFirebaseVersion = (
119117
} catch (_) {
120118
context.logger.warn(
121119
`⚠️ The firebase version in your package.json (${declaredFirebaseVersion}) is not a semver ` +
122-
`range, so it was left as-is; make sure it resolves inside ${firebaseVersionRange}, the ` +
123-
'range @angular/fire requires; a version outside it can leave the install with two copies of the firebase SDK.'
120+
`range, so it was left as-is. Make sure it resolves inside ${firebaseVersionRange}, the ` +
121+
'range @angular/fire requires. A version outside it either fails to install or installs a firebase that @angular/fire was not built against.'
124122
);
125123
continue;
126124
}
127125
if (alreadyCompatible) { continue; }
128126
packageJson[section].firebase = firebaseVersionRange;
129127
context.logger.info(
130128
`Updated the firebase version in your package.json from ${declaredFirebaseVersion} to ` +
131-
`${firebaseVersionRange}, the range @angular/fire requires; a workspace range outside it ` +
132-
'can leave the install with a second copy of the firebase SDK, which fails at runtime.'
129+
`${firebaseVersionRange}, the range @angular/fire requires. A range outside it either fails ` +
130+
'to install or installs a firebase that @angular/fire was not built against.'
133131
);
134132
changed = true;
135133
}

‎src/schematics/migration.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88
},
99
"migration-v21": {
1010
"version": "21.0.0",
11-
"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)",
11+
"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)",
1212
"factory": "./update/v21#ngUpdate"
13+
},
14+
"migration-v22": {
15+
"version": "22.0.0",
16+
"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",
17+
"factory": "./update/v22#ngUpdate"
1318
}
1419
}
1520
}

‎src/schematics/tsconfig.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@
2929
"setup/index.ts",
3030
"update/v7/index.ts",
3131
"update/v21/index.ts",
32+
"update/v22/index.ts",
3233
]
3334
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { logging } from '@angular-devkit/core';
2+
import { HostTree, SchematicContext } from '@angular-devkit/schematics';
3+
import { firebaseVersionRange } from '../../common.js';
4+
import { ngUpdate } from './index.js';
5+
import 'jasmine';
6+
7+
const contextWithTaskSpy = () => {
8+
const addTask = jasmine.createSpy('addTask');
9+
const context = {
10+
logger: new logging.Logger('test'),
11+
addTask,
12+
} as unknown as SchematicContext;
13+
return { context, addTask };
14+
};
15+
16+
const treeWithDependencies = (dependencies: Record<string, string>) => {
17+
const tree = new HostTree();
18+
tree.create('package.json', JSON.stringify({ name: 'test-app', dependencies }, null, 2));
19+
return tree;
20+
};
21+
22+
const dependenciesIn = (tree: HostTree) => JSON.parse(tree.readText('package.json')).dependencies;
23+
24+
describe('migration-v22 ngUpdate', () => {
25+
26+
it('adds firebase when the workspace never declared it and schedules an install', () => {
27+
const { context, addTask } = contextWithTaskSpy();
28+
const tree = treeWithDependencies({ '@angular/fire': '^22.0.0' });
29+
ngUpdate()(tree, context);
30+
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
31+
expect(addTask).toHaveBeenCalledTimes(1);
32+
});
33+
34+
it('aligns a firebase range outside the peer range and schedules an install', () => {
35+
const { context, addTask } = contextWithTaskSpy();
36+
const tree = treeWithDependencies({ firebase: '^11.0.0' });
37+
ngUpdate()(tree, context);
38+
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
39+
expect(addTask).toHaveBeenCalledTimes(1);
40+
});
41+
42+
it('schedules nothing when the workspace is already aligned', () => {
43+
const { context, addTask } = contextWithTaskSpy();
44+
const tree = treeWithDependencies({ firebase: firebaseVersionRange });
45+
ngUpdate()(tree, context);
46+
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
47+
expect(addTask).not.toHaveBeenCalled();
48+
});
49+
50+
it('is a no-op on the second run', () => {
51+
const { context, addTask } = contextWithTaskSpy();
52+
const tree = treeWithDependencies({});
53+
ngUpdate()(tree, context);
54+
ngUpdate()(tree, context);
55+
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
56+
expect(addTask).toHaveBeenCalledTimes(1);
57+
});
58+
59+
});

‎src/schematics/update/v22/index.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2+
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks/index.js';
3+
import { alignFirebaseVersion } from '../../common.js';
4+
5+
/* `ng update` runs this migration again on every prerelease step of the same release (rc.0 to
6+
* rc.1, rc.1 to 22.0.0), so it must change nothing when the workspace is already aligned. */
7+
export const ngUpdate = (): Rule => (host: Tree, context: SchematicContext) => {
8+
/* `ng update` updates a `firebase` entry the workspace already has, but adds none to a workspace
9+
* that has no `firebase` entry at all. Under a peer dependency that workspace needs one. */
10+
if (alignFirebaseVersion(host, context)) {
11+
context.addTask(new NodePackageInstallTask());
12+
}
13+
return host;
14+
};

0 commit comments

Comments
 (0)