index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. /**
  4. * @license
  5. * Copyright Google Inc. All Rights Reserved.
  6. *
  7. * Use of this source code is governed by an MIT-style license that can be
  8. * found in the LICENSE file at https://angular.io/license
  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. // end helper functions.
  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. // Copy options.
  99. const universalOptions = {
  100. ...options,
  101. };
  102. // Delete non-universal options.
  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. // The workspace gets updated so this needs to be reloaded
  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;