Skip to content

Commit

Permalink
feat(cjs): improve compatibility with other loaders
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Jun 29, 2024
1 parent f748e19 commit 3e1e546
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"node-pty": "^1.0.0",
"outdent": "^0.8.0",
"pkgroll": "^2.1.1",
"proxyquire": "^2.1.3",
"simple-git-hooks": "^2.11.1",
"split2": "^4.2.0",
"strip-ansi": "^7.1.0",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 25 additions & 21 deletions src/cjs/api/module-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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';
import type { LoaderState } from './types.js';

const typescriptExtensions = [
'.cts',
Expand All @@ -23,22 +24,6 @@ const transformExtensions = [
'.mjs',
] as const;

const cloneExtensions = <ObjectType extends object>(
extensions: ObjectType,
) => {
const cloneTo: ObjectType = Object.create(Object.getPrototypeOf(extensions));

// Preserves setters if they exist (e.g. nyc via append-transform)
const descriptors = Object.getOwnPropertyDescriptors(extensions);
for (const property in descriptors) {
if (Object.hasOwn(descriptors, property)) {
Object.defineProperty(cloneTo, property, descriptors[property]);
}
}

return cloneTo;
};

const safeSet = <T extends Record<string, unknown>>(
object: T,
property: keyof T,
Expand Down Expand Up @@ -82,18 +67,20 @@ const safeSet = <T extends Record<string, unknown>>(
};

export const createExtensions = (
extendExtensions: NodeJS.RequireExtensions,
state: LoaderState,
extensions: NodeJS.RequireExtensions,
namespace?: string,
) => {
// Clone Module._extensions with null prototype
const extensions = cloneExtensions(extendExtensions);

const defaultLoader = extensions['.js'];

const transformer = (
module: Module,
filePath: string,
) => {
if (state.enabled === false) {
return defaultLoader(module, filePath);
}

// Make sure __filename doesnt contain query
const [cleanFilePath, query] = filePath.split('?');

Expand Down Expand Up @@ -198,5 +185,22 @@ export const createExtensions = (
configurable: true,
});

return extensions;
// Unregister
return () => {
/**
* The extensions are only reverted if they're still tsx's transformers
*
* Otherwise, it means they have been wrapped by another loader and should
* be left untouched not to remove the other loader
*/
if (extensions['.js'] === transformer) {
extensions['.js'] = defaultLoader;
}

for (const extension of [...implicitlyResolvableExtensions, '.mjs']) {
if (extensions[extension] === transformer) {
delete extensions[extension];
}
}
};
};
7 changes: 6 additions & 1 deletion src/cjs/api/module-resolve-filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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';
import type { ResolveFilename, SimpleResolve } from './types.js';
import type { ResolveFilename, SimpleResolve, LoaderState } from './types.js';
import { createImplicitResolver } from './resolve-implicit-extensions.js';

const nodeModulesPath = `${path.sep}node_modules${path.sep}`;
Expand Down Expand Up @@ -154,6 +154,7 @@ const resolveRequest = (
};

export const createResolveFilename = (
state: LoaderState,
nextResolve: ResolveFilename,
namespace?: string,
): ResolveFilename => (
Expand All @@ -162,6 +163,10 @@ export const createResolveFilename = (
isMain,
options,
) => {
if (state.enabled === false) {
return nextResolve(request, parent, isMain, options);
}

const resolve: SimpleResolve = request_ => nextResolve(
request_,
parent,
Expand Down
27 changes: 18 additions & 9 deletions src/cjs/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import { loadTsconfig } from '../../utils/tsconfig.js';
import type { RequiredProperty } from '../../types.js';
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';
import type { LoaderState } from './types.js';
import { createExtensions } from './module-extensions.js';
import { createResolveFilename } from './module-resolve-filename.js';

Expand Down Expand Up @@ -62,27 +63,35 @@ export const register: Register = (
options,
) => {
const { sourceMapsEnabled } = process;
const { _extensions, _resolveFilename } = Module;
const state: LoaderState = {
enabled: true,
};

loadTsconfig(process.env.TSX_TSCONFIG_PATH);

// register
process.setSourceMapsEnabled(true);
const resolveFilename = createResolveFilename(_resolveFilename, options?.namespace);

const originalResolveFilename = Module._resolveFilename;
const resolveFilename = createResolveFilename(state, originalResolveFilename, options?.namespace);
Module._resolveFilename = resolveFilename;

const extensions = createExtensions(Module._extensions, options?.namespace);
// @ts-expect-error overwriting read-only property
Module._extensions = extensions;
const unregisterExtensions = createExtensions(state, Module._extensions, options?.namespace);

const unregister = () => {
if (sourceMapsEnabled === false) {
process.setSourceMapsEnabled(false);
}

// @ts-expect-error overwriting read-only property
Module._extensions = _extensions;
Module._resolveFilename = _resolveFilename;
state.enabled = false;

/**
* Only revert the _resolveFilename & extensions if they're unwrapped
* by another loader extension
*/
if (Module._resolveFilename === resolveFilename) {
Module._resolveFilename = originalResolveFilename;
}
unregisterExtensions();
};

if (options?.namespace) {
Expand Down
4 changes: 4 additions & 0 deletions src/cjs/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type Module from 'module';

export type LoaderState = {
enabled: boolean;
};

export type ResolveFilename = typeof Module._resolveFilename;

export type SimpleResolve = (request: string) => string;
34 changes: 33 additions & 1 deletion tests/specs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default testSuite(({ describe }, node: NodeApis) => {
return code;
}, '.ts');
`,
node_modules: ({ symlink }) => symlink(path.resolve('node_modules'), 'junction'),
'node_modules/append-transform': ({ symlink }) => symlink(path.resolve('node_modules/append-transform'), 'junction'),
});

const { stdout } = await execaNode('./index.js', {
Expand Down Expand Up @@ -311,6 +311,38 @@ export default testSuite(({ describe }, node: NodeApis) => {

expect(stdout).toBe('foo bar json file.ts\nfoo bar json file.ts\nfoo bar json file.ts\nUnregistered');
});

test('works with proxyquire (eslint tests)', async () => {
await using fixture = await createFixture({
'index.js': `
const proxyquire = require('proxyquire');
const tsx = require(${JSON.stringify(tsxCjsApiPath)});
tsx.register();
proxyquire('./test.js', {
path: {
sep: 'hello world',
},
});
`,

'test.js': `
const path = require('path');
console.log(path.sep);
`,

'node_modules/proxyquire': ({ symlink }) => symlink(path.resolve('node_modules/proxyquire'), 'junction'),
});

const { stdout } = await execaNode('./index.js', {
cwd: fixture.path,
nodePath: node.path,
nodeOptions: [],
});

expect(stdout).toBe('hello world');
});
});
});

Expand Down

0 comments on commit 3e1e546

Please sign in to comment.