@hyperspan/framework 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 +3 -81
- package/package.json +29 -29
- package/src/assets.ts +141 -0
- package/src/clientjs/hyperspan-client.ts +7 -175
- package/src/clientjs/idiomorph.esm.js +1278 -0
- package/src/clientjs/preact.ts +1 -0
- package/src/server.ts +298 -146
- package/.prettierrc +0 -7
- package/build.ts +0 -29
- package/bun.lockb +0 -0
- package/dist/index.d.ts +0 -50
- package/dist/index.js +0 -468
- package/dist/server.d.ts +0 -109
- package/dist/server.js +0 -1941
- package/src/app.ts +0 -186
- package/src/clientjs/idomorph.esm.js +0 -854
- package/src/document.ts +0 -10
- package/src/forms.ts +0 -110
- package/src/html.test.ts +0 -69
- package/src/html.ts +0 -342
- package/src/index.ts +0 -14
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@preact/compat';
|
package/src/server.ts
CHANGED
|
@@ -1,90 +1,292 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import { basename, extname, join
|
|
3
|
-
import { html,
|
|
2
|
+
import { basename, extname, join } from 'node:path';
|
|
3
|
+
import { TmplHtml, html, renderStream, renderAsync, render } from '@hyperspan/html';
|
|
4
4
|
import { isbot } from 'isbot';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { buildClientJS, buildClientCSS } from './assets';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { serveStatic } from 'hono/bun';
|
|
8
|
+
import type { Context, Handler } from 'hono';
|
|
9
|
+
|
|
10
|
+
import * as v from 'valibot';
|
|
11
|
+
import type {
|
|
12
|
+
AnySchema,
|
|
13
|
+
ArraySchema,
|
|
14
|
+
BigintSchema,
|
|
15
|
+
BooleanSchema,
|
|
16
|
+
DateSchema,
|
|
17
|
+
EnumSchema,
|
|
18
|
+
GenericIssue,
|
|
19
|
+
IntersectSchema,
|
|
20
|
+
LazySchema,
|
|
21
|
+
LiteralSchema,
|
|
22
|
+
NullSchema,
|
|
23
|
+
NullableSchema,
|
|
24
|
+
NullishSchema,
|
|
25
|
+
NumberSchema,
|
|
26
|
+
ObjectSchema,
|
|
27
|
+
ObjectWithRestSchema,
|
|
28
|
+
OptionalSchema,
|
|
29
|
+
PicklistSchema,
|
|
30
|
+
PipeItem,
|
|
31
|
+
RecordSchema,
|
|
32
|
+
SchemaWithPipe,
|
|
33
|
+
StrictObjectSchema,
|
|
34
|
+
StrictTupleSchema,
|
|
35
|
+
StringSchema,
|
|
36
|
+
TupleSchema,
|
|
37
|
+
TupleWithRestSchema,
|
|
38
|
+
UndefinedSchema,
|
|
39
|
+
UnionSchema,
|
|
40
|
+
VariantSchema,
|
|
41
|
+
} from 'valibot';
|
|
7
42
|
|
|
8
43
|
export const IS_PROD = process.env.NODE_ENV === 'production';
|
|
9
|
-
const
|
|
10
|
-
const
|
|
44
|
+
const PWD = import.meta.dir;
|
|
45
|
+
const CWD = process.cwd();
|
|
46
|
+
|
|
47
|
+
type NonPipeSchemas =
|
|
48
|
+
| AnySchema
|
|
49
|
+
| LiteralSchema<any, any>
|
|
50
|
+
| NullSchema<any>
|
|
51
|
+
| NumberSchema<any>
|
|
52
|
+
| BigintSchema<any>
|
|
53
|
+
| StringSchema<any>
|
|
54
|
+
| BooleanSchema<any>
|
|
55
|
+
| NullableSchema<any, any>
|
|
56
|
+
| StrictObjectSchema<any, any>
|
|
57
|
+
| ObjectSchema<any, any>
|
|
58
|
+
| ObjectWithRestSchema<any, any, any>
|
|
59
|
+
| RecordSchema<any, any, any>
|
|
60
|
+
| ArraySchema<any, any>
|
|
61
|
+
| TupleSchema<any, any>
|
|
62
|
+
| StrictTupleSchema<any, any>
|
|
63
|
+
| TupleWithRestSchema<readonly any[], any, any>
|
|
64
|
+
| IntersectSchema<any, any>
|
|
65
|
+
| UnionSchema<any, any>
|
|
66
|
+
| VariantSchema<any, any, any>
|
|
67
|
+
| PicklistSchema<any, any>
|
|
68
|
+
| EnumSchema<any, any>
|
|
69
|
+
| LazySchema<any>
|
|
70
|
+
| DateSchema<any>
|
|
71
|
+
| NullishSchema<any, any>
|
|
72
|
+
| OptionalSchema<any, any>
|
|
73
|
+
| UndefinedSchema<any>;
|
|
74
|
+
|
|
75
|
+
type PipeSchema = SchemaWithPipe<[NonPipeSchemas, ...PipeItem<any, any, GenericIssue<any>>[]]>;
|
|
76
|
+
// Type inference for valibot taken from:
|
|
77
|
+
// @link https://github.com/gcornut/valibot-json-schema/blob/main/src/toJSONSchema/schemas.ts
|
|
78
|
+
export type TSupportedSchema = NonPipeSchemas | PipeSchema;
|
|
11
79
|
|
|
12
|
-
|
|
13
|
-
|
|
80
|
+
/**
|
|
81
|
+
* ===========================================================================
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Route
|
|
86
|
+
* Define a route that can handle a direct HTTP request
|
|
87
|
+
* Route handlers should return a Response or TmplHtml object
|
|
88
|
+
*/
|
|
89
|
+
export function createRoute(handler: Handler): HSRoute {
|
|
90
|
+
return new HSRoute(handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Component
|
|
95
|
+
* Define a component or partial with an optional loading placeholder
|
|
96
|
+
* These can be rendered anywhere inside other templates - even if async.
|
|
97
|
+
*/
|
|
98
|
+
export function createComponent(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
|
|
99
|
+
return new HSComponent(render);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Form + route handler
|
|
104
|
+
* Automatically handles and parses form data
|
|
105
|
+
*
|
|
106
|
+
* INITIAL IDEA OF HOW THIS WILL WORK:
|
|
107
|
+
* ---
|
|
108
|
+
* 1. Renders component as initial form markup for GET request
|
|
109
|
+
* 2. Bind form onSubmit function to custom client JS handling
|
|
110
|
+
* 3. Submits form with JavaScript fetch()
|
|
111
|
+
* 4. Replaces form content with content from server
|
|
112
|
+
* 5. All validation and save logic is on the server
|
|
113
|
+
* 6. Handles any Exception thrown on server as error displayed in client
|
|
114
|
+
*/
|
|
115
|
+
export function createForm(
|
|
116
|
+
renderForm: (data?: any) => THSResponseTypes,
|
|
117
|
+
schema?: TSupportedSchema | null
|
|
118
|
+
): HSFormRoute {
|
|
119
|
+
return new HSFormRoute(renderForm, schema);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Types
|
|
124
|
+
*/
|
|
125
|
+
export type THSComponentReturn = TmplHtml | string | number | null;
|
|
126
|
+
export type THSResponseTypes = TmplHtml | Response | string | null;
|
|
127
|
+
export const HS_DEFAULT_LOADING = () => html`<div>Loading...</div>`;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Route handler helper
|
|
131
|
+
*/
|
|
132
|
+
export class HSComponent {
|
|
133
|
+
_kind = 'hsComponent';
|
|
134
|
+
_handlers: Record<string, Handler> = {};
|
|
135
|
+
_loading?: () => TmplHtml;
|
|
136
|
+
render: () => THSComponentReturn | Promise<THSComponentReturn>;
|
|
137
|
+
constructor(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
|
|
138
|
+
this.render = render;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
loading(fn: () => TmplHtml) {
|
|
142
|
+
this._loading = fn;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Route handler helper
|
|
149
|
+
*/
|
|
150
|
+
export class HSRoute {
|
|
151
|
+
_kind = 'hsRoute';
|
|
152
|
+
_handlers: Record<string, Handler> = {};
|
|
153
|
+
_methods: null | string[] = null;
|
|
154
|
+
constructor(handler: Handler) {
|
|
155
|
+
this._handlers.GET = handler;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
14
158
|
|
|
15
159
|
/**
|
|
16
|
-
*
|
|
160
|
+
* Form route handler helper
|
|
17
161
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
162
|
+
export type THSFormRenderer = (data?: any) => THSResponseTypes;
|
|
163
|
+
export class HSFormRoute {
|
|
164
|
+
_kind = 'hsFormRoute';
|
|
165
|
+
_handlers: Record<string, Handler> = {};
|
|
166
|
+
_form: THSFormRenderer;
|
|
167
|
+
_methods: null | string[] = null;
|
|
168
|
+
_schema: null | TSupportedSchema = null;
|
|
169
|
+
|
|
170
|
+
constructor(renderForm: THSFormRenderer, schema: TSupportedSchema | null = null) {
|
|
171
|
+
// Haz schema?
|
|
172
|
+
if (schema) {
|
|
173
|
+
type TSchema = v.InferInput<typeof schema>;
|
|
174
|
+
this._form = renderForm as (data: TSchema) => THSResponseTypes;
|
|
175
|
+
this._schema = schema;
|
|
176
|
+
} else {
|
|
177
|
+
this._form = renderForm;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// GET request is render form by default
|
|
181
|
+
this._handlers.GET = (ctx: Context) => renderForm(this.getDefaultData());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Form data
|
|
185
|
+
getDefaultData() {
|
|
186
|
+
if (!this._schema) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
20
189
|
|
|
21
|
-
|
|
190
|
+
type TSchema = v.InferInput<typeof this._schema>;
|
|
191
|
+
const data = v.parse(this._schema, {});
|
|
192
|
+
return data as TSchema;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get form renderer method
|
|
197
|
+
*/
|
|
198
|
+
renderForm(data?: any) {
|
|
199
|
+
return this._form(data || this.getDefaultData());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// HTTP handlers
|
|
203
|
+
get(handler: Handler) {
|
|
204
|
+
this._handlers.GET = handler;
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
patch(handler: Handler) {
|
|
209
|
+
this._handlers.PATCH = handler;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
post(handler: Handler) {
|
|
214
|
+
this._handlers.POST = handler;
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
put(handler: Handler) {
|
|
219
|
+
this._handlers.PUT = handler;
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
delete(handler: Handler) {
|
|
224
|
+
this._handlers.DELETE = handler;
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
22
227
|
}
|
|
23
228
|
|
|
24
229
|
/**
|
|
25
230
|
* Run route from file
|
|
26
231
|
*/
|
|
27
|
-
export async function runFileRoute(
|
|
232
|
+
export async function runFileRoute(RouteModule: any, context: Context): Promise<Response | false> {
|
|
28
233
|
const req = context.req;
|
|
29
234
|
const url = new URL(req.url);
|
|
30
235
|
const qs = url.searchParams;
|
|
31
236
|
|
|
32
237
|
// @TODO: Move this to config or something...
|
|
238
|
+
const userIsBot = isbot(context.req.header('User-Agent'));
|
|
33
239
|
const streamOpt = qs.get('__nostream') ? !Boolean(qs.get('__nostream')) : undefined;
|
|
34
|
-
const streamingEnabled = streamOpt !== undefined ? streamOpt : true;
|
|
35
|
-
|
|
36
|
-
// Import route component
|
|
37
|
-
const RouteModule = _routeCache[routeFile] || (await import(routeFile));
|
|
38
|
-
|
|
39
|
-
if (IS_PROD) {
|
|
40
|
-
// Only cache routes in prod
|
|
41
|
-
_routeCache[routeFile] = RouteModule;
|
|
42
|
-
}
|
|
240
|
+
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
43
241
|
|
|
44
242
|
// Route module
|
|
45
243
|
const RouteComponent = RouteModule.default;
|
|
46
244
|
const reqMethod = req.method.toUpperCase();
|
|
47
245
|
|
|
48
|
-
// Middleware?
|
|
49
|
-
const routeMiddleware = RouteModule.middleware || {}; // Example: { auth: apiAuth, logger: logMiddleware, }
|
|
50
|
-
const middlewareResult: any = {};
|
|
51
|
-
|
|
52
246
|
try {
|
|
53
|
-
// Run middleware if present...
|
|
54
|
-
if (Object.keys(routeMiddleware).length) {
|
|
55
|
-
for (const mKey in routeMiddleware) {
|
|
56
|
-
const mRes = await routeMiddleware[mKey](context);
|
|
57
|
-
|
|
58
|
-
if (mRes instanceof Response) {
|
|
59
|
-
return context.resMerge(mRes);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
middlewareResult[mKey] = mRes;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
247
|
// API Route?
|
|
67
248
|
if (RouteModule[reqMethod] !== undefined) {
|
|
68
|
-
return await runAPIRoute(RouteModule[reqMethod], context
|
|
249
|
+
return await runAPIRoute(RouteModule[reqMethod], context);
|
|
69
250
|
}
|
|
70
251
|
|
|
71
|
-
|
|
72
|
-
const routeContent = await RouteComponent(context, middlewareResult);
|
|
252
|
+
let routeContent;
|
|
73
253
|
|
|
74
|
-
|
|
75
|
-
|
|
254
|
+
// No default export in this file...
|
|
255
|
+
if (!RouteComponent) {
|
|
256
|
+
throw new Error('No route was exported by default in matched route file.');
|
|
76
257
|
}
|
|
77
258
|
|
|
78
|
-
|
|
79
|
-
|
|
259
|
+
// Route component
|
|
260
|
+
if (typeof RouteComponent._handlers !== 'undefined') {
|
|
261
|
+
const routeMethodHandler = RouteComponent._handlers[reqMethod];
|
|
262
|
+
|
|
263
|
+
if (!routeMethodHandler) {
|
|
264
|
+
return new Response('Method Not Allowed', {
|
|
265
|
+
status: 405,
|
|
266
|
+
headers: { 'content-type': 'text/plain' },
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
routeContent = await routeMethodHandler(context);
|
|
80
271
|
} else {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const output = await renderToString(routeContent);
|
|
272
|
+
routeContent = await RouteComponent(context);
|
|
273
|
+
}
|
|
84
274
|
|
|
85
|
-
|
|
86
|
-
return
|
|
275
|
+
if (routeContent instanceof Response) {
|
|
276
|
+
return routeContent;
|
|
87
277
|
}
|
|
278
|
+
|
|
279
|
+
// Render TmplHtml if returned from route handler
|
|
280
|
+
if (routeContent instanceof TmplHtml) {
|
|
281
|
+
if (streamingEnabled) {
|
|
282
|
+
return new StreamResponse(renderStream(routeContent)) as Response;
|
|
283
|
+
} else {
|
|
284
|
+
const output = await renderAsync(routeContent);
|
|
285
|
+
return context.html(output);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return routeContent;
|
|
88
290
|
} catch (e) {
|
|
89
291
|
console.error(e);
|
|
90
292
|
return await showErrorReponse(context, e as Error);
|
|
@@ -94,7 +296,7 @@ export async function runFileRoute(routeFile: string, context: HSRequestContext)
|
|
|
94
296
|
/**
|
|
95
297
|
* Run route and handle response
|
|
96
298
|
*/
|
|
97
|
-
async function runAPIRoute(routeFn: any, context:
|
|
299
|
+
async function runAPIRoute(routeFn: any, context: Context, middlewareResult?: any) {
|
|
98
300
|
try {
|
|
99
301
|
return await routeFn(context, middlewareResult);
|
|
100
302
|
} catch (err) {
|
|
@@ -118,8 +320,8 @@ async function runAPIRoute(routeFn: any, context: HSRequestContext, middlewareRe
|
|
|
118
320
|
* Basic error handling
|
|
119
321
|
* @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
|
|
120
322
|
*/
|
|
121
|
-
async function showErrorReponse(context:
|
|
122
|
-
const output =
|
|
323
|
+
async function showErrorReponse(context: Context, err: Error) {
|
|
324
|
+
const output = render(html`
|
|
123
325
|
<main>
|
|
124
326
|
<h1>Error</h1>
|
|
125
327
|
<pre>${err.message}</pre>
|
|
@@ -135,9 +337,10 @@ async function showErrorReponse(context: HSRequestContext, err: Error) {
|
|
|
135
337
|
export type THSServerConfig = {
|
|
136
338
|
appDir: string;
|
|
137
339
|
staticFileRoot: string;
|
|
340
|
+
rewrites?: Array<{ source: string; destination: string }>;
|
|
138
341
|
// For customizing the routes and adding your own...
|
|
139
|
-
beforeRoutesAdded?: (app:
|
|
140
|
-
afterRoutesAdded?: (app:
|
|
342
|
+
beforeRoutesAdded?: (app: Hono) => void;
|
|
343
|
+
afterRoutesAdded?: (app: Hono) => void;
|
|
141
344
|
};
|
|
142
345
|
|
|
143
346
|
export type THSRouteMap = {
|
|
@@ -153,6 +356,7 @@ const ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
|
|
|
153
356
|
export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]> {
|
|
154
357
|
// Walk all pages and add them as routes
|
|
155
358
|
const routesDir = join(config.appDir, 'routes');
|
|
359
|
+
console.log(routesDir);
|
|
156
360
|
const files = await readdir(routesDir, { recursive: true });
|
|
157
361
|
const routes: THSRouteMap[] = [];
|
|
158
362
|
|
|
@@ -189,7 +393,7 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
|
|
|
189
393
|
}
|
|
190
394
|
|
|
191
395
|
routes.push({
|
|
192
|
-
file,
|
|
396
|
+
file: join('./', routesDir, file),
|
|
193
397
|
route: route || '/',
|
|
194
398
|
params,
|
|
195
399
|
});
|
|
@@ -201,15 +405,11 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
|
|
|
201
405
|
/**
|
|
202
406
|
* Create and start Bun HTTP server
|
|
203
407
|
*/
|
|
204
|
-
export async function createServer(config: THSServerConfig): Promise<
|
|
408
|
+
export async function createServer(config: THSServerConfig): Promise<Hono> {
|
|
205
409
|
// Build client JS and CSS bundles so they are available for templates when streaming starts
|
|
206
410
|
await Promise.all([buildClientJS(), buildClientCSS()]);
|
|
207
411
|
|
|
208
|
-
const app = new
|
|
209
|
-
|
|
210
|
-
app.defaultRoute(() => {
|
|
211
|
-
return new Response('Not... found?', { status: 404 });
|
|
212
|
-
});
|
|
412
|
+
const app = new Hono();
|
|
213
413
|
|
|
214
414
|
// [Customization] Before routes added...
|
|
215
415
|
config.beforeRoutesAdded && config.beforeRoutesAdded(app);
|
|
@@ -220,18 +420,31 @@ export async function createServer(config: THSServerConfig): Promise<HSApp> {
|
|
|
220
420
|
|
|
221
421
|
for (let i = 0; i < fileRoutes.length; i++) {
|
|
222
422
|
let route = fileRoutes[i];
|
|
223
|
-
const fullRouteFile = join(
|
|
423
|
+
const fullRouteFile = join(CWD, route.file);
|
|
224
424
|
const routePattern = normalizePath(route.route);
|
|
225
425
|
|
|
226
|
-
routeMap.push({ route: routePattern, file:
|
|
426
|
+
routeMap.push({ route: routePattern, file: route.file });
|
|
427
|
+
|
|
428
|
+
// Import route
|
|
429
|
+
const routeModule = await import(fullRouteFile);
|
|
227
430
|
|
|
228
431
|
app.all(routePattern, async (context) => {
|
|
229
|
-
const matchedRoute = await runFileRoute(
|
|
432
|
+
const matchedRoute = await runFileRoute(routeModule, context);
|
|
230
433
|
if (matchedRoute) {
|
|
231
434
|
return matchedRoute as Response;
|
|
232
435
|
}
|
|
233
436
|
|
|
234
|
-
return
|
|
437
|
+
return context.notFound();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Help route if no routes found
|
|
442
|
+
if (routeMap.length === 0) {
|
|
443
|
+
app.get('/', (context) => {
|
|
444
|
+
return context.text(
|
|
445
|
+
'No routes found. Add routes to app/routes. Example: `app/routes/index.ts`',
|
|
446
|
+
{ status: 404 }
|
|
447
|
+
);
|
|
235
448
|
});
|
|
236
449
|
}
|
|
237
450
|
|
|
@@ -245,80 +458,26 @@ export async function createServer(config: THSServerConfig): Promise<HSApp> {
|
|
|
245
458
|
config.afterRoutesAdded && config.afterRoutesAdded(app);
|
|
246
459
|
|
|
247
460
|
// Static files and catchall
|
|
248
|
-
app.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (IS_PROD) {
|
|
258
|
-
headers = {
|
|
259
|
-
'cache-control': 'public, max-age=31557600',
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return new Response(file, { headers });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return app._defaultRoute(context);
|
|
461
|
+
app.use(
|
|
462
|
+
'*',
|
|
463
|
+
serveStatic({
|
|
464
|
+
root: config.staticFileRoot,
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
app.notFound((context) => {
|
|
469
|
+
return context.text('Not... found?', { status: 404 });
|
|
267
470
|
});
|
|
268
471
|
|
|
269
472
|
return app;
|
|
270
473
|
}
|
|
271
474
|
|
|
272
|
-
/**
|
|
273
|
-
* Build client JS for end users (minimal JS for Hyperspan to work)
|
|
274
|
-
*/
|
|
275
|
-
export let clientJSFile: string;
|
|
276
|
-
export async function buildClientJS() {
|
|
277
|
-
const sourceFile = resolve(CWD, '../', './src/clientjs/hyperspan-client.ts');
|
|
278
|
-
const output = await Bun.build({
|
|
279
|
-
entrypoints: [sourceFile],
|
|
280
|
-
outdir: `./public/_hs/js`,
|
|
281
|
-
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
282
|
-
minify: IS_PROD,
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
clientJSFile = output.outputs[0].path.split('/').reverse()[0];
|
|
286
|
-
return clientJSFile;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Find client CSS file built for end users
|
|
291
|
-
* @TODO: Build this in code here vs. relying on tailwindcss CLI tool from package scripts
|
|
292
|
-
*/
|
|
293
|
-
export let clientCSSFile: string;
|
|
294
|
-
export async function buildClientCSS() {
|
|
295
|
-
if (clientCSSFile) {
|
|
296
|
-
return clientCSSFile;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Find file already built from tailwindcss CLI
|
|
300
|
-
const cssDir = './public/_hs/css/';
|
|
301
|
-
const cssFiles = await readdir(cssDir);
|
|
302
|
-
|
|
303
|
-
for (const file of cssFiles) {
|
|
304
|
-
// Only looking for CSS files
|
|
305
|
-
if (clientCSSFile || !file.endsWith('.css')) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return (clientCSSFile = file.replace(cssDir, ''));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (!clientCSSFile) {
|
|
313
|
-
throw new Error(`Unable to build CSS files from ${cssDir}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
475
|
/**
|
|
318
476
|
* Streaming HTML Response
|
|
319
477
|
*/
|
|
320
|
-
export class StreamResponse {
|
|
478
|
+
export class StreamResponse extends Response {
|
|
321
479
|
constructor(iterator: AsyncIterator<unknown>, options = {}) {
|
|
480
|
+
super();
|
|
322
481
|
const stream = createReadableStreamFromAsyncGenerator(iterator as AsyncGenerator);
|
|
323
482
|
|
|
324
483
|
return new Response(stream, {
|
|
@@ -356,19 +515,12 @@ export function createReadableStreamFromAsyncGenerator(output: AsyncGenerator) {
|
|
|
356
515
|
}
|
|
357
516
|
|
|
358
517
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* 1. Renders component as initial form markup
|
|
363
|
-
* 2. Bind form onSubmit function to custom client JS handling
|
|
364
|
-
* 3. Submits form with JavaScript fetch()
|
|
365
|
-
* 4. Replaces form content with content from server
|
|
366
|
-
* 5. All validation and save logic is on the server
|
|
367
|
-
* 6. Handles any Exception thrown on server as error displayed in client
|
|
518
|
+
* Normalize URL path
|
|
519
|
+
* Removes trailing slash and lowercases path
|
|
368
520
|
*/
|
|
369
|
-
export
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
521
|
+
export function normalizePath(urlPath: string): string {
|
|
522
|
+
return (
|
|
523
|
+
(urlPath.endsWith('/') ? urlPath.substring(0, urlPath.length - 1) : urlPath).toLowerCase() ||
|
|
524
|
+
'/'
|
|
525
|
+
);
|
|
374
526
|
}
|
package/.prettierrc
DELETED
package/build.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import dts from 'bun-plugin-dts';
|
|
2
|
-
|
|
3
|
-
await Promise.all([
|
|
4
|
-
// Build JS
|
|
5
|
-
Bun.build({
|
|
6
|
-
entrypoints: ['./src/index.ts'],
|
|
7
|
-
outdir: './dist',
|
|
8
|
-
target: 'browser',
|
|
9
|
-
}),
|
|
10
|
-
Bun.build({
|
|
11
|
-
entrypoints: ['./src/server.ts'],
|
|
12
|
-
outdir: './dist',
|
|
13
|
-
target: 'node',
|
|
14
|
-
}),
|
|
15
|
-
|
|
16
|
-
// Build type files for TypeScript
|
|
17
|
-
Bun.build({
|
|
18
|
-
entrypoints: ['./src/index.ts'],
|
|
19
|
-
outdir: './dist',
|
|
20
|
-
target: 'browser',
|
|
21
|
-
plugins: [dts()],
|
|
22
|
-
}),
|
|
23
|
-
Bun.build({
|
|
24
|
-
entrypoints: ['./src/server.ts'],
|
|
25
|
-
outdir: './dist',
|
|
26
|
-
target: 'node',
|
|
27
|
-
plugins: [dts()],
|
|
28
|
-
}),
|
|
29
|
-
]);
|
package/bun.lockb
DELETED
|
Binary file
|
package/dist/index.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Template object - used so it will be possible to (eventually) pass down context
|
|
5
|
-
*/
|
|
6
|
-
export declare class HSTemplate {
|
|
7
|
-
__hsTemplate: boolean;
|
|
8
|
-
content: any[];
|
|
9
|
-
constructor(content: any[]);
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* HTML template
|
|
13
|
-
*/
|
|
14
|
-
export declare function html(strings: TemplateStringsArray, ...values: any[]): HSTemplate;
|
|
15
|
-
export declare namespace html {
|
|
16
|
-
var raw: (value: string) => HSTemplate;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Render HSTemplate to async generator that streams output to a string
|
|
20
|
-
*/
|
|
21
|
-
export declare function renderToStream(template: HSTemplate | string): AsyncGenerator<string>;
|
|
22
|
-
/**
|
|
23
|
-
* Render HSTemplate to string (awaits/buffers entire response)
|
|
24
|
-
*/
|
|
25
|
-
export declare function renderToString(template: HSTemplate | string): Promise<string>;
|
|
26
|
-
/**
|
|
27
|
-
* Strip extra spacing between HTML tags (used for tests)
|
|
28
|
-
*/
|
|
29
|
-
export declare function compressHTMLString(str: string): string;
|
|
30
|
-
/**
|
|
31
|
-
* LOL JavaScript...
|
|
32
|
-
*/
|
|
33
|
-
export declare function _typeOf(obj: any): string;
|
|
34
|
-
/**
|
|
35
|
-
* Client component
|
|
36
|
-
*/
|
|
37
|
-
export type THSWCState = Record<string, any>;
|
|
38
|
-
export type THSWCSetStateArg = THSWCState | ((state: THSWCState) => THSWCState);
|
|
39
|
-
export type THSWC = {
|
|
40
|
-
this: THSWC;
|
|
41
|
-
state: THSWCState | undefined;
|
|
42
|
-
id: string;
|
|
43
|
-
setState: (fn: THSWCSetStateArg) => THSWCState;
|
|
44
|
-
mergeState: (newState: THSWCState) => THSWCState;
|
|
45
|
-
render: () => any;
|
|
46
|
-
};
|
|
47
|
-
export type THSWCUser = Pick<THSWC, "render"> & Record<string, any>;
|
|
48
|
-
export declare function clientComponent(id: string, wc: THSWCUser): (attrs?: Record<string, string>, state?: Record<string, any>) => HSTemplate;
|
|
49
|
-
|
|
50
|
-
export {};
|