@solidjs/vite-plugin 3.0.0-next.29 → 3.0.0-next.31

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.
@@ -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
  }
@@ -143,6 +177,7 @@ function joinBase(base, pathname) {
143
177
  * dynamically imported modules register their own styles when they render.
144
178
  */
145
179
 
180
+ const defaultStyleFilter = id => !id.includes('node_modules');
146
181
  // The resolver is created plugin-side (it closes over the dev server) but is
147
182
  // called from the SSR module runner, which only shares `globalThis` with the
148
183
  // plugin when it runs in-process (the default). The primary channel is a
@@ -286,13 +321,15 @@ async function getModuleNode(env, file, importer) {
286
321
  return;
287
322
  }
288
323
  }
289
- async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
324
+ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, importer) {
290
325
  crawled.add(file);
291
326
  const node = await getModuleNode(env, file, importer);
292
327
  if (!node?.id || deps.has(node)) return;
293
328
  deps.add(node);
294
- if (node.file && !node.id.includes('node_modules')) onFile?.(node.file);
295
- if (cssFileRegExp.test(node.url.split('?')[0]) || node.id.includes('node_modules')) return;
329
+ const isCss = cssFileRegExp.test(node.url.split('?')[0]);
330
+ if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
331
+ if (node.file) onFile?.(node.file);
332
+ if (isCss) return;
296
333
  if (!node.transformResult) {
297
334
  await env.transformRequest(node.url).catch(() => {});
298
335
  }
@@ -303,7 +340,7 @@ async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
303
340
  // from dynamicDeps — dynamic imports load their own styles when rendered.
304
341
  for (const dep of directDeps) {
305
342
  if (crawled.has(dep)) continue;
306
- await collectModuleDeps(env, dep, deps, crawled, onFile, node.id);
343
+ await collectModuleDeps(env, dep, deps, crawled, filter, onFile, node.id);
307
344
  }
308
345
  }
309
346
  function injectQuery(url, query) {
@@ -311,11 +348,11 @@ function injectQuery(url, query) {
311
348
  }
312
349
 
313
350
  /** Discovers ambient CSS in an entry graph without choosing how it is transported. */
314
- async function collectDevStyleSources(env, files, onFile) {
351
+ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleFilter) {
315
352
  const deps = new Set();
316
353
  const crawled = new Set();
317
354
  for (const file of files) {
318
- await collectModuleDeps(env, file, deps, crawled, onFile);
355
+ await collectModuleDeps(env, file, deps, crawled, filter, onFile);
319
356
  }
320
357
  const css = [];
321
358
  const seen = new Set();
@@ -342,11 +379,11 @@ async function collectDevStyleSources(env, files, onFile) {
342
379
  * CSS into `<head>` so server-painted content is styled from the first byte
343
380
  * (no FOUC while waiting for Vite's client-side style injection).
344
381
  */
345
- async function collectDevStyles(server, files) {
382
+ async function collectDevStyles(server, files, filter = defaultStyleFilter) {
346
383
  const ssrEnv = server.environments?.ssr;
347
384
  const clientEnv = server.environments?.client;
348
385
  if (!ssrEnv || !clientEnv) return [];
349
- const sources = await collectDevStyleSources(ssrEnv, files.map(file => path.resolve(server.config.root, file)));
386
+ const sources = await collectDevStyleSources(ssrEnv, files.map(file => path.resolve(server.config.root, file)), undefined, filter);
350
387
  const css = [];
351
388
  for (const source of sources) {
352
389
  // `?direct` yields the compiled stylesheet text (what Vite serves for
@@ -402,7 +439,7 @@ function devModuleUrl(root, base, key) {
402
439
  // drive letter on Windows: /@fs/C:/…).
403
440
  return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query);
404
441
  }
405
- function createDevAssetResolver(server) {
442
+ function createDevAssetResolver(server, filter = defaultStyleFilter) {
406
443
  // Server-side lazy() re-requests a module's assets on every retry of a
407
444
  // suspended render pass (retries re-create the component). The build
408
445
  // manifest answers those repeats synchronously and the pass converges; an
@@ -436,7 +473,7 @@ function createDevAssetResolver(server) {
436
473
  // The module's dev URL doubles as its client entry: modulepreload
437
474
  // hint and hydration module-map value.
438
475
  const js = [devModuleUrl(root, base, key)];
439
- const css = await collectDevStyles(server, [key]);
476
+ const css = await collectDevStyles(server, [key], filter);
440
477
  return {
441
478
  js,
442
479
  css
@@ -529,6 +566,11 @@ function boundaryModules() {
529
566
  function isRunnableEnvironment(environment) {
530
567
  return !!environment && typeof environment === 'object' && 'runner' in environment;
531
568
  }
569
+ function getEnvironmentConsumer(environment, options) {
570
+ const consumer = environment?.config?.consumer;
571
+ if (consumer === 'client' || consumer === 'server') return consumer;
572
+ return options?.ssr ? 'server' : 'client';
573
+ }
532
574
 
533
575
  // The `"use server"` directive compiler. This wraps the native
534
576
  // `transformDirectives` pass from @dom-expressions/compiler (Rust/Oxc); the
@@ -965,12 +1007,12 @@ function serverFunctions(options = {}, internal = {}) {
965
1007
  const relative = path.relative(root, entry).split(path.sep).join('/');
966
1008
  return relative.startsWith('..') ? '/@fs/' + entry : '/' + relative;
967
1009
  }
968
- const turnkeyPlugins = [{
1010
+ const startPlugins = [{
969
1011
  name: 'solid:server-functions/handler',
970
1012
  enforce: 'pre',
971
1013
  resolveId(source, _importer, opts) {
972
1014
  if (source === HANDLER_ID$1) {
973
- if (!opts?.ssr) {
1015
+ if (getEnvironmentConsumer(this.environment, opts) !== 'server') {
974
1016
  this.error(`${HANDLER_ID$1} is server-only; import it from your server entry (SSR build).`);
975
1017
  }
976
1018
  return {
@@ -981,7 +1023,7 @@ function serverFunctions(options = {}, internal = {}) {
981
1023
  return null;
982
1024
  },
983
1025
  load(id, opts) {
984
- if (id === HANDLER_ID$1 && opts?.ssr) {
1026
+ if (id === HANDLER_ID$1 && getEnvironmentConsumer(this.environment, opts) === 'server') {
985
1027
  const externalDev = this.environment.mode === 'dev' && (internal.externalDevServer || !isRunnableEnvironment(this.environment));
986
1028
  return handlerModuleCode(isBuild || externalDev);
987
1029
  }
@@ -989,7 +1031,7 @@ function serverFunctions(options = {}, internal = {}) {
989
1031
  }
990
1032
  }];
991
1033
  if (installDevMiddleware) {
992
- turnkeyPlugins.push({
1034
+ startPlugins.push({
993
1035
  name: 'solid:server-functions/dev-middleware',
994
1036
  apply: 'serve',
995
1037
  configureServer(server) {
@@ -1034,7 +1076,7 @@ function serverFunctions(options = {}, internal = {}) {
1034
1076
  nativeEvent: req
1035
1077
  }
1036
1078
  };
1037
- const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl), dispatchOptions);
1079
+ const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions);
1038
1080
  await sendWebResponse(res, response);
1039
1081
  })().catch(error => {
1040
1082
  if (error instanceof Error) server.ssrFixStacktrace(error);
@@ -1101,7 +1143,7 @@ function serverFunctions(options = {}, internal = {}) {
1101
1143
  return null;
1102
1144
  },
1103
1145
  async load(id, opts) {
1104
- const mode = opts?.ssr ? 'server' : 'client';
1146
+ const mode = getEnvironmentConsumer(this.environment, opts);
1105
1147
  if (id === manifestId) {
1106
1148
  if (isBuild && mode === 'server') {
1107
1149
  // Merge the client build's persisted discoveries at load time,
@@ -1124,7 +1166,7 @@ function serverFunctions(options = {}, internal = {}) {
1124
1166
  name: 'solid:server-functions/compiler',
1125
1167
  enforce: 'pre',
1126
1168
  async transform(code, fileId, opts) {
1127
- const mode = opts?.ssr ? 'server' : 'client';
1169
+ const mode = getEnvironmentConsumer(this.environment, opts);
1128
1170
  const [id] = fileId.split('?');
1129
1171
  if (!filter(id)) {
1130
1172
  return null;
@@ -1157,7 +1199,17 @@ function serverFunctions(options = {}, internal = {}) {
1157
1199
  }
1158
1200
  return null;
1159
1201
  }
1160
- }, ...turnkeyPlugins];
1202
+ }, ...startPlugins];
1203
+ }
1204
+
1205
+ const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
1206
+ const DEVTOOLS_ID = 'virtual:solid-devtools';
1207
+ const DEVTOOLS_MOUNT_ID = 'virtual:solid-devtools/mount';
1208
+ function devtoolsModuleCode() {
1209
+ return [`import * as serverFunctions from '@solidjs/web/server-functions';`, `import { DevToolbar, pushServerFunctionCall } from '${DEVTOOLS_PACKAGE}';`, `const observe = Reflect.get(serverFunctions, 'observeServerFunctionCalls');`, `if (typeof observe === 'function') observe(pushServerFunctionCall);`, `export { DevToolbar };`].join('\n');
1210
+ }
1211
+ function devtoolsMountModuleCode() {
1212
+ return [`import ${JSON.stringify(DEVTOOLS_ID)};`, `import { mountDevToolbar } from '${DEVTOOLS_PACKAGE}';`, `mountDevToolbar();`].join('\n');
1161
1213
  }
1162
1214
 
1163
1215
  // Start-mode serving for plain Vite apps: `solid({ start: {...} })` (or the
@@ -1242,6 +1294,7 @@ const RESOLVED_DEV_STYLES_ID = '\0' + DEV_STYLES_ID;
1242
1294
  const ENTRY_SERVER_ID = 'virtual:solid-ssr-entry-server.tsx';
1243
1295
  const ENTRY_CLIENT_ID = 'virtual:solid-ssr-entry-client.tsx';
1244
1296
  const DOCUMENT_ID = 'virtual:solid-ssr-document.tsx';
1297
+ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1245
1298
  const MANIFEST_ID = 'virtual:solid-manifest';
1246
1299
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1247
1300
  const STORAGE_SOURCE = '@solidjs/web/storage';
@@ -1353,6 +1406,12 @@ function startServe(options, internal = {}) {
1353
1406
  // the server-function handler module either way). Everything is gated
1354
1407
  // codegen: with the option off, none of these imports exist anywhere.
1355
1408
  const serverComponents = !!internal.serverComponents;
1409
+ const errorBoundary = options.errorBoundary !== false;
1410
+ const styleFilter = internal.styleFilter;
1411
+ let devtools = false;
1412
+ let devtoolsResolution;
1413
+ /** Resolved module id of `@solidjs/start-devtools` once detection succeeds. */
1414
+ let devtoolsId = null;
1356
1415
  // `external` is server-mode-only (documented no-op in client mode, so a
1357
1416
  // host-integrated config survives the `ssr` boolean flip untouched).
1358
1417
  const externalServer = !clientMode && !!options.external;
@@ -1369,6 +1428,40 @@ function startServe(options, internal = {}) {
1369
1428
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1370
1429
  return entries;
1371
1430
  }
1431
+ async function resolveDevtools(resolve, importer) {
1432
+ if (devtools !== undefined) return devtools;
1433
+ // Detect from the app graph first (the documented install location), then
1434
+ // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1435
+ // dependency of the plugin is not reachable from the app's importers. The
1436
+ // resolved id is kept so the virtual modules' imports of the package can
1437
+ // be delegated to it (see resolveId).
1438
+ devtoolsResolution ??= (async () => ((await resolve(DEVTOOLS_PACKAGE, importer)) ?? (await resolve(DEVTOOLS_PACKAGE, node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))))))?.id ?? null)();
1439
+ devtoolsId = await devtoolsResolution;
1440
+ devtools = devtoolsId !== null;
1441
+ if (!devtools && options.devtools === true) {
1442
+ throw new Error('[@solidjs/vite-plugin] start.devtools requires @solidjs/start-devtools. ' + 'Install it as a development dependency or set start.devtools to false.');
1443
+ }
1444
+ return devtools;
1445
+ }
1446
+
1447
+ /**
1448
+ * Cheap root-walk probe mirroring how the optimizer resolves bare
1449
+ * `optimizeDeps.include` entries: is @solidjs/start-devtools reachable from
1450
+ * the Vite root? Detection proper (resolveDevtools) runs later with a real
1451
+ * importer; this only decides whether the toolbar graph can be pre-bundled
1452
+ * at scan time — it hangs off virtual modules the scanner never sees, so
1453
+ * first-request discovery would force a re-optimize + full page reload.
1454
+ */
1455
+ function devtoolsReachableFromRoot(dir) {
1456
+ for (let current = dir;;) {
1457
+ if (fs.existsSync(path.join(current, 'node_modules', DEVTOOLS_PACKAGE, 'package.json'))) {
1458
+ return true;
1459
+ }
1460
+ const parent = path.dirname(current);
1461
+ if (parent === current) return false;
1462
+ current = parent;
1463
+ }
1464
+ }
1372
1465
 
1373
1466
  /** Import specifier for generated code: absolute for files, id for virtuals. */
1374
1467
  function entryServerSpec() {
@@ -1408,7 +1501,7 @@ function startServe(options, internal = {}) {
1408
1501
  return generated ? [app, ...(document ? [document] : [])] : [path.resolve(root, entryServer)];
1409
1502
  }
1410
1503
  async function devStylesModuleCode(environment, watchFile) {
1411
- const styles = await collectDevStyleSources(environment, styleRoots(), watchFile);
1504
+ const styles = await collectDevStyleSources(environment, styleRoots(), watchFile, styleFilter);
1412
1505
  if (!styles.length) return `export default '';`;
1413
1506
  const imports = styles.map((style, index) => {
1414
1507
  const specifier = style.url.includes('?') ? `${style.url}&inline` : `${style.url}?inline`;
@@ -1416,19 +1509,26 @@ function startServe(options, internal = {}) {
1416
1509
  });
1417
1510
  return [...imports, `const ids = ${JSON.stringify(styles.map(style => style.id))};`, `const css = [${styles.map((_, index) => `css${index}`).join(', ')}];`, `const escapeAttr = value => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');`, `export default css.map((content, index) => {`, ` const id = escapeAttr(ids[index]);`, ` return '<style data-asset="' + id + '" data-vite-dev-id="' + id + '">' +`, ` content.replace(/<\\/(style)/gi, '<\\\\/$1') + '</style>';`, `}).join('');`].join('\n');
1418
1511
  }
1512
+ function errorBoundaryImport() {
1513
+ return isBuild && errorBoundary ? [`import { DefaultErrorBoundary } from ${JSON.stringify(ERROR_BOUNDARY_ID)};`] : [];
1514
+ }
1515
+ function documentTree(root, wrapper) {
1516
+ const content = wrapper ? `<${wrapper}><${root} /></${wrapper}>` : `<${root} />`;
1517
+ return isBuild && errorBoundary ? [` <DefaultErrorBoundary>`, ` <Document>`, ` <DefaultErrorBoundary>`, ` ${content}`, ` </DefaultErrorBoundary>`, ` </Document>`, ` </DefaultErrorBoundary>`] : [` <Document>`, ` ${content}`, ` </Document>`];
1518
+ }
1419
1519
  function generatedEntryServerCode() {
1420
1520
  if (clientMode) {
1421
1521
  // The client-mode shell: the document without the app. Rendered per
1422
1522
  // request in dev (any HTML GET gets it — history-fallback semantics)
1423
1523
  // and once at build time into dist/client/index.html. The client
1424
1524
  // entry script is injected by the handler, exactly like SSR mode.
1425
- return [`import { renderToStream } from '@solidjs/web';`, `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, ``, `export function render(request, context) {`, ` return renderToStream(() => <Document />, { manifest });`, `}`].join('\n');
1525
+ return [`import { renderToStream } from '@solidjs/web';`, `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, ...errorBoundaryImport(), ``, `export function render(request, context) {`, ` return renderToStream(() => (`, ...(isBuild && errorBoundary ? [` <DefaultErrorBoundary>`, ` <Document />`, ` </DefaultErrorBoundary>`] : [` <Document />`]), ` ), { manifest });`, `}`].join('\n');
1426
1526
  }
1427
1527
  const {
1428
1528
  app
1429
1529
  } = requireEntries();
1430
1530
  const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;
1431
- return [`import { renderToStream${setupPath ? ', getRequestEvent' : ''} } from '@solidjs/web';`, ...(serverComponents ? [`import { configureServerFunctionsServer } from '@solidjs/web/server-functions';`, `import { frameTransformDirectResult, ServerComponentPlugin } from '@solidjs/web/frames';`] : []), `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ...(setupPath ? [`import setup from ${JSON.stringify(setupPath)};`] : []), ``, ...(setupPath ? [`if (typeof setup !== 'function') {`, ` throw new Error('[@solidjs/vite-plugin] start.setup must default-export a function ' +`, ` '((event, App) => Component | void | Promise<...>): ' + ${JSON.stringify(options.setup)});`, `}`, ``] : []), ...(serverComponents ? [
1531
+ return [`import { renderToStream${setupPath ? ', getRequestEvent' : ''} } from '@solidjs/web';`, ...(serverComponents ? [`import { configureServerFunctionsServer } from '@solidjs/web/server-functions';`, `import { frameTransformDirectResult, ServerComponentPlugin } from '@solidjs/web/frames';`] : []), `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ...errorBoundaryImport(), ...(setupPath ? [`import setup from ${JSON.stringify(setupPath)};`] : []), ``, ...(setupPath ? [`if (typeof setup !== 'function') {`, ` throw new Error('[@solidjs/vite-plugin] start.setup must default-export a function ' +`, ` '((event, App) => Component | void | Promise<...>): ' + ${JSON.stringify(options.setup)});`, `}`, ``] : []), ...(serverComponents ? [
1432
1532
  // Direct (in-process) server-function calls made during document
1433
1533
  // SSR must resolve to inline-renderable components; the endpoint
1434
1534
  // response transform is installed separately by the
@@ -1442,9 +1542,9 @@ function startServe(options, internal = {}) {
1442
1542
  // the *complete* render) and buffers the stream — so it crosses
1443
1543
  // boxed under a private key the generated handler unboxes
1444
1544
  // (both modules are ours).
1445
- `export function render(request, context) {`, ` const prepared = setup(getRequestEvent(), App);`, ` if (prepared && typeof prepared.then === 'function') {`, ` return prepared.then((component) => ({ ${STREAM_BOX}: renderApp(component || App) }));`, ` }`, ` return renderApp(prepared || App);`, `}`, ``, `function renderApp(Root) {`, ` return renderToStream(() => (`, ` <Document>`, ` <Root />`, ` </Document>`, ` ), ${streamOptions});`, `}`] : [`export function render(request, context) {`, ` return renderToStream(() => (`, ` <Document>`, ` <App />`, ` </Document>`, ` ), ${streamOptions});`, `}`])].join('\n');
1545
+ `export function render(request, context) {`, ` const prepared = setup(getRequestEvent(), App);`, ` if (prepared && typeof prepared.then === 'function') {`, ` return prepared.then((component) => ({ ${STREAM_BOX}: renderApp(component || App) }));`, ` }`, ` return renderApp(prepared || App);`, `}`, ``, `function renderApp(Root) {`, ` return renderToStream(() => (`, ...documentTree('Root'), ` ), ${streamOptions});`, `}`] : [`export function render(request, context) {`, ` return renderToStream(() => (`, ...documentTree('App'), ` ), ${streamOptions});`, `}`])].join('\n');
1446
1546
  }
1447
- function generatedEntryClientCode() {
1547
+ function generatedEntryClientCode(toolbar) {
1448
1548
  const {
1449
1549
  app
1450
1550
  } = requireEntries();
@@ -1454,13 +1554,13 @@ function startServe(options, internal = {}) {
1454
1554
  // app cannot claim server DOM anyway. The entry script is injected
1455
1555
  // without `async` (plain module = deferred), so document.body is
1456
1556
  // complete when this runs.
1457
- return [`import { render } from '@solidjs/web';`, `import App from ${JSON.stringify(app)};`, ``, `render(() => <App />, document.body);`].join('\n');
1557
+ return [`import { render } from '@solidjs/web';`, ...errorBoundaryImport(), ...(toolbar ? [`import { DevToolbar } from ${JSON.stringify(DEVTOOLS_ID)};`] : []), `import App from ${JSON.stringify(app)};`, ``, `render(() => ${isBuild && errorBoundary ? '<DefaultErrorBoundary><App /></DefaultErrorBoundary>' : toolbar ? '<DevToolbar><App /></DevToolbar>' : '<App />'}, document.body);`].join('\n');
1458
1558
  }
1459
- return [`import { hydrate } from '@solidjs/web';`, ...(serverComponents ? [`import { installServerComponents } from '@solidjs/web/frames';`] : []), `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ``, ...(serverComponents ? [
1559
+ return [`import { hydrate } from '@solidjs/web';`, ...(toolbar ? [`import { DevToolbar } from ${JSON.stringify(DEVTOOLS_ID)};`] : []), ...(serverComponents ? [`import { installServerComponents } from '@solidjs/web/frames';`] : []), ...errorBoundaryImport(), `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ``, ...(serverComponents ? [
1460
1560
  // Installs the t=0 document-adoption registry and the transport
1461
1561
  // policy (component responses morph their boundary instead of
1462
1562
  // decoding as data). Must run before hydrate().
1463
- `installServerComponents();`, ``] : []), `hydrate(() => (`, ` <Document>`, ` <App />`, ` </Document>`, `), document);`].join('\n');
1563
+ `installServerComponents();`, ``] : []), `hydrate(() => (`, ...documentTree('App', toolbar ? 'DevToolbar' : undefined), `), document);`].join('\n');
1464
1564
  }
1465
1565
 
1466
1566
  // Built-in document shell: minimal, hydration-ready. The client entry
@@ -1471,6 +1571,7 @@ function startServe(options, internal = {}) {
1471
1571
  // HydrationScript is covered too: the handler strips the event-capture
1472
1572
  // script from the client-mode shell.)
1473
1573
  const documentShellCode = [...(clientMode ? [] : [`import { HydrationScript } from '@solidjs/web';`, ``]), `export default function Document(props) {`, ` return (`, ` <html lang="en">`, ` <head>`, ` <meta charset="utf-8" />`, ` <meta name="viewport" content="width=device-width, initial-scale=1.0" />`, ...(clientMode ? [] : [` <HydrationScript />`]), ` </head>`, ` <body>{props.children}</body>`, ` </html>`, ` );`, `}`].join('\n');
1574
+ const errorBoundaryCode = [`import { Errored } from 'solid-js';`, `import { httpStatus, isServer } from '@solidjs/web';`, ``, `function ErrorFallback(props) {`, ` console.error(props.error());`, ` httpStatus(500);`, ` return (`, ` <span style="font-size:1.5em;text-align:center;position:fixed;left:0;bottom:55%;width:100%">`, ` {isServer ? '500 | Internal Server Error' : 'Error | Uncaught Client Exception'}`, ` </span>`, ` );`, `}`, ``, `export function DefaultErrorBoundary(props) {`, ` return (`, ` <Errored fallback={(error) => <ErrorFallback error={error} />}>`, ` {props.children}`, ` </Errored>`, ` );`, `}`].join('\n');
1474
1575
 
1475
1576
  // The handler module: dev and prod share the render/response plumbing;
1476
1577
  // they differ in how the client entry URL is known (baked dev URL vs a
@@ -1522,7 +1623,7 @@ function startServe(options, internal = {}) {
1522
1623
  // head-open splice actively broke hydration — a script ahead of the
1523
1624
  // authored <head> elements claims as the first walked child and drifts
1524
1625
  // every positional claim after it.
1525
- lines.push(``, `function createHtmlChunkTransform(clientEntry, extraHead) {`, ` let first = true;`, ` let injected = false;`, ` return (chunk) => {`);
1626
+ lines.push(``, `function escapeAttribute(value) {`, ` return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');`, `}`, ``, `function createHtmlChunkTransform(clientEntry, extraHead, nonce) {`, ` const nonceAttr = nonce ? ' nonce="' + escapeAttribute(nonce) + '"' : '';`, ` let first = true;`, ` let injected = false;`, ` return (chunk) => {`);
1526
1627
  if (!generated) {
1527
1628
  // Authored entries reference the client entry by its dev path (the
1528
1629
  // `<script src="/src/entry-client.tsx">` convention); rewrite it to
@@ -1552,7 +1653,7 @@ function startServe(options, internal = {}) {
1552
1653
  // scripts default to deferred execution, which is exactly right for a
1553
1654
  // fresh render-into-body mount (hydration, by contrast, wants to
1554
1655
  // start as early as possible).
1555
- headParts.push(`(clientEntry ? '<script type="module" src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1656
+ headParts.push(`(clientEntry ? '<script type="module"' + nonceAttr + ' src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1556
1657
  }
1557
1658
  if (headParts.length) {
1558
1659
  lines.push(` chunk = chunk.replace('</head>', ${headParts.join(' + ')} + '</head>');`);
@@ -1606,7 +1707,7 @@ function startServe(options, internal = {}) {
1606
1707
  // The runtime's response-head lifecycle: commit at shell flush,
1607
1708
  // pre-flush Location as a real redirect, post-flush Location as the
1608
1709
  // script fallback; the transform injects the doctype/head pieces.
1609
- ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1710
+ ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, options.nonce),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1610
1711
  // `options.event` is the public wrapper->event extension seam: extra
1611
1712
  // fields (conventionally `nativeEvent`, the platform's raw request
1612
1713
  // object) spread over the event's defaults at creation, so hosts and
@@ -1636,6 +1737,9 @@ function startServe(options, internal = {}) {
1636
1737
  enforce: 'pre',
1637
1738
  config(userConfig, env) {
1638
1739
  root = path.resolve(userConfig.root || process.cwd());
1740
+ devtools = env.command === 'serve' && !env.isPreview && options.devtools !== false ? undefined : false;
1741
+ devtoolsResolution = undefined;
1742
+ devtoolsId = null;
1639
1743
  entries = resolveEntries(root, options, clientMode);
1640
1744
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
1641
1745
  // Server-mode only, like `entryServer`/`external` (a documented
@@ -1754,7 +1858,15 @@ function startServe(options, internal = {}) {
1754
1858
  }
1755
1859
  } : {}),
1756
1860
  optimizeDeps: {
1757
- entries: scanEntries
1861
+ entries: scanEntries,
1862
+ // Like the refresh runtime in the main plugin: the toolbar
1863
+ // graph is injected behind virtual modules the scanner never
1864
+ // crawls, so pre-bundle it (and the server-functions runtime
1865
+ // the virtual module pulls in) up front — first-request
1866
+ // discovery would re-optimize and full-reload the page.
1867
+ ...(devtools === undefined && devtoolsReachableFromRoot(root) ? {
1868
+ include: [DEVTOOLS_PACKAGE, '@solidjs/web/server-functions']
1869
+ } : {})
1758
1870
  }
1759
1871
  })
1760
1872
  };
@@ -1764,7 +1876,7 @@ function startServe(options, internal = {}) {
1764
1876
  base = config.base;
1765
1877
  isBuild = config.command === 'build';
1766
1878
  },
1767
- resolveId(source) {
1879
+ resolveId(source, importer) {
1768
1880
  if (source === HANDLER_ID) {
1769
1881
  return {
1770
1882
  id: HANDLER_ID,
@@ -1777,33 +1889,94 @@ function startServe(options, internal = {}) {
1777
1889
  moduleSideEffects: true
1778
1890
  };
1779
1891
  }
1780
- if (source === ENTRY_SERVER_ID || source === ENTRY_CLIENT_ID || source === DOCUMENT_ID) {
1892
+ if (source === ENTRY_SERVER_ID || source === ENTRY_CLIENT_ID || source === DOCUMENT_ID || source === ERROR_BOUNDARY_ID) {
1781
1893
  return {
1782
1894
  id: source,
1783
1895
  moduleSideEffects: source === ENTRY_CLIENT_ID
1784
1896
  };
1785
1897
  }
1898
+ if (devtools && (source === DEVTOOLS_ID || source === DEVTOOLS_MOUNT_ID)) {
1899
+ return {
1900
+ id: source,
1901
+ moduleSideEffects: true
1902
+ };
1903
+ }
1904
+ // The virtual devtools modules import the package by its bare name,
1905
+ // but a virtual importer gives Vite no directory to walk, so the
1906
+ // specifier would only resolve from the Vite root — which fails in
1907
+ // pnpm-isolated apps where the package is not a root-level install.
1908
+ // Delegate to the resolution captured at detection time instead.
1909
+ if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === DEVTOOLS_ID || importer === DEVTOOLS_MOUNT_ID)) {
1910
+ return {
1911
+ id: devtoolsId
1912
+ };
1913
+ }
1786
1914
  return null;
1787
1915
  },
1788
1916
  async load(id, opts) {
1917
+ const consumer = getEnvironmentConsumer(this.environment, opts);
1789
1918
  if (id === HANDLER_ID) {
1790
- if (!opts?.ssr) {
1919
+ if (consumer !== 'server') {
1791
1920
  this.error(`${HANDLER_ID} is server-only; import it from server code (SSR build).`);
1792
1921
  }
1793
1922
  const externalDev = !isBuild && this.environment.mode === 'dev' && (externalServer || !isRunnableEnvironment(this.environment));
1794
1923
  return handlerModuleCode(externalDev);
1795
1924
  }
1796
1925
  if (id === RESOLVED_DEV_STYLES_ID) {
1797
- if (!opts?.ssr || this.environment.mode !== 'dev') {
1926
+ if (consumer !== 'server' || this.environment.mode !== 'dev') {
1798
1927
  this.error(`${DEV_STYLES_ID} is only available to the development server handler.`);
1799
1928
  }
1800
1929
  return devStylesModuleCode(this.environment, file => this.addWatchFile(file));
1801
1930
  }
1802
1931
  if (id === ENTRY_SERVER_ID) return generatedEntryServerCode();
1803
- if (id === ENTRY_CLIENT_ID) return generatedEntryClientCode();
1932
+ if (id === ENTRY_CLIENT_ID) {
1933
+ const toolbar = await resolveDevtools((source, importer) => this.resolve(source, importer, {
1934
+ skipSelf: true
1935
+ }), requireEntries().app);
1936
+ return generatedEntryClientCode(toolbar);
1937
+ }
1804
1938
  if (id === DOCUMENT_ID) return documentShellCode;
1939
+ if (id === ERROR_BOUNDARY_ID) return errorBoundaryCode;
1940
+ if (id === DEVTOOLS_ID || id === DEVTOOLS_MOUNT_ID) {
1941
+ // A cold direct request (stale tab reload) can reach the virtual
1942
+ // module before the entry has triggered detection — run it here so
1943
+ // first-touch order doesn't matter.
1944
+ if (!isBuild && consumer === 'client' && devtools === undefined) {
1945
+ const {
1946
+ app,
1947
+ entryClient
1948
+ } = requireEntries();
1949
+ await resolveDevtools((source, importer) => this.resolve(source, importer, {
1950
+ skipSelf: true
1951
+ }), app ?? path.resolve(root, entryClient));
1952
+ }
1953
+ if (isBuild || !devtools || consumer !== 'client') {
1954
+ this.error(`${id} is only available to the development client.`);
1955
+ }
1956
+ return id === DEVTOOLS_ID ? devtoolsModuleCode() : devtoolsMountModuleCode();
1957
+ }
1805
1958
  return null;
1806
1959
  },
1960
+ async transform(code, id, opts) {
1961
+ if (isBuild || devtools === false) return null;
1962
+ const current = requireEntries();
1963
+ if (current.generated || getEnvironmentConsumer(this.environment, opts) !== 'client') {
1964
+ return null;
1965
+ }
1966
+ // Module ids are always forward-slashed; normalize the path.resolve
1967
+ // side too so the comparison holds on Windows.
1968
+ if (vite.normalizePath(id.split('?')[0]) !== vite.normalizePath(path.resolve(root, current.entryClient))) {
1969
+ return null;
1970
+ }
1971
+ const toolbar = await resolveDevtools((source, importer) => this.resolve(source, importer, {
1972
+ skipSelf: true
1973
+ }), id);
1974
+ if (!toolbar) return null;
1975
+ return {
1976
+ code: `import ${JSON.stringify(DEVTOOLS_MOUNT_ID)};\n${code}`,
1977
+ map: null
1978
+ };
1979
+ },
1807
1980
  configurePreviewServer(server) {
1808
1981
  // `vite build && vite preview` runs the production artifact as-is:
1809
1982
  // Vite's preview statics serve dist/client (see the config hook) and
@@ -1828,7 +2001,7 @@ function startServe(options, internal = {}) {
1828
2001
  // server-function endpoint) and hands the URL to application
1829
2002
  // code, so restore the base — the deployed production handler
1830
2003
  // receives base-prefixed URLs and preview must match it.
1831
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')),
2004
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res),
1832
2005
  // Same event extension the dev middleware and a production
1833
2006
  // Node entry pass: the raw Node request as `nativeEvent`.
1834
2007
  {
@@ -1879,13 +2052,13 @@ function startServe(options, internal = {}) {
1879
2052
  // Loaded through the SSR environment so the app, the request
1880
2053
  // event storage, and the handler share one module registry.
1881
2054
  const handler = await server.ssrLoadModule(HANDLER_ID);
1882
- const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : [];
2055
+ const styles = pageRequest ? await collectDevStyles(server, styleRoots(), styleFilter) : [];
1883
2056
  const devHead = styles.map(renderDevStyleTag).join('');
1884
2057
  // Post middlewares run after Vite's base middleware stripped
1885
2058
  // the configured `base` from req.url; restore it so the app
1886
2059
  // sees the same URLs in dev as in production (where the
1887
2060
  // deployed handler receives base-prefixed requests).
1888
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')), {
2061
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res), {
1889
2062
  devHead,
1890
2063
  pageRequest,
1891
2064
  // The raw Node request on the event, matching what a
@@ -2568,6 +2741,7 @@ const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:';
2568
2741
  const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
2569
2742
  const viteVersionMajor = +vite.version.split('.')[0];
2570
2743
  const isVite8 = viteVersionMajor >= 8;
2744
+ const DEFAULT_STYLE_EXCLUDE = /node_modules/;
2571
2745
  const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
2572
2746
  const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
2573
2747
 
@@ -2822,10 +2996,30 @@ function solidPlugin(options = {}) {
2822
2996
  // `start: true` is sugar for the empty options bag — one start mode,
2823
2997
  // two spellings — so normalize here and let everything downstream see a
2824
2998
  // single shape (`false` behaves exactly like omission).
2825
- const turnkey = options.start === true ? {} : options.start || null;
2999
+ const startOptions = options.start === true ? {} : options.start || null;
3000
+ const styleFilterOptions = startOptions?.css?.filter;
3001
+ // The CSS crawl walks the module graph from the app's own entries, so a
3002
+ // plain createFilter allowlist can't express the option's purpose (opting
3003
+ // node_modules graphs in): a bare `include` would reject the app sources
3004
+ // the crawl has to traverse to ever reach the included package. Instead
3005
+ // `include` rescues files on top of the baseline (everything except
3006
+ // `exclude`, which defaults to node_modules), while a file matching both
3007
+ // patterns stays excluded — createFilter's own conflict rule.
3008
+ const createStyleFilter = resolve => {
3009
+ const opts = resolve === undefined ? undefined : {
3010
+ resolve
3011
+ };
3012
+ const base = vite.createFilter(undefined, styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE, opts);
3013
+ const include = styleFilterOptions?.include;
3014
+ const hasInclude = include != null && (!Array.isArray(include) || include.length > 0);
3015
+ const included = hasInclude ? vite.createFilter(include, styleFilterOptions?.exclude, opts) : null;
3016
+ return id => base(id) || (included ? included(id) : false);
3017
+ };
3018
+ let styleFilter = createStyleFilter();
3019
+ const filterDevStyles = id => styleFilter(id);
2826
3020
  // `start.external` only means something when a server side exists to hand
2827
3021
  // over (SSR start mode); in client mode it is a documented no-op.
2828
- const externalDevServer = !!options.ssr && !!turnkey?.external;
3022
+ const externalDevServer = !!options.ssr && !!startOptions?.external;
2829
3023
  let needHmr = false;
2830
3024
  let replaceDev = false;
2831
3025
  // The live dev server, kept so the dev manifest module can bake the bridge
@@ -3103,6 +3297,7 @@ function solidPlugin(options = {}) {
3103
3297
  filter = vite.createFilter(options.include, options.exclude, {
3104
3298
  resolve: projectRoot
3105
3299
  });
3300
+ styleFilter = createStyleFilter(projectRoot);
3106
3301
  if (serverComponents && !(options.start && options.ssr)) {
3107
3302
  config.logger.warn('[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' + 'option with `ssr: true`), so the plugin only installs the endpoint response transform ' + '(server functions returning components stream correctly). The document wiring — render ' + 'plugin, bootstrap script, and the client-side installServerComponents() call — is ' + "emitted by SSR start mode's generated entries; without it, server components only mount " + 'from post-boot streams and your client code must call installServerComponents() itself.');
3108
3303
  }
@@ -3116,7 +3311,7 @@ function solidPlugin(options = {}) {
3116
3311
  // that don't share globals with this process, through the HTTP bridge
3117
3312
  // endpoint the middleware serves.
3118
3313
  if (options.ssr || options.start) {
3119
- registerDevAssetResolver(server.config.root, createDevAssetResolver(server));
3314
+ registerDevAssetResolver(server.config.root, createDevAssetResolver(server, filterDevStyles));
3120
3315
  installDevManifestBridge(server);
3121
3316
  }
3122
3317
  if (!needHmr) return;
@@ -3232,7 +3427,7 @@ function solidPlugin(options = {}) {
3232
3427
  }
3233
3428
  },
3234
3429
  async transform(source, id, transformOptions) {
3235
- const isSsr = transformOptions && transformOptions.ssr;
3430
+ const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3236
3431
  const currentFileExtension = getExtension(id);
3237
3432
  const extensionsToWatch = options.extensions || [];
3238
3433
  const allExtensions = extensionsToWatch.map(extension =>
@@ -3372,7 +3567,7 @@ function solidPlugin(options = {}) {
3372
3567
  // With start mode on (either variant), the dev middleware dispatches
3373
3568
  // the endpoint through the SSR handler so user middleware and the
3374
3569
  // stub-backed request event front it exactly like page SSR.
3375
- ...(turnkey ? {
3570
+ ...(startOptions ? {
3376
3571
  ssrHandler: SSR_HANDLER_ID
3377
3572
  } : {})
3378
3573
  }), mainPlugin] : [boundaryModules(), mainPlugin];
@@ -3380,15 +3575,16 @@ function solidPlugin(options = {}) {
3380
3575
  // The `start` option opts into start-mode serving on top of the transforms;
3381
3576
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
3382
3577
  // historical transform-only behavior).
3383
- if (turnkey) {
3578
+ if (startOptions) {
3384
3579
  plugins.push(
3385
3580
  // Typed env (`start.env`) rides both start modes: config-time
3386
3581
  // validation, the virtual:env/{server,client} modules, generated
3387
3582
  // types, and the client-bundle leak scan.
3388
- ...startEnv(turnkey.env), ...startServe(turnkey, {
3583
+ ...startEnv(startOptions.env), ...startServe(startOptions, {
3389
3584
  serverFunctions: !!options.serverFunctions,
3390
3585
  serverComponents,
3391
- ssr: !!options.ssr
3586
+ ssr: !!options.ssr,
3587
+ styleFilter: filterDevStyles
3392
3588
  }));
3393
3589
  }
3394
3590