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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class AppComponent {

[Upgrading from v6.0? Check out our guide.](docs/version-7-upgrade.md)

[Upgrading from AngularFire 20? See the v21 upgrade guide.](docs/version-21-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
2 changes: 2 additions & 0 deletions docs/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Firebase AI Logic gives you access to the latest generative AI models from Googl

[Learn more](https://firebase.google.com/docs/ai-logic)

> Firebase AI Logic was previously called **Vertex AI in Firebase**. If you are upgrading from AngularFire 20, the module moved from `@angular/fire/vertexai` to `@angular/fire/ai` and the symbols were renamed (`getVertexAI` to `getAI`, `provideVertexAI` to `provideAI`, `VertexAI` to `AI`). Running `ng update @angular/fire` rewrites these for you. See the [AngularFire 20 to 21 upgrade guide](./version-21-upgrade.md).

## Dependency Injection

As a prerequisite, ensure that `AngularFire` has been added to your project via
Expand Down
35 changes: 35 additions & 0 deletions docs/version-21-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Upgrading to AngularFire 21

AngularFire 21 targets **Angular 21** and the **Firebase JS SDK v12**. Most of the upgrade is handled for you by `ng update`.

## Run the update

```bash
ng update @angular/core @angular/cli # move your app to Angular 21 first
ng update @angular/fire # then AngularFire 21
```

`ng update @angular/fire` runs a migration that:

- **Aligns your `firebase` dependency to `^12.4.0`.** AngularFire 21 requires Firebase JS SDK 12. If your app still requested `firebase` 11, npm would install both 11 and 12 side by side, and the two copies reject each other's objects at runtime. The migration updates the dependency and reinstalls so you end up with a single copy. Verify with `npm ls firebase`.
- **Rewrites Vertex AI imports to AI Logic** (see below).

## Vertex AI is now Firebase AI Logic

The Vertex AI module has been renamed to Firebase AI Logic. The `@angular/fire/vertexai` entry point (and the older `@angular/fire/vertexai-preview`) are removed in favor of `@angular/fire/ai`:

| Before (`@angular/fire/vertexai`) | After (`@angular/fire/ai`) |
|---|---|
| `getVertexAI` | `getAI` |
| `provideVertexAI` | `provideAI` |
| `VertexAI` | `AI` |
| `VertexAIInstances` | `AIInstances` |
| `vertexAIInstance$` | `AIInstance$` |
| `VertexAIModule` | `AIModule` |

`ng update @angular/fire` rewrites these imports and identifiers for you. `getGenerativeModel` and `getImagenModel` keep their names. If you import directly from the Firebase SDK, note it also renamed `firebase/vertexai` to `firebase/ai`. See [ai.md](./ai.md) for current usage.

## Other notes

- **Angular 21 is required.** AngularFire 21 peers `@angular/* ^21.0.0` and does not support Angular 22 (a future AngularFire 22 will).
- The obsolete `@angular/platform-browser-dynamic` peer dependency was removed. No action is needed.
4 changes: 4 additions & 0 deletions src/schematics/update/v21/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// /tasks directory specifier only resolves under CommonJS.
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks/index.js';
import { alignFirebaseVersion } from '../../common.js';
import { rewriteVertexAIToAI } from './vertexai-to-ai.js';

// ng update re-runs this migration on rc-to-stable transitions (the CLI clamps the migration
// range's upper bound to the release version), so it must stay a no-op when nothing changes.
export const ngUpdate = (): Rule => (
host: Tree,
context: SchematicContext
) => {
// Rewrite Vertex AI imports to AI Logic (source-only edits, no dependency change).
rewriteVertexAIToAI(host, context);
// Align firebase. This step changes dependencies, so only it schedules an install.
if (alignFirebaseVersion(host, context)) {
context.addTask(new NodePackageInstallTask());
}
Expand Down
258 changes: 258 additions & 0 deletions src/schematics/update/v21/vertexai-to-ai.jasmine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { logging } from '@angular-devkit/core';
import { HostTree, SchematicContext } from '@angular-devkit/schematics';
import { rewriteVertexAIToAI } from './vertexai-to-ai.js';
import 'jasmine';

const context = { logger: new logging.Logger('test') } as unknown as SchematicContext;

const treeWith = (files: Record<string, string>) => {
const tree = new HostTree();
tree.create('angular.json', JSON.stringify({
projects: { app: { root: '', sourceRoot: 'src' } },
}));
Object.entries(files).forEach(([path, content]) => tree.create(path, content));
return tree;
};

describe('rewriteVertexAIToAI', () => {

it('rewrites a named import and its usages', () => {
const source = [
`import { provideVertexAI, getVertexAI, VertexAI } from '@angular/fire/vertexai';`,
`import { inject } from '@angular/core';`,
``,
`export const providers = [provideVertexAI(() => getVertexAI())];`,
`export class Foo { private ai = inject(VertexAI); }`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

const changed = rewriteVertexAIToAI(tree, context);

expect(changed).toBe(true);
const out = tree.readText('src/app/foo.ts');
expect(out).toContain(`from '@angular/fire/ai'`);
expect(out).not.toContain('vertexai');
expect(out).toContain('provideAI(() => getAI())');
expect(out).toContain('inject(AI)');
expect(out).not.toContain('VertexAI');
});

it('renames only the imported name for an aliased import, leaving usages of the alias', () => {
const source = [
`import { VertexAI as MyAI } from '@angular/fire/vertexai';`,
`import { inject } from '@angular/core';`,
`export class Foo { private ai = inject(MyAI); }`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
expect(out).toContain(`import { AI as MyAI } from '@angular/fire/ai';`);
expect(out).toContain('inject(MyAI)');
});

it('handles the older vertexai-preview entry point', () => {
const tree = treeWith({
'src/app/foo.ts': `import { getVertexAI } from '@angular/fire/vertexai-preview';`,
});

rewriteVertexAIToAI(tree, context);

expect(tree.readText('src/app/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
});

it('rewrites namespace-import member accesses', () => {
const source = [
`import * as vai from '@angular/fire/vertexai';`,
`export const p = vai.provideVertexAI(() => vai.getVertexAI());`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
expect(out).toContain(`import * as vai from '@angular/fire/ai';`);
expect(out).toContain('vai.provideAI(() => vai.getAI())');
});

it('leaves unchanged symbols alone', () => {
const tree = treeWith({
'src/app/foo.ts': `import { getGenerativeModel, getVertexAI } from '@angular/fire/vertexai';`,
});

rewriteVertexAIToAI(tree, context);

expect(tree.readText('src/app/foo.ts'))
.toBe(`import { getGenerativeModel, getAI } from '@angular/fire/ai';`);
});

it('does not touch strings, comments, or unrelated member names (the AST win over regex)', () => {
const source = [
`import { VertexAI } from '@angular/fire/vertexai';`,
`import { inject } from '@angular/core';`,
`// VertexAI is now AI Logic`,
`export const label = 'VertexAI docs';`,
`export class Foo { VertexAI = 1; private ai = inject(VertexAI); }`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
// comment and string keep the old word
expect(out).toContain('// VertexAI is now AI Logic');
expect(out).toContain(`'VertexAI docs'`);
// a class member literally named VertexAI is not the import binding, so it is untouched
expect(out).toContain('VertexAI = 1;');
// the real usage and the import are rewritten
expect(out).toContain(`from '@angular/fire/ai'`);
expect(out).toContain('inject(AI)');
});

it('is a no-op when there is nothing to rewrite', () => {
const tree = treeWith({
'src/app/foo.ts': `import { getAI } from '@angular/fire/ai';`,
});

const changed = rewriteVertexAIToAI(tree, context);

expect(changed).toBe(false);
expect(tree.readText('src/app/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
});

it('does not crash without an angular.json', () => {
const tree = new HostTree();
tree.create('src/app/foo.ts', `import { getVertexAI } from '@angular/fire/vertexai';`);

expect(() => rewriteVertexAIToAI(tree, context)).not.toThrow();
});

it('rewrites files in a non-root project (library / multi-project workspace)', () => {
const tree = new HostTree();
tree.create('angular.json', JSON.stringify({
projects: {
app: { root: '', sourceRoot: 'src' },
lib: { root: 'projects/lib', sourceRoot: 'projects/lib/src' },
},
}));
tree.create('projects/lib/src/foo.ts', `import { getVertexAI } from '@angular/fire/vertexai';`);

const changed = rewriteVertexAIToAI(tree, context);

expect(changed).toBe(true);
expect(tree.readText('projects/lib/src/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
});

it('renames a renamed symbol used in type position', () => {
const source = [
`import { VertexAI } from '@angular/fire/vertexai';`,
`export let x: VertexAI;`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

expect(tree.readText('src/app/foo.ts')).toContain('let x: AI;');
});

it('rewrites namespace member access in type position', () => {
const source = [
`import * as fire from '@angular/fire/vertexai';`,
`export function f(): fire.VertexAI { return fire.getVertexAI(); }`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
expect(out).toContain('fire.AI');
expect(out).toContain('fire.getAI()');
expect(out).not.toContain('VertexAI');
});

it('expands a shorthand property instead of changing its key', () => {
const source = [
`import { getVertexAI } from '@angular/fire/vertexai';`,
`export const registry = { getVertexAI };`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

expect(tree.readText('src/app/foo.ts')).toContain('{ getVertexAI: getAI }');
});

it('preserves the export name for a bare local re-export', () => {
const source = [
`import { VertexAI } from '@angular/fire/vertexai';`,
`export { VertexAI };`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
expect(out).toContain(`import { AI } from '@angular/fire/ai';`);
expect(out).toContain('export { AI as VertexAI };');
});

it('renames the instances token and instance observable', () => {
const tree = treeWith({
'src/app/foo.ts': `import { VertexAIInstances, vertexAIInstance$ } from '@angular/fire/vertexai';`,
});

rewriteVertexAIToAI(tree, context);

expect(tree.readText('src/app/foo.ts'))
.toBe(`import { AIInstances, AIInstance$ } from '@angular/fire/ai';`);
});

it('does not rename a get/set accessor named like an imported symbol', () => {
const source = [
`import { getVertexAI } from '@angular/fire/vertexai';`,
`export class Foo { get getVertexAI() { return 1; } }`,
`export const used = getVertexAI();`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
expect(out).toContain('get getVertexAI()');
expect(out).toContain('getAI()');
});

it('does not rename a destructuring property key, but does rename a binding initializer', () => {
const source = [
`import { getVertexAI } from '@angular/fire/vertexai';`,
`export function a(obj: any) { const { getVertexAI: local } = obj; return local; }`,
`export function b({ cb = getVertexAI }: any) { return cb; }`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);

const out = tree.readText('src/app/foo.ts');
// the property key read from obj is not the import, so it is left untouched
expect(out).toContain('const { getVertexAI: local } = obj;');
// the default initializer IS a genuine use of the import, so it is renamed
expect(out).toContain('cb = getAI');
});

it('is idempotent when re-run on already-migrated code', () => {
const source = [
`import { provideVertexAI, getVertexAI } from '@angular/fire/vertexai';`,
`export const p = provideVertexAI(() => getVertexAI());`,
].join('\n');
const tree = treeWith({ 'src/app/foo.ts': source });

rewriteVertexAIToAI(tree, context);
const afterFirst = tree.readText('src/app/foo.ts');
const changedAgain = rewriteVertexAIToAI(tree, context);

expect(changedAgain).toBe(false);
expect(tree.readText('src/app/foo.ts')).toBe(afterFirst);
});

});
Loading
Loading