diff --git a/docs/node.md b/docs/node.md index c23b3c837..2c0010cbd 100644 --- a/docs/node.md +++ b/docs/node.md @@ -43,9 +43,81 @@ node --import tsx/esm ./file.ts node --loader tsx/esm ./file.ts ``` -### CommonJS only -If you only need to add TypeScript & ESM support in a CommonJS context, you can use the CJS loader: +--- + +# CommonJS + +If you only need to add TypeScript & ESM support in a CommonJS context. + +## Command-line API + +Pass tsx into the `--require` flag: ```sh node --require tsx/cjs ./file.ts ``` + +## Node.js API + +### Globally patching `require` + +#### Enabling TSX Enhancement + +To enhance the global `require()` function with TypeScript support, prepend your script with the following line: + +```js +require('tsx/cjs') +``` + +#### Manual Registration & Unregistration + +To manually register and unregister the TypeScript enhancement on the global `require()`: + +```js +const tsx = require('tsx/cjs/api') + +// Register tsx enhancement for all global require() calls +const unregister = tsx.register() + +// Optionally, unregister the enhancement when needed +unregister() +``` + +### Isolated `require()` + +In situations where TypeScript support is needed only for loading a specific file (e.g. within third-party packages) without affecting the global environment, you can utilize tsx's custom `require` function. + +Note the current file path must be passed in as the second argument so it knows how to resolve relative paths. + +#### CommonJS usage +```js +const tsx = require('tsx/cjs/api') + +const loaded = tsx.require('./file.ts', __filename) +const filepath = tsx.require.resolve('./file.ts', __filename) +``` + +#### ESM usage +```js +import { require } from 'tsx/cjs/api' + +const loaded = require('./file.ts', import.meta.url) +const filepath = require.resolve('./file.ts', import.meta.url) +``` + +#### Module graph + +If you're interested in seeing what files were loaded, you can traverse the CommonJS module graph. This can be useful for watchers: + +```js +// To detect watch files, we can parse the CommonJS module graph +const resolvedPath = require.resolve('./file', import.meta.url) + +const collectDependencies = module => [ + module.filename, + ...module.children.flatMap(collectDependencies) +] + +console.log(collectDependencies(require.cache[resolvedPath])) +// ['/file.ts', '/foo.ts', '/bar.ts'] +``` diff --git a/package.json b/package.json index bb4f2a9e5..98349fd5b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ "./package.json": "./package.json", ".": "./dist/loader.mjs", "./cjs": "./dist/cjs/index.cjs", + "./cjs/api": { + "types": "./dist/cjs/api/index.d.ts", + "default": "./dist/cjs/api/index.cjs" + }, "./esm": "./dist/esm/index.mjs", "./cli": "./dist/cli.mjs", "./source-map": "./dist/source-map.cjs", diff --git a/src/cjs/api/index.ts b/src/cjs/api/index.ts index f3a3d1e56..49a434c36 100644 --- a/src/cjs/api/index.ts +++ b/src/cjs/api/index.ts @@ -1 +1,2 @@ export { register } from './global-require-patch.js'; +export { require } from './require.js'; diff --git a/src/cjs/api/require.ts b/src/cjs/api/require.ts new file mode 100644 index 000000000..3e0b73a3b --- /dev/null +++ b/src/cjs/api/require.ts @@ -0,0 +1,48 @@ +import path from 'path'; +import { fileURLToPath } from 'node:url'; +import { register } from './global-require-patch.js'; +import { resolveFilename } from './module-resolve-filename.js'; + +const getRequestContext = ( + filepath: string | URL, +) => { + if ( + (typeof filepath === 'string' && filepath.startsWith('file://')) + || filepath instanceof URL + ) { + filepath = fileURLToPath(filepath); + } + return path.dirname(filepath); +}; + +const tsxRequire = ( + id: string, + fromFile: string | URL, +) => { + const unregister = register(); + try { + const contextId = path.resolve(getRequestContext(fromFile), id); + + // eslint-disable-next-line import-x/no-dynamic-require, n/global-require + return require(contextId); + } finally { + unregister(); + } +}; + +const resolve = ( + id: string, + fromFile: string | URL, + options?: { paths?: string[] | undefined }, +) => { + const contextId = path.resolve(getRequestContext(fromFile), id); + return resolveFilename(contextId, module, false, options); +}; +resolve.paths = require.resolve.paths; + +tsxRequire.resolve = resolve; +tsxRequire.main = require.main; +tsxRequire.extensions = require.extensions; +tsxRequire.cache = require.cache; + +export { tsxRequire as require };