Skip to content

Commit 9dffab1

Browse files
committed
fix(@angular/build): V8 flags cause errors in ng build/serve processes
This commit removes the use of a worker for Babel transformation of external dependencies within the ESM loader hook. Closes angular#28901
1 parent 64f32c7 commit 9dffab1

File tree

4 files changed

+190
-185
lines changed

4 files changed

+190
-185
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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 PluginItem, transformAsync } from '@babel/core';
10+
import fs from 'node:fs';
11+
import path from 'node:path';
12+
import { loadEsmModule } from '../../utils/load-esm';
13+
14+
export interface BabelTransformOptions {
15+
sourcemap: boolean;
16+
thirdPartySourcemaps: boolean;
17+
advancedOptimizations: boolean;
18+
skipLinker?: boolean;
19+
sideEffects?: boolean;
20+
jit: boolean;
21+
instrumentForCoverage?: boolean;
22+
}
23+
24+
/**
25+
* Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function.
26+
*/
27+
let linkerPluginCreator:
28+
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
29+
| undefined;
30+
31+
/**
32+
* Cached instance of the compiler-cli linker's needsLinking function.
33+
*/
34+
let needsLinking: typeof import('@angular/compiler-cli/linker').needsLinking | undefined;
35+
36+
const textDecoder = new TextDecoder();
37+
const textEncoder = new TextEncoder();
38+
39+
export async function transformWithBabel(
40+
filename: string,
41+
data: string | Uint8Array,
42+
options: BabelTransformOptions,
43+
): Promise<Uint8Array> {
44+
const textData = typeof data === 'string' ? data : textDecoder.decode(data);
45+
const shouldLink = !options.skipLinker && (await requiresLinking(filename, textData));
46+
const useInputSourcemap =
47+
options.sourcemap &&
48+
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
49+
50+
// @ts-expect-error Import attribute syntax plugin does not currently have type definitions
51+
const { default: importAttributePlugin } = await import('@babel/plugin-syntax-import-attributes');
52+
const plugins: PluginItem[] = [importAttributePlugin];
53+
54+
if (options.instrumentForCoverage) {
55+
const { default: coveragePlugin } = await import('./plugins/add-code-coverage.js');
56+
plugins.push(coveragePlugin);
57+
}
58+
59+
if (shouldLink) {
60+
// Lazy load the linker plugin only when linking is required
61+
const linkerPlugin = await createLinkerPlugin(options);
62+
plugins.push(linkerPlugin);
63+
}
64+
65+
if (options.advancedOptimizations) {
66+
const sideEffectFree = options.sideEffects === false;
67+
const safeAngularPackage =
68+
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
69+
70+
const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } =
71+
await import('./plugins');
72+
73+
if (safeAngularPackage) {
74+
plugins.push(markTopLevelPure);
75+
}
76+
77+
plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [
78+
adjustStaticMembers,
79+
{ wrapDecorators: sideEffectFree },
80+
]);
81+
}
82+
83+
// If no additional transformations are needed, return the data directly
84+
if (plugins.length === 0) {
85+
// Strip sourcemaps if they should not be used
86+
return textEncoder.encode(
87+
useInputSourcemap ? textData : textData.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
88+
);
89+
}
90+
91+
const result = await transformAsync(textData, {
92+
filename,
93+
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
94+
sourceMaps: useInputSourcemap ? 'inline' : false,
95+
compact: false,
96+
configFile: false,
97+
babelrc: false,
98+
browserslistConfigFile: false,
99+
plugins,
100+
});
101+
102+
const outputCode = result?.code ?? textData;
103+
104+
// Strip sourcemaps if they should not be used.
105+
// Babel will keep the original comments even if sourcemaps are disabled.
106+
return textEncoder.encode(
107+
useInputSourcemap ? outputCode : outputCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
108+
);
109+
}
110+
111+
async function requiresLinking(path: string, source: string): Promise<boolean> {
112+
// @angular/core and @angular/compiler will cause false positives
113+
// Also, TypeScript files do not require linking
114+
if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) {
115+
return false;
116+
}
117+
118+
if (!needsLinking) {
119+
// Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround.
120+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
121+
// changed to a direct dynamic import.
122+
const linkerModule = await loadEsmModule<typeof import('@angular/compiler-cli/linker')>(
123+
'@angular/compiler-cli/linker',
124+
);
125+
needsLinking = linkerModule.needsLinking;
126+
}
127+
128+
return needsLinking(path, source);
129+
}
130+
131+
async function createLinkerPlugin(options: BabelTransformOptions) {
132+
linkerPluginCreator ??= (
133+
await loadEsmModule<typeof import('@angular/compiler-cli/linker/babel')>(
134+
'@angular/compiler-cli/linker/babel',
135+
)
136+
).createEs2015LinkerPlugin;
137+
138+
const linkerPlugin = linkerPluginCreator({
139+
linkerJitMode: options.jit,
140+
// This is a workaround until https://github.com/angular/angular/issues/42769 is fixed.
141+
sourceMapping: false,
142+
logger: {
143+
level: 1, // Info level
144+
debug(...args: string[]) {
145+
// eslint-disable-next-line no-console
146+
console.debug(args);
147+
},
148+
info(...args: string[]) {
149+
// eslint-disable-next-line no-console
150+
console.info(args);
151+
},
152+
warn(...args: string[]) {
153+
// eslint-disable-next-line no-console
154+
console.warn(args);
155+
},
156+
error(...args: string[]) {
157+
// eslint-disable-next-line no-console
158+
console.error(args);
159+
},
160+
},
161+
fileSystem: {
162+
resolve: path.resolve,
163+
exists: fs.existsSync,
164+
dirname: path.dirname,
165+
relative: path.relative,
166+
readFile: fs.readFileSync,
167+
// Node.JS types don't overlap the Compiler types.
168+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
169+
} as any,
170+
});
171+
172+
return linkerPlugin;
173+
}

packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts

+4-163
Original file line numberDiff line numberDiff line change
@@ -6,180 +6,21 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { type PluginItem, transformAsync } from '@babel/core';
10-
import fs from 'node:fs';
11-
import path from 'node:path';
129
import Piscina from 'piscina';
13-
import { loadEsmModule } from '../../utils/load-esm';
10+
import { BabelTransformOptions, transformWithBabel } from '../babel/transform';
1411

15-
interface JavaScriptTransformRequest {
12+
interface JavaScriptTransformRequest extends BabelTransformOptions {
1613
filename: string;
1714
data: string | Uint8Array;
18-
sourcemap: boolean;
19-
thirdPartySourcemaps: boolean;
20-
advancedOptimizations: boolean;
21-
skipLinker?: boolean;
22-
sideEffects?: boolean;
23-
jit: boolean;
24-
instrumentForCoverage?: boolean;
2515
}
2616

27-
const textDecoder = new TextDecoder();
28-
const textEncoder = new TextEncoder();
29-
3017
export default async function transformJavaScript(
3118
request: JavaScriptTransformRequest,
3219
): Promise<unknown> {
3320
const { filename, data, ...options } = request;
34-
const textData = typeof data === 'string' ? data : textDecoder.decode(data);
3521

36-
const transformedData = await transformWithBabel(filename, textData, options);
22+
const transformedData = await transformWithBabel(filename, data, options);
3723

3824
// Transfer the data via `move` instead of cloning
39-
return Piscina.move(textEncoder.encode(transformedData));
40-
}
41-
42-
/**
43-
* Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function.
44-
*/
45-
let linkerPluginCreator:
46-
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
47-
| undefined;
48-
49-
/**
50-
* Cached instance of the compiler-cli linker's needsLinking function.
51-
*/
52-
let needsLinking: typeof import('@angular/compiler-cli/linker').needsLinking | undefined;
53-
54-
async function transformWithBabel(
55-
filename: string,
56-
data: string,
57-
options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>,
58-
): Promise<string> {
59-
const shouldLink = !options.skipLinker && (await requiresLinking(filename, data));
60-
const useInputSourcemap =
61-
options.sourcemap &&
62-
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
63-
64-
// @ts-expect-error Import attribute syntax plugin does not currently have type definitions
65-
const { default: importAttributePlugin } = await import('@babel/plugin-syntax-import-attributes');
66-
const plugins: PluginItem[] = [importAttributePlugin];
67-
68-
if (options.instrumentForCoverage) {
69-
const { default: coveragePlugin } = await import('../babel/plugins/add-code-coverage.js');
70-
plugins.push(coveragePlugin);
71-
}
72-
73-
if (shouldLink) {
74-
// Lazy load the linker plugin only when linking is required
75-
const linkerPlugin = await createLinkerPlugin(options);
76-
plugins.push(linkerPlugin);
77-
}
78-
79-
if (options.advancedOptimizations) {
80-
const sideEffectFree = options.sideEffects === false;
81-
const safeAngularPackage =
82-
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
83-
84-
const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } =
85-
await import('../babel/plugins');
86-
87-
if (safeAngularPackage) {
88-
plugins.push(markTopLevelPure);
89-
}
90-
91-
plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [
92-
adjustStaticMembers,
93-
{ wrapDecorators: sideEffectFree },
94-
]);
95-
}
96-
97-
// If no additional transformations are needed, return the data directly
98-
if (plugins.length === 0) {
99-
// Strip sourcemaps if they should not be used
100-
return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
101-
}
102-
103-
const result = await transformAsync(data, {
104-
filename,
105-
inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
106-
sourceMaps: useInputSourcemap ? 'inline' : false,
107-
compact: false,
108-
configFile: false,
109-
babelrc: false,
110-
browserslistConfigFile: false,
111-
plugins,
112-
});
113-
114-
const outputCode = result?.code ?? data;
115-
116-
// Strip sourcemaps if they should not be used.
117-
// Babel will keep the original comments even if sourcemaps are disabled.
118-
return useInputSourcemap
119-
? outputCode
120-
: outputCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
121-
}
122-
123-
async function requiresLinking(path: string, source: string): Promise<boolean> {
124-
// @angular/core and @angular/compiler will cause false positives
125-
// Also, TypeScript files do not require linking
126-
if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) {
127-
return false;
128-
}
129-
130-
if (!needsLinking) {
131-
// Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround.
132-
// Once TypeScript provides support for keeping the dynamic import this workaround can be
133-
// changed to a direct dynamic import.
134-
const linkerModule = await loadEsmModule<typeof import('@angular/compiler-cli/linker')>(
135-
'@angular/compiler-cli/linker',
136-
);
137-
needsLinking = linkerModule.needsLinking;
138-
}
139-
140-
return needsLinking(path, source);
141-
}
142-
143-
async function createLinkerPlugin(options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>) {
144-
linkerPluginCreator ??= (
145-
await loadEsmModule<typeof import('@angular/compiler-cli/linker/babel')>(
146-
'@angular/compiler-cli/linker/babel',
147-
)
148-
).createEs2015LinkerPlugin;
149-
150-
const linkerPlugin = linkerPluginCreator({
151-
linkerJitMode: options.jit,
152-
// This is a workaround until https://github.com/angular/angular/issues/42769 is fixed.
153-
sourceMapping: false,
154-
logger: {
155-
level: 1, // Info level
156-
debug(...args: string[]) {
157-
// eslint-disable-next-line no-console
158-
console.debug(args);
159-
},
160-
info(...args: string[]) {
161-
// eslint-disable-next-line no-console
162-
console.info(args);
163-
},
164-
warn(...args: string[]) {
165-
// eslint-disable-next-line no-console
166-
console.warn(args);
167-
},
168-
error(...args: string[]) {
169-
// eslint-disable-next-line no-console
170-
console.error(args);
171-
},
172-
},
173-
fileSystem: {
174-
resolve: path.resolve,
175-
exists: fs.existsSync,
176-
dirname: path.dirname,
177-
relative: path.relative,
178-
readFile: fs.readFileSync,
179-
// Node.JS types don't overlap the Compiler types.
180-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
181-
} as any,
182-
});
183-
184-
return linkerPlugin;
25+
return Piscina.move(transformedData);
18526
}

packages/angular/build/src/tools/esbuild/javascript-transformer.ts

-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import { createHash } from 'node:crypto';
1010
import { readFile } from 'node:fs/promises';
11-
import { IMPORT_EXEC_ARGV } from '../../utils/server-rendering/esm-in-memory-loader/utils';
1211
import { WorkerPool } from '../../utils/worker-pool';
1312
import { Cache } from './cache';
1413

@@ -59,8 +58,6 @@ export class JavaScriptTransformer {
5958
this.#workerPool ??= new WorkerPool({
6059
filename: require.resolve('./javascript-transformer-worker'),
6160
maxThreads: this.maxThreads,
62-
// Prevent passing `--import` (loader-hooks) from parent to child worker.
63-
execArgv: process.execArgv.filter((v) => v !== IMPORT_EXEC_ARGV),
6461
});
6562

6663
return this.#workerPool;

0 commit comments

Comments
 (0)