diff --git a/docs/guide/index.md b/docs/guide/index.md index b096428f..d60a6ea3 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -98,6 +98,33 @@ pnpm dev Then open your app in the browser and open the DevTools panel. +#### Building with the App + +You can also generate a static DevTools build alongside your app's build output by enabling the `build.withApp` option: + +```ts [vite.config.ts] twoslash +import { DevTools } from '@vitejs/devtools' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + DevTools({ + build: { + withApp: true, // generate DevTools output during `vite build` + // outDir: 'custom-dir', // optional, defaults to Vite's build.outDir + }, + }), + ], + build: { + rolldownOptions: { + devtools: {}, + }, + } +}) +``` + +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. + ## What's Next? Now that you have Vite DevTools set up, you can: diff --git a/docs/kit/rpc.md b/docs/kit/rpc.md index 0322a398..bca8bd19 100644 --- a/docs/kit/rpc.md +++ b/docs/kit/rpc.md @@ -121,7 +121,7 @@ const plugin: Plugin = { ### Dump Feature for Build Mode -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. +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. #### How It Works diff --git a/packages/core/src/node/build-static.ts b/packages/core/src/node/build-static.ts new file mode 100644 index 00000000..e8e7e8c2 --- /dev/null +++ b/packages/core/src/node/build-static.ts @@ -0,0 +1,78 @@ +/* eslint-disable no-console */ + +import type { DevToolsNodeContext } from '@vitejs/devtools-kit' +import { existsSync } from 'node:fs' +import fs from 'node:fs/promises' +import { + DEVTOOLS_CONNECTION_META_FILENAME, + DEVTOOLS_DIRNAME, + DEVTOOLS_DOCK_IMPORTS_FILENAME, + DEVTOOLS_MOUNT_PATH, + DEVTOOLS_RPC_DUMP_DIRNAME, + DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME, +} from '@vitejs/devtools-kit/constants' +import c from 'ansis' +import { dirname, join, relative, resolve } from 'pathe' +import { dirClientStandalone } from '../dirs' +import { MARK_NODE } from './constants' + +export interface BuildStaticOptions { + context: DevToolsNodeContext + outDir: string +} + +export async function buildStaticDevTools(options: BuildStaticOptions): Promise { + const { context, outDir } = options + + if (existsSync(outDir)) + await fs.rm(outDir, { recursive: true }) + + const devToolsRoot = join(outDir, DEVTOOLS_DIRNAME) + await fs.mkdir(devToolsRoot, { recursive: true }) + await fs.cp(dirClientStandalone, devToolsRoot, { recursive: true }) + + for (const { baseUrl, distDir } of context.views.buildStaticDirs) { + console.log(c.cyan`${MARK_NODE} Copying static files from ${distDir} to ${join(outDir, baseUrl)}`) + await fs.mkdir(join(outDir, baseUrl), { recursive: true }) + await fs.cp(distDir, join(outDir, baseUrl), { recursive: true }) + } + + const { renderDockImportsMap } = await import('./plugins/server') + + await fs.mkdir(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_DIRNAME), { recursive: true }) + await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_CONNECTION_META_FILENAME), JSON.stringify({ backend: 'static' }, null, 2), 'utf-8') + await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_DOCK_IMPORTS_FILENAME), renderDockImportsMap(context.docks.values()), 'utf-8') + + console.log(c.cyan`${MARK_NODE} Writing RPC dump to ${resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME)}`) + const { collectStaticRpcDump } = await import('./static-dump') + const dump = await collectStaticRpcDump( + context.rpc.definitions.values(), + context, + ) + for (const [filepath, data] of Object.entries(dump.files)) { + const fullpath = resolve(devToolsRoot, filepath) + await fs.mkdir(dirname(fullpath), { recursive: true }) + await fs.writeFile(fullpath, JSON.stringify(data, null, 2), 'utf-8') + } + await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), JSON.stringify(dump.manifest, null, 2), 'utf-8') + await fs.writeFile( + resolve(outDir, 'index.html'), + [ + '', + '', + '', + ' ', + ' ', + ' Vite DevTools', + ` `, + '', + '', + ` `, + '', + '', + ].join('\n'), + 'utf-8', + ) + + console.log(c.green`${MARK_NODE} Built DevTools to ${relative(context.cwd, outDir)}`) +} diff --git a/packages/core/src/node/cli-commands.ts b/packages/core/src/node/cli-commands.ts index ca07a9fb..f2e0e79d 100644 --- a/packages/core/src/node/cli-commands.ts +++ b/packages/core/src/node/cli-commands.ts @@ -1,18 +1,10 @@ /* eslint-disable no-console */ -import { existsSync } from 'node:fs' -import fs from 'node:fs/promises' import { - DEVTOOLS_CONNECTION_META_FILENAME, - DEVTOOLS_DIRNAME, - DEVTOOLS_DOCK_IMPORTS_FILENAME, DEVTOOLS_MOUNT_PATH, - DEVTOOLS_RPC_DUMP_DIRNAME, - DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME, } from '@vitejs/devtools-kit/constants' import c from 'ansis' -import { dirname, join, relative, resolve } from 'pathe' -import { dirClientStandalone } from '../dirs' +import { resolve } from 'pathe' import { MARK_NODE } from './constants' import { normalizeHttpServerUrl } from './utils' @@ -93,57 +85,12 @@ export async function build(options: BuildOptions) { const outDir = resolve(devtools.config.root, options.outDir) - if (existsSync(outDir)) - await fs.rm(outDir, { recursive: true }) - - const devToolsRoot = join(outDir, DEVTOOLS_DIRNAME) - await fs.mkdir(devToolsRoot, { recursive: true }) - await fs.cp(dirClientStandalone, devToolsRoot, { recursive: true }) - - for (const { baseUrl, distDir } of devtools.context.views.buildStaticDirs) { - console.log(c.cyan`${MARK_NODE} Copying static files from ${distDir} to ${join(outDir, baseUrl)}`) - await fs.mkdir(join(outDir, baseUrl), { recursive: true }) - await fs.cp(distDir, join(outDir, baseUrl), { recursive: true }) - } + const { buildStaticDevTools } = await import('./build-static') + await buildStaticDevTools({ + context: devtools.context, + outDir, + }) - const { renderDockImportsMap } = await import('./plugins/server') - - await fs.mkdir(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_DIRNAME), { recursive: true }) - await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_CONNECTION_META_FILENAME), JSON.stringify({ backend: 'static' }, null, 2), 'utf-8') - await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_DOCK_IMPORTS_FILENAME), renderDockImportsMap(devtools.context.docks.values()), 'utf-8') - - console.log(c.cyan`${MARK_NODE} Writing RPC dump to ${resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME)}`) - const { collectStaticRpcDump } = await import('./static-dump') - const dump = await collectStaticRpcDump( - devtools.context.rpc.definitions.values(), - devtools.context, - ) - for (const [filepath, data] of Object.entries(dump.files)) { - const fullpath = resolve(devToolsRoot, filepath) - await fs.mkdir(dirname(fullpath), { recursive: true }) - await fs.writeFile(fullpath, JSON.stringify(data, null, 2), 'utf-8') - } - await fs.writeFile(resolve(devToolsRoot, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), JSON.stringify(dump.manifest, null, 2), 'utf-8') - await fs.writeFile( - resolve(outDir, 'index.html'), - [ - '', - '', - '', - ' ', - ' ', - ' Vite DevTools', - ` `, - '', - '', - ` `, - '', - '', - ].join('\n'), - 'utf-8', - ) - - console.log(c.green`${MARK_NODE} Built to ${relative(devtools.config.root, outDir)}`) console.warn(c.yellow`${MARK_NODE} Static build is still experimental and not yet complete.`) console.warn(c.yellow`${MARK_NODE} Generated output may be missing features and can change without notice.`) } diff --git a/packages/core/src/node/plugins/build.ts b/packages/core/src/node/plugins/build.ts new file mode 100644 index 00000000..a204dce1 --- /dev/null +++ b/packages/core/src/node/plugins/build.ts @@ -0,0 +1,41 @@ +/* eslint-disable no-console */ + +import type { DevToolsNodeContext } from '@vitejs/devtools-kit' +import type { Plugin, ResolvedConfig } from 'vite' +import c from 'ansis' +import { resolve } from 'pathe' +import { MARK_NODE } from '../constants' + +export interface DevToolsBuildOptions { + outDir?: string +} + +export function DevToolsBuild(options: DevToolsBuildOptions = {}): Plugin { + let context: DevToolsNodeContext + let resolvedConfig: ResolvedConfig + + return { + name: 'vite:devtools:build', + apply: 'build', + + configResolved(config) { + resolvedConfig = config + }, + + async buildStart() { + const { createDevToolsContext } = await import('../context') + context = await createDevToolsContext(resolvedConfig) + }, + + async closeBundle() { + console.log(c.cyan`${MARK_NODE} Building static Vite DevTools...`) + + const outDir = options.outDir + ? resolve(resolvedConfig.root, options.outDir) + : resolve(resolvedConfig.root, resolvedConfig.build.outDir) + + const { buildStaticDevTools } = await import('../build-static') + await buildStaticDevTools({ context, outDir }) + }, + } +} diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts index 710a116b..8022d485 100644 --- a/packages/core/src/node/plugins/index.ts +++ b/packages/core/src/node/plugins/index.ts @@ -1,4 +1,5 @@ import type { Plugin } from 'vite' +import { DevToolsBuild } from './build' import { DevToolsInjection } from './injection' import { DevToolsServer } from './server' @@ -9,11 +10,29 @@ export interface DevToolsOptions { * @default true */ builtinDevTools?: boolean + + /** + * Options for building static DevTools output alongside `vite build`. + */ + build?: { + /** + * Automatically build DevTools when running `vite build`. + * + * @default false + */ + withApp?: boolean + /** + * Output directory for the DevTools build (relative to root). + * Defaults to Vite's `build.outDir`. + */ + outDir?: string + } } export async function DevTools(options: DevToolsOptions = {}): Promise { const { builtinDevTools = true, + build, } = options const plugins = [ @@ -21,6 +40,10 @@ export async function DevTools(options: DevToolsOptions = {}): Promise DevToolsServer(), ] + if (build?.withApp) { + plugins.push(DevToolsBuild({ outDir: build.outDir })) + } + if (builtinDevTools) { // eslint-disable-next-line ts/ban-ts-comment // @ts-ignore ignore the type error @@ -31,6 +54,7 @@ export async function DevTools(options: DevToolsOptions = {}): Promise } export { + DevToolsBuild, DevToolsInjection, DevToolsServer, }