-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathindex.ts
More file actions
1536 lines (1497 loc) · 70.2 KB
/
Copy pathindex.ts
File metadata and controls
1536 lines (1497 loc) · 70.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Start-mode serving for plain Vite apps: `solid({ start: {...} })` (or the
// zero-config sugar `start: true`) adds a serving layer with conventional
// entries so no hand-rolled wiring is needed, and the plugin's `ssr`
// boolean picks the mode — `ssr: true` server-renders the app per request;
// `ssr: false`/omitted is client mode (the same conventions, but the
// document shell is served/prerendered empty and the app `render()`s
// client-side). The flip between them is that one boolean.
//
// SSR mode (`start` + `ssr: true`):
// - Dev: runnable SSR environments are served by a Vite middleware. Provider-
// owned environments serve through `virtual:solid-ssr-handler` instead.
// Both paths inject the Vite client, dev style patch, and entry CSS as
// `<style data-vite-dev-id>` tags before the body can paint.
// - Prod: the plugin configures a full-app build (client + server bundles
// via the Vite environments/builder API — a single `vite build` builds
// both) whose server entry is `virtual:solid-ssr-handler`: an
// adapter-agnostic named `handleRequest(Request) => Promise<Response>` plus
// a default Fetchable `{ fetch(request) }` export. Both scope each request
// with `provideRequestEvent`, stream the render, and resolve hashed client
// assets through `virtual:solid-manifest`.
// - Entries are conventional with escape hatches: `src/entry-server.*` /
// `src/entry-client.*` are used when present (or set explicitly); when
// absent, default entries are generated from a single root component
// (`start.app`, defaulting to `src/App.*`) wrapped in a document shell
// (`start.document`, defaulting to `src/Document.*`, else a built-in one).
// - When `serverFunctions` is also enabled, the handler composes the
// endpoint on every surface; the runnable-dev server-function middleware
// pre-loads the referenced module, then dispatches through this handler.
// - Every dispatch runs under a stub-backed request event
// (`createRequestEvent`) with the optional `start.middleware` chain fronting
// it, and page responses go through the runtime's `createSSRResponse`
// head lifecycle (commit at shell flush, real pre-flush redirects, the
// script fallback post-flush).
// - `vite preview` serves dist/client statically and dispatches everything
// else through the built handler — the production path, middleware
// included, with no server file needed.
//
// Client mode (`start` without `ssr: true`) rides the same machinery with
// three deltas: the generated server entry renders the document shell
// WITHOUT the app (dev serving doubles as history fallback, and a
// post-build hook prerenders it once into dist/client/index.html), the
// generated client entry render()s instead of hydrating, and dist/server is
// dropped from the output unless `serverFunctions` needs it for the
// endpoint. Client code compiles non-hydratable, exactly like a plain SPA.
import { existsSync, rmSync, writeFileSync } from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
type DevEnvironment,
type FilterPattern,
normalizePath,
type Plugin,
type PreviewServer,
type ViteDevServer,
} from 'vite';
import { getEnvironmentConsumer, isRunnableEnvironment } from '../environment.js';
import {
DEVTOOLS_MOUNT_ID,
DEVTOOLS_PACKAGE,
devtoolsMountModuleCode,
} from '../devtools/index.js';
import { DIAGNOSTICS_CLIENT_ID } from '../diagnostics/index.js';
import {
collectDevStyles,
collectDevStyleSources,
type DevStyleFilter,
devStylePatch,
renderDevStyleTag,
} from '../dev-manifest.js';
import { joinBase, sendWebResponse, webRequestFromNode } from '../http.js';
/**
* Options for the main plugin's `start` option (`start: true` is
* sugar for the empty bag). One bag serves both modes — the plugin's `ssr`
* boolean picks between them, so flipping a project between
* client-rendered and server-rendered is toggling that boolean, never
* reshaping this object. Server-only options (`entryServer`, `external`)
* are documented no-ops in client mode: they stay in the config across a
* flip instead of erroring.
*/
export interface StartOptions {
/**
* Root component module for generated entries (the zero-config path).
* Resolved relative to the Vite root.
*
* @default "src/App.{tsx,jsx,ts,js}" (also probes lowercase "src/app.*")
*/
app?: string;
/** Options for development CSS crawling. */
css?: {
/**
* Filter for the modules traversed while collecting the CSS that dev
* SSR inlines into `<head>`. Patterns are
* [picomatch](https://github.com/micromatch/picomatch) globs or regexes;
* relative globs resolve against the Vite root. CSS files themselves
* and virtual modules always pass — the filter decides which module
* graphs are crawled, not which stylesheets are kept.
*
* `exclude` prunes matching graphs and defaults to `/node_modules/`
* (providing your own replaces the default). `include` opts matching
* files back in on top of that baseline — typically a package whose
* CSS should be server-inlined to avoid a development FOUC, e.g.
* `{ include: /node_modules\/some-ui-lib/ }`. A file matching both
* stays excluded. Development only: production CSS always comes from
* the built assets.
*/
filter?: {
include?: FilterPattern;
exclude?: FilterPattern;
};
};
/**
* Server entry module. Must export `render(request?, context?)` returning
* a `renderToStream` result, an HTML string, or a `Response`.
* `context.clientEntry` carries the resolved client entry URL.
*
* Server mode only — ignored in client mode, where the server entry is
* always generated (it renders the document shell without the app, for
* dev serving and the build-time prerender). Conventional
* `src/entry-server.*` files are likewise ignored there.
*
* @default "src/entry-server.{tsx,jsx,ts,js,mjs}" when present, else a
* generated entry rendering `<Document><App /></Document>`
*/
entryServer?: string;
/**
* Client entry module. In SSR mode it hydrates; in client mode it mounts
* (a generated one calls `render()`), and it stands alone — no pairing
* rule with a server entry.
*
* @default "src/entry-client.{tsx,jsx,ts,js,mjs}" when present, else a
* generated entry
*/
entryClient?: string;
/**
* Document shell component wrapping the app in generated entries. Receives
* `props.children` and must render the full `<html>` document including
* `<HydrationScript />` (in client mode, where nothing hydrates, the
* handler strips its output from the served/prerendered shell — a shared
* Document costs nothing across the flip; the built-in shell omits it per
* mode). Only used when the server entry is generated.
*
* @default "src/Document.{tsx,jsx}" when present, else a built-in shell
*/
document?: string;
/**
* Path to a server-only module (resolved relative to the Vite root) whose
* default export is one fetch-style middleware function — `(request,
* next) => Response | Promise<Response>` — or an array of them, composed
* in order. The chain fronts every request the plugin dispatches — page
* SSR, the server-function endpoint, dev and production, `vite preview` —
* and runs inside the request-event scope, so `getRequestEvent()` works
* exactly as it does in application code (decorate `locals`, write the
* `response` stub). `next()` advances the chain (pass a `Request` to
* substitute it downstream); nothing reaches the wire until the outermost
* middleware returns, so headers on the returned `Response` stay mutable
* after `next()` — streamed bodies included — and error middleware is a
* plain `try { return await next(); } catch { ... }`.
*
* All methods and accept types dispatch through the chain — API routes
* and no-JS form POSTs included, in dev exactly as in production. A
* non-page request (anything but an HTML-accepting GET) that no
* middleware handled falls back to Vite's own pipeline in dev instead of
* rendering the page at it.
*
* @default undefined
*/
middleware?: string;
/**
* Path to a server-only module (resolved relative to the Vite root) whose
* default export runs once per request in the generated server entry,
* after the middleware chain has dispatched to the page render and
* immediately before `renderToStream`: `(event, App) => Component | void |
* Promise<Component | void>`. The per-request seam for routers that must
* prepare an app instance before SSR begins (create a router bound to the
* request, `await router.load()`, then render): return a component and the
* generated entry renders it in the app's place inside the Document;
* return nothing and `<App />` renders unchanged. `event` is the shared
* request event — the same one the middleware chain decorated (`locals`
* are visible) — and the hook runs inside the request scope, so
* `getRequestEvent()` answers in anything it calls.
*
* Only meaningful with generated entries: an authored `entry-server`
* already owns its render function, so configuring both is an error.
* Server mode only — ignored in client mode (there is no per-request app
* render to prepare), so the config survives the `ssr` boolean flip.
*
* @default undefined
*/
setup?: string;
/**
* Typed, validated environment variables. A schema file — conventionally
* `env.ts` (or `env.js`) at the project root, probed automatically —
* default-exports `{ server?, client? }` maps of Standard Schema
* validators (zod, valibot, arktype, mixable per key), and the plugin
* exposes the validated values through `virtual:env/server` (all vars,
* server module graphs only — a client-graph import is a hard error) and
* `virtual:env/client` (the `VITE_`-prefixed `client` side; the prefix is
* enforced at config time). Validation runs at config/build time in node
* only against Vite's `loadEnv` merge of the `.env*` files (with
* `process.env` winning), which the plugin also folds into `process.env`
* itself — no `loadEnv` boilerplate in vite.config. Failures fail the
* build / render the dev error overlay with the per-key report, and a
* `solid-env.d.ts` is generated next to the schema so both virtual
* modules are fully typed by inference.
*
* Client values are baked as plain JSON (that's what `VITE_` means); no
* validator ships in a client bundle, and a client-build leak scan
* errors when a server value shows up in a client chunk. Server values
* are NOT baked: `virtual:env/server` reads `process.env` at server boot
* and validates through your schema (imported into the server bundle
* only), so platform-injected vars work and secrets rotate without a
* rebuild — no secret exists in any dist artifact. Build-time server
* failures are a deferred-to-boot warning; dev failures stay hard.
* Boot validation is synchronous — the generated module carries no
* top-level await, so server bundles work on non-esnext targets
* (Nitro's node-server preset needs no `esnext` override) — which is
* why async validators are rejected for `server` keys at config time
* (`client` keys may stay async: they are awaited at build time).
*
* `true` requires the conventional file (error when missing); a string
* is an explicit schema path; `false` disables even the probing.
* Env is a start-mode feature: without `start` there is no env layer.
*
* @default undefined (probe env.ts / env.js; off when absent)
*/
env?: boolean | string;
/**
* Enable the development toolbar. By default it is enabled when
* `@solidjs/start-devtools` is installed. Setting this to `true` requires
* the package, while `false` disables it.
*
* @default undefined
*/
devtools?: boolean;
/**
* Add the default production error boundary to generated entries.
* Disable this when application middleware owns error handling. Authored
* entries are unaffected.
*
* @default true
*/
errorBoundary?: boolean;
/**
* Let a host integration own the server environment — build wiring and
* HTTP serving alike. The plugin skips its start-mode server-build config and
* stands its dev middlewares down (SSR serving and the server-function
* endpoint); the generated `virtual:solid-ssr-handler` self-serves
* instead, inlining dev styles through a virtual module and composing the
* server-function endpoint. Its named `handleRequest(request)` and default
* Fetchable exports provide the same contract in dev and production.
* Generated entries and the client manifest are still provided.
*
* Often unnecessary: a provider-owned (non-runnable) `ssr` dev environment
* is detected automatically and the middlewares stand down on their own;
* the normal `ssr` environment also exposes the handler as an `index`
* service entry for provider build orchestrators. Set this only when the
* host does not adopt that environment — for example, when it uses a
* different name or independently configures the server build. To hand
* over only the server-function endpoint, use
* `serverFunctions.devMiddleware: false` instead.
*
* Server mode only — ignored in client mode (there is no server side to
* hand over; the shell prerender and, with `serverFunctions`, the
* endpoint handler are the whole story).
*
* @default false
*/
external?: boolean;
}
// Server-only start-mode request handler; also the server bundle's entry so a
// production server is one import away from `Request -> Response`. Exported
// for the main plugin to thread into the server-function dev middleware,
// which dispatches through it when SSR start mode is active (one middleware
// chain and one request event across both dispatch paths).
export const SSR_HANDLER_ID = 'virtual:solid-ssr-handler';
const HANDLER_ID = SSR_HANDLER_ID;
// Dev-only response marker: the generated dev handler answers non-page
// requests that fell through the whole middleware chain to the terminal
// page dispatch with a marked 404 instead of rendering HTML at them, and
// the dev middleware hands those back to Vite's pipeline. Production has no
// such seam — every unhandled request renders — but production also has no
// Vite pipeline to fall back to.
const DEV_FALLTHROUGH_HEADER = 'x-solid-dev-fallthrough';
// Private protocol between the two generated modules when `start.setup` is
// async: the entry hands the handler the renderToStream result under this
// key, because a promise resolving to the stream BARE would adopt the
// stream's thenable (which waits for the complete render) and buffer it.
const STREAM_BOX = '__solidSetupStream';
const DEV_STYLES_ID = 'virtual:solid-ssr-dev-styles';
const RESOLVED_DEV_STYLES_ID = '\0' + DEV_STYLES_ID;
// Generated default entries / document shell. The `.tsx` suffix routes them
// through the plugin's normal JSX transform (per-environment SSR/DOM
// compile), exactly like user-authored entry files.
const ENTRY_SERVER_ID = 'virtual:solid-ssr-entry-server.tsx';
const ENTRY_CLIENT_ID = 'virtual:solid-ssr-entry-client.tsx';
const DOCUMENT_ID = 'virtual:solid-ssr-document.tsx';
const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
const MANIFEST_ID = 'virtual:solid-manifest';
const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
const STORAGE_SOURCE = '@solidjs/web/storage';
const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs'];
const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx'];
function probe(root: string, stem: string, extensions: string[]): string | null {
for (const ext of extensions) {
if (existsSync(path.resolve(root, stem + ext))) return stem + ext;
}
return null;
}
/** Normalizes a user-supplied module path to a root-relative one (no leading slash). */
function normalizeUserPath(root: string, spec: string, option: string): string {
const absolute = path.isAbsolute(spec) ? spec : path.resolve(root, spec);
if (!existsSync(absolute)) {
throw new Error(`[@solidjs/vite-plugin] start.${option} does not exist: ${spec}`);
}
const relative = path.relative(root, absolute).split(path.sep).join('/');
if (relative.startsWith('..')) {
throw new Error(`[@solidjs/vite-plugin] start.${option} must live inside the Vite root: ${spec}`);
}
return relative;
}
interface ResolvedEntries {
/** Root-relative path or virtual id. */
entryServer: string;
/** Root-relative path or virtual id. */
entryClient: string;
/** Whether the entries are generated virtual modules. */
generated: boolean;
/** Absolute path of the app root component (generated entries only). */
app: string | null;
/** Absolute path of the document shell, or the built-in virtual id. */
document: string | null;
}
function resolveEntries(root: string, options: StartOptions, clientMode: boolean): ResolvedEntries {
const explicitClient = options.entryClient
? normalizeUserPath(root, options.entryClient, 'entryClient')
: null;
if (clientMode) {
// Client mode: the server entry is always generated (it renders the
// document shell only — no App — for dev serving and the build-time
// prerender); `start.entryServer` and conventional src/entry-server.*
// files are documented no-ops here, so a project flipping the `ssr`
// boolean never has to touch them. No entry pairing rule either: an
// authored client entry stands alone. The document resolves in every
// case because it IS the page in this mode.
const document = options.document
? normalizeUserPath(root, options.document, 'document')
: probe(root, 'src/Document', DOCUMENT_EXTENSIONS);
const entryClient = explicitClient ?? probe(root, 'src/entry-client', ENTRY_EXTENSIONS);
if (entryClient) {
return {
entryServer: ENTRY_SERVER_ID,
entryClient,
generated: false,
app: null,
document: document ? path.resolve(root, document) : null,
};
}
const app = options.app
? normalizeUserPath(root, options.app, 'app')
: (probe(root, 'src/App', APP_EXTENSIONS) ?? probe(root, 'src/app', APP_EXTENSIONS));
if (!app) {
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.`,
);
}
return {
entryServer: ENTRY_SERVER_ID,
entryClient: ENTRY_CLIENT_ID,
generated: true,
app: path.resolve(root, app),
document: document ? path.resolve(root, document) : null,
};
}
const explicitServer = options.entryServer
? normalizeUserPath(root, options.entryServer, 'entryServer')
: null;
const entryServer = explicitServer ?? probe(root, 'src/entry-server', ENTRY_EXTENSIONS);
const entryClient = explicitClient ?? probe(root, 'src/entry-client', ENTRY_EXTENSIONS);
if (entryServer && entryClient) {
return { entryServer, entryClient, generated: false, app: null, document: null };
}
if (entryServer || entryClient) {
// One authored entry with a generated counterpart is a hydration
// mismatch waiting to happen — the generated side wraps the app in the
// document shell, which the authored side knows nothing about.
const found = entryServer ? 'entry-server' : 'entry-client';
const missing = entryServer ? 'entry-client' : 'entry-server';
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).`,
);
}
const app = options.app
? normalizeUserPath(root, options.app, 'app')
: (probe(root, 'src/App', APP_EXTENSIONS) ?? probe(root, 'src/app', APP_EXTENSIONS));
if (!app) {
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.`,
);
}
const document = options.document
? normalizeUserPath(root, options.document, 'document')
: probe(root, 'src/Document', DOCUMENT_EXTENSIONS);
return {
entryServer: ENTRY_SERVER_ID,
entryClient: ENTRY_CLIENT_ID,
generated: true,
app: path.resolve(root, app),
document: document ? path.resolve(root, document) : null,
};
}
export function startServe(
options: StartOptions,
internal: {
serverFunctions?: boolean;
serverComponents?: boolean;
ssr?: boolean;
styleFilter?: DevStyleFilter;
diagnostics?: boolean;
} = {},
): Plugin[] {
// Client mode (the `start` option without `ssr: true`) rides this exact
// plugin with three deltas: the generated server entry renders the
// document shell WITHOUT the app (dev serving doubles as history
// fallback, and a post-build hook prerenders it once into
// dist/client/index.html), the generated client entry render()s instead
// of hydrating, and dist/server is dropped from the output unless
// `serverFunctions` needs it for the endpoint. Everything else — entry
// probing, the handler, middleware, dev styles, the manifest — is shared,
// which is what makes flipping a project between the modes a one-boolean
// config change.
const clientMode = !internal.ssr;
// Server components (`serverFunctions: { components: true }`): generated
// entries additionally emit the document-SSR wiring — the render plugin +
// direct-call transform server-side, the bootstrap script in <head>, and
// the client-side installServerComponents() call. Authored entries carry
// those pieces themselves (the endpoint response transform is installed by
// the server-function handler module either way). Everything is gated
// codegen: with the option off, none of these imports exist anywhere.
const serverComponents = !!internal.serverComponents;
const errorBoundary = options.errorBoundary !== false;
const styleFilter = internal.styleFilter;
const diagnostics = !!internal.diagnostics;
let devtoolsEnabled = false;
let devtoolsResolutions: Partial<
Record<'client' | 'server', Promise<string | null>>
> = {};
let devtoolsIds: Partial<Record<'client' | 'server', string | null>> = {};
// `external` is server-mode-only (documented no-op in client mode, so a
// host-integrated config survives the `ssr` boolean flip untouched).
const externalServer = !clientMode && !!options.external;
let root = process.cwd();
let base = '/';
let isBuild = false;
let entries: ResolvedEntries | undefined;
/** Absolute path of the user's middleware module, when configured. */
let middlewarePath: string | null = null;
/** Absolute path of the per-request setup module, when configured (server mode). */
let setupPath: string | null = null;
function requireEntries(): ResolvedEntries {
// config() always runs before resolveId/load/configureServer.
if (!entries) throw new Error('[@solidjs/vite-plugin] SSR entries not resolved yet');
return entries;
}
async function resolveDevtools(
resolve: (source: string, importer: string) => Promise<{ id: string } | null>,
importer: string,
consumer: 'client' | 'server',
): Promise<boolean> {
if (!devtoolsEnabled) return false;
// Detect from the app graph first (the documented install location), then
// from the plugin's own file: in pnpm-isolated apps a copy that is only a
// dependency of the plugin is not reachable from the app's importers. The
// resolved id is kept so imports from generated modules can use it.
devtoolsResolutions[consumer] ??= (async () => {
// Resolving from the plugin's own file never yields null when the
// package is absent: it is declared an optional peer dependency, so
// Vite answers with its `__vite-optional-peer-dep:` stub (an empty
// module). Treat that stub as "not installed".
const realId = (resolved: { id: string } | null) =>
resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null;
return (
realId(await resolve(DEVTOOLS_PACKAGE, importer)) ??
realId(await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url)))
);
})();
const id = await devtoolsResolutions[consumer];
devtoolsIds[consumer] = id;
if (!id && options.devtools === true) {
throw new Error(
'[@solidjs/vite-plugin] start.devtools requires @solidjs/start-devtools. ' +
'Install it as a development dependency or set start.devtools to false.',
);
}
return id !== null;
}
/**
* Cheap walk-up probe mirroring how the optimizer resolves bare
* `optimizeDeps.include` entries: is @solidjs/start-devtools reachable from
* this directory? Detection proper (resolveDevtools) runs later with a real
* importer; this only decides whether the toolbar graph can be pre-bundled
* at scan time.
*/
function devtoolsReachableFrom(dir: string): boolean {
for (let current = dir; ; ) {
if (existsSync(path.join(current, 'node_modules', DEVTOOLS_PACKAGE, 'package.json'))) {
return true;
}
const parent = path.dirname(current);
if (parent === current) return false;
current = parent;
}
}
/**
* The `optimizeDeps.include` spec that pre-bundles the toolbar graph, or
* null when it cannot be resolved at all. Pre-bundling it is not just a
* warm-start nicety: the toolbar hangs off virtual modules the scanner
* never crawls, so without an include the optimizer only discovers it on
* first request. That re-optimize can pair chunks from different passes
* whose shared minified exports disagree, taking down the whole client
* entry graph. The spec must therefore cover every install shape
* resolveDevtools accepts: bare when the app installs the package, and
* Vite's nested-include form (`plugin > dep`) when it is only a dependency
* of this plugin (pnpm-isolated installs).
*/
function devtoolsIncludeSpec(rootDir: string): string | null {
if (devtoolsReachableFrom(rootDir)) return DEVTOOLS_PACKAGE;
if (devtoolsReachableFrom(path.dirname(fileURLToPath(import.meta.url)))) {
return `@solidjs/vite-plugin > ${DEVTOOLS_PACKAGE}`;
}
return null;
}
/** Import specifier for generated code: absolute for files, id for virtuals. */
function entryServerSpec(): string {
const { entryServer } = requireEntries();
return entryServer === ENTRY_SERVER_ID ? entryServer : path.resolve(root, entryServer);
}
/** Browser URL of the client entry on the dev server (base applied). */
function devClientEntryUrl(): string {
const { entryClient } = requireEntries();
return entryClient === ENTRY_CLIENT_ID
? joinBase(base, '/@id/' + ENTRY_CLIENT_ID)
: joinBase(base, '/' + entryClient);
}
function documentSpec(): string {
const { document } = requireEntries();
return document ?? DOCUMENT_ID;
}
function styleRoots(): string[] {
const { generated, app, document, entryServer, entryClient } = requireEntries();
if (clientMode) {
// The app graph's CSS is inlined into the dev shell too (not just the
// document's): the client injects it again when the modules load and
// the dev style patch dedupes, so this is pure anti-flash.
return [
generated ? app! : path.resolve(root, entryClient),
...(document ? [document] : []),
];
}
return generated ? [app!, ...(document ? [document] : [])] : [path.resolve(root, entryServer)];
}
async function devStylesModuleCode(
environment: DevEnvironment,
watchFile: (file: string) => void,
): Promise<string> {
const styles = await collectDevStyleSources(
environment,
styleRoots(),
watchFile,
styleFilter,
);
if (!styles.length) return `export default '';`;
const imports = styles.map((style, index) => {
const specifier = style.url.includes('?') ? `${style.url}&inline` : `${style.url}?inline`;
return `import css${index} from ${JSON.stringify(specifier)};`;
});
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');
}
function errorBoundaryImport(): string[] {
return isBuild && errorBoundary
? [`import { DefaultErrorBoundary } from ${JSON.stringify(ERROR_BOUNDARY_ID)};`]
: [];
}
function documentTree(root: string, wrapper?: string): string[] {
const content = wrapper ? `<${wrapper}><${root} /></${wrapper}>` : `<${root} />`;
return isBuild && errorBoundary
? [
` <DefaultErrorBoundary>`,
` <Document>`,
` <DefaultErrorBoundary>`,
` ${content}`,
` </DefaultErrorBoundary>`,
` </Document>`,
` </DefaultErrorBoundary>`,
]
: [` <Document>`, ` ${content}`, ` </Document>`];
}
function generatedEntryServerCode(toolbar: boolean): string {
if (clientMode) {
// The client-mode shell: the document without the app. Rendered per
// request in dev (any HTML GET gets it — history-fallback semantics)
// and once at build time into dist/client/index.html. The client
// entry script is injected by the handler, exactly like SSR mode.
return [
`import { renderToStream } from '@solidjs/web';`,
`import manifest from ${JSON.stringify(MANIFEST_ID)};`,
`import Document from ${JSON.stringify(documentSpec())};`,
...errorBoundaryImport(),
``,
`export function render(request, context) {`,
` return renderToStream(() => (`,
...(isBuild && errorBoundary
? [
` <DefaultErrorBoundary>`,
` <Document />`,
` </DefaultErrorBoundary>`,
]
: [` <Document />`]),
` ), { manifest });`,
`}`,
].join('\n');
}
const { app } = requireEntries();
const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;
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)};`,
...(toolbar ? [`import { DevToolbar } from ${JSON.stringify(DEVTOOLS_PACKAGE)};`] : []),
...errorBoundaryImport(),
...(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
? [
// Direct (in-process) server-function calls made during document
// SSR must resolve to inline-renderable components; the endpoint
// response transform is installed separately by the
// server-function handler module (configure calls merge per key).
`configureServerFunctionsServer({ transformDirectResult: frameTransformDirectResult });`,
``,
]
: []),
...(setupPath
? [
// The per-request seam: the hook sees the same event the
// middleware chain decorated and finishes before renderToStream
// starts. When it is async, the stream must NOT cross the
// promise boundary bare — a promise resolving to a
// renderToStream result adopts its thenable (which waits for
// the *complete* render) and buffers the stream — so it crosses
// boxed under a private key the generated handler unboxes
// (both modules are ours).
`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(() => (`,
...documentTree('Root', toolbar ? 'DevToolbar' : undefined),
` ), ${streamOptions});`,
`}`,
]
: [
`export function render(request, context) {`,
` return renderToStream(() => (`,
...documentTree('App', toolbar ? 'DevToolbar' : undefined),
` ), ${streamOptions});`,
`}`,
]),
].join('\n');
}
function generatedEntryClientCode(toolbar: boolean): string {
const { app } = requireEntries();
// Dev-only: the diagnostics bridge fronts dev-mode channels, so builds
// never see this import (mirrors the plugin's own serve-only `apply`).
const diagnosticsImport =
diagnostics && !isBuild ? [`import ${JSON.stringify(DIAGNOSTICS_CLIENT_ID)};`] : [];
if (clientMode) {
// render(), not hydrate(): the shell's body is empty, the app mounts
// fresh. Client code compiles non-hydratable in client mode, so the
// app cannot claim server DOM anyway. The entry script is injected
// without `async` (plain module = deferred), so document.body is
// complete when this runs.
return [
...diagnosticsImport,
`import { render } from '@solidjs/web';`,
...errorBoundaryImport(),
...(toolbar ? [`import { DevToolbar } from ${JSON.stringify(DEVTOOLS_PACKAGE)};`] : []),
`import App from ${JSON.stringify(app)};`,
``,
`render(() => ${
isBuild && errorBoundary
? '<DefaultErrorBoundary><App /></DefaultErrorBoundary>'
: toolbar
? '<DevToolbar><App /></DevToolbar>'
: '<App />'
}, document.body);`,
].join('\n');
}
return [
...diagnosticsImport,
`import { hydrate } from '@solidjs/web';`,
...(toolbar ? [`import { DevToolbar } from ${JSON.stringify(DEVTOOLS_PACKAGE)};`] : []),
...(serverComponents
? [`import { installServerComponents } from '@solidjs/web/frames';`]
: []),
...errorBoundaryImport(),
`import Document from ${JSON.stringify(documentSpec())};`,
`import App from ${JSON.stringify(app)};`,
``,
...(serverComponents
? [
// Installs the t=0 document-adoption registry and the transport
// policy (component responses morph their boundary instead of
// decoding as data). Must run before hydrate().
`installServerComponents();`,
``,
]
: []),
`hydrate(() => (`,
...documentTree('App', toolbar ? 'DevToolbar' : undefined),
`), document);`,
].join('\n');
}
// Built-in document shell: minimal, hydration-ready. The client entry
// script is injected into <head> by the handler (not rendered here) so its
// URL never has to survive hydration or a manifest lookup client-side.
// The client-mode variant drops <HydrationScript /> — nothing hydrates,
// so the shell stays inert HTML. (A user-authored Document carrying
// HydrationScript is covered too: the handler strips the event-capture
// script from the client-mode shell.)
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');
const errorBoundaryCode = [
`import { Errored } from 'solid-js';`,
`import { httpStatus, isServer } from '@solidjs/web';`,
``,
`function ErrorFallback(props) {`,
` console.error(props.error());`,
` httpStatus(500);`,
` return (`,
` <span style="font-size:1.5em;text-align:center;position:fixed;left:0;bottom:55%;width:100%">`,
` {isServer ? '500 | Internal Server Error' : 'Error | Uncaught Client Exception'}`,
` </span>`,
` );`,
`}`,
``,
`export function DefaultErrorBoundary(props) {`,
` return (`,
` <Errored fallback={(error) => <ErrorFallback error={error} />}>`,
` {props.children}`,
` </Errored>`,
` );`,
`}`,
].join('\n');
// The handler module: dev and prod share the render/response plumbing;
// they differ in how the client entry URL is known (baked dev URL vs a
// manifest scan) and what gets injected into <head> (Vite client + style
// patch in dev). The response-head lifecycle is the runtime's
// (`createRequestEvent`/`createSSRResponse`/`commitEventResponse` from
// @solidjs/web): every request runs under a stub-backed event,
// `httpStatus`/`httpHeader` writes land on the wire at shell flush, a
// pre-flush redirect becomes a real 3xx and a post-flush one the script
// fallback, and a Response that skipped the render lifecycle (middleware
// early return, raw entry.render Response, server functions) has the
// stub folded on at the handler edge after the middleware chain fully
// unwinds. When
// `serverFunctions` is enabled the endpoint is dispatched here on every
// surface (the runnable-dev middleware routes through this module), so
// user middleware and the shared request event front it identically.
function handlerModuleCode(externalDev: boolean): string {
const { generated, entryClient } = requireEntries();
const composeServerFunctions = internal.serverFunctions;
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)};`,
]
: []),
];
if (isBuild) {
lines.push(`import manifest from ${JSON.stringify(MANIFEST_ID)};`);
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;`,
// The plugin's manifest module normalizes lazy facade chunks
// (isDynamicEntry) so exactly one real entry remains flagged.
` for (const key in manifest) {`,
` const chunk = manifest[key];`,
` if (chunk && chunk.isEntry && chunk.file) {`,
` clientEntryUrl = joinAssetPath(manifest._base, chunk.file);`,
` break;`,
` }`,
` }`,
` return clientEntryUrl;`,
`}`,
);
} else {
const devHead =
`<script>${devStylePatch}</script>` +
`<script type="module" src="${joinBase(base, '/@vite/client')}"></script>`;
lines.push(``, `const DEV_HEAD = ${JSON.stringify(devHead)};`);
}
// Middleware: the user module default-exports one fetch-style function
// or an array, composed in order. Without one, the chain degenerates to
// the terminal dispatch.
lines.push(``);
if (middlewarePath) {
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);`,
);
} else {
lines.push(`const runMiddleware = (request, next) => next(request);`);
}
// No `_$SC` bootstrap injection: the runtime's serialized
// server-component references self-bootstrap the registry (each
// hydration script's first reference carries it as an idempotent
// expression), so nothing needs to precede the data scripts. The old
// head-open splice actively broke hydration — a script ahead of the
// authored <head> elements claims as the first walked child and drifts
// every positional claim after it.
lines.push(
``,
`function escapeAttribute(value) {`,
` return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');`,
`}`,
``,
`function createHtmlChunkTransform(clientEntry, extraHead, nonce) {`,
` const nonceAttr = nonce ? ' nonce="' + escapeAttribute(nonce) + '"' : '';`,
` let first = true;`,
` let injected = false;`,
` return (chunk) => {`,
);
if (!generated) {
// Authored entries reference the client entry by its dev path (the
// `<script src="/src/entry-client.tsx">` convention); rewrite it to
// the resolved URL like the classic server harnesses do.
lines.push(
` if (clientEntry && chunk.includes(${JSON.stringify('/' + entryClient)})) {`,
` chunk = chunk.split(${JSON.stringify('/' + entryClient)}).join(clientEntry);`,
` }`,
);
}
lines.push(` if (!injected && chunk.includes('</head>')) {`, ` injected = true;`);
if (clientMode) {
// Nothing hydrates in client mode, so the event-capture bootstrap
// `<HydrationScript />` renders (`window._$HY||...`) is dead weight —
// but a Document shared with SSR mode carries it by design (the flip
// story). Strip it from the shell here instead of making users fork
// their Document per mode. (`<!--xs-->` is the script's stream
// marker; the shell head always arrives in one chunk.)
lines.push(
` chunk = chunk.replace(/<script(?:\\s[^>]*)?>window\\._\\$HY\\|\\|[\\s\\S]*?<\\/script>(?:<!--xs-->)?/, '');`,
);
}
const headParts: string[] = [];
// Dev: the style patch + Vite client, then either middleware-provided
// styles or the external environment's HMR-tracked virtual styles module.
if (!isBuild) {
headParts.push(
`DEV_HEAD`,
externalDev
? `(extraHead === undefined ? DEV_STYLES_HEAD : extraHead)`
: `(extraHead || '')`,
);
}
if (generated || clientMode) {
// Client-mode note: the shell never references its client entry
// itself (even an authored one — the Document knows nothing about
// entries), so the handler always injects it. Without `async`: module
// scripts default to deferred execution, which is exactly right for a
// fresh render-into-body mount (hydration, by contrast, wants to
// start as early as possible).
headParts.push(
`(clientEntry ? '<script type="module"' + nonceAttr + ' src="' + clientEntry + '"${clientMode ? '' : ' async'}></' + 'script>' : '')`,
);
}
if (headParts.length) {
lines.push(` chunk = chunk.replace('</head>', ${headParts.join(' + ')} + '</head>');`);
}
lines.push(
` }`,
` if (first) { first = false; chunk = '<!DOCTYPE html>' + chunk; }`,
` return chunk;`,
` };`,
`}`,
);
// The handler-edge commit fold — the runtime's `commitEventResponse`
// (named import above), the second of the response lifecycle's two
// exits: page results leave through `createSSRResponse`, any other
// Response (a middleware early return, a raw Response from
// entry.render, a server-function response) leaves through
// `commitEventResponse`, which folds the event's response stub onto it
// (cookies append entry-by-entry, other headers gap-fill, status stays
// the response's own) and commits the stub. Committed stubs pass