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