Skip to content

Commit 4be7c7e

Browse files
committed
refactor(cjs): implement custom resolver (#36)
1 parent de900a1 commit 4be7c7e

File tree

2 files changed

+87
-56
lines changed

2 files changed

+87
-56
lines changed

src/cjs/api/module-extensions.ts

+29-33
Original file line numberDiff line numberDiff line change
@@ -87,42 +87,38 @@ const transformer = (
8787
module._compile(code, cleanFilePath);
8888
};
8989

90-
[
91-
/**
92-
* Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders
93-
*
94-
* Any file requested with an explicit extension will be loaded using the .js loader:
95-
* https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430
96-
*/
97-
'.js',
90+
/**
91+
* Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders
92+
*
93+
* Any file requested with an explicit extension will be loaded using the .js loader:
94+
* https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430
95+
*/
96+
extensions['.js'] = transformer;
9897

99-
/**
100-
* Loaders for implicitly resolvable extensions
101-
* https://github.com/nodejs/node/blob/v12.16.0/lib/internal/modules/cjs/loader.js#L1166
102-
*/
98+
[
10399
'.ts',
104100
'.tsx',
105101
'.jsx',
106-
].forEach((extension) => {
107-
extensions[extension] = transformer;
108-
});
109-
110-
/**
111-
* Loaders for explicitly resolvable extensions
112-
* (basically just .mjs because CJS loader has a special handler for it)
113-
*
114-
* Loaders for extensions .cjs, .cts, & .mts don't need to be
115-
* registered because they're explicitly specified and unknown
116-
* extensions (incl .cjs) fallsback to using the '.js' loader:
117-
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
118-
*
119-
* That said, it's actually ".js" and ".mjs" that get special treatment
120-
* rather than ".cjs" (it might as well be ".random-ext")
121-
*/
122-
Object.defineProperty(extensions, '.mjs', {
123-
value: transformer,
124102

125-
// Prevent Object.keys from detecting these extensions
126-
// when CJS loader iterates over the possible extensions
127-
enumerable: false,
103+
/**
104+
* Loaders for extensions .cjs, .cts, & .mts don't need to be
105+
* registered because they're explicitly specified. And unknown
106+
* extensions (incl .cjs) fallsback to using the '.js' loader:
107+
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
108+
*
109+
* That said, it's actually ".js" and ".mjs" that get special treatment
110+
* rather than ".cjs" (it might as well be ".random-ext")
111+
*/
112+
'.mjs',
113+
].forEach((extension) => {
114+
Object.defineProperty(extensions, extension, {
115+
value: transformer,
116+
117+
/**
118+
* Prevent Object.keys from detecting these extensions
119+
* when CJS loader iterates over the possible extensions
120+
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609
121+
*/
122+
enumerable: false,
123+
});
128124
});

src/cjs/api/module-resolve-filename.ts

+58-23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';
88

99
type ResolveFilename = typeof Module._resolveFilename;
1010

11+
type SimpleResolve = (request: string) => string;
12+
1113
const nodeModulesPath = `${path.sep}node_modules${path.sep}`;
1214

1315
export const interopCjsExports = (
@@ -39,11 +41,9 @@ export const interopCjsExports = (
3941
* Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions
4042
*/
4143
const resolveTsFilename = (
42-
nextResolve: ResolveFilename,
44+
resolve: SimpleResolve,
4345
request: string,
4446
parent: Module.Parent,
45-
isMain: boolean,
46-
options?: Record<PropertyKey, unknown>,
4747
) => {
4848
if (
4949
!(parent?.filename && tsExtensionsPattern.test(parent.filename))
@@ -59,12 +59,7 @@ const resolveTsFilename = (
5959

6060
for (const tryTsPath of tsPath) {
6161
try {
62-
return nextResolve(
63-
tryTsPath,
64-
parent,
65-
isMain,
66-
options,
67-
);
62+
return resolve(tryTsPath);
6863
} catch (error) {
6964
const { code } = error as NodeError;
7065
if (
@@ -77,6 +72,19 @@ const resolveTsFilename = (
7772
}
7873
};
7974

75+
const extensions = ['.ts', '.tsx', '.jsx'] as const;
76+
77+
const tryExtensions = (
78+
resolve: SimpleResolve,
79+
request: string,
80+
) => {
81+
for (const extension of extensions) {
82+
try {
83+
return resolve(request + extension);
84+
} catch {}
85+
}
86+
};
87+
8088
export const createResolveFilename = (
8189
nextResolve: ResolveFilename,
8290
): ResolveFilename => (
@@ -99,39 +107,66 @@ export const createResolveFilename = (
99107
request = fileURLToPath(request);
100108
}
101109

110+
const resolve: SimpleResolve = request_ => nextResolve(
111+
request_,
112+
parent,
113+
isMain,
114+
options,
115+
);
116+
102117
// Resolve TS path alias
103118
if (
104119
tsconfigPathsMatcher
105120

106-
// bare specifier
107-
&& !isRelativePath(request)
121+
// bare specifier
122+
&& !isRelativePath(request)
108123

109-
// Dependency paths should not be resolved using tsconfig.json
110-
&& !parent?.filename?.includes(nodeModulesPath)
124+
// Dependency paths should not be resolved using tsconfig.json
125+
&& !parent?.filename?.includes(nodeModulesPath)
111126
) {
112127
const possiblePaths = tsconfigPathsMatcher(request);
113128

114129
for (const possiblePath of possiblePaths) {
115-
const tsFilename = resolveTsFilename(nextResolve, possiblePath, parent, isMain, options);
130+
const tsFilename = resolveTsFilename(resolve, possiblePath, parent);
116131
if (tsFilename) {
117132
return tsFilename + query;
118133
}
119134

120135
try {
121-
return nextResolve(
122-
possiblePath,
123-
parent,
124-
isMain,
125-
options,
126-
) + query;
127-
} catch {}
136+
return resolve(possiblePath) + query;
137+
} catch {
138+
/**
139+
* Try order:
140+
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L410-L413
141+
*/
142+
const resolved = (
143+
tryExtensions(resolve, possiblePath)
144+
|| tryExtensions(resolve, path.resolve(possiblePath, 'index'))
145+
);
146+
if (resolved) {
147+
return resolved + query;
148+
}
149+
}
128150
}
129151
}
130152

131-
const tsFilename = resolveTsFilename(nextResolve, request, parent, isMain, options);
153+
// If extension exists
154+
const tsFilename = resolveTsFilename(resolve, request, parent);
132155
if (tsFilename) {
133156
return tsFilename + query;
134157
}
135158

136-
return nextResolve(request, parent, isMain, options) + query;
159+
try {
160+
return resolve(request) + query;
161+
} catch (error) {
162+
const resolved = (
163+
tryExtensions(resolve, request)
164+
|| tryExtensions(resolve, path.resolve(request, 'index'))
165+
);
166+
if (resolved) {
167+
return resolved + query;
168+
}
169+
170+
throw error;
171+
}
137172
};

0 commit comments

Comments
 (0)