Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(vite-node): allow dumpping the transformed code to debug #1723

Merged
merged 6 commits into from Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -66,7 +66,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 @@ -107,11 +107,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 @@ -279,12 +278,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"]
}