Skip to content

Commit 22621ca

Browse files
antfuclaude
andauthored
feat: add build.withApp option to auto-generate static devtools (#249)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2d5befc commit 22621ca

File tree

6 files changed

+177
-60
lines changed

6 files changed

+177
-60
lines changed

docs/guide/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,33 @@ pnpm dev
9898

9999
Then open your app in the browser and open the DevTools panel.
100100

101+
#### Building with the App
102+
103+
You can also generate a static DevTools build alongside your app's build output by enabling the `build.withApp` option:
104+
105+
```ts [vite.config.ts] twoslash
106+
import { DevTools } from '@vitejs/devtools'
107+
import { defineConfig } from 'vite'
108+
109+
export default defineConfig({
110+
plugins: [
111+
DevTools({
112+
build: {
113+
withApp: true, // generate DevTools output during `vite build`
114+
// outDir: 'custom-dir', // optional, defaults to Vite's build.outDir
115+
},
116+
}),
117+
],
118+
build: {
119+
rolldownOptions: {
120+
devtools: {},
121+
},
122+
}
123+
})
124+
```
125+
126+
When `build.withApp` is enabled, running `pnpm build` will automatically generate the static DevTools output into the build output directory. This captures real build data from the same build context, so DevTools can display accurate build analysis without a separate build step.
127+
101128
## What's Next?
102129

103130
Now that you have Vite DevTools set up, you can:

docs/kit/rpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const plugin: Plugin = {
121121

122122
### Dump Feature for Build Mode
123123

124-
When using `vite devtools build` to create a static DevTools build, the server cannot execute functions at runtime. The **dump feature** solves this by pre-computing RPC results at build time.
124+
When creating a static DevTools build (via `vite devtools build` CLI or the [`build.withApp`](/guide/#building-with-the-app) plugin option), the server cannot execute functions at runtime. The **dump feature** solves this by pre-computing RPC results at build time.
125125

126126
#### How It Works
127127

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/* eslint-disable no-console */
2+
3+
import type { DevToolsNodeContext } from '@vitejs/devtools-kit'
4+
import { existsSync } from 'node:fs'
5+
import fs from 'node:fs/promises'
6+
import {
7+
DEVTOOLS_CONNECTION_META_FILENAME,
8+
DEVTOOLS_DIRNAME,
9+
DEVTOOLS_DOCK_IMPORTS_FILENAME,
10+
DEVTOOLS_MOUNT_PATH,
11+
DEVTOOLS_RPC_DUMP_DIRNAME,
12+
DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME,
13+
} from '@vitejs/devtools-kit/constants'
14+
import c from 'ansis'
15+
import { dirname, join, relative, resolve } from 'pathe'
16+
import { dirClientStandalone } from '../dirs'
17+
import { MARK_NODE } from './constants'
18+
19+
export interface BuildStaticOptions {
20+
context: DevToolsNodeContext
21+
outDir: string
22+
}
23+
24+
export async function buildStaticDevTools(options: BuildStaticOptions): Promise<void> {
25+
const { context, outDir } = options
26+
27+
if (existsSync(outDir))
28+
await fs.rm(outDir, { recursive: true })
29+
30+
const devToolsRoot = join(outDir, DEVTOOLS_DIRNAME)
31+
await fs.mkdir(devToolsRoot, { recursive: true })
32+
await fs.cp(dirClientStandalone, devToolsRoot, { recursive: true })
33+
34+
for (const { baseUrl, distDir } of context.views.buildStaticDirs) {
35+
console.log(c.cyan`${MARK_NODE} Copying static files from ${distDir} to ${join(outDir, baseUrl)}`)
36+
await fs.mkdir(join(outDir, baseUrl), { recursive: true })
37+
await fs.cp(distDir, join(outDir, baseUrl), { recursive: true })
38+
}
39+
40+
const { renderDockImportsMap } = await import('./plugins/server')
41+
42+
await fs.mkdir(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_DIRNAME), { recursive: true })
43+
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_CONNECTION_META_FILENAME), JSON.stringify({ backend: 'static' }, null, 2), 'utf-8')
44+
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_DOCK_IMPORTS_FILENAME), renderDockImportsMap(context.docks.values()), 'utf-8')
45+
46+
console.log(c.cyan`${MARK_NODE} Writing RPC dump to ${resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME)}`)
47+
const { collectStaticRpcDump } = await import('./static-dump')
48+
const dump = await collectStaticRpcDump(
49+
context.rpc.definitions.values(),
50+
context,
51+
)
52+
for (const [filepath, data] of Object.entries(dump.files)) {
53+
const fullpath = resolve(devToolsRoot, filepath)
54+
await fs.mkdir(dirname(fullpath), { recursive: true })
55+
await fs.writeFile(fullpath, JSON.stringify(data, null, 2), 'utf-8')
56+
}
57+
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), JSON.stringify(dump.manifest, null, 2), 'utf-8')
58+
await fs.writeFile(
59+
resolve(outDir, 'index.html'),
60+
[
61+
'<!doctype html>',
62+
'<html lang="en">',
63+
'<head>',
64+
' <meta charset="UTF-8">',
65+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
66+
' <title>Vite DevTools</title>',
67+
` <meta http-equiv="refresh" content="0; url=${DEVTOOLS_MOUNT_PATH}">`,
68+
'</head>',
69+
'<body>',
70+
` <script>location.replace(${JSON.stringify(DEVTOOLS_MOUNT_PATH)})</script>`,
71+
'</body>',
72+
'</html>',
73+
].join('\n'),
74+
'utf-8',
75+
)
76+
77+
console.log(c.green`${MARK_NODE} Built DevTools to ${relative(context.cwd, outDir)}`)
78+
}

packages/core/src/node/cli-commands.ts

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
/* eslint-disable no-console */
22

3-
import { existsSync } from 'node:fs'
4-
import fs from 'node:fs/promises'
53
import {
6-
DEVTOOLS_CONNECTION_META_FILENAME,
7-
DEVTOOLS_DIRNAME,
8-
DEVTOOLS_DOCK_IMPORTS_FILENAME,
94
DEVTOOLS_MOUNT_PATH,
10-
DEVTOOLS_RPC_DUMP_DIRNAME,
11-
DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME,
125
} from '@vitejs/devtools-kit/constants'
136
import c from 'ansis'
14-
import { dirname, join, relative, resolve } from 'pathe'
15-
import { dirClientStandalone } from '../dirs'
7+
import { resolve } from 'pathe'
168
import { MARK_NODE } from './constants'
179
import { normalizeHttpServerUrl } from './utils'
1810

@@ -93,57 +85,12 @@ export async function build(options: BuildOptions) {
9385

9486
const outDir = resolve(devtools.config.root, options.outDir)
9587

96-
if (existsSync(outDir))
97-
await fs.rm(outDir, { recursive: true })
98-
99-
const devToolsRoot = join(outDir, DEVTOOLS_DIRNAME)
100-
await fs.mkdir(devToolsRoot, { recursive: true })
101-
await fs.cp(dirClientStandalone, devToolsRoot, { recursive: true })
102-
103-
for (const { baseUrl, distDir } of devtools.context.views.buildStaticDirs) {
104-
console.log(c.cyan`${MARK_NODE} Copying static files from ${distDir} to ${join(outDir, baseUrl)}`)
105-
await fs.mkdir(join(outDir, baseUrl), { recursive: true })
106-
await fs.cp(distDir, join(outDir, baseUrl), { recursive: true })
107-
}
88+
const { buildStaticDevTools } = await import('./build-static')
89+
await buildStaticDevTools({
90+
context: devtools.context,
91+
outDir,
92+
})
10893

109-
const { renderDockImportsMap } = await import('./plugins/server')
110-
111-
await fs.mkdir(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_DIRNAME), { recursive: true })
112-
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_CONNECTION_META_FILENAME), JSON.stringify({ backend: 'static' }, null, 2), 'utf-8')
113-
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_DOCK_IMPORTS_FILENAME), renderDockImportsMap(devtools.context.docks.values()), 'utf-8')
114-
115-
console.log(c.cyan`${MARK_NODE} Writing RPC dump to ${resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME)}`)
116-
const { collectStaticRpcDump } = await import('./static-dump')
117-
const dump = await collectStaticRpcDump(
118-
devtools.context.rpc.definitions.values(),
119-
devtools.context,
120-
)
121-
for (const [filepath, data] of Object.entries(dump.files)) {
122-
const fullpath = resolve(devToolsRoot, filepath)
123-
await fs.mkdir(dirname(fullpath), { recursive: true })
124-
await fs.writeFile(fullpath, JSON.stringify(data, null, 2), 'utf-8')
125-
}
126-
await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), JSON.stringify(dump.manifest, null, 2), 'utf-8')
127-
await fs.writeFile(
128-
resolve(outDir, 'index.html'),
129-
[
130-
'<!doctype html>',
131-
'<html lang="en">',
132-
'<head>',
133-
' <meta charset="UTF-8">',
134-
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
135-
' <title>Vite DevTools</title>',
136-
` <meta http-equiv="refresh" content="0; url=${DEVTOOLS_MOUNT_PATH}">`,
137-
'</head>',
138-
'<body>',
139-
` <script>location.replace(${JSON.stringify(DEVTOOLS_MOUNT_PATH)})</script>`,
140-
'</body>',
141-
'</html>',
142-
].join('\n'),
143-
'utf-8',
144-
)
145-
146-
console.log(c.green`${MARK_NODE} Built to ${relative(devtools.config.root, outDir)}`)
14794
console.warn(c.yellow`${MARK_NODE} Static build is still experimental and not yet complete.`)
14895
console.warn(c.yellow`${MARK_NODE} Generated output may be missing features and can change without notice.`)
14996
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable no-console */
2+
3+
import type { DevToolsNodeContext } from '@vitejs/devtools-kit'
4+
import type { Plugin, ResolvedConfig } from 'vite'
5+
import c from 'ansis'
6+
import { resolve } from 'pathe'
7+
import { MARK_NODE } from '../constants'
8+
9+
export interface DevToolsBuildOptions {
10+
outDir?: string
11+
}
12+
13+
export function DevToolsBuild(options: DevToolsBuildOptions = {}): Plugin {
14+
let context: DevToolsNodeContext
15+
let resolvedConfig: ResolvedConfig
16+
17+
return {
18+
name: 'vite:devtools:build',
19+
apply: 'build',
20+
21+
configResolved(config) {
22+
resolvedConfig = config
23+
},
24+
25+
async buildStart() {
26+
const { createDevToolsContext } = await import('../context')
27+
context = await createDevToolsContext(resolvedConfig)
28+
},
29+
30+
async closeBundle() {
31+
console.log(c.cyan`${MARK_NODE} Building static Vite DevTools...`)
32+
33+
const outDir = options.outDir
34+
? resolve(resolvedConfig.root, options.outDir)
35+
: resolve(resolvedConfig.root, resolvedConfig.build.outDir)
36+
37+
const { buildStaticDevTools } = await import('../build-static')
38+
await buildStaticDevTools({ context, outDir })
39+
},
40+
}
41+
}

packages/core/src/node/plugins/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Plugin } from 'vite'
2+
import { DevToolsBuild } from './build'
23
import { DevToolsInjection } from './injection'
34
import { DevToolsServer } from './server'
45

@@ -9,18 +10,40 @@ export interface DevToolsOptions {
910
* @default true
1011
*/
1112
builtinDevTools?: boolean
13+
14+
/**
15+
* Options for building static DevTools output alongside `vite build`.
16+
*/
17+
build?: {
18+
/**
19+
* Automatically build DevTools when running `vite build`.
20+
*
21+
* @default false
22+
*/
23+
withApp?: boolean
24+
/**
25+
* Output directory for the DevTools build (relative to root).
26+
* Defaults to Vite's `build.outDir`.
27+
*/
28+
outDir?: string
29+
}
1230
}
1331

1432
export async function DevTools(options: DevToolsOptions = {}): Promise<Plugin[]> {
1533
const {
1634
builtinDevTools = true,
35+
build,
1736
} = options
1837

1938
const plugins = [
2039
DevToolsInjection(),
2140
DevToolsServer(),
2241
]
2342

43+
if (build?.withApp) {
44+
plugins.push(DevToolsBuild({ outDir: build.outDir }))
45+
}
46+
2447
if (builtinDevTools) {
2548
// eslint-disable-next-line ts/ban-ts-comment
2649
// @ts-ignore ignore the type error
@@ -31,6 +54,7 @@ export async function DevTools(options: DevToolsOptions = {}): Promise<Plugin[]>
3154
}
3255

3356
export {
57+
DevToolsBuild,
3458
DevToolsInjection,
3559
DevToolsServer,
3660
}

0 commit comments

Comments
 (0)