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

refactor(@angular-devkit/build-angular): move TS/NG configuration reading into compiler classes #25028

Merged
merged 1 commit into from Apr 18, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -9,6 +9,7 @@
import type ng from '@angular/compiler-cli';
import type ts from 'typescript';
import { loadEsmModule } from '../../../utils/load-esm';
import { profileSync } from '../profiling';
import type { AngularHostOptions } from './angular-host';

export interface EmitFileResult {
Expand All @@ -32,12 +33,29 @@ export abstract class AngularCompilation {
return AngularCompilation.#angularCompilerCliModule;
}

protected async loadConfiguration(tsconfig: string): Promise<ng.CompilerOptions> {
const { readConfiguration } = await AngularCompilation.loadCompilerCli();

return profileSync('NG_READ_CONFIG', () =>
readConfiguration(tsconfig, {
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
suppressOutputPathCheck: true,
outDir: undefined,
sourceMap: false,
declaration: false,
declarationMap: false,
allowEmptyCodegenFiles: false,
annotationsAs: 'decorators',
enableResourceInlining: false,
}),
);
}

abstract initialize(
rootNames: string[],
compilerOptions: ts.CompilerOptions,
tsconfig: string,
hostOptions: AngularHostOptions,
configurationDiagnostics?: ts.Diagnostic[],
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }>;
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile>; compilerOptions: ng.CompilerOptions }>;

abstract collectDiagnostics(): Iterable<ts.Diagnostic>;

Expand Down
Expand Up @@ -39,14 +39,22 @@ export class AotCompilation extends AngularCompilation {
#state?: AngularCompilationState;

async initialize(
rootNames: string[],
compilerOptions: ng.CompilerOptions,
tsconfig: string,
hostOptions: AngularHostOptions,
configurationDiagnostics?: ts.Diagnostic[],
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }> {
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile>; compilerOptions: ng.CompilerOptions }> {
// Dynamically load the Angular compiler CLI package
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();

// Load the compiler configuration and transform as needed
const {
options: originalCompilerOptions,
rootNames,
errors: configurationDiagnostics,
} = await this.loadConfiguration(tsconfig);
const compilerOptions =
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;

// Create Angular compiler host
const host = createAngularCompilerHost(compilerOptions, hostOptions);

Expand Down Expand Up @@ -79,7 +87,7 @@ export class AotCompilation extends AngularCompilation {
this.#state?.diagnosticCache,
);

return { affectedFiles };
return { affectedFiles, compilerOptions };
}

*collectDiagnostics(): Iterable<ts.Diagnostic> {
Expand Down
Expand Up @@ -165,15 +165,13 @@ export function createCompilerPlugin(
name: 'angular-compiler',
// eslint-disable-next-line max-lines-per-function
async setup(build: PluginBuild): Promise<void> {
let setupWarnings: PartialMessage[] | undefined;
let setupWarnings: PartialMessage[] | undefined = [];

// Initialize a worker pool for JavaScript transformations
const javascriptTransformer = new JavaScriptTransformer(pluginOptions, maxWorkers);

const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT, readConfiguration } =
await AngularCompilation.loadCompilerCli();

// Setup defines based on the values provided by the Angular compiler-cli
const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT } = await AngularCompilation.loadCompilerCli();
build.initialOptions.define ??= {};
for (const [key, value] of Object.entries(GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) {
if (key in build.initialOptions.define) {
Expand All @@ -189,71 +187,26 @@ export function createCompilerPlugin(
build.initialOptions.define[key] = value.toString();
}

// The tsconfig is loaded in setup instead of in start to allow the esbuild target build option to be modified.
// esbuild build options can only be modified in setup prior to starting the build.
const {
options: compilerOptions,
rootNames,
errors: configurationDiagnostics,
} = profileSync('NG_READ_CONFIG', () =>
readConfiguration(pluginOptions.tsconfig, {
noEmitOnError: false,
suppressOutputPathCheck: true,
outDir: undefined,
inlineSources: pluginOptions.sourcemap,
inlineSourceMap: pluginOptions.sourcemap,
sourceMap: false,
mapRoot: undefined,
sourceRoot: undefined,
declaration: false,
declarationMap: false,
allowEmptyCodegenFiles: false,
annotationsAs: 'decorators',
enableResourceInlining: false,
}),
);

if (compilerOptions.target === undefined || compilerOptions.target < ts.ScriptTarget.ES2022) {
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
compilerOptions.target = ts.ScriptTarget.ES2022;
compilerOptions.useDefineForClassFields ??= false;

(setupWarnings ??= []).push({
text:
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
'"false" respectively by the Angular CLI.\n' +
`NOTE: You can set the "target" to "ES2022" in the project's tsconfig to remove this warning.`,
location: { file: pluginOptions.tsconfig },
notes: [
{
text:
'To control ECMA version and features use the Browerslist configuration. ' +
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
},
],
});
}

// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
let fileEmitter: FileEmitter | undefined;

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

let stylesheetMetafiles: Metafile[];

let compilation: AngularCompilation | undefined;
// Create new reusable compilation for the appropriate mode based on the `jit` plugin option
const compilation: AngularCompilation = pluginOptions.jit
? new JitCompilation()
: new AotCompilation();

// Determines if TypeScript should process JavaScript files based on tsconfig `allowJs` option
let shouldTsIgnoreJs = true;

build.onStart(async () => {
const result: OnStartResult = {
warnings: setupWarnings,
};

// Reset the setup warnings so that they are only shown during the first build.
setupWarnings = undefined;

// Reset debug performance tracking
resetCumulativeDurations();

Expand Down Expand Up @@ -293,21 +246,48 @@ export function createCompilerPlugin(
},
};

// Create new compilation if first build; otherwise, use existing for rebuilds
if (pluginOptions.jit) {
compilation ??= new JitCompilation();
} else {
compilation ??= new AotCompilation();
}

// Initialize the Angular compilation for the current build.
// In watch mode, previous build state will be reused.
const { affectedFiles } = await compilation.initialize(
rootNames,
compilerOptions,
hostOptions,
configurationDiagnostics,
);
const {
affectedFiles,
compilerOptions: { allowJs },
} = await compilation.initialize(pluginOptions.tsconfig, hostOptions, (compilerOptions) => {
if (
compilerOptions.target === undefined ||
compilerOptions.target < ts.ScriptTarget.ES2022
) {
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
compilerOptions.target = ts.ScriptTarget.ES2022;
compilerOptions.useDefineForClassFields ??= false;

// Only add the warning on the initial build
setupWarnings?.push({
text:
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
'"false" respectively by the Angular CLI.',
location: { file: pluginOptions.tsconfig },
notes: [
{
text:
'To control ECMA version and features use the Browerslist configuration. ' +
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
},
],
});
}

return {
...compilerOptions,
noEmitOnError: false,
inlineSources: pluginOptions.sourcemap,
inlineSourceMap: pluginOptions.sourcemap,
mapRoot: undefined,
sourceRoot: undefined,
};
});
shouldTsIgnoreJs = !allowJs;

// Clear affected files from the cache (if present)
if (pluginOptions.sourceFileCache) {
Expand All @@ -319,8 +299,7 @@ export function createCompilerPlugin(
}

profileSync('NG_DIAGNOSTICS_TOTAL', () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
for (const diagnostic of compilation!.collectDiagnostics()) {
for (const diagnostic of compilation.collectDiagnostics()) {
const message = convertTypeScriptDiagnostic(diagnostic);
if (diagnostic.category === ts.DiagnosticCategory.Error) {
(result.errors ??= []).push(message);
Expand All @@ -332,67 +311,73 @@ export function createCompilerPlugin(

fileEmitter = compilation.createFileEmitter();

// Reset the setup warnings so that they are only shown during the first build.
setupWarnings = undefined;

return result;
});

build.onLoad(
{ filter: compilerOptions.allowJs ? /\.[cm]?[jt]sx?$/ : /\.[cm]?tsx?$/ },
(args) =>
profileAsync(
'NG_EMIT_TS*',
async () => {
assert.ok(fileEmitter, 'Invalid plugin execution order');

const request = pluginOptions.fileReplacements?.[args.path] ?? args.path;

// The filename is currently used as a cache key. Since the cache is memory only,
// the options cannot change and do not need to be represented in the key. If the
// cache is later stored to disk, then the options that affect transform output
// would need to be added to the key as well as a check for any change of content.
let contents = pluginOptions.sourceFileCache?.typeScriptFileCache.get(
pathToFileURL(request).href,
);
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) =>
profileAsync(
'NG_EMIT_TS*',
async () => {
assert.ok(fileEmitter, 'Invalid plugin execution order');

if (contents === undefined) {
const typescriptResult = await fileEmitter(request);
if (!typescriptResult?.content) {
// 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 (compilerOptions.allowJs && /\.[cm]?js$/.test(request)) {
return undefined;
}

// Otherwise return an error
return {
errors: [
createMissingFileError(
request,
args.path,
build.initialOptions.absWorkingDir ?? '',
),
],
};
}
const request = pluginOptions.fileReplacements?.[args.path] ?? args.path;

contents = await javascriptTransformer.transformData(
request,
typescriptResult.content,
true /* skipLinker */,
);
// Skip TS load attempt if JS TypeScript compilation not enabled and file is JS
if (shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
return undefined;
}

pluginOptions.sourceFileCache?.typeScriptFileCache.set(
pathToFileURL(request).href,
contents,
);
// The filename is currently used as a cache key. Since the cache is memory only,
// the options cannot change and do not need to be represented in the key. If the
// cache is later stored to disk, then the options that affect transform output
// would need to be added to the key as well as a check for any change of content.
let contents = pluginOptions.sourceFileCache?.typeScriptFileCache.get(
pathToFileURL(request).href,
);

if (contents === undefined) {
const typescriptResult = await fileEmitter(request);
if (!typescriptResult?.content) {
// 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)) {
return undefined;
}

// Otherwise return an error
return {
errors: [
createMissingFileError(
request,
args.path,
build.initialOptions.absWorkingDir ?? '',
),
],
};
}

return {
contents = await javascriptTransformer.transformData(
request,
typescriptResult.content,
true /* skipLinker */,
);

pluginOptions.sourceFileCache?.typeScriptFileCache.set(
pathToFileURL(request).href,
contents,
loader: 'js',
};
},
true,
),
);
}

return {
contents,
loader: 'js',
};
},
true,
),
);

build.onLoad({ filter: /\.[cm]?js$/ }, (args) =>
Expand Down