Skip to content

Commit

Permalink
feat: treat pseudo ESM as ESM with custom loader, disable custom Node…
Browse files Browse the repository at this point in the history
… Loader by default (#1778)

* feat: treat pseudo ESM as ESM

* chore: remove empty main fields

* chore: cleanup

* chore: disable node loader by default

* chore: cleanup

* chore: register loader for esm test

* chore: don't disable loader for now

* chore: revert

* chore: cleanup

* chore disable node loader by default

* chore: disable Node Loader tests
  • Loading branch information
sheremet-va committed Aug 5, 2022
1 parent 135b6d8 commit 155943c
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 27 deletions.
2 changes: 1 addition & 1 deletion docs/config/index.md
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions examples/solid/vite.config.mjs
Expand Up @@ -10,6 +10,9 @@ export default defineConfig({
transformMode: {
web: [/.[jt]sx?/],
},
deps: {
registerNodeLoader: true,
},
threads: false,
isolate: false,
},
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions 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'
Expand Down Expand Up @@ -66,7 +66,7 @@ export class ViteNodeServer {

async resolveId(id: string, importer?: string): Promise<ViteNodeResolveId | null> {
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' })
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/config.ts
Expand Up @@ -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
Expand Down
92 changes: 77 additions & 15 deletions 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<string, ContextCache>()

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
}
2 changes: 1 addition & 1 deletion packages/vitest/src/types/config.ts
Expand Up @@ -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
}
Expand Down
10 changes: 6 additions & 4 deletions packages/vitest/src/types/loader.ts
@@ -1,34 +1,36 @@
import type { Awaitable } from './general'

interface ModuleContext {
interface ModuleContext extends Record<string, unknown> {
conditions: string[]
parentURL?: string
}

enum ModuleFormat {
export enum ModuleFormat {
Builtin = 'builtin',
Commonjs = 'commonjs',
Json = 'json',
Module = 'module',
Wasm = 'wasm',
}

interface ResolveResult {
export interface ResolveResult {
url: string
shortCircuit?: boolean
format?: ModuleFormat
}

export interface Resolver {
(url: string, context: ModuleContext, next: Resolver): Awaitable<ResolveResult>
}

interface LoaderContext {
interface LoaderContext extends Record<string, any> {
format: ModuleFormat
importAssertions: Record<string, string>
}

interface LoaderResult {
format: ModuleFormat
shortCircuit?: boolean
source: string | ArrayBuffer | SharedArrayBuffer | Uint8Array
}

Expand Down
12 changes: 11 additions & 1 deletion pnpm-lock.yaml

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

15 changes: 15 additions & 0 deletions 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:*"
}
}
9 changes: 9 additions & 0 deletions 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()
})
10 changes: 10 additions & 0 deletions 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,
},
},
})

0 comments on commit 155943c

Please sign in to comment.