@solidjs/vite-plugin 3.0.0-next.38 → 3.0.0-next.40

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.
@@ -10,6 +10,7 @@ var mergeAnything = require('merge-anything');
10
10
  var module$1 = require('module');
11
11
  var path = require('path');
12
12
  var node_stream = require('node:stream');
13
+ var crypto = require('crypto');
13
14
  var vite = require('vite');
14
15
  var node_url = require('node:url');
15
16
  var vitefu = require('vitefu');
@@ -757,6 +758,22 @@ function solidDiagnostics(mode = 'auto') {
757
758
  apply(_config, env) {
758
759
  return env.command === 'serve' && !env.isPreview && env.mode !== 'test';
759
760
  },
761
+ // The bridge module is virtual and reaches the page behind the
762
+ // scanner's back (a script injected into index.html at transform time,
763
+ // or an import the generated start entry adds), so the optimizer never
764
+ // sees its two package imports up front. Pre-bundle them: otherwise the
765
+ // first page load discovers them, re-optimizes and full-reloads — a
766
+ // flash at best, and a broken page whenever anything else on the page
767
+ // was resolved against the first optimizer pass.
768
+ config(userConfig) {
769
+ const rootDir = path.resolve(userConfig.root || process.cwd());
770
+ if (mode !== true && !detectDiagnosticsPackage(rootDir)) return;
771
+ return {
772
+ optimizeDeps: {
773
+ include: [`${DIAGNOSTICS_PACKAGE}/browser`, `${DIAGNOSTICS_PACKAGE}/protocol`]
774
+ }
775
+ };
776
+ },
760
777
  configResolved(config) {
761
778
  root = config.root;
762
779
  base = config.base;
@@ -1102,29 +1119,50 @@ const HANDLER_ID$1 = 'virtual:solid-server-function-handler';
1102
1119
  // (`vite build` then `vite build --ssr`) does not, so the client build
1103
1120
  // persists its findings for the SSR build to merge (mirroring the plugin's
1104
1121
  // dist/client/.vite/manifest.json convention).
1122
+ //
1123
+ // The file doubles as the build's statement of which server functions the
1124
+ // CLIENT can reach — every reference the client compile emitted, by wire id
1125
+ // — for build tooling that needs that set without re-deriving it from
1126
+ // compiled output (a static-site prerenderer checking that each reachable
1127
+ // function was captured, for example). Paths are root-relative, posix.
1105
1128
  const PERSISTED_MANIFEST_PATH = '.vite/solid-server-functions.json';
1129
+
1130
+ /** The persisted manifest's on-disk shape (the array form is the pre-`functions` legacy). */
1131
+
1106
1132
  function readPersistedManifest(root) {
1107
1133
  const file = path.resolve(root, 'dist/client', PERSISTED_MANIFEST_PATH);
1108
1134
  if (!fs.existsSync(file)) return new Set();
1109
1135
  try {
1110
- const entries = JSON.parse(fs.readFileSync(file, 'utf-8'));
1136
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
1137
+ const entries = Array.isArray(parsed) ? parsed : parsed.modules;
1111
1138
  return new Set(entries.map(entry => path.resolve(root, entry)).filter(entry => fs.existsSync(entry)));
1112
1139
  } catch {
1113
1140
  return new Set();
1114
1141
  }
1115
1142
  }
1116
- function writePersistedManifest(root, outDir, entries) {
1143
+ function writePersistedManifest(root, outDir, entries, functions) {
1117
1144
  const file = path.resolve(root, outDir, PERSISTED_MANIFEST_PATH);
1118
1145
  fs.mkdirSync(path.dirname(file), {
1119
1146
  recursive: true
1120
1147
  });
1121
- const relative = [...entries].map(entry => path.relative(root, entry).split(path.sep).join('/'));
1122
- fs.writeFileSync(file, JSON.stringify(relative, null, 2));
1148
+ const relative = entry => path.relative(root, entry).split(path.sep).join('/');
1149
+ const manifest = {
1150
+ modules: [...entries].map(relative),
1151
+ functions: [...functions].map(([id, record]) => ({
1152
+ id,
1153
+ name: record.name,
1154
+ module: relative(record.module)
1155
+ }))
1156
+ };
1157
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2));
1123
1158
  }
1124
1159
  function createManifest() {
1125
1160
  return {
1126
- server: new Set(),
1127
- client: new Set()
1161
+ modules: {
1162
+ server: new Set(),
1163
+ client: new Set()
1164
+ },
1165
+ clientFunctions: new Map()
1128
1166
  };
1129
1167
  }
1130
1168
  function createDeferredPromise() {
@@ -1232,6 +1270,23 @@ function serverFunctions(options = {}, internal = {}) {
1232
1270
  client: undefined
1233
1271
  };
1234
1272
  let currentServer;
1273
+
1274
+ // THE DEPLOYMENT SECRET (solidjs/solid#3239): the runtime's flash cookie
1275
+ // — the no-JS form outcome — carries the submitted input, so it is
1276
+ // AES-GCM encrypted under a key derived from a deployment-wide secret,
1277
+ // and without one the outcome is withheld entirely. This plugin provides
1278
+ // that secret with zero configuration through the internal
1279
+ // `globalThis.__SOLID_SECRET__ ??=` contract: generated once per plugin
1280
+ // instance, so a production build bakes one value into the emitted server
1281
+ // chunk — every instance of that deployment shares it (a per-process
1282
+ // value would silently lose flashes behind a load balancer) — and a dev
1283
+ // session holds one for its lifetime (a restart invalidates in-flight
1284
+ // flashes, which are 60-second one-shot cookies; the next render just
1285
+ // reads "no flash"). Server output only, never the client graph. The
1286
+ // `??=` keeps an explicit `configureServerFunctionsServer({ secret })` —
1287
+ // or a value injected by an outer harness — authoritative.
1288
+ const deploymentSecret = crypto.randomBytes(32).toString('hex');
1289
+ const deploymentSecretSnippet = `globalThis.__SOLID_SECRET__ ??= ${JSON.stringify(deploymentSecret)};`;
1235
1290
  const clientOptions = {
1236
1291
  directive,
1237
1292
  definitions: {
@@ -1291,6 +1346,14 @@ function serverFunctions(options = {}, internal = {}) {
1291
1346
  // import is only emitted when the option is on, so disabled setups keep
1292
1347
  // a server-component-free graph.
1293
1348
  return [
1349
+ // The deployment secret. Imports are hoisted above it, but nothing
1350
+ // reads the global at module evaluation — the runtime resolves it
1351
+ // lazily per encode/decode — so leading textually is just the honest
1352
+ // placement. This module is loaded before any dispatch on both
1353
+ // surfaces, and the generated SSR handler imports it at module load,
1354
+ // so the secret is in place for the flash's encode (the form POST)
1355
+ // and its decode (the render that follows the redirect) alike.
1356
+ deploymentSecretSnippet,
1294
1357
  // The user's `configure` module comes first: a side-effect import in
1295
1358
  // the handler graph, evaluated before any dispatch on both surfaces
1296
1359
  // (dev middleware and prod handler) and bundled into the handler
@@ -1332,13 +1395,13 @@ function serverFunctions(options = {}, internal = {}) {
1332
1395
  const hashIndex = new Map();
1333
1396
  let hashIndexSize = -1;
1334
1397
  function moduleForFunctionId(functionId) {
1335
- if (manifest.server.size !== hashIndexSize) {
1398
+ if (manifest.modules.server.size !== hashIndexSize) {
1336
1399
  hashIndex.clear();
1337
- for (const entry of manifest.server) {
1400
+ for (const entry of manifest.modules.server) {
1338
1401
  const relative = path.relative(root, entry).split(path.sep).join('/');
1339
1402
  hashIndex.set(xxHash32(relative).toString(16), entry);
1340
1403
  }
1341
- hashIndexSize = manifest.server.size;
1404
+ hashIndexSize = manifest.modules.server.size;
1342
1405
  }
1343
1406
  return hashIndex.get(functionId.split('-')[1]);
1344
1407
  }
@@ -1462,9 +1525,25 @@ function serverFunctions(options = {}, internal = {}) {
1462
1525
  sourceMap: !tsrx || !!internal.tsrxSourceMap
1463
1526
  });
1464
1527
  if (!result.valid) return null;
1528
+
1529
+ // The client compile is the authority on what the browser can dispatch:
1530
+ // record every reference it emitted, by wire id, for the persisted
1531
+ // manifest. A module is re-transformed on change, so its previous ids
1532
+ // are dropped first (a renamed function must not linger as reachable).
1533
+ if (mode === 'client') {
1534
+ for (const [functionId, record] of manifest.clientFunctions) {
1535
+ if (record.module === id) manifest.clientFunctions.delete(functionId);
1536
+ }
1537
+ for (const fn of result.functions) {
1538
+ manifest.clientFunctions.set(fn.id, {
1539
+ name: fn.name,
1540
+ module: id
1541
+ });
1542
+ }
1543
+ }
1465
1544
  const preloader = preload[mode];
1466
1545
  if (preloader) preloader.defer();
1467
- invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1546
+ invalidateModules(currentServer, mergeManifestRecord(manifest.modules.server, new Set([id])), manifestId);
1468
1547
  return {
1469
1548
  // Appended (not prepended) so the source map for the compiled module
1470
1549
  // stays valid; imports hoist and the endpoint is only read at call time.
@@ -1512,7 +1591,7 @@ function serverFunctions(options = {}, internal = {}) {
1512
1591
  // build discovered so the server manifest registers them even when
1513
1592
  // the SSR module graph never imports them.
1514
1593
  for (const entry of readPersistedManifest(root)) {
1515
- manifest.server.add(entry);
1594
+ manifest.modules.server.add(entry);
1516
1595
  }
1517
1596
  }
1518
1597
  },
@@ -1527,7 +1606,7 @@ function serverFunctions(options = {}, internal = {}) {
1527
1606
  const consumer = ctx.environment?.config?.consumer;
1528
1607
  const isClient = consumer ? consumer === 'client' : !isSsrBuild;
1529
1608
  if (isBuild && isClient) {
1530
- writePersistedManifest(root, outDir, manifest.server);
1609
+ writePersistedManifest(root, outDir, manifest.modules.server, manifest.clientFunctions);
1531
1610
  }
1532
1611
  }
1533
1612
  }, {
@@ -1552,10 +1631,10 @@ function serverFunctions(options = {}, internal = {}) {
1552
1631
  // configs resolve before the client build has written the file,
1553
1632
  // but this load runs once the SSR environment builds — after it.
1554
1633
  for (const entry of readPersistedManifest(root)) {
1555
- manifest.server.add(entry);
1634
+ manifest.modules.server.add(entry);
1556
1635
  }
1557
1636
  }
1558
- const current = new Debouncer(() => [...manifest[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1637
+ const current = new Debouncer(() => [...manifest.modules[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1559
1638
  preload[mode] = current;
1560
1639
  const result = await current.promise.reference;
1561
1640
  return result;
@@ -1604,6 +1683,11 @@ function devtoolsMountModuleCode() {
1604
1683
  // it, and page responses go through the runtime's `createSSRResponse`
1605
1684
  // head lifecycle (commit at shell flush, real pre-flush redirects, the
1606
1685
  // script fallback post-flush).
1686
+ // - `start.renderMode` decides how a page render becomes a body: `'stream'`
1687
+ // (default) flushes the shell with fallbacks and streams boundaries in
1688
+ // behind it; `'async'` awaits the settled document (no fallbacks or swap
1689
+ // scripts — complete for no-JS clients); a module path decides per
1690
+ // request, and `handleRequest(request, { renderMode })` overrides both.
1607
1691
  // - `vite preview` serves dist/client statically and dispatches everything
1608
1692
  // else through the built handler — the production path, middleware
1609
1693
  // included, with no server file needed.
@@ -1679,6 +1763,37 @@ function normalizeUserPath(root, spec, option) {
1679
1763
  }
1680
1764
  return relative;
1681
1765
  }
1766
+ /**
1767
+ * Resolves `start.renderMode` at config time: a literal mode, or the
1768
+ * absolute path of a per-request module (`mode` stays the `'stream'`
1769
+ * default then; the module decides per request). Anything else is a
1770
+ * config error with the fix in the message — an unknown literal is far
1771
+ * more likely a typo than a file, so the message names both readings.
1772
+ */
1773
+ function resolveRenderMode(root, value) {
1774
+ if (value === undefined) return {
1775
+ mode: 'stream',
1776
+ path: null
1777
+ };
1778
+ // (`string & {}` keeps editor completion for the literals; TS cannot
1779
+ // narrow it away by equality, hence the assertion.)
1780
+ if (value === 'stream' || value === 'async') return {
1781
+ mode: value,
1782
+ path: null
1783
+ };
1784
+ const usage = `start.renderMode must be 'stream' (the default), 'async', or the path of a module ` + `(relative to the Vite root) default-exporting a per-request function ` + `((event) => 'stream' | 'async' | Promise<...>)`;
1785
+ if (typeof value !== 'string') {
1786
+ throw new Error(`[@solidjs/vite-plugin] ${usage}; got ${typeof value}. A Vite config cannot serialize a ` + `closure into the generated handler — put the function in a module (e.g. ` + `./src/render-mode.ts) and pass its path instead.`);
1787
+ }
1788
+ const absolute = path.isAbsolute(value) ? value : path.resolve(root, value);
1789
+ if (!fs.existsSync(absolute)) {
1790
+ throw new Error(`[@solidjs/vite-plugin] ${usage}; got ${JSON.stringify(value)}, which is neither a mode ` + `nor an existing file.`);
1791
+ }
1792
+ return {
1793
+ mode: 'stream',
1794
+ path: path.resolve(root, normalizeUserPath(root, value, 'renderMode'))
1795
+ };
1796
+ }
1682
1797
  function resolveEntries(root, options, clientMode) {
1683
1798
  const explicitClient = options.entryClient ? normalizeUserPath(root, options.entryClient, 'entryClient') : null;
1684
1799
  if (clientMode) {
@@ -1771,8 +1886,11 @@ function startServe(options, internal = {}) {
1771
1886
  // any of the (lazy) uses in entry codegen and the entry transform.
1772
1887
  let diagnostics = internal.diagnostics === true;
1773
1888
  let devtoolsEnabled = false;
1774
- let devtoolsResolutions = {};
1775
- let devtoolsIds = {};
1889
+ // Toolbar detection, memoized per consumer: whether @solidjs/start-devtools
1890
+ // resolves at all, and the app importer it was probed from. Only the
1891
+ // verdict is kept — the resolved id is deliberately not (see resolveId).
1892
+ let devtoolsDetections = {};
1893
+ let devtoolsImporters = {};
1776
1894
  // `external` is server-mode-only (documented no-op in client mode, so a
1777
1895
  // host-integrated config survives the `ssr` boolean flip untouched).
1778
1896
  const externalServer = !clientMode && !!options.external;
@@ -1784,31 +1902,45 @@ function startServe(options, internal = {}) {
1784
1902
  let middlewarePath = null;
1785
1903
  /** Absolute path of the per-request setup module, when configured (server mode). */
1786
1904
  let setupPath = null;
1905
+ /**
1906
+ * `start.renderMode`, resolved at config time: the static mode baked into
1907
+ * the handler, or the absolute path of the per-request module deciding it
1908
+ * (server mode only — a documented no-op in client mode, whose shell has
1909
+ * no boundaries to settle).
1910
+ */
1911
+ let renderMode = 'stream';
1912
+ let renderModePath = null;
1787
1913
  function requireEntries() {
1788
1914
  // config() always runs before resolveId/load/configureServer.
1789
1915
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1790
1916
  return entries;
1791
1917
  }
1918
+ /**
1919
+ * Resolve @solidjs/start-devtools for generated code: from the app graph
1920
+ * first (the documented install location), then from the plugin's own
1921
+ * file — in pnpm-isolated apps a copy that is only a dependency of the
1922
+ * plugin is not reachable from the app's importers. Resolving from the
1923
+ * plugin's own file never yields null when the package is absent: it is
1924
+ * declared an optional peer dependency, so Vite answers with its
1925
+ * `__vite-optional-peer-dep:` stub (an empty module). That stub counts as
1926
+ * "not installed".
1927
+ */
1928
+ async function resolveDevtoolsId(resolve, importer) {
1929
+ const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1930
+ return realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? realId(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)))));
1931
+ }
1792
1932
  async function resolveDevtools(resolve, importer, consumer) {
1793
1933
  if (!devtoolsEnabled) return false;
1794
- // Detect from the app graph first (the documented install location), then
1795
- // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1796
- // dependency of the plugin is not reachable from the app's importers. The
1797
- // resolved id is kept so imports from generated modules can use it.
1798
- devtoolsResolutions[consumer] ??= (async () => {
1799
- // Resolving from the plugin's own file never yields null when the
1800
- // package is absent: it is declared an optional peer dependency, so
1801
- // Vite answers with its `__vite-optional-peer-dep:` stub (an empty
1802
- // module). Treat that stub as "not installed".
1803
- const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1804
- return realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? realId(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)))));
1805
- })();
1806
- const id = await devtoolsResolutions[consumer];
1807
- devtoolsIds[consumer] = id;
1808
- if (!id && options.devtools === true) {
1934
+ // An install cannot change under a running server, so the verdict is
1935
+ // memoized per consumer. The id it was reached with is not reused for
1936
+ // generated imports: resolveId resolves afresh from the same importer.
1937
+ devtoolsImporters[consumer] ??= importer;
1938
+ devtoolsDetections[consumer] ??= resolveDevtoolsId(resolve, importer).then(id => id !== null);
1939
+ const detected = await devtoolsDetections[consumer];
1940
+ if (!detected && options.devtools === true) {
1809
1941
  throw new Error('[@solidjs/vite-plugin] start.devtools requires @solidjs/start-devtools. ' + 'Install it as a development dependency or set start.devtools to false.');
1810
1942
  }
1811
- return id !== null;
1943
+ return detected;
1812
1944
  }
1813
1945
 
1814
1946
  /**
@@ -1983,7 +2115,7 @@ function startServe(options, internal = {}) {
1983
2115
  entryClient
1984
2116
  } = requireEntries();
1985
2117
  const composeServerFunctions = internal.serverFunctions;
1986
- const lines = [`import { createRequestEvent, createSSRResponse, commitEventResponse${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`, `import { provideRequestEvent } from ${JSON.stringify(STORAGE_SOURCE)};`, `import * as entry from ${JSON.stringify(entryServerSpec())};`, ...(middlewarePath ? [`import middlewareModule from ${JSON.stringify(middlewarePath)};`] : []), ...(externalDev ? [`import DEV_STYLES_HEAD from ${JSON.stringify(DEV_STYLES_ID)};`] : []), ...(composeServerFunctions ? [`import { handleServerFunctionRequest, endpoint } from ${JSON.stringify(SERVER_FUNCTION_HANDLER_ID)};`] : [])];
2118
+ const lines = [`import { createRequestEvent, createSSRResponse, commitEventResponse${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`, `import { provideRequestEvent } from ${JSON.stringify(STORAGE_SOURCE)};`, `import * as entry from ${JSON.stringify(entryServerSpec())};`, ...(middlewarePath ? [`import middlewareModule from ${JSON.stringify(middlewarePath)};`] : []), ...(renderModePath ? [`import renderModeModule from ${JSON.stringify(renderModePath)};`] : []), ...(externalDev ? [`import DEV_STYLES_HEAD from ${JSON.stringify(DEV_STYLES_ID)};`] : []), ...(composeServerFunctions ? [`import { handleServerFunctionRequest, endpoint } from ${JSON.stringify(SERVER_FUNCTION_HANDLER_ID)};`] : [])];
1987
2119
  if (isBuild) {
1988
2120
  lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
1989
2121
  lines.push(``, `function joinAssetPath(base, file) {`, ` if (typeof base !== 'string' || !base) base = '/';`, ` if (base[base.length - 1] !== '/') base += '/';`, ` return base + (file[0] === '/' ? file.slice(1) : file);`, `}`, ``, `let clientEntryUrl;`, `function resolveClientEntry() {`, ` if (clientEntryUrl !== undefined) return clientEntryUrl;`, ` clientEntryUrl = null;`,
@@ -2005,6 +2137,21 @@ function startServe(options, internal = {}) {
2005
2137
  lines.push(`const runMiddleware = (request, next) => next(request);`);
2006
2138
  }
2007
2139
 
2140
+ // Render mode (`start.renderMode`): 'stream' flushes the shell with
2141
+ // fallbacks in place and streams boundary content after it; 'async'
2142
+ // adopts the renderToStream result's thenable — which waits for the
2143
+ // complete render — so one settled document goes out (the fix for
2144
+ // no-JS clients, solidjs/solid#3280). Precedence per request: the
2145
+ // `handleRequest` option (hosts driving the handler directly), then the
2146
+ // configured module's per-request result, then the static config. Every
2147
+ // source is validated against the two literals with the offender named.
2148
+ lines.push(``, `function assertRenderMode(mode, source) {`, ` if (mode !== 'stream' && mode !== 'async') {`, ` throw new Error('[@solidjs/vite-plugin] ' + source + " must be 'stream' or 'async', got " + JSON.stringify(mode));`, ` }`, ` return mode;`, `}`);
2149
+ if (renderModePath) {
2150
+ lines.push(`if (typeof renderModeModule !== 'function') {`, ` throw new Error('[@solidjs/vite-plugin] start.renderMode must default-export a function ' +`, ` "((event) => 'stream' | 'async' | Promise<...>): " + ${JSON.stringify(renderModePath)});`, `}`, `async function resolveRenderMode(event, options) {`, ` if (options.renderMode !== undefined) return assertRenderMode(options.renderMode, 'handleRequest options.renderMode');`, ` return assertRenderMode(await renderModeModule(event), 'the start.renderMode module (' + ${JSON.stringify(renderModePath)} + ') result');`, `}`);
2151
+ } else {
2152
+ lines.push(`function resolveRenderMode(event, options) {`, ` if (options.renderMode !== undefined) return assertRenderMode(options.renderMode, 'handleRequest options.renderMode');`, ` return ${JSON.stringify(renderMode)};`, `}`);
2153
+ }
2154
+
2008
2155
  // No `_$SC` bootstrap injection: the runtime's serialized
2009
2156
  // server-component references self-bootstrap the registry (each
2010
2157
  // hydration script's first reference carries it as an idempotent
@@ -2085,7 +2232,11 @@ function startServe(options, internal = {}) {
2085
2232
  // flag, so external-host dispatch and preview stay render-always.
2086
2233
  lines.push(` if (options.pageRequest === false) {`, ` return new Response(null, { status: 404, headers: { ${JSON.stringify(DEV_FALLTHROUGH_HEADER)}: '1' } });`, ` }`);
2087
2234
  }
2088
- lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2235
+ lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`,
2236
+ // Decided before the render starts (the module form may be async),
2237
+ // inside the request scope and after the middleware chain, so a
2238
+ // per-request policy sees the decorated event.
2239
+ ` const renderMode = await resolveRenderMode(event, options);`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2089
2240
  // renderToStream results are thenables whose then() waits for the
2090
2241
  // *complete* render — check for pipe first so streaming survives, and
2091
2242
  // only await plain promises (async render functions).
@@ -2094,6 +2245,14 @@ function startServe(options, internal = {}) {
2094
2245
  // entry): a bare promise resolution would adopt the stream's
2095
2246
  // thenable and buffer the whole render.
2096
2247
  ` if (result && result.${STREAM_BOX}) result = result.${STREAM_BOX};`] : []),
2248
+ // Async mode adopts the thenable deliberately — the very thing the
2249
+ // pipe-first check and the setup box exist to avoid in stream mode.
2250
+ // It resolves with the full HTML once every boundary settled, each
2251
+ // spliced in place pre-flush (no fallbacks, no swap scripts; hydration
2252
+ // data still serialized), and the string then takes
2253
+ // createSSRResponse's string path below: stub commit, transformChunk
2254
+ // on the whole document, and a mid-render Location as a real 3xx.
2255
+ ` if (renderMode === 'async' && result && typeof result.then === 'function') {`, ` result = await result;`, ` }`,
2097
2256
  // Raw Responses fold at the handler edge (handleRequest), after the
2098
2257
  // middleware chain unwinds — not here, where middleware above this
2099
2258
  // frame could still legitimately mutate headers.
@@ -2132,8 +2291,8 @@ function startServe(options, internal = {}) {
2132
2291
  config(userConfig, env) {
2133
2292
  root = path.resolve(userConfig.root || process.cwd());
2134
2293
  devtoolsEnabled = env.command === 'serve' && !env.isPreview && options.devtools !== false;
2135
- devtoolsResolutions = {};
2136
- devtoolsIds = {};
2294
+ devtoolsDetections = {};
2295
+ devtoolsImporters = {};
2137
2296
  entries = resolveEntries(root, options, clientMode);
2138
2297
  internal.onDocumentResolved?.(entries.document);
2139
2298
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
@@ -2145,6 +2304,17 @@ function startServe(options, internal = {}) {
2145
2304
  // hook needs does not exist there.
2146
2305
  throw new Error('[@solidjs/vite-plugin] start.setup only applies to generated entries: your ' + 'entry-server owns render() already, so call your setup step there instead ' + `(remove start.setup or the authored entry): ${options.setup}`);
2147
2306
  }
2307
+ // Server-mode only as well: the client-mode shell renders no app,
2308
+ // so there is nothing to settle. Validated in every mode though —
2309
+ // a typo should not hide behind the `ssr` boolean.
2310
+ ({
2311
+ mode: renderMode,
2312
+ path: renderModePath
2313
+ } = resolveRenderMode(root, options.renderMode));
2314
+ if (clientMode) {
2315
+ renderMode = 'stream';
2316
+ renderModePath = null;
2317
+ }
2148
2318
  if (env.isPreview) {
2149
2319
  if (clientMode) {
2150
2320
  // Client-mode builds emit a real dist/client/index.html (the
@@ -2286,7 +2456,7 @@ function startServe(options, internal = {}) {
2286
2456
  diagnostics = detectDiagnosticsPackage(root);
2287
2457
  }
2288
2458
  },
2289
- resolveId(source, importer, opts) {
2459
+ async resolveId(source, importer, opts) {
2290
2460
  if (source === HANDLER_ID) {
2291
2461
  return {
2292
2462
  id: HANDLER_ID,
@@ -2311,13 +2481,25 @@ function startServe(options, internal = {}) {
2311
2481
  moduleSideEffects: true
2312
2482
  };
2313
2483
  }
2314
- // Generated modules have no directory for bare-package resolution.
2315
- // Reuse the app-relative id captured during detection.
2316
- const devtoolsId = devtoolsIds[getEnvironmentConsumer(this.environment, opts)];
2317
- if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2318
- return {
2319
- id: devtoolsId
2320
- };
2484
+ if (source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2485
+ // Generated modules have no directory for bare-package resolution:
2486
+ // resolve from the app importer detection probed. Resolve afresh on
2487
+ // every request rather than reusing detection's id in the client
2488
+ // environment that id is the optimizer's pre-bundled URL, stamped
2489
+ // with the browserHash of the pass that produced it. Any dependency
2490
+ // discovered after the initial scan re-optimizes: the toolbar's
2491
+ // chunks are re-emitted under new names and the hash moves on, and
2492
+ // a frozen id would keep the entry on the previous pass — its lazy
2493
+ // chunks answer 504 (Outdated Optimize Dep) and the stale bundle
2494
+ // brings a second solid-js instance into the page.
2495
+ const from = devtoolsImporters[getEnvironmentConsumer(this.environment, opts)];
2496
+ if (!from) return null;
2497
+ const id = await resolveDevtoolsId((s, i) => this.resolve(s, i, {
2498
+ skipSelf: true
2499
+ }), from);
2500
+ return id ? {
2501
+ id
2502
+ } : null;
2321
2503
  }
2322
2504
  return null;
2323
2505
  },
@@ -3369,7 +3551,69 @@ function combineSourcemaps(maps) {
3369
3551
  // remapping expects most-recent-first.
3370
3552
  return JSON.parse(remapping(chain.reverse(), () => null).toString());
3371
3553
  }
3554
+ function toPosixPath(p) {
3555
+ return p.split(path.sep).join('/');
3556
+ }
3557
+ function tryRealpath(p) {
3558
+ try {
3559
+ return fs.realpathSync.native(p);
3560
+ } catch {
3561
+ return null;
3562
+ }
3563
+ }
3564
+
3565
+ /** The `input` a build environment's config resolves to, in any spelling. */
3566
+ function configuredBuildInput(build) {
3567
+ if (!build) return undefined;
3568
+ return build.rolldownOptions?.input ?? build.rollupOptions?.input ?? build.lib?.entry;
3569
+ }
3372
3570
 
3571
+ /**
3572
+ * The genuine entries of a client build, derived from its configured input
3573
+ * (`build.rollupOptions.input` as a string / array / record, or Vite's
3574
+ * default `index.html`). Rollup and rolldown only ever flag two kinds of
3575
+ * chunk `isEntry`: those facades and chunks plugins emit with
3576
+ * `emitFile({ type: 'chunk' })` — so this is exactly the knowledge that
3577
+ * tells a real application entry apart from an emitted lazy facade.
3578
+ *
3579
+ * `moduleIds` — every spelling the entry's facade module id can take: as
3580
+ * written (virtual ids resolve to themselves), resolved against the root
3581
+ * (Vite resolves relative file inputs there), and the real path of either
3582
+ * (Vite's resolver follows symlinks).
3583
+ * `manifestKeys` — the manifest.json keys Vite derives from those facades
3584
+ * (root-relative, `\0` stripped), matching Vite's own `getChunkName`.
3585
+ */
3586
+ function resolveConfiguredEntries(input, root) {
3587
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3588
+ const moduleIds = new Set();
3589
+ for (const id of raw) {
3590
+ if (typeof id !== 'string') continue;
3591
+ const clean = id.replace(/\0/g, '');
3592
+ const candidates = [clean, path.resolve(root, clean)];
3593
+ for (const candidate of candidates) {
3594
+ moduleIds.add(candidate);
3595
+ moduleIds.add(toPosixPath(candidate));
3596
+ const real = tryRealpath(candidate);
3597
+ if (real) {
3598
+ moduleIds.add(real);
3599
+ moduleIds.add(toPosixPath(real));
3600
+ }
3601
+ }
3602
+ }
3603
+ const manifestKeys = new Set();
3604
+ for (const id of moduleIds) manifestKeys.add(toPosixPath(path.relative(root, id)));
3605
+ return {
3606
+ moduleIds,
3607
+ manifestKeys,
3608
+ isEntryModule(id) {
3609
+ if (!id) return false;
3610
+ const clean = id.replace(/\0/g, '');
3611
+ if (moduleIds.has(clean) || moduleIds.has(toPosixPath(clean))) return true;
3612
+ const real = tryRealpath(clean);
3613
+ return !!real && (moduleIds.has(real) || moduleIds.has(toPosixPath(real)));
3614
+ }
3615
+ };
3616
+ }
3373
3617
  /**
3374
3618
  * Chunks emitted for lazy() targets are marked `isEntry` by Rollup even
3375
3619
  * though they are semantically dynamic entries. Reclassify any entry that is
@@ -3378,18 +3622,57 @@ function combineSourcemaps(maps) {
3378
3622
  * the real client entry. Works on both the Vite manifest.json shape and the
3379
3623
  * raw Rollup output bundle — both key entries by name and expose
3380
3624
  * `dynamicImports` / `isEntry` with the same meaning.
3625
+ *
3626
+ * Being a dynamic-import target alone does not make a chunk a lazy facade,
3627
+ * though: the real client entry becomes one whenever it absorbs a module
3628
+ * that is also dynamically imported somewhere else. Solid 2 produces that
3629
+ * shape on its own — `@solidjs/web/frames/client` lazily imports the
3630
+ * serialization decoder (`loadCodec()`), so a static import of
3631
+ * `@solidjs/web/serialization/decode` anywhere in the client graph merges
3632
+ * the decoder into the entry chunk, and the entry then lists itself (or is
3633
+ * listed by another lazy chunk) under `dynamicImports`. Stripping `isEntry`
3634
+ * there leaves the bundle with no entry at all ("No entry file found"
3635
+ * downstream, e.g. TanStack Start's manifest capture, #342). Genuine
3636
+ * configured entries are therefore never reclassified, and a chunk's
3637
+ * dynamic import of itself is not an edge worth acting on.
3638
+ *
3639
+ * Rolldown caveat: of the flags written here only `isEntry` is synced back
3640
+ * to the native bundle after the hook (rolldown's `update_output_chunk`
3641
+ * copies `code`, `map`, `imports`, `dynamicImports`, `isEntry` and the file
3642
+ * name; `isDynamicEntry` is kept from the original chunk). Later plugins
3643
+ * and Vite's manifest plugin therefore see reclassified facades as neither
3644
+ * entry nor dynamic entry under rolldown. The manifest `load` path repairs
3645
+ * `isDynamicEntry` on the plugin's own manifest module, the one place it
3646
+ * controls end to end.
3381
3647
  */
3382
- function normalizeEmittedLazyEntries(manifest) {
3383
- const dynamicKeys = new Set();
3648
+ function normalizeEmittedLazyEntries(manifest, {
3649
+ isConfiguredEntry,
3650
+ knownLazyKeys,
3651
+ warn,
3652
+ repairDynamicEntries
3653
+ }) {
3654
+ const dynamicKeys = new Map();
3384
3655
  for (const key in manifest) {
3385
3656
  const imports = manifest[key].dynamicImports;
3386
- if (imports) for (const dep of imports) dynamicKeys.add(dep);
3657
+ if (!imports) continue;
3658
+ for (const dep of imports) {
3659
+ // A chunk that absorbed one of its own lazy targets imports itself;
3660
+ // that says nothing about whether it is an entry.
3661
+ if (dep !== key && !dynamicKeys.has(dep)) dynamicKeys.set(dep, key);
3662
+ }
3387
3663
  }
3388
- for (const key of dynamicKeys) {
3664
+ for (const [key, importer] of dynamicKeys) {
3389
3665
  const entry = manifest[key];
3390
- if (entry && entry.isEntry) {
3666
+ if (!entry || entry.type === 'asset') continue;
3667
+ if (isConfiguredEntry(key, entry)) continue;
3668
+ if (entry.isEntry) {
3391
3669
  entry.isEntry = false;
3392
3670
  entry.isDynamicEntry = true;
3671
+ if (warn && !knownLazyKeys?.has(key)) {
3672
+ warn(`[@solidjs/vite-plugin] Reclassified the entry chunk "${key}" as a dynamic entry ` + `because "${importer}" dynamically imports it and it does not match a configured ` + `build input. If "${key}" is the application entry, its chunk absorbed a module ` + 'that is also imported dynamically elsewhere (for example a static import of ' + '"@solidjs/web/serialization/decode" alongside Solid\'s own lazy import of it); ' + 'list the entry in `build.rollupOptions.input` so the plugin can recognize it.');
3673
+ }
3674
+ } else if (repairDynamicEntries && !entry.isDynamicEntry) {
3675
+ entry.isDynamicEntry = true;
3393
3676
  }
3394
3677
  }
3395
3678
  }
@@ -3451,6 +3734,11 @@ function solidPlugin(options = {}) {
3451
3734
  let isSsrBuild = false;
3452
3735
  let base = '/';
3453
3736
  let clientOutDir = null;
3737
+ // The client environment's resolved build options, for the configured
3738
+ // entry input. Read off the resolved config so the SSR half of a
3739
+ // two-invocation build (`vite build --ssr`) still knows the client's
3740
+ // entries when it bakes the client manifest in.
3741
+ let clientBuildConfig = null;
3454
3742
  let solidPkgsConfig;
3455
3743
  const tsrxCss = new Map();
3456
3744
 
@@ -3756,6 +4044,29 @@ function solidPlugin(options = {}) {
3756
4044
  // So the dev flag has to reach both lists.
3757
4045
  if (replaceDev && config.consumer !== 'client' && name !== 'client') {
3758
4046
  config.resolve.externalConditions = ['development', ...(config.resolve.externalConditions ?? vite.defaultExternalConditions)];
4047
+
4048
+ // `externalConditions` only reaches the imports the module runner
4049
+ // resolves itself. An externalized package's OWN imports are resolved
4050
+ // by Node, with Node's conditions — never `development`. Since
4051
+ // solid 2.0.0-rc.7 both `solid-js` and `@solidjs/web` ship a
4052
+ // `dist/server.dev.*` behind that condition, so leaving them external
4053
+ // splits the framework in two under `vite dev`: the app's `solid-js`
4054
+ // is the runner's dev copy while `@solidjs/web`'s `import "solid-js"`
4055
+ // lands on Node's prod copy. `renderToStream` then installs the asset
4056
+ // resolver on one `sharedConfig` and `lazy()` reads the other ("no
4057
+ // asset manifest is set"), with every other module-level singleton
4058
+ // (owner tracking, request events, hydration keys) split the same
4059
+ // way. Inlining the two core packages makes every resolution — theirs
4060
+ // included — go through the environment's conditions, so one dev
4061
+ // build is loaded end to end. Framework packages that declare the
4062
+ // `solid` export condition are already inlined via vitefu below and
4063
+ // reach the same copy. Vitest projects manage their own inlining
4064
+ // (`test.server.deps` above) and are left alone, as is a host that
4065
+ // set `noExternal: true` (everything is inlined already).
4066
+ if (!isTestMode && config.resolve.noExternal !== true) {
4067
+ const noExternal = config.resolve.noExternal;
4068
+ config.resolve.noExternal = [...(Array.isArray(noExternal) ? noExternal : noExternal ? [noExternal] : []), 'solid-js', '@solidjs/web'];
4069
+ }
3759
4070
  }
3760
4071
 
3761
4072
  // Set resolve.noExternal and resolve.external for the SSR environment.
@@ -3772,6 +4083,7 @@ function solidPlugin(options = {}) {
3772
4083
  isSsrBuild = !!config.build.ssr;
3773
4084
  base = config.base;
3774
4085
  projectRoot = config.root;
4086
+ clientBuildConfig = config.environments?.client?.build ?? config.build;
3775
4087
  filter = vite.createFilter(options.include, options.exclude, {
3776
4088
  resolve: projectRoot
3777
4089
  });
@@ -3905,7 +4217,23 @@ function solidPlugin(options = {}) {
3905
4217
  const manifestPath = clientManifestPath();
3906
4218
  if (manifestPath) {
3907
4219
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
3908
- normalizeEmittedLazyEntries(manifest);
4220
+ // Manifest records are keyed the way Vite keys entry chunks (the
4221
+ // root-relative facade path, also carried as `src`), so the
4222
+ // configured client inputs identify the genuine entries here too —
4223
+ // independent of `isEntry`, which the serialized manifest may have
4224
+ // lost already (older plugin builds stripped it; see #342).
4225
+ const entries = resolveConfiguredEntries(configuredBuildInput(clientBuildConfig), projectRoot);
4226
+ const isConfiguredEntry = (key, record) => entries.manifestKeys.has(key) || typeof record.src === 'string' && entries.manifestKeys.has(record.src);
4227
+ for (const key in manifest) {
4228
+ if (isConfiguredEntry(key, manifest[key]) && manifest[key].file) {
4229
+ manifest[key].isEntry = true;
4230
+ }
4231
+ }
4232
+ normalizeEmittedLazyEntries(manifest, {
4233
+ isConfiguredEntry,
4234
+ warn: message => this.warn(message),
4235
+ repairDynamicEntries: true
4236
+ });
3909
4237
  manifest._base = base;
3910
4238
  return `export default ${JSON.stringify(manifest)};`;
3911
4239
  }
@@ -3922,6 +4250,12 @@ function solidPlugin(options = {}) {
3922
4250
  // the bundle don't mistake them for application entries. Must precede
3923
4251
  // the client asset map build, which keys off dynamic entries.
3924
4252
  if (options.ssr) {
4253
+ // The genuine entries are the configured inputs of this very
4254
+ // environment — the plugin injects the client entry itself in start
4255
+ // mode, and Vite's default is index.html — so their facade chunks
4256
+ // are recognizable regardless of what dynamically imports them.
4257
+ const entries = resolveConfiguredEntries(configuredBuildInput(this.environment?.config?.build ?? clientBuildConfig), projectRoot);
4258
+ const knownLazyKeys = new Set();
3925
4259
  for (const ref of emittedLazyChunkRefs) {
3926
4260
  let fileName;
3927
4261
  try {
@@ -3932,10 +4266,17 @@ function solidPlugin(options = {}) {
3932
4266
  }
3933
4267
  const chunk = bundle[fileName];
3934
4268
  if (!chunk || chunk.type !== 'chunk') continue;
4269
+ // An entry that is also lazily imported stays an entry.
4270
+ if (entries.isEntryModule(chunk.facadeModuleId)) continue;
4271
+ knownLazyKeys.add(fileName);
3935
4272
  chunk.isEntry = false;
3936
4273
  chunk.isDynamicEntry = true;
3937
4274
  }
3938
- normalizeEmittedLazyEntries(bundle);
4275
+ normalizeEmittedLazyEntries(bundle, {
4276
+ isConfiguredEntry: (_key, chunk) => entries.isEntryModule(chunk.facadeModuleId),
4277
+ knownLazyKeys,
4278
+ warn: message => this.warn(message)
4279
+ });
3939
4280
  }
3940
4281
  },
3941
4282
  async transform(source, id, transformOptions) {