From b1094323f0f63244b30a643fa3c5152edb91d168 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Fri, 22 Aug 2025 23:51:49 -0500 Subject: [PATCH 1/3] working middleware --- packages/start/package.json | 5 +- packages/start/src/config/index.ts | 240 ++++++++++------------- packages/start/src/config/nitroPlugin.ts | 175 +++++++++++------ packages/start/src/middleware/index.tsx | 65 ++++++ 4 files changed, 285 insertions(+), 200 deletions(-) create mode 100644 packages/start/src/middleware/index.tsx diff --git a/packages/start/package.json b/packages/start/package.json index 5165a12fa..723b2374c 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -28,7 +28,8 @@ "types": "./dist/router.d.ts" }, "./server/spa": "./dist/server/spa/index.jsx", - "./client/spa": "./dist/client/spa/index.jsx" + "./client/spa": "./dist/client/spa/index.jsx", + "./middleware": "./dist/middleware/index.jsx" }, "dependencies": { "@babel/core": "^7.28.3", @@ -64,4 +65,4 @@ "devDependencies": { "@types/babel__core": "^7.20.5" } -} +} \ No newline at end of file diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 1f2b02e4d..08213677f 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -1,26 +1,23 @@ +import { createTanStackServerFnPlugin } from "@tanstack/server-functions-plugin"; +import { defu } from "defu"; import { existsSync } from "node:fs"; import path, { isAbsolute, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; import type { StartServerManifest } from "solid-start:server-manifest"; -import { createTanStackServerFnPlugin } from "@tanstack/server-functions-plugin"; -import { defu } from "defu"; -import { normalizePath, type ViteDevServer, type PluginOption, type Rollup } from "vite"; +import { normalizePath, type PluginOption, type Rollup, type ViteDevServer } from "vite"; import solid, { type Options as SolidOptions } from "vite-plugin-solid"; -import { - SolidStartClientFileRouter, - SolidStartServerFileRouter, -} from "./fs-router.js"; +import { isCssModulesFile } from "../server/collect-styles.js"; +import { getSsrDevManifest } from "../server/manifest/dev-server-manifest.js"; +import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js"; import { fsRoutes } from "./fs-routes/index.js"; import { clientDistDir, nitroPlugin, serverDistDir, ssrEntryFile, - type UserNitroConfig, + type UserNitroConfig } from "./nitroPlugin.js"; -import { isCssModulesFile } from "../server/collect-styles.js"; -import { getSsrDevManifest } from "../server/manifest/dev-server-manifest.js"; const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"]; @@ -32,6 +29,7 @@ export interface SolidStartOptions { routeDir?: string; extensions?: string[]; server?: UserNitroConfig; + middleware?: string; } const SolidStartServerFnsPlugin = createTanStackServerFnPlugin({ @@ -41,31 +39,27 @@ const SolidStartServerFnsPlugin = createTanStackServerFnPlugin({ client: { getRuntimeCode: () => `import { createServerReference } from "${normalize( - fileURLToPath(new URL("../server/server-runtime.js", import.meta.url)), + fileURLToPath(new URL("../server/server-runtime.js", import.meta.url)) )}"`, - replacer: (opts) => - `createServerReference(${() => { }}, '${opts.functionId}', '${opts.extractedFilename}')`, + replacer: opts => + `createServerReference(${() => {}}, '${opts.functionId}', '${opts.extractedFilename}')` }, ssr: { getRuntimeCode: () => `import { createServerReference } from '${normalize( - fileURLToPath( - new URL("../server/server-fns-runtime.js", import.meta.url), - ), + fileURLToPath(new URL("../server/server-fns-runtime.js", import.meta.url)) )}'`, - replacer: (opts) => - `createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')`, + replacer: opts => + `createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')` }, server: { getRuntimeCode: () => `import { createServerReference } from '${normalize( - fileURLToPath( - new URL("../server/server-fns-runtime.js", import.meta.url), - ), + fileURLToPath(new URL("../server/server-fns-runtime.js", import.meta.url)) )}'`, - replacer: (opts) => - `createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')`, - }, + replacer: opts => + `createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')` + } }); const absolute = (path: string, root: string) => @@ -78,46 +72,43 @@ const VIRTUAL_MODULES = { serverManifest: "solid-start:server-manifest", getClientManifest: "solid-start:get-client-manifest", getSsrManifest: "solid-start:get-ssr-manifest", - getManifest: "solid-start:get-manifest", + getManifest: "solid-start:get-manifest" } as const; export const CLIENT_BASE_PATH = "_build"; -function solidStartVitePlugin( - options?: SolidStartOptions, -): Array { +function solidStartVitePlugin(options?: SolidStartOptions): Array { const start = defu(options ?? {}, { appRoot: "./src", routeDir: "./routes", ssr: true, devOverlay: true, experimental: { - islands: false, + islands: false }, solid: {}, server: { routeRules: { "/_build/assets/**": { - headers: { "cache-control": "public, immutable, max-age=31536000" }, - }, + headers: { "cache-control": "public, immutable, max-age=31536000" } + } }, experimental: { - asyncContext: true, - }, + asyncContext: true + } }, - extensions: [], + extensions: [] }); const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])]; const routeDir = join(start.appRoot, start.routeDir); let entryExtension = ".tsx"; - if (existsSync(join(process.cwd(), start.appRoot, "app.jsx"))) - entryExtension = ".jsx"; + if (existsSync(join(process.cwd(), start.appRoot, "app.jsx"))) entryExtension = ".jsx"; const handlers = { client: `${start.appRoot}/entry-client${entryExtension}`, - server: `${start.appRoot}/entry-server${entryExtension}`, + server: `${start.appRoot}/entry-server${entryExtension}` }; // console.log(new URL('../server/manifest/ssr-manifest.js', import.meta.url).pathname) @@ -129,8 +120,8 @@ function solidStartVitePlugin( configEnvironment(name) { return { define: { - "import.meta.env.SSR": JSON.stringify(name === "server"), - }, + "import.meta.env.SSR": JSON.stringify(name === "server") + } }; }, config(_, env) { @@ -145,18 +136,14 @@ function solidStartVitePlugin( manifest: true, rollupOptions: { input: { - client: handlers.client, + client: handlers.client }, output: { - dir: path.resolve( - process.cwd(), - clientDistDir, - CLIENT_BASE_PATH, - ), + dir: path.resolve(process.cwd(), clientDistDir, CLIENT_BASE_PATH) }, - external: ["node:fs", "node:path", "node:os", "node:crypto"], - }, - }, + external: ["node:fs", "node:path", "node:os", "node:crypto"] + } + } }, server: { consumer: "server", @@ -170,7 +157,7 @@ function solidStartVitePlugin( rollupOptions: { output: { dir: path.resolve(process.cwd(), serverDistDir), - entryFileNames: ssrEntryFile, + entryFileNames: ssrEntryFile }, plugins: [ { @@ -178,55 +165,51 @@ function solidStartVitePlugin( generateBundle(options, bundle) { // TODO can this hook be called more than once? ssrBundle = bundle; - }, - }, - ] as Array, + } + } + ] as Array }, commonjsOptions: { - include: [/node_modules/], - }, - }, - }, + include: [/node_modules/] + } + } + } }, resolve: { alias: { - "#start/app": join( - process.cwd(), - start.appRoot, - `app${entryExtension}`, - ), + "#start/app": join(process.cwd(), start.appRoot, `app${entryExtension}`), "~": join(process.cwd(), start.appRoot), ...(!start.ssr ? { - "@solidjs/start/server": "@solidjs/start/server/spa", - "@solidjs/start/client": "@solidjs/start/client/spa", - } - : {}), - }, + "@solidjs/start/server": "@solidjs/start/server/spa", + "@solidjs/start/client": "@solidjs/start/client/spa" + } + : {}) + } }, define: { "import.meta.env.MANIFEST": `globalThis.MANIFEST`, - "import.meta.env.START_SSR": JSON.stringify(start.ssr), - }, + "import.meta.env.START_SSR": JSON.stringify(start.ssr) + } }; - }, + } }, css(), fsRoutes({ handlers, routers: { - client: (config) => + client: config => new SolidStartClientFileRouter({ dir: absolute(routeDir, config.root), - extensions, + extensions }), - server: (config) => + server: config => new SolidStartServerFileRouter({ dir: absolute(routeDir, config.root), extensions, - dataOnly: !start.ssr, - }), - }, + dataOnly: !start.ssr + }) + } }), // Must be placed after fsRoutes, as treeShake will remove the // server fn exports added in by this plugin @@ -236,22 +219,21 @@ function solidStartVitePlugin( applyToEnvironment(env) { if (env.name === "server") return SolidStartServerFnsPlugin.server; return SolidStartServerFnsPlugin.client; - }, + } }, { name: "solid-start:manifest-plugin", enforce: "pre", resolveId(id) { - if (id === VIRTUAL_MODULES.serverManifest) - return `\0${VIRTUAL_MODULES.serverManifest}`; + if (id === VIRTUAL_MODULES.serverManifest) return `\0${VIRTUAL_MODULES.serverManifest}`; if (id === VIRTUAL_MODULES.getClientManifest) - return new URL('../server/manifest/client-manifest.js', import.meta.url).pathname + return new URL("../server/manifest/client-manifest.js", import.meta.url).pathname; if (id === VIRTUAL_MODULES.getSsrManifest) - return new URL('../server/manifest/ssr-manifest.js', import.meta.url).pathname; + return new URL("../server/manifest/ssr-manifest.js", import.meta.url).pathname; if (id === VIRTUAL_MODULES.getManifest) return this.environment.config.consumer === "server" - ? new URL('../server/manifest/ssr-manifest.js', import.meta.url).pathname - : new URL('../server/manifest/client-manifest.js', import.meta.url).pathname + ? new URL("../server/manifest/ssr-manifest.js", import.meta.url).pathname + : new URL("../server/manifest/client-manifest.js", import.meta.url).pathname; }, async load(id) { if (id === `\0${VIRTUAL_MODULES.serverManifest}`) { @@ -259,64 +241,54 @@ function solidStartVitePlugin( const manifest: StartServerManifest = { clientEntryId: normalizePath(handlers.client), clientViteManifest: {}, - clientManifestData: {}, + clientManifestData: {} }; return `export const manifest = ${JSON.stringify(manifest)}`; } const entry = Object.values(globalThis.START_CLIENT_BUNDLE).find( - (v) => "isEntry" in v && v.isEntry, + v => "isEntry" in v && v.isEntry ); if (!entry) throw new Error("No client entry found"); - const clientManifest: Record< - string, - Record - > = JSON.parse( - (globalThis.START_CLIENT_BUNDLE[".vite/manifest.json"] as any) - .source, + const clientManifest: Record> = JSON.parse( + (globalThis.START_CLIENT_BUNDLE[".vite/manifest.json"] as any).source ); - const clientAssetManifest = Object.entries(clientManifest).reduce( - (acc, [id, entry]) => { - const assets = [ - ...(entry.assets?.filter(Boolean) || []), - ...(entry.css?.filter(Boolean) || []), - ] - .filter( - (asset) => - asset.endsWith(".css") || - asset.endsWith(".js") || - asset.endsWith(".mjs"), - ) - .map( - (asset) => - ({ - tag: "link", - attrs: { - href: join("/", CLIENT_BASE_PATH, asset), - key: join("/", CLIENT_BASE_PATH, asset), - ...(asset.endsWith(".css") - ? { rel: "stylesheet", fetchPriority: "high" } - : { rel: "modulepreload" }), - }, - }) satisfies ManifestAsset, - ); + const clientAssetManifest = Object.entries(clientManifest).reduce((acc, [id, entry]) => { + const assets = [ + ...(entry.assets?.filter(Boolean) || []), + ...(entry.css?.filter(Boolean) || []) + ] + .filter( + asset => asset.endsWith(".css") || asset.endsWith(".js") || asset.endsWith(".mjs") + ) + .map( + asset => + ({ + tag: "link", + attrs: { + href: join("/", CLIENT_BASE_PATH, asset), + key: join("/", CLIENT_BASE_PATH, asset), + ...(asset.endsWith(".css") + ? { rel: "stylesheet", fetchPriority: "high" } + : { rel: "modulepreload" }) + } + } satisfies ManifestAsset) + ); - acc[id] = { - output: `/${CLIENT_BASE_PATH}/${entry.file}`, - assets, - }; - return acc; - }, - {} as ClientManifest, - ); + acc[id] = { + output: `/${CLIENT_BASE_PATH}/${entry.file}`, + assets + }; + return acc; + }, {} as ClientManifest); const manifest: StartServerManifest = { clientEntryId: normalizePath(handlers.client), clientViteManifest: clientManifest as any, - clientManifestData: clientAssetManifest, + clientManifestData: clientAssetManifest }; return `export const manifest = ${JSON.stringify(manifest)};`; @@ -330,25 +302,25 @@ function solidStartVitePlugin( throw new Error("Missing id to get assets."); } return `export default ${JSON.stringify( - await getSsrDevManifest(true, handlers.client).getAssets(id), + await getSsrDevManifest(true, handlers.client).getAssets(id) )}`; } } - }, + } }, - nitroPlugin({ root: process.cwd() }, () => ssrBundle, start.server), + nitroPlugin({ root: process.cwd() }, () => ssrBundle, start.server, start.middleware), { name: "solid-start:capture-client-bundle", enforce: "post", generateBundle(_options, bundle) { globalThis.START_CLIENT_BUNDLE = bundle; - }, + } }, solid({ ...start.solid, ssr: start.ssr, - extensions: extensions.map((ext) => `.${ext}`), - }), + extensions: extensions.map(ext => `.${ext}`) + }) ]; } @@ -377,8 +349,8 @@ function css(): PluginOption { event: "css-update", data: { file, - contents: resp.code, - }, + contents: resp.code + } }); } }, @@ -386,6 +358,6 @@ function css(): PluginOption { if (isCssModulesFile(id)) { cssModules[id] = code; } - }, - } + } + }; } diff --git a/packages/start/src/config/nitroPlugin.ts b/packages/start/src/config/nitroPlugin.ts index bba17885b..290ea963f 100644 --- a/packages/start/src/config/nitroPlugin.ts +++ b/packages/start/src/config/nitroPlugin.ts @@ -1,6 +1,24 @@ +import { + _RequestMiddleware, + _ResponseMiddleware, + createApp, + createEvent, + eventHandler, + getHeader, + H3Event, + sendWebResponse +} from "h3"; +import { + build, + copyPublicAssets, + createNitro, + Nitro, + prepare, + prerender, + type NitroConfig +} from "nitropack"; import { promises as fsp } from "node:fs"; -import path, { dirname, join } from "node:path"; -import { build, copyPublicAssets, createNitro, Nitro, prepare, prerender, type NitroConfig } from "nitropack"; +import path, { dirname, resolve } from "node:path"; import { Connect, EnvironmentOptions, @@ -9,19 +27,21 @@ import { Rollup, ViteDevServer } from "vite"; -import { resolve } from "node:path"; -import { createApp, createEvent, eventHandler, getHeader, H3Event, sendStream, sendWebResponse, setHeader, setHeaders } from "h3"; export const clientDistDir = "node_modules/.solid-start/client-dist"; export const serverDistDir = "node_modules/.solid-start/server-dist"; export const ssrEntryFile = "ssr.mjs"; -export type UserNitroConfig = Omit; +export type UserNitroConfig = Omit< + NitroConfig, + "dev" | "publicAssets" | "renderer" | "rollupConfig" +>; export function nitroPlugin( options: { root: string }, getSsrBundle: () => Rollup.OutputBundle, - nitroConfig?: UserNitroConfig + nitroConfig?: UserNitroConfig, + middleware?: string // handlers: { client: string; server: string } ): Array { return [ @@ -40,50 +60,74 @@ export function nitroPlugin( if (!isRunnableDevEnvironment(serverEnv)) throw new Error("Server environment is not runnable"); - h3App.use(eventHandler(async (event) => { - try { - const serverEntry: { - default: (e: H3Event) => Promise; - } = await serverEnv.runner.import( - "./src/entry-server.tsx", - ); + h3App.use( + eventHandler({ + onRequest: async e => { + if (!middleware) return; + const middlewareEntry = await serverEnv.runner.import(middleware); + const { + onRequest + }: { onRequest?: _RequestMiddleware[] | _RequestMiddleware } = + middlewareEntry?.default || {}; + if (Array.isArray(onRequest)) { + onRequest?.forEach(handler => handler(e)); + } else { + onRequest?.(e); + } + }, + onBeforeResponse: async (e, response) => { + if (!middleware) return; + const middlewareEntry = await serverEnv.runner.import(middleware); + const { + onBeforeResponse + }: { + onBeforeResponse?: + | _ResponseMiddleware[] + | _ResponseMiddleware; + } = middlewareEntry?.default || {}; + if (Array.isArray(onBeforeResponse)) { + onBeforeResponse?.forEach(handler => handler(e, response)); + } else { + onBeforeResponse?.(e, response); + } + }, + handler: async event => { + try { + const serverEntry: { + default: (e: H3Event) => Promise; + } = await serverEnv.runner.import("./src/entry-server.tsx"); - return await serverEntry.default(event); - } catch (e) { - console.error(e); - viteDevServer.ssrFixStacktrace(e as Error); - if ( - getHeader(event, "content-type")?.includes( - "application/json", - ) - ) { - return sendWebResponse( - event, - new Response( - JSON.stringify( - { - status: 500, - error: "Internal Server Error", - message: - "An unexpected error occurred. Please try again later.", - timestamp: new Date().toISOString(), - }, - null, - 2, - ), - { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }, - ), - ); - } - return sendWebResponse( - event, - new Response( - ` + return await serverEntry.default(event); + } catch (e) { + console.error(e); + viteDevServer.ssrFixStacktrace(e as Error); + if (getHeader(event, "content-type")?.includes("application/json")) { + return sendWebResponse( + event, + new Response( + JSON.stringify( + { + status: 500, + error: "Internal Server Error", + message: "An unexpected error occurred. Please try again later.", + timestamp: new Date().toISOString() + }, + null, + 2 + ), + { + status: 500, + headers: { + "Content-Type": "application/json" + } + } + ) + ); + } + return sendWebResponse( + event, + new Response( + ` @@ -92,24 +136,26 @@ export function nitroPlugin( `, - { - status: 500, - headers: { - "Content-Type": "text/html", - }, - }, - ), - ); - } - })) + { + status: 500, + headers: { + "Content-Type": "text/html" + } + } + ) + ); + } + } + }) + ); viteDevServer.middlewares.use(async (req, res) => { const event = createEvent(req, res); @@ -166,7 +212,7 @@ export function nitroPlugin( renderer: ssrEntryFile, rollupConfig: { plugins: [virtualBundlePlugin(getSsrBundle()) as any] - }, + } }; const nitro = await createNitro(resolvedNitroConfig); @@ -278,8 +324,9 @@ function removeHtmlMiddlewares(server: ViteDevServer) { function prepareError(req: Connect.IncomingMessage, error: unknown) { const e = error as Error; return { - message: `An error occured while server rendering ${req.url}:\n\n\t${typeof e === "string" ? e : e.message - } `, + message: `An error occured while server rendering ${req.url}:\n\n\t${ + typeof e === "string" ? e : e.message + } `, stack: typeof e === "string" ? "" : e.stack }; } diff --git a/packages/start/src/middleware/index.tsx b/packages/start/src/middleware/index.tsx new file mode 100644 index 000000000..728517dae --- /dev/null +++ b/packages/start/src/middleware/index.tsx @@ -0,0 +1,65 @@ +// @refresh skip +import { getFetchEvent } from "../server/fetchEvent"; +import { H3Event as HTTPEvent, defineMiddleware, sendWebResponse } from "../server/h3"; +import type { FetchEvent } from "../server/types"; + +/** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */ + +export type MiddlewareFn = (event: FetchEvent) => Promise | unknown; +/** This composes an array of Exchanges into a single ExchangeIO function */ + +export type RequestMiddleware = ( + event: FetchEvent +) => Response | Promise | void | Promise | Promise; + +// copy-pasted from h3/dist/index.d.ts +type EventHandlerResponse = T | Promise; +type ResponseMiddlewareResponseParam = { body?: Awaited }; + +export type ResponseMiddleware = ( + event: FetchEvent, + response: ResponseMiddlewareResponseParam +) => Response | Promise | void | Promise; + +function wrapRequestMiddleware(onRequest: RequestMiddleware) { + return async (h3Event: HTTPEvent) => { + const fetchEvent = getFetchEvent(h3Event); + const response = await onRequest(fetchEvent); + if (response) { + await sendWebResponse(h3Event, response); + } + }; +} + +function wrapResponseMiddleware(onBeforeResponse: ResponseMiddleware) { + return async (h3Event: HTTPEvent, response: ResponseMiddlewareResponseParam) => { + const fetchEvent = getFetchEvent(h3Event); + const mwResponse = await onBeforeResponse(fetchEvent, response); + if (mwResponse) { + await sendWebResponse(h3Event, mwResponse); + } + }; +} + +export function createMiddleware({ + onRequest, + onBeforeResponse +}: { + onRequest?: RequestMiddleware | RequestMiddleware[] | undefined; + onBeforeResponse?: ResponseMiddleware | ResponseMiddleware[] | undefined; +}) { + return defineMiddleware({ + onRequest: + typeof onRequest === "function" + ? wrapRequestMiddleware(onRequest) + : Array.isArray(onRequest) + ? onRequest.map(wrapRequestMiddleware) + : undefined, + onBeforeResponse: + typeof onBeforeResponse === "function" + ? wrapResponseMiddleware(onBeforeResponse) + : Array.isArray(onBeforeResponse) + ? onBeforeResponse.map(wrapResponseMiddleware) + : undefined + }); +} From c04afbcaf65c8ee604e459cec583b5ed74e9828c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sat, 23 Aug 2025 14:28:12 +0800 Subject: [PATCH 2/3] dev and prod middleware --- examples/with-strict-csp/src/middleware.ts | 11 +- examples/with-strict-csp/tsconfig.json | 1 + packages/start/src/config/index.ts | 40 +++--- packages/start/src/config/nitroPlugin.ts | 157 +++++++++------------ packages/start/src/env.d.ts | 1 + packages/start/src/server/index.tsx | 120 ++++++++-------- packages/start/src/virtual.d.ts | 8 ++ 7 files changed, 167 insertions(+), 171 deletions(-) diff --git a/examples/with-strict-csp/src/middleware.ts b/examples/with-strict-csp/src/middleware.ts index fa937b47e..9dcbe0a58 100644 --- a/examples/with-strict-csp/src/middleware.ts +++ b/examples/with-strict-csp/src/middleware.ts @@ -20,12 +20,11 @@ export default createMiddleware({ // For more details, see: https://vite.dev/config/build-options.html#build-assetsinlinelimit const csp = ` default-src 'self'; - script-src ${ - isProd - ? // Note: The `https:` and `'unsafe-inline'` directives do not reduce the effectiveness of the CSP. - // They are only fallbacks for older browsers that don't support `'strict-dynamic'`. - `'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval' https: 'unsafe-inline'` - : "'self' 'unsafe-inline' 'unsafe-eval' https: http:" + script-src ${isProd + ? // Note: The `https:` and `'unsafe-inline'` directives do not reduce the effectiveness of the CSP. + // They are only fallbacks for older browsers that don't support `'strict-dynamic'`. + `'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval' https: 'unsafe-inline'` + : "'self' 'unsafe-inline' 'unsafe-eval' https: http:" }; style-src ${isProd ? `'nonce-${nonce}'` : "'self' 'unsafe-inline'"}; img-src 'self' data:; diff --git a/examples/with-strict-csp/tsconfig.json b/examples/with-strict-csp/tsconfig.json index 4ea27f698..b9f69f64a 100644 --- a/examples/with-strict-csp/tsconfig.json +++ b/examples/with-strict-csp/tsconfig.json @@ -11,6 +11,7 @@ "strict": true, "noEmit": true, "isolatedModules": true, + "types": ["vite/client"], "paths": { "~/*": ["./src/*"] } diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 325db6720..92d0f569a 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -42,7 +42,7 @@ const SolidStartServerFnsPlugin = createTanStackServerFnPlugin({ fileURLToPath(new URL("../server/server-runtime.js", import.meta.url)) )}"`, replacer: opts => - `createServerReference(${() => {}}, '${opts.functionId}', '${opts.extractedFilename}')` + `createServerReference(${() => { }}, '${opts.functionId}', '${opts.extractedFilename}')` }, ssr: { getRuntimeCode: () => @@ -72,7 +72,8 @@ const VIRTUAL_MODULES = { serverManifest: "solid-start:server-manifest", getClientManifest: "solid-start:get-client-manifest", getSsrManifest: "solid-start:get-ssr-manifest", - getManifest: "solid-start:get-manifest" + getManifest: "solid-start:get-manifest", + middleware: "solid-start:middleware" } as const; export const CLIENT_BASE_PATH = "_build"; @@ -179,9 +180,9 @@ function solidStartVitePlugin(options?: SolidStartOptions): Array "~": join(process.cwd(), start.appRoot), ...(!start.ssr ? { - "@solidjs/start/server": "@solidjs/start/server/spa", - "@solidjs/start/client": "@solidjs/start/client/spa" - } + "@solidjs/start/server": "@solidjs/start/server/spa", + "@solidjs/start/client": "@solidjs/start/client/spa" + } : {}) } }, @@ -222,7 +223,7 @@ function solidStartVitePlugin(options?: SolidStartOptions): Array { name: "solid-start:manifest-plugin", enforce: "pre", - resolveId(id) { + async resolveId(id) { if (id === VIRTUAL_MODULES.serverManifest) return `\0${VIRTUAL_MODULES.serverManifest}`; if (id === VIRTUAL_MODULES.getClientManifest) return new URL("../server/manifest/client-manifest.js", import.meta.url).pathname; @@ -232,6 +233,11 @@ function solidStartVitePlugin(options?: SolidStartOptions): Array return this.environment.config.consumer === "server" ? new URL("../server/manifest/ssr-manifest.js", import.meta.url).pathname : new URL("../server/manifest/client-manifest.js", import.meta.url).pathname; + if (id === VIRTUAL_MODULES.middleware) { + if (start.middleware) return await this.resolve(start.middleware); + + return `\0${VIRTUAL_MODULES.middleware}`; + } }, async load(id) { if (id === `\0${VIRTUAL_MODULES.serverManifest}`) { @@ -264,16 +270,16 @@ function solidStartVitePlugin(options?: SolidStartOptions): Array ) .map( asset => - ({ - tag: "link", - attrs: { - href: join("/", CLIENT_BASE_PATH, asset), - key: join("/", CLIENT_BASE_PATH, asset), - ...(asset.endsWith(".css") - ? { rel: "stylesheet", fetchPriority: "high" } - : { rel: "modulepreload" }) - } - } satisfies ManifestAsset) + ({ + tag: "link", + attrs: { + href: join("/", CLIENT_BASE_PATH, asset), + key: join("/", CLIENT_BASE_PATH, asset), + ...(asset.endsWith(".css") + ? { rel: "stylesheet", fetchPriority: "high" } + : { rel: "modulepreload" }) + } + } satisfies ManifestAsset) ); acc[id] = { @@ -303,7 +309,7 @@ function solidStartVitePlugin(options?: SolidStartOptions): Array await getSsrDevManifest(true, handlers.client).getAssets(id) )}`; } - } + } else if (id === VIRTUAL_MODULES.middleware) return "export default {};" } }, nitroPlugin({ root: process.cwd() }, () => ssrBundle, start.server, start.middleware), diff --git a/packages/start/src/config/nitroPlugin.ts b/packages/start/src/config/nitroPlugin.ts index 290ea963f..b7a0a661b 100644 --- a/packages/start/src/config/nitroPlugin.ts +++ b/packages/start/src/config/nitroPlugin.ts @@ -3,7 +3,9 @@ import { _ResponseMiddleware, createApp, createEvent, + EventHandler, eventHandler, + EventHandlerObject, getHeader, H3Event, sendWebResponse @@ -52,109 +54,85 @@ export function nitroPlugin( return async () => { removeHtmlMiddlewares(viteDevServer); - const h3App = createApp(); - const serverEnv = viteDevServer.environments.server; if (!serverEnv) throw new Error("Server environment not found"); if (!isRunnableDevEnvironment(serverEnv)) throw new Error("Server environment is not runnable"); + const h3App = createApp(); + h3App.use( - eventHandler({ - onRequest: async e => { - if (!middleware) return; - const middlewareEntry = await serverEnv.runner.import(middleware); - const { - onRequest - }: { onRequest?: _RequestMiddleware[] | _RequestMiddleware } = - middlewareEntry?.default || {}; - if (Array.isArray(onRequest)) { - onRequest?.forEach(handler => handler(e)); - } else { - onRequest?.(e); - } - }, - onBeforeResponse: async (e, response) => { - if (!middleware) return; - const middlewareEntry = await serverEnv.runner.import(middleware); - const { - onBeforeResponse - }: { - onBeforeResponse?: - | _ResponseMiddleware[] - | _ResponseMiddleware; - } = middlewareEntry?.default || {}; - if (Array.isArray(onBeforeResponse)) { - onBeforeResponse?.forEach(handler => handler(e, response)); - } else { - onBeforeResponse?.(e, response); - } - }, - handler: async event => { - try { - const serverEntry: { - default: (e: H3Event) => Promise; - } = await serverEnv.runner.import("./src/entry-server.tsx"); + eventHandler(async (event) => { + const serverEntry: { + default: EventHandler; + } = await serverEnv.runner.import("./src/entry-server.tsx"); - return await serverEntry.default(event); - } catch (e) { - console.error(e); - viteDevServer.ssrFixStacktrace(e as Error); - if (getHeader(event, "content-type")?.includes("application/json")) { - return sendWebResponse( - event, - new Response( - JSON.stringify( - { - status: 500, - error: "Internal Server Error", - message: "An unexpected error occurred. Please try again later.", - timestamp: new Date().toISOString() - }, - null, - 2 - ), - { - status: 500, - headers: { - "Content-Type": "application/json" - } - } - ) - ); - } + return await serverEntry.default(event).catch((e: unknown) => { + console.error(e); + viteDevServer.ssrFixStacktrace(e as Error); + if ( + getHeader(event, "content-type")?.includes( + "application/json", + ) + ) { return sendWebResponse( event, new Response( - ` - - - - - Error - - - - - - `, + JSON.stringify( + { + status: 500, + error: "Internal Server Error", + message: + "An unexpected error occurred. Please try again later.", + timestamp: new Date().toISOString(), + }, + null, + 2, + ), { status: 500, headers: { - "Content-Type": "text/html" - } - } - ) + "Content-Type": "application/json", + }, + }, + ), ); } - } - }) + return sendWebResponse( + event, + new Response( + ` + + + + + Error + + + + + + `, + { + status: 500, + headers: { + "Content-Type": "text/html", + }, + }, + ), + ); + }) + } + ), ); viteDevServer.middlewares.use(async (req, res) => { @@ -324,9 +302,8 @@ function removeHtmlMiddlewares(server: ViteDevServer) { function prepareError(req: Connect.IncomingMessage, error: unknown) { const e = error as Error; return { - message: `An error occured while server rendering ${req.url}:\n\n\t${ - typeof e === "string" ? e : e.message - } `, + message: `An error occured while server rendering ${req.url}:\n\n\t${typeof e === "string" ? e : e.message + } `, stack: typeof e === "string" ? "" : e.stack }; } diff --git a/packages/start/src/env.d.ts b/packages/start/src/env.d.ts index d0828d27d..1457ed713 100644 --- a/packages/start/src/env.d.ts +++ b/packages/start/src/env.d.ts @@ -1,3 +1,4 @@ +/// // This file is an augmentation to the built-in ImportMeta interface // Thus cannot contain any top-level imports // diff --git a/packages/start/src/server/index.tsx b/packages/start/src/server/index.tsx index 2a49b8215..f8ce9826c 100644 --- a/packages/start/src/server/index.tsx +++ b/packages/start/src/server/index.tsx @@ -5,6 +5,7 @@ import { sharedConfig } from "solid-js"; import { renderToStream, renderToString } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; import { getSsrManifest } from "solid-start:get-ssr-manifest"; +import middleware from "solid-start:middleware"; import { createRoutes } from "../router.jsx"; import { getFetchEvent } from "./fetchEvent.js"; @@ -92,73 +93,76 @@ export function createHandler( fn: (context: PageEvent) => JSX.Element, options: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise) = {} ) { - return eventHandler(async (e: H3Event) => { - const event = getFetchEvent(e); - - return await provideRequestEvent(event, async () => { - const url = new URL(event.request.url); - const pathname = url.pathname; - - const serverFunctionTest = join("/", SERVER_FN_BASE); - if (pathname.startsWith(serverFunctionTest)) { - const serverFnResponse = await handleServerFunction(e); - - if (serverFnResponse instanceof Response) return serverFnResponse; - - return new Response(serverFnResponse as any, { - headers: getResponseHeaders(e) as any - }); - } - - const match = matchAPIRoute(pathname, event.request.method); - if (match) { - const mod = await match.handler.import(); - const fn = - event.request.method === "HEAD" ? mod["HEAD"] || mod["GET"] : mod[event.request.method]; - (event as APIEvent).params = match.params || {}; - // @ts-expect-error - sharedConfig.context = { event }; - const res = await fn!(event); - if (res !== undefined) return res; - if (event.request.method !== "GET") { - throw new Error( - `API handler for ${event.request.method} "${event.request.url}" did not return a response.` - ); + return eventHandler({ + ...middleware, + handler: async (e: H3Event) => { + const event = getFetchEvent(e); + + return await provideRequestEvent(event, async () => { + const url = new URL(event.request.url); + const pathname = url.pathname; + + const serverFunctionTest = join("/", SERVER_FN_BASE); + if (pathname.startsWith(serverFunctionTest)) { + const serverFnResponse = await handleServerFunction(e); + + if (serverFnResponse instanceof Response) return serverFnResponse; + + return new Response(serverFnResponse as any, { + headers: getResponseHeaders(e) as any + }); } - } - const context = await createPageEvent(event); + const match = matchAPIRoute(pathname, event.request.method); + if (match) { + const mod = await match.handler.import(); + const fn = + event.request.method === "HEAD" ? mod["HEAD"] || mod["GET"] : mod[event.request.method]; + (event as APIEvent).params = match.params || {}; + // @ts-expect-error + sharedConfig.context = { event }; + const res = await fn!(event); + if (res !== undefined) return res; + if (event.request.method !== "GET") { + throw new Error( + `API handler for ${event.request.method} "${event.request.url}" did not return a response.` + ); + } + } - const resolvedOptions = - typeof options === "function" ? await options(context) : { ...options }; - const mode = resolvedOptions.mode || "stream"; - if (resolvedOptions.nonce) context.nonce = resolvedOptions.nonce; + const context = await createPageEvent(event); - if (mode === "sync" || !import.meta.env.START_SSR) { - const html = renderToString(() => { - (sharedConfig.context as any).event = context; - return fn(context); - }); - context.complete = true; + const resolvedOptions = + typeof options === "function" ? await options(context) : { ...options }; + const mode = resolvedOptions.mode || "stream"; + if (resolvedOptions.nonce) context.nonce = resolvedOptions.nonce; - // insert redirect handling here + if (mode === "sync" || !import.meta.env.START_SSR) { + const html = renderToString(() => { + (sharedConfig.context as any).event = context; + return fn(context); + }); + context.complete = true; + + // insert redirect handling here - return html; - } + return html; + } - const stream = renderToStream(() => { - (sharedConfig.context as any).event = context; - return fn(context); - }, resolvedOptions); + const stream = renderToStream(() => { + (sharedConfig.context as any).event = context; + return fn(context); + }, resolvedOptions); - // insert redirect handling here + // insert redirect handling here - if (mode === "async") return stream as unknown as Promise; // stream has a hidden 'then' method + if (mode === "async") return stream as unknown as Promise; // stream has a hidden 'then' method - // fix cloudflare streaming - const { writable, readable } = new TransformStream(); - stream.pipeTo(writable); - return readable; - }); + // fix cloudflare streaming + const { writable, readable } = new TransformStream(); + stream.pipeTo(writable); + return readable; + }); + } }); } diff --git a/packages/start/src/virtual.d.ts b/packages/start/src/virtual.d.ts index 0cffcfc29..fe1befe45 100644 --- a/packages/start/src/virtual.d.ts +++ b/packages/start/src/virtual.d.ts @@ -43,3 +43,11 @@ declare module "solid-start:get-manifest" { declare module "#start/app" { export default App as import("solid-js").Component; } + +declare module "solid-start:middleware" { + type MaybeArray = T | Array; + export default Middleware as { + onRequest?: MaybeArray; + onBeforeResponse?: MaybeArray; + }; +} From 2094aa7678fdcf300888da54846bdf5316346899 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sat, 23 Aug 2025 14:29:54 +0800 Subject: [PATCH 3/3] cleaup --- packages/start/src/env.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/src/env.d.ts b/packages/start/src/env.d.ts index 1457ed713..d0828d27d 100644 --- a/packages/start/src/env.d.ts +++ b/packages/start/src/env.d.ts @@ -1,4 +1,3 @@ -/// // This file is an augmentation to the built-in ImportMeta interface // Thus cannot contain any top-level imports //