Skip to content

Commit bbe74b8

Browse files
alan-agius4clydin
authored andcommitted
fix(@schematics/angular): provide actionable error message when routing declaration cannot be found
Due to incorrect castings previously the code would crash when the module doesn't contain an routing module with the following error: ``` Cannot read property 'properties' of undefined ``` Closes #21397 (cherry picked from commit 7db433b)
1 parent c97c8e7 commit bbe74b8

File tree

3 files changed

+54
-40
lines changed

3 files changed

+54
-40
lines changed

packages/schematics/angular/utility/ast-utils.ts

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
189189

190190
export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null {
191191
if (node.kind === kind && node.getText() === text) {
192-
// throw new Error(node.getText());
193192
return node;
194193
}
195194

@@ -367,29 +366,28 @@ export function addSymbolToNgModuleMetadata(
367366
importPath: string | null = null,
368367
): Change[] {
369368
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
370-
let node: any = nodes[0]; // eslint-disable-line @typescript-eslint/no-explicit-any
369+
const node = nodes[0];
371370

372371
// Find the decorator declaration.
373-
if (!node) {
372+
if (!node || !ts.isObjectLiteralExpression(node)) {
374373
return [];
375374
}
376375

377376
// Get all the children property assignment of object literals.
378-
const matchingProperties = getMetadataField(node as ts.ObjectLiteralExpression, metadataField);
377+
const matchingProperties = getMetadataField(node, metadataField);
379378

380379
if (matchingProperties.length == 0) {
381380
// We haven't found the field in the metadata declaration. Insert a new field.
382-
const expr = node as ts.ObjectLiteralExpression;
383381
let position: number;
384382
let toInsert: string;
385-
if (expr.properties.length == 0) {
386-
position = expr.getEnd() - 1;
383+
if (node.properties.length == 0) {
384+
position = node.getEnd() - 1;
387385
toInsert = `\n ${metadataField}: [\n${tags.indentBy(4)`${symbolName}`}\n ]\n`;
388386
} else {
389-
node = expr.properties[expr.properties.length - 1];
390-
position = node.getEnd();
387+
const childNode = node.properties[node.properties.length - 1];
388+
position = childNode.getEnd();
391389
// Get the indentation of the last element, if any.
392-
const text = node.getFullText(source);
390+
const text = childNode.getFullText(source);
393391
const matches = text.match(/^(\r?\n)(\s*)/);
394392
if (matches) {
395393
toInsert =
@@ -408,47 +406,48 @@ export function addSymbolToNgModuleMetadata(
408406
return [new InsertChange(ngModulePath, position, toInsert)];
409407
}
410408
}
411-
const assignment = matchingProperties[0] as ts.PropertyAssignment;
409+
const assignment = matchingProperties[0];
412410

413411
// If it's not an array, nothing we can do really.
414-
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
412+
if (
413+
!ts.isPropertyAssignment(assignment) ||
414+
!ts.isArrayLiteralExpression(assignment.initializer)
415+
) {
415416
return [];
416417
}
417418

418-
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
419-
if (arrLiteral.elements.length == 0) {
420-
// Forward the property.
421-
node = arrLiteral;
422-
} else {
423-
node = arrLiteral.elements;
424-
}
419+
let expresssion: ts.Expression | ts.ArrayLiteralExpression;
420+
const assignmentInit = assignment.initializer;
421+
const elements = assignmentInit.elements;
425422

426-
if (Array.isArray(node)) {
427-
const nodeArray = (node as {}) as Array<ts.Node>;
428-
const symbolsArray = nodeArray.map((node) => tags.oneLine`${node.getText()}`);
423+
if (elements.length) {
424+
const symbolsArray = elements.map((node) => tags.oneLine`${node.getText()}`);
429425
if (symbolsArray.includes(tags.oneLine`${symbolName}`)) {
430426
return [];
431427
}
432428

433-
node = node[node.length - 1];
429+
expresssion = elements[elements.length - 1];
430+
} else {
431+
expresssion = assignmentInit;
434432
}
435433

436434
let toInsert: string;
437-
let position = node.getEnd();
438-
if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
435+
let position = expresssion.getEnd();
436+
if (ts.isArrayLiteralExpression(expresssion)) {
439437
// We found the field but it's empty. Insert it just before the `]`.
440438
position--;
441439
toInsert = `\n${tags.indentBy(4)`${symbolName}`}\n `;
442440
} else {
443441
// Get the indentation of the last element, if any.
444-
const text = node.getFullText(source);
442+
const text = expresssion.getFullText(source);
445443
const matches = text.match(/^(\r?\n)(\s*)/);
446444
if (matches) {
447445
toInsert = `,${matches[1]}${tags.indentBy(matches[2].length)`${symbolName}`}`;
448446
} else {
449447
toInsert = `, ${symbolName}`;
450448
}
451449
}
450+
452451
if (importPath !== null) {
453452
return [
454453
new InsertChange(ngModulePath, position, toInsert),
@@ -604,9 +603,12 @@ export function getEnvironmentExportName(source: ts.SourceFile): string | null {
604603
*/
605604
export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression | undefined {
606605
const result = getDecoratorMetadata(source, 'NgModule', '@angular/core');
607-
const node = result[0] as ts.ObjectLiteralExpression;
608-
const matchingProperties = getMetadataField(node, 'imports');
606+
const node = result[0];
607+
if (!node || !ts.isObjectLiteralExpression(node)) {
608+
return undefined;
609+
}
609610

611+
const matchingProperties = getMetadataField(node, 'imports');
610612
if (!matchingProperties) {
611613
return;
612614
}
@@ -634,7 +636,10 @@ export function addRouteDeclarationToModule(
634636
): Change {
635637
const routerModuleExpr = getRouterModuleDeclaration(source);
636638
if (!routerModuleExpr) {
637-
throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`);
639+
throw new Error(
640+
`Couldn't find a route declaration in ${fileToAdd}.\n` +
641+
`Use the '--module' option to specify a different routing module.`,
642+
);
638643
}
639644
const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression).arguments;
640645
if (!scopeConfigMethodArgs.length) {

packages/schematics/angular/utility/ast-utils_spec.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('ast utils', () => {
260260
const elements = (arrayNode.pop() as ts.ArrayLiteralExpression).elements;
261261

262262
const change = insertAfterLastOccurrence(
263-
(elements as unknown) as ts.Node[],
263+
elements as unknown as ts.Node[],
264264
`, 'bar'`,
265265
filePath,
266266
elements.pos,
@@ -281,7 +281,7 @@ describe('ast utils', () => {
281281
const elements = (arrayNode.pop() as ts.ArrayLiteralExpression).elements;
282282

283283
const change = insertAfterLastOccurrence(
284-
(elements as unknown) as ts.Node[],
284+
elements as unknown as ts.Node[],
285285
`'bar'`,
286286
filePath,
287287
elements.pos,
@@ -312,7 +312,7 @@ describe('ast utils', () => {
312312

313313
const source = getTsSource(modulePath, moduleContent);
314314
const change = () => addRouteDeclarationToModule(source, './src/app', '');
315-
expect(change).toThrowError(`Couldn't find a route declaration in ./src/app.`);
315+
expect(change).toThrowError(/Couldn't find a route declaration in \.\/src\/app/);
316316
});
317317

318318
it(`should throw an error when router module doesn't have arguments`, () => {
@@ -632,6 +632,17 @@ describe('ast utils', () => {
632632
/RouterModule\.forRoot\(\[\r?\n?\s*{ path: 'foo', component: FooComponent },\r?\n?\s*{ path: 'bar', component: BarComponent }\r?\n?\s*\]\)/,
633633
);
634634
});
635+
636+
it('should error if sourcefile is empty', () => {
637+
const change = () =>
638+
addRouteDeclarationToModule(
639+
getTsSource(modulePath, ''),
640+
'./src/app',
641+
`{ path: 'foo', component: FooComponent }`,
642+
);
643+
644+
expect(change).toThrowError(/Couldn't find a route declaration in \.\/src\/app/);
645+
});
635646
});
636647

637648
describe('findNodes', () => {

packages/schematics/angular/utility/find-module.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path
5454

5555
const candidatesDirs = [...candidateSet].sort((a, b) => b.length - a.length);
5656
for (const c of candidatesDirs) {
57-
const candidateFiles = [
58-
'',
59-
`${moduleBaseName}.ts`,
60-
`${moduleBaseName}${moduleExt}`,
61-
].map((x) => join(c, x));
57+
const candidateFiles = ['', `${moduleBaseName}.ts`, `${moduleBaseName}${moduleExt}`].map(
58+
(x) => join(c, x),
59+
);
6260

6361
for (const sc of candidateFiles) {
6462
if (host.exists(sc)) {
@@ -96,7 +94,7 @@ export function findModule(
9694
return join(dir.path, filteredMatches[0]);
9795
} else if (filteredMatches.length > 1) {
9896
throw new Error(
99-
'More than one module matches. Use the skip-import option to skip importing ' +
97+
`More than one module matches. Use the '--skip-import' option to skip importing ` +
10098
'the component into the closest module or use the module option to specify a module.',
10199
);
102100
}
@@ -107,8 +105,8 @@ export function findModule(
107105
const errorMsg = foundRoutingModule
108106
? 'Could not find a non Routing NgModule.' +
109107
`\nModules with suffix '${routingModuleExt}' are strictly reserved for routing.` +
110-
'\nUse the skip-import option to skip importing in NgModule.'
111-
: 'Could not find an NgModule. Use the skip-import option to skip importing in NgModule.';
108+
`\nUse the '--skip-import' option to skip importing in NgModule.`
109+
: `Could not find an NgModule. Use the '--skip-import' option to skip importing in NgModule.`;
112110

113111
throw new Error(errorMsg);
114112
}

0 commit comments

Comments
 (0)