@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.
@@ -6,8 +6,8 @@ import { mergeAndConcat } from 'merge-anything';
6
6
  import { createRequire } from 'module';
7
7
  import path from 'path';
8
8
  import { Readable } from 'node:stream';
9
- import { createFilter, loadEnv, version } from 'vite';
10
- import { pathToFileURL } from 'node:url';
9
+ import { createFilter, normalizePath, loadEnv, version } from 'vite';
10
+ import { pathToFileURL, fileURLToPath } from 'node:url';
11
11
  import { crawlFrameworkPkgs } from 'vitefu';
12
12
 
13
13
  // Node <-> web-standard request/response bridging shared by the plugin's dev
@@ -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
  }
@@ -119,6 +153,7 @@ function joinBase(base, pathname) {
119
153
  * dynamically imported modules register their own styles when they render.
120
154
  */
121
155
 
156
+ const defaultStyleFilter = id => !id.includes('node_modules');
122
157
  // The resolver is created plugin-side (it closes over the dev server) but is
123
158
  // called from the SSR module runner, which only shares `globalThis` with the
124
159
  // plugin when it runs in-process (the default). The primary channel is a
@@ -262,13 +297,15 @@ async function getModuleNode(env, file, importer) {
262
297
  return;
263
298
  }
264
299
  }
265
- async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
300
+ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, importer) {
266
301
  crawled.add(file);
267
302
  const node = await getModuleNode(env, file, importer);
268
303
  if (!node?.id || deps.has(node)) return;
269
304
  deps.add(node);
270
- if (node.file && !node.id.includes('node_modules')) onFile?.(node.file);
271
- if (cssFileRegExp.test(node.url.split('?')[0]) || node.id.includes('node_modules')) return;
305
+ const isCss = cssFileRegExp.test(node.url.split('?')[0]);
306
+ if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
307
+ if (node.file) onFile?.(node.file);
308
+ if (isCss) return;
272
309
  if (!node.transformResult) {
273
310
  await env.transformRequest(node.url).catch(() => {});
274
311
  }
@@ -279,7 +316,7 @@ async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
279
316
  // from dynamicDeps — dynamic imports load their own styles when rendered.
280
317
  for (const dep of directDeps) {
281
318
  if (crawled.has(dep)) continue;
282
- await collectModuleDeps(env, dep, deps, crawled, onFile, node.id);
319
+ await collectModuleDeps(env, dep, deps, crawled, filter, onFile, node.id);
283
320
  }
284
321
  }
285
322
  function injectQuery(url, query) {
@@ -287,11 +324,11 @@ function injectQuery(url, query) {
287
324
  }
288
325
 
289
326
  /** Discovers ambient CSS in an entry graph without choosing how it is transported. */
290
- async function collectDevStyleSources(env, files, onFile) {
327
+ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleFilter) {
291
328
  const deps = new Set();
292
329
  const crawled = new Set();
293
330
  for (const file of files) {
294
- await collectModuleDeps(env, file, deps, crawled, onFile);
331
+ await collectModuleDeps(env, file, deps, crawled, filter, onFile);
295
332
  }
296
333
  const css = [];
297
334
  const seen = new Set();
@@ -318,11 +355,11 @@ async function collectDevStyleSources(env, files, onFile) {
318
355
  * CSS into `<head>` so server-painted content is styled from the first byte
319
356
  * (no FOUC while waiting for Vite's client-side style injection).
320
357
  */
321
- async function collectDevStyles(server, files) {
358
+ async function collectDevStyles(server, files, filter = defaultStyleFilter) {
322
359
  const ssrEnv = server.environments?.ssr;
323
360
  const clientEnv = server.environments?.client;
324
361
  if (!ssrEnv || !clientEnv) return [];
325
- const sources = await collectDevStyleSources(ssrEnv, files.map(file => path.resolve(server.config.root, file)));
362
+ const sources = await collectDevStyleSources(ssrEnv, files.map(file => path.resolve(server.config.root, file)), undefined, filter);
326
363
  const css = [];
327
364
  for (const source of sources) {
328
365
  // `?direct` yields the compiled stylesheet text (what Vite serves for
@@ -378,7 +415,7 @@ function devModuleUrl(root, base, key) {
378
415
  // drive letter on Windows: /@fs/C:/…).
379
416
  return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query);
380
417
  }
381
- function createDevAssetResolver(server) {
418
+ function createDevAssetResolver(server, filter = defaultStyleFilter) {
382
419
  // Server-side lazy() re-requests a module's assets on every retry of a
383
420
  // suspended render pass (retries re-create the component). The build
384
421
  // manifest answers those repeats synchronously and the pass converges; an
@@ -412,7 +449,7 @@ function createDevAssetResolver(server) {
412
449
  // The module's dev URL doubles as its client entry: modulepreload
413
450
  // hint and hydration module-map value.
414
451
  const js = [devModuleUrl(root, base, key)];
415
- const css = await collectDevStyles(server, [key]);
452
+ const css = await collectDevStyles(server, [key], filter);
416
453
  return {
417
454
  js,
418
455
  css
@@ -505,6 +542,11 @@ function boundaryModules() {
505
542
  function isRunnableEnvironment(environment) {
506
543
  return !!environment && typeof environment === 'object' && 'runner' in environment;
507
544
  }
545
+ function getEnvironmentConsumer(environment, options) {
546
+ const consumer = environment?.config?.consumer;
547
+ if (consumer === 'client' || consumer === 'server') return consumer;
548
+ return options?.ssr ? 'server' : 'client';
549
+ }
508
550
 
509
551
  // The `"use server"` directive compiler. This wraps the native
510
552
  // `transformDirectives` pass from @dom-expressions/compiler (Rust/Oxc); the
@@ -941,12 +983,12 @@ function serverFunctions(options = {}, internal = {}) {
941
983
  const relative = path.relative(root, entry).split(path.sep).join('/');
942
984
  return relative.startsWith('..') ? '/@fs/' + entry : '/' + relative;
943
985
  }
944
- const turnkeyPlugins = [{
986
+ const startPlugins = [{
945
987
  name: 'solid:server-functions/handler',
946
988
  enforce: 'pre',
947
989
  resolveId(source, _importer, opts) {
948
990
  if (source === HANDLER_ID$1) {
949
- if (!opts?.ssr) {
991
+ if (getEnvironmentConsumer(this.environment, opts) !== 'server') {
950
992
  this.error(`${HANDLER_ID$1} is server-only; import it from your server entry (SSR build).`);
951
993
  }
952
994
  return {
@@ -957,7 +999,7 @@ function serverFunctions(options = {}, internal = {}) {
957
999
  return null;
958
1000
  },
959
1001
  load(id, opts) {
960
- if (id === HANDLER_ID$1 && opts?.ssr) {
1002
+ if (id === HANDLER_ID$1 && getEnvironmentConsumer(this.environment, opts) === 'server') {
961
1003
  const externalDev = this.environment.mode === 'dev' && (internal.externalDevServer || !isRunnableEnvironment(this.environment));
962
1004
  return handlerModuleCode(isBuild || externalDev);
963
1005
  }
@@ -965,7 +1007,7 @@ function serverFunctions(options = {}, internal = {}) {
965
1007
  }
966
1008
  }];
967
1009
  if (installDevMiddleware) {
968
- turnkeyPlugins.push({
1010
+ startPlugins.push({
969
1011
  name: 'solid:server-functions/dev-middleware',
970
1012
  apply: 'serve',
971
1013
  configureServer(server) {
@@ -1010,7 +1052,7 @@ function serverFunctions(options = {}, internal = {}) {
1010
1052
  nativeEvent: req
1011
1053
  }
1012
1054
  };
1013
- const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl), dispatchOptions);
1055
+ const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl, res), dispatchOptions);
1014
1056
  await sendWebResponse(res, response);
1015
1057
  })().catch(error => {
1016
1058
  if (error instanceof Error) server.ssrFixStacktrace(error);
@@ -1077,7 +1119,7 @@ function serverFunctions(options = {}, internal = {}) {
1077
1119
  return null;
1078
1120
  },
1079
1121
  async load(id, opts) {
1080
- const mode = opts?.ssr ? 'server' : 'client';
1122
+ const mode = getEnvironmentConsumer(this.environment, opts);
1081
1123
  if (id === manifestId) {
1082
1124
  if (isBuild && mode === 'server') {
1083
1125
  // Merge the client build's persisted discoveries at load time,
@@ -1100,7 +1142,7 @@ function serverFunctions(options = {}, internal = {}) {
1100
1142
  name: 'solid:server-functions/compiler',
1101
1143
  enforce: 'pre',
1102
1144
  async transform(code, fileId, opts) {
1103
- const mode = opts?.ssr ? 'server' : 'client';
1145
+ const mode = getEnvironmentConsumer(this.environment, opts);
1104
1146
  const [id] = fileId.split('?');
1105
1147
  if (!filter(id)) {
1106
1148
  return null;
@@ -1133,7 +1175,17 @@ function serverFunctions(options = {}, internal = {}) {
1133
1175
  }
1134
1176
  return null;
1135
1177
  }
1136
- }, ...turnkeyPlugins];
1178
+ }, ...startPlugins];
1179
+ }
1180
+
1181
+ const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
1182
+ const DEVTOOLS_ID = 'virtual:solid-devtools';
1183
+ const DEVTOOLS_MOUNT_ID = 'virtual:solid-devtools/mount';
1184
+ function devtoolsModuleCode() {
1185
+ 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');
1186
+ }
1187
+ function devtoolsMountModuleCode() {
1188
+ return [`import ${JSON.stringify(DEVTOOLS_ID)};`, `import { mountDevToolbar } from '${DEVTOOLS_PACKAGE}';`, `mountDevToolbar();`].join('\n');
1137
1189
  }
1138
1190
 
1139
1191
  // Start-mode serving for plain Vite apps: `solid({ start: {...} })` (or the
@@ -1218,6 +1270,7 @@ const RESOLVED_DEV_STYLES_ID = '\0' + DEV_STYLES_ID;
1218
1270
  const ENTRY_SERVER_ID = 'virtual:solid-ssr-entry-server.tsx';
1219
1271
  const ENTRY_CLIENT_ID = 'virtual:solid-ssr-entry-client.tsx';
1220
1272
  const DOCUMENT_ID = 'virtual:solid-ssr-document.tsx';
1273
+ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1221
1274
  const MANIFEST_ID = 'virtual:solid-manifest';
1222
1275
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1223
1276
  const STORAGE_SOURCE = '@solidjs/web/storage';
@@ -1329,6 +1382,12 @@ function startServe(options, internal = {}) {
1329
1382
  // the server-function handler module either way). Everything is gated
1330
1383
  // codegen: with the option off, none of these imports exist anywhere.
1331
1384
  const serverComponents = !!internal.serverComponents;
1385
+ const errorBoundary = options.errorBoundary !== false;
1386
+ const styleFilter = internal.styleFilter;
1387
+ let devtools = false;
1388
+ let devtoolsResolution;
1389
+ /** Resolved module id of `@solidjs/start-devtools` once detection succeeds. */
1390
+ let devtoolsId = null;
1332
1391
  // `external` is server-mode-only (documented no-op in client mode, so a
1333
1392
  // host-integrated config survives the `ssr` boolean flip untouched).
1334
1393
  const externalServer = !clientMode && !!options.external;
@@ -1345,6 +1404,40 @@ function startServe(options, internal = {}) {
1345
1404
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1346
1405
  return entries;
1347
1406
  }
1407
+ async function resolveDevtools(resolve, importer) {
1408
+ if (devtools !== undefined) return devtools;
1409
+ // Detect from the app graph first (the documented install location), then
1410
+ // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1411
+ // dependency of the plugin is not reachable from the app's importers. The
1412
+ // resolved id is kept so the virtual modules' imports of the package can
1413
+ // be delegated to it (see resolveId).
1414
+ devtoolsResolution ??= (async () => ((await resolve(DEVTOOLS_PACKAGE, importer)) ?? (await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url))))?.id ?? null)();
1415
+ devtoolsId = await devtoolsResolution;
1416
+ devtools = devtoolsId !== null;
1417
+ if (!devtools && options.devtools === true) {
1418
+ throw new Error('[@solidjs/vite-plugin] start.devtools requires @solidjs/start-devtools. ' + 'Install it as a development dependency or set start.devtools to false.');
1419
+ }
1420
+ return devtools;
1421
+ }
1422
+
1423
+ /**
1424
+ * Cheap root-walk probe mirroring how the optimizer resolves bare
1425
+ * `optimizeDeps.include` entries: is @solidjs/start-devtools reachable from
1426
+ * the Vite root? Detection proper (resolveDevtools) runs later with a real
1427
+ * importer; this only decides whether the toolbar graph can be pre-bundled
1428
+ * at scan time — it hangs off virtual modules the scanner never sees, so
1429
+ * first-request discovery would force a re-optimize + full page reload.
1430
+ */
1431
+ function devtoolsReachableFromRoot(dir) {
1432
+ for (let current = dir;;) {
1433
+ if (existsSync(path.join(current, 'node_modules', DEVTOOLS_PACKAGE, 'package.json'))) {
1434
+ return true;
1435
+ }
1436
+ const parent = path.dirname(current);
1437
+ if (parent === current) return false;
1438
+ current = parent;
1439
+ }
1440
+ }
1348
1441
 
1349
1442
  /** Import specifier for generated code: absolute for files, id for virtuals. */
1350
1443
  function entryServerSpec() {
@@ -1384,7 +1477,7 @@ function startServe(options, internal = {}) {
1384
1477
  return generated ? [app, ...(document ? [document] : [])] : [path.resolve(root, entryServer)];
1385
1478
  }
1386
1479
  async function devStylesModuleCode(environment, watchFile) {
1387
- const styles = await collectDevStyleSources(environment, styleRoots(), watchFile);
1480
+ const styles = await collectDevStyleSources(environment, styleRoots(), watchFile, styleFilter);
1388
1481
  if (!styles.length) return `export default '';`;
1389
1482
  const imports = styles.map((style, index) => {
1390
1483
  const specifier = style.url.includes('?') ? `${style.url}&inline` : `${style.url}?inline`;
@@ -1392,19 +1485,26 @@ function startServe(options, internal = {}) {
1392
1485
  });
1393
1486
  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');
1394
1487
  }
1488
+ function errorBoundaryImport() {
1489
+ return isBuild && errorBoundary ? [`import { DefaultErrorBoundary } from ${JSON.stringify(ERROR_BOUNDARY_ID)};`] : [];
1490
+ }
1491
+ function documentTree(root, wrapper) {
1492
+ const content = wrapper ? `<${wrapper}><${root} /></${wrapper}>` : `<${root} />`;
1493
+ return isBuild && errorBoundary ? [` <DefaultErrorBoundary>`, ` <Document>`, ` <DefaultErrorBoundary>`, ` ${content}`, ` </DefaultErrorBoundary>`, ` </Document>`, ` </DefaultErrorBoundary>`] : [` <Document>`, ` ${content}`, ` </Document>`];
1494
+ }
1395
1495
  function generatedEntryServerCode() {
1396
1496
  if (clientMode) {
1397
1497
  // The client-mode shell: the document without the app. Rendered per
1398
1498
  // request in dev (any HTML GET gets it — history-fallback semantics)
1399
1499
  // and once at build time into dist/client/index.html. The client
1400
1500
  // entry script is injected by the handler, exactly like SSR mode.
1401
- 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');
1501
+ 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');
1402
1502
  }
1403
1503
  const {
1404
1504
  app
1405
1505
  } = requireEntries();
1406
1506
  const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;
1407
- 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 ? [
1507
+ 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 ? [
1408
1508
  // Direct (in-process) server-function calls made during document
1409
1509
  // SSR must resolve to inline-renderable components; the endpoint
1410
1510
  // response transform is installed separately by the
@@ -1418,9 +1518,9 @@ function startServe(options, internal = {}) {
1418
1518
  // the *complete* render) and buffers the stream — so it crosses
1419
1519
  // boxed under a private key the generated handler unboxes
1420
1520
  // (both modules are ours).
1421
- `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');
1521
+ `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');
1422
1522
  }
1423
- function generatedEntryClientCode() {
1523
+ function generatedEntryClientCode(toolbar) {
1424
1524
  const {
1425
1525
  app
1426
1526
  } = requireEntries();
@@ -1430,13 +1530,13 @@ function startServe(options, internal = {}) {
1430
1530
  // app cannot claim server DOM anyway. The entry script is injected
1431
1531
  // without `async` (plain module = deferred), so document.body is
1432
1532
  // complete when this runs.
1433
- return [`import { render } from '@solidjs/web';`, `import App from ${JSON.stringify(app)};`, ``, `render(() => <App />, document.body);`].join('\n');
1533
+ 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');
1434
1534
  }
1435
- 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 ? [
1535
+ 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 ? [
1436
1536
  // Installs the t=0 document-adoption registry and the transport
1437
1537
  // policy (component responses morph their boundary instead of
1438
1538
  // decoding as data). Must run before hydrate().
1439
- `installServerComponents();`, ``] : []), `hydrate(() => (`, ` <Document>`, ` <App />`, ` </Document>`, `), document);`].join('\n');
1539
+ `installServerComponents();`, ``] : []), `hydrate(() => (`, ...documentTree('App', toolbar ? 'DevToolbar' : undefined), `), document);`].join('\n');
1440
1540
  }
1441
1541
 
1442
1542
  // Built-in document shell: minimal, hydration-ready. The client entry
@@ -1447,6 +1547,7 @@ function startServe(options, internal = {}) {
1447
1547
  // HydrationScript is covered too: the handler strips the event-capture
1448
1548
  // script from the client-mode shell.)
1449
1549
  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');
1550
+ 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');
1450
1551
 
1451
1552
  // The handler module: dev and prod share the render/response plumbing;
1452
1553
  // they differ in how the client entry URL is known (baked dev URL vs a
@@ -1498,7 +1599,7 @@ function startServe(options, internal = {}) {
1498
1599
  // head-open splice actively broke hydration — a script ahead of the
1499
1600
  // authored <head> elements claims as the first walked child and drifts
1500
1601
  // every positional claim after it.
1501
- lines.push(``, `function createHtmlChunkTransform(clientEntry, extraHead) {`, ` let first = true;`, ` let injected = false;`, ` return (chunk) => {`);
1602
+ 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) => {`);
1502
1603
  if (!generated) {
1503
1604
  // Authored entries reference the client entry by its dev path (the
1504
1605
  // `<script src="/src/entry-client.tsx">` convention); rewrite it to
@@ -1528,7 +1629,7 @@ function startServe(options, internal = {}) {
1528
1629
  // scripts default to deferred execution, which is exactly right for a
1529
1630
  // fresh render-into-body mount (hydration, by contrast, wants to
1530
1631
  // start as early as possible).
1531
- headParts.push(`(clientEntry ? '<script type="module" src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1632
+ headParts.push(`(clientEntry ? '<script type="module"' + nonceAttr + ' src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1532
1633
  }
1533
1634
  if (headParts.length) {
1534
1635
  lines.push(` chunk = chunk.replace('</head>', ${headParts.join(' + ')} + '</head>');`);
@@ -1582,7 +1683,7 @@ function startServe(options, internal = {}) {
1582
1683
  // The runtime's response-head lifecycle: commit at shell flush,
1583
1684
  // pre-flush Location as a real redirect, post-flush Location as the
1584
1685
  // script fallback; the transform injects the doctype/head pieces.
1585
- ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1686
+ ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, options.nonce),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`,
1586
1687
  // `options.event` is the public wrapper->event extension seam: extra
1587
1688
  // fields (conventionally `nativeEvent`, the platform's raw request
1588
1689
  // object) spread over the event's defaults at creation, so hosts and
@@ -1612,6 +1713,9 @@ function startServe(options, internal = {}) {
1612
1713
  enforce: 'pre',
1613
1714
  config(userConfig, env) {
1614
1715
  root = path.resolve(userConfig.root || process.cwd());
1716
+ devtools = env.command === 'serve' && !env.isPreview && options.devtools !== false ? undefined : false;
1717
+ devtoolsResolution = undefined;
1718
+ devtoolsId = null;
1615
1719
  entries = resolveEntries(root, options, clientMode);
1616
1720
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
1617
1721
  // Server-mode only, like `entryServer`/`external` (a documented
@@ -1730,7 +1834,15 @@ function startServe(options, internal = {}) {
1730
1834
  }
1731
1835
  } : {}),
1732
1836
  optimizeDeps: {
1733
- entries: scanEntries
1837
+ entries: scanEntries,
1838
+ // Like the refresh runtime in the main plugin: the toolbar
1839
+ // graph is injected behind virtual modules the scanner never
1840
+ // crawls, so pre-bundle it (and the server-functions runtime
1841
+ // the virtual module pulls in) up front — first-request
1842
+ // discovery would re-optimize and full-reload the page.
1843
+ ...(devtools === undefined && devtoolsReachableFromRoot(root) ? {
1844
+ include: [DEVTOOLS_PACKAGE, '@solidjs/web/server-functions']
1845
+ } : {})
1734
1846
  }
1735
1847
  })
1736
1848
  };
@@ -1740,7 +1852,7 @@ function startServe(options, internal = {}) {
1740
1852
  base = config.base;
1741
1853
  isBuild = config.command === 'build';
1742
1854
  },
1743
- resolveId(source) {
1855
+ resolveId(source, importer) {
1744
1856
  if (source === HANDLER_ID) {
1745
1857
  return {
1746
1858
  id: HANDLER_ID,
@@ -1753,33 +1865,94 @@ function startServe(options, internal = {}) {
1753
1865
  moduleSideEffects: true
1754
1866
  };
1755
1867
  }
1756
- if (source === ENTRY_SERVER_ID || source === ENTRY_CLIENT_ID || source === DOCUMENT_ID) {
1868
+ if (source === ENTRY_SERVER_ID || source === ENTRY_CLIENT_ID || source === DOCUMENT_ID || source === ERROR_BOUNDARY_ID) {
1757
1869
  return {
1758
1870
  id: source,
1759
1871
  moduleSideEffects: source === ENTRY_CLIENT_ID
1760
1872
  };
1761
1873
  }
1874
+ if (devtools && (source === DEVTOOLS_ID || source === DEVTOOLS_MOUNT_ID)) {
1875
+ return {
1876
+ id: source,
1877
+ moduleSideEffects: true
1878
+ };
1879
+ }
1880
+ // The virtual devtools modules import the package by its bare name,
1881
+ // but a virtual importer gives Vite no directory to walk, so the
1882
+ // specifier would only resolve from the Vite root — which fails in
1883
+ // pnpm-isolated apps where the package is not a root-level install.
1884
+ // Delegate to the resolution captured at detection time instead.
1885
+ if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === DEVTOOLS_ID || importer === DEVTOOLS_MOUNT_ID)) {
1886
+ return {
1887
+ id: devtoolsId
1888
+ };
1889
+ }
1762
1890
  return null;
1763
1891
  },
1764
1892
  async load(id, opts) {
1893
+ const consumer = getEnvironmentConsumer(this.environment, opts);
1765
1894
  if (id === HANDLER_ID) {
1766
- if (!opts?.ssr) {
1895
+ if (consumer !== 'server') {
1767
1896
  this.error(`${HANDLER_ID} is server-only; import it from server code (SSR build).`);
1768
1897
  }
1769
1898
  const externalDev = !isBuild && this.environment.mode === 'dev' && (externalServer || !isRunnableEnvironment(this.environment));
1770
1899
  return handlerModuleCode(externalDev);
1771
1900
  }
1772
1901
  if (id === RESOLVED_DEV_STYLES_ID) {
1773
- if (!opts?.ssr || this.environment.mode !== 'dev') {
1902
+ if (consumer !== 'server' || this.environment.mode !== 'dev') {
1774
1903
  this.error(`${DEV_STYLES_ID} is only available to the development server handler.`);
1775
1904
  }
1776
1905
  return devStylesModuleCode(this.environment, file => this.addWatchFile(file));
1777
1906
  }
1778
1907
  if (id === ENTRY_SERVER_ID) return generatedEntryServerCode();
1779
- if (id === ENTRY_CLIENT_ID) return generatedEntryClientCode();
1908
+ if (id === ENTRY_CLIENT_ID) {
1909
+ const toolbar = await resolveDevtools((source, importer) => this.resolve(source, importer, {
1910
+ skipSelf: true
1911
+ }), requireEntries().app);
1912
+ return generatedEntryClientCode(toolbar);
1913
+ }
1780
1914
  if (id === DOCUMENT_ID) return documentShellCode;
1915
+ if (id === ERROR_BOUNDARY_ID) return errorBoundaryCode;
1916
+ if (id === DEVTOOLS_ID || id === DEVTOOLS_MOUNT_ID) {
1917
+ // A cold direct request (stale tab reload) can reach the virtual
1918
+ // module before the entry has triggered detection — run it here so
1919
+ // first-touch order doesn't matter.
1920
+ if (!isBuild && consumer === 'client' && devtools === undefined) {
1921
+ const {
1922
+ app,
1923
+ entryClient
1924
+ } = requireEntries();
1925
+ await resolveDevtools((source, importer) => this.resolve(source, importer, {
1926
+ skipSelf: true
1927
+ }), app ?? path.resolve(root, entryClient));
1928
+ }
1929
+ if (isBuild || !devtools || consumer !== 'client') {
1930
+ this.error(`${id} is only available to the development client.`);
1931
+ }
1932
+ return id === DEVTOOLS_ID ? devtoolsModuleCode() : devtoolsMountModuleCode();
1933
+ }
1781
1934
  return null;
1782
1935
  },
1936
+ async transform(code, id, opts) {
1937
+ if (isBuild || devtools === false) return null;
1938
+ const current = requireEntries();
1939
+ if (current.generated || getEnvironmentConsumer(this.environment, opts) !== 'client') {
1940
+ return null;
1941
+ }
1942
+ // Module ids are always forward-slashed; normalize the path.resolve
1943
+ // side too so the comparison holds on Windows.
1944
+ if (normalizePath(id.split('?')[0]) !== normalizePath(path.resolve(root, current.entryClient))) {
1945
+ return null;
1946
+ }
1947
+ const toolbar = await resolveDevtools((source, importer) => this.resolve(source, importer, {
1948
+ skipSelf: true
1949
+ }), id);
1950
+ if (!toolbar) return null;
1951
+ return {
1952
+ code: `import ${JSON.stringify(DEVTOOLS_MOUNT_ID)};\n${code}`,
1953
+ map: null
1954
+ };
1955
+ },
1783
1956
  configurePreviewServer(server) {
1784
1957
  // `vite build && vite preview` runs the production artifact as-is:
1785
1958
  // Vite's preview statics serve dist/client (see the config hook) and
@@ -1804,7 +1977,7 @@ function startServe(options, internal = {}) {
1804
1977
  // server-function endpoint) and hands the URL to application
1805
1978
  // code, so restore the base — the deployed production handler
1806
1979
  // receives base-prefixed URLs and preview must match it.
1807
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')),
1980
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res),
1808
1981
  // Same event extension the dev middleware and a production
1809
1982
  // Node entry pass: the raw Node request as `nativeEvent`.
1810
1983
  {
@@ -1855,13 +2028,13 @@ function startServe(options, internal = {}) {
1855
2028
  // Loaded through the SSR environment so the app, the request
1856
2029
  // event storage, and the handler share one module registry.
1857
2030
  const handler = await server.ssrLoadModule(HANDLER_ID);
1858
- const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : [];
2031
+ const styles = pageRequest ? await collectDevStyles(server, styleRoots(), styleFilter) : [];
1859
2032
  const devHead = styles.map(renderDevStyleTag).join('');
1860
2033
  // Post middlewares run after Vite's base middleware stripped
1861
2034
  // the configured `base` from req.url; restore it so the app
1862
2035
  // sees the same URLs in dev as in production (where the
1863
2036
  // deployed handler receives base-prefixed requests).
1864
- const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')), {
2037
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/'), res), {
1865
2038
  devHead,
1866
2039
  pageRequest,
1867
2040
  // The raw Node request on the event, matching what a
@@ -2544,6 +2717,7 @@ const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:';
2544
2717
  const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
2545
2718
  const viteVersionMajor = +version.split('.')[0];
2546
2719
  const isVite8 = viteVersionMajor >= 8;
2720
+ const DEFAULT_STYLE_EXCLUDE = /node_modules/;
2547
2721
  const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
2548
2722
  const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
2549
2723
 
@@ -2798,10 +2972,30 @@ function solidPlugin(options = {}) {
2798
2972
  // `start: true` is sugar for the empty options bag — one start mode,
2799
2973
  // two spellings — so normalize here and let everything downstream see a
2800
2974
  // single shape (`false` behaves exactly like omission).
2801
- const turnkey = options.start === true ? {} : options.start || null;
2975
+ const startOptions = options.start === true ? {} : options.start || null;
2976
+ const styleFilterOptions = startOptions?.css?.filter;
2977
+ // The CSS crawl walks the module graph from the app's own entries, so a
2978
+ // plain createFilter allowlist can't express the option's purpose (opting
2979
+ // node_modules graphs in): a bare `include` would reject the app sources
2980
+ // the crawl has to traverse to ever reach the included package. Instead
2981
+ // `include` rescues files on top of the baseline (everything except
2982
+ // `exclude`, which defaults to node_modules), while a file matching both
2983
+ // patterns stays excluded — createFilter's own conflict rule.
2984
+ const createStyleFilter = resolve => {
2985
+ const opts = resolve === undefined ? undefined : {
2986
+ resolve
2987
+ };
2988
+ const base = createFilter(undefined, styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE, opts);
2989
+ const include = styleFilterOptions?.include;
2990
+ const hasInclude = include != null && (!Array.isArray(include) || include.length > 0);
2991
+ const included = hasInclude ? createFilter(include, styleFilterOptions?.exclude, opts) : null;
2992
+ return id => base(id) || (included ? included(id) : false);
2993
+ };
2994
+ let styleFilter = createStyleFilter();
2995
+ const filterDevStyles = id => styleFilter(id);
2802
2996
  // `start.external` only means something when a server side exists to hand
2803
2997
  // over (SSR start mode); in client mode it is a documented no-op.
2804
- const externalDevServer = !!options.ssr && !!turnkey?.external;
2998
+ const externalDevServer = !!options.ssr && !!startOptions?.external;
2805
2999
  let needHmr = false;
2806
3000
  let replaceDev = false;
2807
3001
  // The live dev server, kept so the dev manifest module can bake the bridge
@@ -3079,6 +3273,7 @@ function solidPlugin(options = {}) {
3079
3273
  filter = createFilter(options.include, options.exclude, {
3080
3274
  resolve: projectRoot
3081
3275
  });
3276
+ styleFilter = createStyleFilter(projectRoot);
3082
3277
  if (serverComponents && !(options.start && options.ssr)) {
3083
3278
  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.');
3084
3279
  }
@@ -3092,7 +3287,7 @@ function solidPlugin(options = {}) {
3092
3287
  // that don't share globals with this process, through the HTTP bridge
3093
3288
  // endpoint the middleware serves.
3094
3289
  if (options.ssr || options.start) {
3095
- registerDevAssetResolver(server.config.root, createDevAssetResolver(server));
3290
+ registerDevAssetResolver(server.config.root, createDevAssetResolver(server, filterDevStyles));
3096
3291
  installDevManifestBridge(server);
3097
3292
  }
3098
3293
  if (!needHmr) return;
@@ -3208,7 +3403,7 @@ function solidPlugin(options = {}) {
3208
3403
  }
3209
3404
  },
3210
3405
  async transform(source, id, transformOptions) {
3211
- const isSsr = transformOptions && transformOptions.ssr;
3406
+ const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3212
3407
  const currentFileExtension = getExtension(id);
3213
3408
  const extensionsToWatch = options.extensions || [];
3214
3409
  const allExtensions = extensionsToWatch.map(extension =>
@@ -3348,7 +3543,7 @@ function solidPlugin(options = {}) {
3348
3543
  // With start mode on (either variant), the dev middleware dispatches
3349
3544
  // the endpoint through the SSR handler so user middleware and the
3350
3545
  // stub-backed request event front it exactly like page SSR.
3351
- ...(turnkey ? {
3546
+ ...(startOptions ? {
3352
3547
  ssrHandler: SSR_HANDLER_ID
3353
3548
  } : {})
3354
3549
  }), mainPlugin] : [boundaryModules(), mainPlugin];
@@ -3356,15 +3551,16 @@ function solidPlugin(options = {}) {
3356
3551
  // The `start` option opts into start-mode serving on top of the transforms;
3357
3552
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
3358
3553
  // historical transform-only behavior).
3359
- if (turnkey) {
3554
+ if (startOptions) {
3360
3555
  plugins.push(
3361
3556
  // Typed env (`start.env`) rides both start modes: config-time
3362
3557
  // validation, the virtual:env/{server,client} modules, generated
3363
3558
  // types, and the client-bundle leak scan.
3364
- ...startEnv(turnkey.env), ...startServe(turnkey, {
3559
+ ...startEnv(startOptions.env), ...startServe(startOptions, {
3365
3560
  serverFunctions: !!options.serverFunctions,
3366
3561
  serverComponents,
3367
- ssr: !!options.ssr
3562
+ ssr: !!options.ssr,
3563
+ styleFilter: filterDevStyles
3368
3564
  }));
3369
3565
  }
3370
3566