Skip to content

Commit 597af7f

Browse files
committed
fix(@schematics/angular): infer app component name and path in server schematic
Currently the `server` schematic assumes that the app component is called `App` and it's places in `./app/app`. This will fail if the user renamed it or moved it to a different file. These changes add a utility function to resolve the component name and path from the source the source code, and they use the new function to produce a more accurate result.
1 parent d8a5647 commit 597af7f

File tree

4 files changed

+113
-3
lines changed

4 files changed

+113
-3
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bootstrapApplication } from '@angular/platform-browser';
2-
import { App } from './app/app';
2+
import { <%= appComponentName %> } from '<%= appComponentPath %>';
33
import { config } from './app/app.config.server';
44

5-
const bootstrap = () => bootstrapApplication(App, config);
5+
const bootstrap = () => bootstrapApplication(<%= appComponentName %>, config);
66

77
export default bootstrap;

packages/schematics/angular/server/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { latestVersions } from '../utility/latest-versions';
2727
import { isStandaloneApp } from '../utility/ng-ast-utils';
2828
import { relativePathToWorkspaceRoot } from '../utility/paths';
2929
import { isUsingApplicationBuilder, targetBuildNotFoundError } from '../utility/project-targets';
30-
import { getMainFilePath } from '../utility/standalone/util';
30+
import { findBootstrappedComponent, getMainFilePath } from '../utility/standalone/util';
3131
import { getWorkspace, updateWorkspace } from '../utility/workspace';
3232
import { Builders } from '../utility/workspace-models';
3333
import { Schema as ServerOptions } from './schema';
@@ -187,10 +187,16 @@ export default function (options: ServerOptions): Rule {
187187
let filesUrl = `./files/${usingApplicationBuilder ? 'application-builder/' : 'server-builder/'}`;
188188
filesUrl += isStandalone ? 'standalone-src' : 'ngmodule-src';
189189

190+
const bootstrappedComponent = findBootstrappedComponent(host, browserEntryPoint) || {
191+
name: 'App',
192+
importPathWithinMain: './app/app',
193+
};
190194
const templateSource = apply(url(filesUrl), [
191195
applyTemplates({
192196
...strings,
193197
...options,
198+
appComponentName: bootstrappedComponent.name,
199+
appComponentPath: bootstrappedComponent.importPathWithinMain,
194200
}),
195201
move(sourceRoot),
196202
]);

packages/schematics/angular/server/index_spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,48 @@ describe('Server Schematic', () => {
127127
expect(contents).toContain(`bootstrapApplication(App, config)`);
128128
});
129129

130+
it('should account for renamed app component', async () => {
131+
appTree.overwrite(
132+
'/projects/bar/src/main.ts',
133+
`
134+
import { bootstrapApplication } from '@angular/platform-browser';
135+
import { appConfig } from './app/app.config';
136+
import { MyCustomApp } from './foo/bar/baz/app.foo';
137+
138+
bootstrapApplication(MyCustomApp, appConfig)
139+
.catch((err) => console.error(err));
140+
`,
141+
);
142+
143+
const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree);
144+
const filePath = '/projects/bar/src/main.server.ts';
145+
expect(tree.exists(filePath)).toBeTrue();
146+
const contents = tree.readContent(filePath);
147+
expect(contents).toContain(`import { MyCustomApp } from './foo/bar/baz/app.foo';`);
148+
expect(contents).toContain(`bootstrapApplication(MyCustomApp, config)`);
149+
});
150+
151+
it('should account for renamed app component that is aliased within the main file', async () => {
152+
appTree.overwrite(
153+
'/projects/bar/src/main.ts',
154+
`
155+
import { bootstrapApplication } from '@angular/platform-browser';
156+
import { appConfig } from './app/app.config';
157+
import { MyCustomApp as MyCustomAlias } from './foo/bar/baz/app.foo';
158+
159+
bootstrapApplication(MyCustomAlias, appConfig)
160+
.catch((err) => console.error(err));
161+
`,
162+
);
163+
164+
const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree);
165+
const filePath = '/projects/bar/src/main.server.ts';
166+
expect(tree.exists(filePath)).toBeTrue();
167+
const contents = tree.readContent(filePath);
168+
expect(contents).toContain(`import { MyCustomApp } from './foo/bar/baz/app.foo';`);
169+
expect(contents).toContain(`bootstrapApplication(MyCustomApp, config)`);
170+
});
171+
130172
it('should create server app config file', async () => {
131173
const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree);
132174
const filePath = '/projects/bar/src/app/app.config.server.ts';

packages/schematics/angular/utility/standalone/util.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,68 @@ export function findBootstrapApplicationCall(tree: Tree, mainFilePath: string):
8181
throw new SchematicsException(`Could not find bootstrapApplication call in ${mainFilePath}`);
8282
}
8383

84+
/**
85+
* Finds the original name and path relative to the `main.ts` of the bootrstrapped app component.
86+
* @param tree File tree in which to look for the component.
87+
* @param mainFilePath Path of the `main` file.
88+
*/
89+
export function findBootstrappedComponent(
90+
tree: Tree,
91+
mainFilePath: string,
92+
): {
93+
name: string;
94+
importPathWithinMain: string;
95+
} | null {
96+
let bootstrapCall: ts.CallExpression | null = null;
97+
98+
try {
99+
bootstrapCall = findBootstrapApplicationCall(tree, mainFilePath);
100+
} catch (e) {
101+
// `findBootstrapApplicationCall` will throw if it can't find the `bootrstrapApplication` call.
102+
// Handle it gracefully by returning `null` instead so the consumer can recover.
103+
if (!(e instanceof SchematicsException)) {
104+
throw e;
105+
}
106+
}
107+
108+
if (
109+
bootstrapCall === null ||
110+
bootstrapCall.arguments.length < 1 ||
111+
!ts.isIdentifier(bootstrapCall.arguments[0])
112+
) {
113+
return null;
114+
}
115+
116+
const name = bootstrapCall.arguments[0].text;
117+
const sourceFile = bootstrapCall.getSourceFile();
118+
119+
// Try to resolve the import path by looking at the top-level named imports of the file.
120+
for (const node of sourceFile.statements) {
121+
if (
122+
!ts.isImportDeclaration(node) ||
123+
!ts.isStringLiteral(node.moduleSpecifier) ||
124+
!node.importClause ||
125+
!node.importClause.namedBindings ||
126+
!ts.isNamedImports(node.importClause.namedBindings)
127+
) {
128+
continue;
129+
}
130+
131+
for (const element of node.importClause.namedBindings.elements) {
132+
if (element.name.text === name) {
133+
return {
134+
// Note that we use `propertyName` if available, because it contains
135+
// the real name in the case where the import is aliased.
136+
name: (element.propertyName || element.name).text,
137+
importPathWithinMain: node.moduleSpecifier.text,
138+
};
139+
}
140+
}
141+
}
142+
143+
return null;
144+
}
145+
84146
/**
85147
* Finds the local name of an imported symbol. Could be the symbol name itself or its alias.
86148
* @param sourceFile File within which to search for the import.

0 commit comments

Comments
 (0)