Skip to content

Commit 8cf0d17

Browse files
clydinangular-robot[bot]
authored andcommittedFeb 3, 2023
feat(@angular-devkit/build-angular): support JIT compilation with esbuild
When using the experimental esbuild-based browser application builder, the `aot` build option can now be set to `false` to enable JIT compilation mode. The JIT mode compilation operates in a similar fashion to the Webpack-based builder in JIT mode. All external Component stylesheet and template references are converted to static import statements and then the content is bundled as text. All inline styles are also processed in this way as well to support inline style languages such as Sass. This approach also has the advantage of minimizing the processing necessary during rebuilds. In JIT watch mode, TypeScript code does not need to be reprocessed if only an external stylesheet or template is changed.
1 parent fac1e58 commit 8cf0d17

File tree

10 files changed

+674
-24
lines changed

10 files changed

+674
-24
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 assert from 'node:assert';
10+
import ts from 'typescript';
11+
import { AngularCompilation } from '../angular-compilation';
12+
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
13+
import { profileSync } from '../profiling';
14+
import { createJitResourceTransformer } from './jit-resource-transformer';
15+
16+
class JitCompilationState {
17+
constructor(
18+
public readonly typeScriptProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
19+
public readonly constructorParametersDownlevelTransform: ts.TransformerFactory<ts.SourceFile>,
20+
public readonly replaceResourcesTransform: ts.TransformerFactory<ts.SourceFile>,
21+
) {}
22+
}
23+
24+
export interface EmitFileResult {
25+
content?: string;
26+
map?: string;
27+
dependencies: readonly string[];
28+
}
29+
export type FileEmitter = (file: string) => Promise<EmitFileResult | undefined>;
30+
31+
export class JitCompilation {
32+
#state?: JitCompilationState;
33+
34+
async initialize(
35+
rootNames: string[],
36+
compilerOptions: ts.CompilerOptions,
37+
hostOptions: AngularHostOptions,
38+
configurationDiagnostics?: ts.Diagnostic[],
39+
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }> {
40+
// Dynamically load the Angular compiler CLI package
41+
const { constructorParametersDownlevelTransform } = await AngularCompilation.loadCompilerCli();
42+
43+
// Create Angular compiler host
44+
const host = createAngularCompilerHost(compilerOptions, hostOptions);
45+
46+
// Create the TypeScript Program
47+
const typeScriptProgram = profileSync('TS_CREATE_PROGRAM', () =>
48+
ts.createEmitAndSemanticDiagnosticsBuilderProgram(
49+
rootNames,
50+
compilerOptions,
51+
host,
52+
this.#state?.typeScriptProgram,
53+
configurationDiagnostics,
54+
),
55+
);
56+
57+
const affectedFiles = profileSync('TS_FIND_AFFECTED', () =>
58+
findAffectedFiles(typeScriptProgram),
59+
);
60+
61+
this.#state = new JitCompilationState(
62+
typeScriptProgram,
63+
constructorParametersDownlevelTransform(typeScriptProgram.getProgram()),
64+
createJitResourceTransformer(() => typeScriptProgram.getProgram().getTypeChecker()),
65+
);
66+
67+
return { affectedFiles };
68+
}
69+
70+
*collectDiagnostics(): Iterable<ts.Diagnostic> {
71+
assert(this.#state, 'Compilation must be initialized prior to collecting diagnostics.');
72+
const { typeScriptProgram } = this.#state;
73+
74+
// Collect program level diagnostics
75+
yield* typeScriptProgram.getConfigFileParsingDiagnostics();
76+
yield* typeScriptProgram.getOptionsDiagnostics();
77+
yield* typeScriptProgram.getGlobalDiagnostics();
78+
yield* profileSync('NG_DIAGNOSTICS_SYNTACTIC', () =>
79+
typeScriptProgram.getSyntacticDiagnostics(),
80+
);
81+
yield* profileSync('NG_DIAGNOSTICS_SEMANTIC', () => typeScriptProgram.getSemanticDiagnostics());
82+
}
83+
84+
createFileEmitter(onAfterEmit?: (sourceFile: ts.SourceFile) => void): FileEmitter {
85+
assert(this.#state, 'Compilation must be initialized prior to emitting files.');
86+
const {
87+
typeScriptProgram,
88+
constructorParametersDownlevelTransform,
89+
replaceResourcesTransform,
90+
} = this.#state;
91+
92+
const transformers = {
93+
before: [replaceResourcesTransform, constructorParametersDownlevelTransform],
94+
};
95+
96+
return async (file: string) => {
97+
const sourceFile = typeScriptProgram.getSourceFile(file);
98+
if (!sourceFile) {
99+
return undefined;
100+
}
101+
102+
let content: string | undefined;
103+
typeScriptProgram.emit(
104+
sourceFile,
105+
(filename, data) => {
106+
if (/\.[cm]?js$/.test(filename)) {
107+
content = data;
108+
}
109+
},
110+
undefined /* cancellationToken */,
111+
undefined /* emitOnlyDtsFiles */,
112+
transformers,
113+
);
114+
115+
onAfterEmit?.(sourceFile);
116+
117+
return { content, dependencies: [] };
118+
};
119+
}
120+
}
121+
122+
function findAffectedFiles(
123+
builder: ts.EmitAndSemanticDiagnosticsBuilderProgram,
124+
): Set<ts.SourceFile> {
125+
const affectedFiles = new Set<ts.SourceFile>();
126+
127+
let result;
128+
while ((result = builder.getSemanticDiagnosticsOfNextAffectedFile())) {
129+
affectedFiles.add(result.affected as ts.SourceFile);
130+
}
131+
132+
return affectedFiles;
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 { OutputFile, PluginBuild } from 'esbuild';
10+
import { readFile } from 'node:fs/promises';
11+
import path from 'node:path';
12+
import { BundleStylesheetOptions, bundleComponentStylesheet } from '../stylesheets';
13+
import {
14+
JIT_NAMESPACE_REGEXP,
15+
JIT_STYLE_NAMESPACE,
16+
JIT_TEMPLATE_NAMESPACE,
17+
parseJitUri,
18+
} from './uri';
19+
20+
/**
21+
* Loads/extracts the contents from a load callback Angular JIT entry.
22+
* An Angular JIT entry represents either a file path for a component resource or base64
23+
* encoded data for an inline component resource.
24+
* @param entry The value that represents content to load.
25+
* @param root The absolute path for the root of the build (typically the workspace root).
26+
* @param skipRead If true, do not attempt to read the file; if false, read file content from disk.
27+
* This option has no effect if the entry does not originate from a file. Defaults to false.
28+
* @returns An object containing the absolute path of the contents and optionally the actual contents.
29+
* For inline entries the contents will always be provided.
30+
*/
31+
async function loadEntry(
32+
entry: string,
33+
root: string,
34+
skipRead?: boolean,
35+
): Promise<{ path: string; contents?: string }> {
36+
if (entry.startsWith('file:')) {
37+
const specifier = path.join(root, entry.slice(5));
38+
39+
return {
40+
path: specifier,
41+
contents: skipRead ? undefined : await readFile(specifier, 'utf-8'),
42+
};
43+
} else if (entry.startsWith('inline:')) {
44+
const [importer, data] = entry.slice(7).split(';', 2);
45+
46+
return {
47+
path: path.join(root, importer),
48+
contents: Buffer.from(data, 'base64').toString(),
49+
};
50+
} else {
51+
throw new Error('Invalid data for Angular JIT entry.');
52+
}
53+
}
54+
55+
/**
56+
* Sets up esbuild resolve and load callbacks to support Angular JIT mode processing
57+
* for both Component stylesheets and templates. These callbacks work alongside the JIT
58+
* resource TypeScript transformer to convert and then bundle Component resources as
59+
* static imports.
60+
* @param build An esbuild {@link PluginBuild} instance used to add callbacks.
61+
* @param styleOptions The options to use when bundling stylesheets.
62+
* @param stylesheetResourceFiles An array where stylesheet resources will be added.
63+
*/
64+
export function setupJitPluginCallbacks(
65+
build: PluginBuild,
66+
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
67+
stylesheetResourceFiles: OutputFile[],
68+
): void {
69+
const root = build.initialOptions.absWorkingDir ?? '';
70+
71+
// Add a resolve callback to capture and parse any JIT URIs that were added by the
72+
// JIT resource TypeScript transformer.
73+
// Resources originating from a file are resolved as relative from the containing file (importer).
74+
build.onResolve({ filter: JIT_NAMESPACE_REGEXP }, (args) => {
75+
const parsed = parseJitUri(args.path);
76+
if (!parsed) {
77+
return undefined;
78+
}
79+
80+
const { namespace, origin, specifier } = parsed;
81+
82+
if (origin === 'file') {
83+
return {
84+
// Use a relative path to prevent fully resolved paths in the metafile (JSON stats file).
85+
// This is only necessary for custom namespaces. esbuild will handle the file namespace.
86+
path: 'file:' + path.relative(root, path.join(path.dirname(args.importer), specifier)),
87+
namespace,
88+
};
89+
} else {
90+
// Inline data may need the importer to resolve imports/references within the content
91+
const importer = path.relative(root, args.importer);
92+
93+
return {
94+
path: `inline:${importer};${specifier}`,
95+
namespace,
96+
};
97+
}
98+
});
99+
100+
// Add a load callback to handle Component stylesheets (both inline and external)
101+
build.onLoad({ filter: /./, namespace: JIT_STYLE_NAMESPACE }, async (args) => {
102+
// skipRead is used here because the stylesheet bundling will read a file stylesheet
103+
// directly either via a preprocessor or esbuild itself.
104+
const entry = await loadEntry(args.path, root, true /* skipRead */);
105+
106+
const { contents, resourceFiles, errors, warnings } = await bundleComponentStylesheet(
107+
styleOptions.inlineStyleLanguage,
108+
// The `data` parameter is only needed for a stylesheet if it was inline
109+
entry.contents ?? '',
110+
entry.path,
111+
entry.contents !== undefined,
112+
styleOptions,
113+
);
114+
115+
stylesheetResourceFiles.push(...resourceFiles);
116+
117+
return {
118+
errors,
119+
warnings,
120+
contents,
121+
loader: 'text',
122+
};
123+
});
124+
125+
// Add a load callback to handle Component templates
126+
// NOTE: While this callback supports both inline and external templates, the transformer
127+
// currently only supports generating URIs for external templates.
128+
build.onLoad({ filter: /./, namespace: JIT_TEMPLATE_NAMESPACE }, async (args) => {
129+
const { contents } = await loadEntry(args.path, root);
130+
131+
return {
132+
contents,
133+
loader: 'text',
134+
};
135+
});
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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 ts from 'typescript';
10+
import { generateJitFileUri, generateJitInlineUri } from './uri';
11+
12+
/**
13+
* Creates a TypeScript Transformer to transform Angular Component resource references into
14+
* static import statements. This transformer is used in Angular's JIT compilation mode to
15+
* support processing of component resources. When in AOT mode, the Angular AOT compiler handles
16+
* this processing and this transformer is not used.
17+
* @param getTypeChecker A function that returns a TypeScript TypeChecker instance for the program.
18+
* @returns A TypeScript transformer factory.
19+
*/
20+
export function createJitResourceTransformer(
21+
getTypeChecker: () => ts.TypeChecker,
22+
): ts.TransformerFactory<ts.SourceFile> {
23+
return (context: ts.TransformationContext) => {
24+
const typeChecker = getTypeChecker();
25+
const nodeFactory = context.factory;
26+
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
27+
28+
const visitNode: ts.Visitor = (node: ts.Node) => {
29+
if (ts.isClassDeclaration(node)) {
30+
const decorators = ts.getDecorators(node);
31+
32+
if (!decorators || decorators.length === 0) {
33+
return node;
34+
}
35+
36+
return nodeFactory.updateClassDeclaration(
37+
node,
38+
[
39+
...decorators.map((current) =>
40+
visitDecorator(nodeFactory, current, typeChecker, resourceImportDeclarations),
41+
),
42+
...(ts.getModifiers(node) ?? []),
43+
],
44+
node.name,
45+
node.typeParameters,
46+
node.heritageClauses,
47+
node.members,
48+
);
49+
}
50+
51+
return ts.visitEachChild(node, visitNode, context);
52+
};
53+
54+
return (sourceFile) => {
55+
const updatedSourceFile = ts.visitEachChild(sourceFile, visitNode, context);
56+
57+
if (resourceImportDeclarations.length > 0) {
58+
return nodeFactory.updateSourceFile(
59+
updatedSourceFile,
60+
ts.setTextRange(
61+
nodeFactory.createNodeArray(
62+
[...resourceImportDeclarations, ...updatedSourceFile.statements],
63+
updatedSourceFile.statements.hasTrailingComma,
64+
),
65+
updatedSourceFile.statements,
66+
),
67+
updatedSourceFile.isDeclarationFile,
68+
updatedSourceFile.referencedFiles,
69+
updatedSourceFile.typeReferenceDirectives,
70+
updatedSourceFile.hasNoDefaultLib,
71+
updatedSourceFile.libReferenceDirectives,
72+
);
73+
} else {
74+
return updatedSourceFile;
75+
}
76+
};
77+
};
78+
}
79+
80+
function visitDecorator(
81+
nodeFactory: ts.NodeFactory,
82+
node: ts.Decorator,
83+
typeChecker: ts.TypeChecker,
84+
resourceImportDeclarations: ts.ImportDeclaration[],
85+
): ts.Decorator {
86+
const origin = getDecoratorOrigin(node, typeChecker);
87+
if (!origin || origin.module !== '@angular/core' || origin.name !== 'Component') {
88+
return node;
89+
}
90+
91+
if (!ts.isCallExpression(node.expression)) {
92+
return node;
93+
}
94+
95+
const decoratorFactory = node.expression;
96+
const args = decoratorFactory.arguments;
97+
if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) {
98+
// Unsupported component metadata
99+
return node;
100+
}
101+
102+
const objectExpression = args[0] as ts.ObjectLiteralExpression;
103+
const styleReplacements: ts.Expression[] = [];
104+
105+
// visit all properties
106+
let properties = ts.visitNodes(objectExpression.properties, (node) =>
107+
ts.isObjectLiteralElementLike(node)
108+
? visitComponentMetadata(nodeFactory, node, styleReplacements, resourceImportDeclarations)
109+
: node,
110+
);
111+
112+
// replace properties with updated properties
113+
if (styleReplacements.length > 0) {
114+
const styleProperty = nodeFactory.createPropertyAssignment(
115+
nodeFactory.createIdentifier('styles'),
116+
nodeFactory.createArrayLiteralExpression(styleReplacements),
117+
);
118+
119+
properties = nodeFactory.createNodeArray([...properties, styleProperty]);
120+
}
121+
122+
return nodeFactory.updateDecorator(
123+
node,
124+
nodeFactory.updateCallExpression(
125+
decoratorFactory,
126+
decoratorFactory.expression,
127+
decoratorFactory.typeArguments,
128+
[nodeFactory.updateObjectLiteralExpression(objectExpression, properties)],
129+
),
130+
);
131+
}
132+
133+
function visitComponentMetadata(
134+
nodeFactory: ts.NodeFactory,
135+
node: ts.ObjectLiteralElementLike,
136+
styleReplacements: ts.Expression[],
137+
resourceImportDeclarations: ts.ImportDeclaration[],
138+
): ts.ObjectLiteralElementLike | undefined {
139+
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
140+
return node;
141+
}
142+
143+
switch (node.name.text) {
144+
case 'templateUrl':
145+
// Only analyze string literals
146+
if (
147+
!ts.isStringLiteral(node.initializer) &&
148+
!ts.isNoSubstitutionTemplateLiteral(node.initializer)
149+
) {
150+
return node;
151+
}
152+
153+
const url = node.initializer.text;
154+
if (!url) {
155+
return node;
156+
}
157+
158+
return nodeFactory.updatePropertyAssignment(
159+
node,
160+
nodeFactory.createIdentifier('template'),
161+
createResourceImport(
162+
nodeFactory,
163+
generateJitFileUri(url, 'template'),
164+
resourceImportDeclarations,
165+
),
166+
);
167+
case 'styles':
168+
if (!ts.isArrayLiteralExpression(node.initializer)) {
169+
return node;
170+
}
171+
172+
const inlineStyles = ts.visitNodes(node.initializer.elements, (node) => {
173+
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
174+
return node;
175+
}
176+
177+
const contents = node.text;
178+
if (!contents) {
179+
// An empty inline style is equivalent to not having a style element
180+
return undefined;
181+
}
182+
183+
return createResourceImport(
184+
nodeFactory,
185+
generateJitInlineUri(contents, 'style'),
186+
resourceImportDeclarations,
187+
);
188+
});
189+
190+
// Inline styles should be placed first
191+
styleReplacements.unshift(...inlineStyles);
192+
193+
// The inline styles will be added afterwards in combination with any external styles
194+
return undefined;
195+
case 'styleUrls':
196+
if (!ts.isArrayLiteralExpression(node.initializer)) {
197+
return node;
198+
}
199+
200+
const externalStyles = ts.visitNodes(node.initializer.elements, (node) => {
201+
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
202+
return node;
203+
}
204+
205+
const url = node.text;
206+
if (!url) {
207+
return node;
208+
}
209+
210+
return createResourceImport(
211+
nodeFactory,
212+
generateJitFileUri(url, 'style'),
213+
resourceImportDeclarations,
214+
);
215+
});
216+
217+
// External styles are applied after any inline styles
218+
styleReplacements.push(...externalStyles);
219+
220+
// The external styles will be added afterwards in combination with any inline styles
221+
return undefined;
222+
default:
223+
// All other elements are passed through
224+
return node;
225+
}
226+
}
227+
228+
function createResourceImport(
229+
nodeFactory: ts.NodeFactory,
230+
url: string,
231+
resourceImportDeclarations: ts.ImportDeclaration[],
232+
): ts.Identifier {
233+
const urlLiteral = nodeFactory.createStringLiteral(url);
234+
235+
const importName = nodeFactory.createIdentifier(
236+
`__NG_CLI_RESOURCE__${resourceImportDeclarations.length}`,
237+
);
238+
resourceImportDeclarations.push(
239+
nodeFactory.createImportDeclaration(
240+
undefined,
241+
nodeFactory.createImportClause(false, importName, undefined),
242+
urlLiteral,
243+
),
244+
);
245+
246+
return importName;
247+
}
248+
249+
function getDecoratorOrigin(
250+
decorator: ts.Decorator,
251+
typeChecker: ts.TypeChecker,
252+
): { name: string; module: string } | null {
253+
if (!ts.isCallExpression(decorator.expression)) {
254+
return null;
255+
}
256+
257+
let identifier: ts.Node;
258+
let name = '';
259+
260+
if (ts.isPropertyAccessExpression(decorator.expression.expression)) {
261+
identifier = decorator.expression.expression.expression;
262+
name = decorator.expression.expression.name.text;
263+
} else if (ts.isIdentifier(decorator.expression.expression)) {
264+
identifier = decorator.expression.expression;
265+
} else {
266+
return null;
267+
}
268+
269+
// NOTE: resolver.getReferencedImportDeclaration would work as well but is internal
270+
const symbol = typeChecker.getSymbolAtLocation(identifier);
271+
if (symbol && symbol.declarations && symbol.declarations.length > 0) {
272+
const declaration = symbol.declarations[0];
273+
let module: string;
274+
275+
if (ts.isImportSpecifier(declaration)) {
276+
name = (declaration.propertyName || declaration.name).text;
277+
module = (declaration.parent.parent.parent.moduleSpecifier as ts.StringLiteral).text;
278+
} else if (ts.isNamespaceImport(declaration)) {
279+
// Use the name from the decorator namespace property access
280+
module = (declaration.parent.parent.moduleSpecifier as ts.StringLiteral).text;
281+
} else if (ts.isImportClause(declaration)) {
282+
name = (declaration.name as ts.Identifier).text;
283+
module = (declaration.parent.moduleSpecifier as ts.StringLiteral).text;
284+
} else {
285+
return null;
286+
}
287+
288+
return { name, module };
289+
}
290+
291+
return null;
292+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
/**
10+
* A string value representing the base namespace for Angular JIT mode related imports.
11+
*/
12+
const JIT_BASE_NAMESPACE = 'angular:jit';
13+
14+
/**
15+
* A string value representing the namespace for Angular JIT mode related imports for
16+
* Component styles. This namespace is used for both inline (`styles`) and external
17+
* (`styleUrls`) styles.
18+
*/
19+
export const JIT_STYLE_NAMESPACE = `${JIT_BASE_NAMESPACE}:style` as const;
20+
21+
/**
22+
* A string value representing the namespace for Angular JIT mode related imports for
23+
* Component templates. This namespace is currently only used for external (`templateUrl`)
24+
* templates.
25+
*/
26+
export const JIT_TEMPLATE_NAMESPACE = `${JIT_BASE_NAMESPACE}:template` as const;
27+
28+
/**
29+
* A regular expression that can be used to match a Angular JIT mode namespace URI.
30+
* It contains capture groups for the type (template/style), origin (file/inline), and specifier.
31+
* The {@link parseJitUri} function can be used to parse and return an object representation of a JIT URI.
32+
*/
33+
export const JIT_NAMESPACE_REGEXP = new RegExp(
34+
`^${JIT_BASE_NAMESPACE}:(template|style):(file|inline);(.*)$`,
35+
);
36+
37+
/**
38+
* Generates an Angular JIT mode namespace URI for a given file.
39+
* @param file The path of the file to be included.
40+
* @param type The type of the file (`style` or `template`).
41+
* @returns A string containing the full JIT namespace URI.
42+
*/
43+
export function generateJitFileUri(file: string, type: 'style' | 'template') {
44+
return `${JIT_BASE_NAMESPACE}:${type}:file;${file}`;
45+
}
46+
47+
/**
48+
* Generates an Angular JIT mode namespace URI for a given inline style or template.
49+
* The provided content is base64 encoded and included in the URI.
50+
* @param data The content to encode within the URI.
51+
* @param type The type of the content (`style` or `template`).
52+
* @returns A string containing the full JIT namespace URI.
53+
*/
54+
export function generateJitInlineUri(data: string | Uint8Array, type: 'style' | 'template') {
55+
return `${JIT_BASE_NAMESPACE}:${type}:inline;${Buffer.from(data).toString('base64')}`;
56+
}
57+
58+
/**
59+
* Parses a string containing a JIT namespace URI.
60+
* JIT namespace URIs are used to encode the information for an Angular component's stylesheets
61+
* and templates when compiled in JIT mode.
62+
* @param uri The URI to parse into its underlying components.
63+
* @returns An object containing the namespace, type, origin, and specifier of the URI;
64+
* `undefined` if not a JIT namespace URI.
65+
*/
66+
export function parseJitUri(uri: string) {
67+
const matches = JIT_NAMESPACE_REGEXP.exec(uri);
68+
if (!matches) {
69+
return undefined;
70+
}
71+
72+
return {
73+
namespace: `${JIT_BASE_NAMESPACE}:${matches[1]}`,
74+
type: matches[1] as 'style' | 'template',
75+
origin: matches[2] as 'file' | 'inline',
76+
specifier: matches[3],
77+
};
78+
}

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import * as path from 'node:path';
2121
import { pathToFileURL } from 'node:url';
2222
import ts from 'typescript';
2323
import { maxWorkers } from '../../utils/environment-options';
24+
import { JitCompilation } from './angular/jit-compilation';
25+
import { setupJitPluginCallbacks } from './angular/jit-plugin-callbacks';
2426
import { AngularCompilation, FileEmitter } from './angular-compilation';
2527
import { AngularHostOptions } from './angular-host';
2628
import { JavaScriptTransformer } from './javascript-transformer';
@@ -32,11 +34,6 @@ import {
3234
} from './profiling';
3335
import { BundleStylesheetOptions, bundleComponentStylesheet } from './stylesheets';
3436

35-
/**
36-
* A counter for component styles used to generate unique build-time identifiers for each stylesheet.
37-
*/
38-
let componentStyleCounter = 0;
39-
4037
/**
4138
* Converts TypeScript Diagnostic related information into an esbuild compatible note object.
4239
* Related information is a subset of a full TypeScript Diagnostic and also used for diagnostic
@@ -147,6 +144,7 @@ export class SourceFileCache extends Map<string, ts.SourceFile> {
147144
export interface CompilerPluginOptions {
148145
sourcemap: boolean;
149146
tsconfig: string;
147+
jit?: boolean;
150148
advancedOptimizations?: boolean;
151149
thirdPartySourcemaps?: boolean;
152150
fileReplacements?: Record<string, string>;
@@ -236,7 +234,7 @@ export function createCompilerPlugin(
236234
let fileEmitter: FileEmitter | undefined;
237235

238236
// The stylesheet resources from component stylesheets that will be added to the build results output files
239-
let stylesheetResourceFiles: OutputFile[];
237+
let stylesheetResourceFiles: OutputFile[] = [];
240238

241239
let stylesheetMetafiles: Metafile[];
242240

@@ -267,8 +265,6 @@ export function createCompilerPlugin(
267265
const filename = stylesheetFile ?? containingFile;
268266

269267
const stylesheetResult = await bundleComponentStylesheet(
270-
// TODO: Evaluate usage of a fast hash instead
271-
`${++componentStyleCounter}`,
272268
styleOptions.inlineStyleLanguage,
273269
data,
274270
filename,
@@ -291,7 +287,11 @@ export function createCompilerPlugin(
291287
};
292288

293289
// Create new compilation if first build; otherwise, use existing for rebuilds
294-
compilation ??= new AngularCompilation();
290+
if (pluginOptions.jit) {
291+
compilation ??= new JitCompilation();
292+
} else {
293+
compilation ??= new AngularCompilation();
294+
}
295295

296296
// Initialize the Angular compilation for the current build.
297297
// In watch mode, previous build state will be reused.
@@ -411,6 +411,11 @@ export function createCompilerPlugin(
411411
),
412412
);
413413

414+
// Setup bundling of component templates and stylesheets when in JIT mode
415+
if (pluginOptions.jit) {
416+
setupJitPluginCallbacks(build, styleOptions, stylesheetResourceFiles);
417+
}
418+
414419
build.onEnd((result) => {
415420
// Add any component stylesheet resource files to the output files
416421
if (stylesheetResourceFiles.length) {

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+3-11
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ function createCodeBundleOptions(
284284
stylePreprocessorOptions,
285285
advancedOptimizations,
286286
inlineStyleLanguage,
287+
jit,
287288
} = options;
288289

289290
return {
@@ -318,6 +319,7 @@ function createCodeBundleOptions(
318319
sourcemap: !!sourcemapOptions.scripts,
319320
thirdPartySourcemaps: sourcemapOptions.vendor,
320321
tsconfig,
322+
jit,
321323
advancedOptimizations,
322324
fileReplacements,
323325
sourceFileCache,
@@ -344,8 +346,7 @@ function createCodeBundleOptions(
344346
// Angular turns `ngDevMode` into an object for development debugging purposes when not defined
345347
// which a constant true value would break.
346348
...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined),
347-
// Only AOT mode is supported currently
348-
'ngJitMode': 'false',
349+
'ngJitMode': jit ? 'true' : 'false',
349350
},
350351
};
351352
}
@@ -475,15 +476,6 @@ export async function* buildEsbuildBrowser(
475476
initialOptions: BrowserBuilderOptions,
476477
context: BuilderContext,
477478
): AsyncIterable<BuilderOutput> {
478-
// Only AOT is currently supported
479-
if (initialOptions.aot !== true) {
480-
context.logger.error(
481-
'JIT mode is currently not supported by this experimental builder. AOT mode must be used.',
482-
);
483-
484-
return;
485-
}
486-
487479
// Inform user of experimental status of builder and options
488480
logExperimentalWarnings(initialOptions, context);
489481

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/javascript-transformer-worker.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface JavaScriptTransformRequest {
2020
advancedOptimizations: boolean;
2121
forceAsyncTransformation?: boolean;
2222
skipLinker: boolean;
23+
jit: boolean;
2324
}
2425

2526
export default async function transformJavaScript(
@@ -80,7 +81,7 @@ async function transformWithBabel({
8081
{
8182
angularLinker: linkerPluginCreator && {
8283
shouldLink,
83-
jitMode: false,
84+
jitMode: options.jit,
8485
linkerPluginCreator,
8586
},
8687
forceAsyncTransformation,

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/javascript-transformer.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface JavaScriptTransformerOptions {
1515
sourcemap: boolean;
1616
thirdPartySourcemaps?: boolean;
1717
advancedOptimizations?: boolean;
18+
jit?: boolean;
1819
}
1920

2021
/**
@@ -35,11 +36,17 @@ export class JavaScriptTransformer {
3536
});
3637

3738
// Extract options to ensure only the named options are serialized and sent to the worker
38-
const { sourcemap, thirdPartySourcemaps = false, advancedOptimizations = false } = options;
39+
const {
40+
sourcemap,
41+
thirdPartySourcemaps = false,
42+
advancedOptimizations = false,
43+
jit = false,
44+
} = options;
3945
this.#commonOptions = {
4046
sourcemap,
4147
thirdPartySourcemaps,
4248
advancedOptimizations,
49+
jit,
4350
};
4451
}
4552

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

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export async function normalizeOptions(
133133
// Initial options to keep
134134
const {
135135
allowedCommonJsDependencies,
136+
aot,
136137
baseHref,
137138
buildOptimizer,
138139
crossOrigin,
@@ -158,6 +159,7 @@ export async function normalizeOptions(
158159
externalDependencies,
159160
extractLicenses,
160161
inlineStyleLanguage,
162+
jit: !aot,
161163
stats: !!statsJson,
162164
poll,
163165
// If not explicitly set, default to the Node.js process argument

‎packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { createCssResourcePlugin } from './css-resource-plugin';
1212
import { BundlerContext } from './esbuild';
1313
import { createSassPlugin } from './sass-plugin';
1414

15+
/**
16+
* A counter for component styles used to generate unique build-time identifiers for each stylesheet.
17+
*/
18+
let componentStyleCounter = 0;
19+
1520
export interface BundleStylesheetOptions {
1621
workspaceRoot: string;
1722
optimization: boolean;
@@ -73,15 +78,14 @@ export function createStylesheetBundleOptions(
7378
* @returns An object containing the output of the bundling operation.
7479
*/
7580
export async function bundleComponentStylesheet(
76-
identifier: string,
7781
language: string,
7882
data: string,
7983
filename: string,
8084
inline: boolean,
8185
options: BundleStylesheetOptions,
8286
) {
8387
const namespace = 'angular:styles/component';
84-
const entry = [language, identifier, filename].join(';');
88+
const entry = [language, componentStyleCounter++, filename].join(';');
8589

8690
const buildOptions = createStylesheetBundleOptions(options, { [entry]: data });
8791
buildOptions.entryPoints = [`${namespace};${entry}`];

0 commit comments

Comments
 (0)
Please sign in to comment.