Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(@angular/build): use direct transpilation with isolated modules #27752

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Behavior: "TypeScript isolated modules direct transpilation"', () => {
it('should successfully build with isolated modules enabled and disabled optimizations', async () => {
// Enable tsconfig isolatedModules option in tsconfig
await harness.modifyFile('tsconfig.json', (content) => {
const tsconfig = JSON.parse(content);
tsconfig.compilerOptions.isolatedModules = true;

return JSON.stringify(tsconfig);
});

harness.useTarget('build', {
...BASE_OPTIONS,
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
});

it('should successfully build with isolated modules enabled and enabled optimizations', async () => {
// Enable tsconfig isolatedModules option in tsconfig
await harness.modifyFile('tsconfig.json', (content) => {
const tsconfig = JSON.parse(content);
tsconfig.compilerOptions.isolatedModules = true;

return JSON.stringify(tsconfig);
});

harness.useTarget('build', {
...BASE_OPTIONS,
optimization: true,
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,12 @@ export class AotCompilation extends AngularCompilation {

emitAffectedFiles(): Iterable<EmitFileResult> {
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
const { angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } = this.#state;
const buildInfoFilename =
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
const { affectedFiles, angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } =
this.#state;
const compilerOptions = typeScriptProgram.getCompilerOptions();
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
const useTypeScriptTranspilation =
!compilerOptions.isolatedModules || !!compilerOptions.sourceMap;

const emittedFiles = new Map<ts.SourceFile, EmitFileResult>();
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
Expand All @@ -228,11 +231,33 @@ export class AotCompilation extends AngularCompilation {
);
transformers.before.push(webWorkerTransform);

// TypeScript will loop until there are no more affected files in the program
while (
typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)
) {
/* empty */
// Emit is handled in write file callback when using TypeScript
if (useTypeScriptTranspilation) {
// TypeScript will loop until there are no more affected files in the program
while (
typeScriptProgram.emitNextAffectedFile(
writeFileCallback,
undefined,
undefined,
transformers,
)
) {
/* empty */
}
} else if (compilerOptions.tsBuildInfoFile) {
// Manually get the builder state for the persistent cache
// The TypeScript API currently embeds this behavior inside the program emit
// via emitNextAffectedFile but that also applies all internal transforms.
const programWithGetState = typeScriptProgram.getProgram() as ts.Program & {
emitBuildInfo(writeFileCallback?: ts.WriteFileCallback): void;
};

assert(
typeof programWithGetState.emitBuildInfo === 'function',
'TypeScript program emitBuildInfo is missing.',
);

programWithGetState.emitBuildInfo();
}

// Angular may have files that must be emitted but TypeScript does not consider affected
Expand All @@ -245,11 +270,45 @@ export class AotCompilation extends AngularCompilation {
continue;
}

if (angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)) {
if (
angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile) &&
!affectedFiles.has(sourceFile)
) {
continue;
}

if (useTypeScriptTranspilation) {
typeScriptProgram.emit(sourceFile, writeFileCallback, undefined, undefined, transformers);
continue;
}

typeScriptProgram.emit(sourceFile, writeFileCallback, undefined, undefined, transformers);
// When not using TypeScript transpilation, directly apply only Angular specific transformations
const transformResult = ts.transform(
clydin marked this conversation as resolved.
Show resolved Hide resolved
sourceFile,
[
...(transformers.before ?? []),
...(transformers.after ?? []),
] as ts.TransformerFactory<ts.SourceFile>[],
compilerOptions,
);

assert(
transformResult.transformed.length === 1,
'TypeScript transforms should not produce multiple outputs for ' + sourceFile.fileName,
);

let contents;
if (sourceFile === transformResult.transformed[0]) {
// Use original content if no changes were made
contents = sourceFile.text;
} else {
// Otherwise, print the transformed source file
const printer = ts.createPrinter(compilerOptions, transformResult);
contents = printer.printFile(transformResult.transformed[0]);
}

angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
}

return emittedFiles.values();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ export async function initialize(request: InitRequest) {

return {
referencedFiles,
// TODO: Expand? `allowJs` is the only field needed currently.
compilerOptions: { allowJs: compilerOptions.allowJs },
// TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap` are the only fields needed currently.
compilerOptions: {
allowJs: compilerOptions.allowJs,
isolatedModules: compilerOptions.isolatedModules,
sourceMap: compilerOptions.sourceMap,
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export function createCompilerPlugin(

// Determines if TypeScript should process JavaScript files based on tsconfig `allowJs` option
let shouldTsIgnoreJs = true;
// Determines if transpilation should be handle by TypeScript or esbuild
let useTypeScriptTranspilation = true;

// Track incremental component stylesheet builds
const stylesheetBundler = new ComponentStylesheetBundler(
Expand Down Expand Up @@ -250,6 +252,11 @@ export function createCompilerPlugin(
createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks),
);
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
// Isolated modules option ensures safe non-TypeScript transpilation.
// Typescript printing support for sourcemaps is not yet integrated.
useTypeScriptTranspilation =
!initializationResult.compilerOptions.isolatedModules ||
!!initializationResult.compilerOptions.sourceMap;
referencedFiles = initializationResult.referencedFiles;
} catch (error) {
(result.errors ??= []).push({
Expand Down Expand Up @@ -335,9 +342,10 @@ export function createCompilerPlugin(
const request = path.normalize(
pluginOptions.fileReplacements?.[path.normalize(args.path)] ?? args.path,
);
const isJS = /\.[cm]?js$/.test(request);

// Skip TS load attempt if JS TypeScript compilation not enabled and file is JS
if (shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
if (shouldTsIgnoreJs && isJS) {
return undefined;
}

Expand All @@ -356,7 +364,7 @@ export function createCompilerPlugin(

// No TS result indicates the file is not part of the TypeScript program.
// If allowJs is enabled and the file is JS then defer to the next load hook.
if (!shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
if (!shouldTsIgnoreJs && isJS) {
return undefined;
}

Expand All @@ -366,8 +374,9 @@ export function createCompilerPlugin(
createMissingFileError(request, args.path, build.initialOptions.absWorkingDir ?? ''),
],
};
} else if (typeof contents === 'string') {
// A string indicates untransformed output from the TS/NG compiler
} else if (typeof contents === 'string' && (useTypeScriptTranspilation || isJS)) {
// A string indicates untransformed output from the TS/NG compiler.
// This step is unneeded when using esbuild transpilation.
const sideEffects = await hasSideEffects(request);
contents = await javascriptTransformer.transformData(
request,
Expand All @@ -382,7 +391,7 @@ export function createCompilerPlugin(

return {
contents,
loader: 'js',
loader: useTypeScriptTranspilation || isJS ? 'js' : 'ts',
};
});

Expand Down