solidstep 0.1.0 → 0.1.2

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,18 +1,535 @@
1
- # @varlabs/create-solidstep
2
- Next Step SolidJS CLI for building web applications.
3
-
4
- ## Getting Started
5
-
6
- To get started with a new SolidJS project using the Next Step CLI, run the following command:
7
-
8
- ```bash
9
- npx @varlabs/create-solidstep@latest my-app
10
- ```
11
-
12
- or
13
-
14
- ```bash
15
- pnpm dlx @varlabs/create-solidstep@latest my-app
16
- ```
17
-
18
- Replace `my-app` with the name of your project or if you don't specify a name, it will create a project in the current directory.
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
+ - 🚀 **File-based Routing** - Automatic routing based on your file structure
8
+ - ⚡ **Server-Side Rendering (SSR)** - Fast initial page loads with full SSR support
9
+ - 🔄 **Data Loading** - Built-in loaders for efficient data fetching
10
+ - 🎨 **Layouts & Groups** - Nested layouts and parallel route groups
11
+ - 🛡️ **Security First** - Built-in CSP, CORS, CSRF, and cookie utilities
12
+ - 🎯 **Server Actions** - Type-safe server functions with automatic serialization
13
+ - ⚙️ **Middleware Support** - Request/response interceptors
14
+ - 📦 **Caching** - Built-in page-level caching
15
+ - 🔥 **Hot Module Replacement** - Fast development experience
16
+ - 📝 **TypeScript** - Full TypeScript support out of the box
17
+
18
+ ## Getting Started
19
+
20
+ ### Create a New Project
21
+
22
+ ```bash
23
+ [npx | yarn dlx | pnpm dlx | bunx] @varlabs/create-solidstep@latest my-app
24
+ cd my-app
25
+ [npm | yarn | pnpm | bun] install
26
+ [npm | yarn | pnpm | bun] run dev
27
+ ```
28
+
29
+ ### Special Files
30
+
31
+ - `page.tsx` - Page component
32
+ - `layout.tsx` - Layout wrapper
33
+ - `loading.tsx` - Loading state (Streaming - optional)
34
+ - `error.tsx` - Error boundary (optional)
35
+ - `not-found.tsx` - 404 page (root only - optional)
36
+ - `route.ts` - API route handler
37
+ - `middleware.ts` - Request middleware
38
+
39
+ **A route is defined by either the presence of a `page.tsx` or `route.ts` file in a directory.**
40
+
41
+ ### Configuration
42
+
43
+ Configure your app in `app.config.ts`:
44
+
45
+ ```tsx
46
+ import { defineConfig } from 'solidstep';
47
+
48
+ export default defineConfig({
49
+ server: {
50
+ preset: 'node',
51
+ },
52
+ plugins: [
53
+ {
54
+ type: 'both',
55
+ plugin: myVitePlugin(),
56
+ },
57
+ ],
58
+ });
59
+ ```
60
+
61
+ ### Project Structure
62
+
63
+ ```
64
+ my-app/
65
+ ├── app/
66
+ │ ├── page.tsx # Home page (/)
67
+ │ ├── layout.tsx # Root layout
68
+ │ ├── middleware.ts # Request middleware
69
+ │ ├── about/
70
+ │ │ └── page.tsx # About page (/about)
71
+ │ ├── (admin)/
72
+ │ | └── dashboard/
73
+ │ | └── page.tsx # Group route (/dashboard)
74
+ │ └── blog/
75
+ │ ├── layout.tsx # Blog layout
76
+ │ ├── page.tsx # Blog index (/blog)
77
+ │ └── [slug]/
78
+ │ └── page.tsx # Dynamic route (/blog/:slug)
79
+ ├── public/
80
+ │ └── favicon.ico
81
+ ├── app.config.ts
82
+ └── package.json
83
+ ```
84
+
85
+ ## Core Concepts
86
+
87
+ ### Layouts
88
+
89
+ Wrap multiple pages with shared UI:
90
+
91
+ ```tsx
92
+ export default function BlogLayout(props: { children: any }) {
93
+ return (
94
+ <div>
95
+ <nav>Blog Navigation</nav>
96
+ {props.children()}
97
+ </div>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Pages
103
+
104
+ Create a `page.tsx` file in any directory under `app/` to define a route:
105
+
106
+ ```tsx
107
+ export default function HomePage() {
108
+ return <h1>Welcome to SolidStep!</h1>;
109
+ }
110
+ ```
111
+
112
+ **Similar to NextJS, only content returned by a `page` or `route` is sent to the client**
113
+
114
+ ### Group Routes
115
+ Use parentheses to group routes without affecting the URL:
116
+
117
+ ```app/
118
+ ├── (admin)/
119
+ │ └── dashboard/
120
+ │ └── page.tsx // matches /dashboard
121
+ └── (user)/
122
+ └── profile/
123
+ └── page.tsx // matches /profile
124
+ ```
125
+
126
+ ### Dynamic Routes
127
+
128
+ Use square brackets for dynamic segments:
129
+
130
+ ```tsx
131
+ // app/blog/[slug]/page.tsx - matches /blog/my-post, /blog/another-post, etc.
132
+
133
+ export default function BlogPost(props: { routeParams: { slug: string } }) {
134
+ return <h1>Post: {props.routeParams.slug}</h1>;
135
+ }
136
+ ```
137
+
138
+ **Catch-all routes:**
139
+ ```tsx
140
+ // app/docs/[...path]/page.tsx - matches /docs/a, /docs/a/b, etc.
141
+ ```
142
+
143
+ **Catch-all routes (Optional):**
144
+ ```tsx
145
+ // app/docs/[[...path]]/page.tsx - matches /docs, /docs/a, /docs/a/b, etc.
146
+ ```
147
+
148
+ ### Parallel Routes (Groups)
149
+
150
+ Render multiple sections simultaneously:
151
+
152
+ ```
153
+ app/
154
+ ├── layout.tsx
155
+ ├── page.tsx
156
+ └── @graph1/
157
+ └── page.tsx
158
+ └── @graph2/
159
+ └── page.tsx
160
+ ```
161
+
162
+ ```tsx
163
+ export default function RootLayout(props: {
164
+ children: any;
165
+ slots: { graph1: any; graph2: any; };
166
+ }) {
167
+ return (
168
+ <div>
169
+ <main>
170
+ {props.children()}
171
+ <aside>
172
+ <div>{props.slots.graph1()}</div>
173
+ <div>{props.slots.graph2()}</div>
174
+ </aside>
175
+ </main>
176
+ </div>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ### Data Loading
182
+
183
+ Use `defineLoader` to fetch data on the server:
184
+
185
+ ```tsx
186
+ import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
187
+
188
+ export const loader = defineLoader(async (request) => {
189
+ const posts = await fetchPosts();
190
+ return { posts };
191
+ });
192
+
193
+ type LoaderData = LoaderDataFromFunction<typeof loader>;
194
+
195
+ export default function BlogPage(props: { loaderData: LoaderData }) {
196
+ return (
197
+ <ul>
198
+ <For each={props.loaderData.posts}>
199
+ {(post) => <li>{post.title}</li>}
200
+ </For>
201
+ </ul>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ### Server Actions
207
+
208
+ Create type-safe server functions:
209
+
210
+ ```tsx
211
+ 'use server';
212
+
213
+ export const createPost = async (data: { title: string }) => {
214
+ await db.posts.create(data);
215
+ return { success: true };
216
+ };
217
+ ```
218
+
219
+ Call from client:
220
+
221
+ ```tsx
222
+ import { createPost } from './actions';
223
+
224
+ function CreatePostForm() {
225
+ const handleSubmit = async (e: Event) => {
226
+ e.preventDefault();
227
+ await createPost({ title: 'My Post' });
228
+ };
229
+
230
+ return <form onSubmit={handleSubmit}>...</form>;
231
+ }
232
+ ```
233
+
234
+ ### Metadata
235
+
236
+ Define metadata for SEO:
237
+
238
+ ```tsx
239
+ import type { Meta } from 'solidstep/utils/types';
240
+
241
+ export const generateMeta = async () => {
242
+ return {
243
+ title: {
244
+ type: 'title',
245
+ content: 'My Site',
246
+ attributes: {},
247
+ },
248
+ description: {
249
+ type: 'meta',
250
+ attributes: {
251
+ name: 'description',
252
+ content: 'My awesome site',
253
+ },
254
+ },
255
+ // manifest
256
+ manifest: {
257
+ type: 'link',
258
+ attributes: {
259
+ rel: 'manifest',
260
+ href: '/site.webmanifest',
261
+ },
262
+ },
263
+ // google fonts
264
+ 'google-font-link': {
265
+ type: 'link',
266
+ attributes: {
267
+ rel: 'preconnect',
268
+ href: 'https://fonts.googleapis.com'
269
+ }
270
+ },
271
+ 'gstatic-font-link': {
272
+ type: 'link',
273
+ attributes: {
274
+ rel: 'preconnect',
275
+ href: 'https://fonts.gstatic.com',
276
+ crossorigin: ''
277
+ }
278
+ },
279
+ 'inter-font': {
280
+ type: 'link',
281
+ attributes: {
282
+ rel: 'stylesheet',
283
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
284
+ }
285
+ },
286
+ // external js
287
+ 'analytics-script': {
288
+ type: 'script',
289
+ attributes: {
290
+ src: 'analytics.js',
291
+ defer: true,
292
+ }
293
+ }
294
+ } satisfies Meta;
295
+ };
296
+ ```
297
+
298
+ ### Middleware
299
+
300
+ Intercept and modify requests:
301
+
302
+ ```tsx
303
+ import { eventHandler } from 'vinxi/http';
304
+
305
+ export default eventHandler((event) => {
306
+ event.locals = { user: getCurrentUser() };
307
+
308
+ // you can also modify request/response here
309
+ // also include cors, csrf, csp logic if needed
310
+ });
311
+ ```
312
+
313
+ ### Page Options
314
+
315
+ Configure page-level caching:
316
+
317
+ ```tsx
318
+ export const options = {
319
+ cache: {
320
+ ttl: 60000,
321
+ },
322
+ };
323
+ ```
324
+
325
+ ## API Routes
326
+
327
+ Create REST endpoints:
328
+
329
+ ```tsx
330
+ export async function GET(request: Request, { params }: any) {
331
+ const posts = await fetchPosts();
332
+ return new Response(JSON.stringify(posts), {
333
+ headers: { 'Content-Type': 'application/json' },
334
+ });
335
+ }
336
+
337
+ export async function POST(request: Request) {
338
+ const data = await request.json();
339
+ }
340
+ ```
341
+
342
+ ## Utilities
343
+
344
+ ### Cookies
345
+ ```tsx
346
+ import { getCookie, setCookie } from 'solidstep/utils/cookies';
347
+
348
+ export const loader = defineLoader(async () => {
349
+ const userData = await getCookie();
350
+
351
+ if (!userData) {
352
+ return [];
353
+ }
354
+
355
+ const userId = userData.id;
356
+
357
+ const { data, error } = await getDocumentsByUserId(userId);
358
+
359
+ if (error || !data) {
360
+ return [];
361
+ }
362
+
363
+ return data as Document[];
364
+ });
365
+
366
+ const action = async () => {
367
+ 'use server';
368
+
369
+ await setCookie('session', JSON.stringify({ id: 'user-id' }), { httpOnly: true, secure: true, maxAge: 3600 });
370
+
371
+ return { success: true };
372
+ };
373
+ ```
374
+
375
+ ### CORS
376
+ ```tsx
377
+ import { cors } from 'solidstep/utils/cors';
378
+
379
+ const trustedOrigins = ['https://example.com', 'https://another-example.com'];
380
+
381
+ const corsMiddleware = cors(trustedOrigins);
382
+
383
+ ...
384
+
385
+ const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
386
+
387
+ ...
388
+ ```
389
+
390
+ ### CSP
391
+ ```tsx
392
+ import { createBasePolicy, serializePolicy, withNonce } from '@varlabs/solidstep/utils/csp';
393
+
394
+ let cspPolicy = createBasePolicy();
395
+
396
+ ...
397
+
398
+ cspPolicy = withNonce(cspPolicy, nonce);
399
+
400
+ ...
401
+
402
+ event.response.headers.set('Content-Security-Policy', serializePolicy(cspPolicy));
403
+
404
+ ...
405
+ ```
406
+
407
+ ### CSRF Protection
408
+ ```tsx
409
+ import { csrf } from '@varlabs/solidstep/utils/csrf';
410
+
411
+ const trustedOrigins = ['https://example.com', 'https://another-example.com'];
412
+
413
+ const csrfMiddleware = csrf(trustedOrigins);
414
+
415
+ ...
416
+
417
+ const csrfResult = csrfMiddleware(
418
+ event.node.req.method,
419
+ requestUrl,
420
+ origin,
421
+ event.node.req.headers.referer
422
+ );
423
+
424
+ if (!csrfResult.success) {
425
+ event.node.res.statusCode = 403; // Forbidden
426
+ event.node.res.end(csrfResult.message);
427
+ return;
428
+ }
429
+ ```
430
+
431
+ ### Redirects
432
+ ```tsx
433
+ import { redirect } from 'solidstep/utils/redirect';
434
+
435
+ export const loader = defineLoader(async () => {
436
+ redirect('/login');
437
+ });
438
+
439
+ // or in client
440
+ export function MyComponent() {
441
+ const handleClick = () => {
442
+ redirect('/dashboard');
443
+ };
444
+
445
+ return <button onClick={handleClick}>Go to Dashboard</button>;
446
+ }
447
+ ```
448
+
449
+ ### Error Handling
450
+ ```tsx
451
+ // first define an error collection
452
+ import { createErrorFactory } from 'solidstep/utils/error-handler';
453
+
454
+ export const createError = createErrorFactory({
455
+ 'db-query-error': {
456
+ message: 'Something went wrong with the database query, not idea what',
457
+ severity: 'high',
458
+ action: (error) => {
459
+ console.error('Generic DB query error', error);
460
+ throw error;
461
+ },
462
+ },
463
+ 'auth-error': {
464
+ message: 'User authentication failed',
465
+ severity: 'high',
466
+ action: (error) => {
467
+ console.error('User authentication error', error);
468
+ throw error;
469
+ },
470
+ },
471
+ 'service-error': {
472
+ message:
473
+ 'Some service (external or internal that is interfacing with the app) failed',
474
+ severity: 'high',
475
+ action: (error) => {
476
+ console.error('Service error', error);
477
+ throw error;
478
+ },
479
+ },
480
+ });
481
+
482
+ // then use it in your loaders, actions or routes
483
+ export const loader = defineLoader(async () => {
484
+ const data = await tryCatch(fetchDataFromDB());
485
+ if (data.error) {
486
+ // handle the error using the defined error collection
487
+ createError('db-query-error').action();
488
+
489
+ // or overwrite the defaults
490
+ createError('db-query-error', {
491
+ // customize the error
492
+ message: data.error.message,
493
+ action: (error) => {
494
+ // just log it for example
495
+ console.error('Custom action for DB error', error);
496
+ },
497
+ severity: 'critical',
498
+ cause: data.error,
499
+ metadata: { query: 'SELECT * FROM users' },
500
+ }).action();
501
+
502
+ // defer the definition and the handling
503
+ const error = createError('db-query-error');
504
+ // some logic
505
+ error.action();
506
+
507
+ // or throw the error
508
+ const error = createError('db-query-error', {
509
+ cause: data.error,
510
+ });
511
+ throw error;
512
+ }
513
+ return data.result;
514
+ });
515
+ ```
516
+
517
+ ## Future Plans
518
+ - Support for dynamic site.webmanifest, robots.txt, sitemap.xml, manifest.json, and llms.txt
519
+ - Support loading and error pages for parallel routes
520
+ - Support deferring loaders
521
+ - Image/font optimizations
522
+ - Possible CSR/SPA, SSG, ISR, and PPR
523
+
524
+ ## License
525
+
526
+ MIT
527
+
528
+ ## Links
529
+
530
+ - [GitHub](https://github.com/HamzaKV/solidstep)
531
+ - [SolidJS Documentation](https://www.solidjs.com/)
532
+
533
+ ## Special Mentions
534
+ - Inspired by [Remix](https://remix.run/), [Next.js](https://nextjs.org/), and [TanStack](https://tanstack.com/)
535
+ - Built with [Vite](https://vitejs.dev/), [SolidJS](https://www.solidjs.com/), [Vinxi](https://github.com/nksaraf/vinxi) and [Seroval](https://github.com/lxsmnsyc/seroval)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidstep",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Next Step SolidJS Framework for building web applications.",
5
5
  "type": "module",
6
6
  "author": "HamzaKV <hamzakv333@gmail.com>",
@@ -45,14 +45,24 @@
45
45
  "@vinxi/server-functions": "0.5.1",
46
46
  "seroval": "^1.3.2",
47
47
  "seroval-plugins": "^1.3.2",
48
- "solid-js": "^1.9.7",
49
48
  "undici": "^7.15.0",
50
- "vinxi": "^0.5.8"
49
+ "vite-plugin-solid": "^2.11.7"
51
50
  },
52
51
  "devDependencies": {
53
52
  "copyfiles": "^2.4.1",
54
53
  "rimraf": "^6.0.1",
54
+ "solid-js": "^1.9.7",
55
55
  "typescript": "^5.8.3",
56
- "vite-plugin-solid": "^2.11.7"
56
+ "vinxi": "^0.5.8"
57
+ },
58
+ "peerDependencies": {
59
+ "solid-js": "^1.9.7",
60
+ "vinxi": "^0.5.8"
61
+ },
62
+ "engines": {
63
+ "node": ">=20"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
57
67
  }
58
68
  }
package/utils/cors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const cors: (trustedOrigins: string[]) => (origin: string, isPreflight: boolean) => {
1
+ export declare const cors: (trustedOrigins: string[], allowMethods?: string[], allowHeaders?: string[]) => (origin: string, isPreflight: boolean) => {
2
2
  'Access-Control-Allow-Origin': string;
3
3
  'Access-Control-Allow-Methods': string;
4
4
  'Access-Control-Allow-Headers': string;
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../utils/cors.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,GAAI,gBAAgB,MAAM,EAAE,MAAM,QAAQ,MAAM,EAAE,aAAa,OAAO;;;;;;;;;;;;CActF,CAAA"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../utils/cors.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,GACb,gBAAgB,MAAM,EAAE,EACxB,eAAc,MAAM,EAAyD,EAC7E,eAAc,MAAM,EAAsC,MACxD,QAAQ,MAAM,EAAE,aAAa,OAAO;;;;;;;;;;;;CAczC,CAAA"}
package/utils/cors.js CHANGED
@@ -1,10 +1,10 @@
1
- export const cors = (trustedOrigins) => (origin, isPreflight) => {
1
+ export const cors = (trustedOrigins, allowMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders = ['Content-Type', 'Authorization']) => (origin, isPreflight) => {
2
2
  if (trustedOrigins.includes(origin)) {
3
3
  if (isPreflight) {
4
4
  return {
5
5
  'Access-Control-Allow-Origin': origin,
6
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
7
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
6
+ 'Access-Control-Allow-Methods': allowMethods.join(', '),
7
+ 'Access-Control-Allow-Headers': allowHeaders.join(', '),
8
8
  };
9
9
  }
10
10
  return {
package/utils/csrf.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const csrf: (trustedOrigins: string[]) => (requestMethod: string, requestUrl: URL, origin?: string, referer?: string) => {
1
+ export declare const csrf: (trustedOrigins: string[], safeMethods?: string[]) => (requestMethod: string, requestUrl: URL, origin?: string, referer?: string) => {
2
2
  success: boolean;
3
3
  message: string;
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../utils/csrf.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,GAAI,gBAAgB,MAAM,EAAE,MAErC,eAAe,MAAM,EACrB,YAAY,GAAG,EACf,SAAS,MAAM,EACf,UAAU,MAAM;;;CAsDnB,CAAC"}
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../utils/csrf.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,GACb,gBAAgB,MAAM,EAAE,EACxB,cAAa,MAAM,EAAiB,MAGhC,eAAe,MAAM,EACrB,YAAY,GAAG,EACf,SAAS,MAAM,EACf,UAAU,MAAM;;;CAsDnB,CAAC"}
package/utils/csrf.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const SAFE_METHODS = ['GET', 'OPTIONS', 'HEAD', 'TRACE'];
2
- export const csrf = (trustedOrigins) => (requestMethod, requestUrl, origin, referer) => {
2
+ export const csrf = (trustedOrigins, safeMethods = SAFE_METHODS) => (requestMethod, requestUrl, origin, referer) => {
3
3
  // Check if the request method is safe
4
- if (!SAFE_METHODS.includes(requestMethod)) {
4
+ if (!safeMethods.includes(requestMethod)) {
5
5
  // If we have an Origin header, check it against our allowlist.
6
6
  if (origin) {
7
7
  const parsedOrigin = new URL(origin);