Skip to content

Commit

Permalink
feat(vite-node): allow dumpping the transformed code to debug (#1723)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jul 26, 2022
1 parent b971ede commit 57c2367
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -10,11 +10,12 @@ node_modules
.cache
dist
.idea
.vite-node
ltex*
.DS_Store
bench/test/*/*/
cypress/videos
cypress/downloads
cypress/screenshots
docs/public/user-avatars
docs/public/sponsors
docs/public/sponsors
43 changes: 43 additions & 0 deletions packages/vite-node/README.md
Expand Up @@ -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).
Expand Down
7 changes: 3 additions & 4 deletions packages/vite-node/src/cli.ts
Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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)),
},
Expand Down
11 changes: 2 additions & 9 deletions packages/vite-node/src/client.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
62 changes: 62 additions & 0 deletions 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<void> | 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<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!,
}
}
}
3 changes: 2 additions & 1 deletion packages/vite-node/src/externalize.ts
Expand Up @@ -41,10 +41,11 @@ export function guessCJSversion(id: string): string | undefined {
}
}

const _defaultExternalizeCache = new Map<string, Promise<string | false>>()
export async function shouldExternalize(
id: string,
options?: DepsHandlingOptions,
cache = new Map<string, Promise<string | false>>(),
cache = _defaultExternalizeCache,
) {
if (!cache.has(id))
cache.set(id, _shouldExternalize(id, options))
Expand Down
26 changes: 24 additions & 2 deletions 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'

Expand All @@ -21,6 +22,10 @@ export class ViteNodeServer {
result: FetchResult
}>()

externalizeCache = new Map<string, Promise<string | false>>()

debugger?: Debugger

constructor(
public server: ViteDevServer,
public options: ViteNodeServerOptions = {},
Expand All @@ -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(<DebuggerOptions>{
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<ViteNodeResolveId | null> {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
15 changes: 15 additions & 0 deletions packages/vite-node/src/types.ts
Expand Up @@ -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 }
1 change: 1 addition & 0 deletions packages/vite-node/tsconfig.json
@@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": ["./dist"]
}

0 comments on commit 57c2367

Please sign in to comment.