Skip to content

Commit

Permalink
feat(@schematics/angular): add optional migration to use application …
Browse files Browse the repository at this point in the history
…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`
  • Loading branch information
alan-agius4 committed Dec 1, 2023
1 parent 03985a4 commit b513d89
Show file tree
Hide file tree
Showing 28 changed files with 1,251 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"version": "17.0.0",
"factory": "./update-17/update-workspace-config",
"description": "Replace deprecated options in 'angular.json'."
},
"use-application-builder": {
"version": "18.0.0",
"factory": "./update-17/use-application-builder",
"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",
"optional": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* @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.io/license
*/

import { workspaces } from '@angular-devkit/core';
import {
Rule,
SchematicContext,
SchematicsException,
chain,
externalSchematic,
} from '@angular-devkit/schematics';
import { dirname } from 'node:path';
import { JSONFile } from '../../utility/json-file';
import { TreeWorkspaceHost, allTargetOptions, getWorkspace } from '../../utility/workspace';
import { Builders, ProjectType } from '../../utility/workspace-models';

export default function (): Rule {
return async (tree, context) => {
const rules: Rule[] = [];
const workspace = await getWorkspace(tree);

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) {
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;
}

// Update builder target and options
buildTarget.builder = Builders.Application;
const hasServerTarget = project.targets.has('server');

for (const [, options] of allTargetOptions(buildTarget, false)) {
// Show warnings for using no longer supported options
if (usesNoLongerSupportedOptions(options, context, name)) {
continue;
}

// Rename and transform options
options['browser'] = options['main'];
if (hasServerTarget && 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']];
}

if (typeof options['outputPath'] === 'string') {
options['outputPath'] = options['outputPath']?.replace(/\/browser\/?$/, '');
}

// Delete removed options
delete options['deployUrl'];
delete options['vendorChunk'];
delete options['commonChunk'];
delete options['resourcesOutputPath'];
delete options['buildOptimizer'];
delete options['main'];
delete options['ngswConfigPath'];
}

// Merge browser and server tsconfig
if (hasServerTarget) {
const browserTsConfig = buildTarget?.options?.tsConfig;
const serverTsConfig = project.targets.get('server')?.options?.tsConfig;

if (typeof browserTsConfig !== 'string') {
throw new SchematicsException(
`Cannot update project "${name}" to use the application builder` +
` as the browser tsconfig cannot be located.`,
);
}

if (typeof serverTsConfig !== 'string') {
throw new SchematicsException(
`Cannot update project "${name}" 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
tree.delete(serverTsConfig);
}

// Update main tsconfig
const rootJson = new JSONFile(tree, 'tsconfig.json');
rootJson.modify(['compilerOptions', 'esModuleInterop'], true);
rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined);
rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined);

// Update server file
const ssrMainFile = project.targets.get('server')?.options?.['main'];
if (typeof ssrMainFile === 'string') {
tree.delete(ssrMainFile);

rules.push(
externalSchematic('@schematics/angular', 'ssr', {
project: name,
skipInstall: true,
}),
);
}

// Delete package.json helper scripts
const pkgJson = new JSONFile(tree, 'package.json');
['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) =>
pkgJson.remove(['scripts', s]),
);

// 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;
}
}
}

// Save workspace changes
await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(tree));

return chain(rules);
};
}

function usesNoLongerSupportedOptions(
{ deployUrl, resourcesOutputPath }: Record<string, unknown>,
context: SchematicContext,
projectName: string,
): boolean {
let hasUsage = false;
if (typeof deployUrl === 'string') {
hasUsage = true;
context.logger.warn(
`Skipping migration for project "${projectName}". "deployUrl" option is not available in the application builder.`,
);
}

if (typeof resourcesOutputPath === 'string' && /^\/?media\/?$/.test(resourcesOutputPath)) {
hasUsage = true;
context.logger.warn(
`Skipping migration for project "${projectName}". "resourcesOutputPath" option is not available in the application builder.` +
`Media files will be output into a "media" directory within the output location.`,
);
}

return hasUsage;
}
4 changes: 2 additions & 2 deletions packages/schematics/angular/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
apply,
applyTemplates,
chain,
externalSchematic,
mergeWith,
move,
schematic,
url,
} from '@angular-devkit/schematics';
import { Schema as ServerOptions } from '../server/schema';
Expand Down Expand Up @@ -273,7 +273,7 @@ export default function (options: SSROptions): Rule {
clientProject.targets.get('build')?.builder === Builders.Application;

return chain([
externalSchematic('@schematics/angular', 'server', {
schematic('server', {
...options,
skipInstall: true,
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/schematics/angular/utility/workspace-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export enum Builders {
AppShell = '@angular-devkit/build-angular:app-shell',
Server = '@angular-devkit/build-angular:server',
Browser = '@angular-devkit/build-angular:browser',
SsrDevServer = '@angular-devkit/build-angular:ssr-dev-server',
Prerender = '@angular-devkit/build-angular:prerender',
BrowserEsbuild = '@angular-devkit/build-angular:browser-esbuild',
Karma = '@angular-devkit/build-angular:karma',
TsLint = '@angular-devkit/build-angular:tslint',
Expand Down
10 changes: 4 additions & 6 deletions packages/schematics/angular/utility/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type TargetDefinition = workspaces.TargetDefinition;
/**
* A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance.
*/
class TreeWorkspaceHost implements workspaces.WorkspaceHost {
export class TreeWorkspaceHost implements workspaces.WorkspaceHost {
constructor(private readonly tree: Tree) {}

async readFile(path: string): Promise<string> {
Expand Down Expand Up @@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost {
export function updateWorkspace(
updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
): Rule {
return async (tree: Tree) => {
const host = new TreeWorkspaceHost(tree);

const { workspace } = await workspaces.readWorkspace(DEFAULT_WORKSPACE_PATH, host);
return async (host: Tree) => {
const workspace = await getWorkspace(host);

const result = await updater(workspace);

await workspaces.writeWorkspace(workspace, host);
await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(host));

return result || noop;
};
Expand Down
42 changes: 42 additions & 0 deletions tests/legacy-cli/e2e/assets/17-ssr-project-webpack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings

# System files
.DS_Store
Thumbs.db
27 changes: 27 additions & 0 deletions tests/legacy-cli/e2e/assets/17-ssr-project-webpack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 17SsrProjectWebpack

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.10.

## Development server

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.

## Code scaffolding

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`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

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.

## Further help

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 comments on commit b513d89

Please sign in to comment.