Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add "strictESM" option to Node environment #2854

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/vite-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.3.4",
"local-pkg": "^0.4.2",
"mlly": "^1.1.0",
"pathe": "^1.1.0",
"picocolors": "^1.0.0",
Expand Down
44 changes: 31 additions & 13 deletions packages/vite-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ export class ViteNodeRunner {
if (id in requestStubs)
return requestStubs[id]

let { code: transformed, externalize } = await this.options.fetchModule(id)
const { code: transformed, externalize, format } = await this.options.fetchModule(id)

mod.format = format

if (externalize) {
debugNative(externalize)
Expand Down Expand Up @@ -342,7 +344,18 @@ export class ViteNodeRunner {

Object.assign(mod, { code: transformed, exports })
const __filename = fileURLToPath(href)
const __dirname = dirname(__filename)
const moduleRequire = createRequire(href)
const moduleProxy = {
children: [],
paths: [],
id: __filename,
filename: __filename,
isPreloading: false,
loaded: false,
parent: null,
path: __dirname,
require: moduleRequire,
set exports(value) {
exportAll(cjsExports, value)
exports.default = value
Expand Down Expand Up @@ -371,7 +384,7 @@ export class ViteNodeRunner {
// changing context will change amount of code added on line :114 (vm.runInThisContext)
// this messes up sourcemaps for coverage
// adjust `offset` variable in packages/coverage-c8/src/provider.ts#86 if you do change this
const context = this.prepareContext({
const context = this.prepareContext(mod, {
// esm transformed by Vite
__vite_ssr_import__: request,
__vite_ssr_dynamic_import__: request,
Expand All @@ -380,34 +393,39 @@ export class ViteNodeRunner {
__vite_ssr_import_meta__: meta,

// cjs compact
require: createRequire(href),
require: moduleRequire,
exports: cjsExports,
module: moduleProxy,
__filename,
__dirname: dirname(__filename),
__dirname,
})

debugExecute(__filename)

// remove shebang
if (transformed[0] === '#')
transformed = transformed.replace(/^\#\!.*/, s => ' '.repeat(s.length))

// add 'use strict' since ESM enables it by default
const codeDefinition = `'use strict';async (${Object.keys(context).join(',')})=>{{`
const code = `${codeDefinition}${transformed}\n}}`
const code = this.prepareCode(mod, context, transformed)
const fn = vm.runInThisContext(code, {
filename: __filename,
lineOffset: 0,
columnOffset: -codeDefinition.length,
})

await fn(...Object.values(context))

return exports
}

prepareContext(context: Record<string, any>) {
prepareCode(module: ModuleCache, context: Record<string, any>, transformed: string) {
// remove shebang
if (transformed[0] === '#')
transformed = transformed.replace(/^\#\!.*/, s => ' '.repeat(s.length))

// add 'use strict' since ESM enables it by default
const codeDefinition = `'use strict';async (${Object.keys(context).join(',')})=>{{`
const code = `${codeDefinition}${transformed}\n}}`

return code
}

prepareContext(module: ModuleCache, context: Record<string, any>) {
return context
}

Expand Down
67 changes: 59 additions & 8 deletions packages/vite-node/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { performance } from 'node:perf_hooks'
import { resolve } from 'pathe'
import { dirname, extname, resolve } from 'pathe'
import type { TransformResult, ViteDevServer } from 'vite'
import { getPackageInfo } from 'local-pkg'
import createDebug from 'debug'
import type { DebuggerOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types'
import type { DebuggerOptions, FetchOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types'
import { shouldExternalize } from './externalize'
import { normalizeModuleId, toArray, toFilePath } from './utils'
import { Debugger } from './debug'
Expand All @@ -15,6 +16,8 @@ const debugRequest = createDebug('vite-node:server:request')
export class ViteNodeServer {
private fetchPromiseMap = new Map<string, Promise<FetchResult>>()
private transformPromiseMap = new Map<string, Promise<TransformResult | null | undefined>>()
private idToFormatMap = new Map<string, 'esm' | 'cjs'>()
private pkgCache = new Map<string, { version: string; type?: 'module' | 'commonjs' }>()

fetchCache = new Map<string, {
duration?: number
Expand Down Expand Up @@ -68,7 +71,7 @@ export class ViteNodeServer {
if (importer && !importer.startsWith(this.server.config.root))
importer = resolve(this.server.config.root, importer)
const mode = (importer && this.getTransformMode(importer)) || 'ssr'
return this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' })
return await this.server.pluginContainer.resolveId(id, importer, { ssr: mode === 'ssr' })
}

getSourceMap(source: string) {
Expand All @@ -79,12 +82,12 @@ export class ViteNodeServer {
return (ssrTransformResult?.map || null) as unknown as RawSourceMap | null
}

async fetchModule(id: string): Promise<FetchResult> {
async fetchModule(id: string, options: FetchOptions = {}): Promise<FetchResult> {
id = normalizeModuleId(id)
// reuse transform for concurrent requests
if (!this.fetchPromiseMap.has(id)) {
this.fetchPromiseMap.set(id,
this._fetchModule(id)
this._fetchModule(id, options)
.then((r) => {
return this.options.sourcemap !== true ? { ...r, map: undefined } : r
})
Expand Down Expand Up @@ -122,7 +125,7 @@ export class ViteNodeServer {
return 'web'
}

private async _fetchModule(id: string): Promise<FetchResult> {
private async _fetchModule(id: string, options: FetchOptions): Promise<FetchResult> {
let result: FetchResult

const { path: filePath } = toFilePath(id, this.server.config.root)
Expand All @@ -142,9 +145,14 @@ export class ViteNodeServer {
}
else {
const start = performance.now()
const r = await this._transformRequest(id)
const [r, format] = await Promise.all([
this._transformRequest(id),
options.loadFormat ? this._getPackageFormat(id) : undefined,
])
if (format)
this.idToFormatMap.set(id, format)
duration = performance.now() - start
result = { code: r?.code, map: r?.map as unknown as RawSourceMap }
result = { format, code: r?.code, map: r?.map as unknown as RawSourceMap }
}

this.fetchCache.set(filePath, {
Expand All @@ -156,6 +164,49 @@ export class ViteNodeServer {
return result
}

private _getCachedPackageInfo(url: string) {
while (url) {
const dir = dirname(url)
if (url === dir)
return null
url = dir
const cached = this.pkgCache.get(url)
if (cached)
return cached
}
return null
}

private async _getPackageFormat(fsPath: string) {
// TODO: clear all cache on watcher package.json change
const cachedFormat = this.idToFormatMap.get(fsPath)
if (cachedFormat)
return cachedFormat
switch (extname(fsPath)) {
case '.cts':
case '.cjs':
return 'cjs'
case '.mts':
case '.mjs':
return 'esm'
}
const pkg = await this._getPackageInfo(fsPath)
return pkg?.type === 'module' ? 'esm' : 'cjs'
}

private async _getPackageInfo(url: string) {
// TODO: clear cache on watcher change
const info = this._getCachedPackageInfo(url)
if (info)
return info
const pkg = await getPackageInfo(url)
if (!pkg)
return null
const pkgPath = dirname(pkg.packageJsonPath)
this.pkgCache.set(pkgPath, pkg.packageJson)
return pkg.packageJson
}

private async _transformRequest(id: string) {
debugRequest(id)

Expand Down
8 changes: 7 additions & 1 deletion packages/vite-node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ export interface RawSourceMap extends StartOfSourceMap {
export interface FetchResult {
code?: string
externalize?: string
format?: 'esm' | 'cjs'
map?: RawSourceMap
}

export interface FetchOptions {
loadFormat?: boolean
}

export type HotContext = Omit<ViteHotContext, 'acceptDeps' | 'decline'>

export type FetchFunction = (id: string) => Promise<FetchResult>
export type FetchFunction = (id: string, options?: FetchOptions) => Promise<FetchResult>

export type ResolveIdFunction = (id: string, importer?: string) => Promise<ViteNodeResolveId | null>

Expand All @@ -44,6 +49,7 @@ export type CreateHotContextFunction = (runner: ViteNodeRunner, url: string) =>
export interface ModuleCache {
promise?: Promise<any>
exports?: any
format?: 'esm' | 'cjs'
evaluated?: boolean
resolving?: boolean
code?: string
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"cac": "^6.7.14",
"chai": "^4.3.7",
"debug": "^4.3.4",
"import-meta-resolve": "^2.2.1",
"local-pkg": "^0.4.2",
"pathe": "^1.1.0",
"picocolors": "^1.0.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/vitest/src/integrations/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import edge from './edge-runtime'

export const environments = {
node,
'node-strict': node,
jsdom,
'happy-dom': happy,
'edge-runtime': edge,
}

export const envs = Object.keys(environments)

export const envPackageNames: Record<Exclude<keyof typeof environments, 'node'>, string> = {
export const envPackageNames: Record<Exclude<keyof typeof environments, 'node' | 'node-strict'>, string> = {
'jsdom': 'jsdom',
'happy-dom': 'happy-dom',
'edge-runtime': '@edge-runtime/vm',
}

export const getEnvPackageName = (env: VitestEnvironment) => {
if (env === 'node')
if (env === 'node' || env === 'node-strict')
return null
if (env in envPackageNames)
return (envPackageNames as any)[env]
Expand Down
13 changes: 6 additions & 7 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,14 @@ export class Vitest {
this.registerWatcher()

this.vitenode = new ViteNodeServer(server, this.config)
const node = this.vitenode
this.runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id: string) {
return node.fetchModule(id)
fetchModule: (id: string) => {
return this.vitenode.fetchModule(id)
},
resolveId(id: string, importer?: string) {
return node.resolveId(id, importer)
resolveId: (id: string, importer?: string) => {
return this.vitenode.resolveId(id, importer)
},
})

Expand Down Expand Up @@ -537,12 +536,12 @@ export class Vitest {
return true
}

this.invalidates.add(id)

const mod = this.server.moduleGraph.getModuleById(id)
if (!mod)
return false

this.invalidates.add(id)

if (this.state.filesMap.has(id)) {
this.changedTests.add(id)
return true
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export { createVitest } from './create'
export { VitestPlugin } from './plugins'
export { startVitest } from './cli-api'

export { VitestExecutor } from '../runtime/execute'
export type { ExecuteOptions } from '../runtime/execute'
export { VitestExecutor } from '../runtime/executors/vitest'
export type { ExecuteOptions } from '../runtime/executors/vitest'

export type { TestSequencer, TestSequencerConstructor } from './sequencers/types'
export { BaseSequencer } from './sequencers/BaseSequencer'
5 changes: 3 additions & 2 deletions packages/vitest/src/node/plugins/envReplacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import MagicString from 'magic-string'
import type { Plugin } from 'vite'
import { stripLiteral } from 'strip-literal'
import { cleanUrl } from 'vite-node/utils'
import type { Vitest } from '../core'

// so people can reassign envs at runtime
// import.meta.env.VITE_NAME = 'app' -> process.env.VITE_NAME = 'app'
export const EnvReplacerPlugin = (): Plugin => {
export const EnvReplacerPlugin = (ctx: Vitest): Plugin => {
return {
name: 'vitest:env-replacer',
enforce: 'pre',
transform(code, id) {
if (!/\bimport\.meta\.env\b/g.test(code))
if (ctx.config.environment === 'node-strict' || !/\bimport\.meta\.env\b/g.test(code))
return null

let s: MagicString | null = null
Expand Down
4 changes: 3 additions & 1 deletion packages/vitest/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GlobalSetupPlugin } from './globalSetup'
import { MocksPlugin } from './mock'
import { CSSEnablerPlugin } from './cssEnabler'
import { CoverageTransform } from './coverageTransform'
import { NodeStrictPlugin } from './nodeStrict'

export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise<VitePlugin[]> {
const getRoot = () => ctx.config?.root || options.root || process.cwd()
Expand All @@ -27,6 +28,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
}

return [
NodeStrictPlugin(ctx),
<VitePlugin>{
name: 'vitest',
enforce: 'pre',
Expand Down Expand Up @@ -199,7 +201,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
await server.watcher.close()
},
},
EnvReplacerPlugin(),
EnvReplacerPlugin(ctx),
MocksPlugin(),
GlobalSetupPlugin(ctx),
...(options.browser
Expand Down
Loading