Skip to content

Commit 0de7266

Browse files
committed
fix(common): update TS module resolution flow
1 parent 3b194f1 commit 0de7266

File tree

14 files changed

+152
-116
lines changed

14 files changed

+152
-116
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/src/load-module.ts

Lines changed: 9 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,7 @@ import * as path from 'node:path';
22
import * as url from 'node:url';
33
import type { logging } from '@angular-devkit/core';
44

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-
}
5+
import { registerTsProject } from './register-ts-project';
556

567
/**
578
* This uses a dynamic import to load a module which may be ESM.
@@ -72,22 +23,20 @@ function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
7223
/**
7324
* Loads CJS and ESM modules based on extension
7425
*/
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-
26+
export async function loadModule<T>(modulePath: string, tsConfig: string): Promise<T> {
8227
switch (path.extname(modulePath)) {
8328
case '.mjs':
8429
// Load the ESM configuration file using the TypeScript dynamic import workaround.
8530
// Once TypeScript provides support for keeping the dynamic import this workaround can be
8631
// changed to a direct dynamic import.
8732
return (await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))).default;
33+
8834
case '.cjs':
8935
return require(modulePath);
36+
9037
case '.ts':
38+
const unregisterTsProject = registerTsProject(tsConfig);
39+
9140
try {
9241
// If it's a TS file then there are 2 cases for exporing an object.
9342
// The first one is `export blah`, transpiled into `module.exports = { blah} `.
@@ -101,7 +50,10 @@ export async function loadModule<T>(
10150
return (await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))).default;
10251
}
10352
throw e;
53+
} finally {
54+
unregisterTsProject();
10455
}
56+
10557
//.js
10658
default:
10759
// The file could be either CommonJS or ESM.
@@ -120,19 +72,3 @@ export async function loadModule<T>(
12072
}
12173
}
12274
}
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as path from 'node:path';
2+
import type { CompilerOptions } from 'typescript';
3+
4+
let ts: typeof import('typescript');
5+
let isTsEsmLoaderRegistered = false;
6+
7+
export function registerTsProject(tsConfig: string) {
8+
const cleanupFunctions = [registerTsConfigPaths(tsConfig), registerTsNodeService(tsConfig)];
9+
10+
// Add ESM support for `.ts` files.
11+
// NOTE: There is no cleanup function for this, as it's not possible to unregister the loader.
12+
// Based on limited testing, it doesn't seem to matter if we register it multiple times, but just in
13+
// case let's keep a flag to prevent it.
14+
if (!isTsEsmLoaderRegistered) {
15+
const module = require('node:module');
16+
if (module.register && packageIsInstalled('ts-node/esm')) {
17+
const url = require('node:url');
18+
module.register(url.pathToFileURL(require.resolve('ts-node/esm')));
19+
}
20+
isTsEsmLoaderRegistered = true;
21+
}
22+
23+
return () => {
24+
cleanupFunctions.forEach(fn => fn());
25+
};
26+
}
27+
28+
function registerTsNodeService(tsConfig: string): VoidFunction {
29+
const { register } = require('ts-node') as typeof import('ts-node');
30+
31+
const service = register({
32+
project: tsConfig,
33+
compilerOptions: {
34+
module: 'CommonJS',
35+
types: [
36+
'node', // NOTE: `node` is added because users scripts can also use pure node's packages as webpack or others
37+
],
38+
},
39+
});
40+
41+
return () => {
42+
service.enabled(false);
43+
};
44+
}
45+
46+
function registerTsConfigPaths(tsConfig: string): VoidFunction {
47+
const tsConfigPaths = require('tsconfig-paths') as typeof import('tsconfig-paths');
48+
const result = tsConfigPaths.loadConfig(tsConfig);
49+
if (result.resultType === 'success') {
50+
const { absoluteBaseUrl: baseUrl, paths } = result;
51+
if (baseUrl && paths) {
52+
// Returns a function to undo paths registration.
53+
return tsConfigPaths.register({ baseUrl, paths });
54+
}
55+
}
56+
57+
// We cannot return anything here if paths failed to be registered.
58+
// Additionally, I don't think we should perform any logging in this
59+
// context, considering that this is internal information not exposed
60+
// to the end user
61+
return () => {};
62+
}
63+
64+
function packageIsInstalled(m: string): boolean {
65+
try {
66+
require.resolve(m);
67+
return true;
68+
} catch {
69+
return false;
70+
}
71+
}
72+
73+
function readCompilerOptions(tsConfig: string): CompilerOptions {
74+
ts ??= require('typescript');
75+
76+
const jsonContent = ts.readConfigFile(tsConfig, ts.sys.readFile);
77+
const { options } = ts.parseJsonConfigFileContent(
78+
jsonContent.config,
79+
ts.sys,
80+
path.dirname(tsConfig)
81+
);
82+
83+
// This property is returned in compiler options for some reason, but not part of the typings.
84+
// ts-node fails on unknown props, so we have to remove it.
85+
delete options.configFilePath;
86+
return options;
87+
}

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);

packages/custom-esbuild/src/load-plugins.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import * as path from 'node:path';
22
import type { Plugin } from 'esbuild';
3-
import type { logging } from '@angular-devkit/core';
43
import { loadModule } from '@angular-builders/common';
54

65
export async function loadPlugins(
76
paths: string[] | undefined,
87
workspaceRoot: string,
9-
tsConfig: string,
10-
logger: logging.LoggerApi
8+
tsConfig: string
119
): Promise<Plugin[]> {
1210
const plugins = await Promise.all(
1311
(paths || []).map(pluginPath =>
14-
loadModule<Plugin | Plugin[]>(path.join(workspaceRoot, pluginPath), tsConfig, logger)
12+
loadModule<Plugin | Plugin[]>(path.join(workspaceRoot, pluginPath), tsConfig)
1513
)
1614
);
1715

0 commit comments

Comments
 (0)