@hyperspan/framework 0.5.4 → 1.0.0-alpha.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/package.json +35 -27
- package/src/callsites.ts +17 -0
- package/src/client/css.ts +2 -0
- package/src/client/js.ts +82 -0
- package/src/layout.ts +31 -0
- package/src/middleware/zod.ts +46 -0
- package/src/middleware.ts +53 -14
- package/src/plugins.ts +64 -58
- package/src/server.test.ts +88 -0
- package/src/server.ts +285 -428
- package/src/types.ts +110 -0
- package/src/utils.ts +5 -0
- package/build.ts +0 -16
- package/dist/assets.js +0 -120
- package/dist/chunk-atw8cdg1.js +0 -19
- package/dist/middleware.js +0 -179
- package/dist/server.js +0 -2266
- package/src/actions.test.ts +0 -106
- package/src/actions.ts +0 -256
- package/src/assets.ts +0 -176
package/src/actions.test.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod/v4';
|
|
2
|
-
import { unstable__createAction } from './actions';
|
|
3
|
-
import { describe, it, expect } from 'bun:test';
|
|
4
|
-
import { html, render, type HSHtml } from '@hyperspan/html';
|
|
5
|
-
import type { THSContext } from './server';
|
|
6
|
-
|
|
7
|
-
describe('createAction', () => {
|
|
8
|
-
const formWithNameOnly = (c: THSContext, { data }: { data?: { name: string } }) => {
|
|
9
|
-
return html`
|
|
10
|
-
<form>
|
|
11
|
-
<p>
|
|
12
|
-
Name:
|
|
13
|
-
<input type="text" name="name" value="${data?.name || ''}" />
|
|
14
|
-
</p>
|
|
15
|
-
<button type="submit">Submit</button>
|
|
16
|
-
</form>
|
|
17
|
-
`;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
describe('with form content', () => {
|
|
21
|
-
it('should create an action with a form that renders provided data', async () => {
|
|
22
|
-
const schema = z.object({
|
|
23
|
-
name: z.string(),
|
|
24
|
-
});
|
|
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;
|
|
36
|
-
|
|
37
|
-
const formResponse = render(action.render(mockContext, { data: { name: 'John' } }) as HSHtml);
|
|
38
|
-
expect(formResponse).toContain('value="John"');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('when data is valid', () => {
|
|
43
|
-
it('should run the handler and return the result', async () => {
|
|
44
|
-
const schema = z.object({
|
|
45
|
-
name: z.string().nonempty(),
|
|
46
|
-
});
|
|
47
|
-
const action = unstable__createAction(schema, formWithNameOnly)
|
|
48
|
-
.post((c, { data }) => {
|
|
49
|
-
return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
|
|
50
|
-
})
|
|
51
|
-
.error((c, { error }) => {
|
|
52
|
-
return html`<div>There was an error! ${error?.message}</div>`;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Mock context to run action
|
|
56
|
-
const mockContext = {
|
|
57
|
-
req: {
|
|
58
|
-
method: 'POST',
|
|
59
|
-
formData: async () => {
|
|
60
|
-
const formData = new FormData();
|
|
61
|
-
formData.append('name', 'John');
|
|
62
|
-
return formData;
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
} as THSContext;
|
|
66
|
-
|
|
67
|
-
const response = await action.run(mockContext);
|
|
68
|
-
|
|
69
|
-
const formResponse = render(response as HSHtml);
|
|
70
|
-
expect(formResponse).toContain('Thanks for submitting the form, John!');
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe.skip('when data is invalid', () => {
|
|
75
|
-
it('should return the content of the form with error', async () => {
|
|
76
|
-
const schema = z.object({
|
|
77
|
-
name: z.string().nonempty(),
|
|
78
|
-
});
|
|
79
|
-
const action = unstable__createAction(schema)
|
|
80
|
-
.form(formWithNameOnly)
|
|
81
|
-
.post((c, { data }) => {
|
|
82
|
-
return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
|
|
83
|
-
})
|
|
84
|
-
.error((c, { error }) => {
|
|
85
|
-
return html`<div>There was an error! ${error?.message}</div>`;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Mock context to run action
|
|
89
|
-
const mockContext = {
|
|
90
|
-
req: {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
formData: async () => {
|
|
93
|
-
const formData = new FormData();
|
|
94
|
-
formData.append('name', ''); // No name = error
|
|
95
|
-
return formData;
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
} as THSContext;
|
|
99
|
-
|
|
100
|
-
const response = await action.run(mockContext);
|
|
101
|
-
|
|
102
|
-
const formResponse = render(response as HSHtml);
|
|
103
|
-
expect(formResponse).toContain('There was an error!');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
package/src/actions.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { html, HSHtml } from '@hyperspan/html';
|
|
2
|
-
import * as z from 'zod/v4';
|
|
3
|
-
import { HTTPException } from 'hono/http-exception';
|
|
4
|
-
import { assetHash } from './assets';
|
|
5
|
-
import { IS_PROD, returnHTMLResponse, type THSContext, type THSResponseTypes } from './server';
|
|
6
|
-
import type { MiddlewareHandler } from 'hono';
|
|
7
|
-
import type { HandlerResponse, Next, TypedResponse } from 'hono/types';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Actions = Form + route handler
|
|
11
|
-
* Automatically handles and parses form data
|
|
12
|
-
*
|
|
13
|
-
* HOW THIS WORKS:
|
|
14
|
-
* ---
|
|
15
|
-
* 1. Renders in any template as initial form markup with action.render()
|
|
16
|
-
* 2. Binds form onSubmit function to custom client JS handling via <hs-action> web component
|
|
17
|
-
* 3. Submits form with JavaScript fetch() + FormData as normal POST form submission
|
|
18
|
-
* 4. All validation and save logic is run on the server
|
|
19
|
-
* 5. Replaces form content in place with HTML response content from server via the Idiomorph library
|
|
20
|
-
* 6. Handles any Exception thrown on server as error displayed back to user on the page
|
|
21
|
-
*/
|
|
22
|
-
export type TActionResponse =
|
|
23
|
-
| THSResponseTypes
|
|
24
|
-
| HandlerResponse<any>
|
|
25
|
-
| TypedResponse<any, any, any>;
|
|
26
|
-
export interface HSAction<T extends z.ZodTypeAny> {
|
|
27
|
-
_kind: string;
|
|
28
|
-
_route: string;
|
|
29
|
-
_form: Parameters<HSAction<T>['form']>[0];
|
|
30
|
-
form(
|
|
31
|
-
renderForm: (
|
|
32
|
-
c: THSContext,
|
|
33
|
-
{ data, error }: { data?: Partial<z.infer<T>>; error?: z.ZodError | Error }
|
|
34
|
-
) => HSHtml | void | null | Promise<HSHtml | void | null>
|
|
35
|
-
): HSAction<T>;
|
|
36
|
-
post(
|
|
37
|
-
handler: (
|
|
38
|
-
c: THSContext,
|
|
39
|
-
{ data }: { data?: Partial<z.infer<T>> }
|
|
40
|
-
) => TActionResponse | Promise<TActionResponse>
|
|
41
|
-
): HSAction<T>;
|
|
42
|
-
error(
|
|
43
|
-
handler: (
|
|
44
|
-
c: THSContext,
|
|
45
|
-
{ data, error }: { data?: Partial<z.infer<T>>; error?: z.ZodError | Error }
|
|
46
|
-
) => TActionResponse
|
|
47
|
-
): HSAction<T>;
|
|
48
|
-
render(
|
|
49
|
-
c: THSContext,
|
|
50
|
-
props?: { data?: Partial<z.infer<T>>; error?: z.ZodError | Error }
|
|
51
|
-
): TActionResponse;
|
|
52
|
-
run(c: THSContext): TActionResponse | Promise<TActionResponse>;
|
|
53
|
-
middleware: (
|
|
54
|
-
middleware: Array<
|
|
55
|
-
| MiddlewareHandler
|
|
56
|
-
| ((context: THSContext) => TActionResponse | Promise<TActionResponse> | void | Promise<void>)
|
|
57
|
-
>
|
|
58
|
-
) => HSAction<T>;
|
|
59
|
-
_getRouteHandlers: () => Array<
|
|
60
|
-
| MiddlewareHandler
|
|
61
|
-
| ((context: THSContext, next: Next) => TActionResponse | Promise<TActionResponse>)
|
|
62
|
-
| ((context: THSContext) => TActionResponse | Promise<TActionResponse>)
|
|
63
|
-
>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function unstable__createAction<T extends z.ZodTypeAny>(
|
|
67
|
-
schema: T | null = null,
|
|
68
|
-
form: Parameters<HSAction<T>['form']>[0]
|
|
69
|
-
) {
|
|
70
|
-
let _handler: Parameters<HSAction<T>['post']>[0] | null = null,
|
|
71
|
-
_form: Parameters<HSAction<T>['form']>[0] = form,
|
|
72
|
-
_errorHandler: Parameters<HSAction<T>['error']>[0] | null = null,
|
|
73
|
-
_middleware: Array<
|
|
74
|
-
| MiddlewareHandler
|
|
75
|
-
| ((context: THSContext, next: Next) => TActionResponse | Promise<TActionResponse>)
|
|
76
|
-
| ((context: THSContext) => TActionResponse | Promise<TActionResponse>)
|
|
77
|
-
> = [];
|
|
78
|
-
|
|
79
|
-
const api: HSAction<T> = {
|
|
80
|
-
_kind: 'hsAction',
|
|
81
|
-
_route: `/__actions/${assetHash(_form.toString())}`,
|
|
82
|
-
_form,
|
|
83
|
-
form(renderForm) {
|
|
84
|
-
_form = renderForm;
|
|
85
|
-
return api;
|
|
86
|
-
},
|
|
87
|
-
/**
|
|
88
|
-
* Process form data
|
|
89
|
-
*
|
|
90
|
-
* Returns result from form processing if successful
|
|
91
|
-
* Re-renders form with data and error information otherwise
|
|
92
|
-
*/
|
|
93
|
-
post(handler) {
|
|
94
|
-
_handler = handler;
|
|
95
|
-
return api;
|
|
96
|
-
},
|
|
97
|
-
/**
|
|
98
|
-
* Cusotm error handler if you want to display something other than the default
|
|
99
|
-
*/
|
|
100
|
-
error(handler) {
|
|
101
|
-
_errorHandler = handler;
|
|
102
|
-
return api;
|
|
103
|
-
},
|
|
104
|
-
/**
|
|
105
|
-
* Add middleware specific to this route
|
|
106
|
-
*/
|
|
107
|
-
middleware(middleware) {
|
|
108
|
-
_middleware = middleware;
|
|
109
|
-
return api;
|
|
110
|
-
},
|
|
111
|
-
/**
|
|
112
|
-
* Get form renderer method
|
|
113
|
-
*/
|
|
114
|
-
render(c: THSContext, formState?: { data?: Partial<z.infer<T>>; error?: z.ZodError | Error }) {
|
|
115
|
-
const form = _form ? _form(c, formState || {}) : null;
|
|
116
|
-
return form ? html`<hs-action url="${this._route}">${form}</hs-action>` : null;
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
_getRouteHandlers() {
|
|
120
|
-
return [
|
|
121
|
-
..._middleware,
|
|
122
|
-
async (c: THSContext) => {
|
|
123
|
-
const response = await returnHTMLResponse(c, () => api.run(c));
|
|
124
|
-
|
|
125
|
-
// Replace redirects with special header because fetch() automatically follows redirects
|
|
126
|
-
// and we want to redirect the user to the actual full page instead
|
|
127
|
-
if ([301, 302, 307, 308].includes(response.status)) {
|
|
128
|
-
response.headers.set('X-Redirect-Location', response.headers.get('Location') || '/');
|
|
129
|
-
response.headers.delete('Location');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return response;
|
|
133
|
-
},
|
|
134
|
-
];
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Run action
|
|
139
|
-
*
|
|
140
|
-
* Returns result from form processing if successful
|
|
141
|
-
* Re-renders form with data and error information otherwise
|
|
142
|
-
*/
|
|
143
|
-
async run(c) {
|
|
144
|
-
const method = c.req.method;
|
|
145
|
-
|
|
146
|
-
if (method === 'GET') {
|
|
147
|
-
return await api.render(c);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (method !== 'POST') {
|
|
151
|
-
throw new HTTPException(405, { message: 'Actions only support GET and POST requests' });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const formData = await c.req.formData();
|
|
155
|
-
const jsonData = unstable__formDataToJSON(formData) as Partial<z.infer<T>>;
|
|
156
|
-
const schemaData = schema ? schema.safeParse(jsonData) : null;
|
|
157
|
-
const data = schemaData?.success ? (schemaData.data as Partial<z.infer<T>>) : jsonData;
|
|
158
|
-
let error: z.ZodError | Error | null = null;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
if (schema && schemaData?.error) {
|
|
162
|
-
throw schemaData.error;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!_handler) {
|
|
166
|
-
throw new Error('Action POST handler not set! Every action must have a POST handler.');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return await _handler(c, { data });
|
|
170
|
-
} catch (e) {
|
|
171
|
-
error = e as Error | z.ZodError;
|
|
172
|
-
!IS_PROD && console.error(error);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (error && _errorHandler) {
|
|
176
|
-
// @ts-ignore
|
|
177
|
-
return await returnHTMLResponse(c, () => _errorHandler(c, { data, error }), {
|
|
178
|
-
status: 400,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return await returnHTMLResponse(c, () => api.render(c, { data, error }), { status: 400 });
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
return api;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Return JSON data structure for a given FormData object
|
|
191
|
-
* Accounts for array fields (e.g. name="options[]" or <select multiple>)
|
|
192
|
-
*
|
|
193
|
-
* @link https://stackoverflow.com/a/75406413
|
|
194
|
-
*/
|
|
195
|
-
export function unstable__formDataToJSON(formData: FormData): Record<string, string | string[]> {
|
|
196
|
-
let object = {};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Parses FormData key xxx`[x][x][x]` fields into array
|
|
200
|
-
*/
|
|
201
|
-
const parseKey = (key: string) => {
|
|
202
|
-
const subKeyIdx = key.indexOf('[');
|
|
203
|
-
|
|
204
|
-
if (subKeyIdx !== -1) {
|
|
205
|
-
const keys = [key.substring(0, subKeyIdx)];
|
|
206
|
-
key = key.substring(subKeyIdx);
|
|
207
|
-
|
|
208
|
-
for (const match of key.matchAll(/\[(?<key>.*?)]/gm)) {
|
|
209
|
-
if (match.groups) {
|
|
210
|
-
keys.push(match.groups.key);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return keys;
|
|
214
|
-
} else {
|
|
215
|
-
return [key];
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Recursively iterates over keys and assigns key/values to object
|
|
221
|
-
*/
|
|
222
|
-
const assign = (keys: string[], value: FormDataEntryValue, object: any): void => {
|
|
223
|
-
const key = keys.shift();
|
|
224
|
-
|
|
225
|
-
// When last key in the iterations
|
|
226
|
-
if (key === '' || key === undefined) {
|
|
227
|
-
return object.push(value);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (Reflect.has(object, key)) {
|
|
231
|
-
// If key has been found, but final pass - convert the value to array
|
|
232
|
-
if (keys.length === 0) {
|
|
233
|
-
if (!Array.isArray(object[key])) {
|
|
234
|
-
object[key] = [object[key], value];
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// Recurse again with found object
|
|
239
|
-
return assign(keys, value, object[key]);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Create empty object for key, if next key is '' do array instead, otherwise set value
|
|
243
|
-
if (keys.length >= 1) {
|
|
244
|
-
object[key] = keys[0] === '' ? [] : {};
|
|
245
|
-
return assign(keys, value, object[key]);
|
|
246
|
-
} else {
|
|
247
|
-
object[key] = value;
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
for (const pair of formData.entries()) {
|
|
252
|
-
assign(parseKey(pair[0]), pair[1], object);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return object;
|
|
256
|
-
}
|
package/src/assets.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { html } from '@hyperspan/html';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readdir } from 'node:fs/promises';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export type THSIslandOptions = {
|
|
7
|
-
ssr?: boolean;
|
|
8
|
-
loading?: 'lazy' | undefined;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
12
|
-
const PWD = import.meta.dir;
|
|
13
|
-
|
|
14
|
-
export const CLIENTJS_PUBLIC_PATH = '/_hs/js';
|
|
15
|
-
export const ISLAND_PUBLIC_PATH = '/_hs/js/islands';
|
|
16
|
-
export const clientImportMap = new Map<string, string>();
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Build client JS for end users (minimal JS for Hyperspan to work)
|
|
20
|
-
*/
|
|
21
|
-
export const clientJSFiles = new Map<string, { src: string; type?: string }>();
|
|
22
|
-
export async function buildClientJS() {
|
|
23
|
-
const sourceFile = resolve(PWD, '../', './src/clientjs/hyperspan-client.ts');
|
|
24
|
-
const output = await Bun.build({
|
|
25
|
-
entrypoints: [sourceFile],
|
|
26
|
-
outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
|
|
27
|
-
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
28
|
-
minify: IS_PROD,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const jsFile = output.outputs[0].path.split('/').reverse()[0];
|
|
32
|
-
|
|
33
|
-
clientJSFiles.set('_hs', { src: `${CLIENTJS_PUBLIC_PATH}/${jsFile}` });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Render a client JS module as a script tag
|
|
38
|
-
*/
|
|
39
|
-
export function renderClientJS<T>(module: T, loadScript?: ((module: T) => void) | string) {
|
|
40
|
-
// @ts-ignore
|
|
41
|
-
if (!module.__CLIENT_JS) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return html.raw(
|
|
48
|
-
// @ts-ignore
|
|
49
|
-
module.__CLIENT_JS.renderScriptTag({
|
|
50
|
-
loadScript: loadScript
|
|
51
|
-
? typeof loadScript === 'string'
|
|
52
|
-
? loadScript
|
|
53
|
-
: functionToString(loadScript)
|
|
54
|
-
: undefined,
|
|
55
|
-
})
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Convert a function to a string (results in loss of context!)
|
|
61
|
-
* Handles named, async, and arrow functions
|
|
62
|
-
*/
|
|
63
|
-
export function functionToString(fn: any) {
|
|
64
|
-
let str = fn.toString().trim();
|
|
65
|
-
|
|
66
|
-
// Ensure consistent output & handle async
|
|
67
|
-
if (!str.includes('function ')) {
|
|
68
|
-
if (str.includes('async ')) {
|
|
69
|
-
str = 'async function ' + str.replace('async ', '');
|
|
70
|
-
} else {
|
|
71
|
-
str = 'function ' + str;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const lines = str.split('\n');
|
|
76
|
-
const firstLine = lines[0];
|
|
77
|
-
const lastLine = lines[lines.length - 1];
|
|
78
|
-
|
|
79
|
-
// Arrow function conversion
|
|
80
|
-
if (!lastLine?.includes('}')) {
|
|
81
|
-
return str.replace('=> ', '{ return ') + '; }';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Cleanup arrow function
|
|
85
|
-
if (firstLine.includes('=>')) {
|
|
86
|
-
return str.replace('=> ', '');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return str;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Find client CSS file built for end users
|
|
94
|
-
* @TODO: Build this in code here vs. relying on tailwindcss CLI tool from package scripts
|
|
95
|
-
*/
|
|
96
|
-
export const clientCSSFiles = new Map<string, string>();
|
|
97
|
-
export async function buildClientCSS() {
|
|
98
|
-
if (clientCSSFiles.has('_hs')) {
|
|
99
|
-
return clientCSSFiles.get('_hs');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Find file already built from tailwindcss CLI
|
|
103
|
-
const cssDir = './public/_hs/css/';
|
|
104
|
-
const cssFiles = await readdir(cssDir);
|
|
105
|
-
let foundCSSFile: string = '';
|
|
106
|
-
|
|
107
|
-
for (const file of cssFiles) {
|
|
108
|
-
// Only looking for CSS files
|
|
109
|
-
if (!file.endsWith('.css')) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
foundCSSFile = file.replace(cssDir, '');
|
|
114
|
-
clientCSSFiles.set('_hs', foundCSSFile);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!foundCSSFile) {
|
|
119
|
-
console.log(`Unable to build CSS files from ${cssDir}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Output HTML style tag for Hyperspan app
|
|
125
|
-
*/
|
|
126
|
-
export function hyperspanStyleTags() {
|
|
127
|
-
const cssFiles = Array.from(clientCSSFiles.entries());
|
|
128
|
-
return html`${cssFiles.map(
|
|
129
|
-
([_, file]) => html`<link rel="stylesheet" href="/_hs/css/${file}" />`
|
|
130
|
-
)}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Output HTML script tag for Hyperspan app
|
|
135
|
-
* Required for functioning streaming so content can pop into place properly once ready
|
|
136
|
-
*/
|
|
137
|
-
export function hyperspanScriptTags() {
|
|
138
|
-
const jsFiles = Array.from(clientJSFiles.entries());
|
|
139
|
-
|
|
140
|
-
return html`
|
|
141
|
-
<script type="importmap">
|
|
142
|
-
{"imports": ${Object.fromEntries(clientImportMap)}}
|
|
143
|
-
</script>
|
|
144
|
-
${jsFiles.map(
|
|
145
|
-
([key, file]) =>
|
|
146
|
-
html`<script
|
|
147
|
-
id="js-${key}"
|
|
148
|
-
type="${file.type || 'text/javascript'}"
|
|
149
|
-
src="${file.src}"
|
|
150
|
-
></script>`
|
|
151
|
-
)}
|
|
152
|
-
`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function assetHash(content: string): string {
|
|
156
|
-
return createHash('md5').update(content).digest('hex');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Island defaults
|
|
161
|
-
*/
|
|
162
|
-
export const ISLAND_DEFAULTS: () => THSIslandOptions = () => ({
|
|
163
|
-
ssr: true,
|
|
164
|
-
loading: undefined,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
export function renderIsland(Component: any, props: any, options = ISLAND_DEFAULTS()) {
|
|
168
|
-
// Render island with its own logic
|
|
169
|
-
if (Component.__HS_ISLAND?.render) {
|
|
170
|
-
return html.raw(Component.__HS_ISLAND.render(props, options));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
throw new Error(
|
|
174
|
-
`Module ${Component.name} was not loaded with an island plugin! Did you forget to install an island plugin and add it to the createServer() 'islandPlugins' config?`
|
|
175
|
-
);
|
|
176
|
-
}
|