@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.
@@ -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, resolve } from 'node:path';
3
- import { html, renderToStream, renderToString } from './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 { HSTemplate } from './html';
6
- import { HSApp, HSRequestContext, normalizePath } from './app';
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 CWD = import.meta.dir;
10
- const STATIC_FILE_MATCHER = /[^/\\&\?]+\.([a-zA-Z]+)$/;
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
- // Cached route components
13
- const _routeCache: { [key: string]: any } = {};
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
- * Did request come from a bot?
160
+ * Form route handler helper
17
161
  */
18
- function requestIsBot(req: Request) {
19
- const ua = req.headers.get('User-Agent');
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
- return ua ? isbot(ua) : false;
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(routeFile: string, context: HSRequestContext) {
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, middlewareResult);
249
+ return await runAPIRoute(RouteModule[reqMethod], context);
69
250
  }
70
251
 
71
- // Route component
72
- const routeContent = await RouteComponent(context, middlewareResult);
252
+ let routeContent;
73
253
 
74
- if (routeContent instanceof Response) {
75
- return context.resMerge(routeContent);
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
- if (streamingEnabled && !requestIsBot(req)) {
79
- return context.resMerge(new StreamResponse(renderToStream(routeContent)) as Response);
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
- // Render content and template
82
- // TODO: Use any context variables from RouteComponent rendering to set values in layout (dynamic title, etc.)...
83
- const output = await renderToString(routeContent);
272
+ routeContent = await RouteComponent(context);
273
+ }
84
274
 
85
- // Render it...
86
- return context.html(output);
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: HSRequestContext, middlewareResult?: any) {
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: HSRequestContext, err: Error) {
122
- const output = await renderToString(html`
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: HSApp) => void;
140
- afterRoutesAdded?: (app: HSApp) => void;
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<HSApp> {
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 HSApp();
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('../../', config.appDir, 'routes', route.file);
423
+ const fullRouteFile = join(CWD, route.file);
224
424
  const routePattern = normalizePath(route.route);
225
425
 
226
- routeMap.push({ route: routePattern, file: config.appDir + '/routes/' + route.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(fullRouteFile, context);
432
+ const matchedRoute = await runFileRoute(routeModule, context);
230
433
  if (matchedRoute) {
231
434
  return matchedRoute as Response;
232
435
  }
233
436
 
234
- return app._defaultRoute(context);
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.all('*', (context) => {
249
- const req = context.req;
250
-
251
- // Static files
252
- if (STATIC_FILE_MATCHER.test(req.url)) {
253
- const filePath = config.staticFileRoot + new URL(req.url).pathname;
254
- const file = Bun.file(filePath);
255
- let headers = {};
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
- * Form route
360
- * Automatically handles and parses form data
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 type TFormRouteFn = (context: HSRequestContext) => HSTemplate | Response;
370
- export function formRoute(handlerFn: TFormRouteFn) {
371
- return function _formRouteHandler(context: HSRequestContext) {
372
- // @TODO: Parse form data and pass it into form route handler
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
@@ -1,7 +0,0 @@
1
- {
2
- "printWidth": 100,
3
- "trailingComma": "es5",
4
- "tabWidth": 2,
5
- "semi": true,
6
- "singleQuote": true
7
- }
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 {};