Skip to content

Commit ae2a1bc

Browse files
authoredJun 28, 2024··
fix(cjs): leaking implicit extension resolver
1 parent b94482d commit ae2a1bc

File tree

4 files changed

+128
-125
lines changed

4 files changed

+128
-125
lines changed
 

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

+49-51
Original file line numberDiff line numberDiff line change
@@ -156,71 +156,69 @@ const resolveRequest = (
156156
export const createResolveFilename = (
157157
nextResolve: ResolveFilename,
158158
namespace?: string,
159-
): ResolveFilename => {
159+
): ResolveFilename => (
160+
request,
161+
parent,
162+
isMain,
163+
options,
164+
) => {
165+
const resolve: SimpleResolve = request_ => nextResolve(
166+
request_,
167+
parent,
168+
isMain,
169+
options,
170+
);
171+
172+
request = interopCjsExports(request);
173+
174+
if (parent?.filename) {
175+
const filePath = getOriginalFilePath(parent.filename);
176+
if (filePath) {
177+
parent.filename = filePath.split('?')[0];
178+
}
179+
}
180+
181+
// Strip query string
182+
const requestAndQuery = request.split('?');
183+
const searchParams = new URLSearchParams(requestAndQuery[1]);
184+
185+
// Inherit parent namespace if it exists
186+
if (parent?.filename) {
187+
const parentQuery = new URLSearchParams(parent.filename.split('?')[1]);
188+
const parentNamespace = parentQuery.get('namespace');
189+
if (parentNamespace) {
190+
searchParams.append('namespace', parentNamespace);
191+
}
192+
}
193+
194+
// If request namespace doesnt match the namespace, ignore
195+
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
196+
return resolve(request);
197+
}
198+
160199
if (namespace) {
161200
/**
162201
* When namespaced, the loaders are registered to the extensions in a hidden way
163202
* so Node's built-in resolver will not try those extensions
164203
*
165-
* To support implicit extensions, we need to wrap the resolver with our own
204+
* To support implicit extensions, we need to enhance the resolver with our own
166205
* re-implementation of the implicit extension resolution
167206
*/
168207
nextResolve = createImplicitResolver(nextResolve);
169208
}
170209

171-
return (
172-
request,
173-
parent,
174-
isMain,
175-
options,
176-
) => {
177-
const resolve: SimpleResolve = request_ => nextResolve(
178-
request_,
179-
parent,
180-
isMain,
181-
options,
182-
);
183-
184-
request = interopCjsExports(request);
185-
186-
if (parent?.filename) {
187-
const filePath = getOriginalFilePath(parent.filename);
188-
if (filePath) {
189-
parent.filename = filePath.split('?')[0];
190-
}
191-
}
192-
193-
// Strip query string
194-
const requestAndQuery = request.split('?');
195-
const searchParams = new URLSearchParams(requestAndQuery[1]);
210+
let resolved = resolveRequest(requestAndQuery[0], parent, resolve);
196211

197-
// Inherit parent namespace if it exists
198-
if (parent?.filename) {
199-
const parentQuery = new URLSearchParams(parent.filename.split('?')[1]);
200-
const parentNamespace = parentQuery.get('namespace');
201-
if (parentNamespace) {
202-
searchParams.append('namespace', parentNamespace);
203-
}
204-
}
205-
206-
// If request namespace doesnt match the namespace, ignore
207-
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
208-
return resolve(request);
209-
}
210-
211-
let resolved = resolveRequest(requestAndQuery[0], parent, resolve);
212-
213-
// Only add query back if it's a file path (not a core Node module)
214-
if (
215-
path.isAbsolute(resolved)
212+
// Only add query back if it's a file path (not a core Node module)
213+
if (
214+
path.isAbsolute(resolved)
216215

217216
// These two have native loaders which don't support queries
218217
&& !resolved.endsWith('.json')
219218
&& !resolved.endsWith('.node')
220-
) {
221-
resolved += urlSearchParamsStringify(searchParams);
222-
}
219+
) {
220+
resolved += urlSearchParamsStringify(searchParams);
221+
}
223222

224-
return resolved;
225-
};
223+
return resolved;
226224
};

‎src/cjs/api/resolve-implicit-extensions.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path';
22
import type { NodeError } from '../../types.js';
3+
import { isBarePackageNamePattern } from '../../utils/path-utils.js';
34
import type { ResolveFilename } from './types.js';
45

56
export const implicitlyResolvableExtensions = [
@@ -34,6 +35,7 @@ export const createImplicitResolver = (
3435
const nodeError = _error as NodeError;
3536
if (
3637
nodeError.code === 'MODULE_NOT_FOUND'
38+
&& !isBarePackageNamePattern.test(request)
3739
) {
3840
const resolved = (
3941
tryExtensions(resolve, request, ...args)

‎src/utils/path-utils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ export const isJsonPattern = /\.json($|\?)/;
5353
export const isDirectoryPattern = /\/(?:$|\?)/;
5454

5555
// Only matches packages names without subpaths (e.g. `foo` but not `foo/bar`)
56-
export const isBarePackageNamePattern = /^(?:@[^/]+\/)?[^/]+$/;
56+
// Back slash included to exclude Windows paths
57+
export const isBarePackageNamePattern = /^(?:@[^/]+\/)?[^/\\]+$/;

‎tests/specs/api.ts

+75-73
Original file line numberDiff line numberDiff line change
@@ -135,42 +135,84 @@ export default testSuite(({ describe }, node: NodeApis) => {
135135
expect(stdout).toBe('js working\nts working');
136136
});
137137

138-
test('register / unregister', async () => {
139-
await using fixture = await createFixture({
140-
'register.cjs': `
141-
const { register } = require(${JSON.stringify(tsxCjsApiPath)});
142-
try {
143-
require('./file');
144-
} catch {
145-
console.log('Fails as expected');
146-
}
147-
148-
const unregister = register();
149-
150-
const loaded = require('./file');
151-
console.log(loaded.message);
152-
153-
// Remove from cache
154-
const loadedPath = require.resolve('./file');
155-
delete require.cache[loadedPath];
156-
157-
unregister();
158-
159-
try {
160-
require('./file');
161-
} catch {
162-
console.log('Unregistered');
163-
}
164-
`,
165-
...tsFiles,
166-
});
138+
describe('register', ({ test }) => {
139+
test('register / unregister', async () => {
140+
await using fixture = await createFixture({
141+
'register.cjs': `
142+
const { register } = require(${JSON.stringify(tsxCjsApiPath)});
143+
try {
144+
require('./file');
145+
} catch {
146+
console.log('Fails as expected');
147+
}
148+
149+
const unregister = register();
150+
151+
const loaded = require('./file');
152+
console.log(loaded.message);
153+
154+
// Remove from cache
155+
const loadedPath = require.resolve('./file');
156+
delete require.cache[loadedPath];
157+
158+
unregister();
159+
160+
try {
161+
require('./file');
162+
} catch {
163+
console.log('Unregistered');
164+
}
165+
`,
166+
...tsFiles,
167+
});
167168

168-
const { stdout } = await execaNode(fixture.getPath('register.cjs'), [], {
169-
nodePath: node.path,
170-
nodeOptions: [],
169+
const { stdout } = await execaNode(fixture.getPath('register.cjs'), [], {
170+
nodePath: node.path,
171+
nodeOptions: [],
172+
});
173+
174+
expect(stdout).toBe('Fails as expected\nfoo bar json file.ts\nUnregistered');
171175
});
172176

173-
expect(stdout).toBe('Fails as expected\nfoo bar json file.ts\nUnregistered');
177+
test('namespace', async () => {
178+
await using fixture = await createFixture({
179+
'require.cjs': `
180+
const { expectErrors } = require('expect-errors');
181+
const path = require('node:path');
182+
const tsx = require(${JSON.stringify(tsxCjsApiPath)});
183+
184+
const api = tsx.register({ namespace: 'abcd' });
185+
186+
expectErrors(
187+
// Loading explicit/resolved file path should be ignored by loader (extensions)
188+
[() => require('./file.ts'), 'SyntaxError'],
189+
190+
// resolver should preserve full file path when ignoring
191+
[() => require('./file.ts?asdf'), "Cannot find module './file.ts?asdf'"]
192+
);
193+
194+
const { message, async } = api.require('./file', __filename);
195+
console.log(message);
196+
async.then(m => console.log(m.default));
197+
198+
api.require('./tsx?query=1', __filename);
199+
api.require('./jsx', __filename);
200+
api.require('./dir?query=3', __filename);
201+
`,
202+
...tsFiles,
203+
204+
'tsx.tsx': 'console.log(\'tsx\');',
205+
'jsx.jsx': 'console.log(\'jsx\');',
206+
'dir/index.jsx': 'console.log(\'dir\');',
207+
});
208+
209+
const { stdout } = await execaNode(fixture.getPath('require.cjs'), [], {
210+
nodePath: node.path,
211+
nodeOptions: [],
212+
});
213+
214+
expect(stdout).toBe('foo bar json file.ts\ntsx\njsx\ndir\nasync');
215+
});
174216
});
175217

176218
describe('tsx.require()', ({ test }) => {
@@ -265,46 +307,6 @@ export default testSuite(({ describe }, node: NodeApis) => {
265307

266308
expect(stdout).toBe('foo bar json file.ts\nfoo bar json file.ts\nfoo bar json file.ts\nUnregistered');
267309
});
268-
269-
test('namespace', async () => {
270-
await using fixture = await createFixture({
271-
'require.cjs': `
272-
const { expectErrors } = require('expect-errors');
273-
const path = require('node:path');
274-
const tsx = require(${JSON.stringify(tsxCjsApiPath)});
275-
276-
const api = tsx.register({ namespace: 'abcd' });
277-
278-
expectErrors(
279-
// Loading explicit/resolved file path should be ignored by loader (extensions)
280-
[() => require('./file.ts'), 'SyntaxError'],
281-
282-
// resolver should preserve full file path when ignoring
283-
[() => require('./file.ts?asdf'), "Cannot find module './file.ts?asdf'"]
284-
);
285-
286-
const { message, async } = api.require('./file', __filename);
287-
console.log(message);
288-
async.then(m => console.log(m.default));
289-
290-
api.require('./tsx?query=1', __filename);
291-
api.require('./jsx', __filename);
292-
api.require('./dir?query=3', __filename);
293-
`,
294-
...tsFiles,
295-
296-
'tsx.tsx': 'console.log(\'tsx\');',
297-
'jsx.jsx': 'console.log(\'jsx\');',
298-
'dir/index.jsx': 'console.log(\'dir\');',
299-
});
300-
301-
const { stdout } = await execaNode(fixture.getPath('require.cjs'), [], {
302-
nodePath: node.path,
303-
nodeOptions: [],
304-
});
305-
306-
expect(stdout).toBe('foo bar json file.ts\ntsx\njsx\ndir\nasync');
307-
});
308310
});
309311
});
310312

0 commit comments

Comments
 (0)
Please sign in to comment.