diff --git a/docs/node.md b/docs/node.md index 4821ee15b..9ed4cc777 100644 --- a/docs/node.md +++ b/docs/node.md @@ -54,20 +54,27 @@ node --import tsx/esm ./file.ts node --loader tsx/esm ./file.ts ``` -### Hooks API -> Previously known as _Loaders_ ([renamed in Node.js v21](https://github.com/nodejs/loaders/issues/95)) +### Registration & Unregistration +```js +import { register } from 'tsx/esm/api' -You can use the [Hooks API](https://nodejs.org/api/module.html#customization-hooks) to load TypeScript files with `tsx/esm`: +// register tsx enhancement +const unregister = register() -```js -import { register } from 'node:module' +// Unregister when needed +unregister() +``` -register('tsx/esm', { - parentURL: import.meta.url, - data: true -}) +#### Tracking loaded files +Detect files that get loaded with the `onImport` hook: -const loaded = await import('./hello.ts') +```ts +register({ + onImport: (file: string) => { + console.log(file) + // file:///foo.ts + } +}) ``` ## Only CommonJS enhancement @@ -82,21 +89,15 @@ Pass _tsx_ into the `--require` flag: node --require tsx/cjs ./file.ts ``` -### Node.js API - -#### Globally patching `require` - -##### Enabling TSX Enhancement - -Add the following line at the top of your entry file: +This is the equivalent of adding the following at the top of your entry file, which you can also do: ```js require('tsx/cjs') ``` -##### Manual Registration & Unregistration +### Registration & Unregistration -To manually register and unregister the TypeScript enhancement: +To manually register and unregister the tsx enhancement: ```js const tsx = require('tsx/cjs/api') @@ -108,13 +109,60 @@ const unregister = tsx.register() unregister() ``` -## `tsx.require()` +## Enhanced `import()` & `require()` + +tsx exports enhanced `import()` or `require()` functions, allowing you to load TypeScript/ESM files without affecting the runtime environment. + +### `tsImport()` + +The `import()` function enhanced to support TypeScript. Because it's the native `import()`, it supports [top-level await](https://v8.dev/features/top-level-await). + +::: warning Caveat +`require()` calls in the loaded files are not enhanced. +::: + +#### ESM usage + +Note, the current file path must be passed in as the second argument to resolve the import context. + +```js +import { tsImport } from 'tsx/esm/api' + +const loaded = await tsImport('./file.ts', import.meta.url) +``` + +#### CommonJS usage + +```js +const { tsImport } = require('tsx/esm/api') + +const loaded = await tsImport('./file.ts', __filename) +``` + +#### Tracking loaded files +Detect files that get loaded with the `onImport` hook: -For loading a TypeScript file without affecting the environment, `tsx` exports a custom `require(id, loadFromPath)` function. +```ts +tsImport('./file.ts', { + parentURL: import.meta.url, + onImport: (file: string) => { + console.log(file) + // file:///foo.ts + } +}) +``` + +### `tsx.require()` + +The `require()` function enhanced to support TypeScript and ESM. + +::: warning Caveat +`import()` & asynchronous `require()` calls in the loaded files are not enhanced. +::: -Note, the current file path must be passed in as the second argument so it knows how to resolve relative paths. +#### CommonJS usage -### CommonJS usage +Note, the current file path must be passed in as the second argument to resolve the import context. ```js const tsx = require('tsx/cjs/api') @@ -123,7 +171,7 @@ const loaded = tsx.require('./file.ts', __filename) const filepath = tsx.require.resolve('./file.ts', __filename) ``` -### ESM usage +#### ESM usage ```js import { require } from 'tsx/cjs/api' @@ -132,7 +180,7 @@ const loaded = require('./file.ts', import.meta.url) const filepath = require.resolve('./file.ts', import.meta.url) ``` -### Module graph inspection +#### Tracking loaded files Because the CommonJS API tracks loaded modules in `require.cache`, you can use it to identify loaded files for dependency tracking. This can be useful when implementing a watcher. diff --git a/package.json b/package.json index 5c47a9773..85253405b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,16 @@ "default": "./dist/cjs/api/index.cjs" }, "./esm": "./dist/esm/index.mjs", + "./esm/api": { + "import": { + "types": "./dist/esm/api/index.d.mts", + "default": "./dist/esm/api/index.mjs" + }, + "require": { + "types": "./dist/esm/api/index.d.cts", + "default": "./dist/esm/api/index.cjs" + } + }, "./cli": "./dist/cli.mjs", "./suppress-warnings": "./dist/suppress-warnings.cjs", "./preflight": "./dist/preflight.cjs", @@ -38,7 +48,7 @@ }, "scripts": { "prepare": "pnpm simple-git-hooks", - "build": "pkgroll --target=node12.19 --minify", + "build": "pkgroll --minify", "lint": "lintroll --node --cache .", "type-check": "tsc --noEmit", "test": "pnpm build && node ./dist/cli.mjs tests/index.ts", @@ -80,7 +90,7 @@ "get-node": "^15.0.0", "kolorist": "^1.8.0", "lint-staged": "^15.2.2", - "lintroll": "^1.5.0", + "lintroll": "^1.5.1", "magic-string": "^0.30.10", "manten": "^1.3.0", "memfs": "^4.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fffbeecec..e9c7db74f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,8 +68,8 @@ importers: specifier: ^15.2.2 version: 15.2.2 lintroll: - specifier: ^1.5.0 - version: 1.5.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(typescript@5.4.5) + specifier: ^1.5.1 + version: 1.5.1(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(typescript@5.4.5) magic-string: specifier: ^0.30.10 version: 0.30.10 @@ -117,7 +117,7 @@ importers: version: 3.4.3 vitepress: specifier: ^1.1.0 - version: 1.1.4(@algolia/client-search@4.23.3)(@types/node@20.12.7)(@types/react@18.3.1)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5) + version: 1.1.4(@algolia/client-search@4.23.3)(@types/node@20.12.8)(@types/react@18.3.1)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5) packages: @@ -246,276 +246,138 @@ packages: search-insights: optional: true - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} @@ -865,31 +727,31 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} - '@stylistic/eslint-plugin-js@1.7.2': - resolution: {integrity: sha512-ZYX7C5p7zlHbACwFLU+lISVh6tdcRP/++PWegh2Sy0UgMT5kU0XkPa2tKWEtJYzZmPhJxu9LxbnWcnE/tTwSDQ==} + '@stylistic/eslint-plugin-js@1.8.0': + resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' - '@stylistic/eslint-plugin-jsx@1.7.2': - resolution: {integrity: sha512-lNZR5PR0HLJPs+kY0y8fy6KroKlYqA5PwsYWpVYWzqZWiL5jgAeUo4s9yLFYjJjzildJ5MsTVMy/xP81Qz6GXg==} + '@stylistic/eslint-plugin-jsx@1.8.0': + resolution: {integrity: sha512-PC7tYXipF03TTilGJva1amAham7qOAFXT5r5jLTY6iIxkFqyb6H7Ljx5pv8d7n98VyIVidOEKY/AP8vNzAFNKg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' - '@stylistic/eslint-plugin-plus@1.7.2': - resolution: {integrity: sha512-luUfRVbBVtt0+/FNt8/76BANJEzb/nHWasHD7UUjyMrch2U9xUKpObrkTCzqBuisKek+uFupwGjqXqDP07+fQw==} + '@stylistic/eslint-plugin-plus@1.8.0': + resolution: {integrity: sha512-TkrjzzYmTuAaLvFwtxomsgMUD8g8PREOQOQzTfKmiJ6oc4XOyFW4q/L9ES1J3UFSLybNCwbhu36lhXJut1w2Sg==} peerDependencies: eslint: '*' - '@stylistic/eslint-plugin-ts@1.7.2': - resolution: {integrity: sha512-szX89YPocwCe4T0eT3alj7MwEzDHt5+B+kb/vQfSSLIjI9CGgoWrgj50zU8PtaDctTh4ZieFBzU/lRmkSUo0RQ==} + '@stylistic/eslint-plugin-ts@1.8.0': + resolution: {integrity: sha512-WuCIhz4JEHxzhAWjrBASMGj6Or1wAjDqTsRIck3DRRrw/FJ8C/8AAuHPk8ECHNSDI5PZ0OT72nF2uSUn0aQq1w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' - '@stylistic/eslint-plugin@1.7.2': - resolution: {integrity: sha512-TesaPR4AOCeD4unwu9gZCdTe8SsUpykriICuwXV8GFBgESuVbfVp+S8g6xTWe9ntVR803bNMtnr2UhxHW0iFqg==} + '@stylistic/eslint-plugin@1.8.0': + resolution: {integrity: sha512-JRR0lCDU97AiE0X6qTc/uf8Hv0yETUdyJgoNzTLUIWdhVJVe/KGPnFmEsO1iXfNUIS6vhv3JJ5vaZ2qtXhZe1g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' @@ -940,6 +802,9 @@ packages: '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@20.12.8': + resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1579,11 +1444,6 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -2335,8 +2195,8 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - lintroll@1.5.0: - resolution: {integrity: sha512-f/sxgma8hkz6LFrGWHTCGOrEldDfB48qLH10mPtNDdnw/dZjPlezZ3pORF1o/nkekB4NcDx78NLIsVxC/r2Ycw==} + lintroll@1.5.1: + resolution: {integrity: sha512-UvsmRRKIJBGpbomo/GHhmhfYH29uoJGCoDQLagNJuqoH38ZjLfpwi56j1PRQrpauwyOeNrW8LQHihvXztTvnkg==} hasBin: true listr2@8.0.1: @@ -3180,8 +3040,8 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tsx@4.7.3: - resolution: {integrity: sha512-+fQnMqIp/jxZEXLcj6WzYy9FhcS5/Dfk8y4AtzJ6ejKcKqmfTF8Gso/jtrzDggCF2zTU20gJa6n8XqPYwDAUYQ==} + tsx@4.8.2: + resolution: {integrity: sha512-hmmzS4U4mdy1Cnzpl/NQiPUC2k34EcNSTZYVJThYKhdqTwuBeF+4cG9KUK/PFQ7KHaAaYwqlb7QfmsE2nuj+WA==} engines: {node: '>=18.0.0'} hasBin: true @@ -3560,141 +3420,72 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.20.2': optional: true - '@esbuild/android-arm64@0.19.12': - optional: true - '@esbuild/android-arm64@0.20.2': optional: true - '@esbuild/android-arm@0.19.12': - optional: true - '@esbuild/android-arm@0.20.2': optional: true - '@esbuild/android-x64@0.19.12': - optional: true - '@esbuild/android-x64@0.20.2': optional: true - '@esbuild/darwin-arm64@0.19.12': - optional: true - '@esbuild/darwin-arm64@0.20.2': optional: true - '@esbuild/darwin-x64@0.19.12': - optional: true - '@esbuild/darwin-x64@0.20.2': optional: true - '@esbuild/freebsd-arm64@0.19.12': - optional: true - '@esbuild/freebsd-arm64@0.20.2': optional: true - '@esbuild/freebsd-x64@0.19.12': - optional: true - '@esbuild/freebsd-x64@0.20.2': optional: true - '@esbuild/linux-arm64@0.19.12': - optional: true - '@esbuild/linux-arm64@0.20.2': optional: true - '@esbuild/linux-arm@0.19.12': - optional: true - '@esbuild/linux-arm@0.20.2': optional: true - '@esbuild/linux-ia32@0.19.12': - optional: true - '@esbuild/linux-ia32@0.20.2': optional: true - '@esbuild/linux-loong64@0.19.12': - optional: true - '@esbuild/linux-loong64@0.20.2': optional: true - '@esbuild/linux-mips64el@0.19.12': - optional: true - '@esbuild/linux-mips64el@0.20.2': optional: true - '@esbuild/linux-ppc64@0.19.12': - optional: true - '@esbuild/linux-ppc64@0.20.2': optional: true - '@esbuild/linux-riscv64@0.19.12': - optional: true - '@esbuild/linux-riscv64@0.20.2': optional: true - '@esbuild/linux-s390x@0.19.12': - optional: true - '@esbuild/linux-s390x@0.20.2': optional: true - '@esbuild/linux-x64@0.19.12': - optional: true - '@esbuild/linux-x64@0.20.2': optional: true - '@esbuild/netbsd-x64@0.19.12': - optional: true - '@esbuild/netbsd-x64@0.20.2': optional: true - '@esbuild/openbsd-x64@0.19.12': - optional: true - '@esbuild/openbsd-x64@0.20.2': optional: true - '@esbuild/sunos-x64@0.19.12': - optional: true - '@esbuild/sunos-x64@0.20.2': optional: true - '@esbuild/win32-arm64@0.19.12': - optional: true - '@esbuild/win32-arm64@0.20.2': optional: true - '@esbuild/win32-ia32@0.19.12': - optional: true - '@esbuild/win32-ia32@0.20.2': optional: true - '@esbuild/win32-x64@0.19.12': - optional: true - '@esbuild/win32-x64@0.20.2': optional: true @@ -3983,7 +3774,7 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@stylistic/eslint-plugin-js@1.7.2(eslint@8.57.0)': + '@stylistic/eslint-plugin-js@1.8.0(eslint@8.57.0)': dependencies: '@types/eslint': 8.56.10 acorn: 8.11.3 @@ -3992,15 +3783,15 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 - '@stylistic/eslint-plugin-jsx@1.7.2(eslint@8.57.0)': + '@stylistic/eslint-plugin-jsx@1.8.0(eslint@8.57.0)': dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) '@types/eslint': 8.56.10 eslint: 8.57.0 estraverse: 5.3.0 picomatch: 4.0.2 - '@stylistic/eslint-plugin-plus@1.7.2(eslint@8.57.0)(typescript@5.4.5)': + '@stylistic/eslint-plugin-plus@1.8.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@types/eslint': 8.56.10 '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) @@ -4009,9 +3800,9 @@ snapshots: - supports-color - typescript - '@stylistic/eslint-plugin-ts@1.7.2(eslint@8.57.0)(typescript@5.4.5)': + '@stylistic/eslint-plugin-ts@1.8.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) '@types/eslint': 8.56.10 '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 @@ -4019,12 +3810,12 @@ snapshots: - supports-color - typescript - '@stylistic/eslint-plugin@1.7.2(eslint@8.57.0)(typescript@5.4.5)': + '@stylistic/eslint-plugin@1.8.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) - '@stylistic/eslint-plugin-jsx': 1.7.2(eslint@8.57.0) - '@stylistic/eslint-plugin-plus': 1.7.2(eslint@8.57.0)(typescript@5.4.5) - '@stylistic/eslint-plugin-ts': 1.7.2(eslint@8.57.0)(typescript@5.4.5) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) + '@stylistic/eslint-plugin-jsx': 1.8.0(eslint@8.57.0) + '@stylistic/eslint-plugin-plus': 1.8.0(eslint@8.57.0)(typescript@5.4.5) + '@stylistic/eslint-plugin-ts': 1.8.0(eslint@8.57.0)(typescript@5.4.5) '@types/eslint': 8.56.10 eslint: 8.57.0 transitivePeerDependencies: @@ -4079,6 +3870,11 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@20.12.8': + dependencies: + undici-types: 5.26.5 + optional: true + '@types/normalize-package-data@2.4.4': {} '@types/prop-types@15.7.12': @@ -4177,7 +3973,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.5.4 + semver: 7.6.0 ts-api-utils: 1.3.0(typescript@5.4.5) optionalDependencies: typescript: 5.4.5 @@ -4208,7 +4004,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) eslint: 8.57.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -4239,9 +4035,9 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@5.0.4(vite@5.2.10(@types/node@20.12.7))(vue@3.4.26(typescript@5.4.5))': + '@vitejs/plugin-vue@5.0.4(vite@5.2.10(@types/node@20.12.8))(vue@3.4.26(typescript@5.4.5))': dependencies: - vite: 5.2.10(@types/node@20.12.7) + vite: 5.2.10(@types/node@20.12.8) vue: 3.4.26(typescript@5.4.5) '@vue/compiler-core@3.4.26': @@ -4853,32 +4649,6 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -4918,7 +4688,7 @@ snapshots: eslint-compat-utils@0.5.0(eslint@8.57.0): dependencies: eslint: 8.57.0 - semver: 7.5.4 + semver: 7.6.0 eslint-import-resolver-node@0.3.9: dependencies: @@ -5042,7 +4812,7 @@ snapshots: globals: 15.1.0 ignore: 5.3.1 minimatch: 9.0.4 - semver: 7.5.4 + semver: 7.6.0 eslint-plugin-no-use-extend-native@0.5.0: dependencies: @@ -5109,7 +4879,7 @@ snapshots: read-pkg-up: 7.0.1 regexp-tree: 0.1.27 regjsparser: 0.10.0 - semver: 7.5.4 + semver: 7.6.0 strip-indent: 3.0.0 transitivePeerDependencies: - supports-color @@ -5766,7 +5536,7 @@ snapshots: acorn: 8.11.3 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - semver: 7.5.4 + semver: 7.6.0 jsx-ast-utils@3.3.5: dependencies: @@ -5820,11 +5590,11 @@ snapshots: transitivePeerDependencies: - supports-color - lintroll@1.5.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(typescript@5.4.5): + lintroll@1.5.1(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(typescript@5.4.5): dependencies: '@eslint-community/eslint-plugin-eslint-comments': 4.3.0(eslint@8.57.0) '@eslint/js': 8.57.0 - '@stylistic/eslint-plugin': 1.7.2(eslint@8.57.0)(typescript@5.4.5) + '@stylistic/eslint-plugin': 1.8.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) cleye: 1.3.2 @@ -5848,7 +5618,7 @@ snapshots: get-tsconfig: 4.7.3 globals: 15.1.0 resolve-pkg-maps: 1.0.0 - tsx: 4.7.3 + tsx: 4.8.2 vue-eslint-parser: 9.4.2(eslint@8.57.0) yaml-eslint-parser: 1.2.2 transitivePeerDependencies: @@ -6770,9 +6540,9 @@ snapshots: tslib@2.6.2: {} - tsx@4.7.3: + tsx@4.8.2: dependencies: - esbuild: 0.19.12 + esbuild: 0.20.2 get-tsconfig: 4.7.3 optionalDependencies: fsevents: 2.3.3 @@ -6855,23 +6625,23 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite@5.2.10(@types/node@20.12.7): + vite@5.2.10(@types/node@20.12.8): dependencies: esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.17.1 optionalDependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.8 fsevents: 2.3.3 - vitepress@1.1.4(@algolia/client-search@4.23.3)(@types/node@20.12.7)(@types/react@18.3.1)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5): + vitepress@1.1.4(@algolia/client-search@4.23.3)(@types/node@20.12.8)(@types/react@18.3.1)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5): dependencies: '@docsearch/css': 3.6.0 '@docsearch/js': 3.6.0(@algolia/client-search@4.23.3)(@types/react@18.3.1)(search-insights@2.13.0) '@shikijs/core': 1.3.0 '@shikijs/transformers': 1.3.0 '@types/markdown-it': 14.0.1 - '@vitejs/plugin-vue': 5.0.4(vite@5.2.10(@types/node@20.12.7))(vue@3.4.26(typescript@5.4.5)) + '@vitejs/plugin-vue': 5.0.4(vite@5.2.10(@types/node@20.12.8))(vue@3.4.26(typescript@5.4.5)) '@vue/devtools-api': 7.1.3(vue@3.4.26(typescript@5.4.5)) '@vueuse/core': 10.9.0(vue@3.4.26(typescript@5.4.5)) '@vueuse/integrations': 10.9.0(focus-trap@7.5.4)(vue@3.4.26(typescript@5.4.5)) @@ -6879,7 +6649,7 @@ snapshots: mark.js: 8.11.1 minisearch: 6.3.0 shiki: 1.3.0 - vite: 5.2.10(@types/node@20.12.7) + vite: 5.2.10(@types/node@20.12.8) vue: 3.4.26(typescript@5.4.5) optionalDependencies: postcss: 8.4.38 @@ -6923,7 +6693,7 @@ snapshots: espree: 9.6.1 esquery: 1.5.0 lodash: 4.17.21 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color diff --git a/src/esm/api/index.ts b/src/esm/api/index.ts index 01bc72c1a..f976c3d4d 100644 --- a/src/esm/api/index.ts +++ b/src/esm/api/index.ts @@ -1 +1,2 @@ export { register } from './register.js'; +export { tsImport } from './ts-import.js'; diff --git a/src/esm/api/register.ts b/src/esm/api/register.ts index 911b09b0f..8b7966373 100644 --- a/src/esm/api/register.ts +++ b/src/esm/api/register.ts @@ -1,13 +1,70 @@ import module from 'node:module'; +import { MessageChannel, type MessagePort } from 'node:worker_threads'; +import type { Message } from '../types.js'; -export const register = () => { +type Options = { + namespace?: string; + onImport?: (url: string) => void; +}; + +export type InitializationOptions = { + namespace?: string; + port?: MessagePort; +}; + +export const register = ( + options?: Options, +) => { + const { sourceMapsEnabled } = process; process.setSourceMapsEnabled(true); + const { port1, port2 } = new MessageChannel(); module.register( - './index.mjs', + // Load new copy of loader so it can be registered multiple times + `./esm/index.mjs?${Date.now()}`, { parentURL: import.meta.url, - data: true, + data: { + namespace: options?.namespace, + port: port2, + } satisfies InitializationOptions, + transferList: [port2], }, ); + + const onImport = options?.onImport; + const importHandler = onImport && ((message: Message) => { + if (message.type === 'load') { + onImport(message.url); + } + }); + + if (importHandler) { + port1.on('message', importHandler); + port1.unref(); + } + + // unregister + return () => { + if (sourceMapsEnabled === false) { + process.setSourceMapsEnabled(false); + } + + if (importHandler) { + port1.off('message', importHandler); + } + + port1.postMessage('deactivate'); + + // Not necessary to wait, but provide the option + return new Promise((resolve) => { + const onDeactivated = (message: Message) => { + if (message.type === 'deactivated') { + resolve(); + port1.off('message', onDeactivated); + } + }; + port1.on('message', onDeactivated); + }); + }; }; diff --git a/src/esm/api/ts-import.ts b/src/esm/api/ts-import.ts new file mode 100644 index 000000000..184180515 --- /dev/null +++ b/src/esm/api/ts-import.ts @@ -0,0 +1,81 @@ +import { pathToFileURL } from 'node:url'; +import { register } from './register.js'; + +const resolveSpecifier = ( + specifier: string, + fromFile: string, + namespace: string, +) => { + const base = ( + fromFile.startsWith('file://') + ? fromFile + : pathToFileURL(fromFile) + ); + const resolvedUrl = new URL(specifier, base); + + /** + * A namespace query is added so we get our own module cache + * + * I considered using an import attribute for this, but it doesn't seem to + * make the request unique so it gets cached. + */ + resolvedUrl.searchParams.set('tsx-namespace', namespace); + + return resolvedUrl.toString(); +}; + +type Options = { + parentURL: string; + onImport?: (url: string) => void; +}; +const tsImport = ( + specifier: string, + options: string | Options, +) => { + if ( + !options + || (typeof options === 'object' && !options.parentURL) + ) { + throw new Error('The current file path (import.meta.url) must be provided in the second argument of tsImport()'); + } + + const isOptionsString = typeof options === 'string'; + const parentURL = isOptionsString ? options : options.parentURL; + const namespace = Date.now().toString(); + const resolvedUrl = resolveSpecifier(specifier, parentURL, namespace); + + /** + * We don't want to unregister this after load since there can be child import() calls + * that need TS support + * + * This is not accessible to others because of the namespace + */ + register({ + namespace, + onImport: isOptionsString ? undefined : options.onImport, + }); + return import(resolvedUrl); +}; + +/** + * Considered implmenting import.meta.resolve(), but natively, it doesn't seem to actully + * resolve relative file paths. + * + * For example, this doesn't throw: import.meta.resolve('./missing-file') + */ +// tsImport.meta = { +// resolve: ( +// specifier: string, +// fromFile: string, +// ) => { +// const resolvedUrl = resolveSpecifier(specifier, fromFile); +// const unregister = register(); +// try { +// return import.meta.resolve(resolvedUrl); +// } finally { +// unregister(); +// } +// } +// }; + +export { tsImport }; diff --git a/src/esm/hook/index.ts b/src/esm/hook/index.ts index 3d8819cbb..589174620 100644 --- a/src/esm/hook/index.ts +++ b/src/esm/hook/index.ts @@ -1,12 +1,3 @@ -import type { GlobalPreloadHook, InitializeHook } from 'node:module'; - -export const initialize: InitializeHook = async (data) => { - if (!data) { - throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0 and v18.19.0'); - } -}; - -export const globalPreload: GlobalPreloadHook = () => 'process.setSourceMapsEnabled(true);'; - +export { initialize, globalPreload } from './initialize.js'; export { load } from './load.js'; export { resolve } from './resolve.js'; diff --git a/src/esm/hook/initialize.ts b/src/esm/hook/initialize.ts new file mode 100644 index 000000000..5d71f35d4 --- /dev/null +++ b/src/esm/hook/initialize.ts @@ -0,0 +1,35 @@ +import type { GlobalPreloadHook, InitializeHook } from 'node:module'; +import type { InitializationOptions } from '../api/register.js'; +import type { Message } from '../types.js'; + +type Data = InitializationOptions & { + active: boolean; +}; + +export const data: Data = { + active: true, +}; + +export const initialize: InitializeHook = async ( + options?: InitializationOptions, +) => { + if (!options) { + throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0 and v18.19.0'); + } + + data.namespace = options.namespace; + + if (options.port) { + data.port = options.port; + + // Unregister + options.port.on('message', (message: string) => { + if (message === 'deactivate') { + data.active = false; + options.port!.postMessage({ type: 'deactivated' } satisfies Message); + } + }); + } +}; + +export const globalPreload: GlobalPreloadHook = () => 'process.setSourceMapsEnabled(true);'; diff --git a/src/esm/hook/load.ts b/src/esm/hook/load.ts index a93392960..cc2376929 100644 --- a/src/esm/hook/load.ts +++ b/src/esm/hook/load.ts @@ -6,11 +6,14 @@ import { transformDynamicImport } from '../../utils/transform/transform-dynamic- import { inlineSourceMap } from '../../source-map.js'; import { isFeatureSupported, importAttributes } from '../../utils/node-features.js'; import { parent } from '../../utils/ipc/client.js'; +import type { Message } from '../types.js'; import { fileMatcher, tsExtensionsPattern, isJsonPattern, + getNamespace, } from './utils.js'; +import { data } from './initialize.js'; const contextAttributesProperty = ( isFeatureSupported(importAttributes) @@ -21,8 +24,25 @@ const contextAttributesProperty = ( export const load: LoadHook = async ( url, context, - defaultLoad, + nextLoad, ) => { + if (!data.active) { + return nextLoad(url, context); + } + + if (data.namespace && data.namespace !== getNamespace(url)) { + return nextLoad(url, context); + } + + if (data.port) { + const parsedUrl = new URL(url); + parsedUrl.searchParams.delete('tsx-namespace'); + data.port.postMessage({ + type: 'load', + url: parsedUrl.toString(), + } satisfies Message); + } + /* Filter out node:* Maybe only handle files that start with file:// @@ -42,7 +62,7 @@ export const load: LoadHook = async ( context[contextAttributesProperty]!.type = 'json'; } - const loaded = await defaultLoad(url, context); + const loaded = await nextLoad(url, context); // CommonJS and Internal modules (e.g. node:*) if (!loaded.source) { diff --git a/src/esm/hook/resolve.ts b/src/esm/hook/resolve.ts index e89a0d9b1..8a36e55b2 100644 --- a/src/esm/hook/resolve.ts +++ b/src/esm/hook/resolve.ts @@ -12,8 +12,11 @@ import { getFormatFromFileUrl, fileProtocol, allowJs, + namespaceQuery, + getNamespace, type MaybePromise, } from './utils.js'; +import { data } from './initialize.js'; const isDirectoryPattern = /\/(?:$|\?)/; @@ -30,11 +33,11 @@ type resolve = ( ) => MaybePromise; const resolveExplicitPath = async ( - defaultResolve: NextResolve, + nextResolve: NextResolve, specifier: string, context: ResolveHookContext, ) => { - const resolved = await defaultResolve(specifier, context); + const resolved = await nextResolve(specifier, context); if ( !resolved.format @@ -51,14 +54,14 @@ const extensions = ['.js', '.json', '.ts', '.tsx', '.jsx'] as const; const tryExtensions = async ( specifier: string, context: ResolveHookContext, - defaultResolve: NextResolve, + nextResolve: NextResolve, ) => { const [specifierWithoutQuery, query] = specifier.split('?'); let throwError: Error | undefined; for (const extension of extensions) { try { return await resolveExplicitPath( - defaultResolve, + nextResolve, specifierWithoutQuery + extension + (query ? `?${query}` : ''), context, ); @@ -81,7 +84,7 @@ const tryExtensions = async ( const tryDirectory = async ( specifier: string, context: ResolveHookContext, - defaultResolve: NextResolve, + nextResolve: NextResolve, ) => { const isExplicitDirectory = isDirectoryPattern.test(specifier); const appendIndex = isExplicitDirectory ? 'index' : '/index'; @@ -91,12 +94,12 @@ const tryDirectory = async ( return await tryExtensions( specifierWithoutQuery + appendIndex + (query ? `?${query}` : ''), context, - defaultResolve, + nextResolve, ); } catch (_error) { if (!isExplicitDirectory) { try { - return await tryExtensions(specifier, context, defaultResolve); + return await tryExtensions(specifier, context, nextResolve); } catch {} } @@ -111,12 +114,29 @@ const tryDirectory = async ( export const resolve: resolve = async ( specifier, context, - defaultResolve, + nextResolve, recursiveCall, ) => { + if (!data.active) { + return nextResolve(specifier, context); + } + + let requestNamespace = getNamespace(specifier); + if (context.parentURL) { + const parentNamespace = getNamespace(context.parentURL); + if (parentNamespace && !requestNamespace) { + requestNamespace = parentNamespace; + specifier += `${specifier.includes('?') ? '&' : '?'}${namespaceQuery}${parentNamespace}`; + } + } + + if (data.namespace && data.namespace !== requestNamespace) { + return nextResolve(specifier, context); + } + // If directory, can be index.js, index.ts, etc. if (isDirectoryPattern.test(specifier)) { - return await tryDirectory(specifier, context, defaultResolve); + return await tryDirectory(specifier, context, nextResolve); } const isPath = ( @@ -135,7 +155,7 @@ export const resolve: resolve = async ( return await resolve( pathToFileURL(possiblePath).toString(), context, - defaultResolve, + nextResolve, ); } catch {} } @@ -150,8 +170,8 @@ export const resolve: resolve = async ( if (tsPaths) { for (const tsPath of tsPaths) { try { - return await resolveExplicitPath(defaultResolve, tsPath, context); - // return await resolve(tsPath, context, defaultResolve, true); + return await resolveExplicitPath(nextResolve, tsPath, context); + // return await resolve(tsPath, context, nextResolve, true); } catch (error) { const { code } = error as NodeError; if ( @@ -166,7 +186,7 @@ export const resolve: resolve = async ( } try { - return await resolveExplicitPath(defaultResolve, specifier, context); + return await resolveExplicitPath(nextResolve, specifier, context); } catch (error) { if ( error instanceof Error @@ -175,7 +195,7 @@ export const resolve: resolve = async ( const { code } = error as NodeError; if (code === 'ERR_UNSUPPORTED_DIR_IMPORT') { try { - return await tryDirectory(specifier, context, defaultResolve); + return await tryDirectory(specifier, context, nextResolve); } catch (error_) { if ((error_ as NodeError).code !== 'ERR_PACKAGE_IMPORT_NOT_DEFINED') { throw error_; @@ -185,7 +205,7 @@ export const resolve: resolve = async ( if (code === 'ERR_MODULE_NOT_FOUND') { try { - return await tryExtensions(specifier, context, defaultResolve); + return await tryExtensions(specifier, context, nextResolve); } catch {} } } diff --git a/src/esm/hook/utils.ts b/src/esm/hook/utils.ts index 2503466e2..e7cee7ff2 100644 --- a/src/esm/hook/utils.ts +++ b/src/esm/hook/utils.ts @@ -57,3 +57,27 @@ export const getFormatFromFileUrl = (fileUrl: string) => { }; export type MaybePromise = T | Promise; + +export const namespaceQuery = 'tsx-namespace='; +export const getNamespace = ( + url: string, +) => { + const index = url.indexOf(namespaceQuery); + if (index === -1) { + return; + } + + const charBefore = url[index - 1]; + if (charBefore !== '?' && charBefore !== '&') { + return; + } + + const startIndex = index + namespaceQuery.length; + const endIndex = url.indexOf('&', startIndex); + + return ( + endIndex === -1 + ? url.slice(startIndex) + : url.slice(startIndex, endIndex) + ); +}; diff --git a/src/esm/types.ts b/src/esm/types.ts new file mode 100644 index 000000000..ee1372704 --- /dev/null +++ b/src/esm/types.ts @@ -0,0 +1,6 @@ +export type Message = { + type: 'deactivated'; +} | { + type: 'load'; + url: string; +}; diff --git a/src/run.ts b/src/run.ts index 950b7203d..7d7ad8d9d 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,7 +1,7 @@ import type { StdioOptions } from 'node:child_process'; import { pathToFileURL } from 'node:url'; import spawn from 'cross-spawn'; -import { isFeatureSupported, moduleRegister } from './utils/node-features'; +import { isFeatureSupported, moduleRegister } from './utils/node-features.js'; export const run = ( argv: string[], diff --git a/src/utils/resolve-ts-path.ts b/src/utils/resolve-ts-path.ts index 523bbc0bd..5380d0e80 100644 --- a/src/utils/resolve-ts-path.ts +++ b/src/utils/resolve-ts-path.ts @@ -9,17 +9,17 @@ tsExtensions['.mjs'] = ['.mts']; export const resolveTsPath = ( filePath: string, ) => { - const extension = path.extname(filePath); - const [extensionNoQuery, query] = path.extname(filePath).split('?'); - const possibleExtensions = tsExtensions[extensionNoQuery]; + const [pathname, search] = filePath.split('?'); + const extension = path.extname(pathname); + const possibleExtensions = tsExtensions[extension]; if (possibleExtensions) { - const extensionlessPath = filePath.slice(0, -extension.length); + const extensionlessPath = pathname.slice(0, -extension.length); return possibleExtensions.map( tsExtension => ( extensionlessPath + tsExtension - + (query ? `?${query}` : '') + + (search ? `?${search}` : '') ), ); } diff --git a/tests/index.ts b/tests/index.ts index f9184eeec..4b71a283d 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -12,7 +12,6 @@ import { nodeVersions } from './utils/node-versions'; await describe(`Node ${node.version}`, async ({ runTestSuite }) => { await runTestSuite(import('./specs/api'), node); await runTestSuite(import('./specs/cli'), node); - await runTestSuite(import('./specs/api'), node); await runTestSuite(import('./specs/watch'), node); await runTestSuite(import('./specs/loaders'), node); await runTestSuite( diff --git a/tests/specs/api.ts b/tests/specs/api.ts index b46c504ad..63e20152b 100644 --- a/tests/specs/api.ts +++ b/tests/specs/api.ts @@ -3,7 +3,12 @@ import { execaNode } from 'execa'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; import { - tsxEsmPath, tsxCjsApiPath, type NodeApis, tsxCjsPath, + tsxCjsPath, + tsxCjsApiPath, + tsxEsmPath, + tsxEsmApiPath, + tsxEsmApiCjsPath, + type NodeApis, } from '../utils/tsx.js'; const tsFiles = { @@ -79,6 +84,7 @@ export default testSuite(({ describe }, node: NodeApis) => { test('loads', async ({ onTestFinish }) => { const fixture = await createFixture({ 'require.cjs': ` + const path = require('node:path'); const tsx = require(${JSON.stringify(tsxCjsApiPath)}); try { require('./file'); @@ -91,6 +97,7 @@ export default testSuite(({ describe }, node: NodeApis) => { // Remove from cache const loadedPath = tsx.require.resolve('./file', __filename); + console.log(loadedPath.split(path.sep).pop()); delete require.cache[loadedPath]; try { @@ -108,7 +115,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodeOptions: [], }); - expect(stdout).toBe('Fails as expected\nfoo bar\nUnpolluted global require'); + expect(stdout).toBe('Fails as expected\nfoo bar\nfile.ts\nUnpolluted global require'); }); test('catchable', async ({ onTestFinish }) => { @@ -131,10 +138,10 @@ export default testSuite(({ describe }, node: NodeApis) => { }); }); - describe('node:module', ({ test }) => { + describe('module', ({ describe, test }) => { test('cli', async ({ onTestFinish }) => { const fixture = await createFixture({ - 'package.json': JSON.stringify({ type: 'node:module' }), + 'package.json': JSON.stringify({ type: 'module' }), 'index.ts': 'import { message } from \'./file\';\n\nconsole.log(message, new Error().stack);', ...tsFiles, }); @@ -151,7 +158,7 @@ export default testSuite(({ describe }, node: NodeApis) => { if (node.supports.moduleRegister) { test('module.register', async ({ onTestFinish }) => { const fixture = await createFixture({ - 'package.json': JSON.stringify({ type: 'node:module' }), + 'package.json': JSON.stringify({ type: 'module' }), 'module-register.mjs': ` import { register } from 'node:module'; @@ -178,6 +185,181 @@ export default testSuite(({ describe }, node: NodeApis) => { expect(stdout).toBe('Fails as expected\nfoo bar'); }); + + describe('register / unregister', ({ test }) => { + test('register / unregister', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'register.mjs': ` + import { register } from ${JSON.stringify(tsxEsmApiPath)}; + try { + await import('./file.ts?1'); + } catch { + console.log('Fails as expected 1'); + } + + const unregister = register(); + + const { message } = await import('./file?2'); + console.log(message); + + await unregister(); + + try { + await import('./file.ts?3'); + } catch { + console.log('Fails as expected 2'); + } + `, + ...tsFiles, + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'register.mjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('Fails as expected 1\nfoo bar\nFails as expected 2'); + }); + + test('onImport', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'register.mjs': ` + import { register } from ${JSON.stringify(tsxEsmApiPath)}; + + const unregister = register({ + onImport(file) { + console.log(file.split('/').pop()); + }, + }); + + await import('./file'); + `, + ...tsFiles, + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'register.mjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('file.ts\nfoo.ts\nbar.ts'); + }); + }); + + // add CJS test + describe('tsImport()', ({ test }) => { + test('module', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'import.mjs': ` + import { tsImport } from ${JSON.stringify(tsxEsmApiPath)}; + + await import('./file.ts').catch((error) => { + console.log('Fails as expected 1'); + }); + + const { message } = await tsImport('./file.ts', import.meta.url); + console.log(message); + + const { message: message2 } = await tsImport('./file.ts?with-query', import.meta.url); + console.log(message2); + + // Global not polluted + await import('./file.ts?nocache').catch((error) => { + console.log('Fails as expected 2'); + }); + `, + ...tsFiles, + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'import.mjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('Fails as expected 1\nfoo bar\nfoo bar\nFails as expected 2'); + }); + + test('commonjs', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'import.cjs': ` + const { tsImport } = require(${JSON.stringify(tsxEsmApiCjsPath)}); + + (async () => { + await import('./file.ts').catch((error) => { + console.log('Fails as expected 1'); + }); + + const { message } = await tsImport('./file.ts', __filename); + console.log(message); + + const { message: message2 } = await tsImport('./file.ts?with-query', __filename); + console.log(message2); + + // Global not polluted + await import('./file.ts?nocache').catch((error) => { + console.log('Fails as expected 2'); + }); + })(); + `, + ...tsFiles, + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'import.cjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('Fails as expected 1\nfoo bar\nfoo bar\nFails as expected 2'); + }); + + test('namespace allows async nested calls', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'import.mjs': ` + import { tsImport } from ${JSON.stringify(tsxEsmApiPath)}; + tsImport('./file.ts', import.meta.url); + import('./file.ts').catch(() => console.log('Fails as expected')) + `, + 'file.ts': 'import(\'./foo.ts\')', + 'foo.ts': 'console.log(\'foo\' as string)', + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'import.mjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('Fails as expected\nfoo'); + }); + + test('onImport', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + 'import.mjs': ` + import { tsImport } from ${JSON.stringify(tsxEsmApiPath)}; + tsImport('./file.ts', { + parentURL: import.meta.url, + onImport(file) { + console.log(file.split('/').pop()); + }, + }); + `, + 'file.ts': 'import(\'./foo.ts\')', + 'foo.ts': 'console.log(\'foo\' as string)', + }); + onTestFinish(async () => await fixture.rm()); + + const { stdout } = await execaNode(path.join(fixture.path, 'import.mjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + expect(stdout).toBe('file.ts\nfoo.ts\nfoo'); + }); + }); } }); }); diff --git a/tests/utils/tsx.ts b/tests/utils/tsx.ts index 87ad22e39..4eb04d537 100644 --- a/tests/utils/tsx.ts +++ b/tests/utils/tsx.ts @@ -18,6 +18,8 @@ export const tsxPath = fileURLToPath(new URL('../../dist/cli.mjs', import.meta.u export const tsxCjsPath = fileURLToPath(new URL('../../dist/cjs/index.cjs', import.meta.url).toString()); export const tsxCjsApiPath = fileURLToPath(new URL('../../dist/cjs/api/index.cjs', import.meta.url).toString()); export const tsxEsmPath = new URL('../../dist/esm/index.mjs', import.meta.url).toString(); +export const tsxEsmApiPath = new URL('../../dist/esm/api/index.mjs', import.meta.url).toString(); +export const tsxEsmApiCjsPath = fileURLToPath(new URL('../../dist/esm/api/index.cjs', import.meta.url).toString()); const cjsPatchPath = fileURLToPath(new URL('../../dist/cjs/index.cjs', import.meta.url).toString()); const hookPath = new URL('../../dist/esm/index.cjs', import.meta.url).toString();