migration.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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 schematics_1 = require("@angular-devkit/schematics");
  12. const posix_1 = require("node:path/posix");
  13. const dependencies_1 = require("../../utility/dependencies");
  14. const dependency_1 = require("../../utility/dependency");
  15. const json_file_1 = require("../../utility/json-file");
  16. const latest_versions_1 = require("../../utility/latest-versions");
  17. const workspace_1 = require("../../utility/workspace");
  18. const workspace_models_1 = require("../../utility/workspace-models");
  19. const css_import_lexer_1 = require("./css-import-lexer");
  20. function* updateBuildTarget(projectName, buildTarget, serverTarget, tree, context) {
  21. // Update builder target and options
  22. buildTarget.builder = workspace_models_1.Builders.Application;
  23. for (const [, options] of (0, workspace_1.allTargetOptions)(buildTarget, false)) {
  24. if (options['index'] === '') {
  25. options['index'] = false;
  26. }
  27. // Rename and transform options
  28. options['browser'] = options['main'];
  29. if (serverTarget && typeof options['browser'] === 'string') {
  30. options['server'] = (0, posix_1.dirname)(options['browser']) + '/main.server.ts';
  31. }
  32. options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker'];
  33. if (typeof options['polyfills'] === 'string') {
  34. options['polyfills'] = [options['polyfills']];
  35. }
  36. let outputPath = options['outputPath'];
  37. if (typeof outputPath === 'string') {
  38. if (!/\/browser\/?$/.test(outputPath)) {
  39. // TODO: add prompt.
  40. context.logger.warn(`The output location of the browser build has been updated from "${outputPath}" to ` +
  41. `"${(0, posix_1.join)(outputPath, 'browser')}". ` +
  42. 'You might need to adjust your deployment pipeline or, as an alternative, ' +
  43. 'set outputPath.browser to "" in order to maintain the previous functionality.');
  44. }
  45. else {
  46. outputPath = outputPath.replace(/\/browser\/?$/, '');
  47. }
  48. options['outputPath'] = {
  49. base: outputPath,
  50. };
  51. if (typeof options['resourcesOutputPath'] === 'string') {
  52. const media = options['resourcesOutputPath'].replaceAll('/', '');
  53. if (media && media !== 'media') {
  54. options['outputPath'] = {
  55. base: outputPath,
  56. media,
  57. };
  58. }
  59. }
  60. }
  61. // Delete removed options
  62. delete options['vendorChunk'];
  63. delete options['commonChunk'];
  64. delete options['resourcesOutputPath'];
  65. delete options['buildOptimizer'];
  66. delete options['main'];
  67. delete options['ngswConfigPath'];
  68. }
  69. // Merge browser and server tsconfig
  70. if (serverTarget) {
  71. const browserTsConfig = buildTarget.options?.tsConfig;
  72. const serverTsConfig = serverTarget.options?.tsConfig;
  73. if (typeof browserTsConfig !== 'string') {
  74. throw new schematics_1.SchematicsException(`Cannot update project "${projectName}" to use the application builder` +
  75. ` as the browser tsconfig cannot be located.`);
  76. }
  77. if (typeof serverTsConfig !== 'string') {
  78. throw new schematics_1.SchematicsException(`Cannot update project "${projectName}" to use the application builder` +
  79. ` as the server tsconfig cannot be located.`);
  80. }
  81. const browserJson = new json_file_1.JSONFile(tree, browserTsConfig);
  82. const serverJson = new json_file_1.JSONFile(tree, serverTsConfig);
  83. const filesPath = ['files'];
  84. const files = new Set([
  85. ...(browserJson.get(filesPath) ?? []),
  86. ...(serverJson.get(filesPath) ?? []),
  87. ]);
  88. // Server file will be added later by the means of the ssr schematic.
  89. files.delete('server.ts');
  90. browserJson.modify(filesPath, Array.from(files));
  91. const typesPath = ['compilerOptions', 'types'];
  92. browserJson.modify(typesPath, Array.from(new Set([
  93. ...(browserJson.get(typesPath) ?? []),
  94. ...(serverJson.get(typesPath) ?? []),
  95. ])));
  96. // Delete server tsconfig
  97. yield deleteFile(serverTsConfig);
  98. }
  99. // Update server file
  100. const ssrMainFile = serverTarget?.options?.['main'];
  101. if (typeof ssrMainFile === 'string') {
  102. // Do not delete the server main file if it's the same as the browser file.
  103. if (buildTarget.options?.browser !== ssrMainFile) {
  104. yield deleteFile(ssrMainFile);
  105. }
  106. yield (0, schematics_1.externalSchematic)('@schematics/angular', 'ssr', {
  107. project: projectName,
  108. skipInstall: true,
  109. });
  110. }
  111. }
  112. function updateProjects(tree, context) {
  113. return (0, workspace_1.updateWorkspace)((workspace) => {
  114. const rules = [];
  115. for (const [name, project] of workspace.projects) {
  116. if (project.extensions.projectType !== workspace_models_1.ProjectType.Application) {
  117. // Only interested in application projects since these changes only effects application builders
  118. continue;
  119. }
  120. const buildTarget = project.targets.get('build');
  121. if (!buildTarget || buildTarget.builder === workspace_models_1.Builders.Application) {
  122. continue;
  123. }
  124. if (buildTarget.builder !== workspace_models_1.Builders.BrowserEsbuild &&
  125. buildTarget.builder !== workspace_models_1.Builders.Browser) {
  126. context.logger.error(`Cannot update project "${name}" to use the application builder.` +
  127. ` Only "${workspace_models_1.Builders.BrowserEsbuild}" and "${workspace_models_1.Builders.Browser}" can be automatically migrated.`);
  128. continue;
  129. }
  130. const serverTarget = project.targets.get('server');
  131. rules.push(...updateBuildTarget(name, buildTarget, serverTarget, tree, context));
  132. // Delete all redundant targets
  133. for (const [key, target] of project.targets) {
  134. switch (target.builder) {
  135. case workspace_models_1.Builders.Server:
  136. case workspace_models_1.Builders.Prerender:
  137. case workspace_models_1.Builders.AppShell:
  138. case workspace_models_1.Builders.SsrDevServer:
  139. project.targets.delete(key);
  140. break;
  141. }
  142. }
  143. // Update CSS/Sass import specifiers
  144. const projectSourceRoot = (0, posix_1.join)(project.root, project.sourceRoot ?? 'src');
  145. updateStyleImports(tree, projectSourceRoot, buildTarget);
  146. }
  147. // Check for @angular-devkit/build-angular Webpack usage
  148. let hasAngularDevkitUsage = false;
  149. for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
  150. switch (target.builder) {
  151. case workspace_models_1.Builders.Application:
  152. case workspace_models_1.Builders.DevServer:
  153. case workspace_models_1.Builders.ExtractI18n:
  154. case workspace_models_1.Builders.NgPackagr:
  155. // Ignore application, dev server, and i18n extraction for devkit usage check.
  156. // Both will be replaced if no other usage is found.
  157. continue;
  158. }
  159. if (target.builder.startsWith('@angular-devkit/build-angular:')) {
  160. hasAngularDevkitUsage = true;
  161. break;
  162. }
  163. }
  164. // Use @angular/build directly if there is no devkit package usage
  165. if (!hasAngularDevkitUsage) {
  166. for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
  167. switch (target.builder) {
  168. case workspace_models_1.Builders.Application:
  169. target.builder = '@angular/build:application';
  170. break;
  171. case workspace_models_1.Builders.DevServer:
  172. target.builder = '@angular/build:dev-server';
  173. break;
  174. case workspace_models_1.Builders.ExtractI18n:
  175. target.builder = '@angular/build:extract-i18n';
  176. break;
  177. case workspace_models_1.Builders.NgPackagr:
  178. target.builder = '@angular/build:ng-packagr';
  179. break;
  180. }
  181. }
  182. // Add direct @angular/build dependencies and remove @angular-devkit/build-angular
  183. rules.push((0, dependency_1.addDependency)('@angular/build', latest_versions_1.latestVersions.DevkitBuildAngular, {
  184. type: dependency_1.DependencyType.Dev,
  185. // Always is set here since removePackageJsonDependency below does not automatically
  186. // trigger the package manager execution.
  187. install: dependency_1.InstallBehavior.Always,
  188. existing: dependency_1.ExistingBehavior.Replace,
  189. }));
  190. (0, dependencies_1.removePackageJsonDependency)(tree, '@angular-devkit/build-angular');
  191. // Add less dependency if any projects contain a Less stylesheet file.
  192. // This check does not consider Node.js packages due to the performance
  193. // cost of searching such a large directory structure. A build time error
  194. // will provide instructions to install the package in this case.
  195. if (hasLessStylesheets(tree)) {
  196. rules.push((0, dependency_1.addDependency)('less', latest_versions_1.latestVersions['less'], {
  197. type: dependency_1.DependencyType.Dev,
  198. existing: dependency_1.ExistingBehavior.Skip,
  199. }));
  200. }
  201. // Add postcss dependency if any projects have a custom postcss configuration file.
  202. // The build system only supports files in a project root or workspace root with
  203. // names of either 'postcss.config.json' or '.postcssrc.json'.
  204. if (hasPostcssConfiguration(tree, workspace)) {
  205. rules.push((0, dependency_1.addDependency)('postcss', latest_versions_1.latestVersions['postcss'], {
  206. type: dependency_1.DependencyType.Dev,
  207. existing: dependency_1.ExistingBehavior.Replace,
  208. }));
  209. }
  210. }
  211. return (0, schematics_1.chain)(rules);
  212. });
  213. }
  214. /**
  215. * Searches the schematic tree for files that have a `.less` extension.
  216. *
  217. * @param tree A Schematics tree instance to search
  218. * @returns true if Less stylesheet files are found; otherwise, false
  219. */
  220. function hasLessStylesheets(tree) {
  221. const directories = [tree.getDir('/')];
  222. let current;
  223. while ((current = directories.pop())) {
  224. for (const path of current.subfiles) {
  225. if (path.endsWith('.less')) {
  226. return true;
  227. }
  228. }
  229. for (const path of current.subdirs) {
  230. if (path === 'node_modules' || path.startsWith('.')) {
  231. continue;
  232. }
  233. directories.push(current.dir(path));
  234. }
  235. }
  236. }
  237. /**
  238. * Searches for a Postcss configuration file within the workspace root
  239. * or any of the project roots.
  240. *
  241. * @param tree A Schematics tree instance to search
  242. * @param workspace A Workspace to check for projects
  243. * @returns true, if a Postcss configuration file is found; otherwise, false
  244. */
  245. function hasPostcssConfiguration(tree, workspace) {
  246. // Add workspace root
  247. const searchDirectories = [''];
  248. // Add each project root
  249. for (const { root } of workspace.projects.values()) {
  250. if (root) {
  251. searchDirectories.push(root);
  252. }
  253. }
  254. return searchDirectories.some((dir) => tree.exists((0, posix_1.join)(dir, 'postcss.config.json')) || tree.exists((0, posix_1.join)(dir, '.postcssrc.json')));
  255. }
  256. function* visit(directory) {
  257. for (const path of directory.subfiles) {
  258. const sass = path.endsWith('.scss');
  259. if (path.endsWith('.css') || sass) {
  260. const entry = directory.file(path);
  261. if (entry) {
  262. const content = entry.content;
  263. yield [entry.path, content.toString(), sass];
  264. }
  265. }
  266. }
  267. for (const path of directory.subdirs) {
  268. if (path === 'node_modules' || path.startsWith('.')) {
  269. continue;
  270. }
  271. yield* visit(directory.dir(path));
  272. }
  273. }
  274. // Based on https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart
  275. function* potentialSassImports(specifier, base, fromImport) {
  276. const directory = (0, posix_1.join)(base, (0, posix_1.dirname)(specifier));
  277. const extension = (0, posix_1.extname)(specifier);
  278. const hasStyleExtension = extension === '.scss' || extension === '.sass' || extension === '.css';
  279. // Remove the style extension if present to allow adding the `.import` suffix
  280. const filename = (0, posix_1.basename)(specifier, hasStyleExtension ? extension : undefined);
  281. if (hasStyleExtension) {
  282. if (fromImport) {
  283. yield (0, posix_1.join)(directory, filename + '.import' + extension);
  284. yield (0, posix_1.join)(directory, '_' + filename + '.import' + extension);
  285. }
  286. yield (0, posix_1.join)(directory, filename + extension);
  287. yield (0, posix_1.join)(directory, '_' + filename + extension);
  288. }
  289. else {
  290. if (fromImport) {
  291. yield (0, posix_1.join)(directory, filename + '.import.scss');
  292. yield (0, posix_1.join)(directory, filename + '.import.sass');
  293. yield (0, posix_1.join)(directory, filename + '.import.css');
  294. yield (0, posix_1.join)(directory, '_' + filename + '.import.scss');
  295. yield (0, posix_1.join)(directory, '_' + filename + '.import.sass');
  296. yield (0, posix_1.join)(directory, '_' + filename + '.import.css');
  297. }
  298. yield (0, posix_1.join)(directory, filename + '.scss');
  299. yield (0, posix_1.join)(directory, filename + '.sass');
  300. yield (0, posix_1.join)(directory, filename + '.css');
  301. yield (0, posix_1.join)(directory, '_' + filename + '.scss');
  302. yield (0, posix_1.join)(directory, '_' + filename + '.sass');
  303. yield (0, posix_1.join)(directory, '_' + filename + '.css');
  304. }
  305. }
  306. function updateStyleImports(tree, projectSourceRoot, buildTarget) {
  307. const external = new Set();
  308. let needWorkspaceIncludePath = false;
  309. for (const file of visit(tree.getDir(projectSourceRoot))) {
  310. const [path, content, sass] = file;
  311. const relativeBase = (0, posix_1.dirname)(path);
  312. let updater;
  313. for (const { start, specifier, fromUse } of (0, css_import_lexer_1.findImports)(content, sass)) {
  314. if (specifier[0] === '~') {
  315. updater ??= tree.beginUpdate(path);
  316. // start position includes the opening quote
  317. updater.remove(start + 1, 1);
  318. }
  319. else if (specifier[0] === '^') {
  320. updater ??= tree.beginUpdate(path);
  321. // start position includes the opening quote
  322. updater.remove(start + 1, 1);
  323. // Add to externalDependencies
  324. external.add(specifier.slice(1));
  325. }
  326. else if (sass &&
  327. [...potentialSassImports(specifier, relativeBase, !fromUse)].every((v) => !tree.exists(v)) &&
  328. [...potentialSassImports(specifier, '/', !fromUse)].some((v) => tree.exists(v))) {
  329. needWorkspaceIncludePath = true;
  330. }
  331. }
  332. if (updater) {
  333. tree.commitUpdate(updater);
  334. }
  335. }
  336. if (needWorkspaceIncludePath) {
  337. buildTarget.options ??= {};
  338. buildTarget.options['stylePreprocessorOptions'] ??= {};
  339. (buildTarget.options['stylePreprocessorOptions']['includePaths'] ??= []).push('.');
  340. }
  341. if (external.size > 0) {
  342. buildTarget.options ??= {};
  343. (buildTarget.options['externalDependencies'] ??= []).push(...external);
  344. }
  345. }
  346. function deleteFile(path) {
  347. return (tree) => {
  348. tree.delete(path);
  349. };
  350. }
  351. function updateJsonFile(path, updater) {
  352. return (tree, ctx) => {
  353. if (tree.exists(path)) {
  354. updater(new json_file_1.JSONFile(tree, path));
  355. }
  356. else {
  357. ctx.logger.info(`Skipping updating '${path}' as it does not exist.`);
  358. }
  359. };
  360. }
  361. /**
  362. * Migration main entrypoint
  363. */
  364. function default_1() {
  365. return (0, schematics_1.chain)([
  366. updateProjects,
  367. // Delete package.json helper scripts
  368. updateJsonFile('package.json', (pkgJson) => ['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) => pkgJson.remove(['scripts', s]))),
  369. // Update main tsconfig
  370. updateJsonFile('tsconfig.json', (rootJson) => {
  371. rootJson.modify(['compilerOptions', 'esModuleInterop'], true);
  372. rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined);
  373. rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined);
  374. }),
  375. ]);
  376. }