From 6674ade3b9b623378d627a12b82f5cf1a2d1fda5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 11 Oct 2024 18:21:17 +0200 Subject: [PATCH] feat: implement fetchable runner --- .../vite/src/module-runner/fetchableRunner.ts | 59 ++++++++++++++ packages/vite/src/module-runner/index.ts | 2 + packages/vite/src/node/server/index.ts | 2 + .../middlewares/environmentTransform.ts | 77 +++++++++++++++++++ .../__tests__/server-fetchable-runner.spec.ts | 36 +++++++++ packages/vite/src/shared/constants.ts | 2 + playground/test-utils.ts | 1 + 7 files changed, 179 insertions(+) create mode 100644 packages/vite/src/module-runner/fetchableRunner.ts create mode 100644 packages/vite/src/node/server/middlewares/environmentTransform.ts create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/server-fetchable-runner.spec.ts diff --git a/packages/vite/src/module-runner/fetchableRunner.ts b/packages/vite/src/module-runner/fetchableRunner.ts new file mode 100644 index 00000000000000..3b535df264bc24 --- /dev/null +++ b/packages/vite/src/module-runner/fetchableRunner.ts @@ -0,0 +1,59 @@ +import { ENVIRONMENT_URL_PUBLIC_PATH } from '../shared/constants' +import { ESModulesEvaluator } from './esmEvaluator' +import { ModuleRunner } from './runner' +import type { ModuleRunnerOptions } from './types' + +export interface FetchableModuleRunnerOptions + extends Pick< + ModuleRunnerOptions, + 'sourcemapInterceptor' | 'evaluatedModules' | 'hmr' + > { + root: string + serverURL: string + environmentName: string +} + +export function createFetchableModuleRunner( + options: FetchableModuleRunnerOptions, +): ModuleRunner { + const { serverURL, environmentName } = options + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const fetch = globalThis.fetch + if (!fetch) { + throw new TypeError('fetch is not available in this environment') + } + return new ModuleRunner( + { + root: options.root, + transport: { + async fetchModule(moduleUrl, importer, { cached, startOffset } = {}) { + const serverUrl = new URL( + `${ENVIRONMENT_URL_PUBLIC_PATH}/${environmentName}`, + serverURL, + ) + serverUrl.searchParams.set('moduleUrl', encodeURIComponent(moduleUrl)) + if (importer) { + serverUrl.searchParams.set('importer', encodeURIComponent(importer)) + } + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const request = new Request(serverUrl, { + headers: { + 'x-vite-cache': String(cached ?? false), + 'x-vite-start-offset': String(startOffset ?? ''), + }, + }) + const response = await fetch(request) + if (response.status !== 200) { + // TODO: better error? + throw new Error( + `Failed to fetch module ${moduleUrl}, responded with ${response.status} (${response.statusText})`, + ) + } + return await response.json() + }, + }, + hmr: options.hmr, + }, + new ESModulesEvaluator(), + ) +} diff --git a/packages/vite/src/module-runner/index.ts b/packages/vite/src/module-runner/index.ts index 7130795f862767..828314fdba2b9f 100644 --- a/packages/vite/src/module-runner/index.ts +++ b/packages/vite/src/module-runner/index.ts @@ -5,6 +5,8 @@ export { ModuleRunner } from './runner' export { ESModulesEvaluator } from './esmEvaluator' export { RemoteRunnerTransport } from './runnerTransport' +export { createFetchableModuleRunner } from './fetchableRunner' + export type { RunnerTransport } from './runnerTransport' export type { HMRLogger, HMRConnection } from '../shared/hmr' export type { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index bf2897326e120a..8b70b9428bcba8 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -95,6 +95,7 @@ import { transformRequest } from './transformRequest' import { searchForPackageRoot, searchForWorkspaceRoot } from './searchRoot' import { warmupFiles } from './warmup' import type { DevEnvironment } from './environment' +import { environmentTransformMiddleware } from './middlewares/environmentTransform' export interface ServerOptions extends CommonServerOptions { /** @@ -829,6 +830,7 @@ export async function _createServer( } middlewares.use(cachedTransformMiddleware(server)) + middlewares.use(environmentTransformMiddleware(server)) // proxy const { proxy } = serverConfig diff --git a/packages/vite/src/node/server/middlewares/environmentTransform.ts b/packages/vite/src/node/server/middlewares/environmentTransform.ts new file mode 100644 index 00000000000000..faa2e668522b00 --- /dev/null +++ b/packages/vite/src/node/server/middlewares/environmentTransform.ts @@ -0,0 +1,77 @@ +import type { Connect } from 'dep-types/connect' +import type { ViteDevServer } from '..' +import { + ENVIRONMENT_URL_PUBLIC_PATH, + NULL_BYTE_PLACEHOLDER, +} from '../../../shared/constants' + +export function environmentTransformMiddleware( + server: ViteDevServer, +): Connect.NextHandleFunction { + return async function viteEnvironmentTransformMiddleware(req, res, next) { + if (req.method !== 'GET') { + return next() + } + + let url: string + try { + url = decodeURI(req.url!).replace(NULL_BYTE_PLACEHOLDER, '\0') + } catch (e) { + return next(e) + } + + if (!url.startsWith(ENVIRONMENT_URL_PUBLIC_PATH)) { + return next() + } + + const { pathname, searchParams } = new URL(url, 'http://localhost') + const environmentName = pathname.slice( + ENVIRONMENT_URL_PUBLIC_PATH.length + 1, + ) + const environment = server.environments[environmentName] + + if (!environmentName || !environment) { + res.statusCode = 404 + res.end() + return + } + + const moduleUrl = searchParams.get('moduleUrl') + + if (!moduleUrl) { + res.statusCode = 404 + res.end() + return + } + + // TODO: how to check consistently for all environments(?) + // currently ignores if the consumer is a `server` + // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/transformRequest.ts:271 + // if (!ensureServingAccess(moduleUrl, server, res, next)) { + // return + // } + + try { + const importer = searchParams.get('importer') || '' + const cached = req.headers['x-vite-cache'] === 'true' + const startOffset = req.headers['x-vite-start-offset'] + ? Number(req.headers['x-vite-start-offset']) + : undefined + const moduleResult = await environment.fetchModule(moduleUrl, importer, { + cached, + startOffset, + }) + + if (res.writableEnded) { + return + } + + res.setHeader('Content-Type', 'application/json') + res.statusCode = 200 + res.end(JSON.stringify(moduleResult)) + return + } catch (e) { + return next(e) + } + } +} diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-fetchable-runner.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-fetchable-runner.spec.ts new file mode 100644 index 00000000000000..1c9884721b5dfb --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-fetchable-runner.spec.ts @@ -0,0 +1,36 @@ +import { createRunnableDevEnvironment, createServer } from 'vite' +import { createFetchableModuleRunner } from 'vite/module-runner' +import { expect, test } from 'vitest' + +test('fetchable module runner works correctly', async () => { + const server = await createServer({ + root: import.meta.dirname, + server: { + port: 5010, + watch: null, + hmr: false, + }, + environments: { + custom: { + dev: { + createEnvironment(name, config) { + return createRunnableDevEnvironment(name, config, { + hot: false, + }) + }, + }, + }, + }, + }) + await server.listen() + + const runner = createFetchableModuleRunner({ + root: server.config.root, + serverURL: 'http://localhost:5010', + environmentName: 'custom', + sourcemapInterceptor: false, + }) + + const mod = await runner.import('/fixtures/basic.js') + expect(mod.name).toBe('basic') +}) diff --git a/packages/vite/src/shared/constants.ts b/packages/vite/src/shared/constants.ts index a12c674cc98ed6..b56647b093e481 100644 --- a/packages/vite/src/shared/constants.ts +++ b/packages/vite/src/shared/constants.ts @@ -16,6 +16,8 @@ export const VALID_ID_PREFIX = `/@id/` */ export const NULL_BYTE_PLACEHOLDER = `__x00__` +export const ENVIRONMENT_URL_PUBLIC_PATH = '/@vite/import' + export let SOURCEMAPPING_URL = 'sourceMa' SOURCEMAPPING_URL += 'ppingURL' diff --git a/playground/test-utils.ts b/playground/test-utils.ts index 544bc31f8c0fa1..0fe6b3415348d9 100644 --- a/playground/test-utils.ts +++ b/playground/test-utils.ts @@ -47,6 +47,7 @@ export const ports = { 'css/dynamic-import': 5007, 'css/lightningcss-proxy': 5008, 'backend-integration': 5009, + 'runner-fetchable': 5010, // not imported but used in `server-fetchable-runner.spec.ts` } export const hmrPorts = { 'optimize-missing-deps': 24680,