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/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 d63c0f2dd..92d0f569a 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) => @@ -79,45 +73,43 @@ const VIRTUAL_MODULES = { getClientManifest: "solid-start:get-client-manifest", getSsrManifest: "solid-start:get-ssr-manifest", getManifest: "solid-start:get-manifest", + middleware: "solid-start:middleware" } 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}` }; return [ @@ -127,8 +119,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) { @@ -143,18 +135,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", @@ -168,7 +156,7 @@ function solidStartVitePlugin( rollupOptions: { output: { dir: path.resolve(process.cwd(), serverDistDir), - entryFileNames: ssrEntryFile, + entryFileNames: ssrEntryFile }, plugins: [ { @@ -176,55 +164,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/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 @@ -234,22 +218,26 @@ 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}`; + 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 + 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; + 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}`) { @@ -257,64 +245,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)};`; @@ -328,25 +306,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) )}`; } - } - }, + } else if (id === VIRTUAL_MODULES.middleware) return "export default {};" + } }, - 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}`) + }) ]; } @@ -375,8 +353,8 @@ function css(): PluginOption { event: "css-update", data: { file, - contents: resp.code, - }, + contents: resp.code + } }); } }, @@ -384,6 +362,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..b7a0a661b 100644 --- a/packages/start/src/config/nitroPlugin.ts +++ b/packages/start/src/config/nitroPlugin.ts @@ -1,6 +1,26 @@ +import { + _RequestMiddleware, + _ResponseMiddleware, + createApp, + createEvent, + EventHandler, + eventHandler, + EventHandlerObject, + 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 +29,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 [ @@ -32,84 +54,86 @@ 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"); - h3App.use(eventHandler(async (event) => { - try { + const h3App = createApp(); + + h3App.use( + eventHandler(async (event) => { const serverEntry: { - default: (e: H3Event) => Promise; - } = await serverEnv.runner.import( - "./src/entry-server.tsx", - ); + 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( + 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( + 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, - error: "Internal Server Error", - message: - "An unexpected error occurred. Please try again later.", - timestamp: new Date().toISOString(), + headers: { + "Content-Type": "application/json", + }, }, - null, - 2, ), + ); + } + return sendWebResponse( + event, + new Response( + ` + + + + + Error + + + + + + `, { status: 500, headers: { - "Content-Type": "application/json", + "Content-Type": "text/html", }, }, ), ); - } - return sendWebResponse( - event, - new Response( - ` - - - - - Error - - - - - - `, - { - status: 500, - headers: { - "Content-Type": "text/html", - }, - }, - ), - ); + }) } - })) + ), + ); viteDevServer.middlewares.use(async (req, res) => { const event = createEvent(req, res); @@ -166,7 +190,7 @@ export function nitroPlugin( renderer: ssrEntryFile, rollupConfig: { plugins: [virtualBundlePlugin(getSsrBundle()) as any] - }, + } }; const nitro = await createNitro(resolvedNitroConfig); 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 + }); +} 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; + }; +}