hyperspan 1.0.0-alpha.1 → 1.0.0-alpha.10
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/package.json +2 -1
- package/src/index.ts +18 -32
- package/src/runtimes/bun.ts +57 -0
- package/src/server.ts +123 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperspan",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
4
4
|
"description": "Hyperspan CLI - for @hyperspan/framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"public": true,
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@hyperspan/framework": "^1.0.0-alpha",
|
|
36
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
36
37
|
"commander": "^14.0.2",
|
|
37
38
|
"degit": "^2.8.4"
|
|
38
39
|
},
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,8 @@ import degit from 'degit';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import packageJson from '../package.json';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { join } from 'node:path';
|
|
8
|
+
import { createHyperspanServer } from './server';
|
|
9
|
+
import { startBunServer } from './runtimes/bun';
|
|
11
10
|
|
|
12
11
|
const program = new Command();
|
|
13
12
|
|
|
@@ -43,9 +42,19 @@ program
|
|
|
43
42
|
*/
|
|
44
43
|
program
|
|
45
44
|
.command('start')
|
|
45
|
+
.alias('dev')
|
|
46
46
|
.option('--dir <path>', 'directory of your hyperspan project', './')
|
|
47
47
|
.description('Start the server')
|
|
48
|
-
.action(async (options)
|
|
48
|
+
.action(async function (options) {
|
|
49
|
+
|
|
50
|
+
const IS_DEV_MODE = process.argv.includes('dev');
|
|
51
|
+
|
|
52
|
+
// Developer mode (extra logging, etc.)
|
|
53
|
+
if (IS_DEV_MODE) {
|
|
54
|
+
console.log('[Hyperspan] Developer mode enabled 🛠️');
|
|
55
|
+
process.env.NODE_ENV = 'development';
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
// Ensure we are in a hyperspan project
|
|
50
59
|
const serverFile = `${options.dir}/app/routes`;
|
|
51
60
|
|
|
@@ -59,36 +68,13 @@ program
|
|
|
59
68
|
console.log('\n========================================');
|
|
60
69
|
console.log('[Hyperspan] Starting...');
|
|
61
70
|
|
|
62
|
-
const server = await
|
|
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
|
-
});
|
|
71
|
+
const server = await createHyperspanServer({ development: IS_DEV_MODE });
|
|
72
|
+
const httpServer = startBunServer(server);
|
|
89
73
|
|
|
90
74
|
console.log(`[Hyperspan] Server started on http://localhost:${httpServer.port} (Press Ctrl+C to stop)`);
|
|
91
75
|
console.log('========================================\n');
|
|
76
|
+
|
|
77
|
+
return httpServer;
|
|
92
78
|
});
|
|
93
79
|
|
|
94
80
|
program
|
|
@@ -108,7 +94,7 @@ program
|
|
|
108
94
|
|
|
109
95
|
const server = await import(serverFile);
|
|
110
96
|
|
|
111
|
-
|
|
97
|
+
// @TODO: Build the project for SSG...
|
|
112
98
|
});
|
|
113
99
|
|
|
114
100
|
program.parse();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createContext } from "@hyperspan/framework";
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { Hyperspan as HS } from '@hyperspan/framework';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Use Bun server. We don't have to do any path parsing here because Bun has its own path parsing logic with param passing in req.params.
|
|
8
|
+
* Using Bun HTTP server directly is the fastest way to serve the app, and is highly recommended for production.
|
|
9
|
+
*/
|
|
10
|
+
export function startBunServer(server: HS.Server) {
|
|
11
|
+
const routes: Record<string, ((request: Request) => Promise<Response>) | Response> = {};
|
|
12
|
+
|
|
13
|
+
// Add routemap for Bun server
|
|
14
|
+
for (const route of server._routes) {
|
|
15
|
+
const path = route._path();
|
|
16
|
+
|
|
17
|
+
// Add main route
|
|
18
|
+
routes[path] = (request: Request) => {
|
|
19
|
+
return route.fetch(request);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add trailing slash route to redirect to the main route if the main route doesn't have a trailing slash
|
|
23
|
+
if (!path.endsWith('/')) {
|
|
24
|
+
routes[path + '/'] = Response.redirect(path);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wildcard routes need a base route *without* the slash to redirect to the route *with* the slash
|
|
28
|
+
// Bun seems to not allow wildcard routes without a slash before the wildcard segment, so they always have a trailing slash
|
|
29
|
+
if (path.endsWith('/*')) {
|
|
30
|
+
const pathWithoutWildcard = path.replace('/*', '');
|
|
31
|
+
routes[pathWithoutWildcard] = Response.redirect(pathWithoutWildcard + '/');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const httpServer = Bun.serve({
|
|
36
|
+
routes,
|
|
37
|
+
fetch: async (request: Request) => {
|
|
38
|
+
// Serve static files from the public directory
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
if (url.pathname.startsWith('/_hs/')) {
|
|
41
|
+
return new Response(Bun.file(join('./', server._config.publicDir, url.pathname)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Other static file from the public directory
|
|
45
|
+
const file = Bun.file(join('./', server._config.publicDir, url.pathname))
|
|
46
|
+
const fileExists = await file.exists()
|
|
47
|
+
if (fileExists) {
|
|
48
|
+
return new Response(file);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Not found
|
|
52
|
+
return createContext(request).res.notFound();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return httpServer;
|
|
57
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { Glob } from
|
|
2
|
-
import { createServer, getRunnableRoute, IS_PROD
|
|
1
|
+
import { Glob } from 'bun';
|
|
2
|
+
import { createServer, getRunnableRoute, IS_PROD } from '@hyperspan/framework';
|
|
3
3
|
import { CSS_PUBLIC_PATH, CSS_ROUTE_MAP } from '@hyperspan/framework/client/css';
|
|
4
|
-
import {
|
|
4
|
+
import { isValidRoutePath, parsePath } from '@hyperspan/framework/utils';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import tailwind from "bun-plugin-tailwind"
|
|
5
7
|
|
|
6
8
|
import type { Hyperspan as HS } from '@hyperspan/framework';
|
|
9
|
+
|
|
7
10
|
type startConfig = {
|
|
8
11
|
development?: boolean;
|
|
9
|
-
}
|
|
12
|
+
};
|
|
10
13
|
|
|
11
14
|
const CWD = process.cwd();
|
|
12
15
|
|
|
13
16
|
export async function loadConfig(): Promise<HS.Config> {
|
|
14
|
-
const configFile = join(CWD,
|
|
15
|
-
const configModule = await import(configFile)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const configFile = join(CWD, 'hyperspan.config.ts');
|
|
18
|
+
const configModule = await import(configFile)
|
|
19
|
+
.then((module) => module.default)
|
|
20
|
+
.catch((error) => {
|
|
21
|
+
console.error(`[Hyperspan] Unable to load config file: ${error}`);
|
|
22
|
+
console.error(
|
|
23
|
+
`[Hyperspan] Please create a hyperspan.config.ts file in the root of your project.`
|
|
24
|
+
);
|
|
25
|
+
console.log(`[Hyperspan] Example:
|
|
19
26
|
import { createConfig } from '@hyperspan/framework';
|
|
20
27
|
|
|
21
28
|
export default createConfig({
|
|
@@ -23,92 +30,140 @@ export default createConfig({
|
|
|
23
30
|
publicDir: './public',
|
|
24
31
|
});
|
|
25
32
|
`);
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
28
35
|
return configModule;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
export async function
|
|
38
|
+
export async function createHyperspanServer(startConfig: startConfig = {}): Promise<HS.Server> {
|
|
32
39
|
console.log('[Hyperspan] Loading config...');
|
|
33
40
|
const config = await loadConfig();
|
|
34
41
|
const server = await createServer(config);
|
|
35
42
|
console.log('[Hyperspan] Adding routes...');
|
|
36
|
-
await
|
|
43
|
+
await addDirectoryAsRoutes(server, 'routes', startConfig);
|
|
44
|
+
console.log('[Hyperspan] Adding actions...');
|
|
45
|
+
await addDirectoryAsRoutes(server, 'actions', startConfig);
|
|
37
46
|
return server;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
export async function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
export async function addDirectoryAsRoutes(
|
|
50
|
+
server: HS.Server,
|
|
51
|
+
relativeDirectory: string,
|
|
52
|
+
startConfig: startConfig = {}
|
|
53
|
+
) {
|
|
54
|
+
const routesGlob = new Glob('**/*.ts');
|
|
55
|
+
const files: string[] = [];
|
|
56
|
+
const appDir = server._config.appDir || './app';
|
|
57
|
+
const relativeAppPath = join(appDir, relativeDirectory);
|
|
58
|
+
const directoryPath = join(CWD, appDir, relativeDirectory);
|
|
45
59
|
const buildDir = join(CWD, '.build');
|
|
46
60
|
const cssPublicDir = join(CWD, server._config.publicDir, CSS_PUBLIC_PATH);
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
try {
|
|
63
|
+
// Scan directory for TypeScript files
|
|
64
|
+
for await (const file of routesGlob.scan(directoryPath)) {
|
|
65
|
+
const filePath = join(directoryPath, file);
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
// Hidden directories and files start with a double underscore.
|
|
68
|
+
// These do not get added to the routes. Nothing nested under them gets added to the routes either.
|
|
69
|
+
if (filePath.includes('/__')) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
files.push(filePath);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`[Hyperspan] Directory not found: ${directoryPath}`);
|
|
58
77
|
}
|
|
59
78
|
|
|
60
|
-
const routeMap: { route: string
|
|
61
|
-
const routes: HS.Route
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
79
|
+
const routeMap: { route: string; file: string }[] = [];
|
|
80
|
+
const routes: Array<HS.Route> = (await Promise.all(
|
|
81
|
+
files.map(async (filePath) => {
|
|
82
|
+
try {
|
|
83
|
+
const relativeFilePath = filePath.split(relativeAppPath).pop() || '';
|
|
84
|
+
if (!isValidRoutePath(relativeFilePath)) {
|
|
85
|
+
return null;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
const module = await import(filePath);
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
const route = getRunnableRoute(routeModule);
|
|
89
|
+
const route = getRunnableRoute(module);
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
route._config.path = path;
|
|
91
|
+
const parsedPath = parsePath(relativeFilePath);
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
// If route has a _path() method that returns a meaningful path, use it
|
|
94
|
+
// Otherwise, parse path from file path
|
|
95
|
+
let path = parsedPath.path;
|
|
96
|
+
if (typeof route._path === 'function') {
|
|
97
|
+
const routePath = route._path();
|
|
98
|
+
// If _path() returns a meaningful path (not just '/'), use it
|
|
99
|
+
if (routePath && routePath !== '/') {
|
|
100
|
+
path = routePath;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
let cssFiles: string[] = [];
|
|
105
|
+
|
|
106
|
+
// Build the route just for the CSS files (expensive, but easiest way to do CSS compilation by route)
|
|
107
|
+
// @TODO: Optimize this at some later date... This is O(n) for each route and doesn't scale well for large projects.
|
|
108
|
+
// @TODO: This will also currently re-compile the same CSS file(s) that are included in multiple routes, which is dumb.
|
|
109
|
+
const buildResult = await Bun.build({
|
|
110
|
+
plugins: [tailwind],
|
|
111
|
+
entrypoints: [filePath],
|
|
112
|
+
outdir: buildDir,
|
|
113
|
+
naming: `${relativeAppPath}/${path.endsWith('/') ? path + 'index' : path}-[hash].[ext]`,
|
|
114
|
+
minify: IS_PROD,
|
|
115
|
+
format: 'esm',
|
|
116
|
+
target: 'node',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Move CSS files to the public directory
|
|
120
|
+
for (const output of buildResult.outputs) {
|
|
121
|
+
if (output.path.endsWith('.css')) {
|
|
122
|
+
const cssFileName = output.path.split('/').pop()!;
|
|
123
|
+
await Bun.write(join(cssPublicDir, cssFileName), Bun.file(output.path));
|
|
124
|
+
cssFiles.push(cssFileName);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
// Set route path based on the file path (if not already set)
|
|
129
|
+
if (!route._config.path) {
|
|
130
|
+
route._config.path = path;
|
|
131
|
+
|
|
132
|
+
// Initialize params object if it doesn't exist
|
|
133
|
+
if (parsedPath.params.length > 0) {
|
|
134
|
+
const params = route._config.params ?? {};
|
|
135
|
+
parsedPath.params.forEach(param => {
|
|
136
|
+
params[param] = undefined;
|
|
137
|
+
});
|
|
138
|
+
route._config.params = params;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (cssFiles.length > 0) {
|
|
143
|
+
route._config.cssImports = cssFiles;
|
|
144
|
+
CSS_ROUTE_MAP.set(path, cssFiles);
|
|
145
|
+
}
|
|
104
146
|
|
|
105
|
-
|
|
106
|
-
|
|
147
|
+
routeMap.push({ route: path, file: filePath.replace(CWD, '') });
|
|
148
|
+
|
|
149
|
+
return route;
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`[Hyperspan] Error loading route: ${filePath}`);
|
|
153
|
+
console.error(error);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
)).filter((route) => route !== null);
|
|
158
|
+
|
|
159
|
+
if (routeMap.length === 0) {
|
|
160
|
+
console.log(`[Hyperspan] No routes found in ${relativeDirectory}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
107
163
|
|
|
108
164
|
if (startConfig.development) {
|
|
109
|
-
console.log('[Hyperspan] Loaded routes:');
|
|
110
165
|
console.table(routeMap);
|
|
111
166
|
}
|
|
112
167
|
|
|
113
168
|
server._routes.push(...routes);
|
|
114
|
-
}
|
|
169
|
+
}
|