Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions docs/version-22-upgrade.md
Original file line number Diff line number Diff line change
@@ -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 `^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`

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 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.
2 changes: 1 addition & 1 deletion src/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"allowedNonPeerDependencies": [
"@angular-devkit/architect", "@angular-devkit/core",
"@angular-devkit/schematics", "@schematics/angular",
"jsonc-parser", "firebase", "rxfire"
"jsonc-parser", "rxfire"
]
}
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/schematics/common.jasmine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
24 changes: 11 additions & 13 deletions src/schematics/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -119,17 +117,17 @@ 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;
}
if (alreadyCompatible) { continue; }
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;
}
Expand Down
7 changes: 6 additions & 1 deletion src/schematics/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
1 change: 1 addition & 0 deletions src/schematics/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"setup/index.ts",
"update/v7/index.ts",
"update/v21/index.ts",
"update/v22/index.ts",
]
}
59 changes: 59 additions & 0 deletions src/schematics/update/v22/index.jasmine.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) => {
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);
});

});
14 changes: 14 additions & 0 deletions src/schematics/update/v22/index.ts
Original file line number Diff line number Diff line change
@@ -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;
};
1 change: 1 addition & 0 deletions tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ const schematicEntryPoints = [
['setup', 'index'],
['update', 'v7', 'index'],
['update', 'v21', 'index'],
['update', 'v22', 'index'],
];

const externalPackages = [
Expand Down
Loading