diff --git a/.gitignore b/.gitignore index 7bf39a87245a..5168f474f52b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ node_modules .cache dist .idea +.vite-node ltex* .DS_Store bench/test/*/*/ @@ -17,4 +18,4 @@ cypress/videos cypress/downloads cypress/screenshots docs/public/user-avatars -docs/public/sponsors \ No newline at end of file +docs/public/sponsors diff --git a/packages/vite-node/README.md b/packages/vite-node/README.md index 9f24d4833232..d0b40bfd25a6 100644 --- a/packages/vite-node/README.md +++ b/packages/vite-node/README.md @@ -81,6 +81,49 @@ await runner.executeFile('./example.ts') 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 dumpped 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 get stuck, it might because there is a unresolvable circular dependencies, you can set `VITE_NODE_DEBUG_RUNNER=true` to vite-node warn about it. + +```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). diff --git a/packages/vite-node/src/cli.ts b/packages/vite-node/src/cli.ts index 7cce96604c01..6384108674b4 100644 --- a/packages/vite-node/src/cli.ts +++ b/packages/vite-node/src/cli.ts @@ -42,9 +42,9 @@ async function run(files: string[], options: CliOptions = {}) { // forward argv process.argv = [...process.argv.slice(0, 2), ...(options['--'] || [])] - const parsedServerOptions = options.options + const serverOptions = options.options ? parseServerOptions(options.options) - : undefined + : {} const server = await createServer({ logLevel: 'error', @@ -56,7 +56,7 @@ async function run(files: string[], options: CliOptions = {}) { }) await server.pluginContainer.buildStart({}) - const node = new ViteNodeServer(server, parsedServerOptions) + const node = new ViteNodeServer(server, serverOptions) const runner = new ViteNodeRunner({ root: server.config.root, @@ -109,7 +109,6 @@ function parseServerOptions(serverOptions: ViteNodeServerOptionsCLI): ViteNodeSe transformMode: { ...serverOptions.transformMode, - ssr: toArray(serverOptions.transformMode?.ssr).map(dep => new RegExp(dep)), web: toArray(serverOptions.transformMode?.web).map(dep => new RegExp(dep)), }, diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index dee2cbf69cca..2d1bd39951aa 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -80,7 +80,7 @@ export class ViteNodeRunner { 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 : false) + this.debug = options.debug ?? (typeof process !== 'undefined' ? !!process.env.VITE_NODE_DEBUG_RUNNER : false) } async executeFile(file: string) { @@ -121,11 +121,10 @@ export class ViteNodeRunner { let debugTimer: any if (this.debug) - debugTimer = setTimeout(() => this.debugLog(() => `module ${fsPath} takes over 2s to load.\n${getStack()}`), 2000) + debugTimer = setTimeout(() => console.warn(() => `module ${fsPath} takes over 2s to load.\n${getStack()}`), 2000) try { if (callstack.includes(fsPath)) { - this.debugLog(() => `circular dependency, ${getStack()}`) const depExports = this.moduleCache.get(fsPath)?.exports if (depExports) return depExports @@ -293,12 +292,6 @@ export class ViteNodeRunner { hasNestedDefault(target: any) { return '__esModule' in target && target.__esModule && 'default' in target.default } - - private debugLog(msg: () => string) { - if (this.debug) - // eslint-disable-next-line no-console - console.log(`[vite-node] ${msg()}`) - } } function proxyMethod(name: 'get' | 'set' | 'has' | 'deleteProperty', tryDefault: boolean) { diff --git a/packages/vite-node/src/debug.ts b/packages/vite-node/src/debug.ts new file mode 100644 index 000000000000..22413cbf10f5 --- /dev/null +++ b/packages/vite-node/src/debug.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ +import { existsSync, promises as fs } from 'fs' +import { join, resolve } from 'pathe' +import type { TransformResult } from 'vite' +import { gray } from 'kolorist' +import type { DebuggerOptions } from './types' + +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 | undefined + + 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(gray(`[vite-node] [debug] load modules from ${this.dumpDir}`)) + else + console.info(gray(`[vite-node] [debug] dump modules to ${this.dumpDir}`)) + } + this.initPromise = this.clearDump() + } + + async clearDump() { + 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 dumpFile(id: string, result: TransformResult | null) { + 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 { + 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!, + } + } +} diff --git a/packages/vite-node/src/externalize.ts b/packages/vite-node/src/externalize.ts index 23be15f48717..b5331e3595e4 100644 --- a/packages/vite-node/src/externalize.ts +++ b/packages/vite-node/src/externalize.ts @@ -41,10 +41,11 @@ export function guessCJSversion(id: string): string | undefined { } } +const _defaultExternalizeCache = new Map>() export async function shouldExternalize( id: string, options?: DepsHandlingOptions, - cache = new Map>(), + cache = _defaultExternalizeCache, ) { if (!cache.has(id)) cache.set(id, _shouldExternalize(id, options)) diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index 929b663c33c3..429a2dafb211 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -1,9 +1,10 @@ import { join } from 'pathe' import type { TransformResult, ViteDevServer } from 'vite' import createDebug from 'debug' -import type { FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types' +import type { DebuggerOptions, FetchResult, RawSourceMap, ViteNodeResolveId, ViteNodeServerOptions } from './types' import { shouldExternalize } from './externalize' import { toArray, toFilePath, withInlineSourcemap } from './utils' +import type { Debugger } from './debug' export * from './externalize' @@ -21,6 +22,10 @@ export class ViteNodeServer { result: FetchResult }>() + externalizeCache = new Map>() + + debugger?: Debugger + constructor( public server: ViteDevServer, public options: ViteNodeServerOptions = {}, @@ -45,10 +50,18 @@ export class ViteNodeServer { options.deps.inline.push(...toArray(ssrOptions.noExternal)) } } + if (process.env.VITE_NODE_DEBUG_DUMP) { + options.debug = Object.assign({ + dumpModules: !!process.env.VITE_NODE_DEBUG_DUMP, + loadDumppedModules: process.env.VITE_NODE_DEBUG_DUMP === 'load', + }, options.debug ?? {}) + } + if (options.debug) + import('./debug').then(r => this.debugger = new r.Debugger(server.config.root, options.debug!)) } shouldExternalize(id: string) { - return shouldExternalize(id, this.options.deps) + return shouldExternalize(id, this.options.deps, this.externalizeCache) } async resolveId(id: string, importer?: string): Promise { @@ -133,6 +146,12 @@ export class ViteNodeServer { let result: TransformResult | null = null + if (this.options.debug?.loadDumppedModules) { + result = await this.debugger?.loadDump(id) ?? null + if (result) + return result + } + if (this.getTransformMode(id) === 'web') { // for components like Vue, we want to use the client side // plugins but then convert the code to be consumed by the server @@ -148,6 +167,9 @@ export class ViteNodeServer { if (sourcemap === 'inline' && result && !id.includes('node_modules')) withInlineSourcemap(result) + if (this.options.debug?.dumpModules) + await this.debugger?.dumpFile(id, result) + return result } } diff --git a/packages/vite-node/src/types.ts b/packages/vite-node/src/types.ts index f0e594e67fd0..030808e59f0a 100644 --- a/packages/vite-node/src/types.ts +++ b/packages/vite-node/src/types.ts @@ -84,6 +84,21 @@ export interface ViteNodeServerOptions { 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 } diff --git a/packages/vite-node/tsconfig.json b/packages/vite-node/tsconfig.json index e2018da9028e..a7fd86107b10 100644 --- a/packages/vite-node/tsconfig.json +++ b/packages/vite-node/tsconfig.json @@ -1,4 +1,5 @@ { "extends": "../../tsconfig.json", + "include": ["./src/**/*.ts"], "exclude": ["./dist"] }