Skip to content

Commit 095f5ab

Browse files
alan-agius4dgp1130
authored andcommittedJul 5, 2023
feat(@angular-devkit/build-angular): add initial support for server bundle generation using esbuild
This commit adds initial support to generate the server bundle using esbuild as the underlying bundler.
1 parent 333da08 commit 095f5ab

File tree

15 files changed

+538
-186
lines changed

15 files changed

+538
-186
lines changed
 

‎packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99
import { BuilderContext } from '@angular-devkit/architect';
1010
import { SourceFileCache } from '../../tools/esbuild/angular/compiler-plugin';
11-
import { createCodeBundleOptions } from '../../tools/esbuild/application-code-bundle';
11+
import { createBrowserCodeBundleOptions } from '../../tools/esbuild/browser-code-bundle';
1212
import { BundlerContext } from '../../tools/esbuild/bundler-context';
1313
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1414
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
1515
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
1616
import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles';
1717
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
1818
import { extractLicenses } from '../../tools/esbuild/license-extractor';
19+
import { createServerCodeBundleOptions } from '../../tools/esbuild/server-code-bundle';
1920
import {
2021
calculateEstimatedTransferSizes,
2122
logBuildStats,
@@ -39,6 +40,7 @@ export async function executeBuild(
3940
workspaceRoot,
4041
serviceWorker,
4142
optimizationOptions,
43+
serverEntryPoint,
4244
assets,
4345
indexHtmlOptions,
4446
cacheOptions,
@@ -55,12 +57,12 @@ export async function executeBuild(
5557
if (bundlerContexts === undefined) {
5658
bundlerContexts = [];
5759

58-
// Application code
60+
// Browser application code
5961
bundlerContexts.push(
6062
new BundlerContext(
6163
workspaceRoot,
6264
!!options.watch,
63-
createCodeBundleOptions(options, target, browsers, codeBundleCache),
65+
createBrowserCodeBundleOptions(options, target, browsers, codeBundleCache),
6466
),
6567
);
6668

@@ -93,6 +95,25 @@ export async function executeBuild(
9395
}
9496
}
9597
}
98+
99+
// Server application code
100+
if (serverEntryPoint) {
101+
bundlerContexts.push(
102+
new BundlerContext(
103+
workspaceRoot,
104+
!!options.watch,
105+
createServerCodeBundleOptions(
106+
options,
107+
// NOTE: earlier versions of Node.js are not supported due to unsafe promise patching.
108+
// See: https://github.com/angular/angular/pull/50552#issue-1737967592
109+
[...target, 'node18.13'],
110+
browsers,
111+
codeBundleCache,
112+
),
113+
() => false,
114+
),
115+
);
116+
}
96117
}
97118

98119
const bundlingResult = await BundlerContext.bundleAll(bundlerContexts);

‎packages/angular_devkit/build_angular/src/builders/application/options.ts

+27-21
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type NormalizedApplicationBuildOptions = Awaited<ReturnType<typeof normal
2525
/** Internal options hidden from builder schema but available when invoked programmatically. */
2626
interface InternalOptions {
2727
/**
28-
* Entry points to use for the compilation. Incompatible with `main`, which must not be provided. May be relative or absolute paths.
28+
* Entry points to use for the compilation. Incompatible with `browser`, which must not be provided. May be relative or absolute paths.
2929
* If given a relative path, it is resolved relative to the current workspace and will generate an output at the same relative location
3030
* in the output directory. If given an absolute path, the output will be generated in the root of the output directory with the same base
3131
* name.
@@ -42,7 +42,7 @@ interface InternalOptions {
4242
externalPackages?: boolean;
4343
}
4444

45-
/** Full set of options for `browser-esbuild` builder. */
45+
/** Full set of options for `application` builder. */
4646
export type ApplicationBuilderInternalOptions = Omit<
4747
ApplicationBuilderOptions & InternalOptions,
4848
'browser'
@@ -164,6 +164,13 @@ export async function normalizeOptions(
164164
};
165165
}
166166

167+
let serverEntryPoint: string | undefined;
168+
if (options.server) {
169+
serverEntryPoint = path.join(workspaceRoot, options.server);
170+
} else if (options.server === '') {
171+
throw new Error('`server` option cannot be an empty string.');
172+
}
173+
167174
// Initial options to keep
168175
const {
169176
allowedCommonJsDependencies,
@@ -182,7 +189,6 @@ export async function normalizeOptions(
182189
stylePreprocessorOptions,
183190
subresourceIntegrity,
184191
verbose,
185-
server,
186192
watch,
187193
progress = true,
188194
externalPackages,
@@ -210,7 +216,7 @@ export async function normalizeOptions(
210216
preserveSymlinks: preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'),
211217
stylePreprocessorOptions,
212218
subresourceIntegrity,
213-
server: !!server && path.join(workspaceRoot, server),
219+
serverEntryPoint,
214220
verbose,
215221
watch,
216222
workspaceRoot,
@@ -233,39 +239,39 @@ export async function normalizeOptions(
233239
}
234240

235241
/**
236-
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `main` option which defines a
237-
* single entry point. However, we also want to support multiple entry points as an internal option. The two options are mutually exclusive
238-
* and if `main` is provided it will be used as the sole entry point. If `entryPoints` are provided, they will be used as the set of entry
239-
* points.
242+
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser`
243+
* option which defines a single entry point. However, we also want to support multiple entry points as an internal option.
244+
* The two options are mutually exclusive and if `browser` is provided it will be used as the sole entry point.
245+
* If `entryPoints` are provided, they will be used as the set of entry points.
240246
*
241247
* @param workspaceRoot Path to the root of the Angular workspace.
242-
* @param main The `main` option pointing at the application entry point. While required per the schema file, it may be omitted by
248+
* @param browser The `browser` option pointing at the application entry point. While required per the schema file, it may be omitted by
243249
* programmatic usages of `browser-esbuild`.
244250
* @param entryPoints Set of entry points to use if provided.
245251
* @returns An object mapping entry point names to their file paths.
246252
*/
247253
function normalizeEntryPoints(
248254
workspaceRoot: string,
249-
main: string | undefined,
255+
browser: string | undefined,
250256
entryPoints: Set<string> = new Set(),
251257
): Record<string, string> {
252-
if (main === '') {
253-
throw new Error('`main` option cannot be an empty string.');
258+
if (browser === '') {
259+
throw new Error('`browser` option cannot be an empty string.');
254260
}
255261

256-
// `main` and `entryPoints` are mutually exclusive.
257-
if (main && entryPoints.size > 0) {
258-
throw new Error('Only one of `main` or `entryPoints` may be provided.');
262+
// `browser` and `entryPoints` are mutually exclusive.
263+
if (browser && entryPoints.size > 0) {
264+
throw new Error('Only one of `browser` or `entryPoints` may be provided.');
259265
}
260-
if (!main && entryPoints.size === 0) {
266+
if (!browser && entryPoints.size === 0) {
261267
// Schema should normally reject this case, but programmatic usages of the builder might make this mistake.
262-
throw new Error('Either `main` or at least one `entryPoints` value must be provided.');
268+
throw new Error('Either `browser` or at least one `entryPoints` value must be provided.');
263269
}
264270

265-
// Schema types force `main` to always be provided, but it may be omitted when the builder is invoked programmatically.
266-
if (main) {
267-
// Use `main` alone.
268-
return { 'main': path.join(workspaceRoot, main) };
271+
// Schema types force `browser` to always be provided, but it may be omitted when the builder is invoked programmatically.
272+
if (browser) {
273+
// Use `browser` alone.
274+
return { 'main': path.join(workspaceRoot, browser) };
269275
} else {
270276
// Use `entryPoints` alone.
271277
const entryPointPaths: Record<string, string> = {};

‎packages/angular_devkit/build_angular/src/builders/application/tests/options/browser_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
6666
browser: '',
6767
});
6868

69-
const { result, error } = await harness.executeOnce();
69+
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
7070
expect(result).toBeUndefined();
7171

7272
expect(error?.message).toContain('cannot be an empty string');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 { buildApplication } from '../../index';
10+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
13+
beforeEach(async () => {
14+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
15+
const tsConfig = JSON.parse(content);
16+
tsConfig.files ??= [];
17+
tsConfig.files.push('main.server.ts');
18+
19+
return JSON.stringify(tsConfig);
20+
});
21+
});
22+
23+
describe('Option: "server"', () => {
24+
it('uses a provided TypeScript file', async () => {
25+
harness.useTarget('build', {
26+
...BASE_OPTIONS,
27+
server: 'src/main.server.ts',
28+
});
29+
30+
const { result } = await harness.executeOnce();
31+
expect(result?.success).toBeTrue();
32+
33+
harness.expectFile('dist/server.mjs').toExist();
34+
harness.expectFile('dist/main.js').toExist();
35+
});
36+
37+
it('uses a provided JavaScript file', async () => {
38+
await harness.writeFile('src/server.js', `console.log('server');`);
39+
40+
harness.useTarget('build', {
41+
...BASE_OPTIONS,
42+
server: 'src/server.js',
43+
});
44+
45+
const { result } = await harness.executeOnce();
46+
expect(result?.success).toBeTrue();
47+
48+
harness.expectFile('dist/server.mjs').content.toContain('console.log("server")');
49+
});
50+
51+
it('fails and shows an error when file does not exist', async () => {
52+
harness.useTarget('build', {
53+
...BASE_OPTIONS,
54+
server: 'src/missing.ts',
55+
});
56+
57+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
58+
59+
expect(result?.success).toBeFalse();
60+
expect(logs).toContain(
61+
jasmine.objectContaining({ message: jasmine.stringMatching('Could not resolve "') }),
62+
);
63+
64+
harness.expectFile('dist/main.js').toNotExist();
65+
harness.expectFile('dist/server.mjs').toNotExist();
66+
});
67+
68+
it('throws an error when given an empty string', async () => {
69+
harness.useTarget('build', {
70+
...BASE_OPTIONS,
71+
server: '',
72+
});
73+
74+
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
75+
expect(result).toBeUndefined();
76+
77+
expect(error?.message).toContain('cannot be an empty string');
78+
});
79+
80+
it('resolves an absolute path as relative inside the workspace root', async () => {
81+
await harness.writeFile('file.mjs', `console.log('Hello!');`);
82+
83+
harness.useTarget('build', {
84+
...BASE_OPTIONS,
85+
server: '/file.mjs',
86+
});
87+
88+
const { result } = await harness.executeOnce();
89+
expect(result?.success).toBeTrue();
90+
91+
// Always uses the name `server.mjs` for the `server` option.
92+
harness.expectFile('dist/server.mjs').toExist();
93+
});
94+
});
95+
});

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/main_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => {
6666
main: '',
6767
});
6868

69-
const { result, error } = await harness.executeOnce();
69+
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
7070
expect(result).toBeUndefined();
7171

7272
expect(error?.message).toContain('cannot be an empty string');

‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/angular-compilation.ts ‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/compilation/angular-compilation.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import type ng from '@angular/compiler-cli';
1010
import type ts from 'typescript';
11-
import { loadEsmModule } from '../../../utils/load-esm';
12-
import { profileSync } from '../profiling';
13-
import type { AngularHostOptions } from './angular-host';
11+
import { loadEsmModule } from '../../../../utils/load-esm';
12+
import { profileSync } from '../../profiling';
13+
import type { AngularHostOptions } from '../angular-host';
1414

1515
export interface EmitFileResult {
1616
filename: string;

‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/aot-compilation.ts ‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/compilation/aot-compilation.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
import type ng from '@angular/compiler-cli';
1010
import assert from 'node:assert';
1111
import ts from 'typescript';
12-
import { profileAsync, profileSync } from '../profiling';
13-
import { AngularCompilation, EmitFileResult } from './angular-compilation';
12+
import { profileAsync, profileSync } from '../../profiling';
1413
import {
1514
AngularHostOptions,
1615
createAngularCompilerHost,
1716
ensureSourceFileVersions,
18-
} from './angular-host';
17+
} from '../angular-host';
18+
import { AngularCompilation, EmitFileResult } from './angular-compilation';
1919

2020
// Temporary deep import for transformer support
2121
// TODO: Move these to a private exports location or move the implementation into this package.

‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-compilation.ts ‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/compilation/jit-compilation.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import type ng from '@angular/compiler-cli';
1010
import assert from 'node:assert';
1111
import ts from 'typescript';
12-
import { profileSync } from '../profiling';
12+
import { profileSync } from '../../profiling';
13+
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
14+
import { createJitResourceTransformer } from '../jit-resource-transformer';
1315
import { AngularCompilation, EmitFileResult } from './angular-compilation';
14-
import { AngularHostOptions, createAngularCompilerHost } from './angular-host';
15-
import { createJitResourceTransformer } from './jit-resource-transformer';
1616

1717
class JitCompilationState {
1818
constructor(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 type ng from '@angular/compiler-cli';
10+
import ts from 'typescript';
11+
import { AngularHostOptions } from '../angular-host';
12+
import { AngularCompilation } from './angular-compilation';
13+
14+
export class NoopCompilation extends AngularCompilation {
15+
async initialize(
16+
tsconfig: string,
17+
hostOptions: AngularHostOptions,
18+
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
19+
): Promise<{
20+
affectedFiles: ReadonlySet<ts.SourceFile>;
21+
compilerOptions: ng.CompilerOptions;
22+
referencedFiles: readonly string[];
23+
}> {
24+
// Load the compiler configuration and transform as needed
25+
const { options: originalCompilerOptions } = await this.loadConfiguration(tsconfig);
26+
const compilerOptions =
27+
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
28+
29+
return { affectedFiles: new Set(), compilerOptions, referencedFiles: [] };
30+
}
31+
32+
collectDiagnostics(): never {
33+
throw new Error('Not available when using noop compilation.');
34+
}
35+
36+
emitAffectedFiles(): never {
37+
throw new Error('Not available when using noop compilation.');
38+
}
39+
}

‎packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import {
2929
resetCumulativeDurations,
3030
} from '../profiling';
3131
import { BundleStylesheetOptions, bundleComponentStylesheet } from '../stylesheets/bundle-options';
32-
import { AngularCompilation } from './angular-compilation';
3332
import { AngularHostOptions } from './angular-host';
34-
import { AotCompilation } from './aot-compilation';
33+
import { AngularCompilation } from './compilation/angular-compilation';
34+
import { AotCompilation } from './compilation/aot-compilation';
35+
import { JitCompilation } from './compilation/jit-compilation';
36+
import { NoopCompilation } from './compilation/noop-compilation';
3537
import { convertTypeScriptDiagnostic } from './diagnostics';
36-
import { JitCompilation } from './jit-compilation';
3738
import { setupJitPluginCallbacks } from './jit-plugin-callbacks';
3839

3940
const USING_WINDOWS = platform() === 'win32';
@@ -73,18 +74,31 @@ export interface CompilerPluginOptions {
7374
sourcemap: boolean;
7475
tsconfig: string;
7576
jit?: boolean;
77+
/** Skip TypeScript compilation setup. This is useful to re-use the TypeScript compilation from another plugin. */
78+
noopTypeScriptCompilation?: boolean;
7679
advancedOptimizations?: boolean;
7780
thirdPartySourcemaps?: boolean;
7881
fileReplacements?: Record<string, string>;
7982
sourceFileCache?: SourceFileCache;
8083
loadResultCache?: LoadResultCache;
8184
}
8285

86+
// TODO: find a better way to unblock TS compilation of server bundles.
87+
let TS_COMPILATION_READY: Promise<void> | undefined;
88+
8389
// eslint-disable-next-line max-lines-per-function
8490
export function createCompilerPlugin(
8591
pluginOptions: CompilerPluginOptions,
8692
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
8793
): Plugin {
94+
let resolveCompilationReady: (() => void) | undefined;
95+
96+
if (!pluginOptions.noopTypeScriptCompilation) {
97+
TS_COMPILATION_READY = new Promise<void>((resolve) => {
98+
resolveCompilationReady = resolve;
99+
});
100+
}
101+
88102
return {
89103
name: 'angular-compiler',
90104
// eslint-disable-next-line max-lines-per-function
@@ -132,7 +146,9 @@ export function createCompilerPlugin(
132146
let stylesheetMetafiles: Metafile[];
133147

134148
// Create new reusable compilation for the appropriate mode based on the `jit` plugin option
135-
const compilation: AngularCompilation = pluginOptions.jit
149+
const compilation: AngularCompilation = pluginOptions.noopTypeScriptCompilation
150+
? new NoopCompilation()
151+
: pluginOptions.jit
136152
? new JitCompilation()
137153
: new AotCompilation();
138154

@@ -239,6 +255,12 @@ export function createCompilerPlugin(
239255
});
240256
shouldTsIgnoreJs = !allowJs;
241257

258+
if (compilation instanceof NoopCompilation) {
259+
await TS_COMPILATION_READY;
260+
261+
return result;
262+
}
263+
242264
profileSync('NG_DIAGNOSTICS_TOTAL', () => {
243265
for (const diagnostic of compilation.collectDiagnostics()) {
244266
const message = convertTypeScriptDiagnostic(diagnostic);
@@ -265,6 +287,9 @@ export function createCompilerPlugin(
265287
// Reset the setup warnings so that they are only shown during the first build.
266288
setupWarnings = undefined;
267289

290+
// TODO: find a better way to unblock TS compilation of server bundles.
291+
resolveCompilationReady?.();
292+
268293
return result;
269294
});
270295

‎packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

-147
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 type { BuildOptions } from 'esbuild';
10+
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
11+
import { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin';
12+
import { createCompilerPluginOptions } from './compiler-plugin';
13+
import { createExternalPackagesPlugin } from './external-packages-plugin';
14+
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
15+
import {
16+
getEsBuildCommonOptions as getEsBuildCodeBundleCommonOptions,
17+
getFeatureSupport,
18+
} from './utils';
19+
import { createVirtualModulePlugin } from './virtual-module-plugin';
20+
21+
export function createBrowserCodeBundleOptions(
22+
options: NormalizedApplicationBuildOptions,
23+
target: string[],
24+
browsers: string[],
25+
sourceFileCache?: SourceFileCache,
26+
): BuildOptions {
27+
const { workspaceRoot, entryPoints, outputNames, jit } = options;
28+
29+
const { pluginOptions, styleOptions } = createCompilerPluginOptions(
30+
options,
31+
target,
32+
browsers,
33+
sourceFileCache,
34+
);
35+
36+
const buildOptions: BuildOptions = {
37+
...getEsBuildCodeBundleCommonOptions(options),
38+
platform: 'browser',
39+
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
40+
// match and the ES5 distribution would be bundled and ends up breaking at
41+
// runtime with the RxJS testing library.
42+
// More details: https://github.com/angular/angular-cli/issues/25405.
43+
mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
44+
entryNames: outputNames.bundles,
45+
entryPoints,
46+
target,
47+
supported: getFeatureSupport(target),
48+
plugins: [
49+
createSourcemapIngorelistPlugin(),
50+
createCompilerPlugin(
51+
// JS/TS options
52+
pluginOptions,
53+
// Component stylesheet options
54+
styleOptions,
55+
),
56+
],
57+
};
58+
59+
if (options.externalPackages) {
60+
buildOptions.plugins ??= [];
61+
buildOptions.plugins.push(createExternalPackagesPlugin());
62+
}
63+
64+
const polyfills = options.polyfills ? [...options.polyfills] : [];
65+
if (jit) {
66+
polyfills.push('@angular/compiler');
67+
}
68+
69+
if (polyfills?.length) {
70+
const namespace = 'angular:polyfills';
71+
buildOptions.entryPoints = {
72+
...buildOptions.entryPoints,
73+
'polyfills': namespace,
74+
};
75+
76+
buildOptions.plugins?.unshift(
77+
createVirtualModulePlugin({
78+
namespace,
79+
loadContent: () => ({
80+
contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'),
81+
loader: 'js',
82+
resolveDir: workspaceRoot,
83+
}),
84+
}),
85+
);
86+
}
87+
88+
return buildOptions;
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 { NormalizedApplicationBuildOptions } from '../../builders/application/options';
10+
import type { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin';
11+
12+
type CreateCompilerPluginParameters = Parameters<typeof createCompilerPlugin>;
13+
14+
export function createCompilerPluginOptions(
15+
options: NormalizedApplicationBuildOptions,
16+
target: string[],
17+
browsers: string[],
18+
sourceFileCache?: SourceFileCache,
19+
): {
20+
pluginOptions: CreateCompilerPluginParameters[0];
21+
styleOptions: CreateCompilerPluginParameters[1];
22+
} {
23+
const {
24+
workspaceRoot,
25+
optimizationOptions,
26+
sourcemapOptions,
27+
tsconfig,
28+
outputNames,
29+
fileReplacements,
30+
externalDependencies,
31+
preserveSymlinks,
32+
stylePreprocessorOptions,
33+
advancedOptimizations,
34+
inlineStyleLanguage,
35+
jit,
36+
tailwindConfiguration,
37+
} = options;
38+
39+
return {
40+
// JS/TS options
41+
pluginOptions: {
42+
sourcemap: !!sourcemapOptions.scripts,
43+
thirdPartySourcemaps: sourcemapOptions.vendor,
44+
tsconfig,
45+
jit,
46+
advancedOptimizations,
47+
fileReplacements,
48+
sourceFileCache,
49+
loadResultCache: sourceFileCache?.loadResultCache,
50+
},
51+
// Component stylesheet options
52+
styleOptions: {
53+
workspaceRoot,
54+
optimization: !!optimizationOptions.styles.minify,
55+
sourcemap:
56+
// Hidden component stylesheet sourcemaps are inaccessible which is effectively
57+
// the same as being disabled. Disabling has the advantage of avoiding the overhead
58+
// of sourcemap processing.
59+
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
60+
outputNames,
61+
includePaths: stylePreprocessorOptions?.includePaths,
62+
externalDependencies,
63+
target,
64+
inlineStyleLanguage,
65+
preserveSymlinks,
66+
browsers,
67+
tailwindConfiguration,
68+
},
69+
};
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 type { BuildOptions } from 'esbuild';
10+
import assert from 'node:assert';
11+
import path from 'node:path';
12+
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
13+
import { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin';
14+
import { createCompilerPluginOptions } from './compiler-plugin';
15+
import { createExternalPackagesPlugin } from './external-packages-plugin';
16+
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
17+
import { getEsBuildCommonOptions, getFeatureSupport } from './utils';
18+
import { createVirtualModulePlugin } from './virtual-module-plugin';
19+
20+
/**
21+
* Create an esbuild 'build' options object for the server bundle.
22+
* @param options The builder's user-provider normalized options.
23+
* @returns An esbuild BuildOptions object.
24+
*/
25+
export function createServerCodeBundleOptions(
26+
options: NormalizedApplicationBuildOptions,
27+
target: string[],
28+
browsers: string[],
29+
sourceFileCache: SourceFileCache,
30+
): BuildOptions {
31+
const { jit, serverEntryPoint, workspaceRoot } = options;
32+
33+
assert(
34+
serverEntryPoint,
35+
'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.',
36+
);
37+
38+
const { pluginOptions, styleOptions } = createCompilerPluginOptions(
39+
options,
40+
target,
41+
browsers,
42+
sourceFileCache,
43+
);
44+
45+
const namespace = 'angular:server-entry';
46+
47+
const buildOptions: BuildOptions = {
48+
...getEsBuildCommonOptions(options),
49+
platform: 'node',
50+
outExtension: { '.js': '.mjs' },
51+
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
52+
// match and the ES5 distribution would be bundled and ends up breaking at
53+
// runtime with the RxJS testing library.
54+
// More details: https://github.com/angular/angular-cli/issues/25405.
55+
mainFields: ['es2020', 'es2015', 'module', 'main'],
56+
entryNames: '[name]',
57+
target,
58+
banner: {
59+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
60+
// See: https://github.com/evanw/esbuild/issues/1921.
61+
js: [
62+
`import { createRequire } from 'node:module';`,
63+
`globalThis['require'] ??= createRequire(import.meta.url);`,
64+
].join('\n'),
65+
},
66+
entryPoints: {
67+
'server': namespace,
68+
},
69+
supported: getFeatureSupport(target),
70+
plugins: [
71+
createSourcemapIngorelistPlugin(),
72+
createCompilerPlugin(
73+
// JS/TS options
74+
{ ...pluginOptions, noopTypeScriptCompilation: true },
75+
// Component stylesheet options
76+
styleOptions,
77+
),
78+
createVirtualModulePlugin({
79+
namespace,
80+
loadContent: () => {
81+
const importAndExportDec: string[] = [
82+
`import '@angular/platform-server/init';`,
83+
`import './${path.relative(workspaceRoot, serverEntryPoint).replace(/\\/g, '/')}';`,
84+
`export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
85+
];
86+
87+
if (jit) {
88+
importAndExportDec.unshift(`import '@angular/compiler';`);
89+
}
90+
91+
return {
92+
contents: importAndExportDec.join('\n'),
93+
loader: 'js',
94+
resolveDir: workspaceRoot,
95+
};
96+
},
97+
}),
98+
],
99+
};
100+
101+
if (options.externalPackages) {
102+
buildOptions.plugins ??= [];
103+
buildOptions.plugins.push(createExternalPackagesPlugin());
104+
}
105+
106+
return buildOptions;
107+
}

‎packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts

+47
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import fs from 'node:fs/promises';
1313
import path from 'node:path';
1414
import { promisify } from 'node:util';
1515
import { brotliCompress } from 'node:zlib';
16+
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
17+
import { allowMangle } from '../../utils/environment-options';
1618
import { Spinner } from '../../utils/spinner';
1719
import { BundleStats, generateBuildStatsTable } from '../webpack/utils/stats';
1820
import { InitialFileRecord } from './bundler-context';
@@ -256,3 +258,48 @@ export function transformSupportedBrowsersToTargets(supportedBrowsers: string[])
256258

257259
return transformed;
258260
}
261+
262+
export function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions {
263+
const {
264+
workspaceRoot,
265+
outExtension,
266+
optimizationOptions,
267+
sourcemapOptions,
268+
tsconfig,
269+
externalDependencies,
270+
outputNames,
271+
preserveSymlinks,
272+
jit,
273+
} = options;
274+
275+
return {
276+
absWorkingDir: workspaceRoot,
277+
bundle: true,
278+
format: 'esm',
279+
assetNames: outputNames.media,
280+
conditions: ['es2020', 'es2015', 'module'],
281+
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'],
282+
metafile: true,
283+
legalComments: options.extractLicenses ? 'none' : 'eof',
284+
logLevel: options.verbose ? 'debug' : 'silent',
285+
minifyIdentifiers: optimizationOptions.scripts && allowMangle,
286+
minifySyntax: optimizationOptions.scripts,
287+
minifyWhitespace: optimizationOptions.scripts,
288+
pure: ['forwardRef'],
289+
outdir: workspaceRoot,
290+
outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined,
291+
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
292+
splitting: true,
293+
tsconfig,
294+
external: externalDependencies,
295+
write: false,
296+
preserveSymlinks,
297+
define: {
298+
// Only set to false when script optimizations are enabled. It should not be set to true because
299+
// Angular turns `ngDevMode` into an object for development debugging purposes when not defined
300+
// which a constant true value would break.
301+
...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined),
302+
'ngJitMode': jit ? 'true' : 'false',
303+
},
304+
};
305+
}

0 commit comments

Comments
 (0)
Please sign in to comment.