Skip to content
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
Expand Up @@ -20,6 +20,7 @@ export interface ApplicationPresetOptions {
angularLinker?: {
shouldLink: boolean;
jitMode: boolean;
linkerPluginCreator: typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin;
};

forceES5?: boolean;
Expand All @@ -28,6 +29,11 @@ export interface ApplicationPresetOptions {
diagnosticReporter?: DiagnosticReporter;
}

// Extract Logger type from the linker function to avoid deep importing to access the type
type NgtscLogger = Parameters<
typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
>[0]['logger'];

type I18nDiagnostics = import('@angular/localize/src/tools/src/diagnostics').Diagnostics;
function createI18nDiagnostics(reporter: DiagnosticReporter | undefined): I18nDiagnostics {
// Babel currently is synchronous so import cannot be used
Expand Down Expand Up @@ -106,9 +112,7 @@ function createI18nPlugins(
return plugins;
}

function createNgtscLogger(
reporter: DiagnosticReporter | undefined,
): import('@angular/compiler-cli/src/ngtsc/logging').Logger {
function createNgtscLogger(reporter: DiagnosticReporter | undefined): NgtscLogger {
return {
level: 1, // Info level
debug(...args: string[]) {},
Expand All @@ -130,12 +134,8 @@ export default function (api: unknown, options: ApplicationPresetOptions) {
let needRuntimeTransform = false;

if (options.angularLinker?.shouldLink) {
// Babel currently is synchronous so import cannot be used
const { createEs2015LinkerPlugin } =
require('@angular/compiler-cli/linker/babel') as typeof import('@angular/compiler-cli/linker/babel');

plugins.push(
createEs2015LinkerPlugin({
options.angularLinker.linkerPluginCreator({
linkerJitMode: options.angularLinker.jitMode,
// This is a workaround until https://github.com/angular/angular/issues/42769 is fixed.
sourceMapping: false,
Expand Down
37 changes: 35 additions & 2 deletions packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import { needsLinking } from '@angular/compiler-cli/linker';
import { custom } from 'babel-loader';
import { ScriptTarget } from 'typescript';
import { loadEsmModule } from '../utils/load-esm';
import { ApplicationPresetOptions } from './presets/application';

interface AngularCustomOptions extends Pick<ApplicationPresetOptions, 'angularLinker' | 'i18n'> {
Expand All @@ -21,13 +21,35 @@ interface AngularCustomOptions extends Pick<ApplicationPresetOptions, 'angularLi
};
}

function requiresLinking(path: string, source: string): boolean {
/**
* Cached instance of the compiler-cli linker's needsLinking function.
*/
let needsLinking: typeof import('@angular/compiler-cli/linker').needsLinking | undefined;

/**
* Cached instance of the compiler-cli linker's Babel plugin factory function.
*/
let linkerPluginCreator:
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
| undefined;

async function requiresLinking(path: string, source: string): Promise<boolean> {
// @angular/core and @angular/compiler will cause false positives
// Also, TypeScript files do not require linking
if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) {
return false;
}

if (!needsLinking) {
// Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const linkerModule = await loadEsmModule<typeof import('@angular/compiler-cli/linker')>(
'@angular/compiler-cli/linker',
);
needsLinking = linkerModule.needsLinking;
}

return needsLinking(path, source);
}

Expand Down Expand Up @@ -55,9 +77,20 @@ export default custom<AngularCustomOptions>(() => {

// Analyze file for linking
if (await requiresLinking(this.resourcePath, source)) {
if (!linkerPluginCreator) {
// Load ESM `@angular/compiler-cli/linker/babel` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const linkerBabelModule = await loadEsmModule<
typeof import('@angular/compiler-cli/linker/babel')
>('@angular/compiler-cli/linker/babel');
linkerPluginCreator = linkerBabelModule.createEs2015LinkerPlugin;
}

customOptions.angularLinker = {
shouldLink: true,
jitMode: aot !== true,
linkerPluginCreator,
};
shouldProcess = true;
}
Expand Down
36 changes: 36 additions & 0 deletions packages/angular_devkit/build_angular/src/utils/load-esm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @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
*/

/**
* This uses a dynamic import to load a module which may be ESM.
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
* will currently, unconditionally downlevel dynamic import into a require call.
* require calls cannot load ESM code and will result in a runtime error. To workaround
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
* Once TypeScript provides support for keeping the dynamic import this workaround can
* be dropped.
*
* @param modulePath The path of the module to load.
* @returns A Promise that resolves to the dynamically imported module.
*/
export async function loadEsmModule<T>(modulePath: string): Promise<T> {
try {
return (await new Function('modulePath', `return import(modulePath);`)(modulePath)) as T;
} catch (e) {
// Temporary workaround to handle directory imports for current packages. ESM does not support
// directory imports.
// TODO_ESM: Remove once FW packages are fully ESM with defined `exports` package.json fields
if (e.code !== 'ERR_UNSUPPORTED_DIR_IMPORT') {
throw e;
}

return (await new Function('modulePath', `return import(modulePath);`)(
modulePath + '/index.js',
)) as T;
}
}
16 changes: 8 additions & 8 deletions packages/angular_devkit/build_angular/src/utils/read-tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import type { ParsedConfiguration } from '@angular/compiler-cli';
import * as path from 'path';
import { loadEsmModule } from './load-esm';

/**
* Reads and parses a given TsConfig file.
Expand All @@ -22,15 +23,14 @@ export async function readTsconfig(
): Promise<ParsedConfiguration> {
const tsConfigFullPath = workspaceRoot ? path.resolve(workspaceRoot, tsconfigPath) : tsconfigPath;

// This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
// CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
// will currently, unconditionally downlevel dynamic import into a require call.
// require calls cannot load ESM code and will result in a runtime error. To workaround
// this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
// Once TypeScript provides support for keeping the dynamic import this workaround can
// be dropped.
const compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)();
// Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const compilerCliModule = await loadEsmModule<{ readConfiguration: unknown; default: unknown }>(
'@angular/compiler-cli',
);
// If it is not ESM then the functions needed will be stored in the `default` property.
// TODO_ESM: This can be removed once `@angular/compiler-cli` is ESM only.
const { formatDiagnostics, readConfiguration } = (
compilerCliModule.readConfiguration ? compilerCliModule : compilerCliModule.default
) as typeof import('@angular/compiler-cli');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
*/

import { Path, getSystemPath, normalize } from '@angular-devkit/core';
import { Config, Filesystem, Generator } from '@angular/service-worker/config';
import type { Config, Filesystem } from '@angular/service-worker/config';
import * as crypto from 'crypto';
import { createReadStream, promises as fs, constants as fsConstants } from 'fs';
import * as path from 'path';
import { pipeline } from 'stream';
import { loadEsmModule } from './load-esm';

class CliFilesystem implements Filesystem {
constructor(private base: string) {}
Expand Down Expand Up @@ -103,8 +104,14 @@ export async function augmentAppWithServiceWorker(
}
}

// Load ESM `@angular/service-worker/config` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const GeneratorConstructor = (
await loadEsmModule<typeof import('@angular/service-worker/config')>(swConfigPath)
).Generator;

// Generate the manifest
const GeneratorConstructor = require(swConfigPath).Generator as typeof Generator;
const generator = new GeneratorConstructor(new CliFilesystem(distPath), baseHref);
const output = await generator.process(config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
persistentBuildCacheEnabled,
profilingEnabled,
} from '../../utils/environment-options';
import { loadEsmModule } from '../../utils/load-esm';
import { Spinner } from '../../utils/spinner';
import { addError } from '../../utils/webpack-diagnostics';
import { DedupeModuleResolvePlugin, ScriptsWebpackPlugin } from '../plugins';
Expand All @@ -49,15 +50,15 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
const extraRules: RuleSetRule[] = [];
const entryPoints: { [key: string]: [string, ...string[]] } = {};

// This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
// CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
// will currently, unconditionally downlevel dynamic import into a require call.
// require calls cannot load ESM code and will result in a runtime error. To workaround
// this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
// Once TypeScript provides support for keeping the dynamic import this workaround can
// be dropped.
const compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)();
// Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const compilerCliModule = await loadEsmModule<{
GLOBAL_DEFS_FOR_TERSER: unknown;
default: unknown;
}>('@angular/compiler-cli');
// If it is not ESM then the values needed will be stored in the `default` property.
// TODO_ESM: This can be removed once `@angular/compiler-cli` is ESM only.
const {
GLOBAL_DEFS_FOR_TERSER,
GLOBAL_DEFS_FOR_TERSER_WITH_AOT,
Expand Down