solidstep 0.3.3 → 0.3.5

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 CHANGED
@@ -1,829 +1,864 @@
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
- **A route is defined by either the presence of a `page.tsx` or `route.ts` file in a directory.**
42
-
43
- **Similar to NextJS, routes are not indexed if they have a '_' placed at the beginning of the name**
44
-
45
- ### Configuration
46
-
47
- Configure your app in `app.config.ts`:
48
-
49
- ```tsx
50
- import { defineConfig } from 'solidstep';
51
- import tailwindcss from '@tailwindcss/vite';
52
-
53
- export default defineConfig({
54
- server: {
55
- preset: 'node',
56
- },
57
- plugins: [
58
- {
59
- type: 'client', // or 'server' or 'both' - depends on where you want to use the plugin
60
- plugin: tailwindcss()
61
- }
62
- ],
63
- });
64
- ```
65
-
66
- #### Vite Configuration
67
-
68
- You can customize Vite settings for both client and server builds.
69
-
70
- __When trying to configure absolute path imports__
71
- 1. Add the path alias in tsconfig.json (for TypeScript support):
72
- ```json
73
- {
74
- "compilerOptions": {
75
- "baseUrl": ".",
76
- "paths": {
77
- "@/*": ["./*"]
78
- }
79
- }
80
- }
81
- ```
82
-
83
- 2. Then add the same alias in the Vite config inside `app.config.ts` to ensure it works during build and runtime:
84
- ```tsx
85
- import { defineConfig } from 'solidstep';
86
- import { resolve } from 'node:path';
87
- import { dirname } from 'node:path';
88
- import { fileURLToPath } from 'node:url';
89
-
90
- const __dirname = dirname(fileURLToPath(import.meta.url));
91
-
92
- export default defineConfig({
93
- server: {
94
- preset: 'node',
95
- },
96
- vite: {
97
- resolve: {
98
- alias: {
99
- '@': resolve(__dirname, '.'),
100
- },
101
- },
102
- },
103
- });
104
- ```
105
-
106
- ### Project Structure
107
-
108
- ```
109
- my-app/
110
- ├── app/
111
- │ ├── page.tsx # Home page (/)
112
- │ ├── layout.tsx # Root layout
113
- │ ├── middleware.ts # Request middleware
114
- │ ├── about/
115
- │ │ └── page.tsx # About page (/about)
116
- │ ├── (admin)/
117
- │ | └── dashboard/
118
- │ | └── page.tsx # Group route (/dashboard)
119
- │ └── blog/
120
- │ ├── layout.tsx # Blog layout
121
- │ ├── page.tsx # Blog index (/blog)
122
- │ └── [slug]/
123
- │ └── page.tsx # Dynamic route (/blog/:slug)
124
- ├── public/
125
- │ └── favicon.ico
126
- ├── app.config.ts
127
- └── package.json
128
- ```
129
-
130
- ## Core Concepts
131
-
132
- ### Layouts
133
-
134
- Wrap multiple pages with shared UI:
135
-
136
- ```tsx
137
- export default function BlogLayout(props: { children: any }) {
138
- return (
139
- <div>
140
- <nav>Blog Navigation</nav>
141
- {props.children()}
142
- </div>
143
- );
144
- }
145
- ```
146
-
147
- ### Pages
148
-
149
- Create a `page.tsx` file in any directory under `app/` to define a route:
150
-
151
- ```tsx
152
- export default function HomePage() {
153
- return <h1>Welcome to SolidStep!</h1>;
154
- }
155
- ```
156
-
157
- **Similar to NextJS, only content returned by a `page` or `route` is sent to the client**
158
-
159
- ### Group Routes
160
- Use parentheses to group routes without affecting the URL:
161
-
162
- ```app/
163
- ├── (admin)/
164
- │ └── dashboard/
165
- │ └── page.tsx // matches /dashboard
166
- └── (user)/
167
- └── profile/
168
- └── page.tsx // matches /profile
169
- ```
170
-
171
- ### Dynamic Routes
172
-
173
- Use square brackets for dynamic segments:
174
-
175
- ```tsx
176
- // app/blog/[slug]/page.tsx - matches /blog/my-post, /blog/another-post, etc.
177
-
178
- export default function BlogPost(props: { routeParams: { slug: string } }) {
179
- return <h1>Post: {props.routeParams.slug}</h1>;
180
- }
181
- ```
182
-
183
- **Catch-all routes:**
184
- ```tsx
185
- // app/docs/[...path]/page.tsx - matches /docs/a, /docs/a/b, etc.
186
- ```
187
-
188
- **Catch-all routes (Optional):**
189
- ```tsx
190
- // app/docs/[[...path]]/page.tsx - matches /docs, /docs/a, /docs/a/b, etc.
191
- ```
192
-
193
- ### Parallel Routes (Groups)
194
-
195
- Render multiple sections simultaneously:
196
-
197
- ```
198
- app/
199
- ├── layout.tsx
200
- ├── page.tsx
201
- └── @graph1/
202
- └── page.tsx
203
- └── @graph2/
204
- └── page.tsx
205
- ```
206
-
207
- ```tsx
208
- export default function RootLayout(props: {
209
- children: any;
210
- slots: { graph1: any; graph2: any; };
211
- }) {
212
- return (
213
- <main>
214
- {props.children()}
215
- <aside>
216
- <div>{props.slots.graph1()}</div>
217
- <div>{props.slots.graph2()}</div>
218
- </aside>
219
- </main>
220
- );
221
- }
222
- ```
223
-
224
- ### Data Loading
225
-
226
- Use `defineLoader` to fetch data on the server:
227
-
228
- ```tsx
229
- import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
230
-
231
- export const loader = defineLoader(async (request) => {
232
- const posts = await fetchPosts();
233
- return { posts };
234
- });
235
-
236
- type LoaderData = LoaderDataFromFunction<typeof loader>;
237
-
238
- export default function BlogPage(props: { loaderData: LoaderData }) {
239
- return (
240
- <ul>
241
- <For each={props.loaderData.posts}>
242
- {(post) => <li>{post.title}</li>}
243
- </For>
244
- </ul>
245
- );
246
- }
247
- ```
248
-
249
- ### Server Actions
250
-
251
- Create type-safe server functions:
252
-
253
- ```tsx
254
- 'use server';
255
-
256
- export const createPost = async (data: { title: string }) => {
257
- await db.posts.create(data);
258
- return { success: true };
259
- };
260
- ```
261
-
262
- Call from client:
263
-
264
- ```tsx
265
- import { createPost } from './actions';
266
-
267
- function CreatePostForm() {
268
- const handleSubmit = async (e: Event) => {
269
- e.preventDefault();
270
- await createPost({ title: 'My Post' });
271
- };
272
-
273
- return <form onSubmit={handleSubmit}>...</form>;
274
- }
275
- ```
276
-
277
- ### Metadata
278
-
279
- Define metadata for SEO:
280
-
281
- ```tsx
282
- import { meta } from 'solidstep/utils/types';
283
-
284
- // can also be async
285
- export const generateMeta = meta(() => {
286
- return {
287
- title: {
288
- type: 'title',
289
- content: 'My Site',
290
- attributes: {},
291
- },
292
- description: {
293
- type: 'meta',
294
- attributes: {
295
- name: 'description',
296
- content: 'My awesome site',
297
- },
298
- },
299
- // manifest
300
- manifest: {
301
- type: 'link',
302
- attributes: {
303
- rel: 'manifest',
304
- href: '/site.webmanifest',
305
- },
306
- },
307
- // google fonts
308
- 'google-font-link': {
309
- type: 'link',
310
- attributes: {
311
- rel: 'preconnect',
312
- href: 'https://fonts.googleapis.com'
313
- }
314
- },
315
- 'gstatic-font-link': {
316
- type: 'link',
317
- attributes: {
318
- rel: 'preconnect',
319
- href: 'https://fonts.gstatic.com',
320
- crossorigin: ''
321
- }
322
- },
323
- 'inter-font': {
324
- type: 'link',
325
- attributes: {
326
- rel: 'stylesheet',
327
- href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
328
- }
329
- },
330
- // external js
331
- 'analytics-script': {
332
- type: 'script',
333
- attributes: {
334
- src: 'analytics.js',
335
- defer: true,
336
- }
337
- }
338
- };
339
- });
340
- ```
341
-
342
- ### Middleware
343
-
344
- Intercept and modify requests:
345
-
346
- ```tsx
347
- import { defineMiddleware } from 'vinxi/http';
348
-
349
- export default defineMiddleware({
350
- onRequest: async (request) => {
351
- console.log('Incoming request:', request.url);
352
- // Modify request if needed
353
- return request;
354
- },
355
- });
356
- ```
357
-
358
- ### Page Options
359
-
360
- Configure page-level caching:
361
-
362
- ```tsx
363
- export const options = {
364
- cache: {
365
- ttl: 60000, // Cache for 60 seconds
366
- },
367
- responseHeaders: { // Custom headers for pages
368
- 'X-Custom-Header': 'MyValue',
369
- 'Cache-Control': 'public, max-age=60', // Client-side caching
370
- },
371
- };
372
- ```
373
- - Regarding caching, setting `ttl` to `0` or omitting it will disable caching for that page.
374
- - Setting a positive integer value will cache the page for that duration in milliseconds.
375
- - Invalidation of cached pages can be done using the `invalidateCache` and `revalidatePath` utilities.
376
- - The `responseHeaders` option allows you to set custom HTTP headers for the page response.
377
-
378
- ## API Routes
379
-
380
- Create REST endpoints:
381
- - GET
382
- - POST
383
- - PUT
384
- - DELETE
385
- - PATCH
386
-
387
- ```tsx
388
- export async function GET(request: Request, { params }: any) {
389
- const posts = await fetchPosts();
390
- return new Response(JSON.stringify(posts), {
391
- headers: { 'Content-Type': 'application/json' },
392
- });
393
- }
394
-
395
- export async function POST(request: Request) {
396
- const data = await request.json();
397
- }
398
- ```
399
-
400
- ## Server Assets
401
- Serve static files from the `server-assets/` directory:
402
-
403
- ```my-app/
404
- ├── server-assets/
405
- │ └── secret.txt
406
- ```
407
-
408
- Access via `my-app/server-assets/secret.txt` URL:
409
-
410
- ```ts
411
- const TEMPLATE_PATH = join(process.cwd(), 'server-assets', 'templates', 'template.ejs');
412
- const template = await fs.promises.readFile(TEMPLATE_PATH, 'utf-8');
413
- ```
414
-
415
- ## Utilities
416
-
417
- ### Cache (Server-Side)
418
- - Every page can be cached by setting the `options.cache` property in the page.
419
- - You can also manually invalidate the cache for specific routes.
420
- - Invalidation can be done in two ways:
421
- 1. Using the `invalidateCache` utility to only invalidate paths.
422
- ```tsx
423
- import { invalidateCache } from 'solidstep/utils/cache';
424
-
425
- const action = async () => {
426
- 'use server';
427
-
428
- ...
429
-
430
- // Invalidate cache after data mutation
431
- await invalidateCache('/some-route');
432
-
433
- ...
434
-
435
- return { success: true };
436
- };
437
- ```
438
- 2. Using the `revalidatePath` utility to revalidate specific paths and revalidate the frontend DOM - signaling the server action as a Single Flight Mutation query.
439
- ```tsx
440
- import { revalidatePath } from 'solidstep/utils/cache';
441
-
442
- const action = async () => {
443
- 'use server';
444
-
445
- ...
446
-
447
- // Revalidate path after data mutation
448
- await revalidatePath('/some-route');
449
-
450
- ...
451
-
452
- return { success: true };
453
- };
454
- ```
455
-
456
- ### Cookies
457
- ```tsx
458
- import { getCookie, setCookie } from 'solidstep/utils/cookies';
459
-
460
- export const loader = defineLoader(async () => {
461
- const userData = await getCookie();
462
-
463
- if (!userData) {
464
- return [];
465
- }
466
-
467
- const userId = userData.id;
468
-
469
- const { data, error } = await getDocumentsByUserId(userId);
470
-
471
- if (error || !data) {
472
- return [];
473
- }
474
-
475
- return data as Document[];
476
- });
477
-
478
- const action = async () => {
479
- 'use server';
480
-
481
- await setCookie('session', JSON.stringify({ id: 'user-id' }), { httpOnly: true, secure: true, maxAge: 3600 });
482
-
483
- return { success: true };
484
- };
485
- ```
486
-
487
- ### CORS
488
- ```tsx
489
- import { cors } from 'solidstep/utils/cors';
490
-
491
- const trustedOrigins = ['https://example.com', 'https://another-example.com'];
492
-
493
- const corsMiddleware = cors(trustedOrigins);
494
-
495
- ...
496
-
497
- const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
498
-
499
- ...
500
- ```
501
-
502
- ### CSP
503
- ```tsx
504
- import { createBasePolicy, serializePolicy, withNonce } from 'solidstep/utils/csp';
505
-
506
- let cspPolicy = createBasePolicy();
507
-
508
- ...
509
-
510
- cspPolicy = withNonce(cspPolicy, nonce);
511
-
512
- ...
513
-
514
- event.response.headers.set('Content-Security-Policy', serializePolicy(cspPolicy));
515
-
516
- ...
517
- ```
518
-
519
- ### CSRF Protection
520
- ```tsx
521
- import { csrf } from 'solidstep/utils/csrf';
522
-
523
- const trustedOrigins = ['https://example.com', 'https://another-example.com'];
524
-
525
- const csrfMiddleware = csrf(trustedOrigins);
526
-
527
- ...
528
-
529
- const csrfResult = csrfMiddleware(
530
- event.node.req.method,
531
- requestUrl,
532
- origin,
533
- event.node.req.headers.referer
534
- );
535
-
536
- if (!csrfResult.success) {
537
- event.node.res.statusCode = 403; // Forbidden
538
- event.node.res.end(csrfResult.message);
539
- return;
540
- }
541
- ```
542
-
543
- ### Redirects
544
- ```tsx
545
- import { redirect } from 'solidstep/utils/redirect';
546
-
547
- export const loader = defineLoader(async () => {
548
- redirect('/login');
549
- });
550
-
551
- // or in client
552
- export function MyComponent() {
553
- const handleClick = () => {
554
- redirect('/dashboard');
555
- };
556
-
557
- return <button onClick={handleClick}>Go to Dashboard</button>;
558
- }
559
- ```
560
-
561
- ### Error Handling
562
- ```tsx
563
- // first define an error collection
564
- import { createErrorFactory } from 'solidstep/utils/error-handler';
565
-
566
- export const createError = createErrorFactory({
567
- 'db-query-error': {
568
- message: 'Something went wrong with the database query, not idea what',
569
- severity: 'high',
570
- action: (error) => {
571
- console.error('Generic DB query error', error);
572
- throw error;
573
- },
574
- },
575
- 'auth-error': {
576
- message: 'User authentication failed',
577
- severity: 'high',
578
- action: (error) => {
579
- console.error('User authentication error', error);
580
- throw error;
581
- },
582
- },
583
- 'service-error': {
584
- message:
585
- 'Some service (external or internal that is interfacing with the app) failed',
586
- severity: 'high',
587
- action: (error) => {
588
- console.error('Service error', error);
589
- throw error;
590
- },
591
- },
592
- });
593
-
594
- // then use it in your loaders, actions or routes
595
- export const loader = defineLoader(async () => {
596
- const data = await tryCatch(fetchDataFromDB());
597
- if (data.error) {
598
- // handle the error using the defined error collection
599
- createError('db-query-error').action();
600
-
601
- // or overwrite the defaults
602
- createError('db-query-error', {
603
- // customize the error
604
- message: data.error.message,
605
- action: (error) => {
606
- // just log it for example
607
- console.error('Custom action for DB error', error);
608
- },
609
- severity: 'critical',
610
- cause: data.error,
611
- metadata: { query: 'SELECT * FROM users' },
612
- }).action();
613
-
614
- // defer the definition and the handling
615
- const error = createError('db-query-error');
616
- // some logic
617
- error.action();
618
-
619
- // or throw the error
620
- const error = createError('db-query-error', {
621
- cause: data.error,
622
- });
623
- throw error;
624
- }
625
- return data.result;
626
- });
627
- ```
628
-
629
- ### Logging
630
-
631
- SolidStep includes a built-in Pino logger that can be configured globally:
632
-
633
- ```tsx
634
- import { defineConfig } from 'solidstep';
635
-
636
- export default defineConfig({
637
- server: {
638
- preset: 'node',
639
- },
640
- logger: {
641
- level: 'info',
642
- transport: {
643
- target: 'pino-pretty', // Use pino-pretty for human-readable logs
644
- options: {
645
- colorize: true
646
- }
647
- }
648
- }
649
- });
650
- ```
651
-
652
- Use the logger in your code:
653
-
654
- ```tsx
655
- import { logger } from 'solidstep/utils/logger';
656
-
657
- export const loader = defineLoader(async () => {
658
- logger.info('Fetching posts');
659
-
660
- try {
661
- const posts = await fetchPosts();
662
- logger.info(`Fetched ${posts.length} posts`);
663
- return { posts };
664
- } catch (error) {
665
- logger.error('Failed to fetch posts', error);
666
- throw error;
667
- }
668
- });
669
- ```
670
-
671
- **Logger Configuration Options:**
672
- - `false` or `undefined` - Disables logging (silent mode)
673
- - `true` - Enables default Pino logger
674
- - `object` - Custom Pino configuration object [Pino Docs](https://getpino.io/#/docs/api?id=options)
675
-
676
- ### Fetch Utilities
677
-
678
- SolidStep provides type-safe fetch wrappers for both client and server with built-in timeout and error handling:
679
-
680
- **Client-side Fetch:**
681
- ```tsx
682
- import fetch from 'solidstep/utils/fetch.client';
683
-
684
- async function fetchPosts() {
685
- const posts = await fetch<Post[]>('/api/posts', {
686
- method: 'GET',
687
- MAX_FETCH_TIME: 5000,
688
- });
689
-
690
- return posts;
691
- }
692
-
693
- ...
694
-
695
- // To get full response including status, headers, etc.
696
- const response = await fetch<Post[], false>(
697
- '/api/posts',
698
- { method: 'GET' },
699
- false
700
- );
701
-
702
- console.log(response.status); // HTTP status code
703
- ```
704
-
705
- **Server-side Fetch:**
706
- ```tsx
707
- import fetch from 'solidstep/utils/fetch.server';
708
-
709
- export const loader = defineLoader(async () => {
710
- const data = await fetch<ApiResponse>('https://api.example.com/data', {
711
- method: 'POST',
712
- body: JSON.stringify({ query: 'test' }),
713
- headers: {
714
- 'Content-Type': 'application/json',
715
- },
716
- MAX_FETCH_TIME: 10000,
717
- });
718
-
719
- return data;
720
- });
721
- ```
722
-
723
- **Features:**
724
- - Automatic timeout handling with AbortController (default: 4000ms)
725
- - Automatic JSON parsing (optional)
726
- - Error handling for HTTP 4xx/5xx responses
727
- - Type-safe responses with TypeScript generics
728
- - Server-side uses undici for better performance
729
-
730
- ### Server-Only Code
731
-
732
- Ensure code only runs on the server and throws an error if accessed on the client:
733
-
734
- ```tsx
735
- import 'solidstep/utils/server-only';
736
-
737
- export const SECRET_KEY = process.env.SECRET_KEY;
738
- export const DATABASE_URL = process.env.DATABASE_URL;
739
-
740
- export async function queryDatabase(query: string) {
741
- }
742
- ```
743
-
744
- **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).
745
-
746
- ```tsx
747
- import 'solidstep/utils/server-only';
748
-
749
- export const db = createDatabaseConnection(process.env.DATABASE_URL);
750
- ```
751
-
752
- If accidentally imported on the client, it will throw:
753
- ```
754
- Error: This module is only available on the server side.
755
- ```
756
-
757
- ## Preloading/prefetching strategies
758
- 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.
759
-
760
- Some common strategies include:
761
- - **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.
762
- - **Using Intersection Observer**: Implement lazy loading and prefetching of data when certain elements come into the viewport.
763
- - **Using [instant.page](https://instant.page/)**: A small library that preloads pages on hover or touchstart events.
764
- ```tsx
765
- export const RootLayout = (props) => {
766
- return (
767
- <body>
768
- ...
769
- <NoHydration>
770
- <script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
771
- </NoHydration>
772
- </body>
773
- );
774
- };
775
- ```
776
- - **Using [Foresight.js](https://foresightjs.com/)**: A library that preloads pages based on user behavior and patterns.
777
- ```tsx
778
- import { ForesightManager } from "js.foresight";
779
- import { onMount } from "solid-js";
780
-
781
- export const RootLayout = (props) => {
782
- onMount(() => {
783
- ForesightManager.initialize({
784
- // Configuration options
785
- });
786
- });
787
- return (
788
- <body>
789
- ...
790
- </body>
791
- );
792
- };
793
- ```
794
- - **Custom Preloading Logic**: Write custom logic to preload data for specific routes or components based on user behavior or application state.
795
-
796
- ## Environment Variables
797
- 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.
798
-
799
- ## Future Plans
800
- - Support for dynamic site.webmanifest, robots.txt, sitemap.xml, manifest.json, and llms.txt
801
- - Support loading and error pages for parallel routes
802
- - Support deferring loaders
803
- - Image/font optimizations
804
- - Possible SSG, ISR, and PPR
805
- - Advanced caching strategies
806
- - WebSocket support
807
-
808
- ## Testing
809
-
810
- 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.
811
-
812
- ### Testing Server Actions
813
-
814
- When testing server actions, you can use Vitest to accomplish this. Just test as you would with any other async function.
815
-
816
- 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.
817
-
818
- ## License
819
-
820
- MIT
821
-
822
- ## Links
823
-
824
- - [GitHub](https://github.com/HamzaKV/solidstep)
825
- - [SolidJS Documentation](https://www.solidjs.com/)
826
-
827
- ## Special Mentions
828
- - Inspired by [Remix](https://remix.run/), [Next.js](https://nextjs.org/), and [TanStack](https://tanstack.com/)
829
- - 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)
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
+ **A route is defined by either the presence of a `page.tsx` or `route.ts` file in a directory.**
42
+
43
+ **Similar to NextJS, routes are not indexed if they have a '_' placed at the beginning of the name**
44
+
45
+ ### Configuration
46
+
47
+ Configure your app in `app.config.ts`:
48
+
49
+ ```tsx
50
+ import { defineConfig } from 'solidstep';
51
+ import tailwindcss from '@tailwindcss/vite';
52
+
53
+ export default defineConfig({
54
+ server: {
55
+ preset: 'node',
56
+ },
57
+ plugins: [
58
+ {
59
+ type: 'client', // or 'server' or 'both' - depends on where you want to use the plugin
60
+ plugin: tailwindcss()
61
+ }
62
+ ],
63
+ });
64
+ ```
65
+
66
+ #### Vite Configuration
67
+
68
+ You can customize Vite settings for both client and server builds.
69
+
70
+ __When trying to configure absolute path imports__
71
+ 1. Add the path alias in tsconfig.json (for TypeScript support):
72
+ ```json
73
+ {
74
+ "compilerOptions": {
75
+ "baseUrl": ".",
76
+ "paths": {
77
+ "@/*": ["./*"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ 2. Then add the same alias in the Vite config inside `app.config.ts` to ensure it works during build and runtime:
84
+ ```tsx
85
+ import { defineConfig } from 'solidstep';
86
+ import { resolve } from 'node:path';
87
+ import { dirname } from 'node:path';
88
+ import { fileURLToPath } from 'node:url';
89
+
90
+ const __dirname = dirname(fileURLToPath(import.meta.url));
91
+
92
+ export default defineConfig({
93
+ server: {
94
+ preset: 'node',
95
+ },
96
+ vite: {
97
+ resolve: {
98
+ alias: {
99
+ '@': resolve(__dirname, '.'),
100
+ },
101
+ },
102
+ },
103
+ });
104
+ ```
105
+
106
+ ### Project Structure
107
+
108
+ ```
109
+ my-app/
110
+ ├── app/
111
+ │ ├── page.tsx # Home page (/)
112
+ │ ├── layout.tsx # Root layout
113
+ │ ├── middleware.ts # Request middleware
114
+ │ ├── about/
115
+ │ │ └── page.tsx # About page (/about)
116
+ │ ├── (admin)/
117
+ │ | └── dashboard/
118
+ │ | └── page.tsx # Group route (/dashboard)
119
+ │ └── blog/
120
+ │ ├── layout.tsx # Blog layout
121
+ │ ├── page.tsx # Blog index (/blog)
122
+ │ └── [slug]/
123
+ │ └── page.tsx # Dynamic route (/blog/:slug)
124
+ ├── public/
125
+ │ └── favicon.ico
126
+ ├── app.config.ts
127
+ └── package.json
128
+ ```
129
+
130
+ ## Core Concepts
131
+
132
+ ### Layouts
133
+
134
+ Wrap multiple pages with shared UI:
135
+
136
+ ```tsx
137
+ export default function BlogLayout(props: { children: any }) {
138
+ return (
139
+ <div>
140
+ <nav>Blog Navigation</nav>
141
+ {props.children()}
142
+ </div>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ### Pages
148
+
149
+ Create a `page.tsx` file in any directory under `app/` to define a route:
150
+
151
+ ```tsx
152
+ export default function HomePage() {
153
+ return <h1>Welcome to SolidStep!</h1>;
154
+ }
155
+ ```
156
+
157
+ **Similar to NextJS, only content returned by a `page` or `route` is sent to the client**
158
+
159
+ ### Group Routes
160
+ Use parentheses to group routes without affecting the URL:
161
+
162
+ ```app/
163
+ ├── (admin)/
164
+ │ └── dashboard/
165
+ │ └── page.tsx // matches /dashboard
166
+ └── (user)/
167
+ └── profile/
168
+ └── page.tsx // matches /profile
169
+ ```
170
+
171
+ ### Dynamic Routes
172
+
173
+ Use square brackets for dynamic segments:
174
+
175
+ ```tsx
176
+ // app/blog/[slug]/page.tsx - matches /blog/my-post, /blog/another-post, etc.
177
+
178
+ export default function BlogPost(props: { routeParams: { slug: string } }) {
179
+ return <h1>Post: {props.routeParams.slug}</h1>;
180
+ }
181
+ ```
182
+
183
+ **Catch-all routes:**
184
+ ```tsx
185
+ // app/docs/[...path]/page.tsx - matches /docs/a, /docs/a/b, etc.
186
+ ```
187
+
188
+ **Catch-all routes (Optional):**
189
+ ```tsx
190
+ // app/docs/[[...path]]/page.tsx - matches /docs, /docs/a, /docs/a/b, etc.
191
+ ```
192
+
193
+ ### Parallel Routes (Groups)
194
+
195
+ Render multiple sections simultaneously:
196
+
197
+ ```
198
+ app/
199
+ ├── layout.tsx
200
+ ├── page.tsx
201
+ └── @graph1/
202
+ └── page.tsx
203
+ └── @graph2/
204
+ └── page.tsx
205
+ ```
206
+
207
+ ```tsx
208
+ export default function RootLayout(props: {
209
+ children: any;
210
+ slots: { graph1: any; graph2: any; };
211
+ }) {
212
+ return (
213
+ <main>
214
+ {props.children()}
215
+ <aside>
216
+ <div>{props.slots.graph1()}</div>
217
+ <div>{props.slots.graph2()}</div>
218
+ </aside>
219
+ </main>
220
+ );
221
+ }
222
+ ```
223
+
224
+ ### Data Loading
225
+
226
+ Use `defineLoader` to fetch data on the server:
227
+
228
+ ```tsx
229
+ import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
230
+
231
+ export const loader = defineLoader(async (request) => {
232
+ const posts = await fetchPosts();
233
+ return { posts };
234
+ });
235
+
236
+ type LoaderData = LoaderDataFromFunction<typeof loader>;
237
+
238
+ export default function BlogPage(props: { loaderData: LoaderData }) {
239
+ return (
240
+ <ul>
241
+ <For each={props.loaderData.posts}>
242
+ {(post) => <li>{post.title}</li>}
243
+ </For>
244
+ </ul>
245
+ );
246
+ }
247
+ ```
248
+
249
+ ### Server Actions
250
+
251
+ Create type-safe server functions:
252
+
253
+ ```tsx
254
+ 'use server';
255
+
256
+ export const createPost = async (data: { title: string }) => {
257
+ await db.posts.create(data);
258
+ return { success: true };
259
+ };
260
+ ```
261
+
262
+ Call from client:
263
+
264
+ ```tsx
265
+ import { createPost } from './actions';
266
+
267
+ function CreatePostForm() {
268
+ const handleSubmit = async (e: Event) => {
269
+ e.preventDefault();
270
+ await createPost({ title: 'My Post' });
271
+ };
272
+
273
+ return <form onSubmit={handleSubmit}>...</form>;
274
+ }
275
+ ```
276
+
277
+ ### Metadata
278
+
279
+ Define metadata for SEO:
280
+
281
+ ```tsx
282
+ import { meta } from 'solidstep/utils/types';
283
+
284
+ // can also be async
285
+ export const generateMeta = meta(() => {
286
+ return {
287
+ title: {
288
+ type: 'title',
289
+ content: 'My Site',
290
+ attributes: {},
291
+ },
292
+ description: {
293
+ type: 'meta',
294
+ attributes: {
295
+ name: 'description',
296
+ content: 'My awesome site',
297
+ },
298
+ },
299
+ // manifest
300
+ manifest: {
301
+ type: 'link',
302
+ attributes: {
303
+ rel: 'manifest',
304
+ href: '/site.webmanifest',
305
+ },
306
+ },
307
+ // google fonts
308
+ 'google-font-link': {
309
+ type: 'link',
310
+ attributes: {
311
+ rel: 'preconnect',
312
+ href: 'https://fonts.googleapis.com'
313
+ }
314
+ },
315
+ 'gstatic-font-link': {
316
+ type: 'link',
317
+ attributes: {
318
+ rel: 'preconnect',
319
+ href: 'https://fonts.gstatic.com',
320
+ crossorigin: ''
321
+ }
322
+ },
323
+ 'inter-font': {
324
+ type: 'link',
325
+ attributes: {
326
+ rel: 'stylesheet',
327
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
328
+ }
329
+ },
330
+ // external js
331
+ 'analytics-script': {
332
+ type: 'script',
333
+ attributes: {
334
+ src: 'analytics.js',
335
+ defer: true,
336
+ }
337
+ }
338
+ };
339
+ });
340
+ ```
341
+
342
+ ### Middleware
343
+
344
+ Intercept and modify requests:
345
+
346
+ ```tsx
347
+ import { defineMiddleware } from 'vinxi/http';
348
+
349
+ export default defineMiddleware({
350
+ onRequest: async (request) => {
351
+ console.log('Incoming request:', request.url);
352
+ // Modify request if needed
353
+ return request;
354
+ },
355
+ });
356
+ ```
357
+
358
+ ### Page Options
359
+
360
+ Configure page-level caching:
361
+
362
+ ```tsx
363
+ export const options = {
364
+ cache: {
365
+ ttl: 60000, // Cache for 60 seconds
366
+ },
367
+ responseHeaders: { // Custom headers for pages
368
+ 'X-Custom-Header': 'MyValue',
369
+ 'Cache-Control': 'public, max-age=60', // Client-side caching
370
+ },
371
+ };
372
+ ```
373
+ - Regarding caching, setting `ttl` to `0` or omitting it will disable caching for that page.
374
+ - Setting a positive integer value will cache the page for that duration in milliseconds.
375
+ - Invalidation of cached pages can be done using the `invalidateCache` and `revalidatePath` utilities.
376
+ - The `responseHeaders` option allows you to set custom HTTP headers for the page response.
377
+
378
+ ## API Routes
379
+
380
+ Create REST endpoints:
381
+ - GET
382
+ - POST
383
+ - PUT
384
+ - DELETE
385
+ - PATCH
386
+
387
+ ```tsx
388
+ export async function GET(request: Request, { params }: any) {
389
+ const posts = await fetchPosts();
390
+ return new Response(JSON.stringify(posts), {
391
+ headers: { 'Content-Type': 'application/json' },
392
+ });
393
+ }
394
+
395
+ export async function POST(request: Request) {
396
+ const data = await request.json();
397
+ }
398
+ ```
399
+
400
+ ## Server Assets
401
+ Serve static files from the `server-assets/` directory:
402
+
403
+ ```my-app/
404
+ ├── server-assets/
405
+ │ └── secret.txt
406
+ ```
407
+
408
+ Access via `my-app/server-assets/secret.txt` URL:
409
+
410
+ ```ts
411
+ const TEMPLATE_PATH = join(process.cwd(), 'server-assets', 'templates', 'template.ejs');
412
+ const template = await fs.promises.readFile(TEMPLATE_PATH, 'utf-8');
413
+ ```
414
+
415
+ ## Utilities
416
+
417
+ ### Cache (Server-Side)
418
+ - Every page can be cached by setting the `options.cache` property in the page.
419
+ - You can also manually invalidate the cache for specific routes.
420
+ - Invalidation can be done in two ways:
421
+ 1. Using the `invalidateCache` utility to only invalidate paths.
422
+ ```tsx
423
+ import { invalidateCache } from 'solidstep/utils/cache';
424
+
425
+ const action = async () => {
426
+ 'use server';
427
+
428
+ ...
429
+
430
+ // Invalidate cache after data mutation
431
+ await invalidateCache('/some-route');
432
+
433
+ ...
434
+
435
+ return { success: true };
436
+ };
437
+ ```
438
+ 2. Using the `revalidatePath` utility to revalidate specific paths and revalidate the frontend DOM - signaling the server action as a Single Flight Mutation query.
439
+ ```tsx
440
+ import { revalidatePath } from 'solidstep/utils/cache';
441
+
442
+ const action = async () => {
443
+ 'use server';
444
+
445
+ ...
446
+
447
+ // Revalidate path after data mutation
448
+ await revalidatePath('/some-route');
449
+
450
+ ...
451
+
452
+ return { success: true };
453
+ };
454
+ ```
455
+
456
+ ### Cookies
457
+ ```tsx
458
+ import { getCookie, setCookie } from 'solidstep/utils/cookies';
459
+
460
+ export const loader = defineLoader(async () => {
461
+ const userData = await getCookie();
462
+
463
+ if (!userData) {
464
+ return [];
465
+ }
466
+
467
+ const userId = userData.id;
468
+
469
+ const { data, error } = await getDocumentsByUserId(userId);
470
+
471
+ if (error || !data) {
472
+ return [];
473
+ }
474
+
475
+ return data as Document[];
476
+ });
477
+
478
+ const action = async () => {
479
+ 'use server';
480
+
481
+ await setCookie('session', JSON.stringify({ id: 'user-id' }), { httpOnly: true, secure: true, maxAge: 3600 });
482
+
483
+ return { success: true };
484
+ };
485
+ ```
486
+
487
+ ### CORS
488
+ ```tsx
489
+ import { cors } from 'solidstep/utils/cors';
490
+
491
+ const trustedOrigins = ['https://example.com', 'https://another-example.com'];
492
+
493
+ const corsMiddleware = cors(trustedOrigins);
494
+
495
+ ...
496
+
497
+ const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
498
+
499
+ ...
500
+ ```
501
+
502
+ ### CSP
503
+ ```tsx
504
+ import { createBasePolicy, serializePolicy, withNonce } from 'solidstep/utils/csp';
505
+
506
+ let cspPolicy = createBasePolicy();
507
+
508
+ ...
509
+
510
+ cspPolicy = withNonce(cspPolicy, nonce);
511
+
512
+ ...
513
+
514
+ event.response.headers.set('Content-Security-Policy', serializePolicy(cspPolicy));
515
+
516
+ ...
517
+ ```
518
+
519
+ ### CSRF Protection
520
+ ```tsx
521
+ import { csrf } from 'solidstep/utils/csrf';
522
+
523
+ const trustedOrigins = ['https://example.com', 'https://another-example.com'];
524
+
525
+ const csrfMiddleware = csrf(trustedOrigins);
526
+
527
+ ...
528
+
529
+ const csrfResult = csrfMiddleware(
530
+ event.node.req.method,
531
+ requestUrl,
532
+ origin,
533
+ event.node.req.headers.referer
534
+ );
535
+
536
+ if (!csrfResult.success) {
537
+ event.node.res.statusCode = 403; // Forbidden
538
+ event.node.res.end(csrfResult.message);
539
+ return;
540
+ }
541
+ ```
542
+
543
+ ### Redirects
544
+ ```tsx
545
+ import { redirect } from 'solidstep/utils/redirect';
546
+
547
+ export const loader = defineLoader(async () => {
548
+ redirect('/login');
549
+ });
550
+
551
+ // or in client
552
+ export function MyComponent() {
553
+ const handleClick = () => {
554
+ redirect('/dashboard');
555
+ };
556
+
557
+ return <button onClick={handleClick}>Go to Dashboard</button>;
558
+ }
559
+ ```
560
+
561
+ ### Error Handling
562
+ ```tsx
563
+ // first define an error collection
564
+ import { createErrorFactory } from 'solidstep/utils/error-handler';
565
+
566
+ export const createError = createErrorFactory({
567
+ 'db-query-error': {
568
+ message: 'Something went wrong with the database query, not idea what',
569
+ severity: 'high',
570
+ action: (error) => {
571
+ console.error('Generic DB query error', error);
572
+ throw error;
573
+ },
574
+ },
575
+ 'auth-error': {
576
+ message: 'User authentication failed',
577
+ severity: 'high',
578
+ action: (error) => {
579
+ console.error('User authentication error', error);
580
+ throw error;
581
+ },
582
+ },
583
+ 'service-error': {
584
+ message:
585
+ 'Some service (external or internal that is interfacing with the app) failed',
586
+ severity: 'high',
587
+ action: (error) => {
588
+ console.error('Service error', error);
589
+ throw error;
590
+ },
591
+ },
592
+ });
593
+
594
+ // then use it in your loaders, actions or routes
595
+ export const loader = defineLoader(async () => {
596
+ const data = await tryCatch(fetchDataFromDB());
597
+ if (data.error) {
598
+ // handle the error using the defined error collection
599
+ createError('db-query-error').action();
600
+
601
+ // or overwrite the defaults
602
+ createError('db-query-error', {
603
+ // customize the error
604
+ message: data.error.message,
605
+ action: (error) => {
606
+ // just log it for example
607
+ console.error('Custom action for DB error', error);
608
+ },
609
+ severity: 'critical',
610
+ cause: data.error,
611
+ metadata: { query: 'SELECT * FROM users' },
612
+ }).action();
613
+
614
+ // defer the definition and the handling
615
+ const error = createError('db-query-error');
616
+ // some logic
617
+ error.action();
618
+
619
+ // or throw the error
620
+ const error = createError('db-query-error', {
621
+ cause: data.error,
622
+ });
623
+ throw error;
624
+ }
625
+ return data.result;
626
+ });
627
+ ```
628
+
629
+ ### Logging
630
+
631
+ SolidStep includes a built-in Pino logger that can be configured globally:
632
+
633
+ ```tsx
634
+ import { defineConfig } from 'solidstep';
635
+
636
+ export default defineConfig({
637
+ server: {
638
+ preset: 'node',
639
+ },
640
+ logger: {
641
+ level: 'info',
642
+ transport: {
643
+ target: 'pino-pretty', // Use pino-pretty for human-readable logs
644
+ options: {
645
+ colorize: true
646
+ }
647
+ }
648
+ }
649
+ });
650
+ ```
651
+
652
+ Use the logger in your code:
653
+
654
+ ```tsx
655
+ import { logger } from 'solidstep/utils/logger';
656
+
657
+ export const loader = defineLoader(async () => {
658
+ logger.info('Fetching posts');
659
+
660
+ try {
661
+ const posts = await fetchPosts();
662
+ logger.info(`Fetched ${posts.length} posts`);
663
+ return { posts };
664
+ } catch (error) {
665
+ logger.error('Failed to fetch posts', error);
666
+ throw error;
667
+ }
668
+ });
669
+ ```
670
+
671
+ **Logger Configuration Options:**
672
+ - `false` or `undefined` - Disables logging (silent mode)
673
+ - `true` - Enables default Pino logger
674
+ - `object` - Custom Pino configuration object [Pino Docs](https://getpino.io/#/docs/api?id=options)
675
+
676
+ ### Fetch Utilities
677
+
678
+ SolidStep provides type-safe fetch wrappers for both client and server with built-in timeout and error handling:
679
+
680
+ **Client-side Fetch:**
681
+ ```tsx
682
+ import fetch from 'solidstep/utils/fetch.client';
683
+
684
+ async function fetchPosts() {
685
+ const posts = await fetch<Post[]>('/api/posts', {
686
+ method: 'GET',
687
+ MAX_FETCH_TIME: 5000,
688
+ });
689
+
690
+ return posts;
691
+ }
692
+
693
+ ...
694
+
695
+ // To get full response including status, headers, etc.
696
+ const response = await fetch<Post[], false>(
697
+ '/api/posts',
698
+ { method: 'GET' },
699
+ false
700
+ );
701
+
702
+ console.log(response.status); // HTTP status code
703
+ ```
704
+
705
+ **Server-side Fetch:**
706
+ ```tsx
707
+ import fetch from 'solidstep/utils/fetch.server';
708
+
709
+ export const loader = defineLoader(async () => {
710
+ const data = await fetch<ApiResponse>('https://api.example.com/data', {
711
+ method: 'POST',
712
+ body: JSON.stringify({ query: 'test' }),
713
+ headers: {
714
+ 'Content-Type': 'application/json',
715
+ },
716
+ MAX_FETCH_TIME: 10000,
717
+ });
718
+
719
+ return data;
720
+ });
721
+ ```
722
+
723
+ **Features:**
724
+ - Automatic timeout handling with AbortController (default: 4000ms)
725
+ - Automatic JSON parsing (optional)
726
+ - Error handling for HTTP 4xx/5xx responses
727
+ - Type-safe responses with TypeScript generics
728
+ - Server-side uses undici for better performance
729
+
730
+ ### Server-Only Code
731
+
732
+ Ensure code only runs on the server and throws an error if accessed on the client:
733
+
734
+ ```tsx
735
+ import 'solidstep/utils/server-only';
736
+
737
+ export const SECRET_KEY = process.env.SECRET_KEY;
738
+ export const DATABASE_URL = process.env.DATABASE_URL;
739
+
740
+ export async function queryDatabase(query: string) {
741
+ }
742
+ ```
743
+
744
+ **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).
745
+
746
+ ```tsx
747
+ import 'solidstep/utils/server-only';
748
+
749
+ export const db = createDatabaseConnection(process.env.DATABASE_URL);
750
+ ```
751
+
752
+ If accidentally imported on the client, it will throw:
753
+ ```
754
+ Error: This module is only available on the server side.
755
+ ```
756
+
757
+ ## Preloading/prefetching strategies
758
+ 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.
759
+
760
+ Some common strategies include:
761
+ - **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.
762
+ - **Using Intersection Observer**: Implement lazy loading and prefetching of data when certain elements come into the viewport.
763
+ - **Using [instant.page](https://instant.page/)**: A small library that preloads pages on hover or touchstart events.
764
+ ```tsx
765
+ export const RootLayout = (props) => {
766
+ return (
767
+ <body>
768
+ ...
769
+ <NoHydration>
770
+ <script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
771
+ </NoHydration>
772
+ </body>
773
+ );
774
+ };
775
+ ```
776
+ - **Using [Foresight.js](https://foresightjs.com/)**: A library that preloads pages based on user behavior and patterns.
777
+ ```tsx
778
+ import { ForesightManager } from "js.foresight";
779
+ import { onMount } from "solid-js";
780
+
781
+ export const RootLayout = (props) => {
782
+ onMount(() => {
783
+ ForesightManager.initialize({
784
+ // Configuration options
785
+ });
786
+ });
787
+ return (
788
+ <body>
789
+ ...
790
+ </body>
791
+ );
792
+ };
793
+ ```
794
+ - **Custom Preloading Logic**: Write custom logic to preload data for specific routes or components based on user behavior or application state.
795
+
796
+ ## Fonts
797
+ Install fonts (for example, from [Fontsource](https://github.com/fontsource/fontsource)) and import into `globals.css` example:
798
+ ```css
799
+ @import '@fontsource-variable/dm-sans';
800
+ @import '@fontsource-variable/jetbrains-mono';
801
+
802
+ @theme inline {
803
+ --font-sans: 'DM Sans Variable', sans-serif;
804
+ --font-mono: 'JetBrains Mono Variable', monospace;
805
+ /* ... */
806
+ }
807
+ ```
808
+
809
+ ## Images
810
+ Use the package called [Unpic](https://unpic.pics/img/solid/) for images. An open source and powerful tool for images on the web.
811
+ ```bash
812
+ [npm | yarn | pnpm | bun] install @unpic/solid
813
+ ```
814
+
815
+ ```tsx
816
+ import type { Component } from "solid-js";
817
+ import { Image } from "@unpic/solid";
818
+
819
+ const MyComponent: Component = () => {
820
+ return (
821
+ <Image
822
+ src="https://cdn.shopify.com/static/sample-images/bath_grande_crop_center.jpeg"
823
+ layout="constrained"
824
+ width={800}
825
+ height={600}
826
+ alt="A lovely bath"
827
+ />
828
+ );
829
+ };
830
+ ```
831
+
832
+ ## Environment Variables
833
+ 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.
834
+
835
+ ## Future Plans
836
+ - Support for dynamic site.webmanifest, robots.txt, sitemap.xml, manifest.json, and llms.txt
837
+ - Support loading and error pages for parallel routes
838
+ - Support deferring loaders
839
+ - Possible SSG, ISR, and PPR
840
+ - Advanced caching strategies
841
+ - WebSocket support
842
+
843
+ ## Testing
844
+
845
+ 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.
846
+
847
+ ### Testing Server Actions
848
+
849
+ When testing server actions, you can use Vitest to accomplish this. Just test as you would with any other async function.
850
+
851
+ 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.
852
+
853
+ ## License
854
+
855
+ MIT
856
+
857
+ ## Links
858
+
859
+ - [GitHub](https://github.com/HamzaKV/solidstep)
860
+ - [SolidJS Documentation](https://www.solidjs.com/)
861
+
862
+ ## Special Mentions
863
+ - Inspired by [Remix](https://remix.run/), [Next.js](https://nextjs.org/), and [TanStack](https://tanstack.com/)
864
+ - 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)