@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.
@@ -21,24 +21,54 @@ import { crawlFrameworkPkgs } from 'vitefu';
21
21
  * different URL than the one node saw — the dev middlewares use it to
22
22
  * restore the configured Vite `base` that the dev/preview base middleware
23
23
  * stripped, so the handler always sees production-shaped URLs.
24
+ *
25
+ * Handles plain HTTP/1 *and* the HTTP/2 compat API: Vite's dev server uses
26
+ * `http2.createSecureServer({ allowHTTP1: true })` whenever `server.https`
27
+ * is set without a proxy, so under https the middlewares receive
28
+ * `Http2ServerRequest`s. The h2/protocol/abort techniques here are
29
+ * reimplemented from srvx's Node adapter (github.com/h3js/srvx,
30
+ * src/adapters/_node) — reference, not copied code.
24
31
  */
25
- function webRequestFromNode(req, urlPath) {
26
- const url = new URL(urlPath ?? req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
32
+ function webRequestFromNode(req, urlPath, res) {
33
+ // TLS sockets (https and h2) expose `encrypted`; a Request whose url says
34
+ // http: on a TLS connection breaks secure-cookie logic, absolute
35
+ // redirects, and origin checks in application code.
36
+ const protocol = req.socket?.encrypted ? 'https' : 'http';
37
+ // HTTP/2 has no Host header — the authority travels in the `:authority`
38
+ // pseudo-header instead.
39
+ const host = req.headers.host ?? req.headers[':authority'] ?? 'localhost';
40
+ const url = new URL(urlPath ?? req.url ?? '/', `${protocol}://${host}`);
27
41
  const headers = new Headers();
28
42
  for (const [key, value] of Object.entries(req.headers)) {
29
43
  if (value === undefined) continue;
44
+ // HTTP/2 pseudo-headers (:method, :path, :authority, :scheme) are not
45
+ // legal field names — Headers#append throws a TypeError on them.
46
+ if (key[0] === ':') continue;
30
47
  if (Array.isArray(value)) {
31
48
  for (const item of value) headers.append(key, item);
32
49
  } else {
33
50
  headers.append(key, value);
34
51
  }
35
52
  }
53
+ // Surface client disconnects as the request's AbortSignal so handlers can
54
+ // cancel work (streamed SSR renders, in-flight fetches). The response's
55
+ // 'close' fires on normal completion too; `writableEnded` distinguishes a
56
+ // finished response from a client that went away.
57
+ let signal;
58
+ if (res) {
59
+ const controller = new AbortController();
60
+ res.once('close', () => {
61
+ if (!res.writableEnded) controller.abort();
62
+ });
63
+ signal = controller.signal;
64
+ }
36
65
  const method = req.method || 'GET';
37
66
  const body = method === 'GET' || method === 'HEAD' ? undefined : Readable.toWeb(req);
38
67
  return new Request(url, {
39
68
  method,
40
69
  headers,
41
70
  body,
71
+ signal,
42
72
  // undici requires half-duplex for streamed request bodies.
43
73
  ...(body ? {
44
74
  duplex: 'half'
@@ -53,7 +83,11 @@ async function sendWebResponse(res, response) {
53
83
  if (key !== 'set-cookie') res.setHeader(key, value);
54
84
  });
55
85
  if (cookies && cookies.length) res.setHeader('set-cookie', cookies);
56
- if (!response.body) {
86
+ // HEAD gets the head only — and the body must be *cancelled*, not pumped:
87
+ // node discards HEAD body writes, so streaming a long (or endless) body
88
+ // into the void just burns the render. (Technique from srvx.)
89
+ if (!response.body || res.req?.method === 'HEAD') {
90
+ response.body?.cancel().catch(() => {});
57
91
  res.end();
58
92
  return;
59
93
  }
@@ -910,7 +944,15 @@ function serverFunctions(options = {}, internal = {}) {
910
944
  // the same value (config merges per key), but the handler graph cannot
911
945
  // depend on that: in dev, a mutation from an already-open page can be
912
946
  // the first request after a server restart.
913
- `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');
947
+ `configureServerFunctionsServer({ provideEvent: provideRequestEvent, endpoint: ${JSON.stringify(resolvedEndpoint)}${components ? ', transformResult: frameTransformResult, transformFlightResult: frameTransformFlightResult, transformDirectResult: frameTransformDirectResult' : ''} });`, `export const endpoint = ${JSON.stringify(resolvedEndpoint)};`,
948
+ // `options.event` is the same wrapper->event extension seam the SSR
949
+ // handler's handleRequest carries (conventionally `nativeEvent`, the
950
+ // platform's raw request object). The runtime's standalone handler
951
+ // creates its own event (`{ request, locals }`) with no init
952
+ // parameter, so the extension threads through its existing
953
+ // `createEvent` option instead — spread before `...options` so an
954
+ // explicit host-provided createEvent still wins.
955
+ `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');
914
956
  }
915
957
 
916
958
  // Function IDs are `xxHash32(root-relative path)-<count>` (see compile.ts),
@@ -994,7 +1036,15 @@ function serverFunctions(options = {}, internal = {}) {
994
1036
  // middleware chain and one stub-backed request event front the
995
1037
  // endpoint exactly as they front page SSR.
996
1038
  const handler = await server.ssrLoadModule(internal.ssrHandler ?? HANDLER_ID$1);
997
- const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl)) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl));
1039
+ // Both dispatch shapes carry the raw Node request on the event
1040
+ // (the `options.event` seam), matching the SSR dev middleware
1041
+ // and what a production Node entry passes.
1042
+ const dispatchOptions = {
1043
+ event: {
1044
+ nativeEvent: req
1045
+ }
1046
+ };
1047
+ const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions);
998
1048
  await sendWebResponse(res, response);
999
1049
  })().catch(error => {
1000
1050
  if (error instanceof Error) server.ssrFixStacktrace(error);
@@ -1566,7 +1616,14 @@ function startServe(options, internal = {}) {
1566
1616
  // The runtime's response-head lifecycle: commit at shell flush,
1567
1617
  // pre-flush Location as a real redirect, post-flush Location as the
1568
1618
  // script fallback; the transform injects the doctype/head pieces.
1569
- ` 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);`,
1619
+ ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1620
+ // `options.event` is the public wrapper->event extension seam: extra
1621
+ // fields (conventionally `nativeEvent`, the platform's raw request
1622
+ // object) spread over the event's defaults at creation, so hosts and
1623
+ // custom server entries can extend what getRequestEvent() answers
1624
+ // with — no new convention beyond createRequestEvent's own init
1625
+ // parameter (spreading undefined is a no-op).
1626
+ ` const event = createRequestEvent(request, options.event);`,
1570
1627
  // Middleware runs inside the request scope, after event creation —
1571
1628
  // getRequestEvent() answers in middleware exactly as in app code, and
1572
1629
  // nothing reaches the wire until the outermost middleware returns.
@@ -1781,7 +1838,14 @@ function startServe(options, internal = {}) {
1781
1838
  // server-function endpoint) and hands the URL to application
1782
1839
  // code, so restore the base — the deployed production handler
1783
1840
  // receives base-prefixed URLs and preview must match it.
1784
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')));
1841
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res),
1842
+ // Same event extension the dev middleware and a production
1843
+ // Node entry pass: the raw Node request as `nativeEvent`.
1844
+ {
1845
+ event: {
1846
+ nativeEvent: req
1847
+ }
1848
+ });
1785
1849
  // Preview's compression middleware buffers whole responses;
1786
1850
  // opting HTML out keeps SSR streaming observable, matching
1787
1851
  // production behavior.
@@ -1831,9 +1895,16 @@ function startServe(options, internal = {}) {
1831
1895
  // the configured `base` from req.url; restore it so the app
1832
1896
  // sees the same URLs in dev as in production (where the
1833
1897
  // deployed handler receives base-prefixed requests).
1834
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')), {
1898
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res), {
1835
1899
  devHead,
1836
- pageRequest
1900
+ pageRequest,
1901
+ // The raw Node request on the event, matching what a
1902
+ // production Node server entry passes through the
1903
+ // `options.event` seam — getRequestEvent().nativeEvent
1904
+ // answers the same in dev as deployed.
1905
+ event: {
1906
+ nativeEvent: req
1907
+ }
1837
1908
  });
1838
1909
  // A non-page request the chain never handled: the terminal
1839
1910
  // dispatch answered with the marked 404 — hand it back to
@@ -1914,6 +1985,11 @@ function startServe(options, internal = {}) {
1914
1985
  // time work, secrets rotate without a rebuild, and no secret value exists
1915
1986
  // in any dist artifact. Build-time server-value failures downgrade to a
1916
1987
  // warning (boot enforces); dev failures stay hard errors — dev IS runtime.
1988
+ // Boot validation is synchronous by design: the generated server module
1989
+ // contains no top-level await (a TLA chunk forces esnext on downstream
1990
+ // bundlers — Nitro's node-server preset rejects it), which is why async
1991
+ // validators are rejected for `server` keys (client keys may stay async;
1992
+ // they are awaited at build time where the values are baked).
1917
1993
  //
1918
1994
  // A failed validation fails the build; in dev it renders Vite's error
1919
1995
  // overlay with the per-key report (the virtual modules throw it on load)
@@ -2184,7 +2260,23 @@ function startEnv(option) {
2184
2260
  for (const side of ['server', 'client']) {
2185
2261
  for (const [key, validator] of Object.entries(schema[side] ?? {})) {
2186
2262
  let result = validator['~standard'].validate(raw[key]);
2187
- if (result instanceof Promise) result = await result;
2263
+ if (result instanceof Promise) {
2264
+ // Async validation is fine for `client` keys — their values are
2265
+ // baked right here at build time, where awaiting costs nothing.
2266
+ // `server` keys validate process.env at boot through generated
2267
+ // code that is deliberately synchronous (a top-level await in the
2268
+ // server env chunk forces esnext on every downstream bundle
2269
+ // target — Nitro's node-server preset rejects it outright), so a
2270
+ // Promise-returning server validator could only ever fail at
2271
+ // deploy boot. Async-ness is a property of the schema, not the
2272
+ // value, so fail fast here with the fix in the message. Boot
2273
+ // still backstops (schemas whose sync prefix short-circuits at
2274
+ // build time can go async on real values).
2275
+ if (side === 'server') {
2276
+ 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.`);
2277
+ }
2278
+ result = await result;
2279
+ }
2188
2280
  if (result.issues && result.issues.length) {
2189
2281
  for (const issue of result.issues) {
2190
2282
  const at = (issue.path ?? []).map(segment => typeof segment === 'object' && segment !== null && 'key' in segment ? String(segment.key) : String(segment)).join('.');
@@ -2261,6 +2353,16 @@ function startEnv(option) {
2261
2353
  // means. Platform-injected vars that don't exist at build time work,
2262
2354
  // secrets rotate without a rebuild, and no secret value exists in any
2263
2355
  // dist artifact.
2356
+ //
2357
+ // The generated code is deliberately free of top-level await. Module
2358
+ // init is the only point where "validated and frozen before any
2359
+ // importer's body runs" can be guaranteed — user server modules read
2360
+ // `env.KEY` at their own top level — and the only async thing here is
2361
+ // Standard Schema's option to return a Promise from validate(). A TLA
2362
+ // chunk breaks every downstream bundler with a non-esnext target
2363
+ // (Nitro's node-server preset in practice), so validation runs
2364
+ // synchronously and a Promise-returning server validator is itself a
2365
+ // boot issue (build/config time rejects it earlier when detectable).
2264
2366
  function serverEnvModuleCode(loaded) {
2265
2367
  const serverKeys = Object.keys(loaded.schema.server ?? {});
2266
2368
  const baked = `const __env = ${JSON.stringify(loaded.client)};`;
@@ -2271,7 +2373,7 @@ function startEnv(option) {
2271
2373
  };
2272
2374
  }
2273
2375
  return {
2274
- 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'),
2376
+ 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'),
2275
2377
  moduleType: 'js'
2276
2378
  };
2277
2379
  }
@@ -2947,12 +3049,19 @@ function solidPlugin(options = {}) {
2947
3049
  exclude: solidPkgsConfig.optimizeDeps.exclude,
2948
3050
  // Vite 8+ uses Rolldown for dependency scanning. Rolldown defaults to
2949
3051
  // React's automatic JSX runtime for .tsx files, injecting a
2950
- // react/jsx-dev-runtime import. Tell it to preserve JSX as-is since
2951
- // this plugin handles JSX transformation via babel-preset-solid.
3052
+ // react/jsx-dev-runtime import that fails to resolve and aborts the
3053
+ // scan. 'preserve' is no fix: the scanner re-parses the transformed
3054
+ // output as plain JS, so any preserved JSX is a hard parse error
3055
+ // (issue #262). The classic runtime is the only scan-safe lowering:
3056
+ // it emits bare `React.createElement` calls without injecting any
3057
+ // import, and the scan output is never executed — it only exists so
3058
+ // rolldown can walk the import graph.
2952
3059
  ...(isVite8 ? {
2953
3060
  rolldownOptions: {
2954
3061
  transform: {
2955
- jsx: 'preserve'
3062
+ jsx: {
3063
+ runtime: 'classic'
3064
+ }
2956
3065
  }
2957
3066
  }
2958
3067
  } : {})