@hyperspan/framework 0.0.3 → 0.1.1
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/build.ts +24 -26
- package/dist/assets.d.ts +34 -0
- package/dist/assets.js +394 -0
- package/dist/index.d.ts +101 -30
- package/dist/index.js +2421 -408
- package/dist/server.d.ts +85 -73
- package/dist/server.js +2142 -1603
- package/package.json +42 -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/index.ts +1 -14
- package/src/server.ts +232 -151
- package/.prettierrc +0 -7
- package/bun.lockb +0 -0
- 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 -345
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@preact/compat';
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Hyperspan
|
|
3
|
-
* Only export html/client libs by default just incase this gets in a JS bundle
|
|
4
|
-
* Server package must be imported with 'hypserspan/server'
|
|
5
|
-
*/
|
|
6
|
-
export {
|
|
7
|
-
_typeOf,
|
|
8
|
-
clientComponent,
|
|
9
|
-
compressHTMLString,
|
|
10
|
-
html,
|
|
11
|
-
HSTemplate,
|
|
12
|
-
renderToStream,
|
|
13
|
-
renderToString,
|
|
14
|
-
} from './html';
|
|
1
|
+
export * from './server';
|
package/src/server.ts
CHANGED
|
@@ -1,91 +1,222 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { html,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import {readdir} from 'node:fs/promises';
|
|
2
|
+
import {basename, extname, join} from 'node:path';
|
|
3
|
+
import {TmplHtml, html, renderStream, renderAsync, render} from '@hyperspan/html';
|
|
4
|
+
import {isbot} from 'isbot';
|
|
5
|
+
import {buildClientJS, buildClientCSS} from './assets';
|
|
6
|
+
import {Hono} from 'hono';
|
|
7
|
+
import {serveStatic} from 'hono/bun';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import type {Context, Handler} from 'hono';
|
|
7
10
|
|
|
8
11
|
export const IS_PROD = process.env.NODE_ENV === 'production';
|
|
9
|
-
const PWD = import.meta.dir;
|
|
10
12
|
const CWD = process.cwd();
|
|
11
|
-
const STATIC_FILE_MATCHER = /[^/\\&\?]+\.([a-zA-Z]+)$/;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Route
|
|
16
|
+
* Define a route that can handle a direct HTTP request
|
|
17
|
+
* Route handlers should return a Response or TmplHtml object
|
|
18
|
+
*/
|
|
19
|
+
export function createRoute(handler: Handler): HSRoute {
|
|
20
|
+
return new HSRoute(handler);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Component
|
|
25
|
+
* Define a component or partial with an optional loading placeholder
|
|
26
|
+
* These can be rendered anywhere inside other templates - even if async.
|
|
27
|
+
*/
|
|
28
|
+
export function createComponent(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
|
|
29
|
+
return new HSComponent(render);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Form + route handler
|
|
34
|
+
* Automatically handles and parses form data
|
|
35
|
+
*
|
|
36
|
+
* INITIAL IDEA OF HOW THIS WILL WORK:
|
|
37
|
+
* ---
|
|
38
|
+
* 1. Renders component as initial form markup for GET request
|
|
39
|
+
* 2. Bind form onSubmit function to custom client JS handling
|
|
40
|
+
* 3. Submits form with JavaScript fetch()
|
|
41
|
+
* 4. Replaces form content with content from server
|
|
42
|
+
* 5. All validation and save logic is on the server
|
|
43
|
+
* 6. Handles any Exception thrown on server as error displayed in client
|
|
44
|
+
*/
|
|
45
|
+
export function createForm(
|
|
46
|
+
renderForm: (data?: any) => THSResponseTypes,
|
|
47
|
+
schema?: z.ZodSchema | null
|
|
48
|
+
): HSFormRoute {
|
|
49
|
+
return new HSFormRoute(renderForm, schema);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Types
|
|
54
|
+
*/
|
|
55
|
+
export type THSComponentReturn = TmplHtml | string | number | null;
|
|
56
|
+
export type THSResponseTypes = TmplHtml | Response | string | null;
|
|
57
|
+
export const HS_DEFAULT_LOADING = () => html`<div>Loading...</div>`;
|
|
15
58
|
|
|
16
59
|
/**
|
|
17
|
-
*
|
|
60
|
+
* Route handler helper
|
|
18
61
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
62
|
+
export class HSComponent {
|
|
63
|
+
_kind = 'hsComponent';
|
|
64
|
+
_handlers: Record<string, Handler> = {};
|
|
65
|
+
_loading?: () => TmplHtml;
|
|
66
|
+
render: () => THSComponentReturn | Promise<THSComponentReturn>;
|
|
67
|
+
constructor(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
|
|
68
|
+
this.render = render;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
loading(fn: () => TmplHtml) {
|
|
72
|
+
this._loading = fn;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
21
76
|
|
|
22
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Route handler helper
|
|
79
|
+
*/
|
|
80
|
+
export class HSRoute {
|
|
81
|
+
_kind = 'hsRoute';
|
|
82
|
+
_handlers: Record<string, Handler> = {};
|
|
83
|
+
_methods: null | string[] = null;
|
|
84
|
+
constructor(handler: Handler) {
|
|
85
|
+
this._handlers.GET = handler;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Form route handler helper
|
|
91
|
+
*/
|
|
92
|
+
export type THSFormRenderer = (data?: any) => THSResponseTypes;
|
|
93
|
+
export class HSFormRoute {
|
|
94
|
+
_kind = 'hsFormRoute';
|
|
95
|
+
_handlers: Record<string, Handler> = {};
|
|
96
|
+
_form: THSFormRenderer;
|
|
97
|
+
_methods: null | string[] = null;
|
|
98
|
+
_schema: null | z.ZodSchema = null;
|
|
99
|
+
|
|
100
|
+
constructor(renderForm: THSFormRenderer, schema: z.ZodSchema | null = null) {
|
|
101
|
+
// Haz schema?
|
|
102
|
+
if (schema) {
|
|
103
|
+
type TSchema = z.infer<typeof schema>;
|
|
104
|
+
this._form = renderForm as (data: TSchema) => THSResponseTypes;
|
|
105
|
+
this._schema = schema;
|
|
106
|
+
} else {
|
|
107
|
+
this._form = renderForm;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// GET request is render form by default
|
|
111
|
+
this._handlers.GET = () => renderForm(this.getDefaultData());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Form data
|
|
115
|
+
getDefaultData() {
|
|
116
|
+
if (!this._schema) {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type TSchema = z.infer<typeof this._schema>;
|
|
121
|
+
const data = z.parse(this._schema, {});
|
|
122
|
+
return data as TSchema;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get form renderer method
|
|
127
|
+
*/
|
|
128
|
+
renderForm(data?: any) {
|
|
129
|
+
return this._form(data || this.getDefaultData());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// HTTP handlers
|
|
133
|
+
get(handler: Handler) {
|
|
134
|
+
this._handlers.GET = handler;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
patch(handler: Handler) {
|
|
139
|
+
this._handlers.PATCH = handler;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
post(handler: Handler) {
|
|
144
|
+
this._handlers.POST = handler;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
put(handler: Handler) {
|
|
149
|
+
this._handlers.PUT = handler;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
delete(handler: Handler) {
|
|
154
|
+
this._handlers.DELETE = handler;
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
23
157
|
}
|
|
24
158
|
|
|
25
159
|
/**
|
|
26
160
|
* Run route from file
|
|
27
161
|
*/
|
|
28
|
-
export async function runFileRoute(
|
|
162
|
+
export async function runFileRoute(RouteModule: any, context: Context): Promise<Response | false> {
|
|
29
163
|
const req = context.req;
|
|
30
164
|
const url = new URL(req.url);
|
|
31
165
|
const qs = url.searchParams;
|
|
32
166
|
|
|
33
167
|
// @TODO: Move this to config or something...
|
|
168
|
+
const userIsBot = isbot(context.req.header('User-Agent'));
|
|
34
169
|
const streamOpt = qs.get('__nostream') ? !Boolean(qs.get('__nostream')) : undefined;
|
|
35
|
-
const streamingEnabled = streamOpt !== undefined ? streamOpt : true;
|
|
36
|
-
|
|
37
|
-
// Import route component
|
|
38
|
-
const RouteModule = _routeCache[routeFile] || (await import(routeFile));
|
|
39
|
-
|
|
40
|
-
if (IS_PROD) {
|
|
41
|
-
// Only cache routes in prod
|
|
42
|
-
_routeCache[routeFile] = RouteModule;
|
|
43
|
-
}
|
|
170
|
+
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
44
171
|
|
|
45
172
|
// Route module
|
|
46
173
|
const RouteComponent = RouteModule.default;
|
|
47
174
|
const reqMethod = req.method.toUpperCase();
|
|
48
175
|
|
|
49
|
-
// Middleware?
|
|
50
|
-
const routeMiddleware = RouteModule.middleware || {}; // Example: { auth: apiAuth, logger: logMiddleware, }
|
|
51
|
-
const middlewareResult: any = {};
|
|
52
|
-
|
|
53
176
|
try {
|
|
54
|
-
// Run middleware if present...
|
|
55
|
-
if (Object.keys(routeMiddleware).length) {
|
|
56
|
-
for (const mKey in routeMiddleware) {
|
|
57
|
-
const mRes = await routeMiddleware[mKey](context);
|
|
58
|
-
|
|
59
|
-
if (mRes instanceof Response) {
|
|
60
|
-
return context.resMerge(mRes);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
middlewareResult[mKey] = mRes;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
177
|
// API Route?
|
|
68
178
|
if (RouteModule[reqMethod] !== undefined) {
|
|
69
|
-
return await runAPIRoute(RouteModule[reqMethod], context
|
|
179
|
+
return await runAPIRoute(RouteModule[reqMethod], context);
|
|
70
180
|
}
|
|
71
181
|
|
|
72
|
-
|
|
73
|
-
const routeContent = await RouteComponent(context, middlewareResult);
|
|
182
|
+
let routeContent;
|
|
74
183
|
|
|
75
|
-
|
|
76
|
-
|
|
184
|
+
// No default export in this file...
|
|
185
|
+
if (!RouteComponent) {
|
|
186
|
+
throw new Error('No route was exported by default in matched route file.');
|
|
77
187
|
}
|
|
78
188
|
|
|
79
|
-
|
|
80
|
-
|
|
189
|
+
// Route component
|
|
190
|
+
if (typeof RouteComponent._handlers !== 'undefined') {
|
|
191
|
+
const routeMethodHandler = RouteComponent._handlers[reqMethod];
|
|
192
|
+
|
|
193
|
+
if (!routeMethodHandler) {
|
|
194
|
+
return new Response('Method Not Allowed', {
|
|
195
|
+
status: 405,
|
|
196
|
+
headers: {'content-type': 'text/plain'},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
routeContent = await routeMethodHandler(context);
|
|
81
201
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const output = await renderToString(routeContent);
|
|
202
|
+
routeContent = await RouteComponent(context);
|
|
203
|
+
}
|
|
85
204
|
|
|
86
|
-
|
|
87
|
-
return
|
|
205
|
+
if (routeContent instanceof Response) {
|
|
206
|
+
return routeContent;
|
|
88
207
|
}
|
|
208
|
+
|
|
209
|
+
// Render TmplHtml if returned from route handler
|
|
210
|
+
if (routeContent instanceof TmplHtml) {
|
|
211
|
+
if (streamingEnabled) {
|
|
212
|
+
return new StreamResponse(renderStream(routeContent)) as Response;
|
|
213
|
+
} else {
|
|
214
|
+
const output = await renderAsync(routeContent);
|
|
215
|
+
return context.html(output);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return routeContent;
|
|
89
220
|
} catch (e) {
|
|
90
221
|
console.error(e);
|
|
91
222
|
return await showErrorReponse(context, e as Error);
|
|
@@ -95,7 +226,7 @@ export async function runFileRoute(routeFile: string, context: HSRequestContext)
|
|
|
95
226
|
/**
|
|
96
227
|
* Run route and handle response
|
|
97
228
|
*/
|
|
98
|
-
async function runAPIRoute(routeFn: any, context:
|
|
229
|
+
async function runAPIRoute(routeFn: any, context: Context, middlewareResult?: any) {
|
|
99
230
|
try {
|
|
100
231
|
return await routeFn(context, middlewareResult);
|
|
101
232
|
} catch (err) {
|
|
@@ -104,13 +235,13 @@ async function runAPIRoute(routeFn: any, context: HSRequestContext, middlewareRe
|
|
|
104
235
|
|
|
105
236
|
return context.json(
|
|
106
237
|
{
|
|
107
|
-
meta: {
|
|
238
|
+
meta: {success: false},
|
|
108
239
|
data: {
|
|
109
240
|
message: e.message,
|
|
110
241
|
stack: IS_PROD ? undefined : e.stack?.split('\n'),
|
|
111
242
|
},
|
|
112
243
|
},
|
|
113
|
-
{
|
|
244
|
+
{status: 500}
|
|
114
245
|
);
|
|
115
246
|
}
|
|
116
247
|
}
|
|
@@ -119,8 +250,8 @@ async function runAPIRoute(routeFn: any, context: HSRequestContext, middlewareRe
|
|
|
119
250
|
* Basic error handling
|
|
120
251
|
* @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
|
|
121
252
|
*/
|
|
122
|
-
async function showErrorReponse(context:
|
|
123
|
-
const output =
|
|
253
|
+
async function showErrorReponse(context: Context, err: Error) {
|
|
254
|
+
const output = render(html`
|
|
124
255
|
<main>
|
|
125
256
|
<h1>Error</h1>
|
|
126
257
|
<pre>${err.message}</pre>
|
|
@@ -136,9 +267,10 @@ async function showErrorReponse(context: HSRequestContext, err: Error) {
|
|
|
136
267
|
export type THSServerConfig = {
|
|
137
268
|
appDir: string;
|
|
138
269
|
staticFileRoot: string;
|
|
270
|
+
rewrites?: Array<{source: string; destination: string}>;
|
|
139
271
|
// For customizing the routes and adding your own...
|
|
140
|
-
beforeRoutesAdded?: (app:
|
|
141
|
-
afterRoutesAdded?: (app:
|
|
272
|
+
beforeRoutesAdded?: (app: Hono) => void;
|
|
273
|
+
afterRoutesAdded?: (app: Hono) => void;
|
|
142
274
|
};
|
|
143
275
|
|
|
144
276
|
export type THSRouteMap = {
|
|
@@ -154,7 +286,8 @@ const ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
|
|
|
154
286
|
export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]> {
|
|
155
287
|
// Walk all pages and add them as routes
|
|
156
288
|
const routesDir = join(config.appDir, 'routes');
|
|
157
|
-
|
|
289
|
+
console.log(routesDir);
|
|
290
|
+
const files = await readdir(routesDir, {recursive: true});
|
|
158
291
|
const routes: THSRouteMap[] = [];
|
|
159
292
|
|
|
160
293
|
for (const file of files) {
|
|
@@ -176,7 +309,7 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
|
|
|
176
309
|
|
|
177
310
|
if (dynamicPaths) {
|
|
178
311
|
params = [];
|
|
179
|
-
route = route.replace(ROUTE_SEGMENT, (match: string
|
|
312
|
+
route = route.replace(ROUTE_SEGMENT, (match: string) => {
|
|
180
313
|
const paramName = match.replace(/[^a-zA-Z_\.]+/g, '');
|
|
181
314
|
|
|
182
315
|
if (match.includes('...')) {
|
|
@@ -202,15 +335,11 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
|
|
|
202
335
|
/**
|
|
203
336
|
* Create and start Bun HTTP server
|
|
204
337
|
*/
|
|
205
|
-
export async function createServer(config: THSServerConfig): Promise<
|
|
338
|
+
export async function createServer(config: THSServerConfig): Promise<Hono> {
|
|
206
339
|
// Build client JS and CSS bundles so they are available for templates when streaming starts
|
|
207
340
|
await Promise.all([buildClientJS(), buildClientCSS()]);
|
|
208
341
|
|
|
209
|
-
const app = new
|
|
210
|
-
|
|
211
|
-
app.defaultRoute(() => {
|
|
212
|
-
return new Response('Not... found?', { status: 404 });
|
|
213
|
-
});
|
|
342
|
+
const app = new Hono();
|
|
214
343
|
|
|
215
344
|
// [Customization] Before routes added...
|
|
216
345
|
config.beforeRoutesAdded && config.beforeRoutesAdded(app);
|
|
@@ -224,15 +353,28 @@ export async function createServer(config: THSServerConfig): Promise<HSApp> {
|
|
|
224
353
|
const fullRouteFile = join(CWD, route.file);
|
|
225
354
|
const routePattern = normalizePath(route.route);
|
|
226
355
|
|
|
227
|
-
routeMap.push({
|
|
356
|
+
routeMap.push({route: routePattern, file: route.file});
|
|
357
|
+
|
|
358
|
+
// Import route
|
|
359
|
+
const routeModule = await import(fullRouteFile);
|
|
228
360
|
|
|
229
361
|
app.all(routePattern, async (context) => {
|
|
230
|
-
const matchedRoute = await runFileRoute(
|
|
362
|
+
const matchedRoute = await runFileRoute(routeModule, context);
|
|
231
363
|
if (matchedRoute) {
|
|
232
364
|
return matchedRoute as Response;
|
|
233
365
|
}
|
|
234
366
|
|
|
235
|
-
return
|
|
367
|
+
return context.notFound();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Help route if no routes found
|
|
372
|
+
if (routeMap.length === 0) {
|
|
373
|
+
app.get('/', (context) => {
|
|
374
|
+
return context.text(
|
|
375
|
+
'No routes found. Add routes to app/routes. Example: `app/routes/index.ts`',
|
|
376
|
+
{status: 404}
|
|
377
|
+
);
|
|
236
378
|
});
|
|
237
379
|
}
|
|
238
380
|
|
|
@@ -246,80 +388,26 @@ export async function createServer(config: THSServerConfig): Promise<HSApp> {
|
|
|
246
388
|
config.afterRoutesAdded && config.afterRoutesAdded(app);
|
|
247
389
|
|
|
248
390
|
// Static files and catchall
|
|
249
|
-
app.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (IS_PROD) {
|
|
259
|
-
headers = {
|
|
260
|
-
'cache-control': 'public, max-age=31557600',
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return new Response(file, { headers });
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return app._defaultRoute(context);
|
|
391
|
+
app.use(
|
|
392
|
+
'*',
|
|
393
|
+
serveStatic({
|
|
394
|
+
root: config.staticFileRoot,
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
app.notFound((context) => {
|
|
399
|
+
return context.text('Not... found?', {status: 404});
|
|
268
400
|
});
|
|
269
401
|
|
|
270
402
|
return app;
|
|
271
403
|
}
|
|
272
404
|
|
|
273
|
-
/**
|
|
274
|
-
* Build client JS for end users (minimal JS for Hyperspan to work)
|
|
275
|
-
*/
|
|
276
|
-
export let clientJSFile: string;
|
|
277
|
-
export async function buildClientJS() {
|
|
278
|
-
const sourceFile = resolve(PWD, '../', './src/clientjs/hyperspan-client.ts');
|
|
279
|
-
const output = await Bun.build({
|
|
280
|
-
entrypoints: [sourceFile],
|
|
281
|
-
outdir: `./public/_hs/js`,
|
|
282
|
-
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
283
|
-
minify: IS_PROD,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
clientJSFile = output.outputs[0].path.split('/').reverse()[0];
|
|
287
|
-
return clientJSFile;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Find client CSS file built for end users
|
|
292
|
-
* @TODO: Build this in code here vs. relying on tailwindcss CLI tool from package scripts
|
|
293
|
-
*/
|
|
294
|
-
export let clientCSSFile: string;
|
|
295
|
-
export async function buildClientCSS() {
|
|
296
|
-
if (clientCSSFile) {
|
|
297
|
-
return clientCSSFile;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Find file already built from tailwindcss CLI
|
|
301
|
-
const cssDir = './public/_hs/css/';
|
|
302
|
-
const cssFiles = await readdir(cssDir);
|
|
303
|
-
|
|
304
|
-
for (const file of cssFiles) {
|
|
305
|
-
// Only looking for CSS files
|
|
306
|
-
if (clientCSSFile || !file.endsWith('.css')) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return (clientCSSFile = file.replace(cssDir, ''));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!clientCSSFile) {
|
|
314
|
-
throw new Error(`Unable to build CSS files from ${cssDir}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
405
|
/**
|
|
319
406
|
* Streaming HTML Response
|
|
320
407
|
*/
|
|
321
|
-
export class StreamResponse {
|
|
408
|
+
export class StreamResponse extends Response {
|
|
322
409
|
constructor(iterator: AsyncIterator<unknown>, options = {}) {
|
|
410
|
+
super();
|
|
323
411
|
const stream = createReadableStreamFromAsyncGenerator(iterator as AsyncGenerator);
|
|
324
412
|
|
|
325
413
|
return new Response(stream, {
|
|
@@ -343,7 +431,7 @@ export function createReadableStreamFromAsyncGenerator(output: AsyncGenerator) {
|
|
|
343
431
|
return new ReadableStream({
|
|
344
432
|
async start(controller) {
|
|
345
433
|
while (true) {
|
|
346
|
-
const {
|
|
434
|
+
const {done, value} = await output.next();
|
|
347
435
|
|
|
348
436
|
if (done) {
|
|
349
437
|
controller.close();
|
|
@@ -357,19 +445,12 @@ export function createReadableStreamFromAsyncGenerator(output: AsyncGenerator) {
|
|
|
357
445
|
}
|
|
358
446
|
|
|
359
447
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* 1. Renders component as initial form markup
|
|
364
|
-
* 2. Bind form onSubmit function to custom client JS handling
|
|
365
|
-
* 3. Submits form with JavaScript fetch()
|
|
366
|
-
* 4. Replaces form content with content from server
|
|
367
|
-
* 5. All validation and save logic is on the server
|
|
368
|
-
* 6. Handles any Exception thrown on server as error displayed in client
|
|
448
|
+
* Normalize URL path
|
|
449
|
+
* Removes trailing slash and lowercases path
|
|
369
450
|
*/
|
|
370
|
-
export
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
451
|
+
export function normalizePath(urlPath: string): string {
|
|
452
|
+
return (
|
|
453
|
+
(urlPath.endsWith('/') ? urlPath.substring(0, urlPath.length - 1) : urlPath).toLowerCase() ||
|
|
454
|
+
'/'
|
|
455
|
+
);
|
|
375
456
|
}
|
package/.prettierrc
DELETED
package/bun.lockb
DELETED
|
Binary file
|