Skip to content

Commit 418a1ba

Browse files
Broccohansl
authored andcommitted
fix(@schematics/angular): App shell requires router-outlet
Fixes #298
1 parent 69a993b commit 418a1ba

File tree

2 files changed

+111
-96
lines changed

2 files changed

+111
-96
lines changed

packages/schematics/angular/app-shell/index.ts

+87-67
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,94 @@ function getServerModulePath(host: Tree, app: AppConfig): string | null {
6262

6363
return modulePath;
6464
}
65+
66+
interface TemplateInfo {
67+
templateProp?: ts.PropertyAssignment;
68+
templateUrlProp?: ts.PropertyAssignment;
69+
}
70+
71+
function getComponentTemplateInfo(host: Tree, componentPath: string): TemplateInfo {
72+
const compSource = getSourceFile(host, componentPath);
73+
const compMetadata = getDecoratorMetadata(compSource, 'Component', '@angular/core')[0];
74+
75+
return {
76+
templateProp: getMetadataProperty(compMetadata, 'template'),
77+
templateUrlProp: getMetadataProperty(compMetadata, 'templateUrl'),
78+
};
79+
}
80+
81+
function getComponentTemplate(host: Tree, compPath: string, tmplInfo: TemplateInfo): string {
82+
let template = '';
83+
84+
if (tmplInfo.templateProp) {
85+
template = tmplInfo.templateProp.getFullText();
86+
} else if (tmplInfo.templateUrlProp) {
87+
const templateUrl = (tmplInfo.templateUrlProp.initializer as ts.StringLiteral).text;
88+
const dirEntry = host.getDir(compPath);
89+
const dir = dirEntry.parent ? dirEntry.parent.path : '/';
90+
const templatePath = normalize(`/${dir}/${templateUrl}`);
91+
const buffer = host.read(templatePath);
92+
if (buffer) {
93+
template = buffer.toString();
94+
}
95+
}
96+
97+
return template;
98+
}
99+
100+
function getBootstrapComponentPath(host: Tree, appConfig: AppConfig): string {
101+
const modulePath = getAppModulePath(host, appConfig);
102+
const moduleSource = getSourceFile(host, modulePath);
103+
104+
const metadataNode = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core')[0];
105+
const bootstrapProperty = getMetadataProperty(metadataNode, 'bootstrap');
106+
107+
const arrLiteral = (<ts.PropertyAssignment> bootstrapProperty)
108+
.initializer as ts.ArrayLiteralExpression;
109+
110+
const componentSymbol = arrLiteral.elements[0].getText();
111+
112+
const relativePath = getSourceNodes(moduleSource)
113+
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
114+
.filter(imp => {
115+
return findNode(imp, ts.SyntaxKind.Identifier, componentSymbol);
116+
})
117+
.map((imp: ts.ImportDeclaration) => {
118+
const pathStringLiteral = <ts.StringLiteral> imp.moduleSpecifier;
119+
120+
return pathStringLiteral.text;
121+
})[0];
122+
123+
const dirEntry = host.getDir(modulePath);
124+
const dir = dirEntry.parent ? dirEntry.parent.path : '/';
125+
const compPath = normalize(`/${dir}/${relativePath}.ts`);
126+
127+
return compPath;
128+
}
65129
// end helper functions.
66130

131+
function validateProject(options: AppShellOptions): Rule {
132+
return (host: Tree, context: SchematicContext) => {
133+
const routerOutletCheckRegex = /<router\-outlet.*?>([\s\S]*?)<\/router\-outlet>/;
134+
135+
const config = getConfig(host);
136+
const app = getAppFromConfig(config, options.clientApp || '0');
137+
if (app === null) {
138+
throw new SchematicsException(formatMissingAppMsg('Client', options.clientApp));
139+
}
140+
141+
const componentPath = getBootstrapComponentPath(host, app);
142+
const tmpl = getComponentTemplateInfo(host, componentPath);
143+
const template = getComponentTemplate(host, componentPath, tmpl);
144+
if (!routerOutletCheckRegex.test(template)) {
145+
const errorMsg =
146+
`Prerequisite for app shell is to define a router-outlet in your root component.`;
147+
context.logger.error(errorMsg);
148+
throw new SchematicsException(errorMsg);
149+
}
150+
};
151+
}
152+
67153
function addUniversalApp(options: AppShellOptions): Rule {
68154
return (host: Tree, context: SchematicContext) => {
69155
const config = getConfig(host);
@@ -153,72 +239,6 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper
153239
return property as ts.PropertyAssignment;
154240
}
155241

156-
function addRouterOutlet(options: AppShellOptions): Rule {
157-
return (host: Tree) => {
158-
const routerOutletMarkup = `<router-outlet></router-outlet>`;
159-
160-
const config = getConfig(host);
161-
const app = getAppFromConfig(config, options.clientApp || '0');
162-
if (app === null) {
163-
throw new SchematicsException(formatMissingAppMsg('Client', options.clientApp));
164-
}
165-
const modulePath = getAppModulePath(host, app);
166-
const moduleSource = getSourceFile(host, modulePath);
167-
168-
const metadataNode = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core')[0];
169-
const bootstrapProperty = getMetadataProperty(metadataNode, 'bootstrap');
170-
171-
const arrLiteral = (<ts.PropertyAssignment> bootstrapProperty)
172-
.initializer as ts.ArrayLiteralExpression;
173-
174-
const componentSymbol = arrLiteral.elements[0].getText();
175-
176-
const relativePath = getSourceNodes(moduleSource)
177-
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
178-
.filter(imp => {
179-
return findNode(imp, ts.SyntaxKind.Identifier, componentSymbol);
180-
})
181-
.map((imp: ts.ImportDeclaration) => {
182-
const pathStringLiteral = <ts.StringLiteral> imp.moduleSpecifier;
183-
184-
return pathStringLiteral.text;
185-
})[0];
186-
187-
const dirEntry = host.getDir(modulePath);
188-
const dir = dirEntry.parent ? dirEntry.parent.path : '/';
189-
const compPath = normalize(`/${dir}/${relativePath}.ts`);
190-
191-
const compSource = getSourceFile(host, compPath);
192-
const compMetadata = getDecoratorMetadata(compSource, 'Component', '@angular/core')[0];
193-
const templateProp = getMetadataProperty(compMetadata, 'template');
194-
const templateUrlProp = getMetadataProperty(compMetadata, 'templateUrl');
195-
196-
if (templateProp) {
197-
if (!/<router\-outlet>/.test(templateProp.initializer.getText())) {
198-
const recorder = host.beginUpdate(compPath);
199-
recorder.insertRight(templateProp.initializer.getEnd() - 1, routerOutletMarkup);
200-
host.commitUpdate(recorder);
201-
}
202-
} else {
203-
const templateUrl = (templateUrlProp.initializer as ts.StringLiteral).text;
204-
const dirEntry = host.getDir(compPath);
205-
const dir = dirEntry.parent ? dirEntry.parent.path : '/';
206-
const templatePath = normalize(`/${dir}/${templateUrl}`);
207-
const buffer = host.read(templatePath);
208-
if (buffer) {
209-
const content = buffer.toString();
210-
if (!/<router\-outlet>/.test(content)) {
211-
const recorder = host.beginUpdate(templatePath);
212-
recorder.insertRight(buffer.length, routerOutletMarkup);
213-
host.commitUpdate(recorder);
214-
}
215-
}
216-
}
217-
218-
return host;
219-
};
220-
}
221-
222242
function addServerRoutes(options: AppShellOptions): Rule {
223243
return (host: Tree) => {
224244
const config = getConfig(host);
@@ -293,10 +313,10 @@ function addShellComponent(options: AppShellOptions): Rule {
293313

294314
export default function (options: AppShellOptions): Rule {
295315
return chain([
316+
validateProject(options),
296317
addUniversalApp(options),
297318
addAppShellConfig(options),
298319
addRouterModule(options),
299-
addRouterOutlet(options),
300320
addServerRoutes(options),
301321
addShellComponent(options),
302322
]);

packages/schematics/angular/app-shell/index_spec.ts

+24-29
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,34 @@ describe('App Shell Schematic', () => {
2323
};
2424

2525
let appTree: Tree;
26+
const appOptions: ApplicationOptions = {
27+
directory: '',
28+
name: 'app',
29+
path: 'src',
30+
prefix: '',
31+
sourceDir: 'src',
32+
inlineStyle: false,
33+
inlineTemplate: false,
34+
viewEncapsulation: 'None',
35+
changeDetection: 'Default',
36+
version: '1.2.3',
37+
routing: true,
38+
style: 'css',
39+
skipTests: false,
40+
minimal: false,
41+
};
42+
2643
beforeEach(() => {
27-
const appOptions: ApplicationOptions = {
28-
directory: '',
29-
name: 'app',
30-
path: 'src',
31-
prefix: '',
32-
sourceDir: 'src',
33-
inlineStyle: false,
34-
inlineTemplate: false,
35-
viewEncapsulation: 'None',
36-
changeDetection: 'Default',
37-
version: '1.2.3',
38-
routing: false,
39-
style: 'css',
40-
skipTests: false,
41-
minimal: false,
42-
};
4344
appTree = schematicRunner.runSchematic('application', appOptions);
4445
});
4546

47+
it('should ensure the client app has a router-outlet', () => {
48+
appTree = schematicRunner.runSchematic('application', {...appOptions, routing: false});
49+
expect(() => {
50+
schematicRunner.runSchematic('appShell', defaultOptions, appTree);
51+
}).toThrowError();
52+
});
53+
4654
it('should add a universal app', () => {
4755
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
4856
const filePath = '/src/app/app.server.module.ts';
@@ -115,12 +123,6 @@ describe('App Shell Schematic', () => {
115123
tree.delete('/src/app/app.component.html');
116124
}
117125

118-
it('should add the router outlet (external template)', () => {
119-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
120-
const content = tree.read('/src/app/app.component.html') || new Buffer('');
121-
expect(content.toString()).toMatch(/<router\-outlet><\/router\-outlet>/g);
122-
});
123-
124126
it('should not re-add the router outlet (external template)', () => {
125127
const htmlPath = '/src/app/app.component.html';
126128
appTree.overwrite(htmlPath, '<router-outlet></router-outlet>');
@@ -132,13 +134,6 @@ describe('App Shell Schematic', () => {
132134
expect(numMatches).toEqual(1);
133135
});
134136

135-
it('should add the router outlet (inline template)', () => {
136-
makeInlineTemplate(appTree);
137-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
138-
const content = tree.read('/src/app/app.component.ts') || new Buffer('');
139-
expect(content.toString()).toMatch(/<router\-outlet><\/router\-outlet>/g);
140-
});
141-
142137
it('should not re-add the router outlet (inline template)', () => {
143138
makeInlineTemplate(appTree, '<router-outlet></router-outlet>');
144139
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);

0 commit comments

Comments
 (0)