1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | const core_1 = require("@angular-devkit/core");
|
11 | const schematics_1 = require("@angular-devkit/schematics");
|
12 | const ts = require("../third_party/github.com/Microsoft/TypeScript/lib/typescript");
|
13 | const ast_utils_1 = require("../utility/ast-utils");
|
14 | const change_1 = require("../utility/change");
|
15 | const ng_ast_utils_1 = require("../utility/ng-ast-utils");
|
16 | const project_targets_1 = require("../utility/project-targets");
|
17 | const workspace_1 = require("../utility/workspace");
|
18 | const workspace_models_1 = require("../utility/workspace-models");
|
19 | function getSourceFile(host, path) {
|
20 | const buffer = host.read(path);
|
21 | if (!buffer) {
|
22 | throw new schematics_1.SchematicsException(`Could not find ${path}.`);
|
23 | }
|
24 | const content = buffer.toString();
|
25 | const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
26 | return source;
|
27 | }
|
28 | function getServerModulePath(host, sourceRoot, mainPath) {
|
29 | const mainSource = getSourceFile(host, core_1.join(core_1.normalize(sourceRoot), mainPath));
|
30 | const allNodes = ast_utils_1.getSourceNodes(mainSource);
|
31 | const expNode = allNodes.find(node => ts.isExportDeclaration(node));
|
32 | if (!expNode) {
|
33 | return null;
|
34 | }
|
35 | const relativePath = expNode.moduleSpecifier;
|
36 | const modulePath = core_1.normalize(`/${sourceRoot}/${relativePath.text}.ts`);
|
37 | return modulePath;
|
38 | }
|
39 | function getComponentTemplateInfo(host, componentPath) {
|
40 | const compSource = getSourceFile(host, componentPath);
|
41 | const compMetadata = ast_utils_1.getDecoratorMetadata(compSource, 'Component', '@angular/core')[0];
|
42 | return {
|
43 | templateProp: getMetadataProperty(compMetadata, 'template'),
|
44 | templateUrlProp: getMetadataProperty(compMetadata, 'templateUrl'),
|
45 | };
|
46 | }
|
47 | function getComponentTemplate(host, compPath, tmplInfo) {
|
48 | let template = '';
|
49 | if (tmplInfo.templateProp) {
|
50 | template = tmplInfo.templateProp.getFullText();
|
51 | }
|
52 | else if (tmplInfo.templateUrlProp) {
|
53 | const templateUrl = tmplInfo.templateUrlProp.initializer.text;
|
54 | const dir = core_1.dirname(core_1.normalize(compPath));
|
55 | const templatePath = core_1.join(dir, templateUrl);
|
56 | const buffer = host.read(templatePath);
|
57 | if (buffer) {
|
58 | template = buffer.toString();
|
59 | }
|
60 | }
|
61 | return template;
|
62 | }
|
63 | function getBootstrapComponentPath(host, mainPath) {
|
64 | const modulePath = ng_ast_utils_1.getAppModulePath(host, mainPath);
|
65 | const moduleSource = getSourceFile(host, modulePath);
|
66 | const metadataNode = ast_utils_1.getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core')[0];
|
67 | const bootstrapProperty = getMetadataProperty(metadataNode, 'bootstrap');
|
68 | const arrLiteral = bootstrapProperty
|
69 | .initializer;
|
70 | const componentSymbol = arrLiteral.elements[0].getText();
|
71 | const relativePath = ast_utils_1.getSourceNodes(moduleSource)
|
72 | .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
|
73 | .filter(imp => {
|
74 | return ast_utils_1.findNode(imp, ts.SyntaxKind.Identifier, componentSymbol);
|
75 | })
|
76 | .map((imp) => {
|
77 | const pathStringLiteral = imp.moduleSpecifier;
|
78 | return pathStringLiteral.text;
|
79 | })[0];
|
80 | return core_1.join(core_1.dirname(core_1.normalize(modulePath)), relativePath + '.ts');
|
81 | }
|
82 |
|
83 | function validateProject(mainPath) {
|
84 | return (host, context) => {
|
85 | const routerOutletCheckRegex = /<router\-outlet.*?>([\s\S]*?)<\/router\-outlet>/;
|
86 | const componentPath = getBootstrapComponentPath(host, mainPath);
|
87 | const tmpl = getComponentTemplateInfo(host, componentPath);
|
88 | const template = getComponentTemplate(host, componentPath, tmpl);
|
89 | if (!routerOutletCheckRegex.test(template)) {
|
90 | const errorMsg = `Prerequisite for app shell is to define a router-outlet in your root component.`;
|
91 | context.logger.error(errorMsg);
|
92 | throw new schematics_1.SchematicsException(errorMsg);
|
93 | }
|
94 | };
|
95 | }
|
96 | function addUniversalTarget(options) {
|
97 | return () => {
|
98 |
|
99 | const universalOptions = {
|
100 | ...options,
|
101 | };
|
102 |
|
103 | delete universalOptions.universalProject;
|
104 | delete universalOptions.route;
|
105 | delete universalOptions.name;
|
106 | delete universalOptions.outDir;
|
107 | delete universalOptions.root;
|
108 | delete universalOptions.index;
|
109 | delete universalOptions.sourceDir;
|
110 | return schematics_1.schematic('universal', universalOptions);
|
111 | };
|
112 | }
|
113 | function addAppShellConfigToWorkspace(options) {
|
114 | return () => {
|
115 | if (!options.route) {
|
116 | throw new schematics_1.SchematicsException(`Route is not defined`);
|
117 | }
|
118 | return workspace_1.updateWorkspace(workspace => {
|
119 | const project = workspace.projects.get(options.clientProject);
|
120 | if (!project) {
|
121 | return;
|
122 | }
|
123 | project.targets.add({
|
124 | name: 'app-shell',
|
125 | builder: workspace_models_1.Builders.AppShell,
|
126 | options: {
|
127 | browserTarget: `${options.clientProject}:build`,
|
128 | serverTarget: `${options.clientProject}:server`,
|
129 | route: options.route,
|
130 | },
|
131 | configurations: {
|
132 | production: {
|
133 | browserTarget: `${options.clientProject}:build:production`,
|
134 | serverTarget: `${options.clientProject}:server:production`,
|
135 | },
|
136 | },
|
137 | });
|
138 | });
|
139 | };
|
140 | }
|
141 | function addRouterModule(mainPath) {
|
142 | return (host) => {
|
143 | const modulePath = ng_ast_utils_1.getAppModulePath(host, mainPath);
|
144 | const moduleSource = getSourceFile(host, modulePath);
|
145 | const changes = ast_utils_1.addImportToModule(moduleSource, modulePath, 'RouterModule', '@angular/router');
|
146 | const recorder = host.beginUpdate(modulePath);
|
147 | changes.forEach((change) => {
|
148 | if (change instanceof change_1.InsertChange) {
|
149 | recorder.insertLeft(change.pos, change.toAdd);
|
150 | }
|
151 | });
|
152 | host.commitUpdate(recorder);
|
153 | return host;
|
154 | };
|
155 | }
|
156 | function getMetadataProperty(metadata, propertyName) {
|
157 | const properties = metadata.properties;
|
158 | const property = properties
|
159 | .filter(prop => prop.kind === ts.SyntaxKind.PropertyAssignment)
|
160 | .filter((prop) => {
|
161 | const name = prop.name;
|
162 | switch (name.kind) {
|
163 | case ts.SyntaxKind.Identifier:
|
164 | return name.getText() === propertyName;
|
165 | case ts.SyntaxKind.StringLiteral:
|
166 | return name.text === propertyName;
|
167 | }
|
168 | return false;
|
169 | })[0];
|
170 | return property;
|
171 | }
|
172 | function addServerRoutes(options) {
|
173 | return async (host) => {
|
174 |
|
175 | const workspace = await workspace_1.getWorkspace(host);
|
176 | const clientProject = workspace.projects.get(options.clientProject);
|
177 | if (!clientProject) {
|
178 | throw new Error('Universal schematic removed client project.');
|
179 | }
|
180 | const clientServerTarget = clientProject.targets.get('server');
|
181 | if (!clientServerTarget) {
|
182 | throw new Error('Universal schematic did not add server target to client project.');
|
183 | }
|
184 | const clientServerOptions = clientServerTarget.options;
|
185 | if (!clientServerOptions) {
|
186 | throw new schematics_1.SchematicsException('Server target does not contain options.');
|
187 | }
|
188 | const modulePath = getServerModulePath(host, clientProject.sourceRoot || 'src', options.main);
|
189 | if (modulePath === null) {
|
190 | throw new schematics_1.SchematicsException('Universal/server module not found.');
|
191 | }
|
192 | let moduleSource = getSourceFile(host, modulePath);
|
193 | if (!ast_utils_1.isImported(moduleSource, 'Routes', '@angular/router')) {
|
194 | const recorder = host.beginUpdate(modulePath);
|
195 | const routesChange = ast_utils_1.insertImport(moduleSource, modulePath, 'Routes', '@angular/router');
|
196 | if (routesChange.toAdd) {
|
197 | recorder.insertLeft(routesChange.pos, routesChange.toAdd);
|
198 | }
|
199 | const imports = ast_utils_1.getSourceNodes(moduleSource)
|
200 | .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
|
201 | .sort((a, b) => a.getStart() - b.getStart());
|
202 | const insertPosition = imports[imports.length - 1].getEnd();
|
203 | const routeText = `\n\nconst routes: Routes = [ { path: '${options.route}', component: AppShellComponent }];`;
|
204 | recorder.insertRight(insertPosition, routeText);
|
205 | host.commitUpdate(recorder);
|
206 | }
|
207 | moduleSource = getSourceFile(host, modulePath);
|
208 | if (!ast_utils_1.isImported(moduleSource, 'RouterModule', '@angular/router')) {
|
209 | const recorder = host.beginUpdate(modulePath);
|
210 | const routerModuleChange = ast_utils_1.insertImport(moduleSource, modulePath, 'RouterModule', '@angular/router');
|
211 | if (routerModuleChange.toAdd) {
|
212 | recorder.insertLeft(routerModuleChange.pos, routerModuleChange.toAdd);
|
213 | }
|
214 | const metadataChange = ast_utils_1.addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', 'RouterModule.forRoot(routes)');
|
215 | if (metadataChange) {
|
216 | metadataChange.forEach((change) => {
|
217 | recorder.insertRight(change.pos, change.toAdd);
|
218 | });
|
219 | }
|
220 | host.commitUpdate(recorder);
|
221 | }
|
222 | };
|
223 | }
|
224 | function addShellComponent(options) {
|
225 | const componentOptions = {
|
226 | name: 'app-shell',
|
227 | module: options.rootModuleFileName,
|
228 | project: options.clientProject,
|
229 | };
|
230 | return schematics_1.schematic('component', componentOptions);
|
231 | }
|
232 | function default_1(options) {
|
233 | return async (tree) => {
|
234 | const workspace = await workspace_1.getWorkspace(tree);
|
235 | const clientProject = workspace.projects.get(options.clientProject);
|
236 | if (!clientProject || clientProject.extensions.projectType !== 'application') {
|
237 | throw new schematics_1.SchematicsException(`A client project type of "application" is required.`);
|
238 | }
|
239 | const clientBuildTarget = clientProject.targets.get('build');
|
240 | if (!clientBuildTarget) {
|
241 | throw project_targets_1.targetBuildNotFoundError();
|
242 | }
|
243 | const clientBuildOptions = (clientBuildTarget.options || {});
|
244 | return schematics_1.chain([
|
245 | validateProject(clientBuildOptions.main),
|
246 | clientProject.targets.has('server') ? schematics_1.noop() : addUniversalTarget(options),
|
247 | addAppShellConfigToWorkspace(options),
|
248 | addRouterModule(clientBuildOptions.main),
|
249 | addServerRoutes(options),
|
250 | addShellComponent(options),
|
251 | ]);
|
252 | };
|
253 | }
|
254 | exports.default = default_1;
|