Skip to content

Commit a6d6fcf

Browse files
feat(schematics): rewrite Vertex AI imports to AI Logic on ng update
AngularFire 21 renamed the Vertex AI module to Firebase AI Logic: the @angular/fire/vertexai and older @angular/fire/vertexai-preview entry points were removed in favor of @angular/fire/ai, and the exported symbols were renamed (getVertexAI to getAI, provideVertexAI to provideAI, VertexAI to AI, and so on). A workspace on 20 that used Vertex AI would fail to compile after the upgrade. Extend the existing v21 migration (which aligns the firebase dependency) to also rewrite these imports and their usages. The rewrite parses each source file with the TypeScript compiler and edits only genuine references, so it leaves strings, comments, and unrelated identifiers that merely share a name untouched. It handles named imports and their aliases, namespace imports in both value and type position, bare local re-exports, and shorthand properties. The one accepted limitation is name shadowing: because the rewrite matches by name, a local variable that shadows an imported name with the same spelling can be mis-renamed. ng update always presents its changes as a diff for review, so this is caught on inspection. Also add typescript to the schematics esbuild externals so the compiler is resolved from the workspace at ng-update time rather than bundled into the package, matching how Angular's own migrations ship. Docs: add a 20-to-21 upgrade guide, note the rename in the AI Logic guide, and link the upgrade guide from the README. Refs #3686
1 parent 6bdf3a8 commit a6d6fcf

7 files changed

Lines changed: 630 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export class AppComponent {
7474

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

77+
[Upgrading from AngularFire 20? See the v21 upgrade guide.](docs/version-21-upgrade.md)
78+
7779
### Sample app
7880

7981
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.

docs/ai.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Firebase AI Logic gives you access to the latest generative AI models from Googl
88

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

11+
> 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).
12+
1113
## Dependency Injection
1214

1315
As a prerequisite, ensure that `AngularFire` has been added to your project via

docs/version-21-upgrade.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Upgrading to AngularFire 21
2+
3+
AngularFire 21 targets **Angular 21** and the **Firebase JS SDK v12**. Most of the upgrade is handled for you by `ng update`.
4+
5+
## Run the update
6+
7+
```bash
8+
ng update @angular/core @angular/cli # move your app to Angular 21 first
9+
ng update @angular/fire # then AngularFire 21
10+
```
11+
12+
`ng update @angular/fire` runs a migration that:
13+
14+
- **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`.
15+
- **Rewrites Vertex AI imports to AI Logic** (see below).
16+
17+
## Vertex AI is now Firebase AI Logic
18+
19+
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`:
20+
21+
| Before (`@angular/fire/vertexai`) | After (`@angular/fire/ai`) |
22+
|---|---|
23+
| `getVertexAI` | `getAI` |
24+
| `provideVertexAI` | `provideAI` |
25+
| `VertexAI` | `AI` |
26+
| `VertexAIInstances` | `AIInstances` |
27+
| `vertexAIInstance$` | `AIInstance$` |
28+
| `VertexAIModule` | `AIModule` |
29+
30+
`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.
31+
32+
## Other notes
33+
34+
- **Angular 21 is required.** AngularFire 21 peers `@angular/* ^21.0.0` and does not support Angular 22 (a future AngularFire 22 will).
35+
- The obsolete `@angular/platform-browser-dynamic` peer dependency was removed. No action is needed.

src/schematics/update/v21/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
33
// /tasks directory specifier only resolves under CommonJS.
44
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks/index.js';
55
import { alignFirebaseVersion } from '../../common.js';
6+
import { rewriteVertexAIToAI } from './vertexai-to-ai.js';
67

78
// ng update re-runs this migration on rc-to-stable transitions (the CLI clamps the migration
89
// range's upper bound to the release version), so it must stay a no-op when nothing changes.
910
export const ngUpdate = (): Rule => (
1011
host: Tree,
1112
context: SchematicContext
1213
) => {
14+
// Rewrite Vertex AI imports to AI Logic (source-only edits, no dependency change).
15+
rewriteVertexAIToAI(host, context);
16+
// Align firebase. This step changes dependencies, so only it schedules an install.
1317
if (alignFirebaseVersion(host, context)) {
1418
context.addTask(new NodePackageInstallTask());
1519
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { logging } from '@angular-devkit/core';
2+
import { HostTree, SchematicContext } from '@angular-devkit/schematics';
3+
import { rewriteVertexAIToAI } from './vertexai-to-ai.js';
4+
import 'jasmine';
5+
6+
const context = { logger: new logging.Logger('test') } as unknown as SchematicContext;
7+
8+
const treeWith = (files: Record<string, string>) => {
9+
const tree = new HostTree();
10+
tree.create('angular.json', JSON.stringify({
11+
projects: { app: { root: '', sourceRoot: 'src' } },
12+
}));
13+
Object.entries(files).forEach(([path, content]) => tree.create(path, content));
14+
return tree;
15+
};
16+
17+
describe('rewriteVertexAIToAI', () => {
18+
19+
it('rewrites a named import and its usages', () => {
20+
const source = [
21+
`import { provideVertexAI, getVertexAI, VertexAI } from '@angular/fire/vertexai';`,
22+
`import { inject } from '@angular/core';`,
23+
``,
24+
`export const providers = [provideVertexAI(() => getVertexAI())];`,
25+
`export class Foo { private ai = inject(VertexAI); }`,
26+
].join('\n');
27+
const tree = treeWith({ 'src/app/foo.ts': source });
28+
29+
const changed = rewriteVertexAIToAI(tree, context);
30+
31+
expect(changed).toBe(true);
32+
const out = tree.readText('src/app/foo.ts');
33+
expect(out).toContain(`from '@angular/fire/ai'`);
34+
expect(out).not.toContain('vertexai');
35+
expect(out).toContain('provideAI(() => getAI())');
36+
expect(out).toContain('inject(AI)');
37+
expect(out).not.toContain('VertexAI');
38+
});
39+
40+
it('renames only the imported name for an aliased import, leaving usages of the alias', () => {
41+
const source = [
42+
`import { VertexAI as MyAI } from '@angular/fire/vertexai';`,
43+
`import { inject } from '@angular/core';`,
44+
`export class Foo { private ai = inject(MyAI); }`,
45+
].join('\n');
46+
const tree = treeWith({ 'src/app/foo.ts': source });
47+
48+
rewriteVertexAIToAI(tree, context);
49+
50+
const out = tree.readText('src/app/foo.ts');
51+
expect(out).toContain(`import { AI as MyAI } from '@angular/fire/ai';`);
52+
expect(out).toContain('inject(MyAI)');
53+
});
54+
55+
it('handles the older vertexai-preview entry point', () => {
56+
const tree = treeWith({
57+
'src/app/foo.ts': `import { getVertexAI } from '@angular/fire/vertexai-preview';`,
58+
});
59+
60+
rewriteVertexAIToAI(tree, context);
61+
62+
expect(tree.readText('src/app/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
63+
});
64+
65+
it('rewrites namespace-import member accesses', () => {
66+
const source = [
67+
`import * as vai from '@angular/fire/vertexai';`,
68+
`export const p = vai.provideVertexAI(() => vai.getVertexAI());`,
69+
].join('\n');
70+
const tree = treeWith({ 'src/app/foo.ts': source });
71+
72+
rewriteVertexAIToAI(tree, context);
73+
74+
const out = tree.readText('src/app/foo.ts');
75+
expect(out).toContain(`import * as vai from '@angular/fire/ai';`);
76+
expect(out).toContain('vai.provideAI(() => vai.getAI())');
77+
});
78+
79+
it('leaves unchanged symbols alone', () => {
80+
const tree = treeWith({
81+
'src/app/foo.ts': `import { getGenerativeModel, getVertexAI } from '@angular/fire/vertexai';`,
82+
});
83+
84+
rewriteVertexAIToAI(tree, context);
85+
86+
expect(tree.readText('src/app/foo.ts'))
87+
.toBe(`import { getGenerativeModel, getAI } from '@angular/fire/ai';`);
88+
});
89+
90+
it('does not touch strings, comments, or unrelated member names (the AST win over regex)', () => {
91+
const source = [
92+
`import { VertexAI } from '@angular/fire/vertexai';`,
93+
`import { inject } from '@angular/core';`,
94+
`// VertexAI is now AI Logic`,
95+
`export const label = 'VertexAI docs';`,
96+
`export class Foo { VertexAI = 1; private ai = inject(VertexAI); }`,
97+
].join('\n');
98+
const tree = treeWith({ 'src/app/foo.ts': source });
99+
100+
rewriteVertexAIToAI(tree, context);
101+
102+
const out = tree.readText('src/app/foo.ts');
103+
// comment and string keep the old word
104+
expect(out).toContain('// VertexAI is now AI Logic');
105+
expect(out).toContain(`'VertexAI docs'`);
106+
// a class member literally named VertexAI is not the import binding, so it is untouched
107+
expect(out).toContain('VertexAI = 1;');
108+
// the real usage and the import are rewritten
109+
expect(out).toContain(`from '@angular/fire/ai'`);
110+
expect(out).toContain('inject(AI)');
111+
});
112+
113+
it('is a no-op when there is nothing to rewrite', () => {
114+
const tree = treeWith({
115+
'src/app/foo.ts': `import { getAI } from '@angular/fire/ai';`,
116+
});
117+
118+
const changed = rewriteVertexAIToAI(tree, context);
119+
120+
expect(changed).toBe(false);
121+
expect(tree.readText('src/app/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
122+
});
123+
124+
it('does not crash without an angular.json', () => {
125+
const tree = new HostTree();
126+
tree.create('src/app/foo.ts', `import { getVertexAI } from '@angular/fire/vertexai';`);
127+
128+
expect(() => rewriteVertexAIToAI(tree, context)).not.toThrow();
129+
});
130+
131+
it('rewrites files in a non-root project (library / multi-project workspace)', () => {
132+
const tree = new HostTree();
133+
tree.create('angular.json', JSON.stringify({
134+
projects: {
135+
app: { root: '', sourceRoot: 'src' },
136+
lib: { root: 'projects/lib', sourceRoot: 'projects/lib/src' },
137+
},
138+
}));
139+
tree.create('projects/lib/src/foo.ts', `import { getVertexAI } from '@angular/fire/vertexai';`);
140+
141+
const changed = rewriteVertexAIToAI(tree, context);
142+
143+
expect(changed).toBe(true);
144+
expect(tree.readText('projects/lib/src/foo.ts')).toBe(`import { getAI } from '@angular/fire/ai';`);
145+
});
146+
147+
it('renames a renamed symbol used in type position', () => {
148+
const source = [
149+
`import { VertexAI } from '@angular/fire/vertexai';`,
150+
`export let x: VertexAI;`,
151+
].join('\n');
152+
const tree = treeWith({ 'src/app/foo.ts': source });
153+
154+
rewriteVertexAIToAI(tree, context);
155+
156+
expect(tree.readText('src/app/foo.ts')).toContain('let x: AI;');
157+
});
158+
159+
it('rewrites namespace member access in type position', () => {
160+
const source = [
161+
`import * as fire from '@angular/fire/vertexai';`,
162+
`export function f(): fire.VertexAI { return fire.getVertexAI(); }`,
163+
].join('\n');
164+
const tree = treeWith({ 'src/app/foo.ts': source });
165+
166+
rewriteVertexAIToAI(tree, context);
167+
168+
const out = tree.readText('src/app/foo.ts');
169+
expect(out).toContain('fire.AI');
170+
expect(out).toContain('fire.getAI()');
171+
expect(out).not.toContain('VertexAI');
172+
});
173+
174+
it('expands a shorthand property instead of changing its key', () => {
175+
const source = [
176+
`import { getVertexAI } from '@angular/fire/vertexai';`,
177+
`export const registry = { getVertexAI };`,
178+
].join('\n');
179+
const tree = treeWith({ 'src/app/foo.ts': source });
180+
181+
rewriteVertexAIToAI(tree, context);
182+
183+
expect(tree.readText('src/app/foo.ts')).toContain('{ getVertexAI: getAI }');
184+
});
185+
186+
it('preserves the export name for a bare local re-export', () => {
187+
const source = [
188+
`import { VertexAI } from '@angular/fire/vertexai';`,
189+
`export { VertexAI };`,
190+
].join('\n');
191+
const tree = treeWith({ 'src/app/foo.ts': source });
192+
193+
rewriteVertexAIToAI(tree, context);
194+
195+
const out = tree.readText('src/app/foo.ts');
196+
expect(out).toContain(`import { AI } from '@angular/fire/ai';`);
197+
expect(out).toContain('export { AI as VertexAI };');
198+
});
199+
200+
it('renames the instances token and instance observable', () => {
201+
const tree = treeWith({
202+
'src/app/foo.ts': `import { VertexAIInstances, vertexAIInstance$ } from '@angular/fire/vertexai';`,
203+
});
204+
205+
rewriteVertexAIToAI(tree, context);
206+
207+
expect(tree.readText('src/app/foo.ts'))
208+
.toBe(`import { AIInstances, AIInstance$ } from '@angular/fire/ai';`);
209+
});
210+
211+
it('does not rename a get/set accessor named like an imported symbol', () => {
212+
const source = [
213+
`import { getVertexAI } from '@angular/fire/vertexai';`,
214+
`export class Foo { get getVertexAI() { return 1; } }`,
215+
`export const used = getVertexAI();`,
216+
].join('\n');
217+
const tree = treeWith({ 'src/app/foo.ts': source });
218+
219+
rewriteVertexAIToAI(tree, context);
220+
221+
const out = tree.readText('src/app/foo.ts');
222+
expect(out).toContain('get getVertexAI()');
223+
expect(out).toContain('getAI()');
224+
});
225+
226+
it('does not rename a destructuring property key, but does rename a binding initializer', () => {
227+
const source = [
228+
`import { getVertexAI } from '@angular/fire/vertexai';`,
229+
`export function a(obj: any) { const { getVertexAI: local } = obj; return local; }`,
230+
`export function b({ cb = getVertexAI }: any) { return cb; }`,
231+
].join('\n');
232+
const tree = treeWith({ 'src/app/foo.ts': source });
233+
234+
rewriteVertexAIToAI(tree, context);
235+
236+
const out = tree.readText('src/app/foo.ts');
237+
// the property key read from obj is not the import, so it is left untouched
238+
expect(out).toContain('const { getVertexAI: local } = obj;');
239+
// the default initializer IS a genuine use of the import, so it is renamed
240+
expect(out).toContain('cb = getAI');
241+
});
242+
243+
it('is idempotent when re-run on already-migrated code', () => {
244+
const source = [
245+
`import { provideVertexAI, getVertexAI } from '@angular/fire/vertexai';`,
246+
`export const p = provideVertexAI(() => getVertexAI());`,
247+
].join('\n');
248+
const tree = treeWith({ 'src/app/foo.ts': source });
249+
250+
rewriteVertexAIToAI(tree, context);
251+
const afterFirst = tree.readText('src/app/foo.ts');
252+
const changedAgain = rewriteVertexAIToAI(tree, context);
253+
254+
expect(changedAgain).toBe(false);
255+
expect(tree.readText('src/app/foo.ts')).toBe(afterFirst);
256+
});
257+
258+
});

0 commit comments

Comments
 (0)