Skip to content

Commit b6737b9

Browse files
committed
fix(common): update TS module resolution flow
This commit updates the implementation for resolving `.ts` files. Instead of registering the `ts-node` project only once, we now refrain from doing so since there might be multiple projects with different configurations. The current approach involves dynamically switching the implementation for registering and unregistering the project after the `.ts` file has been transpiled and resolved. This change addresses an issue where warnings were encountered when `ts-node` attempted to register with different configurations. The number of configurations is no longer a concern, as each time we need to read a `.ts` file, a new TS project is registered. This adjustment does not impact performance or other attributes because `ts-node` allows native project disabling. Part of the implementation has been adapted from what Nrwl Nx already has; we can find their implementation here: https://github.com/nrwl/nx/blob/master/packages/nx/src/plugins/js/utils/register.ts It's worth noting that their implementation is somewhat versatile, as it also supports SWC. Closes: #1197 Closes: #1213
1 parent 3b194f1 commit b6737b9

File tree

16 files changed

+140
-119
lines changed

16 files changed

+140
-119
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { Configuration } from 'webpack';
22

3+
import { WebpackEsmPlugin } from 'webpack-esm-plugin';
4+
35
export default async (cfg: Configuration) => {
46
const { default: configFromEsm } = await import('./custom-webpack.config.js');
7+
8+
// This is used to ensure we fixed the following issue:
9+
// https://github.com/just-jeb/angular-builders/issues/1213
10+
cfg.plugins!.push(new WebpackEsmPlugin());
11+
512
// Do some stuff with config and configFromEsm
613
return { ...cfg, ...configFromEsm };
714
};

examples/custom-webpack/sanity-app-esm/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"karma-jasmine": "5.1.0",
4545
"karma-jasmine-html-reporter": "2.1.0",
4646
"puppeteer": "21.10.0",
47-
"typescript": "5.3.3"
47+
"typescript": "5.3.3",
48+
"webpack-esm-plugin": "file:./webpack-esm-plugin"
4849
}
4950
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "webpack-esm-plugin",
3+
"version": "0.0.1",
4+
"module": "./webpack-esm-plugin.mjs",
5+
"typings": "./webpack-esm-plugin.d.ts",
6+
"exports": {
7+
"./package.json": {
8+
"default": "./package.json"
9+
},
10+
".": {
11+
"types": "./webpack-esm-plugin.d.ts",
12+
"node": "./webpack-esm-plugin.mjs",
13+
"default": "./webpack-esm-plugin.mjs"
14+
}
15+
},
16+
"sideEffects": false
17+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as webpack from 'webpack';
2+
3+
export declare class WebpackEsmPlugin {
4+
apply(compiler: webpack.Compiler): void;
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class WebpackEsmPlugin {
2+
apply(compiler) {
3+
console.error('hello from the WebpackEsmPlugin');
4+
}
5+
}
6+
7+
export { WebpackEsmPlugin };

packages/common/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"clean": "rimraf dist"
2121
},
2222
"dependencies": {
23-
"@angular-devkit/core": "^17.1.0",
2423
"ts-node": "^10.0.0",
2524
"tsconfig-paths": "^4.1.0"
2625
},

packages/common/src/load-module.ts

Lines changed: 9 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,7 @@
11
import * as path from 'node:path';
22
import * as url from 'node:url';
3-
import type { logging } from '@angular-devkit/core';
43

5-
const _tsNodeRegister = (() => {
6-
let lastTsConfig: string | undefined;
7-
return (tsConfig: string, logger: logging.LoggerApi) => {
8-
// Check if the function was previously called with the same tsconfig
9-
if (lastTsConfig && lastTsConfig !== tsConfig) {
10-
logger.warn(`Trying to register ts-node again with a different tsconfig - skipping the registration.
11-
tsconfig 1: ${lastTsConfig}
12-
tsconfig 2: ${tsConfig}`);
13-
}
14-
15-
if (lastTsConfig) {
16-
return;
17-
}
18-
19-
lastTsConfig = tsConfig;
20-
21-
loadTsNode().register({
22-
project: tsConfig,
23-
compilerOptions: {
24-
module: 'CommonJS',
25-
types: [
26-
'node', // NOTE: `node` is added because users scripts can also use pure node's packages as webpack or others
27-
],
28-
},
29-
});
30-
31-
const tsConfigPaths = loadTsConfigPaths();
32-
const result = tsConfigPaths.loadConfig(tsConfig);
33-
// The `loadConfig` returns a `ConfigLoaderResult` which must be guarded with
34-
// the `resultType` check.
35-
if (result.resultType === 'success') {
36-
const { absoluteBaseUrl: baseUrl, paths } = result;
37-
if (baseUrl && paths) {
38-
tsConfigPaths.register({ baseUrl, paths });
39-
}
40-
}
41-
};
42-
})();
43-
44-
/**
45-
* check for TS node registration
46-
* @param file: file name or file directory are allowed
47-
* @todo tsNodeRegistration: require ts-node if file extension is TypeScript
48-
*/
49-
function tsNodeRegister(file: string = '', tsConfig: string, logger: logging.LoggerApi) {
50-
if (file?.endsWith('.ts')) {
51-
// Register TS compiler lazily
52-
_tsNodeRegister(tsConfig, logger);
53-
}
54-
}
4+
import { registerTsProject } from './register-ts-project';
555

566
/**
577
* This uses a dynamic import to load a module which may be ESM.
@@ -72,22 +22,20 @@ function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
7222
/**
7323
* Loads CJS and ESM modules based on extension
7424
*/
75-
export async function loadModule<T>(
76-
modulePath: string,
77-
tsConfig: string,
78-
logger: logging.LoggerApi
79-
): Promise<T> {
80-
tsNodeRegister(modulePath, tsConfig, logger);
81-
25+
export async function loadModule<T>(modulePath: string, tsConfig: string): Promise<T> {
8226
switch (path.extname(modulePath)) {
8327
case '.mjs':
8428
// Load the ESM configuration file using the TypeScript dynamic import workaround.
8529
// Once TypeScript provides support for keeping the dynamic import this workaround can be
8630
// changed to a direct dynamic import.
8731
return (await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))).default;
32+
8833
case '.cjs':
8934
return require(modulePath);
35+
9036
case '.ts':
37+
const unregisterTsProject = registerTsProject(tsConfig);
38+
9139
try {
9240
// If it's a TS file then there are 2 cases for exporing an object.
9341
// The first one is `export blah`, transpiled into `module.exports = { blah} `.
@@ -101,7 +49,10 @@ export async function loadModule<T>(
10149
return (await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))).default;
10250
}
10351
throw e;
52+
} finally {
53+
unregisterTsProject();
10454
}
55+
10556
//.js
10657
default:
10758
// The file could be either CommonJS or ESM.
@@ -120,19 +71,3 @@ export async function loadModule<T>(
12071
}
12172
}
12273
}
123-
124-
/**
125-
* Loads `ts-node` lazily. Moved to a separate function to declare
126-
* a return type, more readable than an inline variant.
127-
*/
128-
function loadTsNode(): typeof import('ts-node') {
129-
return require('ts-node');
130-
}
131-
132-
/**
133-
* Loads `tsconfig-paths` lazily. Moved to a separate function to declare
134-
* a return type, more readable than an inline variant.
135-
*/
136-
function loadTsConfigPaths(): typeof import('tsconfig-paths') {
137-
return require('tsconfig-paths');
138-
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
let isTsEsmLoaderRegistered = false;
2+
3+
export function registerTsProject(tsConfig: string) {
4+
const cleanupFunctions = [registerTsConfigPaths(tsConfig), registerTsNodeService(tsConfig)];
5+
6+
// Add ESM support for `.ts` files.
7+
// NOTE: There is no cleanup function for this, as it's not possible to unregister the loader.
8+
// Based on limited testing, it doesn't seem to matter if we register it multiple times, but just in
9+
// case let's keep a flag to prevent it.
10+
if (!isTsEsmLoaderRegistered) {
11+
const module = require('node:module');
12+
if (module.register && packageIsInstalled('ts-node/esm')) {
13+
const url = require('node:url');
14+
module.register(url.pathToFileURL(require.resolve('ts-node/esm')));
15+
}
16+
isTsEsmLoaderRegistered = true;
17+
}
18+
19+
return () => {
20+
cleanupFunctions.forEach(fn => fn());
21+
};
22+
}
23+
24+
function registerTsNodeService(tsConfig: string): VoidFunction {
25+
const { register } = require('ts-node') as typeof import('ts-node');
26+
27+
const service = register({
28+
project: tsConfig,
29+
compilerOptions: {
30+
module: 'CommonJS',
31+
types: [
32+
'node', // NOTE: `node` is added because users scripts can also use pure node's packages as webpack or others
33+
],
34+
},
35+
});
36+
37+
return () => {
38+
service.enabled(false);
39+
};
40+
}
41+
42+
function registerTsConfigPaths(tsConfig: string): VoidFunction {
43+
const tsConfigPaths = require('tsconfig-paths') as typeof import('tsconfig-paths');
44+
const result = tsConfigPaths.loadConfig(tsConfig);
45+
if (result.resultType === 'success') {
46+
const { absoluteBaseUrl: baseUrl, paths } = result;
47+
if (baseUrl && paths) {
48+
// Returns a function to undo paths registration.
49+
return tsConfigPaths.register({ baseUrl, paths });
50+
}
51+
}
52+
53+
// We cannot return anything here if paths failed to be registered.
54+
// Additionally, I don't think we should perform any logging in this
55+
// context, considering that this is internal information not exposed
56+
// to the end user
57+
return () => {};
58+
}
59+
60+
function packageIsInstalled(m: string): boolean {
61+
try {
62+
require.resolve(m);
63+
return true;
64+
} catch {
65+
return false;
66+
}
67+
}

packages/custom-esbuild/src/application/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,10 @@ export function buildCustomEsbuildApplication(
1717
const tsConfig = path.join(workspaceRoot, options.tsConfig);
1818

1919
return defer(async () => {
20-
const codePlugins = await loadPlugins(options.plugins, workspaceRoot, tsConfig, context.logger);
20+
const codePlugins = await loadPlugins(options.plugins, workspaceRoot, tsConfig);
2121

2222
const indexHtmlTransformer = options.indexHtmlTransformer
23-
? await loadModule(
24-
path.join(workspaceRoot, options.indexHtmlTransformer),
25-
tsConfig,
26-
context.logger
27-
)
23+
? await loadModule(path.join(workspaceRoot, options.indexHtmlTransformer), tsConfig)
2824
: undefined;
2925

3026
return { codePlugins, indexHtmlTransformer } as ApplicationBuilderExtensions;

packages/custom-esbuild/src/dev-server/index.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,14 @@ export function executeCustomDevServerBuilder(
4242
const middleware = await Promise.all(
4343
(options.middlewares || []).map(middlewarePath =>
4444
// https://github.com/angular/angular-cli/pull/26212/files#diff-a99020cbdb97d20b2bc686bcb64b31942107d56db06fd880171b0a86f7859e6eR52
45-
loadModule<Connect.NextHandleFunction>(
46-
path.join(workspaceRoot, middlewarePath),
47-
tsConfig,
48-
context.logger
49-
)
45+
loadModule<Connect.NextHandleFunction>(path.join(workspaceRoot, middlewarePath), tsConfig)
5046
)
5147
);
5248

53-
const buildPlugins = await loadPlugins(
54-
buildOptions.plugins,
55-
workspaceRoot,
56-
tsConfig,
57-
context.logger
58-
);
49+
const buildPlugins = await loadPlugins(buildOptions.plugins, workspaceRoot, tsConfig);
5950

6051
const indexHtmlTransformer: IndexHtmlTransform = buildOptions.indexHtmlTransformer
61-
? await loadModule(
62-
path.join(workspaceRoot, buildOptions.indexHtmlTransformer),
63-
tsConfig,
64-
context.logger
65-
)
52+
? await loadModule(path.join(workspaceRoot, buildOptions.indexHtmlTransformer), tsConfig)
6653
: undefined;
6754

6855
patchBuilderContext(context, buildTarget);

0 commit comments

Comments
 (0)