diff --git a/docs/guide/api-environment-frameworks.md b/docs/guide/api-environment-frameworks.md index 00493c0c844129..df8f4a6e071347 100644 --- a/docs/guide/api-environment-frameworks.md +++ b/docs/guide/api-environment-frameworks.md @@ -38,7 +38,48 @@ if (isRunnableDevEnvironment(server.environments.ssr)) { ``` :::warning -The `runner` is evaluated eagerly when it's accessed for the first time. Beware that Vite enables source map support when the `runner` is created by calling `process.setSourceMapsEnabled` or by overriding `Error.prepareStackTrace` if it's not available. +The `runner` is evaluated lazily only when it's accessed for the first time. Beware that Vite enables source map support when the `runner` is created by calling `process.setSourceMapsEnabled` or by overriding `Error.prepareStackTrace` if it's not available. +::: + +Frameworks that communicate with their runtime via the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) can utilize the `FetchableDevEnvironment` that provides a standardized way of handling requests via the `handleRequest` method: + +```ts +import { + createServer, + createFetchableDevEnvironment, + isFetchableDevEnvironment, +} from 'vite' + +const server = await createServer({ + server: { middlewareMode: true }, + appType: 'custom', + environments: { + custom: { + dev: { + createEnvironment(name, config) { + return createFetchableDevEnvironment(name, config, { + handleRequest(request: Request): Promise | Response { + // handle Request and return a Response + }, + }) + }, + }, + }, + }, +}) + +// Any consumer of the environment API can now call `dispatchFetch` +if (isFetchableDevEnvironment(server.environments.custom)) { + const response: Response = await server.environments.custom.dispatchFetch( + new Request('/request-to-handle'), + ) +} +``` + +:::warning +Vite validates the input and output of the `dispatchFetch` method: the request must be an instance of the global `Request` class and the response must be the instance of the global `Response` class. Vite will throw a `TypeError` if this is not the case. + +Note that although the `FetchableDevEnvironment` is implemented as a class, it is considered an implementation detail by the Vite team and might change at any moment. ::: ## Default `RunnableDevEnvironment` diff --git a/eslint.config.js b/eslint.config.js index 6fc2d08520070a..2d5e16ded9e379 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -63,7 +63,13 @@ export default tseslint.config( 'n/no-exports-assign': 'error', 'n/no-unpublished-bin': 'error', 'n/no-unsupported-features/es-builtins': 'error', - 'n/no-unsupported-features/node-builtins': 'error', + 'n/no-unsupported-features/node-builtins': [ + 'error', + { + // TODO: remove this when we don't support Node 18 anymore + ignores: ['Response', 'Request', 'fetch'], + }, + ], 'n/process-exit-as-throw': 'error', 'n/hashbang': 'error', diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index 823b11bc167e97..de8f93b206f8d6 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -48,6 +48,8 @@ const disallowedVariables = [ 'createServerModuleRunner', 'createServerModuleRunnerTransport', 'isRunnableDevEnvironment', + 'createFetchableDevEnvironment', + 'isFetchableDevEnvironment', ] disallowedVariables.forEach((name) => { Object.defineProperty(module.exports, name, { diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ee5e4e9c820d28..fcba5de1fd3b90 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -27,6 +27,12 @@ export { type RunnableDevEnvironment, type RunnableDevEnvironmentContext, } from './server/environments/runnableEnvironment' +export { + createFetchableDevEnvironment, + isFetchableDevEnvironment, + type FetchableDevEnvironment, + type FetchableDevEnvironmentContext, +} from './server/environments/fetchableEnvironments' export { DevEnvironment, type DevEnvironmentContext, diff --git a/packages/vite/src/node/server/environments/fetchableEnvironments.ts b/packages/vite/src/node/server/environments/fetchableEnvironments.ts new file mode 100644 index 00000000000000..62ca1ac24626d7 --- /dev/null +++ b/packages/vite/src/node/server/environments/fetchableEnvironments.ts @@ -0,0 +1,64 @@ +import type { ResolvedConfig } from '../../config' +import type { DevEnvironmentContext } from '../environment' +import { DevEnvironment } from '../environment' +import type { Environment } from '../../environment' + +export interface FetchableDevEnvironmentContext extends DevEnvironmentContext { + handleRequest(request: Request): Promise | Response +} + +export function createFetchableDevEnvironment( + name: string, + config: ResolvedConfig, + context: FetchableDevEnvironmentContext, +): FetchableDevEnvironment { + if (typeof Request === 'undefined' || typeof Response === 'undefined') { + throw new TypeError( + 'FetchableDevEnvironment requires a global `Request` and `Response` object.', + ) + } + + if (!context.handleRequest) { + throw new TypeError( + 'FetchableDevEnvironment requires a `handleRequest` method during initialisation.', + ) + } + + return new FetchableDevEnvironment(name, config, context) +} + +export function isFetchableDevEnvironment( + environment: Environment, +): environment is FetchableDevEnvironment { + return environment instanceof FetchableDevEnvironment +} + +class FetchableDevEnvironment extends DevEnvironment { + private _handleRequest: (request: Request) => Promise | Response + + constructor( + name: string, + config: ResolvedConfig, + context: FetchableDevEnvironmentContext, + ) { + super(name, config, context) + this._handleRequest = context.handleRequest + } + + public async dispatchFetch(request: Request): Promise { + if (!(request instanceof Request)) { + throw new TypeError( + 'FetchableDevEnvironment `dispatchFetch` must receive a `Request` object.', + ) + } + const response = await this._handleRequest(request) + if (!(response instanceof Response)) { + throw new TypeError( + 'FetchableDevEnvironment `context.handleRequest` must return a `Response` object.', + ) + } + return response + } +} + +export type { FetchableDevEnvironment }