@solidjs/vite-plugin 3.0.0-next.28 → 3.0.0-next.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020-2026 Alexandre Mouton-Brady and Solid contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -222,6 +222,41 @@ The Fetchable wrapper deliberately accepts only the request. Hosts may pass
222
222
  environment or execution-context arguments after it; those are not the
223
223
  Solid options accepted by `handleRequest`'s second parameter.
224
224
 
225
+ Among those options, **`event`** is the supported public seam for extending
226
+ the request event: its fields spread into the event at creation, so a custom
227
+ server entry (or a host wrapper) can attach whatever its platform knows and
228
+ read it back anywhere in the request scope with `getRequestEvent()`. The
229
+ conventional field name is `nativeEvent` — the platform's raw request
230
+ object. A Node entry passes the `IncomingMessage`:
231
+
232
+ ```js
233
+ import { createServer } from 'node:http';
234
+ import { handleRequest } from './dist/server/server.js';
235
+
236
+ createServer(async (req, res) => {
237
+ const response = await handleRequest(webRequest(req), {
238
+ event: { nativeEvent: req },
239
+ });
240
+ // ... write response to res
241
+ });
242
+ ```
243
+
244
+ ```js
245
+ // anywhere inside the request scope (middleware, setup, app code)
246
+ import { getRequestEvent } from '@solidjs/web';
247
+ const event = getRequestEvent();
248
+ event.nativeEvent; // the Node IncomingMessage the entry passed
249
+ ```
250
+
251
+ The plugin's own dev and preview middlewares (and the server-function dev
252
+ middleware) pass `event: { nativeEvent: req }` with the Node request, so
253
+ `getRequestEvent().nativeEvent` answers the same under `vite dev` and
254
+ `vite preview` as behind a Node entry written like the above. For the
255
+ client's IP on bare Node, read `event.nativeEvent.socket.remoteAddress`;
256
+ behind a proxy or load balancer that address is the proxy's, so read the
257
+ forwarding headers off `event.request` instead (`x-forwarded-for` and
258
+ friends) — only when you trust the proxy that set them.
259
+
225
260
  - **Preview**: `vite build && vite preview` runs the production artifact
226
261
  with no server file — Vite's preview statics serve `dist/client`, and
227
262
  everything else (pages, the server-function endpoint, middleware)
@@ -361,7 +396,14 @@ import { env } from 'virtual:env/client'; // the VITE_-prefixed client vars
361
396
  is fine). Platform-injected vars that don't exist at build time work,
362
397
  secrets rotate without a rebuild, and no secret value exists in any
363
398
  dist artifact; an invalid server environment fails boot with the same
364
- per-key report.
399
+ per-key report. Boot validation is synchronous: the generated server
400
+ env module contains no top-level await, so the server bundle works
401
+ under any downstream build target (Nitro's node-server preset,
402
+ es2020 — no `esnext` override needed). The flip side: `server`
403
+ validators must be synchronous — an async refinement/transform on a
404
+ server key is rejected at config time with the fix in the message
405
+ (`client` keys may stay async; they are awaited at build time where
406
+ the values are baked).
365
407
  - **Leaks are errors.** Importing `virtual:env/server` from a client
366
408
  module graph is a hard error naming the importer (the app root and
367
409
  everything it imports hydrate — they are client code; keep server env
@@ -434,6 +476,19 @@ Two explicit switches remain for custom host setups:
434
476
  serving, hands only server-function dispatch in dev to the host. For
435
477
  setups without `start`, or when only the endpoint should move.
436
478
 
479
+ **`virtual:solid-manifest`** exposes the client asset manifest that serving
480
+ works from — a server-side module, available in dev and in SSR builds. In
481
+ an SSR build its default export is the parsed client manifest
482
+ (`dist/client/.vite/manifest.json`), keyed by source path with the resolved
483
+ Vite `base` attached as `_base`; in dev it exports the live asset resolver
484
+ the plugin uses for dev CSS collection instead of a static object. This is
485
+ the seam for frameworks and routers that do their own asset gating —
486
+ deciding which scripts and styles a response carries, as the TanStack Start
487
+ integration does — without re-reading the manifest from disk or re-deriving
488
+ `base`. Ambient types ship with the plugin
489
+ (`/// <reference types="@solidjs/vite-plugin/virtual-solid-manifest" />`);
490
+ see that `.d.ts` and the exported `ViteManifest` type for the full shape.
491
+
437
492
  Without `ssr: true` — **client mode** (experimental), the same conventions
438
493
  with client-only rendering:
439
494
 
@@ -45,24 +45,54 @@ var babel__namespace = /*#__PURE__*/_interopNamespaceDefault(babel);
45
45
  * different URL than the one node saw — the dev middlewares use it to
46
46
  * restore the configured Vite `base` that the dev/preview base middleware
47
47
  * stripped, so the handler always sees production-shaped URLs.
48
+ *
49
+ * Handles plain HTTP/1 *and* the HTTP/2 compat API: Vite's dev server uses
50
+ * `http2.createSecureServer({ allowHTTP1: true })` whenever `server.https`
51
+ * is set without a proxy, so under https the middlewares receive
52
+ * `Http2ServerRequest`s. The h2/protocol/abort techniques here are
53
+ * reimplemented from srvx's Node adapter (github.com/h3js/srvx,
54
+ * src/adapters/_node) — reference, not copied code.
48
55
  */
49
- function webRequestFromNode(req, urlPath) {
50
- const url = new URL(urlPath ?? req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
56
+ function webRequestFromNode(req, urlPath, res) {
57
+ // TLS sockets (https and h2) expose `encrypted`; a Request whose url says
58
+ // http: on a TLS connection breaks secure-cookie logic, absolute
59
+ // redirects, and origin checks in application code.
60
+ const protocol = req.socket?.encrypted ? 'https' : 'http';
61
+ // HTTP/2 has no Host header — the authority travels in the `:authority`
62
+ // pseudo-header instead.
63
+ const host = req.headers.host ?? req.headers[':authority'] ?? 'localhost';
64
+ const url = new URL(urlPath ?? req.url ?? '/', `${protocol}://${host}`);
51
65
  const headers = new Headers();
52
66
  for (const [key, value] of Object.entries(req.headers)) {
53
67
  if (value === undefined) continue;
68
+ // HTTP/2 pseudo-headers (:method, :path, :authority, :scheme) are not
69
+ // legal field names — Headers#append throws a TypeError on them.
70
+ if (key[0] === ':') continue;
54
71
  if (Array.isArray(value)) {
55
72
  for (const item of value) headers.append(key, item);
56
73
  } else {
57
74
  headers.append(key, value);
58
75
  }
59
76
  }
77
+ // Surface client disconnects as the request's AbortSignal so handlers can
78
+ // cancel work (streamed SSR renders, in-flight fetches). The response's
79
+ // 'close' fires on normal completion too; `writableEnded` distinguishes a
80
+ // finished response from a client that went away.
81
+ let signal;
82
+ if (res) {
83
+ const controller = new AbortController();
84
+ res.once('close', () => {
85
+ if (!res.writableEnded) controller.abort();
86
+ });
87
+ signal = controller.signal;
88
+ }
60
89
  const method = req.method || 'GET';
61
90
  const body = method === 'GET' || method === 'HEAD' ? undefined : node_stream.Readable.toWeb(req);
62
91
  return new Request(url, {
63
92
  method,
64
93
  headers,
65
94
  body,
95
+ signal,
66
96
  // undici requires half-duplex for streamed request bodies.
67
97
  ...(body ? {
68
98
  duplex: 'half'
@@ -77,7 +107,11 @@ async function sendWebResponse(res, response) {
77
107
  if (key !== 'set-cookie') res.setHeader(key, value);
78
108
  });
79
109
  if (cookies && cookies.length) res.setHeader('set-cookie', cookies);
80
- if (!response.body) {
110
+ // HEAD gets the head only — and the body must be *cancelled*, not pumped:
111
+ // node discards HEAD body writes, so streaming a long (or endless) body
112
+ // into the void just burns the render. (Technique from srvx.)
113
+ if (!response.body || res.req?.method === 'HEAD') {
114
+ response.body?.cancel().catch(() => {});
81
115
  res.end();
82
116
  return;
83
117
  }
@@ -934,7 +968,15 @@ function serverFunctions(options = {}, internal = {}) {
934
968
  // the same value (config merges per key), but the handler graph cannot
935
969
  // depend on that: in dev, a mutation from an already-open page can be
936
970
  // the first request after a server restart.
937
- `configureServerFunctionsServer({ provideEvent: provideRequestEvent, endpoint: ${JSON.stringify(resolvedEndpoint)}${components ? ', transformResult: frameTransformResult, transformFlightResult: frameTransformFlightResult, transformDirectResult: frameTransformDirectResult' : ''} });`, `export const endpoint = ${JSON.stringify(resolvedEndpoint)};`, `export function handleServerFunctionRequest(request, options) {`, ` return handle(request, { provideEvent: provideRequestEvent, ...options });`, `}`].join('\n');
971
+ `configureServerFunctionsServer({ provideEvent: provideRequestEvent, endpoint: ${JSON.stringify(resolvedEndpoint)}${components ? ', transformResult: frameTransformResult, transformFlightResult: frameTransformFlightResult, transformDirectResult: frameTransformDirectResult' : ''} });`, `export const endpoint = ${JSON.stringify(resolvedEndpoint)};`,
972
+ // `options.event` is the same wrapper->event extension seam the SSR
973
+ // handler's handleRequest carries (conventionally `nativeEvent`, the
974
+ // platform's raw request object). The runtime's standalone handler
975
+ // creates its own event (`{ request, locals }`) with no init
976
+ // parameter, so the extension threads through its existing
977
+ // `createEvent` option instead — spread before `...options` so an
978
+ // explicit host-provided createEvent still wins.
979
+ `export function handleServerFunctionRequest(request, options) {`, ` const { event: eventInit, ...rest } = options || {};`, ` return handle(request, {`, ` provideEvent: provideRequestEvent,`, ` ...(eventInit ? { createEvent: (req) => ({ request: req, locals: {}, ...eventInit }) } : {}),`, ` ...rest,`, ` });`, `}`].join('\n');
938
980
  }
939
981
 
940
982
  // Function IDs are `xxHash32(root-relative path)-<count>` (see compile.ts),
@@ -1018,7 +1060,15 @@ function serverFunctions(options = {}, internal = {}) {
1018
1060
  // middleware chain and one stub-backed request event front the
1019
1061
  // endpoint exactly as they front page SSR.
1020
1062
  const handler = await server.ssrLoadModule(internal.ssrHandler ?? HANDLER_ID$1);
1021
- const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl)) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl));
1063
+ // Both dispatch shapes carry the raw Node request on the event
1064
+ // (the `options.event` seam), matching the SSR dev middleware
1065
+ // and what a production Node entry passes.
1066
+ const dispatchOptions = {
1067
+ event: {
1068
+ nativeEvent: req
1069
+ }
1070
+ };
1071
+ const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions);
1022
1072
  await sendWebResponse(res, response);
1023
1073
  })().catch(error => {
1024
1074
  if (error instanceof Error) server.ssrFixStacktrace(error);
@@ -1590,7 +1640,14 @@ function startServe(options, internal = {}) {
1590
1640
  // The runtime's response-head lifecycle: commit at shell flush,
1591
1641
  // pre-flush Location as a real redirect, post-flush Location as the
1592
1642
  // script fallback; the transform injects the doctype/head pieces.
1593
- ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`, ` const event = createRequestEvent(request);`,
1643
+ ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1644
+ // `options.event` is the public wrapper->event extension seam: extra
1645
+ // fields (conventionally `nativeEvent`, the platform's raw request
1646
+ // object) spread over the event's defaults at creation, so hosts and
1647
+ // custom server entries can extend what getRequestEvent() answers
1648
+ // with — no new convention beyond createRequestEvent's own init
1649
+ // parameter (spreading undefined is a no-op).
1650
+ ` const event = createRequestEvent(request, options.event);`,
1594
1651
  // Middleware runs inside the request scope, after event creation —
1595
1652
  // getRequestEvent() answers in middleware exactly as in app code, and
1596
1653
  // nothing reaches the wire until the outermost middleware returns.
@@ -1805,7 +1862,14 @@ function startServe(options, internal = {}) {
1805
1862
  // server-function endpoint) and hands the URL to application
1806
1863
  // code, so restore the base — the deployed production handler
1807
1864
  // receives base-prefixed URLs and preview must match it.
1808
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')));
1865
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res),
1866
+ // Same event extension the dev middleware and a production
1867
+ // Node entry pass: the raw Node request as `nativeEvent`.
1868
+ {
1869
+ event: {
1870
+ nativeEvent: req
1871
+ }
1872
+ });
1809
1873
  // Preview's compression middleware buffers whole responses;
1810
1874
  // opting HTML out keeps SSR streaming observable, matching
1811
1875
  // production behavior.
@@ -1855,9 +1919,16 @@ function startServe(options, internal = {}) {
1855
1919
  // the configured `base` from req.url; restore it so the app
1856
1920
  // sees the same URLs in dev as in production (where the
1857
1921
  // deployed handler receives base-prefixed requests).
1858
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')), {
1922
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res), {
1859
1923
  devHead,
1860
- pageRequest
1924
+ pageRequest,
1925
+ // The raw Node request on the event, matching what a
1926
+ // production Node server entry passes through the
1927
+ // `options.event` seam — getRequestEvent().nativeEvent
1928
+ // answers the same in dev as deployed.
1929
+ event: {
1930
+ nativeEvent: req
1931
+ }
1861
1932
  });
1862
1933
  // A non-page request the chain never handled: the terminal
1863
1934
  // dispatch answered with the marked 404 — hand it back to
@@ -1938,6 +2009,11 @@ function startServe(options, internal = {}) {
1938
2009
  // time work, secrets rotate without a rebuild, and no secret value exists
1939
2010
  // in any dist artifact. Build-time server-value failures downgrade to a
1940
2011
  // warning (boot enforces); dev failures stay hard errors — dev IS runtime.
2012
+ // Boot validation is synchronous by design: the generated server module
2013
+ // contains no top-level await (a TLA chunk forces esnext on downstream
2014
+ // bundlers — Nitro's node-server preset rejects it), which is why async
2015
+ // validators are rejected for `server` keys (client keys may stay async;
2016
+ // they are awaited at build time where the values are baked).
1941
2017
  //
1942
2018
  // A failed validation fails the build; in dev it renders Vite's error
1943
2019
  // overlay with the per-key report (the virtual modules throw it on load)
@@ -2208,7 +2284,23 @@ function startEnv(option) {
2208
2284
  for (const side of ['server', 'client']) {
2209
2285
  for (const [key, validator] of Object.entries(schema[side] ?? {})) {
2210
2286
  let result = validator['~standard'].validate(raw[key]);
2211
- if (result instanceof Promise) result = await result;
2287
+ if (result instanceof Promise) {
2288
+ // Async validation is fine for `client` keys — their values are
2289
+ // baked right here at build time, where awaiting costs nothing.
2290
+ // `server` keys validate process.env at boot through generated
2291
+ // code that is deliberately synchronous (a top-level await in the
2292
+ // server env chunk forces esnext on every downstream bundle
2293
+ // target — Nitro's node-server preset rejects it outright), so a
2294
+ // Promise-returning server validator could only ever fail at
2295
+ // deploy boot. Async-ness is a property of the schema, not the
2296
+ // value, so fail fast here with the fix in the message. Boot
2297
+ // still backstops (schemas whose sync prefix short-circuits at
2298
+ // build time can go async on real values).
2299
+ if (side === 'server') {
2300
+ throw new Error(`[@solidjs/vite-plugin] server env var "${key}" in ${envFile} uses an async ` + `validator (validate() returned a Promise). Server env is validated ` + `synchronously at boot — the generated module contains no top-level ` + `await, so server bundles work on non-esnext targets — which async ` + `validators cannot do. Make the validator synchronous (drop async ` + `refinements/transforms), or run the async check in application code.`);
2301
+ }
2302
+ result = await result;
2303
+ }
2212
2304
  if (result.issues && result.issues.length) {
2213
2305
  for (const issue of result.issues) {
2214
2306
  const at = (issue.path ?? []).map(segment => typeof segment === 'object' && segment !== null && 'key' in segment ? String(segment.key) : String(segment)).join('.');
@@ -2285,6 +2377,16 @@ function startEnv(option) {
2285
2377
  // means. Platform-injected vars that don't exist at build time work,
2286
2378
  // secrets rotate without a rebuild, and no secret value exists in any
2287
2379
  // dist artifact.
2380
+ //
2381
+ // The generated code is deliberately free of top-level await. Module
2382
+ // init is the only point where "validated and frozen before any
2383
+ // importer's body runs" can be guaranteed — user server modules read
2384
+ // `env.KEY` at their own top level — and the only async thing here is
2385
+ // Standard Schema's option to return a Promise from validate(). A TLA
2386
+ // chunk breaks every downstream bundler with a non-esnext target
2387
+ // (Nitro's node-server preset in practice), so validation runs
2388
+ // synchronously and a Promise-returning server validator is itself a
2389
+ // boot issue (build/config time rejects it earlier when detectable).
2288
2390
  function serverEnvModuleCode(loaded) {
2289
2391
  const serverKeys = Object.keys(loaded.schema.server ?? {});
2290
2392
  const baked = `const __env = ${JSON.stringify(loaded.client)};`;
@@ -2295,7 +2397,7 @@ function startEnv(option) {
2295
2397
  };
2296
2398
  }
2297
2399
  return {
2298
- code: [`// Generated by @solidjs/vite-plugin (start.env) — server env.`, `// Server values are read from process.env and validated at boot;`, `// client (public) values are baked at build time.`, `import __schema from ${JSON.stringify(envFileAbs)};`, baked, `const __issues = [];`, `for (const __key of ${JSON.stringify(serverKeys)}) {`, ` let __result = __schema.server[__key]['~standard'].validate(process.env[__key]);`, ` if (__result instanceof Promise) __result = await __result;`, ` if (__result.issues && __result.issues.length) {`, ` for (const __issue of __result.issues) __issues.push(' \\u2717 ' + __key + ': ' + __issue.message);`, ` } else {`, ` __env[__key] = __result.value;`, ` }`, `}`, `if (__issues.length) {`, ` throw new Error(`, ` '[@solidjs/vite-plugin] server env validation failed at boot (' + __issues.length +`, ` ' issue' + (__issues.length === 1 ? '' : 's') + ') \\u2014 schema: ' + ${JSON.stringify(envFile)} +`, ` '\\n\\n' + __issues.join('\\n') +`, ` '\\n\\nServer env is read from process.env at boot, not baked at build time: set the ' +`, ` 'variables in the server process environment.'`, ` );`, `}`, `export const env = Object.freeze(__env);`, `export default env;`].join('\n'),
2400
+ code: [`// Generated by @solidjs/vite-plugin (start.env) — server env.`, `// Server values are read from process.env and validated at boot;`, `// client (public) values are baked at build time. Boot validation is`, `// synchronous on purpose: a top-level await here would force esnext`, `// on every downstream bundle target (Nitro's node-server preset and`, `// anything else below esnext rejects a TLA chunk outright).`, `import __schema from ${JSON.stringify(envFileAbs)};`, baked, `const __issues = [];`, `for (const __key of ${JSON.stringify(serverKeys)}) {`, ` const __result = __schema.server[__key]['~standard'].validate(process.env[__key]);`, ` if (__result && typeof __result.then === 'function') {`, ` __issues.push(' \\u2717 ' + __key + ': validator returned a Promise \\u2014 async validators are not supported for server keys (boot validation is synchronous so the server bundle carries no top-level await); make this validator synchronous');`, ` } else if (__result.issues && __result.issues.length) {`, ` for (const __issue of __result.issues) __issues.push(' \\u2717 ' + __key + ': ' + __issue.message);`, ` } else {`, ` __env[__key] = __result.value;`, ` }`, `}`, `if (__issues.length) {`, ` throw new Error(`, ` '[@solidjs/vite-plugin] server env validation failed at boot (' + __issues.length +`, ` ' issue' + (__issues.length === 1 ? '' : 's') + ') \\u2014 schema: ' + ${JSON.stringify(envFile)} +`, ` '\\n\\n' + __issues.join('\\n') +`, ` '\\n\\nServer env is read from process.env at boot, not baked at build time: set the ' +`, ` 'variables in the server process environment.'`, ` );`, `}`, `export const env = Object.freeze(__env);`, `export default env;`].join('\n'),
2299
2401
  moduleType: 'js'
2300
2402
  };
2301
2403
  }
@@ -2971,12 +3073,19 @@ function solidPlugin(options = {}) {
2971
3073
  exclude: solidPkgsConfig.optimizeDeps.exclude,
2972
3074
  // Vite 8+ uses Rolldown for dependency scanning. Rolldown defaults to
2973
3075
  // React's automatic JSX runtime for .tsx files, injecting a
2974
- // react/jsx-dev-runtime import. Tell it to preserve JSX as-is since
2975
- // this plugin handles JSX transformation via babel-preset-solid.
3076
+ // react/jsx-dev-runtime import that fails to resolve and aborts the
3077
+ // scan. 'preserve' is no fix: the scanner re-parses the transformed
3078
+ // output as plain JS, so any preserved JSX is a hard parse error
3079
+ // (issue #262). The classic runtime is the only scan-safe lowering:
3080
+ // it emits bare `React.createElement` calls without injecting any
3081
+ // import, and the scan output is never executed — it only exists so
3082
+ // rolldown can walk the import graph.
2976
3083
  ...(isVite8 ? {
2977
3084
  rolldownOptions: {
2978
3085
  transform: {
2979
- jsx: 'preserve'
3086
+ jsx: {
3087
+ runtime: 'classic'
3088
+ }
2980
3089
  }
2981
3090
  }
2982
3091
  } : {})