From 256c25d7fe70ddc1392d5a973e742c1e1e309cee Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 17 Sep 2026 23:18:55 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20start.instrument=20=E2=80=94=20a=20serv?= =?UTF-8?q?er=20module=20awaited=20to=20completion=20before=20the=20handle?= =?UTF-8?q?r=20graph=20loads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seam APM/OpenTelemetry setup needs: `Sentry.init()` must run before the modules it patches are loaded, and ESM import order cannot provide that — static imports are hoisted and evaluated in dependency order, so `import './instrument'` at the top of an entry still runs after everything the entry imports. With `start.instrument` the plugin hands out the handler entry as `await import(instrument); await import(handler)`, exports re-declared by name, on every surface (dev, build, preview, a host consuming the entry). Replaces the per-host `node --import instrument.mjs` dance. The start-ssr suite proves the contract without an APM: an instrument module with real async work records that nothing from the graph had run when it started and that it had finished by the time middleware.ts (which imports @solidjs/web) evaluated — asserted over dev, prod and preview, plus the wrapper's codegen shape. Also: the componentNames note in getSolidOptions no longer calls the labels DOM-only (the SSR generate emits them from the compilers that carry solidjs/solid#3441, 2.0.0-rc.9), and a new `observe` e2e mode asserts an `observe: true` build resolves the observe artifacts and carries client component labels — the SSR-label and @solidjs/web server-observe checks record why they are not yet assertable on rc.8 and turn real on rc.9. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- .../start-instrument-early-server-import.md | 9 + README.md | 26 +++ examples/start-ssr/src/instrument.ts | 27 +++ examples/start-ssr/src/middleware.ts | 11 ++ examples/start-ssr/test/run.mjs | 161 +++++++++++++++++- examples/start-ssr/vite.config.ts | 13 ++ src/index.ts | 8 +- src/ssr/index.ts | 59 ++++++- 8 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 .changeset/start-instrument-early-server-import.md create mode 100644 examples/start-ssr/src/instrument.ts diff --git a/.changeset/start-instrument-early-server-import.md b/.changeset/start-instrument-early-server-import.md new file mode 100644 index 0000000..22175ec --- /dev/null +++ b/.changeset/start-instrument-early-server-import.md @@ -0,0 +1,9 @@ +--- +'@solidjs/vite-plugin': patch +--- + +`start.instrument`: a server-only module the plugin runs to completion before anything else in the server graph loads — the app, the middleware, `@solidjs/web`, every dependency. The seam for instrumentation that must patch the runtime before the modules it patches are loaded (an APM's OpenTelemetry setup, a profiler, a `module.register` hook), honored on every surface: `vite dev`, `vite build`, `vite preview`, and a host consuming the handler entry. Replaces the per-host `node --import instrument.mjs` dance. + +Import order cannot do this in ESM — static imports are hoisted and evaluated in dependency order — so the generated handler entry becomes `await import(instrument); await import(handler)`, with the handler's surface (`handleRequest`, the `fetch` default) re-declared by name. The module may be async and needs no exports; the server build must keep code splitting on (the default). + +Also: the `componentNames` note in the compiler options no longer calls the labels DOM-only — the SSR generate emits them too from the compilers that carry solidjs/solid#3441 (2.0.0-rc.9), and the start-ssr suite gains an `observe` mode that asserts an `observe: true` production build resolves the observe artifacts and carries component labels (the SSR half asserted once the workspace rides an rc that emits them). diff --git a/README.md b/README.md index da0454f..fb93360 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,32 @@ whatever the hook renders must be matched client-side for hydration — routers that own both sides (their client entry re-creates the router and hydrates the same tree) fit naturally. +**`instrument`** — a server-only module that runs to completion before +anything else in the server graph loads: the app, the middleware, +`@solidjs/web`, every dependency. The seam for instrumentation that must +patch the runtime before the modules it patches are loaded — an APM's +OpenTelemetry setup, a profiler, a `module.register` hook: + +```ts +// vite.config.ts +solid({ start: { instrument: './src/instrument.ts' }, ssr: true }); + +// src/instrument.ts +import * as Sentry from '@sentry/node'; +Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1 }); +``` + +Import order alone cannot do this in ESM: static imports are hoisted and +evaluated in dependency order, so `import './instrument'` at the top of an +entry still runs after everything the entry imports. The plugin therefore +hands out the handler as `await import(instrument); await import(handler)` +— top-level await sequencing, the one construct that guarantees the order — +on every surface (`vite dev`, `vite build`, `vite preview`, a host consuming +the handler entry). This replaces the per-host `node --import +instrument.mjs` dance. The module may be async and needs no exports; the +server build must keep code splitting on (the default), since inlining +dynamic imports would hoist the handler graph back above the instrument. + **`renderMode`** — how a page render becomes a response body: `'stream'` (the default) or `'async'`, or a module path deciding per request. diff --git a/examples/start-ssr/src/instrument.ts b/examples/start-ssr/src/instrument.ts new file mode 100644 index 0000000..fe2f926 --- /dev/null +++ b/examples/start-ssr/src/instrument.ts @@ -0,0 +1,27 @@ +// `start.instrument` (SSR_INSTRUMENT=1, middleware mode): the module the +// plugin awaits to completion before anything else in the server graph +// loads — the seam an APM's OpenTelemetry setup needs (`Sentry.init()` +// must run before the modules it patches are loaded). This fake proves the +// contract without an APM: +// - it evaluates FIRST: nothing from the app graph has run yet, so the +// ordering register it creates is empty (middleware.ts appends to it at +// its own module top level, which cannot happen before this line), +// - it is awaited to COMPLETION: real async work here (a timer) finishes +// before the handler graph is even imported — middleware.ts reads `done` +// at its top level and finds it true. A static `import './instrument'` +// would fail this: ESM hoists static imports and evaluates them in +// dependency order, so the handler's `@solidjs/web` import graph would +// run before this module's body. +// - it counts evaluations: once per server process (dev and prod), not once +// per request. +declare global { + // eslint-disable-next-line no-var + var __solidInstrument: { order: string[]; done: boolean; evaluations: number } | undefined; +} + +const state = (globalThis.__solidInstrument ??= { order: [], done: false, evaluations: 0 }); +state.evaluations++; +const alreadyLoaded = state.order.length; +await new Promise((resolve) => setTimeout(resolve, 20)); +state.order.push(`instrument(before:${alreadyLoaded})`); +state.done = true; diff --git a/examples/start-ssr/src/middleware.ts b/examples/start-ssr/src/middleware.ts index 82b33c2..505e984 100644 --- a/examples/start-ssr/src/middleware.ts +++ b/examples/start-ssr/src/middleware.ts @@ -19,6 +19,16 @@ // handle fall back to Vite's own pipeline in dev. import { getRequestEvent } from '@solidjs/web'; +// `start.instrument` evidence (SSR_INSTRUMENT=1): this module evaluates as +// part of the handler graph — after `@solidjs/web` above — so what the +// instrument register holds HERE is what it held before the graph loaded. +// A `done: true` at this point means the instrument module was awaited to +// completion first, not merely imported first. +const instrumentAtLoad = globalThis.__solidInstrument + ? { ...globalThis.__solidInstrument, order: [...globalThis.__solidInstrument.order] } + : null; +globalThis.__solidInstrument?.order.push('middleware'); + type Next = (request?: Request) => Promise; // A minimal filesystem-routing/createAPIHandler stand-in: owns /api/* and @@ -121,6 +131,7 @@ async function first(request: Request, next: Next): Promise { // these must be observable on the response head. response.headers.set('x-mw-order', (event.locals.order as string[]).join(',')); response.headers.set('x-after-next', 'set-after-next'); + if (instrumentAtLoad) response.headers.set('x-instrument', JSON.stringify(instrumentAtLoad)); return response; } catch (error) { return new Response(`caught: ${error instanceof Error ? error.message : String(error)}`, { diff --git a/examples/start-ssr/test/run.mjs b/examples/start-ssr/test/run.mjs index 8fa0584..b7337e9 100644 --- a/examples/start-ssr/test/run.mjs +++ b/examples/start-ssr/test/run.mjs @@ -131,6 +131,7 @@ // (default: all) import { spawn, execSync } from 'node:child_process'; +import { createRequire } from 'node:module'; import { fileURLToPath, pathToFileURL } from 'node:url'; import path from 'node:path'; import { mkdirSync, rmSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; @@ -2714,6 +2715,37 @@ async function runFramesMode() { // - error middleware (/boom: a render throw becomes the middleware's 500). async function runMiddlewareChecksOverHttp(mode, origin, functionId) { const page = await fetchStreamed(origin + '/'); + // ---- start.instrument (src/instrument.ts) ------------------------------- + // What middleware.ts saw at ITS module load, i.e. after `@solidjs/web` + // and the handler graph began evaluating: the instrument had run first + // (nothing ahead of it in the register), had been awaited to completion + // (its timer finished: `done`), and had evaluated once for the process. + const instrumentRaw = page.headers.get('x-instrument'); + let instrument = null; + try { + instrument = instrumentRaw ? JSON.parse(instrumentRaw) : null; + } catch {} + record( + mode, + 'instrument', + 'instrument module ran before the handler graph, first in the register', + !!instrument && instrument.order[0] === 'instrument(before:0)', + `x-instrument: ${instrumentRaw}`, + ); + record( + mode, + 'instrument', + 'instrument module was awaited to completion before @solidjs/web loaded', + !!instrument && instrument.done === true, + `x-instrument: ${instrumentRaw}`, + ); + record( + mode, + 'instrument', + 'instrument module evaluated once per server process', + !!instrument && instrument.evaluations === 1, + `x-instrument: ${instrumentRaw}`, + ); record( mode, 'mw', @@ -3000,7 +3032,15 @@ async function runMiddlewareMode() { // SSR_SETUP rides the middleware mode: the hook's contract (ordering // after the chain, shared locals) is only observable with a middleware // in front anyway. - const env = { ...process.env, SSR_MIDDLEWARE: '1', SSR_SETUP: '1', SSR_DEVTOOLS: '0' }; + // SSR_INSTRUMENT rides along too: the instrument module's evidence is a + // header the middleware sets, so it needs the chain in front as well. + const env = { + ...process.env, + SSR_MIDDLEWARE: '1', + SSR_SETUP: '1', + SSR_INSTRUMENT: '1', + SSR_DEVTOOLS: '0', + }; let server; let serverLog = ''; @@ -3019,10 +3059,34 @@ async function runMiddlewareMode() { // mutation stays possible through the whole unwind. process.env.SSR_MIDDLEWARE = '1'; process.env.SSR_SETUP = '1'; + process.env.SSR_INSTRUMENT = '1'; let probe; try { probe = await createServer({ root: exampleDir, server: { middlewareMode: true } }); - const transformed = await probe.environments.ssr.transformRequest('virtual:solid-ssr-handler'); + // ---- Codegen: start.instrument sequences the handler behind two awaits + // The entry the plugin hands out is a wrapper: `await import(instrument)` + // then `await import(impl)`, exports re-declared by name. Static + // imports would be hoisted and defeat the ordering. + const wrapper = (await probe.environments.ssr.transformRequest('virtual:solid-ssr-handler'))?.code || ''; + const instrumentAt = wrapper.indexOf('instrument.ts'); + const implAt = wrapper.indexOf('virtual:solid-ssr-handler-impl'); + record( + 'mw-codegen', + 'instrument', + 'handler entry awaits the instrument module before importing the handler', + instrumentAt !== -1 && implAt !== -1 && instrumentAt < implAt && /await\s+__vite_ssr_dynamic_import__|await\s+import/.test(wrapper), + wrapper.slice(0, 400), + ); + record( + 'mw-codegen', + 'instrument', + 'wrapper re-declares the handler surface by name (handleRequest, default)', + // The SSR transform rewrites `export const`/`export default` into + // defineProperty calls on the exports object; match the names. + /["']?handleRequest["']?/.test(wrapper) && /["']default["']|export\s+default/.test(wrapper), + wrapper.slice(0, 400), + ); + const transformed = await probe.environments.ssr.transformRequest('virtual:solid-ssr-handler-impl'); const code = transformed?.code || ''; const unwind = code.indexOf('runMiddleware(request'); // The SSR transform rewrites the imported binding to a member access @@ -3100,6 +3164,7 @@ async function runMiddlewareMode() { await probe?.close(); delete process.env.SSR_MIDDLEWARE; delete process.env.SSR_SETUP; + delete process.env.SSR_INSTRUMENT; } // ---- Dev: the chain fronts the dev middlewares ----------------------- @@ -3192,7 +3257,9 @@ async function runPreviewMode() { // SSR_SETUP rides along like in middleware mode: the shared chain checks // assert the per-request setup hook, and preview must serve the built // entry that threads it exactly like dev and prod. - const env = { ...process.env, SSR_MIDDLEWARE: '1', SSR_SETUP: '1' }; + // SSR_INSTRUMENT too: `vite preview` serves the built handler, so the + // instrument sequencing is asserted on the third surface here. + const env = { ...process.env, SSR_MIDDLEWARE: '1', SSR_SETUP: '1', SSR_INSTRUMENT: '1' }; let server; let serverLog = ''; @@ -4146,6 +4213,92 @@ async function runBabelHmrMode() { } } +// `observe: true` (SOLID_OBSERVE=1, src/App.tsx unchanged): a production +// build on Solid's observe tier. What the built bundles must show: +// - the client resolved the observe artifact (`web.observe` chunk) and every +// component call carries its source label — `createComponent(Comp, props, +// "Comp")` survives minification as a string, which is what lets owner +// paths read `` in a minified app; +// - the server resolved solid-js's observe build (findings and boundary +// records need it); +// - the server bundle carries component labels in its SSR output and +// @solidjs/web's observe server build. Both landed in Solid after +// 2.0.0-rc.8 (solidjs/solid#3441, #3433), so on older installs the two +// checks record the reason instead of failing; they become real +// assertions the moment the workspace rides an rc that carries them. +async function runObserveMode() { + const mode = 'observe'; + console.log(`\n=== ${mode.toUpperCase()} ===`); + const env = { ...process.env, SOLID_OBSERVE: '1' }; + console.log(' building…'); + execSync('pnpm run build', { cwd: exampleDir, stdio: 'pipe', env }); + + const clientDir = path.join(exampleDir, 'dist/client/assets'); + const clientFiles = readdirSync(clientDir).filter((f) => f.endsWith('.js')); + const entryFile = clientFiles.find((f) => f.startsWith('virtual_solid-ssr-entry-client-')); + const entry = entryFile ? readFileSync(path.join(clientDir, entryFile), 'utf-8') : ''; + record( + mode, + 'client', + 'client entry resolves the observe artifact of @solidjs/web', + clientFiles.some((f) => f.startsWith('web.observe-')) && /web\.observe-/.test(entry), + clientFiles.join(', '), + ); + // Minifiers emit the label with either quote style; the App's component + // tags are the labels expected. + const labelsIn = (code) => + [...code.matchAll(/,\s*(["'`])([A-Z][A-Za-z]*)\1\)/g)].map((m) => m[2]); + const clientLabels = new Set(labelsIn(entry)); + record( + mode, + 'client', + 'client components compile with their source labels (componentNames)', + clientLabels.has('HmrTarget') && clientLabels.size >= 5, + `labels: ${[...clientLabels].join(', ') || 'none'}`, + ); + + const serverBundle = readFileSync(path.join(exampleDir, 'dist/server/server.js'), 'utf-8'); + record( + mode, + 'server', + 'server bundle resolves the observe build of solid-js', + /solid-js\/dist\/server\.observe\.js|server\.observe/.test(serverBundle), + ); + // The two rc.9 checks: what the installed Solid can and cannot do yet. + const exampleRequire = createRequire(path.join(exampleDir, 'package.json')); + const prereleaseOf = (pkg) => { + // `package.json` may not be exported; resolve the entry and walk up. + let dir = path.dirname(exampleRequire.resolve(pkg)); + while (!existsSync(path.join(dir, 'package.json'))) dir = path.dirname(dir); + const v = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf-8')).version; + const m = /-rc\.(\d+)$/.exec(v); + return { version: v, rc: m ? Number(m[1]) : Infinity }; + }; + const compiler = prereleaseOf('@solidjs/compiler'); + const web = prereleaseOf('@solidjs/web'); + const serverLabels = new Set(labelsIn(serverBundle)); + const ssrLabelsExpected = compiler.rc >= 9; + record( + mode, + 'server', + 'SSR output compiles with component labels (compilers from solidjs/solid#3441, 2.0.0-rc.9)', + ssrLabelsExpected ? serverLabels.has('HmrTarget') : true, + ssrLabelsExpected + ? `labels: ${[...serverLabels].join(', ') || 'none'}` + : `not asserted: @solidjs/compiler ${compiler.version} predates SSR componentNames`, + ); + const webObserveExpected = web.rc >= 9; + record( + mode, + 'server', + "server bundle resolves @solidjs/web's observe build (server records and findings, 2.0.0-rc.9)", + webObserveExpected ? /@solidjs\/web\/dist\/server\.observe\.js/.test(serverBundle) : true, + webObserveExpected + ? 'expected @solidjs/web/dist/server.observe.js in the bundle' + : `not asserted: @solidjs/web ${web.version} has no server observe build`, + ); +} + async function runExternalMode() { const mode = 'external'; console.log(`\n=== ${mode.toUpperCase()} ===`); @@ -4410,6 +4563,7 @@ const ALL_MODES = [ 'frames', 'babel-hmr', 'external', + 'observe', 'detect', 'vitest', ]; @@ -4434,6 +4588,7 @@ for (const mode of modes) { else if (mode === 'frames') await runFramesMode(); else if (mode === 'babel-hmr') await runBabelHmrMode(); else if (mode === 'external') await runExternalMode(); + else if (mode === 'observe') await runObserveMode(); else if (mode === 'vitest') await runVitestMode(); else await runDetectMode(); } diff --git a/examples/start-ssr/vite.config.ts b/examples/start-ssr/vite.config.ts index 9065227..109a42b 100644 --- a/examples/start-ssr/vite.config.ts +++ b/examples/start-ssr/vite.config.ts @@ -27,6 +27,13 @@ import solidPlugin from '@solidjs/vite-plugin'; // - SSR_SETUP=1 wires src/setup.tsx through `start.setup` (middleware mode): // the per-request app-setup hook, awaited between the middleware chain and // renderToStream with the shared request event in hand. +// - SSR_INSTRUMENT=1 wires src/instrument.ts through `start.instrument` +// (middleware mode): the module awaited to completion before the handler +// graph loads — the APM/OpenTelemetry seam; middleware.ts reports what it +// saw at its own load in an `x-instrument` header. +// - SOLID_OBSERVE=1 (observe mode) turns on `observe`: the `observe` export +// condition everywhere and the compiler's `componentNames` for both +// postures, so the built server AND client bundles carry component labels. // - SSR_RENDER_MODE sets `start.renderMode` (render-mode mode): `module` // wires src/render-mode.ts (the per-request policy: header / crawler UA / // `?nojs`); any other value passes through verbatim — `async` for the @@ -133,6 +140,11 @@ export default defineConfig({ solidPlugin({ compiler: jsxCompiler, ssr: true, + // SOLID_OBSERVE=1 (observe mode): the production-speed runtime that + // keeps `OBSERVE` alive, with component labels compiled into BOTH + // postures — the server bundle's boundary records and findings locate + // by component the same way the client's do. + ...(process.env.SOLID_OBSERVE ? { observe: true } : {}), start: serverComponents ? { app: 'src/frames/FramesApp.tsx' } : process.env.SSR_DOCUMENT @@ -177,6 +189,7 @@ export default defineConfig({ // renderToStream, receiving the event and returning the // component to render (the TanStack-style async router seam). ...(process.env.SSR_SETUP ? { setup: './src/setup.tsx' } : {}), + ...(process.env.SSR_INSTRUMENT ? { instrument: './src/instrument.ts' } : {}), // SSR_RENDER_MODE (render-mode mode): `module` → the // per-request policy module; anything else verbatim // (`async`, or an invalid value for the validation checks). diff --git a/src/index.ts b/src/index.ts index 26c07d2..da3cec9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -553,9 +553,11 @@ function getSolidOptions( // Component labels: the dev and observe runtimes name each component's // owner (``) for diagnostics and attribution paths. Without the // compiler carrying the source tag name, a minified build labels owners by - // whatever the minifier left of `Comp.name`. DOM-only by construction (the - // ssr generate ignores the flag), and the production runtime ignores the - // argument, so it is only emitted for the postures whose runtime reads it. + // whatever the minifier left of `Comp.name`. Both generates emit it — the + // ssr generate from the compilers that carry solidjs/solid#3441 + // (2.0.0-rc.9), so server findings and boundary records locate by + // component too — and the production runtime ignores the argument, so it + // is only emitted for the postures whose runtime reads it. return { ...solidOptions, ...(serverComponents && solidOptions.generate === 'ssr' ? { serverComponents: true } : {}), diff --git a/src/ssr/index.ts b/src/ssr/index.ts index 9261d92..a07537d 100644 --- a/src/ssr/index.ts +++ b/src/ssr/index.ts @@ -171,6 +171,29 @@ export interface StartOptions { * @default undefined */ middleware?: string; + /** + * Path to a server-only module (resolved relative to the Vite root) that + * runs to completion before anything else in the server graph loads — the + * app, the middleware, `@solidjs/web`, every dependency. The seam for + * instrumentation that must patch the runtime before the modules it + * patches are loaded: an APM's OpenTelemetry setup (`Sentry.init()`, + * `NodeSDK.start()`), a profiler, a custom `module.register` hook. + * Replaces the per-host `node --import instrument.mjs` dance with one + * option the plugin honors on every surface: `vite dev`, `vite build`, + * `vite preview`, and a host consuming the handler entry directly. + * + * How: the generated handler entry becomes `await import(instrument); + * await import(handler)` — top-level await sequencing is the only thing + * in ESM that guarantees the order, since static imports are hoisted and + * evaluated in dependency order regardless of where they are written. + * The module may be async (top-level `await` is honored) and needs no + * exports. The server build must keep code splitting on (the default) — + * inlining dynamic imports would hoist the handler graph back above the + * instrument. + * + * @default undefined + */ + instrument?: string; /** * Path to a server-only module (resolved relative to the Vite root) whose * default export runs once per request in the generated server entry, @@ -318,6 +341,13 @@ export interface StartOptions { // chain and one request event across both dispatch paths). export const SSR_HANDLER_ID = 'virtual:solid-ssr-handler'; const HANDLER_ID = SSR_HANDLER_ID; +// With `start.instrument`, the handler id becomes a thin wrapper that awaits +// the instrument module and only THEN imports the real handler under this +// id — the one way ESM can run something to completion before the rest of +// the graph is even fetched (static imports are hoisted and evaluated in +// dependency order, so `import './instrument'` first would still evaluate +// AFTER `@solidjs/web` and every module it pulls in). +const HANDLER_IMPL_ID = 'virtual:solid-ssr-handler-impl'; // Dev-only response marker: the generated dev handler answers non-page // requests that fell through the whole middleware chain to the terminal // page dispatch with a marked 404 instead of rendering HTML at them, and @@ -574,6 +604,8 @@ export function startServe( let entries: ResolvedEntries | undefined; /** Absolute path of the user's middleware module, when configured. */ let middlewarePath: string | null = null; + /** Absolute path of the instrument module awaited before the handler graph, when configured. */ + let instrumentPath: string | null = null; /** Absolute path of the per-request setup module, when configured (server mode). */ let setupPath: string | null = null; /** @@ -967,6 +999,24 @@ export function startServe( // `serverFunctions` is enabled the endpoint is dispatched here on every // surface (the runnable-dev middleware routes through this module), so // user middleware and the shared request event front it identically. + /** + * The handler entry with `start.instrument`: sequence the instrument + * module to completion, then load the real handler. The two awaits are + * the contract — see the option's docs. Exports are re-declared by name + * (the handler's surface is fixed: `handleRequest` and the `fetch` + * default) because a static `export * from` would be hoisted like any + * other static import and defeat the ordering. + */ + function instrumentedHandlerCode(): string { + return [ + `await import(${JSON.stringify(instrumentPath)});`, + `const handler = await import(${JSON.stringify(HANDLER_IMPL_ID)});`, + `export const handleRequest = handler.handleRequest;`, + `export default handler.default;`, + ``, + ].join('\n'); + } + function handlerModuleCode(externalDev: boolean): string { const { generated, entryClient } = requireEntries(); const composeServerFunctions = internal.serverFunctions; @@ -1303,6 +1353,9 @@ export function startServe( middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null; + instrumentPath = options.instrument + ? path.resolve(root, normalizeUserPath(root, options.instrument, 'instrument')) + : null; // Server-mode only, like `entryServer`/`external` (a documented // no-op in client mode so configs survive the `ssr` boolean flip). setupPath = @@ -1463,6 +1516,9 @@ export function startServe( if (source === HANDLER_ID) { return { id: HANDLER_ID, moduleSideEffects: true }; } + if (source === HANDLER_IMPL_ID) { + return { id: HANDLER_IMPL_ID, moduleSideEffects: true }; + } if (source === DEV_STYLES_ID) { return { id: RESOLVED_DEV_STYLES_ID, moduleSideEffects: true }; } @@ -1505,10 +1561,11 @@ export function startServe( }, async load(id, opts) { const consumer = getEnvironmentConsumer(this.environment, opts); - if (id === HANDLER_ID) { + if (id === HANDLER_ID || id === HANDLER_IMPL_ID) { if (consumer !== 'server') { this.error(`${HANDLER_ID} is server-only; import it from server code (SSR build).`); } + if (id === HANDLER_ID && instrumentPath) return instrumentedHandlerCode(); const externalDev = !isBuild && this.environment.mode === 'dev' &&