chore: remove vite-node from the monorepo (#8761)

This commit is contained in:
Vladimir 2025-10-22 14:54:36 +02:00 committed by GitHub
parent 9f0ecccb8b
commit 3dfe09526c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 10 additions and 4908 deletions

View File

@ -44,7 +44,6 @@ Vitest is a next-generation testing framework powered by Vite. This is a monorep
### Core Packages (`packages/`)
- `vitest` - Main testing framework
- `vite-node` - Vite SSR runtime
- `browser` - Browser testing support
- `ui` - Web UI for test results
- `runner` - Test runner core

View File

@ -156,7 +156,7 @@ Color used by CLI and UI can be changed by providing an object with `color` prop
- **Type:** `{ sourcemap?, deps?, ... }`
Vite-Node server options.
Moudle runner options.
#### server.sourcemap
@ -169,7 +169,7 @@ Inject inline source map to modules.
- **Type:** `{ dumpModules?, loadDumppedModules? }`
Vite-Node debugger options.
Module runner debugger options.
#### server.debug.dumpModules

View File

@ -79,24 +79,6 @@ After the tests have run there should be a `main-profile/*.cpuprofile` file gene
## File transform
In cases where your test transform and collection time is high, you can use `DEBUG=vite-node:*` environment variable to see which files are being transformed and executed by `vite-node`.
```bash
$ DEBUG=vite-node:* vitest --run
RUN v2.1.1 /x/vitest/examples/profiling
vite-node:server:request /x/vitest/examples/profiling/global-setup.ts +0ms
vite-node:client:execute /x/vitest/examples/profiling/global-setup.ts +0ms
vite-node:server:request /x/vitest/examples/profiling/test/prime-number.test.ts +45ms
vite-node:client:execute /x/vitest/examples/profiling/test/prime-number.test.ts +26ms
vite-node:server:request /src/prime-number.ts +9ms
vite-node:client:execute /x/vitest/examples/profiling/src/prime-number.ts +9ms
vite-node:server:request /src/unnecessary-file.ts +6ms
vite-node:client:execute /x/vitest/examples/profiling/src/unnecessary-file.ts +4ms
...
```
This profiling strategy is a good way to identify unnecessary transforms caused by [barrel files](https://vitejs.dev/guide/performance.html#avoid-barrel-files).
If these logs contain files that should not be loaded when your test is run, you might have barrel files that are importing files unnecessarily.
@ -131,17 +113,15 @@ test('formatter works', () => {
<img src="/module-graph-barrel-file.png" alt="Vitest UI demonstrating barrel file issues" />
To see how files are transformed, you can use `VITE_NODE_DEBUG_DUMP` environment variable to write transformed files in the file system:
To see how files are transformed, you can use `VITEST_DEBUG_DUMP` environment variable to write transformed files in the file system:
```bash
$ VITE_NODE_DEBUG_DUMP=true vitest --run
[vite-node] [debug] dump modules to /x/examples/profiling/.vite-node/dump
$ VITEST_DEBUG_DUMP=true vitest --run
RUN v2.1.1 /x/vitest/examples/profiling
...
$ ls .vite-node/dump/
$ ls .vitest-dump/
_x_examples_profiling_global-setup_ts-1292904907.js
_x_examples_profiling_test_prime-number_test_ts-1413378098.js
_src_prime-number_ts-525172412.js

View File

@ -135,11 +135,4 @@ export default antfu(
'unicorn/consistent-function-scoping': 'off',
},
},
{
files: [`packages/vite-node/${GLOB_SRC}`],
rules: {
// false positive on "exports" variable
'antfu/no-cjs-exports': 'off',
},
},
)

View File

@ -20,8 +20,6 @@ const stackIgnorePatterns: (string | RegExp)[] = [
/\/@vitest\/\w+\/dist\//,
'/vitest/dist/',
'/vitest/src/',
'/vite-node/dist/',
'/vite-node/src/',
'/node_modules/chai/',
'/node_modules/tinyspy/',
'/vite/dist/node/module-runner',

View File

@ -1 +0,0 @@
*.d.ts

View File

@ -1,193 +0,0 @@
<p align="center">
<img src="https://github.com/vitest-dev/vitest/blob/main/packages/vite-node/assets/vite-node.svg?raw=true" height="120">
</p>
<h1 align="center">
vite-node
</h1>
<p align="center">
Vite as Node runtime.<br>The engine that powers <a href="https://github.com/vitest-dev/vitest">Vitest</a> and <a href="https://github.com/nuxt/nuxt">Nuxt 3 Dev SSR</a>.
<p>
<p align="center">
<a href="https://www.npmjs.com/package/vitest"><img src="https://img.shields.io/npm/v/vite-node?color=FCC72B&label="></a>
<p>
## Features
- On-demand evaluation
- Vite's pipeline, plugins, resolve, aliasing
- Out-of-box ESM & TypeScript support
- Respect `vite.config.ts`
- Hot module replacement (HMR)
- Separate server/client architecture
- Top-level `await`
- Shims for `__dirname` and `__filename` in ESM
- Access to native node modules like `fs`, `path`, etc.
## CLI Usage
Run JS/TS file on Node.js using Vite's resolvers and transformers.
```bash
npx vite-node index.ts
```
Options:
```bash
npx vite-node -h
```
### Options via CLI
[All `ViteNodeServer` options](https://github.com/vitest-dev/vitest/blob/main/packages/vite-node/src/types.ts#L92-L111) are supported by the CLI. They may be defined through the dot syntax, as shown below:
```bash
npx vite-node --options.deps.inline="module-name" --options.deps.external="/module-regexp/" index.ts
```
Note that for options supporting RegExps, strings passed to the CLI must start _and_ end with a `/`;
### Hashbang
If you prefer to write scripts that don't need to be passed into Vite Node, you can declare it in the [hashbang](https://bash.cyberciti.biz/guide/Shebang).
Simply add `#!/usr/bin/env vite-node --script` at the top of your file:
_file.ts_
```ts
#!/usr/bin/env vite-node --script
console.log('argv:', process.argv.slice(2))
```
And make the file executable:
```sh
chmod +x ./file.ts
```
Now, you can run the file without passing it into Vite Node:
```sh
$ ./file.ts hello
argv: [ 'hello' ]
```
Note that when using the `--script` option, Vite Node forwards every argument and option to the script to execute, even the one supported by Vite Node itself.
## Programmatic Usage
In Vite Node, the server and runner (client) are separated, so you can integrate them in different contexts (workers, cross-process, or remote) if needed. The demo below shows a simple example of having both (server and runner) running in the same context
```ts
import { createServer, version as viteVersion } from 'vite'
import { ViteNodeRunner } from 'vite-node/client'
import { ViteNodeServer } from 'vite-node/server'
import { installSourcemapsSupport } from 'vite-node/source-map'
// create vite server
const server = await createServer({
optimizeDeps: {
// It's recommended to disable deps optimization
noDiscovery: true,
include: undefined,
},
})
// For old Vite, this is needed to initialize the plugins.
if (Number(viteVersion.split('.')[0]) < 6) {
await server.pluginContainer.buildStart({})
}
// create vite-node server
const node = new ViteNodeServer(server)
// fixes stacktraces in Errors
installSourcemapsSupport({
getSourceMap: source => node.getSourceMap(source),
})
// create vite-node runner
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
// when having the server and runner in a different context,
// you will need to handle the communication between them
// and pass to this function
fetchModule(id) {
return node.fetchModule(id)
},
resolveId(id, importer) {
return node.resolveId(id, importer)
},
})
// execute the file
await runner.executeFile('./example.ts')
// close the vite server
await server.close()
```
## Debugging
### Debug Transformation
Sometimes you might want to inspect the transformed code to investigate issues. You can set environment variable `VITE_NODE_DEBUG_DUMP=true` to let vite-node write the transformed result of each module under `.vite-node/dump`.
If you want to debug by modifying the dumped code, you can change the value of `VITE_NODE_DEBUG_DUMP` to `load` and search for the dumped files and use them for executing.
```bash
VITE_NODE_DEBUG_DUMP=load vite-node example.ts
```
Or programmatically:
```js
import { ViteNodeServer } from 'vite-node/server'
const server = new ViteNodeServer(viteServer, {
debug: {
dumpModules: true,
loadDumppedModules: true,
},
})
```
### Debug Execution
If the process gets stuck, it might be because there are unresolvable circular dependencies. You can set `VITE_NODE_DEBUG_RUNNER=true` for vite-node to warn about this.
```bash
VITE_NODE_DEBUG_RUNNER=true vite-node example.ts
```
Or programmatically:
```js
import { ViteNodeRunner } from 'vite-node/client'
const runner = new ViteNodeRunner({
debug: true,
})
```
## Credits
Based on [@pi0](https://github.com/pi0)'s brilliant idea of having a Vite server as the on-demand transforming service for [Nuxt's Vite SSR](https://github.com/nuxt/vite/pull/201).
Thanks [@brillout](https://github.com/brillout) for kindly sharing this package name.
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
## License
[MIT](./LICENSE) License © 2021 [Anthony Fu](https://github.com/antfu)

View File

@ -1,4 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60 5.99962L106.765 32.9996V86.9996L60 114L13.2346 86.9996V32.9996L60 5.99962Z" stroke="#99D253" stroke-width="8" stroke-linejoin="round"/>
<path d="M63.1707 49.8053L90.5738 47.6009L60.5096 114.684L56.9315 70.204L29.5284 72.4084L59.5926 5.32538L63.1707 49.8053Z" stroke="#FCC72B" stroke-width="7" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 439 B

View File

@ -1,96 +0,0 @@
{
"name": "vite-node",
"type": "module",
"version": "4.0.0-beta.19",
"description": "Vite as Node.js runtime",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://opencollective.com/vitest",
"homepage": "https://github.com/vitest-dev/vitest/blob/main/packages/vite-node#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/vitest-dev/vitest.git",
"directory": "packages/vite-node"
},
"bugs": {
"url": "https://github.com/vitest-dev/vitest/issues"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.mjs",
"require": "./dist/client.cjs"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.mjs",
"require": "./dist/server.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./hmr": {
"types": "./dist/hmr.d.ts",
"import": "./dist/hmr.mjs",
"require": "./dist/hmr.cjs"
},
"./source-map": {
"types": "./dist/source-map.d.ts",
"import": "./dist/source-map.mjs",
"require": "./dist/source-map.cjs"
},
"./constants": {
"types": "./dist/constants.d.ts",
"import": "./dist/constants.mjs",
"require": "./dist/constants.cjs"
},
"./*": "./*"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/index.d.ts"
]
}
},
"bin": {
"vite-node": "./vite-node.mjs"
},
"files": [
"*.d.ts",
"*.mjs",
"dist"
],
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"scripts": {
"build": "premove dist && rollup -c",
"dev": "rollup -c --watch --watch.include 'src/**' -m inline",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"cac": "catalog:",
"debug": "catalog:",
"es-module-lexer": "^1.7.0",
"pathe": "catalog:",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"devDependencies": {
"@jridgewell/trace-mapping": "catalog:",
"@types/debug": "catalog:",
"tinyrainbow": "catalog:"
}
}

View File

@ -1,102 +0,0 @@
import { builtinModules, createRequire } from 'node:module'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import { defineConfig } from 'rollup'
import oxc from 'unplugin-oxc/rollup'
import { createDtsUtils } from '../../scripts/build-utils.js'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')
const entries = {
'index': 'src/index.ts',
'server': 'src/server.ts',
'types': 'src/types.ts',
'client': 'src/client.ts',
'utils': 'src/utils.ts',
'cli': 'src/cli.ts',
'constants': 'src/constants.ts',
'hmr': 'src/hmr/index.ts',
'source-map': 'src/source-map.ts',
}
const external = [
...builtinModules,
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
'pathe',
'birpc',
'vite',
'node:url',
'node:events',
'node:vm',
]
const dtsUtils = createDtsUtils()
const plugins = [
resolve({
preferBuiltins: true,
}),
json(),
commonjs(),
oxc({
transform: {
target: 'node14',
define: process.env.VITE_TEST_WATCHER_DEBUG === 'false'
? { 'process.env.VITE_TEST_WATCHER_DEBUG': 'false' }
: {},
},
sourcemap: true,
}),
]
export default defineConfig([
{
input: entries,
output: {
dir: 'dist',
format: 'esm',
entryFileNames: '[name].mjs',
chunkFileNames: 'chunk-[name].mjs',
},
external,
plugins: [
...dtsUtils.isolatedDecl(),
...plugins,
],
onwarn,
},
{
input: entries,
output: {
dir: 'dist',
format: 'cjs',
entryFileNames: '[name].cjs',
chunkFileNames: 'chunk-[name].cjs',
},
external,
plugins,
onwarn,
},
{
input: dtsUtils.dtsInput(entries, { ext: 'mts' }),
output: {
dir: 'dist',
entryFileNames: '[name].d.ts',
format: 'esm',
},
watch: false,
external,
plugins: dtsUtils.dts(),
onwarn,
},
])
function onwarn(message) {
if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) {
return
}
console.error(message)
}

View File

@ -1,234 +0,0 @@
import type { ViteNodeServerOptions } from './types'
import { resolve } from 'node:path'
import cac from 'cac'
import c from 'tinyrainbow'
import { createServer, loadEnv, version as viteVersion } from 'vite'
import { version } from '../package.json'
import { ViteNodeRunner } from './client'
import { createHotContext, handleMessage, viteNodeHmrPlugin } from './hmr'
import { ViteNodeServer } from './server'
import { installSourcemapsSupport } from './source-map'
import { toArray } from './utils'
const cli = cac('vite-node')
cli
.option('-r, --root <path>', 'Use specified root directory')
.option('-c, --config <path>', 'Use specified config file')
.option('-m, --mode <mode>', 'Set env mode')
.option('-w, --watch', 'Restart on file changes, similar to "nodemon"')
.option('--script', 'Use vite-node as a script runner')
.option('--options <options>', 'Use specified Vite server options')
.option('-v, --version', 'Output the version number')
.option('-h, --help', 'Display help for command')
cli.command('[...files]').allowUnknownOptions().action(run)
cli.parse(process.argv, { run: false })
if (cli.args.length === 0) {
cli.runMatchedCommand()
}
else {
const i = cli.rawArgs.indexOf(cli.args[0]) + 1
const scriptArgs = cli.rawArgs.slice(i).filter(it => it !== '--')
const executeArgs = [...cli.rawArgs.slice(0, i), '--', ...scriptArgs]
cli.parse(executeArgs)
}
export interface CliOptions {
'root'?: string
'script'?: boolean
'config'?: string
'mode'?: string
'watch'?: boolean
'options'?: ViteNodeServerOptionsCLI
'version'?: boolean
'help'?: boolean
'--'?: string[]
}
async function run(files: string[], options: CliOptions = {}) {
if (options.script) {
files = [files[0]]
options = {}
process.argv = [
process.argv[0],
resolve(files[0]),
...process.argv
.slice(2)
.filter(arg => arg !== '--script' && arg !== files[0]),
]
}
else {
process.argv = [...process.argv.slice(0, 2), ...(options['--'] || [])]
}
if (options.version) {
cli.version(version)
cli.outputVersion()
process.exit(0)
}
if (options.help) {
cli.version(version).outputHelp()
process.exit(0)
}
if (!files.length) {
console.error(c.red('No files specified.'))
cli.version(version).outputHelp()
process.exit(1)
}
const serverOptions = options.options
? parseServerOptions(options.options)
: {}
const server = await createServer({
logLevel: 'error',
configFile: options.config,
root: options.root,
mode: options.mode,
server: {
hmr: !!options.watch,
watch: options.watch ? undefined : null,
},
plugins: [options.watch && viteNodeHmrPlugin()],
})
if (Number(viteVersion.split('.')[0]) < 6) {
await server.pluginContainer.buildStart({})
}
else {
// directly access client plugin container until https://github.com/vitejs/vite/issues/19607
await server.environments.client.pluginContainer.buildStart({})
}
const env = loadEnv(server.config.mode, server.config.envDir, '')
for (const key in env) {
process.env[key] ??= env[key]
}
const node = new ViteNodeServer(server, serverOptions)
installSourcemapsSupport({
getSourceMap: source => node.getSourceMap(source),
})
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id) {
return node.fetchModule(id)
},
resolveId(id, importer) {
return node.resolveId(id, importer)
},
createHotContext(runner, url) {
return createHotContext(runner, server.emitter, files, url)
},
})
// provide the vite define variable in this context
await runner.executeId('/@vite/env')
for (const file of files) {
await runner.executeFile(file)
}
if (!options.watch) {
await server.close()
}
server.emitter?.on('message', (payload) => {
handleMessage(runner, server.emitter, files, payload)
})
if (options.watch) {
process.on('uncaughtException', (err) => {
console.error(c.red('[vite-node] Failed to execute file: \n'), err)
})
if (process.env.VITE_TEST_WATCHER_DEBUG) {
// manually check `watcher.getWatched()` to make sure entry files are ready
// since watcher.on('ready', ...) event is not reliable since 5.1.
// https://github.com/vitejs/vite/blob/63a39c244b08cf1f2299bc2c3cfddcb82070d05b/playground/hmr-ssr/__tests__/hmr.spec.ts#L1065
const nodePath = await import('node:path')
async function waitForWatched(files: string[]): Promise<void> {
while (!files.every(file => isWatched(file))) {
await new Promise(resolve => setTimeout(resolve, 20))
}
}
function isWatched(file: string): boolean {
const watched = server.watcher.getWatched()
const resolved = nodePath.resolve(file)
const dir = nodePath.dirname(resolved)
const base = nodePath.basename(resolved)
return watched[dir]?.includes(base)
}
await waitForWatched(files)
console.log('[debug] watcher is ready')
}
}
}
function parseServerOptions(
serverOptions: ViteNodeServerOptionsCLI,
): ViteNodeServerOptions {
const inlineOptions
= serverOptions.deps?.inline === true
? true
: toArray(serverOptions.deps?.inline)
return {
...serverOptions,
deps: {
...serverOptions.deps,
inlineFiles: toArray(serverOptions.deps?.inlineFiles),
inline:
inlineOptions !== true
? inlineOptions.map((dep) => {
return dep[0] === '/' && dep.endsWith('/')
? new RegExp(dep)
: dep
})
: true,
external: toArray(serverOptions.deps?.external).map((dep) => {
return dep[0] === '/' && dep.endsWith('/') ? new RegExp(dep) : dep
}),
moduleDirectories: serverOptions.deps?.moduleDirectories
? toArray(serverOptions.deps?.moduleDirectories)
: undefined,
},
transformMode: {
...serverOptions.transformMode,
ssr: toArray(serverOptions.transformMode?.ssr).map(
dep => new RegExp(dep),
),
web: toArray(serverOptions.transformMode?.web).map(
dep => new RegExp(dep),
),
},
}
}
type Optional<T> = T | undefined
type ComputeViteNodeServerOptionsCLI<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends Optional<RegExp[]>
? string | string[]
: T[K] extends Optional<(string | RegExp)[]>
? string | string[]
: T[K] extends Optional<(string | RegExp)[] | true>
? string | string[] | true
: T[K] extends Optional<Record<string, any>>
? ComputeViteNodeServerOptionsCLI<T[K]>
: T[K];
}
export type ViteNodeServerOptionsCLI
= ComputeViteNodeServerOptionsCLI<ViteNodeServerOptions>

View File

@ -1,750 +0,0 @@
import type { HotContext, ModuleCache, ViteNodeRunnerOptions } from './types'
import { createRequire } from 'node:module'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import vm from 'node:vm'
import createDebug from 'debug'
import { extractSourceMap } from './source-map'
import {
cleanUrl,
createImportMetaEnvProxy,
isBareImport,
isInternalRequest,
isNodeBuiltin,
isPrimitive,
normalizeModuleId,
normalizeRequestId,
slash,
toFilePath,
} from './utils'
const { setTimeout, clearTimeout } = globalThis
const debugExecute = createDebug('vite-node:client:execute')
const debugNative = createDebug('vite-node:client:native')
const clientStub = {
injectQuery: (id: string) => id,
createHotContext: () => {
return {
accept: () => {},
prune: () => {},
dispose: () => {},
decline: () => {},
invalidate: () => {},
on: () => {},
send: () => {},
}
},
updateStyle: () => {},
removeStyle: () => {},
}
const env = createImportMetaEnvProxy()
export const DEFAULT_REQUEST_STUBS: Record<string, Record<string, unknown>> = {
'/@vite/client': clientStub,
'@vite/client': clientStub,
}
export class ModuleCacheMap extends Map<string, ModuleCache> {
normalizePath(fsPath: string): string {
return normalizeModuleId(fsPath)
}
/**
* Assign partial data to the map
*/
update(fsPath: string, mod: ModuleCache): this {
fsPath = this.normalizePath(fsPath)
if (!super.has(fsPath)) {
this.setByModuleId(fsPath, mod)
}
else {
Object.assign(super.get(fsPath) as ModuleCache, mod)
}
return this
}
setByModuleId(modulePath: string, mod: ModuleCache): this {
return super.set(modulePath, mod)
}
set(fsPath: string, mod: ModuleCache): this {
return this.setByModuleId(this.normalizePath(fsPath), mod)
}
getByModuleId(modulePath: string) {
if (!super.has(modulePath)) {
this.setByModuleId(modulePath, {})
}
const mod = super.get(modulePath)!
if (!mod.imports) {
Object.assign(mod, {
imports: new Set(),
importers: new Set(),
})
}
return mod as ModuleCache
& Required<Pick<ModuleCache, 'imports' | 'importers'>>
}
get(fsPath: string): ModuleCache & Required<Pick<ModuleCache, 'importers' | 'imports'>> {
return this.getByModuleId(this.normalizePath(fsPath))
}
deleteByModuleId(modulePath: string): boolean {
return super.delete(modulePath)
}
delete(fsPath: string): boolean {
return this.deleteByModuleId(this.normalizePath(fsPath))
}
invalidateModule(mod: ModuleCache) {
delete mod.evaluated
delete mod.resolving
delete mod.promise
delete mod.exports
mod.importers?.clear()
mod.imports?.clear()
return true
}
/**
* Invalidate modules that dependent on the given modules, up to the main entry
*/
invalidateDepTree(
ids: string[] | Set<string>,
invalidated: Set<string> = new Set<string>(),
): Set<string> {
for (const _id of ids) {
const id = this.normalizePath(_id)
if (invalidated.has(id)) {
continue
}
invalidated.add(id)
const mod = super.get(id)
if (mod?.importers) {
this.invalidateDepTree(mod.importers, invalidated)
}
super.delete(id)
}
return invalidated
}
/**
* Invalidate dependency modules of the given modules, down to the bottom-level dependencies
*/
invalidateSubDepTree(
ids: string[] | Set<string>,
invalidated: Set<string> = new Set<string>(),
): Set<string> {
for (const _id of ids) {
const id = this.normalizePath(_id)
if (invalidated.has(id)) {
continue
}
invalidated.add(id)
const subIds = Array.from(super.entries())
.filter(([, mod]) => mod.importers?.has(id))
.map(([key]) => key)
if (subIds.length) {
this.invalidateSubDepTree(subIds, invalidated)
}
super.delete(id)
}
return invalidated
}
/**
* Return parsed source map based on inlined source map of the module
*/
getSourceMap(id: string): import('@jridgewell/trace-mapping').EncodedSourceMap | null {
const cache = this.get(id)
if (cache.map) {
return cache.map
}
const map = cache.code && extractSourceMap(cache.code)
if (map) {
cache.map = map
return map
}
return null
}
}
export type ModuleExecutionInfo = Map<string, ModuleExecutionInfoEntry>
export interface ModuleExecutionInfoEntry {
startOffset: number
/** The duration that was spent executing the module. */
duration: number
/** The time that was spent executing the module itself and externalized imports. */
selfTime: number
}
/** Stack to track nested module execution for self-time calculation. */
type ExecutionStack = Array<{
/** The file that is being executed. */
filename: string
/** The start time of this module's execution. */
startTime: number
/** Accumulated time spent importing all sub-imports. */
subImportTime: number
}>
export class ViteNodeRunner {
root: string
debug: boolean
/**
* Holds the cache of modules
* Keys of the map are filepaths, or plain package names
*/
moduleCache: ModuleCacheMap
/**
* Tracks the stack of modules being executed for the purpose of calculating import self-time.
*
* Note that while in most cases, imports are a linear stack of modules,
* this is occasionally not the case, for example when you have parallel top-level dynamic imports like so:
*
* ```ts
* await Promise.all([
* import('./module1'),
* import('./module2'),
* ]);
* ```
*
* In this case, the self time will be reported incorrectly for one of the modules (could go negative).
* As top-level awaits with dynamic imports like this are uncommon, we don't handle this case specifically.
*/
private executionStack: ExecutionStack = []
// `performance` can be mocked, so make sure we're using the original function
private performanceNow = performance.now.bind(performance)
constructor(public options: ViteNodeRunnerOptions) {
this.root = options.root ?? process.cwd()
this.moduleCache = options.moduleCache ?? new ModuleCacheMap()
this.debug
= options.debug
?? (typeof process !== 'undefined'
? !!process.env.VITE_NODE_DEBUG_RUNNER
: false)
}
async executeFile(file: string): Promise<any> {
const url = `/@fs/${slash(resolve(file))}`
return await this.cachedRequest(url, url, [])
}
async executeId(rawId: string): Promise<any> {
const [id, url] = await this.resolveUrl(rawId)
return await this.cachedRequest(id, url, [])
}
/** @internal */
async cachedRequest(id: string, fsPath: string, callstack: string[]) {
const importee = callstack.at(-1)
const mod = this.moduleCache.get(fsPath)
const { imports, importers } = mod
if (importee) {
importers.add(importee)
}
const getStack = () =>
`stack:\n${[...callstack, fsPath]
.reverse()
.map(p => ` - ${p}`)
.join('\n')}`
// check circular dependency
if (
callstack.includes(fsPath)
|| Array.from(imports.values()).some(i => importers.has(i))
) {
if (mod.exports) {
return mod.exports
}
}
let debugTimer: any
if (this.debug) {
debugTimer = setTimeout(
() =>
console.warn(
`[vite-node] module ${fsPath} takes over 2s to load.\n${getStack()}`,
),
2000,
)
}
try {
// cached module
if (mod.promise) {
return await mod.promise
}
const promise = this.directRequest(id, fsPath, callstack)
Object.assign(mod, { promise, evaluated: false })
return await promise
}
finally {
mod.evaluated = true
if (debugTimer) {
clearTimeout(debugTimer)
}
}
}
shouldResolveId(id: string, _importee?: string): boolean {
return (
!isInternalRequest(id) && !isNodeBuiltin(id) && !id.startsWith('data:')
)
}
private async _resolveUrl(
id: string,
importer?: string,
): Promise<[url: string, fsPath: string]> {
const dep = normalizeRequestId(id, this.options.base)
if (!this.shouldResolveId(dep)) {
return [dep, dep]
}
const { path, exists } = toFilePath(dep, this.root)
if (!this.options.resolveId || exists) {
return [dep, path]
}
const resolved = await this.options.resolveId(dep, importer)
// supported since Vite 5-beta.19
if (resolved?.meta?.['vite:alias']?.noResolved) {
const error = new Error(
`Cannot find module '${id}'${
importer ? ` imported from '${importer}'` : ''
}.`
+ '\n\n- If you rely on tsconfig.json\'s "paths" to resolve modules, please install "vite-tsconfig-paths" plugin to handle module resolution.'
+ '\n- Make sure you don\'t have relative aliases in your Vitest config. Use absolute paths instead. Read more: https://vitest.dev/guide/common-errors',
)
Object.defineProperty(error, 'code', {
value: 'ERR_MODULE_NOT_FOUND',
enumerable: true,
})
Object.defineProperty(error, Symbol.for('vitest.error.not_found.data'), {
value: { id: dep, importer },
enumerable: false,
})
throw error
}
const resolvedId = resolved
? normalizeRequestId(resolved.id, this.options.base)
: dep
return [resolvedId, resolvedId]
}
async resolveUrl(id: string, importee?: string): Promise<[url: string, fsPath: string]> {
const resolveKey = `resolve:${id}`
// put info about new import as soon as possible, so we can start tracking it
this.moduleCache.setByModuleId(resolveKey, { resolving: true })
try {
return await this._resolveUrl(id, importee)
}
finally {
this.moduleCache.deleteByModuleId(resolveKey)
}
}
/** @internal */
async dependencyRequest(id: string, fsPath: string, callstack: string[]) {
return await this.cachedRequest(id, fsPath, callstack)
}
private async _fetchModule(id: string, importer?: string) {
try {
return await this.options.fetchModule(id)
}
catch (cause: any) {
// rethrow vite error if it cannot load the module because it's not resolved
if (
(typeof cause === 'object' && cause.code === 'ERR_LOAD_URL')
|| (typeof cause?.message === 'string' && cause.message.includes('Failed to load url'))
) {
const error = new Error(
`Cannot find ${isBareImport(id) ? 'package' : 'module'} '${id}'${importer ? ` imported from '${importer}'` : ''}`,
{ cause },
) as Error & { code: string }
error.code = 'ERR_MODULE_NOT_FOUND'
throw error
}
throw cause
}
}
/** @internal */
async directRequest(id: string, fsPath: string, _callstack: string[]) {
const moduleId = normalizeModuleId(fsPath)
const callstack = [..._callstack, moduleId]
const mod = this.moduleCache.getByModuleId(moduleId)
const request = async (dep: string) => {
const [id, depFsPath] = await this.resolveUrl(String(dep), fsPath)
const depMod = this.moduleCache.getByModuleId(depFsPath)
depMod.importers.add(moduleId)
mod.imports.add(depFsPath)
return this.dependencyRequest(id, depFsPath, callstack)
}
const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS
if (id in requestStubs) {
return requestStubs[id]
}
let { code: transformed, externalize } = await this._fetchModule(
id,
callstack[callstack.length - 2],
)
if (externalize) {
debugNative(externalize)
const exports = await this.interopedImport(externalize)
mod.exports = exports
return exports
}
if (transformed == null) {
throw new Error(
`[vite-node] Failed to load "${id}" imported from ${
callstack[callstack.length - 2]
}`,
)
}
const { Object, Reflect, Symbol } = this.getContextPrimitives()
const modulePath = cleanUrl(moduleId)
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
const href = pathToFileURL(modulePath).href
const __filename = fileURLToPath(href)
const __dirname = dirname(__filename)
const meta = {
url: href,
env,
filename: __filename,
dirname: __dirname,
}
const exports = Object.create(null)
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module',
enumerable: false,
configurable: false,
})
const SYMBOL_NOT_DEFINED = Symbol('not defined')
let moduleExports: unknown = SYMBOL_NOT_DEFINED
// this proxy is triggered only on exports.{name} and module.exports access
// inside the module itself. imported module is always "exports"
const cjsExports = new Proxy(exports, {
get: (target, p, receiver) => {
if (Reflect.has(target, p)) {
return Reflect.get(target, p, receiver)
}
return Reflect.get(Object.prototype, p, receiver)
},
getPrototypeOf: () => Object.prototype,
set: (_, p, value) => {
// treat "module.exports =" the same as "exports.default =" to not have nested "default.default",
// so "exports.default" becomes the actual module
if (
p === 'default'
&& this.shouldInterop(modulePath, { default: value })
&& cjsExports !== value
) {
exportAll(cjsExports, value)
exports.default = value
return true
}
if (!Reflect.has(exports, 'default')) {
exports.default = {}
}
// returns undefined, when accessing named exports, if default is not an object
// but is still present inside hasOwnKeys, this is Node behaviour for CJS
if (
moduleExports !== SYMBOL_NOT_DEFINED
&& isPrimitive(moduleExports)
) {
defineExport(exports, p, () => undefined)
return true
}
if (!isPrimitive(exports.default)) {
exports.default[p] = value
}
if (p !== 'default') {
defineExport(exports, p, () => value)
}
return true
},
})
Object.assign(mod, { code: transformed, exports })
const moduleProxy = {
set exports(value) {
exportAll(cjsExports, value)
exports.default = value
moduleExports = value
},
get exports() {
return cjsExports
},
}
// Vite hot context
let hotContext: HotContext | undefined
if (this.options.createHotContext) {
Object.defineProperty(meta, 'hot', {
enumerable: true,
get: () => {
hotContext ||= this.options.createHotContext?.(this, moduleId)
return hotContext
},
set: (value) => {
hotContext = value
},
})
}
// Be careful when changing this
// changing context will change amount of code added on line :114 (vm.runInThisContext)
// this messes up sourcemaps for coverage
// adjust `WRAPPER_LENGTH` variable in packages/coverage-v8/src/provider.ts if you do change this
const context = this.prepareContext({
// esm transformed by Vite
__vite_ssr_import__: request,
__vite_ssr_dynamic_import__: request,
__vite_ssr_exports__: exports,
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
__vite_ssr_exportName__: (name: string, getter: () => unknown) => Object.defineProperty(exports, name, {
enumerable: true,
configurable: true,
get: getter,
}),
__vite_ssr_import_meta__: meta,
// cjs compact
require: createRequire(href),
exports: cjsExports,
module: moduleProxy,
__filename,
__dirname,
})
debugExecute(__filename)
// remove shebang
if (transformed[0] === '#') {
transformed = transformed.replace(/^#!.*/, s => ' '.repeat(s.length))
}
await this.runModule(context, transformed)
return exports
}
protected getContextPrimitives(): {
Object: ObjectConstructor
Reflect: typeof Reflect
Symbol: SymbolConstructor
} {
return { Object, Reflect, Symbol }
}
protected async runModule(context: Record<string, any>, transformed: string): Promise<void> {
// add 'use strict' since ESM enables it by default
const codeDefinition = `'use strict';async (${Object.keys(context).join(
',',
)})=>{{`
const code = `${codeDefinition}${transformed}\n}}`
const options = {
filename: context.__filename,
lineOffset: 0,
columnOffset: -codeDefinition.length,
}
const finishModuleExecutionInfo = this.startCalculateModuleExecutionInfo(options.filename, codeDefinition.length)
try {
const fn = vm.runInThisContext(code, options)
await fn(...Object.values(context))
}
finally {
this.options.moduleExecutionInfo?.set(options.filename, finishModuleExecutionInfo())
}
}
/**
* Starts calculating the module execution info such as the total duration and self time spent on executing the module.
* Returns a function to call once the module has finished executing.
*/
protected startCalculateModuleExecutionInfo(filename: string, startOffset: number): () => ModuleExecutionInfoEntry {
const startTime = this.performanceNow()
this.executionStack.push({
filename,
startTime,
subImportTime: 0,
})
return () => {
const duration = this.performanceNow() - startTime
const currentExecution = this.executionStack.pop()
if (currentExecution == null) {
throw new Error('Execution stack is empty, this should never happen')
}
const selfTime = duration - currentExecution.subImportTime
if (this.executionStack.length > 0) {
this.executionStack.at(-1)!.subImportTime += duration
}
return {
startOffset,
duration,
selfTime,
}
}
}
prepareContext(context: Record<string, any>): Record<string, any> {
return context
}
/**
* Define if a module should be interop-ed
* This function mostly for the ability to override by subclass
*/
shouldInterop(path: string, mod: any): boolean {
if (this.options.interopDefault === false) {
return false
}
// never interop ESM modules
// TODO: should also skip for `.js` with `type="module"`
return !path.endsWith('.mjs') && 'default' in mod
}
protected importExternalModule(path: string): Promise<any> {
return import(/* @vite-ignore */ path)
}
/**
* Import a module and interop it
*/
async interopedImport(path: string): Promise<any> {
const importedModule = await this.importExternalModule(path)
if (!this.shouldInterop(path, importedModule)) {
return importedModule
}
const { mod, defaultExport } = interopModule(importedModule)
return new Proxy(mod, {
get(mod, prop) {
if (prop === 'default') {
return defaultExport
}
return mod[prop] ?? defaultExport?.[prop]
},
has(mod, prop) {
if (prop === 'default') {
return defaultExport !== undefined
}
return prop in mod || (defaultExport && prop in defaultExport)
},
getOwnPropertyDescriptor(mod, prop) {
const descriptor = Reflect.getOwnPropertyDescriptor(mod, prop)
if (descriptor) {
return descriptor
}
if (prop === 'default' && defaultExport !== undefined) {
return {
value: defaultExport,
enumerable: true,
configurable: true,
}
}
},
})
}
}
function interopModule(mod: any) {
if (isPrimitive(mod)) {
return {
mod: { default: mod },
defaultExport: mod,
}
}
let defaultExport = 'default' in mod ? mod.default : mod
if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) {
mod = defaultExport
if ('default' in defaultExport) {
defaultExport = defaultExport.default
}
}
return { mod, defaultExport }
}
// keep consistency with Vite on how exports are defined
function defineExport(exports: any, key: string | symbol, value: () => any) {
Object.defineProperty(exports, key, {
enumerable: true,
configurable: true,
get: value,
})
}
function exportAll(exports: any, sourceModule: any) {
// #1120 when a module exports itself it causes
// call stack error
if (exports === sourceModule) {
return
}
if (
isPrimitive(sourceModule)
|| Array.isArray(sourceModule)
|| sourceModule instanceof Promise
) {
return
}
for (const key in sourceModule) {
if (key !== 'default' && !(key in exports)) {
try {
defineExport(exports, key, () => sourceModule[key])
}
catch {}
}
}
}

View File

@ -1,41 +0,0 @@
export const KNOWN_ASSET_TYPES: string[] = [
// images
'apng',
'bmp',
'png',
'jpe?g',
'jfif',
'pjpeg',
'pjp',
'gif',
'svg',
'ico',
'webp',
'avif',
// media
'mp4',
'webm',
'ogg',
'mp3',
'wav',
'flac',
'aac',
// fonts
'woff2?',
'eot',
'ttf',
'otf',
// other
'webmanifest',
'pdf',
'txt',
]
export const KNOWN_ASSET_RE: RegExp = new RegExp(
`\\.(${KNOWN_ASSET_TYPES.join('|')})$`,
)
export const CSS_LANGS_RE: RegExp
= /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/

View File

@ -1,110 +0,0 @@
import type { TransformResult } from 'vite'
import type { DebuggerOptions } from './types'
/* eslint-disable no-console */
import { existsSync, promises as fs } from 'node:fs'
import { join, resolve } from 'pathe'
import c from 'tinyrainbow'
function hashCode(s: string) {
return s.split('').reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0)
return a & a
}, 0)
}
export class Debugger {
dumpDir: string | undefined
initPromise: Promise<void> | undefined
externalizeMap: Map<string, string> = new Map()
constructor(root: string, public options: DebuggerOptions) {
if (options.dumpModules) {
this.dumpDir = resolve(
root,
options.dumpModules === true ? '.vite-node/dump' : options.dumpModules,
)
}
if (this.dumpDir) {
if (options.loadDumppedModules) {
console.info(
c.gray(`[vite-node] [debug] load modules from ${this.dumpDir}`),
)
}
else {
console.info(
c.gray(`[vite-node] [debug] dump modules to ${this.dumpDir}`),
)
}
}
this.initPromise = this.clearDump()
}
async clearDump(): Promise<void> {
if (!this.dumpDir) {
return
}
if (!this.options.loadDumppedModules && existsSync(this.dumpDir)) {
await fs.rm(this.dumpDir, { recursive: true, force: true })
}
await fs.mkdir(this.dumpDir, { recursive: true })
}
encodeId(id: string) {
return `${id.replace(/[^\w@\-]/g, '_').replace(/_+/g, '_')}-${hashCode(
id,
)}.js`
}
async recordExternalize(id: string, path: string): Promise<void> {
if (!this.dumpDir) {
return
}
this.externalizeMap.set(id, path)
await this.writeInfo()
}
async dumpFile(id: string, result: TransformResult | null): Promise<void> {
if (!result || !this.dumpDir) {
return
}
await this.initPromise
const name = this.encodeId(id)
return await fs.writeFile(
join(this.dumpDir, name),
`// ${id.replace(/\0/g, '\\0')}\n${result.code}`,
'utf-8',
)
}
async loadDump(id: string): Promise<TransformResult | null> {
if (!this.dumpDir) {
return null
}
await this.initPromise
const name = this.encodeId(id)
const path = join(this.dumpDir, name)
if (!existsSync(path)) {
return null
}
const code = await fs.readFile(path, 'utf-8')
return {
code: code.replace(/^\/\/.*\n/, ''),
map: undefined!,
}
}
async writeInfo(): Promise<void> {
if (!this.dumpDir) {
return
}
const info = JSON.stringify(
{
time: new Date().toLocaleString(),
externalize: Object.fromEntries(this.externalizeMap.entries()),
},
null,
2,
)
return fs.writeFile(join(this.dumpDir, 'info.json'), info, 'utf-8')
}
}

View File

@ -1,191 +0,0 @@
import type { DepsHandlingOptions } from './types'
import { existsSync, promises as fsp } from 'node:fs'
import * as esModuleLexer from 'es-module-lexer'
import { dirname, extname, join } from 'pathe'
import { KNOWN_ASSET_RE } from './constants'
import { findNearestPackageData, isNodeBuiltin, slash } from './utils'
const BUILTIN_EXTENSIONS = new Set(['.mjs', '.cjs', '.node', '.wasm'])
const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/
const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/
const defaultInline = [
/virtual:/,
/\.[mc]?ts$/,
// special Vite query strings
/[?&](init|raw|url|inline)\b/,
// Vite returns a string for assets imports, even if it's inside "node_modules"
KNOWN_ASSET_RE,
]
const depsExternal = [
/\/node_modules\/.*\.cjs\.js$/,
/\/node_modules\/.*\.mjs$/,
]
export function guessCJSversion(id: string): string | undefined {
if (id.match(ESM_EXT_RE)) {
for (const i of [
id.replace(ESM_EXT_RE, '.mjs'),
id.replace(ESM_EXT_RE, '.umd.js'),
id.replace(ESM_EXT_RE, '.cjs.js'),
id.replace(ESM_EXT_RE, '.js'),
]) {
if (existsSync(i)) {
return i
}
}
}
if (id.match(ESM_FOLDER_RE)) {
for (const i of [
id.replace(ESM_FOLDER_RE, '/umd/$1'),
id.replace(ESM_FOLDER_RE, '/cjs/$1'),
id.replace(ESM_FOLDER_RE, '/lib/$1'),
id.replace(ESM_FOLDER_RE, '/$1'),
]) {
if (existsSync(i)) {
return i
}
}
}
}
// The code from https://github.com/unjs/mlly/blob/c5bcca0cda175921344fd6de1bc0c499e73e5dac/src/syntax.ts#L51-L98
async function isValidNodeImport(id: string) {
const extension = extname(id)
if (BUILTIN_EXTENSIONS.has(extension)) {
return true
}
if (extension !== '.js') {
return false
}
id = id.replace('file:///', '')
const package_ = await findNearestPackageData(dirname(id))
if (package_.type === 'module') {
return true
}
if (/\.(?:\w+-)?esm?(?:-\w+)?\.js$|\/esm?\//.test(id)) {
return false
}
try {
await esModuleLexer.init
const code = await fsp.readFile(id, 'utf8')
const [, , , hasModuleSyntax] = esModuleLexer.parse(code)
return !hasModuleSyntax
}
catch {
return false
}
}
const _defaultExternalizeCache = new Map<string, Promise<string | false>>()
export async function shouldExternalize(
id: string,
options?: DepsHandlingOptions,
cache: Map<string, Promise<string | false>> = _defaultExternalizeCache,
): Promise<string | false> {
if (!cache.has(id)) {
cache.set(id, _shouldExternalize(id, options))
}
return cache.get(id)!
}
async function _shouldExternalize(
id: string,
options?: DepsHandlingOptions,
): Promise<string | false> {
if (isNodeBuiltin(id)) {
return id
}
// data: should be processed by native import,
// since it is a feature of ESM.
// also externalize network imports since nodejs allows it when --experimental-network-imports
if (id.startsWith('data:') || /^(?:https?:)?\/\//.test(id)) {
return id
}
id = patchWindowsImportPath(id)
const moduleDirectories = options?.moduleDirectories || ['/node_modules/']
if (matchExternalizePattern(id, moduleDirectories, options?.inline)) {
return false
}
if (options?.inlineFiles && options?.inlineFiles.includes(id)) {
return false
}
if (matchExternalizePattern(id, moduleDirectories, options?.external)) {
return id
}
// Unless the user explicitly opted to inline them, externalize Vite deps.
// They are too big to inline by default.
if (options?.cacheDir && id.includes(options.cacheDir)) {
return id
}
const isLibraryModule = moduleDirectories.some(dir => id.includes(dir))
const guessCJS = isLibraryModule && options?.fallbackCJS
id = guessCJS ? guessCJSversion(id) || id : id
if (matchExternalizePattern(id, moduleDirectories, defaultInline)) {
return false
}
if (matchExternalizePattern(id, moduleDirectories, depsExternal)) {
return id
}
if (isLibraryModule && (await isValidNodeImport(id))) {
return id
}
return false
}
function matchExternalizePattern(
id: string,
moduleDirectories: string[],
patterns?: (string | RegExp)[] | true,
) {
if (patterns == null) {
return false
}
if (patterns === true) {
return true
}
for (const ex of patterns) {
if (typeof ex === 'string') {
if (moduleDirectories.some(dir => id.includes(join(dir, ex)))) {
return true
}
}
else {
if (ex.test(id)) {
return true
}
}
}
return false
}
function patchWindowsImportPath(path: string) {
if (path.match(/^\w:\\/)) {
return `file:///${slash(path)}`
}
else if (path.match(/^\w:\//)) {
return `file:///${path}`
}
else {
return path
}
}

View File

@ -1,77 +0,0 @@
import type { HMRPayload, Plugin } from 'vite'
import { EventEmitter } from 'node:events'
export type EventType = string | symbol
export type Handler<T = unknown> = (event: T) => void
export interface Emitter<Events extends Record<EventType, unknown>> {
on: <Key extends keyof Events>(
type: Key,
handler: Handler<Events[Key]>
) => void
off: <Key extends keyof Events>(
type: Key,
handler?: Handler<Events[Key]>
) => void
emit: (<Key extends keyof Events>(type: Key, event: Events[Key]) => void)
& (<Key extends keyof Events>(
type: undefined extends Events[Key] ? Key : never
) => void)
}
export type HMREmitter = Emitter<{
message: HMRPayload
}>
& EventEmitter
declare module 'vite' {
interface ViteDevServer {
emitter: HMREmitter
}
}
export function createHmrEmitter(): HMREmitter {
const emitter = new EventEmitter()
return emitter as HMREmitter
}
export function viteNodeHmrPlugin(): Plugin {
const emitter = createHmrEmitter()
return {
name: 'vite-node:hmr',
config() {
// chokidar fsevents is unstable on macos when emitting "ready" event
if (
process.platform === 'darwin'
&& process.env.VITE_TEST_WATCHER_DEBUG
) {
return {
server: {
watch: {
useFsEvents: false,
usePolling: false,
},
},
}
}
},
configureServer(server) {
const _send = server.ws.send
server.emitter = emitter
server.ws.send = function (payload: any) {
_send(payload)
emitter.emit('message', payload)
}
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore Vite 6 compat
const environments = server.environments
if (environments) {
environments.ssr.hot.send = function (payload: any) {
_send(payload)
emitter.emit('message', payload)
}
}
},
}
}

View File

@ -1,349 +0,0 @@
/* eslint-disable no-console */
import type { CustomEventMap } from 'vite/types/customEvent.js'
import type { HMRPayload, Update } from 'vite/types/hmrPayload.js'
import type { ViteNodeRunner } from '../client'
import type { HotContext } from '../types'
import type { HMREmitter } from './emitter'
import createDebug from 'debug'
import c from 'tinyrainbow'
import { normalizeRequestId } from '../utils'
export type ModuleNamespace = Record<string, any> & {
[Symbol.toStringTag]: 'Module'
}
const debugHmr = createDebug('vite-node:hmr')
export type InferCustomEventPayload<T extends string>
= T extends keyof CustomEventMap ? CustomEventMap[T] : any
export interface HotModule {
id: string
callbacks: HotCallback[]
}
export interface HotCallback {
// the dependencies must be fetchable paths
deps: string[]
fn: (modules: (ModuleNamespace | undefined)[]) => void
}
interface CacheData {
hotModulesMap: Map<string, HotModule>
dataMap: Map<string, any>
disposeMap: Map<string, (data: any) => void | Promise<void>>
pruneMap: Map<string, (data: any) => void | Promise<void>>
customListenersMap: Map<string, ((data: any) => void)[]>
ctxToListenersMap: Map<string, Map<string, ((data: any) => void)[]>>
messageBuffer: string[]
isFirstUpdate: boolean
pending: boolean
queued: Promise<(() => void) | undefined>[]
}
const cache: WeakMap<ViteNodeRunner, CacheData> = new WeakMap()
export function getCache(runner: ViteNodeRunner): CacheData {
if (!cache.has(runner)) {
cache.set(runner, {
hotModulesMap: new Map(),
dataMap: new Map(),
disposeMap: new Map(),
pruneMap: new Map(),
customListenersMap: new Map(),
ctxToListenersMap: new Map(),
messageBuffer: [],
isFirstUpdate: false,
pending: false,
queued: [],
})
}
return cache.get(runner) as CacheData
}
export function sendMessageBuffer(runner: ViteNodeRunner, emitter: HMREmitter): void {
const maps = getCache(runner)
maps.messageBuffer.forEach(msg => emitter.emit('custom', msg))
maps.messageBuffer.length = 0
}
export async function reload(runner: ViteNodeRunner, files: string[]): Promise<any[]> {
// invalidate module cache but not node_modules
Array.from(runner.moduleCache.keys()).forEach((fsPath) => {
if (!fsPath.includes('node_modules')) {
runner.moduleCache.delete(fsPath)
}
})
return Promise.all(files.map(file => runner.executeId(file)))
}
async function notifyListeners<T extends string>(
runner: ViteNodeRunner,
event: T,
data: InferCustomEventPayload<T>
): Promise<void>
async function notifyListeners(
runner: ViteNodeRunner,
event: string,
data: any,
): Promise<void> {
const maps = getCache(runner)
const cbs = maps.customListenersMap.get(event)
if (cbs) {
await Promise.all(cbs.map(cb => cb(data)))
}
}
async function queueUpdate(
runner: ViteNodeRunner,
p: Promise<(() => void) | undefined>,
) {
const maps = getCache(runner)
maps.queued.push(p)
if (!maps.pending) {
maps.pending = true
await Promise.resolve()
maps.pending = false
const loading = [...maps.queued]
maps.queued = [];
(await Promise.all(loading)).forEach(fn => fn && fn())
}
}
async function fetchUpdate(
runner: ViteNodeRunner,
{ path, acceptedPath }: Update,
) {
path = normalizeRequestId(path)
acceptedPath = normalizeRequestId(acceptedPath)
const maps = getCache(runner)
const mod = maps.hotModulesMap.get(path)
if (!mod) {
// In a code-splitting project,
// it is common that the hot-updating module is not loaded yet.
// https://github.com/vitejs/vite/issues/721
return
}
const isSelfUpdate = path === acceptedPath
let fetchedModule: ModuleNamespace | undefined
// determine the qualified callbacks before we re-import the modules
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
deps.includes(acceptedPath),
)
if (isSelfUpdate || qualifiedCallbacks.length > 0) {
const disposer = maps.disposeMap.get(acceptedPath)
if (disposer) {
await disposer(maps.dataMap.get(acceptedPath))
}
try {
[fetchedModule] = await reload(runner, [acceptedPath])
}
catch (e: any) {
warnFailedFetch(e, acceptedPath)
}
}
return () => {
for (const { deps, fn } of qualifiedCallbacks) {
fn(deps.map(dep => (dep === acceptedPath ? fetchedModule : undefined)))
}
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`${c.cyan('[vite-node]')} hot updated: ${loggedPath}`)
}
}
function warnFailedFetch(err: unknown, path: string | string[]) {
if (!(err instanceof Error) || !err.message.match('fetch')) {
console.error(err)
}
console.error(
`[hmr] Failed to reload ${path}. `
+ 'This could be due to syntax errors or importing non-existent '
+ 'modules. (see errors above)',
)
}
export async function handleMessage(
runner: ViteNodeRunner,
emitter: HMREmitter,
files: string[],
payload: HMRPayload,
): Promise<void> {
const maps = getCache(runner)
switch (payload.type) {
case 'connected':
sendMessageBuffer(runner, emitter)
break
case 'update':
await notifyListeners(runner, 'vite:beforeUpdate', payload)
await Promise.all(
payload.updates.map((update) => {
if (update.type === 'js-update') {
return queueUpdate(runner, fetchUpdate(runner, update))
}
// css-update
console.error(`${c.cyan('[vite-node]')} no support css hmr.}`)
return null
}),
)
await notifyListeners(runner, 'vite:afterUpdate', payload)
break
case 'full-reload':
await notifyListeners(runner, 'vite:beforeFullReload', payload)
maps.customListenersMap.delete('vite:beforeFullReload')
await reload(runner, files)
break
case 'custom':
await notifyListeners(runner, payload.event, payload.data)
break
case 'prune':
await notifyListeners(runner, 'vite:beforePrune', payload)
payload.paths.forEach((path) => {
const fn = maps.pruneMap.get(path)
if (fn) {
fn(maps.dataMap.get(path))
}
})
break
case 'error': {
await notifyListeners(runner, 'vite:error', payload)
const err = payload.err
console.error(
`${c.cyan('[vite-node]')} Internal Server Error\n${err.message}\n${
err.stack
}`,
)
break
}
}
}
export function createHotContext(
runner: ViteNodeRunner,
emitter: HMREmitter,
files: string[],
ownerPath: string,
): HotContext {
debugHmr('createHotContext', ownerPath)
const maps = getCache(runner)
if (!maps.dataMap.has(ownerPath)) {
maps.dataMap.set(ownerPath, {})
}
// when a file is hot updated, a new context is created
// clear its stale callbacks
const mod = maps.hotModulesMap.get(ownerPath)
if (mod) {
mod.callbacks = []
}
const newListeners = new Map()
maps.ctxToListenersMap.set(ownerPath, newListeners)
function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
const mod: HotModule = maps.hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
}
mod.callbacks.push({
deps,
fn: callback,
})
maps.hotModulesMap.set(ownerPath, mod)
}
const hot: HotContext = {
get data() {
return maps.dataMap.get(ownerPath)
},
acceptExports(_, callback?: any) {
acceptDeps([ownerPath], callback && (([mod]) => callback(mod)))
},
accept(deps?: any, callback?: any) {
if (typeof deps === 'function' || !deps) {
// self-accept: hot.accept(() => {})
acceptDeps([ownerPath], ([mod]) => deps && deps(mod))
}
else if (typeof deps === 'string') {
// explicit deps
acceptDeps([deps], ([mod]) => callback && callback(mod))
}
else if (Array.isArray(deps)) {
acceptDeps(deps, callback)
}
else {
throw new TypeError('invalid hot.accept() usage.')
}
},
dispose(cb) {
maps.disposeMap.set(ownerPath, cb)
},
prune(cb: (data: any) => void) {
maps.pruneMap.set(ownerPath, cb)
},
invalidate() {
notifyListeners(runner, 'vite:invalidate', {
path: ownerPath,
message: undefined,
firstInvalidatedBy: ownerPath,
})
return reload(runner, files)
},
on<T extends string>(
event: T,
cb: (payload: InferCustomEventPayload<T>) => void,
): void {
const addToMap = (map: Map<string, any[]>) => {
const existing = map.get(event) || []
existing.push(cb)
map.set(event, existing)
}
addToMap(maps.customListenersMap)
addToMap(newListeners)
},
off<T extends string>(
event: T,
cb: (payload: InferCustomEventPayload<T>) => void,
) {
const removeFromMap = (map: Map<string, any[]>) => {
const existing = map.get(event)
if (existing === undefined) {
return
}
const pruned = existing.filter(l => l !== cb)
if (pruned.length === 0) {
map.delete(event)
return
}
map.set(event, pruned)
}
removeFromMap(maps.customListenersMap)
removeFromMap(newListeners)
},
send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void {
maps.messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
sendMessageBuffer(runner, emitter)
},
}
return hot
}

View File

@ -1,2 +0,0 @@
export * from './emitter'
export * from './hmr'

View File

@ -1 +0,0 @@
export * from './types'

View File

@ -1,441 +0,0 @@
import type { TransformResult, ViteDevServer } from 'vite'
import type {
DebuggerOptions,
EncodedSourceMap,
FetchResult,
ViteNodeResolveId,
ViteNodeServerOptions,
} from './types'
import assert from 'node:assert'
import { existsSync } from 'node:fs'
import { performance } from 'node:perf_hooks'
import { pathToFileURL } from 'node:url'
import createDebug from 'debug'
import { join, normalize, relative, resolve } from 'pathe'
import { version as viteVersion } from 'vite'
import { Debugger } from './debug'
import { shouldExternalize } from './externalize'
import { withInlineSourcemap } from './source-map'
import {
normalizeModuleId,
toArray,
toFilePath,
withTrailingSlash,
} from './utils'
export * from './externalize'
interface FetchCache {
duration?: number
timestamp: number
result: FetchResult
}
const debugRequest = createDebug('vite-node:server:request')
export class ViteNodeServer {
private fetchPromiseMap = {
ssr: new Map<string, Promise<FetchResult>>(),
web: new Map<string, Promise<FetchResult>>(),
}
private transformPromiseMap = {
ssr: new Map<string, Promise<TransformResult | null | undefined>>(),
web: new Map<string, Promise<TransformResult | null | undefined>>(),
}
private durations = {
ssr: new Map<string, number[]>(),
web: new Map<string, number[]>(),
}
private existingOptimizedDeps = new Set<string>()
fetchCaches: Record<'ssr' | 'web', Map<string, FetchCache>> = {
ssr: new Map(),
web: new Map(),
}
fetchCache: Map<string, FetchCache> = new Map()
externalizeCache: Map<string, Promise<string | false>> = new Map()
debugger?: Debugger
constructor(
public server: ViteDevServer,
public options: ViteNodeServerOptions = {},
) {
const ssrOptions = server.config.ssr
options.deps ??= {}
options.deps.cacheDir = relative(
server.config.root,
options.deps.cacheDir || server.config.cacheDir,
)
if (ssrOptions) {
// we don't externalize ssr, because it has different semantics in Vite
// if (ssrOptions.external) {
// options.deps.external ??= []
// options.deps.external.push(...ssrOptions.external)
// }
if (ssrOptions.noExternal === true) {
options.deps.inline ??= true
}
else if (options.deps.inline !== true) {
options.deps.inline ??= []
const inline = options.deps.inline
options.deps.inline.push(
...toArray(ssrOptions.noExternal).filter(
dep => !inline.includes(dep),
),
)
}
}
if (process.env.VITE_NODE_DEBUG_DUMP) {
options.debug = Object.assign(
<DebuggerOptions>{
dumpModules: !!process.env.VITE_NODE_DEBUG_DUMP,
loadDumppedModules: process.env.VITE_NODE_DEBUG_DUMP === 'load',
},
options.debug ?? {},
)
}
if (options.debug) {
this.debugger = new Debugger(server.config.root, options.debug!)
}
if (options.deps.inlineFiles) {
options.deps.inlineFiles = options.deps.inlineFiles.flatMap((file) => {
if (file.startsWith('file://')) {
return file
}
const resolvedId = resolve(file)
return [resolvedId, pathToFileURL(resolvedId).href]
})
}
options.deps.moduleDirectories ??= []
const envValue
= process.env.VITE_NODE_DEPS_MODULE_DIRECTORIES
|| process.env.npm_config_VITE_NODE_DEPS_MODULE_DIRECTORIES
const customModuleDirectories = envValue?.split(',')
if (customModuleDirectories) {
options.deps.moduleDirectories.push(...customModuleDirectories)
}
options.deps.moduleDirectories = options.deps.moduleDirectories.map(
(dir) => {
if (dir[0] !== '/') {
dir = `/${dir}`
}
if (!dir.endsWith('/')) {
dir += '/'
}
return normalize(dir)
},
)
// always add node_modules as a module directory
if (!options.deps.moduleDirectories.includes('/node_modules/')) {
options.deps.moduleDirectories.push('/node_modules/')
}
}
shouldExternalize(id: string): Promise<string | false> {
return shouldExternalize(id, this.options.deps, this.externalizeCache)
}
public getTotalDuration(): number {
const ssrDurations = [...this.durations.ssr.values()].flat()
const webDurations = [...this.durations.web.values()].flat()
return [...ssrDurations, ...webDurations].reduce((a, b) => a + b, 0)
}
private async ensureExists(id: string): Promise<boolean> {
if (this.existingOptimizedDeps.has(id)) {
return true
}
if (existsSync(id)) {
this.existingOptimizedDeps.add(id)
return true
}
return new Promise<boolean>((resolve) => {
setTimeout(() => {
this.ensureExists(id).then(() => {
resolve(true)
})
})
})
}
async resolveId(
id: string,
importer?: string,
transformMode?: 'web' | 'ssr',
): Promise<ViteNodeResolveId | null> {
if (
importer
&& !importer.startsWith(withTrailingSlash(this.server.config.root))
) {
importer = resolve(this.server.config.root, importer)
}
const mode
= transformMode ?? ((importer && this.getTransformMode(importer)) || 'ssr')
return this.server.pluginContainer.resolveId(id, importer, {
ssr: mode === 'ssr',
})
}
getSourceMap(source: string): EncodedSourceMap | null {
source = normalizeModuleId(source)
const fetchResult = this.fetchCache.get(source)?.result
if (fetchResult?.map) {
return fetchResult.map
}
const ssrTransformResult
= this.server.moduleGraph.getModuleById(source)?.ssrTransformResult
return (ssrTransformResult?.map
|| null) as unknown as EncodedSourceMap | null
}
private assertMode(mode: 'web' | 'ssr') {
assert(
mode === 'web' || mode === 'ssr',
`"transformMode" can only be "web" or "ssr", received "${mode}".`,
)
}
async fetchModule(
id: string,
transformMode?: 'web' | 'ssr',
): Promise<FetchResult> {
const mode = transformMode || this.getTransformMode(id)
return this.fetchResult(id, mode).then((r) => {
return this.options.sourcemap !== true ? { ...r, map: undefined } : r
})
}
async fetchResult(id: string, mode: 'web' | 'ssr'): Promise<FetchResult> {
const moduleId = normalizeModuleId(id)
this.assertMode(mode)
const promiseMap = this.fetchPromiseMap[mode]
// reuse transform for concurrent requests
if (!promiseMap.has(moduleId)) {
promiseMap.set(
moduleId,
this._fetchModule(moduleId, mode).finally(() => {
promiseMap.delete(moduleId)
}),
)
}
return promiseMap.get(moduleId)!
}
async transformRequest(
id: string,
filepath: string = id,
transformMode?: 'web' | 'ssr',
): Promise<TransformResult | null | undefined> {
const mode = transformMode || this.getTransformMode(id)
this.assertMode(mode)
const promiseMap = this.transformPromiseMap[mode]
// reuse transform for concurrent requests
if (!promiseMap.has(id)) {
promiseMap.set(
id,
this._transformRequest(id, filepath, mode).finally(() => {
promiseMap.delete(id)
}),
)
}
return promiseMap.get(id)!
}
async transformModule(id: string, transformMode?: 'web' | 'ssr'): Promise<{
code: string | undefined
}> {
if (transformMode !== 'web') {
throw new Error(
'`transformModule` only supports `transformMode: "web"`.',
)
}
const normalizedId = normalizeModuleId(id)
const mod = this.server.moduleGraph.getModuleById(normalizedId)
const result
= mod?.transformResult
|| (await this.server.transformRequest(normalizedId))
return {
code: result?.code,
}
}
getTransformMode(id: string): 'ssr' | 'web' {
const withoutQuery = id.split('?')[0]
if (this.options.transformMode?.web?.some(r => withoutQuery.match(r))) {
return 'web'
}
if (this.options.transformMode?.ssr?.some(r => withoutQuery.match(r))) {
return 'ssr'
}
if (withoutQuery.match(/\.([cm]?[jt]sx?|json)$/)) {
return 'ssr'
}
return 'web'
}
private getChangedModule(id: string, file: string) {
const module
= this.server.moduleGraph.getModuleById(id)
|| this.server.moduleGraph.getModuleById(file)
if (module) {
return module
}
const _modules = this.server.moduleGraph.getModulesByFile(file)
if (!_modules || !_modules.size) {
return null
}
// find the latest changed module
const modules = [..._modules]
let mod = modules[0]
let latestMax = -1
for (const m of _modules) {
const timestamp = Math.max(
m.lastHMRTimestamp,
m.lastInvalidationTimestamp,
)
if (timestamp > latestMax) {
latestMax = timestamp
mod = m
}
}
return mod
}
private async _fetchModule(
id: string,
transformMode: 'web' | 'ssr',
): Promise<FetchResult> {
let result: FetchResult
const cacheDir = this.options.deps?.cacheDir
if (cacheDir && id.includes(cacheDir)) {
if (!id.startsWith(withTrailingSlash(this.server.config.root))) {
id = join(this.server.config.root, id)
}
const timeout = setTimeout(() => {
throw new Error(
`ViteNodeServer: ${id} not found. This is a bug, please report it.`,
)
}, 5000) // CI can be quite slow
await this.ensureExists(id)
clearTimeout(timeout)
}
const { path: filePath } = toFilePath(id, this.server.config.root)
const moduleNode = this.getChangedModule(id, filePath)
const cache = this.fetchCaches[transformMode].get(filePath)
// lastUpdateTimestamp is the timestamp that marks the last time the module was changed
// if lastUpdateTimestamp is 0, then the module was not changed since the server started
// we test "timestamp === 0" for expressiveness, but it's not necessary
const timestamp = moduleNode
? Math.max(
moduleNode.lastHMRTimestamp,
moduleNode.lastInvalidationTimestamp,
)
: 0
if (cache && (timestamp === 0 || cache.timestamp >= timestamp)) {
return cache.result
}
const time = Date.now()
const externalize = await this.shouldExternalize(filePath)
let duration: number | undefined
if (externalize) {
result = { externalize }
this.debugger?.recordExternalize(id, externalize)
}
else {
const start = performance.now()
const r = await this._transformRequest(id, filePath, transformMode)
duration = performance.now() - start
result = { code: r?.code, map: r?.map as any }
}
const cacheEntry = {
duration,
timestamp: time,
result,
}
const durations = this.durations[transformMode].get(filePath) || []
this.durations[transformMode].set(filePath, [...durations, duration ?? 0])
this.fetchCaches[transformMode].set(filePath, cacheEntry)
this.fetchCache.set(filePath, cacheEntry)
return result
}
protected async processTransformResult(
filepath: string,
result: TransformResult,
): Promise<TransformResult> {
const mod = this.server.moduleGraph.getModuleById(filepath)
return withInlineSourcemap(result, {
filepath: mod?.file || filepath,
root: this.server.config.root,
noFirstLineMapping: Number(viteVersion.split('.')[0]) >= 6,
})
}
private async _transformRequest(
id: string,
filepath: string,
transformMode: 'web' | 'ssr',
) {
debugRequest(id)
let result: TransformResult | null = null
if (this.options.debug?.loadDumppedModules) {
result = (await this.debugger?.loadDump(id)) ?? null
if (result) {
return result
}
}
if (transformMode === 'web') {
// for components like Vue, we want to use the client side
// plugins but then convert the code to be consumed by the server
result = await this.server.transformRequest(id)
if (result) {
result = await this.server.ssrTransform(result.code, result.map, id)
}
}
else {
result = await this.server.transformRequest(id, { ssr: true })
}
const sourcemap = this.options.sourcemap ?? 'inline'
if (sourcemap === 'inline' && result) {
result = await this.processTransformResult(filepath, result)
}
if (this.options.debug?.dumpModules) {
await this.debugger?.dumpFile(id, result)
}
return result
}
}

View File

@ -1,511 +0,0 @@
// adapted from https://github.com/evanw/node-source-map-support/blob/master/source-map-support.js
// we need this because "--enable-source-maps" flag doesn't support VM
// we make a custom implementation because we don't need some features from source-map-support,
// like polyfilled Buffer, browser support, etc.
import type {
OriginalMapping,
SourceMapInput,
} from '@jridgewell/trace-mapping'
import fs from 'node:fs'
import path from 'node:path'
import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'
// Only install once if called multiple times
let errorFormatterInstalled = false
// Maps a file path to a string containing the file contents
const fileContentsCache: Record<string, string> = {}
// Maps a file path to a source map for that file
const sourceMapCache: Record<
string,
{ url: string | null; map: TraceMap | null }
> = {}
// Regex for detecting source maps
const reSourceMap = /^data:application\/json[^,]+base64,/
type RetrieveFileHandler = (path: string) => string | null | undefined
type RetrieveMapHandler = (
source: string
) => { url: string; map?: string | SourceMapInput | null } | null | undefined
// Priority list of retrieve handlers
let retrieveFileHandlers: RetrieveFileHandler[] = []
let retrieveMapHandlers: RetrieveMapHandler[] = []
function globalProcessVersion() {
if (typeof process === 'object' && process !== null) {
return process.version
}
else {
return ''
}
}
function handlerExec<T>(list: ((arg: string) => T)[]) {
return function (arg: string) {
for (let i = 0; i < list.length; i++) {
const ret = list[i](arg)
if (ret) {
return ret
}
}
return null
}
}
let retrieveFile = handlerExec(retrieveFileHandlers)
retrieveFileHandlers.push((path) => {
// Trim the path to make sure there is no extra whitespace.
path = path.trim()
if (path.startsWith('file:')) {
// existsSync/readFileSync can't handle file protocol, but once stripped, it works
path = path.replace(/file:\/\/\/(\w:)?/, (protocol, drive) => {
return drive
? '' // file:///C:/dir/file -> C:/dir/file
: '/' // file:///root-dir/file -> /root-dir/file
})
}
if (path in fileContentsCache) {
return fileContentsCache[path]
}
let contents = ''
try {
if (fs.existsSync(path)) {
contents = fs.readFileSync(path, 'utf8')
}
}
catch {
/* ignore any errors */
}
return (fileContentsCache[path] = contents)
})
// Support URLs relative to a directory, but be careful about a protocol prefix
function supportRelativeURL(file: string, url: string) {
if (!file) {
return url
}
const dir = path.dirname(file)
const match = /^\w+:\/\/[^/]*/.exec(dir)
let protocol = match ? match[0] : ''
const startPath = dir.slice(protocol.length)
if (protocol && /^\/\w:/.test(startPath)) {
// handle file:///C:/ paths
protocol += '/'
return (
protocol
+ path.resolve(dir.slice(protocol.length), url).replace(/\\/g, '/')
)
}
return protocol + path.resolve(dir.slice(protocol.length), url)
}
function retrieveSourceMapURL(source: string) {
// Get the URL of the source map
const fileData = retrieveFile(source)
if (!fileData) {
return null
}
const re
= /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm
// Keep executing the search to find the *last* sourceMappingURL to avoid
// picking up sourceMappingURLs from comments, strings, etc.
let lastMatch, match
// eslint-disable-next-line no-cond-assign
while ((match = re.exec(fileData))) {
lastMatch = match
}
if (!lastMatch) {
return null
}
return lastMatch[1]
}
// Can be overridden by the retrieveSourceMap option to install. Takes a
// generated source filename; returns a {map, optional url} object, or null if
// there is no source map. The map field may be either a string or the parsed
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
// constructor).
let retrieveSourceMap = handlerExec(retrieveMapHandlers)
retrieveMapHandlers.push((source) => {
let sourceMappingURL = retrieveSourceMapURL(source)
if (!sourceMappingURL) {
return null
}
// Read the contents of the source map
let sourceMapData
if (reSourceMap.test(sourceMappingURL)) {
// Support source map URL as a data url
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
sourceMapData = Buffer.from(rawData, 'base64').toString()
sourceMappingURL = source
}
else {
// Support source map URLs relative to the source URL
sourceMappingURL = supportRelativeURL(source, sourceMappingURL)
sourceMapData = retrieveFile(sourceMappingURL)
}
if (!sourceMapData) {
return null
}
return {
url: sourceMappingURL,
map: sourceMapData,
}
})
// interface Position {
// source: string
// line: number
// column: number
// }
function mapSourcePosition(position: OriginalMapping) {
if (!position.source) {
return position
}
let sourceMap = sourceMapCache[position.source]
if (!sourceMap) {
// Call the (overridable) retrieveSourceMap function to get the source map.
const urlAndMap = retrieveSourceMap(position.source)
const map = urlAndMap && urlAndMap.map
if (map && !(typeof map === 'object' && 'mappings' in map && map.mappings === '')) {
sourceMap = sourceMapCache[position.source] = {
url: urlAndMap.url,
map: new TraceMap(map),
}
// Load all sources stored inline with the source map into the file cache
// to pretend like they are already loaded. They may not exist on disk.
if (sourceMap.map?.sourcesContent) {
sourceMap.map.sources.forEach((source, i) => {
const contents = sourceMap.map?.sourcesContent?.[i]
if (contents && source && sourceMap.url) {
const url = supportRelativeURL(sourceMap.url, source)
fileContentsCache[url] = contents
}
})
}
}
else {
sourceMap = sourceMapCache[position.source] = {
url: null,
map: null,
}
}
}
// Resolve the source URL relative to the URL of the source map
if (sourceMap && sourceMap.map && sourceMap.url) {
const originalPosition = originalPositionFor(sourceMap.map, position)
// Only return the original position if a matching line was found. If no
// matching line is found then we return position instead, which will cause
// the stack trace to print the path and line for the compiled file. It is
// better to give a precise location in the compiled file than a vague
// location in the original file.
if (originalPosition.source !== null) {
originalPosition.source = supportRelativeURL(
sourceMap.url,
originalPosition.source,
)
return originalPosition
}
}
return position
}
// Parses code generated by FormatEvalOrigin(), a function inside V8:
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js
function mapEvalOrigin(origin: string): string {
// Most eval() calls are in this format
let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin)
if (match) {
const position = mapSourcePosition({
name: null,
source: match[2],
line: +match[3],
column: +match[4] - 1,
})
return `eval at ${match[1]} (${position.source}:${position.line}:${
position.column + 1
})`
}
// Parse nested eval() calls using recursion
match = /^eval at ([^(]+) \((.+)\)$/.exec(origin)
if (match) {
return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`
}
// Make sure we still return useful information if we didn't find anything
return origin
}
interface CallSite extends NodeJS.CallSite {
getScriptNameOrSourceURL: () => string
}
// This is copied almost verbatim from the V8 source code at
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js. The
// implementation of wrapCallSite() used to just forward to the actual source
// code of CallSite.prototype.toString but unfortunately a new release of V8
// did something to the prototype chain and broke the shim. The only fix I
// could find was copy/paste.
function CallSiteToString(this: CallSite) {
let fileName
let fileLocation = ''
if (this.isNative()) {
fileLocation = 'native'
}
else {
fileName = this.getScriptNameOrSourceURL()
if (!fileName && this.isEval()) {
fileLocation = this.getEvalOrigin() as string
fileLocation += ', ' // Expecting source position to follow.
}
if (fileName) {
fileLocation += fileName
}
else {
// Source code does not originate from a file and is not native, but we
// can still get the source position inside the source string, e.g. in
// an eval string.
fileLocation += '<anonymous>'
}
const lineNumber = this.getLineNumber()
if (lineNumber != null) {
fileLocation += `:${lineNumber}`
const columnNumber = this.getColumnNumber()
if (columnNumber) {
fileLocation += `:${columnNumber}`
}
}
}
let line = ''
const functionName = this.getFunctionName()
let addSuffix = true
const isConstructor = this.isConstructor()
const isMethodCall = !(this.isToplevel() || isConstructor)
if (isMethodCall) {
let typeName = this.getTypeName()
// Fixes shim to be backward compatible with Node v0 to v4
if (typeName === '[object Object]') {
typeName = 'null'
}
const methodName = this.getMethodName()
if (functionName) {
if (typeName && functionName.indexOf(typeName) !== 0) {
line += `${typeName}.`
}
line += functionName
if (
methodName
&& functionName.indexOf(`.${methodName}`)
!== functionName.length - methodName.length - 1
) {
line += ` [as ${methodName}]`
}
}
else {
line += `${typeName}.${methodName || '<anonymous>'}`
}
}
else if (isConstructor) {
line += `new ${functionName || '<anonymous>'}`
}
else if (functionName) {
line += functionName
}
else {
line += fileLocation
addSuffix = false
}
if (addSuffix) {
line += ` (${fileLocation})`
}
return line
}
function cloneCallSite(frame: CallSite) {
const object = {} as CallSite
Object.getOwnPropertyNames(Object.getPrototypeOf(frame)).forEach((name) => {
const key = name as keyof CallSite
// @ts-expect-error difficult to type
object[key] = /^(?:is|get)/.test(name)
? function () {
// eslint-disable-next-line no-useless-call
return frame[key].call(frame)
}
: frame[key]
})
object.toString = CallSiteToString
return object
}
interface State {
nextPosition: null | OriginalMapping
curPosition: null | OriginalMapping
}
function wrapCallSite(frame: CallSite, state: State) {
// provides interface backward compatibility
if (state === undefined) {
state = { nextPosition: null, curPosition: null }
}
if (frame.isNative()) {
state.curPosition = null
return frame
}
// Most call sites will return the source file from getFileName(), but code
// passed to eval() ending in "//# sourceURL=..." will return the source file
// from getScriptNameOrSourceURL() instead
const source = frame.getFileName() || frame.getScriptNameOrSourceURL()
if (source) {
const line = frame.getLineNumber() as number
let column = (frame.getColumnNumber() as number) - 1
// Fix position in Node where some (internal) code is prepended.
// See https://github.com/evanw/node-source-map-support/issues/36
// Header removed in node at ^10.16 || >=11.11.0
// v11 is not an LTS candidate, we can just test the one version with it.
// Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11
const noHeader
= /^v(?:10\.1[6-9]|10\.[2-9]\d|10\.\d{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/
const headerLength = noHeader.test(globalProcessVersion()) ? 0 : 62
if (line === 1 && column > headerLength && !frame.isEval()) {
column -= headerLength
}
const position = mapSourcePosition({
name: null,
source,
line,
column,
})
state.curPosition = position
frame = cloneCallSite(frame)
const originalFunctionName = frame.getFunctionName
frame.getFunctionName = function () {
if (state.nextPosition == null) {
return originalFunctionName()
}
return state.nextPosition.name || originalFunctionName()
}
frame.getFileName = function () {
return position.source ?? null
}
frame.getLineNumber = function () {
return position.line
}
frame.getColumnNumber = function () {
return position.column + 1
}
frame.getScriptNameOrSourceURL = function () {
return position.source as string
}
return frame
}
// Code called using eval() needs special handling
let origin = frame.isEval() && frame.getEvalOrigin()
if (origin) {
origin = mapEvalOrigin(origin)
frame = cloneCallSite(frame)
frame.getEvalOrigin = function () {
return origin || undefined
}
return frame
}
// If we get here then we were unable to change the source position
return frame
}
// This function is part of the V8 stack trace API, for more info see:
// https://v8.dev/docs/stack-trace-api
function prepareStackTrace(error: Error, stack: CallSite[]) {
const name = error.name || 'Error'
const message = error.message || ''
const errorString = `${name}: ${message}`
const state = { nextPosition: null, curPosition: null }
const processedStack = []
for (let i = stack.length - 1; i >= 0; i--) {
processedStack.push(`\n at ${wrapCallSite(stack[i], state)}`)
state.nextPosition = state.curPosition
}
state.curPosition = state.nextPosition = null
return errorString + processedStack.reverse().join('')
}
const originalRetrieveFileHandlers = retrieveFileHandlers.slice(0)
const originalRetrieveMapHandlers = retrieveMapHandlers.slice(0)
interface Options {
hookRequire?: boolean
overrideRetrieveFile?: boolean
overrideRetrieveSourceMap?: boolean
retrieveFile?: RetrieveFileHandler
retrieveSourceMap?: RetrieveMapHandler
}
export function install(options: Options): void {
options = options || {}
// Allow sources to be found by methods other than reading the files
// directly from disk.
if (options.retrieveFile) {
if (options.overrideRetrieveFile) {
retrieveFileHandlers.length = 0
}
retrieveFileHandlers.unshift(options.retrieveFile)
}
// Allow source maps to be found by methods other than reading the files
// directly from disk.
if (options.retrieveSourceMap) {
if (options.overrideRetrieveSourceMap) {
retrieveMapHandlers.length = 0
}
retrieveMapHandlers.unshift(options.retrieveSourceMap)
}
// Install the error reformatter
if (!errorFormatterInstalled) {
errorFormatterInstalled = true
Error.prepareStackTrace
= prepareStackTrace as ErrorConstructor['prepareStackTrace']
}
}
export function resetRetrieveHandlers(): void {
retrieveFileHandlers.length = 0
retrieveMapHandlers.length = 0
retrieveFileHandlers = originalRetrieveFileHandlers.slice(0)
retrieveMapHandlers = originalRetrieveMapHandlers.slice(0)
retrieveSourceMap = handlerExec(retrieveMapHandlers)
retrieveFile = handlerExec(retrieveFileHandlers)
}

View File

@ -1,110 +0,0 @@
import type { TransformResult } from 'vite'
import type { EncodedSourceMap } from './types'
import { dirname, isAbsolute, relative, resolve } from 'pathe'
import { install } from './source-map-handler'
import { withTrailingSlash } from './utils'
interface InstallSourceMapSupportOptions {
getSourceMap: (source: string) => EncodedSourceMap | null | undefined
}
let SOURCEMAPPING_URL = 'sourceMa'
SOURCEMAPPING_URL += 'ppingURL'
const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-node'
const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8`
export function withInlineSourcemap(
result: TransformResult,
options: {
root: string // project root path of this resource
filepath: string
noFirstLineMapping?: boolean
},
): TransformResult {
const map = result.map
let code = result.code
if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) {
return result
}
if ('sources' in map) {
map.sources = map.sources?.map((source) => {
if (!source) {
return source
}
// sometimes files here are absolute,
// but they are considered absolute to the server url, not the file system
// this is a bug in Vite
// all files should be either absolute to the file system or relative to the source map file
if (isAbsolute(source)) {
const actualPath
= !source.startsWith(withTrailingSlash(options.root))
&& source[0] === '/'
? resolve(options.root, source.slice(1))
: source
return relative(dirname(options.filepath), actualPath)
}
return source
})
}
// to reduce the payload size, we only inline vite node source map, because it's also the only one we use
const OTHER_SOURCE_MAP_REGEXP = new RegExp(
`//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,([A-Za-z0-9+/=]+)$`,
'gm',
)
while (OTHER_SOURCE_MAP_REGEXP.test(code)) {
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')
}
// If the first line is not present on source maps, add simple 1:1 mapping ([0,0,0,0], [1,0,0,0])
// so that debuggers can be set to break on first line
// Since Vite 6, import statements at the top of the file are preserved correctly,
// so we don't need to add this mapping anymore.
if (!options.noFirstLineMapping && map.mappings[0] === ';') {
map.mappings = `AAAA,CAAA${map.mappings}`
}
const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString(
'base64',
)
result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n`
return result
}
export function extractSourceMap(code: string): EncodedSourceMap | null {
const regexp = new RegExp(
`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`,
'gm',
)
let lastMatch!: RegExpExecArray | null, match!: RegExpExecArray | null
// eslint-disable-next-line no-cond-assign
while ((match = regexp.exec(code))) {
lastMatch = match
}
// pick only the last source map keeping user strings that look like maps
if (lastMatch) {
return JSON.parse(Buffer.from(lastMatch[1], 'base64').toString('utf-8'))
}
return null
}
export function installSourcemapsSupport(
options: InstallSourceMapSupportOptions,
): void {
install({
retrieveSourceMap(source) {
const map = options.getSourceMap(source)
if (map) {
return {
url: source,
map,
}
}
return null
},
})
}

View File

@ -1,144 +0,0 @@
import type { EncodedSourceMap } from '@jridgewell/trace-mapping'
import type { ViteHotContext } from 'vite/types/hot.js'
import type { ModuleCacheMap, ModuleExecutionInfo, ViteNodeRunner } from './client'
export type Nullable<T> = T | null | undefined
export type Arrayable<T> = T | Array<T>
export type Awaitable<T> = T | PromiseLike<T>
export interface DepsHandlingOptions {
external?: (string | RegExp)[]
inline?: (string | RegExp)[] | true
inlineFiles?: string[]
/**
* A list of directories that are considered to hold Node.js modules
* Have to include "/" at the start and end of the path
*
* Vite-Node checks the whole absolute path of the import, so make sure you don't include
* unwanted files accidentally
* @default ['/node_modules/']
*/
moduleDirectories?: string[]
cacheDir?: string
/**
* Try to guess the CJS version of a package when it's invalid ESM
* @default false
*/
fallbackCJS?: boolean
}
export interface StartOfSourceMap {
file?: string
sourceRoot?: string
}
export type {
DecodedSourceMap,
EncodedSourceMap,
SourceMapInput,
} from '@jridgewell/trace-mapping'
export interface RawSourceMap extends StartOfSourceMap {
version: number
sources: string[]
names: string[]
sourcesContent?: (string | null)[]
mappings: string
}
export interface FetchResult {
code?: string
externalize?: string
map?: EncodedSourceMap | null
}
export type HotContext = Omit<ViteHotContext, 'acceptDeps' | 'decline'>
export type FetchFunction = (id: string) => Promise<FetchResult>
export type ResolveIdFunction = (
id: string,
importer?: string
) => Awaitable<ViteNodeResolveId | null | undefined | void>
export type CreateHotContextFunction = (
runner: ViteNodeRunner,
url: string
) => HotContext
export interface ModuleCache {
promise?: Promise<any>
exports?: any
evaluated?: boolean
resolving?: boolean
code?: string
map?: EncodedSourceMap
/**
* Module ids that imports this module
*/
importers?: Set<string>
imports?: Set<string>
}
export interface ViteNodeRunnerOptions {
root: string
fetchModule: FetchFunction
resolveId?: ResolveIdFunction
createHotContext?: CreateHotContextFunction
base?: string
moduleCache?: ModuleCacheMap
moduleExecutionInfo?: ModuleExecutionInfo
interopDefault?: boolean
requestStubs?: Record<string, any>
debug?: boolean
}
export interface ViteNodeResolveId {
external?: boolean | 'absolute' | 'relative'
id: string
meta?: Record<string, any> | null
moduleSideEffects?: boolean | 'no-treeshake' | null
syntheticNamedExports?: boolean | string | null
}
export interface ViteNodeResolveModule {
external: string | null
id: string
fsPath: string
}
export interface ViteNodeServerOptions {
/**
* Inject inline sourcemap to modules
* @default 'inline'
*/
sourcemap?: 'inline' | boolean
/**
* Deps handling
*/
deps?: DepsHandlingOptions
/**
* Transform method for modules
*/
transformMode?: {
ssr?: RegExp[]
web?: RegExp[]
}
debug?: DebuggerOptions
}
export interface DebuggerOptions {
/**
* Dump the transformed module to filesystem
* Passing a string will dump to the specified path
*/
dumpModules?: boolean | string
/**
* Read dumpped module from filesystem whenever exists.
* Useful for debugging by modifying the dump result from the filesystem.
*/
loadDumppedModules?: boolean
}
export type { ModuleCacheMap, ModuleExecutionInfo }

View File

@ -1,317 +0,0 @@
import type { Arrayable, Nullable } from './types'
import { existsSync, promises as fsp } from 'node:fs'
import nodeModule from 'node:module'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { dirname, join, resolve } from 'pathe'
export const isWindows: boolean = process.platform === 'win32'
const drive = isWindows ? process.cwd()[0] : null
const driveOpposite = drive
? drive === drive.toUpperCase()
? drive.toLowerCase()
: drive.toUpperCase()
: null
const driveRegexp = drive ? new RegExp(`(?:^|/@fs/)${drive}(\:[\\/])`) : null
const driveOppositeRegext = driveOpposite
? new RegExp(`(?:^|/@fs/)${driveOpposite}(\:[\\/])`)
: null
export function slash(str: string): string {
return str.replace(/\\/g, '/')
}
const bareImportRE = /^(?![a-z]:)[\w@](?!.*:\/\/)/i
export function isBareImport(id: string): boolean {
return bareImportRE.test(id)
}
export const VALID_ID_PREFIX = '/@id/'
export function normalizeRequestId(id: string, base?: string): string {
if (base && id.startsWith(withTrailingSlash(base))) {
id = `/${id.slice(base.length)}`
}
// keep drive the same as in process cwd. ideally, this should be resolved on Vite side
// Vite always resolves drive letters to the upper case because of the use of `realpathSync`
// https://github.com/vitejs/vite/blob/0ab20a3ee26eacf302415b3087732497d0a2f358/packages/vite/src/node/utils.ts#L635
if (driveRegexp && !driveRegexp?.test(id) && driveOppositeRegext?.test(id)) {
id = id.replace(driveOppositeRegext, `${drive}$1`)
}
if (id.startsWith('file://')) {
// preserve hash/query
const { file, postfix } = splitFileAndPostfix(id)
return fileURLToPath(file) + postfix
}
return id
.replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0`
.replace(/^\/@id\//, '')
.replace(/^__vite-browser-external:/, '')
.replace(/\?v=\w+/, '?') // remove ?v= query
.replace(/&v=\w+/, '') // remove &v= query
.replace(/\?t=\w+/, '?') // remove ?t= query
.replace(/&t=\w+/, '') // remove &t= query
.replace(/\?import/, '?') // remove ?import query
.replace(/&import/, '') // remove &import query
.replace(/\?&/, '?') // replace ?& with just ?
.replace(/\?+$/, '') // remove end query mark
}
const postfixRE = /[?#].*$/
export function cleanUrl(url: string): string {
return url.replace(postfixRE, '')
}
function splitFileAndPostfix(path: string): {
file: string
postfix: string
} {
const file = cleanUrl(path)
return { file, postfix: path.slice(file.length) }
}
const internalRequests = ['@vite/client', '@vite/env']
const internalRequestRegexp = new RegExp(
`^/?(?:${internalRequests.join('|')})$`,
)
export function isInternalRequest(id: string): boolean {
return internalRequestRegexp.test(id)
}
// https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix
const prefixedBuiltins = new Set([
'node:sea',
'node:sqlite',
'node:test',
'node:test/reporters',
])
const builtins = new Set([
...nodeModule.builtinModules,
'assert/strict',
'diagnostics_channel',
'dns/promises',
'fs/promises',
'path/posix',
'path/win32',
'readline/promises',
'stream/consumers',
'stream/promises',
'stream/web',
'timers/promises',
'util/types',
'wasi',
])
export function normalizeModuleId(id: string): string {
// unique id that is not available as "test"
if (prefixedBuiltins.has(id)) {
return id
}
if (id.startsWith('file://')) {
return fileURLToPath(id)
}
return id
.replace(/\\/g, '/')
.replace(/^\/@fs\//, isWindows ? '' : '/')
.replace(/^node:/, '')
.replace(/^\/+/, '/')
}
export function isPrimitive(v: any): boolean {
return v !== Object(v)
}
export function toFilePath(
id: string,
root: string,
): { path: string; exists: boolean } {
let { absolute, exists } = (() => {
if (id.startsWith('/@fs/')) {
return { absolute: id.slice(4), exists: true }
}
// check if /src/module.js -> <root>/src/module.js
if (!id.startsWith(withTrailingSlash(root)) && id[0] === '/') {
const resolved = resolve(root, id.slice(1))
if (existsSync(cleanUrl(resolved))) {
return { absolute: resolved, exists: true }
}
}
else if (
id.startsWith(withTrailingSlash(root))
&& existsSync(cleanUrl(id))
) {
return { absolute: id, exists: true }
}
return { absolute: id, exists: false }
})()
if (absolute.startsWith('//')) {
absolute = absolute.slice(1)
}
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
return {
path:
isWindows && absolute[0] === '/'
? slash(fileURLToPath(pathToFileURL(absolute.slice(1)).href))
: absolute,
exists,
}
}
const NODE_BUILTIN_NAMESPACE = 'node:'
export function isNodeBuiltin(id: string): boolean {
// Added in v18.6.0
if (nodeModule.isBuiltin) {
return nodeModule.isBuiltin(id)
}
if (prefixedBuiltins.has(id)) {
return true
}
return builtins.has(
id.startsWith(NODE_BUILTIN_NAMESPACE)
? id.slice(NODE_BUILTIN_NAMESPACE.length)
: id,
)
}
/**
* Convert `Arrayable<T>` to `Array<T>`
*
* @category Array
*/
export function toArray<T>(array?: Nullable<Arrayable<T>>): Array<T> {
if (array === null || array === undefined) {
array = []
}
if (Array.isArray(array)) {
return array
}
return [array]
}
export function getCachedData<T>(
cache: Map<string, T>,
basedir: string,
originalBasedir: string,
): NonNullable<T> | undefined {
const pkgData = cache.get(getFnpdCacheKey(basedir))
if (pkgData) {
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
cache.set(getFnpdCacheKey(dir), pkgData)
})
return pkgData
}
}
export function setCacheData<T>(
cache: Map<string, T>,
data: T,
basedir: string,
originalBasedir: string,
): void {
cache.set(getFnpdCacheKey(basedir), data)
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
cache.set(getFnpdCacheKey(dir), data)
})
}
function getFnpdCacheKey(basedir: string) {
return `fnpd_${basedir}`
}
/**
* Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir.
* @param longerDir Longer dir path, e.g. `/User/foo/bar/baz`
* @param shorterDir Shorter dir path, e.g. `/User/foo`
*/
function traverseBetweenDirs(
longerDir: string,
shorterDir: string,
cb: (dir: string) => void,
) {
while (longerDir !== shorterDir) {
cb(longerDir)
longerDir = dirname(longerDir)
}
}
export function withTrailingSlash(path: string): string {
if (path.at(-1) !== '/') {
return `${path}/`
}
return path
}
export function createImportMetaEnvProxy(): NodeJS.ProcessEnv {
// packages/vitest/src/node/plugins/index.ts:146
const booleanKeys = ['DEV', 'PROD', 'SSR']
return new Proxy(process.env, {
get(_, key) {
if (typeof key !== 'string') {
return undefined
}
if (booleanKeys.includes(key)) {
return !!process.env[key]
}
return process.env[key]
},
set(_, key, value) {
if (typeof key !== 'string') {
return true
}
if (booleanKeys.includes(key)) {
process.env[key] = value ? '1' : ''
}
else {
process.env[key] = value
}
return true
},
})
}
const packageCache = new Map<string, { type?: 'module' | 'commonjs' }>()
export async function findNearestPackageData(
basedir: string,
): Promise<{ type?: 'module' | 'commonjs' }> {
const originalBasedir = basedir
while (basedir) {
const cached = getCachedData(packageCache, basedir, originalBasedir)
if (cached) {
return cached
}
const pkgPath = join(basedir, 'package.json')
if ((await fsp.stat(pkgPath).catch(() => {}))?.isFile()) {
const pkgData = JSON.parse(await fsp.readFile(pkgPath, 'utf8'))
if (packageCache) {
setCacheData(packageCache, pkgData, basedir, originalBasedir)
}
return pkgData
}
const nextBasedir = dirname(basedir)
if (nextBasedir === basedir) {
break
}
basedir = nextBasedir
}
return {}
}

View File

@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"isolatedDeclarations": true
},
"include": ["./src/**/*.ts"],
"exclude": ["./dist"]
}

View File

@ -1,2 +0,0 @@
#!/usr/bin/env node
import('./dist/cli.mjs')

View File

@ -18,7 +18,6 @@ const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
/^@?vitest(\/|$)/,
'vite-node/utils',
]
const dtsUtils = createDtsUtils()

300
pnpm-lock.yaml generated
View File

@ -918,34 +918,6 @@ importers:
specifier: ^0.3.2
version: 0.3.2(picocolors@1.1.1)
packages/vite-node:
dependencies:
cac:
specifier: 'catalog:'
version: 6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588)
debug:
specifier: 'catalog:'
version: 4.4.3
es-module-lexer:
specifier: ^1.7.0
version: 1.7.0
pathe:
specifier: 'catalog:'
version: 2.0.3
vite:
specifier: ^7.1.5
version: 7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
devDependencies:
'@jridgewell/trace-mapping':
specifier: 'catalog:'
version: 0.3.31
'@types/debug':
specifier: 'catalog:'
version: 4.1.12
tinyrainbow:
specifier: 'catalog:'
version: 3.0.3
packages/vitest:
dependencies:
'@vitest/browser-playwright':
@ -1338,9 +1310,6 @@ importers:
url:
specifier: ^0.11.4
version: 0.11.4
vite-node:
specifier: workspace:*
version: link:../../packages/vite-node
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@ -1510,9 +1479,6 @@ importers:
vite:
specifier: ^7.1.5
version: 7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
vite-node:
specifier: workspace:*
version: link:../../packages/vite-node
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@ -1542,21 +1508,6 @@ importers:
specifier: workspace:*
version: link:../../packages/vitest
test/vite-node:
devDependencies:
'@types/inquirer':
specifier: ^9.0.9
version: 9.0.9
inquirer:
specifier: ^12.9.6
version: 12.9.6(@types/node@22.18.6)
vite-node:
specifier: workspace:*
version: link:../../packages/vite-node
vitest:
specifier: workspace:*
version: link:../../packages/vitest
test/watch:
devDependencies:
'@vitest/browser-playwright':
@ -2947,15 +2898,6 @@ packages:
resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==}
engines: {node: '>=18'}
'@inquirer/checkbox@4.2.4':
resolution: {integrity: sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/confirm@5.1.18':
resolution: {integrity: sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==}
engines: {node: '>=18'}
@ -2974,100 +2916,10 @@ packages:
'@types/node':
optional: true
'@inquirer/editor@4.2.20':
resolution: {integrity: sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/expand@4.0.20':
resolution: {integrity: sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/external-editor@1.0.2':
resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/figures@1.0.13':
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
'@inquirer/input@4.2.4':
resolution: {integrity: sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/number@3.0.20':
resolution: {integrity: sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/password@4.0.20':
resolution: {integrity: sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/prompts@7.8.6':
resolution: {integrity: sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/rawlist@4.1.8':
resolution: {integrity: sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/search@3.1.3':
resolution: {integrity: sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/select@4.3.4':
resolution: {integrity: sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/type@3.0.8':
resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==}
engines: {node: '>=18'}
@ -4216,9 +4068,6 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/inquirer@9.0.9':
resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@ -4294,9 +4143,6 @@ packages:
'@types/tern@0.23.4':
resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==}
'@types/through@0.0.30':
resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@ -5195,9 +5041,6 @@ packages:
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
chardet@2.1.0:
resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==}
cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
@ -6540,10 +6383,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
@ -6590,15 +6429,6 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inquirer@12.9.6:
resolution: {integrity: sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
internal-slot@1.0.7:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
@ -8218,10 +8048,6 @@ packages:
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
engines: {node: '>=18'}
run-async@4.0.5:
resolution: {integrity: sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==}
engines: {node: '>=0.12.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@ -11080,16 +10906,6 @@ snapshots:
'@inquirer/ansi@1.0.0': {}
'@inquirer/checkbox@4.2.4(@types/node@22.18.6)':
dependencies:
'@inquirer/ansi': 1.0.0
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/figures': 1.0.13
'@inquirer/type': 3.0.8(@types/node@22.18.6)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/confirm@5.1.18(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
@ -11110,95 +10926,8 @@ snapshots:
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/editor@4.2.20(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/external-editor': 1.0.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/expand@4.0.20(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/external-editor@1.0.2(@types/node@22.18.6)':
dependencies:
chardet: 2.1.0
iconv-lite: 0.7.0
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/figures@1.0.13': {}
'@inquirer/input@4.2.4(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/number@3.0.20(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/password@4.0.20(@types/node@22.18.6)':
dependencies:
'@inquirer/ansi': 1.0.0
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/prompts@7.8.6(@types/node@22.18.6)':
dependencies:
'@inquirer/checkbox': 4.2.4(@types/node@22.18.6)
'@inquirer/confirm': 5.1.18(@types/node@22.18.6)
'@inquirer/editor': 4.2.20(@types/node@22.18.6)
'@inquirer/expand': 4.0.20(@types/node@22.18.6)
'@inquirer/input': 4.2.4(@types/node@22.18.6)
'@inquirer/number': 3.0.20(@types/node@22.18.6)
'@inquirer/password': 4.0.20(@types/node@22.18.6)
'@inquirer/rawlist': 4.1.8(@types/node@22.18.6)
'@inquirer/search': 3.1.3(@types/node@22.18.6)
'@inquirer/select': 4.3.4(@types/node@22.18.6)
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/rawlist@4.1.8(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/search@3.1.3(@types/node@22.18.6)':
dependencies:
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/figures': 1.0.13
'@inquirer/type': 3.0.8(@types/node@22.18.6)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/select@4.3.4(@types/node@22.18.6)':
dependencies:
'@inquirer/ansi': 1.0.0
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/figures': 1.0.13
'@inquirer/type': 3.0.8(@types/node@22.18.6)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.18.6
'@inquirer/type@3.0.8(@types/node@22.18.6)':
optionalDependencies:
'@types/node': 22.18.6
@ -12153,11 +11882,6 @@ snapshots:
dependencies:
'@types/unist': 3.0.2
'@types/inquirer@9.0.9':
dependencies:
'@types/through': 0.0.30
rxjs: 7.8.2
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-instrument@1.7.8':
@ -12246,10 +11970,6 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
'@types/through@0.0.30':
dependencies:
'@types/node': 22.18.6
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7': {}
@ -13356,8 +13076,6 @@ snapshots:
character-entities@2.0.2: {}
chardet@2.1.0: {}
cheerio-select@2.1.0:
dependencies:
boolbase: 1.0.0
@ -14980,10 +14698,6 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.7.0:
dependencies:
safer-buffer: 2.1.2
idb@7.1.1: {}
ignore@5.3.2: {}
@ -15016,18 +14730,6 @@ snapshots:
ini@1.3.8: {}
inquirer@12.9.6(@types/node@22.18.6):
dependencies:
'@inquirer/ansi': 1.0.0
'@inquirer/core': 10.2.2(@types/node@22.18.6)
'@inquirer/prompts': 7.8.6(@types/node@22.18.6)
'@inquirer/type': 3.0.8(@types/node@22.18.6)
mute-stream: 2.0.0
run-async: 4.0.5
rxjs: 7.8.2
optionalDependencies:
'@types/node': 22.18.6
internal-slot@1.0.7:
dependencies:
es-errors: 1.3.0
@ -16936,8 +16638,6 @@ snapshots:
run-applescript@7.0.0: {}
run-async@4.0.5: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3

View File

@ -1,5 +1,4 @@
import { expect, test } from 'vitest'
import { isWindows } from '../../../packages/vite-node/src/utils'
import { runVitest } from '../../test-utils'
test('with color', async () => {
@ -31,7 +30,7 @@ test('without color', async () => {
expect(stdout).not.toContain('\x1B[33mtrue\x1B[39m\n')
})
test.skipIf(isWindows)('without color, forks pool in non-TTY parent', async () => {
test.skipIf(process.platform === 'win32')('without color, forks pool in non-TTY parent', async () => {
const { stdout } = await runVitest({
root: 'fixtures/console-color',
env: {

View File

@ -37,7 +37,6 @@
"tinyrainbow": "catalog:",
"tinyspy": "^4.0.4",
"url": "^0.11.4",
"vite-node": "workspace:*",
"vitest": "workspace:*",
"vitest-environment-custom": "file:./vitest-environment-custom",
"vitest-package-exports": "^0.1.1",

View File

@ -1,6 +1,4 @@
import { existsSync } from 'node:fs'
import { describe, expect, it, vi } from 'vitest'
import { isWindows, slash, toFilePath } from '../../../packages/vite-node/src/utils'
vi.mock('fs', () => {
return {
@ -8,6 +6,8 @@ vi.mock('fs', () => {
}
})
const isWindows = process.platform === 'win32'
describe('current url', () => {
it('__filename is equal to import.meta.url', () => {
expect(__filename).toEqual(import.meta.filename)
@ -60,118 +60,3 @@ describe('current url', () => {
})
})
})
describe('toFilePath', () => {
// the following tests will work incorrectly on unix systems
describe.runIf(isWindows)('windows', () => {
it('windows', () => {
const root = 'C:/path/to/project'
const id = '/node_modules/pkg/file.js'
const expected = 'C:/path/to/project/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('windows with /@fs/', () => {
const root = 'C:/path/to/project'
const id = '/@fs/C:/path/to/project/node_modules/pkg/file.js'
const expected = 'C:/path/to/project/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
})
// the following tests will work incorrectly on windows systems
describe.runIf(!isWindows)('unix', () => {
it('unix', () => {
const root = '/path/to/project'
const id = '/node_modules/pkg/file.js'
const expected = '/path/to/project/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(true)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('unix with /@fs/', () => {
const root = '/path/to/project'
const id = '/@fs//path/to/project/node_modules/pkg/file.js'
const expected = '/path/to/project/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(true)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('unix in first level catalog', () => {
const root = '/root'
const id = '/node_modules/pkg/file.js'
const expected = '/root/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(true)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('unix with /@fs/ in first level catalog', () => {
const root = '/root'
const id = '/@fs//root/node_modules/pkg/file.js'
const expected = '/root/node_modules/pkg/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(true)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('unix with absolute path in first level catalog', () => {
const root = '/root'
const id = '/root/path/to/file.js'
const expected = '/root/path/to/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(true)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(expected)
})
it('unix with sibling path', () => {
const root = '/path/to/first/package'
const id = '/path/to/second/package/file.js'
const processSpy = vi.spyOn(process, 'cwd').mockReturnValue(root)
const existsSpy = vi.mocked(existsSync).mockReturnValue(false)
const { path: filePath } = toFilePath(id, root)
processSpy.mockRestore()
existsSpy.mockRestore()
expect(slash(filePath)).toEqual(id)
})
})
})

View File

@ -153,7 +153,7 @@ interface CliOptions extends Partial<Options> {
preserveAnsi?: boolean
}
async function runCli(command: 'vitest' | 'vite-node', _options?: CliOptions | string, ...args: string[]) {
async function runCli(command: 'vitest', _options?: CliOptions | string, ...args: string[]) {
let options = _options
if (typeof _options === 'string') {
@ -228,13 +228,6 @@ export async function runVitestCli(_options?: CliOptions | string, ...args: stri
return runCli('vitest', _options, ...args)
}
export async function runViteNodeCli(_options?: CliOptions | string, ...args: string[]) {
process.env.VITE_TEST_WATCHER_DEBUG = 'true'
const { vitest, ...rest } = await runCli('vite-node', _options, ...args)
return { viteNode: vitest, ...rest }
}
export function getInternalState(): WorkerGlobalState {
// @ts-expect-error untyped global
return globalThis.__vitest_worker__

View File

@ -10,7 +10,6 @@
"tinyexec": "^0.3.2",
"tinyrainbow": "catalog:",
"vite": "latest",
"vite-node": "workspace:*",
"vitest": "workspace:*"
}
}

View File

@ -1 +0,0 @@
MY_TEST_ENV=hello

View File

@ -1,18 +0,0 @@
{
"name": "@vitest/test-vite-node",
"type": "module",
"private": true,
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage",
"dev": "vite-node --watch ./src/*.ts",
"debug:dev": "DEBUG=vite-node:* node --inspect-brk ../../packages/vite-node/dist/cli.cjs --watch ./src/*",
"debug": "node --inspect-brk ../../packages/vite-node/dist/cli.cjs"
},
"devDependencies": {
"@types/inquirer": "^9.0.9",
"inquirer": "^12.9.6",
"vite-node": "workspace:*",
"vitest": "workspace:*"
}
}

View File

@ -1 +0,0 @@
export default 'test'

View File

@ -1,21 +0,0 @@
import { defineConfig } from 'vite'
const data: string[] = []
export default defineConfig({
plugins: [
{
name: 'test-plugin',
async buildStart() {
data.push('buildStart:in')
await new Promise(r => setTimeout(r, 100))
data.push('buildStart:out')
},
transform(_code, id) {
if (id.endsWith('/test.ts')) {
console.log(JSON.stringify(data))
}
},
},
],
})

View File

@ -1,3 +0,0 @@
export function a() {
return 'A'
}

View File

@ -1,5 +0,0 @@
import { foo } from '.'
export function b() {
return `B${foo()}`
}

View File

@ -1,14 +0,0 @@
import { a } from './a'
import { b } from './b'
export const index = 'index'
export function foo() {
return index
}
export * from './a'
export * from './b'
// eslint-disable-next-line no-console
console.log(a(), b(), index)

View File

@ -1,5 +0,0 @@
import { c } from './reg'
await new Promise(resolve => setTimeout(resolve, 10))
export const a = `a${c}`

View File

@ -1,6 +0,0 @@
// eslint-disable-next-line unused-imports/no-unused-imports
import { a } from './a'
await new Promise(resolve => setTimeout(resolve, 10))
export const b = 'b'

View File

@ -1,3 +0,0 @@
export const c = 'c'
await new Promise(resolve => setTimeout(resolve, 10))

View File

@ -1,11 +0,0 @@
/* eslint-disable no-console */
import { a } from './a'
import { b } from './b'
/**
* index -> a -> b
* ^ ^v
* reg -> c
*/
console.log(a, b)

View File

@ -1,6 +0,0 @@
export { a } from './a'
await new Promise(resolve => setTimeout(resolve, 20))
export { b } from './b'
export { c } from './c'

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log(process.argv)

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log(JSON.stringify(process.env, null, 2))

View File

@ -1,3 +0,0 @@
body {
margin: 0;
}

View File

@ -1,13 +0,0 @@
import './deps.css'
// eslint-disable-next-line no-console
console.log('[deps.ts] imported')
export {}
if (import.meta.hot) {
import.meta.hot.accept(() => {
// eslint-disable-next-line no-console
console.log('[deps.ts] hot reload!')
})
}

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log(new Error('[ok]').stack)

View File

@ -1,14 +0,0 @@
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
name: 'repro',
transform(code, id) {
if (id.endsWith('/empty-mappings/main.ts')) {
return { code, map: { mappings: '' } }
}
},
},
],
})

View File

@ -1,3 +0,0 @@
export function add(a, b) {
return a + b
}

View File

@ -1,7 +0,0 @@
console.error('Hello!')
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.error('Accept')
})
}

View File

@ -1,11 +0,0 @@
if (import.meta.hot) {
import.meta.hot.accept(() => {})
if (import.meta.hot.data.value == null) {
import.meta.hot.data.value = 0
}
else {
// eslint-disable-next-line no-throw-literal
throw 'some error'
}
}
console.error('ready')

View File

@ -1,12 +0,0 @@
/* eslint-disable no-console */
import { a } from './testMod'
console.log('[main.js] load!')
console.log('[main.js] hello world')
console.log('[main.js]', a)
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log('[main.ts] hot reload!')
})
}

View File

@ -1,9 +0,0 @@
// const ansiEscapes = module.exports
// module.exports.default = ansiEscapes
// ansiEscapes.HelloWorld = 1
// console.log(ansiEscapes.HelloWorld);
import inquirer from 'inquirer'
// eslint-disable-next-line no-console
console.log(inquirer.prompt)

View File

@ -1,4 +0,0 @@
import ansiEscapes, { HelloWorld } from './self-export'
// eslint-disable-next-line no-console
console.log(ansiEscapes, HelloWorld)

View File

@ -1,7 +0,0 @@
declare const ansiEscapes: {
HelloWorld: number
}
export default ansiEscapes
declare const HelloWorld: number
export { HelloWorld }

View File

@ -1,3 +0,0 @@
const ansiEscapes = module.exports
module.exports.default = ansiEscapes
ansiEscapes.HelloWorld = 1

View File

@ -1,6 +0,0 @@
export const a = 'hello testModule'
// eslint-disable-next-line no-console
console.log('[testModule.js] load!')
import('./deps')

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log('test 1')

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log('test 1')

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line no-console
console.log('test 1')

View File

@ -1,16 +0,0 @@
// 1
// 1
async function main() {
try {
// 2
// 2
throw new Error('boom')
}
catch (e) {
// eslint-disable-next-line no-console
console.log(e)
}
}
// 3
// 3
main()

View File

@ -1,15 +0,0 @@
import { resolve } from 'pathe'
import { expect, test } from 'vitest'
import { runViteNodeCli } from '../../test-utils'
test('circular 1', async () => {
const entryPath = resolve(import.meta.dirname, '../src/circular1/index.ts')
const cli = await runViteNodeCli(entryPath)
expect(cli.stdout).toContain('A Bindex index')
}, 60_000)
test('circular 2', async () => {
const entryPath = resolve(import.meta.dirname, '../src/circular2/index.ts')
const cli = await runViteNodeCli(entryPath)
expect(cli.stdout).toContain('ac b')
}, 60_000)

View File

@ -1,82 +0,0 @@
import { resolve } from 'pathe'
import pkg from 'vite-node/package.json'
import { expect, it } from 'vitest'
import { editFile, runViteNodeCli } from '../../test-utils'
const entryPath = resolve(import.meta.dirname, '../src/cli-parse-args.js')
const version = (pkg as any).version
const parseResult = (s: string) => JSON.parse(s.replaceAll('\'', '"'))
it('basic', async () => {
const cli = await runViteNodeCli(entryPath)
expect(cli.stdout).toContain('node')
expect(parseResult(cli.stdout)).toHaveLength(2)
})
it('--help', async () => {
const cli1 = await runViteNodeCli('--help', entryPath)
expect(cli1.stdout).toContain('Usage:')
const cli2 = await runViteNodeCli('-h', entryPath)
expect(cli2.stdout).toContain('Usage:')
})
it('--version', async () => {
const cli1 = await runViteNodeCli('--version', entryPath)
expect(cli1.stdout).toContain(`vite-node/${version}`)
const cli2 = await runViteNodeCli('-v', entryPath)
expect(cli2.stdout).toContain(`vite-node/${version}`)
})
it('script args', async () => {
const cli1 = await runViteNodeCli(entryPath, '--version', '--help')
expect(parseResult(cli1.stdout)).include('--version').include('--help')
})
it('script args in -- after', async () => {
const cli1 = await runViteNodeCli(entryPath, '--', '--version', '--help')
expect(parseResult(cli1.stdout)).include('--version').include('--help')
})
it('exposes .env variables', async () => {
const { stdout } = await runViteNodeCli(resolve(import.meta.dirname, '../src/cli-print-env.js'))
const env = JSON.parse(stdout)
expect(env.MY_TEST_ENV).toBe('hello')
})
it.each(['index.js', 'index.cjs', 'index.mjs'])('correctly runs --watch %s', async (file) => {
const entryPath = resolve(import.meta.dirname, '../src/watch', file)
const { viteNode } = await runViteNodeCli('--watch', entryPath)
await viteNode.waitForStdout('test 1')
editFile(entryPath, c => c.replace('test 1', 'test 2'))
await viteNode.waitForStdout('test 2')
})
it('error stack', async () => {
const entryPath = resolve(import.meta.dirname, '../src/watch/source-map.ts')
const { viteNode } = await runViteNodeCli('--watch', entryPath)
await viteNode.waitForStdout('source-map.ts:7:11')
})
it('buildStart', async () => {
const root = resolve(import.meta.dirname, '../src/buildStart')
const result = await runViteNodeCli('--root', root, resolve(root, 'test.ts'))
await result.viteNode.waitForStdout('["buildStart:in","buildStart:out"]')
})
it('buildStart with all ssr', async () => {
const root = resolve(import.meta.dirname, '../src/buildStart')
const result = await runViteNodeCli(
`--root=${root}`,
'--options.transformMode.ssr=.*',
resolve(root, 'test.ts'),
)
await result.viteNode.waitForStdout('["buildStart:in","buildStart:out"]')
})
it('empty mappings', async () => {
const root = resolve(import.meta.dirname, '../src/empty-mappings')
const result = await runViteNodeCli('--root', root, resolve(root, 'main.ts'))
await result.viteNode.waitForStdout('[ok]')
})

View File

@ -1,35 +0,0 @@
import { resolve } from 'pathe'
import { test } from 'vitest'
import { editFile, runViteNodeCli } from '../../test-utils'
test('hmr.accept works correctly', async () => {
const scriptFile = resolve(import.meta.dirname, '../src/hmr-script.js')
const { viteNode } = await runViteNodeCli('--watch', scriptFile)
await viteNode.waitForStderr('Hello!')
editFile(scriptFile, content => content.replace('Hello!', 'Hello world!'))
await viteNode.waitForStderr('Hello world!')
await viteNode.waitForStderr('Accept')
await viteNode.waitForStdout(`[vite-node] hot updated: ${scriptFile}`)
})
test('can handle top-level throw in self-accepting module', async () => {
const scriptFile = resolve(import.meta.dirname, '../src/hmr-throw.js')
const { viteNode } = await runViteNodeCli('--watch', scriptFile)
await viteNode.waitForStderr('ready')
editFile(scriptFile, content => `${content}\nconsole.error("done")`)
await viteNode.waitForStderr('some error')
await viteNode.waitForStderr(`[hmr] Failed to reload ${scriptFile}. This could be due to syntax errors or importing non-existent modules. (see errors above)`)
})
test('basic', async () => {
const { viteNode } = await runViteNodeCli('--watch', resolve(import.meta.dirname, '../src/testMod.ts'))
await viteNode.waitForStdout('[deps.ts] imported')
})

View File

@ -1,22 +0,0 @@
import { resolve } from 'pathe'
import { expect, it } from 'vitest'
import { runViteNodeCli } from '../../test-utils'
import ansiEscapes, { HelloWorld } from '../src/self-export'
it('should export self', () => {
expect(ansiEscapes.HelloWorld).eq(HelloWorld)
expect(Reflect.get(ansiEscapes, 'default').HelloWorld).eq(HelloWorld)
expect(HelloWorld).eq(1)
})
it('example 1', async () => {
const entryPath = resolve(import.meta.dirname, '../src/self-export-example1.ts')
const { viteNode } = await runViteNodeCli(entryPath)
await viteNode.waitForStdout('Function')
}, 60_000)
it('example 2', async () => {
const entryPath = resolve(import.meta.dirname, '../src/self-export-example2.ts')
const { viteNode } = await runViteNodeCli(entryPath)
await viteNode.waitForStdout('HelloWorld: 1')
}, 60_000)

View File

@ -1,284 +0,0 @@
import type { Plugin, ViteDevServer } from 'vite'
import { join, resolve } from 'pathe'
import { createServer } from 'vite'
import { ViteNodeServer } from 'vite-node/server'
import { describe, expect, test, vi } from 'vitest'
import { extractSourceMap } from '../../../packages/vite-node/src/source-map'
describe('server works correctly', async () => {
test('resolve id considers transform mode', async () => {
const resolveId = vi.fn()
const vnServer = new ViteNodeServer({
pluginContainer: { resolveId },
config: {
root: '/',
},
moduleGraph: {
idToModuleMap: new Map(),
},
} as any, {
transformMode: {
web: [/web/],
ssr: [/ssr/],
},
})
await vnServer.resolveId('/path', '/web path')
expect(resolveId).toHaveBeenCalledWith('/path', '/web path', { ssr: false })
await vnServer.resolveId('/ssr', '/ssr path')
expect(resolveId).toHaveBeenCalledWith('/ssr', '/ssr path', { ssr: true })
})
})
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
describe('server correctly caches data', () => {
const it = test.extend<{
root: string
plugin: Plugin
ssrFiles: string[]
webFiles: string[]
server: ViteDevServer
viteNode: ViteNodeServer
}>({
ssrFiles: async ({}, use) => {
await use([])
},
webFiles: async ({}, use) => {
await use([])
},
root: resolve(import.meta.dirname, '../'),
plugin: async ({ ssrFiles, webFiles }, use) => {
const plugin: Plugin = {
name: 'test',
transform(code, id, options) {
// this should be called only once if cached is configured correctly
if (options?.ssr) {
ssrFiles.push(id)
}
else {
webFiles.push(id)
}
},
}
await use(plugin)
},
server: async ({ root, plugin }, use) => {
const server = await createServer({
configFile: false,
root,
server: {
middlewareMode: true,
watch: null,
},
optimizeDeps: {
disabled: true,
},
plugins: [plugin],
})
await use(server)
await server.close()
},
viteNode: async ({ server }, use) => {
const vnServer = new ViteNodeServer(server)
await use(vnServer)
},
})
it('fetchModule with id, and got sourcemap source in absolute path', async ({ viteNode }) => {
const fetchResult = await viteNode.fetchModule('/src/foo.js')
const sourceMap = extractSourceMap(fetchResult.code!)
// expect got sourcemap source in a valid filesystem path
expect(sourceMap?.sources[0]).toBe('foo.js')
})
it('correctly returns cached and invalidated ssr modules', async ({ root, viteNode, ssrFiles, webFiles, server }) => {
await viteNode.fetchModule('/src/foo.js', 'ssr')
const fsPath = join(root, './src/foo.js')
expect(viteNode.fetchCaches.web.has(fsPath)).toBe(false)
expect(viteNode.fetchCache.has(fsPath)).toBe(true)
expect(viteNode.fetchCaches.ssr.has(fsPath)).toBe(true)
expect(webFiles).toHaveLength(0)
expect(ssrFiles).toHaveLength(1)
expect(ssrFiles).toContain(fsPath)
await viteNode.fetchModule('/src/foo.js', 'ssr')
expect(ssrFiles).toHaveLength(1)
server.moduleGraph.invalidateModule(
server.moduleGraph.getModuleById(fsPath)!,
new Set(),
Date.now(),
false,
)
// wait so TS are different
await wait(100)
await viteNode.fetchModule('/src/foo.js', 'ssr')
expect(ssrFiles).toHaveLength(2)
// another fetch after invalidation returns cached result
await viteNode.fetchModule('/src/foo.js', 'ssr')
expect(ssrFiles).toHaveLength(2)
server.moduleGraph.invalidateModule(
server.moduleGraph.getModuleById(fsPath)!,
new Set(),
Date.now(),
true,
)
// wait so TS are different
await wait(100)
await viteNode.fetchModule('/src/foo.js', 'ssr')
await expect.poll(() => ssrFiles).toHaveLength(3)
// another fetch after invalidation returns cached result
await viteNode.fetchModule('/src/foo.js', 'ssr')
await expect.poll(() => ssrFiles).toHaveLength(3)
expect(webFiles).toHaveLength(0)
})
it('correctly returns cached and invalidated web modules', async ({ root, viteNode, webFiles, ssrFiles, server }) => {
await viteNode.fetchModule('/src/foo.js', 'web')
const fsPath = join(root, './src/foo.js')
expect(viteNode.fetchCaches.ssr.has(fsPath)).toBe(false)
expect(viteNode.fetchCache.has(fsPath)).toBe(true)
expect(viteNode.fetchCaches.web.has(fsPath)).toBe(true)
expect(ssrFiles).toHaveLength(0)
expect(webFiles).toHaveLength(1)
expect(webFiles).toContain(fsPath)
await viteNode.fetchModule('/src/foo.js', 'web')
expect(webFiles).toHaveLength(1)
server.moduleGraph.invalidateModule(
server.moduleGraph.getModuleById(fsPath)!,
new Set(),
Date.now(),
false,
)
// wait so TS are different
await wait(100)
await viteNode.fetchModule('/src/foo.js', 'web')
expect(webFiles).toHaveLength(2)
// another fetch after invalidation returns cached result
await viteNode.fetchModule('/src/foo.js', 'web')
expect(webFiles).toHaveLength(2)
server.moduleGraph.invalidateModule(
server.moduleGraph.getModuleById(fsPath)!,
new Set(),
Date.now(),
true,
)
// wait so TS are different
await wait(100)
await viteNode.fetchModule('/src/foo.js', 'web')
await expect.poll(() => webFiles).toHaveLength(3)
// another fetch after invalidation returns cached result
await viteNode.fetchModule('/src/foo.js', 'web')
await expect.poll(() => webFiles).toHaveLength(3)
await expect.poll(() => ssrFiles).toHaveLength(0)
})
it('correctly processes the same file with both transform modes', async ({ viteNode, ssrFiles, webFiles, root }) => {
await viteNode.fetchModule('/src/foo.js', 'ssr')
await viteNode.fetchModule('/src/foo.js', 'web')
const fsPath = join(root, './src/foo.js')
expect(viteNode.fetchCaches.ssr.has(fsPath)).toBe(true)
expect(viteNode.fetchCache.has(fsPath)).toBe(true)
expect(viteNode.fetchCaches.web.has(fsPath)).toBe(true)
expect(ssrFiles).toHaveLength(1)
expect(webFiles).toHaveLength(1)
await viteNode.fetchModule('/src/foo.js', 'ssr')
await viteNode.fetchModule('/src/foo.js', 'web')
expect(ssrFiles).toHaveLength(1)
expect(webFiles).toHaveLength(1)
})
})
describe('externalize', () => {
describe('by default', () => {
test('should externalize vite\'s cached dependencies', async () => {
const vnServer = new ViteNodeServer({
config: {
root: '/',
cacheDir: '/node_modules/.vite',
},
} as any, {})
const externalize = await vnServer.shouldExternalize('/node_modules/.vite/cached.js')
expect(externalize).toBeTruthy()
})
})
describe('with server.deps.inline: true', () => {
test('should not externalize vite\'s cached dependencies', async () => {
const vnServer = new ViteNodeServer({
config: {
root: '/',
cacheDir: '/node_modules/.vite',
},
} as any, {
deps: {
inline: true,
},
})
const externalize = await vnServer.shouldExternalize('/node_modules/.vite/cached.js')
expect(externalize).toBeFalsy()
})
})
describe('with server.deps.inline including the cache dir', () => {
test('should not externalize vite\'s cached dependencies', async () => {
const vnServer = new ViteNodeServer({
config: {
root: '/',
cacheDir: '/node_modules/.vite',
},
} as any, {
deps: {
inline: [/node_modules\/\.vite/],
},
})
const externalize = await vnServer.shouldExternalize('/node_modules/.vite/cached.js')
expect(externalize).toBeFalsy()
})
})
})

View File

@ -1,91 +0,0 @@
/* eslint-disable no-template-curly-in-string */
import type { TransformResult } from 'vite'
import { describe, expect, it } from 'vitest'
import { withInlineSourcemap } from '../../../packages/vite-node/src/source-map'
it('regex match', () => {
const regex = /\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,[A-Za-z0-9+/=]+$/gm
expect('function foo(src) {\n return `//# sourceMappingURL=data:application/json;base64,${src}`;\n}\nObject.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});\n'.match(regex)).toBeNull()
expect(`function foo(src) {
return \`//# sourceMappingURL=data:application/json;base64,\${src}\`;
}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
//# sourceMappingSource=vite-node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ==`.match(regex)).deep.eq(['//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ=='])
expect(`function foo(src) {
return \`//# sourceMappingURL=data:application/json;base64,\${src}\`;
}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
//# sourceMappingSource=vite-node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ==`.replace(regex, '')).not.include('//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ==')
})
describe('withInlineSourcemap', () => {
const input: TransformResult = {
code: 'function foo(src) {\n return `//# sourceMappingURL=data:application/json;base64,${src}`;\n}\nObject.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});\n',
map: {
version: 3,
mappings: 'AAAO,SAAS,IAAI,KAAqB;AACvC,SAAO,qDAAqD;AAC9D;iHAAA',
names: [],
sources: [
'/foo.ts',
],
sourcesContent: [
'export function foo(src: string): string {\n return `//# sourceMappingURL=data:application/json;base64,${src}`\n}\n',
],
file: '/src/foo.ts',
toUrl: () => '',
},
deps: [
],
dynamicDeps: [
],
}
const options = {
root: '/',
filepath: '/foo.ts',
}
it('Check that the original sourcemap in the string is not removed', () => {
expect(withInlineSourcemap(input, options).code).contain('return `//# sourceMappingURL=data:application/json;base64,${src}`')
})
it('Check that the appended sourcemap is removed', () => {
input.code = `function foo(src) {
return \`//# sourceMappingURL=data:application/json;base64,\${src}\`;
}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
//# sourceMappingSource=other
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udtest==`
expect(withInlineSourcemap(input, options).code).not.toContain('//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udtest==')
})
it('Check that the vite-node sourcemap isnot removed', () => {
input.code = `function foo(src) {
return \`//# sourceMappingURL=data:application/json;base64,\${src}\`;
}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
//# sourceMappingSource=vite-node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ==`
expect(withInlineSourcemap(input, options).code).include('//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQU8sU0FBUyxJQUFJLEtBQXFCO0FBQ3ZDLFNBQU8scURBQXFEO0FBQzlEO2lIQUFBIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uL2Zvby50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZm9vKHNyYzogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAvLyMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LCR7c3JjfWBcbn1cbiJdLCJmaWxlIjoiL3NyYy9mb28udHMifQ==')
})
it('Check that the vite-node in real code', () => {
input.code = `
import { expect, it } from 'vitest'
it('should have sourcemaps', () => {
expect('\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9==').toBeTruthy()
expect("\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9==").toBeTruthy()
expect(\`\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9==\`).toBeTruthy()
})
`
expect(withInlineSourcemap(input, options).code)
.include('\'\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9==\'')
.include('"\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9=="')
.include('`\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9==`')
})
})

View File

@ -1,13 +0,0 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: true,
testTimeout: process.env.CI ? 120_000 : 5_000,
onConsoleLog(log) {
if (log.includes('Port is already')) {
return false
}
},
},
})

View File

@ -28,9 +28,7 @@
"vitest/internal/module-runner": ["./packages/vitest/src/public/module-runner.ts"],
"vitest/globals": ["./packages/vitest/globals.d.ts"],
"vitest/browser": ["./packages/vitest/browser/context.d.ts"],
"vitest/*": ["./packages/vitest/src/public/*"],
"vite-node": ["./packages/vite-node/src/index.ts"],
"vite-node/*": ["./packages/vite-node/src/*"]
"vitest/*": ["./packages/vitest/src/public/*"]
},
"strict": true,
"declaration": true,