Skip to content

Commit b513d89

Browse files
committed
feat(@schematics/angular): add optional migration to use application builder
This commits adds an optional migration to migration existing projects to use the vite and esbuild based application builder. The migration can be opted-in when running `ng update @angular/cli --name=use-application-builder`
1 parent 03985a4 commit b513d89

28 files changed

+1251
-8
lines changed

packages/schematics/angular/migrations/migration-collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
"version": "17.0.0",
1515
"factory": "./update-17/update-workspace-config",
1616
"description": "Replace deprecated options in 'angular.json'."
17+
},
18+
"use-application-builder": {
19+
"version": "18.0.0",
20+
"factory": "./update-17/use-application-builder",
21+
"description": "Migrate application projects using '@angular-devkit/build-angular:browser' and '@angular-devkit/build-angular:browser-esbuild' to use the '@angular-devkit/build-angular:application' builder. Read more about this here: https://angular.dev/tools/cli/esbuild#using-the-application-builder",
22+
"optional": true
1723
}
1824
}
1925
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { workspaces } from '@angular-devkit/core';
10+
import {
11+
Rule,
12+
SchematicContext,
13+
SchematicsException,
14+
chain,
15+
externalSchematic,
16+
} from '@angular-devkit/schematics';
17+
import { dirname } from 'node:path';
18+
import { JSONFile } from '../../utility/json-file';
19+
import { TreeWorkspaceHost, allTargetOptions, getWorkspace } from '../../utility/workspace';
20+
import { Builders, ProjectType } from '../../utility/workspace-models';
21+
22+
export default function (): Rule {
23+
return async (tree, context) => {
24+
const rules: Rule[] = [];
25+
const workspace = await getWorkspace(tree);
26+
27+
for (const [name, project] of workspace.projects) {
28+
if (project.extensions.projectType !== ProjectType.Application) {
29+
// Only interested in application projects since these changes only effects application builders
30+
continue;
31+
}
32+
33+
const buildTarget = project.targets.get('build');
34+
if (!buildTarget || buildTarget.builder === Builders.Application) {
35+
continue;
36+
}
37+
38+
if (
39+
buildTarget.builder !== Builders.BrowserEsbuild &&
40+
buildTarget.builder !== Builders.Browser
41+
) {
42+
context.logger.error(
43+
`Cannot update project "${name}" to use the application builder.` +
44+
` Only "${Builders.BrowserEsbuild}" and "${Builders.Browser}" can be automatically migrated.`,
45+
);
46+
47+
continue;
48+
}
49+
50+
// Update builder target and options
51+
buildTarget.builder = Builders.Application;
52+
const hasServerTarget = project.targets.has('server');
53+
54+
for (const [, options] of allTargetOptions(buildTarget, false)) {
55+
// Show warnings for using no longer supported options
56+
if (usesNoLongerSupportedOptions(options, context, name)) {
57+
continue;
58+
}
59+
60+
// Rename and transform options
61+
options['browser'] = options['main'];
62+
if (hasServerTarget && typeof options['browser'] === 'string') {
63+
options['server'] = dirname(options['browser']) + '/main.server.ts';
64+
}
65+
options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker'];
66+
67+
if (typeof options['polyfills'] === 'string') {
68+
options['polyfills'] = [options['polyfills']];
69+
}
70+
71+
if (typeof options['outputPath'] === 'string') {
72+
options['outputPath'] = options['outputPath']?.replace(/\/browser\/?$/, '');
73+
}
74+
75+
// Delete removed options
76+
delete options['deployUrl'];
77+
delete options['vendorChunk'];
78+
delete options['commonChunk'];
79+
delete options['resourcesOutputPath'];
80+
delete options['buildOptimizer'];
81+
delete options['main'];
82+
delete options['ngswConfigPath'];
83+
}
84+
85+
// Merge browser and server tsconfig
86+
if (hasServerTarget) {
87+
const browserTsConfig = buildTarget?.options?.tsConfig;
88+
const serverTsConfig = project.targets.get('server')?.options?.tsConfig;
89+
90+
if (typeof browserTsConfig !== 'string') {
91+
throw new SchematicsException(
92+
`Cannot update project "${name}" to use the application builder` +
93+
` as the browser tsconfig cannot be located.`,
94+
);
95+
}
96+
97+
if (typeof serverTsConfig !== 'string') {
98+
throw new SchematicsException(
99+
`Cannot update project "${name}" to use the application builder` +
100+
` as the server tsconfig cannot be located.`,
101+
);
102+
}
103+
104+
const browserJson = new JSONFile(tree, browserTsConfig);
105+
const serverJson = new JSONFile(tree, serverTsConfig);
106+
107+
const filesPath = ['files'];
108+
109+
const files = new Set([
110+
...((browserJson.get(filesPath) as string[] | undefined) ?? []),
111+
...((serverJson.get(filesPath) as string[] | undefined) ?? []),
112+
]);
113+
114+
// Server file will be added later by the means of the ssr schematic.
115+
files.delete('server.ts');
116+
117+
browserJson.modify(filesPath, Array.from(files));
118+
119+
const typesPath = ['compilerOptions', 'types'];
120+
browserJson.modify(
121+
typesPath,
122+
Array.from(
123+
new Set([
124+
...((browserJson.get(typesPath) as string[] | undefined) ?? []),
125+
...((serverJson.get(typesPath) as string[] | undefined) ?? []),
126+
]),
127+
),
128+
);
129+
130+
// Delete server tsconfig
131+
tree.delete(serverTsConfig);
132+
}
133+
134+
// Update main tsconfig
135+
const rootJson = new JSONFile(tree, 'tsconfig.json');
136+
rootJson.modify(['compilerOptions', 'esModuleInterop'], true);
137+
rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined);
138+
rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined);
139+
140+
// Update server file
141+
const ssrMainFile = project.targets.get('server')?.options?.['main'];
142+
if (typeof ssrMainFile === 'string') {
143+
tree.delete(ssrMainFile);
144+
145+
rules.push(
146+
externalSchematic('@schematics/angular', 'ssr', {
147+
project: name,
148+
skipInstall: true,
149+
}),
150+
);
151+
}
152+
153+
// Delete package.json helper scripts
154+
const pkgJson = new JSONFile(tree, 'package.json');
155+
['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) =>
156+
pkgJson.remove(['scripts', s]),
157+
);
158+
159+
// Delete all redundant targets
160+
for (const [key, target] of project.targets) {
161+
switch (target.builder) {
162+
case Builders.Server:
163+
case Builders.Prerender:
164+
case Builders.AppShell:
165+
case Builders.SsrDevServer:
166+
project.targets.delete(key);
167+
break;
168+
}
169+
}
170+
}
171+
172+
// Save workspace changes
173+
await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(tree));
174+
175+
return chain(rules);
176+
};
177+
}
178+
179+
function usesNoLongerSupportedOptions(
180+
{ deployUrl, resourcesOutputPath }: Record<string, unknown>,
181+
context: SchematicContext,
182+
projectName: string,
183+
): boolean {
184+
let hasUsage = false;
185+
if (typeof deployUrl === 'string') {
186+
hasUsage = true;
187+
context.logger.warn(
188+
`Skipping migration for project "${projectName}". "deployUrl" option is not available in the application builder.`,
189+
);
190+
}
191+
192+
if (typeof resourcesOutputPath === 'string' && /^\/?media\/?$/.test(resourcesOutputPath)) {
193+
hasUsage = true;
194+
context.logger.warn(
195+
`Skipping migration for project "${projectName}". "resourcesOutputPath" option is not available in the application builder.` +
196+
`Media files will be output into a "media" directory within the output location.`,
197+
);
198+
}
199+
200+
return hasUsage;
201+
}

packages/schematics/angular/ssr/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
apply,
1515
applyTemplates,
1616
chain,
17-
externalSchematic,
1817
mergeWith,
1918
move,
19+
schematic,
2020
url,
2121
} from '@angular-devkit/schematics';
2222
import { Schema as ServerOptions } from '../server/schema';
@@ -273,7 +273,7 @@ export default function (options: SSROptions): Rule {
273273
clientProject.targets.get('build')?.builder === Builders.Application;
274274

275275
return chain([
276-
externalSchematic('@schematics/angular', 'server', {
276+
schematic('server', {
277277
...options,
278278
skipInstall: true,
279279
}),

packages/schematics/angular/utility/workspace-models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export enum Builders {
2222
AppShell = '@angular-devkit/build-angular:app-shell',
2323
Server = '@angular-devkit/build-angular:server',
2424
Browser = '@angular-devkit/build-angular:browser',
25+
SsrDevServer = '@angular-devkit/build-angular:ssr-dev-server',
26+
Prerender = '@angular-devkit/build-angular:prerender',
2527
BrowserEsbuild = '@angular-devkit/build-angular:browser-esbuild',
2628
Karma = '@angular-devkit/build-angular:karma',
2729
TsLint = '@angular-devkit/build-angular:tslint',

packages/schematics/angular/utility/workspace.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type TargetDefinition = workspaces.TargetDefinition;
2020
/**
2121
* A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance.
2222
*/
23-
class TreeWorkspaceHost implements workspaces.WorkspaceHost {
23+
export class TreeWorkspaceHost implements workspaces.WorkspaceHost {
2424
constructor(private readonly tree: Tree) {}
2525

2626
async readFile(path: string): Promise<string> {
@@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost {
5858
export function updateWorkspace(
5959
updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
6060
): Rule {
61-
return async (tree: Tree) => {
62-
const host = new TreeWorkspaceHost(tree);
63-
64-
const { workspace } = await workspaces.readWorkspace(DEFAULT_WORKSPACE_PATH, host);
61+
return async (host: Tree) => {
62+
const workspace = await getWorkspace(host);
6563

6664
const result = await updater(workspace);
6765

68-
await workspaces.writeWorkspace(workspace, host);
66+
await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(host));
6967

7068
return result || noop;
7169
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# See http://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# Compiled output
4+
/dist
5+
/tmp
6+
/out-tsc
7+
/bazel-out
8+
9+
# Node
10+
/node_modules
11+
npm-debug.log
12+
yarn-error.log
13+
14+
# IDEs and editors
15+
.idea/
16+
.project
17+
.classpath
18+
.c9/
19+
*.launch
20+
.settings/
21+
*.sublime-workspace
22+
23+
# Visual Studio Code
24+
.vscode/*
25+
!.vscode/settings.json
26+
!.vscode/tasks.json
27+
!.vscode/launch.json
28+
!.vscode/extensions.json
29+
.history/*
30+
31+
# Miscellaneous
32+
/.angular/cache
33+
.sass-cache/
34+
/connect.lock
35+
/coverage
36+
/libpeerconnection.log
37+
testem.log
38+
/typings
39+
40+
# System files
41+
.DS_Store
42+
Thumbs.db
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# 17SsrProjectWebpack
2+
3+
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.10.
4+
5+
## Development server
6+
7+
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
8+
9+
## Code scaffolding
10+
11+
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12+
13+
## Build
14+
15+
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
16+
17+
## Running unit tests
18+
19+
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20+
21+
## Running end-to-end tests
22+
23+
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
24+
25+
## Further help
26+
27+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

0 commit comments

Comments
 (0)