Skip to content

Commit

Permalink
feat(cjs/api): register() to support namespace (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber authored Jun 7, 2024
1 parent 4be7c7e commit c703300
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 160 deletions.
2 changes: 1 addition & 1 deletion docs/node/tsx-require.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Use this function for importing TypeScript files in CommonJS mode without adding
Note, the current file path must be passed in as the second argument to resolve the import context.

::: warning Caveats
- `import()` & asynchronous `require()` calls in the loaded files are not enhanced.
- `import()` calls in the loaded files are not enhanced.
- Because it compiles ESM syntax to run in CommonJS mode, top-level await is not supported
:::

Expand Down
176 changes: 95 additions & 81 deletions src/cjs/api/module-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,103 +22,117 @@ const transformExtensions = [
'.mjs',
] as const;

// Clone Module._extensions with null prototype
export const extensions: NodeJS.RequireExtensions = Object.assign(
Object.create(null),
Module._extensions,
);

const defaultLoader = extensions['.js'];

const transformer = (
module: Module,
filePath: string,
export const createExtensions = (
extendExtensions: NodeJS.RequireExtensions,
namespace?: string,
) => {
// Make sure __filename doesnt contain query
const cleanFilePath = filePath.split('?')[0];

// For tracking dependencies in watch mode
if (parent?.send) {
parent.send({
type: 'dependency',
path: cleanFilePath,
});
}
// Clone Module._extensions with null prototype
const extensions: NodeJS.RequireExtensions = Object.assign(
Object.create(null),
extendExtensions,
);

const defaultLoader = extensions['.js'];

const transformer = (
module: Module,
filePath: string,
) => {
// Make sure __filename doesnt contain query
const [cleanFilePath, query] = filePath.split('?');

const searchParams = new URLSearchParams(query);

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

const transformTs = typescriptExtensions.some(extension => cleanFilePath.endsWith(extension));
const transformJs = transformExtensions.some(extension => cleanFilePath.endsWith(extension));
if (!transformTs && !transformJs) {
return defaultLoader(module, cleanFilePath);
}
// For tracking dependencies in watch mode
if (parent?.send) {
parent.send({
type: 'dependency',
path: cleanFilePath,
});
}

let code = fs.readFileSync(cleanFilePath, 'utf8');
const transformTs = typescriptExtensions.some(extension => cleanFilePath.endsWith(extension));
const transformJs = transformExtensions.some(extension => cleanFilePath.endsWith(extension));
if (!transformTs && !transformJs) {
return defaultLoader(module, cleanFilePath);
}

let code = fs.readFileSync(cleanFilePath, 'utf8');

if (cleanFilePath.endsWith('.cjs')) {
// Contains native ESM check
const transformed = transformDynamicImport(filePath, code);
if (transformed) {
code = (
shouldApplySourceMap()
? inlineSourceMap(transformed)
: transformed.code
);
}
} else if (
transformTs

// CommonJS file but uses ESM import/export
|| isESM(code)
) {
const transformed = transformSync(
code,
filePath,
{
tsconfigRaw: fileMatcher?.(cleanFilePath) as TransformOptions['tsconfigRaw'],
},
);

if (cleanFilePath.endsWith('.cjs')) {
// Contains native ESM check
const transformed = transformDynamicImport(filePath, code);
if (transformed) {
code = (
shouldApplySourceMap()
? inlineSourceMap(transformed)
: transformed.code
);
}
} else if (
transformTs

// CommonJS file but uses ESM import/export
|| isESM(code)
) {
const transformed = transformSync(
code,
filePath,
{
tsconfigRaw: fileMatcher?.(cleanFilePath) as TransformOptions['tsconfigRaw'],
},
);

code = (
shouldApplySourceMap()
? inlineSourceMap(transformed)
: transformed.code
);
}

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
*/
extensions['.js'] = transformer;

[
'.ts',
'.tsx',
'.jsx',
module._compile(code, cleanFilePath);
};

/**
* 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
* Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders
*
* That said, it's actually ".js" and ".mjs" that get special treatment
* rather than ".cjs" (it might as well be ".random-ext")
* 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
*/
'.mjs',
].forEach((extension) => {
Object.defineProperty(extensions, extension, {
value: transformer,
extensions['.js'] = transformer;

[
'.ts',
'.tsx',
'.jsx',

/**
* 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
* 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")
*/
enumerable: false,
'.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,
});
});
});

return extensions;
};
45 changes: 33 additions & 12 deletions src/cjs/api/module-resolve-filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolveTsPath } from '../../utils/resolve-ts-path.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;

Expand Down Expand Up @@ -87,33 +88,50 @@ const tryExtensions = (

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 queryIndex = request.indexOf('?');
const query = queryIndex === -1 ? '' : request.slice(queryIndex);
if (queryIndex !== -1) {
request = request.slice(0, queryIndex);
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);
}

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

// Resolve TS path alias
if (
tsconfigPathsMatcher
Expand Down Expand Up @@ -157,7 +175,10 @@ export const createResolveFilename = (
}

try {
return resolve(request) + query;
const resolved = resolve(request);

// Can be a node core module
return resolved + (path.isAbsolute(resolved) ? query : '');
} catch (error) {
const resolved = (
tryExtensions(resolve, request)
Expand Down
Loading

0 comments on commit c703300

Please sign in to comment.