Skip to content

Commit e0fe00a

Browse files
committed
feat(): support angular standalone default
1 parent aa13edc commit e0fe00a

File tree

5 files changed

+115
-22
lines changed

5 files changed

+115
-22
lines changed

packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ describe("migrateComponents", () => {
257257
);
258258
});
259259

260-
it("should detect Ionic components within *ngIf expressions", () => {
260+
it("should detect Ionic components within *ngIf expressions", async () => {
261261
const project = new Project({ useInMemoryFileSystem: true });
262262

263263
const component = `
@@ -297,7 +297,7 @@ describe("migrateComponents", () => {
297297
dedent(component),
298298
);
299299

300-
migrateComponents(project, { dryRun: false });
300+
await migrateComponents(project, { dryRun: false });
301301

302302
expect(dedent(componentSourceFile.getText())).toBe(
303303
dedent(`

packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { saveFileChanges } from "../../utils/log-utils";
3434
export const migrateComponents = async (
3535
project: Project,
3636
cliOptions: CliOptions,
37+
dir: string
3738
) => {
3839
for (const sourceFile of project.getSourceFiles()) {
3940
if (sourceFile.getFilePath().endsWith(".html")) {
@@ -60,6 +61,7 @@ export const migrateComponents = async (
6061
hasRouterLink,
6162
hasRouterLinkWithHref,
6263
cliOptions,
64+
dir,
6365
);
6466

6567
await saveFileChanges(tsSourceFile, cliOptions);
@@ -87,6 +89,7 @@ export const migrateComponents = async (
8789
hasRouterLink,
8890
hasRouterLinkWithHref,
8991
cliOptions,
92+
dir,
9093
);
9194

9295
if (ionicComponents.length > 0 || ionIcons.length > 0) {
@@ -105,11 +108,12 @@ async function migrateAngularComponentClass(
105108
hasRouterLink: boolean,
106109
hasRouterLinkWithHref: boolean,
107110
cliOptions: CliOptions,
111+
dir: string
108112
) {
109113
let ngModuleSourceFile: SourceFile | undefined;
110114
let modifiedNgModule = false;
111115

112-
if (!isAngularComponentStandalone(sourceFile)) {
116+
if (!await isAngularComponentStandalone(sourceFile, dir)) {
113117
ngModuleSourceFile = findNgModuleClassForComponent(sourceFile);
114118
}
115119

@@ -131,7 +135,7 @@ async function migrateAngularComponentClass(
131135

132136
if (hasRouterLink) {
133137
addImportToClass(sourceFile, "IonRouterLink", "@ionic/angular/standalone");
134-
addImportToComponentDecorator(sourceFile, "IonRouterLink");
138+
await addImportToComponentDecorator(sourceFile, "IonRouterLink", dir);
135139
}
136140

137141
if (hasRouterLinkWithHref) {
@@ -140,14 +144,14 @@ async function migrateAngularComponentClass(
140144
"IonRouterLinkWithHref",
141145
"@ionic/angular/standalone",
142146
);
143-
addImportToComponentDecorator(sourceFile, "IonRouterLinkWithHref");
147+
await addImportToComponentDecorator(sourceFile, "IonRouterLinkWithHref", dir);
144148
}
145149

146150
for (const ionicComponent of ionicComponents) {
147-
if (isAngularComponentStandalone(sourceFile)) {
151+
if (await isAngularComponentStandalone(sourceFile, dir)) {
148152
const componentClassName = kebabCaseToPascalCase(ionicComponent);
149-
addImportToComponentDecorator(sourceFile, componentClassName);
150-
removeImportFromComponentDecorator(sourceFile, "IonicModule");
153+
await addImportToComponentDecorator(sourceFile, componentClassName, dir);
154+
await removeImportFromComponentDecorator(sourceFile, "IonicModule", dir);
151155
removeImportFromClass(sourceFile, "IonicModule", "@ionic/angular");
152156
addImportToClass(
153157
sourceFile,

packages/cli/src/angular/migrations/standalone/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const runStandaloneMigration = async ({
4747
// Migrate standalone projects using bootstrapApplication
4848
await migrateBootstrapApplication(project, cliOptions);
4949
// Migrate components using Ionic components
50-
await migrateComponents(project, cliOptions);
50+
await migrateComponents(project, cliOptions, dir);
5151
// Migrate import statements to @ionic/angular/standalone
5252
await migrateImportStatements(project, cliOptions);
5353
// Migrate the assets array in angular.json

packages/cli/src/angular/utils/angular-utils.test.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22

33
import {
44
getAngularNgModuleDecorator,
@@ -7,9 +7,14 @@ import {
77
isAngularComponentStandalone,
88
} from "./angular-utils";
99
import { Project } from "ts-morph";
10+
import { getActualPackageVersion } from "./package-utils";
1011

1112
import { dedent } from "ts-dedent";
1213

14+
vi.mock("./package-utils", () => ({
15+
getActualPackageVersion: vi.fn(),
16+
}));
17+
1318
describe("getAngularNgModuleDecorator", () => {
1419
it("should return the NgModule decorator", () => {
1520
const sourceFileContent = `
@@ -115,7 +120,7 @@ describe("isAngularComponentClass", () => {
115120
});
116121

117122
describe("isAngularComponentStandalone", () => {
118-
it("should return true if the component has standalone: true", () => {
123+
it("should return true if the component has standalone: true", async () => {
119124
const sourceFileContent = `
120125
import { Component } from '@angular/core';
121126
@@ -130,10 +135,10 @@ describe("isAngularComponentStandalone", () => {
130135
const project = new Project({ useInMemoryFileSystem: true });
131136
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
132137

133-
expect(isAngularComponentStandalone(sourceFile)).toBe(true);
138+
expect(await isAngularComponentStandalone(sourceFile)).toBe(true);
134139
});
135140

136-
it("should return false if the component has standalone: false", () => {
141+
it("should return false if the component has standalone: false", async () => {
137142
const sourceFileContent = `
138143
import { Component } from '@angular/core';
139144
@@ -148,10 +153,10 @@ describe("isAngularComponentStandalone", () => {
148153
const project = new Project({ useInMemoryFileSystem: true });
149154
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
150155

151-
expect(isAngularComponentStandalone(sourceFile)).toBe(false);
156+
expect(await isAngularComponentStandalone(sourceFile)).toBe(false);
152157
});
153158

154-
it("should return false if the component does not have the standalone flag", () => {
159+
it("should return false if the component does not have the standalone flag", async () => {
155160
const sourceFileContent = `
156161
import { Component } from '@angular/core';
157162
@@ -165,6 +170,66 @@ describe("isAngularComponentStandalone", () => {
165170
const project = new Project({ useInMemoryFileSystem: true });
166171
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
167172

168-
expect(isAngularComponentStandalone(sourceFile)).toBe(false);
173+
expect(await isAngularComponentStandalone(sourceFile)).toBe(false);
174+
});
175+
176+
describe("with Angular version check", () => {
177+
it("should return true for Angular 19+ even without standalone flag", async () => {
178+
const sourceFileContent = `
179+
import { Component } from '@angular/core';
180+
181+
@Component({
182+
selector: 'my-component',
183+
template: ''
184+
})
185+
export class MyComponent {}
186+
`;
187+
188+
const project = new Project({ useInMemoryFileSystem: true });
189+
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
190+
191+
vi.mocked(getActualPackageVersion).mockResolvedValue("19.0.0");
192+
193+
expect(await isAngularComponentStandalone(sourceFile, "/test/dir")).toBe(true);
194+
});
195+
196+
it("should return false for Angular 18 even without standalone flag", async () => {
197+
const sourceFileContent = `
198+
import { Component } from '@angular/core';
199+
200+
@Component({
201+
selector: 'my-component',
202+
template: ''
203+
})
204+
export class MyComponent {}
205+
`;
206+
207+
const project = new Project({ useInMemoryFileSystem: true });
208+
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
209+
210+
vi.mocked(getActualPackageVersion).mockResolvedValue("18.0.0");
211+
212+
expect(await isAngularComponentStandalone(sourceFile, "/test/dir")).toBe(false);
213+
});
214+
215+
it("should check standalone flag when Angular version cannot be determined", async () => {
216+
const sourceFileContent = `
217+
import { Component } from '@angular/core';
218+
219+
@Component({
220+
selector: 'my-component',
221+
template: '',
222+
standalone: true
223+
})
224+
export class MyComponent {}
225+
`;
226+
227+
const project = new Project({ useInMemoryFileSystem: true });
228+
const sourceFile = project.createSourceFile("foo.ts", sourceFileContent);
229+
230+
vi.mocked(getActualPackageVersion).mockResolvedValue(null);
231+
232+
expect(await isAngularComponentStandalone(sourceFile, "/test/dir")).toBe(true);
233+
});
169234
});
170235
});

packages/cli/src/angular/utils/angular-utils.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getDecoratorArgument,
55
insertIntoDecoratorArgArray,
66
} from "./decorator-utils";
7+
import { getActualPackageVersion } from "./package-utils";
78

89
/**
910
* Finds the NgModule class that declares a given component.
@@ -103,12 +104,14 @@ export function findComponentTypescriptFileForTemplateFile(
103104
* Adds a new import to the imports array in the Component decorator.
104105
* @param sourceFile The source file to add the import to.
105106
* @param importName The name of the import to add.
107+
* @param dir The directory of the project.
106108
*/
107-
export function addImportToComponentDecorator(
109+
export async function addImportToComponentDecorator(
108110
sourceFile: SourceFile,
109111
importName: string,
112+
dir: string,
110113
) {
111-
if (!isAngularComponentStandalone(sourceFile)) {
114+
if (!(await isAngularComponentStandalone(sourceFile, dir))) {
112115
console.warn(
113116
"[Ionic Dev] Cannot add import to component decorator. Component is not standalone.",
114117
);
@@ -126,12 +129,14 @@ export function addImportToComponentDecorator(
126129
* Removes an import from the imports array in the Component decorator.
127130
* @param sourceFile The source file to remove the import from.
128131
* @param importName The name of the import to remove.
132+
* @param dir The directory of the project.
129133
*/
130-
export function removeImportFromComponentDecorator(
134+
export async function removeImportFromComponentDecorator(
131135
sourceFile: SourceFile,
132136
importName: string,
137+
dir: string,
133138
) {
134-
if (!isAngularComponentStandalone(sourceFile)) {
139+
if (!(await isAngularComponentStandalone(sourceFile, dir))) {
135140
console.warn(
136141
"[Ionic Dev] Cannot remove import from component decorator. Component is not standalone.",
137142
);
@@ -183,8 +188,10 @@ export const removeImportFromNgModuleDecorator = (
183188
* Checks if the source file is an Angular component using
184189
* the standalone: true option in the @Component decorator.
185190
* @param sourceFile The source file to check.
191+
* @param dir The directory of the project.
192+
* Since many tests do not specify dir, we have prepared dir = undefined.
186193
*/
187-
export function isAngularComponentStandalone(sourceFile: SourceFile) {
194+
export async function isAngularComponentStandalone(sourceFile: SourceFile, dir: string | undefined = undefined): Promise<boolean> {
188195
if (!isAngularComponentClass(sourceFile)) {
189196
return false;
190197
}
@@ -194,12 +201,29 @@ export function isAngularComponentStandalone(sourceFile: SourceFile) {
194201
return false;
195202
}
196203

204+
const standaloneDefault = await (async () => {
205+
if (dir) {
206+
const angularCoreVersion = await getActualPackageVersion(
207+
dir,
208+
"@angular/core"
209+
);
210+
211+
if (angularCoreVersion) {
212+
const [major] = angularCoreVersion.split(".");
213+
if (parseInt(major) >= 19) {
214+
return true;
215+
}
216+
}
217+
}
218+
return false;
219+
})();
220+
197221
const standalonePropertyAssignment = getDecoratorArgument(
198222
componentDecorator,
199223
"standalone",
200224
);
201225
if (!standalonePropertyAssignment) {
202-
return false;
226+
return standaloneDefault;
203227
}
204228

205229
const standalonePropertyValue =

0 commit comments

Comments
 (0)