Skip to content

Commit 72f4b0f

Browse files
committed
refactor(@angular/build): extract compiler plugin utilities
This commit refactors the main esbuild compiler plugin by extracting several utility functions into their own dedicated files. This improves modularity, readability, and maintainability of the plugin. The following functions have been moved: - `bundleWebWorker` -> `web-worker-bundler.ts` - `createCompilerOptionsTransformer` -> `compiler-options-transformer.ts` - `createMissingFileDiagnostic` -> `diagnostics.ts` No functional changes are introduced. The main `compiler-plugin.ts` now imports these utilities, making its own logic more focused and easier to follow.
1 parent 3c9b892 commit 72f4b0f

4 files changed

Lines changed: 198 additions & 174 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.dev/license
7+
*/
8+
9+
import type { PartialMessage } from 'esbuild';
10+
import * as path from 'node:path';
11+
import type { AngularCompilation } from '../../angular/compilation';
12+
import type { CompilerPluginOptions } from './compiler-plugin';
13+
14+
export function createCompilerOptionsTransformer(
15+
setupWarnings: PartialMessage[] | undefined,
16+
pluginOptions: CompilerPluginOptions,
17+
preserveSymlinks: boolean | undefined,
18+
customConditions: string[] | undefined,
19+
): Parameters<AngularCompilation['initialize']>[2] {
20+
return (compilerOptions) => {
21+
// target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
22+
if (compilerOptions.target === undefined || compilerOptions.target < 9 /** ES2022 */) {
23+
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
24+
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
25+
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
26+
compilerOptions.target = 9 /** ES2022 */;
27+
compilerOptions.useDefineForClassFields ??= false;
28+
29+
// Only add the warning on the initial build
30+
setupWarnings?.push({
31+
text:
32+
`TypeScript compiler options 'target' and 'useDefineForClassFields' are set to 'ES2022' and ` +
33+
`'false' respectively by the Angular CLI.`,
34+
location: { file: pluginOptions.tsconfig },
35+
notes: [
36+
{
37+
text:
38+
'To control ECMA version and features use the Browserslist configuration. ' +
39+
'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility',
40+
},
41+
],
42+
});
43+
}
44+
45+
if (compilerOptions.compilationMode === 'partial') {
46+
setupWarnings?.push({
47+
text: 'Angular partial compilation mode is not supported when building applications.',
48+
location: null,
49+
notes: [{ text: 'Full compilation mode will be used instead.' }],
50+
});
51+
compilerOptions.compilationMode = 'full';
52+
}
53+
54+
// Enable incremental compilation by default if caching is enabled and incremental is not explicitly disabled
55+
if (
56+
compilerOptions.incremental !== false &&
57+
pluginOptions.sourceFileCache?.persistentCachePath
58+
) {
59+
compilerOptions.incremental = true;
60+
// Set the build info file location to the configured cache directory
61+
compilerOptions.tsBuildInfoFile = path.join(
62+
pluginOptions.sourceFileCache?.persistentCachePath,
63+
'.tsbuildinfo',
64+
);
65+
} else {
66+
compilerOptions.incremental = false;
67+
}
68+
69+
if (compilerOptions.module === undefined || compilerOptions.module < 5 /** ES2015 */) {
70+
compilerOptions.module = 7; /** ES2022 */
71+
setupWarnings?.push({
72+
text: `TypeScript compiler options 'module' values 'CommonJS', 'UMD', 'System' and 'AMD' are not supported.`,
73+
location: null,
74+
notes: [{ text: `The 'module' option will be set to 'ES2022' instead.` }],
75+
});
76+
}
77+
78+
if (compilerOptions.isolatedModules && compilerOptions.emitDecoratorMetadata) {
79+
setupWarnings?.push({
80+
text: `TypeScript compiler option 'isolatedModules' may prevent the 'emitDecoratorMetadata' option from emitting all metadata.`,
81+
location: null,
82+
notes: [
83+
{
84+
text:
85+
`The 'emitDecoratorMetadata' option is not required by Angular` +
86+
'and can be removed if not explictly required by the project.',
87+
},
88+
],
89+
});
90+
}
91+
92+
// Synchronize custom resolve conditions.
93+
// Set if using the supported bundler resolution mode (bundler is the default in new projects)
94+
if (
95+
compilerOptions.moduleResolution === 100 /* ModuleResolutionKind.Bundler */ ||
96+
compilerOptions.module === 200 /** ModuleKind.Preserve */
97+
) {
98+
compilerOptions.customConditions = customConditions;
99+
}
100+
101+
return {
102+
...compilerOptions,
103+
noEmitOnError: false,
104+
composite: false,
105+
inlineSources: !!pluginOptions.sourcemap,
106+
inlineSourceMap: !!pluginOptions.sourcemap,
107+
sourceMap: undefined,
108+
mapRoot: undefined,
109+
sourceRoot: undefined,
110+
preserveSymlinks,
111+
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
112+
_enableHmr: !!pluginOptions.templateUpdates,
113+
supportTestBed: !!pluginOptions.includeTestMetadata,
114+
supportJitMode: !!pluginOptions.includeTestMetadata,
115+
};
116+
};
117+
}

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 3 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
*/
88

99
import type {
10-
BuildFailure,
1110
Loader,
1211
Metafile,
1312
OnStartResult,
1413
OutputFile,
1514
PartialMessage,
16-
PartialNote,
1715
Plugin,
1816
PluginBuild,
1917
} from 'esbuild';
@@ -28,11 +26,14 @@ import { JavaScriptTransformer } from '../javascript-transformer';
2826
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
2927
import { logCumulativeDurations, profileAsync, resetCumulativeDurations } from '../profiling';
3028
import { SharedTSCompilationState, getSharedCompilationState } from './compilation-state';
29+
import { createCompilerOptionsTransformer } from './compiler-options-transformer';
3130
import { ComponentStylesheetBundler } from './component-stylesheets';
31+
import { createMissingFileDiagnostic } from './diagnostics';
3232
import { FileReferenceTracker } from './file-reference-tracker';
3333
import { setupJitPluginCallbacks } from './jit-plugin-callbacks';
3434
import { rewriteForBazel } from './rewrite-bazel-paths';
3535
import { SourceFileCache } from './source-file-cache';
36+
import { bundleWebWorker } from './web-worker-bundler';
3637

3738
export interface CompilerPluginOptions {
3839
sourcemap: boolean | 'external';
@@ -642,178 +643,6 @@ async function bundleExternalStylesheet(
642643
}
643644
}
644645

645-
function createCompilerOptionsTransformer(
646-
setupWarnings: PartialMessage[] | undefined,
647-
pluginOptions: CompilerPluginOptions,
648-
preserveSymlinks: boolean | undefined,
649-
customConditions: string[] | undefined,
650-
): Parameters<AngularCompilation['initialize']>[2] {
651-
return (compilerOptions) => {
652-
// target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
653-
if (compilerOptions.target === undefined || compilerOptions.target < 9 /** ES2022 */) {
654-
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
655-
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
656-
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
657-
compilerOptions.target = 9 /** ES2022 */;
658-
compilerOptions.useDefineForClassFields ??= false;
659-
660-
// Only add the warning on the initial build
661-
setupWarnings?.push({
662-
text:
663-
`TypeScript compiler options 'target' and 'useDefineForClassFields' are set to 'ES2022' and ` +
664-
`'false' respectively by the Angular CLI.`,
665-
location: { file: pluginOptions.tsconfig },
666-
notes: [
667-
{
668-
text:
669-
'To control ECMA version and features use the Browserslist configuration. ' +
670-
'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility',
671-
},
672-
],
673-
});
674-
}
675-
676-
if (compilerOptions.compilationMode === 'partial') {
677-
setupWarnings?.push({
678-
text: 'Angular partial compilation mode is not supported when building applications.',
679-
location: null,
680-
notes: [{ text: 'Full compilation mode will be used instead.' }],
681-
});
682-
compilerOptions.compilationMode = 'full';
683-
}
684-
685-
// Enable incremental compilation by default if caching is enabled and incremental is not explicitly disabled
686-
if (
687-
compilerOptions.incremental !== false &&
688-
pluginOptions.sourceFileCache?.persistentCachePath
689-
) {
690-
compilerOptions.incremental = true;
691-
// Set the build info file location to the configured cache directory
692-
compilerOptions.tsBuildInfoFile = path.join(
693-
pluginOptions.sourceFileCache?.persistentCachePath,
694-
'.tsbuildinfo',
695-
);
696-
} else {
697-
compilerOptions.incremental = false;
698-
}
699-
700-
if (compilerOptions.module === undefined || compilerOptions.module < 5 /** ES2015 */) {
701-
compilerOptions.module = 7; /** ES2022 */
702-
setupWarnings?.push({
703-
text: `TypeScript compiler options 'module' values 'CommonJS', 'UMD', 'System' and 'AMD' are not supported.`,
704-
location: null,
705-
notes: [{ text: `The 'module' option will be set to 'ES2022' instead.` }],
706-
});
707-
}
708-
709-
if (compilerOptions.isolatedModules && compilerOptions.emitDecoratorMetadata) {
710-
setupWarnings?.push({
711-
text: `TypeScript compiler option 'isolatedModules' may prevent the 'emitDecoratorMetadata' option from emitting all metadata.`,
712-
location: null,
713-
notes: [
714-
{
715-
text:
716-
`The 'emitDecoratorMetadata' option is not required by Angular` +
717-
'and can be removed if not explictly required by the project.',
718-
},
719-
],
720-
});
721-
}
722-
723-
// Synchronize custom resolve conditions.
724-
// Set if using the supported bundler resolution mode (bundler is the default in new projects)
725-
if (
726-
compilerOptions.moduleResolution === 100 /* ModuleResolutionKind.Bundler */ ||
727-
compilerOptions.module === 200 /** ModuleKind.Preserve */
728-
) {
729-
compilerOptions.customConditions = customConditions;
730-
}
731-
732-
return {
733-
...compilerOptions,
734-
noEmitOnError: false,
735-
composite: false,
736-
inlineSources: !!pluginOptions.sourcemap,
737-
inlineSourceMap: !!pluginOptions.sourcemap,
738-
sourceMap: undefined,
739-
mapRoot: undefined,
740-
sourceRoot: undefined,
741-
preserveSymlinks,
742-
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
743-
_enableHmr: !!pluginOptions.templateUpdates,
744-
supportTestBed: !!pluginOptions.includeTestMetadata,
745-
supportJitMode: !!pluginOptions.includeTestMetadata,
746-
};
747-
};
748-
}
749-
750-
function bundleWebWorker(
751-
build: PluginBuild,
752-
pluginOptions: CompilerPluginOptions,
753-
workerFile: string,
754-
) {
755-
try {
756-
return build.esbuild.buildSync({
757-
...build.initialOptions,
758-
platform: 'browser',
759-
write: false,
760-
bundle: true,
761-
metafile: true,
762-
format: 'esm',
763-
entryNames: 'worker-[hash]',
764-
entryPoints: [workerFile],
765-
sourcemap: pluginOptions.sourcemap,
766-
// Zone.js is not used in Web workers so no need to disable
767-
supported: undefined,
768-
// Plugins are not supported in sync esbuild calls
769-
plugins: undefined,
770-
});
771-
} catch (error) {
772-
if (error && typeof error === 'object' && 'errors' in error && 'warnings' in error) {
773-
return error as BuildFailure;
774-
}
775-
throw error;
776-
}
777-
}
778-
779-
function createMissingFileDiagnostic(
780-
request: string,
781-
original: string,
782-
root: string,
783-
angular: boolean,
784-
): PartialMessage {
785-
const relativeRequest = path.relative(root, request);
786-
const notes: PartialNote[] = [];
787-
788-
if (angular) {
789-
notes.push({
790-
text:
791-
`Files containing Angular metadata ('@Component'/'@Directive'/etc.) must be part of the TypeScript compilation.` +
792-
` You can ensure the file is part of the TypeScript program via the 'files' or 'include' property.`,
793-
});
794-
} else {
795-
notes.push({
796-
text:
797-
`The file will be bundled and included in the output but will not be type-checked at build time.` +
798-
` To remove this message you can add the file to the TypeScript program via the 'files' or 'include' property.`,
799-
});
800-
}
801-
802-
const relativeOriginal = path.relative(root, original);
803-
if (relativeRequest !== relativeOriginal) {
804-
notes.push({
805-
text: `File is requested from a file replacement of '${relativeOriginal}'.`,
806-
});
807-
}
808-
809-
const diagnostic = {
810-
text: `File '${relativeRequest}' not found in TypeScript compilation.`,
811-
notes,
812-
};
813-
814-
return diagnostic;
815-
}
816-
817646
const POTENTIAL_METADATA_REGEX = /@angular\/core|@Component|@Directive|@Injectable|@Pipe|@NgModule/;
818647

819648
function requiresAngularCompiler(contents: string): boolean {

packages/angular/build/src/tools/esbuild/angular/diagnostics.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import type { PartialMessage, PartialNote } from 'esbuild';
1010
import { platform } from 'node:os';
11+
import * as path from 'node:path';
1112
import type ts from 'typescript';
1213

1314
/**
@@ -99,3 +100,41 @@ export function convertTypeScriptDiagnostic(
99100

100101
return message;
101102
}
103+
104+
export function createMissingFileDiagnostic(
105+
request: string,
106+
original: string,
107+
root: string,
108+
angular: boolean,
109+
): PartialMessage {
110+
const relativeRequest = path.relative(root, request);
111+
const notes: PartialNote[] = [];
112+
113+
if (angular) {
114+
notes.push({
115+
text:
116+
`Files containing Angular metadata ('@Component'/'@Directive'/etc.) must be part of the TypeScript compilation.` +
117+
` You can ensure the file is part of the TypeScript program via the 'files' or 'include' property.`,
118+
});
119+
} else {
120+
notes.push({
121+
text:
122+
`The file will be bundled and included in the output but will not be type-checked at build time.` +
123+
` To remove this message you can add the file to the TypeScript program via the 'files' or 'include' property.`,
124+
});
125+
}
126+
127+
const relativeOriginal = path.relative(root, original);
128+
if (relativeRequest !== relativeOriginal) {
129+
notes.push({
130+
text: `File is requested from a file replacement of '${relativeOriginal}'.`,
131+
});
132+
}
133+
134+
const diagnostic = {
135+
text: `File '${relativeRequest}' not found in TypeScript compilation.`,
136+
notes,
137+
};
138+
139+
return diagnostic;
140+
}

0 commit comments

Comments
 (0)