@varlabs/create-solidstep 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ import { defineInstrumentation } from 'solidstep/utils/instrumentation';
2
+
3
+ export default defineInstrumentation({
4
+ async register() {
5
+ // Called once at server startup.
6
+ // Initialize your telemetry SDK here (e.g., OpenTelemetry, Sentry).
7
+ console.log('[instrumentation] Server starting...');
8
+ },
9
+
10
+ async onRequest(request, context) {
11
+ // Called before each request is processed.
12
+ // context.metadata is a mutable object you can use to pass data between hooks.
13
+ context.metadata.requestId = crypto.randomUUID();
14
+ console.log(
15
+ `[instrumentation] ${request.method} ${context.pathname} (${context.routeType})`,
16
+ );
17
+ },
18
+
19
+ async onResponseEnd(request, context) {
20
+ // Called after the response stream is complete.
21
+ console.log(
22
+ `[instrumentation] ${context.statusCode} ${context.pathname} ${context.duration.toFixed(1)}ms`,
23
+ );
24
+ },
25
+
26
+ async onRequestError(error, request, context) {
27
+ // Called when an unhandled error occurs during request processing.
28
+ console.error(
29
+ `[instrumentation] Error in ${context.pathname}:`,
30
+ error.message,
31
+ );
32
+ },
33
+ });
@@ -1,39 +1,37 @@
1
- import type { Component, JSX } from 'solid-js';
2
- import './globals.css';
3
-
4
- export const generateMeta = () => ({
5
- 'title': {
6
- type: 'title',
7
- attributes: {},
8
- content: 'SolidStep App'
9
- },
10
- 'description': {
11
- type: 'meta',
12
- attributes: {
13
- name: 'description',
14
- content: 'This is simple SolidStep application.'
15
- },
16
- },
17
- 'favicon': {
18
- type: 'link',
19
- attributes: {
20
- rel: 'icon',
21
- href: '/favicon-32x32.png',
22
- type: 'image/png'
23
- }
24
- },
25
- });
26
-
27
- const Layout: Component<{
28
- children: JSX.Element;
29
- }> = ({
30
- children,
31
- }) => {
32
- return (
33
- <body>
34
- {children}
35
- </body>
36
- );
37
- }
38
-
39
- export default Layout;
1
+ import type { Component, JSX } from 'solid-js';
2
+ import './globals.css';
3
+
4
+ export const generateMeta = () => ({
5
+ 'title': {
6
+ type: 'title',
7
+ attributes: {},
8
+ content: 'SolidStep App'
9
+ },
10
+ 'description': {
11
+ type: 'meta',
12
+ attributes: {
13
+ name: 'description',
14
+ content: 'This is a simple SolidStep application.'
15
+ },
16
+ },
17
+ 'favicon': {
18
+ type: 'link',
19
+ attributes: {
20
+ rel: 'icon',
21
+ href: '/favicon-32x32.png',
22
+ type: 'image/png'
23
+ }
24
+ },
25
+ });
26
+
27
+ const Layout: Component<{
28
+ children: () => JSX.Element;
29
+ }> = (props) => {
30
+ return (
31
+ <body>
32
+ {props.children()}
33
+ </body>
34
+ );
35
+ };
36
+
37
+ export default Layout;
@@ -1,49 +1,66 @@
1
- import { defineMiddleware } from 'vinxi/http';
2
- import { csp } from '../utils/csp';
3
- import { cors } from '../utils/cors';
4
- import { csrf } from '../utils/csrf';
5
-
6
- const trustedOrigins = ['https://example.com', 'https://another-example.com'];
7
-
8
- const corsMiddleware = cors(trustedOrigins);
9
- const csrfMiddleware = csrf(trustedOrigins);
10
-
11
- const middleware = defineMiddleware({
12
- onRequest: async (event) => {
13
- event.node.res.setHeader('Content-Security-Policy', csp);
14
- event.node.res.setHeader('Vary', 'Origin, Access-Control-Request-Method');
15
-
16
- const origin = event.node.req.headers.origin || '';
17
- const requestUrl = new URL(event.node.req.url, `http://${event.node.req.headers.host || 'localhost'}`);
18
-
19
- const csrfResult = csrfMiddleware(
20
- event.node.req.method,
21
- requestUrl,
22
- origin,
23
- event.node.req.headers.referer
24
- );
25
-
26
- if (!csrfResult.success) {
27
- event.node.res.statusCode = 403; // Forbidden
28
- event.node.res.end(csrfResult.message);
29
- return;
30
- }
31
-
32
- if (origin) {
33
- const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
34
- for (const [key, value] of Object.entries(corsHeaders)) {
35
- event.node.res.setHeader(key, value);
36
- }
37
- if (
38
- event.node.req.method === 'OPTIONS'
39
- && event.node.req.headers['access-control-request-method']
40
- ) {
41
- event.node.res.statusCode = 204; // No Content for preflight requests
42
- event.node.res.end();
43
- return;
44
- }
45
- }
46
- },
47
- });
48
-
49
- export default middleware;
1
+ import { defineMiddleware, type Middleware } from 'solidstep/utils/middleware';
2
+ import { createBasePolicy, serializePolicy, withNonce } from 'solidstep/utils/csp';
3
+ import { cors } from 'solidstep/utils/cors';
4
+ import { csrf } from 'solidstep/utils/csrf';
5
+ import { randomBytes } from 'node:crypto';
6
+
7
+ const trustedOrigins = ['https://example.com', 'https://another-example.com'];
8
+
9
+ const corsMiddleware = cors(trustedOrigins);
10
+ const csrfMiddleware = csrf(trustedOrigins);
11
+
12
+ // Logs every request. A lightweight composable unit.
13
+ const logger: Middleware = {
14
+ onRequest: (event) => {
15
+ console.log(`[req] ${event.node.req.method} ${event.path}`);
16
+ },
17
+ };
18
+
19
+ // Sets a per-request CSP nonce, enforces CSRF on unsafe methods, and applies
20
+ // CORS for trusted origins. Short-circuits the chain by returning a Response.
21
+ const security: Middleware = {
22
+ onRequest: (event) => {
23
+ const nonce = randomBytes(16).toString('base64');
24
+ (event as any).locals = { cspNonce: nonce };
25
+
26
+ let policy = createBasePolicy();
27
+ policy = withNonce(policy, nonce);
28
+ event.node.res.setHeader('Content-Security-Policy', serializePolicy(policy));
29
+ event.node.res.setHeader('Vary', 'Origin, Access-Control-Request-Method');
30
+
31
+ const origin = (event.node.req.headers.origin as string) || '';
32
+ const protocol = origin.startsWith('https') ? 'https' : 'http';
33
+ const requestUrl = new URL(
34
+ event.node.req.url || '/',
35
+ `${protocol}://${event.node.req.headers.host || 'localhost'}`,
36
+ );
37
+
38
+ const csrfResult = csrfMiddleware(
39
+ event.node.req.method || 'GET',
40
+ requestUrl,
41
+ origin,
42
+ event.node.req.headers.referer,
43
+ );
44
+ if (!csrfResult.success) {
45
+ return new Response(csrfResult.message, { status: 403 });
46
+ }
47
+
48
+ if (origin) {
49
+ const corsHeaders = corsMiddleware(
50
+ origin,
51
+ event.node.req.method === 'OPTIONS',
52
+ );
53
+ for (const [key, value] of Object.entries(corsHeaders)) {
54
+ event.node.res.setHeader(key, value);
55
+ }
56
+ if (
57
+ event.node.req.method === 'OPTIONS' &&
58
+ event.node.req.headers['access-control-request-method']
59
+ ) {
60
+ return new Response(null, { status: 204 });
61
+ }
62
+ }
63
+ },
64
+ };
65
+
66
+ export default defineMiddleware([logger, security]);
@@ -1,40 +1,37 @@
1
- import { defineLoader, type LoaderDataFromFunction } from '../utils/loader';
2
- import { NoHydration } from 'solid-js/web';
3
-
4
- export const loader = defineLoader(async () => {
5
- const response = await fetch('https://jsonplaceholder.typicode.com/todos/2');
6
- if (!response.ok) {
7
- throw new Error('Failed to fetch data');
8
- }
9
- console.log('Fetching data from API...');
10
- const data = await response.json() as Promise<{ userId: number; id: number; title: string; completed: boolean }>;
11
- return data;
12
- });
13
-
14
- export const generateMeta = () => ({
15
- 'title': {
16
- type: 'title',
17
- attributes: {},
18
- content: 'SolidStep Example Main Page'
19
- },
20
- });
21
-
22
- type LoaderData = LoaderDataFromFunction<typeof loader>;
23
-
24
- const Page = ({
25
- loaderData
26
- }: {
27
- loaderData: LoaderData;
28
- }) => {
29
- return (
30
- <div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
31
- <NoHydration>
32
- <p class="text-lg text-gray-700">ID: {loaderData.id}</p>
33
- </NoHydration>
34
- <h1 class="text-4xl font-bold mb-4">Welcome to My App</h1>
35
- <p class="text-lg text-gray-700">This is a simple Next.js application.</p>
36
- </div>
37
- );
38
- };
39
-
40
- export default Page;
1
+ import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
2
+
3
+ export const loader = defineLoader(async () => {
4
+ const response = await fetch('https://jsonplaceholder.typicode.com/todos/2');
5
+ if (!response.ok) {
6
+ throw new Error('Failed to fetch data');
7
+ }
8
+ const data = (await response.json()) as {
9
+ userId: number;
10
+ id: number;
11
+ title: string;
12
+ completed: boolean;
13
+ };
14
+ return data;
15
+ });
16
+
17
+ export const generateMeta = () => ({
18
+ 'title': {
19
+ type: 'title',
20
+ attributes: {},
21
+ content: 'SolidStep Main Page'
22
+ },
23
+ });
24
+
25
+ type LoaderData = LoaderDataFromFunction<typeof loader>;
26
+
27
+ const Page = (props: { loaderData: LoaderData }) => {
28
+ return (
29
+ <main>
30
+ <h1>Welcome to SolidStep</h1>
31
+ <p>Edit <code>app/page.tsx</code> to get started.</p>
32
+ <p>Loaded todo #{props.loaderData.id}: {props.loaderData.title}</p>
33
+ </main>
34
+ );
35
+ };
36
+
37
+ export default Page;
@@ -1,69 +1,18 @@
1
- import { createApp } from 'vinxi';
2
- import solid from 'vite-plugin-solid';
3
- import { serverFunctions } from '@vinxi/server-functions/plugin';
4
- import { ServerRouter, ClientRouter } from './utils/router';
5
- import path from 'path';
6
- import { dirname } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
-
11
- export default createApp({
12
- server: {
13
- experimental: {
14
- asyncContext: true,
15
- },
16
- },
17
- routers: [
18
- {
19
- name: 'public',
20
- type: 'static',
21
- dir: './public',
22
- base: '/',
23
- },
24
- {
25
- name: 'client',
26
- type: 'client',
27
- target: 'browser',
28
- handler: './client.ts',
29
- plugins: () => [serverFunctions.client(), solid({ ssr: true })],
30
- base: '/_build',
31
- routes: (router, app) => {
32
- return new ClientRouter(
33
- {
34
- dir: path.join(__dirname, 'app'),
35
- extensions: ['jsx', 'js', 'tsx', 'ts'],
36
- },
37
- router,
38
- app
39
- );
40
- }
41
- },
42
- {
43
- name: 'ssr',
44
- type: 'http',
45
- base: '/',
46
- handler: './server.ts',
47
- target: 'server',
48
- plugins: () => [
49
- serverFunctions.server(),
50
- solid({ ssr: true })
51
- ],
52
- // link: {
53
- // client: 'client',
54
- // },
55
- middleware: './app/middleware.ts',
56
- routes: (router, app) => {
57
- return new ServerRouter(
58
- {
59
- dir: path.join(__dirname, 'app'),
60
- extensions: ['jsx', 'js', 'tsx', 'ts'],
61
- },
62
- router,
63
- app
64
- );
65
- }
66
- },
67
- // serverFunctions.router(),
68
- ],
69
- });
1
+ import { defineConfig } from 'solidstep';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ // Nitro's default Node database connector bundles an unused `import 'node:sqlite'`,
5
+ // a builtin that only exists in Node 22.5+. This starter uses no database, so we both
6
+ // pass an empty `database` config and alias the builtin to an empty stub, keeping the
7
+ // production server runnable on Node 20/21.
8
+ const sqliteStub = fileURLToPath(new URL('./sqlite-stub.mjs', import.meta.url));
9
+
10
+ export default defineConfig({
11
+ server: {
12
+ preset: 'node-server',
13
+ database: {},
14
+ alias: {
15
+ 'node:sqlite': sqliteStub,
16
+ },
17
+ },
18
+ });
@@ -11,12 +11,11 @@
11
11
  "keywords": [],
12
12
  "author": "",
13
13
  "dependencies": {
14
- "@vinxi/server-functions": "0.5.1",
15
- "solid-js": "^1.9.7",
14
+ "solid-js": "^1.9.9",
15
+ "solidstep": "^0.3.5",
16
16
  "vinxi": "^0.5.8"
17
17
  },
18
18
  "devDependencies": {
19
- "typescript": "^5.8.3",
20
- "vite-plugin-solid": "^2.11.7"
19
+ "typescript": "^5.8.3"
21
20
  }
22
21
  }
@@ -0,0 +1,9 @@
1
+ // Stub for `node:sqlite`.
2
+ //
3
+ // Nitro's default Node database connector bundles an (unused) `import 'node:sqlite'`
4
+ // into the server output. That builtin only exists in Node 22.5+, so on Node 20/21
5
+ // the server crashes at startup even though no database is ever used. This app has
6
+ // no database, so app.config.ts aliases `node:sqlite` to this empty stub.
7
+ export class DatabaseSync {}
8
+ export class StatementSync {}
9
+ export default { DatabaseSync, StatementSync };
@@ -8,9 +8,10 @@
8
8
  "jsx": "preserve",
9
9
  "jsxImportSource": "solid-js",
10
10
  "allowJs": true,
11
- "checkJs": true,
11
+ "checkJs": false,
12
12
  "noEmit": true,
13
13
  "types": ["vinxi/client"],
14
- "isolatedModules": true
14
+ "isolatedModules": true,
15
+ "skipLibCheck": true
15
16
  }
16
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varlabs/create-solidstep",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Next Step SolidJS CLI for building web applications.",
5
5
  "type": "module",
6
6
  "author": "HamzaKV <hamzakv333@gmail.com>",
@@ -1,105 +0,0 @@
1
- import { hydrate } from 'solid-js/web';
2
- import 'vinxi/client';
3
- import fileRoutes from 'vinxi/routes';
4
- import { getManifest } from 'vinxi/manifest';
5
-
6
- const importModule = async (routeModule: any) => {
7
- const manifest = getManifest('client');
8
- if ((import.meta as any).env.DEV) {
9
- return await manifest.inputs[routeModule.src].import();
10
- }
11
- return await routeModule.import();
12
- };
13
-
14
- export const main = async (
15
- modulePath: string,
16
- routeParams: Record<string, string> = {},
17
- searchParams: Record<string, string> = {},
18
- loaderDataManifest: Record<string, any> = {}
19
- ) => {
20
- // find the route that matches the path
21
- const pageModule = fileRoutes.find((route) => route.path === modulePath);
22
- if (!pageModule) {
23
- console.error(`No route found for path: ${modulePath}`);
24
- return;
25
- }
26
- const pageLoaderData = loaderDataManifest[modulePath];
27
-
28
- const segments = modulePath.split('/').slice(2);
29
- if (segments.at(0)) {
30
- segments.unshift('');
31
- }
32
- const layouts: any[] = [];
33
- const layoutLoaderData: any[] = [];
34
- const groups: Record<string, any> = {};
35
- for (let i = 0; i < segments.length; i++) {
36
- const path = '/' + segments.slice(1, segments.length - i).join('/');
37
- const loaderData = loaderDataManifest[`/layout${path}`];
38
- const layoutModule = fileRoutes.find((route) => {
39
- const routePath = '/' + route.path.split('/').slice(2).join('/');
40
- return routePath === path && (route as any).type === 'layout';
41
- });
42
- if (layoutModule) {
43
- layouts.unshift(layoutModule);
44
- }
45
- if (loaderData) {
46
- layoutLoaderData.unshift(loaderData);
47
- }
48
- }
49
- const groupModules = fileRoutes.filter((route) => {
50
- const parentPath = (route as any).parent || '';
51
- return parentPath === segments.join('/') && (route as any).type === 'group';
52
- });
53
- if (groupModules && groupModules.length > 0) {
54
- for (const groupModule of groupModules) {
55
- const groupName = groupModule.path.split('/').at(-1).replace('@', '');
56
- groups[groupName] = groupModule;
57
- }
58
- }
59
- const compose = layouts.reduceRight(
60
- (children, layout, index) => async () => {
61
- const { default: layoutModule } = await importModule(layout.$component);
62
- const loaderData = layoutLoaderData[index] || {};
63
- const slots: Record<string, any> = {};
64
- const slotPromises: Promise<any>[] = [children()];
65
- if (index === layouts.length - 1) {
66
- // last layout, we can render slots
67
- for (const [groupName, group] of Object.entries(groups)) {
68
- slotPromises.push(
69
- (async () => {
70
- const { default: groupPage } = await importModule(group.$component);
71
- const groupLoaderData = loaderDataManifest[group.path] || {};
72
- slots[groupName] = () => groupPage({
73
- routeParams,
74
- searchParams,
75
- loaderData: groupLoaderData,
76
- });
77
- })()
78
- );
79
- }
80
- }
81
- const [childrenRendered] = await Promise.all(slotPromises);
82
- return () => layoutModule({
83
- children: childrenRendered,
84
- routeParams,
85
- searchParams,
86
- slots: slots,
87
- loaderData,
88
- });
89
- },
90
- async () => {
91
- const { default: page } = await importModule(pageModule.$component);
92
- return () => page({
93
- routeParams,
94
- searchParams,
95
- loaderData: pageLoaderData || {},
96
- });
97
- }
98
- );
99
-
100
- const composed = await compose();
101
-
102
- hydrate(() => composed(), document);
103
- };
104
-
105
- export default main;