/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/schematics/angular/migrations/use-application-builder/migration.ts
381 строка
12 KB
deku-nattsu
fix(@schematics/angular): silently skip when the build target already uses one of the new builders
24 ноя 2025, 17:56
24 ноя 2025, 17:56
cacd5d9
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { Rule, SchematicContext, SchematicsException, Tree, chain, externalSchematic, } from '@angular-devkit/schematics'; import { dirname, join } from 'node:path/posix'; import { DependencyType, ExistingBehavior, addDependency, removeDependency, } from '../../utility/dependency'; import { JSONFile } from '../../utility/json-file'; import { latestVersions } from '../../utility/latest-versions'; import { TargetDefinition, allTargetOptions, allWorkspaceTargets, updateWorkspace, } from '../../utility/workspace'; import { Builders, ProjectType } from '../../utility/workspace-models'; import { hasLessStylesheets, hasPostcssConfiguration, updateStyleImports, } from './stylesheet-updates'; function* updateBuildTarget( projectName: string, buildTarget: TargetDefinition, serverTarget: TargetDefinition | undefined, tree: Tree, context: SchematicContext, ): Iterable<Rule> { // Update builder target and options buildTarget.builder = Builders.Application; for (const [, options] of allTargetOptions(buildTarget, false)) { if (options['index'] === '') { options['index'] = false; } // Rename and transform options options['browser'] = options['main']; if (serverTarget && typeof options['browser'] === 'string') { options['server'] = dirname(options['browser']) + '/main.server.ts'; } options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker']; if (typeof options['polyfills'] === 'string') { options['polyfills'] = [options['polyfills']]; } let outputPath = options['outputPath']; if (typeof outputPath === 'string') { if (!/\/browser\/?$/.test(outputPath)) { // TODO: add prompt. context.logger.warn( `The output location of the browser build has been updated from "${outputPath}" to ` + `"${join(outputPath, 'browser')}". ` + 'You might need to adjust your deployment pipeline or, as an alternative, ' + 'set outputPath.browser to "" in order to maintain the previous functionality.', ); } else { outputPath = outputPath.replace(/\/browser\/?$/, ''); } options['outputPath'] = { base: outputPath, }; if (typeof options['resourcesOutputPath'] === 'string') { const media = options['resourcesOutputPath'].replaceAll('/', ''); if (media && media !== 'media') { options['outputPath'] = { base: outputPath, media, }; } } } // Delete removed options delete options['vendorChunk']; delete options['commonChunk']; delete options['resourcesOutputPath']; delete options['buildOptimizer']; delete options['main']; delete options['ngswConfigPath']; } // Merge browser and server tsconfig if (serverTarget) { const browserTsConfig = buildTarget.options?.tsConfig; const serverTsConfig = serverTarget.options?.tsConfig; if (typeof browserTsConfig !== 'string') { throw new SchematicsException( `Cannot update project "${projectName}" to use the application builder` + ` as the browser tsconfig cannot be located.`, ); } if (typeof serverTsConfig !== 'string') { throw new SchematicsException( `Cannot update project "${projectName}" to use the application builder` + ` as the server tsconfig cannot be located.`, ); } const browserJson = new JSONFile(tree, browserTsConfig); const serverJson = new JSONFile(tree, serverTsConfig); const filesPath = ['files']; const files = new Set([ ...((browserJson.get(filesPath) as string[] | undefined) ?? []), ...((serverJson.get(filesPath) as string[] | undefined) ?? []), ]); // Server file will be added later by the means of the ssr schematic. files.delete('server.ts'); browserJson.modify(filesPath, Array.from(files)); const typesPath = ['compilerOptions', 'types']; browserJson.modify( typesPath, Array.from( new Set([ ...((browserJson.get(typesPath) as string[] | undefined) ?? []), ...((serverJson.get(typesPath) as string[] | undefined) ?? []), ]), ), ); // Delete server tsconfig yield deleteFile(serverTsConfig); } // Update server file const ssrMainFile = serverTarget?.options?.['main']; if (typeof ssrMainFile === 'string') { // Do not delete the server main file if it's the same as the browser file. if (buildTarget.options?.browser !== ssrMainFile) { yield deleteFile(ssrMainFile); } yield externalSchematic('@schematics/angular', 'ssr', { project: projectName, }); } } function updateProjects(tree: Tree, context: SchematicContext) { return updateWorkspace((workspace) => { const rules: Rule[] = []; for (const [name, project] of workspace.projects) { if (project.extensions.projectType !== ProjectType.Application) { // Only interested in application projects since these changes only effects application builders continue; } const buildTarget = project.targets.get('build'); if ( !buildTarget || buildTarget.builder === Builders.Application || buildTarget.builder === Builders.BuildApplication ) { continue; } if ( buildTarget.builder !== Builders.BrowserEsbuild && buildTarget.builder !== Builders.Browser ) { context.logger.error( `Cannot update project "${name}" to use the application builder.` + ` Only "${Builders.BrowserEsbuild}" and "${Builders.Browser}" can be automatically migrated.`, ); continue; } const serverTarget = project.targets.get('server'); rules.push(...updateBuildTarget(name, buildTarget, serverTarget, tree, context)); // Delete all redundant targets for (const [key, target] of project.targets) { switch (target.builder) { case Builders.Server: case Builders.Prerender: case Builders.AppShell: case Builders.SsrDevServer: project.targets.delete(key); break; } } // Update CSS/Sass import specifiers const projectSourceRoot = join(project.root, project.sourceRoot ?? 'src'); updateStyleImports(tree, projectSourceRoot, buildTarget); } // Check for @angular-devkit/build-angular Webpack usage let hasAngularDevkitUsage = false; for (const [, target] of allWorkspaceTargets(workspace)) { switch (target.builder) { case Builders.Application: case Builders.DevServer: case Builders.ExtractI18n: case Builders.Karma: case Builders.NgPackagr: // Ignore application, dev server, and i18n extraction for devkit usage check. // Both will be replaced if no other usage is found. continue; } if (target.builder.startsWith('@angular-devkit/build-angular:')) { hasAngularDevkitUsage = true; break; } } // Use @angular/build directly if there is no devkit package usage if (!hasAngularDevkitUsage) { const karmaConfigFiles = new Set<string>(); for (const [, target] of allWorkspaceTargets(workspace)) { switch (target.builder) { case Builders.Application: target.builder = '@angular/build:application'; break; case Builders.DevServer: target.builder = '@angular/build:dev-server'; break; case Builders.ExtractI18n: target.builder = '@angular/build:extract-i18n'; break; case Builders.Karma: target.builder = '@angular/build:karma'; for (const [, karmaOptions] of allTargetOptions(target)) { // Remove "builderMode" option since the builder will always use "application" delete karmaOptions['builderMode']; // Collect custom karma configurations for @angular-devkit/build-angular plugin removal const karmaConfig = karmaOptions['karmaConfig']; if (karmaConfig && typeof karmaConfig === 'string') { karmaConfigFiles.add(karmaConfig); } } break; case Builders.NgPackagr: target.builder = '@angular/build:ng-packagr'; break; } } // Add direct @angular/build dependencies and remove @angular-devkit/build-angular rules.push( addDependency('@angular/build', latestVersions.DevkitBuildAngular, { type: DependencyType.Dev, existing: ExistingBehavior.Replace, }), removeDependency('@angular-devkit/build-angular'), ); // Add less dependency if any projects contain a Less stylesheet file. // This check does not consider Node.js packages due to the performance // cost of searching such a large directory structure. A build time error // will provide instructions to install the package in this case. if (hasLessStylesheets(tree)) { rules.push( addDependency('less', latestVersions['less'], { type: DependencyType.Dev, existing: ExistingBehavior.Skip, }), ); } // Add postcss dependency if any projects have a custom postcss configuration file. // The build system only supports files in a project root or workspace root with // names of either 'postcss.config.json' or '.postcssrc.json'. if (hasPostcssConfiguration(tree, workspace)) { rules.push( addDependency('postcss', latestVersions['postcss'], { type: DependencyType.Dev, existing: ExistingBehavior.Replace, }), ); } for (const karmaConfigFile of karmaConfigFiles) { if (!tree.exists(karmaConfigFile)) { continue; } try { const originalKarmaConfigText = tree.readText(karmaConfigFile); const updatedKarmaConfigText = originalKarmaConfigText .replaceAll(`require('@angular-devkit/build-angular/plugins/karma'),`, '') .replaceAll(`require('@angular-devkit/build-angular/plugins/karma')`, ''); if (updatedKarmaConfigText.includes('@angular-devkit/build-angular/plugins')) { throw new Error( 'Migration does not support found usage of "@angular-devkit/build-angular".', ); } else { tree.overwrite(karmaConfigFile, updatedKarmaConfigText); } } catch (error) { const reason = error instanceof Error ? `Reason: ${error.message}` : ''; context.logger.warn( `Unable to update custom karma configuration file ("${karmaConfigFile}"). ` + reason + '\nReferences to the "@angular-devkit/build-angular" package within the file may need to be removed manually.', ); } } } return chain(rules); }); } function deleteFile(path: string): Rule { return (tree) => { tree.delete(path); }; } function updateJsonFile(path: string, updater: (json: JSONFile) => void): Rule { return (tree, ctx) => { if (tree.exists(path)) { updater(new JSONFile(tree, path)); } else { ctx.logger.info(`Skipping updating '${path}' as it does not exist.`); } }; } /** * Migration main entrypoint */ export default function (): Rule { return chain([ updateProjects, // Delete package.json helper scripts updateJsonFile('package.json', (pkgJson) => ['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) => pkgJson.remove(['scripts', s]), ), ), // Update main tsconfig updateJsonFile('tsconfig.json', (rootJson) => { const module = rootJson.get(['compilerOptions', 'module']); const hasPreserveModule = typeof module === 'string' && module.toLowerCase() === 'preserve'; if (!hasPreserveModule) { rootJson.modify(['compilerOptions', 'esModuleInterop'], true); rootJson.modify(['compilerOptions', 'moduleResolution'], 'bundler'); } rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined); rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined); }), ]); }