index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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 tasks_1 = require("@angular-devkit/schematics/tasks");
  13. const ts = require("../third_party/github.com/Microsoft/TypeScript/lib/typescript");
  14. const ast_utils_1 = require("../utility/ast-utils");
  15. const change_1 = require("../utility/change");
  16. const dependencies_1 = require("../utility/dependencies");
  17. const ng_ast_utils_1 = require("../utility/ng-ast-utils");
  18. const project_targets_1 = require("../utility/project-targets");
  19. const workspace_1 = require("../utility/workspace");
  20. const workspace_models_1 = require("../utility/workspace-models");
  21. function updateConfigFile(options, tsConfigDirectory) {
  22. return workspace_1.updateWorkspace(workspace => {
  23. const clientProject = workspace.projects.get(options.clientProject);
  24. if (clientProject) {
  25. const buildTarget = clientProject.targets.get('build');
  26. let fileReplacements;
  27. if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) {
  28. fileReplacements = buildTarget.configurations.production.fileReplacements;
  29. }
  30. if (buildTarget && buildTarget.options) {
  31. buildTarget.options.outputPath = `dist/${options.clientProject}/browser`;
  32. }
  33. // In case the browser builder hashes the assets
  34. // we need to add this setting to the server builder
  35. // as otherwise when assets it will be requested twice.
  36. // One for the server which will be unhashed, and other on the client which will be hashed.
  37. let outputHashing;
  38. if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) {
  39. switch (buildTarget.configurations.production.outputHashing) {
  40. case 'all':
  41. case 'media':
  42. outputHashing = 'media';
  43. break;
  44. }
  45. }
  46. const mainPath = options.main;
  47. clientProject.targets.add({
  48. name: 'server',
  49. builder: workspace_models_1.Builders.Server,
  50. options: {
  51. outputPath: `dist/${options.clientProject}/server`,
  52. main: core_1.join(core_1.normalize(clientProject.root), 'src', mainPath.endsWith('.ts') ? mainPath : mainPath + '.ts'),
  53. tsConfig: core_1.join(tsConfigDirectory, `${options.tsconfigFileName}.json`),
  54. },
  55. configurations: {
  56. production: {
  57. outputHashing,
  58. fileReplacements,
  59. sourceMap: false,
  60. optimization: true,
  61. },
  62. },
  63. });
  64. }
  65. });
  66. }
  67. function findBrowserModuleImport(host, modulePath) {
  68. const moduleBuffer = host.read(modulePath);
  69. if (!moduleBuffer) {
  70. throw new schematics_1.SchematicsException(`Module file (${modulePath}) not found`);
  71. }
  72. const moduleFileText = moduleBuffer.toString('utf-8');
  73. const source = ts.createSourceFile(modulePath, moduleFileText, ts.ScriptTarget.Latest, true);
  74. const decoratorMetadata = ast_utils_1.getDecoratorMetadata(source, 'NgModule', '@angular/core')[0];
  75. const browserModuleNode = ast_utils_1.findNode(decoratorMetadata, ts.SyntaxKind.Identifier, 'BrowserModule');
  76. if (browserModuleNode === null) {
  77. throw new schematics_1.SchematicsException(`Cannot find BrowserModule import in ${modulePath}`);
  78. }
  79. return browserModuleNode;
  80. }
  81. function wrapBootstrapCall(mainFile) {
  82. return (host) => {
  83. const mainPath = core_1.normalize('/' + mainFile);
  84. let bootstrapCall = ng_ast_utils_1.findBootstrapModuleCall(host, mainPath);
  85. if (bootstrapCall === null) {
  86. throw new schematics_1.SchematicsException('Bootstrap module not found.');
  87. }
  88. let bootstrapCallExpression = null;
  89. let currentCall = bootstrapCall;
  90. while (bootstrapCallExpression === null && currentCall.parent) {
  91. currentCall = currentCall.parent;
  92. if (ts.isExpressionStatement(currentCall) || ts.isVariableStatement(currentCall)) {
  93. bootstrapCallExpression = currentCall;
  94. }
  95. }
  96. bootstrapCall = currentCall;
  97. // In case the bootstrap code is a variable statement
  98. // we need to determine it's usage
  99. if (bootstrapCallExpression && ts.isVariableStatement(bootstrapCallExpression)) {
  100. const declaration = bootstrapCallExpression.declarationList.declarations[0];
  101. const bootstrapVar = declaration.name.text;
  102. const sf = bootstrapCallExpression.getSourceFile();
  103. bootstrapCall = findCallExpressionNode(sf, bootstrapVar) || currentCall;
  104. }
  105. // indent contents
  106. const triviaWidth = bootstrapCall.getLeadingTriviaWidth();
  107. const beforeText = `document.addEventListener('DOMContentLoaded', () => {\n`
  108. + ' '.repeat(triviaWidth > 2 ? triviaWidth + 1 : triviaWidth);
  109. const afterText = `\n${triviaWidth > 2 ? ' '.repeat(triviaWidth - 1) : ''}});`;
  110. // in some cases we need to cater for a trailing semicolon such as;
  111. // bootstrap().catch(err => console.log(err));
  112. const lastToken = bootstrapCall.parent.getLastToken();
  113. let endPos = bootstrapCall.getEnd();
  114. if (lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken) {
  115. endPos = lastToken.getEnd();
  116. }
  117. const recorder = host.beginUpdate(mainPath);
  118. recorder.insertLeft(bootstrapCall.getStart(), beforeText);
  119. recorder.insertRight(endPos, afterText);
  120. host.commitUpdate(recorder);
  121. };
  122. }
  123. function findCallExpressionNode(node, text) {
  124. if (ts.isCallExpression(node)
  125. && ts.isIdentifier(node.expression)
  126. && node.expression.text === text) {
  127. return node;
  128. }
  129. let foundNode = null;
  130. ts.forEachChild(node, childNode => {
  131. foundNode = findCallExpressionNode(childNode, text);
  132. if (foundNode) {
  133. return true;
  134. }
  135. });
  136. return foundNode;
  137. }
  138. function addServerTransition(options, mainFile, clientProjectRoot) {
  139. return (host) => {
  140. const mainPath = core_1.normalize('/' + mainFile);
  141. const bootstrapModuleRelativePath = ng_ast_utils_1.findBootstrapModulePath(host, mainPath);
  142. const bootstrapModulePath = core_1.normalize(`/${clientProjectRoot}/src/${bootstrapModuleRelativePath}.ts`);
  143. const browserModuleImport = findBrowserModuleImport(host, bootstrapModulePath);
  144. const appId = options.appId;
  145. const transitionCall = `.withServerTransition({ appId: '${appId}' })`;
  146. const position = browserModuleImport.pos + browserModuleImport.getFullText().length;
  147. const transitionCallChange = new change_1.InsertChange(bootstrapModulePath, position, transitionCall);
  148. const transitionCallRecorder = host.beginUpdate(bootstrapModulePath);
  149. transitionCallRecorder.insertLeft(transitionCallChange.pos, transitionCallChange.toAdd);
  150. host.commitUpdate(transitionCallRecorder);
  151. };
  152. }
  153. function addDependencies() {
  154. return (host) => {
  155. const coreDep = dependencies_1.getPackageJsonDependency(host, '@angular/core');
  156. if (coreDep === null) {
  157. throw new schematics_1.SchematicsException('Could not find version.');
  158. }
  159. const platformServerDep = {
  160. ...coreDep,
  161. name: '@angular/platform-server',
  162. };
  163. dependencies_1.addPackageJsonDependency(host, platformServerDep);
  164. return host;
  165. };
  166. }
  167. function getTsConfigOutDir(host, tsConfigPath) {
  168. const tsConfigBuffer = host.read(tsConfigPath);
  169. if (!tsConfigBuffer) {
  170. throw new schematics_1.SchematicsException(`Could not read ${tsConfigPath}`);
  171. }
  172. const tsConfigContent = tsConfigBuffer.toString();
  173. const tsConfig = core_1.parseJson(tsConfigContent, core_1.JsonParseMode.Loose);
  174. if (tsConfig === null || typeof tsConfig !== 'object' || Array.isArray(tsConfig) ||
  175. tsConfig.compilerOptions === null || typeof tsConfig.compilerOptions !== 'object' ||
  176. Array.isArray(tsConfig.compilerOptions)) {
  177. throw new schematics_1.SchematicsException(`Invalid tsconfig - ${tsConfigPath}`);
  178. }
  179. const outDir = tsConfig.compilerOptions.outDir;
  180. return outDir;
  181. }
  182. function default_1(options) {
  183. return async (host, context) => {
  184. const workspace = await workspace_1.getWorkspace(host);
  185. const clientProject = workspace.projects.get(options.clientProject);
  186. if (!clientProject || clientProject.extensions.projectType !== 'application') {
  187. throw new schematics_1.SchematicsException(`Universal requires a project type of "application".`);
  188. }
  189. const clientBuildTarget = clientProject.targets.get('build');
  190. if (!clientBuildTarget) {
  191. throw project_targets_1.targetBuildNotFoundError();
  192. }
  193. const clientBuildOptions = (clientBuildTarget.options || {});
  194. const outDir = getTsConfigOutDir(host, clientBuildOptions.tsConfig);
  195. const clientTsConfig = core_1.normalize(clientBuildOptions.tsConfig);
  196. const tsConfigExtends = core_1.basename(clientTsConfig);
  197. // this is needed because prior to version 8, tsconfig might have been in 'src'
  198. // and we don't want to break the 'ng add @nguniversal/express-engine schematics'
  199. const rootInSrc = clientProject.root === '' && clientTsConfig.includes('src/');
  200. const tsConfigDirectory = core_1.join(core_1.normalize(clientProject.root), rootInSrc ? 'src' : '');
  201. if (!options.skipInstall) {
  202. context.addTask(new tasks_1.NodePackageInstallTask());
  203. }
  204. const templateSource = schematics_1.apply(schematics_1.url('./files/src'), [
  205. schematics_1.applyTemplates({
  206. ...core_1.strings,
  207. ...options,
  208. stripTsExtension: (s) => s.replace(/\.ts$/, ''),
  209. hasLocalizePackage: !!dependencies_1.getPackageJsonDependency(host, '@angular/localize'),
  210. }),
  211. schematics_1.move(core_1.join(core_1.normalize(clientProject.root), 'src')),
  212. ]);
  213. const rootSource = schematics_1.apply(schematics_1.url('./files/root'), [
  214. schematics_1.applyTemplates({
  215. ...core_1.strings,
  216. ...options,
  217. stripTsExtension: (s) => s.replace(/\.ts$/, ''),
  218. outDir,
  219. tsConfigExtends,
  220. rootInSrc,
  221. }),
  222. schematics_1.move(tsConfigDirectory),
  223. ]);
  224. return schematics_1.chain([
  225. schematics_1.mergeWith(templateSource),
  226. schematics_1.mergeWith(rootSource),
  227. addDependencies(),
  228. updateConfigFile(options, tsConfigDirectory),
  229. wrapBootstrapCall(clientBuildOptions.main),
  230. addServerTransition(options, clientBuildOptions.main, clientProject.root),
  231. ]);
  232. };
  233. }
  234. exports.default = default_1;