@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.
@@ -1,11 +1,12 @@
1
1
  import * as babel from '@babel/core';
2
2
  import remapping from '@ampproject/remapping';
3
3
  import solid from '@solidjs/babel-plugin';
4
- import fs, { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
4
+ import fs, { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, realpathSync } from 'fs';
5
5
  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 { randomBytes } from 'crypto';
9
10
  import { createFilter, normalizePath, loadEnv, runnerImport, transformWithOxc, defaultClientConditions, defaultServerConditions, defaultExternalConditions } from 'vite';
10
11
  import { pathToFileURL, fileURLToPath } from 'node:url';
11
12
  import { crawlFrameworkPkgs } from 'vitefu';
@@ -733,6 +734,22 @@ function solidDiagnostics(mode = 'auto') {
733
734
  apply(_config, env) {
734
735
  return env.command === 'serve' && !env.isPreview && env.mode !== 'test';
735
736
  },
737
+ // The bridge module is virtual and reaches the page behind the
738
+ // scanner's back (a script injected into index.html at transform time,
739
+ // or an import the generated start entry adds), so the optimizer never
740
+ // sees its two package imports up front. Pre-bundle them: otherwise the
741
+ // first page load discovers them, re-optimizes and full-reloads — a
742
+ // flash at best, and a broken page whenever anything else on the page
743
+ // was resolved against the first optimizer pass.
744
+ config(userConfig) {
745
+ const rootDir = path.resolve(userConfig.root || process.cwd());
746
+ if (mode !== true && !detectDiagnosticsPackage(rootDir)) return;
747
+ return {
748
+ optimizeDeps: {
749
+ include: [`${DIAGNOSTICS_PACKAGE}/browser`, `${DIAGNOSTICS_PACKAGE}/protocol`]
750
+ }
751
+ };
752
+ },
736
753
  configResolved(config) {
737
754
  root = config.root;
738
755
  base = config.base;
@@ -1078,29 +1095,50 @@ const HANDLER_ID$1 = 'virtual:solid-server-function-handler';
1078
1095
  // (`vite build` then `vite build --ssr`) does not, so the client build
1079
1096
  // persists its findings for the SSR build to merge (mirroring the plugin's
1080
1097
  // dist/client/.vite/manifest.json convention).
1098
+ //
1099
+ // The file doubles as the build's statement of which server functions the
1100
+ // CLIENT can reach — every reference the client compile emitted, by wire id
1101
+ // — for build tooling that needs that set without re-deriving it from
1102
+ // compiled output (a static-site prerenderer checking that each reachable
1103
+ // function was captured, for example). Paths are root-relative, posix.
1081
1104
  const PERSISTED_MANIFEST_PATH = '.vite/solid-server-functions.json';
1105
+
1106
+ /** The persisted manifest's on-disk shape (the array form is the pre-`functions` legacy). */
1107
+
1082
1108
  function readPersistedManifest(root) {
1083
1109
  const file = path.resolve(root, 'dist/client', PERSISTED_MANIFEST_PATH);
1084
1110
  if (!existsSync(file)) return new Set();
1085
1111
  try {
1086
- const entries = JSON.parse(readFileSync(file, 'utf-8'));
1112
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'));
1113
+ const entries = Array.isArray(parsed) ? parsed : parsed.modules;
1087
1114
  return new Set(entries.map(entry => path.resolve(root, entry)).filter(entry => existsSync(entry)));
1088
1115
  } catch {
1089
1116
  return new Set();
1090
1117
  }
1091
1118
  }
1092
- function writePersistedManifest(root, outDir, entries) {
1119
+ function writePersistedManifest(root, outDir, entries, functions) {
1093
1120
  const file = path.resolve(root, outDir, PERSISTED_MANIFEST_PATH);
1094
1121
  mkdirSync(path.dirname(file), {
1095
1122
  recursive: true
1096
1123
  });
1097
- const relative = [...entries].map(entry => path.relative(root, entry).split(path.sep).join('/'));
1098
- writeFileSync(file, JSON.stringify(relative, null, 2));
1124
+ const relative = entry => path.relative(root, entry).split(path.sep).join('/');
1125
+ const manifest = {
1126
+ modules: [...entries].map(relative),
1127
+ functions: [...functions].map(([id, record]) => ({
1128
+ id,
1129
+ name: record.name,
1130
+ module: relative(record.module)
1131
+ }))
1132
+ };
1133
+ writeFileSync(file, JSON.stringify(manifest, null, 2));
1099
1134
  }
1100
1135
  function createManifest() {
1101
1136
  return {
1102
- server: new Set(),
1103
- client: new Set()
1137
+ modules: {
1138
+ server: new Set(),
1139
+ client: new Set()
1140
+ },
1141
+ clientFunctions: new Map()
1104
1142
  };
1105
1143
  }
1106
1144
  function createDeferredPromise() {
@@ -1208,6 +1246,23 @@ function serverFunctions(options = {}, internal = {}) {
1208
1246
  client: undefined
1209
1247
  };
1210
1248
  let currentServer;
1249
+
1250
+ // THE DEPLOYMENT SECRET (solidjs/solid#3239): the runtime's flash cookie
1251
+ // — the no-JS form outcome — carries the submitted input, so it is
1252
+ // AES-GCM encrypted under a key derived from a deployment-wide secret,
1253
+ // and without one the outcome is withheld entirely. This plugin provides
1254
+ // that secret with zero configuration through the internal
1255
+ // `globalThis.__SOLID_SECRET__ ??=` contract: generated once per plugin
1256
+ // instance, so a production build bakes one value into the emitted server
1257
+ // chunk — every instance of that deployment shares it (a per-process
1258
+ // value would silently lose flashes behind a load balancer) — and a dev
1259
+ // session holds one for its lifetime (a restart invalidates in-flight
1260
+ // flashes, which are 60-second one-shot cookies; the next render just
1261
+ // reads "no flash"). Server output only, never the client graph. The
1262
+ // `??=` keeps an explicit `configureServerFunctionsServer({ secret })` —
1263
+ // or a value injected by an outer harness — authoritative.
1264
+ const deploymentSecret = randomBytes(32).toString('hex');
1265
+ const deploymentSecretSnippet = `globalThis.__SOLID_SECRET__ ??= ${JSON.stringify(deploymentSecret)};`;
1211
1266
  const clientOptions = {
1212
1267
  directive,
1213
1268
  definitions: {
@@ -1267,6 +1322,14 @@ function serverFunctions(options = {}, internal = {}) {
1267
1322
  // import is only emitted when the option is on, so disabled setups keep
1268
1323
  // a server-component-free graph.
1269
1324
  return [
1325
+ // The deployment secret. Imports are hoisted above it, but nothing
1326
+ // reads the global at module evaluation — the runtime resolves it
1327
+ // lazily per encode/decode — so leading textually is just the honest
1328
+ // placement. This module is loaded before any dispatch on both
1329
+ // surfaces, and the generated SSR handler imports it at module load,
1330
+ // so the secret is in place for the flash's encode (the form POST)
1331
+ // and its decode (the render that follows the redirect) alike.
1332
+ deploymentSecretSnippet,
1270
1333
  // The user's `configure` module comes first: a side-effect import in
1271
1334
  // the handler graph, evaluated before any dispatch on both surfaces
1272
1335
  // (dev middleware and prod handler) and bundled into the handler
@@ -1308,13 +1371,13 @@ function serverFunctions(options = {}, internal = {}) {
1308
1371
  const hashIndex = new Map();
1309
1372
  let hashIndexSize = -1;
1310
1373
  function moduleForFunctionId(functionId) {
1311
- if (manifest.server.size !== hashIndexSize) {
1374
+ if (manifest.modules.server.size !== hashIndexSize) {
1312
1375
  hashIndex.clear();
1313
- for (const entry of manifest.server) {
1376
+ for (const entry of manifest.modules.server) {
1314
1377
  const relative = path.relative(root, entry).split(path.sep).join('/');
1315
1378
  hashIndex.set(xxHash32(relative).toString(16), entry);
1316
1379
  }
1317
- hashIndexSize = manifest.server.size;
1380
+ hashIndexSize = manifest.modules.server.size;
1318
1381
  }
1319
1382
  return hashIndex.get(functionId.split('-')[1]);
1320
1383
  }
@@ -1438,9 +1501,25 @@ function serverFunctions(options = {}, internal = {}) {
1438
1501
  sourceMap: !tsrx || !!internal.tsrxSourceMap
1439
1502
  });
1440
1503
  if (!result.valid) return null;
1504
+
1505
+ // The client compile is the authority on what the browser can dispatch:
1506
+ // record every reference it emitted, by wire id, for the persisted
1507
+ // manifest. A module is re-transformed on change, so its previous ids
1508
+ // are dropped first (a renamed function must not linger as reachable).
1509
+ if (mode === 'client') {
1510
+ for (const [functionId, record] of manifest.clientFunctions) {
1511
+ if (record.module === id) manifest.clientFunctions.delete(functionId);
1512
+ }
1513
+ for (const fn of result.functions) {
1514
+ manifest.clientFunctions.set(fn.id, {
1515
+ name: fn.name,
1516
+ module: id
1517
+ });
1518
+ }
1519
+ }
1441
1520
  const preloader = preload[mode];
1442
1521
  if (preloader) preloader.defer();
1443
- invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1522
+ invalidateModules(currentServer, mergeManifestRecord(manifest.modules.server, new Set([id])), manifestId);
1444
1523
  return {
1445
1524
  // Appended (not prepended) so the source map for the compiled module
1446
1525
  // stays valid; imports hoist and the endpoint is only read at call time.
@@ -1488,7 +1567,7 @@ function serverFunctions(options = {}, internal = {}) {
1488
1567
  // build discovered so the server manifest registers them even when
1489
1568
  // the SSR module graph never imports them.
1490
1569
  for (const entry of readPersistedManifest(root)) {
1491
- manifest.server.add(entry);
1570
+ manifest.modules.server.add(entry);
1492
1571
  }
1493
1572
  }
1494
1573
  },
@@ -1503,7 +1582,7 @@ function serverFunctions(options = {}, internal = {}) {
1503
1582
  const consumer = ctx.environment?.config?.consumer;
1504
1583
  const isClient = consumer ? consumer === 'client' : !isSsrBuild;
1505
1584
  if (isBuild && isClient) {
1506
- writePersistedManifest(root, outDir, manifest.server);
1585
+ writePersistedManifest(root, outDir, manifest.modules.server, manifest.clientFunctions);
1507
1586
  }
1508
1587
  }
1509
1588
  }, {
@@ -1528,10 +1607,10 @@ function serverFunctions(options = {}, internal = {}) {
1528
1607
  // configs resolve before the client build has written the file,
1529
1608
  // but this load runs once the SSR environment builds — after it.
1530
1609
  for (const entry of readPersistedManifest(root)) {
1531
- manifest.server.add(entry);
1610
+ manifest.modules.server.add(entry);
1532
1611
  }
1533
1612
  }
1534
- const current = new Debouncer(() => [...manifest[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1613
+ const current = new Debouncer(() => [...manifest.modules[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1535
1614
  preload[mode] = current;
1536
1615
  const result = await current.promise.reference;
1537
1616
  return result;
@@ -1580,6 +1659,11 @@ function devtoolsMountModuleCode() {
1580
1659
  // it, and page responses go through the runtime's `createSSRResponse`
1581
1660
  // head lifecycle (commit at shell flush, real pre-flush redirects, the
1582
1661
  // script fallback post-flush).
1662
+ // - `start.renderMode` decides how a page render becomes a body: `'stream'`
1663
+ // (default) flushes the shell with fallbacks and streams boundaries in
1664
+ // behind it; `'async'` awaits the settled document (no fallbacks or swap
1665
+ // scripts — complete for no-JS clients); a module path decides per
1666
+ // request, and `handleRequest(request, { renderMode })` overrides both.
1583
1667
  // - `vite preview` serves dist/client statically and dispatches everything
1584
1668
  // else through the built handler — the production path, middleware
1585
1669
  // included, with no server file needed.
@@ -1655,6 +1739,37 @@ function normalizeUserPath(root, spec, option) {
1655
1739
  }
1656
1740
  return relative;
1657
1741
  }
1742
+ /**
1743
+ * Resolves `start.renderMode` at config time: a literal mode, or the
1744
+ * absolute path of a per-request module (`mode` stays the `'stream'`
1745
+ * default then; the module decides per request). Anything else is a
1746
+ * config error with the fix in the message — an unknown literal is far
1747
+ * more likely a typo than a file, so the message names both readings.
1748
+ */
1749
+ function resolveRenderMode(root, value) {
1750
+ if (value === undefined) return {
1751
+ mode: 'stream',
1752
+ path: null
1753
+ };
1754
+ // (`string & {}` keeps editor completion for the literals; TS cannot
1755
+ // narrow it away by equality, hence the assertion.)
1756
+ if (value === 'stream' || value === 'async') return {
1757
+ mode: value,
1758
+ path: null
1759
+ };
1760
+ 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<...>)`;
1761
+ if (typeof value !== 'string') {
1762
+ 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.`);
1763
+ }
1764
+ const absolute = path.isAbsolute(value) ? value : path.resolve(root, value);
1765
+ if (!existsSync(absolute)) {
1766
+ throw new Error(`[@solidjs/vite-plugin] ${usage}; got ${JSON.stringify(value)}, which is neither a mode ` + `nor an existing file.`);
1767
+ }
1768
+ return {
1769
+ mode: 'stream',
1770
+ path: path.resolve(root, normalizeUserPath(root, value, 'renderMode'))
1771
+ };
1772
+ }
1658
1773
  function resolveEntries(root, options, clientMode) {
1659
1774
  const explicitClient = options.entryClient ? normalizeUserPath(root, options.entryClient, 'entryClient') : null;
1660
1775
  if (clientMode) {
@@ -1747,8 +1862,11 @@ function startServe(options, internal = {}) {
1747
1862
  // any of the (lazy) uses in entry codegen and the entry transform.
1748
1863
  let diagnostics = internal.diagnostics === true;
1749
1864
  let devtoolsEnabled = false;
1750
- let devtoolsResolutions = {};
1751
- let devtoolsIds = {};
1865
+ // Toolbar detection, memoized per consumer: whether @solidjs/start-devtools
1866
+ // resolves at all, and the app importer it was probed from. Only the
1867
+ // verdict is kept — the resolved id is deliberately not (see resolveId).
1868
+ let devtoolsDetections = {};
1869
+ let devtoolsImporters = {};
1752
1870
  // `external` is server-mode-only (documented no-op in client mode, so a
1753
1871
  // host-integrated config survives the `ssr` boolean flip untouched).
1754
1872
  const externalServer = !clientMode && !!options.external;
@@ -1760,31 +1878,45 @@ function startServe(options, internal = {}) {
1760
1878
  let middlewarePath = null;
1761
1879
  /** Absolute path of the per-request setup module, when configured (server mode). */
1762
1880
  let setupPath = null;
1881
+ /**
1882
+ * `start.renderMode`, resolved at config time: the static mode baked into
1883
+ * the handler, or the absolute path of the per-request module deciding it
1884
+ * (server mode only — a documented no-op in client mode, whose shell has
1885
+ * no boundaries to settle).
1886
+ */
1887
+ let renderMode = 'stream';
1888
+ let renderModePath = null;
1763
1889
  function requireEntries() {
1764
1890
  // config() always runs before resolveId/load/configureServer.
1765
1891
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1766
1892
  return entries;
1767
1893
  }
1894
+ /**
1895
+ * Resolve @solidjs/start-devtools for generated code: from the app graph
1896
+ * first (the documented install location), then from the plugin's own
1897
+ * file — in pnpm-isolated apps a copy that is only a dependency of the
1898
+ * plugin is not reachable from the app's importers. Resolving from the
1899
+ * plugin's own file never yields null when the package is absent: it is
1900
+ * declared an optional peer dependency, so Vite answers with its
1901
+ * `__vite-optional-peer-dep:` stub (an empty module). That stub counts as
1902
+ * "not installed".
1903
+ */
1904
+ async function resolveDevtoolsId(resolve, importer) {
1905
+ const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1906
+ return realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? realId(await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url)));
1907
+ }
1768
1908
  async function resolveDevtools(resolve, importer, consumer) {
1769
1909
  if (!devtoolsEnabled) return false;
1770
- // Detect from the app graph first (the documented install location), then
1771
- // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1772
- // dependency of the plugin is not reachable from the app's importers. The
1773
- // resolved id is kept so imports from generated modules can use it.
1774
- devtoolsResolutions[consumer] ??= (async () => {
1775
- // Resolving from the plugin's own file never yields null when the
1776
- // package is absent: it is declared an optional peer dependency, so
1777
- // Vite answers with its `__vite-optional-peer-dep:` stub (an empty
1778
- // module). Treat that stub as "not installed".
1779
- const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1780
- return realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? realId(await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url)));
1781
- })();
1782
- const id = await devtoolsResolutions[consumer];
1783
- devtoolsIds[consumer] = id;
1784
- if (!id && options.devtools === true) {
1910
+ // An install cannot change under a running server, so the verdict is
1911
+ // memoized per consumer. The id it was reached with is not reused for
1912
+ // generated imports: resolveId resolves afresh from the same importer.
1913
+ devtoolsImporters[consumer] ??= importer;
1914
+ devtoolsDetections[consumer] ??= resolveDevtoolsId(resolve, importer).then(id => id !== null);
1915
+ const detected = await devtoolsDetections[consumer];
1916
+ if (!detected && options.devtools === true) {
1785
1917
  throw new Error('[@solidjs/vite-plugin] start.devtools requires @solidjs/start-devtools. ' + 'Install it as a development dependency or set start.devtools to false.');
1786
1918
  }
1787
- return id !== null;
1919
+ return detected;
1788
1920
  }
1789
1921
 
1790
1922
  /**
@@ -1959,7 +2091,7 @@ function startServe(options, internal = {}) {
1959
2091
  entryClient
1960
2092
  } = requireEntries();
1961
2093
  const composeServerFunctions = internal.serverFunctions;
1962
- 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)};`] : [])];
2094
+ 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)};`] : [])];
1963
2095
  if (isBuild) {
1964
2096
  lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
1965
2097
  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;`,
@@ -1981,6 +2113,21 @@ function startServe(options, internal = {}) {
1981
2113
  lines.push(`const runMiddleware = (request, next) => next(request);`);
1982
2114
  }
1983
2115
 
2116
+ // Render mode (`start.renderMode`): 'stream' flushes the shell with
2117
+ // fallbacks in place and streams boundary content after it; 'async'
2118
+ // adopts the renderToStream result's thenable — which waits for the
2119
+ // complete render — so one settled document goes out (the fix for
2120
+ // no-JS clients, solidjs/solid#3280). Precedence per request: the
2121
+ // `handleRequest` option (hosts driving the handler directly), then the
2122
+ // configured module's per-request result, then the static config. Every
2123
+ // source is validated against the two literals with the offender named.
2124
+ 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;`, `}`);
2125
+ if (renderModePath) {
2126
+ 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');`, `}`);
2127
+ } else {
2128
+ lines.push(`function resolveRenderMode(event, options) {`, ` if (options.renderMode !== undefined) return assertRenderMode(options.renderMode, 'handleRequest options.renderMode');`, ` return ${JSON.stringify(renderMode)};`, `}`);
2129
+ }
2130
+
1984
2131
  // No `_$SC` bootstrap injection: the runtime's serialized
1985
2132
  // server-component references self-bootstrap the registry (each
1986
2133
  // hydration script's first reference carries it as an idempotent
@@ -2061,7 +2208,11 @@ function startServe(options, internal = {}) {
2061
2208
  // flag, so external-host dispatch and preview stay render-always.
2062
2209
  lines.push(` if (options.pageRequest === false) {`, ` return new Response(null, { status: 404, headers: { ${JSON.stringify(DEV_FALLTHROUGH_HEADER)}: '1' } });`, ` }`);
2063
2210
  }
2064
- lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2211
+ lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`,
2212
+ // Decided before the render starts (the module form may be async),
2213
+ // inside the request scope and after the middleware chain, so a
2214
+ // per-request policy sees the decorated event.
2215
+ ` const renderMode = await resolveRenderMode(event, options);`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2065
2216
  // renderToStream results are thenables whose then() waits for the
2066
2217
  // *complete* render — check for pipe first so streaming survives, and
2067
2218
  // only await plain promises (async render functions).
@@ -2070,6 +2221,14 @@ function startServe(options, internal = {}) {
2070
2221
  // entry): a bare promise resolution would adopt the stream's
2071
2222
  // thenable and buffer the whole render.
2072
2223
  ` if (result && result.${STREAM_BOX}) result = result.${STREAM_BOX};`] : []),
2224
+ // Async mode adopts the thenable deliberately — the very thing the
2225
+ // pipe-first check and the setup box exist to avoid in stream mode.
2226
+ // It resolves with the full HTML once every boundary settled, each
2227
+ // spliced in place pre-flush (no fallbacks, no swap scripts; hydration
2228
+ // data still serialized), and the string then takes
2229
+ // createSSRResponse's string path below: stub commit, transformChunk
2230
+ // on the whole document, and a mid-render Location as a real 3xx.
2231
+ ` if (renderMode === 'async' && result && typeof result.then === 'function') {`, ` result = await result;`, ` }`,
2073
2232
  // Raw Responses fold at the handler edge (handleRequest), after the
2074
2233
  // middleware chain unwinds — not here, where middleware above this
2075
2234
  // frame could still legitimately mutate headers.
@@ -2108,8 +2267,8 @@ function startServe(options, internal = {}) {
2108
2267
  config(userConfig, env) {
2109
2268
  root = path.resolve(userConfig.root || process.cwd());
2110
2269
  devtoolsEnabled = env.command === 'serve' && !env.isPreview && options.devtools !== false;
2111
- devtoolsResolutions = {};
2112
- devtoolsIds = {};
2270
+ devtoolsDetections = {};
2271
+ devtoolsImporters = {};
2113
2272
  entries = resolveEntries(root, options, clientMode);
2114
2273
  internal.onDocumentResolved?.(entries.document);
2115
2274
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
@@ -2121,6 +2280,17 @@ function startServe(options, internal = {}) {
2121
2280
  // hook needs does not exist there.
2122
2281
  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}`);
2123
2282
  }
2283
+ // Server-mode only as well: the client-mode shell renders no app,
2284
+ // so there is nothing to settle. Validated in every mode though —
2285
+ // a typo should not hide behind the `ssr` boolean.
2286
+ ({
2287
+ mode: renderMode,
2288
+ path: renderModePath
2289
+ } = resolveRenderMode(root, options.renderMode));
2290
+ if (clientMode) {
2291
+ renderMode = 'stream';
2292
+ renderModePath = null;
2293
+ }
2124
2294
  if (env.isPreview) {
2125
2295
  if (clientMode) {
2126
2296
  // Client-mode builds emit a real dist/client/index.html (the
@@ -2262,7 +2432,7 @@ function startServe(options, internal = {}) {
2262
2432
  diagnostics = detectDiagnosticsPackage(root);
2263
2433
  }
2264
2434
  },
2265
- resolveId(source, importer, opts) {
2435
+ async resolveId(source, importer, opts) {
2266
2436
  if (source === HANDLER_ID) {
2267
2437
  return {
2268
2438
  id: HANDLER_ID,
@@ -2287,13 +2457,25 @@ function startServe(options, internal = {}) {
2287
2457
  moduleSideEffects: true
2288
2458
  };
2289
2459
  }
2290
- // Generated modules have no directory for bare-package resolution.
2291
- // Reuse the app-relative id captured during detection.
2292
- const devtoolsId = devtoolsIds[getEnvironmentConsumer(this.environment, opts)];
2293
- if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2294
- return {
2295
- id: devtoolsId
2296
- };
2460
+ if (source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2461
+ // Generated modules have no directory for bare-package resolution:
2462
+ // resolve from the app importer detection probed. Resolve afresh on
2463
+ // every request rather than reusing detection's id in the client
2464
+ // environment that id is the optimizer's pre-bundled URL, stamped
2465
+ // with the browserHash of the pass that produced it. Any dependency
2466
+ // discovered after the initial scan re-optimizes: the toolbar's
2467
+ // chunks are re-emitted under new names and the hash moves on, and
2468
+ // a frozen id would keep the entry on the previous pass — its lazy
2469
+ // chunks answer 504 (Outdated Optimize Dep) and the stale bundle
2470
+ // brings a second solid-js instance into the page.
2471
+ const from = devtoolsImporters[getEnvironmentConsumer(this.environment, opts)];
2472
+ if (!from) return null;
2473
+ const id = await resolveDevtoolsId((s, i) => this.resolve(s, i, {
2474
+ skipSelf: true
2475
+ }), from);
2476
+ return id ? {
2477
+ id
2478
+ } : null;
2297
2479
  }
2298
2480
  return null;
2299
2481
  },
@@ -3345,7 +3527,69 @@ function combineSourcemaps(maps) {
3345
3527
  // remapping expects most-recent-first.
3346
3528
  return JSON.parse(remapping(chain.reverse(), () => null).toString());
3347
3529
  }
3530
+ function toPosixPath(p) {
3531
+ return p.split(path.sep).join('/');
3532
+ }
3533
+ function tryRealpath(p) {
3534
+ try {
3535
+ return realpathSync.native(p);
3536
+ } catch {
3537
+ return null;
3538
+ }
3539
+ }
3540
+
3541
+ /** The `input` a build environment's config resolves to, in any spelling. */
3542
+ function configuredBuildInput(build) {
3543
+ if (!build) return undefined;
3544
+ return build.rolldownOptions?.input ?? build.rollupOptions?.input ?? build.lib?.entry;
3545
+ }
3348
3546
 
3547
+ /**
3548
+ * The genuine entries of a client build, derived from its configured input
3549
+ * (`build.rollupOptions.input` as a string / array / record, or Vite's
3550
+ * default `index.html`). Rollup and rolldown only ever flag two kinds of
3551
+ * chunk `isEntry`: those facades and chunks plugins emit with
3552
+ * `emitFile({ type: 'chunk' })` — so this is exactly the knowledge that
3553
+ * tells a real application entry apart from an emitted lazy facade.
3554
+ *
3555
+ * `moduleIds` — every spelling the entry's facade module id can take: as
3556
+ * written (virtual ids resolve to themselves), resolved against the root
3557
+ * (Vite resolves relative file inputs there), and the real path of either
3558
+ * (Vite's resolver follows symlinks).
3559
+ * `manifestKeys` — the manifest.json keys Vite derives from those facades
3560
+ * (root-relative, `\0` stripped), matching Vite's own `getChunkName`.
3561
+ */
3562
+ function resolveConfiguredEntries(input, root) {
3563
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3564
+ const moduleIds = new Set();
3565
+ for (const id of raw) {
3566
+ if (typeof id !== 'string') continue;
3567
+ const clean = id.replace(/\0/g, '');
3568
+ const candidates = [clean, path.resolve(root, clean)];
3569
+ for (const candidate of candidates) {
3570
+ moduleIds.add(candidate);
3571
+ moduleIds.add(toPosixPath(candidate));
3572
+ const real = tryRealpath(candidate);
3573
+ if (real) {
3574
+ moduleIds.add(real);
3575
+ moduleIds.add(toPosixPath(real));
3576
+ }
3577
+ }
3578
+ }
3579
+ const manifestKeys = new Set();
3580
+ for (const id of moduleIds) manifestKeys.add(toPosixPath(path.relative(root, id)));
3581
+ return {
3582
+ moduleIds,
3583
+ manifestKeys,
3584
+ isEntryModule(id) {
3585
+ if (!id) return false;
3586
+ const clean = id.replace(/\0/g, '');
3587
+ if (moduleIds.has(clean) || moduleIds.has(toPosixPath(clean))) return true;
3588
+ const real = tryRealpath(clean);
3589
+ return !!real && (moduleIds.has(real) || moduleIds.has(toPosixPath(real)));
3590
+ }
3591
+ };
3592
+ }
3349
3593
  /**
3350
3594
  * Chunks emitted for lazy() targets are marked `isEntry` by Rollup even
3351
3595
  * though they are semantically dynamic entries. Reclassify any entry that is
@@ -3354,18 +3598,57 @@ function combineSourcemaps(maps) {
3354
3598
  * the real client entry. Works on both the Vite manifest.json shape and the
3355
3599
  * raw Rollup output bundle — both key entries by name and expose
3356
3600
  * `dynamicImports` / `isEntry` with the same meaning.
3601
+ *
3602
+ * Being a dynamic-import target alone does not make a chunk a lazy facade,
3603
+ * though: the real client entry becomes one whenever it absorbs a module
3604
+ * that is also dynamically imported somewhere else. Solid 2 produces that
3605
+ * shape on its own — `@solidjs/web/frames/client` lazily imports the
3606
+ * serialization decoder (`loadCodec()`), so a static import of
3607
+ * `@solidjs/web/serialization/decode` anywhere in the client graph merges
3608
+ * the decoder into the entry chunk, and the entry then lists itself (or is
3609
+ * listed by another lazy chunk) under `dynamicImports`. Stripping `isEntry`
3610
+ * there leaves the bundle with no entry at all ("No entry file found"
3611
+ * downstream, e.g. TanStack Start's manifest capture, #342). Genuine
3612
+ * configured entries are therefore never reclassified, and a chunk's
3613
+ * dynamic import of itself is not an edge worth acting on.
3614
+ *
3615
+ * Rolldown caveat: of the flags written here only `isEntry` is synced back
3616
+ * to the native bundle after the hook (rolldown's `update_output_chunk`
3617
+ * copies `code`, `map`, `imports`, `dynamicImports`, `isEntry` and the file
3618
+ * name; `isDynamicEntry` is kept from the original chunk). Later plugins
3619
+ * and Vite's manifest plugin therefore see reclassified facades as neither
3620
+ * entry nor dynamic entry under rolldown. The manifest `load` path repairs
3621
+ * `isDynamicEntry` on the plugin's own manifest module, the one place it
3622
+ * controls end to end.
3357
3623
  */
3358
- function normalizeEmittedLazyEntries(manifest) {
3359
- const dynamicKeys = new Set();
3624
+ function normalizeEmittedLazyEntries(manifest, {
3625
+ isConfiguredEntry,
3626
+ knownLazyKeys,
3627
+ warn,
3628
+ repairDynamicEntries
3629
+ }) {
3630
+ const dynamicKeys = new Map();
3360
3631
  for (const key in manifest) {
3361
3632
  const imports = manifest[key].dynamicImports;
3362
- if (imports) for (const dep of imports) dynamicKeys.add(dep);
3633
+ if (!imports) continue;
3634
+ for (const dep of imports) {
3635
+ // A chunk that absorbed one of its own lazy targets imports itself;
3636
+ // that says nothing about whether it is an entry.
3637
+ if (dep !== key && !dynamicKeys.has(dep)) dynamicKeys.set(dep, key);
3638
+ }
3363
3639
  }
3364
- for (const key of dynamicKeys) {
3640
+ for (const [key, importer] of dynamicKeys) {
3365
3641
  const entry = manifest[key];
3366
- if (entry && entry.isEntry) {
3642
+ if (!entry || entry.type === 'asset') continue;
3643
+ if (isConfiguredEntry(key, entry)) continue;
3644
+ if (entry.isEntry) {
3367
3645
  entry.isEntry = false;
3368
3646
  entry.isDynamicEntry = true;
3647
+ if (warn && !knownLazyKeys?.has(key)) {
3648
+ 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.');
3649
+ }
3650
+ } else if (repairDynamicEntries && !entry.isDynamicEntry) {
3651
+ entry.isDynamicEntry = true;
3369
3652
  }
3370
3653
  }
3371
3654
  }
@@ -3427,6 +3710,11 @@ function solidPlugin(options = {}) {
3427
3710
  let isSsrBuild = false;
3428
3711
  let base = '/';
3429
3712
  let clientOutDir = null;
3713
+ // The client environment's resolved build options, for the configured
3714
+ // entry input. Read off the resolved config so the SSR half of a
3715
+ // two-invocation build (`vite build --ssr`) still knows the client's
3716
+ // entries when it bakes the client manifest in.
3717
+ let clientBuildConfig = null;
3430
3718
  let solidPkgsConfig;
3431
3719
  const tsrxCss = new Map();
3432
3720
 
@@ -3732,6 +4020,29 @@ function solidPlugin(options = {}) {
3732
4020
  // So the dev flag has to reach both lists.
3733
4021
  if (replaceDev && config.consumer !== 'client' && name !== 'client') {
3734
4022
  config.resolve.externalConditions = ['development', ...(config.resolve.externalConditions ?? defaultExternalConditions)];
4023
+
4024
+ // `externalConditions` only reaches the imports the module runner
4025
+ // resolves itself. An externalized package's OWN imports are resolved
4026
+ // by Node, with Node's conditions — never `development`. Since
4027
+ // solid 2.0.0-rc.7 both `solid-js` and `@solidjs/web` ship a
4028
+ // `dist/server.dev.*` behind that condition, so leaving them external
4029
+ // splits the framework in two under `vite dev`: the app's `solid-js`
4030
+ // is the runner's dev copy while `@solidjs/web`'s `import "solid-js"`
4031
+ // lands on Node's prod copy. `renderToStream` then installs the asset
4032
+ // resolver on one `sharedConfig` and `lazy()` reads the other ("no
4033
+ // asset manifest is set"), with every other module-level singleton
4034
+ // (owner tracking, request events, hydration keys) split the same
4035
+ // way. Inlining the two core packages makes every resolution — theirs
4036
+ // included — go through the environment's conditions, so one dev
4037
+ // build is loaded end to end. Framework packages that declare the
4038
+ // `solid` export condition are already inlined via vitefu below and
4039
+ // reach the same copy. Vitest projects manage their own inlining
4040
+ // (`test.server.deps` above) and are left alone, as is a host that
4041
+ // set `noExternal: true` (everything is inlined already).
4042
+ if (!isTestMode && config.resolve.noExternal !== true) {
4043
+ const noExternal = config.resolve.noExternal;
4044
+ config.resolve.noExternal = [...(Array.isArray(noExternal) ? noExternal : noExternal ? [noExternal] : []), 'solid-js', '@solidjs/web'];
4045
+ }
3735
4046
  }
3736
4047
 
3737
4048
  // Set resolve.noExternal and resolve.external for the SSR environment.
@@ -3748,6 +4059,7 @@ function solidPlugin(options = {}) {
3748
4059
  isSsrBuild = !!config.build.ssr;
3749
4060
  base = config.base;
3750
4061
  projectRoot = config.root;
4062
+ clientBuildConfig = config.environments?.client?.build ?? config.build;
3751
4063
  filter = createFilter(options.include, options.exclude, {
3752
4064
  resolve: projectRoot
3753
4065
  });
@@ -3881,7 +4193,23 @@ function solidPlugin(options = {}) {
3881
4193
  const manifestPath = clientManifestPath();
3882
4194
  if (manifestPath) {
3883
4195
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
3884
- normalizeEmittedLazyEntries(manifest);
4196
+ // Manifest records are keyed the way Vite keys entry chunks (the
4197
+ // root-relative facade path, also carried as `src`), so the
4198
+ // configured client inputs identify the genuine entries here too —
4199
+ // independent of `isEntry`, which the serialized manifest may have
4200
+ // lost already (older plugin builds stripped it; see #342).
4201
+ const entries = resolveConfiguredEntries(configuredBuildInput(clientBuildConfig), projectRoot);
4202
+ const isConfiguredEntry = (key, record) => entries.manifestKeys.has(key) || typeof record.src === 'string' && entries.manifestKeys.has(record.src);
4203
+ for (const key in manifest) {
4204
+ if (isConfiguredEntry(key, manifest[key]) && manifest[key].file) {
4205
+ manifest[key].isEntry = true;
4206
+ }
4207
+ }
4208
+ normalizeEmittedLazyEntries(manifest, {
4209
+ isConfiguredEntry,
4210
+ warn: message => this.warn(message),
4211
+ repairDynamicEntries: true
4212
+ });
3885
4213
  manifest._base = base;
3886
4214
  return `export default ${JSON.stringify(manifest)};`;
3887
4215
  }
@@ -3898,6 +4226,12 @@ function solidPlugin(options = {}) {
3898
4226
  // the bundle don't mistake them for application entries. Must precede
3899
4227
  // the client asset map build, which keys off dynamic entries.
3900
4228
  if (options.ssr) {
4229
+ // The genuine entries are the configured inputs of this very
4230
+ // environment — the plugin injects the client entry itself in start
4231
+ // mode, and Vite's default is index.html — so their facade chunks
4232
+ // are recognizable regardless of what dynamically imports them.
4233
+ const entries = resolveConfiguredEntries(configuredBuildInput(this.environment?.config?.build ?? clientBuildConfig), projectRoot);
4234
+ const knownLazyKeys = new Set();
3901
4235
  for (const ref of emittedLazyChunkRefs) {
3902
4236
  let fileName;
3903
4237
  try {
@@ -3908,10 +4242,17 @@ function solidPlugin(options = {}) {
3908
4242
  }
3909
4243
  const chunk = bundle[fileName];
3910
4244
  if (!chunk || chunk.type !== 'chunk') continue;
4245
+ // An entry that is also lazily imported stays an entry.
4246
+ if (entries.isEntryModule(chunk.facadeModuleId)) continue;
4247
+ knownLazyKeys.add(fileName);
3911
4248
  chunk.isEntry = false;
3912
4249
  chunk.isDynamicEntry = true;
3913
4250
  }
3914
- normalizeEmittedLazyEntries(bundle);
4251
+ normalizeEmittedLazyEntries(bundle, {
4252
+ isConfiguredEntry: (_key, chunk) => entries.isEntryModule(chunk.facadeModuleId),
4253
+ knownLazyKeys,
4254
+ warn: message => this.warn(message)
4255
+ });
3915
4256
  }
3916
4257
  },
3917
4258
  async transform(source, id, transformOptions) {