hyperspan 0.0.2 → 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/README.md +19 -0
- package/examples/app/routes/__custom/index.ts +14 -0
- package/examples/app/routes/index.ts +12 -0
- package/examples/app/routes/posts/[postId].ts +26 -0
- package/examples/app/routes/test.ts +12 -0
- package/package.json +11 -9
- package/src/index.ts +56 -1
- package/src/server.ts +114 -0
- package/dist/index.js +0 -12991
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Hyperspan CLI
|
|
2
|
+
|
|
3
|
+
Use the Hyperspan CLI to create a new Hyperspan project or run certain commands on an existing Hyperspan project.
|
|
4
|
+
|
|
5
|
+
## Create Hyperspan App
|
|
6
|
+
|
|
7
|
+
To create a new Hyperspan application in the `myapp` directory:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
bunx hyperspan create myapp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Static Site Generator Build
|
|
14
|
+
|
|
15
|
+
To create a static build of your Hyperspan app (for blogs or content pages), run from your app root directory:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
bunx hyperspan build:ssg
|
|
19
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRoute } from "@hyperspan/framework";
|
|
2
|
+
import { html } from "@hyperspan/html";
|
|
3
|
+
|
|
4
|
+
// This should NOT be a route, because it is in a directory that starts with a double underscore (__custom)
|
|
5
|
+
export default createRoute().get(async (c) => {
|
|
6
|
+
return html`
|
|
7
|
+
<html>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Inaccessible Route</h1>
|
|
10
|
+
<p>This route is not accessible because it is in a directory that starts with a double underscore (<code>__custom</code>).</p>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
`;
|
|
14
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRoute } from '@hyperspan/framework';
|
|
2
|
+
import { html } from '@hyperspan/html';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { validateBody } from '@hyperspan/framework/middleware/zod';
|
|
5
|
+
|
|
6
|
+
export default createRoute().get(async (c) => {
|
|
7
|
+
return html`
|
|
8
|
+
<html>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Post Page</h1>
|
|
11
|
+
<p>Post ID: ${c.route.params.postId}</p>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
14
|
+
`;
|
|
15
|
+
}).post(async (c) => {
|
|
16
|
+
return html`
|
|
17
|
+
<html>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>Post Page</h1>
|
|
20
|
+
<p>Post ID: ${c.route.params.postId}</p>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
`;
|
|
24
|
+
}, {
|
|
25
|
+
middleware: [validateBody(z.object({ title: z.string(), content: z.string() }))]
|
|
26
|
+
})
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperspan",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Hyperspan CLI - for @hyperspan/framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"public": true,
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
-
"main": "
|
|
10
|
+
"main": "src/index.ts",
|
|
11
11
|
"types": "src/index.ts",
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
14
|
"types": "./src/index.ts",
|
|
15
|
-
"default": "./
|
|
15
|
+
"default": "./src/index.ts"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
|
-
"hyperspan": "
|
|
19
|
+
"hyperspan": "src/index.ts"
|
|
20
20
|
},
|
|
21
21
|
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
22
22
|
"license": "BSD-3-Clause",
|
|
@@ -29,13 +29,15 @@
|
|
|
29
29
|
"url": "https://github.com/vlucas/hyperspan/issues"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "bun test"
|
|
33
|
-
"build": "bun build src/index.ts --outdir dist --target node",
|
|
34
|
-
"prepack": "npm run build"
|
|
32
|
+
"test": "bun test"
|
|
35
33
|
},
|
|
36
34
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"commander": "^14.0.0",
|
|
35
|
+
"commander": "^14.0.2",
|
|
39
36
|
"degit": "^2.8.4"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "^1.3.2",
|
|
40
|
+
"@types/degit": "^2.8.6",
|
|
41
|
+
"@types/node": "^24.10.0"
|
|
40
42
|
}
|
|
41
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
1
3
|
import { Command } from 'commander';
|
|
2
4
|
import degit from 'degit';
|
|
3
5
|
import fs from 'node:fs';
|
|
4
6
|
import { execSync } from 'node:child_process';
|
|
5
7
|
import packageJson from '../package.json';
|
|
8
|
+
import { startServer } from './server';
|
|
9
|
+
import { createContext } from '@hyperspan/framework';
|
|
10
|
+
import { join } from 'node:path';
|
|
6
11
|
|
|
7
12
|
const program = new Command();
|
|
8
13
|
|
|
@@ -34,8 +39,58 @@ program
|
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
|
-
*
|
|
42
|
+
* Start the server
|
|
38
43
|
*/
|
|
44
|
+
program
|
|
45
|
+
.command('start')
|
|
46
|
+
.option('--dir <path>', 'directory of your hyperspan project', './')
|
|
47
|
+
.description('Start the server')
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
// Ensure we are in a hyperspan project
|
|
50
|
+
const serverFile = `${options.dir}/app/routes`;
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(serverFile)) {
|
|
53
|
+
console.error(
|
|
54
|
+
'Error: Could not find app/routes - Are you in a Hyperspan project directory?'
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('\n========================================');
|
|
60
|
+
console.log('[Hyperspan] Starting...');
|
|
61
|
+
|
|
62
|
+
const server = await startServer({ development: process.env.NODE_ENV !== 'production' });
|
|
63
|
+
|
|
64
|
+
const routes: Record<string, (request: Request) => Promise<Response>> = {};
|
|
65
|
+
for (const route of server._routes) {
|
|
66
|
+
routes[route._path()] = (request: Request) => route.fetch(request);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const httpServer = Bun.serve({
|
|
70
|
+
routes,
|
|
71
|
+
fetch: async (request: Request) => {
|
|
72
|
+
// Serve static files from the public directory
|
|
73
|
+
const url = new URL(request.url);
|
|
74
|
+
if (url.pathname.startsWith('/_hs/')) {
|
|
75
|
+
return new Response(Bun.file(join('./', server._config.publicDir, url.pathname)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Other static file from the public directory
|
|
79
|
+
const file = Bun.file(join('./', server._config.publicDir, url.pathname))
|
|
80
|
+
const fileExists = await file.exists()
|
|
81
|
+
if (fileExists) {
|
|
82
|
+
return new Response(file);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Not found
|
|
86
|
+
return createContext(request).res.notFound();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`[Hyperspan] Server started on http://localhost:${httpServer.port} (Press Ctrl+C to stop)`);
|
|
91
|
+
console.log('========================================\n');
|
|
92
|
+
});
|
|
93
|
+
|
|
39
94
|
program
|
|
40
95
|
.command('build:ssg')
|
|
41
96
|
.option('--dir <path>', 'directory of your hyperspan project', './')
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
2
|
+
import { createServer, getRunnableRoute, IS_PROD, parsePath } from '@hyperspan/framework';
|
|
3
|
+
import { CSS_PUBLIC_PATH, CSS_ROUTE_MAP } from '@hyperspan/framework/client/css';
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { Hyperspan as HS } from '@hyperspan/framework';
|
|
7
|
+
type startConfig = {
|
|
8
|
+
development?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CWD = process.cwd();
|
|
12
|
+
|
|
13
|
+
export async function loadConfig(): Promise<HS.Config> {
|
|
14
|
+
const configFile = join(CWD, "hyperspan.config.ts");
|
|
15
|
+
const configModule = await import(configFile).then((module) => module.default).catch((error) => {
|
|
16
|
+
console.error(`[Hyperspan] Unable to load config file: ${error}`);
|
|
17
|
+
console.error(`[Hyperspan] Please create a hyperspan.config.ts file in the root of your project.`);
|
|
18
|
+
console.log(`[Hyperspan] Example:
|
|
19
|
+
import { createConfig } from '@hyperspan/framework';
|
|
20
|
+
|
|
21
|
+
export default createConfig({
|
|
22
|
+
appDir: './app',
|
|
23
|
+
publicDir: './public',
|
|
24
|
+
});
|
|
25
|
+
`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
return configModule;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function startServer(startConfig: startConfig = {}): Promise<HS.Server> {
|
|
32
|
+
console.log('[Hyperspan] Loading config...');
|
|
33
|
+
const config = await loadConfig();
|
|
34
|
+
const server = await createServer(config);
|
|
35
|
+
console.log('[Hyperspan] Adding routes...');
|
|
36
|
+
await addRoutes(server, startConfig);
|
|
37
|
+
return server;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function addRoutes(server: HS.Server, startConfig: startConfig) {
|
|
41
|
+
const routesGlob = new Glob("**/*.ts");
|
|
42
|
+
const routeFiles: string[] = [];
|
|
43
|
+
const appDir = server._config.appDir || "./app";
|
|
44
|
+
const routesDir = join(CWD, appDir, "routes");
|
|
45
|
+
const buildDir = join(CWD, '.build');
|
|
46
|
+
const cssPublicDir = join(CWD, server._config.publicDir, CSS_PUBLIC_PATH);
|
|
47
|
+
|
|
48
|
+
for await (const file of routesGlob.scan(routesDir)) {
|
|
49
|
+
const filePath = join(routesDir, file);
|
|
50
|
+
|
|
51
|
+
// Hidden directories and files start with a double underscore.
|
|
52
|
+
// These do not get added to the routes. Nothing nested under them gets added to the routes either.
|
|
53
|
+
if (filePath.includes('/__')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
routeFiles.push(filePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const routeMap: { route: string, file: string }[] = [];
|
|
61
|
+
const routes: HS.Route[] = await Promise.all(routeFiles.map(async (filePath) => {
|
|
62
|
+
const relativePath = filePath.split('app/routes/').pop();
|
|
63
|
+
const { path } = parsePath(relativePath ?? '/');
|
|
64
|
+
|
|
65
|
+
if (path) {
|
|
66
|
+
let cssFiles: string[] = [];
|
|
67
|
+
|
|
68
|
+
// Build the route just for the CSS files
|
|
69
|
+
// Wasteful perhaps to compile the JS also and then just discard it, but it's an easy way to do CSS compilation by route
|
|
70
|
+
const buildResult = await Bun.build({
|
|
71
|
+
entrypoints: [filePath],
|
|
72
|
+
outdir: buildDir,
|
|
73
|
+
naming: `app/routes/${path.endsWith('/') ? path + 'index' : path}-[hash].[ext]`,
|
|
74
|
+
minify: IS_PROD,
|
|
75
|
+
format: 'esm',
|
|
76
|
+
target: 'node',
|
|
77
|
+
env: 'APP_PUBLIC_*',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Move CSS files to the public directory
|
|
81
|
+
for (const output of buildResult.outputs) {
|
|
82
|
+
if (output.path.endsWith('.css')) {
|
|
83
|
+
const cssFileName = output.path.split('/').pop()!;
|
|
84
|
+
await Bun.write(join(cssPublicDir, cssFileName), Bun.file(output.path));
|
|
85
|
+
cssFiles.push(cssFileName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const routeModule = await import(filePath);
|
|
90
|
+
const route = getRunnableRoute(routeModule);
|
|
91
|
+
|
|
92
|
+
// Set route path based on the file path
|
|
93
|
+
route._config.path = path;
|
|
94
|
+
|
|
95
|
+
if (cssFiles.length > 0) {
|
|
96
|
+
route._config.cssImports = cssFiles;
|
|
97
|
+
CSS_ROUTE_MAP.set(path, cssFiles);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
routeMap.push({ route: path, file: filePath.replace(CWD, '') });
|
|
101
|
+
|
|
102
|
+
return route;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}).filter(route => route !== null));
|
|
107
|
+
|
|
108
|
+
if (startConfig.development) {
|
|
109
|
+
console.log('[Hyperspan] Loaded routes:');
|
|
110
|
+
console.table(routeMap);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
server._routes.push(...routes);
|
|
114
|
+
}
|