index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. "use strict";
  2. /**
  3. * @license
  4. * Copyright Google LLC All Rights Reserved.
  5. *
  6. * Use of this source code is governed by an MIT-style license that can be
  7. * found in the LICENSE file at https://angular.dev/license
  8. */
  9. Object.defineProperty(exports, "__esModule", { value: true });
  10. exports.default = default_1;
  11. const core_1 = require("@angular-devkit/core");
  12. const schematics_1 = require("@angular-devkit/schematics");
  13. const node_path_1 = require("node:path");
  14. const utility_1 = require("../utility");
  15. const json_file_1 = require("../utility/json-file");
  16. const latest_versions_1 = require("../utility/latest-versions");
  17. const ng_ast_utils_1 = require("../utility/ng-ast-utils");
  18. const project_targets_1 = require("../utility/project-targets");
  19. const util_1 = require("../utility/standalone/util");
  20. const workspace_1 = require("../utility/workspace");
  21. const SERVE_SSR_TARGET_NAME = 'serve-ssr';
  22. const PRERENDER_TARGET_NAME = 'prerender';
  23. const DEFAULT_BROWSER_DIR = 'browser';
  24. const DEFAULT_MEDIA_DIR = 'media';
  25. const DEFAULT_SERVER_DIR = 'server';
  26. async function getLegacyOutputPaths(host, projectName, target) {
  27. // Generate new output paths
  28. const workspace = await (0, utility_1.readWorkspace)(host);
  29. const project = workspace.projects.get(projectName);
  30. const architectTarget = project?.targets.get(target);
  31. if (!architectTarget?.options) {
  32. throw new schematics_1.SchematicsException(`Cannot find 'options' for ${projectName} ${target} target.`);
  33. }
  34. const { outputPath } = architectTarget.options;
  35. if (typeof outputPath !== 'string') {
  36. throw new schematics_1.SchematicsException(`outputPath for ${projectName} ${target} target is not a string.`);
  37. }
  38. return outputPath;
  39. }
  40. async function getApplicationBuilderOutputPaths(host, projectName) {
  41. // Generate new output paths
  42. const target = 'build';
  43. const workspace = await (0, utility_1.readWorkspace)(host);
  44. const project = workspace.projects.get(projectName);
  45. const architectTarget = project?.targets.get(target);
  46. if (!architectTarget?.options) {
  47. throw new schematics_1.SchematicsException(`Cannot find 'options' for ${projectName} ${target} target.`);
  48. }
  49. let { outputPath } = architectTarget.options;
  50. // Use default if not explicitly specified
  51. outputPath ??= node_path_1.posix.join('dist', projectName);
  52. const defaultDirs = {
  53. server: DEFAULT_SERVER_DIR,
  54. browser: DEFAULT_BROWSER_DIR,
  55. };
  56. if (outputPath && (0, core_1.isJsonObject)(outputPath)) {
  57. return {
  58. ...defaultDirs,
  59. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  60. ...outputPath,
  61. };
  62. }
  63. if (typeof outputPath !== 'string') {
  64. throw new schematics_1.SchematicsException(`outputPath for ${projectName} ${target} target is not a string.`);
  65. }
  66. return {
  67. base: outputPath,
  68. ...defaultDirs,
  69. };
  70. }
  71. function addScriptsRule({ project }, isUsingApplicationBuilder) {
  72. return async (host) => {
  73. const pkgPath = '/package.json';
  74. const pkg = host.readJson(pkgPath);
  75. if (pkg === null) {
  76. throw new schematics_1.SchematicsException('Could not find package.json');
  77. }
  78. if (isUsingApplicationBuilder) {
  79. const { base, server } = await getApplicationBuilderOutputPaths(host, project);
  80. pkg.scripts ??= {};
  81. pkg.scripts[`serve:ssr:${project}`] = `node ${node_path_1.posix.join(base, server)}/server.mjs`;
  82. }
  83. else {
  84. const serverDist = await getLegacyOutputPaths(host, project, 'server');
  85. pkg.scripts = {
  86. ...pkg.scripts,
  87. 'dev:ssr': `ng run ${project}:${SERVE_SSR_TARGET_NAME}`,
  88. 'serve:ssr': `node ${serverDist}/main.js`,
  89. 'build:ssr': `ng build && ng run ${project}:server`,
  90. 'prerender': `ng run ${project}:${PRERENDER_TARGET_NAME}`,
  91. };
  92. }
  93. host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
  94. };
  95. }
  96. function updateApplicationBuilderTsConfigRule(options) {
  97. return async (host) => {
  98. const workspace = await (0, utility_1.readWorkspace)(host);
  99. const project = workspace.projects.get(options.project);
  100. const buildTarget = project?.targets.get('build');
  101. if (!buildTarget || !buildTarget.options) {
  102. return;
  103. }
  104. const tsConfigPath = buildTarget.options.tsConfig;
  105. if (!tsConfigPath || typeof tsConfigPath !== 'string') {
  106. // No tsconfig path
  107. return;
  108. }
  109. const json = new json_file_1.JSONFile(host, tsConfigPath);
  110. // Skip adding the files entry if the server entry would already be included
  111. const include = json.get(['include']);
  112. if (Array.isArray(include) && include.includes('src/**/*.ts')) {
  113. return;
  114. }
  115. const filesPath = ['files'];
  116. const files = new Set(json.get(filesPath) ?? []);
  117. files.add('src/server.ts');
  118. json.modify(filesPath, [...files]);
  119. };
  120. }
  121. function updateApplicationBuilderWorkspaceConfigRule(projectSourceRoot, options, { logger }) {
  122. return (0, utility_1.updateWorkspace)((workspace) => {
  123. const buildTarget = workspace.projects.get(options.project)?.targets.get('build');
  124. if (!buildTarget) {
  125. return;
  126. }
  127. let outputPath = buildTarget.options?.outputPath;
  128. if (outputPath && (0, core_1.isJsonObject)(outputPath)) {
  129. if (outputPath.browser === '') {
  130. const base = outputPath.base;
  131. logger.warn(`The output location of the browser build has been updated from "${base}" to "${node_path_1.posix.join(base, DEFAULT_BROWSER_DIR)}".
  132. You might need to adjust your deployment pipeline.`);
  133. if ((outputPath.media && outputPath.media !== DEFAULT_MEDIA_DIR) ||
  134. (outputPath.server && outputPath.server !== DEFAULT_SERVER_DIR)) {
  135. delete outputPath.browser;
  136. }
  137. else {
  138. outputPath = outputPath.base;
  139. }
  140. }
  141. }
  142. buildTarget.options = {
  143. ...buildTarget.options,
  144. outputPath,
  145. outputMode: 'server',
  146. ssr: {
  147. entry: (0, core_1.join)((0, core_1.normalize)(projectSourceRoot), 'server.ts'),
  148. },
  149. };
  150. });
  151. }
  152. function updateWebpackBuilderWorkspaceConfigRule(projectSourceRoot, options) {
  153. return (0, utility_1.updateWorkspace)((workspace) => {
  154. const projectName = options.project;
  155. const project = workspace.projects.get(projectName);
  156. if (!project) {
  157. return;
  158. }
  159. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  160. const serverTarget = project.targets.get('server');
  161. (serverTarget.options ??= {}).main = node_path_1.posix.join(projectSourceRoot, 'server.ts');
  162. const serveSSRTarget = project.targets.get(SERVE_SSR_TARGET_NAME);
  163. if (serveSSRTarget) {
  164. return;
  165. }
  166. project.targets.add({
  167. name: SERVE_SSR_TARGET_NAME,
  168. builder: '@angular-devkit/build-angular:ssr-dev-server',
  169. defaultConfiguration: 'development',
  170. options: {},
  171. configurations: {
  172. development: {
  173. browserTarget: `${projectName}:build:development`,
  174. serverTarget: `${projectName}:server:development`,
  175. },
  176. production: {
  177. browserTarget: `${projectName}:build:production`,
  178. serverTarget: `${projectName}:server:production`,
  179. },
  180. },
  181. });
  182. const prerenderTarget = project.targets.get(PRERENDER_TARGET_NAME);
  183. if (prerenderTarget) {
  184. return;
  185. }
  186. project.targets.add({
  187. name: PRERENDER_TARGET_NAME,
  188. builder: '@angular-devkit/build-angular:prerender',
  189. defaultConfiguration: 'production',
  190. options: {
  191. routes: ['/'],
  192. },
  193. configurations: {
  194. production: {
  195. browserTarget: `${projectName}:build:production`,
  196. serverTarget: `${projectName}:server:production`,
  197. },
  198. development: {
  199. browserTarget: `${projectName}:build:development`,
  200. serverTarget: `${projectName}:server:development`,
  201. },
  202. },
  203. });
  204. });
  205. }
  206. function updateWebpackBuilderServerTsConfigRule(options) {
  207. return async (host) => {
  208. const workspace = await (0, utility_1.readWorkspace)(host);
  209. const project = workspace.projects.get(options.project);
  210. const serverTarget = project?.targets.get('server');
  211. if (!serverTarget || !serverTarget.options) {
  212. return;
  213. }
  214. const tsConfigPath = serverTarget.options.tsConfig;
  215. if (!tsConfigPath || typeof tsConfigPath !== 'string') {
  216. // No tsconfig path
  217. return;
  218. }
  219. const tsConfig = new json_file_1.JSONFile(host, tsConfigPath);
  220. const filesAstNode = tsConfig.get(['files']);
  221. const serverFilePath = 'src/server.ts';
  222. if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) {
  223. tsConfig.modify(['files'], [...filesAstNode, serverFilePath]);
  224. }
  225. };
  226. }
  227. function addDependencies({ skipInstall }, isUsingApplicationBuilder) {
  228. const install = skipInstall ? utility_1.InstallBehavior.None : utility_1.InstallBehavior.Auto;
  229. const rules = [
  230. (0, utility_1.addDependency)('express', latest_versions_1.latestVersions['express'], {
  231. type: utility_1.DependencyType.Default,
  232. install,
  233. existing: utility_1.ExistingBehavior.Replace,
  234. }),
  235. (0, utility_1.addDependency)('@types/express', latest_versions_1.latestVersions['@types/express'], {
  236. type: utility_1.DependencyType.Dev,
  237. install,
  238. existing: utility_1.ExistingBehavior.Replace,
  239. }),
  240. ];
  241. if (!isUsingApplicationBuilder) {
  242. rules.push((0, utility_1.addDependency)('browser-sync', latest_versions_1.latestVersions['browser-sync'], {
  243. type: utility_1.DependencyType.Dev,
  244. install,
  245. }));
  246. }
  247. return (0, schematics_1.chain)(rules);
  248. }
  249. function addServerFile(projectSourceRoot, options, isStandalone) {
  250. return async (host) => {
  251. const projectName = options.project;
  252. const workspace = await (0, utility_1.readWorkspace)(host);
  253. const project = workspace.projects.get(projectName);
  254. if (!project) {
  255. throw new schematics_1.SchematicsException(`Invalid project name (${projectName})`);
  256. }
  257. const usingApplicationBuilder = (0, project_targets_1.isUsingApplicationBuilder)(project);
  258. const browserDistDirectory = usingApplicationBuilder
  259. ? (await getApplicationBuilderOutputPaths(host, projectName)).browser
  260. : await getLegacyOutputPaths(host, projectName, 'build');
  261. return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)(`./files/${usingApplicationBuilder ? 'application-builder' : 'server-builder'}`), [
  262. (0, schematics_1.applyTemplates)({
  263. ...core_1.strings,
  264. ...options,
  265. browserDistDirectory,
  266. isStandalone,
  267. }),
  268. (0, schematics_1.move)(projectSourceRoot),
  269. ]));
  270. };
  271. }
  272. function default_1(options) {
  273. return async (host, context) => {
  274. const browserEntryPoint = await (0, util_1.getMainFilePath)(host, options.project);
  275. const isStandalone = (0, ng_ast_utils_1.isStandaloneApp)(host, browserEntryPoint);
  276. const workspace = await (0, workspace_1.getWorkspace)(host);
  277. const clientProject = workspace.projects.get(options.project);
  278. if (!clientProject) {
  279. throw (0, project_targets_1.targetBuildNotFoundError)();
  280. }
  281. const usingApplicationBuilder = (0, project_targets_1.isUsingApplicationBuilder)(clientProject);
  282. const sourceRoot = clientProject.sourceRoot ?? node_path_1.posix.join(clientProject.root, 'src');
  283. return (0, schematics_1.chain)([
  284. (0, schematics_1.schematic)('server', {
  285. ...options,
  286. skipInstall: true,
  287. }),
  288. ...(usingApplicationBuilder
  289. ? [
  290. updateApplicationBuilderWorkspaceConfigRule(sourceRoot, options, context),
  291. updateApplicationBuilderTsConfigRule(options),
  292. ]
  293. : [
  294. updateWebpackBuilderServerTsConfigRule(options),
  295. updateWebpackBuilderWorkspaceConfigRule(sourceRoot, options),
  296. ]),
  297. addServerFile(sourceRoot, options, isStandalone),
  298. addScriptsRule(options, usingApplicationBuilder),
  299. addDependencies(options, usingApplicationBuilder),
  300. ]);
  301. };
  302. }