Skip to content

Commit fba2799

Browse files
committed
refactor(@angular/build): allow caching of TypeScript build contexts for rebuilds
Now that the TypeScript bundler contexts perform additional checks for validity, they can now be cached in memory during watch modes. This allows the TypeScript bundler contexts to skip rebundling when not affected by a file changed. This is particularly beneficial for style related file changes where no code processing is otherwise required.
1 parent 2aa25a7 commit fba2799

File tree

4 files changed

+177
-176
lines changed

4 files changed

+177
-176
lines changed

packages/angular/build/src/builders/application/setup-bundling.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export function setupBundlerContexts(
126126
const serverPolyfillBundleOptions = createServerPolyfillBundleOptions(
127127
options,
128128
nodeTargets,
129-
codeBundleCache,
129+
codeBundleCache.loadResultCache,
130130
);
131131

132132
if (serverPolyfillBundleOptions) {

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

+172-168
Original file line numberDiff line numberDiff line change
@@ -38,62 +38,64 @@ export function createBrowserCodeBundleOptions(
3838
target: string[],
3939
sourceFileCache: SourceFileCache,
4040
stylesheetBundler: ComponentStylesheetBundler,
41-
): BuildOptions {
42-
const { entryPoints, outputNames, polyfills } = options;
43-
44-
const pluginOptions = createCompilerPluginOptions(options, sourceFileCache);
45-
46-
const zoneless = isZonelessApp(polyfills);
47-
48-
const buildOptions: BuildOptions = {
49-
...getEsBuildCommonOptions(options),
50-
platform: 'browser',
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', 'browser', 'module', 'main'],
56-
entryNames: outputNames.bundles,
57-
entryPoints,
58-
target,
59-
supported: getFeatureSupport(target, zoneless),
60-
plugins: [
61-
createLoaderImportAttributePlugin(),
62-
createWasmPlugin({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }),
63-
createSourcemapIgnorelistPlugin(),
64-
createCompilerPlugin(
65-
// JS/TS options
66-
pluginOptions,
67-
// Component stylesheet bundler
68-
stylesheetBundler,
69-
),
70-
],
71-
};
41+
): BundlerOptionsFactory {
42+
return (loadCache) => {
43+
const { entryPoints, outputNames, polyfills } = options;
44+
45+
const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadCache);
46+
47+
const zoneless = isZonelessApp(polyfills);
48+
49+
const buildOptions: BuildOptions = {
50+
...getEsBuildCommonOptions(options),
51+
platform: 'browser',
52+
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
53+
// match and the ES5 distribution would be bundled and ends up breaking at
54+
// runtime with the RxJS testing library.
55+
// More details: https://github.com/angular/angular-cli/issues/25405.
56+
mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
57+
entryNames: outputNames.bundles,
58+
entryPoints,
59+
target,
60+
supported: getFeatureSupport(target, zoneless),
61+
plugins: [
62+
createLoaderImportAttributePlugin(),
63+
createWasmPlugin({ allowAsync: zoneless, cache: loadCache }),
64+
createSourcemapIgnorelistPlugin(),
65+
createCompilerPlugin(
66+
// JS/TS options
67+
pluginOptions,
68+
// Component stylesheet bundler
69+
stylesheetBundler,
70+
),
71+
],
72+
};
7273

73-
if (options.plugins) {
74-
buildOptions.plugins?.push(...options.plugins);
75-
}
74+
if (options.plugins) {
75+
buildOptions.plugins?.push(...options.plugins);
76+
}
7677

77-
if (options.externalPackages) {
78-
// Package files affected by a customized loader should not be implicitly marked as external
79-
if (
80-
options.loaderExtensions ||
81-
options.plugins ||
82-
typeof options.externalPackages === 'object'
83-
) {
84-
// Plugin must be added after custom plugins to ensure any added loader options are considered
85-
buildOptions.plugins?.push(
86-
createExternalPackagesPlugin(
87-
options.externalPackages !== true ? options.externalPackages : undefined,
88-
),
89-
);
90-
} else {
91-
// Safe to use the packages external option directly
92-
buildOptions.packages = 'external';
78+
if (options.externalPackages) {
79+
// Package files affected by a customized loader should not be implicitly marked as external
80+
if (
81+
options.loaderExtensions ||
82+
options.plugins ||
83+
typeof options.externalPackages === 'object'
84+
) {
85+
// Plugin must be added after custom plugins to ensure any added loader options are considered
86+
buildOptions.plugins?.push(
87+
createExternalPackagesPlugin(
88+
options.externalPackages !== true ? options.externalPackages : undefined,
89+
),
90+
);
91+
} else {
92+
// Safe to use the packages external option directly
93+
buildOptions.packages = 'external';
94+
}
9395
}
94-
}
9596

96-
return buildOptions;
97+
return buildOptions;
98+
};
9799
}
98100

99101
export function createBrowserPolyfillBundleOptions(
@@ -158,7 +160,7 @@ export function createBrowserPolyfillBundleOptions(
158160
export function createServerPolyfillBundleOptions(
159161
options: NormalizedApplicationBuildOptions,
160162
target: string[],
161-
sourceFileCache?: SourceFileCache,
163+
loadResultCache: LoadResultCache | undefined,
162164
): BundlerOptionsFactory | undefined {
163165
const serverPolyfills: string[] = [];
164166
const polyfillsFromConfig = new Set(options.polyfills);
@@ -185,7 +187,7 @@ export function createServerPolyfillBundleOptions(
185187
},
186188
namespace,
187189
false,
188-
sourceFileCache?.loadResultCache,
190+
loadResultCache,
189191
);
190192

191193
if (!polyfillBundleOptions) {
@@ -372,137 +374,139 @@ export function createSsrEntryCodeBundleOptions(
372374
target: string[],
373375
sourceFileCache: SourceFileCache,
374376
stylesheetBundler: ComponentStylesheetBundler,
375-
): BuildOptions {
377+
): BundlerOptionsFactory {
376378
const { workspaceRoot, ssrOptions, externalPackages } = options;
377379
const serverEntryPoint = ssrOptions?.entry;
378380
assert(
379381
serverEntryPoint,
380382
'createSsrEntryCodeBundleOptions should not be called without a defined serverEntryPoint.',
381383
);
382384

383-
const pluginOptions = createCompilerPluginOptions(options, sourceFileCache);
384-
385-
const ssrEntryNamespace = 'angular:ssr-entry';
386-
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
387-
const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require';
388-
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
389-
390-
const inject: string[] = [ssrInjectManifestNamespace];
391-
if (isNodePlatform) {
392-
inject.unshift(ssrInjectRequireNamespace);
393-
}
394-
395-
const buildOptions: BuildOptions = {
396-
...getEsBuildServerCommonOptions(options),
397-
target,
398-
entryPoints: {
399-
// TODO: consider renaming to index
400-
'server': ssrEntryNamespace,
401-
},
402-
supported: getFeatureSupport(target, true),
403-
plugins: [
404-
createSourcemapIgnorelistPlugin(),
405-
createCompilerPlugin(
406-
// JS/TS options
407-
{ ...pluginOptions, noopTypeScriptCompilation: true },
408-
// Component stylesheet bundler
409-
stylesheetBundler,
410-
),
411-
],
412-
inject,
413-
};
414-
415-
buildOptions.plugins ??= [];
385+
return (loadResultCache) => {
386+
const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadResultCache);
416387

417-
if (externalPackages) {
418-
buildOptions.packages = 'external';
419-
} else {
420-
buildOptions.plugins.push(createRxjsEsmResolutionPlugin());
421-
}
388+
const ssrEntryNamespace = 'angular:ssr-entry';
389+
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
390+
const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require';
391+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
422392

423-
// Mark manifest file as external. As this will be generated later on.
424-
(buildOptions.external ??= []).push('*/main.server.mjs', ...SERVER_GENERATED_EXTERNALS);
393+
const inject: string[] = [ssrInjectManifestNamespace];
394+
if (isNodePlatform) {
395+
inject.unshift(ssrInjectRequireNamespace);
396+
}
425397

426-
if (!isNodePlatform) {
427-
// `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client.
428-
// Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms.
429-
// Note: The framework already issues a warning when using XHR with SSR.
430-
buildOptions.external.push('xhr2');
431-
}
398+
const buildOptions: BuildOptions = {
399+
...getEsBuildServerCommonOptions(options),
400+
target,
401+
entryPoints: {
402+
// TODO: consider renaming to index
403+
'server': ssrEntryNamespace,
404+
},
405+
supported: getFeatureSupport(target, true),
406+
plugins: [
407+
createSourcemapIgnorelistPlugin(),
408+
createCompilerPlugin(
409+
// JS/TS options
410+
{ ...pluginOptions, noopTypeScriptCompilation: true },
411+
// Component stylesheet bundler
412+
stylesheetBundler,
413+
),
414+
],
415+
inject,
416+
};
432417

433-
buildOptions.plugins.push(
434-
createServerBundleMetadata({ ssrEntryBundle: true }),
435-
createVirtualModulePlugin({
436-
namespace: ssrInjectRequireNamespace,
437-
cache: sourceFileCache?.loadResultCache,
438-
loadContent: () => {
439-
const contents: string[] = [
440-
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
441-
// See: https://github.com/evanw/esbuild/issues/1921.
442-
`import { createRequire } from 'node:module';`,
443-
`globalThis['require'] ??= createRequire(import.meta.url);`,
444-
];
418+
buildOptions.plugins ??= [];
445419

446-
return {
447-
contents: contents.join('\n'),
448-
loader: 'js',
449-
resolveDir: workspaceRoot,
450-
};
451-
},
452-
}),
453-
createVirtualModulePlugin({
454-
namespace: ssrInjectManifestNamespace,
455-
cache: sourceFileCache?.loadResultCache,
456-
loadContent: () => {
457-
const contents: string[] = [
458-
// Configure `@angular/ssr` app engine manifest.
459-
`import manifest from './${SERVER_APP_ENGINE_MANIFEST_FILENAME}';`,
460-
`import { ɵsetAngularAppEngineManifest } from '@angular/ssr';`,
461-
`ɵsetAngularAppEngineManifest(manifest);`,
462-
];
420+
if (externalPackages) {
421+
buildOptions.packages = 'external';
422+
} else {
423+
buildOptions.plugins.push(createRxjsEsmResolutionPlugin());
424+
}
463425

464-
return {
465-
contents: contents.join('\n'),
466-
loader: 'js',
467-
resolveDir: workspaceRoot,
468-
};
469-
},
470-
}),
471-
createVirtualModulePlugin({
472-
namespace: ssrEntryNamespace,
473-
cache: sourceFileCache?.loadResultCache,
474-
loadContent: () => {
475-
const serverEntryPointJsImport = entryFileToWorkspaceRelative(
476-
workspaceRoot,
477-
serverEntryPoint,
478-
);
479-
const contents: string[] = [
480-
// Re-export all symbols including default export
481-
`import * as server from '${serverEntryPointJsImport}';`,
482-
`export * from '${serverEntryPointJsImport}';`,
483-
// The below is needed to avoid
484-
// `Import "default" will always be undefined because there is no matching export` warning when no default is present.
485-
`const defaultExportName = 'default';`,
486-
`export default server[defaultExportName]`,
426+
// Mark manifest file as external. As this will be generated later on.
427+
(buildOptions.external ??= []).push('*/main.server.mjs', ...SERVER_GENERATED_EXTERNALS);
487428

488-
// Add @angular/ssr exports
489-
`export { AngularAppEngine } from '@angular/ssr';`,
490-
];
429+
if (!isNodePlatform) {
430+
// `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client.
431+
// Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms.
432+
// Note: The framework already issues a warning when using XHR with SSR.
433+
buildOptions.external.push('xhr2');
434+
}
491435

492-
return {
493-
contents: contents.join('\n'),
494-
loader: 'js',
495-
resolveDir: workspaceRoot,
496-
};
497-
},
498-
}),
499-
);
436+
buildOptions.plugins.push(
437+
createServerBundleMetadata({ ssrEntryBundle: true }),
438+
createVirtualModulePlugin({
439+
namespace: ssrInjectRequireNamespace,
440+
cache: loadResultCache,
441+
loadContent: () => {
442+
const contents: string[] = [
443+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
444+
// See: https://github.com/evanw/esbuild/issues/1921.
445+
`import { createRequire } from 'node:module';`,
446+
`globalThis['require'] ??= createRequire(import.meta.url);`,
447+
];
448+
449+
return {
450+
contents: contents.join('\n'),
451+
loader: 'js',
452+
resolveDir: workspaceRoot,
453+
};
454+
},
455+
}),
456+
createVirtualModulePlugin({
457+
namespace: ssrInjectManifestNamespace,
458+
cache: loadResultCache,
459+
loadContent: () => {
460+
const contents: string[] = [
461+
// Configure `@angular/ssr` app engine manifest.
462+
`import manifest from './${SERVER_APP_ENGINE_MANIFEST_FILENAME}';`,
463+
`import { ɵsetAngularAppEngineManifest } from '@angular/ssr';`,
464+
`ɵsetAngularAppEngineManifest(manifest);`,
465+
];
466+
467+
return {
468+
contents: contents.join('\n'),
469+
loader: 'js',
470+
resolveDir: workspaceRoot,
471+
};
472+
},
473+
}),
474+
createVirtualModulePlugin({
475+
namespace: ssrEntryNamespace,
476+
cache: loadResultCache,
477+
loadContent: () => {
478+
const serverEntryPointJsImport = entryFileToWorkspaceRelative(
479+
workspaceRoot,
480+
serverEntryPoint,
481+
);
482+
const contents: string[] = [
483+
// Re-export all symbols including default export
484+
`import * as server from '${serverEntryPointJsImport}';`,
485+
`export * from '${serverEntryPointJsImport}';`,
486+
// The below is needed to avoid
487+
// `Import "default" will always be undefined because there is no matching export` warning when no default is present.
488+
`const defaultExportName = 'default';`,
489+
`export default server[defaultExportName]`,
490+
491+
// Add @angular/ssr exports
492+
`export { AngularAppEngine } from '@angular/ssr';`,
493+
];
494+
495+
return {
496+
contents: contents.join('\n'),
497+
loader: 'js',
498+
resolveDir: workspaceRoot,
499+
};
500+
},
501+
}),
502+
);
500503

501-
if (options.plugins) {
502-
buildOptions.plugins.push(...options.plugins);
503-
}
504+
if (options.plugins) {
505+
buildOptions.plugins.push(...options.plugins);
506+
}
504507

505-
return buildOptions;
508+
return buildOptions;
509+
};
506510
}
507511

508512
function getEsBuildServerCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions {

0 commit comments

Comments
 (0)