@solidjs/vite-plugin 3.0.0-next.30 → 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.
@@ -177,6 +177,7 @@ function joinBase(base, pathname) {
177
177
  * dynamically imported modules register their own styles when they render.
178
178
  */
179
179
 
180
+ const defaultStyleFilter = id => !id.includes('node_modules');
180
181
  // The resolver is created plugin-side (it closes over the dev server) but is
181
182
  // called from the SSR module runner, which only shares `globalThis` with the
182
183
  // plugin when it runs in-process (the default). The primary channel is a
@@ -320,13 +321,15 @@ async function getModuleNode(env, file, importer) {
320
321
  return;
321
322
  }
322
323
  }
323
- async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
324
+ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, importer) {
324
325
  crawled.add(file);
325
326
  const node = await getModuleNode(env, file, importer);
326
327
  if (!node?.id || deps.has(node)) return;
327
328
  deps.add(node);
328
- if (node.file && !node.id.includes('node_modules')) onFile?.(node.file);
329
- 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;
330
333
  if (!node.transformResult) {
331
334
  await env.transformRequest(node.url).catch(() => {});
332
335
  }
@@ -337,7 +340,7 @@ async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
337
340
  // from dynamicDeps — dynamic imports load their own styles when rendered.
338
341
  for (const dep of directDeps) {
339
342
  if (crawled.has(dep)) continue;
340
- await collectModuleDeps(env, dep, deps, crawled, onFile, node.id);
343
+ await collectModuleDeps(env, dep, deps, crawled, filter, onFile, node.id);
341
344
  }
342
345
  }
343
346
  function injectQuery(url, query) {
@@ -345,11 +348,11 @@ function injectQuery(url, query) {
345
348
  }
346
349
 
347
350
  /** Discovers ambient CSS in an entry graph without choosing how it is transported. */
348
- async function collectDevStyleSources(env, files, onFile) {
351
+ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleFilter) {
349
352
  const deps = new Set();
350
353
  const crawled = new Set();
351
354
  for (const file of files) {
352
- await collectModuleDeps(env, file, deps, crawled, onFile);
355
+ await collectModuleDeps(env, file, deps, crawled, filter, onFile);
353
356
  }
354
357
  const css = [];
355
358
  const seen = new Set();
@@ -376,11 +379,11 @@ async function collectDevStyleSources(env, files, onFile) {
376
379
  * CSS into `<head>` so server-painted content is styled from the first byte
377
380
  * (no FOUC while waiting for Vite's client-side style injection).
378
381
  */
379
- async function collectDevStyles(server, files) {
382
+ async function collectDevStyles(server, files, filter = defaultStyleFilter) {
380
383
  const ssrEnv = server.environments?.ssr;
381
384
  const clientEnv = server.environments?.client;
382
385
  if (!ssrEnv || !clientEnv) return [];
383
- 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);
384
387
  const css = [];
385
388
  for (const source of sources) {
386
389
  // `?direct` yields the compiled stylesheet text (what Vite serves for
@@ -436,7 +439,7 @@ function devModuleUrl(root, base, key) {
436
439
  // drive letter on Windows: /@fs/C:/…).
437
440
  return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query);
438
441
  }
439
- function createDevAssetResolver(server) {
442
+ function createDevAssetResolver(server, filter = defaultStyleFilter) {
440
443
  // Server-side lazy() re-requests a module's assets on every retry of a
441
444
  // suspended render pass (retries re-create the component). The build
442
445
  // manifest answers those repeats synchronously and the pass converges; an
@@ -470,7 +473,7 @@ function createDevAssetResolver(server) {
470
473
  // The module's dev URL doubles as its client entry: modulepreload
471
474
  // hint and hydration module-map value.
472
475
  const js = [devModuleUrl(root, base, key)];
473
- const css = await collectDevStyles(server, [key]);
476
+ const css = await collectDevStyles(server, [key], filter);
474
477
  return {
475
478
  js,
476
479
  css
@@ -563,6 +566,11 @@ function boundaryModules() {
563
566
  function isRunnableEnvironment(environment) {
564
567
  return !!environment && typeof environment === 'object' && 'runner' in environment;
565
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
+ }
566
574
 
567
575
  // The `"use server"` directive compiler. This wraps the native
568
576
  // `transformDirectives` pass from @dom-expressions/compiler (Rust/Oxc); the
@@ -999,12 +1007,12 @@ function serverFunctions(options = {}, internal = {}) {
999
1007
  const relative = path.relative(root, entry).split(path.sep).join('/');
1000
1008
  return relative.startsWith('..') ? '/@fs/' + entry : '/' + relative;
1001
1009
  }
1002
- const turnkeyPlugins = [{
1010
+ const startPlugins = [{
1003
1011
  name: 'solid:server-functions/handler',
1004
1012
  enforce: 'pre',
1005
1013
  resolveId(source, _importer, opts) {
1006
1014
  if (source === HANDLER_ID$1) {
1007
- if (!opts?.ssr) {
1015
+ if (getEnvironmentConsumer(this.environment, opts) !== 'server') {
1008
1016
  this.error(`${HANDLER_ID$1} is server-only; import it from your server entry (SSR build).`);
1009
1017
  }
1010
1018
  return {
@@ -1015,7 +1023,7 @@ function serverFunctions(options = {}, internal = {}) {
1015
1023
  return null;
1016
1024
  },
1017
1025
  load(id, opts) {
1018
- if (id === HANDLER_ID$1 && opts?.ssr) {
1026
+ if (id === HANDLER_ID$1 && getEnvironmentConsumer(this.environment, opts) === 'server') {
1019
1027
  const externalDev = this.environment.mode === 'dev' && (internal.externalDevServer || !isRunnableEnvironment(this.environment));
1020
1028
  return handlerModuleCode(isBuild || externalDev);
1021
1029
  }
@@ -1023,7 +1031,7 @@ function serverFunctions(options = {}, internal = {}) {
1023
1031
  }
1024
1032
  }];
1025
1033
  if (installDevMiddleware) {
1026
- turnkeyPlugins.push({
1034
+ startPlugins.push({
1027
1035
  name: 'solid:server-functions/dev-middleware',
1028
1036
  apply: 'serve',
1029
1037
  configureServer(server) {
@@ -1135,7 +1143,7 @@ function serverFunctions(options = {}, internal = {}) {
1135
1143
  return null;
1136
1144
  },
1137
1145
  async load(id, opts) {
1138
- const mode = opts?.ssr ? 'server' : 'client';
1146
+ const mode = getEnvironmentConsumer(this.environment, opts);
1139
1147
  if (id === manifestId) {
1140
1148
  if (isBuild && mode === 'server') {
1141
1149
  // Merge the client build's persisted discoveries at load time,
@@ -1158,7 +1166,7 @@ function serverFunctions(options = {}, internal = {}) {
1158
1166
  name: 'solid:server-functions/compiler',
1159
1167
  enforce: 'pre',
1160
1168
  async transform(code, fileId, opts) {
1161
- const mode = opts?.ssr ? 'server' : 'client';
1169
+ const mode = getEnvironmentConsumer(this.environment, opts);
1162
1170
  const [id] = fileId.split('?');
1163
1171
  if (!filter(id)) {
1164
1172
  return null;
@@ -1191,7 +1199,17 @@ function serverFunctions(options = {}, internal = {}) {
1191
1199
  }
1192
1200
  return null;
1193
1201
  }
1194
- }, ...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');
1195
1213
  }
1196
1214
 
1197
1215
  // Start-mode serving for plain Vite apps: `solid({ start: {...} })` (or the
@@ -1276,6 +1294,7 @@ const RESOLVED_DEV_STYLES_ID = '\0' + DEV_STYLES_ID;
1276
1294
  const ENTRY_SERVER_ID = 'virtual:solid-ssr-entry-server.tsx';
1277
1295
  const ENTRY_CLIENT_ID = 'virtual:solid-ssr-entry-client.tsx';
1278
1296
  const DOCUMENT_ID = 'virtual:solid-ssr-document.tsx';
1297
+ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1279
1298
  const MANIFEST_ID = 'virtual:solid-manifest';
1280
1299
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1281
1300
  const STORAGE_SOURCE = '@solidjs/web/storage';
@@ -1387,6 +1406,12 @@ function startServe(options, internal = {}) {
1387
1406
  // the server-function handler module either way). Everything is gated
1388
1407
  // codegen: with the option off, none of these imports exist anywhere.
1389
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;
1390
1415
  // `external` is server-mode-only (documented no-op in client mode, so a
1391
1416
  // host-integrated config survives the `ssr` boolean flip untouched).
1392
1417
  const externalServer = !clientMode && !!options.external;
@@ -1403,6 +1428,40 @@ function startServe(options, internal = {}) {
1403
1428
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1404
1429
  return entries;
1405
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
+ }
1406
1465
 
1407
1466
  /** Import specifier for generated code: absolute for files, id for virtuals. */
1408
1467
  function entryServerSpec() {
@@ -1442,7 +1501,7 @@ function startServe(options, internal = {}) {
1442
1501
  return generated ? [app, ...(document ? [document] : [])] : [path.resolve(root, entryServer)];
1443
1502
  }
1444
1503
  async function devStylesModuleCode(environment, watchFile) {
1445
- const styles = await collectDevStyleSources(environment, styleRoots(), watchFile);
1504
+ const styles = await collectDevStyleSources(environment, styleRoots(), watchFile, styleFilter);
1446
1505
  if (!styles.length) return `export default '';`;
1447
1506
  const imports = styles.map((style, index) => {
1448
1507
  const specifier = style.url.includes('?') ? `${style.url}&inline` : `${style.url}?inline`;
@@ -1450,19 +1509,26 @@ function startServe(options, internal = {}) {
1450
1509
  });
1451
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');
1452
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
+ }
1453
1519
  function generatedEntryServerCode() {
1454
1520
  if (clientMode) {
1455
1521
  // The client-mode shell: the document without the app. Rendered per
1456
1522
  // request in dev (any HTML GET gets it — history-fallback semantics)
1457
1523
  // and once at build time into dist/client/index.html. The client
1458
1524
  // entry script is injected by the handler, exactly like SSR mode.
1459
- 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');
1460
1526
  }
1461
1527
  const {
1462
1528
  app
1463
1529
  } = requireEntries();
1464
1530
  const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;
1465
- 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 ? [
1466
1532
  // Direct (in-process) server-function calls made during document
1467
1533
  // SSR must resolve to inline-renderable components; the endpoint
1468
1534
  // response transform is installed separately by the
@@ -1476,9 +1542,9 @@ function startServe(options, internal = {}) {
1476
1542
  // the *complete* render) and buffers the stream — so it crosses
1477
1543
  // boxed under a private key the generated handler unboxes
1478
1544
  // (both modules are ours).
1479
- `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');
1480
1546
  }
1481
- function generatedEntryClientCode() {
1547
+ function generatedEntryClientCode(toolbar) {
1482
1548
  const {
1483
1549
  app
1484
1550
  } = requireEntries();
@@ -1488,13 +1554,13 @@ function startServe(options, internal = {}) {
1488
1554
  // app cannot claim server DOM anyway. The entry script is injected
1489
1555
  // without `async` (plain module = deferred), so document.body is
1490
1556
  // complete when this runs.
1491
- 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');
1492
1558
  }
1493
- 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 ? [
1494
1560
  // Installs the t=0 document-adoption registry and the transport
1495
1561
  // policy (component responses morph their boundary instead of
1496
1562
  // decoding as data). Must run before hydrate().
1497
- `installServerComponents();`, ``] : []), `hydrate(() => (`, ` <Document>`, ` <App />`, ` </Document>`, `), document);`].join('\n');
1563
+ `installServerComponents();`, ``] : []), `hydrate(() => (`, ...documentTree('App', toolbar ? 'DevToolbar' : undefined), `), document);`].join('\n');
1498
1564
  }
1499
1565
 
1500
1566
  // Built-in document shell: minimal, hydration-ready. The client entry
@@ -1505,6 +1571,7 @@ function startServe(options, internal = {}) {
1505
1571
  // HydrationScript is covered too: the handler strips the event-capture
1506
1572
  // script from the client-mode shell.)
1507
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');
1508
1575
 
1509
1576
  // The handler module: dev and prod share the render/response plumbing;
1510
1577
  // they differ in how the client entry URL is known (baked dev URL vs a
@@ -1556,7 +1623,7 @@ function startServe(options, internal = {}) {
1556
1623
  // head-open splice actively broke hydration — a script ahead of the
1557
1624
  // authored <head> elements claims as the first walked child and drifts
1558
1625
  // every positional claim after it.
1559
- 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) => {`);
1560
1627
  if (!generated) {
1561
1628
  // Authored entries reference the client entry by its dev path (the
1562
1629
  // `<script src="/src/entry-client.tsx">` convention); rewrite it to
@@ -1586,7 +1653,7 @@ function startServe(options, internal = {}) {
1586
1653
  // scripts default to deferred execution, which is exactly right for a
1587
1654
  // fresh render-into-body mount (hydration, by contrast, wants to
1588
1655
  // start as early as possible).
1589
- headParts.push(`(clientEntry ? '<script type="module" src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1656
+ headParts.push(`(clientEntry ? '<script type="module"' + nonceAttr + ' src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1590
1657
  }
1591
1658
  if (headParts.length) {
1592
1659
  lines.push(` chunk = chunk.replace('</head>', ${headParts.join(' + ')} + '</head>');`);
@@ -1640,7 +1707,7 @@ function startServe(options, internal = {}) {
1640
1707
  // The runtime's response-head lifecycle: commit at shell flush,
1641
1708
  // pre-flush Location as a real redirect, post-flush Location as the
1642
1709
  // script fallback; the transform injects the doctype/head pieces.
1643
- ` 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 = {}) {`,
1644
1711
  // `options.event` is the public wrapper->event extension seam: extra
1645
1712
  // fields (conventionally `nativeEvent`, the platform's raw request
1646
1713
  // object) spread over the event's defaults at creation, so hosts and
@@ -1670,6 +1737,9 @@ function startServe(options, internal = {}) {
1670
1737
  enforce: 'pre',
1671
1738
  config(userConfig, env) {
1672
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;
1673
1743
  entries = resolveEntries(root, options, clientMode);
1674
1744
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
1675
1745
  // Server-mode only, like `entryServer`/`external` (a documented
@@ -1788,7 +1858,15 @@ function startServe(options, internal = {}) {
1788
1858
  }
1789
1859
  } : {}),
1790
1860
  optimizeDeps: {
1791
- 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
+ } : {})
1792
1870
  }
1793
1871
  })
1794
1872
  };
@@ -1798,7 +1876,7 @@ function startServe(options, internal = {}) {
1798
1876
  base = config.base;
1799
1877
  isBuild = config.command === 'build';
1800
1878
  },
1801
- resolveId(source) {
1879
+ resolveId(source, importer) {
1802
1880
  if (source === HANDLER_ID) {
1803
1881
  return {
1804
1882
  id: HANDLER_ID,
@@ -1811,33 +1889,94 @@ function startServe(options, internal = {}) {
1811
1889
  moduleSideEffects: true
1812
1890
  };
1813
1891
  }
1814
- 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) {
1815
1893
  return {
1816
1894
  id: source,
1817
1895
  moduleSideEffects: source === ENTRY_CLIENT_ID
1818
1896
  };
1819
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
+ }
1820
1914
  return null;
1821
1915
  },
1822
1916
  async load(id, opts) {
1917
+ const consumer = getEnvironmentConsumer(this.environment, opts);
1823
1918
  if (id === HANDLER_ID) {
1824
- if (!opts?.ssr) {
1919
+ if (consumer !== 'server') {
1825
1920
  this.error(`${HANDLER_ID} is server-only; import it from server code (SSR build).`);
1826
1921
  }
1827
1922
  const externalDev = !isBuild && this.environment.mode === 'dev' && (externalServer || !isRunnableEnvironment(this.environment));
1828
1923
  return handlerModuleCode(externalDev);
1829
1924
  }
1830
1925
  if (id === RESOLVED_DEV_STYLES_ID) {
1831
- if (!opts?.ssr || this.environment.mode !== 'dev') {
1926
+ if (consumer !== 'server' || this.environment.mode !== 'dev') {
1832
1927
  this.error(`${DEV_STYLES_ID} is only available to the development server handler.`);
1833
1928
  }
1834
1929
  return devStylesModuleCode(this.environment, file => this.addWatchFile(file));
1835
1930
  }
1836
1931
  if (id === ENTRY_SERVER_ID) return generatedEntryServerCode();
1837
- 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
+ }
1838
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
+ }
1839
1958
  return null;
1840
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
+ },
1841
1980
  configurePreviewServer(server) {
1842
1981
  // `vite build && vite preview` runs the production artifact as-is:
1843
1982
  // Vite's preview statics serve dist/client (see the config hook) and
@@ -1913,7 +2052,7 @@ function startServe(options, internal = {}) {
1913
2052
  // Loaded through the SSR environment so the app, the request
1914
2053
  // event storage, and the handler share one module registry.
1915
2054
  const handler = await server.ssrLoadModule(HANDLER_ID);
1916
- const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : [];
2055
+ const styles = pageRequest ? await collectDevStyles(server, styleRoots(), styleFilter) : [];
1917
2056
  const devHead = styles.map(renderDevStyleTag).join('');
1918
2057
  // Post middlewares run after Vite's base middleware stripped
1919
2058
  // the configured `base` from req.url; restore it so the app
@@ -2602,6 +2741,7 @@ const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:';
2602
2741
  const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
2603
2742
  const viteVersionMajor = +vite.version.split('.')[0];
2604
2743
  const isVite8 = viteVersionMajor >= 8;
2744
+ const DEFAULT_STYLE_EXCLUDE = /node_modules/;
2605
2745
  const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
2606
2746
  const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
2607
2747
 
@@ -2856,10 +2996,30 @@ function solidPlugin(options = {}) {
2856
2996
  // `start: true` is sugar for the empty options bag — one start mode,
2857
2997
  // two spellings — so normalize here and let everything downstream see a
2858
2998
  // single shape (`false` behaves exactly like omission).
2859
- 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);
2860
3020
  // `start.external` only means something when a server side exists to hand
2861
3021
  // over (SSR start mode); in client mode it is a documented no-op.
2862
- const externalDevServer = !!options.ssr && !!turnkey?.external;
3022
+ const externalDevServer = !!options.ssr && !!startOptions?.external;
2863
3023
  let needHmr = false;
2864
3024
  let replaceDev = false;
2865
3025
  // The live dev server, kept so the dev manifest module can bake the bridge
@@ -3137,6 +3297,7 @@ function solidPlugin(options = {}) {
3137
3297
  filter = vite.createFilter(options.include, options.exclude, {
3138
3298
  resolve: projectRoot
3139
3299
  });
3300
+ styleFilter = createStyleFilter(projectRoot);
3140
3301
  if (serverComponents && !(options.start && options.ssr)) {
3141
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.');
3142
3303
  }
@@ -3150,7 +3311,7 @@ function solidPlugin(options = {}) {
3150
3311
  // that don't share globals with this process, through the HTTP bridge
3151
3312
  // endpoint the middleware serves.
3152
3313
  if (options.ssr || options.start) {
3153
- registerDevAssetResolver(server.config.root, createDevAssetResolver(server));
3314
+ registerDevAssetResolver(server.config.root, createDevAssetResolver(server, filterDevStyles));
3154
3315
  installDevManifestBridge(server);
3155
3316
  }
3156
3317
  if (!needHmr) return;
@@ -3266,7 +3427,7 @@ function solidPlugin(options = {}) {
3266
3427
  }
3267
3428
  },
3268
3429
  async transform(source, id, transformOptions) {
3269
- const isSsr = transformOptions && transformOptions.ssr;
3430
+ const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3270
3431
  const currentFileExtension = getExtension(id);
3271
3432
  const extensionsToWatch = options.extensions || [];
3272
3433
  const allExtensions = extensionsToWatch.map(extension =>
@@ -3406,7 +3567,7 @@ function solidPlugin(options = {}) {
3406
3567
  // With start mode on (either variant), the dev middleware dispatches
3407
3568
  // the endpoint through the SSR handler so user middleware and the
3408
3569
  // stub-backed request event front it exactly like page SSR.
3409
- ...(turnkey ? {
3570
+ ...(startOptions ? {
3410
3571
  ssrHandler: SSR_HANDLER_ID
3411
3572
  } : {})
3412
3573
  }), mainPlugin] : [boundaryModules(), mainPlugin];
@@ -3414,15 +3575,16 @@ function solidPlugin(options = {}) {
3414
3575
  // The `start` option opts into start-mode serving on top of the transforms;
3415
3576
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
3416
3577
  // historical transform-only behavior).
3417
- if (turnkey) {
3578
+ if (startOptions) {
3418
3579
  plugins.push(
3419
3580
  // Typed env (`start.env`) rides both start modes: config-time
3420
3581
  // validation, the virtual:env/{server,client} modules, generated
3421
3582
  // types, and the client-bundle leak scan.
3422
- ...startEnv(turnkey.env), ...startServe(turnkey, {
3583
+ ...startEnv(startOptions.env), ...startServe(startOptions, {
3423
3584
  serverFunctions: !!options.serverFunctions,
3424
3585
  serverComponents,
3425
- ssr: !!options.ssr
3586
+ ssr: !!options.ssr,
3587
+ styleFilter: filterDevStyles
3426
3588
  }));
3427
3589
  }
3428
3590