Skip to content

Commit

Permalink
refactor(cjs): implement custom resolver (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Jun 7, 2024
1 parent de900a1 commit 4be7c7e
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 56 deletions.
62 changes: 29 additions & 33 deletions src/cjs/api/module-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,42 +87,38 @@ const transformer = (
module._compile(code, cleanFilePath);
};

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

/**
* Loaders for implicitly resolvable extensions
* https://github.com/nodejs/node/blob/v12.16.0/lib/internal/modules/cjs/loader.js#L1166
*/
[
'.ts',
'.tsx',
'.jsx',
].forEach((extension) => {
extensions[extension] = transformer;
});

/**
* Loaders for explicitly resolvable extensions
* (basically just .mjs because CJS loader has a special handler for it)
*
* Loaders for extensions .cjs, .cts, & .mts don't need to be
* registered because they're explicitly specified and unknown
* extensions (incl .cjs) fallsback to using the '.js' loader:
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
*
* That said, it's actually ".js" and ".mjs" that get special treatment
* rather than ".cjs" (it might as well be ".random-ext")
*/
Object.defineProperty(extensions, '.mjs', {
value: transformer,

// Prevent Object.keys from detecting these extensions
// when CJS loader iterates over the possible extensions
enumerable: false,
/**
* Loaders for extensions .cjs, .cts, & .mts don't need to be
* registered because they're explicitly specified. And unknown
* extensions (incl .cjs) fallsback to using the '.js' loader:
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
*
* That said, it's actually ".js" and ".mjs" that get special treatment
* rather than ".cjs" (it might as well be ".random-ext")
*/
'.mjs',
].forEach((extension) => {
Object.defineProperty(extensions, extension, {
value: transformer,

/**
* Prevent Object.keys from detecting these extensions
* when CJS loader iterates over the possible extensions
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609
*/
enumerable: false,
});
});
81 changes: 58 additions & 23 deletions src/cjs/api/module-resolve-filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';

type ResolveFilename = typeof Module._resolveFilename;

type SimpleResolve = (request: string) => string;

const nodeModulesPath = `${path.sep}node_modules${path.sep}`;

export const interopCjsExports = (
Expand Down Expand Up @@ -39,11 +41,9 @@ export const interopCjsExports = (
* Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions
*/
const resolveTsFilename = (
nextResolve: ResolveFilename,
resolve: SimpleResolve,
request: string,
parent: Module.Parent,
isMain: boolean,
options?: Record<PropertyKey, unknown>,
) => {
if (
!(parent?.filename && tsExtensionsPattern.test(parent.filename))
Expand All @@ -59,12 +59,7 @@ const resolveTsFilename = (

for (const tryTsPath of tsPath) {
try {
return nextResolve(
tryTsPath,
parent,
isMain,
options,
);
return resolve(tryTsPath);
} catch (error) {
const { code } = error as NodeError;
if (
Expand All @@ -77,6 +72,19 @@ const resolveTsFilename = (
}
};

const extensions = ['.ts', '.tsx', '.jsx'] as const;

const tryExtensions = (
resolve: SimpleResolve,
request: string,
) => {
for (const extension of extensions) {
try {
return resolve(request + extension);
} catch {}
}
};

export const createResolveFilename = (
nextResolve: ResolveFilename,
): ResolveFilename => (

Check warning on line 90 in src/cjs/api/module-resolve-filename.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Arrow function has a complexity of 16. Maximum allowed is 10

Check warning on line 90 in src/cjs/api/module-resolve-filename.ts

View workflow job for this annotation

GitHub Actions / Release

Arrow function has a complexity of 16. Maximum allowed is 10
Expand All @@ -99,39 +107,66 @@ export const createResolveFilename = (
request = fileURLToPath(request);
}

const resolve: SimpleResolve = request_ => nextResolve(
request_,
parent,
isMain,
options,
);

// Resolve TS path alias
if (
tsconfigPathsMatcher

// bare specifier
&& !isRelativePath(request)
// bare specifier
&& !isRelativePath(request)

// Dependency paths should not be resolved using tsconfig.json
&& !parent?.filename?.includes(nodeModulesPath)
// Dependency paths should not be resolved using tsconfig.json
&& !parent?.filename?.includes(nodeModulesPath)
) {
const possiblePaths = tsconfigPathsMatcher(request);

for (const possiblePath of possiblePaths) {
const tsFilename = resolveTsFilename(nextResolve, possiblePath, parent, isMain, options);
const tsFilename = resolveTsFilename(resolve, possiblePath, parent);
if (tsFilename) {
return tsFilename + query;
}

try {
return nextResolve(
possiblePath,
parent,
isMain,
options,
) + query;
} catch {}
return resolve(possiblePath) + query;
} catch {
/**
* Try order:
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L410-L413
*/
const resolved = (
tryExtensions(resolve, possiblePath)
|| tryExtensions(resolve, path.resolve(possiblePath, 'index'))
);
if (resolved) {
return resolved + query;
}
}
}
}

const tsFilename = resolveTsFilename(nextResolve, request, parent, isMain, options);
// If extension exists
const tsFilename = resolveTsFilename(resolve, request, parent);
if (tsFilename) {
return tsFilename + query;
}

return nextResolve(request, parent, isMain, options) + query;
try {
return resolve(request) + query;
} catch (error) {
const resolved = (
tryExtensions(resolve, request)
|| tryExtensions(resolve, path.resolve(request, 'index'))
);
if (resolved) {
return resolved + query;
}

throw error;
}
};

0 comments on commit 4be7c7e

Please sign in to comment.