solidstep 0.3.4 → 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 +864 -829
- package/package.json +1 -1
- package/server.d.ts.map +1 -1
- package/server.js +4 -0
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
|
-
##
|
|
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
|
+
|
|
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)
|