Skip to content

Commit d0a9748

Browse files
authored
fix(schematics): ng add failed when call twice (#9171)
1 parent 4f5177c commit d0a9748

3 files changed

Lines changed: 162 additions & 35 deletions

File tree

schematics/ng-add/index.spec.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,18 +173,14 @@ describe('ng-add schematic', () => {
173173
expect(fileContent).toContain('registerLocaleData(zh)');
174174
});
175175

176-
/**
177-
* Test skip because it seems that it's not possible anymore to call the runSchematics method twice in the same test.
178-
* error: getStart of undefined
179-
*/
180-
xit('should not add locale id if locale id is set up', async () => {
176+
it('should not add locale id if locale id is set up', async () => {
181177
const options = { ...defaultOptions, i18n: 'zh_CN' };
182178
await runner.runSchematic('ng-add-setup-project', { ...defaultOptions }, appTree);
183179

184180
spyOn(console, 'log');
185181

186182
const tree = await runner.runSchematic('ng-add-setup-project', options, appTree);
187-
const fileContent = getFileContent(tree, '/projects/ng-zorro/src/app/app.module.ts');
183+
const fileContent = getFileContent(tree, '/projects/ng-zorro/src/app/app-module.ts');
188184

189185
expect(fileContent).toContain('provideNzI18n(en_US)');
190186
expect(fileContent).toContain('registerLocaleData(en)');

schematics/ng-add/setup-project/register-locale.ts

Lines changed: 159 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,75 @@ export function registerLocale(options: Schema): Rule {
4141
};
4242
}
4343

44+
/**
45+
* Safely creates an import change, returning NoopChange if the moduleSource is invalid
46+
* or if the import already exists.
47+
*/
48+
function safeInsertImport(moduleSource: ts.SourceFile | undefined, filePath: string, symbolName: string, fileName: string, isDefault = false): Change {
49+
if (!moduleSource) {
50+
console.log();
51+
console.log(yellow(`Could not insert import for ${symbolName} in file (${blue(filePath)}).`));
52+
console.log(yellow(`The source file is invalid.`));
53+
return new NoopChange();
54+
}
55+
56+
// Check if the import already exists
57+
const allImports = findNodes(moduleSource, ts.SyntaxKind.ImportDeclaration);
58+
if (!allImports) {
59+
return new NoopChange();
60+
}
61+
62+
const importExists = allImports.some(node => {
63+
// Make sure it's an import declaration
64+
if (!ts.isImportDeclaration(node)){
65+
return false;
66+
}
67+
68+
if (!node.moduleSpecifier) {
69+
return false;
70+
}
71+
72+
// Check if this import is from the same file
73+
if (!ts.isStringLiteral(node.moduleSpecifier)) {
74+
return false;
75+
}
76+
const importPath = node.moduleSpecifier.text;
77+
if (importPath !== fileName) {
78+
return false;
79+
}
80+
81+
// Check if the symbol is already imported
82+
if (!node.importClause) {
83+
return false;
84+
}
85+
const namedBindings = node.importClause.namedBindings;
86+
if (!namedBindings){
87+
return false;
88+
}
89+
90+
if (ts.isNamedImports(namedBindings)) {
91+
return namedBindings.elements.some(element =>
92+
element.name.text === symbolName
93+
);
94+
}
95+
96+
return false;
97+
});
98+
99+
if (importExists) {
100+
return new NoopChange();
101+
}
102+
103+
try {
104+
return insertImport(moduleSource, filePath, symbolName, fileName, isDefault);
105+
} catch (e) {
106+
console.log();
107+
console.log(yellow(`Could not insert import for ${symbolName} in file (${blue(filePath)}).`));
108+
console.log(yellow(`Error: ${e.message}`));
109+
return new NoopChange();
110+
}
111+
}
112+
44113
function registerLocaleInAppModule(mainFile: string, options: Schema): Rule {
45114
return async (host: Tree) => {
46115
const appModulePath = getAppModulePath(host, mainFile);
@@ -50,13 +119,13 @@ function registerLocaleInAppModule(mainFile: string, options: Schema): Rule {
50119
const localePrefix = locale.split('_')[0];
51120

52121
applyChangesToFile(host, appModulePath, [
53-
insertImport(moduleSource, appModulePath, 'provideNzI18n',
122+
safeInsertImport(moduleSource, appModulePath, 'provideNzI18n',
54123
'ng-zorro-antd/i18n'),
55-
insertImport(moduleSource, appModulePath, locale,
124+
safeInsertImport(moduleSource, appModulePath, locale,
56125
'ng-zorro-antd/i18n'),
57-
insertImport(moduleSource, appModulePath, 'registerLocaleData',
126+
safeInsertImport(moduleSource, appModulePath, 'registerLocaleData',
58127
'@angular/common'),
59-
insertImport(moduleSource, appModulePath, localePrefix,
128+
safeInsertImport(moduleSource, appModulePath, localePrefix,
60129
`@angular/common/locales/${localePrefix}`, true),
61130
registerLocaleData(moduleSource, appModulePath, localePrefix),
62131
...insertI18nTokenProvide(moduleSource, appModulePath, locale)
@@ -69,18 +138,40 @@ function registerLocaleInStandaloneApp(mainFile: string, options: Schema): Rule
69138

70139
return chain([
71140
async (host: Tree) => {
72-
const bootstrapCall = findBootstrapApplicationCall(host, mainFile);
73-
const appConfig = findAppConfig(bootstrapCall, host, mainFile);
74-
const appConfigFile = appConfig.filePath;
75-
const appConfigSource = parseSourceFile(host, appConfig.filePath);
76-
const localePrefix = locale.split('_')[0];
77-
78-
applyChangesToFile(host, appConfigFile, [
79-
insertImport(appConfigSource, appConfigFile, locale, 'ng-zorro-antd/i18n'),
80-
insertImport(appConfigSource, appConfigFile, 'registerLocaleData', '@angular/common'),
81-
insertImport(appConfigSource, appConfigFile, localePrefix, `@angular/common/locales/${localePrefix}`, true),
82-
registerLocaleData(appConfigSource, appConfigFile, localePrefix)
83-
]);
141+
try {
142+
const bootstrapCall = findBootstrapApplicationCall(host, mainFile);
143+
if (!bootstrapCall) {
144+
console.log();
145+
console.log(yellow(`Could not find bootstrap application call in file (${blue(mainFile)}).`));
146+
return void 0;
147+
}
148+
149+
const appConfig = findAppConfig(bootstrapCall, host, mainFile);
150+
if (!appConfig || !appConfig.filePath) {
151+
console.log();
152+
console.log(yellow(`Could not find app config in file (${blue(mainFile)}).`));
153+
return void 0;
154+
}
155+
156+
const appConfigFile = appConfig.filePath;
157+
const appConfigSource = parseSourceFile(host, appConfig.filePath);
158+
if (!appConfigSource) {
159+
console.log();
160+
console.log(yellow(`Could not parse app config file (${blue(appConfigFile)}).`));
161+
return void 0;
162+
}
163+
164+
const localePrefix = locale.split('_')[0];
165+
166+
applyChangesToFile(host, appConfigFile, [
167+
safeInsertImport(appConfigSource, appConfigFile, locale, 'ng-zorro-antd/i18n'),
168+
safeInsertImport(appConfigSource, appConfigFile, 'registerLocaleData', '@angular/common'),
169+
safeInsertImport(appConfigSource, appConfigFile, localePrefix, `@angular/common/locales/${localePrefix}`, true),
170+
registerLocaleData(appConfigSource, appConfigFile, localePrefix)
171+
]);
172+
} catch (e) {
173+
console.log(yellow(`Error registering locale in standalone app: ${e.message}`));
174+
}
84175
},
85176
addRootProvider(options.project, ({ code, external }) => {
86177
return code`${external('provideNzI18n', 'ng-zorro-antd/i18n')}(${locale})`;
@@ -92,16 +183,41 @@ function registerLocaleData(moduleSource: ts.SourceFile, modulePath: string, loc
92183
const allImports = findNodes(moduleSource, ts.SyntaxKind.ImportDeclaration);
93184
const allFun = findNodes(moduleSource, ts.SyntaxKind.ExpressionStatement);
94185

186+
// Check if allImports is valid before proceeding
187+
if (!allImports || allImports.length === 0) {
188+
console.log(yellow(`Could not add the registerLocaleData to file (${blue(modulePath)}).` +
189+
`because no import declarations were found.`));
190+
console.log(yellow(`Please manually add the following code:`));
191+
console.log(cyan(`registerLocaleData(${locale});`));
192+
return new NoopChange();
193+
}
194+
195+
// Safely filter the expression statements
95196
const registerLocaleDataFun = allFun.filter(node => {
96-
const fun = node.getChildren();
97-
return fun[0].getChildren()[0]?.getText() === 'registerLocaleData';
197+
if (!node) return false;
198+
const children = node.getChildren();
199+
if (!children || children.length === 0){
200+
return false;
201+
}
202+
const firstChild = children[0];
203+
if (!firstChild) {
204+
return false;
205+
}
206+
const firstChildChildren = firstChild.getChildren();
207+
if (!firstChildChildren || firstChildChildren.length === 0) {
208+
return false;
209+
}
210+
const firstChildFirstChild = firstChildChildren[0];
211+
if (!firstChildFirstChild) {
212+
return false;
213+
}
214+
return firstChildFirstChild.getText() === 'registerLocaleData';
98215
});
99216

100217
if (registerLocaleDataFun.length === 0) {
101218
return insertAfterLastOccurrence(allImports, `\n\nregisterLocaleData(${locale});`,
102219
modulePath, 0) as InsertChange;
103220
} else {
104-
console.log();
105221
console.log(yellow(`Could not add the registerLocaleData to file (${blue(modulePath)}).` +
106222
`because there is already a registerLocaleData function.`));
107223
console.log(yellow(`Please manually add the following code:`));
@@ -113,6 +229,15 @@ function registerLocaleData(moduleSource: ts.SourceFile, modulePath: string, loc
113229
function insertI18nTokenProvide(moduleSource: ts.SourceFile, modulePath: string, locale: string): Change[] {
114230
const metadataField = 'providers';
115231
const nodes = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core');
232+
233+
// Check if nodes are valid
234+
if (!nodes || nodes.length === 0) {
235+
console.log(yellow(`Could not find NgModule decorator in file (${blue(modulePath)}).`));
236+
console.log(yellow(`Please manually add the following code to your providers:`));
237+
console.log(cyan(`provideNzI18n(${locale})`));
238+
return [];
239+
}
240+
116241
const addProvide = addSymbolToNgModuleMetadata(
117242
moduleSource,
118243
modulePath,
@@ -127,10 +252,19 @@ function insertI18nTokenProvide(moduleSource: ts.SourceFile, modulePath: string,
127252
return [];
128253
}
129254

255+
// Check if a node has properties
256+
if (!node.properties) {
257+
console.log(yellow(`Could not find properties in NgModule decorator in file (${blue(modulePath)}).`));
258+
console.log(yellow(`Please manually add the following code to your providers:`));
259+
console.log(cyan(`provideNzI18n(${locale})`));
260+
return [];
261+
}
262+
130263
const matchingProperties: ts.ObjectLiteralElement[] =
131264
(node as ts.ObjectLiteralExpression).properties
132-
.filter(prop => prop.kind === ts.SyntaxKind.PropertyAssignment)
265+
.filter(prop => prop && prop.kind === ts.SyntaxKind.PropertyAssignment)
133266
.filter((prop: ts.PropertyAssignment) => {
267+
if (!prop || !prop.name) return false;
134268
const name = prop.name;
135269
switch (name.kind) {
136270
case ts.SyntaxKind.Identifier:
@@ -148,15 +282,16 @@ function insertI18nTokenProvide(moduleSource: ts.SourceFile, modulePath: string,
148282

149283
if (matchingProperties.length) {
150284
const assignment = matchingProperties[0] as ts.PropertyAssignment;
151-
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
285+
if (!assignment || !assignment.initializer || assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
152286
return [];
153287
}
154288
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
155-
if (arrLiteral.elements.length === 0) {
289+
if (!arrLiteral.elements || arrLiteral.elements.length === 0) {
156290
return addProvide;
157291
} else {
158-
const provideWithToken = arrLiteral.elements.some(e => e.getText?.().includes('NZ_I18N'));
159-
const provideWithFunc = arrLiteral.elements.some(e => e.getText?.().includes('provideNzI18n'));
292+
// Safely check for getText method before calling it
293+
const provideWithToken = arrLiteral.elements.some(e => e && typeof e.getText === 'function' && e.getText().includes('NZ_I18N'));
294+
const provideWithFunc = arrLiteral.elements.some(e => e && typeof e.getText === 'function' && e.getText().includes('provideNzI18n'));
160295

161296
if (!provideWithFunc && !provideWithToken) {
162297
return addProvide;

schematics/ng-add/standalone.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,7 @@ describe('[standalone] ng-add schematic', () => {
173173
expect(fileContent).toContain('registerLocaleData(zh)');
174174
});
175175

176-
/**
177-
* Test skip because it seems that it's not possible anymore to call the runSchematics method twice in the same test.
178-
* error: getStart of undefined
179-
*/
180-
xit('should not add locale id if locale id is set up', async () => {
176+
it('should not add locale id if locale id is set up', async () => {
181177
const options = { ...defaultOptions, i18n: 'zh_CN' };
182178
await runner.runSchematic('ng-add-setup-project', { ...defaultOptions }, appTree);
183179

0 commit comments

Comments
 (0)