@varlabs/create-solidstep 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/generate/app/globals.css +4 -0
- package/generate/app/layout.tsx +39 -0
- package/generate/app/page.tsx +40 -0
- package/generate/app.config.ts +63 -0
- package/generate/client.ts +113 -0
- package/generate/package.json +22 -0
- package/generate/public/dummy-image.jpg +0 -0
- package/generate/public/favicon-32x32.png +0 -0
- package/generate/server.ts +626 -0
- package/generate/utils/loader.ts +26 -0
- package/generate/utils/router.ts +136 -0
- package/generate/utils/types.ts +8 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hamza Varvani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineLoader } 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 = Awaited<ReturnType<Exclude<typeof loader, null>>>['data'];
|
|
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;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createApp } from 'vinxi';
|
|
2
|
+
import solid from 'vite-plugin-solid';
|
|
3
|
+
import { serverFunctions } from '@vinxi/server-functions/plugin';
|
|
4
|
+
import { Router } 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
|
+
},
|
|
14
|
+
routers: [
|
|
15
|
+
{
|
|
16
|
+
name: 'public',
|
|
17
|
+
type: 'static',
|
|
18
|
+
dir: './public',
|
|
19
|
+
base: '/',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'ssr',
|
|
23
|
+
type: 'http',
|
|
24
|
+
base: '/',
|
|
25
|
+
handler: './server.ts',
|
|
26
|
+
target: 'server',
|
|
27
|
+
plugins: () => [solid({ ssr: true })],
|
|
28
|
+
link: {
|
|
29
|
+
client: 'client',
|
|
30
|
+
},
|
|
31
|
+
middleware: './app/middleware.ts',
|
|
32
|
+
routes: (router, app) => {
|
|
33
|
+
return new Router(
|
|
34
|
+
{
|
|
35
|
+
dir: path.join(__dirname, 'app'),
|
|
36
|
+
extensions: ['jsx', 'js', 'tsx', 'ts'],
|
|
37
|
+
},
|
|
38
|
+
router,
|
|
39
|
+
app
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'client',
|
|
45
|
+
type: 'client',
|
|
46
|
+
target: 'browser',
|
|
47
|
+
handler: './client.ts',
|
|
48
|
+
plugins: () => [serverFunctions.client(), solid({ ssr: true })],
|
|
49
|
+
base: '/_build',
|
|
50
|
+
routes: (router, app) => {
|
|
51
|
+
return new Router(
|
|
52
|
+
{
|
|
53
|
+
dir: path.join(__dirname, 'app'),
|
|
54
|
+
extensions: ['jsx', 'js', 'tsx', 'ts'],
|
|
55
|
+
},
|
|
56
|
+
router,
|
|
57
|
+
app
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
serverFunctions.router(),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
const assets = await manifest.inputs?.[routeModule.src].assets();
|
|
12
|
+
if (typeof window !== 'undefined' && assets && assets.length > 0) {
|
|
13
|
+
const styles = assets.filter(
|
|
14
|
+
(asset) => asset.tag === 'style' || asset.attrs.rel === 'stylesheet',
|
|
15
|
+
);
|
|
16
|
+
for (const asset of styles) {
|
|
17
|
+
const attributeString = Object.entries(asset.attrs)
|
|
18
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
19
|
+
.join(' ');
|
|
20
|
+
if (asset.tag === 'style') {
|
|
21
|
+
document.head.insertAdjacentHTML(
|
|
22
|
+
'beforeend',
|
|
23
|
+
`<style ${attributeString}>${asset.children}</style>`,
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
const link = document.createElement('link');
|
|
27
|
+
link.rel = 'stylesheet';
|
|
28
|
+
link.href = asset.attrs.href;
|
|
29
|
+
Object.entries(asset.attrs).forEach(([key, value]) => {
|
|
30
|
+
link.setAttribute(key, value);
|
|
31
|
+
});
|
|
32
|
+
document.head.appendChild(link);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return await routeModule.import();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const main = async (
|
|
40
|
+
modulePath: string,
|
|
41
|
+
routeParams: Record<string, string> = {},
|
|
42
|
+
searchParams: Record<string, string> = {},
|
|
43
|
+
) => {
|
|
44
|
+
// find the route that matches the path
|
|
45
|
+
const pageModule = fileRoutes.find((route) => route.path === modulePath);
|
|
46
|
+
if (!pageModule) {
|
|
47
|
+
console.error(`No route found for path: ${modulePath}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const segments = modulePath.split('/').slice(2);
|
|
52
|
+
if (segments.at(0)) {
|
|
53
|
+
segments.unshift('');
|
|
54
|
+
}
|
|
55
|
+
let layouts: any[] = [];
|
|
56
|
+
let groups: Record<string, any> = {};
|
|
57
|
+
for (let i = 0; i < segments.length; i++) {
|
|
58
|
+
const path = '/' + segments.slice(1, segments.length - i).join('/');
|
|
59
|
+
const layoutModule = fileRoutes.find((route) => {
|
|
60
|
+
const routePath = '/' + route.path.split('/').slice(2).join('/');
|
|
61
|
+
return routePath === path && (route as any).type === 'layout';
|
|
62
|
+
});
|
|
63
|
+
if (layoutModule) {
|
|
64
|
+
layouts.unshift(layoutModule);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const groupModules = fileRoutes.filter((route) => {
|
|
68
|
+
const parentPath = (route as any).parent || '';
|
|
69
|
+
return parentPath === segments.join('/') && (route as any).type === 'group';
|
|
70
|
+
});
|
|
71
|
+
if (groupModules && groupModules.length > 0) {
|
|
72
|
+
for (const groupModule of groupModules) {
|
|
73
|
+
const groupName = groupModule.path.split('/').at(-1).replace('@', '');
|
|
74
|
+
groups[groupName] = groupModule;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const compose = layouts.reduceRight(
|
|
78
|
+
(children, layout, index) => async () => {
|
|
79
|
+
const { default: layoutModule } = await importModule(layout.$component);
|
|
80
|
+
let slots: Record<string, any> = {};
|
|
81
|
+
if (index === layouts.length - 1) {
|
|
82
|
+
// last layout, we can render slots
|
|
83
|
+
for (const [groupName, group] of Object.entries(groups)) {
|
|
84
|
+
const { default: groupPage } = await importModule(group.$component);
|
|
85
|
+
slots[groupName] = () => groupPage({
|
|
86
|
+
routeParams,
|
|
87
|
+
searchParams,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const childrenRendered = await children();
|
|
92
|
+
return () => layoutModule({
|
|
93
|
+
children: childrenRendered,
|
|
94
|
+
routeParams,
|
|
95
|
+
searchParams,
|
|
96
|
+
slots: slots,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
async () => {
|
|
100
|
+
const { default: page } = await importModule(pageModule.$component);
|
|
101
|
+
return () => page({
|
|
102
|
+
routeParams,
|
|
103
|
+
searchParams,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const composed = await compose();
|
|
109
|
+
|
|
110
|
+
hydrate(() => composed(), document);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default main;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "SolidStep App",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple SolidStep application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vinxi dev",
|
|
8
|
+
"build": "vinxi build",
|
|
9
|
+
"start": "vinxi start"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@vinxi/server-functions": "0.5.1",
|
|
15
|
+
"solid-js": "^1.9.7",
|
|
16
|
+
"vinxi": "^0.5.8"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.8.3",
|
|
20
|
+
"vite-plugin-solid": "^2.11.7"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { eventHandler } from 'vinxi/http';
|
|
2
|
+
import { getManifest } from 'vinxi/manifest';
|
|
3
|
+
import { generateHydrationScript, renderToString } from 'solid-js/web';
|
|
4
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
5
|
+
import { join, relative, sep } from 'node:path';
|
|
6
|
+
import type { Meta } from './utils/types';
|
|
7
|
+
import type { ServerResponse, IncomingMessage } from 'node:http';
|
|
8
|
+
import fileRoutes, { type RouteModule } from 'vinxi/routes';
|
|
9
|
+
|
|
10
|
+
type Import = {
|
|
11
|
+
src: string;
|
|
12
|
+
import: any;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RoutePageEntry = {
|
|
16
|
+
type: 'page';
|
|
17
|
+
mainPage: {
|
|
18
|
+
manifestPath: string;
|
|
19
|
+
page: Import;
|
|
20
|
+
loader?: Import;
|
|
21
|
+
generateMeta?: Import;
|
|
22
|
+
};
|
|
23
|
+
loadingPage?: {
|
|
24
|
+
manifestPath: string;
|
|
25
|
+
page: Import;
|
|
26
|
+
generateMeta?: Import;
|
|
27
|
+
};
|
|
28
|
+
errorPage?: {
|
|
29
|
+
manifestPath: string;
|
|
30
|
+
page: Import;
|
|
31
|
+
generateMeta?: Import;
|
|
32
|
+
};
|
|
33
|
+
notFoundPage?: {
|
|
34
|
+
manifestPath: string;
|
|
35
|
+
page: Import;
|
|
36
|
+
generateMeta?: Import;
|
|
37
|
+
};
|
|
38
|
+
layouts: {
|
|
39
|
+
manifestPath: string;
|
|
40
|
+
layout: Import;
|
|
41
|
+
loader?: Import;
|
|
42
|
+
generateMeta?: Import;
|
|
43
|
+
}[];
|
|
44
|
+
groups?: {
|
|
45
|
+
[key: string]: {
|
|
46
|
+
manifestPath: string;
|
|
47
|
+
page: Import;
|
|
48
|
+
loader?: Import;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type RouteEntry = {
|
|
54
|
+
type: 'route';
|
|
55
|
+
handler: Import;
|
|
56
|
+
} | RoutePageEntry;
|
|
57
|
+
|
|
58
|
+
type RouteManifest = {
|
|
59
|
+
[key: string]: RouteEntry;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isPageFile = (file: string) =>
|
|
63
|
+
file.endsWith('page.tsx')
|
|
64
|
+
|| file.endsWith('page.jsx')
|
|
65
|
+
|| file.endsWith('page.ts')
|
|
66
|
+
|| file.endsWith('page.js');
|
|
67
|
+
|
|
68
|
+
const isRouteFile = (file: string) =>
|
|
69
|
+
file.endsWith('route.ts') || file.endsWith('route.js');
|
|
70
|
+
|
|
71
|
+
const parseSegment = (part: string) =>
|
|
72
|
+
part.startsWith('[') ? ':' + part.slice(1, -1).replace(/\.\.\./, '*') : part;
|
|
73
|
+
|
|
74
|
+
const createRouteManifest = async (baseDir = 'app') => {
|
|
75
|
+
const entries: RouteManifest = {};
|
|
76
|
+
|
|
77
|
+
const walk = async (dir: string) => {
|
|
78
|
+
const contents = await readdir(dir);
|
|
79
|
+
for (const entry of contents) {
|
|
80
|
+
const fullPath = join(dir, entry);
|
|
81
|
+
if ((await stat(fullPath)).isDirectory()) {
|
|
82
|
+
await walk(fullPath);
|
|
83
|
+
} else if (isPageFile(fullPath)) {
|
|
84
|
+
const rel = relative(baseDir, fullPath);
|
|
85
|
+
const parts = rel.split(sep);
|
|
86
|
+
const segments = parts.slice(0, -1); // drop 'page.tsx'
|
|
87
|
+
|
|
88
|
+
if (segments.find(s => s.startsWith('@'))) {
|
|
89
|
+
// don't include parallel routes in the manifest
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const urlSegments = segments
|
|
94
|
+
.filter(s => !(s.startsWith('('))) // drop route groups
|
|
95
|
+
.map(parseSegment);
|
|
96
|
+
const routePath = '/' + urlSegments.join('/');
|
|
97
|
+
const mainPage = fileRoutes.find(route => {
|
|
98
|
+
const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
|
|
99
|
+
return (route as any).type === 'route' && path === routePath;
|
|
100
|
+
});
|
|
101
|
+
const loadingPage = fileRoutes.find(route => {
|
|
102
|
+
const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
|
|
103
|
+
return (route as any).type === 'loading' && path === routePath;
|
|
104
|
+
});
|
|
105
|
+
let errorPage: RouteModule;
|
|
106
|
+
const layouts: RoutePageEntry['layouts'] = [];
|
|
107
|
+
const fileSegments = segments
|
|
108
|
+
.map(parseSegment);
|
|
109
|
+
for (let i = 0; i < fileSegments.length + 1; i++) {
|
|
110
|
+
const route = '/' + fileSegments.slice(0, fileSegments.length-i).join('/');
|
|
111
|
+
|
|
112
|
+
for (const fileRoute of fileRoutes) {
|
|
113
|
+
const path = '/' + fileRoute.path.split('/').slice(2).map(parseSegment).join('/');
|
|
114
|
+
if (!errorPage && (fileRoute as any).type === 'error' && path === route) {
|
|
115
|
+
errorPage = fileRoute;
|
|
116
|
+
}
|
|
117
|
+
if ((fileRoute as any).type === 'layout' && path === route) {
|
|
118
|
+
layouts.unshift({
|
|
119
|
+
layout: fileRoute.$component,
|
|
120
|
+
loader: fileRoute.$loader,
|
|
121
|
+
generateMeta: fileRoute.$generateMeta,
|
|
122
|
+
manifestPath: fileRoute.path,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let groups: RoutePageEntry['groups'] = {};
|
|
128
|
+
for (const fileRoute of fileRoutes) {
|
|
129
|
+
const groupParentPath = (fileRoute as any).parent ? '/' + (fileRoute as any).parent.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/') : '';
|
|
130
|
+
if ((fileRoute as any).type === 'group' && groupParentPath === routePath) {
|
|
131
|
+
const groupName = fileRoute.path.split('/').filter(s => !(s.startsWith('('))).map(parseSegment).at(-1);
|
|
132
|
+
groups[groupName] = {
|
|
133
|
+
page: fileRoute.$component,
|
|
134
|
+
loader: fileRoute.$loader,
|
|
135
|
+
manifestPath: fileRoute.path,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let notFoundPage: RouteModule | undefined;
|
|
140
|
+
if (routePath === '/') {
|
|
141
|
+
notFoundPage = fileRoutes.find(route => {
|
|
142
|
+
const path = '/' + route.path.split('/').slice(2).join('/');
|
|
143
|
+
return (route as any).type === 'not-found' && path === routePath;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
entries[routePath] = {
|
|
147
|
+
type: 'page',
|
|
148
|
+
mainPage: {
|
|
149
|
+
manifestPath: mainPage.path,
|
|
150
|
+
page: mainPage.$component,
|
|
151
|
+
loader: mainPage.$loader,
|
|
152
|
+
generateMeta: mainPage.$generateMeta,
|
|
153
|
+
},
|
|
154
|
+
loadingPage: loadingPage ? {
|
|
155
|
+
page: loadingPage.$component,
|
|
156
|
+
generateMeta: loadingPage.$generateMeta,
|
|
157
|
+
manifestPath: loadingPage.path,
|
|
158
|
+
} : undefined,
|
|
159
|
+
errorPage: errorPage ? {
|
|
160
|
+
page: errorPage.$component,
|
|
161
|
+
generateMeta: errorPage.$generateMeta,
|
|
162
|
+
manifestPath: errorPage.path,
|
|
163
|
+
} : undefined,
|
|
164
|
+
layouts,
|
|
165
|
+
groups,
|
|
166
|
+
notFoundPage: notFoundPage ? {
|
|
167
|
+
page: notFoundPage.$component,
|
|
168
|
+
generateMeta: notFoundPage.$generateMeta,
|
|
169
|
+
manifestPath: notFoundPage.path,
|
|
170
|
+
} : undefined,
|
|
171
|
+
};
|
|
172
|
+
} else if (isRouteFile(fullPath)) {
|
|
173
|
+
const rel = relative(baseDir, fullPath);
|
|
174
|
+
const parts = rel.split(sep);
|
|
175
|
+
const segments = parts.slice(0, -1); // drop 'route.ts'
|
|
176
|
+
|
|
177
|
+
const urlSegments = segments
|
|
178
|
+
.filter(s => !(s.startsWith('('))) // drop route groups
|
|
179
|
+
.map(parseSegment);
|
|
180
|
+
|
|
181
|
+
const routePath = '/' + urlSegments.join('/');
|
|
182
|
+
const mainRoute = fileRoutes.find(route => {
|
|
183
|
+
const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
|
|
184
|
+
return (route as any).type === 'route' && path === routePath;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
entries[routePath] = {
|
|
188
|
+
type: 'route',
|
|
189
|
+
handler: mainRoute.$handler,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await walk(baseDir);
|
|
195
|
+
return entries;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const extractRouteParams = (route: string, url: string) => {
|
|
199
|
+
const routeSegments = route.split('/').filter(Boolean);
|
|
200
|
+
const urlSegments = url.split('/').filter(Boolean);
|
|
201
|
+
|
|
202
|
+
if (routeSegments.length !== urlSegments.length) return null;
|
|
203
|
+
|
|
204
|
+
const params = {};
|
|
205
|
+
let matched = true;
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
208
|
+
const routeSeg = routeSegments[i];
|
|
209
|
+
const urlSeg = urlSegments[i];
|
|
210
|
+
|
|
211
|
+
const isDynamic = routeSeg.startsWith('[') && routeSeg.endsWith(']');
|
|
212
|
+
if (isDynamic) {
|
|
213
|
+
const paramName = routeSeg.slice(1, -1);
|
|
214
|
+
params[paramName] = urlSeg;
|
|
215
|
+
} else if (routeSeg !== urlSeg) {
|
|
216
|
+
matched = false;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (matched) return { route, params };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const template = `
|
|
225
|
+
<!DOCTYPE html>
|
|
226
|
+
<html lang="en">
|
|
227
|
+
<head><!--app-head--></head>
|
|
228
|
+
<!--app-body-->
|
|
229
|
+
</html>
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const generateHtmlHead = (meta: Meta) => {
|
|
233
|
+
const head = Object.entries(meta)
|
|
234
|
+
.map(([key, value]) => {
|
|
235
|
+
if (value.type === 'title') {
|
|
236
|
+
return `<title>${value.content}</title>`;
|
|
237
|
+
} else if (value.type === 'meta') {
|
|
238
|
+
const attrs = Object.entries(value.attributes)
|
|
239
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
|
|
240
|
+
.join(' ');
|
|
241
|
+
return `<meta ${attrs}>`;
|
|
242
|
+
} else if (value.type === 'link' || value.type === 'style' || value.type === 'script') {
|
|
243
|
+
const attrs = Object.entries(value.attributes)
|
|
244
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
|
|
245
|
+
.join(' ');
|
|
246
|
+
return `<${value.type} ${attrs}></${value.type}>`;
|
|
247
|
+
}
|
|
248
|
+
return '';
|
|
249
|
+
})
|
|
250
|
+
.join('\n');
|
|
251
|
+
return head;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const sendNodeResponse = async (
|
|
255
|
+
res: ServerResponse & { req: IncomingMessage },
|
|
256
|
+
response: Response
|
|
257
|
+
) => {
|
|
258
|
+
// Set status code
|
|
259
|
+
res.statusCode = response.status;
|
|
260
|
+
|
|
261
|
+
// Set headers
|
|
262
|
+
response.headers.forEach((value, key) => {
|
|
263
|
+
res.setHeader(key, value);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Stream the body
|
|
267
|
+
if (response.body) {
|
|
268
|
+
const reader = response.body.getReader();
|
|
269
|
+
|
|
270
|
+
const push = async () => {
|
|
271
|
+
const { done, value } = await reader.read();
|
|
272
|
+
if (done) {
|
|
273
|
+
res.end();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
res.write(Buffer.from(value));
|
|
277
|
+
await push();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
await push();
|
|
281
|
+
} else {
|
|
282
|
+
const text = await response.text()
|
|
283
|
+
res.end(text)
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const render = async (
|
|
288
|
+
toRender: 'main' | 'loading' | 'error' | 'not-found',
|
|
289
|
+
entry: RoutePageEntry,
|
|
290
|
+
routeParams: Record<string, string>,
|
|
291
|
+
searchParams: Record<string, string>,
|
|
292
|
+
req: Request
|
|
293
|
+
) => {
|
|
294
|
+
let meta: Meta = {};
|
|
295
|
+
const compose = entry.layouts.reduceRight(
|
|
296
|
+
(children, layout, index) => async () => {
|
|
297
|
+
const { default: layoutModule } = await layout.layout.import();
|
|
298
|
+
const { loader: layoutLoader } = layout.loader ? await layout.loader.import() : { loader: null };
|
|
299
|
+
const { generateMeta: generateMetaPage } = layout.generateMeta ? await layout.generateMeta.import() : { generateMeta: null };
|
|
300
|
+
let data = {};
|
|
301
|
+
if (generateMetaPage) {
|
|
302
|
+
const metaData = await generateMetaPage(req);
|
|
303
|
+
if (metaData) {
|
|
304
|
+
meta = {
|
|
305
|
+
...meta,
|
|
306
|
+
...metaData
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (layoutLoader) {
|
|
311
|
+
const result = await layoutLoader(req);
|
|
312
|
+
data = result.data || {};
|
|
313
|
+
}
|
|
314
|
+
let slots: Record<string, any> = {};
|
|
315
|
+
if (index === entry.layouts.length - 1) {
|
|
316
|
+
// last layout, we can render slots
|
|
317
|
+
const groups = entry.groups || {};
|
|
318
|
+
for (const [groupName, group] of Object.entries(groups)) {
|
|
319
|
+
const { default: groupPage } = await group.page.import();
|
|
320
|
+
const { loader: groupLoader } = group.loader ? await group.loader.import() : { loader: null };
|
|
321
|
+
let data = {};
|
|
322
|
+
if (groupLoader) {
|
|
323
|
+
const result = await groupLoader(req);
|
|
324
|
+
data = result.data || {};
|
|
325
|
+
}
|
|
326
|
+
slots[groupName.replace('@', '')] = () => groupPage({
|
|
327
|
+
routeParams,
|
|
328
|
+
searchParams,
|
|
329
|
+
loaderData: data
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const childrenRendered = await children();
|
|
334
|
+
return () => layoutModule({
|
|
335
|
+
children: childrenRendered,
|
|
336
|
+
routeParams,
|
|
337
|
+
searchParams,
|
|
338
|
+
loaderData: data,
|
|
339
|
+
slots: slots
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
async () => {
|
|
343
|
+
const pageToRender: any = toRender === 'loading'
|
|
344
|
+
? entry.loadingPage
|
|
345
|
+
: toRender === 'error'
|
|
346
|
+
? entry.errorPage
|
|
347
|
+
: toRender === 'not-found'
|
|
348
|
+
? entry.notFoundPage
|
|
349
|
+
: entry.mainPage;
|
|
350
|
+
const { default: page } = await pageToRender.page.import();
|
|
351
|
+
const { loader: pageLoader } = pageToRender.loader ? await pageToRender.loader.import() : { loader: null };
|
|
352
|
+
const { generateMeta } = pageToRender.generateMeta ? await pageToRender.generateMeta.import() : { generateMeta: null };
|
|
353
|
+
let data = {};
|
|
354
|
+
if (pageLoader) {
|
|
355
|
+
const result = await pageLoader(req);
|
|
356
|
+
data = result.data || {};
|
|
357
|
+
}
|
|
358
|
+
if (generateMeta) {
|
|
359
|
+
const metaData = await generateMeta(req);
|
|
360
|
+
if (metaData) {
|
|
361
|
+
meta = {
|
|
362
|
+
...meta,
|
|
363
|
+
...metaData
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return () => page({
|
|
368
|
+
routeParams,
|
|
369
|
+
searchParams,
|
|
370
|
+
loaderData: data
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const composed = await compose();
|
|
376
|
+
const rendered = await renderToString(() => composed());
|
|
377
|
+
return {
|
|
378
|
+
rendered: rendered,
|
|
379
|
+
documentMeta: meta
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
let routeManifest: RouteManifest = {};
|
|
384
|
+
|
|
385
|
+
const handler = eventHandler(async (event) => {
|
|
386
|
+
const clientManifest = getManifest('client');
|
|
387
|
+
|
|
388
|
+
if (!routeManifest || Object.keys(routeManifest).length === 0) {
|
|
389
|
+
routeManifest = await createRouteManifest();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const req = event.node.req;
|
|
393
|
+
const res = event.node.res;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const url = req.url || '/';
|
|
397
|
+
// extract route params and search params
|
|
398
|
+
const params: Record<string, string> = {};
|
|
399
|
+
const searchParams: Record<string, string> = {};
|
|
400
|
+
const [pathnamePart, searchParamPart] = url.split('?');
|
|
401
|
+
if (searchParamPart) {
|
|
402
|
+
searchParamPart.split('&').forEach((param) => {
|
|
403
|
+
const [key, value] = param.split('=');
|
|
404
|
+
searchParams[key] = decodeURIComponent(value || '');
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const matched = Object.entries(routeManifest).find(([path, entry]) => {
|
|
409
|
+
const pattern = path.replace(/:[^/]+/g, '[^/]+').replace(/\*$/, '.*');
|
|
410
|
+
const re = new RegExp(`^${pattern}$`);
|
|
411
|
+
return re.test(pathnamePart);
|
|
412
|
+
})?.[1] as RouteEntry;
|
|
413
|
+
|
|
414
|
+
const routePath = matched ? (matched as RoutePageEntry).mainPage.manifestPath.split('/').slice(2).join('/') : '/';
|
|
415
|
+
|
|
416
|
+
const routeParams = extractRouteParams(routePath, pathnamePart);
|
|
417
|
+
if (routeParams) {
|
|
418
|
+
Object.assign(params, routeParams.params);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (matched && matched.type === 'route') {
|
|
422
|
+
const routeModule = await matched.handler.import();
|
|
423
|
+
const reqMethod = req.method?.toUpperCase();
|
|
424
|
+
if (reqMethod) {
|
|
425
|
+
const handler = routeModule[reqMethod];
|
|
426
|
+
if (typeof handler === 'function') {
|
|
427
|
+
const result = await handler(req, {
|
|
428
|
+
params: params,
|
|
429
|
+
searchParams: searchParams,
|
|
430
|
+
});
|
|
431
|
+
await sendNodeResponse(res, result);
|
|
432
|
+
return;
|
|
433
|
+
} else {
|
|
434
|
+
throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
throw new Error(`Unsupported request method: ${reqMethod}`);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
let loading = false;
|
|
441
|
+
let html;
|
|
442
|
+
let meta: Meta = {
|
|
443
|
+
charset: {
|
|
444
|
+
type: 'meta',
|
|
445
|
+
attributes: {
|
|
446
|
+
charset: 'UTF-8'
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
viewport: {
|
|
450
|
+
type: 'meta',
|
|
451
|
+
attributes: {
|
|
452
|
+
name: 'viewport',
|
|
453
|
+
content: 'width=device-width, initial-scale=1.0'
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
title: {
|
|
457
|
+
type: 'title',
|
|
458
|
+
attributes: {},
|
|
459
|
+
content: 'SolidStep'
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const assets = await clientManifest.inputs[clientManifest.handler].assets();
|
|
463
|
+
const assetsHtml = assets.map((asset) => {
|
|
464
|
+
const attributeString = Object.entries(asset.attrs)
|
|
465
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
466
|
+
.join(' ');
|
|
467
|
+
if (asset.tag === 'script') {
|
|
468
|
+
return `<script ${attributeString}></script>`;
|
|
469
|
+
}
|
|
470
|
+
if (asset.tag === 'link') {
|
|
471
|
+
return `<link ${attributeString}>`;
|
|
472
|
+
}
|
|
473
|
+
if (asset.tag === 'style') {
|
|
474
|
+
return `<style ${attributeString}>${asset.children || ''}</style>`;
|
|
475
|
+
}
|
|
476
|
+
}).join('\n');
|
|
477
|
+
const manifestHtml = `<script>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
|
|
478
|
+
let clientHydrationScript;
|
|
479
|
+
|
|
480
|
+
res.setHeader('Content-Type', 'text/html');
|
|
481
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
482
|
+
try {
|
|
483
|
+
if (!matched) {
|
|
484
|
+
try {
|
|
485
|
+
const notFoundPage = routeManifest['/'] as RoutePageEntry;
|
|
486
|
+
const { rendered, documentMeta } = await render(
|
|
487
|
+
'not-found',
|
|
488
|
+
notFoundPage,
|
|
489
|
+
{},
|
|
490
|
+
{},
|
|
491
|
+
req as unknown as Request
|
|
492
|
+
);
|
|
493
|
+
clientHydrationScript = `
|
|
494
|
+
<script type="module">
|
|
495
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
496
|
+
main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
497
|
+
</script>
|
|
498
|
+
`;
|
|
499
|
+
html = rendered;
|
|
500
|
+
meta = {
|
|
501
|
+
...meta,
|
|
502
|
+
...documentMeta
|
|
503
|
+
};
|
|
504
|
+
res.statusCode = 404;
|
|
505
|
+
} catch (e) {
|
|
506
|
+
console.error('404 module not found:', e);
|
|
507
|
+
res.statusCode = 404;
|
|
508
|
+
return res.end('Not Found');
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
try {
|
|
512
|
+
const { rendered, documentMeta } = await render(
|
|
513
|
+
'loading',
|
|
514
|
+
matched as RoutePageEntry,
|
|
515
|
+
params,
|
|
516
|
+
searchParams,
|
|
517
|
+
req as unknown as Request
|
|
518
|
+
);
|
|
519
|
+
const html = `
|
|
520
|
+
<!doctype html>
|
|
521
|
+
<html lang="en">
|
|
522
|
+
<head>
|
|
523
|
+
${generateHtmlHead({
|
|
524
|
+
...meta,
|
|
525
|
+
...documentMeta,
|
|
526
|
+
})}
|
|
527
|
+
${generateHydrationScript()}
|
|
528
|
+
</head>
|
|
529
|
+
<noscript>
|
|
530
|
+
Please enable JavaScript to view the content.<br/>
|
|
531
|
+
</noscript>
|
|
532
|
+
${rendered}
|
|
533
|
+
</html>
|
|
534
|
+
`;
|
|
535
|
+
res.write(html);
|
|
536
|
+
res.write(`
|
|
537
|
+
<script type="module" data-hydration="loading">
|
|
538
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
539
|
+
main('${(matched as RoutePageEntry).loadingPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
540
|
+
</script>
|
|
541
|
+
`);
|
|
542
|
+
loading = true;
|
|
543
|
+
} catch (e) {
|
|
544
|
+
// skip
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { rendered, documentMeta } = await render(
|
|
548
|
+
'main',
|
|
549
|
+
matched as RoutePageEntry,
|
|
550
|
+
params,
|
|
551
|
+
searchParams,
|
|
552
|
+
req as unknown as Request
|
|
553
|
+
);
|
|
554
|
+
clientHydrationScript = `
|
|
555
|
+
<script type="module">
|
|
556
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
557
|
+
main('${(matched as RoutePageEntry).mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
558
|
+
</script>
|
|
559
|
+
`;
|
|
560
|
+
html = rendered;
|
|
561
|
+
meta = {
|
|
562
|
+
...meta,
|
|
563
|
+
...documentMeta
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
} catch (e1) {
|
|
567
|
+
try {
|
|
568
|
+
const errorPage = (matched as RoutePageEntry).errorPage;
|
|
569
|
+
if (!errorPage) {
|
|
570
|
+
throw e1;
|
|
571
|
+
}
|
|
572
|
+
const { rendered, documentMeta } = await render(
|
|
573
|
+
'error',
|
|
574
|
+
matched as RoutePageEntry,
|
|
575
|
+
params,
|
|
576
|
+
searchParams,
|
|
577
|
+
req as unknown as Request
|
|
578
|
+
);
|
|
579
|
+
clientHydrationScript = `
|
|
580
|
+
<script type="module">
|
|
581
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
582
|
+
main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
583
|
+
</script>
|
|
584
|
+
`;
|
|
585
|
+
html = rendered;
|
|
586
|
+
meta = {
|
|
587
|
+
...meta,
|
|
588
|
+
...documentMeta
|
|
589
|
+
};
|
|
590
|
+
res.statusCode = 500;
|
|
591
|
+
} catch (e2) {
|
|
592
|
+
throw e1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (loading) {
|
|
597
|
+
res.write(`
|
|
598
|
+
<script>
|
|
599
|
+
const head = document.querySelector('head');
|
|
600
|
+
const scripts = Array.from(head.querySelectorAll('script'));
|
|
601
|
+
head.innerHTML = \`${generateHtmlHead(meta)}\`;
|
|
602
|
+
scripts.forEach(script => {
|
|
603
|
+
head.appendChild(script);
|
|
604
|
+
});
|
|
605
|
+
document.querySelector('script[data-hydration="loading"]')?.remove();
|
|
606
|
+
const loading = document.querySelector('body');
|
|
607
|
+
loading.innerHTML = \`${html}\`;
|
|
608
|
+
</script>
|
|
609
|
+
`);
|
|
610
|
+
res.write(manifestHtml);
|
|
611
|
+
return res.end(clientHydrationScript);
|
|
612
|
+
} else {
|
|
613
|
+
const transformHtml = template
|
|
614
|
+
.replace(`<!--app-head-->`, generateHtmlHead(meta) + '\n' + assetsHtml + '\n' + generateHydrationScript())
|
|
615
|
+
.replace(`<!--app-body-->`, (html ?? '') + manifestHtml + clientHydrationScript);
|
|
616
|
+
return res.end(transformHtml);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} catch (e) {
|
|
620
|
+
console.error(e);
|
|
621
|
+
res.statusCode = 500;
|
|
622
|
+
return res.end('Internal Server Error');
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
export default handler;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isServer } from 'solid-js/web';
|
|
2
|
+
|
|
3
|
+
type LoaderFunction<T> = (request?: Request) => Promise<T>;
|
|
4
|
+
|
|
5
|
+
type LoaderOptions = {
|
|
6
|
+
type?: 'defer' | 'sequential';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const defineLoader = <T>(loader: LoaderFunction<T>, options?: LoaderOptions) => {
|
|
10
|
+
if (isServer) {
|
|
11
|
+
return async (request?: Request) => {
|
|
12
|
+
try {
|
|
13
|
+
const loaderData = await loader(request);
|
|
14
|
+
return {
|
|
15
|
+
data: loaderData,
|
|
16
|
+
type: options?.type || 'sequential',
|
|
17
|
+
};
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error in loader:', error);
|
|
20
|
+
throw error; // Re-throw to allow error handling upstream
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return null; // Return null if not on the server
|
|
26
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { BaseFileSystemRouter } from 'vinxi/fs-router';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export class Router extends BaseFileSystemRouter {
|
|
8
|
+
toPath(src: string) {
|
|
9
|
+
src = src
|
|
10
|
+
.slice((__dirname + '/app').length);
|
|
11
|
+
|
|
12
|
+
const routePath = src
|
|
13
|
+
.replace(new RegExp(`\.(${(this.config.extensions ?? []).join('|')})$`), '')
|
|
14
|
+
.replace(/\/(page|route|layout|error|not-found|loading)$/, '');
|
|
15
|
+
|
|
16
|
+
return routePath?.length > 0 ? routePath : '/';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toRoute(filePath: string) {
|
|
20
|
+
const path = this.toPath(filePath);
|
|
21
|
+
|
|
22
|
+
if ((/route\.(js|ts)$/).test(filePath)) {
|
|
23
|
+
return {
|
|
24
|
+
type: 'route',
|
|
25
|
+
path: '/route' + path,
|
|
26
|
+
$handler: {
|
|
27
|
+
src: filePath,
|
|
28
|
+
pick: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const scopedPackageMatch = path.match(/@[^]+/g);
|
|
34
|
+
if (scopedPackageMatch) {
|
|
35
|
+
// Remove the scoped package part
|
|
36
|
+
const scopedPackage = scopedPackageMatch[0];
|
|
37
|
+
const parent = path.replace('/' + scopedPackage, '');
|
|
38
|
+
return {
|
|
39
|
+
type: 'group',
|
|
40
|
+
parent: parent,
|
|
41
|
+
path: '/group' + path,
|
|
42
|
+
$component: {
|
|
43
|
+
src: filePath,
|
|
44
|
+
pick: ['default'],
|
|
45
|
+
},
|
|
46
|
+
$loader: {
|
|
47
|
+
src: filePath,
|
|
48
|
+
pick: ['loader'],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ((/page\.(jsx|js|tsx|ts)$/).test(filePath)) {
|
|
54
|
+
return {
|
|
55
|
+
type: 'route',
|
|
56
|
+
path: '/route' + path,
|
|
57
|
+
$component: {
|
|
58
|
+
src: filePath,
|
|
59
|
+
pick: ['default'],
|
|
60
|
+
},
|
|
61
|
+
$loader: {
|
|
62
|
+
src: filePath,
|
|
63
|
+
pick: ['loader'],
|
|
64
|
+
},
|
|
65
|
+
$generateMeta: {
|
|
66
|
+
src: filePath,
|
|
67
|
+
pick: ['generateMeta'],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ((/layout\.(jsx|js|tsx|ts)$/).test(filePath)) {
|
|
73
|
+
return {
|
|
74
|
+
type: 'layout',
|
|
75
|
+
path: '/layout' + path,
|
|
76
|
+
$component: {
|
|
77
|
+
src: filePath,
|
|
78
|
+
pick: ['default'],
|
|
79
|
+
},
|
|
80
|
+
$loader: {
|
|
81
|
+
src: filePath,
|
|
82
|
+
pick: ['loader'],
|
|
83
|
+
},
|
|
84
|
+
$generateMeta: {
|
|
85
|
+
src: filePath,
|
|
86
|
+
pick: ['generateMeta'],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ((/error\.(jsx|js|tsx|ts)$/).test(filePath)) {
|
|
92
|
+
return {
|
|
93
|
+
type: 'error',
|
|
94
|
+
path: '/error' + path,
|
|
95
|
+
$component: {
|
|
96
|
+
src: filePath,
|
|
97
|
+
pick: ['default'],
|
|
98
|
+
},
|
|
99
|
+
$generateMeta: {
|
|
100
|
+
src: filePath,
|
|
101
|
+
pick: ['generateMeta'],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ((/loading\.(jsx|js|tsx|ts)$/).test(filePath)) {
|
|
107
|
+
return {
|
|
108
|
+
type: 'loading',
|
|
109
|
+
path: '/loading' + path,
|
|
110
|
+
$component: {
|
|
111
|
+
src: filePath,
|
|
112
|
+
pick: ['default'],
|
|
113
|
+
},
|
|
114
|
+
$generateMeta: {
|
|
115
|
+
src: filePath,
|
|
116
|
+
pick: ['generateMeta'],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ((/not-found\.(jsx|js|tsx|ts)$/).test(filePath) && path === '/') {
|
|
122
|
+
return {
|
|
123
|
+
type: 'not-found',
|
|
124
|
+
path: '/not-found' + path,
|
|
125
|
+
$component: {
|
|
126
|
+
src: filePath,
|
|
127
|
+
pick: ['default'],
|
|
128
|
+
},
|
|
129
|
+
$generateMeta: {
|
|
130
|
+
src: filePath,
|
|
131
|
+
pick: ['generateMeta'],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varlabs/create-solidstep",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next Step SolidJS CLI for building web applications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "HamzaKV <hamzakv333@gmail.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/HamzaKV/solidstep.git"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-solidstep": "./bin/main.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rimraf ./dist",
|
|
17
|
+
"copy-files:root": "copyfiles -u 0 README.md package.json generate/**/* LICENSE ./dist",
|
|
18
|
+
"dev": "tsx --no-cache ./bin/main -v",
|
|
19
|
+
"start": "node ./dist/bin/main.js",
|
|
20
|
+
"build": "pnpm clean && tsc && pnpm copy-files:root",
|
|
21
|
+
"test:local": "pnpm build && cd ./dist && pnpm link",
|
|
22
|
+
"test:local:clean": "pnpm unlink && pnpm clean",
|
|
23
|
+
"git:main": "git checkout \"main\"",
|
|
24
|
+
"git:push:main": "git push -u origin \"main\" --tags",
|
|
25
|
+
"patch": "pnpm git:main && npm version patch && pnpm git:push:main",
|
|
26
|
+
"minor": "pnpm git:main && npm version minor && pnpm git:push:main",
|
|
27
|
+
"major": "pnpm git:main && npm version major && pnpm git:push:main",
|
|
28
|
+
"roll": "pnpm build && cd dist && npm publish",
|
|
29
|
+
"roll:patch": "pnpm patch && pnpm roll",
|
|
30
|
+
"roll:minor": "pnpm minor && pnpm roll",
|
|
31
|
+
"roll:major": "pnpm major && pnpm roll"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.15.17",
|
|
35
|
+
"copyfiles": "^2.4.1",
|
|
36
|
+
"rimraf": "^6.0.1",
|
|
37
|
+
"tsx": "^4.19.4",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"inquirer": "^12.6.3"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"solidjs",
|
|
45
|
+
"cli",
|
|
46
|
+
"create-solidstep",
|
|
47
|
+
"web-development",
|
|
48
|
+
"typescript",
|
|
49
|
+
"npm"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|