Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): initial development server for e…
Browse files Browse the repository at this point in the history
…sbuild-based builder

When using the experimental esbuild-based browser application builder, the preexisting `dev-server` builder
can now be used to execute the `ng serve` command with an esbuild bundled application. The `dev-server` builder
provides an alternate development server that will execute the `browser-esbuild` builder to build the application
and then serve the output files within a development server with live reload capabilities.
This is an initial integration of the development server. It is not yet fully optimized and all features
may not yet be supported. SSL, in particular, does not yet work.

If already using the esbuild-based builder, no additional changes to the Angular configuration are required.
The `dev-server` builder will automatically detect the application builder and use the relevent development
server implementation. As the esbuild-based browser application builders is currently experimental, using
the development server in this mode is also considered experimental.
  • Loading branch information
clydin authored and angular-robot[bot] committed Apr 4, 2023
1 parent aae34fc commit 8c55030
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 17 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
"typescript": "~5.0.2",
"verdaccio": "5.22.1",
"verdaccio-auth-memory": "^10.0.0",
"vite": "4.2.1",
"webpack": "5.76.2",
"webpack-dev-middleware": "6.0.2",
"webpack-dev-server": "4.13.1",
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ ts_library(
"@npm//tree-kill",
"@npm//tslib",
"@npm//typescript",
"@npm//vite",
"@npm//webpack",
"@npm//webpack-dev-middleware",
"@npm//webpack-dev-server",
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"text-table": "0.2.0",
"tree-kill": "1.2.2",
"tslib": "2.5.0",
"vite": "4.2.1",
"webpack": "5.76.2",
"webpack-dev-middleware": "6.0.2",
"webpack-dev-server": "4.13.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,12 @@ export async function* buildEsbuildBrowser(
infrastructureSettings?: {
write?: boolean;
},
): AsyncIterable<BuilderOutput & { outputFiles?: OutputFile[] }> {
): AsyncIterable<
BuilderOutput & {
outputFiles?: OutputFile[];
assetFiles?: { source: string; destination: string }[];
}
> {
// Inform user of experimental status of builder and options
logExperimentalWarnings(userOptions, context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/

import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import type { BuilderContext } from '@angular-devkit/architect';
import { EMPTY, Observable, defer, switchMap } from 'rxjs';
import { ExecutionTransformer } from '../../transforms';
import type { ExecutionTransformer } from '../../transforms';
import { checkPort } from '../../utils/check-port';
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
import type { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
import { normalizeOptions } from './options';
import { Schema as DevServerBuilderOptions } from './schema';
import { DevServerBuilderOutput, serveWebpackBrowser } from './webpack-server';
import type { Schema as DevServerBuilderOptions } from './schema';
import type { DevServerBuilderOutput } from './webpack-server';

/**
* A Builder that executes a development server based on the provided browser target option.
Expand Down Expand Up @@ -44,16 +44,19 @@ export function execute(

return defer(() => initialize(options, projectName, context)).pipe(
switchMap(({ builderName, normalizedOptions }) => {
// Issue a warning that the dev-server does not currently support the experimental esbuild-
// based builder and will use Webpack.
// Use vite-based development server for esbuild-based builds
if (builderName === '@angular-devkit/build-angular:browser-esbuild') {
context.logger.warn(
'WARNING: The experimental esbuild-based builder is not currently supported ' +
'by the dev-server. The stable Webpack-based builder will be used instead.',
return defer(() => import('./vite-server')).pipe(
switchMap(({ serveWithVite }) => serveWithVite(normalizedOptions, builderName, context)),
);
}

return serveWebpackBrowser(normalizedOptions, builderName, context, transforms);
// Use Webpack for all other browser targets
return defer(() => import('./webpack-server')).pipe(
switchMap(({ serveWebpackBrowser }) =>
serveWebpackBrowser(normalizedOptions, builderName, context, transforms),
),
);
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @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 { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { extname, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { assertIsError } from '../../utils/error';
import { loadEsmModule } from '../../utils/load-esm';

export async function loadProxyConfiguration(root: string, proxyConfig: string | undefined) {
if (!proxyConfig) {
return undefined;
}

const proxyPath = resolve(root, proxyConfig);

if (!existsSync(proxyPath)) {
throw new Error(`Proxy configuration file ${proxyPath} does not exist.`);
}

switch (extname(proxyPath)) {
case '.json': {
const content = await readFile(proxyPath, 'utf-8');

const { parse, printParseErrorCode } = await import('jsonc-parser');
const parseErrors: import('jsonc-parser').ParseError[] = [];
const proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true });

if (parseErrors.length > 0) {
let errorMessage = `Proxy configuration file ${proxyPath} contains parse errors:`;
for (const parseError of parseErrors) {
const { line, column } = getJsonErrorLineColumn(parseError.offset, content);
errorMessage += `\n[${line}, ${column}] ${printParseErrorCode(parseError.error)}`;
}
throw new Error(errorMessage);
}

return proxyConfiguration;
}
case '.mjs':
// Load the ESM configuration file using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default;
case '.cjs':
return require(proxyPath);
default:
// The file could be either CommonJS or ESM.
// CommonJS is tried first then ESM if loading fails.
try {
return require(proxyPath);
} catch (e) {
assertIsError(e);
if (e.code === 'ERR_REQUIRE_ESM') {
// Load the ESM configuration file using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default;
}

throw e;
}
}
}

/**
* Calculates the line and column for an error offset in the content of a JSON file.
* @param location The offset error location from the beginning of the content.
* @param content The full content of the file containing the error.
* @returns An object containing the line and column
*/
function getJsonErrorLineColumn(offset: number, content: string) {
if (offset === 0) {
return { line: 1, column: 1 };
}

let line = 0;
let position = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
++line;

const nextNewline = content.indexOf('\n', position);

This comment has been minimized.

Copy link
@Lonli-Lokli

Lonli-Lokli Apr 4, 2023

I think it's possible to have \r\n as new line delimiters

if (nextNewline === -1 || nextNewline > offset) {
break;
}

position = nextNewline + 1;
}

return { line, column: offset - position + 1 };
}
Loading

0 comments on commit 8c55030

Please sign in to comment.