Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): add initial application builder …
Browse files Browse the repository at this point in the history
…implementation

This commits add the initial application builder schema and build configuration and refactors several files.
  • Loading branch information
alan-agius4 authored and clydin committed Jun 23, 2023
1 parent 4d87b7d commit c05c83b
Show file tree
Hide file tree
Showing 46 changed files with 1,294 additions and 688 deletions.
1 change: 1 addition & 0 deletions packages/angular/cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ ts_library(

# @external_begin
CLI_SCHEMA_DATA = [
"//packages/angular_devkit/build_angular:src/builders/application/schema.json",
"//packages/angular_devkit/build_angular:src/builders/app-shell/schema.json",
"//packages/angular_devkit/build_angular:src/builders/browser/schema.json",
"//packages/angular_devkit/build_angular:src/builders/browser-esbuild/schema.json",
Expand Down
23 changes: 23 additions & 0 deletions packages/angular/cli/lib/config/workspace-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@
"description": "The builder used for this package.",
"not": {
"enum": [
"@angular-devkit/build-angular:application",
"@angular-devkit/build-angular:app-shell",
"@angular-devkit/build-angular:browser",
"@angular-devkit/build-angular:browser-esbuild",
Expand Down Expand Up @@ -385,6 +386,28 @@
"additionalProperties": false,
"required": ["builder"]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:application"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/builders/application/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/builders/application/schema.json"
}
}
}
},
{
"type": "object",
"additionalProperties": false,
Expand Down
16 changes: 12 additions & 4 deletions packages/angular_devkit/build_angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ licenses(["notice"])

package(default_visibility = ["//visibility:public"])

ts_json_schema(
name = "application_schema",
src = "src/builders/application/schema.json",
)

ts_json_schema(
name = "app_shell_schema",
src = "src/builders/app-shell/schema.json",
Expand Down Expand Up @@ -80,6 +85,7 @@ ts_library(
],
) + [
"//packages/angular_devkit/build_angular:src/builders/app-shell/schema.ts",
"//packages/angular_devkit/build_angular:src/builders/application/schema.ts",
"//packages/angular_devkit/build_angular:src/builders/browser-esbuild/schema.ts",
"//packages/angular_devkit/build_angular:src/builders/browser/schema.ts",
"//packages/angular_devkit/build_angular:src/builders/dev-server/schema.ts",
Expand Down Expand Up @@ -290,6 +296,12 @@ ts_library(
)

LARGE_SPECS = {
"application": {
"shards": 10,
"extra_deps": [
"@npm//buffer",
],
},
"app-shell": {
},
"dev-server": {
Expand Down Expand Up @@ -347,10 +359,6 @@ LARGE_SPECS = {
],
},
"browser-esbuild": {
"shards": 10,
"extra_deps": [
"@npm//buffer",
],
},
"jest": {
"extra_deps": [
Expand Down
5 changes: 5 additions & 0 deletions packages/angular_devkit/build_angular/builders.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"$schema": "../architect/src/builders-schema.json",
"builders": {
"application": {
"implementation": "./src/builders/application",
"schema": "./src/builders/application/schema.json",
"description": "Build an application."
},
"app-shell": {
"implementation": "./src/builders/app-shell",
"schema": "./src/builders/app-shell/schema.json",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* @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 { BuilderOutput } from '@angular-devkit/architect';
import type { logging } from '@angular-devkit/core';
import fs from 'node:fs/promises';
import path from 'node:path';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils';
import { assertIsError } from '../../utils/error';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';

export async function* runEsBuildBuildAction(
action: (rebuildState?: RebuildState) => ExecutionResult | Promise<ExecutionResult>,
options: {
workspaceRoot: string;
projectRoot: string;
outputPath: string;
logger: logging.LoggerApi;
cacheOptions: NormalizedCachedOptions;
writeToFileSystem?: boolean;
watch?: boolean;
verbose?: boolean;
progress?: boolean;
deleteOutputPath?: boolean;
poll?: number;
},
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
const {
writeToFileSystem = true,
watch,
poll,
logger,
deleteOutputPath,
cacheOptions,
outputPath,
verbose,
projectRoot,
workspaceRoot,
progress,
} = options;

if (writeToFileSystem) {
// Clean output path if enabled
if (deleteOutputPath) {
if (outputPath === workspaceRoot) {
logger.error('Output path MUST not be workspace root directory!');

return;
}

await fs.rm(outputPath, { force: true, recursive: true, maxRetries: 3 });
}

// Create output directory if needed
try {
await fs.mkdir(outputPath, { recursive: true });
} catch (e) {
assertIsError(e);
logger.error('Unable to create output directory: ' + e.message);

return;
}
}

const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;

// Initial build
let result: ExecutionResult;
try {
result = await withProgress('Building...', () => action());

if (writeToFileSystem) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);

yield result.output;
} else {
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield result.outputWithFiles as any;
}

// Finish if watch mode is not enabled
if (!watch) {
return;
}
} finally {
// Ensure Sass workers are shutdown if not watching
if (!watch) {
shutdownSassWorkerPool();
}
}

if (progress) {
logger.info('Watch mode enabled. Watching for file changes...');
}

// Setup a watcher
const { createWatcher } = await import('../../tools/esbuild/watcher');
const watcher = createWatcher({
polling: typeof poll === 'number',
interval: poll,
ignored: [
// Ignore the output and cache paths to avoid infinite rebuild cycles
outputPath,
cacheOptions.basePath,
// Ignore all node modules directories to avoid excessive file watchers.
// Package changes are handled below by watching manifest and lock files.
'**/node_modules/**',
'**/.*/**',
],
});

// Temporarily watch the entire project
watcher.add(projectRoot);

// Watch workspace for package manager changes
const packageWatchFiles = [
// manifest can affect module resolution
'package.json',
// npm lock file
'package-lock.json',
// pnpm lock file
'pnpm-lock.yaml',
// yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
'yarn.lock',
'.pnp.cjs',
'.pnp.data.json',
];

watcher.add(packageWatchFiles.map((file) => path.join(workspaceRoot, file)));

// Watch locations provided by the initial build result
let previousWatchFiles = new Set(result.watchFiles);
watcher.add(result.watchFiles);

// Wait for changes and rebuild as needed
try {
for await (const changes of watcher) {
if (verbose) {
logger.info(changes.toDebugString());
}

result = await withProgress('Changes detected. Rebuilding...', () =>
action(result.createRebuildState(changes)),
);

// Update watched locations provided by the new build result.
// Add any new locations
watcher.add(result.watchFiles.filter((watchFile) => !previousWatchFiles.has(watchFile)));
const newWatchFiles = new Set(result.watchFiles);
// Remove any old locations
watcher.remove([...previousWatchFiles].filter((watchFile) => !newWatchFiles.has(watchFile)));
previousWatchFiles = newWatchFiles;

if (writeToFileSystem) {
// Write output files
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);

yield result.output;
} else {
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield result.outputWithFiles as any;
}
}
} finally {
// Stop the watcher and cleanup incremental rebuild state
await Promise.allSettled([watcher.close(), result.dispose()]);

shutdownSassWorkerPool();
}
}
Loading

0 comments on commit c05c83b

Please sign in to comment.