solidstep 0.3.4 → 0.4.0
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 +1094 -829
- package/index.d.ts.map +1 -1
- package/index.js +20 -3
- package/package.json +143 -72
- package/server.d.ts.map +1 -1
- package/server.js +306 -216
- package/utils/cache.d.ts.map +1 -1
- package/utils/cache.js +8 -19
- package/utils/components/form.d.ts +47 -0
- package/utils/components/form.d.ts.map +1 -0
- package/utils/components/form.js +95 -0
- package/utils/csp.d.ts.map +1 -1
- package/utils/csp.js +0 -2
- package/utils/hooks/action-state.d.ts +19 -1
- package/utils/hooks/action-state.d.ts.map +1 -1
- package/utils/hooks/action-state.js +24 -4
- package/utils/hooks/form-status.d.ts +12 -0
- package/utils/hooks/form-status.d.ts.map +1 -0
- package/utils/hooks/form-status.js +16 -0
- package/utils/instrumentation-noop.d.ts +3 -0
- package/utils/instrumentation-noop.d.ts.map +1 -0
- package/utils/instrumentation-noop.js +3 -0
- package/utils/instrumentation.d.ts +94 -0
- package/utils/instrumentation.d.ts.map +1 -0
- package/utils/instrumentation.js +132 -0
- package/utils/loader.d.ts.map +1 -1
- package/utils/loader.js +5 -11
- package/utils/middleware.d.ts +43 -0
- package/utils/middleware.d.ts.map +1 -0
- package/utils/middleware.js +48 -0
- package/utils/path-router.js +10 -10
- package/utils/router.d.ts.map +1 -1
- package/utils/router.js +3 -1
- package/utils/server-action.server.d.ts.map +1 -1
- package/utils/server-action.server.js +56 -22
package/README.md
CHANGED
|
@@ -1,829 +1,1094 @@
|
|
|
1
|
-
# SolidStep
|
|
2
|
-
|
|
3
|
-
Next Solid Step towards a more performant web - A full-stack SolidJS framework for building modern web applications with file-based routing, SSR, and built-in security.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🌟 **Built on SolidJS and Vite** - Leverage the power of SolidJS for reactive and efficient UIs
|
|
8
|
-
- 🚀 **File-based Routing** - Automatic routing based on your file structure
|
|
9
|
-
- ⚡ **Server-Side Rendering (SSR)** - Fast initial page loads with full SSR support
|
|
10
|
-
- 🔄 **Data Loading** - Built-in loaders for efficient data fetching
|
|
11
|
-
- 🎨 **Layouts & Groups** - Nested layouts and parallel route groups
|
|
12
|
-
- 🛡️ **Security First** - Built-in CSP, CORS, CSRF, and cookie utilities
|
|
13
|
-
- 🎯 **Server Actions** - Type-safe server functions with automatic serialization
|
|
14
|
-
- ⚙️ **Middleware Support** - Request/response interceptors
|
|
15
|
-
- 📦 **Caching** - Built-in page-level caching
|
|
16
|
-
- 📝 **TypeScript** - Full TypeScript support out of the box
|
|
17
|
-
- 📊 **Built-in Logging** - Configurable Pino logger for logging
|
|
18
|
-
- 🌐 **Fetch Utilities** - Type-safe fetch wrappers with timeout and error handling for both client and server
|
|
19
|
-
|
|
20
|
-
## Getting Started
|
|
21
|
-
|
|
22
|
-
### Create a New Project
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
[npx | yarn dlx | pnpm dlx | bunx] @varlabs/create-solidstep@latest my-app
|
|
26
|
-
cd my-app
|
|
27
|
-
[npm | yarn | pnpm | bun] install
|
|
28
|
-
[npm | yarn | pnpm | bun] run dev
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Special Files
|
|
32
|
-
|
|
33
|
-
- `page.tsx` - Page component
|
|
34
|
-
- `layout.tsx` - Layout wrapper
|
|
35
|
-
- `loading.tsx` - Loading state (Streaming - optional)
|
|
36
|
-
- `error.tsx` - Error boundary (optional)
|
|
37
|
-
- `not-found.tsx` - 404 page (root only - optional)
|
|
38
|
-
- `route.ts` - API route handler
|
|
39
|
-
- `middleware.ts` - Request middleware
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
import
|
|
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
|
-
import {
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
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
|
-
<div>{props.slots.
|
|
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
|
-
```tsx
|
|
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
|
-
return {
|
|
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
|
-
|
|
1
|
+
# SolidStep
|
|
2
|
+
|
|
3
|
+
Next Solid Step towards a more performant web - A full-stack SolidJS framework for building modern web applications with file-based routing, SSR, and built-in security.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🌟 **Built on SolidJS and Vite** - Leverage the power of SolidJS for reactive and efficient UIs
|
|
8
|
+
- 🚀 **File-based Routing** - Automatic routing based on your file structure
|
|
9
|
+
- ⚡ **Server-Side Rendering (SSR)** - Fast initial page loads with full SSR support
|
|
10
|
+
- 🔄 **Data Loading** - Built-in loaders for efficient data fetching
|
|
11
|
+
- 🎨 **Layouts & Groups** - Nested layouts and parallel route groups
|
|
12
|
+
- 🛡️ **Security First** - Built-in CSP, CORS, CSRF, and cookie utilities
|
|
13
|
+
- 🎯 **Server Actions** - Type-safe server functions with automatic serialization
|
|
14
|
+
- ⚙️ **Middleware Support** - Request/response interceptors
|
|
15
|
+
- 📦 **Caching** - Built-in page-level caching
|
|
16
|
+
- 📝 **TypeScript** - Full TypeScript support out of the box
|
|
17
|
+
- 📊 **Built-in Logging** - Configurable Pino logger for logging
|
|
18
|
+
- 🌐 **Fetch Utilities** - Type-safe fetch wrappers with timeout and error handling for both client and server
|
|
19
|
+
|
|
20
|
+
## Getting Started
|
|
21
|
+
|
|
22
|
+
### Create a New Project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
[npx | yarn dlx | pnpm dlx | bunx] @varlabs/create-solidstep@latest my-app
|
|
26
|
+
cd my-app
|
|
27
|
+
[npm | yarn | pnpm | bun] install
|
|
28
|
+
[npm | yarn | pnpm | bun] run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Special Files
|
|
32
|
+
|
|
33
|
+
- `page.tsx` - Page component
|
|
34
|
+
- `layout.tsx` - Layout wrapper
|
|
35
|
+
- `loading.tsx` - Loading state (Streaming - optional)
|
|
36
|
+
- `error.tsx` - Error boundary (optional)
|
|
37
|
+
- `not-found.tsx` - 404 page (root only - optional)
|
|
38
|
+
- `route.ts` - API route handler
|
|
39
|
+
- `middleware.ts` - Request middleware
|
|
40
|
+
- `instrumentation.ts` - Server instrumentation hooks (optional)
|
|
41
|
+
|
|
42
|
+
**A route is defined by either the presence of a `page.tsx` or `route.ts` file in a directory.**
|
|
43
|
+
|
|
44
|
+
**Similar to NextJS, routes are not indexed if they have a '_' placed at the beginning of the name**
|
|
45
|
+
|
|
46
|
+
### Configuration
|
|
47
|
+
|
|
48
|
+
Configure your app in `app.config.ts`:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { defineConfig } from 'solidstep';
|
|
52
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
server: {
|
|
56
|
+
preset: 'node',
|
|
57
|
+
},
|
|
58
|
+
plugins: [
|
|
59
|
+
{
|
|
60
|
+
type: 'client', // or 'server' or 'both' - depends on where you want to use the plugin
|
|
61
|
+
plugin: tailwindcss()
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Vite Configuration
|
|
68
|
+
|
|
69
|
+
You can customize Vite settings for both client and server builds.
|
|
70
|
+
|
|
71
|
+
__When trying to configure absolute path imports__
|
|
72
|
+
1. Add the path alias in tsconfig.json (for TypeScript support):
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"compilerOptions": {
|
|
76
|
+
"baseUrl": ".",
|
|
77
|
+
"paths": {
|
|
78
|
+
"@/*": ["./*"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. Then add the same alias in the Vite config inside `app.config.ts` to ensure it works during build and runtime:
|
|
85
|
+
```tsx
|
|
86
|
+
import { defineConfig } from 'solidstep';
|
|
87
|
+
import { resolve } from 'node:path';
|
|
88
|
+
import { dirname } from 'node:path';
|
|
89
|
+
import { fileURLToPath } from 'node:url';
|
|
90
|
+
|
|
91
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
server: {
|
|
95
|
+
preset: 'node',
|
|
96
|
+
},
|
|
97
|
+
vite: {
|
|
98
|
+
resolve: {
|
|
99
|
+
alias: {
|
|
100
|
+
'@': resolve(__dirname, '.'),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Project Structure
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
my-app/
|
|
111
|
+
├── app/
|
|
112
|
+
│ ├── page.tsx # Home page (/)
|
|
113
|
+
│ ├── layout.tsx # Root layout
|
|
114
|
+
│ ├── middleware.ts # Request middleware
|
|
115
|
+
│ ├── about/
|
|
116
|
+
│ │ └── page.tsx # About page (/about)
|
|
117
|
+
│ ├── (admin)/
|
|
118
|
+
│ | └── dashboard/
|
|
119
|
+
│ | └── page.tsx # Group route (/dashboard)
|
|
120
|
+
│ └── blog/
|
|
121
|
+
│ ├── layout.tsx # Blog layout
|
|
122
|
+
│ ├── page.tsx # Blog index (/blog)
|
|
123
|
+
│ └── [slug]/
|
|
124
|
+
│ └── page.tsx # Dynamic route (/blog/:slug)
|
|
125
|
+
├── public/
|
|
126
|
+
│ └── favicon.ico
|
|
127
|
+
├── app.config.ts
|
|
128
|
+
└── package.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Core Concepts
|
|
132
|
+
|
|
133
|
+
### Layouts
|
|
134
|
+
|
|
135
|
+
Wrap multiple pages with shared UI:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
export default function BlogLayout(props: { children: any }) {
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<nav>Blog Navigation</nav>
|
|
142
|
+
{props.children()}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Pages
|
|
149
|
+
|
|
150
|
+
Create a `page.tsx` file in any directory under `app/` to define a route:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
export default function HomePage() {
|
|
154
|
+
return <h1>Welcome to SolidStep!</h1>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Similar to NextJS, only content returned by a `page` or `route` is sent to the client**
|
|
159
|
+
|
|
160
|
+
### Group Routes
|
|
161
|
+
Use parentheses to group routes without affecting the URL:
|
|
162
|
+
|
|
163
|
+
```app/
|
|
164
|
+
├── (admin)/
|
|
165
|
+
│ └── dashboard/
|
|
166
|
+
│ └── page.tsx // matches /dashboard
|
|
167
|
+
└── (user)/
|
|
168
|
+
└── profile/
|
|
169
|
+
└── page.tsx // matches /profile
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Dynamic Routes
|
|
173
|
+
|
|
174
|
+
Use square brackets for dynamic segments:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// app/blog/[slug]/page.tsx - matches /blog/my-post, /blog/another-post, etc.
|
|
178
|
+
|
|
179
|
+
export default function BlogPost(props: { routeParams: { slug: string } }) {
|
|
180
|
+
return <h1>Post: {props.routeParams.slug}</h1>;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Catch-all routes:**
|
|
185
|
+
```tsx
|
|
186
|
+
// app/docs/[...path]/page.tsx - matches /docs/a, /docs/a/b, etc.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Catch-all routes (Optional):**
|
|
190
|
+
```tsx
|
|
191
|
+
// app/docs/[[...path]]/page.tsx - matches /docs, /docs/a, /docs/a/b, etc.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Parallel Routes (Groups)
|
|
195
|
+
|
|
196
|
+
Render multiple sections simultaneously:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
app/
|
|
200
|
+
├── layout.tsx
|
|
201
|
+
├── page.tsx
|
|
202
|
+
└── @graph1/
|
|
203
|
+
└── page.tsx
|
|
204
|
+
└── @graph2/
|
|
205
|
+
└── page.tsx
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
export default function RootLayout(props: {
|
|
210
|
+
children: any;
|
|
211
|
+
slots: { graph1: any; graph2: any; };
|
|
212
|
+
}) {
|
|
213
|
+
return (
|
|
214
|
+
<main>
|
|
215
|
+
{props.children()}
|
|
216
|
+
<aside>
|
|
217
|
+
<div>{props.slots.graph1()}</div>
|
|
218
|
+
<div>{props.slots.graph2()}</div>
|
|
219
|
+
</aside>
|
|
220
|
+
</main>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Data Loading
|
|
226
|
+
|
|
227
|
+
Use `defineLoader` to fetch data on the server:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
|
|
231
|
+
|
|
232
|
+
export const loader = defineLoader(async (request) => {
|
|
233
|
+
const posts = await fetchPosts();
|
|
234
|
+
return { posts };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
type LoaderData = LoaderDataFromFunction<typeof loader>;
|
|
238
|
+
|
|
239
|
+
export default function BlogPage(props: { loaderData: LoaderData }) {
|
|
240
|
+
return (
|
|
241
|
+
<ul>
|
|
242
|
+
<For each={props.loaderData.posts}>
|
|
243
|
+
{(post) => <li>{post.title}</li>}
|
|
244
|
+
</For>
|
|
245
|
+
</ul>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Server Actions
|
|
251
|
+
|
|
252
|
+
Create type-safe server functions:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
'use server';
|
|
256
|
+
|
|
257
|
+
export const createPost = async (data: { title: string }) => {
|
|
258
|
+
await db.posts.create(data);
|
|
259
|
+
return { success: true };
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Call from client:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { createPost } from './actions';
|
|
267
|
+
|
|
268
|
+
function CreatePostForm() {
|
|
269
|
+
const handleSubmit = async (e: Event) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
await createPost({ title: 'My Post' });
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return <form onSubmit={handleSubmit}>...</form>;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Form Actions
|
|
279
|
+
|
|
280
|
+
Use the `<Form>` component to submit forms via server actions — similar to Next.js form handling:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// app/invoices/actions.ts
|
|
284
|
+
'use server';
|
|
285
|
+
|
|
286
|
+
export async function createInvoice(formData: FormData) {
|
|
287
|
+
const rawFormData = {
|
|
288
|
+
customerId: formData.get('customerId'),
|
|
289
|
+
amount: formData.get('amount'),
|
|
290
|
+
status: formData.get('status'),
|
|
291
|
+
};
|
|
292
|
+
// mutate data, revalidate cache
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// app/invoices/page.tsx
|
|
298
|
+
import { Form } from 'solidstep/form';
|
|
299
|
+
import { createInvoice } from './actions';
|
|
300
|
+
|
|
301
|
+
export default function Page() {
|
|
302
|
+
return (
|
|
303
|
+
<Form action={createInvoice}>
|
|
304
|
+
<input name="customerId" />
|
|
305
|
+
<input name="amount" type="number" />
|
|
306
|
+
<select name="status">
|
|
307
|
+
<option value="pending">Pending</option>
|
|
308
|
+
<option value="paid">Paid</option>
|
|
309
|
+
</select>
|
|
310
|
+
<button type="submit">Create Invoice</button>
|
|
311
|
+
</Form>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Passing additional arguments with `bind`:**
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import { Form } from 'solidstep/form';
|
|
320
|
+
import { updateUser } from './actions';
|
|
321
|
+
|
|
322
|
+
export function UserProfile(props: { userId: string }) {
|
|
323
|
+
const updateUserWithId = updateUser.bind(null, props.userId);
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<Form action={updateUserWithId}>
|
|
327
|
+
<input type="text" name="name" />
|
|
328
|
+
<button type="submit">Update User Name</button>
|
|
329
|
+
</Form>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Form validation with `useActionState`:**
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useActionState } from 'solidstep/hooks/action-state';
|
|
338
|
+
import { Form } from 'solidstep/form';
|
|
339
|
+
import { signup } from './actions';
|
|
340
|
+
|
|
341
|
+
export function SignupForm() {
|
|
342
|
+
const [state, formAction, pending, error] = useActionState(signup, {
|
|
343
|
+
errors: {} as Record<string, string[]>,
|
|
344
|
+
message: '',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<Form action={formAction}>
|
|
349
|
+
<label for="email">Email</label>
|
|
350
|
+
<input type="email" id="email" name="email" required />
|
|
351
|
+
{state().errors.email && <span>{state().errors.email[0]}</span>}
|
|
352
|
+
<p aria-live="polite">{state().message}</p>
|
|
353
|
+
{error() && <p role="alert" style="color:red">{error()!.message}</p>}
|
|
354
|
+
<button disabled={pending()}>
|
|
355
|
+
{pending() ? 'Signing up...' : 'Sign up'}
|
|
356
|
+
</button>
|
|
357
|
+
</Form>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Pending state with `useFormStatus`:**
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { useFormStatus } from 'solidstep/hooks/form-status';
|
|
366
|
+
|
|
367
|
+
export function SubmitButton() {
|
|
368
|
+
const { pending } = useFormStatus();
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<button disabled={pending()} type="submit">
|
|
372
|
+
{pending() ? 'Submitting...' : 'Submit'}
|
|
373
|
+
</button>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
> **Good to know:**
|
|
379
|
+
> - `<Form>` supports progressive enhancement — when JS is disabled, forms submit natively to the server action endpoint.
|
|
380
|
+
> - `useActionState` returns SolidJS accessors: call `state()`, `pending()`, and `error()` to read values. `error()` is `null` until the action throws, and resets to `null` on the next submission.
|
|
381
|
+
> - `useFormStatus` must be used in a component nested inside `<Form>`.
|
|
382
|
+
|
|
383
|
+
### Metadata
|
|
384
|
+
|
|
385
|
+
Define metadata for SEO:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { meta } from 'solidstep/utils/types';
|
|
389
|
+
|
|
390
|
+
// can also be async
|
|
391
|
+
export const generateMeta = meta(() => {
|
|
392
|
+
return {
|
|
393
|
+
title: {
|
|
394
|
+
type: 'title',
|
|
395
|
+
content: 'My Site',
|
|
396
|
+
attributes: {},
|
|
397
|
+
},
|
|
398
|
+
description: {
|
|
399
|
+
type: 'meta',
|
|
400
|
+
attributes: {
|
|
401
|
+
name: 'description',
|
|
402
|
+
content: 'My awesome site',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
// manifest
|
|
406
|
+
manifest: {
|
|
407
|
+
type: 'link',
|
|
408
|
+
attributes: {
|
|
409
|
+
rel: 'manifest',
|
|
410
|
+
href: '/site.webmanifest',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
// google fonts
|
|
414
|
+
'google-font-link': {
|
|
415
|
+
type: 'link',
|
|
416
|
+
attributes: {
|
|
417
|
+
rel: 'preconnect',
|
|
418
|
+
href: 'https://fonts.googleapis.com'
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
'gstatic-font-link': {
|
|
422
|
+
type: 'link',
|
|
423
|
+
attributes: {
|
|
424
|
+
rel: 'preconnect',
|
|
425
|
+
href: 'https://fonts.gstatic.com',
|
|
426
|
+
crossorigin: ''
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
'inter-font': {
|
|
430
|
+
type: 'link',
|
|
431
|
+
attributes: {
|
|
432
|
+
rel: 'stylesheet',
|
|
433
|
+
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
// external js
|
|
437
|
+
'analytics-script': {
|
|
438
|
+
type: 'script',
|
|
439
|
+
attributes: {
|
|
440
|
+
src: 'analytics.js',
|
|
441
|
+
defer: true,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Middleware
|
|
449
|
+
|
|
450
|
+
Intercept and modify requests. SolidStep's `defineMiddleware` composes an ordered array of middleware units into a single handler, so you can keep concerns (auth, CORS, CSRF, logging) in separate, reusable pieces:
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
// app/middleware.ts
|
|
454
|
+
import { defineMiddleware, type Middleware } from 'solidstep/utils/middleware';
|
|
455
|
+
|
|
456
|
+
const logger: Middleware = {
|
|
457
|
+
onRequest: (event) => {
|
|
458
|
+
console.log('Incoming request:', event.path);
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const auth: Middleware = {
|
|
463
|
+
onRequest: (event) => {
|
|
464
|
+
if (!event.headers.get('authorization')) {
|
|
465
|
+
// Return a Response to short-circuit — later middleware and the
|
|
466
|
+
// route handler are skipped.
|
|
467
|
+
return new Response('Unauthorized', { status: 401 });
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
export default defineMiddleware([logger, auth]);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- `onRequest` hooks run in array order and stop as soon as one returns a `Response` (or calls `event.respondWith(...)`).
|
|
476
|
+
- `onBeforeResponse` hooks always all run, in array order, and receive the resolved response so it can be inspected or mutated.
|
|
477
|
+
|
|
478
|
+
You can still use Vinxi's own single-object form from `vinxi/http` if you prefer:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
import { defineMiddleware } from 'vinxi/http';
|
|
482
|
+
|
|
483
|
+
export default defineMiddleware({
|
|
484
|
+
onRequest: (event) => {
|
|
485
|
+
console.log('Incoming request:', event.path);
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Instrumentation
|
|
491
|
+
|
|
492
|
+
SolidStep provides a server-side instrumentation API for observability, telemetry, and error tracking. Create an `app/instrumentation.ts` file to hook into the request lifecycle — no configuration required.
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
// app/instrumentation.ts
|
|
496
|
+
import { defineInstrumentation } from 'solidstep/utils/instrumentation';
|
|
497
|
+
|
|
498
|
+
export default defineInstrumentation({
|
|
499
|
+
async register() {
|
|
500
|
+
// Called once at server startup.
|
|
501
|
+
// Initialize your telemetry SDK here (e.g., OpenTelemetry, Sentry).
|
|
502
|
+
console.log('[instrumentation] Server starting...');
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
async onRequest(request, context) {
|
|
506
|
+
// Called before each request is processed.
|
|
507
|
+
context.metadata.requestId = crypto.randomUUID();
|
|
508
|
+
console.log(`[instrumentation] ${request.method} ${context.pathname} (${context.routeType})`);
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
async onResponseEnd(request, context) {
|
|
512
|
+
// Called after the response is complete.
|
|
513
|
+
console.log(`[instrumentation] ${context.statusCode} ${context.pathname} ${context.duration.toFixed(1)}ms`);
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
async onRequestError(error, request, context) {
|
|
517
|
+
// Called when an unhandled error occurs during request processing.
|
|
518
|
+
console.error(`[instrumentation] Error in ${context.pathname}:`, error.message);
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Available Hooks:**
|
|
524
|
+
|
|
525
|
+
| Hook | When it fires | Arguments |
|
|
526
|
+
|------|--------------|-----------|
|
|
527
|
+
| `register` | Once at server startup | None |
|
|
528
|
+
| `onRequest` | Before each request | `(request: Request, context: RequestContext)` |
|
|
529
|
+
| `onResponseStart` | When response is ready, before streaming | `(request: Request, response: Response, context: ResponseContext)` |
|
|
530
|
+
| `onResponseEnd` | After response stream is complete | `(request: Request, context: ResponseContext)` |
|
|
531
|
+
| `onRequestError` | When an unhandled error occurs | `(error: Error, context: RequestContext)` |
|
|
532
|
+
|
|
533
|
+
**Context Objects:**
|
|
534
|
+
|
|
535
|
+
- `RequestContext` — includes `routePath`, `pathname`, `routeType` (`'page'` | `'api'` | `'server-action'` | `'not-found'`), `params`, `searchParams`, `startTime`, `startTimeEpoch`, and `metadata`
|
|
536
|
+
- `ResponseContext` — extends `RequestContext` with `statusCode` and `duration` (ms)
|
|
537
|
+
- `metadata` — a mutable `Record<string, unknown>` shared across all hooks for the same request. Use it to pass data between hooks (e.g., OpenTelemetry spans, request IDs).
|
|
538
|
+
|
|
539
|
+
**Key Behaviors:**
|
|
540
|
+
- Zero-config: if `app/instrumentation.ts` doesn't exist, the framework silently uses a no-op fallback
|
|
541
|
+
- `register()` completes before the first request is handled
|
|
542
|
+
- Errors in hooks are caught and logged — they never crash user requests
|
|
543
|
+
- Works with page routes, API routes, and server actions
|
|
544
|
+
- Compatible with OpenTelemetry, Sentry, Datadog, and any Node.js telemetry SDK
|
|
545
|
+
|
|
546
|
+
**OpenTelemetry Example:**
|
|
547
|
+
|
|
548
|
+
```tsx
|
|
549
|
+
// app/instrumentation.ts
|
|
550
|
+
import { defineInstrumentation } from 'solidstep/utils/instrumentation';
|
|
551
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
552
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
553
|
+
import { trace } from '@opentelemetry/api';
|
|
554
|
+
|
|
555
|
+
let sdk: NodeSDK;
|
|
556
|
+
|
|
557
|
+
export default defineInstrumentation({
|
|
558
|
+
async register() {
|
|
559
|
+
sdk = new NodeSDK({
|
|
560
|
+
traceExporter: new OTLPTraceExporter(),
|
|
561
|
+
serviceName: 'my-solidstep-app',
|
|
562
|
+
});
|
|
563
|
+
sdk.start();
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
async onRequest(request, context) {
|
|
567
|
+
const tracer = trace.getTracer('solidstep');
|
|
568
|
+
const span = tracer.startSpan(`${request.method} ${context.routePath}`);
|
|
569
|
+
context.metadata.span = span;
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
async onResponseEnd(request, context) {
|
|
573
|
+
const span = context.metadata.span as any;
|
|
574
|
+
span?.setAttribute('http.status_code', context.statusCode);
|
|
575
|
+
span?.end();
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
async onRequestError(error, request, context) {
|
|
579
|
+
const span = context.metadata.span as any;
|
|
580
|
+
span?.recordException(error);
|
|
581
|
+
span?.end();
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Page Options
|
|
587
|
+
|
|
588
|
+
Configure page-level caching:
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
export const options = {
|
|
592
|
+
cache: {
|
|
593
|
+
ttl: 60000, // Cache for 60 seconds
|
|
594
|
+
},
|
|
595
|
+
responseHeaders: { // Custom headers for pages
|
|
596
|
+
'X-Custom-Header': 'MyValue',
|
|
597
|
+
'Cache-Control': 'public, max-age=60', // Client-side caching
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
```
|
|
601
|
+
- Regarding caching, setting `ttl` to `0` or omitting it will disable caching for that page.
|
|
602
|
+
- Setting a positive integer value will cache the page for that duration in milliseconds.
|
|
603
|
+
- Invalidation of cached pages can be done using the `invalidateCache` and `revalidatePath` utilities.
|
|
604
|
+
- The `responseHeaders` option allows you to set custom HTTP headers for the page response.
|
|
605
|
+
|
|
606
|
+
## API Routes
|
|
607
|
+
|
|
608
|
+
Create REST endpoints:
|
|
609
|
+
- GET
|
|
610
|
+
- POST
|
|
611
|
+
- PUT
|
|
612
|
+
- DELETE
|
|
613
|
+
- PATCH
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
export async function GET(request: Request, { params }: any) {
|
|
617
|
+
const posts = await fetchPosts();
|
|
618
|
+
return new Response(JSON.stringify(posts), {
|
|
619
|
+
headers: { 'Content-Type': 'application/json' },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function POST(request: Request) {
|
|
624
|
+
const data = await request.json();
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## Server Assets
|
|
629
|
+
Serve static files from the `server-assets/` directory:
|
|
630
|
+
|
|
631
|
+
```my-app/
|
|
632
|
+
├── server-assets/
|
|
633
|
+
│ └── secret.txt
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Access via `my-app/server-assets/secret.txt` URL:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
const TEMPLATE_PATH = join(process.cwd(), 'server-assets', 'templates', 'template.ejs');
|
|
640
|
+
const template = await fs.promises.readFile(TEMPLATE_PATH, 'utf-8');
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## Utilities
|
|
644
|
+
|
|
645
|
+
### Cache (Server-Side)
|
|
646
|
+
- Every page can be cached by setting the `options.cache` property in the page.
|
|
647
|
+
- You can also manually invalidate the cache for specific routes.
|
|
648
|
+
- Invalidation can be done in two ways:
|
|
649
|
+
1. Using the `invalidateCache` utility to only invalidate paths.
|
|
650
|
+
```tsx
|
|
651
|
+
import { invalidateCache } from 'solidstep/utils/cache';
|
|
652
|
+
|
|
653
|
+
const action = async () => {
|
|
654
|
+
'use server';
|
|
655
|
+
|
|
656
|
+
...
|
|
657
|
+
|
|
658
|
+
// Invalidate cache after data mutation
|
|
659
|
+
await invalidateCache('/some-route');
|
|
660
|
+
|
|
661
|
+
...
|
|
662
|
+
|
|
663
|
+
return { success: true };
|
|
664
|
+
};
|
|
665
|
+
```
|
|
666
|
+
2. Using the `revalidatePath` utility to revalidate specific paths and revalidate the frontend DOM - signaling the server action as a Single Flight Mutation query.
|
|
667
|
+
```tsx
|
|
668
|
+
import { revalidatePath } from 'solidstep/utils/cache';
|
|
669
|
+
|
|
670
|
+
const action = async () => {
|
|
671
|
+
'use server';
|
|
672
|
+
|
|
673
|
+
...
|
|
674
|
+
|
|
675
|
+
// Revalidate path after data mutation
|
|
676
|
+
await revalidatePath('/some-route');
|
|
677
|
+
|
|
678
|
+
...
|
|
679
|
+
|
|
680
|
+
return { success: true };
|
|
681
|
+
};
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Cookies
|
|
685
|
+
```tsx
|
|
686
|
+
import { getCookie, setCookie } from 'solidstep/utils/cookies';
|
|
687
|
+
|
|
688
|
+
export const loader = defineLoader(async () => {
|
|
689
|
+
const userData = await getCookie();
|
|
690
|
+
|
|
691
|
+
if (!userData) {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const userId = userData.id;
|
|
696
|
+
|
|
697
|
+
const { data, error } = await getDocumentsByUserId(userId);
|
|
698
|
+
|
|
699
|
+
if (error || !data) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return data as Document[];
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const action = async () => {
|
|
707
|
+
'use server';
|
|
708
|
+
|
|
709
|
+
await setCookie('session', JSON.stringify({ id: 'user-id' }), { httpOnly: true, secure: true, maxAge: 3600 });
|
|
710
|
+
|
|
711
|
+
return { success: true };
|
|
712
|
+
};
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### CORS
|
|
716
|
+
```tsx
|
|
717
|
+
import { cors } from 'solidstep/utils/cors';
|
|
718
|
+
|
|
719
|
+
const trustedOrigins = ['https://example.com', 'https://another-example.com'];
|
|
720
|
+
|
|
721
|
+
const corsMiddleware = cors(trustedOrigins);
|
|
722
|
+
|
|
723
|
+
...
|
|
724
|
+
|
|
725
|
+
const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
|
|
726
|
+
|
|
727
|
+
...
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### CSP
|
|
731
|
+
```tsx
|
|
732
|
+
import { createBasePolicy, serializePolicy, withNonce } from 'solidstep/utils/csp';
|
|
733
|
+
|
|
734
|
+
let cspPolicy = createBasePolicy();
|
|
735
|
+
|
|
736
|
+
...
|
|
737
|
+
|
|
738
|
+
cspPolicy = withNonce(cspPolicy, nonce);
|
|
739
|
+
|
|
740
|
+
...
|
|
741
|
+
|
|
742
|
+
event.response.headers.set('Content-Security-Policy', serializePolicy(cspPolicy));
|
|
743
|
+
|
|
744
|
+
...
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### CSRF Protection
|
|
748
|
+
```tsx
|
|
749
|
+
import { csrf } from 'solidstep/utils/csrf';
|
|
750
|
+
|
|
751
|
+
const trustedOrigins = ['https://example.com', 'https://another-example.com'];
|
|
752
|
+
|
|
753
|
+
const csrfMiddleware = csrf(trustedOrigins);
|
|
754
|
+
|
|
755
|
+
...
|
|
756
|
+
|
|
757
|
+
const csrfResult = csrfMiddleware(
|
|
758
|
+
event.node.req.method,
|
|
759
|
+
requestUrl,
|
|
760
|
+
origin,
|
|
761
|
+
event.node.req.headers.referer
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
if (!csrfResult.success) {
|
|
765
|
+
event.node.res.statusCode = 403; // Forbidden
|
|
766
|
+
event.node.res.end(csrfResult.message);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Redirects
|
|
772
|
+
```tsx
|
|
773
|
+
import { redirect } from 'solidstep/utils/redirect';
|
|
774
|
+
|
|
775
|
+
export const loader = defineLoader(async () => {
|
|
776
|
+
redirect('/login');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// or in client
|
|
780
|
+
export function MyComponent() {
|
|
781
|
+
const handleClick = () => {
|
|
782
|
+
redirect('/dashboard');
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
return <button onClick={handleClick}>Go to Dashboard</button>;
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### Error Handling
|
|
790
|
+
```tsx
|
|
791
|
+
// first define an error collection
|
|
792
|
+
import { createErrorFactory } from 'solidstep/utils/error-handler';
|
|
793
|
+
|
|
794
|
+
export const createError = createErrorFactory({
|
|
795
|
+
'db-query-error': {
|
|
796
|
+
message: 'Something went wrong with the database query, not idea what',
|
|
797
|
+
severity: 'high',
|
|
798
|
+
action: (error) => {
|
|
799
|
+
console.error('Generic DB query error', error);
|
|
800
|
+
throw error;
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
'auth-error': {
|
|
804
|
+
message: 'User authentication failed',
|
|
805
|
+
severity: 'high',
|
|
806
|
+
action: (error) => {
|
|
807
|
+
console.error('User authentication error', error);
|
|
808
|
+
throw error;
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
'service-error': {
|
|
812
|
+
message:
|
|
813
|
+
'Some service (external or internal that is interfacing with the app) failed',
|
|
814
|
+
severity: 'high',
|
|
815
|
+
action: (error) => {
|
|
816
|
+
console.error('Service error', error);
|
|
817
|
+
throw error;
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// then use it in your loaders, actions or routes
|
|
823
|
+
export const loader = defineLoader(async () => {
|
|
824
|
+
const data = await tryCatch(fetchDataFromDB());
|
|
825
|
+
if (data.error) {
|
|
826
|
+
// handle the error using the defined error collection
|
|
827
|
+
createError('db-query-error').action();
|
|
828
|
+
|
|
829
|
+
// or overwrite the defaults
|
|
830
|
+
createError('db-query-error', {
|
|
831
|
+
// customize the error
|
|
832
|
+
message: data.error.message,
|
|
833
|
+
action: (error) => {
|
|
834
|
+
// just log it for example
|
|
835
|
+
console.error('Custom action for DB error', error);
|
|
836
|
+
},
|
|
837
|
+
severity: 'critical',
|
|
838
|
+
cause: data.error,
|
|
839
|
+
metadata: { query: 'SELECT * FROM users' },
|
|
840
|
+
}).action();
|
|
841
|
+
|
|
842
|
+
// defer the definition and the handling
|
|
843
|
+
const error = createError('db-query-error');
|
|
844
|
+
// some logic
|
|
845
|
+
error.action();
|
|
846
|
+
|
|
847
|
+
// or throw the error
|
|
848
|
+
const error = createError('db-query-error', {
|
|
849
|
+
cause: data.error,
|
|
850
|
+
});
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
return data.result;
|
|
854
|
+
});
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### Logging
|
|
858
|
+
|
|
859
|
+
SolidStep includes a built-in Pino logger that can be configured globally:
|
|
860
|
+
|
|
861
|
+
```tsx
|
|
862
|
+
import { defineConfig } from 'solidstep';
|
|
863
|
+
|
|
864
|
+
export default defineConfig({
|
|
865
|
+
server: {
|
|
866
|
+
preset: 'node',
|
|
867
|
+
},
|
|
868
|
+
logger: {
|
|
869
|
+
level: 'info',
|
|
870
|
+
transport: {
|
|
871
|
+
target: 'pino-pretty', // Use pino-pretty for human-readable logs
|
|
872
|
+
options: {
|
|
873
|
+
colorize: true
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
Use the logger in your code:
|
|
881
|
+
|
|
882
|
+
```tsx
|
|
883
|
+
import { logger } from 'solidstep/utils/logger';
|
|
884
|
+
|
|
885
|
+
export const loader = defineLoader(async () => {
|
|
886
|
+
logger.info('Fetching posts');
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const posts = await fetchPosts();
|
|
890
|
+
logger.info(`Fetched ${posts.length} posts`);
|
|
891
|
+
return { posts };
|
|
892
|
+
} catch (error) {
|
|
893
|
+
logger.error('Failed to fetch posts', error);
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**Logger Configuration Options:**
|
|
900
|
+
- `false` or `undefined` - Disables logging (silent mode)
|
|
901
|
+
- `true` - Enables default Pino logger
|
|
902
|
+
- `object` - Custom Pino configuration object [Pino Docs](https://getpino.io/#/docs/api?id=options)
|
|
903
|
+
|
|
904
|
+
### Fetch Utilities
|
|
905
|
+
|
|
906
|
+
SolidStep provides type-safe fetch wrappers for both client and server with built-in timeout and error handling:
|
|
907
|
+
|
|
908
|
+
**Client-side Fetch:**
|
|
909
|
+
```tsx
|
|
910
|
+
import fetch from 'solidstep/utils/fetch.client';
|
|
911
|
+
|
|
912
|
+
async function fetchPosts() {
|
|
913
|
+
const posts = await fetch<Post[]>('/api/posts', {
|
|
914
|
+
method: 'GET',
|
|
915
|
+
MAX_FETCH_TIME: 5000,
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
return posts;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
...
|
|
922
|
+
|
|
923
|
+
// To get full response including status, headers, etc.
|
|
924
|
+
const response = await fetch<Post[], false>(
|
|
925
|
+
'/api/posts',
|
|
926
|
+
{ method: 'GET' },
|
|
927
|
+
false
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
console.log(response.status); // HTTP status code
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
**Server-side Fetch:**
|
|
934
|
+
```tsx
|
|
935
|
+
import fetch from 'solidstep/utils/fetch.server';
|
|
936
|
+
|
|
937
|
+
export const loader = defineLoader(async () => {
|
|
938
|
+
const data = await fetch<ApiResponse>('https://api.example.com/data', {
|
|
939
|
+
method: 'POST',
|
|
940
|
+
body: JSON.stringify({ query: 'test' }),
|
|
941
|
+
headers: {
|
|
942
|
+
'Content-Type': 'application/json',
|
|
943
|
+
},
|
|
944
|
+
MAX_FETCH_TIME: 10000,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
return data;
|
|
948
|
+
});
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
**Features:**
|
|
952
|
+
- Automatic timeout handling with AbortController (default: 4000ms)
|
|
953
|
+
- Automatic JSON parsing (optional)
|
|
954
|
+
- Error handling for HTTP 4xx/5xx responses
|
|
955
|
+
- Type-safe responses with TypeScript generics
|
|
956
|
+
- Server-side uses undici for better performance
|
|
957
|
+
|
|
958
|
+
### Server-Only Code
|
|
959
|
+
|
|
960
|
+
Ensure code only runs on the server and throws an error if accessed on the client:
|
|
961
|
+
|
|
962
|
+
```tsx
|
|
963
|
+
import 'solidstep/utils/server-only';
|
|
964
|
+
|
|
965
|
+
export const SECRET_KEY = process.env.SECRET_KEY;
|
|
966
|
+
export const DATABASE_URL = process.env.DATABASE_URL;
|
|
967
|
+
|
|
968
|
+
export async function queryDatabase(query: string) {
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
**Use case:** Import this at the top of any file that should never be used for the client (e.g., database utilities, API keys, server secrets).
|
|
973
|
+
|
|
974
|
+
```tsx
|
|
975
|
+
import 'solidstep/utils/server-only';
|
|
976
|
+
|
|
977
|
+
export const db = createDatabaseConnection(process.env.DATABASE_URL);
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
If accidentally imported on the client, it will throw:
|
|
981
|
+
```
|
|
982
|
+
Error: This module is only available on the server side.
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
## Preloading/prefetching strategies
|
|
986
|
+
SolidStep supports various preloading and prefetching strategies to enhance user experience by loading data and resources ahead of time. This can significantly reduce perceived latency and improve navigation speed within your application. Solidstep does not include any preloading/prefetching by default, but you can implement your own strategies using the built-in fetch utilities and SolidJS features.
|
|
987
|
+
|
|
988
|
+
Some common strategies include:
|
|
989
|
+
- **Link Prefetching**: Use the `<link rel="prefetch">` tag to hint the browser to prefetch resources for links that users are likely to click on next.
|
|
990
|
+
- **Using Intersection Observer**: Implement lazy loading and prefetching of data when certain elements come into the viewport.
|
|
991
|
+
- **Using [instant.page](https://instant.page/)**: A small library that preloads pages on hover or touchstart events.
|
|
992
|
+
```tsx
|
|
993
|
+
export const RootLayout = (props) => {
|
|
994
|
+
return (
|
|
995
|
+
<body>
|
|
996
|
+
...
|
|
997
|
+
<NoHydration>
|
|
998
|
+
<script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
|
|
999
|
+
</NoHydration>
|
|
1000
|
+
</body>
|
|
1001
|
+
);
|
|
1002
|
+
};
|
|
1003
|
+
```
|
|
1004
|
+
- **Using [Foresight.js](https://foresightjs.com/)**: A library that preloads pages based on user behavior and patterns.
|
|
1005
|
+
```tsx
|
|
1006
|
+
import { ForesightManager } from "js.foresight";
|
|
1007
|
+
import { onMount } from "solid-js";
|
|
1008
|
+
|
|
1009
|
+
export const RootLayout = (props) => {
|
|
1010
|
+
onMount(() => {
|
|
1011
|
+
ForesightManager.initialize({
|
|
1012
|
+
// Configuration options
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
return (
|
|
1016
|
+
<body>
|
|
1017
|
+
...
|
|
1018
|
+
</body>
|
|
1019
|
+
);
|
|
1020
|
+
};
|
|
1021
|
+
```
|
|
1022
|
+
- **Custom Preloading Logic**: Write custom logic to preload data for specific routes or components based on user behavior or application state.
|
|
1023
|
+
|
|
1024
|
+
## Fonts
|
|
1025
|
+
Install fonts (for example, from [Fontsource](https://github.com/fontsource/fontsource)) and import into `globals.css` example:
|
|
1026
|
+
```css
|
|
1027
|
+
@import '@fontsource-variable/dm-sans';
|
|
1028
|
+
@import '@fontsource-variable/jetbrains-mono';
|
|
1029
|
+
|
|
1030
|
+
@theme inline {
|
|
1031
|
+
--font-sans: 'DM Sans Variable', sans-serif;
|
|
1032
|
+
--font-mono: 'JetBrains Mono Variable', monospace;
|
|
1033
|
+
/* ... */
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
## Images
|
|
1038
|
+
Use the package called [Unpic](https://unpic.pics/img/solid/) for images. An open source and powerful tool for images on the web.
|
|
1039
|
+
```bash
|
|
1040
|
+
[npm | yarn | pnpm | bun] install @unpic/solid
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
```tsx
|
|
1044
|
+
import type { Component } from "solid-js";
|
|
1045
|
+
import { Image } from "@unpic/solid";
|
|
1046
|
+
|
|
1047
|
+
const MyComponent: Component = () => {
|
|
1048
|
+
return (
|
|
1049
|
+
<Image
|
|
1050
|
+
src="https://cdn.shopify.com/static/sample-images/bath_grande_crop_center.jpeg"
|
|
1051
|
+
layout="constrained"
|
|
1052
|
+
width={800}
|
|
1053
|
+
height={600}
|
|
1054
|
+
alt="A lovely bath"
|
|
1055
|
+
/>
|
|
1056
|
+
);
|
|
1057
|
+
};
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
## Environment Variables
|
|
1061
|
+
As SolidStep is built using Vite, it follows the same guide as stated in [Vite docs](https://vite.dev/guide/env-and-mode) regarding environment variables.
|
|
1062
|
+
|
|
1063
|
+
## Future Plans
|
|
1064
|
+
- Pre-rendering static pages at build time and partial pre-rendering for dynamic pages
|
|
1065
|
+
- Support for dynamic site.webmanifest, robots.txt, sitemap.xml, manifest.json, and llms.txt
|
|
1066
|
+
- Support loading and error pages for parallel routes
|
|
1067
|
+
- Support caching loaders
|
|
1068
|
+
- Support deferring loaders
|
|
1069
|
+
- Possible SSG, ISR, and PPR
|
|
1070
|
+
- Advanced caching strategies
|
|
1071
|
+
- WebSocket support
|
|
1072
|
+
|
|
1073
|
+
## Testing
|
|
1074
|
+
|
|
1075
|
+
SolidStep does not include a built-in testing framework. However, we recommend setting up testing using Vitest ecosystem. You can use [Vitest](https://vitest.dev/) for unit and integration tests, and [Playwright](https://playwright.dev/) for end-to-end testing.
|
|
1076
|
+
|
|
1077
|
+
### Testing Server Actions
|
|
1078
|
+
|
|
1079
|
+
When testing server actions, you can use Vitest to accomplish this. Just test as you would with any other async function.
|
|
1080
|
+
|
|
1081
|
+
When testing pages (e2e tests), you can trigger server actions by simulating user interactions that would call those actions. If needed, you can also intercept network requests to directly test the action endpoints. Use the testing framework's capabilities to intercept the requests and ensure the responses have the expected results. If the server action returns json data, stringify it and add it to the response body as well as setting the content-type header to 'application/json'. If the action has a more complex return type, use seroval to serialize the response before sending it back.
|
|
1082
|
+
|
|
1083
|
+
## License
|
|
1084
|
+
|
|
1085
|
+
MIT
|
|
1086
|
+
|
|
1087
|
+
## Links
|
|
1088
|
+
|
|
1089
|
+
- [GitHub](https://github.com/HamzaKV/solidstep)
|
|
1090
|
+
- [SolidJS Documentation](https://www.solidjs.com/)
|
|
1091
|
+
|
|
1092
|
+
## Special Mentions
|
|
1093
|
+
- Inspired by [Remix](https://remix.run/), [Next.js](https://nextjs.org/), and [TanStack](https://tanstack.com/)
|
|
1094
|
+
- Built with [Vite](https://vitejs.dev/), [SolidJS](https://www.solidjs.com/), [Vinxi](https://github.com/nksaraf/vinxi), [Undici](https://undici.nodejs.org/#/), [Pino](https://getpino.io/#/) and [Seroval](https://github.com/lxsmnsyc/seroval)
|