@hyperspan/framework 0.4.5 → 0.5.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/dist/assets.js +2 -2
- package/dist/server.js +4 -4
- package/package.json +1 -1
- package/src/actions.test.ts +16 -6
- package/src/actions.ts +14 -22
- package/src/assets.ts +6 -2
- package/src/plugins.ts +2 -2
- package/src/server.ts +28 -13
package/dist/assets.js
CHANGED
|
@@ -22,12 +22,12 @@ async function buildClientJS() {
|
|
|
22
22
|
const jsFile = output.outputs[0].path.split("/").reverse()[0];
|
|
23
23
|
clientJSFiles.set("_hs", { src: `${CLIENTJS_PUBLIC_PATH}/${jsFile}` });
|
|
24
24
|
}
|
|
25
|
-
function renderClientJS(module,
|
|
25
|
+
function renderClientJS(module, loadScript) {
|
|
26
26
|
if (!module.__CLIENT_JS) {
|
|
27
27
|
throw new Error(`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`);
|
|
28
28
|
}
|
|
29
29
|
return html.raw(module.__CLIENT_JS.renderScriptTag({
|
|
30
|
-
|
|
30
|
+
loadScript: loadScript ? typeof loadScript === "string" ? loadScript : functionToString(loadScript) : undefined
|
|
31
31
|
}));
|
|
32
32
|
}
|
|
33
33
|
function functionToString(fn) {
|
package/dist/server.js
CHANGED
|
@@ -54,8 +54,8 @@ export const __CLIENT_JS = {
|
|
|
54
54
|
esmName: "${esmName}",
|
|
55
55
|
sourceFile: "${args.path}",
|
|
56
56
|
outputFile: "${result.outputs[0].path}",
|
|
57
|
-
renderScriptTag: ({
|
|
58
|
-
const fn =
|
|
57
|
+
renderScriptTag: ({ loadScript }) => {
|
|
58
|
+
const fn = loadScript ? (typeof loadScript === 'string' ? loadScript : \`const fn = \${functionToString(loadScript)}; fn(${fnArgs});\`) : '';
|
|
59
59
|
return \`<script type="module" data-source-id="${jsId}">import ${exports} from "${esmName}";
|
|
60
60
|
\${fn}</script>\`;
|
|
61
61
|
},
|
|
@@ -2117,7 +2117,7 @@ async function buildRoutes(config) {
|
|
|
2117
2117
|
const files = await readdir(routesDir, { recursive: true });
|
|
2118
2118
|
const routes = [];
|
|
2119
2119
|
for (const file of files) {
|
|
2120
|
-
if (!file.includes(".") || basename(file).startsWith(".")) {
|
|
2120
|
+
if (!file.includes(".") || basename(file).startsWith(".") || file.includes(".test.") || file.includes(".spec.")) {
|
|
2121
2121
|
continue;
|
|
2122
2122
|
}
|
|
2123
2123
|
let route = "/" + file.replace(extname(file), "");
|
|
@@ -2155,7 +2155,7 @@ async function buildActions(config) {
|
|
|
2155
2155
|
const files = await readdir(routesDir, { recursive: true });
|
|
2156
2156
|
const routes = [];
|
|
2157
2157
|
for (const file of files) {
|
|
2158
|
-
if (!file.includes(".") || basename(file).startsWith(".")) {
|
|
2158
|
+
if (!file.includes(".") || basename(file).startsWith(".") || file.includes(".test.") || file.includes(".spec.")) {
|
|
2159
2159
|
continue;
|
|
2160
2160
|
}
|
|
2161
2161
|
let route = assetHash("/" + file.replace(extname(file), ""));
|
package/package.json
CHANGED
package/src/actions.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
2
|
import { unstable__createAction } from './actions';
|
|
3
3
|
import { describe, it, expect } from 'bun:test';
|
|
4
4
|
import { html, render, type HSHtml } from '@hyperspan/html';
|
|
5
|
-
import type {
|
|
5
|
+
import type { THSContext } from './server';
|
|
6
6
|
|
|
7
7
|
describe('createAction', () => {
|
|
8
|
-
const formWithNameOnly = ({ data }: { data?: { name: string } }) => {
|
|
8
|
+
const formWithNameOnly = (c: THSContext, { data }: { data?: { name: string } }) => {
|
|
9
9
|
return html`
|
|
10
10
|
<form>
|
|
11
11
|
<p>
|
|
@@ -23,8 +23,18 @@ describe('createAction', () => {
|
|
|
23
23
|
name: z.string(),
|
|
24
24
|
});
|
|
25
25
|
const action = unstable__createAction(schema, formWithNameOnly);
|
|
26
|
+
const mockContext = {
|
|
27
|
+
req: {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
formData: async () => {
|
|
30
|
+
const formData = new FormData();
|
|
31
|
+
formData.append('name', 'John');
|
|
32
|
+
return formData;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} as THSContext;
|
|
26
36
|
|
|
27
|
-
const formResponse = render(action.render({ data: { name: 'John' } }) as HSHtml);
|
|
37
|
+
const formResponse = render(action.render(mockContext, { data: { name: 'John' } }) as HSHtml);
|
|
28
38
|
expect(formResponse).toContain('value="John"');
|
|
29
39
|
});
|
|
30
40
|
});
|
|
@@ -52,7 +62,7 @@ describe('createAction', () => {
|
|
|
52
62
|
return formData;
|
|
53
63
|
},
|
|
54
64
|
},
|
|
55
|
-
} as
|
|
65
|
+
} as THSContext;
|
|
56
66
|
|
|
57
67
|
const response = await action.run(mockContext);
|
|
58
68
|
|
|
@@ -85,7 +95,7 @@ describe('createAction', () => {
|
|
|
85
95
|
return formData;
|
|
86
96
|
},
|
|
87
97
|
},
|
|
88
|
-
} as
|
|
98
|
+
} as THSContext;
|
|
89
99
|
|
|
90
100
|
const response = await action.run(mockContext);
|
|
91
101
|
|
package/src/actions.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { html, HSHtml } from '@hyperspan/html';
|
|
|
2
2
|
import * as z from 'zod/v4';
|
|
3
3
|
import { HTTPException } from 'hono/http-exception';
|
|
4
4
|
import { assetHash } from './assets';
|
|
5
|
-
import { IS_PROD, returnHTMLResponse, type THSResponseTypes } from './server';
|
|
6
|
-
import type {
|
|
5
|
+
import { IS_PROD, returnHTMLResponse, type THSContext, type THSResponseTypes } from './server';
|
|
6
|
+
import type { MiddlewareHandler } from 'hono';
|
|
7
7
|
import type { HandlerResponse, Next, TypedResponse } from 'hono/types';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -29,39 +29,34 @@ export interface HSAction<T extends z.ZodTypeAny> {
|
|
|
29
29
|
_form: Parameters<HSAction<T>['form']>[0];
|
|
30
30
|
form(
|
|
31
31
|
renderForm: (
|
|
32
|
-
c:
|
|
32
|
+
c: THSContext,
|
|
33
33
|
{ data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }
|
|
34
34
|
) => HSHtml | void | null | Promise<HSHtml | void | null>
|
|
35
35
|
): HSAction<T>;
|
|
36
36
|
post(
|
|
37
37
|
handler: (
|
|
38
|
-
c:
|
|
38
|
+
c: THSContext,
|
|
39
39
|
{ data }: { data?: z.infer<T> }
|
|
40
40
|
) => TActionResponse | Promise<TActionResponse>
|
|
41
41
|
): HSAction<T>;
|
|
42
42
|
error(
|
|
43
43
|
handler: (
|
|
44
|
-
c:
|
|
44
|
+
c: THSContext,
|
|
45
45
|
{ data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }
|
|
46
46
|
) => TActionResponse
|
|
47
47
|
): HSAction<T>;
|
|
48
|
-
render(
|
|
49
|
-
|
|
50
|
-
props?: { data?: z.infer<T>; error?: z.ZodError | Error }
|
|
51
|
-
): TActionResponse;
|
|
52
|
-
run(c: Context<any, any, {}>): TActionResponse | Promise<TActionResponse>;
|
|
48
|
+
render(c: THSContext, props?: { data?: z.infer<T>; error?: z.ZodError | Error }): TActionResponse;
|
|
49
|
+
run(c: THSContext): TActionResponse | Promise<TActionResponse>;
|
|
53
50
|
middleware: (
|
|
54
51
|
middleware: Array<
|
|
55
52
|
| MiddlewareHandler
|
|
56
|
-
| ((
|
|
57
|
-
context: Context<any, string, {}>
|
|
58
|
-
) => TActionResponse | Promise<TActionResponse> | void | Promise<void>)
|
|
53
|
+
| ((context: THSContext) => TActionResponse | Promise<TActionResponse> | void | Promise<void>)
|
|
59
54
|
>
|
|
60
55
|
) => HSAction<T>;
|
|
61
56
|
_getRouteHandlers: () => Array<
|
|
62
57
|
| MiddlewareHandler
|
|
63
|
-
| ((context:
|
|
64
|
-
| ((context:
|
|
58
|
+
| ((context: THSContext, next: Next) => TActionResponse | Promise<TActionResponse>)
|
|
59
|
+
| ((context: THSContext) => TActionResponse | Promise<TActionResponse>)
|
|
65
60
|
>;
|
|
66
61
|
}
|
|
67
62
|
|
|
@@ -74,8 +69,8 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
|
|
|
74
69
|
_errorHandler: Parameters<HSAction<T>['error']>[0] | null = null,
|
|
75
70
|
_middleware: Array<
|
|
76
71
|
| MiddlewareHandler
|
|
77
|
-
| ((context:
|
|
78
|
-
| ((context:
|
|
72
|
+
| ((context: THSContext, next: Next) => TActionResponse | Promise<TActionResponse>)
|
|
73
|
+
| ((context: THSContext) => TActionResponse | Promise<TActionResponse>)
|
|
79
74
|
> = [];
|
|
80
75
|
|
|
81
76
|
const api: HSAction<T> = {
|
|
@@ -113,10 +108,7 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
|
|
|
113
108
|
/**
|
|
114
109
|
* Get form renderer method
|
|
115
110
|
*/
|
|
116
|
-
render(
|
|
117
|
-
c: Context<any, any, {}>,
|
|
118
|
-
formState?: { data?: z.infer<T>; error?: z.ZodError | Error }
|
|
119
|
-
) {
|
|
111
|
+
render(c: THSContext, formState?: { data?: z.infer<T>; error?: z.ZodError | Error }) {
|
|
120
112
|
const form = _form ? _form(c, formState || {}) : null;
|
|
121
113
|
return form ? html`<hs-action url="${this._route}">${form}</hs-action>` : null;
|
|
122
114
|
},
|
|
@@ -124,7 +116,7 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
|
|
|
124
116
|
_getRouteHandlers() {
|
|
125
117
|
return [
|
|
126
118
|
..._middleware,
|
|
127
|
-
async (c:
|
|
119
|
+
async (c: THSContext) => {
|
|
128
120
|
const response = await returnHTMLResponse(c, () => api.run(c));
|
|
129
121
|
|
|
130
122
|
// Replace redirects with special header because fetch() automatically follows redirects
|
package/src/assets.ts
CHANGED
|
@@ -36,7 +36,7 @@ export async function buildClientJS() {
|
|
|
36
36
|
/**
|
|
37
37
|
* Render a client JS module as a script tag
|
|
38
38
|
*/
|
|
39
|
-
export function renderClientJS<T>(module: T,
|
|
39
|
+
export function renderClientJS<T>(module: T, loadScript?: ((module: T) => void) | string) {
|
|
40
40
|
// @ts-ignore
|
|
41
41
|
if (!module.__CLIENT_JS) {
|
|
42
42
|
throw new Error(
|
|
@@ -47,7 +47,11 @@ export function renderClientJS<T>(module: T, onLoad?: (module: T) => void | stri
|
|
|
47
47
|
return html.raw(
|
|
48
48
|
// @ts-ignore
|
|
49
49
|
module.__CLIENT_JS.renderScriptTag({
|
|
50
|
-
|
|
50
|
+
loadScript: loadScript
|
|
51
|
+
? typeof loadScript === 'string'
|
|
52
|
+
? loadScript
|
|
53
|
+
: functionToString(loadScript)
|
|
54
|
+
: undefined,
|
|
51
55
|
})
|
|
52
56
|
);
|
|
53
57
|
}
|
package/src/plugins.ts
CHANGED
|
@@ -71,8 +71,8 @@ export const __CLIENT_JS = {
|
|
|
71
71
|
esmName: "${esmName}",
|
|
72
72
|
sourceFile: "${args.path}",
|
|
73
73
|
outputFile: "${result.outputs[0].path}",
|
|
74
|
-
renderScriptTag: ({
|
|
75
|
-
const fn =
|
|
74
|
+
renderScriptTag: ({ loadScript }) => {
|
|
75
|
+
const fn = loadScript ? (typeof loadScript === 'string' ? loadScript : \`const fn = \${functionToString(loadScript)}; fn(${fnArgs});\`) : '';
|
|
76
76
|
return \`<script type="module" data-source-id="${jsId}">import ${exports} from "${esmName}";\n\${fn}</script>\`;
|
|
77
77
|
},
|
|
78
78
|
}
|
package/src/server.ts
CHANGED
|
@@ -18,16 +18,19 @@ const CWD = process.cwd();
|
|
|
18
18
|
/**
|
|
19
19
|
* Types
|
|
20
20
|
*/
|
|
21
|
+
export type THSContext = Context<any, any, {}>;
|
|
21
22
|
export type THSResponseTypes = HSHtml | Response | string | null;
|
|
22
|
-
export type THSRouteHandler = (context:
|
|
23
|
-
export type THSAPIRouteHandler = (context:
|
|
23
|
+
export type THSRouteHandler = (context: THSContext) => THSResponseTypes | Promise<THSResponseTypes>;
|
|
24
|
+
export type THSAPIRouteHandler = (context: THSContext) => Promise<any> | any;
|
|
24
25
|
|
|
25
26
|
export type THSRoute = {
|
|
26
27
|
_kind: 'hsRoute';
|
|
27
28
|
get: (handler: THSRouteHandler) => THSRoute;
|
|
28
29
|
post: (handler: THSRouteHandler) => THSRoute;
|
|
29
30
|
middleware: (middleware: Array<MiddlewareHandler>) => THSRoute;
|
|
30
|
-
_getRouteHandlers: () => Array<
|
|
31
|
+
_getRouteHandlers: () => Array<
|
|
32
|
+
MiddlewareHandler | ((context: THSContext) => HandlerResponse<any>)
|
|
33
|
+
>;
|
|
31
34
|
};
|
|
32
35
|
export type THSAPIRoute = {
|
|
33
36
|
_kind: 'hsAPIRoute';
|
|
@@ -37,7 +40,9 @@ export type THSAPIRoute = {
|
|
|
37
40
|
delete: (handler: THSAPIRouteHandler) => THSAPIRoute;
|
|
38
41
|
patch: (handler: THSAPIRouteHandler) => THSAPIRoute;
|
|
39
42
|
middleware: (middleware: Array<MiddlewareHandler>) => THSAPIRoute;
|
|
40
|
-
_getRouteHandlers: () => Array<
|
|
43
|
+
_getRouteHandlers: () => Array<
|
|
44
|
+
MiddlewareHandler | ((context: THSContext) => HandlerResponse<any>)
|
|
45
|
+
>;
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
export function createConfig(config: THSServerConfig): THSServerConfig {
|
|
@@ -82,7 +87,7 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
82
87
|
_getRouteHandlers() {
|
|
83
88
|
return [
|
|
84
89
|
..._middleware,
|
|
85
|
-
async (context:
|
|
90
|
+
async (context: THSContext) => {
|
|
86
91
|
const method = context.req.method.toUpperCase();
|
|
87
92
|
|
|
88
93
|
// Handle CORS preflight requests
|
|
@@ -166,7 +171,7 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
|
|
|
166
171
|
_getRouteHandlers() {
|
|
167
172
|
return [
|
|
168
173
|
..._middleware,
|
|
169
|
-
async (context:
|
|
174
|
+
async (context: THSContext) => {
|
|
170
175
|
const method = context.req.method.toUpperCase();
|
|
171
176
|
|
|
172
177
|
// Handle CORS preflight requests
|
|
@@ -246,7 +251,7 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
|
|
|
246
251
|
* Return HTML response from userland route handler
|
|
247
252
|
*/
|
|
248
253
|
export async function returnHTMLResponse(
|
|
249
|
-
context:
|
|
254
|
+
context: THSContext,
|
|
250
255
|
handlerFn: () => unknown,
|
|
251
256
|
responseOptions?: { status?: ContentfulStatusCode; headers?: Headers | Record<string, string> }
|
|
252
257
|
): Promise<Response> {
|
|
@@ -333,7 +338,7 @@ export function isRunnableRoute(route: unknown): boolean {
|
|
|
333
338
|
* @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
|
|
334
339
|
*/
|
|
335
340
|
async function showErrorReponse(
|
|
336
|
-
context:
|
|
341
|
+
context: THSContext,
|
|
337
342
|
err: Error,
|
|
338
343
|
responseOptions?: { status?: ContentfulStatusCode; headers?: Headers | Record<string, string> }
|
|
339
344
|
) {
|
|
@@ -408,8 +413,13 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
|
|
|
408
413
|
const routes: THSRouteMap[] = [];
|
|
409
414
|
|
|
410
415
|
for (const file of files) {
|
|
411
|
-
// No directories
|
|
412
|
-
if (
|
|
416
|
+
// No directories or test files
|
|
417
|
+
if (
|
|
418
|
+
!file.includes('.') ||
|
|
419
|
+
basename(file).startsWith('.') ||
|
|
420
|
+
file.includes('.test.') ||
|
|
421
|
+
file.includes('.spec.')
|
|
422
|
+
) {
|
|
413
423
|
continue;
|
|
414
424
|
}
|
|
415
425
|
|
|
@@ -466,8 +476,13 @@ export async function buildActions(config: THSServerConfig): Promise<THSRouteMap
|
|
|
466
476
|
const routes: THSRouteMap[] = [];
|
|
467
477
|
|
|
468
478
|
for (const file of files) {
|
|
469
|
-
// No directories
|
|
470
|
-
if (
|
|
479
|
+
// No directories or test files
|
|
480
|
+
if (
|
|
481
|
+
!file.includes('.') ||
|
|
482
|
+
basename(file).startsWith('.') ||
|
|
483
|
+
file.includes('.test.') ||
|
|
484
|
+
file.includes('.spec.')
|
|
485
|
+
) {
|
|
471
486
|
continue;
|
|
472
487
|
}
|
|
473
488
|
|
|
@@ -496,7 +511,7 @@ export async function buildActions(config: THSServerConfig): Promise<THSRouteMap
|
|
|
496
511
|
*/
|
|
497
512
|
export function createRouteFromModule(
|
|
498
513
|
RouteModule: any
|
|
499
|
-
): Array<MiddlewareHandler | ((context:
|
|
514
|
+
): Array<MiddlewareHandler | ((context: THSContext) => HandlerResponse<any>)> {
|
|
500
515
|
const route = getRunnableRoute(RouteModule);
|
|
501
516
|
return route._getRouteHandlers();
|
|
502
517
|
}
|