Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 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.
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