@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.
@@ -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;
@@ -1229,6 +1246,23 @@ function serverFunctions(options = {}, internal = {}) {
1229
1246
  client: undefined
1230
1247
  };
1231
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)};`;
1232
1266
  const clientOptions = {
1233
1267
  directive,
1234
1268
  definitions: {
@@ -1288,6 +1322,14 @@ function serverFunctions(options = {}, internal = {}) {
1288
1322
  // import is only emitted when the option is on, so disabled setups keep
1289
1323
  // a server-component-free graph.
1290
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,
1291
1333
  // The user's `configure` module comes first: a side-effect import in
1292
1334
  // the handler graph, evaluated before any dispatch on both surfaces
1293
1335
  // (dev middleware and prod handler) and bundled into the handler
@@ -1617,6 +1659,11 @@ function devtoolsMountModuleCode() {
1617
1659
  // it, and page responses go through the runtime's `createSSRResponse`
1618
1660
  // head lifecycle (commit at shell flush, real pre-flush redirects, the
1619
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.
1620
1667
  // - `vite preview` serves dist/client statically and dispatches everything
1621
1668
  // else through the built handler — the production path, middleware
1622
1669
  // included, with no server file needed.
@@ -1692,6 +1739,37 @@ function normalizeUserPath(root, spec, option) {
1692
1739
  }
1693
1740
  return relative;
1694
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
+ }
1695
1773
  function resolveEntries(root, options, clientMode) {
1696
1774
  const explicitClient = options.entryClient ? normalizeUserPath(root, options.entryClient, 'entryClient') : null;
1697
1775
  if (clientMode) {
@@ -1784,8 +1862,11 @@ function startServe(options, internal = {}) {
1784
1862
  // any of the (lazy) uses in entry codegen and the entry transform.
1785
1863
  let diagnostics = internal.diagnostics === true;
1786
1864
  let devtoolsEnabled = false;
1787
- let devtoolsResolutions = {};
1788
- 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 = {};
1789
1870
  // `external` is server-mode-only (documented no-op in client mode, so a
1790
1871
  // host-integrated config survives the `ssr` boolean flip untouched).
1791
1872
  const externalServer = !clientMode && !!options.external;
@@ -1797,31 +1878,45 @@ function startServe(options, internal = {}) {
1797
1878
  let middlewarePath = null;
1798
1879
  /** Absolute path of the per-request setup module, when configured (server mode). */
1799
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;
1800
1889
  function requireEntries() {
1801
1890
  // config() always runs before resolveId/load/configureServer.
1802
1891
  if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1803
1892
  return entries;
1804
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
+ }
1805
1908
  async function resolveDevtools(resolve, importer, consumer) {
1806
1909
  if (!devtoolsEnabled) return false;
1807
- // Detect from the app graph first (the documented install location), then
1808
- // from the plugin's own file: in pnpm-isolated apps a copy that is only a
1809
- // dependency of the plugin is not reachable from the app's importers. The
1810
- // resolved id is kept so imports from generated modules can use it.
1811
- devtoolsResolutions[consumer] ??= (async () => {
1812
- // Resolving from the plugin's own file never yields null when the
1813
- // package is absent: it is declared an optional peer dependency, so
1814
- // Vite answers with its `__vite-optional-peer-dep:` stub (an empty
1815
- // module). Treat that stub as "not installed".
1816
- const realId = resolved => resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
1817
- return realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? realId(await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url)));
1818
- })();
1819
- const id = await devtoolsResolutions[consumer];
1820
- devtoolsIds[consumer] = id;
1821
- 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) {
1822
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.');
1823
1918
  }
1824
- return id !== null;
1919
+ return detected;
1825
1920
  }
1826
1921
 
1827
1922
  /**
@@ -1996,13 +2091,17 @@ function startServe(options, internal = {}) {
1996
2091
  entryClient
1997
2092
  } = requireEntries();
1998
2093
  const composeServerFunctions = internal.serverFunctions;
1999
- 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)};`] : [])];
2000
2095
  if (isBuild) {
2001
2096
  lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
2002
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;`,
2003
- // The plugin's manifest module normalizes lazy facade chunks
2004
- // (isDynamicEntry) so exactly one real entry remains flagged.
2005
- ` for (const key in manifest) {`, ` const chunk = manifest[key];`, ` if (chunk && chunk.isEntry && chunk.file) {`, ` clientEntryUrl = joinAssetPath(manifest._base, chunk.file);`, ` break;`, ` }`, ` }`, ` return clientEntryUrl;`, `}`);
2098
+ // The plugin's manifest module names the client entry it injected
2099
+ // into the build (`_entry`): every configured input is a genuine
2100
+ // `isEntry` record (a filesystem router's `buildInputs` lists every
2101
+ // route module), so scanning for the first flagged record would pick
2102
+ // whichever sorts first (#353). The scan stays as the fallback for
2103
+ // hand-rolled manifests without the stamp.
2104
+ ` 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;`, `}`);
2006
2105
  } else {
2007
2106
  const devHead = `<script>${devStylePatch}</script>` + `<script type="module" src="${joinBase(base, '/@vite/client')}"></script>`;
2008
2107
  lines.push(``, `const DEV_HEAD = ${JSON.stringify(devHead)};`);
@@ -2018,6 +2117,21 @@ function startServe(options, internal = {}) {
2018
2117
  lines.push(`const runMiddleware = (request, next) => next(request);`);
2019
2118
  }
2020
2119
 
2120
+ // Render mode (`start.renderMode`): 'stream' flushes the shell with
2121
+ // fallbacks in place and streams boundary content after it; 'async'
2122
+ // adopts the renderToStream result's thenable — which waits for the
2123
+ // complete render — so one settled document goes out (the fix for
2124
+ // no-JS clients, solidjs/solid#3280). Precedence per request: the
2125
+ // `handleRequest` option (hosts driving the handler directly), then the
2126
+ // configured module's per-request result, then the static config. Every
2127
+ // source is validated against the two literals with the offender named.
2128
+ 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;`, `}`);
2129
+ if (renderModePath) {
2130
+ 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');`, `}`);
2131
+ } else {
2132
+ lines.push(`function resolveRenderMode(event, options) {`, ` if (options.renderMode !== undefined) return assertRenderMode(options.renderMode, 'handleRequest options.renderMode');`, ` return ${JSON.stringify(renderMode)};`, `}`);
2133
+ }
2134
+
2021
2135
  // No `_$SC` bootstrap injection: the runtime's serialized
2022
2136
  // server-component references self-bootstrap the registry (each
2023
2137
  // hydration script's first reference carries it as an idempotent
@@ -2098,7 +2212,11 @@ function startServe(options, internal = {}) {
2098
2212
  // flag, so external-host dispatch and preview stay render-always.
2099
2213
  lines.push(` if (options.pageRequest === false) {`, ` return new Response(null, { status: 404, headers: { ${JSON.stringify(DEV_FALLTHROUGH_HEADER)}: '1' } });`, ` }`);
2100
2214
  }
2101
- lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2215
+ lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`,
2216
+ // Decided before the render starts (the module form may be async),
2217
+ // inside the request scope and after the middleware chain, so a
2218
+ // per-request policy sees the decorated event.
2219
+ ` const renderMode = await resolveRenderMode(event, options);`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
2102
2220
  // renderToStream results are thenables whose then() waits for the
2103
2221
  // *complete* render — check for pipe first so streaming survives, and
2104
2222
  // only await plain promises (async render functions).
@@ -2107,6 +2225,14 @@ function startServe(options, internal = {}) {
2107
2225
  // entry): a bare promise resolution would adopt the stream's
2108
2226
  // thenable and buffer the whole render.
2109
2227
  ` if (result && result.${STREAM_BOX}) result = result.${STREAM_BOX};`] : []),
2228
+ // Async mode adopts the thenable deliberately — the very thing the
2229
+ // pipe-first check and the setup box exist to avoid in stream mode.
2230
+ // It resolves with the full HTML once every boundary settled, each
2231
+ // spliced in place pre-flush (no fallbacks, no swap scripts; hydration
2232
+ // data still serialized), and the string then takes
2233
+ // createSSRResponse's string path below: stub commit, transformChunk
2234
+ // on the whole document, and a mid-render Location as a real 3xx.
2235
+ ` if (renderMode === 'async' && result && typeof result.then === 'function') {`, ` result = await result;`, ` }`,
2110
2236
  // Raw Responses fold at the handler edge (handleRequest), after the
2111
2237
  // middleware chain unwinds — not here, where middleware above this
2112
2238
  // frame could still legitimately mutate headers.
@@ -2145,8 +2271,8 @@ function startServe(options, internal = {}) {
2145
2271
  config(userConfig, env) {
2146
2272
  root = path.resolve(userConfig.root || process.cwd());
2147
2273
  devtoolsEnabled = env.command === 'serve' && !env.isPreview && options.devtools !== false;
2148
- devtoolsResolutions = {};
2149
- devtoolsIds = {};
2274
+ devtoolsDetections = {};
2275
+ devtoolsImporters = {};
2150
2276
  entries = resolveEntries(root, options, clientMode);
2151
2277
  internal.onDocumentResolved?.(entries.document);
2152
2278
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
@@ -2158,6 +2284,17 @@ function startServe(options, internal = {}) {
2158
2284
  // hook needs does not exist there.
2159
2285
  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}`);
2160
2286
  }
2287
+ // Server-mode only as well: the client-mode shell renders no app,
2288
+ // so there is nothing to settle. Validated in every mode though —
2289
+ // a typo should not hide behind the `ssr` boolean.
2290
+ ({
2291
+ mode: renderMode,
2292
+ path: renderModePath
2293
+ } = resolveRenderMode(root, options.renderMode));
2294
+ if (clientMode) {
2295
+ renderMode = 'stream';
2296
+ renderModePath = null;
2297
+ }
2161
2298
  if (env.isPreview) {
2162
2299
  if (clientMode) {
2163
2300
  // Client-mode builds emit a real dist/client/index.html (the
@@ -2188,6 +2325,7 @@ function startServe(options, internal = {}) {
2188
2325
  }
2189
2326
  const build = env.command === 'build';
2190
2327
  const clientInput = entries.generated ? ENTRY_CLIENT_ID : path.resolve(root, entries.entryClient);
2328
+ internal.onClientEntryResolved?.(clientInput);
2191
2329
  // Real files only — the dep scanner can't crawl virtual modules.
2192
2330
  // (In client mode the resolved document joins the scan/style roots
2193
2331
  // even with an authored client entry; in SSR mode authored entries
@@ -2299,7 +2437,7 @@ function startServe(options, internal = {}) {
2299
2437
  diagnostics = detectDiagnosticsPackage(root);
2300
2438
  }
2301
2439
  },
2302
- resolveId(source, importer, opts) {
2440
+ async resolveId(source, importer, opts) {
2303
2441
  if (source === HANDLER_ID) {
2304
2442
  return {
2305
2443
  id: HANDLER_ID,
@@ -2324,13 +2462,25 @@ function startServe(options, internal = {}) {
2324
2462
  moduleSideEffects: true
2325
2463
  };
2326
2464
  }
2327
- // Generated modules have no directory for bare-package resolution.
2328
- // Reuse the app-relative id captured during detection.
2329
- const devtoolsId = devtoolsIds[getEnvironmentConsumer(this.environment, opts)];
2330
- if (devtoolsId && source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2331
- return {
2332
- id: devtoolsId
2333
- };
2465
+ if (source === DEVTOOLS_PACKAGE && (importer === ENTRY_SERVER_ID || importer === ENTRY_CLIENT_ID || importer === DEVTOOLS_MOUNT_ID)) {
2466
+ // Generated modules have no directory for bare-package resolution:
2467
+ // resolve from the app importer detection probed. Resolve afresh on
2468
+ // every request rather than reusing detection's id in the client
2469
+ // environment that id is the optimizer's pre-bundled URL, stamped
2470
+ // with the browserHash of the pass that produced it. Any dependency
2471
+ // discovered after the initial scan re-optimizes: the toolbar's
2472
+ // chunks are re-emitted under new names and the hash moves on, and
2473
+ // a frozen id would keep the entry on the previous pass — its lazy
2474
+ // chunks answer 504 (Outdated Optimize Dep) and the stale bundle
2475
+ // brings a second solid-js instance into the page.
2476
+ const from = devtoolsImporters[getEnvironmentConsumer(this.environment, opts)];
2477
+ if (!from) return null;
2478
+ const id = await resolveDevtoolsId((s, i) => this.resolve(s, i, {
2479
+ skipSelf: true
2480
+ }), from);
2481
+ return id ? {
2482
+ id
2483
+ } : null;
2334
2484
  }
2335
2485
  return null;
2336
2486
  },
@@ -3281,6 +3431,23 @@ function getExtension(filename) {
3281
3431
  const index = filename.lastIndexOf('.');
3282
3432
  return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, '');
3283
3433
  }
3434
+ // The packages whose dev/production server builds are selected by the
3435
+ // `development` export condition. A dependency on either means the package
3436
+ // consumes the runtime and must resolve it through Vite in dev.
3437
+ const SOLID_RUNTIME_PKGS = ['solid-js', '@solidjs/web'];
3438
+
3439
+ // Tooling that declares solid-js as a peer but never runs inside the SSR
3440
+ // module runner. Kept out of the crawl entirely: classifying them as
3441
+ // semi-framework would also crawl THEIR dependencies, which vitefu deep-
3442
+ // includes in the client optimizer (`@solidjs/vite-plugin > @babel/core`
3443
+ // pre-bundled for the browser — ~2.6 MB of dead weight per cold start).
3444
+ // Mirrors vite-plugin-svelte's isCommonDepWithoutSvelteField list.
3445
+ const NON_RUNTIME_SOLID_PKGS = ['@solidjs/vite-plugin', 'vite', 'vitest', 'eslint-plugin-solid'];
3446
+ const NON_RUNTIME_SOLID_PREFIXES = ['vite-plugin-', 'eslint-plugin-', 'prettier-plugin-', '@types/'];
3447
+ function isNonRuntimeSolidPkg(name) {
3448
+ const bare = name.slice(name.lastIndexOf('/') + 1);
3449
+ return NON_RUNTIME_SOLID_PKGS.includes(name) || NON_RUNTIME_SOLID_PREFIXES.some(p => (p.startsWith('@') ? name : bare).startsWith(p));
3450
+ }
3284
3451
  function containsSolidField(fields) {
3285
3452
  const keys = Object.keys(fields);
3286
3453
  for (let i = 0; i < keys.length; i++) {
@@ -3382,7 +3549,69 @@ function combineSourcemaps(maps) {
3382
3549
  // remapping expects most-recent-first.
3383
3550
  return JSON.parse(remapping(chain.reverse(), () => null).toString());
3384
3551
  }
3552
+ function toPosixPath(p) {
3553
+ return p.split(path.sep).join('/');
3554
+ }
3555
+ function tryRealpath(p) {
3556
+ try {
3557
+ return realpathSync.native(p);
3558
+ } catch {
3559
+ return null;
3560
+ }
3561
+ }
3562
+
3563
+ /** The `input` a build environment's config resolves to, in any spelling. */
3564
+ function configuredBuildInput(build) {
3565
+ if (!build) return undefined;
3566
+ return build.rolldownOptions?.input ?? build.rollupOptions?.input ?? build.lib?.entry;
3567
+ }
3385
3568
 
3569
+ /**
3570
+ * The genuine entries of a client build, derived from its configured input
3571
+ * (`build.rollupOptions.input` as a string / array / record, or Vite's
3572
+ * default `index.html`). Rollup and rolldown only ever flag two kinds of
3573
+ * chunk `isEntry`: those facades and chunks plugins emit with
3574
+ * `emitFile({ type: 'chunk' })` — so this is exactly the knowledge that
3575
+ * tells a real application entry apart from an emitted lazy facade.
3576
+ *
3577
+ * `moduleIds` — every spelling the entry's facade module id can take: as
3578
+ * written (virtual ids resolve to themselves), resolved against the root
3579
+ * (Vite resolves relative file inputs there), and the real path of either
3580
+ * (Vite's resolver follows symlinks).
3581
+ * `manifestKeys` — the manifest.json keys Vite derives from those facades
3582
+ * (root-relative, `\0` stripped), matching Vite's own `getChunkName`.
3583
+ */
3584
+ function resolveConfiguredEntries(input, root) {
3585
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3586
+ const moduleIds = new Set();
3587
+ for (const id of raw) {
3588
+ if (typeof id !== 'string') continue;
3589
+ const clean = id.replace(/\0/g, '');
3590
+ const candidates = [clean, path.resolve(root, clean)];
3591
+ for (const candidate of candidates) {
3592
+ moduleIds.add(candidate);
3593
+ moduleIds.add(toPosixPath(candidate));
3594
+ const real = tryRealpath(candidate);
3595
+ if (real) {
3596
+ moduleIds.add(real);
3597
+ moduleIds.add(toPosixPath(real));
3598
+ }
3599
+ }
3600
+ }
3601
+ const manifestKeys = new Set();
3602
+ for (const id of moduleIds) manifestKeys.add(toPosixPath(path.relative(root, id)));
3603
+ return {
3604
+ moduleIds,
3605
+ manifestKeys,
3606
+ isEntryModule(id) {
3607
+ if (!id) return false;
3608
+ const clean = id.replace(/\0/g, '');
3609
+ if (moduleIds.has(clean) || moduleIds.has(toPosixPath(clean))) return true;
3610
+ const real = tryRealpath(clean);
3611
+ return !!real && (moduleIds.has(real) || moduleIds.has(toPosixPath(real)));
3612
+ }
3613
+ };
3614
+ }
3386
3615
  /**
3387
3616
  * Chunks emitted for lazy() targets are marked `isEntry` by Rollup even
3388
3617
  * though they are semantically dynamic entries. Reclassify any entry that is
@@ -3391,20 +3620,119 @@ function combineSourcemaps(maps) {
3391
3620
  * the real client entry. Works on both the Vite manifest.json shape and the
3392
3621
  * raw Rollup output bundle — both key entries by name and expose
3393
3622
  * `dynamicImports` / `isEntry` with the same meaning.
3623
+ *
3624
+ * Being a dynamic-import target alone does not make a chunk a lazy facade,
3625
+ * though: the real client entry becomes one whenever it absorbs a module
3626
+ * that is also dynamically imported somewhere else. Solid 2 produces that
3627
+ * shape on its own — `@solidjs/web/frames/client` lazily imports the
3628
+ * serialization decoder (`loadCodec()`), so a static import of
3629
+ * `@solidjs/web/serialization/decode` anywhere in the client graph merges
3630
+ * the decoder into the entry chunk, and the entry then lists itself (or is
3631
+ * listed by another lazy chunk) under `dynamicImports`. Stripping `isEntry`
3632
+ * there leaves the bundle with no entry at all ("No entry file found"
3633
+ * downstream, e.g. TanStack Start's manifest capture, #342). Genuine
3634
+ * configured entries are therefore never reclassified, and a chunk's
3635
+ * dynamic import of itself is not an edge worth acting on.
3636
+ *
3637
+ * Rolldown caveat: of the flags written here only `isEntry` is synced back
3638
+ * to the native bundle after the hook (rolldown's `update_output_chunk`
3639
+ * copies `code`, `map`, `imports`, `dynamicImports`, `isEntry` and the file
3640
+ * name; `isDynamicEntry` is kept from the original chunk). Later plugins
3641
+ * and Vite's manifest plugin therefore see reclassified facades as neither
3642
+ * entry nor dynamic entry under rolldown. The manifest `load` path repairs
3643
+ * `isDynamicEntry` on the plugin's own manifest module, the one place it
3644
+ * controls end to end.
3394
3645
  */
3395
- function normalizeEmittedLazyEntries(manifest) {
3396
- const dynamicKeys = new Set();
3646
+ function normalizeEmittedLazyEntries(manifest, {
3647
+ isConfiguredEntry,
3648
+ knownLazyKeys,
3649
+ warn,
3650
+ repairDynamicEntries
3651
+ }) {
3652
+ const dynamicKeys = new Map();
3397
3653
  for (const key in manifest) {
3398
3654
  const imports = manifest[key].dynamicImports;
3399
- if (imports) for (const dep of imports) dynamicKeys.add(dep);
3655
+ if (!imports) continue;
3656
+ for (const dep of imports) {
3657
+ // A chunk that absorbed one of its own lazy targets imports itself;
3658
+ // that says nothing about whether it is an entry.
3659
+ if (dep !== key && !dynamicKeys.has(dep)) dynamicKeys.set(dep, key);
3660
+ }
3400
3661
  }
3401
- for (const key of dynamicKeys) {
3662
+ for (const [key, importer] of dynamicKeys) {
3402
3663
  const entry = manifest[key];
3403
- if (entry && entry.isEntry) {
3664
+ if (!entry || entry.type === 'asset') continue;
3665
+ if (isConfiguredEntry(key, entry)) continue;
3666
+ if (entry.isEntry) {
3404
3667
  entry.isEntry = false;
3405
3668
  entry.isDynamicEntry = true;
3669
+ if (warn && !knownLazyKeys?.has(key)) {
3670
+ 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.');
3671
+ }
3672
+ } else if (repairDynamicEntries && !entry.isDynamicEntry) {
3673
+ entry.isDynamicEntry = true;
3674
+ }
3675
+ }
3676
+ }
3677
+
3678
+ /**
3679
+ * The manifest key of THE client entry — the chunk whose `<script
3680
+ * type="module">` boots the page and whose static import graph carries the
3681
+ * global CSS. `isEntry` cannot answer this: every configured build input is
3682
+ * a genuine entry (#347 keeps them flagged), and plugins routinely add more
3683
+ * inputs than the application entry (filesystem-routing's `buildInputs`
3684
+ * lists every route module, and route keys sort ahead of the plugin's own
3685
+ * `virtual:` entry). So the identity comes from configuration instead: the
3686
+ * entry start mode injected itself, or — outside start mode — the single
3687
+ * configured input when there is exactly one (including Vite's default
3688
+ * `index.html`). Several inputs and no start entry: no answer (null), and
3689
+ * consumers keep their first-`isEntry` scan.
3690
+ *
3691
+ * Matched by key or `src`, the same two spellings `isConfiguredEntry` uses.
3692
+ */
3693
+ function resolveClientEntryKey(manifest, startClientEntryId, clientBuild, root) {
3694
+ let entryId = startClientEntryId;
3695
+ if (!entryId) {
3696
+ const input = configuredBuildInput(clientBuild);
3697
+ const raw = input == null ? ['index.html'] : typeof input === 'string' ? [input] : Array.isArray(input) ? input : Object.values(input);
3698
+ if (raw.length !== 1 || typeof raw[0] !== 'string') return null;
3699
+ entryId = raw[0];
3700
+ }
3701
+ const {
3702
+ manifestKeys
3703
+ } = resolveConfiguredEntries(entryId, root);
3704
+ for (const key in manifest) {
3705
+ const record = manifest[key];
3706
+ if (!record || typeof record !== 'object' || !record.file) continue;
3707
+ if (manifestKeys.has(key) || typeof record.src === 'string' && manifestKeys.has(record.src)) {
3708
+ return key;
3406
3709
  }
3407
3710
  }
3711
+ return null;
3712
+ }
3713
+
3714
+ /**
3715
+ * Serializes the plugin's manifest module with the client entry made
3716
+ * explicit: `_entry` names its key (the generated handler reads it before
3717
+ * falling back to scanning for `isEntry`), and its record is moved to the
3718
+ * front. The ordering matters for consumers that still identify the entry
3719
+ * by the first `isEntry` record — `@solidjs/web`'s `registerEntryAssets`,
3720
+ * which links the entry graph's stylesheets and modulepreloads into
3721
+ * `<head>`, and hand-rolled server entries — so they and `_entry` agree on
3722
+ * the same chunk. Other configured inputs keep `isEntry`; they are genuine
3723
+ * entries, just not the one the document boots.
3724
+ */
3725
+ function stampClientEntry(manifest, entryKey, base) {
3726
+ const ordered = {};
3727
+ if (entryKey && manifest[entryKey]) {
3728
+ ordered[entryKey] = manifest[entryKey];
3729
+ }
3730
+ for (const key in manifest) {
3731
+ if (key !== entryKey) ordered[key] = manifest[key];
3732
+ }
3733
+ ordered._base = base;
3734
+ if (entryKey && manifest[entryKey]) ordered._entry = entryKey;
3735
+ return ordered;
3408
3736
  }
3409
3737
  function solidPlugin(options = {}) {
3410
3738
  if (typeof options.ssr === 'object') {
@@ -3464,6 +3792,16 @@ function solidPlugin(options = {}) {
3464
3792
  let isSsrBuild = false;
3465
3793
  let base = '/';
3466
3794
  let clientOutDir = null;
3795
+ // The client environment's resolved build options, for the configured
3796
+ // entry input. Read off the resolved config so the SSR half of a
3797
+ // two-invocation build (`vite build --ssr`) still knows the client's
3798
+ // entries when it bakes the client manifest in.
3799
+ let clientBuildConfig = null;
3800
+ // The client entry start mode injects into the client build's input
3801
+ // (reported by startServe): the one input that IS the application entry,
3802
+ // as opposed to further inputs other plugins add (e.g. filesystem-routing's
3803
+ // `buildInputs`, which lists every route module). Null outside start mode.
3804
+ let startClientEntryId = null;
3467
3805
  let solidPkgsConfig;
3468
3806
  const tsrxCss = new Map();
3469
3807
 
@@ -3625,6 +3963,28 @@ function solidPlugin(options = {}) {
3625
3963
  isBuild: command === 'build',
3626
3964
  isFrameworkPkgByJson(pkgJson) {
3627
3965
  return containsSolidField(pkgJson.exports || {});
3966
+ },
3967
+ // `false` = neither framework nor semi-framework, and don't crawl
3968
+ // its deps; `undefined` = unknown, fall through to the json checks.
3969
+ isFrameworkPkgByName(name) {
3970
+ return isNonRuntimeSolidPkg(name) ? false : undefined;
3971
+ },
3972
+ // Under `vite dev` the runtime must not be split in two. Inlined
3973
+ // modules resolve `solid-js` through Vite with `development` (its dev
3974
+ // server build); an externalized package's own imports are resolved by
3975
+ // Node, which has no `development` condition, so it loads the
3976
+ // production build instead. Both then run, each with its own
3977
+ // `sharedConfig` — the manifest `renderToStream` sets lands on one and
3978
+ // `lazy()` reads the other. `resolve.externalConditions` below only
3979
+ // fixes the external's own entry, not what it imports, so every
3980
+ // package that consumes the runtime has to go through Vite as well.
3981
+ // Semi-framework is the right class: `ssr.noExternal` without
3982
+ // `optimizeDeps.exclude`, since these hold no raw Solid components.
3983
+ isSemiFrameworkPkgByJson(pkgJson) {
3984
+ // Same gate as the core inlining in configEnvironment: dev serve
3985
+ // only, never vitest (it manages inlining via test.server.deps).
3986
+ if (!replaceDev || isTestMode) return false;
3987
+ return SOLID_RUNTIME_PKGS.some(name => pkgJson.dependencies?.[name] || pkgJson.peerDependencies?.[name]);
3628
3988
  }
3629
3989
  });
3630
3990
 
@@ -3769,14 +4129,45 @@ function solidPlugin(options = {}) {
3769
4129
  // So the dev flag has to reach both lists.
3770
4130
  if (replaceDev && config.consumer !== 'client' && name !== 'client') {
3771
4131
  config.resolve.externalConditions = ['development', ...(config.resolve.externalConditions ?? defaultExternalConditions)];
4132
+
4133
+ // `externalConditions` only reaches the imports the module runner
4134
+ // resolves itself. An externalized package's OWN imports are resolved
4135
+ // by Node, with Node's conditions — never `development`. Since
4136
+ // solid 2.0.0-rc.7 both `solid-js` and `@solidjs/web` ship a
4137
+ // `dist/server.dev.*` behind that condition, so leaving them external
4138
+ // splits the framework in two under `vite dev`: the app's `solid-js`
4139
+ // is the runner's dev copy while `@solidjs/web`'s `import "solid-js"`
4140
+ // lands on Node's prod copy. `renderToStream` then installs the asset
4141
+ // resolver on one `sharedConfig` and `lazy()` reads the other ("no
4142
+ // asset manifest is set"), with every other module-level singleton
4143
+ // (owner tracking, request events, hydration keys) split the same
4144
+ // way. Inlining the two core packages makes every resolution — theirs
4145
+ // included — go through the environment's conditions, so one dev
4146
+ // build is loaded end to end. Framework packages that declare the
4147
+ // `solid` export condition are already inlined via vitefu below and
4148
+ // reach the same copy. Vitest projects manage their own inlining
4149
+ // (`test.server.deps` above) and are left alone, as is a host that
4150
+ // set `noExternal: true` (everything is inlined already).
4151
+ if (!isTestMode && config.resolve.noExternal !== true) {
4152
+ const noExternal = config.resolve.noExternal;
4153
+ config.resolve.noExternal = [...(Array.isArray(noExternal) ? noExternal : noExternal ? [noExternal] : []), 'solid-js', '@solidjs/web'];
4154
+ }
3772
4155
  }
3773
4156
 
3774
4157
  // Set resolve.noExternal and resolve.external for the SSR environment.
3775
4158
  // Only set resolve.external if noExternal is not true (to avoid conflicts with plugins like Cloudflare)
3776
4159
  if (name === 'ssr' && solidPkgsConfig) {
3777
4160
  if (config.resolve.noExternal !== true) {
3778
- config.resolve.noExternal = [...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal];
3779
- config.resolve.external = [...(Array.isArray(config.resolve.external) ? config.resolve.external : []), ...solidPkgsConfig.ssr.external];
4161
+ const noExternal = [...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal];
4162
+ config.resolve.noExternal = noExternal;
4163
+ // vitefu externalizes the non-framework deps of every framework
4164
+ // package in dev, and Vite gives `external` precedence over
4165
+ // `noExternal`. A framework package that lists solid-js or
4166
+ // @solidjs/web under `dependencies` (not peer — e.g.
4167
+ // @tanstack/solid-router 2.0.0-rc.7 → @solidjs/web) would therefore
4168
+ // re-externalize a core inlined above and split the runtime again.
4169
+ // Nothing inlined may appear in `external`.
4170
+ config.resolve.external = [...(Array.isArray(config.resolve.external) ? config.resolve.external : []), ...solidPkgsConfig.ssr.external.filter(dep => !noExternal.includes(dep))];
3780
4171
  }
3781
4172
  }
3782
4173
  },
@@ -3785,6 +4176,7 @@ function solidPlugin(options = {}) {
3785
4176
  isSsrBuild = !!config.build.ssr;
3786
4177
  base = config.base;
3787
4178
  projectRoot = config.root;
4179
+ clientBuildConfig = config.environments?.client?.build ?? config.build;
3788
4180
  filter = createFilter(options.include, options.exclude, {
3789
4181
  resolve: projectRoot
3790
4182
  });
@@ -3918,9 +4310,24 @@ function solidPlugin(options = {}) {
3918
4310
  const manifestPath = clientManifestPath();
3919
4311
  if (manifestPath) {
3920
4312
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
3921
- normalizeEmittedLazyEntries(manifest);
3922
- manifest._base = base;
3923
- return `export default ${JSON.stringify(manifest)};`;
4313
+ // Manifest records are keyed the way Vite keys entry chunks (the
4314
+ // root-relative facade path, also carried as `src`), so the
4315
+ // configured client inputs identify the genuine entries here too —
4316
+ // independent of `isEntry`, which the serialized manifest may have
4317
+ // lost already (older plugin builds stripped it; see #342).
4318
+ const entries = resolveConfiguredEntries(configuredBuildInput(clientBuildConfig), projectRoot);
4319
+ const isConfiguredEntry = (key, record) => entries.manifestKeys.has(key) || typeof record.src === 'string' && entries.manifestKeys.has(record.src);
4320
+ for (const key in manifest) {
4321
+ if (isConfiguredEntry(key, manifest[key]) && manifest[key].file) {
4322
+ manifest[key].isEntry = true;
4323
+ }
4324
+ }
4325
+ normalizeEmittedLazyEntries(manifest, {
4326
+ isConfiguredEntry,
4327
+ warn: message => this.warn(message),
4328
+ repairDynamicEntries: true
4329
+ });
4330
+ return `export default ${JSON.stringify(stampClientEntry(manifest, resolveClientEntryKey(manifest, startClientEntryId, clientBuildConfig, projectRoot), base))};`;
3924
4331
  }
3925
4332
  // SSR build before the client build produced a manifest: bake in the
3926
4333
  // dev-shaped fallback (registry miss degrades to js-only resolution).
@@ -3935,6 +4342,12 @@ function solidPlugin(options = {}) {
3935
4342
  // the bundle don't mistake them for application entries. Must precede
3936
4343
  // the client asset map build, which keys off dynamic entries.
3937
4344
  if (options.ssr) {
4345
+ // The genuine entries are the configured inputs of this very
4346
+ // environment — the plugin injects the client entry itself in start
4347
+ // mode, and Vite's default is index.html — so their facade chunks
4348
+ // are recognizable regardless of what dynamically imports them.
4349
+ const entries = resolveConfiguredEntries(configuredBuildInput(this.environment?.config?.build ?? clientBuildConfig), projectRoot);
4350
+ const knownLazyKeys = new Set();
3938
4351
  for (const ref of emittedLazyChunkRefs) {
3939
4352
  let fileName;
3940
4353
  try {
@@ -3945,10 +4358,17 @@ function solidPlugin(options = {}) {
3945
4358
  }
3946
4359
  const chunk = bundle[fileName];
3947
4360
  if (!chunk || chunk.type !== 'chunk') continue;
4361
+ // An entry that is also lazily imported stays an entry.
4362
+ if (entries.isEntryModule(chunk.facadeModuleId)) continue;
4363
+ knownLazyKeys.add(fileName);
3948
4364
  chunk.isEntry = false;
3949
4365
  chunk.isDynamicEntry = true;
3950
4366
  }
3951
- normalizeEmittedLazyEntries(bundle);
4367
+ normalizeEmittedLazyEntries(bundle, {
4368
+ isConfiguredEntry: (_key, chunk) => entries.isEntryModule(chunk.facadeModuleId),
4369
+ knownLazyKeys,
4370
+ warn: message => this.warn(message)
4371
+ });
3952
4372
  }
3953
4373
  },
3954
4374
  async transform(source, id, transformOptions) {
@@ -4219,6 +4639,9 @@ function solidPlugin(options = {}) {
4219
4639
  onDocumentResolved(documentPath) {
4220
4640
  // Normalize to forward slashes to match Vite's transform ids.
4221
4641
  documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
4642
+ },
4643
+ onClientEntryResolved(entryId) {
4644
+ startClientEntryId = entryId;
4222
4645
  }
4223
4646
  }));
4224
4647
  }