Skip to content

Commit

Permalink
refactor(cjs): organize resolver logic
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Jun 9, 2024
1 parent 9e647a5 commit bbbf1a7
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 122 deletions.
9 changes: 3 additions & 6 deletions src/cjs/api/module-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isESM } from '../../utils/es-module-lexer.js';
import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js';
import { parent } from '../../utils/ipc/client.js';
import { fileMatcher } from '../../utils/tsconfig.js';
import { implicitlyResolvableExtensions } from './resolve-implicit-extensions.js';

const typescriptExtensions = [
'.cts',
Expand Down Expand Up @@ -106,11 +107,7 @@ export const createExtensions = (
*/
extensions['.js'] = transformer;

[
'.ts',
'.tsx',
'.jsx',
].forEach((extension) => {
for (const extension of implicitlyResolvableExtensions) {
const descriptor = Object.getOwnPropertyDescriptor(extensions, extension);
Object.defineProperty(extensions, extension, {
value: transformer,
Expand All @@ -124,7 +121,7 @@ export const createExtensions = (
*/
enumerable: descriptor?.enumerable || !namespace,
});
});
}

/**
* Loaders for extensions .cjs, .cts, & .mts don't need to be
Expand Down
173 changes: 75 additions & 98 deletions src/cjs/api/module-resolve-filename.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import path from 'node:path';
import Module from 'node:module';
import { fileURLToPath } from 'node:url';
import { resolveTsPath } from '../../utils/resolve-ts-path.js';
import { mapTsExtensions } from '../../utils/map-ts-extensions.js';
import type { NodeError } from '../../types.js';
import { isRelativePath, fileUrlPrefix, tsExtensionsPattern } from '../../utils/path-utils.js';
import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';

type ResolveFilename = typeof Module._resolveFilename;

type SimpleResolve = (request: string) => string;
import type { ResolveFilename, SimpleResolve } from './types.js';
import { createImplicitResolver } from './resolve-implicit-extensions.js';

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

Expand All @@ -26,18 +24,13 @@ export const interopCjsExports = (
}

const searchParams = new URLSearchParams(request.slice(queryIndex + 1));
let realPath = searchParams.get('filePath');
if (realPath) {
const namespace = searchParams.get('namespace');
if (namespace) {
realPath += `?namespace=${encodeURIComponent(namespace)}`;
}

const filePath = searchParams.get('filePath');
if (filePath) {
// The CJS module cache needs to be updated with the actual path for export parsing to work
// https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/esm/translators.js#L338
Module._cache[realPath] = Module._cache[request];
Module._cache[filePath] = Module._cache[request];
delete Module._cache[request];
request = realPath;
request = filePath;
}

return request;
Expand All @@ -58,7 +51,7 @@ const resolveTsFilename = (
return;
}

const tsPath = resolveTsPath(request);
const tsPath = mapTsExtensions(request);
if (!tsPath) {
return;
}
Expand All @@ -78,60 +71,11 @@ const resolveTsFilename = (
}
};

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

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

export const createResolveFilename = (
nextResolve: ResolveFilename,
namespace?: string,
): ResolveFilename => (
request,
parent,
isMain,
options,
) => {
const resolve: SimpleResolve = request_ => nextResolve(
request_,
parent,
isMain,
options,
);

request = interopCjsExports(request);

// Strip query string
const [cleanRequest, queryString] = request.split('?');
const searchParams = new URLSearchParams(queryString);

// Inherit parent namespace if it exists
if (parent?.filename) {
const parentQuery = new URLSearchParams(parent.filename.split('?')[1]);
const parentNamespace = parentQuery.get('namespace');
if (parentNamespace) {
searchParams.append('namespace', parentNamespace);
}
}

// If request namespace doesnt match the namespace, ignore
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
return resolve(request);
}

const query = urlSearchParamsStringify(searchParams);

// Temporarily remove query since default resolver can't handle it. Added back later.
request = cleanRequest;

// Support file protocol
if (request.startsWith(fileUrlPrefix)) {
request = fileURLToPath(request);
Expand All @@ -152,53 +96,38 @@ export const createResolveFilename = (
for (const possiblePath of possiblePaths) {
const tsFilename = resolveTsFilename(resolve, possiblePath, parent);
if (tsFilename) {
return tsFilename + query;
return tsFilename;
}

try {
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, `${request}${path.sep}index`)
);
if (resolved) {
return resolved + query;
}
}
return resolve(possiblePath);
} catch {}
}
}

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

try {
const resolved = resolve(request);

// Can be a node core module
return resolved + (path.isAbsolute(resolved) ? query : '');
return resolve(request);
} catch (error) {
const nodeError = error as NodeError;

// Exports map resolution
if (
nodeError.code === 'MODULE_NOT_FOUND'
&& typeof nodeError.path === 'string'
&& nodeError.path.endsWith('package.json')
&& nodeError.path.endsWith(`${path.sep}package.json`)
) {
const isExportsPath = nodeError.message.match(/^Cannot find module '([^']+)'$/);
if (isExportsPath) {
const exportsPath = isExportsPath[1];
const tsFilename = resolveTsFilename(resolve, exportsPath, parent);
if (tsFilename) {
return tsFilename + query;
return tsFilename;
}
}

Expand All @@ -207,20 +136,68 @@ export const createResolveFilename = (
const mainPath = isMainPath[1];
const tsFilename = resolveTsFilename(resolve, mainPath, parent);
if (tsFilename) {
return tsFilename + query;
return tsFilename;
}
}
}

const resolved = (
tryExtensions(resolve, request)
// Default resolve handles resovling paths relative to the parent
|| tryExtensions(resolve, `${request}${path.sep}index`)
);
if (resolved) {
return resolved + query;
throw nodeError;
}
};

export const createResolveFilename = (
nextResolve: ResolveFilename,
namespace?: string,
): ResolveFilename => (
request,
parent,
isMain,
options,
) => {
let resolve: SimpleResolve = request_ => nextResolve(
request_,
parent,
isMain,
options,
);

if (namespace) {
/**
* When namespaced, the loaders are registered to the extensions in a hidden way
* so Node's built-in resolver will not try those extensions
*
* To support implicit extensions, we need to wrap the resolver with our own
* re-implementation of the implicit extension resolution
*/
resolve = createImplicitResolver(resolve);
}

request = interopCjsExports(request);

// Strip query string
const requestAndQuery = request.split('?');
const searchParams = new URLSearchParams(requestAndQuery[1]);

// Inherit parent namespace if it exists
if (parent?.filename) {
const parentQuery = new URLSearchParams(parent.filename.split('?')[1]);
const parentNamespace = parentQuery.get('namespace');
if (parentNamespace) {
searchParams.append('namespace', parentNamespace);
}
}

throw nodeError;
// If request namespace doesnt match the namespace, ignore
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
return resolve(request);
}

let resolved = resolveRequest(requestAndQuery[0], parent, resolve);

// Only add query back if it's a file path (not a core Node module)
if (path.isAbsolute(resolved)) {
resolved += urlSearchParamsStringify(searchParams);
}

return resolved;
};
45 changes: 45 additions & 0 deletions src/cjs/api/resolve-implicit-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import path from 'node:path';
import type { NodeError } from '../../types.js';
import type { SimpleResolve } from './types.js';

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

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

export const createImplicitResolver = (
resolve: SimpleResolve,
): SimpleResolve => (request) => {
try {
return resolve(request);
} catch (_error) {
const nodeError = _error as NodeError;
if (
nodeError.code === 'MODULE_NOT_FOUND'
) {
const resolved = (
tryExtensions(resolve, request)

// Default resolve handles resovling paths relative to the parent
|| tryExtensions(resolve, `${request}${path.sep}index`)
);
if (resolved) {
return resolved;
}
}

throw nodeError;
}
};
5 changes: 5 additions & 0 deletions src/cjs/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type Module from 'module';

export type ResolveFilename = typeof Module._resolveFilename;

export type SimpleResolve = (request: string) => string;
7 changes: 2 additions & 5 deletions src/esm/hook/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,10 @@ export const load: LoadHook = async (
},
);

const parameters = new URLSearchParams({ filePath });
if (urlNamespace) {
parameters.set('namespace', urlNamespace);
}
const filePathWithNamespace = urlNamespace ? `${filePath}?namespace=${encodeURIComponent(urlNamespace)}` : filePath;

// TODO: re-exports from relative paths cant get detected because of the data URL
loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?${parameters.toString()}`;
loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?filePath=${encodeURIComponent(filePathWithNamespace)}`;
return loaded;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/esm/hook/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
} from 'node:module';
import type { PackageJson } from 'type-fest';
import { readJsonFile } from '../../utils/read-json-file.js';
import { resolveTsPath } from '../../utils/resolve-ts-path.js';
import { mapTsExtensions } from '../../utils/map-ts-extensions.js';
import type { NodeError } from '../../types.js';
import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';
import {
Expand Down Expand Up @@ -118,7 +118,7 @@ const tryTsPaths = async (
context: ResolveHookContext,
nextResolve: NextResolve,
) => {
const tsPaths = resolveTsPath(url);
const tsPaths = mapTsExtensions(url);
if (!tsPaths) {
return;
}
Expand Down
Loading

0 comments on commit bbbf1a7

Please sign in to comment.