@solidjs/vite-plugin 3.0.0-next.27

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.
@@ -0,0 +1,3386 @@
1
+ import * as babel from '@babel/core';
2
+ import remapping from '@ampproject/remapping';
3
+ import solid from 'babel-preset-solid';
4
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
5
+ import { mergeAndConcat } from 'merge-anything';
6
+ import { createRequire } from 'module';
7
+ import path from 'path';
8
+ import { Readable } from 'node:stream';
9
+ import { createFilter, loadEnv, version } from 'vite';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { crawlFrameworkPkgs } from 'vitefu';
12
+
13
+ // Node <-> web-standard request/response bridging shared by the plugin's dev
14
+ // middlewares (server functions and SSR). The virtual production handlers
15
+ // speak web Request/Response only; this is the node:http glue the dev server
16
+ // needs to talk to them.
17
+
18
+
19
+ /**
20
+ * `urlPath` overrides `req.url` when the middleware needs to dispatch a
21
+ * different URL than the one node saw — the dev middlewares use it to
22
+ * restore the configured Vite `base` that the dev/preview base middleware
23
+ * stripped, so the handler always sees production-shaped URLs.
24
+ */
25
+ function webRequestFromNode(req, urlPath) {
26
+ const url = new URL(urlPath ?? req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
27
+ const headers = new Headers();
28
+ for (const [key, value] of Object.entries(req.headers)) {
29
+ if (value === undefined) continue;
30
+ if (Array.isArray(value)) {
31
+ for (const item of value) headers.append(key, item);
32
+ } else {
33
+ headers.append(key, value);
34
+ }
35
+ }
36
+ const method = req.method || 'GET';
37
+ const body = method === 'GET' || method === 'HEAD' ? undefined : Readable.toWeb(req);
38
+ return new Request(url, {
39
+ method,
40
+ headers,
41
+ body,
42
+ // undici requires half-duplex for streamed request bodies.
43
+ ...(body ? {
44
+ duplex: 'half'
45
+ } : {})
46
+ });
47
+ }
48
+ async function sendWebResponse(res, response) {
49
+ res.statusCode = response.status;
50
+ // set-cookie is the one header that must not be comma-joined.
51
+ const cookies = response.headers.getSetCookie?.();
52
+ response.headers.forEach((value, key) => {
53
+ if (key !== 'set-cookie') res.setHeader(key, value);
54
+ });
55
+ if (cookies && cookies.length) res.setHeader('set-cookie', cookies);
56
+ if (!response.body) {
57
+ res.end();
58
+ return;
59
+ }
60
+ const reader = response.body.getReader();
61
+ res.on('close', () => {
62
+ reader.cancel().catch(() => {});
63
+ });
64
+ try {
65
+ while (true) {
66
+ const {
67
+ done,
68
+ value
69
+ } = await reader.read();
70
+ if (done) break;
71
+ // A response whose client already went away never emits 'drain'
72
+ // (writes are no-ops), so a backpressure wait must also settle on
73
+ // 'close'/'error' or an aborted streaming response parks this promise
74
+ // — and the reader and Response it holds — forever.
75
+ if (res.destroyed) return;
76
+ if (!res.write(value)) {
77
+ const drained = await new Promise(resolve => {
78
+ const settle = ok => {
79
+ res.off('drain', onDrain);
80
+ res.off('close', onGone);
81
+ res.off('error', onGone);
82
+ resolve(ok);
83
+ };
84
+ const onDrain = () => settle(true);
85
+ const onGone = () => settle(false);
86
+ res.once('drain', onDrain);
87
+ res.once('close', onGone);
88
+ res.once('error', onGone);
89
+ });
90
+ // Client gone mid-stream; the 'close' handler cancels the reader.
91
+ if (!drained) return;
92
+ }
93
+ }
94
+ res.end();
95
+ } catch {
96
+ res.destroy();
97
+ }
98
+ }
99
+ function joinBase(base, pathname) {
100
+ // Absolute-URL or relative bases (CDN deploys, './') don't prefix
101
+ // same-origin server paths.
102
+ if (!base.startsWith('/')) return pathname;
103
+ return (base.endsWith('/') ? base.slice(0, -1) : base) + pathname;
104
+ }
105
+
106
+ /**
107
+ * Dev-mode asset resolution: the `virtual:solid-manifest` module exports a
108
+ * resolver function in dev (instead of the static object a build produces),
109
+ * and the runtime installs it as `context.resolveAssets` verbatim. When
110
+ * server-side `lazy()` resolves a module key, the resolver walks the SSR
111
+ * environment's live module graph collecting transitively imported CSS and
112
+ * answers with inline-style descriptors — SSR'd `<style data-vite-dev-id>`
113
+ * tags that Vite's HMR client adopts on startup, so dev CSS is styled from
114
+ * the first streamed byte without fighting Vite's own style injection.
115
+ *
116
+ * The walk design follows SolidStart's collect-styles (by @katywings): crawl
117
+ * `transformResult.deps` on the SSR environment (the client environment's
118
+ * transform results don't list CSS deps), skipping dynamic imports since
119
+ * dynamically imported modules register their own styles when they render.
120
+ */
121
+
122
+ // The resolver is created plugin-side (it closes over the dev server) but is
123
+ // called from the SSR module runner, which only shares `globalThis` with the
124
+ // plugin when it runs in-process (the default). The primary channel is a
125
+ // `Symbol.for`-keyed registry mapping project roots to resolvers; isolated
126
+ // runners (nitro's dev worker, workerd) won't find it and instead fall back
127
+ // to fetching the HTTP bridge endpoint below.
128
+ const DEV_MANIFEST_REGISTRY_KEY = '@solidjs/vite-plugin:dev-manifest';
129
+ function registerDevAssetResolver(root, resolver) {
130
+ const key = Symbol.for(DEV_MANIFEST_REGISTRY_KEY);
131
+ const registry = globalThis[key] ??= {};
132
+ registry[root] = resolver;
133
+ }
134
+
135
+ /**
136
+ * HTTP bridge endpoint for isolated SSR runners. Hosts that evaluate server
137
+ * modules outside the Vite process (nitro's dev worker, workerd via
138
+ * @cloudflare/vite-plugin) can't see the `globalThis` registry, so the dev
139
+ * server itself serves asset resolution: `GET
140
+ * /@solidjs/vite-plugin/dev-manifest?key=<module key>` answers with the
141
+ * resolver's `ResolvedAssets` JSON (`null` when the key can't be resolved).
142
+ * The dev flavor of `virtual:solid-manifest` falls back to fetching it when
143
+ * the registry has no entry for the root — in-process consumers hit the
144
+ * registry and never touch HTTP.
145
+ */
146
+ const DEV_MANIFEST_ENDPOINT = '/@solidjs/vite-plugin/dev-manifest';
147
+ function installDevManifestBridge(server) {
148
+ // configureServer middlewares run ahead of Vite's internals, so `req.url`
149
+ // may or may not still carry the configured `base` — accept both forms.
150
+ const base = (server.config.base || '/').replace(/\/$/, '');
151
+ const basedEndpoint = base + DEV_MANIFEST_ENDPOINT;
152
+ server.middlewares.use(async (req, res, next) => {
153
+ const url = new URL(req.url || '/', 'http://localhost');
154
+ if (url.pathname !== DEV_MANIFEST_ENDPOINT && url.pathname !== basedEndpoint) return next();
155
+ const key = url.searchParams.get('key');
156
+ if (!key) {
157
+ res.statusCode = 400;
158
+ return res.end('Missing asset key');
159
+ }
160
+ try {
161
+ const registry = globalThis[Symbol.for(DEV_MANIFEST_REGISTRY_KEY)];
162
+ const resolver = registry?.[server.config.root];
163
+ if (!resolver) {
164
+ // A silent null strips the module's client assets from the SSR'd
165
+ // hydration asset map and hydration fails much later with a cryptic
166
+ // client-side error — report the miss where it happens.
167
+ console.error(`[@solidjs/vite-plugin] The dev manifest registry has no resolver for root "${server.config.root}" ` + `(requested asset key "${key}"). The module's client assets cannot be resolved and hydration ` + 'will fail for it. Typical causes: the dev server was not restarted after dependency changes, ' + 'or the install is stale.');
168
+ }
169
+ const assets = resolver ? await resolver.resolve(key) : null;
170
+ if (resolver && assets == null) {
171
+ console.error(`[@solidjs/vite-plugin] Dev manifest resolver returned no assets for key "${key}" (root "${server.config.root}"). ` + "The module's hydration preload entry will be missing.");
172
+ }
173
+ res.setHeader('content-type', 'application/json');
174
+ res.setHeader('cache-control', 'no-store');
175
+ return res.end(JSON.stringify(assets));
176
+ } catch (error) {
177
+ return next(error);
178
+ }
179
+ });
180
+ }
181
+
182
+ /**
183
+ * The absolute URL isolated runners should fetch the bridge from, baked into
184
+ * the dev flavor of `virtual:solid-manifest` when its code is generated.
185
+ * Generation happens while serving an SSR request, so the server is already
186
+ * listening and `resolvedUrls` carries the real origin (a config-time define
187
+ * could only guess the port). Middleware-mode servers have no origin of
188
+ * their own to advertise — returns null there, and the manifest module keeps
189
+ * the js-only fallback (in-process registry hits are unaffected either way).
190
+ */
191
+ function devManifestBridgeUrl(server) {
192
+ const local = server.resolvedUrls?.local?.[0];
193
+ let origin = null;
194
+ if (local) {
195
+ origin = new URL(local).origin;
196
+ } else if (!server.config.server.middlewareMode) {
197
+ const address = server.httpServer?.address();
198
+ if (address && typeof address === 'object') {
199
+ const https = !!server.config.server.https;
200
+ origin = `${https ? 'https' : 'http'}://localhost:${address.port}`;
201
+ }
202
+ }
203
+ if (!origin) return null;
204
+ const base = (server.config.base || '/').replace(/\/$/, '');
205
+ return origin + base + DEV_MANIFEST_ENDPOINT;
206
+ }
207
+
208
+ // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts
209
+ const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
210
+ // Queried css imports (?url, ?inline, ?raw) are not ambient styles — the
211
+ // importer controls them — so they must not be SSR'd as style tags.
212
+ const nonAmbientQueryRegExp = /[?&](url|inline|raw)\b/;
213
+ const NULL_BYTE_PLACEHOLDER = '/@id/__x00__';
214
+
215
+ // Per Vite's convention virtual module ids are prefixed with `\0`, which
216
+ // cannot appear in an HTML attribute (the parser replaces it). Serialize the
217
+ // same placeholder form Vite's own URLs use. Adoption of virtual-module
218
+ // styles additionally needs `devStylePatch` (below) to run client-side;
219
+ // fs-backed CSS (the overwhelmingly common case) adopts without it.
220
+ function wrapId(id) {
221
+ return id.replace(/^\0/, NULL_BYTE_PLACEHOLDER);
222
+ }
223
+
224
+ /**
225
+ * Inline dev script reconciling SSR'd style tags with Vite's HMR client.
226
+ * Frameworks that server-render whole documents should inline this in dev,
227
+ * in `<head>` before any module script. It does two things, via a
228
+ * MutationObserver so styles appended by streamed boundaries are handled as
229
+ * they arrive (Vite's client seeds its stylesheet registry from the DOM only
230
+ * once, when its module evaluates):
231
+ *
232
+ * - Rewrites serialized virtual-module ids (`/@id/__x00__…`) back to Vite's
233
+ * null-byte form so seeding matches (a raw `\0` can't survive HTML).
234
+ * - Dedupes twins: a style tag that streams in after Vite's client has
235
+ * seeded is missed by the scan, so the CSS module injects its own copy
236
+ * client-side. Whenever two style tags share a `data-vite-dev-id`, the
237
+ * SSR'd one (marked `data-asset`) is removed in favor of the Vite-owned
238
+ * one, which is the tag HMR updates.
239
+ *
240
+ * Observation is two-phase to stay cheap: a document-wide subtree observer
241
+ * only for the streaming window (SSR tags can only arrive while the parser
242
+ * is consuming the stream; DOMContentLoaded marks its end), then a
243
+ * childList-only observer on `document.head` for the page lifetime — Vite
244
+ * injects twins into the head during hydration, which continues past
245
+ * DOMContentLoaded, and a non-subtree head observer never fires on app DOM
246
+ * churn, only on head insertions.
247
+ *
248
+ * Descends from SolidStart's PatchVirtualDevStyles (by @katywings); this
249
+ * belongs in Vite itself eventually.
250
+ */
251
+ const devStylePatch = `(function(){var P=${JSON.stringify(NULL_BYTE_PLACEHOLDER)};var handle=function(el){var v=el.getAttribute("data-vite-dev-id");if(!v)return;if(v.indexOf(P)===0){v="\\0"+v.slice(P.length);el.setAttribute("data-vite-dev-id",v)}var all=document.querySelectorAll("style[data-vite-dev-id]");for(var i=0;i<all.length;i++){var o=all[i];if(o!==el&&o.getAttribute("data-vite-dev-id")===v){var ssr=o.hasAttribute("data-asset")?o:el.hasAttribute("data-asset")?el:null;if(ssr)ssr.remove();break}}};var scan=function(n){if(n.nodeType!==1)return;if(n.tagName==="STYLE")handle(n);else if(n.querySelectorAll)n.querySelectorAll("style[data-vite-dev-id]").forEach(handle)};var onMuts=function(muts){for(var i=0;i<muts.length;i++)muts[i].addedNodes.forEach(scan)};var headPhase=function(){scan(document.documentElement);new MutationObserver(onMuts).observe(document.head,{childList:true})};scan(document.documentElement);if(document.readyState==="loading"){var mo=new MutationObserver(onMuts);mo.observe(document.documentElement,{childList:true,subtree:true});document.addEventListener("DOMContentLoaded",function(){mo.disconnect();headPhase()})}else headPhase()})();`;
252
+ async function getModuleNode(env, file, importer) {
253
+ try {
254
+ // fetchModule resolves through the plugin container with importer
255
+ // context, so dep strings that are placeholder-wrapped virtual URLs
256
+ // (`/@id/__x00__…`) or importer-relative specifiers land on the right
257
+ // module id — a raw moduleGraph/transformRequest lookup would miss them.
258
+ const resolved = await env.fetchModule(file, importer);
259
+ if (!('id' in resolved)) return;
260
+ return env.moduleGraph.getModuleById(resolved.id);
261
+ } catch {
262
+ return;
263
+ }
264
+ }
265
+ async function collectModuleDeps(env, file, deps, crawled, onFile, importer) {
266
+ crawled.add(file);
267
+ const node = await getModuleNode(env, file, importer);
268
+ if (!node?.id || deps.has(node)) return;
269
+ deps.add(node);
270
+ if (node.file && !node.id.includes('node_modules')) onFile?.(node.file);
271
+ if (cssFileRegExp.test(node.url.split('?')[0]) || node.id.includes('node_modules')) return;
272
+ if (!node.transformResult) {
273
+ await env.transformRequest(node.url).catch(() => {});
274
+ }
275
+ const directDeps = node.transformResult?.deps;
276
+ if (!directDeps) return;
277
+
278
+ // transformResult.deps (unlike importedModules) separates static imports
279
+ // from dynamicDeps — dynamic imports load their own styles when rendered.
280
+ for (const dep of directDeps) {
281
+ if (crawled.has(dep)) continue;
282
+ await collectModuleDeps(env, dep, deps, crawled, onFile, node.id);
283
+ }
284
+ }
285
+ function injectQuery(url, query) {
286
+ return url.includes('?') ? `${url}&${query}` : `${url}?${query}`;
287
+ }
288
+
289
+ /** Discovers ambient CSS in an entry graph without choosing how it is transported. */
290
+ async function collectDevStyleSources(env, files, onFile) {
291
+ const deps = new Set();
292
+ const crawled = new Set();
293
+ for (const file of files) {
294
+ await collectModuleDeps(env, file, deps, crawled, onFile);
295
+ }
296
+ const css = [];
297
+ const seen = new Set();
298
+ for (const node of deps) {
299
+ if (!node.id) continue;
300
+ const cleanUrl = node.url.split('?')[0];
301
+ if (!cssFileRegExp.test(cleanUrl) || nonAmbientQueryRegExp.test(node.url)) continue;
302
+ const id = wrapId(node.id);
303
+ if (seen.has(id)) continue;
304
+ seen.add(id);
305
+ css.push({
306
+ id,
307
+ url: node.url
308
+ });
309
+ }
310
+ return css;
311
+ }
312
+
313
+ /**
314
+ * Walks the SSR module graph from `files` (root-relative or absolute) and
315
+ * returns inline-style descriptors for every transitively imported CSS
316
+ * module — the same shape the dev asset resolver answers with for lazy
317
+ * modules. Used by SSR start mode's dev middleware to inline the root entry's
318
+ * CSS into `<head>` so server-painted content is styled from the first byte
319
+ * (no FOUC while waiting for Vite's client-side style injection).
320
+ */
321
+ async function collectDevStyles(server, files) {
322
+ const ssrEnv = server.environments?.ssr;
323
+ const clientEnv = server.environments?.client;
324
+ if (!ssrEnv || !clientEnv) return [];
325
+ const sources = await collectDevStyleSources(ssrEnv, files.map(file => path.resolve(server.config.root, file)));
326
+ const css = [];
327
+ for (const source of sources) {
328
+ // `?direct` yields the compiled stylesheet text (what Vite serves for
329
+ // <link> requests) — through the client environment, whose css
330
+ // pipeline matches what the browser will run for HMR updates.
331
+ const result = await clientEnv.transformRequest(injectQuery(source.url, 'direct')).catch(() => null);
332
+ if (result?.code == null) continue;
333
+ css.push({
334
+ id: source.id,
335
+ content: result.code,
336
+ attrs: {
337
+ 'data-vite-dev-id': source.id
338
+ }
339
+ });
340
+ }
341
+ return css;
342
+ }
343
+ function escapeAttr(value) {
344
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
345
+ }
346
+
347
+ /**
348
+ * Serializes a dev style descriptor to the exact tag shape the SSR runtime
349
+ * emits for lazy-registered assets (`data-asset` marks the SSR'd copy so
350
+ * `devStylePatch` knows which twin to drop when Vite's client injects its
351
+ * own), so the dedup story is identical for entry styles and lazy styles.
352
+ */
353
+ function renderDevStyleTag(desc) {
354
+ let attrs = '';
355
+ for (const name in desc.attrs) {
356
+ attrs += ` ${name}="${escapeAttr(String(desc.attrs[name]))}"`;
357
+ }
358
+ const content = desc.content.replace(/<\/(style)/gi, '<\\/$1');
359
+ return `<style data-asset="${escapeAttr(desc.id)}"${attrs}>${content}</style>`;
360
+ }
361
+
362
+ /**
363
+ * Browser URL for a lazy module's dev asset key (a project-root-relative
364
+ * path, query included when the module identity carries one). Vite only
365
+ * serves module URLs under the configured `base`, so it is always applied;
366
+ * root-external keys (`../…`, e.g. sibling workspace packages) can't be
367
+ * expressed as root-relative URLs at all — they get Vite's `/@fs/` form on
368
+ * the resolved absolute path instead. Mirrored by the generated fallback in
369
+ * `devManifestCode` (src/index.ts) — keep the two in sync.
370
+ */
371
+ function devModuleUrl(root, base, key) {
372
+ const queryIndex = key.indexOf('?');
373
+ const file = queryIndex === -1 ? key : key.slice(0, queryIndex);
374
+ const query = queryIndex === -1 ? '' : key.slice(queryIndex);
375
+ if (!file.startsWith('..')) return joinBase(base, '/' + key);
376
+ const absolute = path.resolve(root, file).split(path.sep).join('/');
377
+ // Vite's fs URLs collapse the leading slash: /@fs/Users/… (and keep the
378
+ // drive letter on Windows: /@fs/C:/…).
379
+ return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query);
380
+ }
381
+ function createDevAssetResolver(server) {
382
+ // Server-side lazy() re-requests a module's assets on every retry of a
383
+ // suspended render pass (retries re-create the component). The build
384
+ // manifest answers those repeats synchronously and the pass converges; an
385
+ // always-async resolver instead suspends every retry on a brand-new
386
+ // promise, so a pass whose retry path re-creates the lazy component (a
387
+ // nested route's outlet does) loops forever — each cycle nests one resume
388
+ // closure until the render stack overflows and the escaped rejection kills
389
+ // the dev server. So: dedupe in-flight walks per key and answer
390
+ // synchronously once a key's assets are known. Any watcher event drops the
391
+ // cache — the next request re-walks the updated module graph, keeping dev
392
+ // CSS fresh.
393
+ const resolved = new Map();
394
+ const pending = new Map();
395
+ const {
396
+ root,
397
+ base
398
+ } = server.config;
399
+ let generation = 0;
400
+ server.watcher.on('all', () => {
401
+ generation++;
402
+ resolved.clear();
403
+ pending.clear();
404
+ });
405
+ const resolve = function resolveDevAssets(key) {
406
+ const cached = resolved.get(key);
407
+ if (cached) return cached;
408
+ let walk = pending.get(key);
409
+ if (!walk) {
410
+ const startedAt = generation;
411
+ walk = (async () => {
412
+ // The module's dev URL doubles as its client entry: modulepreload
413
+ // hint and hydration module-map value.
414
+ const js = [devModuleUrl(root, base, key)];
415
+ const css = await collectDevStyles(server, [key]);
416
+ return {
417
+ js,
418
+ css
419
+ };
420
+ })().then(assets => {
421
+ if (generation === startedAt) {
422
+ resolved.set(key, assets);
423
+ pending.delete(key);
424
+ }
425
+ return assets;
426
+ }, error => {
427
+ if (generation === startedAt) pending.delete(key);
428
+ throw error;
429
+ });
430
+ pending.set(key, walk);
431
+ }
432
+ return walk;
433
+ };
434
+ return {
435
+ resolve,
436
+ resolveSync: key => resolved.get(key) ?? {
437
+ js: [devModuleUrl(root, base, key)],
438
+ css: []
439
+ }
440
+ };
441
+ }
442
+
443
+ const VIRTUAL_ID = '\0@solidjs/vite-plugin:boundary-modules';
444
+
445
+ /**
446
+ * `server-only` and `client-only` marker modules: importing `server-only`
447
+ * from a module bundled for the client fails the build at resolve time with
448
+ * a descriptive error (and vice versa for `client-only`); in the allowed
449
+ * environment the marker resolves to an empty module.
450
+ *
451
+ * Server-only code pulled into a client bundle otherwise ships silently and
452
+ * crashes at runtime (in hydrating apps, typically as a cryptic hydration
453
+ * failure far from the real cause) — the marker turns that into a build
454
+ * error naming the importer.
455
+ *
456
+ * Always on (`enforce: 'pre'`), so the bare specifiers are claimed by this
457
+ * plugin even when React's `server-only`/`client-only` npm packages are
458
+ * installed — the environment semantics are the same, and claiming them
459
+ * keeps the behavior deterministic and the errors identifiable as ours.
460
+ */
461
+ function boundaryModules() {
462
+ return {
463
+ name: 'solid:boundary-modules',
464
+ enforce: 'pre',
465
+ resolveId(id, importer, options) {
466
+ // The dep scanner (`vite:dep-scan`) crawls the client entries' RAW
467
+ // import graph — no directive transforms have run, so it walks
468
+ // straight through 'use server' modules into genuinely server-only
469
+ // code. That graph is legal once transforms split it, so the guard
470
+ // must not fire on the scan pass (`options.scan`, the flag Vite's
471
+ // scanner sets on plugin-container resolves in v6/7 and the rolldown
472
+ // scanner in v8). Still claim the specifier: resolving to the empty
473
+ // virtual module keeps the scanner from chasing `server-only` /
474
+ // `client-only` as missing bare dependencies, which would abort the
475
+ // scan all the same. Real dev/build module graphs resolve without
476
+ // the flag and stay fully guarded.
477
+ const scan = !!options?.scan;
478
+ if (id === 'server-only') {
479
+ if (!options?.ssr && !scan) this.error(`[@solidjs/vite-plugin] Attempt to import 'server-only' in a client module: ${importer}. ` + `Code that uses this module must run only on the server — make sure it is only ` + `imported by server code (e.g. a server entry, a "use server" module, or code ` + `reached exclusively from them).`);
480
+ } else if (id === 'client-only') {
481
+ if (options?.ssr && !scan) this.error(`[@solidjs/vite-plugin] Attempt to import 'client-only' in a server module: ${importer}. ` + `Code that uses this module must run only in the browser — make sure it is only ` + `imported by client code (e.g. behind a client-only lazy boundary).`);
482
+ } else {
483
+ return null;
484
+ }
485
+ return VIRTUAL_ID;
486
+ },
487
+ load(id) {
488
+ if (id === VIRTUAL_ID) return 'export {}';
489
+ }
490
+ };
491
+ }
492
+
493
+ /**
494
+ * Cross-instance-safe stand-in for vite's `isRunnableDevEnvironment`.
495
+ *
496
+ * Vite's helper is an `instanceof RunnableDevEnvironment` check against the
497
+ * class of whichever `vite` module the CALLER imported. When this plugin is
498
+ * consumed through a workspace/`link:` install, its own `vite` import can
499
+ * resolve to a different physical copy than the one running the dev server —
500
+ * and then the `instanceof` is false for every environment, silently standing
501
+ * the SSR/dev middlewares down. The `runner` accessor is the type's defining
502
+ * member (`RunnableDevEnvironment` is exactly "a DevEnvironment with a
503
+ * runner"), so presence-check it instead of trusting class identity.
504
+ */
505
+ function isRunnableEnvironment(environment) {
506
+ return !!environment && typeof environment === 'object' && 'runner' in environment;
507
+ }
508
+
509
+ // The `"use server"` directive compiler. This wraps the native
510
+ // `transformDirectives` pass from @dom-expressions/compiler (Rust/Oxc); the
511
+ // original Babel implementation (hoisted from solid-start) lived in this
512
+ // directory through vite-plugin-solid@c052963e and remains the frozen
513
+ // reference for the native pass's fixture suite in dom-expressions.
514
+
515
+ let compilerPromise;
516
+
517
+ // Loaded lazily so importing the plugin never pays for the native binding —
518
+ // only setups that enable server functions load it (mirrors the JSX
519
+ // compiler's opt-in loader in index.ts).
520
+ async function loadCompiler() {
521
+ try {
522
+ return await (compilerPromise ??= import('@dom-expressions/compiler'));
523
+ } catch (error) {
524
+ compilerPromise = undefined;
525
+ const reason = error instanceof Error ? `\n\nCause: ${error.message}` : '';
526
+ throw new Error('@solidjs/vite-plugin: failed to load @dom-expressions/compiler (the "use server" ' + 'transform). Your platform should get a prebuilt native binary or the ' + '@dom-expressions/compiler-wasm32-wasi fallback — check that optional ' + 'dependencies were installed.' + reason);
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Runs the directive transform over one module. Function IDs are
532
+ * `hash(relative path)-<counter>`, so the client and server builds of the
533
+ * same checkout agree on every ID (the wire contract) without baking
534
+ * machine-specific absolute paths into the output. A `valid: false` result
535
+ * means the module contained no matching directive and must be left
536
+ * untransformed. Invalid closure captures (a server function referencing a
537
+ * non-top-level binding) throw with the variable name and location.
538
+ */
539
+ async function compile(id, code, options) {
540
+ const {
541
+ transformDirectives
542
+ } = await loadCompiler();
543
+ const result = transformDirectives(code, {
544
+ filename: id,
545
+ root: options.root,
546
+ mode: options.mode,
547
+ env: options.env,
548
+ directive: options.directive,
549
+ sourceMap: true,
550
+ register: options.definitions.register,
551
+ create: options.definitions.create
552
+ });
553
+ return {
554
+ valid: result.valid,
555
+ code: result.code,
556
+ map: result.map ?? null,
557
+ functions: result.functions
558
+ };
559
+ }
560
+
561
+ // @ts-nocheck
562
+ /**
563
+ * Hoisted from solid-start (packages/start/src/directives/xxhash32.ts).
564
+ *
565
+ * Copyright (c) 2019 Jason Dent
566
+ * https://github.com/Jason3S/xxhash
567
+ */
568
+ const PRIME32_1 = 2654435761;
569
+ const PRIME32_2 = 2246822519;
570
+ const PRIME32_3 = 3266489917;
571
+ const PRIME32_4 = 668265263;
572
+ const PRIME32_5 = 374761393;
573
+ function toUtf8(text) {
574
+ const bytes = [];
575
+ for (let i = 0, n = text.length; i < n; ++i) {
576
+ const c = text.charCodeAt(i);
577
+ if (c < 0x80) {
578
+ bytes.push(c);
579
+ } else if (c < 0x800) {
580
+ bytes.push(0xc0 | c >> 6, 0x80 | c & 0x3f);
581
+ } else if (c < 0xd800 || c >= 0xe000) {
582
+ bytes.push(0xe0 | c >> 12, 0x80 | c >> 6 & 0x3f, 0x80 | c & 0x3f);
583
+ } else {
584
+ const cp = 0x10000 + ((c & 0x3ff) << 10 | text.charCodeAt(++i) & 0x3ff);
585
+ bytes.push(0xf0 | cp >> 18 & 0x7, 0x80 | cp >> 12 & 0x3f, 0x80 | cp >> 6 & 0x3f, 0x80 | cp & 0x3f);
586
+ }
587
+ }
588
+ return new Uint8Array(bytes);
589
+ }
590
+
591
+ /**
592
+ * @param buffer - byte array or string
593
+ * @param seed - optional seed (32-bit unsigned)
594
+ */
595
+ function xxHash32(buffer, seed = 0) {
596
+ buffer = typeof buffer === 'string' ? toUtf8(buffer) : buffer;
597
+ const b = buffer;
598
+
599
+ // Step 1. Initialize internal accumulators
600
+ let acc = seed + PRIME32_5 & 0xffffffff;
601
+ let offset = 0;
602
+ if (b.length >= 16) {
603
+ const accN = [seed + PRIME32_1 + PRIME32_2 & 0xffffffff, seed + PRIME32_2 & 0xffffffff, seed + 0 & 0xffffffff, seed - PRIME32_1 & 0xffffffff];
604
+
605
+ // Step 2. Process stripes (16 bytes = 4 lanes of 4 bytes)
606
+ const b = buffer;
607
+ const limit = b.length - 16;
608
+ let lane = 0;
609
+ for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) {
610
+ const i = offset;
611
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
612
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
613
+ const laneNP = laneN0 * PRIME32_2 + (laneN1 * PRIME32_2 << 16);
614
+ let acc = accN[lane] + laneNP & 0xffffffff;
615
+ acc = acc << 13 | acc >>> 19;
616
+ const acc0 = acc & 0xffff;
617
+ const acc1 = acc >>> 16;
618
+ accN[lane] = acc0 * PRIME32_1 + (acc1 * PRIME32_1 << 16) & 0xffffffff;
619
+ lane = lane + 1 & 0x3;
620
+ }
621
+
622
+ // Step 3. Accumulator convergence
623
+ acc = (accN[0] << 1 | accN[0] >>> 31) + (accN[1] << 7 | accN[1] >>> 25) + (accN[2] << 12 | accN[2] >>> 20) + (accN[3] << 18 | accN[3] >>> 14) & 0xffffffff;
624
+ }
625
+
626
+ // Step 4. Add input length
627
+ acc = acc + buffer.length & 0xffffffff;
628
+
629
+ // Step 5. Consume remaining input (up to 15 bytes)
630
+ const limit = buffer.length - 4;
631
+ for (; offset <= limit; offset += 4) {
632
+ const i = offset;
633
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
634
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
635
+ const laneP = laneN0 * PRIME32_3 + (laneN1 * PRIME32_3 << 16);
636
+ acc = acc + laneP & 0xffffffff;
637
+ acc = acc << 17 | acc >>> 15;
638
+ acc = (acc & 0xffff) * PRIME32_4 + ((acc >>> 16) * PRIME32_4 << 16) & 0xffffffff;
639
+ }
640
+ for (; offset < b.length; ++offset) {
641
+ const lane = b[offset];
642
+ acc += lane * PRIME32_5;
643
+ acc = acc << 11 | acc >>> 21;
644
+ acc = (acc & 0xffff) * PRIME32_1 + ((acc >>> 16) * PRIME32_1 << 16) & 0xffffffff;
645
+ }
646
+
647
+ // Step 6. Final mix (avalanche)
648
+ acc ^= acc >>> 15;
649
+ acc = ((acc & 0xffff) * PRIME32_2 & 0xffffffff) + ((acc >>> 16) * PRIME32_2 << 16);
650
+ acc ^= acc >>> 13;
651
+ acc = ((acc & 0xffff) * PRIME32_3 & 0xffffffff) + ((acc >>> 16) * PRIME32_3 << 16);
652
+ acc ^= acc >>> 16;
653
+
654
+ // turn any negatives back into a positive number;
655
+ return acc < 0 ? acc + 4294967296 : acc;
656
+ }
657
+
658
+ // Hoisted from solid-start (packages/start/src/directives/index.ts).
659
+ //
660
+ // Standalone `"use server"` support for Vite. The compiler half of server
661
+ // functions lives here; the runtime half (registration on the server, a
662
+ // transport on the client) is @solidjs/web/server-functions by default —
663
+ // the compiled output imports `registerServerReference` /
664
+ // `createServerReference` from that specifier and the package's export
665
+ // conditions resolve the right half per environment. Any runtime satisfying
666
+ // that contract can be swapped in through `options.runtime` (SolidStart's,
667
+ // or your own).
668
+
669
+ /**
670
+ * Picomatch patterns selecting the modules the directive compiler runs on.
671
+ * Relative patterns (the defaults included) are resolved against the Vite
672
+ * root — not the invocation directory — so running `vite` from outside the
673
+ * project keeps compiling the same files. Absolute patterns are used as-is.
674
+ *
675
+ * @default include "src/**\/*.{jsx,tsx,ts,js,mjs,cjs}", exclude "node_modules/**\/*.{jsx,tsx,ts,js,mjs,cjs}"
676
+ */
677
+
678
+ const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,ts,js,mjs,cjs}';
679
+ const DEFAULT_EXCLUDE = 'node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}';
680
+ const DEFAULT_MANIFEST = 'virtual:solid-server-function-manifest';
681
+ const DEFAULT_DIRECTIVE = 'use server';
682
+ const DEFAULT_RUNTIME = '@solidjs/web/server-functions';
683
+ // Must match the runtime's built-in default — when the resolved endpoint
684
+ // equals it, no configure calls need to be emitted at all.
685
+ const DEFAULT_ENDPOINT = '/_server';
686
+ const STORAGE_SOURCE$1 = '@solidjs/web/storage';
687
+ // Server-only handler: importing it wires the endpoint in one line
688
+ // (registrations via the manifest, request-event scoping, endpoint config).
689
+ const HANDLER_ID$1 = 'virtual:solid-server-function-handler';
690
+
691
+ // Server functions referenced only from client-side code (e.g. event
692
+ // handlers, which the SSR JSX compile drops) never get imported — or even
693
+ // transformed — by the server build, so their registrations would be missing
694
+ // at runtime. That's why the client transform records modules into the
695
+ // *server* manifest set. The dev server and Vite's builder mode share one
696
+ // process where that just works; the classic two-invocation build
697
+ // (`vite build` then `vite build --ssr`) does not, so the client build
698
+ // persists its findings for the SSR build to merge (mirroring the plugin's
699
+ // dist/client/.vite/manifest.json convention).
700
+ const PERSISTED_MANIFEST_PATH = '.vite/solid-server-functions.json';
701
+ function readPersistedManifest(root) {
702
+ const file = path.resolve(root, 'dist/client', PERSISTED_MANIFEST_PATH);
703
+ if (!existsSync(file)) return new Set();
704
+ try {
705
+ const entries = JSON.parse(readFileSync(file, 'utf-8'));
706
+ return new Set(entries.map(entry => path.resolve(root, entry)).filter(entry => existsSync(entry)));
707
+ } catch {
708
+ return new Set();
709
+ }
710
+ }
711
+ function writePersistedManifest(root, outDir, entries) {
712
+ const file = path.resolve(root, outDir, PERSISTED_MANIFEST_PATH);
713
+ mkdirSync(path.dirname(file), {
714
+ recursive: true
715
+ });
716
+ const relative = [...entries].map(entry => path.relative(root, entry).split(path.sep).join('/'));
717
+ writeFileSync(file, JSON.stringify(relative, null, 2));
718
+ }
719
+ function createManifest() {
720
+ return {
721
+ server: new Set(),
722
+ client: new Set()
723
+ };
724
+ }
725
+ function createDeferredPromise() {
726
+ let resolve;
727
+ let reject;
728
+ return {
729
+ reference: new Promise((res, rej) => {
730
+ resolve = res;
731
+ reject = rej;
732
+ }),
733
+ resolve(value) {
734
+ resolve(value);
735
+ },
736
+ reject(value) {
737
+ reject(value);
738
+ }
739
+ };
740
+ }
741
+
742
+ // The manifest can only be emitted once every module has been transformed
743
+ // (each transform may register new entries), but Vite gives no such signal —
744
+ // so the manifest load resolves a debounced snapshot that transforms keep
745
+ // pushing back while they are still landing.
746
+ class Debouncer {
747
+ constructor(source) {
748
+ this.source = source;
749
+ this.promise = createDeferredPromise();
750
+ this.defer();
751
+ }
752
+ defer() {
753
+ if (this.timeout) {
754
+ clearTimeout(this.timeout);
755
+ this.timeout = undefined;
756
+ }
757
+ this.timeout = setTimeout(() => {
758
+ this.promise.resolve(this.source());
759
+ }, 1000);
760
+ }
761
+ }
762
+ function mergeManifestRecord(source, target) {
763
+ const current = source.size;
764
+ for (const entry of target) {
765
+ source.add(entry);
766
+ }
767
+ return {
768
+ invalidPreload: current !== source.size,
769
+ invalidated: [...source]
770
+ };
771
+ }
772
+ function invalidateModule(moduleGraph, path) {
773
+ const target = moduleGraph.getModuleById(path);
774
+ if (target) {
775
+ moduleGraph.invalidateModule(target);
776
+ }
777
+ }
778
+ function invalidateModules(server, result, manifest) {
779
+ // `environments` requires Vite 6+; older versions just miss the eager
780
+ // manifest invalidation (the debounced reload still converges).
781
+ if (server?.environments && result.invalidPreload) {
782
+ invalidateModule(server.environments.client.moduleGraph, manifest);
783
+ invalidateModule(server.environments.ssr.moduleGraph, manifest);
784
+ }
785
+ }
786
+
787
+ /**
788
+ * The second parameter is internal wiring for the main plugin's
789
+ * `serverFunctions` option: the built-in dev middleware is only installed
790
+ * through that path, so meta-frameworks composing this factory directly
791
+ * (and dispatching to `handleServerFunctionRequest` themselves) never race
792
+ * it for the endpoint. On the main plugin's path the public
793
+ * `options.devMiddleware` (default true) can opt back out of it.
794
+ */
795
+ function serverFunctions(options = {}, internal = {}) {
796
+ const filterInclude = options.filter?.include || DEFAULT_INCLUDE;
797
+ const filterExclude = options.filter?.exclude || DEFAULT_EXCLUDE;
798
+ // Recreated in configResolved: relative patterns (the defaults included)
799
+ // must resolve against the Vite root, not process.cwd() — running `vite`
800
+ // from outside the project would otherwise silently skip every module.
801
+ let filter = createFilter(filterInclude, filterExclude);
802
+ const manifestId = options.manifest || DEFAULT_MANIFEST;
803
+ const directive = options.directive || DEFAULT_DIRECTIVE;
804
+ const runtime = options.runtime || {
805
+ server: DEFAULT_RUNTIME,
806
+ client: DEFAULT_RUNTIME
807
+ };
808
+ const endpointOption = options.endpoint || DEFAULT_ENDPOINT;
809
+ const endpoint = endpointOption.startsWith('/') ? endpointOption : '/' + endpointOption;
810
+ const components = !!options.components;
811
+ // The middleware only exists on the main plugin's path to begin with (see the
812
+ // `internal` parameter doc); the public option opts out of it there.
813
+ const installDevMiddleware = !!internal.devMiddleware && options.devMiddleware !== false;
814
+ let env;
815
+ let root = process.cwd();
816
+ let base = '/';
817
+ let isBuild = false;
818
+ let isSsrBuild = false;
819
+ let outDir = 'dist';
820
+ // Endpoint with Vite `base` applied; final after configResolved, which
821
+ // runs before every transform/load/middleware that reads it.
822
+ let resolvedEndpoint = endpoint;
823
+ // Absolute path of the user's `configure` module; resolved (and existence-
824
+ // checked) in configResolved, before any handler load can read it.
825
+ let configureModulePath = null;
826
+ const manifest = createManifest();
827
+ const preload = {
828
+ server: undefined,
829
+ client: undefined
830
+ };
831
+ let currentServer;
832
+ const clientOptions = {
833
+ directive,
834
+ definitions: {
835
+ register: {
836
+ kind: 'named',
837
+ name: 'registerServerReference',
838
+ source: runtime.client
839
+ },
840
+ create: {
841
+ kind: 'named',
842
+ name: 'createServerReference',
843
+ source: runtime.client
844
+ }
845
+ }
846
+ };
847
+ const serverOptions = {
848
+ directive,
849
+ definitions: {
850
+ register: {
851
+ kind: 'named',
852
+ name: 'registerServerReference',
853
+ source: runtime.server
854
+ },
855
+ create: {
856
+ kind: 'named',
857
+ name: 'createServerReference',
858
+ source: runtime.server
859
+ }
860
+ }
861
+ };
862
+
863
+ // A non-default endpoint (custom option, or Vite `base` prefixing the
864
+ // default) must reach the runtime on both sides — the client transport
865
+ // reads it for every fetch, the server for rendered reference `.url`s.
866
+ // References are only reachable through compiled modules, so appending the
867
+ // configure call to each guarantees it runs before any reference is used.
868
+ // The default endpoint appends nothing, keeping compiled output byte-
869
+ // identical for setups that wire the runtime themselves.
870
+ function endpointConfigureSnippet(mode) {
871
+ if (resolvedEndpoint === DEFAULT_ENDPOINT) return '';
872
+ const name = mode === 'server' ? 'configureServerFunctionsServer' : 'configureServerFunctionsClient';
873
+ const source = mode === 'server' ? runtime.server : runtime.client;
874
+ return `\nimport { ${name} as $$configureServerFunctions } from ${JSON.stringify(source)};` + `\n$$configureServerFunctions({ endpoint: ${JSON.stringify(resolvedEndpoint)} });\n`;
875
+ }
876
+
877
+ // Dev omits the manifest import: the middleware loads the referenced
878
+ // module on demand instead (importing the debounced manifest would stall
879
+ // the first request and eagerly SSR-load every server-function module).
880
+ // Builds import it so tree-shaking can't drop registrations for functions
881
+ // only client code references.
882
+ function handlerModuleCode(includeManifest) {
883
+ // Server components ride the frame-stream wire protocol: the transform
884
+ // serves a function's component result as streamed HTML instead of data.
885
+ // Installing it here (config-level, merged with the other keys) covers
886
+ // both dispatch surfaces — the dev middleware and the prod handler load
887
+ // this module before dispatching — with zero per-request wiring. The
888
+ // import is only emitted when the option is on, so disabled setups keep
889
+ // a server-component-free graph.
890
+ return [
891
+ // The user's `configure` module comes first: a side-effect import in
892
+ // the handler graph, evaluated before any dispatch on both surfaces
893
+ // (dev middleware and prod handler) and bundled into the handler
894
+ // chunk by production builds. Order relative to the configure call
895
+ // below doesn't actually matter — runtime config merges per key —
896
+ // import-first is just the cleaner shape.
897
+ ...(configureModulePath ? [`import ${JSON.stringify(configureModulePath)};`] : []), ...(includeManifest ? [`import ${JSON.stringify(manifestId)};`] : []), `import { handleServerFunctionRequest as handle, configureServerFunctionsServer } from ${JSON.stringify(runtime.server)};`, `import { provideRequestEvent } from ${JSON.stringify(STORAGE_SOURCE$1)};`, ...(components ? [`import { frameTransformResult, frameTransformFlightResult, frameTransformDirectResult } from '@solidjs/web/frames';`] : []),
898
+ // `transformFlightResult` is the single-flight leg of the same wire
899
+ // protocol: a mutation whose invalidated payload includes markup gets
900
+ // the frame stream as its carrier (regions + envelope in one
901
+ // response). It only runs when a router registered a collectFlightData
902
+ // hook, so installing it unconditionally alongside the result
903
+ // transform costs disabled setups nothing.
904
+ //
905
+ // `transformDirectResult` is ALSO installed here — not just in the
906
+ // generated SSR entry — because flight collection makes direct
907
+ // (in-process) calls during handler dispatch, and the transform is what
908
+ // brands their results with the call address the client matches showing
909
+ // boundaries against. The SSR entry usually loads first and installs
910
+ // the same value (config merges per key), but the handler graph cannot
911
+ // depend on that: in dev, a mutation from an already-open page can be
912
+ // the first request after a server restart.
913
+ `configureServerFunctionsServer({ provideEvent: provideRequestEvent, endpoint: ${JSON.stringify(resolvedEndpoint)}${components ? ', transformResult: frameTransformResult, transformFlightResult: frameTransformFlightResult, transformDirectResult: frameTransformDirectResult' : ''} });`, `export const endpoint = ${JSON.stringify(resolvedEndpoint)};`, `export function handleServerFunctionRequest(request, options) {`, ` return handle(request, { provideEvent: provideRequestEvent, ...options });`, `}`].join('\n');
914
+ }
915
+
916
+ // Function IDs are `xxHash32(root-relative path)-<count>` (see compile.ts),
917
+ // so the hash segment maps an incoming ID back to its module. Rebuilt
918
+ // whenever a transform has grown the manifest.
919
+ const hashIndex = new Map();
920
+ let hashIndexSize = -1;
921
+ function moduleForFunctionId(functionId) {
922
+ if (manifest.server.size !== hashIndexSize) {
923
+ hashIndex.clear();
924
+ for (const entry of manifest.server) {
925
+ const relative = path.relative(root, entry).split(path.sep).join('/');
926
+ hashIndex.set(xxHash32(relative).toString(16), entry);
927
+ }
928
+ hashIndexSize = manifest.server.size;
929
+ }
930
+ return hashIndex.get(functionId.split('-', 1)[0]);
931
+ }
932
+ function moduleDevUrl(entry) {
933
+ const relative = path.relative(root, entry).split(path.sep).join('/');
934
+ return relative.startsWith('..') ? '/@fs/' + entry : '/' + relative;
935
+ }
936
+ const turnkeyPlugins = [{
937
+ name: 'solid:server-functions/handler',
938
+ enforce: 'pre',
939
+ resolveId(source, _importer, opts) {
940
+ if (source === HANDLER_ID$1) {
941
+ if (!opts?.ssr) {
942
+ this.error(`${HANDLER_ID$1} is server-only; import it from your server entry (SSR build).`);
943
+ }
944
+ return {
945
+ id: HANDLER_ID$1,
946
+ moduleSideEffects: true
947
+ };
948
+ }
949
+ return null;
950
+ },
951
+ load(id, opts) {
952
+ if (id === HANDLER_ID$1 && opts?.ssr) {
953
+ const externalDev = this.environment.mode === 'dev' && (internal.externalDevServer || !isRunnableEnvironment(this.environment));
954
+ return handlerModuleCode(isBuild || externalDev);
955
+ }
956
+ return null;
957
+ }
958
+ }];
959
+ if (installDevMiddleware) {
960
+ turnkeyPlugins.push({
961
+ name: 'solid:server-functions/dev-middleware',
962
+ apply: 'serve',
963
+ configureServer(server) {
964
+ const ssrEnvironment = server.environments.ssr;
965
+ if (internal.externalDevServer || ssrEnvironment && !isRunnableEnvironment(ssrEnvironment)) {
966
+ return;
967
+ }
968
+ server.middlewares.use((req, res, next) => {
969
+ const url = new URL(req.url || '/', 'http://localhost');
970
+ // Match with and without `base` — middleware-mode hosts may mount
971
+ // vite.middlewares below the base themselves.
972
+ if (url.pathname !== resolvedEndpoint && url.pathname !== endpoint) {
973
+ return next();
974
+ }
975
+ // When the stripped form matched, restore the base for dispatch:
976
+ // the generated handler compares the request pathname against the
977
+ // base-prefixed endpoint, and production handlers only ever see
978
+ // base-prefixed URLs.
979
+ const dispatchUrl = url.pathname === resolvedEndpoint ? undefined : joinBase(base, req.url || '/');
980
+ (async () => {
981
+ // Make sure the referenced module has been evaluated in the SSR
982
+ // environment so its registration exists — functions only client
983
+ // code references are never loaded by the SSR render itself.
984
+ const headerId = req.headers['x-server-function-id'];
985
+ const functionId = (typeof headerId === 'string' ? headerId.split('#')[0] : undefined) || url.searchParams.get('id');
986
+ if (functionId) {
987
+ const entry = moduleForFunctionId(functionId);
988
+ if (entry) await server.ssrLoadModule(moduleDevUrl(entry));
989
+ }
990
+ // Dispatch through a module evaluated in the SSR environment so
991
+ // the handler shares the registry instance with the app modules.
992
+ // With SSR start mode active the main plugin threads its handler id
993
+ // in, and dispatch goes through `handleRequest` instead — one
994
+ // middleware chain and one stub-backed request event front the
995
+ // endpoint exactly as they front page SSR.
996
+ const handler = await server.ssrLoadModule(internal.ssrHandler ?? HANDLER_ID$1);
997
+ const response = internal.ssrHandler ? await handler.handleRequest(webRequestFromNode(req, dispatchUrl)) : await handler.handleServerFunctionRequest(webRequestFromNode(req, dispatchUrl));
998
+ await sendWebResponse(res, response);
999
+ })().catch(error => {
1000
+ if (error instanceof Error) server.ssrFixStacktrace(error);
1001
+ next(error);
1002
+ });
1003
+ });
1004
+ }
1005
+ });
1006
+ }
1007
+ return [{
1008
+ name: 'solid:server-functions/setup',
1009
+ enforce: 'pre',
1010
+ configResolved(config) {
1011
+ env = config.mode !== 'production' ? 'development' : 'production';
1012
+ root = config.root;
1013
+ base = config.base;
1014
+ filter = createFilter(filterInclude, filterExclude, {
1015
+ resolve: root
1016
+ });
1017
+ isBuild = config.command === 'build';
1018
+ isSsrBuild = !!config.build.ssr;
1019
+ outDir = config.build.outDir;
1020
+ resolvedEndpoint = joinBase(config.base, endpoint);
1021
+ if (options.configure) {
1022
+ const absolute = path.isAbsolute(options.configure) ? options.configure : path.resolve(root, options.configure);
1023
+ if (!existsSync(absolute)) {
1024
+ throw new Error(`[@solidjs/vite-plugin] serverFunctions.configure does not exist: ${options.configure}`);
1025
+ }
1026
+ configureModulePath = absolute;
1027
+ }
1028
+ if (isBuild && isSsrBuild) {
1029
+ // Classic two-invocation build: pick up the modules the client
1030
+ // build discovered so the server manifest registers them even when
1031
+ // the SSR module graph never imports them.
1032
+ for (const entry of readPersistedManifest(root)) {
1033
+ manifest.server.add(entry);
1034
+ }
1035
+ }
1036
+ },
1037
+ configureServer(server) {
1038
+ currentServer = server;
1039
+ },
1040
+ writeBundle() {
1041
+ // Same client-build detection as the main plugin: builder-mode builds
1042
+ // run both environments in one process, so prefer the per-environment
1043
+ // consumer over the process-wide --ssr flag.
1044
+ const ctx = this;
1045
+ const consumer = ctx.environment?.config?.consumer;
1046
+ const isClient = consumer ? consumer === 'client' : !isSsrBuild;
1047
+ if (isBuild && isClient) {
1048
+ writePersistedManifest(root, outDir, manifest.server);
1049
+ }
1050
+ }
1051
+ }, {
1052
+ name: 'solid:server-functions/manifest',
1053
+ enforce: 'pre',
1054
+ resolveId(source) {
1055
+ if (source === manifestId) {
1056
+ return {
1057
+ id: manifestId,
1058
+ moduleSideEffects: true
1059
+ };
1060
+ }
1061
+ return null;
1062
+ },
1063
+ async load(id, opts) {
1064
+ const mode = opts?.ssr ? 'server' : 'client';
1065
+ if (id === manifestId) {
1066
+ if (isBuild && mode === 'server') {
1067
+ // Merge the client build's persisted discoveries at load time,
1068
+ // not just configResolved: in builder mode (single process,
1069
+ // `vite build` with the environments API) all environment
1070
+ // configs resolve before the client build has written the file,
1071
+ // but this load runs once the SSR environment builds — after it.
1072
+ for (const entry of readPersistedManifest(root)) {
1073
+ manifest.server.add(entry);
1074
+ }
1075
+ }
1076
+ const current = new Debouncer(() => [...manifest[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1077
+ preload[mode] = current;
1078
+ const result = await current.promise.reference;
1079
+ return result;
1080
+ }
1081
+ return null;
1082
+ }
1083
+ }, {
1084
+ name: 'solid:server-functions/compiler',
1085
+ enforce: 'pre',
1086
+ async transform(code, fileId, opts) {
1087
+ const mode = opts?.ssr ? 'server' : 'client';
1088
+ const [id] = fileId.split('?');
1089
+ if (!filter(id)) {
1090
+ return null;
1091
+ }
1092
+
1093
+ // Fast path: the directive has to appear literally, so anything
1094
+ // without the substring can skip the native parse entirely.
1095
+ if (!code.includes(directive)) {
1096
+ return null;
1097
+ }
1098
+ const result = await compile(id, code, {
1099
+ ...(mode === 'server' ? serverOptions : clientOptions),
1100
+ mode,
1101
+ env,
1102
+ root
1103
+ });
1104
+ if (result.valid) {
1105
+ const preloader = preload[mode];
1106
+ if (preloader) {
1107
+ preloader.defer();
1108
+ }
1109
+ invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1110
+ return {
1111
+ // Appended (not prepended) so the source map for the compiled
1112
+ // module stays valid; imports hoist and the endpoint is only
1113
+ // read at call time, never during module evaluation.
1114
+ code: (result.code || '') + endpointConfigureSnippet(mode),
1115
+ map: result.map
1116
+ };
1117
+ }
1118
+ return null;
1119
+ }
1120
+ }, ...turnkeyPlugins];
1121
+ }
1122
+
1123
+ // Start-mode serving for plain Vite apps: `solid({ start: {...} })` (or the
1124
+ // zero-config sugar `start: true`) adds a serving layer with conventional
1125
+ // entries so no hand-rolled wiring is needed, and the plugin's `ssr`
1126
+ // boolean picks the mode — `ssr: true` server-renders the app per request;
1127
+ // `ssr: false`/omitted is client mode (the same conventions, but the
1128
+ // document shell is served/prerendered empty and the app `render()`s
1129
+ // client-side). The flip between them is that one boolean.
1130
+ //
1131
+ // SSR mode (`start` + `ssr: true`):
1132
+ // - Dev: runnable SSR environments are served by a Vite middleware. Provider-
1133
+ // owned environments serve through `virtual:solid-ssr-handler` instead.
1134
+ // Both paths inject the Vite client, dev style patch, and entry CSS as
1135
+ // `<style data-vite-dev-id>` tags before the body can paint.
1136
+ // - Prod: the plugin configures a full-app build (client + server bundles
1137
+ // via the Vite 6+ environments/builder API — a single `vite build` builds
1138
+ // both) whose server entry is `virtual:solid-ssr-handler`: an
1139
+ // adapter-agnostic named `handleRequest(Request) => Promise<Response>` plus
1140
+ // a default Fetchable `{ fetch(request) }` export. Both scope each request
1141
+ // with `provideRequestEvent`, stream the render, and resolve hashed client
1142
+ // assets through `virtual:solid-manifest`.
1143
+ // - Entries are conventional with escape hatches: `src/entry-server.*` /
1144
+ // `src/entry-client.*` are used when present (or set explicitly); when
1145
+ // absent, default entries are generated from a single root component
1146
+ // (`start.app`, defaulting to `src/App.*`) wrapped in a document shell
1147
+ // (`start.document`, defaulting to `src/Document.*`, else a built-in one).
1148
+ // - When `serverFunctions` is also enabled, the handler composes the
1149
+ // endpoint on every surface; the runnable-dev server-function middleware
1150
+ // pre-loads the referenced module, then dispatches through this handler.
1151
+ // - Every dispatch runs under a stub-backed request event
1152
+ // (`createRequestEvent`) with the optional `start.middleware` chain fronting
1153
+ // it, and page responses go through the runtime's `createSSRResponse`
1154
+ // head lifecycle (commit at shell flush, real pre-flush redirects, the
1155
+ // script fallback post-flush).
1156
+ // - `vite preview` serves dist/client statically and dispatches everything
1157
+ // else through the built handler — the production path, middleware
1158
+ // included, with no server file needed.
1159
+ //
1160
+ // Client mode (`start` without `ssr: true`) rides the same machinery with
1161
+ // three deltas: the generated server entry renders the document shell
1162
+ // WITHOUT the app (dev serving doubles as history fallback, and a
1163
+ // post-build hook prerenders it once into dist/client/index.html), the
1164
+ // generated client entry render()s instead of hydrating, and dist/server is
1165
+ // dropped from the output unless `serverFunctions` needs it for the
1166
+ // endpoint. Client code compiles non-hydratable, exactly like a plain SPA.
1167
+
1168
+ /**
1169
+ * Options for the main plugin's `start` option (`start: true` is
1170
+ * sugar for the empty bag). One bag serves both modes — the plugin's `ssr`
1171
+ * boolean picks between them, so flipping a project between
1172
+ * client-rendered and server-rendered is toggling that boolean, never
1173
+ * reshaping this object. Server-only options (`entryServer`, `external`)
1174
+ * are documented no-ops in client mode: they stay in the config across a
1175
+ * flip instead of erroring.
1176
+ */
1177
+
1178
+ // Server-only start-mode request handler; also the server bundle's entry so a
1179
+ // production server is one import away from `Request -> Response`. Exported
1180
+ // for the main plugin to thread into the server-function dev middleware,
1181
+ // which dispatches through it when SSR start mode is active (one middleware
1182
+ // chain and one request event across both dispatch paths).
1183
+ const SSR_HANDLER_ID = 'virtual:solid-ssr-handler';
1184
+ const HANDLER_ID = SSR_HANDLER_ID;
1185
+ // Dev-only response marker: the generated dev handler answers non-page
1186
+ // requests that fell through the whole middleware chain to the terminal
1187
+ // page dispatch with a marked 404 instead of rendering HTML at them, and
1188
+ // the dev middleware hands those back to Vite's pipeline. Production has no
1189
+ // such seam — every unhandled request renders — but production also has no
1190
+ // Vite pipeline to fall back to.
1191
+ const DEV_FALLTHROUGH_HEADER = 'x-solid-dev-fallthrough';
1192
+ // Private protocol between the two generated modules when `start.setup` is
1193
+ // async: the entry hands the handler the renderToStream result under this
1194
+ // key, because a promise resolving to the stream BARE would adopt the
1195
+ // stream's thenable (which waits for the complete render) and buffer it.
1196
+ const STREAM_BOX = '__solidSetupStream';
1197
+ const DEV_STYLES_ID = 'virtual:solid-ssr-dev-styles';
1198
+ const RESOLVED_DEV_STYLES_ID = '\0' + DEV_STYLES_ID;
1199
+ // Generated default entries / document shell. The `.tsx` suffix routes them
1200
+ // through the plugin's normal JSX transform (per-environment SSR/DOM
1201
+ // compile), exactly like user-authored entry files.
1202
+ const ENTRY_SERVER_ID = 'virtual:solid-ssr-entry-server.tsx';
1203
+ const ENTRY_CLIENT_ID = 'virtual:solid-ssr-entry-client.tsx';
1204
+ const DOCUMENT_ID = 'virtual:solid-ssr-document.tsx';
1205
+ const MANIFEST_ID = 'virtual:solid-manifest';
1206
+ const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1207
+ const STORAGE_SOURCE = '@solidjs/web/storage';
1208
+ const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs'];
1209
+ const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
1210
+ const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx'];
1211
+ function probe(root, stem, extensions) {
1212
+ for (const ext of extensions) {
1213
+ if (existsSync(path.resolve(root, stem + ext))) return stem + ext;
1214
+ }
1215
+ return null;
1216
+ }
1217
+
1218
+ /** Normalizes a user-supplied module path to a root-relative one (no leading slash). */
1219
+ function normalizeUserPath(root, spec, option) {
1220
+ const absolute = path.isAbsolute(spec) ? spec : path.resolve(root, spec);
1221
+ if (!existsSync(absolute)) {
1222
+ throw new Error(`[@solidjs/vite-plugin] start.${option} does not exist: ${spec}`);
1223
+ }
1224
+ const relative = path.relative(root, absolute).split(path.sep).join('/');
1225
+ if (relative.startsWith('..')) {
1226
+ throw new Error(`[@solidjs/vite-plugin] start.${option} must live inside the Vite root: ${spec}`);
1227
+ }
1228
+ return relative;
1229
+ }
1230
+ function resolveEntries(root, options, clientMode) {
1231
+ const explicitClient = options.entryClient ? normalizeUserPath(root, options.entryClient, 'entryClient') : null;
1232
+ if (clientMode) {
1233
+ // Client mode: the server entry is always generated (it renders the
1234
+ // document shell only — no App — for dev serving and the build-time
1235
+ // prerender); `start.entryServer` and conventional src/entry-server.*
1236
+ // files are documented no-ops here, so a project flipping the `ssr`
1237
+ // boolean never has to touch them. No entry pairing rule either: an
1238
+ // authored client entry stands alone. The document resolves in every
1239
+ // case because it IS the page in this mode.
1240
+ const document = options.document ? normalizeUserPath(root, options.document, 'document') : probe(root, 'src/Document', DOCUMENT_EXTENSIONS);
1241
+ const entryClient = explicitClient ?? probe(root, 'src/entry-client', ENTRY_EXTENSIONS);
1242
+ if (entryClient) {
1243
+ return {
1244
+ entryServer: ENTRY_SERVER_ID,
1245
+ entryClient,
1246
+ generated: false,
1247
+ app: null,
1248
+ document: document ? path.resolve(root, document) : null
1249
+ };
1250
+ }
1251
+ const app = options.app ? normalizeUserPath(root, options.app, 'app') : probe(root, 'src/App', APP_EXTENSIONS) ?? probe(root, 'src/app', APP_EXTENSIONS);
1252
+ if (!app) {
1253
+ throw new Error(`[@solidjs/vite-plugin] the \`start\` option needs an app root: add src/App.tsx ` + `(or set start.app), or provide a src/entry-client.* entry.`);
1254
+ }
1255
+ return {
1256
+ entryServer: ENTRY_SERVER_ID,
1257
+ entryClient: ENTRY_CLIENT_ID,
1258
+ generated: true,
1259
+ app: path.resolve(root, app),
1260
+ document: document ? path.resolve(root, document) : null
1261
+ };
1262
+ }
1263
+ const explicitServer = options.entryServer ? normalizeUserPath(root, options.entryServer, 'entryServer') : null;
1264
+ const entryServer = explicitServer ?? probe(root, 'src/entry-server', ENTRY_EXTENSIONS);
1265
+ const entryClient = explicitClient ?? probe(root, 'src/entry-client', ENTRY_EXTENSIONS);
1266
+ if (entryServer && entryClient) {
1267
+ return {
1268
+ entryServer,
1269
+ entryClient,
1270
+ generated: false,
1271
+ app: null,
1272
+ document: null
1273
+ };
1274
+ }
1275
+ if (entryServer || entryClient) {
1276
+ // One authored entry with a generated counterpart is a hydration
1277
+ // mismatch waiting to happen — the generated side wraps the app in the
1278
+ // document shell, which the authored side knows nothing about.
1279
+ const found = entryServer ? 'entry-server' : 'entry-client';
1280
+ const missing = entryServer ? 'entry-client' : 'entry-server';
1281
+ throw new Error(`[@solidjs/vite-plugin] found ${found} but no ${missing}; entry files come in pairs. ` + `Provide both (src/entry-server.* and src/entry-client.*, or the start.entryServer / ` + `start.entryClient options) or neither (to generate both from start.app).`);
1282
+ }
1283
+ const app = options.app ? normalizeUserPath(root, options.app, 'app') : probe(root, 'src/App', APP_EXTENSIONS) ?? probe(root, 'src/app', APP_EXTENSIONS);
1284
+ if (!app) {
1285
+ throw new Error(`[@solidjs/vite-plugin] the \`start\` option needs an app root: add src/App.tsx ` + `(or set start.app), or provide src/entry-server.* and src/entry-client.* entries.`);
1286
+ }
1287
+ const document = options.document ? normalizeUserPath(root, options.document, 'document') : probe(root, 'src/Document', DOCUMENT_EXTENSIONS);
1288
+ return {
1289
+ entryServer: ENTRY_SERVER_ID,
1290
+ entryClient: ENTRY_CLIENT_ID,
1291
+ generated: true,
1292
+ app: path.resolve(root, app),
1293
+ document: document ? path.resolve(root, document) : null
1294
+ };
1295
+ }
1296
+ function startServe(options, internal = {}) {
1297
+ // Client mode (the `start` option without `ssr: true`) rides this exact
1298
+ // plugin with three deltas: the generated server entry renders the
1299
+ // document shell WITHOUT the app (dev serving doubles as history
1300
+ // fallback, and a post-build hook prerenders it once into
1301
+ // dist/client/index.html), the generated client entry render()s instead
1302
+ // of hydrating, and dist/server is dropped from the output unless
1303
+ // `serverFunctions` needs it for the endpoint. Everything else — entry
1304
+ // probing, the handler, middleware, dev styles, the manifest — is shared,
1305
+ // which is what makes flipping a project between the modes a one-boolean
1306
+ // config change.
1307
+ const clientMode = !internal.ssr;
1308
+ // Server components (`serverFunctions: { components: true }`): generated
1309
+ // entries additionally emit the document-SSR wiring — the render plugin +
1310
+ // direct-call transform server-side, the bootstrap script in <head>, and
1311
+ // the client-side installServerComponents() call. Authored entries carry
1312
+ // those pieces themselves (the endpoint response transform is installed by
1313
+ // the server-function handler module either way). Everything is gated
1314
+ // codegen: with the option off, none of these imports exist anywhere.
1315
+ const serverComponents = !!internal.serverComponents;
1316
+ // `external` is server-mode-only (documented no-op in client mode, so a
1317
+ // host-integrated config survives the `ssr` boolean flip untouched).
1318
+ const externalServer = !clientMode && !!options.external;
1319
+ let root = process.cwd();
1320
+ let base = '/';
1321
+ let isBuild = false;
1322
+ let entries;
1323
+ /** Absolute path of the user's middleware module, when configured. */
1324
+ let middlewarePath = null;
1325
+ /** Absolute path of the per-request setup module, when configured (server mode). */
1326
+ let setupPath = null;
1327
+ function requireEntries() {
1328
+ // config() always runs before resolveId/load/configureServer.
1329
+ if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
1330
+ return entries;
1331
+ }
1332
+
1333
+ /** Import specifier for generated code: absolute for files, id for virtuals. */
1334
+ function entryServerSpec() {
1335
+ const {
1336
+ entryServer
1337
+ } = requireEntries();
1338
+ return entryServer === ENTRY_SERVER_ID ? entryServer : path.resolve(root, entryServer);
1339
+ }
1340
+
1341
+ /** Browser URL of the client entry on the dev server (base applied). */
1342
+ function devClientEntryUrl() {
1343
+ const {
1344
+ entryClient
1345
+ } = requireEntries();
1346
+ return entryClient === ENTRY_CLIENT_ID ? joinBase(base, '/@id/' + ENTRY_CLIENT_ID) : joinBase(base, '/' + entryClient);
1347
+ }
1348
+ function documentSpec() {
1349
+ const {
1350
+ document
1351
+ } = requireEntries();
1352
+ return document ?? DOCUMENT_ID;
1353
+ }
1354
+ function styleRoots() {
1355
+ const {
1356
+ generated,
1357
+ app,
1358
+ document,
1359
+ entryServer,
1360
+ entryClient
1361
+ } = requireEntries();
1362
+ if (clientMode) {
1363
+ // The app graph's CSS is inlined into the dev shell too (not just the
1364
+ // document's): the client injects it again when the modules load and
1365
+ // the dev style patch dedupes, so this is pure anti-flash.
1366
+ return [generated ? app : path.resolve(root, entryClient), ...(document ? [document] : [])];
1367
+ }
1368
+ return generated ? [app, ...(document ? [document] : [])] : [path.resolve(root, entryServer)];
1369
+ }
1370
+ async function devStylesModuleCode(environment, watchFile) {
1371
+ const styles = await collectDevStyleSources(environment, styleRoots(), watchFile);
1372
+ if (!styles.length) return `export default '';`;
1373
+ const imports = styles.map((style, index) => {
1374
+ const specifier = style.url.includes('?') ? `${style.url}&inline` : `${style.url}?inline`;
1375
+ return `import css${index} from ${JSON.stringify(specifier)};`;
1376
+ });
1377
+ return [...imports, `const ids = ${JSON.stringify(styles.map(style => style.id))};`, `const css = [${styles.map((_, index) => `css${index}`).join(', ')}];`, `const escapeAttr = value => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');`, `export default css.map((content, index) => {`, ` const id = escapeAttr(ids[index]);`, ` return '<style data-asset="' + id + '" data-vite-dev-id="' + id + '">' +`, ` content.replace(/<\\/(style)/gi, '<\\\\/$1') + '</style>';`, `}).join('');`].join('\n');
1378
+ }
1379
+ function generatedEntryServerCode() {
1380
+ if (clientMode) {
1381
+ // The client-mode shell: the document without the app. Rendered per
1382
+ // request in dev (any HTML GET gets it — history-fallback semantics)
1383
+ // and once at build time into dist/client/index.html. The client
1384
+ // entry script is injected by the handler, exactly like SSR mode.
1385
+ return [`import { renderToStream } from '@solidjs/web';`, `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, ``, `export function render(request, context) {`, ` return renderToStream(() => <Document />, { manifest });`, `}`].join('\n');
1386
+ }
1387
+ const {
1388
+ app
1389
+ } = requireEntries();
1390
+ const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;
1391
+ return [`import { renderToStream${setupPath ? ', getRequestEvent' : ''} } from '@solidjs/web';`, ...(serverComponents ? [`import { configureServerFunctionsServer } from '@solidjs/web/server-functions';`, `import { frameTransformDirectResult, ServerComponentPlugin } from '@solidjs/web/frames';`] : []), `import manifest from ${JSON.stringify(MANIFEST_ID)};`, `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ...(setupPath ? [`import setup from ${JSON.stringify(setupPath)};`] : []), ``, ...(setupPath ? [`if (typeof setup !== 'function') {`, ` throw new Error('[@solidjs/vite-plugin] start.setup must default-export a function ' +`, ` '((event, App) => Component | void | Promise<...>): ' + ${JSON.stringify(options.setup)});`, `}`, ``] : []), ...(serverComponents ? [
1392
+ // Direct (in-process) server-function calls made during document
1393
+ // SSR must resolve to inline-renderable components; the endpoint
1394
+ // response transform is installed separately by the
1395
+ // server-function handler module (configure calls merge per key).
1396
+ `configureServerFunctionsServer({ transformDirectResult: frameTransformDirectResult });`, ``] : []), ...(setupPath ? [
1397
+ // The per-request seam: the hook sees the same event the
1398
+ // middleware chain decorated and finishes before renderToStream
1399
+ // starts. When it is async, the stream must NOT cross the
1400
+ // promise boundary bare — a promise resolving to a
1401
+ // renderToStream result adopts its thenable (which waits for
1402
+ // the *complete* render) and buffers the stream — so it crosses
1403
+ // boxed under a private key the generated handler unboxes
1404
+ // (both modules are ours).
1405
+ `export function render(request, context) {`, ` const prepared = setup(getRequestEvent(), App);`, ` if (prepared && typeof prepared.then === 'function') {`, ` return prepared.then((component) => ({ ${STREAM_BOX}: renderApp(component || App) }));`, ` }`, ` return renderApp(prepared || App);`, `}`, ``, `function renderApp(Root) {`, ` return renderToStream(() => (`, ` <Document>`, ` <Root />`, ` </Document>`, ` ), ${streamOptions});`, `}`] : [`export function render(request, context) {`, ` return renderToStream(() => (`, ` <Document>`, ` <App />`, ` </Document>`, ` ), ${streamOptions});`, `}`])].join('\n');
1406
+ }
1407
+ function generatedEntryClientCode() {
1408
+ const {
1409
+ app
1410
+ } = requireEntries();
1411
+ if (clientMode) {
1412
+ // render(), not hydrate(): the shell's body is empty, the app mounts
1413
+ // fresh. Client code compiles non-hydratable in client mode, so the
1414
+ // app cannot claim server DOM anyway. The entry script is injected
1415
+ // without `async` (plain module = deferred), so document.body is
1416
+ // complete when this runs.
1417
+ return [`import { render } from '@solidjs/web';`, `import App from ${JSON.stringify(app)};`, ``, `render(() => <App />, document.body);`].join('\n');
1418
+ }
1419
+ return [`import { hydrate } from '@solidjs/web';`, ...(serverComponents ? [`import { installServerComponents } from '@solidjs/web/frames';`] : []), `import Document from ${JSON.stringify(documentSpec())};`, `import App from ${JSON.stringify(app)};`, ``, ...(serverComponents ? [
1420
+ // Installs the t=0 document-adoption registry and the transport
1421
+ // policy (component responses morph their boundary instead of
1422
+ // decoding as data). Must run before hydrate().
1423
+ `installServerComponents();`, ``] : []), `hydrate(() => (`, ` <Document>`, ` <App />`, ` </Document>`, `), document);`].join('\n');
1424
+ }
1425
+
1426
+ // Built-in document shell: minimal, hydration-ready. The client entry
1427
+ // script is injected into <head> by the handler (not rendered here) so its
1428
+ // URL never has to survive hydration or a manifest lookup client-side.
1429
+ // The client-mode variant drops <HydrationScript /> — nothing hydrates,
1430
+ // so the shell stays inert HTML. (A user-authored Document carrying
1431
+ // HydrationScript is covered too: the handler strips the event-capture
1432
+ // script from the client-mode shell.)
1433
+ const documentShellCode = [...(clientMode ? [] : [`import { HydrationScript } from '@solidjs/web';`, ``]), `export default function Document(props) {`, ` return (`, ` <html lang="en">`, ` <head>`, ` <meta charset="utf-8" />`, ` <meta name="viewport" content="width=device-width, initial-scale=1.0" />`, ...(clientMode ? [] : [` <HydrationScript />`]), ` </head>`, ` <body>{props.children}</body>`, ` </html>`, ` );`, `}`].join('\n');
1434
+
1435
+ // The handler module: dev and prod share the render/response plumbing;
1436
+ // they differ in how the client entry URL is known (baked dev URL vs a
1437
+ // manifest scan) and what gets injected into <head> (Vite client + style
1438
+ // patch in dev). The response-head lifecycle is the runtime's
1439
+ // (`createRequestEvent`/`createSSRResponse`/`commitEventResponse` from
1440
+ // @solidjs/web): every request runs under a stub-backed event,
1441
+ // `httpStatus`/`httpHeader` writes land on the wire at shell flush, a
1442
+ // pre-flush redirect becomes a real 3xx and a post-flush one the script
1443
+ // fallback, and a Response that skipped the render lifecycle (middleware
1444
+ // early return, raw entry.render Response, server functions) has the
1445
+ // stub folded on at the handler edge after the middleware chain fully
1446
+ // unwinds. When
1447
+ // `serverFunctions` is enabled the endpoint is dispatched here on every
1448
+ // surface (the runnable-dev middleware routes through this module), so
1449
+ // user middleware and the shared request event front it identically.
1450
+ function handlerModuleCode(externalDev) {
1451
+ const {
1452
+ generated,
1453
+ entryClient
1454
+ } = requireEntries();
1455
+ const composeServerFunctions = internal.serverFunctions;
1456
+ 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)};`] : [])];
1457
+ if (isBuild) {
1458
+ lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
1459
+ 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;`,
1460
+ // The plugin's manifest module normalizes lazy facade chunks
1461
+ // (isDynamicEntry) so exactly one real entry remains flagged.
1462
+ ` for (const key in manifest) {`, ` const chunk = manifest[key];`, ` if (chunk && chunk.isEntry && chunk.file) {`, ` clientEntryUrl = joinAssetPath(manifest._base, chunk.file);`, ` break;`, ` }`, ` }`, ` return clientEntryUrl;`, `}`);
1463
+ } else {
1464
+ const devHead = `<script>${devStylePatch}</script>` + `<script type="module" src="${joinBase(base, '/@vite/client')}"></script>`;
1465
+ lines.push(``, `const DEV_HEAD = ${JSON.stringify(devHead)};`);
1466
+ }
1467
+
1468
+ // Middleware: the user module default-exports one fetch-style function
1469
+ // or an array, composed in order. Without one, the chain degenerates to
1470
+ // the terminal dispatch.
1471
+ lines.push(``);
1472
+ if (middlewarePath) {
1473
+ lines.push(`const middlewares = Array.isArray(middlewareModule) ? middlewareModule : [middlewareModule];`, `for (const mw of middlewares) {`, ` if (typeof mw !== 'function') {`, ` throw new Error('[@solidjs/vite-plugin] start.middleware must default-export a function or an array of functions: ' + ${JSON.stringify(middlewarePath)});`, ` }`, `}`, `const runMiddleware = composeMiddleware(middlewares);`);
1474
+ } else {
1475
+ lines.push(`const runMiddleware = (request, next) => next(request);`);
1476
+ }
1477
+
1478
+ // No `_$SC` bootstrap injection: the runtime's serialized
1479
+ // server-component references self-bootstrap the registry (each
1480
+ // hydration script's first reference carries it as an idempotent
1481
+ // expression), so nothing needs to precede the data scripts. The old
1482
+ // head-open splice actively broke hydration — a script ahead of the
1483
+ // authored <head> elements claims as the first walked child and drifts
1484
+ // every positional claim after it.
1485
+ lines.push(``, `function createHtmlChunkTransform(clientEntry, extraHead) {`, ` let first = true;`, ` let injected = false;`, ` return (chunk) => {`);
1486
+ if (!generated) {
1487
+ // Authored entries reference the client entry by its dev path (the
1488
+ // `<script src="/src/entry-client.tsx">` convention); rewrite it to
1489
+ // the resolved URL like the classic server harnesses do.
1490
+ lines.push(` if (clientEntry && chunk.includes(${JSON.stringify('/' + entryClient)})) {`, ` chunk = chunk.split(${JSON.stringify('/' + entryClient)}).join(clientEntry);`, ` }`);
1491
+ }
1492
+ lines.push(` if (!injected && chunk.includes('</head>')) {`, ` injected = true;`);
1493
+ if (clientMode) {
1494
+ // Nothing hydrates in client mode, so the event-capture bootstrap
1495
+ // `<HydrationScript />` renders (`window._$HY||...`) is dead weight —
1496
+ // but a Document shared with SSR mode carries it by design (the flip
1497
+ // story). Strip it from the shell here instead of making users fork
1498
+ // their Document per mode. (`<!--xs-->` is the script's stream
1499
+ // marker; the shell head always arrives in one chunk.)
1500
+ lines.push(` chunk = chunk.replace(/<script(?:\\s[^>]*)?>window\\._\\$HY\\|\\|[\\s\\S]*?<\\/script>(?:<!--xs-->)?/, '');`);
1501
+ }
1502
+ const headParts = [];
1503
+ // Dev: the style patch + Vite client, then either middleware-provided
1504
+ // styles or the external environment's HMR-tracked virtual styles module.
1505
+ if (!isBuild) {
1506
+ headParts.push(`DEV_HEAD`, externalDev ? `(extraHead === undefined ? DEV_STYLES_HEAD : extraHead)` : `(extraHead || '')`);
1507
+ }
1508
+ if (generated || clientMode) {
1509
+ // Client-mode note: the shell never references its client entry
1510
+ // itself (even an authored one — the Document knows nothing about
1511
+ // entries), so the handler always injects it. Without `async`: module
1512
+ // scripts default to deferred execution, which is exactly right for a
1513
+ // fresh render-into-body mount (hydration, by contrast, wants to
1514
+ // start as early as possible).
1515
+ headParts.push(`(clientEntry ? '<script type="module" src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`);
1516
+ }
1517
+ if (headParts.length) {
1518
+ lines.push(` chunk = chunk.replace('</head>', ${headParts.join(' + ')} + '</head>');`);
1519
+ }
1520
+ lines.push(` }`, ` if (first) { first = false; chunk = '<!DOCTYPE html>' + chunk; }`, ` return chunk;`, ` };`, `}`);
1521
+
1522
+ // The handler-edge commit fold — the runtime's `commitEventResponse`
1523
+ // (named import above), the second of the response lifecycle's two
1524
+ // exits: page results leave through `createSSRResponse`, any other
1525
+ // Response (a middleware early return, a raw Response from
1526
+ // entry.render, a server-function response) leaves through
1527
+ // `commitEventResponse`, which folds the event's response stub onto it
1528
+ // (cookies append entry-by-entry, other headers gap-fill, status stays
1529
+ // the response's own) and commits the stub. Committed stubs pass
1530
+ // through untouched, so the edge applies it unconditionally.
1531
+ lines.push(``, `async function dispatchRequest(request, event, options) {`);
1532
+ if (composeServerFunctions) {
1533
+ lines.push(` if (new URL(request.url).pathname === endpoint) {`,
1534
+ // The call shares the middleware chain's event (locals decoration,
1535
+ // the response stub); an explicit host-provided createEvent wins.
1536
+ // No fold here: the runtime's server-function handler runs the
1537
+ // commit seam itself, and anything it left uncommitted is caught by
1538
+ // the unconditional edge fold in handleRequest.
1539
+ ` return handleServerFunctionRequest(request, {`, ` createEvent: () => event,`, ` ...options.serverFunctions,`, ` });`, ` }`);
1540
+ }
1541
+ if (!isBuild) {
1542
+ // Dev terminal gate: the dev middleware dispatches every request the
1543
+ // middleware chain might handle (API routes, no-JS form POSTs — all
1544
+ // methods and accept types, matching production), passing
1545
+ // `pageRequest: false` for the non-page ones. When such a request
1546
+ // falls through the whole chain to this terminal dispatch, nothing
1547
+ // owns it — answer with the marked 404 so the dev middleware hands
1548
+ // it back to Vite's pipeline instead of rendering HTML at it. The
1549
+ // gate reflects the wire request: only the dev middleware sets the
1550
+ // flag, so external-host dispatch and preview stay render-always.
1551
+ lines.push(` if (options.pageRequest === false) {`, ` return new Response(null, { status: 404, headers: { ${JSON.stringify(DEV_FALLTHROUGH_HEADER)}: '1' } });`, ` }`);
1552
+ }
1553
+ lines.push(isBuild ? ` const clientEntry = options.clientEntry || resolveClientEntry();` : ` const clientEntry = options.clientEntry || ${JSON.stringify(devClientEntryUrl())};`, ` let result = entry.render(request, { clientEntry, ...options.context });`,
1554
+ // renderToStream results are thenables whose then() waits for the
1555
+ // *complete* render — check for pipe first so streaming survives, and
1556
+ // only await plain promises (async render functions).
1557
+ ` if (result && typeof result.pipe !== 'function' && typeof result.then === 'function') {`, ` result = await result;`, ` }`, ...(setupPath ? [
1558
+ // start.setup's async path boxes the stream (see the generated
1559
+ // entry): a bare promise resolution would adopt the stream's
1560
+ // thenable and buffer the whole render.
1561
+ ` if (result && result.${STREAM_BOX}) result = result.${STREAM_BOX};`] : []),
1562
+ // Raw Responses fold at the handler edge (handleRequest), after the
1563
+ // middleware chain unwinds — not here, where middleware above this
1564
+ // frame could still legitimately mutate headers.
1565
+ ` if (result instanceof Response) return result;`,
1566
+ // The runtime's response-head lifecycle: commit at shell flush,
1567
+ // pre-flush Location as a real redirect, post-flush Location as the
1568
+ // script fallback; the transform injects the doctype/head pieces.
1569
+ ` return createSSRResponse(result, event, {`, ` responseInit: options.responseInit,`, ` nonce: options.nonce,`, ` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead),`, ` });`, `}`, ``, `export async function handleRequest(request, options = {}) {`, ` const event = createRequestEvent(request);`,
1570
+ // Middleware runs inside the request scope, after event creation —
1571
+ // getRequestEvent() answers in middleware exactly as in app code, and
1572
+ // nothing reaches the wire until the outermost middleware returns.
1573
+ ` const response = await provideRequestEvent(event, () =>`, ` runMiddleware(request, (req) => dispatchRequest(req || request, event, options)),`, ` );`,
1574
+ // The fold runs strictly AFTER the outermost middleware returned:
1575
+ // headers stay mutable through the whole unwind, and a middleware
1576
+ // early return (an API handler that never called next()) gets its
1577
+ // stub writes — cookies set inside the request scope, status — onto
1578
+ // the wire. Unconditional: page responses come back from
1579
+ // createSSRResponse committed and pass through untouched.
1580
+ ` return commitEventResponse(response, event);`, `}`, ``, `export default {`, ` fetch(request) {`,
1581
+ // Hosts may pass environment/context arguments after the request.
1582
+ // Do not alias fetch directly to handleRequest: its second argument is
1583
+ // the Solid handler options bag, not a provider binding object.
1584
+ ` return handleRequest(request);`, ` },`, `};`);
1585
+ return lines.join('\n');
1586
+ }
1587
+ return [{
1588
+ name: 'solid:ssr/setup',
1589
+ enforce: 'pre',
1590
+ config(userConfig, env) {
1591
+ root = path.resolve(userConfig.root || process.cwd());
1592
+ entries = resolveEntries(root, options, clientMode);
1593
+ middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
1594
+ // Server-mode only, like `entryServer`/`external` (a documented
1595
+ // no-op in client mode so configs survive the `ssr` boolean flip).
1596
+ setupPath = !clientMode && options.setup ? path.resolve(root, normalizeUserPath(root, options.setup, 'setup')) : null;
1597
+ if (setupPath && !entries.generated) {
1598
+ // An authored entry-server owns its render function — the seam the
1599
+ // hook needs does not exist there.
1600
+ 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}`);
1601
+ }
1602
+ if (env.isPreview) {
1603
+ if (clientMode) {
1604
+ // Client-mode builds emit a real dist/client/index.html (the
1605
+ // prerendered shell), so preview is Vite's stock static +
1606
+ // history-fallback story. When server functions are on, the
1607
+ // endpoint dispatches through the kept dist/server handler
1608
+ // (configurePreviewServer).
1609
+ return {
1610
+ appType: 'spa',
1611
+ build: {
1612
+ outDir: 'dist/client'
1613
+ }
1614
+ };
1615
+ }
1616
+ // `vite preview` serves `build.outDir` statically; point it at the
1617
+ // client bundle so hashed assets resolve, while HTML (and
1618
+ // everything else unhandled) falls through to the
1619
+ // configurePreviewServer dispatch below. No index.html exists, so
1620
+ // `custom` keeps preview from attempting an SPA fallback.
1621
+ return {
1622
+ appType: 'custom',
1623
+ ...(externalServer ? {} : {
1624
+ build: {
1625
+ outDir: 'dist/client'
1626
+ }
1627
+ })
1628
+ };
1629
+ }
1630
+ const build = env.command === 'build';
1631
+ const clientInput = entries.generated ? ENTRY_CLIENT_ID : path.resolve(root, entries.entryClient);
1632
+ // Real files only — the dep scanner can't crawl virtual modules.
1633
+ // (In client mode the resolved document joins the scan/style roots
1634
+ // even with an authored client entry; in SSR mode authored entries
1635
+ // own the whole graph.)
1636
+ const scanEntries = entries.generated ? [entries.app, ...(entries.document ? [entries.document] : [])] : [path.resolve(root, entries.entryClient), ...(clientMode && entries.document ? [entries.document] : [])];
1637
+ return {
1638
+ // No index.html: dev must not fall back to SPA-serving one, and
1639
+ // the dep scanner needs explicit entries instead.
1640
+ appType: 'custom',
1641
+ ...(build ? externalServer ? {
1642
+ environments: {
1643
+ client: {
1644
+ build: {
1645
+ manifest: true,
1646
+ rollupOptions: {
1647
+ input: clientInput
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ } : {
1653
+ environments: {
1654
+ client: {
1655
+ build: {
1656
+ manifest: true,
1657
+ outDir: 'dist/client',
1658
+ rollupOptions: {
1659
+ input: clientInput
1660
+ }
1661
+ }
1662
+ },
1663
+ ssr: {
1664
+ consumer: 'server',
1665
+ build: {
1666
+ outDir: 'dist/server',
1667
+ rollupOptions: {
1668
+ // `index` is the Vite service convention consumed
1669
+ // by provider orchestrators such as Nitro. Keep the
1670
+ // standalone artifact's established filename.
1671
+ input: {
1672
+ index: HANDLER_ID
1673
+ },
1674
+ output: {
1675
+ entryFileNames: 'server.js'
1676
+ }
1677
+ }
1678
+ }
1679
+ }
1680
+ },
1681
+ // Presence of `builder` makes a plain `vite build` build the
1682
+ // whole app (all environments: client then ssr) on Vite 6+.
1683
+ // A classic `vite build --ssr` invocation must stay a
1684
+ // single-environment build, so it doesn't get the flag.
1685
+ ...(env.isSsrBuild ? {} : {
1686
+ builder: {}
1687
+ })
1688
+ } : {
1689
+ ...(!clientMode && !externalServer ? {
1690
+ environments: {
1691
+ ssr: {
1692
+ consumer: 'server',
1693
+ build: {
1694
+ outDir: 'dist/server',
1695
+ rollupOptions: {
1696
+ // Expose the same service entry during serve so
1697
+ // provider runtimes can discover and own it.
1698
+ input: {
1699
+ index: HANDLER_ID
1700
+ },
1701
+ output: {
1702
+ entryFileNames: 'server.js'
1703
+ }
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ } : {}),
1709
+ optimizeDeps: {
1710
+ entries: scanEntries
1711
+ }
1712
+ })
1713
+ };
1714
+ },
1715
+ configResolved(config) {
1716
+ root = config.root;
1717
+ base = config.base;
1718
+ isBuild = config.command === 'build';
1719
+ },
1720
+ resolveId(source) {
1721
+ if (source === HANDLER_ID) {
1722
+ return {
1723
+ id: HANDLER_ID,
1724
+ moduleSideEffects: true
1725
+ };
1726
+ }
1727
+ if (source === DEV_STYLES_ID) {
1728
+ return {
1729
+ id: RESOLVED_DEV_STYLES_ID,
1730
+ moduleSideEffects: true
1731
+ };
1732
+ }
1733
+ if (source === ENTRY_SERVER_ID || source === ENTRY_CLIENT_ID || source === DOCUMENT_ID) {
1734
+ return {
1735
+ id: source,
1736
+ moduleSideEffects: source === ENTRY_CLIENT_ID
1737
+ };
1738
+ }
1739
+ return null;
1740
+ },
1741
+ async load(id, opts) {
1742
+ if (id === HANDLER_ID) {
1743
+ if (!opts?.ssr) {
1744
+ this.error(`${HANDLER_ID} is server-only; import it from server code (SSR build).`);
1745
+ }
1746
+ const externalDev = !isBuild && this.environment.mode === 'dev' && (externalServer || !isRunnableEnvironment(this.environment));
1747
+ return handlerModuleCode(externalDev);
1748
+ }
1749
+ if (id === RESOLVED_DEV_STYLES_ID) {
1750
+ if (!opts?.ssr || this.environment.mode !== 'dev') {
1751
+ this.error(`${DEV_STYLES_ID} is only available to the development server handler.`);
1752
+ }
1753
+ return devStylesModuleCode(this.environment, file => this.addWatchFile(file));
1754
+ }
1755
+ if (id === ENTRY_SERVER_ID) return generatedEntryServerCode();
1756
+ if (id === ENTRY_CLIENT_ID) return generatedEntryClientCode();
1757
+ if (id === DOCUMENT_ID) return documentShellCode;
1758
+ return null;
1759
+ },
1760
+ configurePreviewServer(server) {
1761
+ // `vite build && vite preview` runs the production artifact as-is:
1762
+ // Vite's preview statics serve dist/client (see the config hook) and
1763
+ // everything else — pages, the server-function endpoint, middleware
1764
+ // included — dispatches through the built handler, exactly like a
1765
+ // deployed server. Hosts owning the server build (`start.external`)
1766
+ // preview through their own runner instead.
1767
+ // Client mode: pages are the static index.html (preview's own
1768
+ // history fallback serves them before this post middleware runs);
1769
+ // only the server-function endpoint needs the handler, and without
1770
+ // server functions there is no dist/server at all.
1771
+ if (externalServer || clientMode && !internal.serverFunctions) return;
1772
+ return () => {
1773
+ let handlerPromise = null;
1774
+ server.middlewares.use((req, res, next) => {
1775
+ (async () => {
1776
+ handlerPromise ??= import(pathToFileURL(path.resolve(root, 'dist/server/server.js')).href);
1777
+ const handler = await handlerPromise;
1778
+ // Preview's base middleware runs before this post hook and
1779
+ // strips the configured `base` from req.url; the built handler
1780
+ // compares pathnames against base-prefixed endpoints (the
1781
+ // server-function endpoint) and hands the URL to application
1782
+ // code, so restore the base — the deployed production handler
1783
+ // receives base-prefixed URLs and preview must match it.
1784
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')));
1785
+ // Preview's compression middleware buffers whole responses;
1786
+ // opting HTML out keeps SSR streaming observable, matching
1787
+ // production behavior.
1788
+ if ((response.headers.get('content-type') || '').includes('text/html')) {
1789
+ res.setHeader('content-encoding', 'identity');
1790
+ }
1791
+ await sendWebResponse(res, response);
1792
+ })().catch(next);
1793
+ });
1794
+ };
1795
+ },
1796
+ configureServer(server) {
1797
+ // The files whose static import graphs carry the app's entry CSS:
1798
+ // the app root (+ document) for generated entries, the authored
1799
+ // server entry otherwise. Their transitively imported styles are
1800
+ // inlined into <head> per request (Vite injects entry CSS from
1801
+ // client JS only, so SSR'd markup would flash unstyled without
1802
+ // this); the SSR'd tags carry data-asset + data-vite-dev-id so the
1803
+ // dev style patch drops them once Vite's own injection takes over —
1804
+ // exactly the lazy-asset dedup story, HMR included.
1805
+ // Post middleware: Vite's own middlewares (transforms, static, the
1806
+ // server-function endpoint) run first; whatever asks for HTML after
1807
+ // that gets the streamed SSR render.
1808
+ return () => {
1809
+ const ssrEnvironment = server.environments.ssr;
1810
+ if (externalServer || ssrEnvironment && !isRunnableEnvironment(ssrEnvironment)) {
1811
+ return;
1812
+ }
1813
+ server.middlewares.use((req, res, next) => {
1814
+ const url = new URL(req.url || '/', 'http://localhost');
1815
+ if (url.pathname.startsWith('/@')) return next();
1816
+ const accept = req.headers.accept || '';
1817
+ const pageRequest = req.method === 'GET' && accept.includes('text/html');
1818
+ // Production dispatches every request through the handler, so
1819
+ // dev must too or API routes and no-JS form POSTs served by
1820
+ // `start.middleware` are unreachable under `vite dev`. Without
1821
+ // a middleware chain, non-page requests have nothing to reach —
1822
+ // they stay on Vite's pipeline (404s) instead of rendering HTML.
1823
+ if (!pageRequest && !middlewarePath) return next();
1824
+ (async () => {
1825
+ // Loaded through the SSR environment so the app, the request
1826
+ // event storage, and the handler share one module registry.
1827
+ const handler = await server.ssrLoadModule(HANDLER_ID);
1828
+ const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : [];
1829
+ const devHead = styles.map(renderDevStyleTag).join('');
1830
+ // Post middlewares run after Vite's base middleware stripped
1831
+ // the configured `base` from req.url; restore it so the app
1832
+ // sees the same URLs in dev as in production (where the
1833
+ // deployed handler receives base-prefixed requests).
1834
+ const response = await handler.handleRequest(webRequestFromNode(req, joinBase(base, req.url || '/')), {
1835
+ devHead,
1836
+ pageRequest
1837
+ });
1838
+ // A non-page request the chain never handled: the terminal
1839
+ // dispatch answered with the marked 404 — hand it back to
1840
+ // Vite (its 404, other post middlewares) rather than sending
1841
+ // a rendered page at a fetch()/form client.
1842
+ if (response.headers.has(DEV_FALLTHROUGH_HEADER)) return next();
1843
+ await sendWebResponse(res, response);
1844
+ })().catch(error => {
1845
+ if (error instanceof Error) server.ssrFixStacktrace(error);
1846
+ // Vite's error middleware renders the overlay-enabled 500 page.
1847
+ next(error);
1848
+ });
1849
+ });
1850
+ };
1851
+ }
1852
+ }, ...(clientMode ? [{
1853
+ name: 'solid:start/prerender',
1854
+ apply: 'build',
1855
+ buildApp: {
1856
+ // Post order: this hook owns the whole client-mode app build (the
1857
+ // client-build-first orchestration pair is SSR-only). It
1858
+ // builds client-then-ssr itself — building anything from a
1859
+ // hook suppresses Vite's build-all fallback, so the ordering
1860
+ // is guaranteed and the manifest is on disk before the shell
1861
+ // bundle bakes it in — then runs the built handler once to
1862
+ // prerender the shell into dist/client/index.html and drops
1863
+ // dist/server unless server functions still need its handler.
1864
+ // The shell arrives complete from the handler: the runtime
1865
+ // registers every manifest entry's CSS during the render
1866
+ // (registerEntryAssets), so the entry graph's stylesheet
1867
+ // links are already in its head — injecting them here again
1868
+ // double-links every stylesheet.
1869
+ order: 'post',
1870
+ async handler(builder) {
1871
+ const client = builder.environments.client;
1872
+ const ssrEnvironment = builder.environments.ssr;
1873
+ if (client && !client.isBuilt) await builder.build(client);
1874
+ if (ssrEnvironment && !ssrEnvironment.isBuilt) {
1875
+ await builder.build(ssrEnvironment);
1876
+ }
1877
+ const serverDir = path.resolve(root, 'dist/server');
1878
+ const handler = await import(pathToFileURL(path.join(serverDir, 'server.js')).href);
1879
+ const response = await handler.handleRequest(new Request(new URL(base || '/', 'http://localhost')));
1880
+ writeFileSync(path.resolve(root, 'dist/client/index.html'), await response.text());
1881
+ if (!internal.serverFunctions) {
1882
+ rmSync(serverDir, {
1883
+ recursive: true,
1884
+ force: true
1885
+ });
1886
+ }
1887
+ }
1888
+ }
1889
+ }] : [])];
1890
+ }
1891
+
1892
+ // Typed, validated environment variables as a start-mode feature (`start.env`):
1893
+ // an `env.ts` at the project root default-exports `{ server, client }` maps
1894
+ // of Standard Schema validators (zod, valibot, arktype — mixable per key),
1895
+ // and the plugin exposes the validated values through two virtual modules:
1896
+ //
1897
+ // - `virtual:env/server` — every var; importable only from server module
1898
+ // graphs (a client-graph import is a hard error naming the importer).
1899
+ // - `virtual:env/client` — the `client` side only, whose keys must carry
1900
+ // the public env prefix (`VITE_` unless `envPrefix` says otherwise).
1901
+ //
1902
+ // Validation runs at config/build time in node only, against Vite's
1903
+ // `loadEnv` merge of the `.env*` files with `process.env` winning (CI
1904
+ // secrets take precedence) — and the plugin folds the file-loaded vars into
1905
+ // `process.env` itself, so templates don't need the classic
1906
+ // `process.env = { ...process.env, ...loadEnv(mode, root, '') }` one-liner.
1907
+ //
1908
+ // Client values are baked into the bundles as validated plain JSON —
1909
+ // that's what the public `VITE_` prefix means — so no validator library
1910
+ // ever reaches a browser bundle. Server values are NOT baked anywhere:
1911
+ // `virtual:env/server` reads `process.env` at module init (server boot)
1912
+ // and validates through the user's own schema, which only the server
1913
+ // module graph imports. Platform-injected vars that don't exist at build
1914
+ // time work, secrets rotate without a rebuild, and no secret value exists
1915
+ // in any dist artifact. Build-time server-value failures downgrade to a
1916
+ // warning (boot enforces); dev failures stay hard errors — dev IS runtime.
1917
+ //
1918
+ // A failed validation fails the build; in dev it renders Vite's error
1919
+ // overlay with the per-key report (the virtual modules throw it on load)
1920
+ // and `.env*`/schema edits revalidate live. A `solid-env.d.ts` is generated
1921
+ // next to the schema file so both virtual modules are fully typed by
1922
+ // inference from the user's own schema — no manual declarations.
1923
+ //
1924
+ // Design credit: the shape of this feature — env.ts schema file, the
1925
+ // virtual module pair and their names, build-time validation with baked
1926
+ // JSON values, the leak scan — follows @vite-env/core by pyyupsk (MIT,
1927
+ // https://github.com/pyyupsk/vite-env), the design-correct prior art. The
1928
+ // implementation is fresh against this plugin's machinery: Standard Schema
1929
+ // is the only contract (no zod dependency or zod-specific paths), the
1930
+ // schema file loads through Vite's own `runnerImport`/`loadConfigFromFile`
1931
+ // (no jiti), server-graph protection keys off the environment *consumer*
1932
+ // rather than environment-name lists, and the types are inferred from the
1933
+ // user's schema instead of introspected per-library.
1934
+ const CLIENT_ENV_ID = 'virtual:env/client';
1935
+ const SERVER_ENV_ID = 'virtual:env/server';
1936
+ const RESOLVED_CLIENT_ENV_ID = '\0' + CLIENT_ENV_ID;
1937
+ const RESOLVED_SERVER_ENV_ID = '\0' + SERVER_ENV_ID;
1938
+
1939
+ // Conventional schema locations, project-root relative. TypeScript first —
1940
+ // the whole point is inferred types — but a plain-JS project works too.
1941
+ const ENV_FILE_CANDIDATES = ['env.ts', 'env.js'];
1942
+ // Generated ambient types, written next to the schema file. Deliberately
1943
+ // NOT `env.d.ts`: a declaration file sharing the schema file's stem would
1944
+ // shadow it in TS resolution (and self-reference its own `typeof import`).
1945
+ const GENERATED_TYPES_FILE = 'solid-env.d.ts';
1946
+
1947
+ /**
1948
+ * The minimal structural slice of the Standard Schema v1 interface
1949
+ * (https://standardschema.dev) this plugin consumes — the spec is designed
1950
+ * to be vendored so validating libraries stay decoupled.
1951
+ */
1952
+
1953
+ function isStandardSchema(value) {
1954
+ return !!value && typeof value === 'object' && typeof value['~standard']?.validate === 'function';
1955
+ }
1956
+
1957
+ /**
1958
+ * Keys the loadEnv fold added to process.env, tracked process-globally.
1959
+ * Real environment always wins over files — but values *we* folded must not
1960
+ * count as "real" on revalidation, or the first fold would pin every .env
1961
+ * value forever and live edits would never be seen. Process-global (not
1962
+ * plugin-closure) state because Vite restarts the dev server on .env
1963
+ * changes, recreating the plugin instances inside the same node process:
1964
+ * the new instance must be able to clear the old instance's fold.
1965
+ */
1966
+ const FOLDED_KEYS = Symbol.for('@solidjs/vite-plugin:env-folded-keys');
1967
+ function foldedKeys() {
1968
+ const holder = globalThis;
1969
+ return holder[FOLDED_KEYS] ??= new Set();
1970
+ }
1971
+ function stringEntries(env) {
1972
+ const out = {};
1973
+ for (const key in env) {
1974
+ const value = env[key];
1975
+ if (typeof value === 'string') out[key] = value;
1976
+ }
1977
+ return out;
1978
+ }
1979
+
1980
+ /** Formats the per-key validation report shared by builds and the dev overlay. */
1981
+ function formatValidationError(issues, envFile, mode) {
1982
+ const lines = issues.map(({
1983
+ key,
1984
+ message
1985
+ }) => ` ✗ ${key}: ${message}`);
1986
+ return `[@solidjs/vite-plugin] env validation failed (${issues.length} issue${issues.length === 1 ? '' : 's'}) — schema: ${envFile}, mode: ${mode}\n\n` + lines.join('\n') + `\n\nSet the variables in your environment or .env files, or adjust the schema.`;
1987
+ }
1988
+
1989
+ /**
1990
+ * Loads the schema module at config time through Vite itself: `runnerImport`
1991
+ * (Vite 6.1+) evaluates TypeScript in-process with project resolution;
1992
+ * older Vite 6 falls back to `loadConfigFromFile`, the exact machinery that
1993
+ * loads vite.config.ts.
1994
+ */
1995
+ async function importSchemaModule(envFileAbs, root, mode) {
1996
+ const vite = await import('vite');
1997
+ if (typeof vite.runnerImport === 'function') {
1998
+ const {
1999
+ module,
2000
+ dependencies
2001
+ } = await vite.runnerImport(envFileAbs, {
2002
+ root,
2003
+ mode
2004
+ });
2005
+ return {
2006
+ exported: module?.default,
2007
+ dependencies: (dependencies || []).map(dep => path.resolve(root, dep)).filter(dep => existsSync(dep))
2008
+ };
2009
+ }
2010
+ const result = await vite.loadConfigFromFile({
2011
+ command: 'serve',
2012
+ mode
2013
+ }, envFileAbs, root);
2014
+ if (!result) {
2015
+ throw new Error(`[@solidjs/vite-plugin] failed to load env schema from ${envFileAbs}`);
2016
+ }
2017
+ return {
2018
+ exported: result.config,
2019
+ dependencies: (result.dependencies || []).map(dep => path.resolve(root, dep)).filter(dep => existsSync(dep))
2020
+ };
2021
+ }
2022
+ function assertSchemaShape(exported, envFile, envPrefixes) {
2023
+ if (!exported || typeof exported !== 'object') {
2024
+ throw new Error(`[@solidjs/vite-plugin] ${envFile} must default-export an object of the shape ` + `{ server?: { VAR: schema }, client?: { VITE_VAR: schema } } where every schema ` + `is a Standard Schema validator (zod, valibot, arktype, ...). Got: ${exported === null ? 'null' : typeof exported}.`);
2025
+ }
2026
+ const schema = exported;
2027
+ for (const key of Object.keys(schema)) {
2028
+ if (key !== 'server' && key !== 'client') {
2029
+ throw new Error(`[@solidjs/vite-plugin] unknown key "${key}" in ${envFile}: the env schema takes ` + `only \`server\` and \`client\` maps of Standard Schema validators.`);
2030
+ }
2031
+ }
2032
+ for (const side of ['server', 'client']) {
2033
+ const shape = schema[side];
2034
+ if (shape === undefined) continue;
2035
+ if (!shape || typeof shape !== 'object') {
2036
+ throw new Error(`[@solidjs/vite-plugin] \`${side}\` in ${envFile} must be an object mapping variable ` + `names to Standard Schema validators.`);
2037
+ }
2038
+ for (const [key, validator] of Object.entries(shape)) {
2039
+ if (!isStandardSchema(validator)) {
2040
+ throw new Error(`[@solidjs/vite-plugin] ${side}.${key} in ${envFile} is not a Standard Schema ` + `validator (no callable \`~standard.validate\`). Any zod/valibot/arktype ` + `schema qualifies; plain values and functions don't.`);
2041
+ }
2042
+ }
2043
+ }
2044
+ const typed = schema;
2045
+ for (const key of Object.keys(typed.client ?? {})) {
2046
+ if (!envPrefixes.some(prefix => key.startsWith(prefix))) {
2047
+ const wanted = envPrefixes[0] ?? 'VITE_';
2048
+ throw new Error(`[@solidjs/vite-plugin] client env var "${key}" in ${envFile} must carry the public ` + `env prefix ("${envPrefixes.join('" or "')}") — client vars are baked into the ` + `browser bundle. Rename it to "${wanted}${key}", or move it to \`server\` if it ` + `is a secret.`);
2049
+ }
2050
+ if (typed.server && key in typed.server) {
2051
+ throw new Error(`[@solidjs/vite-plugin] "${key}" is defined in both \`server\` and \`client\` in ` + `${envFile}; a variable belongs to exactly one side (\`client\` vars are ` + `visible to the server too).`);
2052
+ }
2053
+ }
2054
+ // The reverse guard: Vite itself bakes every prefixed variable into
2055
+ // `import.meta.env` for the browser, so declaring one under `server`
2056
+ // cannot keep it secret — it leaks through Vite's channel with no
2057
+ // diagnostics from this plugin's leak scan (which only watches the
2058
+ // virtual server module's values).
2059
+ for (const key of Object.keys(typed.server ?? {})) {
2060
+ const prefix = envPrefixes.find(p => key.startsWith(p));
2061
+ if (prefix) {
2062
+ const bare = key.slice(prefix.length);
2063
+ throw new Error(`[@solidjs/vite-plugin] server env var "${key}" in ${envFile} carries the public ` + `env prefix "${prefix}". Vite exposes every "${prefix}"-prefixed variable to ` + `the browser through import.meta.env no matter which side declares it, so a ` + `\`server\` entry cannot keep it secret. ` + (bare ? `Rename it to "${bare}" (in the schema and in your .env/environment), or ` : `Rename it without the prefix, or `) + `move it to \`client\` if it is public.`);
2064
+ }
2065
+ }
2066
+ return typed;
2067
+ }
2068
+
2069
+ /**
2070
+ * Generates the ambient `solid-env.d.ts` next to the schema file. The file
2071
+ * is self-contained: it infers each variable's type from the user's own
2072
+ * schema through the Standard Schema `~standard.types.output` phantom, so
2073
+ * any compliant validator library yields full types with no per-library
2074
+ * introspection. Only rewritten when the content actually changes.
2075
+ */
2076
+ function generateTypes(schema, envFileAbs) {
2077
+ const dtsPath = path.join(path.dirname(envFileAbs), GENERATED_TYPES_FILE);
2078
+ const importSpec = './' + path.basename(envFileAbs).replace(/\.[mc]?[tj]s$/, '');
2079
+ const field = (side, key) => ` readonly ${JSON.stringify(key)}: __Out<__Schema[${JSON.stringify(side)}][${JSON.stringify(key)}]>;`;
2080
+ const moduleBlock = (id, fields) => [`declare module '${id}' {`, ` type __Schema = typeof import(${JSON.stringify(importSpec)})['default'];`, ` type __Out<T> = T extends { '~standard': { types?: { output: infer O } | undefined } }`, ` ? O`, ` : string;`, ` const env: {`, ...fields, ` };`, ` export { env };`, ` export default env;`, `}`].join('\n');
2081
+ const clientFields = Object.keys(schema.client ?? {}).map(key => field('client', key));
2082
+ const serverFields = [...Object.keys(schema.server ?? {}).map(key => field('server', key)), ...clientFields];
2083
+ const content = `// Generated by @solidjs/vite-plugin (start.env) — do not edit.\n` + `// Regenerated on every dev server and build start from ${path.basename(envFileAbs)}.\n` + `// Keep this file (and the schema) inside your tsconfig "include".\n\n` + moduleBlock(CLIENT_ENV_ID, clientFields) + '\n\n' + moduleBlock(SERVER_ENV_ID, serverFields) + '\n';
2084
+ try {
2085
+ if (existsSync(dtsPath) && readFileSync(dtsPath, 'utf-8') === content) return;
2086
+ writeFileSync(dtsPath, content);
2087
+ } catch (error) {
2088
+ const reason = error instanceof Error ? `: ${error.message}` : '';
2089
+ console.warn(`[@solidjs/vite-plugin] could not write ${GENERATED_TYPES_FILE} next to the env schema` + `${reason} — the virtual env modules stay untyped until it can be written.`);
2090
+ }
2091
+ }
2092
+
2093
+ /**
2094
+ * Start-mode typed env (the `start.env` option). Returns no plugin when the
2095
+ * feature is off (`env: false`, or nothing to probe); the feature is
2096
+ * start-only by construction — the option lives on `start`, so a bare
2097
+ * `ssr: true` setup has no env layer (documented).
2098
+ */
2099
+ function startEnv(option) {
2100
+ if (option === false) return [];
2101
+ let root = process.cwd();
2102
+ let config;
2103
+ let isBuild = false;
2104
+ let isPreview = false;
2105
+ let enabled = false;
2106
+ /** Absolute path of the schema file once resolved. */
2107
+ let envFileAbs = null;
2108
+ /** Root-relative schema path for messages. */
2109
+ let envFile = 'env.ts';
2110
+ let envPromise = null;
2111
+ let devErrorLogged = false;
2112
+ function resolveEnvFile() {
2113
+ if (typeof option === 'string') {
2114
+ const absolute = path.isAbsolute(option) ? option : path.resolve(root, option);
2115
+ if (!existsSync(absolute)) {
2116
+ throw new Error(`[@solidjs/vite-plugin] start.env does not exist: ${option}`);
2117
+ }
2118
+ const relative = path.relative(root, absolute).split(path.sep).join('/');
2119
+ if (relative.startsWith('..')) {
2120
+ throw new Error(`[@solidjs/vite-plugin] start.env must live inside the Vite root: ${option}`);
2121
+ }
2122
+ envFileAbs = absolute;
2123
+ envFile = relative;
2124
+ enabled = true;
2125
+ return;
2126
+ }
2127
+ for (const candidate of ENV_FILE_CANDIDATES) {
2128
+ const absolute = path.resolve(root, candidate);
2129
+ if (existsSync(absolute)) {
2130
+ envFileAbs = absolute;
2131
+ envFile = candidate;
2132
+ enabled = true;
2133
+ return;
2134
+ }
2135
+ }
2136
+ if (option === true) {
2137
+ throw new Error(`[@solidjs/vite-plugin] start.env is enabled but no schema file was found: add ` + `${ENV_FILE_CANDIDATES.join(' or ')} at the project root (default-exporting ` + `{ server, client } maps of Standard Schema validators), or point start.env ` + `at a path.`);
2138
+ }
2139
+ }
2140
+ function envPrefixes() {
2141
+ const prefix = config?.envPrefix ?? 'VITE_';
2142
+ return Array.isArray(prefix) ? prefix : [prefix];
2143
+ }
2144
+ async function loadAndValidate() {
2145
+ const {
2146
+ exported,
2147
+ dependencies
2148
+ } = await (async () => {
2149
+ try {
2150
+ return await importSchemaModule(envFileAbs, root, config.mode);
2151
+ } catch (error) {
2152
+ const reason = error instanceof Error ? `\n\nCause: ${error.message}` : '';
2153
+ throw new Error(`[@solidjs/vite-plugin] could not load the env schema at ${envFile}. It must be a ` + `server-side module default-exporting { server?, client? } maps of Standard ` + `Schema validators.${reason}`);
2154
+ }
2155
+ })();
2156
+ const schema = assertSchemaShape(exported, envFile, envPrefixes());
2157
+ // Types depend only on the schema, not the values: generate before
2158
+ // validating so a missing variable doesn't also break editor types.
2159
+ generateTypes(schema, envFileAbs);
2160
+
2161
+ // Vite's .env story with `loadEnv` merge priority — process.env wins
2162
+ // (CI/pipeline secrets over files), then .env.[mode].local down to .env.
2163
+ // The fold into process.env is what removes the template's classic
2164
+ // `process.env = { ...process.env, ...loadEnv(mode, root, '') }` line:
2165
+ // server code reading process.env directly (db clients, SDKs) sees the
2166
+ // file-loaded vars too, in dev, build, and the client-mode prerender.
2167
+ const envDir = config.envDir === false ? null : config.envDir || root;
2168
+ const folded = foldedKeys();
2169
+ for (const key of folded) delete process.env[key];
2170
+ folded.clear();
2171
+ const fileEnv = envDir ? loadEnv(config.mode, envDir, '') : {};
2172
+ for (const [key, value] of Object.entries(fileEnv)) {
2173
+ if (!(key in process.env)) {
2174
+ process.env[key] = value;
2175
+ folded.add(key);
2176
+ }
2177
+ }
2178
+ const raw = {
2179
+ ...fileEnv,
2180
+ ...stringEntries(process.env)
2181
+ };
2182
+ const issues = [];
2183
+ const all = {};
2184
+ for (const side of ['server', 'client']) {
2185
+ for (const [key, validator] of Object.entries(schema[side] ?? {})) {
2186
+ let result = validator['~standard'].validate(raw[key]);
2187
+ if (result instanceof Promise) result = await result;
2188
+ if (result.issues && result.issues.length) {
2189
+ for (const issue of result.issues) {
2190
+ const at = (issue.path ?? []).map(segment => typeof segment === 'object' && segment !== null && 'key' in segment ? String(segment.key) : String(segment)).join('.');
2191
+ issues.push({
2192
+ key: at ? `${key}.${at}` : key,
2193
+ message: issue.message,
2194
+ side
2195
+ });
2196
+ }
2197
+ } else {
2198
+ all[key] = result.value;
2199
+ }
2200
+ }
2201
+ }
2202
+ if (issues.length) {
2203
+ // Client values are baked at build time, so their failures always
2204
+ // fail hard. Server values are read from process.env at boot: a
2205
+ // build may legitimately run without them (platform-injected vars),
2206
+ // so build-time server failures downgrade to a warning and boot
2207
+ // validation enforces. Dev failures stay hard — dev IS runtime.
2208
+ const clientIssues = issues.filter(issue => issue.side === 'client');
2209
+ if (!isBuild || clientIssues.length) {
2210
+ throw new Error(formatValidationError(!isBuild ? issues : clientIssues, envFile, config.mode));
2211
+ }
2212
+ config.logger.warn(`\n[@solidjs/vite-plugin] server env not valid at build time (deferred to boot ` + `validation — server env is read from process.env at runtime):\n` + issues.map(({
2213
+ key,
2214
+ message
2215
+ }) => ` ⚠ ${key}: ${message}`).join('\n') + '\n');
2216
+ }
2217
+ const client = {};
2218
+ for (const key of Object.keys(schema.client ?? {})) client[key] = all[key];
2219
+ return {
2220
+ schema,
2221
+ all,
2222
+ client,
2223
+ dependencies
2224
+ };
2225
+ }
2226
+ function ensureEnv() {
2227
+ return envPromise ??= loadAndValidate();
2228
+ }
2229
+
2230
+ /**
2231
+ * Whether the current hook runs for a server-destined module graph. The
2232
+ * environment's `consumer` is authoritative (covers workerd and friends
2233
+ * without name lists); classic contexts fall back to the ssr flag.
2234
+ */
2235
+ function isServerContext(ctx, opts) {
2236
+ const consumer = ctx.environment?.config?.consumer;
2237
+ if (consumer) return consumer === 'server';
2238
+ return !!opts?.ssr;
2239
+ }
2240
+ function serverOnlyError(importer) {
2241
+ return `[@solidjs/vite-plugin] ${SERVER_ENV_ID} is server-only and was imported from the ` + `client module graph` + (importer ? ` (by ${importer})` : '') + `. Server env values must never reach the browser bundle: import ` + `${CLIENT_ENV_ID} for the public ${envPrefixes().join('/')}-prefixed vars, or move ` + `this import into a server-only module (a "use server" module, middleware, or the ` + `server entry).`;
2242
+ }
2243
+
2244
+ // Baked-JSON module emission (pattern from @vite-env/core, MIT): the
2245
+ // validated output values serialize as a frozen object literal, so no
2246
+ // validator code exists in the client bundle and tree-shaking sees plain
2247
+ // data. `moduleType` marks the virtual source as plain JS for rolldown
2248
+ // (Vite 8).
2249
+ function envModuleCode(values) {
2250
+ return {
2251
+ code: `// Generated by @solidjs/vite-plugin (start.env)\n` + `export const env = Object.freeze(${JSON.stringify(values)});\n` + `export default env;`,
2252
+ moduleType: 'js'
2253
+ };
2254
+ }
2255
+
2256
+ // The server module is NOT baked: server values are read from
2257
+ // process.env at module init (server boot) and validated through the
2258
+ // user's own schema, imported straight into the server graph (the
2259
+ // validator library is server-only, so shipping it there is fine).
2260
+ // Client (public) values stay baked — that's what the VITE_ prefix
2261
+ // means. Platform-injected vars that don't exist at build time work,
2262
+ // secrets rotate without a rebuild, and no secret value exists in any
2263
+ // dist artifact.
2264
+ function serverEnvModuleCode(loaded) {
2265
+ const serverKeys = Object.keys(loaded.schema.server ?? {});
2266
+ const baked = `const __env = ${JSON.stringify(loaded.client)};`;
2267
+ if (!serverKeys.length) {
2268
+ return {
2269
+ code: `// Generated by @solidjs/vite-plugin (start.env) — server env.\n` + `${baked}\n` + `export const env = Object.freeze(__env);\n` + `export default env;`,
2270
+ moduleType: 'js'
2271
+ };
2272
+ }
2273
+ return {
2274
+ code: [`// Generated by @solidjs/vite-plugin (start.env) — server env.`, `// Server values are read from process.env and validated at boot;`, `// client (public) values are baked at build time.`, `import __schema from ${JSON.stringify(envFileAbs)};`, baked, `const __issues = [];`, `for (const __key of ${JSON.stringify(serverKeys)}) {`, ` let __result = __schema.server[__key]['~standard'].validate(process.env[__key]);`, ` if (__result instanceof Promise) __result = await __result;`, ` if (__result.issues && __result.issues.length) {`, ` for (const __issue of __result.issues) __issues.push(' \\u2717 ' + __key + ': ' + __issue.message);`, ` } else {`, ` __env[__key] = __result.value;`, ` }`, `}`, `if (__issues.length) {`, ` throw new Error(`, ` '[@solidjs/vite-plugin] server env validation failed at boot (' + __issues.length +`, ` ' issue' + (__issues.length === 1 ? '' : 's') + ') \\u2014 schema: ' + ${JSON.stringify(envFile)} +`, ` '\\n\\n' + __issues.join('\\n') +`, ` '\\n\\nServer env is read from process.env at boot, not baked at build time: set the ' +`, ` 'variables in the server process environment.'`, ` );`, `}`, `export const env = Object.freeze(__env);`, `export default env;`].join('\n'),
2275
+ moduleType: 'js'
2276
+ };
2277
+ }
2278
+ return [{
2279
+ name: 'solid:start-env',
2280
+ config(userConfig, env) {
2281
+ root = path.resolve(userConfig.root || process.cwd());
2282
+ isPreview = !!env.isPreview;
2283
+ resolveEnvFile();
2284
+ // Preview serves finished artifacts, so no schema loading or
2285
+ // validation happens there — but the built server module reads
2286
+ // process.env at boot, and `vite preview` should smoke-test the
2287
+ // artifact as hands-off as dev runs it: fold the .env files into
2288
+ // process.env (real environment still wins). A production process
2289
+ // brings its own environment instead.
2290
+ if (isPreview && enabled) {
2291
+ const envDirOption = userConfig.envDir;
2292
+ const envDir = envDirOption === false ? null : path.resolve(root, envDirOption || '.');
2293
+ if (envDir) {
2294
+ const folded = foldedKeys();
2295
+ for (const key of folded) delete process.env[key];
2296
+ folded.clear();
2297
+ const fileEnv = loadEnv(userConfig.mode || env.mode, envDir, '');
2298
+ for (const [key, value] of Object.entries(fileEnv)) {
2299
+ if (!(key in process.env)) {
2300
+ process.env[key] = value;
2301
+ folded.add(key);
2302
+ }
2303
+ }
2304
+ }
2305
+ }
2306
+ },
2307
+ configResolved(resolved) {
2308
+ config = resolved;
2309
+ root = resolved.root;
2310
+ isBuild = resolved.command === 'build';
2311
+ if (!enabled || isPreview) return;
2312
+ // Kick validation off eagerly (the fold must precede any server
2313
+ // module execution); buildStart awaits it and owns error routing.
2314
+ ensureEnv().catch(() => {});
2315
+ },
2316
+ async buildStart() {
2317
+ if (!enabled) return;
2318
+ try {
2319
+ await ensureEnv();
2320
+ } catch (error) {
2321
+ // Builds fail with the report; dev logs it once and lets the
2322
+ // virtual modules rethrow on load, which renders Vite's error
2323
+ // overlay (client imports) or the overlay-enabled 500 (SSR).
2324
+ if (isBuild) throw error;
2325
+ if (!devErrorLogged) {
2326
+ devErrorLogged = true;
2327
+ config.logger.error('\n' + (error instanceof Error ? error.message : String(error)) + '\n');
2328
+ }
2329
+ }
2330
+ },
2331
+ resolveId(source, importer, options) {
2332
+ if (!enabled) return null;
2333
+ if (source === CLIENT_ENV_ID) return RESOLVED_CLIENT_ENV_ID;
2334
+ if (source === SERVER_ENV_ID) {
2335
+ // The dep scanner probes client entries' import graphs without
2336
+ // executing them; deny real client-graph imports only (the load
2337
+ // hook double-checks — scanners never load `\0` ids).
2338
+ if (!options?.scan && !isServerContext(this, options)) {
2339
+ this.error(serverOnlyError(importer));
2340
+ }
2341
+ return RESOLVED_SERVER_ENV_ID;
2342
+ }
2343
+ return null;
2344
+ },
2345
+ async load(id, opts) {
2346
+ if (!enabled) return null;
2347
+ if (id !== RESOLVED_CLIENT_ENV_ID && id !== RESOLVED_SERVER_ENV_ID) return null;
2348
+ // Throws the validation report when env is invalid — the dev
2349
+ // overlay / failed build carries the per-key details.
2350
+ const loaded = await ensureEnv();
2351
+ if (id === RESOLVED_SERVER_ENV_ID) {
2352
+ if (!isServerContext(this, opts)) this.error(serverOnlyError());
2353
+ return serverEnvModuleCode(loaded);
2354
+ }
2355
+ return envModuleCode(loaded.client);
2356
+ },
2357
+ configureServer(server) {
2358
+ if (!enabled) return;
2359
+ const envDir = config.envDir === false ? null : config.envDir || root;
2360
+ // Explicit file list (no globs — chokidar 4 dropped them): the four
2361
+ // .env variants Vite consults for this mode, the schema module, and
2362
+ // whatever it transitively loaded.
2363
+ const envFiles = envDir ? ['.env', '.env.local', `.env.${config.mode}`, `.env.${config.mode}.local`].map(file => path.join(envDir, file)) : [];
2364
+ const watched = new Set([...envFiles, envFileAbs]);
2365
+ server.watcher.add([...watched]);
2366
+ ensureEnv().then(({
2367
+ dependencies
2368
+ }) => {
2369
+ for (const dep of dependencies) watched.add(dep);
2370
+ server.watcher.add(dependencies);
2371
+ }).catch(() => {});
2372
+
2373
+ // Live revalidation (flow from @vite-env/core, MIT): reload sources,
2374
+ // rerun the schema, invalidate both virtual modules in every
2375
+ // environment, and full-reload — on failure the reload makes the
2376
+ // client re-import the virtual module, whose load() now throws the
2377
+ // fresh report into the error overlay.
2378
+ let debounce;
2379
+ const onFileEvent = file => {
2380
+ if (!watched.has(file)) return;
2381
+ clearTimeout(debounce);
2382
+ debounce = setTimeout(async () => {
2383
+ envPromise = null;
2384
+ devErrorLogged = false;
2385
+ let failed = false;
2386
+ try {
2387
+ const {
2388
+ dependencies
2389
+ } = await ensureEnv();
2390
+ for (const dep of dependencies) watched.add(dep);
2391
+ server.watcher.add(dependencies);
2392
+ } catch (error) {
2393
+ failed = true;
2394
+ devErrorLogged = true;
2395
+ config.logger.error('\n' + (error instanceof Error ? error.message : String(error)) + '\n');
2396
+ }
2397
+ let invalidated = false;
2398
+ for (const environment of Object.values(server.environments ?? {})) {
2399
+ const graph = environment.moduleGraph;
2400
+ if (!graph) continue;
2401
+ for (const id of [RESOLVED_CLIENT_ENV_ID, RESOLVED_SERVER_ENV_ID]) {
2402
+ const mod = graph.getModuleById(id);
2403
+ if (mod) {
2404
+ graph.invalidateModule(mod);
2405
+ invalidated = true;
2406
+ }
2407
+ }
2408
+ }
2409
+ if (invalidated) {
2410
+ const hot = server.hot ?? server.ws;
2411
+ hot?.send({
2412
+ type: 'full-reload'
2413
+ });
2414
+ }
2415
+ if (!failed) {
2416
+ config.logger.info(`[@solidjs/vite-plugin] env revalidated (${envFile})`);
2417
+ }
2418
+ }, 100);
2419
+ };
2420
+ server.watcher.on('change', onFileEvent);
2421
+ server.watcher.on('add', onFileEvent);
2422
+ server.watcher.on('unlink', onFileEvent);
2423
+ },
2424
+ // Leak scan (heuristics from @vite-env/core, MIT): a server var's
2425
+ // *value* appearing as a quoted string literal in a client chunk means
2426
+ // something inlined it (an env.ts import from shared code, a define,
2427
+ // a copy-paste). Values under 8 chars skip (too collision-prone), as
2428
+ // do values shared with a client var and pure-vendor chunks.
2429
+ async generateBundle(_options, bundle) {
2430
+ if (!enabled || !isBuild || isServerContext(this, {
2431
+ ssr: !!config.build.ssr
2432
+ })) return;
2433
+ const loaded = await ensureEnv().catch(() => null);
2434
+ if (!loaded) return;
2435
+ const clientValues = new Set(Object.values(loaded.client));
2436
+ const secrets = Object.entries(loaded.all).filter(entry => !(entry[0] in loaded.client) && typeof entry[1] === 'string' && entry[1].length >= 8 && !clientValues.has(entry[1]));
2437
+ if (!secrets.length) return;
2438
+ const leaks = [];
2439
+ for (const [fileName, chunk] of Object.entries(bundle)) {
2440
+ if (chunk.type !== 'chunk' || !chunk.code) continue;
2441
+ const moduleIds = chunk.moduleIds ?? [];
2442
+ if (moduleIds.length > 0 && moduleIds.every(id => /[\\/]node_modules[\\/]/.test(id))) continue;
2443
+ for (const [key, value] of secrets) {
2444
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2445
+ if (new RegExp(`(["'\`])${escaped}\\1`).test(chunk.code)) {
2446
+ leaks.push(`${key} in ${fileName}`);
2447
+ }
2448
+ }
2449
+ }
2450
+ if (leaks.length) {
2451
+ this.error(`[@solidjs/vite-plugin] server env values leaked into client chunks:\n` + leaks.map(leak => ` ✗ ${leak}`).join('\n') + `\n\nServer env values belong to the server process only (they are not even ` + `baked into the server bundle). Check for hand-inlined values and imports of ` + `the env schema module from client code, and use ${SERVER_ENV_ID} from ` + `server-only modules instead.`);
2452
+ }
2453
+ }
2454
+ }];
2455
+ }
2456
+
2457
+ const require$1 = createRequire(import.meta.url);
2458
+
2459
+ /**
2460
+ * The `lazy()` module-URL placeholder contract, shared with the native
2461
+ * compiler's `transformLazy` pass: `lazy(() => import("spec"))` calls gain a
2462
+ * second string-literal argument of the form
2463
+ * `"__SOLID_LAZY_MODULE__:" + spec`, which `resolveLazyModuleUrls` swaps for
2464
+ * the project-relative resolved module path. The prefix and shape are FROZEN
2465
+ * — the emitting side lives in @dom-expressions/compiler and must match.
2466
+ */
2467
+ const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:';
2468
+
2469
+ /**
2470
+ * The HMR runtime: the dev-only `solid-js/refresh` core entry. Refresh
2471
+ * wrappers are compiled by the native `transformRefresh` pass in every mode
2472
+ * and import the runtime through normal module resolution (the legacy
2473
+ * solid-refresh package — whose runtime carries a known Solid 2.0 HMR bug,
2474
+ * solid-refresh#85 — is no longer used at all).
2475
+ */
2476
+ const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
2477
+ const viteVersionMajor = +version.split('.')[0];
2478
+ const isVite8 = viteVersionMajor >= 8;
2479
+ const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
2480
+ const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
2481
+
2482
+ // In dev the virtual manifest exports a `{ resolve, resolveSync }` resolver:
2483
+ // lazy modules resolve to their dev URL plus transitively imported CSS as
2484
+ // inline-style descriptors collected from the live module graph. The resolver
2485
+ // itself lives plugin-side (it closes over the dev server) and is reached
2486
+ // through a global registry; isolated module runners that don't share
2487
+ // globals (nitro's dev worker, workerd) fall back to fetching the dev
2488
+ // server's bridge endpoint, whose URL is baked in at generation time
2489
+ // (`bridgeUrl` — null outside a live dev server, e.g. the manifest-less SSR
2490
+ // build fallback, where js-only resolution remains). Bridge failures log
2491
+ // loudly and resolve to null so the runtime's own no-assets warning stays
2492
+ // the final catch-all.
2493
+ //
2494
+ // The generated `moduleUrl` mirrors `devModuleUrl` (src/dev-manifest.ts) —
2495
+ // base-prefixed root-relative URLs, `/@fs/` for root-external keys — for the
2496
+ // degraded paths that can't reach the plugin-side resolver (no registry and
2497
+ // no bridge, or a resolveSync call before the bridge cache warms). Keep the
2498
+ // two in sync.
2499
+ const devManifestCode = (root, base, bridgeUrl) => `const registry = globalThis[Symbol.for(${JSON.stringify(DEV_MANIFEST_REGISTRY_KEY)})];
2500
+ const projectRoot = ${JSON.stringify(root.split(path.sep).join('/'))};
2501
+ const base = ${JSON.stringify(base.startsWith('/') ? base.replace(/\/$/, '') : '')};
2502
+ function moduleUrl(key) {
2503
+ const queryIndex = key.indexOf("?");
2504
+ const file = queryIndex === -1 ? key : key.slice(0, queryIndex);
2505
+ const query = queryIndex === -1 ? "" : key.slice(queryIndex);
2506
+ if (file.slice(0, 2) !== "..") return base + "/" + key;
2507
+ const segments = (projectRoot + "/" + file).split("/");
2508
+ const resolved = [];
2509
+ for (const segment of segments) {
2510
+ if (segment === "..") resolved.pop();
2511
+ else if (segment && segment !== ".") resolved.push(segment);
2512
+ }
2513
+ return base + "/@fs/" + resolved.join("/") + query;
2514
+ }
2515
+ const jsOnly = key => ({ js: [moduleUrl(key)], css: [] });
2516
+ const bridgeUrl = ${JSON.stringify(bridgeUrl)};
2517
+ function createBridgeResolver() {
2518
+ // Convergence cache, mirroring the in-process resolver: server-side lazy()
2519
+ // re-requests assets on every retry of a suspended render pass, and only a
2520
+ // synchronous answer lets the pass converge (a fresh promise per call
2521
+ // suspends every retry anew — nested routes then loop until the render
2522
+ // stack overflows). Cached entries can go stale after a CSS edit (no
2523
+ // watcher reaches this side of the bridge); the HMR client replaces SSR'd
2524
+ // dev styles on load, so staleness self-heals at hydration. Only successful
2525
+ // answers are cached: a null (bridge failure) must stay retryable, or one
2526
+ // transient miss would strip the module's client assets — silently — for
2527
+ // the rest of the dev session. In-flight dedupe still gives retries of the
2528
+ // same pass a stable promise, so convergence holds either way.
2529
+ const cache = new Map();
2530
+ const inFlight = new Map();
2531
+ return {
2532
+ resolve(key) {
2533
+ const cached = cache.get(key);
2534
+ if (cached) return cached;
2535
+ let request = inFlight.get(key);
2536
+ if (!request) {
2537
+ request = fetchAssets(key).then(
2538
+ (assets) => {
2539
+ if (assets) cache.set(key, assets);
2540
+ inFlight.delete(key);
2541
+ return assets;
2542
+ },
2543
+ (error) => {
2544
+ inFlight.delete(key);
2545
+ throw error;
2546
+ },
2547
+ );
2548
+ inFlight.set(key, request);
2549
+ }
2550
+ return request;
2551
+ },
2552
+ resolveSync: (key) => cache.get(key) || jsOnly(key),
2553
+ };
2554
+ }
2555
+ async function fetchAssets(key) {
2556
+ const url = new URL(bridgeUrl);
2557
+ url.searchParams.set("key", key);
2558
+ let response;
2559
+ try {
2560
+ response = await fetch(url);
2561
+ } catch (error) {
2562
+ console.error(
2563
+ '[@solidjs/vite-plugin] Dev manifest bridge request failed for module key "' + key +
2564
+ '" (' + url.href + '): ' + ((error && error.message) || error) +
2565
+ ". SSR will render without this module's client assets, so its hydration preload entry will be missing.",
2566
+ );
2567
+ return null;
2568
+ }
2569
+ if (!response.ok) {
2570
+ // A silent null here strips the module's client assets from the
2571
+ // SSR'd hydration asset map and hydration fails much later with a
2572
+ // cryptic client-side error — report the miss where it happens.
2573
+ console.error(
2574
+ '[@solidjs/vite-plugin] Dev manifest bridge request failed with status ' + response.status +
2575
+ ' for module key "' + key + '" (' + url.href +
2576
+ "). SSR will render without this module's client assets, so its hydration preload entry will be missing.",
2577
+ );
2578
+ return null;
2579
+ }
2580
+ return response.json();
2581
+ }
2582
+ export default (registry && registry[${JSON.stringify(root)}]) ||
2583
+ (bridgeUrl ? createBridgeResolver() : { resolve: jsOnly, resolveSync: jsOnly });`;
2584
+ const SOLID_BUILT_INS = ['For', 'Show', 'Switch', 'Match', 'Loading', 'Reveal', 'Portal', 'Repeat', 'Dynamic', 'Errored'];
2585
+
2586
+ /** Possible options for the extensions property */
2587
+
2588
+ let nativeCompilerPromise;
2589
+ async function loadNativeCompiler() {
2590
+ try {
2591
+ return await (nativeCompilerPromise ??= import('@dom-expressions/compiler'));
2592
+ } catch (error) {
2593
+ nativeCompilerPromise = undefined;
2594
+ const reason = error instanceof Error ? `\n\nCause: ${error.message}` : '';
2595
+ throw new Error('@solidjs/vite-plugin: failed to load @dom-expressions/compiler, which is required ' + 'in every mode (it drives the lazy, refresh, and server-function transforms; ' + 'compiler: "babel" only switches the JSX transform). Your platform should get ' + 'a prebuilt native binary or the @dom-expressions/compiler-wasm32-wasi fallback ' + '— check that optional dependencies were installed.' + reason);
2596
+ }
2597
+ }
2598
+
2599
+ /** Configuration options for @solidjs/vite-plugin. */
2600
+
2601
+ /** Options for the solid-refresh HMR transform (dev only). */
2602
+
2603
+ function getExtension(filename) {
2604
+ const index = filename.lastIndexOf('.');
2605
+ return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, '');
2606
+ }
2607
+ function containsSolidField(fields) {
2608
+ const keys = Object.keys(fields);
2609
+ for (let i = 0; i < keys.length; i++) {
2610
+ const key = keys[i];
2611
+ if (key === 'solid') return true;
2612
+ if (typeof fields[key] === 'object' && fields[key] != null && containsSolidField(fields[key])) return true;
2613
+ }
2614
+ return false;
2615
+ }
2616
+ function getJestDomExport(setupFiles) {
2617
+ return setupFiles?.some(path => /jest-dom/.test(path)) ? undefined : ['@testing-library/jest-dom/vitest', '@testing-library/jest-dom/extend-expect'].find(path => {
2618
+ try {
2619
+ require$1.resolve(path);
2620
+ return true;
2621
+ } catch (e) {
2622
+ return false;
2623
+ }
2624
+ });
2625
+ }
2626
+ function getSolidOptions(options, isSsr, dev, isTestMode = false) {
2627
+ let solidOptions;
2628
+ if (isTestMode) {
2629
+ // Vitest compiles with the client posture regardless of the app's `ssr`
2630
+ // flag: component tests exercise DOM code and nothing hydrates in a
2631
+ // test, so hydratable output would look for markers that aren't there.
2632
+ // `generate` still follows the transform's own ssr flag, so explicit
2633
+ // node-environment tests (renderToString) keep their server codegen.
2634
+ solidOptions = {
2635
+ generate: isSsr ? 'ssr' : 'dom',
2636
+ hydratable: false
2637
+ };
2638
+ } else if (options.start && !options.ssr) {
2639
+ // Client start mode: client code compiles exactly like a plain SPA
2640
+ // (dom, non-hydratable — nothing hydrates); only the document shell
2641
+ // render goes through the SSR transforms, also non-hydratable since
2642
+ // the shell is inert HTML the client never claims.
2643
+ solidOptions = {
2644
+ generate: isSsr ? 'ssr' : 'dom',
2645
+ hydratable: false
2646
+ };
2647
+ } else if (options.ssr) {
2648
+ if (isSsr) {
2649
+ solidOptions = {
2650
+ generate: 'ssr',
2651
+ hydratable: true
2652
+ };
2653
+ } else {
2654
+ solidOptions = {
2655
+ generate: 'dom',
2656
+ hydratable: true
2657
+ };
2658
+ }
2659
+ } else {
2660
+ solidOptions = {
2661
+ generate: 'dom',
2662
+ hydratable: false
2663
+ };
2664
+ }
2665
+ return {
2666
+ moduleName: '@solidjs/web',
2667
+ builtIns: SOLID_BUILT_INS,
2668
+ contextToCustomElements: true,
2669
+ wrapConditionals: true,
2670
+ ...solidOptions,
2671
+ dev,
2672
+ ...(options.solid || {})
2673
+ };
2674
+ }
2675
+ async function getBabelUserOptions(options, source, id, isSsr) {
2676
+ if (!options.babel) return {};
2677
+ if (typeof options.babel !== 'function') return options.babel;
2678
+ const babelOptions = options.babel(source, id, isSsr);
2679
+ return babelOptions instanceof Promise ? await babelOptions : babelOptions;
2680
+ }
2681
+ function normalizeSourceMap(map) {
2682
+ if (typeof map === 'string') return JSON.parse(map);
2683
+ return map || null;
2684
+ }
2685
+ /**
2686
+ * Merges the sourcemaps of sequential whole-file transforms (given in
2687
+ * application order, earliest first) into one map tracing back to the
2688
+ * original source.
2689
+ */
2690
+ function combineSourcemaps(maps) {
2691
+ const chain = maps.filter(map => !!map);
2692
+ if (chain.length === 0) return null;
2693
+ if (chain.length === 1) return normalizeSourceMap(chain[0]);
2694
+ // remapping expects most-recent-first.
2695
+ return JSON.parse(remapping(chain.reverse(), () => null).toString());
2696
+ }
2697
+
2698
+ /**
2699
+ * Chunks emitted for lazy() targets are marked `isEntry` by Rollup even
2700
+ * though they are semantically dynamic entries. Reclassify any entry that is
2701
+ * dynamically imported by another chunk so the runtime's entry-asset
2702
+ * detection (which keys off `isEntry`) can't pick a lazy facade instead of
2703
+ * the real client entry. Works on both the Vite manifest.json shape and the
2704
+ * raw Rollup output bundle — both key entries by name and expose
2705
+ * `dynamicImports` / `isEntry` with the same meaning.
2706
+ */
2707
+ function normalizeEmittedLazyEntries(manifest) {
2708
+ const dynamicKeys = new Set();
2709
+ for (const key in manifest) {
2710
+ const imports = manifest[key].dynamicImports;
2711
+ if (imports) for (const dep of imports) dynamicKeys.add(dep);
2712
+ }
2713
+ for (const key of dynamicKeys) {
2714
+ const entry = manifest[key];
2715
+ if (entry && entry.isEntry) {
2716
+ entry.isEntry = false;
2717
+ entry.isDynamicEntry = true;
2718
+ }
2719
+ }
2720
+ }
2721
+ function solidPlugin(options = {}) {
2722
+ if (typeof options.ssr === 'object') {
2723
+ throw new Error('[@solidjs/vite-plugin] `ssr` now only accepts a boolean ("is the app server-rendered"); ' + 'move start-mode options to `start: {}` and set `ssr: true`. Example: ' + '`solid({ ssr: { document: … } })` becomes `solid({ start: { document: … }, ssr: true })`.');
2724
+ }
2725
+ // Recreated in configResolved: relative include/exclude patterns must
2726
+ // resolve against the Vite root, not process.cwd() — running `vite` from
2727
+ // outside the project would otherwise change what the filter matches.
2728
+ let filter = createFilter(options.include, options.exclude);
2729
+ const serverComponents = typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
2730
+ // `start: true` is sugar for the empty options bag — one start mode,
2731
+ // two spellings — so normalize here and let everything downstream see a
2732
+ // single shape (`false` behaves exactly like omission).
2733
+ const turnkey = options.start === true ? {} : options.start || null;
2734
+ // `start.external` only means something when a server side exists to hand
2735
+ // over (SSR start mode); in client mode it is a documented no-op.
2736
+ const externalDevServer = !!options.ssr && !!turnkey?.external;
2737
+ let needHmr = false;
2738
+ let replaceDev = false;
2739
+ // The live dev server, kept so the dev manifest module can bake the bridge
2740
+ // endpoint URL in when its code is generated (see devManifestBridgeUrl).
2741
+ let devServer = null;
2742
+ let projectRoot = process.cwd();
2743
+ let isTestMode = false;
2744
+ let serverTestPosture = false;
2745
+ let isBuild = false;
2746
+ let isSsrBuild = false;
2747
+ let base = '/';
2748
+ let clientOutDir = null;
2749
+ let solidPkgsConfig;
2750
+
2751
+ // The client build's manifest, read back by SSR builds. In builder-mode
2752
+ // (single process, e.g. SolidStart's nitro plugin) the client build runs
2753
+ // first and generateBundle records its actual outDir — authoritative, since
2754
+ // such setups relocate it. Two-invocation builds (`vite build --outDir
2755
+ // dist/client` then `vite build --ssr`) run in separate processes, so the
2756
+ // SSR process falls back to the `dist/client` convention.
2757
+ function clientManifestPath() {
2758
+ for (const dir of [clientOutDir, 'dist/client']) {
2759
+ if (!dir) continue;
2760
+ const manifestPath = path.resolve(projectRoot, dir, '.vite/manifest.json');
2761
+ if (existsSync(manifestPath)) return manifestPath;
2762
+ }
2763
+ return null;
2764
+ }
2765
+
2766
+ // Dynamically imported project modules in the client build. Each is
2767
+ // emitted as an explicit chunk so it always gets its own manifest entry
2768
+ // keyed by source path — even when manualChunks or dual static/dynamic
2769
+ // imports would otherwise fold it facade-less into a shared chunk (which
2770
+ // would break resolveAssets lookups and hydration module preloading).
2771
+ // Driven from moduleParsed so it covers every lazy() target, including
2772
+ // import.meta.glob entries that never pass through the moduleUrl transform.
2773
+ const emittedLazyChunks = new Set();
2774
+ // Keep the emitted references because a lazy module's importer may be
2775
+ // removed from the final bundle, leaving no dynamic-import edge to identify
2776
+ // its facade chunk during generateBundle.
2777
+ const emittedLazyChunkRefs = [];
2778
+
2779
+ // Whether the current hook invocation belongs to a client (browser) build.
2780
+ // Builder-mode builds (e.g. SolidStart's nitro plugin) run the client and
2781
+ // ssr environments through one Vite process with shared plugins, so the
2782
+ // process-wide isSsrBuild flag from configResolved can't tell them apart —
2783
+ // the per-environment consumer can. Classic two-invocation builds
2784
+ // (`vite build` / `vite build --ssr`) fall back to the flag.
2785
+ function isClientBuild(ctx) {
2786
+ const consumer = ctx.environment?.config?.consumer;
2787
+ if (consumer) return consumer === 'client';
2788
+ return !isSsrBuild;
2789
+ }
2790
+
2791
+ /**
2792
+ * Replaces lazy() moduleUrl placeholders injected by the babel plugin with
2793
+ * project-relative module paths resolved through Vite's resolver.
2794
+ */
2795
+ async function resolveLazyModuleUrls(ctx, code, importer) {
2796
+ const placeholderRe = new RegExp('"' + LAZY_PLACEHOLDER_PREFIX + '([^"]+)"', 'g');
2797
+ let match;
2798
+ const resolutions = [];
2799
+ while ((match = placeholderRe.exec(code)) !== null) {
2800
+ const specifier = match[1];
2801
+ const resolved = await ctx.resolve(specifier, importer);
2802
+ if (resolved) {
2803
+ // The query is part of the module identity: Rollup keys the facade
2804
+ // chunk (and thus the Vite manifest entry) by the queried module id,
2805
+ // and in dev the queried URL can serve different plugin output than
2806
+ // the bare one — stripping it here would break both lookups.
2807
+ const queryIndex = resolved.id.indexOf('?');
2808
+ const file = queryIndex === -1 ? resolved.id : resolved.id.slice(0, queryIndex);
2809
+ const query = queryIndex === -1 ? '' : resolved.id.slice(queryIndex);
2810
+ const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
2811
+ resolutions.push({
2812
+ placeholder: match[0],
2813
+ resolved: '"' + relativeId + '"'
2814
+ });
2815
+ }
2816
+ }
2817
+ for (const {
2818
+ placeholder,
2819
+ resolved
2820
+ } of resolutions) {
2821
+ code = code.replace(placeholder, resolved);
2822
+ }
2823
+ return code;
2824
+ }
2825
+
2826
+ /**
2827
+ * SSR transforms append a `$$moduleUrl` export carrying the module's
2828
+ * client-manifest key (project-relative source path, module query
2829
+ * included — a queried module is its own identity, with its own facade
2830
+ * chunk and manifest entry). Server-side `lazy()` reads it off the
2831
+ * resolved module when the callsite has no static import specifier to
2832
+ * transform — e.g. `lazy` over an `import.meta.glob` entry — so asset
2833
+ * resolution and hydration preloading still work. Client builds are
2834
+ * untouched.
2835
+ */
2836
+ function injectSsrModuleId(code, id, isSsr) {
2837
+ if (!isSsr || /node_modules/.test(id) || code.includes('$$moduleUrl')) return code;
2838
+ const queryIndex = id.indexOf('?');
2839
+ const file = queryIndex === -1 ? id : id.slice(0, queryIndex);
2840
+ const query = queryIndex === -1 ? '' : id.slice(queryIndex);
2841
+ const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
2842
+ return code + `\nexport const $$moduleUrl = ${JSON.stringify(relativeId)};\n`;
2843
+ }
2844
+ const mainPlugin = {
2845
+ name: 'solid',
2846
+ enforce: 'pre',
2847
+ async config(userConfig, {
2848
+ command
2849
+ }) {
2850
+ // We inject the dev mode only if the user explicitly wants it or if we are in dev (serve) mode
2851
+ replaceDev = options.dev === true || options.dev !== false && command === 'serve';
2852
+ projectRoot = userConfig.root || projectRoot;
2853
+ isTestMode = userConfig.mode === 'test';
2854
+ // Per-vitest-project posture: the client posture (browser conditions,
2855
+ // dom codegen, jsdom default) is right for DOM component tests but
2856
+ // wrong for server-runtime unit tests. A project that explicitly opts
2857
+ // into a server runtime — `test: { environment: 'node' }` (or
2858
+ // 'edge-runtime') — gets the server posture end to end: no browser
2859
+ // condition injection, so the framework resolves its real server
2860
+ // build (isServer true) with no inline/alias workarounds. DOM
2861
+ // environments (the jsdom default, happy-dom, browser mode) keep the
2862
+ // client posture. Each vitest project resolves its own config, so the
2863
+ // hooks below see the posture of the project they serve.
2864
+ serverTestPosture = isTestMode && (userConfig.test?.environment === 'node' || userConfig.test?.environment === 'edge-runtime');
2865
+ solidPkgsConfig = await crawlFrameworkPkgs({
2866
+ viteUserConfig: userConfig,
2867
+ root: projectRoot || process.cwd(),
2868
+ isBuild: command === 'build',
2869
+ isFrameworkPkgByJson(pkgJson) {
2870
+ return containsSolidField(pkgJson.exports || {});
2871
+ }
2872
+ });
2873
+
2874
+ // fix for bundling dev in production
2875
+ const nestedDeps = replaceDev ? ['solid-js', '@solidjs/web'] : [];
2876
+ const userTest = userConfig.test ?? {};
2877
+ const test = {};
2878
+ if (userConfig.mode === 'test') {
2879
+ // to simplify the processing of the config, we normalize the setupFiles to an array
2880
+ const userSetupFiles = typeof userTest.setupFiles === 'string' ? [userTest.setupFiles] : userTest.setupFiles || [];
2881
+
2882
+ // Regardless of the app's `ssr` flag: tests run with the client
2883
+ // posture (DOM component tests are the norm), so the default test
2884
+ // environment is a DOM. Node-environment tests opt in explicitly.
2885
+ // Browser-mode projects get the real browser DOM, so don't default
2886
+ // them to jsdom — vitest probes for the environment's package at
2887
+ // startup and fails the run if jsdom isn't installed. They fall
2888
+ // back to vitest's own node default (no package probe).
2889
+ if (!userTest.environment && !userTest.browser?.enabled) {
2890
+ test.environment = 'jsdom';
2891
+ }
2892
+ if (serverTestPosture) {
2893
+ // The worker pool is shared across the whole vitest workspace and
2894
+ // imports externalized deps natively with `--conditions` derived
2895
+ // from the ROOT config — which carries the client posture's
2896
+ // 'browser'. Inline the framework so every resolution goes through
2897
+ // THIS project's (server) conditions instead: one server-build
2898
+ // instance end to end (request-event storage included).
2899
+ if (!userTest.server?.deps?.inline) {
2900
+ test.server = {
2901
+ deps: {
2902
+ inline: [/solid-js/, /@solidjs[+/]web/]
2903
+ }
2904
+ };
2905
+ }
2906
+ } else if (!userTest.server?.deps?.external?.find(item => /solid-js/.test(item.toString()))) {
2907
+ test.server = {
2908
+ deps: {
2909
+ external: [/solid-js/]
2910
+ }
2911
+ };
2912
+ }
2913
+ // jest-dom's DOM matchers have no place in a server-posture project;
2914
+ // vitest browser mode already has bundled jest-dom assertions
2915
+ // https://main.vitest.dev/guide/browser/assertion-api.html#assertion-api
2916
+ if (!userTest.browser?.enabled && !serverTestPosture) {
2917
+ const jestDomImport = getJestDomExport(userSetupFiles);
2918
+ if (jestDomImport) {
2919
+ test.setupFiles = [jestDomImport];
2920
+ }
2921
+ }
2922
+ }
2923
+ return {
2924
+ /**
2925
+ * We only need esbuild on .ts or .js files.
2926
+ * .tsx & .jsx files are handled by us
2927
+ */
2928
+ // esbuild: { include: /\.ts$/ },
2929
+ // resolve.conditions is handled per-environment in configEnvironment.
2930
+ resolve: {
2931
+ dedupe: nestedDeps
2932
+ },
2933
+ optimizeDeps: {
2934
+ include: [...nestedDeps,
2935
+ // Dev refresh wrappers import the solid-js/refresh runtime in
2936
+ // every mode; pre-bundle it up front so its discovery doesn't
2937
+ // trigger a re-optimize + full reload on first use.
2938
+ ...(command === 'serve' && options.hot !== false && !options.refresh?.disabled ? [REFRESH_RUNTIME_SOURCE] : []),
2939
+ // The server-components client runtime is imported by the
2940
+ // (virtual) client entry, and compiled function references
2941
+ // import the server-function client runtime; pre-bundle both up
2942
+ // front — in one optimizer pass — so a mid-session discovery
2943
+ // can't trigger a re-optimize + full reload, and both entries
2944
+ // share one instance of the transport config module (the
2945
+ // server-components runtime installs its response policy there).
2946
+ ...(command === 'serve' && serverComponents ? ['@solidjs/web/frames', '@solidjs/web/server-functions'] : []), ...solidPkgsConfig.optimizeDeps.include],
2947
+ exclude: solidPkgsConfig.optimizeDeps.exclude,
2948
+ // Vite 8+ uses Rolldown for dependency scanning. Rolldown defaults to
2949
+ // React's automatic JSX runtime for .tsx files, injecting a
2950
+ // react/jsx-dev-runtime import. Tell it to preserve JSX as-is since
2951
+ // this plugin handles JSX transformation via babel-preset-solid.
2952
+ ...(isVite8 ? {
2953
+ rolldownOptions: {
2954
+ transform: {
2955
+ jsx: 'preserve'
2956
+ }
2957
+ }
2958
+ } : {})
2959
+ },
2960
+ ...(Object.keys(test).length ? {
2961
+ test
2962
+ } : {})
2963
+ };
2964
+ },
2965
+ // @ts-ignore This hook only works in Vite 6
2966
+ async configEnvironment(name, config, opts) {
2967
+ config.resolve ??= {};
2968
+ // Emulate Vite default fallback for `resolve.conditions` if not set
2969
+ if (config.resolve.conditions == null) {
2970
+ // @ts-ignore These exports only exist in Vite 6
2971
+ const {
2972
+ defaultClientConditions,
2973
+ defaultServerConditions
2974
+ } = await import('vite');
2975
+ if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
2976
+ config.resolve.conditions = [...defaultClientConditions];
2977
+ } else {
2978
+ config.resolve.conditions = [...defaultServerConditions];
2979
+ }
2980
+ }
2981
+ config.resolve.conditions = ['solid', ...(replaceDev ? ['development'] : []),
2982
+ // Tests resolve the browser builds even when the app is
2983
+ // server-rendered — the client posture applies to the whole test
2984
+ // pipeline, not just the codegen. Projects that explicitly opt into
2985
+ // a server runtime (`test.environment: 'node'` / 'edge-runtime')
2986
+ // keep the default server conditions instead, so the framework's
2987
+ // real server build resolves (isServer true).
2988
+ ...(isTestMode && !serverTestPosture && !opts.isSsrTargetWebworker ? ['browser'] : []), ...config.resolve.conditions];
2989
+
2990
+ // Set resolve.noExternal and resolve.external for the SSR environment.
2991
+ // Only set resolve.external if noExternal is not true (to avoid conflicts with plugins like Cloudflare)
2992
+ if (name === 'ssr' && solidPkgsConfig) {
2993
+ if (config.resolve.noExternal !== true) {
2994
+ config.resolve.noExternal = [...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal];
2995
+ config.resolve.external = [...(Array.isArray(config.resolve.external) ? config.resolve.external : []), ...solidPkgsConfig.ssr.external];
2996
+ }
2997
+ }
2998
+ },
2999
+ configResolved(config) {
3000
+ isBuild = config.command === 'build';
3001
+ isSsrBuild = !!config.build.ssr;
3002
+ base = config.base;
3003
+ projectRoot = config.root;
3004
+ filter = createFilter(options.include, options.exclude, {
3005
+ resolve: projectRoot
3006
+ });
3007
+ if (serverComponents && !(options.start && options.ssr)) {
3008
+ config.logger.warn('[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' + 'option with `ssr: true`), so the plugin only installs the endpoint response transform ' + '(server functions returning components stream correctly). The document wiring — render ' + 'plugin, bootstrap script, and the client-side installServerComponents() call — is ' + "emitted by SSR start mode's generated entries; without it, server components only mount " + 'from post-boot streams and your client code must call installServerComponents() itself.');
3009
+ }
3010
+ needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false && !options.refresh?.disabled;
3011
+ },
3012
+ configureServer(server) {
3013
+ devServer = server;
3014
+ // Dev asset resolution for SSR: the virtual manifest module (evaluated
3015
+ // in the SSR environment) picks this resolver up through the global
3016
+ // registry keyed by project root — or, from isolated module runners
3017
+ // that don't share globals with this process, through the HTTP bridge
3018
+ // endpoint the middleware serves.
3019
+ if (options.ssr || options.start) {
3020
+ registerDevAssetResolver(server.config.root, createDevAssetResolver(server));
3021
+ installDevManifestBridge(server);
3022
+ }
3023
+ if (!needHmr) return;
3024
+ // When a module has a syntax error, Vite sends the error overlay via
3025
+ // WebSocket but the failed import triggers invalidation in solid-refresh.
3026
+ // This propagates up to @refresh reload boundaries (e.g. document-level
3027
+ // App components in SSR), causing a full-reload that overrides the overlay.
3028
+ // We suppress update/full-reload messages that immediately follow an error.
3029
+ const hot = server.hot ?? server.ws;
3030
+ if (!hot) return;
3031
+ let lastErrorTime = 0;
3032
+ const origSend = hot.send.bind(hot);
3033
+ hot.send = function (...args) {
3034
+ const payload = args[0];
3035
+ if (typeof payload === 'object' && payload) {
3036
+ if (payload.type === 'error') {
3037
+ lastErrorTime = Date.now();
3038
+ } else if (lastErrorTime && (payload.type === 'full-reload' || payload.type === 'update')) {
3039
+ if (Date.now() - lastErrorTime < 200) return;
3040
+ lastErrorTime = 0;
3041
+ }
3042
+ }
3043
+ return origSend(...args);
3044
+ };
3045
+ },
3046
+ hotUpdate({
3047
+ modules
3048
+ }) {
3049
+ // solid-refresh only injects HMR boundaries into client modules, so
3050
+ // non-client environments have no accept handlers. Without this, Vite
3051
+ // would see no boundaries and send full-reload messages that race with
3052
+ // client-side HMR updates. Provider-owned (non-runnable) environments
3053
+ // fall through instead: their plugin needs the real module list to
3054
+ // invalidate its remote runner, and its channel never reaches the
3055
+ // browser websocket.
3056
+ if (this.environment.name !== 'client' && isRunnableEnvironment(this.environment)) {
3057
+ // Returning [] also suppresses the signal environment-runner based
3058
+ // servers (e.g. nitro's dev worker) rely on to re-evaluate modules,
3059
+ // leaving SSR stale until a manual restart. Send the reload on this
3060
+ // environment's own channel — for runner-based environments that is
3061
+ // the runner, for the default ssr environment a no-op, and never the
3062
+ // browser websocket, so client HMR stays free of full-reload races.
3063
+ if (modules.length > 0) {
3064
+ this.environment.hot.send({
3065
+ type: 'full-reload'
3066
+ });
3067
+ }
3068
+ return [];
3069
+ }
3070
+ },
3071
+ resolveId(id) {
3072
+ if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
3073
+ },
3074
+ moduleParsed(info) {
3075
+ // SSR-mode client builds only: give every dynamically imported project
3076
+ // module its own facade chunk (exports-only preserves `default`
3077
+ // re-exports) so it keeps a manifest entry keyed by its source path
3078
+ // even when chunk grouping would otherwise absorb it. Plain SPA builds
3079
+ // have no manifest lookups to protect.
3080
+ if (!isBuild || !options.ssr || !isClientBuild(this)) return;
3081
+ for (const depId of info.dynamicallyImportedIds || []) {
3082
+ const cleanId = depId.split('?')[0];
3083
+ if (/node_modules/.test(cleanId) || cleanId.startsWith('\0')) continue;
3084
+ if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue;
3085
+ if (emittedLazyChunks.has(depId)) continue;
3086
+ emittedLazyChunks.add(depId);
3087
+ emittedLazyChunkRefs.push(this.emitFile({
3088
+ type: 'chunk',
3089
+ id: depId,
3090
+ preserveSignature: 'exports-only'
3091
+ }));
3092
+ }
3093
+ },
3094
+ load(id) {
3095
+ if (id === RESOLVED_VIRTUAL_MANIFEST_ID) {
3096
+ if (!isBuild) {
3097
+ return devManifestCode(projectRoot, base, devServer ? devManifestBridgeUrl(devServer) : null);
3098
+ }
3099
+ const manifestPath = clientManifestPath();
3100
+ if (manifestPath) {
3101
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
3102
+ normalizeEmittedLazyEntries(manifest);
3103
+ manifest._base = base;
3104
+ return `export default ${JSON.stringify(manifest)};`;
3105
+ }
3106
+ // SSR build before the client build produced a manifest: bake in the
3107
+ // dev-shaped fallback (registry miss degrades to js-only resolution).
3108
+ return devManifestCode(projectRoot, base, null);
3109
+ }
3110
+ },
3111
+ generateBundle(outputOptions, bundle) {
3112
+ if (!isBuild || !isClientBuild(this)) return;
3113
+ clientOutDir = outputOptions.dir ?? null;
3114
+ // Reclassify emitted lazy facade chunks in the raw bundle (not just the
3115
+ // serialized manifest read back later) so downstream plugins inspecting
3116
+ // the bundle don't mistake them for application entries. Must precede
3117
+ // the client asset map build, which keys off dynamic entries.
3118
+ if (options.ssr) {
3119
+ for (const ref of emittedLazyChunkRefs) {
3120
+ let fileName;
3121
+ try {
3122
+ fileName = this.getFileName(ref);
3123
+ } catch {
3124
+ // Ignore references retained from a previous watch build.
3125
+ continue;
3126
+ }
3127
+ const chunk = bundle[fileName];
3128
+ if (!chunk || chunk.type !== 'chunk') continue;
3129
+ chunk.isEntry = false;
3130
+ chunk.isDynamicEntry = true;
3131
+ }
3132
+ normalizeEmittedLazyEntries(bundle);
3133
+ }
3134
+ },
3135
+ async transform(source, id, transformOptions) {
3136
+ const isSsr = transformOptions && transformOptions.ssr;
3137
+ const currentFileExtension = getExtension(id);
3138
+ const extensionsToWatch = options.extensions || [];
3139
+ const allExtensions = extensionsToWatch.map(extension =>
3140
+ // An extension can be a string or a tuple [extension, options]
3141
+ typeof extension === 'string' ? extension : extension[0]);
3142
+ if (!filter(id)) {
3143
+ return null;
3144
+ }
3145
+
3146
+ // The queried id is the module's real identity (facade chunk /
3147
+ // manifest key / dev URL); keep it for the `$$moduleUrl` injection
3148
+ // while the transform pipeline below works on the clean file path.
3149
+ const moduleId = id;
3150
+ id = id.replace(/\?.*$/, '');
3151
+ if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
3152
+ return null;
3153
+ }
3154
+ const inNodeModules = /node_modules/.test(id);
3155
+ const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, isTestMode);
3156
+
3157
+ // We need to know if the current file extension has a typescript options tied to it
3158
+ const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || extensionsToWatch.some(extension => {
3159
+ if (typeof extension === 'string') {
3160
+ return extension.includes('tsx');
3161
+ }
3162
+ const [extensionName, extensionOptions] = extension;
3163
+ if (extensionName !== currentFileExtension) return false;
3164
+ return extensionOptions.typescript;
3165
+ });
3166
+ const plugins = ['jsx', 'decorators'];
3167
+ if (shouldBeProcessedWithTypescript) {
3168
+ plugins.push('typescript');
3169
+ }
3170
+ const needRefresh = needHmr && !isSsr && !inNodeModules;
3171
+ const babelUserOptions = await getBabelUserOptions(options, source, id, !!isSsr);
3172
+
3173
+ // The native compiler picks its parser dialect from the file
3174
+ // extension; custom extensions registered through `options.extensions`
3175
+ // are unknown to it, so borrow a standard one matching the configured
3176
+ // TypeScript-ness.
3177
+ const nativeFilename = /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3178
+
3179
+ // Shared native prelude for every mode: the lazy() module-URL pass,
3180
+ // then (dev/client/non-node_modules) the solid-refresh HMR pass, both
3181
+ // operating on pre-JSX source. Only the JSX transform itself differs
3182
+ // between compiler backends. Sourcemaps are collected in application
3183
+ // order and merged at the end.
3184
+ const compiler = await loadNativeCompiler();
3185
+ let code = source;
3186
+ const maps = [];
3187
+ const lazyResult = await compiler.transformLazyAsync(code, {
3188
+ filename: nativeFilename,
3189
+ sourceMap: true
3190
+ });
3191
+ code = lazyResult.code;
3192
+ maps.push(lazyResult.map);
3193
+ if (needRefresh) {
3194
+ const refreshResult = await compiler.transformRefreshAsync(code, {
3195
+ filename: nativeFilename,
3196
+ bundler: 'vite',
3197
+ fixRender: true,
3198
+ // The napi validator rejects explicit undefined; omit to get the
3199
+ // pass's default (true).
3200
+ ...(typeof options.refresh?.granular === 'boolean' ? {
3201
+ granular: options.refresh.granular
3202
+ } : {}),
3203
+ jsx: false,
3204
+ importSource: REFRESH_RUNTIME_SOURCE,
3205
+ sourceMap: true
3206
+ });
3207
+ code = refreshResult.code;
3208
+ maps.push(refreshResult.map);
3209
+ }
3210
+ const babelBaseOptions = {
3211
+ root: projectRoot,
3212
+ filename: id,
3213
+ sourceFileName: id,
3214
+ ast: false,
3215
+ sourceMaps: true,
3216
+ configFile: false,
3217
+ babelrc: false,
3218
+ parserOpts: {
3219
+ plugins
3220
+ }
3221
+ };
3222
+ if (options.compiler !== 'babel') {
3223
+ if (options.babel) {
3224
+ // Custom babel options reintroduce a Babel support pass hosting
3225
+ // only the user's plugins, ahead of the native JSX transform.
3226
+ const supportOptions = mergeAndConcat(babelUserOptions, babelBaseOptions);
3227
+ const supportResult = await babel.transformAsync(code, supportOptions);
3228
+ if (!supportResult) {
3229
+ return undefined;
3230
+ }
3231
+ code = supportResult.code || '';
3232
+ maps.push(supportResult.map);
3233
+ }
3234
+ const result = await compiler.transformAsync(code, {
3235
+ ...solidOptions,
3236
+ filename: nativeFilename,
3237
+ sourceMap: true
3238
+ });
3239
+ maps.push(result.map);
3240
+ const finalCode = injectSsrModuleId(await resolveLazyModuleUrls(this, result.code || '', id), moduleId, !!isSsr);
3241
+ return {
3242
+ code: finalCode,
3243
+ map: combineSourcemaps(maps)
3244
+ };
3245
+ }
3246
+
3247
+ // Babel JSX backend: one babel.transformAsync hosting the user's
3248
+ // options plus babel-preset-solid.
3249
+ const babelOptions = mergeAndConcat(babelUserOptions, {
3250
+ ...babelBaseOptions,
3251
+ presets: [[solid, solidOptions]]
3252
+ });
3253
+ const result = await babel.transformAsync(code, babelOptions);
3254
+ if (!result) {
3255
+ return undefined;
3256
+ }
3257
+ maps.push(result.map);
3258
+ const finalCode = injectSsrModuleId(await resolveLazyModuleUrls(this, result.code || '', id), moduleId, !!isSsr);
3259
+ return {
3260
+ code: finalCode,
3261
+ map: combineSourcemaps(maps)
3262
+ };
3263
+ }
3264
+ };
3265
+
3266
+ // The directive transform must run before the JSX transform (it operates
3267
+ // on raw directives, and client-mode module-level extraction must happen
3268
+ // before templates are generated), so its sub-plugins go first. The
3269
+ // boundary markers (`server-only` / `client-only`) are always on.
3270
+ const plugins = options.serverFunctions ? [boundaryModules(), ...serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
3271
+ devMiddleware: true,
3272
+ externalDevServer,
3273
+ // With start mode on (either variant), the dev middleware dispatches
3274
+ // the endpoint through the SSR handler so user middleware and the
3275
+ // stub-backed request event front it exactly like page SSR.
3276
+ ...(turnkey ? {
3277
+ ssrHandler: SSR_HANDLER_ID
3278
+ } : {})
3279
+ }), mainPlugin] : [boundaryModules(), mainPlugin];
3280
+
3281
+ // The `start` option opts into start-mode serving on top of the transforms;
3282
+ // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
3283
+ // historical transform-only behavior).
3284
+ if (turnkey) {
3285
+ plugins.push(
3286
+ // Typed env (`start.env`) rides both start modes: config-time
3287
+ // validation, the virtual:env/{server,client} modules, generated
3288
+ // types, and the client-bundle leak scan.
3289
+ ...startEnv(turnkey.env), ...startServe(turnkey, {
3290
+ serverFunctions: !!options.serverFunctions,
3291
+ serverComponents,
3292
+ ssr: !!options.ssr
3293
+ }));
3294
+ }
3295
+
3296
+ // Builder-mode (environments API) client-before-server build ordering.
3297
+ // Server builds read the client manifest — `virtual:solid-manifest` bakes
3298
+ // dist/client/.vite/manifest.json in, and the persisted server-function
3299
+ // manifest merges the client build's discoveries — so the client
3300
+ // environment must build first. Start mode's own orchestration already
3301
+ // orders it that way (environment definition order), but a composed setup
3302
+ // whose orchestrator builds server environments first (e.g.
3303
+ // @cloudflare/vite-plugin's buildApp, which builds workers before client)
3304
+ // would bake a manifest-less fallback into the server bundle. Every user
3305
+ // of such a setup had to hand-write this ordering plugin; absorb it.
3306
+ //
3307
+ // Semantics (Vite 7.1+; Vite 6 has no plugin `buildApp` hook and ignores
3308
+ // these, keeping its build-everything default):
3309
+ // - The first hook builds the client environment first, but only where
3310
+ // the ordering matters: a client build that emits a manifest and
3311
+ // actually has an input. It runs at *normal* order, deliberately not
3312
+ // `pre`: pre-order buildApp hooks are where hosts do destructive
3313
+ // preparation — nitro v3's `nitro:prepare` rm -rf's the whole output
3314
+ // directory from a pre-order hook, so a pre-order client build sorted
3315
+ // before it built into a directory that was then wiped (client assets
3316
+ // and manifest gone, the manifest-less fallback baked into the server
3317
+ // bundle, prod 500s). Normal order still runs before every known
3318
+ // server-first orchestrator: a config-level `builder.buildApp`
3319
+ // (@cloudflare/vite-plugin's workers-before-client orchestrator) is
3320
+ // invoked by Vite only after all pre- and normal-order plugin hooks
3321
+ // (just before the first post-order hook), and hook-based orchestrators
3322
+ // (nitro's `nitro:main`, cloudflare's own companion hook) declare
3323
+ // post order. Orchestrators running after skip the client via `isBuilt`
3324
+ // (or at worst rebuild it, which is wasteful but correct — the manifest
3325
+ // exists either way when the server environments build).
3326
+ // - Building anything from a hook suppresses Vite's own
3327
+ // build-all-environments fallback (it only runs when *no* environment
3328
+ // is built), so a setup with no real orchestrator — e.g. start mode's
3329
+ // plain `builder: {}` — would end up with only the client built. The
3330
+ // post-order hook reinstates exactly that fallback: when nothing but
3331
+ // our own client build has happened and no other plugin stakes a claim
3332
+ // on the app build, build the remaining environments in definition
3333
+ // order, precisely what Vite would have done. Another plugin declaring
3334
+ // a non-pre `buildApp` hook counts as such a claim even when it hasn't
3335
+ // built anything yet (its post-order hook may sort after ours):
3336
+ // building on its behalf would break staged orchestration (nitro
3337
+ // prerenders and copies public assets before its final server bundle)
3338
+ // and can error outright on environments the orchestrator knows to
3339
+ // skip (e.g. ones with no rollup input). Pre-order hooks don't count —
3340
+ // by convention they prepare (clean output dirs) rather than build.
3341
+ if (options.ssr) {
3342
+ let clientBuiltFirst = false;
3343
+ plugins.push({
3344
+ name: 'solid:client-build-first',
3345
+ apply: 'build',
3346
+ async buildApp(builder) {
3347
+ const client = builder.environments.client;
3348
+ if (!client || client.isBuilt) return;
3349
+ const clientBuild = client.config.build;
3350
+ const hasInput = !!clientBuild.rollupOptions?.input || existsSync(path.resolve(builder.config.root, 'index.html'));
3351
+ if (!clientBuild.manifest || !hasInput) return;
3352
+ await builder.build(client);
3353
+ clientBuiltFirst = true;
3354
+ }
3355
+ }, {
3356
+ name: 'solid:client-build-first/complete',
3357
+ apply: 'build',
3358
+ buildApp: {
3359
+ order: 'post',
3360
+ async handler(builder) {
3361
+ if (!clientBuiltFirst) return;
3362
+ // Another plugin declares its own (non-pre) buildApp hook — the
3363
+ // app build is spoken for, even if that hook sorts after this
3364
+ // one and hasn't run yet.
3365
+ const otherOrchestrator = builder.config.plugins.some(p => {
3366
+ if (!p.buildApp || p.name.startsWith('solid:client-build-first')) return false;
3367
+ return typeof p.buildApp !== 'object' || p.buildApp.order !== 'pre';
3368
+ });
3369
+ if (otherOrchestrator) return;
3370
+ const environments = Object.values(builder.environments);
3371
+ // A config-level orchestrator built something of its own — the
3372
+ // app build is spoken for, don't build environments it may have
3373
+ // skipped intentionally.
3374
+ if (environments.some(env => env.isBuilt && env.name !== 'client')) return;
3375
+ for (const environment of environments) {
3376
+ if (!environment.isBuilt) await builder.build(environment);
3377
+ }
3378
+ }
3379
+ }
3380
+ });
3381
+ }
3382
+ return plugins;
3383
+ }
3384
+
3385
+ export { solidPlugin as default, devStylePatch, serverFunctions };
3386
+ //# sourceMappingURL=index.mjs.map