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