diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 67d20edca..dd86c8436 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: 💬 Help / Questions / Discussions - url: https://github.com/esbuild-kit/tsx/discussions + url: https://github.com/privatenumber/tsx/discussions about: Use GitHub Discussions for anything else - name: 🚀 Priority Support diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a0f26735..1972ad4b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,15 +3,14 @@ on: push: branches: [develop] pull_request: - branches: [master, develop, next] jobs: test: name: Test runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] - timeout-minutes: 15 + os: [ubuntu-latest, windows-latest] + timeout-minutes: 5 steps: - name: Checkout diff --git a/README.md b/README.md index fa9c7d7b0..99e839797 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,9 @@ - Blazing fast on-demand TypeScript & ESM compilation - Works in both [CommonJS and ESM packages](https://nodejs.org/api/packages.html#type) - Supports next-gen TypeScript extensions (`.cts` & `.mts`) -- Supports `node:` import prefixes - Hides experimental feature warnings - TypeScript REPL - Resolves `tsconfig.json` [`paths`](https://www.typescriptlang.org/tsconfig#paths) -- Tested on Linux & Windows with Node.js v12~20 > **💡 Protip: Looking to bundle your TypeScript project?** > @@ -45,7 +43,7 @@ How does it compare to [ts-node](https://github.com/TypeStrong/ts-node)? Checkou tsx strives to: 1. Enhance Node.js with TypeScript compatibility 2. Improve ESM <-> CJS interoperability -3. Support the latest major version of Node.js v12 and up _(likely to change in the future)_ +3. Support the [LTS versions of Node.js](https://endoflife.date/nodejs) ## Install @@ -106,10 +104,10 @@ To set a custom path, use the `--tsconfig` flag: tsx --tsconfig ./path/to/tsconfig.custom.json ./file.ts ``` -Alternatively, use the `ESBK_TSCONFIG_PATH` environment variable: +Alternatively, use the `TSX_TSCONFIG_PATH` environment variable: ```sh -ESBK_TSCONFIG_PATH=./path/to/tsconfig.custom.json tsx ./file.ts +TSX_TSCONFIG_PATH=./path/to/tsconfig.custom.json tsx ./file.ts ``` ### Watch mode @@ -149,6 +147,12 @@ Set the `--no-cache` flag to disable the cache: tsx --no-cache ./file.ts ``` +Alternatively, use the `TSX_DISABLE_CACHE` environment variable: + +```sh +TSX_DISABLE_CACHE=1 tsx ./file.ts +``` + ### Node.js Loader `tsx` is a standalone binary designed to be used in place of `node`, but sometimes you'll want to use `node` directly. For example, when adding TypeScript & ESM support to npm-installed binaries. diff --git a/package.json b/package.json index dae629516..8cfcc2ff2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "typescript" ], "license": "MIT", - "repository": "esbuild-kit/tsx", + "repository": "privatenumber/tsx", "author": { "name": "Hiroki Osame", "email": "hiroki.osame@gmail.com" @@ -47,6 +47,9 @@ "lint-staged": { "*.{js,ts,mjs,mts,cjs,cts,json}": "pnpm lint" }, + "engines": { + "node": ">=18.0.0" + }, "dependencies": { "esbuild": "~0.18.20", "get-tsconfig": "^4.7.2", @@ -76,7 +79,7 @@ "kolorist": "^1.8.0", "lint-staged": "^14.0.1", "magic-string": "^0.30.3", - "manten": "^1.1.0", + "manten": "^1.2.0", "node-pty": "^1.0.0", "outdent": "^0.8.0", "pkgroll": "^1.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2bd97f6b..615be531d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,8 +82,8 @@ devDependencies: specifier: ^0.30.3 version: 0.30.3 manten: - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^1.2.0 + version: 1.2.0 node-pty: specifier: ^1.0.0 version: 1.0.0 @@ -401,10 +401,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 '@types/node': 20.6.0 - '@types/yargs': 17.0.24 + '@types/yargs': 17.0.31 chalk: 4.1.2 dev: true @@ -625,20 +625,20 @@ packages: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true - /@types/istanbul-lib-coverage@2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: true - /@types/istanbul-lib-report@3.0.0: - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} dependencies: - '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-lib-coverage': 2.0.6 dev: true - /@types/istanbul-reports@3.0.1: - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} dependencies: - '@types/istanbul-lib-report': 3.0.0 + '@types/istanbul-lib-report': 3.0.3 dev: true /@types/json-schema@7.0.12: @@ -693,22 +693,22 @@ packages: source-map: 0.6.1 dev: true - /@types/stack-utils@2.0.1: - resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true /@types/unist@2.0.8: resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} dev: true - /@types/yargs-parser@21.0.0: - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true - /@types/yargs@17.0.24: - resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + /@types/yargs@17.0.31: + resolution: {integrity: sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==} dependencies: - '@types/yargs-parser': 21.0.0 + '@types/yargs-parser': 21.0.3 dev: true /@typescript-eslint/eslint-plugin@6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2): @@ -2684,7 +2684,7 @@ packages: dependencies: '@babel/code-frame': 7.22.13 '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.1 + '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 @@ -2931,8 +2931,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /manten@1.1.0: - resolution: {integrity: sha512-DyEpskY9RzqXWO3IZqH8SKxsYHdKyN+k+cLtLoTTg82/DGKbKZuHs6nBV17xchnBVFxibAWGhNSXo7h1EHO0jw==} + /manten@1.2.0: + resolution: {integrity: sha512-H6+meeudHQqh8k4N5IKB40TP27V1rjcYfmRdkJ5ffCaMYxVKW0M1p9+LVAH0uGoxcMcHKJ4GNUPhOit4Ok/ykA==} dependencies: expect: 29.7.0 dev: true diff --git a/src/cjs/index.ts b/src/cjs/index.ts index 936ceb9c3..00ee26fc6 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -8,20 +8,20 @@ import { createFilesMatcher, } from 'get-tsconfig'; import type { TransformOptions } from 'esbuild'; -import { installSourceMapSupport } from '../source-map'; +import { installSourceMapSupport, shouldStripSourceMap, stripSourceMap } from '../source-map'; import { transformSync, transformDynamicImport } from '../utils/transform'; import { resolveTsPath } from '../utils/resolve-ts-path'; -import { nodeSupportsImport, supportsNodePrefix } from '../utils/node-features'; +import { isESM } from '../utils/esm-pattern'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; const nodeModulesPath = `${path.sep}node_modules${path.sep}`; const tsconfig = ( - process.env.ESBK_TSCONFIG_PATH + process.env.TSX_TSCONFIG_PATH ? { - path: path.resolve(process.env.ESBK_TSCONFIG_PATH), - config: parseTsconfig(process.env.ESBK_TSCONFIG_PATH), + path: path.resolve(process.env.TSX_TSCONFIG_PATH), + config: parseTsconfig(process.env.TSX_TSCONFIG_PATH), } : getTsconfig() ); @@ -34,29 +34,25 @@ const applySourceMap = installSourceMapSupport(); const extensions = Module._extensions; const defaultLoader = extensions['.js']; -const transformExtensions = [ - '.js', - '.cjs', +const typescriptExtensions = [ '.cts', - '.mjs', '.mts', '.ts', '.tsx', '.jsx', ]; +const transformExtensions = [ + '.js', + '.cjs', + '.mjs', +]; + const transformer = ( module: Module, filePath: string, ) => { - const shouldTransformFile = transformExtensions.some(extension => filePath.endsWith(extension)); - if (!shouldTransformFile) { - return defaultLoader(module, filePath); - } - - /** - * For tracking dependencies in watch mode - */ + // For tracking dependencies in watch mode if (process.send) { process.send({ type: 'dependency', @@ -64,14 +60,31 @@ const transformer = ( }); } + const transformTs = typescriptExtensions.some(extension => filePath.endsWith(extension)); + const transformJs = transformExtensions.some(extension => filePath.endsWith(extension)); + if (!transformTs && !transformJs) { + return defaultLoader(module, filePath); + } + let code = fs.readFileSync(filePath, 'utf8'); - if (filePath.endsWith('.cjs') && nodeSupportsImport) { + // Strip source maps if originally disabled + if (shouldStripSourceMap) { + code = stripSourceMap(code); + } + + if (filePath.endsWith('.cjs')) { + // Contains native ESM check const transformed = transformDynamicImport(filePath, code); if (transformed) { code = applySourceMap(transformed, filePath); } - } else { + } else if ( + transformTs + + // CommonJS file but uses ESM import/export + || isESM(code) + ) { const transformed = transformSync( code, filePath, @@ -126,13 +139,12 @@ Object.defineProperty(extensions, '.mjs', { enumerable: false, }); -// Add support for "node:" protocol const defaultResolveFilename = Module._resolveFilename.bind(Module); Module._resolveFilename = (request, parent, isMain, options) => { - // Added in v12.20.0 - // https://nodejs.org/api/esm.html#esm_node_imports - if (!supportsNodePrefix && request.startsWith('node:')) { - request = request.slice(5); + // Strip query string + const queryIndex = request.indexOf('?'); + if (queryIndex !== -1) { + request = request.slice(0, queryIndex); } if ( @@ -191,20 +203,22 @@ const resolveTsFilename = ( && isTsFilePatten.test(parent.filename) && tsPath ) { - try { - return defaultResolveFilename( - tsPath[0], - parent, - isMain, - options, - ); - } catch (error) { - const { code } = error as NodeError; - if ( - code !== 'MODULE_NOT_FOUND' - && code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' - ) { - throw error; + for (const tryTsPath of tsPath) { + try { + return defaultResolveFilename( + tryTsPath, + parent, + isMain, + options, + ); + } catch (error) { + const { code } = error as NodeError; + if ( + code !== 'MODULE_NOT_FOUND' + && code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' + ) { + throw error; + } } } } diff --git a/src/esm/index.ts b/src/esm/index.ts index 09e116e4c..f31120979 100644 --- a/src/esm/index.ts +++ b/src/esm/index.ts @@ -11,4 +11,3 @@ if ( } export * from './loaders.js'; -export * from './loaders-deprecated.js'; diff --git a/src/esm/loaders-deprecated.ts b/src/esm/loaders-deprecated.ts deleted file mode 100644 index 0ac52b34f..000000000 --- a/src/esm/loaders-deprecated.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Deprecated ESM loaders used in Node v12 & 14 - * https://nodejs.org/docs/latest-v12.x/api/esm.html#esm_hooks - * https://nodejs.org/docs/latest-v14.x/api/esm.html#esm_hooks - */ -import { fileURLToPath } from 'url'; -import type { ModuleFormat } from 'module'; -import type { TransformOptions } from 'esbuild'; -import { transform, transformDynamicImport } from '../utils/transform'; -import { nodeSupportsDeprecatedLoaders } from '../utils/node-features'; -import { - applySourceMap, - fileMatcher, - tsExtensionsPattern, - getFormatFromFileUrl, - fileProtocol, - isJsonPattern, - type MaybePromise, - type NodeError, -} from './utils.js'; - -type getFormat = ( - url: string, - context: Record, - defaultGetFormat: getFormat, -) => MaybePromise<{ format: ModuleFormat }>; - -const _getFormat: getFormat = async function ( - url, - context, - defaultGetFormat, -) { - if (isJsonPattern.test(url)) { - return { format: 'module' }; - } - - try { - return await defaultGetFormat(url, context, defaultGetFormat); - } catch (error) { - if ( - (error as NodeError).code === 'ERR_UNKNOWN_FILE_EXTENSION' - && url.startsWith(fileProtocol) - ) { - const format = await getFormatFromFileUrl(url); - if (format) { - return { format }; - } - } - - throw error; - } -}; - -type Source = string | SharedArrayBuffer | Uint8Array; - -type transformSource = ( - source: Source, - context: { - url: string; - format: ModuleFormat; - }, - defaultTransformSource: transformSource, -) => MaybePromise<{ source: Source }> - -const _transformSource: transformSource = async function ( - source, - context, - defaultTransformSource, -) { - const { url } = context; - const filePath = url.startsWith('file://') ? fileURLToPath(url) : url; - - if (process.send) { - process.send({ - type: 'dependency', - path: url, - }); - } - - if ( - isJsonPattern.test(url) - || tsExtensionsPattern.test(url) - ) { - const transformed = await transform( - source.toString(), - filePath, - { - tsconfigRaw: fileMatcher?.(filePath) as TransformOptions['tsconfigRaw'], - }, - ); - - return { - source: applySourceMap(transformed, url), - }; - } - - const result = await defaultTransformSource(source, context, defaultTransformSource); - - if (context.format === 'module') { - const dynamicImportTransformed = transformDynamicImport(filePath, result.source.toString()); - if (dynamicImportTransformed) { - result.source = applySourceMap( - dynamicImportTransformed, - url, - ); - } - } - - return result; -}; - -export const getFormat = nodeSupportsDeprecatedLoaders ? _getFormat : undefined; -export const transformSource = nodeSupportsDeprecatedLoaders ? _transformSource : undefined; diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index 4f105e88e..deb43c7d5 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -7,11 +7,8 @@ import type { import type { TransformOptions } from 'esbuild'; import { transform, transformDynamicImport } from '../utils/transform'; import { resolveTsPath } from '../utils/resolve-ts-path'; +import { installSourceMapSupport, shouldStripSourceMap, stripSourceMap } from '../source-map'; import { - supportsNodePrefix, -} from '../utils/node-features'; -import { - applySourceMap, tsconfigPathsMatcher, fileMatcher, tsExtensionsPattern, @@ -22,6 +19,8 @@ import { type NodeError, } from './utils.js'; +const applySourceMap = installSourceMapSupport(); + const isDirectoryPattern = /\/(?:$|\?)/; type NextResolve = ( @@ -164,12 +163,6 @@ export const resolve: resolve = async function ( defaultResolve, recursiveCall, ) { - // Added in v12.20.0 - // https://nodejs.org/api/esm.html#esm_node_imports - if (!supportsNodePrefix && specifier.startsWith('node:')) { - specifier = specifier.slice(5); - } - // If directory, can be index.js, index.ts, etc. if (isDirectoryPattern.test(specifier)) { return await tryDirectory(specifier, context, defaultResolve); @@ -257,6 +250,10 @@ export const load: LoadHook = async function ( context, defaultLoad, ) { + /* + Filter out node:* + Maybe only handle files that start with file:// + */ if (sendToParent) { sendToParent({ type: 'dependency', @@ -273,12 +270,18 @@ export const load: LoadHook = async function ( const loaded = await defaultLoad(url, context); + // CommonJS and Internal modules (e.g. node:*) if (!loaded.source) { return loaded; } const filePath = url.startsWith('file://') ? fileURLToPath(url) : url; - const code = loaded.source.toString(); + let code = loaded.source.toString(); + + // Strip source maps if originally disabled + if (shouldStripSourceMap) { + code = stripSourceMap(code); + } if ( // Support named imports in JSON modules diff --git a/src/esm/utils.ts b/src/esm/utils.ts index 98718dfb2..cfecb1874 100644 --- a/src/esm/utils.ts +++ b/src/esm/utils.ts @@ -6,16 +6,13 @@ import { createPathsMatcher, createFilesMatcher, } from 'get-tsconfig'; -import { installSourceMapSupport } from '../source-map'; import { getPackageType } from './package-json.js'; -export const applySourceMap = installSourceMapSupport(); - const tsconfig = ( - process.env.ESBK_TSCONFIG_PATH + process.env.TSX_TSCONFIG_PATH ? { - path: path.resolve(process.env.ESBK_TSCONFIG_PATH), - config: parseTsconfig(process.env.ESBK_TSCONFIG_PATH), + path: path.resolve(process.env.TSX_TSCONFIG_PATH), + config: parseTsconfig(process.env.TSX_TSCONFIG_PATH), } : getTsconfig() ); diff --git a/src/run.ts b/src/run.ts index 79fc375e2..9d1cf5c30 100644 --- a/src/run.ts +++ b/src/run.ts @@ -21,11 +21,11 @@ export function run( if (options) { if (options.noCache) { - environment.ESBK_DISABLE_CACHE = '1'; + environment.TSX_DISABLE_CACHE = '1'; } if (options.tsconfigPath) { - environment.ESBK_TSCONFIG_PATH = options.tsconfigPath; + environment.TSX_TSCONFIG_PATH = options.tsconfigPath; } } diff --git a/src/source-map.ts b/src/source-map.ts index 148e56028..9e026b5a7 100644 --- a/src/source-map.ts +++ b/src/source-map.ts @@ -10,15 +10,31 @@ type PortMessage = { map: RawSourceMap; }; -const inlineSourceMapPrefix = '\n//# sourceMappingURL=data:application/json;base64,'; +// If Node.js has source map disabled, we should strip source maps to speed up processing +export const shouldStripSourceMap = ( + ('sourceMapsEnabled' in process) + && process.sourceMapsEnabled === false +); -export function installSourceMapSupport( +const sourceMapPrefix = '\n//# sourceMappingURL='; + +export const stripSourceMap = (code: string) => { + const sourceMapIndex = code.indexOf(sourceMapPrefix); + if (sourceMapIndex !== -1) { + return code.slice(0, sourceMapIndex); + } + return code; +}; + +const inlineSourceMapPrefix = `${sourceMapPrefix}data:application/json;base64,`; + +export const installSourceMapSupport = ( /** * To support Node v20 where loaders are executed in its own thread * https://nodejs.org/docs/latest-v20.x/api/esm.html#globalpreload */ loaderPort?: MessagePort, -) { +) => { const hasNativeSourceMapSupport = ( /** * Check if native source maps are supported by seeing if the API is available @@ -77,4 +93,4 @@ export function installSourceMapSupport( } return code; }; -} +}; diff --git a/src/utils/debug.ts b/src/utils/debug.ts new file mode 100644 index 000000000..7525106b6 --- /dev/null +++ b/src/utils/debug.ts @@ -0,0 +1,37 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const time = unknown>( + name: string, + _function: T, + threshold = 100, +): T => function ( + this: unknown, + ...args: Parameters +) { + const timeStart = Date.now(); + const logTimeElapsed = () => { + const elapsed = Date.now() - timeStart; + + if (elapsed > threshold) { + console.log(name, { + args, + elapsed, + }); + } + }; + + const result = Reflect.apply(_function, this, args); + if ( + result + && typeof result === 'object' + && 'then' in result + ) { + (result as Promise).then( + logTimeElapsed, + // Ignore error in this chain + () => {}, + ); + } else { + logTimeElapsed(); + } + return result; +} as T; diff --git a/src/utils/esm-pattern.ts b/src/utils/esm-pattern.ts new file mode 100644 index 000000000..9b7f0782b --- /dev/null +++ b/src/utils/esm-pattern.ts @@ -0,0 +1,29 @@ +import { parseEsm } from './es-module-lexer'; + +/* +Previously, this regex was used as a naive ESM catch, +but turns out regex is slower than the lexer so removing +it made the check faster. + +Catches: +import a from 'b' +import 'b'; +import('b'); +export{a}; +export default a; + +Doesn't catch: +EXPORT{a} +exports.a = 1 +module.exports = 1 + +const esmPattern = /\b(?:import|export)\b/; +*/ + +export const isESM = (code: string) => { + if (code.includes('import') || code.includes('export')) { + const [imports, exports] = parseEsm(code); + return imports.length > 0 || exports.length > 0; + } + return false; +}; diff --git a/src/utils/node-features.ts b/src/utils/node-features.ts index d9c23b14d..825017182 100644 --- a/src/utils/node-features.ts +++ b/src/utils/node-features.ts @@ -8,24 +8,6 @@ const compareNodeVersion = (version: Version) => ( || nodeVersion[2] - version[2] ); -export const nodeSupportsImport = ( - // v13.2.0 and higher - compareNodeVersion([13, 2, 0]) >= 0 - - // 12.20.0 ~ 13.0.0 - || ( - compareNodeVersion([12, 20, 0]) >= 0 - && compareNodeVersion([13, 0, 0]) < 0 - ) -); - -export const supportsNodePrefix = ( - compareNodeVersion([16, 0, 0]) >= 0 - || compareNodeVersion([14, 18, 0]) >= 0 -); - -export const nodeSupportsDeprecatedLoaders = compareNodeVersion([16, 12, 0]) < 0; - /** * Node.js loaders are isolated from v20 * https://github.com/nodejs/node/issues/49455#issuecomment-1703812193 diff --git a/src/utils/transform/cache.ts b/src/utils/transform/cache.ts index 47656a5e7..311b63ae3 100644 --- a/src/utils/transform/cache.ts +++ b/src/utils/transform/cache.ts @@ -145,7 +145,7 @@ class FileCache extends Map { } export default ( - process.env.ESBK_DISABLE_CACHE + process.env.TSX_DISABLE_CACHE ? new Map() : new FileCache() ); diff --git a/src/utils/transform/transform-dynamic-import.ts b/src/utils/transform/transform-dynamic-import.ts index 7bc18bf17..682d56db2 100644 --- a/src/utils/transform/transform-dynamic-import.ts +++ b/src/utils/transform/transform-dynamic-import.ts @@ -2,25 +2,33 @@ import MagicString from 'magic-string'; import type { RawSourceMap } from '../../source-map'; import { parseEsm } from '../es-module-lexer'; -const checkEsModule = `.then((mod)=>{ - const exports = Object.keys(mod); - if( - exports.length===1&&exports[0]==='default'&&mod.default&&mod.default.__esModule - ){ - return mod.default +const handlerName = '___tsxInteropDynamicImport'; +const handleEsModuleFunction = `function ${handlerName}${(function (imported: Record) { + const d = 'default'; + const exports = Object.keys(imported); + if ( + exports.length === 1 + && exports[0] === d + && imported[d] + && typeof imported[d] === 'object' + && '__esModule' in imported[d] + ) { + return imported[d]; } - return mod -})` - // replaceAll is not supported in Node 12 - // eslint-disable-next-line unicorn/prefer-string-replace-all - .replace(/[\n\t]+/g, ''); -export function transformDynamicImport( + return imported; +}).toString().slice('function'.length)}`; + +const handleDynamicImport = `.then(${handlerName})`; + +const esmImportPattern = /\bimport\b/; + +export const transformDynamicImport = ( filePath: string, code: string, -) { +) => { // Naive check - if (!code.includes('import')) { + if (!esmImportPattern.test(code)) { return; } @@ -33,14 +41,25 @@ export function transformDynamicImport( const magicString = new MagicString(code); for (const dynamicImport of dynamicImports) { - magicString.appendRight(dynamicImport.se, checkEsModule); + magicString.appendRight(dynamicImport.se, handleDynamicImport); } + magicString.append(handleEsModuleFunction); + + const newCode = magicString.toString(); + const newMap = magicString.generateMap({ + source: filePath, + includeContent: false, + + /** + * The performance hit on this is very high + * Since we're only transforming import()s, I think this may be overkill + */ + // hires: 'boundary', + }) as unknown as RawSourceMap; + return { - code: magicString.toString(), - map: magicString.generateMap({ - source: filePath, - hires: true, - }) as unknown as RawSourceMap, + code: newCode, + map: newMap, }; -} +}; diff --git a/tests/fixtures/catch-signals.js b/tests/fixtures/catch-signals.js deleted file mode 100644 index e04df1006..000000000 --- a/tests/fixtures/catch-signals.js +++ /dev/null @@ -1,18 +0,0 @@ -const signals = [ - 'SIGINT', - 'SIGTERM', -]; - -for (const name of signals) { - process.on(name, () => { - console.log(name); - - setTimeout(() => { - console.log(name, 'HANDLER COMPLETED'); - process.exit(200); - }, 100); - }); -} - -setTimeout(() => {}, 1e5); -console.log('READY'); diff --git a/tests/fixtures/import-file.js b/tests/fixtures/import-file.js deleted file mode 100644 index ab57f282e..000000000 --- a/tests/fixtures/import-file.js +++ /dev/null @@ -1,2 +0,0 @@ -// Must be .js file so it can toggle between commonjs and module -import(process.argv[2]).then(m => console.log(JSON.stringify(m))); diff --git a/tests/fixtures/import-file.ts b/tests/fixtures/import-file.ts deleted file mode 100644 index ab57f282e..000000000 --- a/tests/fixtures/import-file.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Must be .js file so it can toggle between commonjs and module -import(process.argv[2]).then(m => console.log(JSON.stringify(m))); diff --git a/tests/fixtures/keep-alive.js b/tests/fixtures/keep-alive.js deleted file mode 100644 index 3948c11df..000000000 --- a/tests/fixtures/keep-alive.js +++ /dev/null @@ -1,2 +0,0 @@ -setTimeout(() => {}, 1e5); -console.log('READY'); diff --git a/tests/fixtures/lib/cjs-ext-cjs/index.cjs b/tests/fixtures/lib/cjs-ext-cjs/index.cjs deleted file mode 100644 index 80f48bdde..000000000 --- a/tests/fixtures/lib/cjs-ext-cjs/index.cjs +++ /dev/null @@ -1,56 +0,0 @@ -async function test(description, testFunction) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${error.toString().split('\n').shift()}`); - } -} - -console.log('loaded cjs-ext-cjs/index.cjs'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -// esbuild uses import.meta as a signal for ESM -// test( -// 'import.meta.url', -// () => Boolean(import.meta.url), -// ); - -test( - 'name in error', - () => { - let nameInError; - try { - nameInError(); - } catch (error) { - return error.message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack.includes(':38:'), -); - -test( - 'resolves optional node prefix', - () => Boolean(require('node:fs')), -); - -test( - 'resolves required node prefix', - () => Boolean(require('node:test')), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -module.exports = 1234; diff --git a/tests/fixtures/lib/cjs-ext-js/index.js b/tests/fixtures/lib/cjs-ext-js/index.js deleted file mode 100644 index c2b54b9c3..000000000 --- a/tests/fixtures/lib/cjs-ext-js/index.js +++ /dev/null @@ -1,59 +0,0 @@ -async function test(description, testFunction) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${error.toString().split('\n').shift()}`); - } -} - -console.log('loaded cjs-ext-js/index.js'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -// esbuild uses import.meta as a signal for ESM -// test( -// 'import.meta.url', -// () => Boolean(import.meta.url), -// ); - -test( - 'name in error', - () => { - let nameInError; - try { - nameInError(); - } catch (error) { - return error.message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack.includes(':38:'), -); - -test( - 'resolves optional node prefix', - () => Boolean(require('node:fs')), -); - -test( - 'resolves required node prefix', - () => Boolean(require('node:test')), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'module exports', - () => module.exports = 1234, -); diff --git a/tests/fixtures/lib/esm-ext-js/index.js b/tests/fixtures/lib/esm-ext-js/index.js deleted file mode 100644 index 45659781a..000000000 --- a/tests/fixtures/lib/esm-ext-js/index.js +++ /dev/null @@ -1,55 +0,0 @@ -async function test(description, testFunction) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${error.toString().split('\n').shift()}`); - } -} - -console.log('loaded esm-ext-js/index.js'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - nameInError(); - } catch (error) { - return error.message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack.includes(':37:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/esm-ext-mjs/index.mjs b/tests/fixtures/lib/esm-ext-mjs/index.mjs deleted file mode 100644 index d1c0e5aec..000000000 --- a/tests/fixtures/lib/esm-ext-mjs/index.mjs +++ /dev/null @@ -1,55 +0,0 @@ -async function test(description, testFunction) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${error.toString().split('\n').shift()}`); - } -} - -console.log('loaded esm-ext-mjs/index.mjs'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - nameInError(); - } catch (error) { - return error.message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack.includes(':37:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/ts-ext-cts/index.cts b/tests/fixtures/lib/ts-ext-cts/index.cts deleted file mode 100644 index 05d74947a..000000000 --- a/tests/fixtures/lib/ts-ext-cts/index.cts +++ /dev/null @@ -1,56 +0,0 @@ -async function test(description: string, testFunction: () => any | Promise) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${(error as any).toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-cts/index.cts'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - // @ts-expect-error - this is a test - nameInError(); - } catch (error) { - return (error as any).message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack!.includes(':38:'), -); - -test( - 'resolves optional node prefix', - () => Boolean(require('node:fs')), -); - -test( - 'resolves required node prefix', - () => Boolean(require('node:test')), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/ts-ext-jsx/index.jsx b/tests/fixtures/lib/ts-ext-jsx/index.jsx deleted file mode 100644 index fd462a717..000000000 --- a/tests/fixtures/lib/ts-ext-jsx/index.jsx +++ /dev/null @@ -1,59 +0,0 @@ -async function test(description, testFunction) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${error.toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-jsx/index.jsx'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - nameInError(); - } catch (error) { - return error.message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack.includes(':37:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -const React = { - createElement: (...args) => Array.from(args), -}; - -export default (
hello world
); diff --git a/tests/fixtures/lib/ts-ext-mts/index.mts b/tests/fixtures/lib/ts-ext-mts/index.mts deleted file mode 100644 index 9df079336..000000000 --- a/tests/fixtures/lib/ts-ext-mts/index.mts +++ /dev/null @@ -1,56 +0,0 @@ -async function test(description: string, testFunction: () => any | Promise) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${(error as any).toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-mts/index.mts'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - // @ts-expect-error - this is a test - nameInError(); - } catch (error) { - return (error as any).message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack!.includes(':38:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/ts-ext-ts/index.ts b/tests/fixtures/lib/ts-ext-ts/index.ts deleted file mode 100644 index e36c69b8b..000000000 --- a/tests/fixtures/lib/ts-ext-ts/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -async function test(description: string, testFunction: () => any | Promise) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${(error as any).toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-ts/index.ts'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - // @ts-expect-error - this is a test - nameInError(); - } catch (error) { - return (error as any).message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack!.includes(':38:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/ts-ext-ts/index.tsx.ts b/tests/fixtures/lib/ts-ext-ts/index.tsx.ts deleted file mode 100644 index 7e012596b..000000000 --- a/tests/fixtures/lib/ts-ext-ts/index.tsx.ts +++ /dev/null @@ -1,56 +0,0 @@ -async function test(description: string, testFunction: () => any | Promise) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${(error as any).toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-ts/index.tsx.ts'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - // @ts-expect-error - this is a test - nameInError(); - } catch (error) { - return (error as any).message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack!.includes(':38:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -export default 1234; diff --git a/tests/fixtures/lib/ts-ext-tsx/index.tsx b/tests/fixtures/lib/ts-ext-tsx/index.tsx deleted file mode 100644 index f1ce11bfa..000000000 --- a/tests/fixtures/lib/ts-ext-tsx/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -async function test(description: string, testFunction: () => any | Promise) { - try { - const result = await testFunction(); - if (!result) { throw result; } - console.log(`✔ ${description}`); - } catch (error) { - console.log(`✖ ${description}: ${(error as any).toString().split('\n').shift()}`); - } -} - -console.log('loaded ts-ext-tsx/index.tsx'); - -test( - 'has CJS context', - () => typeof require !== 'undefined' || typeof module !== 'undefined', -); - -test( - 'import.meta.url', - () => Boolean(import.meta.url), -); - -test( - 'name in error', - () => { - let nameInError; - try { - // @ts-expect-error - this is a test - nameInError(); - } catch (error) { - return (error as any).message.includes('nameInError'); - } - }, -); - -test( - 'sourcemaps', - () => new Error().stack!.includes(':38:'), -); - -test( - 'has dynamic import', - () => import('fs').then(Boolean), -); - -test( - 'resolves optional node prefix', - () => import('node:fs').then(Boolean), -); - -test( - 'resolves required node prefix', - () => import('node:test').then(Boolean), -); - -const React = { - createElement: (...args: any[]) => Array.from(args), -}; - -export default (
hello world
); diff --git a/tests/fixtures/lib/wasm/index.js b/tests/fixtures/lib/wasm/index.js deleted file mode 100644 index 2e6d373be..000000000 --- a/tests/fixtures/lib/wasm/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { myValue } from './test.wasm'; - -console.log(myValue.valueOf()); diff --git a/tests/fixtures/log-argv.ts b/tests/fixtures/log-argv.ts deleted file mode 100644 index a8fd079cd..000000000 --- a/tests/fixtures/log-argv.ts +++ /dev/null @@ -1 +0,0 @@ -console.log(JSON.stringify(process.argv) as string); // Unnecessary TS syntax to test diff --git a/tests/fixtures/node_modules/package-exports/index.js b/tests/fixtures/node_modules/package-exports/index.js deleted file mode 100644 index b7a20332d..000000000 --- a/tests/fixtures/node_modules/package-exports/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export default 'default export'; -export const namedExport = 'named export'; diff --git a/tests/fixtures/node_modules/package-exports/package.json b/tests/fixtures/node_modules/package-exports/package.json deleted file mode 100644 index 02f110a9a..000000000 --- a/tests/fixtures/node_modules/package-exports/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "package-exports", - "exports": { - "./index.js": "./index.js" - } -} diff --git a/tests/fixtures/node_modules/package-module/index.js b/tests/fixtures/node_modules/package-module/index.js deleted file mode 100644 index b7a20332d..000000000 --- a/tests/fixtures/node_modules/package-module/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export default 'default export'; -export const namedExport = 'named export'; diff --git a/tests/fixtures/node_modules/package-module/package.json b/tests/fixtures/node_modules/package-module/package.json deleted file mode 100644 index 611b46a8b..000000000 --- a/tests/fixtures/node_modules/package-module/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "package-module", - "type": "module", - "main": "index.js" -} diff --git a/tests/fixtures/node_modules/package-module/ts.ts b/tests/fixtures/node_modules/package-module/ts.ts deleted file mode 100644 index 091f60949..000000000 --- a/tests/fixtures/node_modules/package-module/ts.ts +++ /dev/null @@ -1,2 +0,0 @@ -export default 'ts default export'; -export const namedExport: string = 'ts named export'; diff --git a/tests/fixtures/node_modules/package-typescript-export/index.ts b/tests/fixtures/node_modules/package-typescript-export/index.ts deleted file mode 100644 index 091f60949..000000000 --- a/tests/fixtures/node_modules/package-typescript-export/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export default 'ts default export'; -export const namedExport: string = 'ts named export'; diff --git a/tests/fixtures/node_modules/package-typescript-export/package.json b/tests/fixtures/node_modules/package-typescript-export/package.json deleted file mode 100644 index 507f6aae5..000000000 --- a/tests/fixtures/node_modules/package-typescript-export/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "package-typescript-export", - "type": "module", - "exports": { - "import": "./index.ts" - } -} diff --git a/tests/fixtures/require-file.cjs b/tests/fixtures/require-file.cjs deleted file mode 100644 index 77bb3f859..000000000 --- a/tests/fixtures/require-file.cjs +++ /dev/null @@ -1 +0,0 @@ -console.log(JSON.stringify(require(process.argv[2]))); diff --git a/tests/fixtures/require-file.cts b/tests/fixtures/require-file.cts deleted file mode 100644 index 77bb3f859..000000000 --- a/tests/fixtures/require-file.cts +++ /dev/null @@ -1 +0,0 @@ -console.log(JSON.stringify(require(process.argv[2]))); diff --git a/tests/fixtures/test-runner-file.ts b/tests/fixtures/test-runner-file.ts deleted file mode 100644 index 6e7fc8aa8..000000000 --- a/tests/fixtures/test-runner-file.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { test } from 'node:test'; -import assert from 'assert'; - -test('passing test', () => { - assert.strictEqual(1, 1); -}); diff --git a/tests/fixtures/lib/wasm/test.wasm b/tests/fixtures/test.wasm similarity index 100% rename from tests/fixtures/lib/wasm/test.wasm rename to tests/fixtures/test.wasm diff --git a/tests/fixtures/tsconfig/dependency-resolve-current-directory.ts b/tests/fixtures/tsconfig/dependency-resolve-current-directory.ts deleted file mode 100644 index 5295f76ed..000000000 --- a/tests/fixtures/tsconfig/dependency-resolve-current-directory.ts +++ /dev/null @@ -1 +0,0 @@ -import 'resolve-current-directory'; diff --git a/tests/fixtures/tsconfig/dependency-should-not-resolve-baseUrl.ts b/tests/fixtures/tsconfig/dependency-should-not-resolve-baseUrl.ts deleted file mode 100644 index 84f5a6724..000000000 --- a/tests/fixtures/tsconfig/dependency-should-not-resolve-baseUrl.ts +++ /dev/null @@ -1 +0,0 @@ -import 'should-not-resolve-baseUrl'; diff --git a/tests/fixtures/tsconfig/dependency-should-not-resolve-paths.ts b/tests/fixtures/tsconfig/dependency-should-not-resolve-paths.ts deleted file mode 100644 index c8fd4ce7a..000000000 --- a/tests/fixtures/tsconfig/dependency-should-not-resolve-paths.ts +++ /dev/null @@ -1 +0,0 @@ -import 'should-not-resolve-paths'; diff --git a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.cjs b/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.cjs deleted file mode 100644 index 9ed959542..000000000 --- a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.cjs +++ /dev/null @@ -1 +0,0 @@ -require('.'); diff --git a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.mjs b/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.mjs deleted file mode 100644 index 8f7bcd336..000000000 --- a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/file.mjs +++ /dev/null @@ -1 +0,0 @@ -import '.'; diff --git a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/index.js b/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/index.js deleted file mode 100644 index 7f2bfb16a..000000000 --- a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log('resolved'); diff --git a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/package.json b/tests/fixtures/tsconfig/node_modules/resolve-current-directory/package.json deleted file mode 100644 index aa3034061..000000000 --- a/tests/fixtures/tsconfig/node_modules/resolve-current-directory/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "exports": { - "module": "./lib/file.mjs", - "default": "./lib/file.cjs" - } -} diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.cjs b/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.cjs deleted file mode 100644 index 4e1262c5a..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.cjs +++ /dev/null @@ -1 +0,0 @@ -console.log(require('resolve-target')); diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.mjs b/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.mjs deleted file mode 100644 index 4d49952c5..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/index.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'resolve-target'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/index.js b/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/index.js deleted file mode 100644 index 08c72abce..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'resolved'; \ No newline at end of file diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/package.json b/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/package.json deleted file mode 100644 index 0967ef424..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/node_modules/resolve-target/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/package.json b/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/package.json deleted file mode 100644 index ba5e30e4c..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-baseUrl/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "exports": { - "module": "./index.mjs", - "default": "./index.cjs" - } -} diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.cjs b/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.cjs deleted file mode 100644 index 0a617e463..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.cjs +++ /dev/null @@ -1 +0,0 @@ -console.log(require('p/nested-resolve-target')); diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.mjs b/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.mjs deleted file mode 100644 index 1b9fc008c..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/index.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'p/nested-resolve-target'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/nested-resolve-target.js b/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/nested-resolve-target.js deleted file mode 100644 index 08c72abce..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/nested-resolve-target.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'resolved'; \ No newline at end of file diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/package.json b/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/package.json deleted file mode 100644 index 0967ef424..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/node_modules/p/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/package.json b/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/package.json deleted file mode 100644 index ba5e30e4c..000000000 --- a/tests/fixtures/tsconfig/node_modules/should-not-resolve-paths/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "exports": { - "module": "./index.mjs", - "default": "./index.cjs" - } -} diff --git a/tests/fixtures/tsconfig/src/base-url.ts b/tests/fixtures/tsconfig/src/base-url.ts deleted file mode 100644 index 4d49952c5..000000000 --- a/tests/fixtures/tsconfig/src/base-url.ts +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'resolve-target'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/src/index.ts b/tests/fixtures/tsconfig/src/index.ts deleted file mode 100644 index f9729c6b0..000000000 --- a/tests/fixtures/tsconfig/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('Should not run'); diff --git a/tests/fixtures/tsconfig/src/paths-exact-match.ts b/tests/fixtures/tsconfig/src/paths-exact-match.ts deleted file mode 100644 index 1b4b714fc..000000000 --- a/tests/fixtures/tsconfig/src/paths-exact-match.ts +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'paths-exact-match'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/src/paths-prefix-match.ts b/tests/fixtures/tsconfig/src/paths-prefix-match.ts deleted file mode 100644 index 1b9fc008c..000000000 --- a/tests/fixtures/tsconfig/src/paths-prefix-match.ts +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'p/nested-resolve-target'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/src/paths-suffix-match.ts b/tests/fixtures/tsconfig/src/paths-suffix-match.ts deleted file mode 100644 index 73cb17386..000000000 --- a/tests/fixtures/tsconfig/src/paths-suffix-match.ts +++ /dev/null @@ -1,3 +0,0 @@ -import value from 'nested-resolve-target/s'; - -console.log(value); diff --git a/tests/fixtures/tsconfig/src/resolve-target.ts b/tests/fixtures/tsconfig/src/resolve-target.ts deleted file mode 100644 index 3e8855613..000000000 --- a/tests/fixtures/tsconfig/src/resolve-target.ts +++ /dev/null @@ -1 +0,0 @@ -export default 'resolve-target'; diff --git a/tests/fixtures/tsconfig/src/tsx.tsx b/tests/fixtures/tsconfig/src/tsx.tsx deleted file mode 100644 index 9b979274c..000000000 --- a/tests/fixtures/tsconfig/src/tsx.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export default [ - ( -
- hello world -
- ), - ( - <> - goodbye world - - ), -]; diff --git a/tests/fixtures/tsconfig/src/utils/nested-resolve-target.ts b/tests/fixtures/tsconfig/src/utils/nested-resolve-target.ts deleted file mode 100644 index 65a22c6cb..000000000 --- a/tests/fixtures/tsconfig/src/utils/nested-resolve-target.ts +++ /dev/null @@ -1 +0,0 @@ -export default 'nested-resolve-target'; diff --git a/tests/fixtures/tsconfig/tsconfig-custom/tsconfig.custom-name.json b/tests/fixtures/tsconfig/tsconfig-custom/tsconfig.custom-name.json deleted file mode 100644 index f8e649961..000000000 --- a/tests/fixtures/tsconfig/tsconfig-custom/tsconfig.custom-name.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "jsxFactory": "console.error" - }, - "include": [ - "../src" - ] -} diff --git a/tests/fixtures/tsconfig/tsconfig.json b/tests/fixtures/tsconfig/tsconfig.json deleted file mode 100644 index 40c2faa92..000000000 --- a/tests/fixtures/tsconfig/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "jsx": "react", - "jsxFactory": "console.log", - "jsxFragmentFactory": "null", - "baseUrl": "./src", - "paths": { - "paths-exact-match": ["resolve-target"], - "p/*": ["utils/*"], - "*/s": ["utils/*"] - }, - } -} diff --git a/tests/index.ts b/tests/index.ts index c5e014756..7e7d0cb16 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,63 +1,22 @@ import { describe } from 'manten'; -import { createFixture } from 'fs-fixture'; import { createNode } from './utils/tsx'; import { nodeVersions } from './utils/node-versions'; -const packageTypes = [ - 'commonjs', - 'module', -] as const; - (async () => { - for (const packageType of packageTypes) { - await describe(`Package type: ${packageType}`, async ({ describe, onFinish }) => { - const fixture = await createFixture('./tests/fixtures/'); - - onFinish(async () => await fixture.rm()); + await describe('tsx', async ({ runTestSuite, describe }) => { + await runTestSuite(import('./specs/cli')); + await runTestSuite(import('./specs/watch')); + await runTestSuite(import('./specs/repl')); - await fixture.writeJson('package.json', { - type: packageType, - }); + for (const nodeVersion of nodeVersions) { + const node = await createNode(nodeVersion); - await describe('tsx', ({ runTestSuite }) => { - runTestSuite( - import('./specs/cli'), - fixture.path, - ); + await describe(`Node ${node.version}`, ({ runTestSuite }) => { runTestSuite( - import('./specs/watch'), - fixture.path, + import('./specs/smoke'), + node, ); }); - - for (const nodeVersion of nodeVersions) { - const node = await createNode(nodeVersion, fixture.path); - - node.packageType = packageType; - - await describe(`Node ${node.version}`, ({ runTestSuite }) => { - runTestSuite( - import('./specs/repl'), - node, - ); - runTestSuite( - import('./specs/javascript'), - node, - ); - runTestSuite( - import('./specs/typescript'), - node, - ); - runTestSuite( - import('./specs/json'), - node, - ); - runTestSuite( - import('./specs/wasm'), - node, - ); - }); - } - }); - } + } + }); })(); diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 65c32d8e6..f5f5b6e1a 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -1,69 +1,91 @@ import path from 'path'; import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; import packageJson from '../../package.json'; import { tsx, tsxPath } from '../utils/tsx'; import { ptyShell, isWindows } from '../utils/pty-shell'; +import { expectMatchInOrder } from '../utils/expect-match-in-order'; -export default testSuite(({ describe }, fixturePath: string) => { +export default testSuite(({ describe }) => { describe('CLI', ({ describe, test }) => { - describe('version', ({ test }) => { - test('shows version', async () => { - const tsxProcess = await tsx({ - args: ['--version'], - }); - - expect(tsxProcess.exitCode).toBe(0); - expect(tsxProcess.stdout).toBe(`tsx v${packageJson.version}\nnode ${process.version}`); - expect(tsxProcess.stderr).toBe(''); + describe('argv', async ({ describe, onFinish }) => { + const fixture = await createFixture({ + // Unnecessary TS to test syntax + 'log-argv.ts': 'console.log(JSON.stringify(process.argv) as string)', }); + onFinish(async () => await fixture.rm()); - test('doesn\'t show version with file', async () => { - const tsxProcess = await tsx({ - args: [ - path.join(fixturePath, 'log-argv.ts'), - '--version', - ], + describe('version', ({ test }) => { + test('shows version', async () => { + const tsxProcess = await tsx({ + args: ['--version'], + }); + + expect(tsxProcess.exitCode).toBe(0); + expect(tsxProcess.stdout).toBe(`tsx v${packageJson.version}\nnode ${process.version}`); + expect(tsxProcess.stderr).toBe(''); }); - expect(tsxProcess.exitCode).toBe(0); - expect(tsxProcess.stdout).toMatch('"--version"'); - expect(tsxProcess.stdout).not.toMatch(packageJson.version); - expect(tsxProcess.stderr).toBe(''); - }); - }); + test('doesn\'t show version with file', async () => { + const tsxProcess = await tsx({ + args: [ + path.join(fixture.path, 'log-argv.ts'), + '--version', + ], + }); - describe('help', ({ test }) => { - test('shows help', async () => { - const tsxProcess = await tsx({ - args: ['--help'], + expect(tsxProcess.exitCode).toBe(0); + expect(tsxProcess.stdout).toMatch('"--version"'); + expect(tsxProcess.stdout).not.toMatch(packageJson.version); + expect(tsxProcess.stderr).toBe(''); }); - - expect(tsxProcess.exitCode).toBe(0); - expect(tsxProcess.stdout).toMatch('Node.js runtime enhanced with esbuild for loading TypeScript & ESM'); - expect(tsxProcess.stdout).toMatch('Usage: node [options] [ script.js ] [arguments]'); - expect(tsxProcess.stderr).toBe(''); }); - test('doesn\'t show help with file', async () => { - const tsxProcess = await tsx({ - args: [ - path.join(fixturePath, 'log-argv.ts'), - '--help', - ], + describe('help', ({ test }) => { + test('shows help', async () => { + const tsxProcess = await tsx({ + args: ['--help'], + }); + + expect(tsxProcess.exitCode).toBe(0); + expect(tsxProcess.stdout).toMatch('Node.js runtime enhanced with esbuild for loading TypeScript & ESM'); + expect(tsxProcess.stdout).toMatch('Usage: node [options] [ script.js ] [arguments]'); + expect(tsxProcess.stderr).toBe(''); }); - expect(tsxProcess.exitCode).toBe(0); - expect(tsxProcess.stdout).toMatch('"--help"'); - expect(tsxProcess.stdout).not.toMatch('tsx'); - expect(tsxProcess.stderr).toBe(''); + test('doesn\'t show help with file', async () => { + const tsxProcess = await tsx({ + args: [ + path.join(fixture.path, 'log-argv.ts'), + '--help', + ], + }); + + expect(tsxProcess.exitCode).toBe(0); + expect(tsxProcess.stdout).toMatch('"--help"'); + expect(tsxProcess.stdout).not.toMatch('tsx'); + expect(tsxProcess.stderr).toBe(''); + }); }); }); - test('Node.js test runner', async () => { + test('Node.js test runner', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'test.ts': ` + import { test } from 'node:test'; + import assert from 'assert'; + + test('passing test', () => { + assert.strictEqual(1, 1); + }); + `, + }); + onTestFinish(async () => await fixture.rm()); + const tsxProcess = await tsx({ args: [ '--test', - path.join(fixturePath, 'test-runner-file.ts'), + path.join(fixture.path, 'test.ts'), ], }); @@ -71,71 +93,110 @@ export default testSuite(({ describe }, fixturePath: string) => { expect(tsxProcess.exitCode).toBe(0); }, 10_000); - describe('Relays kill signal', ({ test }) => { - const signals = ['SIGINT', 'SIGTERM']; - - for (const signal of signals) { - test(signal, async () => { - const tsxProcess = tsx({ - args: [ - path.join(fixturePath, 'catch-signals.js'), - ], + describe('Signals', async ({ describe, onFinish }) => { + const fixture = await createFixture({ + 'catch-signals.js': ` + const signals = [ + 'SIGINT', + 'SIGTERM', + ]; + + for (const name of signals) { + process.on(name, () => { + console.log(name); + + setTimeout(() => { + console.log(name, 'HANDLER COMPLETED'); + process.exit(200); + }, 100); }); + } + + setTimeout(() => {}, 1e5); + console.log('READY'); + `, + 'keep-alive.js': ` + setTimeout(() => {}, 1e5); + console.log('READY'); + `, + }); + onFinish(async () => await fixture.rm()); - tsxProcess.stdout!.once('data', () => { - tsxProcess.kill(signal, { - forceKillAfterTimeout: false, + describe('Relays kill signal', ({ test }) => { + const signals = ['SIGINT', 'SIGTERM']; + + for (const signal of signals) { + test(signal, async ({ onTestFail }) => { + const tsxProcess = tsx({ + args: [ + path.join(fixture.path, 'catch-signals.js'), + ], }); - }); - const tsxProcessResolved = await tsxProcess; - - if (process.platform === 'win32') { - /** - * Windows doesn't support sending signals to processes. - * https://nodejs.org/api/process.html#signal-events - * - * Sending SIGINT, SIGTERM, and SIGKILL will cause the unconditional termination - * of the target process, and afterwards, subprocess will report that the process - * was terminated by signal. - */ - expect(tsxProcessResolved.stdout).toBe('READY'); - } else { - expect(tsxProcessResolved.exitCode).toBe(200); - expect(tsxProcessResolved.stdout).toBe(`READY\n${signal}\n${signal} HANDLER COMPLETED`); - } + tsxProcess.stdout!.once('data', () => { + tsxProcess.kill(signal, { + forceKillAfterTimeout: false, + }); + }); + + const tsxProcessResolved = await tsxProcess; + + onTestFail(() => { + console.log(tsxProcessResolved); + }); + + if (process.platform === 'win32') { + /** + * Windows doesn't support sending signals to processes. + * https://nodejs.org/api/process.html#signal-events + * + * Sending SIGINT, SIGTERM, and SIGKILL will cause the unconditional termination + * of the target process, and afterwards, subprocess will report that the process + * was terminated by signal. + */ + expect(tsxProcessResolved.stdout).toBe('READY'); + } else { + expect(tsxProcessResolved.exitCode).toBe(200); + expectMatchInOrder(tsxProcessResolved.stdout, [ + 'READY\n', + `${signal}\n`, + `${signal} HANDLER COMPLETED`, + ]); + } + }, 10_000); + } + }); + + describe('Ctrl + C', ({ test }) => { + test('Exit code', async () => { + const output = await ptyShell( + [ + `${process.execPath} ${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, + stdout => stdout.includes('READY') && '\u0003', + `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + ], + ); + expect(output).toMatch(/EXIT_CODE:\s+130/); }, 10_000); - } - }); - describe('Ctrl + C', ({ test }) => { - test('Exit code', async () => { - const output = await ptyShell( - [ - `${process.execPath} ${tsxPath} ./tests/fixtures/keep-alive.js\r`, - stdout => stdout.includes('READY') && '\u0003', - `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - ], - ); - expect(output).toMatch(/EXIT_CODE:\s+130/); - }, 10_000); - - test('Catchable', async () => { - const output = await ptyShell( - [ - `${process.execPath} ${tsxPath} ./tests/fixtures/catch-signals.js\r`, - stdout => stdout.includes('READY') && '\u0003', - `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - ], - ); - - expect(output).toMatch( - process.platform === 'win32' - ? 'READY\r\nSIGINT\r\nSIGINT HANDLER COMPLETED\r\n' - : 'READY\r\n^CSIGINT\r\nSIGINT HANDLER COMPLETED\r\n', - ); - expect(output).toMatch(/EXIT_CODE:\s+200/); - }, 10_000); + test('Catchable', async () => { + const output = await ptyShell( + [ + `${process.execPath} ${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, + stdout => stdout.includes('READY') && '\u0003', + `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + ], + ); + + expectMatchInOrder(output, [ + 'READY\r\n', + process.platform === 'win32' ? '' : '^C', + 'SIGINT\r\n', + 'SIGINT HANDLER COMPLETED\r\n', + /EXIT_CODE:\s+200/, + ]); + }, 10_000); + }); }); }); }); diff --git a/tests/specs/javascript/cjs.ts b/tests/specs/javascript/cjs.ts deleted file mode 100644 index 136d4c449..000000000 --- a/tests/specs/javascript/cjs.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Load CJS', ({ describe }) => { - describe('.cjs extension', ({ describe }) => { - function assertResults({ stdout, stderr }: ExecaReturnValue) { - expect(stdout).toMatch('loaded cjs-ext-cjs/index.cjs'); - expect(stdout).toMatch('✔ has CJS context'); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/cjs-ext-cjs/index.cjs'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('TypeScript Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('1234'); - }); - }); - - describe('extensionless - should not work', ({ test }) => { - const importPath = './lib/cjs-ext-cjs/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/cjs-ext-cjs'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - }); - - describe('.js extension', ({ describe }) => { - function assertCjsResults({ stdout, stderr }: ExecaReturnValue) { - expect(stdout).toMatch('loaded cjs-ext-js/index.js'); - expect(stdout).toMatch('✔ has CJS context'); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - - expect(stderr).not.toMatch(/loader/i); - } - - function assertEsmResults({ stdout, stderr }: ExecaReturnValue) { - expect(stdout).toMatch('loaded cjs-ext-js/index.js'); - expect(stdout).toMatch('✖ has CJS context'); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stderr).toBe(''); - } - - describe('full path', ({ test }) => { - const importPath = './lib/cjs-ext-js/index.js'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('1234'); - }); - }); - - describe('extensionless', ({ test }) => { - const importPath = './lib/cjs-ext-js/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('1234'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/cjs-ext-js'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - - if (node.isCJS) { - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - } else { - assertEsmResults(nodeProcess); - } - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - assertCjsResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('1234'); - }); - }); - }); - }); -}); diff --git a/tests/specs/javascript/dependencies.ts b/tests/specs/javascript/dependencies.ts deleted file mode 100644 index 1fc0e2d7e..000000000 --- a/tests/specs/javascript/dependencies.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { testSuite, expect } from 'manten'; -import type { NodeApis } from '../../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Dependencies', ({ describe }) => { - describe('module dependency', ({ test }) => { - const output = '{"default":"default export","namedExport":"named export"}'; - - test('Import', async () => { - const nodeProcess = await node.import('package-module'); - expect(nodeProcess.stdout).toBe(output); - expect(nodeProcess.stderr).toBe(''); - }); - }); - }); -}); diff --git a/tests/specs/javascript/esm.ts b/tests/specs/javascript/esm.ts deleted file mode 100644 index 4fad2dee2..000000000 --- a/tests/specs/javascript/esm.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Load ESM', ({ describe }) => { - describe('.mjs extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - ) { - expect(stdout).toMatch('loaded esm-ext-mjs/index.mjs'); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/esm-ext-mjs/index.mjs'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('TypeScript Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless - should not work', ({ test }) => { - const importPath = './lib/esm-ext-mjs/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - - describe('directory - should not work', ({ test }) => { - const importPath = './lib/esm-ext-mjs'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - }); - - describe('.js extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - ) { - expect(stdout).toMatch('loaded esm-ext-js/index.js'); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/esm-ext-js/index.js'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless', ({ test }) => { - const importPath = './lib/esm-ext-js/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/esm-ext-js'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - }); - }); -}); diff --git a/tests/specs/javascript/index.ts b/tests/specs/javascript/index.ts deleted file mode 100644 index 36a852168..000000000 --- a/tests/specs/javascript/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { testSuite } from 'manten'; -import type { NodeApis } from '../../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('JavaScript', ({ runTestSuite }) => { - runTestSuite(import('./cjs'), node); - runTestSuite(import('./esm'), node); - runTestSuite(import('./dependencies'), node); - }); -}); diff --git a/tests/specs/json.ts b/tests/specs/json.ts deleted file mode 100644 index ccb2fe3f8..000000000 --- a/tests/specs/json.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { testSuite, expect } from 'manten'; -import { createFixture } from 'fs-fixture'; -import type { NodeApis } from '../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Load JSON', async ({ describe, onFinish }) => { - const fixture = await createFixture({ - 'package.json': JSON.stringify({ - type: node.packageType, - }), - 'index.json': JSON.stringify({ - loaded: 'json', - }), - }); - - onFinish(async () => await fixture.rm()); - - describe('full path', async ({ test }) => { - test('Load', async () => { - const nodeProcess = await node.loadFile(fixture.path, './index.json'); - expect(nodeProcess.stdout).toBe(''); - expect(nodeProcess.exitCode).toBe(0); - }); - - test('Import', async () => { - const nodeProcess = await node.importFile(fixture.path, './index.json'); - expect(nodeProcess.stdout).toMatch('default: { loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - - test('Require', async () => { - const nodeProcess = await node.requireFile(fixture.path, './index.json'); - expect(nodeProcess.stdout).toBe('{ loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - }); - - describe('extensionless', ({ test }) => { - test('Load', async () => { - const nodeProcess = await node.loadFile(fixture.path, './index'); - expect(nodeProcess.stdout).toBe(''); - expect(nodeProcess.exitCode).toBe(0); - }); - - test('Import', async () => { - const nodeProcess = await node.importFile(fixture.path, './index'); - expect(nodeProcess.stdout).toMatch('default: { loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - - test('Require', async () => { - const nodeProcess = await node.requireFile(fixture.path, './index'); - expect(nodeProcess.stdout).toBe('{ loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - }); - - describe('directory', ({ test }) => { - test('Load', async () => { - const nodeProcess = await node.loadFile(fixture.path, '.'); - expect(nodeProcess.stdout).toBe(''); - expect(nodeProcess.exitCode).toBe(0); - }); - - test('Import', async () => { - const nodeProcess = await node.importFile(fixture.path, '.'); - expect(nodeProcess.stdout).toMatch('default: { loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - - test('Require', async () => { - const nodeProcess = await node.requireFile(fixture.path, '.'); - expect(nodeProcess.stdout).toBe('{ loaded: \'json\' }'); - expect(nodeProcess.stderr).toBe(''); - }); - }); - }); -}); diff --git a/tests/specs/repl.ts b/tests/specs/repl.ts index 838e661e8..18ba6c1fb 100644 --- a/tests/specs/repl.ts +++ b/tests/specs/repl.ts @@ -1,109 +1,111 @@ import { testSuite } from 'manten'; -import { type NodeApis } from '../utils/tsx'; +import { tsx } from '../utils/tsx'; +import { processInteract } from '../utils/process-interact'; -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('repl', ({ test }) => { +export default testSuite(async ({ describe }) => { + describe('REPL', ({ test }) => { test('handles ts', async () => { - const tsxProcess = node.tsx({ + const tsxProcess = tsx({ args: ['--interactive'], }); - const commands = [ - 'const message: string = "SUCCESS"', - 'message', - ]; - - await new Promise((resolve) => { - tsxProcess.stdout!.on('data', (data: Buffer) => { - const chunkString = data.toString(); - - if (chunkString.includes('SUCCESS')) { - return resolve(); - } - - if (chunkString.includes('> ') && commands.length > 0) { - const command = commands.shift(); - tsxProcess.stdin!.write(`${command}\r`); - } - }); - }); + await processInteract( + tsxProcess.stdout!, + [ + (data) => { + if (data.includes('> ')) { + tsxProcess.stdin!.write('const message: string = "SUCCESS"\r'); + return true; + } + }, + (data) => { + if (data.includes('> ')) { + tsxProcess.stdin!.write('message\r'); + return true; + } + }, + data => data.includes('SUCCESS'), + ], + 5000, + ); tsxProcess.kill(); }, 40_000); test('doesn\'t error on require', async () => { - const tsxProcess = node.tsx({ + const tsxProcess = tsx({ args: ['--interactive'], }); - await new Promise((resolve, reject) => { - tsxProcess.stdout!.on('data', (data: Buffer) => { - const chunkString = data.toString(); - - if (chunkString.includes('unsupported-require-call')) { - return reject(chunkString); - } - - if (chunkString.includes('[Function: resolve]')) { - return resolve(); - } - - if (chunkString.includes('> ')) { - tsxProcess.stdin!.write('require("path")\r'); - } - }); - }); + await processInteract( + tsxProcess.stdout!, + [ + (data) => { + if (data.includes('> ')) { + tsxProcess.stdin!.write('require("path")\r'); + return true; + } + }, + data => data.includes('[Function: resolve]'), + ], + 5000, + ); tsxProcess.kill(); }, 40_000); test('supports incomplete expression in segments', async () => { - const tsxProcess = node.tsx({ + const tsxProcess = tsx({ args: ['--interactive'], }); - const commands = [ - ['> ', '('], - ['... ', '1'], - ['... ', ')'], - ['1'], - ]; - - let [expected, nextCommand] = commands.shift()!; - await new Promise((resolve) => { - tsxProcess.stdout!.on('data', (data: Buffer) => { - const chunkString = data.toString(); - if (chunkString.includes(expected)) { - if (nextCommand) { - tsxProcess.stdin!.write(`${nextCommand}\r`); - [expected, nextCommand] = commands.shift()!; - } else { - resolve(); + await processInteract( + tsxProcess.stdout!, + [ + (data) => { + if (data.includes('> ')) { + tsxProcess.stdin!.write('(\r'); + return true; } - } - }); - }); + }, + (data) => { + if (data.includes('... ')) { + tsxProcess.stdin!.write('1\r'); + return true; + } + }, + (data) => { + if (data.includes('... ')) { + tsxProcess.stdin!.write(')\r'); + return true; + } + }, + data => data.includes('1'), + ], + 5000, + ); + tsxProcess.kill(); }, 40_000); test('errors on import statement', async () => { - const tsxProcess = node.tsx({ + const tsxProcess = tsx({ args: ['--interactive'], }); - await new Promise((resolve) => { - tsxProcess.stdout!.on('data', (data: Buffer) => { - const chunkString = data.toString(); - - if (chunkString.includes('SyntaxError: Cannot use import statement')) { - return resolve(); - } - - if (chunkString.includes('> ')) { - tsxProcess.stdin!.write('import fs from "fs"\r'); - } - }); - }); + await processInteract( + tsxProcess.stdout!, + [ + (data) => { + if (data.includes('> ')) { + tsxProcess.stdin!.write('import fs from "fs"\r'); + return true; + } + }, + data => data.includes('SyntaxError: Cannot use import statement'), + ], + 5000, + ); tsxProcess.kill(); }, 40_000); diff --git a/tests/specs/smoke.ts b/tests/specs/smoke.ts new file mode 100644 index 000000000..f4dba0a6b --- /dev/null +++ b/tests/specs/smoke.ts @@ -0,0 +1,646 @@ +import path from 'path'; +import { pathToFileURL } from 'url'; +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import type { NodeApis } from '../utils/tsx'; + +const cjsContextCheck = 'typeof module !== \'undefined\''; +const tsCheck = '1 as number'; + +const declareReact = ` +const React = { + createElement: (...args) => Array.from(args), +}; +`; +const jsxCheck = '<>
JSX
'; + +const preserveName = ` +assert( + (function functionName() {}).name === 'functionName', + 'Name should be preserved' +); +`; + +const wasmPath = path.resolve('tests/fixtures/test.wasm'); +const wasmPathUrl = pathToFileURL(wasmPath).toString(); + +const syntaxLowering = ` +// es2016 - Exponentiation operator +10 ** 4; + +// es2017 - Async functions +(async () => {}); + +// es2018 - Spread properties +({...Object}); + +// es2018 - Rest properties +const {...x} = Object; + +// es2019 - Optional catch binding +try {} catch {} + +// es2020 - Optional chaining +Object?.keys; + +// es2020 - Nullish coalescing +Object ?? true + +// es2020 - import.meta +// import.meta + +// es2021 - Logical assignment operators +// let a = false; a ??= true; a ||= true; a &&= true; + +// es2022 - Class instance fields +(class { x }); + +// es2022 - Static class fields +(class { static x }); + +// es2022 - Private instance methods +(class { #x() {} }); + +// es2022 - Private instance fields +(class { #x }); + +// es2022 - Private static methods +(class { static #x() {} }); + +// es2022 - Private static fields +(class { static #x }); + +// es2022 - Class static blocks +(class { static {} }); + +export const named = 2; +export default 1; +`; + +const sourcemap = { + test: 'const { stack } = new Error(); assert(stack.includes(\':SOURCEMAP_LINE\'), \'Expected SOURCEMAP_LINE in stack:\' + stack)', + tag: ( + strings: TemplateStringsArray, + ...values: string[] + ) => { + const finalString = String.raw({ raw: strings }, ...values); + const lineNumber = finalString.split('\n').findIndex(line => line.includes('SOURCEMAP_LINE')) + 1; + return finalString.replaceAll('SOURCEMAP_LINE', lineNumber.toString()); + }, +}; + +const files = { + 'js/index.js': ` + import assert from 'assert'; + ${syntaxLowering} + ${preserveName} + export const cjsContext = ${cjsContextCheck}; + `, + + 'json/index.json': JSON.stringify({ loaded: 'json' }), + + 'cjs/index.cjs': sourcemap.tag` + const assert = require('node:assert'); + assert(${cjsContextCheck}, 'Should have CJS context'); + ${preserveName} + ${sourcemap.test} + exports.named = 'named'; + `, + + 'mjs/index.mjs': ` + export const mjsHasCjsContext = ${cjsContextCheck}; + `, + + 'ts/index.ts': sourcemap.tag` + import assert from 'assert'; + import type {Type} from 'resolved-by-tsc' + + interface Foo {} + + type Foo = number + + declare module 'foo' {} + + enum BasicEnum { + Left, + Right, + } + + enum NamedEnum { + SomeEnum = 'some-value', + } + + export const a = BasicEnum.Left; + + export const b = NamedEnum.SomeEnum; + + export default function foo(): string { + return 'foo' + } + + // For "ts as tsx" test + const bar = (value: T) => fn(); + + ${preserveName} + ${sourcemap.test} + export const cjsContext = ${cjsContextCheck}; + ${tsCheck}; + `, + + // TODO: test resolution priority for files 'index.tsx` & 'index.tsx.ts` via 'index.tsx' + + 'jsx/index.jsx': sourcemap.tag` + import assert from 'assert'; + export const cjsContext = ${cjsContextCheck}; + ${declareReact} + export const jsx = ${jsxCheck}; + ${preserveName} + ${sourcemap.test} + `, + + 'tsx/index.tsx': sourcemap.tag` + import assert from 'assert'; + export const cjsContext = ${cjsContextCheck}; + ${tsCheck}; + ${declareReact} + export const jsx = ${jsxCheck}; + ${preserveName} + ${sourcemap.test} + `, + + 'mts/index.mts': sourcemap.tag` + import assert from 'assert'; + export const mjsHasCjsContext = ${cjsContextCheck}; + ${tsCheck}; + ${preserveName} + ${sourcemap.test} + `, + + 'cts/index.cts': sourcemap.tag` + const assert = require('assert'); + assert(${cjsContextCheck}, 'Should have CJS context'); + ${tsCheck}; + ${preserveName} + ${sourcemap.test} + `, + + 'expect-errors.js': ` + export const expectErrors = async (...assertions) => { + let errors = await Promise.all( + assertions.map(async ([fn, expectedError]) => { + let thrown; + try { + await fn(); + } catch (error) { + thrown = error; + } + + if (!thrown) { + return new Error('No error thrown'); + } else if (!thrown.message.includes(expectedError)) { + return new Error(\`Message \${JSON.stringify(expectedError)} not found in \${JSON.stringify(thrown.message)}\`); + } + }), + ); + + errors = errors.filter(Boolean); + + if (errors.length > 0) { + console.error(errors); + process.exitCode = 1; + } + }; + `, + + 'file.txt': 'hello', + + node_modules: { + 'pkg-commonjs': { + 'package.json': JSON.stringify({ + type: 'commonjs', + }), + 'index.js': syntaxLowering, + }, + 'pkg-module': { + 'package.json': JSON.stringify({ + type: 'module', + exports: './index.js', + }), + 'index.js': syntaxLowering, + }, + }, + + tsconfig: { + 'file.ts': '', + + 'jsx.jsx': ` + // tsconfig not applied to jsx because allowJs is not set + import { expectErrors } from '../expect-errors'; + expectErrors( + [() => ${jsxCheck}, 'React is not defined'], + + // These should throw unless allowJs is set + // [() => import('prefix/file'), "Cannot find package 'prefix'"], + // [() => import('paths-exact-match'), "Cannot find package 'paths-exact-match'"], + // [() => import('file'), "Cannot find package 'file'"], + ); + `, + + 'node_modules/tsconfig-should-not-apply': { + 'package.json': JSON.stringify({ + exports: { + import: './index.mjs', + default: './index.cjs', + }, + }), + 'index.mjs': ` + import { expectErrors } from '../../../expect-errors'; + expectErrors( + [() => import('prefix/file'), "Cannot find package 'prefix'"], + [() => import('paths-exact-match'), "Cannot find package 'paths-exact-match'"], + [() => import('file'), "Cannot find package 'file'"], + ); + `, + 'index.cjs': ` + const { expectErrors } = require('../../../expect-errors'); + expectErrors( + [() => require('prefix/file'), "Cannot find module"], + [() => require('paths-exact-match'), "Cannot find module"], + [() => require('file'), "Cannot find module"], + ); + `, + }, + + 'index.tsx': ` + ${jsxCheck}; + + import './jsx'; + + // Resolves relative to baseUrl + import 'file'; + + // Resolves paths - exact match + import 'paths-exact-match'; + + // Resolves paths - prefix match + import 'prefix/file'; + + // Resolves paths - suffix match + import 'file/suffix'; + + // tsconfig should not apply to dependency + import "tsconfig-should-not-apply"; + `, + + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + jsxFactory: 'Array', + jsxFragmentFactory: 'null', + baseUrl: '.', + paths: { + 'paths-exact-match': ['file'], + 'prefix/*': ['*'], + '*/suffix': ['*'], + }, + }, + }), + + 'tsconfig-allowJs.json': JSON.stringify({ + extends: './tsconfig.json', + compilerOptions: { + allowJs: true, + }, + }), + }, +}; + +const packageTypes = [ + 'module', + 'commonjs', +] as const; + +export default testSuite(async ({ describe }, { tsx }: NodeApis) => { + describe('Smoke', ({ describe }) => { + for (const packageType of packageTypes) { + const isCommonJs = packageType === 'commonjs'; + + describe(packageType, ({ test, describe }) => { + test('from .js', async ({ onTestFinish, onTestFail }) => { + const fixture = await createFixture({ + ...files, + 'package.json': JSON.stringify({ type: packageType }), + 'import-from-js.js': ` + import assert from 'assert'; + import { expectErrors } from './expect-errors'; + + // node: prefix + import 'node:fs'; + + import * as pkgCommonjs from 'pkg-commonjs'; + import * as pkgModule from 'pkg-module'; + + // .js + import * as js from './js/index.js'; + import './js/index.js?query=123'; + import './js/index'; + import './js/'; + + // No double .default.default in Dynamic Import + import('./js/index.js').then(m => { + if (typeof m.default === 'object') { + assert( + !('default' in m.default), + 'Should not have double .default.default in Dynamic Import', + ); + } + }); + + // .json + import * as json from './json/index.json'; + import './json/index'; + import './json/'; + + // .cjs + import * as cjs from './cjs/index.cjs'; + expectErrors( + [() => import('./cjs/index'), 'Cannot find module'], + [() => import('./cjs/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./cjs/index'), 'Cannot find module'], + [() => require('./cjs/'), 'Cannot find module'], + ` + : '' + } + ); + + // .mjs + import * as mjs from './mjs/index.mjs'; + expectErrors( + [() => import('./mjs/index'), 'Cannot find module'], + [() => import('./mjs/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./mjs/index'), 'Cannot find module'], + [() => require('./mjs/'), 'Cannot find module'], + ` + : '' + } + ); + + // Is TS loadable here? + // Import jsx? + + // Unsupported files + expectErrors( + [() => import('./file.txt'), 'Unknown file extension'], + [() => import(${JSON.stringify(wasmPathUrl)}), 'Unknown file extension'], + ${ + isCommonJs + ? ` + [() => require('./file.txt'), 'hello is not defined'], + [() => require(${JSON.stringify(wasmPath)}), 'Invalid or unexpected token'], + ` + : '' + } + ); + + console.log(JSON.stringify({ + js, + json, + cjs, + mjs, + pkgCommonjs, + pkgModule, + })); + + // Could .js import TS files? + `, + }); + onTestFinish(async () => await fixture.rm()); + + const p = await tsx(['import-from-js.js'], fixture.path); + onTestFail((error) => { + console.error(error); + console.log(p); + }); + expect(p.failed).toBe(false); + expect(p.stdout).toMatch(`"js":{"cjsContext":${isCommonJs},"default":1,"named":2}`); + expect(p.stdout).toMatch('"json":{"default":{"loaded":"json"},"loaded":"json"}'); + expect(p.stdout).toMatch('"cjs":{"default":{"named":"named"},"named":"named"}'); + expect(p.stdout).toMatch('"pkgModule":{"default":1,"named":2}'); + if (isCommonJs) { + expect(p.stdout).toMatch('"pkgCommonjs":{"default":1,"named":2}'); + } else { + expect(p.stdout).toMatch('"pkgCommonjs":{"default":{"default":1,"named":2}}'); + } + + // By "require()"ing an ESM file, it forces it to be compiled in a CJS context + expect(p.stdout).toMatch(`"mjs":{"mjsHasCjsContext":${isCommonJs}}`); + + expect(p.stderr).toBe(''); + }); + + describe('from .ts', async ({ test, onFinish }) => { + const fixture = await createFixture({ + ...files, + 'package.json': JSON.stringify({ type: packageType }), + + 'import-from-ts.ts': ` + import assert from 'assert'; + import { expectErrors } from './expect-errors'; + + // node: prefix + import 'node:fs'; + + // Dependencies + import * as pkgCommonjs from 'pkg-commonjs'; + import * as pkgModule from 'pkg-module'; + + // TODO: Test resolving TS files in dependencies (e.g. implicit extensions & export maps) + + // .js + import * as js from './js/index.js'; + import './js/index.js?query=123'; + import './js/index'; + import './js/'; + + // No double .default.default in Dynamic Import + import('./js/index.js').then(m => { + if (typeof m.default === 'object') { + assert( + !('default' in m.default), + 'Should not have double .default.default in Dynamic Import', + ); + } + }); + + // .json + import * as json from './json/index.json'; + import './json/index'; + import './json/'; + + // .cjs + import * as cjs from './cjs/index.cjs'; + expectErrors( + [() => import('./cjs/index'), 'Cannot find module'], + [() => import('./cjs/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./cjs/index'), 'Cannot find module'], + [() => require('./cjs/'), 'Cannot find module'], + ` + : '' + } + ); + + // .mjs + import * as mjs from './mjs/index.mjs'; + expectErrors( + [() => import('./mjs/index'), 'Cannot find module'], + [() => import('./mjs/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./mjs/index'), 'Cannot find module'], + [() => require('./mjs/'), 'Cannot find module'], + ` + : '' + } + ); + + // .ts + import './ts/index.ts'; + import './ts/index.js'; + import './ts/index.jsx'; + import './ts/index'; + import './ts/'; + + // .jsx + import * as jsx from './jsx/index.jsx'; + import './jsx/index.js'; + import './jsx/index'; + import './jsx/'; + + // .tsx + import './tsx/index.tsx'; + import './tsx/index.js'; + import './tsx/index.jsx'; + import './tsx/index'; + import './tsx/'; + + // .cts + import './cts/index.cjs'; + expectErrors( + // TODO: + // [() => import('./cts/index.cts'), 'Cannot find module'], + [() => import('./cts/index'), 'Cannot find module'], + [() => import('./cts/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./cts/index'), 'Cannot find module'], + [() => require('./cts/'), 'Cannot find module'], + ` + : '' + } + ); + // Loading via Node arg should not work via .cjs but with .cts + + // .mts + import './mts/index.mjs'; + expectErrors( + // TODO: + // [() => import('./mts/index.mts'), 'Cannot find module'], + [() => import('./mts/index'), 'Cannot find module'], + [() => import('./mts/'), 'Cannot find module'], + ${ + isCommonJs + ? ` + [() => require('./mts/index'), 'Cannot find module'], + [() => require('./mts/'), 'Cannot find module'], + ` + : '' + } + ); + // Loading via Node arg should not work via .mjs but with .mts + + // Unsupported files + expectErrors( + [() => import('./file.txt'), 'Unknown file extension'], + [() => import(${JSON.stringify(wasmPathUrl)}), 'Unknown file extension'], + ${ + isCommonJs + ? ` + [() => require('./file.txt'), 'hello is not defined'], + [() => require(${JSON.stringify(wasmPath)}), 'Invalid or unexpected token'], + ` + : '' + } + ); + + console.log(JSON.stringify({ + js, + json, + jsx, + cjs, + mjs, + pkgCommonjs, + pkgModule, + })); + `, + }); + onFinish(async () => await fixture.rm()); + + test('import all', async ({ onTestFail }) => { + const p = await tsx(['import-from-ts.ts'], fixture.path); + onTestFail((error) => { + console.error(error); + console.log(p); + }); + expect(p.failed).toBe(false); + expect(p.stdout).toMatch(`"js":{"cjsContext":${isCommonJs},"default":1,"named":2}`); + expect(p.stdout).toMatch('"json":{"default":{"loaded":"json"},"loaded":"json"}'); + expect(p.stdout).toMatch('"cjs":{"default":{"named":"named"},"named":"named"}'); + expect(p.stdout).toMatch(`"jsx":{"cjsContext":${isCommonJs},"jsx":[null,null,["div",null,"JSX"]]}`); + expect(p.stdout).toMatch('"pkgModule":{"default":1,"named":2}'); + if (isCommonJs) { + expect(p.stdout).toMatch('"pkgCommonjs":{"default":1,"named":2}'); + } else { + expect(p.stdout).toMatch('"pkgCommonjs":{"default":{"default":1,"named":2}}'); + } + + // By "require()"ing an ESM file, it forces it to be compiled in a CJS context + expect(p.stdout).toMatch(`"mjs":{"mjsHasCjsContext":${isCommonJs}}`); + expect(p.stderr).toBe(''); + }); + + test('tsconfig', async ({ onTestFail }) => { + const pTsconfig = await tsx(['index.tsx'], path.join(fixture.path, 'tsconfig')); + onTestFail((error) => { + console.error(error); + console.log(pTsconfig); + }); + expect(pTsconfig.failed).toBe(false); + expect(pTsconfig.stderr).toBe(''); + expect(pTsconfig.stdout).toBe(''); + }); + + test('custom tsconfig', async ({ onTestFail }) => { + const pTsconfigAllowJs = await tsx(['--tsconfig', 'tsconfig-allowJs.json', 'jsx.jsx'], path.join(fixture.path, 'tsconfig')); + onTestFail((error) => { + console.error(error); + console.log(pTsconfigAllowJs); + }); + expect(pTsconfigAllowJs.failed).toBe(true); + expect(pTsconfigAllowJs.stderr).toMatch('Error: No error thrown'); + expect(pTsconfigAllowJs.stdout).toBe(''); + }); + }); + }); + } + }); +}); diff --git a/tests/specs/typescript/cts.ts b/tests/specs/typescript/cts.ts deleted file mode 100644 index e68dc3ad2..000000000 --- a/tests/specs/typescript/cts.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('.cts extension', ({ describe }) => { - function assertResults({ stdout, stderr }: ExecaReturnValue) { - expect(stdout).toMatch('loaded ts-ext-cts/index.cts'); - expect(stdout).toMatch('✔ has CJS context'); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/ts-ext-cts/index.cts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('full path via .cjs', ({ test }) => { - const importPath = './lib/ts-ext-cts/index.cjs'; - - test('Load - should not work', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath, { typescript: true }); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless - should not work', ({ test }) => { - const importPath = './lib/ts-ext-cts/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - - describe('directory - should not work', ({ test }) => { - const importPath = './lib/ts-ext-cts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - }); -}); diff --git a/tests/specs/typescript/dependencies.ts b/tests/specs/typescript/dependencies.ts deleted file mode 100644 index 987eea347..000000000 --- a/tests/specs/typescript/dependencies.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { testSuite, expect } from 'manten'; -import type { NodeApis } from '../../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Dependencies', ({ describe }) => { - describe('TypeScript dependency', ({ test }) => { - const output = '{"default":"ts default export","namedExport":"ts named export"}'; - - test('Import', async () => { - const nodeProcess = await node.import('package-module/ts.ts'); - expect(nodeProcess.stdout).toBe(output); - expect(nodeProcess.stderr).toBe(''); - }); - - test('Import extensionless', async () => { - const nodeProcess = await node.import('package-module/ts'); - expect(nodeProcess.stdout).toBe(output); - expect(nodeProcess.stderr).toBe(''); - }); - - test('Import', async () => { - const nodeProcess = await node.import('package-typescript-export'); - expect(nodeProcess.stdout).toBe(output); - expect(nodeProcess.stderr).toBe(''); - }); - }); - - describe('Export map', ({ test }) => { - const output = '{"default":"default export","namedExport":"named export"}'; - - test('Import', async () => { - const nodeProcess = await node.import('package-exports/index.js', { - typescript: true, - }); - expect(nodeProcess.stdout).toBe(output); - }); - }); - }); -}); diff --git a/tests/specs/typescript/index.ts b/tests/specs/typescript/index.ts deleted file mode 100644 index 2b947c276..000000000 --- a/tests/specs/typescript/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { testSuite } from 'manten'; -import type { NodeApis } from '../../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('TypeScript', async ({ runTestSuite }) => { - runTestSuite(import('./ts'), node); - runTestSuite(import('./tsx'), node); - runTestSuite(import('./jsx'), node); - runTestSuite(import('./mts'), node); - runTestSuite(import('./cts'), node); - runTestSuite(import('./tsconfig'), node); - runTestSuite(import('./dependencies'), node); - }); -}); diff --git a/tests/specs/typescript/jsx.ts b/tests/specs/typescript/jsx.ts deleted file mode 100644 index 58215b009..000000000 --- a/tests/specs/typescript/jsx.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('.jsx extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - ) { - expect(stdout).toMatch('loaded ts-ext-jsx/index.jsx'); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/ts-ext-jsx/index.jsx'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - - describe('extensionless', ({ test }) => { - const importPath = './lib/ts-ext-jsx/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/ts-ext-jsx'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - }); -}); diff --git a/tests/specs/typescript/mts.ts b/tests/specs/typescript/mts.ts deleted file mode 100644 index 6d1ad432e..000000000 --- a/tests/specs/typescript/mts.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('.mts extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - ) { - expect(stdout).toMatch('loaded ts-ext-mts/index.mts'); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/ts-ext-mts/index.mts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('full path via .mjs', ({ test }) => { - const importPath = './lib/ts-ext-mts/index.mjs'; - - test('Load - should not work', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath, { typescript: true }); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless - should not work', ({ test }) => { - const importPath = './lib/ts-ext-mts/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - - describe('directory - should not work', ({ test }) => { - const importPath = './lib/ts-ext-mts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - }); - }); -}); diff --git a/tests/specs/typescript/ts.ts b/tests/specs/typescript/ts.ts deleted file mode 100644 index d9cbd37a5..000000000 --- a/tests/specs/typescript/ts.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('.ts extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - loadedMessage = 'loaded ts-ext-ts/index.ts', - ) { - expect(stdout).toMatch(loadedMessage); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/ts-ext-ts/index.ts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require flag', async () => { - const nodeProcess = await node.requireFlag(importPath); - assertResults(nodeProcess, true); - }); - }); - - describe('full path via .js', ({ test }) => { - const importPath = './lib/ts-ext-ts/index.js'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - expect(nodeProcess.stderr).toMatch('Cannot find module'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath, { typescript: true }); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless', ({ test }) => { - const importPath = './lib/ts-ext-ts/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('extensionless with subextension', ({ test }) => { - const importPath = './lib/ts-ext-ts/index.tsx'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS, 'loaded ts-ext-ts/index.tsx.ts'); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS, 'loaded ts-ext-ts/index.tsx.ts'); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true, 'loaded ts-ext-ts/index.tsx.ts'); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/ts-ext-ts'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); - }); - }); - }); -}); diff --git a/tests/specs/typescript/tsconfig.ts b/tests/specs/typescript/tsconfig.ts deleted file mode 100644 index 9b9d70835..000000000 --- a/tests/specs/typescript/tsconfig.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { testSuite, expect } from 'manten'; -import type { NodeApis } from '../../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('tsconfig', ({ test, describe }) => { - test('jsxFactory & jsxFragmentFactory', async () => { - const nodeProcess = await node.load('./src/tsx.tsx', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('div null hello world\nnull null goodbye world'); - }); - - test('Custom tsconfig.json path', async () => { - const nodeProcess = await node.load('./src/tsx.tsx', { - cwd: './tsconfig', - args: ['--tsconfig', './tsconfig-custom/tsconfig.custom-name.json'], - }); - expect(nodeProcess.stdout).toBe(''); - expect(nodeProcess.stderr).toBe('div null hello world\nnull null goodbye world'); - }); - - describe('paths', ({ test, describe }) => { - test('resolves baseUrl', async () => { - const nodeProcess = await node.load('./src/base-url.ts', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('resolve-target'); - }); - - test('resolves paths exact match', async () => { - const nodeProcess = await node.load('./src/paths-exact-match.ts', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('resolve-target'); - }); - - test('resolves paths prefix', async () => { - const nodeProcess = await node.load('./src/paths-prefix-match.ts', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('nested-resolve-target'); - }); - - test('resolves paths suffix', async () => { - const nodeProcess = await node.load('./src/paths-suffix-match.ts', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('nested-resolve-target'); - }); - - describe('dependency', ({ test }) => { - test('resolve current directory', async () => { - const nodeProcess = await node.load('./dependency-resolve-current-directory', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('resolved'); - }); - - test('should not resolve baseUrl', async () => { - const nodeProcess = await node.load('./dependency-should-not-resolve-baseUrl', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('resolved'); - }); - - test('should not resolve paths', async () => { - const nodeProcess = await node.load('./dependency-should-not-resolve-paths', { - cwd: './tsconfig', - }); - expect(nodeProcess.stdout).toBe('resolved'); - }); - }); - }); - }); -}); diff --git a/tests/specs/typescript/tsx.ts b/tests/specs/typescript/tsx.ts deleted file mode 100644 index 2f7a847c7..000000000 --- a/tests/specs/typescript/tsx.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { testSuite, expect } from 'manten'; -import semver from 'semver'; -import type { ExecaReturnValue } from 'execa'; -import type { NodeApis } from '../../utils/tsx'; -import nodeSupports from '../../utils/node-supports'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('.tsx extension', ({ describe }) => { - function assertResults( - { stdout, stderr }: ExecaReturnValue, - cjsContext = false, - ) { - expect(stdout).toMatch('loaded ts-ext-tsx/index.tsx'); - expect(stdout).toMatch( - cjsContext - ? '✔ has CJS context' - : '✖ has CJS context', - ); - expect(stdout).toMatch('✔ name in error'); - expect(stdout).toMatch('✔ sourcemaps'); - expect(stdout).toMatch('✔ has dynamic import'); - expect(stdout).toMatch('✔ resolves optional node prefix'); - expect(stdout).toMatch( - semver.satisfies(node.version, nodeSupports.testRunner) - ? '✔ resolves required node prefix' - : '✖ resolves required node prefix: Error', - ); - expect(stderr).not.toMatch(/loader/i); - } - - describe('full path', ({ test }) => { - const importPath = './lib/ts-ext-tsx/index.tsx'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - - describe('extensionless', ({ test }) => { - const importPath = './lib/ts-ext-tsx/index'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - - describe('directory', ({ test }) => { - const importPath = './lib/ts-ext-tsx'; - - test('Load', async () => { - const nodeProcess = await node.load(importPath); - assertResults(nodeProcess, node.isCJS); - }); - - test('Import', async () => { - const nodeProcess = await node.import(importPath); - assertResults(nodeProcess, node.isCJS); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - - test('Require', async () => { - const nodeProcess = await node.require(importPath); - - // By "require()"ing an ESM file, it forces it to be compiled in a CJS context - assertResults(nodeProcess, true); - expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); - }); - }); - }); -}); diff --git a/tests/specs/wasm.ts b/tests/specs/wasm.ts deleted file mode 100644 index 823e07211..000000000 --- a/tests/specs/wasm.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { testSuite, expect } from 'manten'; -import type { NodeApis } from '../utils/tsx'; - -export default testSuite(async ({ describe }, node: NodeApis) => { - describe('Load WASM', ({ test }) => { - const importPath = './lib/wasm/index.js'; - - test('Unsupported extension error', async () => { - const nodeProcess = await node.load(importPath); - - expect(nodeProcess.exitCode).toBe(1); - - if (node.isCJS) { - expect(nodeProcess.stderr).toMatch('Invalid or unexpected token'); - } else { - expect(nodeProcess.stderr).toMatch('ERR_UNKNOWN_FILE_EXTENSION'); - } - }); - - test('Loads with experimental flag', async () => { - const nodeProcess = await node.load(importPath, { - args: ['--experimental-wasm-modules'], - }); - - if (node.isCJS) { - expect(nodeProcess.exitCode).toBe(1); - expect(nodeProcess.stderr).toMatch('Invalid or unexpected token'); - } else { - expect(nodeProcess.exitCode).toBe(0); - expect(nodeProcess.stdout).toBe('1234'); - } - }); - }); -}); diff --git a/tests/specs/watch.ts b/tests/specs/watch.ts index 3ce64dffa..11996e0f9 100644 --- a/tests/specs/watch.ts +++ b/tests/specs/watch.ts @@ -1,34 +1,18 @@ -import { type Readable } from 'node:stream'; import path from 'path'; import { setTimeout } from 'timers/promises'; -import { on } from 'events'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; import { tsx } from '../utils/tsx'; +import { processInteract } from '../utils/process-interact'; + +export default testSuite(async ({ describe }) => { + describe('watch', async ({ test, describe, onFinish }) => { + const fixture = await createFixture({ + // Unnecessary TS to test syntax + 'log-argv.ts': 'console.log(JSON.stringify(process.argv) as string)', + }); + onFinish(async () => await fixture.rm()); -type MaybePromise = T | Promise; -const interact = async ( - stdout: Readable, - actions: ((data: string) => MaybePromise)[], -) => { - let currentAction = actions.shift(); - - const buffers: Buffer[] = []; - while (currentAction) { - for await (const [chunk] of on(stdout, 'data')) { - buffers.push(chunk); - if (await currentAction(chunk.toString())) { - currentAction = actions.shift(); - break; - } - } - } - - return Buffer.concat(buffers).toString(); -}; - -export default testSuite(async ({ describe }, fixturePath: string) => { - describe('watch', ({ test, describe }) => { test('require file path', async () => { const tsxProcess = await tsx({ args: ['watch'], @@ -39,7 +23,7 @@ export default testSuite(async ({ describe }, fixturePath: string) => { test('watch files for changes', async ({ onTestFinish }) => { let initialValue = Date.now(); - const fixture = await createFixture({ + const fixtureWatch = await createFixture({ 'package.json': JSON.stringify({ type: 'module', }), @@ -49,27 +33,29 @@ export default testSuite(async ({ describe }, fixturePath: string) => { `, 'value.js': `export const value = ${initialValue};`, }); - onTestFinish(async () => await fixture.rm()); + onTestFinish(async () => await fixtureWatch.rm()); const tsxProcess = tsx({ args: [ 'watch', - path.join(fixture.path, 'index.js'), + 'index.js', ], + cwd: fixtureWatch.path, }); - await interact( + await processInteract( tsxProcess.stdout!, [ async (data) => { if (data.includes(`${initialValue}\n`)) { initialValue = Date.now(); - await fixture.writeFile('value.js', `export const value = ${initialValue};`); + await fixtureWatch.writeFile('value.js', `export const value = ${initialValue};`); return true; } }, data => data.includes(`${initialValue}\n`), ], + 5000, ); tsxProcess.kill(); @@ -81,11 +67,12 @@ export default testSuite(async ({ describe }, fixturePath: string) => { const tsxProcess = tsx({ args: [ 'watch', - path.join(fixturePath, 'log-argv.ts'), + 'log-argv.ts', ], + cwd: fixture.path, }); - await interact( + await processInteract( tsxProcess.stdout!, [ (data) => { @@ -96,6 +83,7 @@ export default testSuite(async ({ describe }, fixturePath: string) => { }, data => data.includes('log-argv.ts'), ], + 5000, ); tsxProcess.kill(); @@ -109,14 +97,16 @@ export default testSuite(async ({ describe }, fixturePath: string) => { const tsxProcess = tsx({ args: [ 'watch', - path.join(fixturePath, 'log-argv.ts'), + 'log-argv.ts', '--some-flag', ], + cwd: fixture.path, }); - await interact( + await processInteract( tsxProcess.stdout!, [data => data.startsWith('["')], + 5000, ); tsxProcess.kill(); @@ -125,8 +115,8 @@ export default testSuite(async ({ describe }, fixturePath: string) => { expect(all).toMatch('"--some-flag"'); }, 10_000); - test('wait for exit', async ({ onTestFinish }) => { - const fixture = await createFixture({ + test('wait for exit', async ({ onTestFinish, onTestFail }) => { + const fixtureExit = await createFixture({ 'index.js': ` console.log('start'); const sleepSync = (delay) => { @@ -140,16 +130,29 @@ export default testSuite(async ({ describe }, fixturePath: string) => { `, }); - onTestFinish(async () => await fixture.rm()); - const tsxProcess = tsx({ args: [ 'watch', - path.join(fixture.path, 'index.js'), + 'index.js', ], + cwd: fixtureExit.path, + }); + + onTestFail(async () => { + if (tsxProcess.exitCode === null) { + console.log('Force killing hanging process\n\n'); + tsxProcess.kill('SIGKILL'); + console.log({ + tsxProcess: await tsxProcess, + }); + } + }); + + onTestFinish(async () => { + await fixtureExit.rm(); }); - await interact( + await processInteract( tsxProcess.stdout!, [ (data) => { @@ -160,6 +163,7 @@ export default testSuite(async ({ describe }, fixturePath: string) => { }, data => data.includes('end\n'), ], + 5000, ); tsxProcess.kill(); @@ -179,35 +183,41 @@ export default testSuite(async ({ describe }, fixturePath: string) => { expect(tsxProcess.stderr).toBe(''); }); - test('passes down --help to file', async () => { + test('passes down --help to file', async ({ onTestFail }) => { const tsxProcess = tsx({ args: [ 'watch', - path.join(fixturePath, 'log-argv.ts'), + 'log-argv.ts', '--help', ], + cwd: fixture.path, }); - await interact( + await processInteract( tsxProcess.stdout!, [data => data.startsWith('["')], + 5000, ); tsxProcess.kill(); const { all } = await tsxProcess; + onTestFail(() => { + console.log(all); + }); + expect(all).toMatch('"--help"'); - }, 5000); + }, 10_000); }); describe('ignore', ({ test }) => { - test('file path & glob', async ({ onTestFinish }) => { + test('file path & glob', async ({ onTestFinish, onTestFail }) => { const entryFile = 'index.js'; const fileA = 'file-a.js'; const fileB = 'directory/file-b.js'; const depA = 'node_modules/a/index.js'; - const fixture = await createFixture({ + const fixtureGlob = await createFixture({ [fileA]: 'export default "logA"', [fileB]: 'export default "logB"', [depA]: 'export default "logC"', @@ -219,50 +229,63 @@ export default testSuite(async ({ describe }, fixturePath: string) => { `.trim(), }); - onTestFinish(async () => await fixture.rm()); + onTestFinish(async () => await fixtureGlob.rm()); const tsxProcess = tsx({ - cwd: fixture.path, + cwd: fixtureGlob.path, args: [ 'watch', '--clear-screen=false', `--ignore=${fileA}`, - `--ignore=${path.join(fixture.path, 'directory/*')}`, + `--ignore=${path.join(fixtureGlob.path, 'directory/*')}`, entryFile, ], }); - const negativeSignal = '"fail"'; - await interact( + onTestFail(async () => { + // If timed out, force kill process + if (tsxProcess.exitCode === null) { + console.log('Force killing hanging process\n\n'); + tsxProcess.kill(); + console.log({ + tsxProcess: await tsxProcess, + }); + } + }); + + const negativeSignal = 'fail'; + + await processInteract( tsxProcess.stdout!, [ async (data) => { - if (data.includes('fail')) { + if (data.includes(negativeSignal)) { throw new Error('should not log ignored file'); } if (data === 'logA logB logC\n') { // These changes should not trigger a re-run await Promise.all([ - fixture.writeFile(fileA, `export default ${negativeSignal}`), - fixture.writeFile(fileB, `export default ${negativeSignal}`), - fixture.writeFile(depA, `export default ${negativeSignal}`), + fixtureGlob.writeFile(fileA, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(fileB, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(depA, `export default "${negativeSignal}"`), ]); - await setTimeout(1500); - await fixture.writeFile(entryFile, 'console.log("TERMINATE")'); + await setTimeout(1000); + fixtureGlob.writeFile(entryFile, 'console.log("TERMINATE")'); return true; } }, data => data === 'TERMINATE\n', ], + 9000, ); tsxProcess.kill(); - const { all, stderr } = await tsxProcess; - expect(all).not.toMatch('fail'); - expect(stderr).toBe(''); + const p = await tsxProcess; + expect(p.all).not.toMatch('fail'); + expect(p.stderr).toBe(''); }, 10_000); }); }); diff --git a/tests/utils/expect-match-in-order.ts b/tests/utils/expect-match-in-order.ts new file mode 100644 index 000000000..0cf95a403 --- /dev/null +++ b/tests/utils/expect-match-in-order.ts @@ -0,0 +1,37 @@ +type Searchable = string | RegExp; + +const stringify = ( + value: { toString(): string }, +) => JSON.stringify( + value.toString(), // Might be RegExp which requires .toString() +); + +const expectedError = ( + expected: Searchable, + before?: Searchable, +) => new Error(`Expected ${stringify(expected)} ${before ? `to be after ${stringify(before)}` : ''}`); + +export const expectMatchInOrder = ( + subject: string, + expectedOrder: Searchable[], +) => { + let remaining = subject; + for (let i = 0; i < expectedOrder.length; i += 1) { + const previousElement = i > 0 ? expectedOrder[i - 1] : undefined; + const currentElement = expectedOrder[i]; + + if (typeof currentElement === 'string') { + const index = remaining.indexOf(currentElement); + if (index === -1) { + throw expectedError(currentElement, previousElement); + } + remaining = remaining.slice(index + currentElement.length); + } else { + const match = remaining.match(currentElement); + if (!match) { + throw expectedError(currentElement, previousElement); + } + remaining = remaining.slice(match.index! + match[0].length); + } + } +}; diff --git a/tests/utils/node-versions.ts b/tests/utils/node-versions.ts index 707b8aac3..0d757a8b2 100644 --- a/tests/utils/node-versions.ts +++ b/tests/utils/node-versions.ts @@ -1,5 +1,4 @@ export const nodeVersions = [ - '18', '20', ...( ( @@ -7,11 +6,7 @@ export const nodeVersions = [ && process.platform !== 'win32' ) ? [ - '12.20.0', // CJS named export detection added - '12', - '14', - '16', - '17', + '18', ] as const : [] as const ), diff --git a/tests/utils/process-interact.ts b/tests/utils/process-interact.ts new file mode 100644 index 000000000..27575c8b1 --- /dev/null +++ b/tests/utils/process-interact.ts @@ -0,0 +1,44 @@ +import type { Readable } from 'node:stream'; +import { on } from 'events'; +import { setTimeout } from 'timers/promises'; + +type MaybePromise = T | Promise; + +export const processInteract = async ( + stdout: Readable, + actions: ((data: string) => MaybePromise)[], + timeout: number, +) => { + const startTime = Date.now(); + const logs: [time: number, string][] = []; + + let currentAction = actions.shift(); + + const ac = new AbortController(); + setTimeout(timeout, true, ac).then( + () => { + if (currentAction) { + console.error(`Timeout ${timeout}ms exceeded:`); + console.log(logs); + } + }, + () => {}, + ); + + while (currentAction) { + for await (const [chunk] of on(stdout, 'data')) { + const chunkString = chunk.toString(); + logs.push([ + Date.now() - startTime, + chunkString, + ]); + + const gotoNextAction = await currentAction(chunkString); + if (gotoNextAction) { + currentAction = actions.shift(); + break; + } + } + } + ac.abort(); +}; diff --git a/tests/utils/pty-shell/index.ts b/tests/utils/pty-shell/index.ts index 527523719..eeef888f2 100644 --- a/tests/utils/pty-shell/index.ts +++ b/tests/utils/pty-shell/index.ts @@ -22,7 +22,7 @@ const getStdin = ( export const ptyShell = ( stdins: StdInArray, -) => new Promise((resolve, reject) => { +) => new Promise((resolve, reject) => { const childProcess = execaNode( fileURLToPath(new URL('node-pty.mjs', import.meta.url)), [shell], diff --git a/tests/utils/tsx.ts b/tests/utils/tsx.ts index 00152509f..c0f5958bb 100644 --- a/tests/utils/tsx.ts +++ b/tests/utils/tsx.ts @@ -1,5 +1,4 @@ import path from 'path'; -import fs from 'fs/promises'; import { fileURLToPath } from 'url'; import { execaNode } from 'execa'; import getNode from 'get-node'; @@ -20,7 +19,8 @@ export const tsx = ( options.args, { env: { - ESBK_DISABLE_CACHE: '1', + TSX_DISABLE_CACHE: '1', + DEBUG: '1', }, nodePath: options.nodePath, nodeOptions: [], @@ -30,10 +30,9 @@ export const tsx = ( }, ); -export async function createNode( +export const createNode = async ( nodeVersion: string, - fixturePath: string, -) { +) => { console.log('Getting node', nodeVersion); const startTime = Date.now(); const node = await getNode(nodeVersion, { @@ -43,125 +42,25 @@ export async function createNode( return { version: node.version, - packageType: '', - get isCJS() { - return this.packageType === 'commonjs'; - }, - tsx( - options: Options, - ) { - return tsx({ - ...options, - nodePath: node.path, - }); - }, - load( - filePath: string, - options?: { - cwd?: string; - args?: string[]; - }, - ) { - return this.tsx( - { - args: [ - ...(options?.args ?? []), - filePath, - ], - cwd: path.join(fixturePath, options?.cwd ?? ''), + tsx: ( + args: string[], + cwd?: string, + ) => execaNode( + tsxPath, + args, + { + cwd, + env: { + TSX_DISABLE_CACHE: '1', + DEBUG: '1', }, - ); - }, - import( - filePath: string, - options?: { - typescript?: boolean; - }, - ) { - return this.tsx({ - args: [ - `./import-file${options?.typescript ? '.ts' : '.js'}`, - filePath, - ], - cwd: fixturePath, - }); - }, - require( - filePath: string, - options?: { - typescript?: boolean; - }, - ) { - return this.tsx({ - args: [ - `./require-file${options?.typescript ? '.cts' : '.cjs'}`, - filePath, - ], - cwd: fixturePath, - }); - }, - requireFlag( - filePath: string, - ) { - return this.tsx({ - args: [ - '--eval', - 'null', - '--require', - filePath, - ], - cwd: fixturePath, - }); - }, - - loadFile( - cwd: string, - filePath: string, - options?: { - args?: string[]; + nodePath: node.path, + nodeOptions: [], + reject: false, + all: true, }, - ) { - return this.tsx( - { - args: [ - ...(options?.args ?? []), - filePath, - ], - cwd, - }, - ); - }, - - async importFile( - cwd: string, - importFrom: string, - fileExtension = '.mjs', - ) { - const fileName = `_${Math.random().toString(36).slice(2)}${fileExtension}`; - const filePath = path.resolve(cwd, fileName); - await fs.writeFile(filePath, `import * as _ from '${importFrom}';console.log(_)`); - try { - return await this.loadFile(cwd, filePath); - } finally { - await fs.rm(filePath); - } - }, - - async requireFile( - cwd: string, - requireFrom: string, - fileExtension = '.cjs', - ) { - const fileName = `_${Math.random().toString(36).slice(2)}${fileExtension}`; - const filePath = path.resolve(cwd, fileName); - await fs.writeFile(filePath, `const _ = require('${requireFrom}');console.log(_)`); - try { - return await this.loadFile(cwd, filePath); - } finally { - await fs.rm(filePath); - } - }, + ), }; -} +}; export type NodeApis = Awaited>;