@solidjs/vite-plugin 3.0.0-next.39 → 3.0.0-next.41

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;
@@ -1253,6 +1270,23 @@ function serverFunctions(options = {}, internal = {}) {
1253
1270
  client: undefined
1254
1271
  };
1255
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)};`;
1256
1290
  const clientOptions = {
1257
1291
  directive,
1258
1292
  definitions: {
@@ -1312,6 +1346,14 @@ function serverFunctions(options = {}, internal = {}) {
1312
1346
  // import is only emitted when the option is on, so disabled setups keep
1313
1347
  // a server-component-free graph.
1314
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,
1315
1357
  // The user's `configure` module comes first: a side-effect import in
1316
1358
  // the handler graph, evaluated before any dispatch on both surfaces
1317
1359
  // (dev middleware and prod handler) and bundled into the handler
@@ -1641,6 +1683,11 @@ function devtoolsMountModuleCode() {
1641
1683
  // it, and page responses go through the runtime's `createSSRResponse`
1642
1684
  // head lifecycle (commit at shell flush, real pre-flush redirects, the
1643
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.
1644
1691
  // - `vite preview` serves dist/client statically and dispatches everything
1645
1692
  // else through the built handler — the production path, middleware
1646
1693
  // included, with no server file needed.
@@ -1716,6 +1763,37 @@ function normalizeUserPath(root, spec, option) {
1716
1763
  }
1717
1764
  return relative;
1718
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
+ }
1719
1797
  function resolveEntries(root, options, clientMode) {
1720
1798
  const explicitClient = options.entryClient ? normalizeUserPath(root, options.entryClient, 'entryClient') : null;
1721
1799
  if (clientMode) {
@@ -1808,8 +1886,11 @@ function startServe(options, internal = {}) {
1808
1886
  // any of the (lazy) uses in entry codegen and the entry transform.
1809
1887
  let diagnostics = internal.diagnostics === true;
1810
1888
  let devtoolsEnabled = false;
1811
- let devtoolsResolutions = {};
1812
- 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 = {};
1813
1894
  // `external` is server-mode-only (documented no-op in client mode, so a
1814
1895
  // host-integrated config survives the `ssr` boolean flip untouched).
1815
1896
  const externalServer = !clientMode && !!options.external;
@@ -1821,31 +1902,45 @@ function startServe(options, internal = {}) {
1821
1902
  let middlewarePath = null;
1822
1903
  /** Absolute path of the per-request setup module, when configured (server mode). */
1823
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;
1824
1913
  function requireEntries() {
1825
1914
  // config() always runs before resolveId/load/configureServer.
1826
1915
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1827
1916
  return entries;
1828
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
+ }
1829
1932
  async function resolveDevtools(resolve, importer, consumer) {
1830
1933
  if (!devtoolsEnabled) return false;
1831
- // Detect from the app graph first (the documented install location), then
1832
- // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1833
- // dependency of the plugin is not reachable from the app's importers. The
1834
- // resolved id is kept so imports from generated modules can use it.
1835
- devtoolsResolutions[consumer] ??= (async () => {
1836
- // Resolving from the plugin's own file never yields null when the
1837
- // package is absent: it is declared an optional peer dependency, so
1838
- // Vite answers with its `__vite-optional-peer-dep:` stub (an empty
1839
- // module). Treat that stub as "not installed".
1840
- const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1841
- 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)))));
1842
- })();
1843
- const id = await devtoolsResolutions[consumer];
1844
- devtoolsIds[consumer] = id;
1845
- 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) {
1846
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.');
1847
1942
  }
1848
- return id !== null;
1943
+ return detected;
1849
1944
  }
1850
1945
 
1851
1946
  /**
@@ -2020,13 +2115,17 @@ function startServe(options, internal = {}) {
2020
2115
  entryClient
2021
2116
  } = requireEntries();
2022
2117
  const composeServerFunctions = internal.serverFunctions;
2023
- 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)};`] : [])];
2024
2119
  if (isBuild) {
2025
2120
  lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
2026
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;`,
2027
- // The plugin's manifest module normalizes lazy facade chunks
2028
- // (isDynamicEntry) so exactly one real entry remains flagged.
2029
- ` for (const key in manifest) {`, ` const chunk = manifest[key];`, ` if (chunk && chunk.isEntry && chunk.file) {`, ` clientEntryUrl = joinAssetPath(manifest._base, chunk.file);`, ` break;`, ` }`, ` }`, ` return clientEntryUrl;`, `}`);
2122
+ // The plugin's manifest module names the client entry it injected
2123
+ // into the build (`_entry`): every configured input is a genuine
2124
+ // `isEntry` record (a filesystem router's `buildInputs` lists every
2125
+ // route module), so scanning for the first flagged record would pick
2126
+ // whichever sorts first (#353). The scan stays as the fallback for
2127
+ // hand-rolled manifests without the stamp.
2128
+ ` const stamped = manifest._entry && manifest[manifest._entry];`, ` if (stamped && stamped.file) {`, ` return (clientEntryUrl = joinAssetPath(manifest._base, stamped.file));`, ` }`, ` for (const key in manifest) {`, ` const chunk = manifest[key];`, ` if (chunk && chunk.isEntry && chunk.file) {`, ` clientEntryUrl = joinAssetPath(manifest._base, chunk.file);`, ` break;`, ` }`, ` }`, ` return clientEntryUrl;`, `}`);
2030
2129
  } else {
2031
2130
  const devHead = `<script>${devStylePatch}</script>` + `<script type="module" src="${joinBase(base, '/@vite/client')}"></script>`;
2032
2131
  lines.push(``, `const DEV_HEAD = ${JSON.stringify(devHead)};`);
@@ -2042,6 +2141,21 @@ function startServe(options, internal = {}) {
2042
2141
  lines.push(`const runMiddleware = (request, next) => next(request);`);
2043
2142
  }
2044
2143
 
2144
+ // Render mode (`start.renderMode`): 'stream' flushes the shell with
2145
+ // fallbacks in place and streams boundary content after it; 'async'
2146
+ // adopts the renderToStream result's thenable — which waits for the
2147
+ // complete render — so one settled document goes out (the fix for
2148
+ // no-JS clients, solidjs/solid#3280). Precedence per request: the
2149
+ // `handleRequest` option (hosts driving the handler directly), then the
2150
+ // configured module's per-request result, then the static config. Every
2151
+ // source is validated against the two literals with the offender named.
2152
+ 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;`, `}`);
2153
+ if (renderModePath) {
2154
+ 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');`, `}`);
2155
+ } else {
2156
+ lines.push(`function resolveRenderMode(event, options) {`, ` if (options.renderMode !== undefined) return assertRenderMode(options.renderMode, 'handleRequest options.renderMode');`, ` return ${JSON.stringify(renderMode)};`, `}`);
2157
+ }
2158
+
2045
2159
  // No `_$SC` bootstrap injection: the runtime's serialized
2046
2160
  // server-component references self-bootstrap the registry (each
2047
2161
  // hydration script's first reference carries it as an idempotent
@@ -2122,7 +2236,11 @@ function startServe(options, internal = {}) {
2122
2236
  // flag, so external-host dispatch and preview stay render-always.
2123
2237
  lines.push(` if (options.pageRequest === false) {`, ` return new Response(null, { status: 404, headers: { ${JSON.stringify(DEV_FALLTHROUGH_HEADER)}: '1' } });`, ` }`);
2124
2238
  }
2125
- lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2239
+ lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`,
2240
+ // Decided before the render starts (the module form may be async),
2241
+ // inside the request scope and after the middleware chain, so a
2242
+ // per-request policy sees the decorated event.
2243
+ ` const renderMode = await resolveRenderMode(event, options);`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2126
2244
  // renderToStream results are thenables whose then() waits for the
2127
2245
  // *complete* render — check for pipe first so streaming survives, and
2128
2246
  // only await plain promises (async render functions).
@@ -2131,6 +2249,14 @@ function startServe(options, internal = {}) {
2131
2249
  // entry): a bare promise resolution would adopt the stream's
2132
2250
  // thenable and buffer the whole render.
2133
2251
  ` if (result && result.${STREAM_BOX}) result = result.${STREAM_BOX};`] : []),
2252
+ // Async mode adopts the thenable deliberately — the very thing the
2253
+ // pipe-first check and the setup box exist to avoid in stream mode.
2254
+ // It resolves with the full HTML once every boundary settled, each
2255
+ // spliced in place pre-flush (no fallbacks, no swap scripts; hydration
2256
+ // data still serialized), and the string then takes
2257
+ // createSSRResponse's string path below: stub commit, transformChunk
2258
+ // on the whole document, and a mid-render Location as a real 3xx.
2259
+ ` if (renderMode === 'async' && result && typeof result.then === 'function') {`, ` result = await result;`, ` }`,
2134
2260
  // Raw Responses fold at the handler edge (handleRequest), after the
2135
2261
  // middleware chain unwinds — not here, where middleware above this
2136
2262
  // frame could still legitimately mutate headers.
@@ -2169,8 +2295,8 @@ function startServe(options, internal = {}) {
2169
2295
  config(userConfig, env) {
2170
2296
  root = path.resolve(userConfig.root || process.cwd());
2171
2297
  devtoolsEnabled = env.command === 'serve' && !env.isPreview && options.devtools !== false;
2172
- devtoolsResolutions = {};
2173
- devtoolsIds = {};
2298
+ devtoolsDetections = {};
2299
+ devtoolsImporters = {};
2174
2300
  entries = resolveEntries(root, options, clientMode);
2175
2301
  internal.onDocumentResolved?.(entries.document);
2176
2302
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
@@ -2182,6 +2308,17 @@ function startServe(options, internal = {}) {
2182
2308
  // hook needs does not exist there.
2183
2309
  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}`);
2184
2310
  }
2311
+ // Server-mode only as well: the client-mode shell renders no app,
2312
+ // so there is nothing to settle. Validated in every mode though —
2313
+ // a typo should not hide behind the `ssr` boolean.
2314
+ ({
2315
+ mode: renderMode,
2316
+ path: renderModePath
2317
+ } = resolveRenderMode(root, options.renderMode));
2318
+ if (clientMode) {
2319
+ renderMode = 'stream';
2320
+ renderModePath = null;
2321
+ }
2185
2322
  if (env.isPreview) {
2186
2323
  if (clientMode) {
2187
2324
  // Client-mode builds emit a real dist/client/index.html (the
@@ -2212,6 +2349,7 @@ function startServe(options, internal = {}) {
2212
2349
  }
2213
2350
  const build = env.command === 'build';
2214
2351
  const clientInput = entries.generated ? ENTRY_CLIENT_ID : path.resolve(root, entries.entryClient);
2352
+ internal.onClientEntryResolved?.(clientInput);
2215
2353
  // Real files only — the dep scanner can't crawl virtual modules.
2216
2354
  // (In client mode the resolved document joins the scan/style roots
2217
2355
  // even with an authored client entry; in SSR mode authored entries
@@ -2323,7 +2461,7 @@ function startServe(options, internal = {}) {
2323
2461
  diagnostics = detectDiagnosticsPackage(root);
2324
2462
  }
2325
2463
  },
2326
- resolveId(source, importer, opts) {
2464
+ async resolveId(source, importer, opts) {
2327
2465
  if (source === HANDLER_ID) {
2328
2466
  return {
2329
2467
  id: HANDLER_ID,
@@ -2348,13 +2486,25 @@ function startServe(options, internal = {}) {
2348
2486
  moduleSideEffects: true
2349
2487
  };
2350
2488
  }
2351
- // Generated modules have no directory for bare-package resolution.
2352
- // Reuse the app-relative id captured during detection.
2353
- const devtoolsId = devtoolsIds[getEnvironmentConsumer(this.environment, opts)];
2354
- if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2355
- return {
2356
- id: devtoolsId
2357
- };
2489
+ if (source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2490
+ // Generated modules have no directory for bare-package resolution:
2491
+ // resolve from the app importer detection probed. Resolve afresh on
2492
+ // every request rather than reusing detection's id in the client
2493
+ // environment that id is the optimizer's pre-bundled URL, stamped
2494
+ // with the browserHash of the pass that produced it. Any dependency
2495
+ // discovered after the initial scan re-optimizes: the toolbar's
2496
+ // chunks are re-emitted under new names and the hash moves on, and
2497
+ // a frozen id would keep the entry on the previous pass — its lazy
2498
+ // chunks answer 504 (Outdated Optimize Dep) and the stale bundle
2499
+ // brings a second solid-js instance into the page.
2500
+ const from = devtoolsImporters[getEnvironmentConsumer(this.environment, opts)];
2501
+ if (!from) return null;
2502
+ const id = await resolveDevtoolsId((s, i) => this.resolve(s, i, {
2503
+ skipSelf: true
2504
+ }), from);
2505
+ return id ? {
2506
+ id
2507
+ } : null;
2358
2508
  }
2359
2509
  return null;
2360
2510
  },
@@ -3305,6 +3455,23 @@ function getExtension(filename) {
3305
3455
  const index = filename.lastIndexOf('.');
3306
3456
  return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, '');
3307
3457
  }
3458
+ // The packages whose dev/production server builds are selected by the
3459
+ // `development` export condition. A dependency on either means the package
3460
+ // consumes the runtime and must resolve it through Vite in dev.
3461
+ const SOLID_RUNTIME_PKGS = ['solid-js', '@solidjs/web'];
3462
+
3463
+ // Tooling that declares solid-js as a peer but never runs inside the SSR
3464
+ // module runner. Kept out of the crawl entirely: classifying them as
3465
+ // semi-framework would also crawl THEIR dependencies, which vitefu deep-
3466
+ // includes in the client optimizer (`@solidjs/vite-plugin > @babel/core`
3467
+ // pre-bundled for the browser — ~2.6 MB of dead weight per cold start).
3468
+ // Mirrors vite-plugin-svelte's isCommonDepWithoutSvelteField list.
3469
+ const NON_RUNTIME_SOLID_PKGS = ['@solidjs/vite-plugin', 'vite', 'vitest', 'eslint-plugin-solid'];
3470
+ const NON_RUNTIME_SOLID_PREFIXES = ['vite-plugin-', 'eslint-plugin-', 'prettier-plugin-', '@types/'];
3471
+ function isNonRuntimeSolidPkg(name) {
3472
+ const bare = name.slice(name.lastIndexOf('/') + 1);
3473
+ return NON_RUNTIME_SOLID_PKGS.includes(name) || NON_RUNTIME_SOLID_PREFIXES.some(p => (p.startsWith('@') ? name : bare).startsWith(p));
3474
+ }
3308
3475
  function containsSolidField(fields) {
3309
3476
  const keys = Object.keys(fields);
3310
3477
  for (let i = 0; i < keys.length; i++) {
@@ -3406,7 +3573,69 @@ function combineSourcemaps(maps) {
3406
3573
  // remapping expects most-recent-first.
3407
3574
  return JSON.parse(remapping(chain.reverse(), () => null).toString());
3408
3575
  }
3576
+ function toPosixPath(p) {
3577
+ return p.split(path.sep).join('/');
3578
+ }
3579
+ function tryRealpath(p) {
3580
+ try {
3581
+ return fs.realpathSync.native(p);
3582
+ } catch {
3583
+ return null;
3584
+ }
3585
+ }
3586
+
3587
+ /** The `input` a build environment's config resolves to, in any spelling. */
3588
+ function configuredBuildInput(build) {
3589
+ if (!build) return undefined;
3590
+ return build.rolldownOptions?.input ?? build.rollupOptions?.input ?? build.lib?.entry;
3591
+ }
3409
3592
 
3593
+ /**
3594
+ * The genuine entries of a client build, derived from its configured input
3595
+ * (`build.rollupOptions.input` as a string / array / record, or Vite's
3596
+ * default `index.html`). Rollup and rolldown only ever flag two kinds of
3597
+ * chunk `isEntry`: those facades and chunks plugins emit with
3598
+ * `emitFile({ type: 'chunk' })` — so this is exactly the knowledge that
3599
+ * tells a real application entry apart from an emitted lazy facade.
3600
+ *
3601
+ * `moduleIds` — every spelling the entry's facade module id can take: as
3602
+ * written (virtual ids resolve to themselves), resolved against the root
3603
+ * (Vite resolves relative file inputs there), and the real path of either
3604
+ * (Vite's resolver follows symlinks).
3605
+ * `manifestKeys` — the manifest.json keys Vite derives from those facades
3606
+ * (root-relative, `\0` stripped), matching Vite's own `getChunkName`.
3607
+ */
3608
+ function resolveConfiguredEntries(input, root) {
3609
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3610
+ const moduleIds = new Set();
3611
+ for (const id of raw) {
3612
+ if (typeof id !== 'string') continue;
3613
+ const clean = id.replace(/\0/g, '');
3614
+ const candidates = [clean, path.resolve(root, clean)];
3615
+ for (const candidate of candidates) {
3616
+ moduleIds.add(candidate);
3617
+ moduleIds.add(toPosixPath(candidate));
3618
+ const real = tryRealpath(candidate);
3619
+ if (real) {
3620
+ moduleIds.add(real);
3621
+ moduleIds.add(toPosixPath(real));
3622
+ }
3623
+ }
3624
+ }
3625
+ const manifestKeys = new Set();
3626
+ for (const id of moduleIds) manifestKeys.add(toPosixPath(path.relative(root, id)));
3627
+ return {
3628
+ moduleIds,
3629
+ manifestKeys,
3630
+ isEntryModule(id) {
3631
+ if (!id) return false;
3632
+ const clean = id.replace(/\0/g, '');
3633
+ if (moduleIds.has(clean) || moduleIds.has(toPosixPath(clean))) return true;
3634
+ const real = tryRealpath(clean);
3635
+ return !!real && (moduleIds.has(real) || moduleIds.has(toPosixPath(real)));
3636
+ }
3637
+ };
3638
+ }
3410
3639
  /**
3411
3640
  * Chunks emitted for lazy() targets are marked `isEntry` by Rollup even
3412
3641
  * though they are semantically dynamic entries. Reclassify any entry that is
@@ -3415,20 +3644,119 @@ function combineSourcemaps(maps) {
3415
3644
  * the real client entry. Works on both the Vite manifest.json shape and the
3416
3645
  * raw Rollup output bundle — both key entries by name and expose
3417
3646
  * `dynamicImports` / `isEntry` with the same meaning.
3647
+ *
3648
+ * Being a dynamic-import target alone does not make a chunk a lazy facade,
3649
+ * though: the real client entry becomes one whenever it absorbs a module
3650
+ * that is also dynamically imported somewhere else. Solid 2 produces that
3651
+ * shape on its own — `@solidjs/web/frames/client` lazily imports the
3652
+ * serialization decoder (`loadCodec()`), so a static import of
3653
+ * `@solidjs/web/serialization/decode` anywhere in the client graph merges
3654
+ * the decoder into the entry chunk, and the entry then lists itself (or is
3655
+ * listed by another lazy chunk) under `dynamicImports`. Stripping `isEntry`
3656
+ * there leaves the bundle with no entry at all ("No entry file found"
3657
+ * downstream, e.g. TanStack Start's manifest capture, #342). Genuine
3658
+ * configured entries are therefore never reclassified, and a chunk's
3659
+ * dynamic import of itself is not an edge worth acting on.
3660
+ *
3661
+ * Rolldown caveat: of the flags written here only `isEntry` is synced back
3662
+ * to the native bundle after the hook (rolldown's `update_output_chunk`
3663
+ * copies `code`, `map`, `imports`, `dynamicImports`, `isEntry` and the file
3664
+ * name; `isDynamicEntry` is kept from the original chunk). Later plugins
3665
+ * and Vite's manifest plugin therefore see reclassified facades as neither
3666
+ * entry nor dynamic entry under rolldown. The manifest `load` path repairs
3667
+ * `isDynamicEntry` on the plugin's own manifest module, the one place it
3668
+ * controls end to end.
3418
3669
  */
3419
- function normalizeEmittedLazyEntries(manifest) {
3420
- const dynamicKeys = new Set();
3670
+ function normalizeEmittedLazyEntries(manifest, {
3671
+ isConfiguredEntry,
3672
+ knownLazyKeys,
3673
+ warn,
3674
+ repairDynamicEntries
3675
+ }) {
3676
+ const dynamicKeys = new Map();
3421
3677
  for (const key in manifest) {
3422
3678
  const imports = manifest[key].dynamicImports;
3423
- if (imports) for (const dep of imports) dynamicKeys.add(dep);
3679
+ if (!imports) continue;
3680
+ for (const dep of imports) {
3681
+ // A chunk that absorbed one of its own lazy targets imports itself;
3682
+ // that says nothing about whether it is an entry.
3683
+ if (dep !== key && !dynamicKeys.has(dep)) dynamicKeys.set(dep, key);
3684
+ }
3424
3685
  }
3425
- for (const key of dynamicKeys) {
3686
+ for (const [key, importer] of dynamicKeys) {
3426
3687
  const entry = manifest[key];
3427
- if (entry && entry.isEntry) {
3688
+ if (!entry || entry.type === 'asset') continue;
3689
+ if (isConfiguredEntry(key, entry)) continue;
3690
+ if (entry.isEntry) {
3428
3691
  entry.isEntry = false;
3429
3692
  entry.isDynamicEntry = true;
3693
+ if (warn && !knownLazyKeys?.has(key)) {
3694
+ 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.');
3695
+ }
3696
+ } else if (repairDynamicEntries && !entry.isDynamicEntry) {
3697
+ entry.isDynamicEntry = true;
3698
+ }
3699
+ }
3700
+ }
3701
+
3702
+ /**
3703
+ * The manifest key of THE client entry — the chunk whose `<script
3704
+ * type="module">` boots the page and whose static import graph carries the
3705
+ * global CSS. `isEntry` cannot answer this: every configured build input is
3706
+ * a genuine entry (#347 keeps them flagged), and plugins routinely add more
3707
+ * inputs than the application entry (filesystem-routing's `buildInputs`
3708
+ * lists every route module, and route keys sort ahead of the plugin's own
3709
+ * `virtual:` entry). So the identity comes from configuration instead: the
3710
+ * entry start mode injected itself, or — outside start mode — the single
3711
+ * configured input when there is exactly one (including Vite's default
3712
+ * `index.html`). Several inputs and no start entry: no answer (null), and
3713
+ * consumers keep their first-`isEntry` scan.
3714
+ *
3715
+ * Matched by key or `src`, the same two spellings `isConfiguredEntry` uses.
3716
+ */
3717
+ function resolveClientEntryKey(manifest, startClientEntryId, clientBuild, root) {
3718
+ let entryId = startClientEntryId;
3719
+ if (!entryId) {
3720
+ const input = configuredBuildInput(clientBuild);
3721
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3722
+ if (raw.length !== 1 || typeof raw[0] !== 'string') return null;
3723
+ entryId = raw[0];
3724
+ }
3725
+ const {
3726
+ manifestKeys
3727
+ } = resolveConfiguredEntries(entryId, root);
3728
+ for (const key in manifest) {
3729
+ const record = manifest[key];
3730
+ if (!record || typeof record !== 'object' || !record.file) continue;
3731
+ if (manifestKeys.has(key) || typeof record.src === 'string' && manifestKeys.has(record.src)) {
3732
+ return key;
3430
3733
  }
3431
3734
  }
3735
+ return null;
3736
+ }
3737
+
3738
+ /**
3739
+ * Serializes the plugin's manifest module with the client entry made
3740
+ * explicit: `_entry` names its key (the generated handler reads it before
3741
+ * falling back to scanning for `isEntry`), and its record is moved to the
3742
+ * front. The ordering matters for consumers that still identify the entry
3743
+ * by the first `isEntry` record — `@solidjs/web`'s `registerEntryAssets`,
3744
+ * which links the entry graph's stylesheets and modulepreloads into
3745
+ * `<head>`, and hand-rolled server entries — so they and `_entry` agree on
3746
+ * the same chunk. Other configured inputs keep `isEntry`; they are genuine
3747
+ * entries, just not the one the document boots.
3748
+ */
3749
+ function stampClientEntry(manifest, entryKey, base) {
3750
+ const ordered = {};
3751
+ if (entryKey && manifest[entryKey]) {
3752
+ ordered[entryKey] = manifest[entryKey];
3753
+ }
3754
+ for (const key in manifest) {
3755
+ if (key !== entryKey) ordered[key] = manifest[key];
3756
+ }
3757
+ ordered._base = base;
3758
+ if (entryKey && manifest[entryKey]) ordered._entry = entryKey;
3759
+ return ordered;
3432
3760
  }
3433
3761
  function solidPlugin(options = {}) {
3434
3762
  if (typeof options.ssr === 'object') {
@@ -3488,6 +3816,16 @@ function solidPlugin(options = {}) {
3488
3816
  let isSsrBuild = false;
3489
3817
  let base = '/';
3490
3818
  let clientOutDir = null;
3819
+ // The client environment's resolved build options, for the configured
3820
+ // entry input. Read off the resolved config so the SSR half of a
3821
+ // two-invocation build (`vite build --ssr`) still knows the client's
3822
+ // entries when it bakes the client manifest in.
3823
+ let clientBuildConfig = null;
3824
+ // The client entry start mode injects into the client build's input
3825
+ // (reported by startServe): the one input that IS the application entry,
3826
+ // as opposed to further inputs other plugins add (e.g. filesystem-routing's
3827
+ // `buildInputs`, which lists every route module). Null outside start mode.
3828
+ let startClientEntryId = null;
3491
3829
  let solidPkgsConfig;
3492
3830
  const tsrxCss = new Map();
3493
3831
 
@@ -3649,6 +3987,28 @@ function solidPlugin(options = {}) {
3649
3987
  isBuild: command === 'build',
3650
3988
  isFrameworkPkgByJson(pkgJson) {
3651
3989
  return containsSolidField(pkgJson.exports || {});
3990
+ },
3991
+ // `false` = neither framework nor semi-framework, and don't crawl
3992
+ // its deps; `undefined` = unknown, fall through to the json checks.
3993
+ isFrameworkPkgByName(name) {
3994
+ return isNonRuntimeSolidPkg(name) ? false : undefined;
3995
+ },
3996
+ // Under `vite dev` the runtime must not be split in two. Inlined
3997
+ // modules resolve `solid-js` through Vite with `development` (its dev
3998
+ // server build); an externalized package's own imports are resolved by
3999
+ // Node, which has no `development` condition, so it loads the
4000
+ // production build instead. Both then run, each with its own
4001
+ // `sharedConfig` — the manifest `renderToStream` sets lands on one and
4002
+ // `lazy()` reads the other. `resolve.externalConditions` below only
4003
+ // fixes the external's own entry, not what it imports, so every
4004
+ // package that consumes the runtime has to go through Vite as well.
4005
+ // Semi-framework is the right class: `ssr.noExternal` without
4006
+ // `optimizeDeps.exclude`, since these hold no raw Solid components.
4007
+ isSemiFrameworkPkgByJson(pkgJson) {
4008
+ // Same gate as the core inlining in configEnvironment: dev serve
4009
+ // only, never vitest (it manages inlining via test.server.deps).
4010
+ if (!replaceDev || isTestMode) return false;
4011
+ return SOLID_RUNTIME_PKGS.some(name => pkgJson.dependencies?.[name] || pkgJson.peerDependencies?.[name]);
3652
4012
  }
3653
4013
  });
3654
4014
 
@@ -3793,14 +4153,45 @@ function solidPlugin(options = {}) {
3793
4153
  // So the dev flag has to reach both lists.
3794
4154
  if (replaceDev && config.consumer !== 'client' && name !== 'client') {
3795
4155
  config.resolve.externalConditions = ['development', ...(config.resolve.externalConditions ?? vite.defaultExternalConditions)];
4156
+
4157
+ // `externalConditions` only reaches the imports the module runner
4158
+ // resolves itself. An externalized package's OWN imports are resolved
4159
+ // by Node, with Node's conditions — never `development`. Since
4160
+ // solid 2.0.0-rc.7 both `solid-js` and `@solidjs/web` ship a
4161
+ // `dist/server.dev.*` behind that condition, so leaving them external
4162
+ // splits the framework in two under `vite dev`: the app's `solid-js`
4163
+ // is the runner's dev copy while `@solidjs/web`'s `import "solid-js"`
4164
+ // lands on Node's prod copy. `renderToStream` then installs the asset
4165
+ // resolver on one `sharedConfig` and `lazy()` reads the other ("no
4166
+ // asset manifest is set"), with every other module-level singleton
4167
+ // (owner tracking, request events, hydration keys) split the same
4168
+ // way. Inlining the two core packages makes every resolution — theirs
4169
+ // included — go through the environment's conditions, so one dev
4170
+ // build is loaded end to end. Framework packages that declare the
4171
+ // `solid` export condition are already inlined via vitefu below and
4172
+ // reach the same copy. Vitest projects manage their own inlining
4173
+ // (`test.server.deps` above) and are left alone, as is a host that
4174
+ // set `noExternal: true` (everything is inlined already).
4175
+ if (!isTestMode && config.resolve.noExternal !== true) {
4176
+ const noExternal = config.resolve.noExternal;
4177
+ config.resolve.noExternal = [...(Array.isArray(noExternal) ? noExternal : noExternal ? [noExternal] : []), 'solid-js', '@solidjs/web'];
4178
+ }
3796
4179
  }
3797
4180
 
3798
4181
  // Set resolve.noExternal and resolve.external for the SSR environment.
3799
4182
  // Only set resolve.external if noExternal is not true (to avoid conflicts with plugins like Cloudflare)
3800
4183
  if (name === 'ssr' && solidPkgsConfig) {
3801
4184
  if (config.resolve.noExternal !== true) {
3802
- config.resolve.noExternal = [...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal];
3803
- config.resolve.external = [...(Array.isArray(config.resolve.external) ? config.resolve.external : []), ...solidPkgsConfig.ssr.external];
4185
+ const noExternal = [...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal];
4186
+ config.resolve.noExternal = noExternal;
4187
+ // vitefu externalizes the non-framework deps of every framework
4188
+ // package in dev, and Vite gives `external` precedence over
4189
+ // `noExternal`. A framework package that lists solid-js or
4190
+ // @solidjs/web under `dependencies` (not peer — e.g.
4191
+ // @tanstack/solid-router 2.0.0-rc.7 → @solidjs/web) would therefore
4192
+ // re-externalize a core inlined above and split the runtime again.
4193
+ // Nothing inlined may appear in `external`.
4194
+ config.resolve.external = [...(Array.isArray(config.resolve.external) ? config.resolve.external : []), ...solidPkgsConfig.ssr.external.filter(dep => !noExternal.includes(dep))];
3804
4195
  }
3805
4196
  }
3806
4197
  },
@@ -3809,6 +4200,7 @@ function solidPlugin(options = {}) {
3809
4200
  isSsrBuild = !!config.build.ssr;
3810
4201
  base = config.base;
3811
4202
  projectRoot = config.root;
4203
+ clientBuildConfig = config.environments?.client?.build ?? config.build;
3812
4204
  filter = vite.createFilter(options.include, options.exclude, {
3813
4205
  resolve: projectRoot
3814
4206
  });
@@ -3942,9 +4334,24 @@ function solidPlugin(options = {}) {
3942
4334
  const manifestPath = clientManifestPath();
3943
4335
  if (manifestPath) {
3944
4336
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
3945
- normalizeEmittedLazyEntries(manifest);
3946
- manifest._base = base;
3947
- return `export default ${JSON.stringify(manifest)};`;
4337
+ // Manifest records are keyed the way Vite keys entry chunks (the
4338
+ // root-relative facade path, also carried as `src`), so the
4339
+ // configured client inputs identify the genuine entries here too —
4340
+ // independent of `isEntry`, which the serialized manifest may have
4341
+ // lost already (older plugin builds stripped it; see #342).
4342
+ const entries = resolveConfiguredEntries(configuredBuildInput(clientBuildConfig), projectRoot);
4343
+ const isConfiguredEntry = (key, record) => entries.manifestKeys.has(key) || typeof record.src === 'string' && entries.manifestKeys.has(record.src);
4344
+ for (const key in manifest) {
4345
+ if (isConfiguredEntry(key, manifest[key]) && manifest[key].file) {
4346
+ manifest[key].isEntry = true;
4347
+ }
4348
+ }
4349
+ normalizeEmittedLazyEntries(manifest, {
4350
+ isConfiguredEntry,
4351
+ warn: message => this.warn(message),
4352
+ repairDynamicEntries: true
4353
+ });
4354
+ return `export default ${JSON.stringify(stampClientEntry(manifest, resolveClientEntryKey(manifest, startClientEntryId, clientBuildConfig, projectRoot), base))};`;
3948
4355
  }
3949
4356
  // SSR build before the client build produced a manifest: bake in the
3950
4357
  // dev-shaped fallback (registry miss degrades to js-only resolution).
@@ -3959,6 +4366,12 @@ function solidPlugin(options = {}) {
3959
4366
  // the bundle don't mistake them for application entries. Must precede
3960
4367
  // the client asset map build, which keys off dynamic entries.
3961
4368
  if (options.ssr) {
4369
+ // The genuine entries are the configured inputs of this very
4370
+ // environment — the plugin injects the client entry itself in start
4371
+ // mode, and Vite's default is index.html — so their facade chunks
4372
+ // are recognizable regardless of what dynamically imports them.
4373
+ const entries = resolveConfiguredEntries(configuredBuildInput(this.environment?.config?.build ?? clientBuildConfig), projectRoot);
4374
+ const knownLazyKeys = new Set();
3962
4375
  for (const ref of emittedLazyChunkRefs) {
3963
4376
  let fileName;
3964
4377
  try {
@@ -3969,10 +4382,17 @@ function solidPlugin(options = {}) {
3969
4382
  }
3970
4383
  const chunk = bundle[fileName];
3971
4384
  if (!chunk || chunk.type !== 'chunk') continue;
4385
+ // An entry that is also lazily imported stays an entry.
4386
+ if (entries.isEntryModule(chunk.facadeModuleId)) continue;
4387
+ knownLazyKeys.add(fileName);
3972
4388
  chunk.isEntry = false;
3973
4389
  chunk.isDynamicEntry = true;
3974
4390
  }
3975
- normalizeEmittedLazyEntries(bundle);
4391
+ normalizeEmittedLazyEntries(bundle, {
4392
+ isConfiguredEntry: (_key, chunk) => entries.isEntryModule(chunk.facadeModuleId),
4393
+ knownLazyKeys,
4394
+ warn: message => this.warn(message)
4395
+ });
3976
4396
  }
3977
4397
  },
3978
4398
  async transform(source, id, transformOptions) {
@@ -4243,6 +4663,9 @@ function solidPlugin(options = {}) {
4243
4663
  onDocumentResolved(documentPath) {
4244
4664
  // Normalize to forward slashes to match Vite's transform ids.
4245
4665
  documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
4666
+ },
4667
+ onClientEntryResolved(entryId) {
4668
+ startClientEntryId = entryId;
4246
4669
  }
4247
4670
  }));
4248
4671
  }