Skip to content

Commit fe1e281

Browse files
feat: accept firebase 13 alongside firebase 12
firebase 13.1.0 is npm latest, so `npm install firebase` followed by `ng add @angular/fire@22` failed with ERESOLVE against the ^12.18.0 peer. The peer range is now ^12.18.0 || ^13.0.0, and rxfire moves to ^6.3.0, whose peer range includes firebase 13. The ng add and ng update schematics now write the range of the firebase major already installed. ng add installs the missing peer at the newest allowed version before the schematic runs, so writing ^12.18.0 over an installed 13 left two @firebase/app copies and the app failed at startup. The lookup walks up to a monorepo root, and an installed copy the app's own entry excludes is ignored. The package keeps building against firebase 12, the oldest supported major, because the generated wrappers import functions by name from the installed firebase. A new CI job runs the specs against firebase 13.1.0 on Node 24, which firebase 13 requires. Fixes #3830
1 parent 398fef4 commit fe1e281

6 files changed

Lines changed: 254 additions & 24 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,44 @@ jobs:
112112
- name: Test browser
113113
run: npm run test:${{ matrix.browser }}
114114

115+
# The package is built against firebase 12, the oldest major the peer range accepts. This job
116+
# runs that build's specs against firebase 13. firebase 13 requires Node 24.12 or newer.
117+
firebase-13:
118+
runs-on: ubuntu-latest
119+
needs: build
120+
name: Test firebase 13
121+
steps:
122+
- name: Checkout
123+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
124+
with:
125+
persist-credentials: false
126+
- name: Setup node
127+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
128+
with:
129+
node-version: '>=24.15.0 <25'
130+
check-latest: false
131+
- name: Setup java
132+
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
133+
with:
134+
distribution: 'temurin'
135+
java-version: '21'
136+
- name: Install deps
137+
run: |
138+
npm ci
139+
npm install --no-save firebase@13.1.0
140+
node -e "const version = require('firebase/package.json').version; console.log('firebase', version); process.exit(version === '13.1.0' ? 0 : 1)"
141+
- name: Download Artifacts
142+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
143+
with:
144+
name: angularfire-${{ github.run_id }}
145+
path: dist
146+
- name: Test Node
147+
run: |
148+
npm run build:jasmine
149+
npm run test:node
150+
- name: Test headless
151+
run: npm run test:chrome-headless
152+
115153
contribute:
116154
runs-on: ${{ matrix.os }}
117155
name: Contribute Node ${{ matrix.node }} (${{ matrix.os }})
@@ -154,14 +192,14 @@ jobs:
154192
test_and_contribute:
155193
runs-on: ubuntu-latest
156194
name: Branch protection
157-
needs: ['test', 'contribute', 'browser']
195+
needs: ['test', 'contribute', 'browser', 'firebase-13']
158196
steps:
159197
- run: true
160198

161199
publish:
162200
runs-on: ubuntu-latest
163201
name: Publish (NPM)
164-
needs: ['build', 'test', 'browser']
202+
needs: ['build', 'test', 'browser', 'firebase-13']
165203
if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'release' }}
166204
# One npm publish at a time, each held until npm lists it (about 20 minutes at most), so every
167205
# check below reads what the previous publish wrote.

‎package-lock.json‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"jsonc-parser": "^3.0.0",
7373
"open": "^7.0.3 || ^8.0.0",
7474
"ora": "^5.3.0",
75-
"rxfire": "^6.2.0",
75+
"rxfire": "^6.3.0",
7676
"rxjs": "~7.8.0",
7777
"semver": "^7.1.3",
7878
"triple-beam": "^1.3.0",

‎src/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +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",
34+
"firebase": "^12.18.0 || ^13.0.0",
3535
"rxjs": "~7.8.0",
3636
"firebase-tools": "^14.0.0 || ^15.0.0",
3737
"typescript": ">=6.0 <6.1"
@@ -41,7 +41,7 @@
4141
"typescript": { "optional": true }
4242
},
4343
"dependencies": {
44-
"rxfire": "^6.2.0",
44+
"rxfire": "^6.3.0",
4545
"@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0",
4646
"tslib": "^2.3.0"
4747
},

‎src/schematics/common.jasmine.ts‎

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { readFileSync } from 'fs';
2-
import { logging } from '@angular-devkit/core';
1+
import { mkdirSync, mkdtempSync, readFileSync, rmdirSync, writeFileSync } from 'fs';
2+
import { tmpdir } from 'os';
3+
import { join } from 'path';
4+
import { logging, normalize, virtualFs } from '@angular-devkit/core';
5+
import { NodeJsSyncHost } from '@angular-devkit/core/node';
36
import { HostTree, SchematicContext } from '@angular-devkit/schematics';
47
import { minVersion as semverMinVersion } from 'semver';
5-
import { alignFirebaseVersion, firebaseVersionRange, pinInstalledPrereleaseVersion } from './common.js';
8+
import {
9+
alignFirebaseVersion,
10+
firebase13VersionRange,
11+
firebaseVersionRange,
12+
pinInstalledPrereleaseVersion,
13+
supportedFirebaseRange,
14+
} from './common.js';
615
import 'jasmine';
716

817
const context = { logger: new logging.Logger('test') } as unknown as SchematicContext;
@@ -135,6 +144,11 @@ const treeWithFirebase = (firebaseVersion?: string, section = 'dependencies') =>
135144
return tree;
136145
};
137146

147+
const withInstalledFirebase = (tree: HostTree, version: string) => {
148+
tree.create('node_modules/firebase/package.json', JSON.stringify({ name: 'firebase', version }));
149+
return tree;
150+
};
151+
138152
describe('alignFirebaseVersion', () => {
139153

140154
it('rewrites a previous-major range', () => {
@@ -214,9 +228,116 @@ describe('alignFirebaseVersion', () => {
214228
expect(sectionIn(tree, 'devDependencies').firebase).toBe(firebaseVersionRange);
215229
});
216230

231+
it('leaves a firebase 13 range untouched', () => {
232+
for (const declared of ['^13.0.0', '^13.1.0', '~13.1.0', supportedFirebaseRange]) {
233+
const tree = treeWithFirebase(declared);
234+
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeFalse();
235+
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(declared);
236+
}
237+
});
238+
239+
it('rewrites an out-of-range entry that starts at firebase 13 to the 13 range, not back to 12', () => {
240+
for (const declared of ['>=13.0.0', '^13.0.0-20260929163754']) {
241+
const tree = treeWithFirebase(declared);
242+
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
243+
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(firebase13VersionRange);
244+
}
245+
});
246+
247+
it('rewrites an out-of-range entry that reaches below firebase 13 to the 12 range when nothing is installed', () => {
248+
for (const declared of ['*', '>=12.18.0']) {
249+
const tree = treeWithFirebase(declared);
250+
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
251+
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(firebaseVersionRange);
252+
}
253+
});
254+
255+
it('adds the range of the firebase major already installed', () => {
256+
for (const [installed, expected] of [['13.1.0', firebase13VersionRange], ['12.19.0', firebaseVersionRange]]) {
257+
const tree = withInstalledFirebase(treeWithFirebase(undefined), installed);
258+
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
259+
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
260+
}
261+
});
262+
263+
it('rewrites an out-of-range entry to the range of the firebase major already installed', () => {
264+
for (const [installed, expected] of [['13.1.0', firebase13VersionRange], ['12.19.0', firebaseVersionRange]]) {
265+
const tree = withInstalledFirebase(treeWithFirebase('*'), installed);
266+
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
267+
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
268+
}
269+
});
270+
271+
it('falls back to the declared range when the installed firebase is outside both majors', () => {
272+
for (const [installed, declared, expected] of [
273+
['11.10.0', '^11.0.0', firebaseVersionRange],
274+
['13.0.0-20260929163754', '^13.0.0-20260929163754', firebase13VersionRange],
275+
]) {
276+
const tree = withInstalledFirebase(treeWithFirebase(declared), installed);
277+
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
278+
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
279+
}
280+
});
281+
282+
it('ignores an installed copy that the workspace entry excludes', () => {
283+
for (const [installed, declared, expected] of [
284+
['13.1.0', '~12.10.0', firebaseVersionRange],
285+
['12.19.0', '>=13.0.0', firebase13VersionRange],
286+
]) {
287+
const tree = withInstalledFirebase(treeWithFirebase(declared), installed);
288+
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
289+
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(expected);
290+
}
291+
});
292+
293+
it('finds firebase installed at a monorepo root above the workspace', () => {
294+
const repository = mkdtempSync(join(tmpdir(), 'angularfire-monorepo-'));
295+
try {
296+
const workspace = join(repository, 'apps', 'web');
297+
mkdirSync(workspace, { recursive: true });
298+
mkdirSync(join(repository, 'node_modules', 'firebase'), { recursive: true });
299+
writeFileSync(join(repository, 'node_modules', 'firebase', 'package.json'),
300+
JSON.stringify({ name: 'firebase', version: '13.1.0', exports: { './package.json': './package.json' } }));
301+
writeFileSync(join(workspace, 'package.json'), JSON.stringify({ name: 'web' }));
302+
// The host `ng add` and `ng update` give a schematic: the real filesystem, scoped to the workspace.
303+
const tree = new HostTree(new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(workspace)));
304+
expect(alignFirebaseVersion(tree, context)).toBeTrue();
305+
expect(dependenciesIn(tree).firebase).toBe(firebase13VersionRange);
306+
} finally {
307+
// rmdirSync, not rmSync: this repo's @types/node predates fs.rmSync.
308+
rmdirSync(repository, { recursive: true });
309+
}
310+
});
311+
312+
it('ignores an installed firebase manifest it cannot read', () => {
313+
const tree = treeWithFirebase(undefined);
314+
tree.create('node_modules/firebase/package.json', 'not json');
315+
expect(alignFirebaseVersion(tree, context)).toBeTrue();
316+
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
317+
});
318+
319+
it('mentions the supported range when it rewrites an entry', () => {
320+
const infoSpy = spyOn(context.logger, 'info');
321+
alignFirebaseVersion(treeWithFirebase('^11.0.0'), context);
322+
expect(infoSpy).toHaveBeenCalledWith(jasmine.stringContaining(supportedFirebaseRange));
323+
});
324+
217325
it('matches the firebase range the library declares as its peer dependency', () => {
218326
const libraryManifest = JSON.parse(readFileSync('src/package.json', 'utf8'));
219-
expect(libraryManifest.peerDependencies.firebase).toBe(firebaseVersionRange);
327+
expect(libraryManifest.peerDependencies.firebase).toBe(supportedFirebaseRange);
328+
});
329+
330+
it('builds against the oldest firebase major the peer dependency accepts', () => {
331+
/* tools/build.ts imports wrapped functions by name from the installed firebase, so building
332+
* on a newer major can emit an import that the older major does not export. */
333+
const rootManifest = JSON.parse(readFileSync('package.json', 'utf8'));
334+
expect(semverMinVersion(rootManifest.dependencies.firebase)?.major)
335+
.toBe(semverMinVersion(supportedFirebaseRange)?.major);
336+
});
337+
338+
it('writes a range for exactly the majors the peer dependency accepts', () => {
339+
expect(supportedFirebaseRange.split('||').map(range => range.trim()).sort())
340+
.toEqual([firebaseVersionRange, firebase13VersionRange].sort());
220341
});
221342

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

0 commit comments

Comments
 (0)