Skip to content

Commit b63019c

Browse files
refactor: cjs loader (#2)
1 parent f38db84 commit b63019c

15 files changed

+322
-263
lines changed

src/cjs/api/global-require-patch.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Module from 'module';
2+
import { extensions } from './module-extensions.js';
3+
import { resolveFilename } from './module-resolve-filename.js';
4+
5+
export const register = () => {
6+
const { sourceMapsEnabled } = process;
7+
const { _extensions, _resolveFilename } = Module;
8+
9+
// register
10+
process.setSourceMapsEnabled(true);
11+
// @ts-expect-error overwriting read-only property
12+
Module._extensions = extensions;
13+
Module._resolveFilename = resolveFilename;
14+
15+
// unregister
16+
return () => {
17+
if (sourceMapsEnabled === false) {
18+
process.setSourceMapsEnabled(false);
19+
}
20+
21+
// @ts-expect-error overwriting read-only property
22+
Module._extensions = _extensions;
23+
Module._resolveFilename = _resolveFilename;
24+
};
25+
};

src/cjs/api/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { register } from './global-require-patch.js';

src/cjs/api/module-extensions.ts

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import fs from 'fs';
2+
import Module from 'module';
3+
import { createFilesMatcher } from 'get-tsconfig';
4+
import type { TransformOptions } from 'esbuild';
5+
import { transformSync } from '../../utils/transform/index.js';
6+
import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js';
7+
import { isESM } from '../../utils/esm-pattern.js';
8+
import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js';
9+
import { parent } from '../../utils/ipc/client.js';
10+
import { tsconfig } from './utils.js';
11+
12+
const typescriptExtensions = [
13+
'.cts',
14+
'.mts',
15+
'.ts',
16+
'.tsx',
17+
'.jsx',
18+
];
19+
20+
const transformExtensions = [
21+
'.js',
22+
'.cjs',
23+
'.mjs',
24+
];
25+
26+
const fileMatcher = tsconfig && createFilesMatcher(tsconfig);
27+
28+
// Clone Module._extensions with null prototype
29+
export const extensions = Object.assign(Object.create(null), Module._extensions);
30+
31+
const defaultLoader = extensions['.js'];
32+
33+
const transformer = (
34+
module: Module,
35+
filePath: string,
36+
) => {
37+
// For tracking dependencies in watch mode
38+
if (parent?.send) {
39+
parent.send({
40+
type: 'dependency',
41+
path: filePath,
42+
});
43+
}
44+
45+
const transformTs = typescriptExtensions.some(extension => filePath.endsWith(extension));
46+
const transformJs = transformExtensions.some(extension => filePath.endsWith(extension));
47+
if (!transformTs && !transformJs) {
48+
return defaultLoader(module, filePath);
49+
}
50+
51+
let code = fs.readFileSync(filePath, 'utf8');
52+
53+
if (filePath.endsWith('.cjs')) {
54+
// Contains native ESM check
55+
const transformed = transformDynamicImport(filePath, code);
56+
if (transformed) {
57+
code = (
58+
shouldApplySourceMap()
59+
? inlineSourceMap(transformed)
60+
: transformed.code
61+
);
62+
}
63+
} else if (
64+
transformTs
65+
66+
// CommonJS file but uses ESM import/export
67+
|| isESM(code)
68+
) {
69+
const transformed = transformSync(
70+
code,
71+
filePath,
72+
{
73+
tsconfigRaw: fileMatcher?.(filePath) as TransformOptions['tsconfigRaw'],
74+
},
75+
);
76+
77+
code = (
78+
shouldApplySourceMap()
79+
? inlineSourceMap(transformed)
80+
: transformed.code
81+
);
82+
}
83+
84+
module._compile(code, filePath);
85+
};
86+
87+
[
88+
/**
89+
* Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders
90+
*
91+
* Any file requested with an explicit extension will be loaded using the .js loader:
92+
* https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430
93+
*/
94+
'.js',
95+
96+
/**
97+
* Loaders for implicitly resolvable extensions
98+
* https://github.com/nodejs/node/blob/v12.16.0/lib/internal/modules/cjs/loader.js#L1166
99+
*/
100+
'.ts',
101+
'.tsx',
102+
'.jsx',
103+
].forEach((extension) => {
104+
extensions[extension] = transformer;
105+
});
106+
107+
/**
108+
* Loaders for explicitly resolvable extensions
109+
* (basically just .mjs because CJS loader has a special handler for it)
110+
*
111+
* Loaders for extensions .cjs, .cts, & .mts don't need to be
112+
* registered because they're explicitly specified and unknown
113+
* extensions (incl .cjs) fallsback to using the '.js' loader:
114+
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
115+
*
116+
* That said, it's actually ".js" and ".mjs" that get special treatment
117+
* rather than ".cjs" (it might as well be ".random-ext")
118+
*/
119+
Object.defineProperty(extensions, '.mjs', {
120+
value: transformer,
121+
122+
// Prevent Object.keys from detecting these extensions
123+
// when CJS loader iterates over the possible extensions
124+
enumerable: false,
125+
});
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import path from 'path';
2+
import Module from 'module';
3+
import { createPathsMatcher } from 'get-tsconfig';
4+
import { resolveTsPath } from '../../utils/resolve-ts-path.js';
5+
import type { NodeError } from '../../types.js';
6+
import { isRelativePathPattern } from '../../utils/is-relative-path-pattern.js';
7+
import {
8+
isTsFilePatten,
9+
tsconfig,
10+
} from './utils.js';
11+
12+
const nodeModulesPath = `${path.sep}node_modules${path.sep}`;
13+
14+
const tsconfigPathsMatcher = tsconfig && createPathsMatcher(tsconfig);
15+
16+
type ResolveFilename = typeof Module._resolveFilename;
17+
18+
const defaultResolver = Module._resolveFilename.bind(Module);
19+
20+
/**
21+
* Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions
22+
*/
23+
const resolveTsFilename = (
24+
request: string,
25+
parent: Module.Parent,
26+
isMain: boolean,
27+
options?: Record<PropertyKey, unknown>,
28+
) => {
29+
const tsPath = resolveTsPath(request);
30+
31+
if (
32+
parent?.filename
33+
&& (
34+
isTsFilePatten.test(parent.filename)
35+
|| tsconfig?.config.compilerOptions?.allowJs
36+
)
37+
&& tsPath
38+
) {
39+
for (const tryTsPath of tsPath) {
40+
try {
41+
return defaultResolver(
42+
tryTsPath,
43+
parent,
44+
isMain,
45+
options,
46+
);
47+
} catch (error) {
48+
const { code } = error as NodeError;
49+
if (
50+
code !== 'MODULE_NOT_FOUND'
51+
&& code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
52+
) {
53+
throw error;
54+
}
55+
}
56+
}
57+
}
58+
};
59+
60+
export const resolveFilename: ResolveFilename = (
61+
request,
62+
parent,
63+
isMain,
64+
options,
65+
) => {
66+
// Strip query string
67+
const queryIndex = request.indexOf('?');
68+
if (queryIndex !== -1) {
69+
request = request.slice(0, queryIndex);
70+
}
71+
72+
if (
73+
tsconfigPathsMatcher
74+
75+
// bare specifier
76+
&& !isRelativePathPattern.test(request)
77+
78+
// Dependency paths should not be resolved using tsconfig.json
79+
&& !parent?.filename?.includes(nodeModulesPath)
80+
) {
81+
const possiblePaths = tsconfigPathsMatcher(request);
82+
83+
for (const possiblePath of possiblePaths) {
84+
const tsFilename = resolveTsFilename(possiblePath, parent, isMain, options);
85+
if (tsFilename) {
86+
return tsFilename;
87+
}
88+
89+
try {
90+
return defaultResolver(
91+
possiblePath,
92+
parent,
93+
isMain,
94+
options,
95+
);
96+
} catch {}
97+
}
98+
}
99+
100+
const tsFilename = resolveTsFilename(request, parent, isMain, options);
101+
if (tsFilename) {
102+
return tsFilename;
103+
}
104+
105+
return defaultResolver(request, parent, isMain, options);
106+
};

src/cjs/api/utils.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import path from 'path';
2+
import {
3+
getTsconfig,
4+
parseTsconfig,
5+
} from 'get-tsconfig';
6+
7+
export const isTsFilePatten = /\.[cm]?tsx?$/;
8+
9+
export const tsconfig = (
10+
process.env.TSX_TSCONFIG_PATH
11+
? {
12+
path: path.resolve(process.env.TSX_TSCONFIG_PATH),
13+
config: parseTsconfig(process.env.TSX_TSCONFIG_PATH),
14+
}
15+
: getTsconfig()
16+
);

0 commit comments

Comments
 (0)