Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,44 @@
- name: Test browser
run: npm run test:${{ matrix.browser }}

# The package is built against firebase 12, the oldest major the peer range accepts. This job
# runs that build's specs against firebase 13. firebase 13 requires Node 24.12 or newer.
firebase-13:
runs-on: ubuntu-latest
needs: build
name: Test firebase 13
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '>=24.15.0 <25'
check-latest: false
- name: Setup java
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'temurin'
java-version: '21'
- name: Install deps
run: |
npm ci
npm install --no-save firebase@13.1.0
node -e "const version = require('firebase/package.json').version; console.log('firebase', version); process.exit(version === '13.1.0' ? 0 : 1)"
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: angularfire-${{ github.run_id }}
path: dist
- name: Test Node
run: |
npm run build:jasmine
npm run test:node
- name: Test headless
run: npm run test:chrome-headless

contribute:
runs-on: ${{ matrix.os }}
name: Contribute Node ${{ matrix.node }} (${{ matrix.os }})
Expand Down Expand Up @@ -154,14 +192,14 @@
test_and_contribute:
runs-on: ubuntu-latest
name: Branch protection
needs: ['test', 'contribute', 'browser']
needs: ['test', 'contribute', 'browser', 'firebase-13']
steps:
- run: true

publish:
runs-on: ubuntu-latest
name: Publish (NPM)
needs: ['build', 'test', 'browser']
needs: ['build', 'test', 'browser', 'firebase-13']
if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'release' }}
# One npm publish at a time, each held until npm lists it (about 20 minutes at most), so every
# check below reads what the previous publish wrote.
Expand Down Expand Up @@ -201,7 +239,7 @@
- name: Publish
id: publish
if: steps.canary_check.outputs.skip != 'true' && steps.release_tag.outputs.published != 'true'
run: npm publish ./dist/packages-dist --access public --registry https://wombat-dressing-room.appspot.com --tag "$NPM_TAG"

Check notice on line 242 in .github/workflows/test.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

use-trusted-publishing

test.yml:242: prefer trusted publishing for authentication: this command
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ steps.release_tag.outputs.tag || 'canary' }}
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"jsonc-parser": "^3.0.0",
"open": "^7.0.3 || ^8.0.0",
"ora": "^5.3.0",
"rxfire": "^6.2.0",
"rxfire": "^6.3.0",
"rxjs": "~7.8.0",
"semver": "^7.1.3",
"triple-beam": "^1.3.0",
Expand Down
4 changes: 2 additions & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@angular/common": "^22.0.0",
"@angular/core": "^22.0.0",
"@angular/platform-browser": "^22.0.0",
"firebase": "^12.18.0",
"firebase": "^12.18.0 || ^13.0.0",
"rxjs": "~7.8.0",
"firebase-tools": "^14.0.0 || ^15.0.0",
"typescript": ">=6.0 <6.1"
Expand All @@ -41,7 +41,7 @@
"typescript": { "optional": true }
},
"dependencies": {
"rxfire": "^6.2.0",
"rxfire": "^6.3.0",
"@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0",
"tslib": "^2.3.0"
},
Expand Down
138 changes: 134 additions & 4 deletions src/schematics/common.jasmine.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { readFileSync } from 'fs';
import { logging } from '@angular-devkit/core';
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { logging, normalize, virtualFs } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { HostTree, SchematicContext } from '@angular-devkit/schematics';
import { minVersion as semverMinVersion } from 'semver';
import { alignFirebaseVersion, firebaseVersionRange, pinInstalledPrereleaseVersion } from './common.js';
import {
alignFirebaseVersion,
firebase13VersionRange,
firebaseVersionRange,
pinInstalledPrereleaseVersion,
supportedFirebaseRange,
} from './common.js';
import 'jasmine';

const context = { logger: new logging.Logger('test') } as unknown as SchematicContext;
Expand Down Expand Up @@ -135,6 +144,21 @@ const treeWithFirebase = (firebaseVersion?: string, section = 'dependencies') =>
return tree;
};

const withInstalledFirebase = (tree: HostTree, version: string) => {
tree.create('node_modules/firebase/package.json', JSON.stringify({ name: 'firebase', version }));
return tree;
};

/* Not rmSync, which this repo's @types/node predates, and not rmdirSync's recursive option,
* which Node 26 removed. */
const removeDirectory = (target: string) => {
for (const entry of readdirSync(target)) {
const entryPath = join(target, entry);
if (lstatSync(entryPath).isDirectory()) { removeDirectory(entryPath); } else { unlinkSync(entryPath); }
}
rmdirSync(target);
};

describe('alignFirebaseVersion', () => {

it('rewrites a previous-major range', () => {
Expand Down Expand Up @@ -214,9 +238,115 @@ describe('alignFirebaseVersion', () => {
expect(sectionIn(tree, 'devDependencies').firebase).toBe(firebaseVersionRange);
});

it('leaves a firebase 13 range untouched', () => {
for (const declared of ['^13.0.0', '^13.1.0', '~13.1.0', supportedFirebaseRange]) {
const tree = treeWithFirebase(declared);
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeFalse();
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(declared);
}
});

it('rewrites an out-of-range entry that starts at firebase 13 to the 13 range, not back to 12', () => {
for (const declared of ['>=13.0.0', '^13.0.0-20260929163754']) {
const tree = treeWithFirebase(declared);
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(firebase13VersionRange);
}
});

it('rewrites an out-of-range entry that reaches below firebase 13 to the 12 range when nothing is installed', () => {
for (const declared of ['*', '>=12.18.0']) {
const tree = treeWithFirebase(declared);
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(firebaseVersionRange);
}
});

it('adds the range of the firebase major already installed', () => {
for (const [installed, expected] of [['13.1.0', firebase13VersionRange], ['12.19.0', firebaseVersionRange]]) {
const tree = withInstalledFirebase(treeWithFirebase(undefined), installed);
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
}
});

it('rewrites an out-of-range entry to the range of the firebase major already installed', () => {
for (const [installed, expected] of [['13.1.0', firebase13VersionRange], ['12.19.0', firebaseVersionRange]]) {
const tree = withInstalledFirebase(treeWithFirebase('*'), installed);
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
}
});

it('falls back to the declared range when the installed firebase is outside both majors', () => {
for (const [installed, declared, expected] of [
['11.10.0', '^11.0.0', firebaseVersionRange],
['13.0.0-20260929163754', '^13.0.0-20260929163754', firebase13VersionRange],
]) {
const tree = withInstalledFirebase(treeWithFirebase(declared), installed);
expect(alignFirebaseVersion(tree, context)).withContext(installed).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(installed).toBe(expected);
}
});

it('ignores an installed copy that the workspace entry excludes', () => {
for (const [installed, declared, expected] of [
['13.1.0', '~12.10.0', firebaseVersionRange],
['12.19.0', '>=13.0.0', firebase13VersionRange],
]) {
const tree = withInstalledFirebase(treeWithFirebase(declared), installed);
expect(alignFirebaseVersion(tree, context)).withContext(declared).toBeTrue();
expect(dependenciesIn(tree).firebase).withContext(declared).toBe(expected);
}
});

it('finds firebase installed at a monorepo root above the workspace', () => {
const repository = mkdtempSync(join(tmpdir(), 'angularfire-monorepo-'));
try {
const workspace = join(repository, 'apps', 'web');
mkdirSync(workspace, { recursive: true });
mkdirSync(join(repository, 'node_modules', 'firebase'), { recursive: true });
writeFileSync(join(repository, 'node_modules', 'firebase', 'package.json'),
JSON.stringify({ name: 'firebase', version: '13.1.0', exports: { './package.json': './package.json' } }));
writeFileSync(join(workspace, 'package.json'), JSON.stringify({ name: 'web' }));
// The host `ng add` and `ng update` give a schematic: the real filesystem, scoped to the workspace.
const tree = new HostTree(new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(workspace)));
expect(alignFirebaseVersion(tree, context)).toBeTrue();
expect(dependenciesIn(tree).firebase).toBe(firebase13VersionRange);
} finally {
removeDirectory(repository);
}
});

it('ignores an installed firebase manifest it cannot read', () => {
const tree = treeWithFirebase(undefined);
tree.create('node_modules/firebase/package.json', 'not json');
expect(alignFirebaseVersion(tree, context)).toBeTrue();
expect(dependenciesIn(tree).firebase).toBe(firebaseVersionRange);
});

it('mentions the supported range when it rewrites an entry', () => {
const infoSpy = spyOn(context.logger, 'info');
alignFirebaseVersion(treeWithFirebase('^11.0.0'), context);
expect(infoSpy).toHaveBeenCalledWith(jasmine.stringContaining(supportedFirebaseRange));
});

it('matches the firebase range the library declares as its peer dependency', () => {
const libraryManifest = JSON.parse(readFileSync('src/package.json', 'utf8'));
expect(libraryManifest.peerDependencies.firebase).toBe(firebaseVersionRange);
expect(libraryManifest.peerDependencies.firebase).toBe(supportedFirebaseRange);
});

it('builds against the oldest firebase major the peer dependency accepts', () => {
/* tools/build.ts imports wrapped functions by name from the installed firebase, so building
* on a newer major can emit an import that the older major does not export. */
const rootManifest = JSON.parse(readFileSync('package.json', 'utf8'));
expect(semverMinVersion(rootManifest.dependencies.firebase)?.major)
.toBe(semverMinVersion(supportedFirebaseRange)?.major);
});

it('writes a range for exactly the majors the peer dependency accepts', () => {
expect(supportedFirebaseRange.split('||').map(range => range.trim()).sort())
.toEqual([firebaseVersionRange, firebase13VersionRange].sort());
});

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