migration.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. });
  109. }
  110. }
  111. function updateProjects(tree, context) {
  112. return (0, workspace_1.updateWorkspace)((workspace) => {
  113. const rules = [];
  114. for (const [name, project] of workspace.projects) {
  115. if (project.extensions.projectType !== workspace_models_1.ProjectType.Application) {
  116. // Only interested in application projects since these changes only effects application builders
  117. continue;
  118. }
  119. const buildTarget = project.targets.get('build');
  120. if (!buildTarget || buildTarget.builder === workspace_models_1.Builders.Application) {
  121. continue;
  122. }
  123. if (buildTarget.builder !== workspace_models_1.Builders.BrowserEsbuild &&
  124. buildTarget.builder !== workspace_models_1.Builders.Browser) {
  125. context.logger.error(`Cannot update project "${name}" to use the application builder.` +
  126. ` Only "${workspace_models_1.Builders.BrowserEsbuild}" and "${workspace_models_1.Builders.Browser}" can be automatically migrated.`);
  127. continue;
  128. }
  129. const serverTarget = project.targets.get('server');
  130. rules.push(...updateBuildTarget(name, buildTarget, serverTarget, tree, context));
  131. // Delete all redundant targets
  132. for (const [key, target] of project.targets) {
  133. switch (target.builder) {
  134. case workspace_models_1.Builders.Server:
  135. case workspace_models_1.Builders.Prerender:
  136. case workspace_models_1.Builders.AppShell:
  137. case workspace_models_1.Builders.SsrDevServer:
  138. project.targets.delete(key);
  139. break;
  140. }
  141. }
  142. // Update CSS/Sass import specifiers
  143. const projectSourceRoot = (0, posix_1.join)(project.root, project.sourceRoot ?? 'src');
  144. updateStyleImports(tree, projectSourceRoot, buildTarget);
  145. }
  146. // Check for @angular-devkit/build-angular Webpack usage
  147. let hasAngularDevkitUsage = false;
  148. for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
  149. switch (target.builder) {
  150. case workspace_models_1.Builders.Application:
  151. case workspace_models_1.Builders.DevServer:
  152. case workspace_models_1.Builders.ExtractI18n:
  153. case workspace_models_1.Builders.Karma:
  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.Karma:
  178. target.builder = '@angular/build:karma';
  179. // Remove "builderMode" option since the builder will always use "application"
  180. for (const [, karmaOptions] of (0, workspace_1.allTargetOptions)(target)) {
  181. delete karmaOptions['builderMode'];
  182. }
  183. break;
  184. case workspace_models_1.Builders.NgPackagr:
  185. target.builder = '@angular/build:ng-packagr';
  186. break;
  187. }
  188. }
  189. // Add direct @angular/build dependencies and remove @angular-devkit/build-angular
  190. rules.push((0, dependency_1.addDependency)('@angular/build', latest_versions_1.latestVersions.DevkitBuildAngular, {
  191. type: dependency_1.DependencyType.Dev,
  192. // Always is set here since removePackageJsonDependency below does not automatically
  193. // trigger the package manager execution.
  194. install: dependency_1.InstallBehavior.Always,
  195. existing: dependency_1.ExistingBehavior.Replace,
  196. }));
  197. (0, dependencies_1.removePackageJsonDependency)(tree, '@angular-devkit/build-angular');
  198. // Add less dependency if any projects contain a Less stylesheet file.
  199. // This check does not consider Node.js packages due to the performance
  200. // cost of searching such a large directory structure. A build time error
  201. // will provide instructions to install the package in this case.
  202. if (hasLessStylesheets(tree)) {
  203. rules.push((0, dependency_1.addDependency)('less', latest_versions_1.latestVersions['less'], {
  204. type: dependency_1.DependencyType.Dev,
  205. existing: dependency_1.ExistingBehavior.Skip,
  206. }));
  207. }
  208. // Add postcss dependency if any projects have a custom postcss configuration file.
  209. // The build system only supports files in a project root or workspace root with
  210. // names of either 'postcss.config.json' or '.postcssrc.json'.
  211. if (hasPostcssConfiguration(tree, workspace)) {
  212. rules.push((0, dependency_1.addDependency)('postcss', latest_versions_1.latestVersions['postcss'], {
  213. type: dependency_1.DependencyType.Dev,
  214. existing: dependency_1.ExistingBehavior.Replace,
  215. }));
  216. }
  217. }
  218. return (0, schematics_1.chain)(rules);
  219. });
  220. }
  221. /**
  222. * Searches the schematic tree for files that have a `.less` extension.
  223. *
  224. * @param tree A Schematics tree instance to search
  225. * @returns true if Less stylesheet files are found; otherwise, false
  226. */
  227. function hasLessStylesheets(tree) {
  228. const directories = [tree.getDir('/')];
  229. let current;
  230. while ((current = directories.pop())) {
  231. for (const path of current.subfiles) {
  232. if (path.endsWith('.less')) {
  233. return true;
  234. }
  235. }
  236. for (const path of current.subdirs) {
  237. if (path === 'node_modules' || path.startsWith('.')) {
  238. continue;
  239. }
  240. directories.push(current.dir(path));
  241. }
  242. }
  243. }
  244. /**
  245. * Searches for a Postcss configuration file within the workspace root
  246. * or any of the project roots.
  247. *
  248. * @param tree A Schematics tree instance to search
  249. * @param workspace A Workspace to check for projects
  250. * @returns true, if a Postcss configuration file is found; otherwise, false
  251. */
  252. function hasPostcssConfiguration(tree, workspace) {
  253. // Add workspace root
  254. const searchDirectories = [''];
  255. // Add each project root
  256. for (const { root } of workspace.projects.values()) {
  257. if (root) {
  258. searchDirectories.push(root);
  259. }
  260. }
  261. return searchDirectories.some((dir) => tree.exists((0, posix_1.join)(dir, 'postcss.config.json')) || tree.exists((0, posix_1.join)(dir, '.postcssrc.json')));
  262. }
  263. function* visit(directory) {
  264. for (const path of directory.subfiles) {
  265. const sass = path.endsWith('.scss');
  266. if (path.endsWith('.css') || sass) {
  267. const entry = directory.file(path);
  268. if (entry) {
  269. const content = entry.content;
  270. yield [entry.path, content.toString(), sass];
  271. }
  272. }
  273. }
  274. for (const path of directory.subdirs) {
  275. if (path === 'node_modules' || path.startsWith('.')) {
  276. continue;
  277. }
  278. yield* visit(directory.dir(path));
  279. }
  280. }
  281. // Based on https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart
  282. function* potentialSassImports(specifier, base, fromImport) {
  283. const directory = (0, posix_1.join)(base, (0, posix_1.dirname)(specifier));
  284. const extension = (0, posix_1.extname)(specifier);
  285. const hasStyleExtension = extension === '.scss' || extension === '.sass' || extension === '.css';
  286. // Remove the style extension if present to allow adding the `.import` suffix
  287. const filename = (0, posix_1.basename)(specifier, hasStyleExtension ? extension : undefined);
  288. if (hasStyleExtension) {
  289. if (fromImport) {
  290. yield (0, posix_1.join)(directory, filename + '.import' + extension);
  291. yield (0, posix_1.join)(directory, '_' + filename + '.import' + extension);
  292. }
  293. yield (0, posix_1.join)(directory, filename + extension);
  294. yield (0, posix_1.join)(directory, '_' + filename + extension);
  295. }
  296. else {
  297. if (fromImport) {
  298. yield (0, posix_1.join)(directory, filename + '.import.scss');
  299. yield (0, posix_1.join)(directory, filename + '.import.sass');
  300. yield (0, posix_1.join)(directory, filename + '.import.css');
  301. yield (0, posix_1.join)(directory, '_' + filename + '.import.scss');
  302. yield (0, posix_1.join)(directory, '_' + filename + '.import.sass');
  303. yield (0, posix_1.join)(directory, '_' + filename + '.import.css');
  304. }
  305. yield (0, posix_1.join)(directory, filename + '.scss');
  306. yield (0, posix_1.join)(directory, filename + '.sass');
  307. yield (0, posix_1.join)(directory, filename + '.css');
  308. yield (0, posix_1.join)(directory, '_' + filename + '.scss');
  309. yield (0, posix_1.join)(directory, '_' + filename + '.sass');
  310. yield (0, posix_1.join)(directory, '_' + filename + '.css');
  311. }
  312. }
  313. function updateStyleImports(tree, projectSourceRoot, buildTarget) {
  314. const external = new Set();
  315. let needWorkspaceIncludePath = false;
  316. for (const file of visit(tree.getDir(projectSourceRoot))) {
  317. const [path, content, sass] = file;
  318. const relativeBase = (0, posix_1.dirname)(path);
  319. let updater;
  320. for (const { start, specifier, fromUse } of (0, css_import_lexer_1.findImports)(content, sass)) {
  321. if (specifier[0] === '~') {
  322. updater ??= tree.beginUpdate(path);
  323. // start position includes the opening quote
  324. updater.remove(start + 1, 1);
  325. }
  326. else if (specifier[0] === '^') {
  327. updater ??= tree.beginUpdate(path);
  328. // start position includes the opening quote
  329. updater.remove(start + 1, 1);
  330. // Add to externalDependencies
  331. external.add(specifier.slice(1));
  332. }
  333. else if (sass &&
  334. [...potentialSassImports(specifier, relativeBase, !fromUse)].every((v) => !tree.exists(v)) &&
  335. [...potentialSassImports(specifier, '/', !fromUse)].some((v) => tree.exists(v))) {
  336. needWorkspaceIncludePath = true;
  337. }
  338. }
  339. if (updater) {
  340. tree.commitUpdate(updater);
  341. }
  342. }
  343. if (needWorkspaceIncludePath) {
  344. buildTarget.options ??= {};
  345. buildTarget.options['stylePreprocessorOptions'] ??= {};
  346. (buildTarget.options['stylePreprocessorOptions']['includePaths'] ??= []).push('.');
  347. }
  348. if (external.size > 0) {
  349. buildTarget.options ??= {};
  350. (buildTarget.options['externalDependencies'] ??= []).push(...external);
  351. }
  352. }
  353. function deleteFile(path) {
  354. return (tree) => {
  355. tree.delete(path);
  356. };
  357. }
  358. function updateJsonFile(path, updater) {
  359. return (tree, ctx) => {
  360. if (tree.exists(path)) {
  361. updater(new json_file_1.JSONFile(tree, path));
  362. }
  363. else {
  364. ctx.logger.info(`Skipping updating '${path}' as it does not exist.`);
  365. }
  366. };
  367. }
  368. /**
  369. * Migration main entrypoint
  370. */
  371. function default_1() {
  372. return (0, schematics_1.chain)([
  373. updateProjects,
  374. // Delete package.json helper scripts
  375. updateJsonFile('package.json', (pkgJson) => ['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) => pkgJson.remove(['scripts', s]))),
  376. // Update main tsconfig
  377. updateJsonFile('tsconfig.json', (rootJson) => {
  378. rootJson.modify(['compilerOptions', 'esModuleInterop'], true);
  379. rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined);
  380. rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined);
  381. }),
  382. ]);
  383. }