diff --git a/docs/config/index.md b/docs/config/index.md index 9a72cc272957..fb668aad8ffa 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -105,7 +105,7 @@ This might potentially cause some misalignment if a package has different logic #### deps.registerNodeLoader - **Type:** `boolean` -- **Default:** `true` +- **Default:** `false` Use [experimental Node loader](https://nodejs.org/api/esm.html#loaders) to resolve imports inside `node_modules`, using Vite resolve algorithm. diff --git a/examples/solid/vite.config.mjs b/examples/solid/vite.config.mjs index 9e837c7e0dd6..1ca41548497d 100644 --- a/examples/solid/vite.config.mjs +++ b/examples/solid/vite.config.mjs @@ -10,6 +10,9 @@ export default defineConfig({ transformMode: { web: [/.[jt]sx?/], }, + deps: { + registerNodeLoader: true, + }, threads: false, isolate: false, }, diff --git a/package.json b/package.json index 55996c62ad93..cb29dbe1c5b8 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "test": "vitest --api -r test/core", "test:run": "vitest run -r test/core", "test:all": "cross-env CI=true pnpm -r --stream run test --allowOnly", - "test:ci": "cross-env CI=true pnpm -r --stream --filter !test-fails --filter !test-browser run test --allowOnly", - "test:ci:single-thread": "cross-env CI=true pnpm -r --stream --filter !test-fails run test --allowOnly --no-threads", + "test:ci": "cross-env CI=true pnpm -r --stream --filter !test-fails --filter !test-browser --filter !test-esm run test --allowOnly", + "test:ci:single-thread": "cross-env CI=true pnpm -r --stream --filter !test-fails --filter !test-esm run test --allowOnly --no-threads", "typecheck": "tsc --noEmit", "typecheck:why": "tsc --noEmit --explainFiles > explainTypes.txt", "ui:build": "vite build packages/ui", diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 555641d1ef15..7588f0eb981d 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -1,4 +1,4 @@ -import { join } from 'pathe' +import { resolve } from 'pathe' import type { TransformResult, ViteDevServer } from 'vite' import createDebug from 'debug' import type { DebuggerOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types' @@ -66,7 +66,7 @@ export class ViteNodeServer { async resolveId(id: string, importer?: string): Promise { if (importer && !importer.startsWith(this.server.config.root)) - importer = join(this.server.config.root, importer) + importer = resolve(this.server.config.root, importer) const mode = (importer && this.getTransformMode(importer)) || 'ssr' return this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' }) } diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 866baf16b082..e2543ac6ca7c 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -131,7 +131,7 @@ export function resolveConfig( // disable loader for Yarn PnP until Node implements chain loader // https://github.com/nodejs/node/pull/43772 - resolved.deps.registerNodeLoader ??= typeof process.versions.pnp === 'undefined' + resolved.deps.registerNodeLoader ??= false resolved.testNamePattern = resolved.testNamePattern ? resolved.testNamePattern instanceof RegExp diff --git a/packages/vitest/src/runtime/loader.ts b/packages/vitest/src/runtime/loader.ts index 38233605da3a..659ca1688a45 100644 --- a/packages/vitest/src/runtime/loader.ts +++ b/packages/vitest/src/runtime/loader.ts @@ -1,32 +1,94 @@ import { pathToFileURL } from 'url' -import { isNodeBuiltin } from 'mlly' +import { readFile } from 'fs/promises' +import { hasCJSSyntax, isNodeBuiltin } from 'mlly' import { normalizeModuleId } from 'vite-node/utils' import { getWorkerState } from '../utils' -import type { Loader, Resolver } from '../types/loader' +import type { Loader, ResolveResult, Resolver } from '../types/loader' +import { ModuleFormat } from '../types/loader' + +// TODO fix in mlly (add "}" as a possible first character: "}export default") +const ESM_RE = /([\s;}]|^)(import[\w,{}\s*]*from|import\s*['"*{]|export\b\s*(?:[*{]|default|class|type|function|const|var|let|async function)|import\.meta\b)/m +function hasESMSyntax(code: string) { + return ESM_RE.test(code) +} + +interface ContextCache { + isPseudoESM: boolean + source: string +} + +const cache = new Map() + +const getPotentialSource = async (filepath: string, result: ResolveResult) => { + if (!result.url.startsWith('file://') || result.format === 'module') + return null + let source = cache.get(result.url)?.source + if (source == null) + source = await readFile(filepath, 'utf8') + return source +} + +const detectESM = (url: string, source: string | null) => { + const cached = cache.get(url) + if (cached) + return cached.isPseudoESM + if (!source) + return false + return (hasESMSyntax(source) && !hasCJSSyntax(source)) +} // apply transformations only to libraries // inline code preccessed by vite-node +// make Node pseudo ESM export const resolve: Resolver = async (url, context, next) => { const { parentURL } = context - if (!parentURL || !parentURL.includes('node_modules') || isNodeBuiltin(url)) + const state = getWorkerState() + const resolver = state?.rpc.resolveId + + if (!parentURL || isNodeBuiltin(url) || !resolver) return next(url, context, next) const id = normalizeModuleId(url) const importer = normalizeModuleId(parentURL) - const state = getWorkerState() - const resolver = state?.rpc.resolveId - if (resolver) { - const resolved = await resolver(id, importer) - if (resolved) { - return { - url: pathToFileURL(resolved.id).toString(), - shortCircuit: true, - } + const resolved = await resolver(id, importer) + + let result: ResolveResult + let filepath: string + if (resolved) { + const resolvedUrl = pathToFileURL(resolved.id).toString() + filepath = resolved.id + result = { + url: resolvedUrl, + shortCircuit: true, } } - return next(url, context, next) + else { + const { url: resolvedUrl, format } = await next(url, context, next) + filepath = new URL(resolvedUrl).pathname + result = { + url: resolvedUrl, + format, + shortCircuit: true, + } + } + + const source = await getPotentialSource(filepath, result) + const isPseudoESM = detectESM(result.url, source) + if (typeof source === 'string') + cache.set(result.url, { isPseudoESM, source }) + if (isPseudoESM) + result.format = ModuleFormat.Module + return result } -export const load: Loader = (url, context, next) => { - return next(url, context, next) +export const load: Loader = async (url, context, next) => { + const result = await next(url, context, next) + const cached = cache.get(url) + if (cached?.isPseudoESM && result.format !== 'module') { + return { + source: cached.source, + format: ModuleFormat.Module, + } + } + return result } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index a78cf232976d..8258f24262f7 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -83,7 +83,7 @@ export interface InlineConfig { /** * Use experimental Node loader to resolve imports inside node_modules using Vite resolve algorithm. - * @default true + * @default false */ registerNodeLoader?: boolean } diff --git a/packages/vitest/src/types/loader.ts b/packages/vitest/src/types/loader.ts index 3b203b4d44d5..3bb0cf406890 100644 --- a/packages/vitest/src/types/loader.ts +++ b/packages/vitest/src/types/loader.ts @@ -1,11 +1,11 @@ import type { Awaitable } from './general' -interface ModuleContext { +interface ModuleContext extends Record { conditions: string[] parentURL?: string } -enum ModuleFormat { +export enum ModuleFormat { Builtin = 'builtin', Commonjs = 'commonjs', Json = 'json', @@ -13,8 +13,9 @@ enum ModuleFormat { Wasm = 'wasm', } -interface ResolveResult { +export interface ResolveResult { url: string + shortCircuit?: boolean format?: ModuleFormat } @@ -22,13 +23,14 @@ export interface Resolver { (url: string, context: ModuleContext, next: Resolver): Awaitable } -interface LoaderContext { +interface LoaderContext extends Record { format: ModuleFormat importAssertions: Record } interface LoaderResult { format: ModuleFormat + shortCircuit?: boolean source: string | ArrayBuffer | SharedArrayBuffer | Uint8Array } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d3889f1fae2..41d9e5da77b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -880,6 +880,17 @@ importers: '@vitest/web-worker': link:../../packages/web-worker vitest: link:../../packages/vitest + test/esm: + specifiers: + css-what: 6.1.0 + tslib: 2.4.0 + vitest: workspace:* + dependencies: + css-what: 6.1.0 + tslib: 2.4.0 + devDependencies: + vitest: link:../../packages/vitest + test/fails: specifiers: execa: ^6.1.0 @@ -10892,7 +10903,6 @@ packages: /css-what/6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - dev: true /css.escape/1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} diff --git a/test/esm/package.json b/test/esm/package.json new file mode 100644 index 000000000000..946754fde20e --- /dev/null +++ b/test/esm/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vitest/test-esm", + "private": true, + "scripts": { + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "dependencies": { + "css-what": "6.1.0", + "tslib": "2.4.0" + }, + "devDependencies": { + "vitest": "workspace:*" + } +} diff --git a/test/esm/test/executes.spec.ts b/test/esm/test/executes.spec.ts new file mode 100644 index 000000000000..de94ea0707dd --- /dev/null +++ b/test/esm/test/executes.spec.ts @@ -0,0 +1,9 @@ +import { __assign } from 'tslib' +import { parse } from 'css-what' +import { expect, test } from 'vitest' + +// TODO check on Linux Node 14 +test.skip('imported libs have incorrect ESM, but still work', () => { + expect(__assign({}, { a: 1 })).toEqual({ a: 1 }) + expect(parse('a')).toBeDefined() +}) diff --git a/test/esm/vite.config.ts b/test/esm/vite.config.ts new file mode 100644 index 000000000000..286c3487b689 --- /dev/null +++ b/test/esm/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + deps: { + external: [/tslib/, /css-what/], + registerNodeLoader: true, + }, + }, +})