diff --git a/packages/core/playground/vite.config.ts b/packages/core/playground/vite.config.ts index c956befd..4b3e091c 100644 --- a/packages/core/playground/vite.config.ts +++ b/packages/core/playground/vite.config.ts @@ -106,6 +106,17 @@ export default defineConfig({ action: ctx.utils.createSimpleClientScript(() => {}), }) + ctx.docks.register({ + id: 'test', + type: 'action', + icon: 'material-symbols:bug-report', + title: 'debug', + // TODO: HMR + action: ctx.utils.createSimpleClientScript(async (ctx) => { + console.log(await ctx.rpc.call('vite:internal:rpc:server:list')) + }), + }) + ctx.docks.register({ id: 'shared-state', type: 'iframe', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 85b777e6..d8d4ff0b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,4 @@ +export { builtinRpcSchemas } from '../../core/src/node/rpc' export { createDevToolsContext } from './node/context' export { DevTools } from './node/plugins' export { createDevToolsMiddleware } from './node/server' diff --git a/packages/core/src/node/cli-commands.ts b/packages/core/src/node/cli-commands.ts index 74717c46..806e9cc5 100644 --- a/packages/core/src/node/cli-commands.ts +++ b/packages/core/src/node/cli-commands.ts @@ -99,7 +99,7 @@ export async function build(options: BuildOptions) { console.log(c.cyan`${MARK_NODE} Writing RPC dump to ${resolve(devToolsRoot, '.vdt-rpc-dump.json')}`) const dump: Record = {} - for (const [key, value] of Object.entries(devtools.context.rpc.functions)) { + for (const [key, value] of Object.entries(devtools.context.rpc.definitions)) { if (value.type === 'static') dump[key] = await value.handler?.() } diff --git a/packages/core/src/node/rpc/index.ts b/packages/core/src/node/rpc/index.ts index d663d90d..839926dc 100644 --- a/packages/core/src/node/rpc/index.ts +++ b/packages/core/src/node/rpc/index.ts @@ -42,6 +42,13 @@ export const builtinRpcDeclarations = [ export type BuiltinServerFunctions = RpcDefinitionsToFunctions +export const builtinRpcSchemas = new Map( + builtinRpcDeclarations.map(d => [ + d.name, + { args: d.argsSchema, returns: d.returnSchema }, + ]), +) + export type BuiltinServerFunctionsStatic = RpcDefinitionsToFunctions< RpcDefinitionsFilter > @@ -51,7 +58,7 @@ export type BuiltinServerFunctionsDump = { } declare module '@vitejs/devtools-kit' { - export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions {} + export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions { } // @keep-sorted export interface DevToolsRpcClientFunctions { diff --git a/packages/core/src/node/rpc/internal/docks-on-launch.ts b/packages/core/src/node/rpc/internal/docks-on-launch.ts index 7834a773..a10b54da 100644 --- a/packages/core/src/node/rpc/internal/docks-on-launch.ts +++ b/packages/core/src/node/rpc/internal/docks-on-launch.ts @@ -1,8 +1,11 @@ import { defineRpcFunction } from '@vitejs/devtools-kit' +import * as v from 'valibot' export const docksOnLaunch = defineRpcFunction({ name: 'vite:internal:docks:on-launch', type: 'action', + args: [v.string()], + return: v.void(), setup: (context) => { const launchMap = new Map>() return { diff --git a/packages/kit/package.json b/packages/kit/package.json index cf615ea9..0ae7627d 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -42,7 +42,8 @@ "@vitejs/devtools-rpc": "workspace:*", "birpc": "catalog:deps", "birpc-x": "catalog:deps", - "immer": "catalog:deps" + "immer": "catalog:deps", + "valibot": "catalog:deps" }, "devDependencies": { "my-ua-parser": "catalog:frontend", diff --git a/packages/kit/src/client/rpc.ts b/packages/kit/src/client/rpc.ts index cbd14222..9681d8e6 100644 --- a/packages/kit/src/client/rpc.ts +++ b/packages/kit/src/client/rpc.ts @@ -9,6 +9,7 @@ import { UAParser } from 'my-ua-parser' import { createEventEmitter } from '../utils/events' import { nanoid } from '../utils/nanoid' import { promiseWithResolver } from '../utils/promise' +import { validateRpcArgs, validateRpcReturn } from '../utils/rpc-validation' import { createRpcSharedStateClientHost } from './rpc-shared-state' const CONNECTION_META_KEY = '__VITE_DEVTOOLS_CONNECTION_META__' @@ -241,10 +242,12 @@ export async function getDevToolsRpcClient( ensureTrusted, requestTrust, call: (...args: any): any => { - return serverRpc.$call( + validateRpcArgs(args[0], args.slice(1)) + const ret = serverRpc.$call( // @ts-expect-error casting ...args, ) + validateRpcReturn(args[0], ret) }, callEvent: (...args: any): any => { return serverRpc.$callEvent( diff --git a/packages/kit/src/utils/define.ts b/packages/kit/src/utils/define.ts index 34ac8361..3fea5a9f 100644 --- a/packages/kit/src/utils/define.ts +++ b/packages/kit/src/utils/define.ts @@ -1,4 +1,39 @@ +import type { RpcFunctionDefinition, RpcFunctionType } from 'birpc-x' +import type { GenericSchema } from 'valibot' import type { DevToolsNodeContext } from '../types' import { createDefineWrapperWithContext } from 'birpc-x' -export const defineRpcFunction = createDefineWrapperWithContext() +export interface RpcOptions< + NAME extends string, + TYPE extends RpcFunctionType, + A extends any[], + R, + AS extends GenericSchema[] | undefined = undefined, + RS extends GenericSchema | undefined = undefined, +> + extends RpcFunctionDefinition { + args?: AS + return?: RS +} + +export function defineRpcFunction< + NAME extends string, + TYPE extends RpcFunctionType, + A extends any[], + R, + AS extends GenericSchema[] | undefined = undefined, + RS extends GenericSchema | undefined = undefined, +>( + options: RpcOptions, +) { + const { args, return: ret, ...rest } = options + const birpc = createDefineWrapperWithContext() + + const fn = birpc(rest) + + const augmentedFn = fn as typeof fn & { argsSchema?: AS, returnSchema?: RS } + augmentedFn.argsSchema = args + augmentedFn.returnSchema = ret + + return augmentedFn +} diff --git a/packages/kit/src/utils/rpc-validation.ts b/packages/kit/src/utils/rpc-validation.ts new file mode 100644 index 00000000..59185ebe --- /dev/null +++ b/packages/kit/src/utils/rpc-validation.ts @@ -0,0 +1,57 @@ +import type { GenericSchema } from 'valibot' +import { builtinRpcSchemas } from '@vitejs/devtools' +import { serverRpcSchemas } from '@vitejs/devtools-vite' +import { parse } from 'valibot' + +const rpcSchemas = new Map([ + ...builtinRpcSchemas, + ...serverRpcSchemas, +]) + +export function validateSchema(schema: GenericSchema, data: any, prefix: string) { + try { + parse(schema, data) + } + catch (e) { + throw new Error(`${prefix}: ${(e as Error).message}`) + } +} + +function getSchema(method: string) { + const schema = rpcSchemas.get(method as any) + if (!schema) + throw new Error(`RPC method "${method}" is not defined.`) + return schema +} + +export function validateRpcArgs(method: string, args: any[]) { + const schema = getSchema(method) + if (!schema) + throw new Error(`RPC method "${method}" is not defined.`) + + const { args: argsSchema } = schema + if (!argsSchema) + return + + if (argsSchema.length !== args.length) + throw new Error(`Invalid number of arguments for RPC method "${method}". Expected ${argsSchema.length}, got ${args.length}.`) + + for (let i = 0; i < argsSchema.length; i++) { + const s = argsSchema[i] + if (!s) + continue + validateSchema(s, args[i], `Invalid argument #${i + 1}`) + } +} + +export function validateRpcReturn(method: string, data: any) { + const schema = getSchema(method) + if (!schema) + throw new Error(`RPC method "${method}" is not defined.`) + + const { returns: returnSchema } = schema + if (!returnSchema) + return + + validateSchema(returnSchema, data, `Invalid return value for RPC method "${method}"`) +} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index a0ed1384..51cf0eb9 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -1 +1,2 @@ export * from './plugin' +export { serverRpcSchemas } from './rpc/index' diff --git a/packages/vite/src/node/rpc/index.ts b/packages/vite/src/node/rpc/index.ts index 583fa4dd..412f1db4 100644 --- a/packages/vite/src/node/rpc/index.ts +++ b/packages/vite/src/node/rpc/index.ts @@ -34,6 +34,13 @@ export const rpcFunctions = [ export type ServerFunctions = RpcDefinitionsToFunctions +export const serverRpcSchemas = new Map( + rpcFunctions.map(d => [ + d.name, + { args: d.argsSchema, returns: d.returnSchema }, + ]), +) + export type ServerFunctionsStatic = RpcDefinitionsToFunctions< RpcDefinitionsFilter > diff --git a/packages/vite/src/nuxt.config.ts b/packages/vite/src/nuxt.config.ts index 763b3099..2b06c574 100644 --- a/packages/vite/src/nuxt.config.ts +++ b/packages/vite/src/nuxt.config.ts @@ -91,6 +91,8 @@ export default defineNuxtConfig({ build: { rolldownOptions: { devtools: {}, + // https://github.com/parcel-bundler/lightningcss/issues/701 + external: ['lightningcss'], }, minify: NUXT_DEBUG_BUILD ? false : undefined, cssMinify: false, diff --git a/packages/vite/tsdown.config.ts b/packages/vite/tsdown.config.ts index 3909fb00..5c93afda 100644 --- a/packages/vite/tsdown.config.ts +++ b/packages/vite/tsdown.config.ts @@ -5,6 +5,14 @@ export default defineConfig({ index: 'src/index.ts', dirs: 'src/dirs.ts', }, + /* + * Since `lightningcss` is a dependency of vite, and devtools/vite -> devtools/kit -> devtools/core, lightningcss + * would be bundled into the final build output, which would cause issues when used in environments where + * lightningcss is expected to be an external dependency. https://github.com/parcel-bundler/lightningcss/issues/701 + */ + external: [ + 'lightningcss', + ], tsconfig: '../../tsconfig.base.json', target: 'esnext', exports: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45228486..0ba1c2c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,9 @@ catalogs: unstorage: specifier: ^1.17.3 version: 1.17.3 + valibot: + specifier: ^1.2.0 + version: 1.2.0 webext-bridge: specifier: ^6.0.1 version: 6.0.1 @@ -623,6 +626,9 @@ importers: immer: specifier: catalog:deps version: 11.1.3 + valibot: + specifier: catalog:deps + version: 1.2.0(typescript@5.9.3) devDependencies: my-ua-parser: specifier: catalog:frontend @@ -6702,6 +6708,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -13749,6 +13763,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dcbc0657..7e75e071 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,6 +54,7 @@ catalogs: tinyglobby: ^0.2.15 unconfig: ^7.4.2 unstorage: ^1.17.3 + valibot: ^1.2.0 webext-bridge: ^6.0.1 ws: ^8.19.0 devtools: diff --git a/test/exports/@vitejs/devtools-vite.yaml b/test/exports/@vitejs/devtools-vite.yaml index ec34cc10..1ab2b362 100644 --- a/test/exports/@vitejs/devtools-vite.yaml +++ b/test/exports/@vitejs/devtools-vite.yaml @@ -1,4 +1,5 @@ .: DevToolsViteUI: function + serverRpcSchemas: object ./dirs: clientPublicDir: string diff --git a/test/exports/@vitejs/devtools.yaml b/test/exports/@vitejs/devtools.yaml index 76311d2c..44a48a9c 100644 --- a/test/exports/@vitejs/devtools.yaml +++ b/test/exports/@vitejs/devtools.yaml @@ -1,4 +1,5 @@ .: + builtinRpcSchemas: object createDevToolsContext: function createDevToolsMiddleware: function DevTools: function