hyper-element 1.0.3 → 2.0.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/index.d.ts CHANGED
@@ -107,18 +107,137 @@ export type OnNextCallback = (
107
107
  store: any | (() => any)
108
108
  ) => (...data: any[]) => void;
109
109
 
110
+ /**
111
+ * Render function for functional components.
112
+ * Receives Html template function, context, and optional store data.
113
+ */
114
+ export type RenderFunction = (
115
+ Html: HtmlFunction,
116
+ ctx: ElementContext,
117
+ ...data: any[]
118
+ ) => void;
119
+
120
+ /**
121
+ * Setup function for functional components.
122
+ * Receives context and onNext callback.
123
+ * @returns Optional teardown function.
124
+ */
125
+ export type SetupFunction = (
126
+ ctx: ElementContext,
127
+ onNext: OnNextCallback
128
+ ) => void | (() => void);
129
+
130
+ /**
131
+ * Method function for functional components.
132
+ * Context is passed as first argument.
133
+ */
134
+ export type MethodFunction = (ctx: ElementContext, ...args: any[]) => any;
135
+
136
+ /**
137
+ * SSR hydration lifecycle hook for functional components.
138
+ * Called before buffered events are replayed.
139
+ */
140
+ export type OnBeforeHydrateFunction = (
141
+ ctx: ElementContext,
142
+ events: BufferedEvent[]
143
+ ) => BufferedEvent[];
144
+
145
+ /**
146
+ * SSR hydration lifecycle hook for functional components.
147
+ * Called after event replay completes.
148
+ */
149
+ export type OnAfterHydrateFunction = (ctx: ElementContext) => void;
150
+
151
+ /**
152
+ * Functional component definition object.
153
+ * Note: observedAttributes is not needed - all attributes are automatically reactive via MutationObserver.
154
+ */
155
+ export interface FunctionalDefinition {
156
+ /** Setup lifecycle function */
157
+ setup?: SetupFunction;
158
+ /** Render function (required) */
159
+ render: RenderFunction;
160
+ /** SSR hydration hook - filter events before replay */
161
+ onBeforeHydrate?: OnBeforeHydrateFunction;
162
+ /** SSR hydration hook - called after replay completes */
163
+ onAfterHydrate?: OnAfterHydrateFunction;
164
+ /** Additional methods */
165
+ [key: string]:
166
+ | SetupFunction
167
+ | RenderFunction
168
+ | OnBeforeHydrateFunction
169
+ | OnAfterHydrateFunction
170
+ | MethodFunction
171
+ | undefined;
172
+ }
173
+
174
+ /**
175
+ * Interface for the hyperElement function/class.
176
+ * Can be used as a class base or called as a factory function.
177
+ */
178
+ export interface HyperElementFactory {
179
+ /**
180
+ * Create and register a custom element with a definition object.
181
+ * @param tagName - Custom element tag name (must contain a hyphen)
182
+ * @param definition - Component definition object
183
+ * @returns The generated class
184
+ */
185
+ (tagName: string, definition: FunctionalDefinition): typeof hyperElement;
186
+
187
+ /**
188
+ * Create and register a custom element with a render function.
189
+ * @param tagName - Custom element tag name (must contain a hyphen)
190
+ * @param render - Render function
191
+ * @returns The generated class
192
+ */
193
+ (tagName: string, render: RenderFunction): typeof hyperElement;
194
+
195
+ /**
196
+ * Create a custom element class without registering.
197
+ * @param definition - Component definition object
198
+ * @returns The generated class (call customElements.define to register)
199
+ */
200
+ (definition: FunctionalDefinition): typeof hyperElement;
201
+
202
+ /**
203
+ * Create a custom element class without registering.
204
+ * @param render - Render function
205
+ * @returns The generated class (call customElements.define to register)
206
+ */
207
+ (render: RenderFunction): typeof hyperElement;
208
+
209
+ /** Prototype for class inheritance */
210
+ prototype: hyperElement;
211
+
212
+ /** Configure SSR hydration settings */
213
+ configureSSR(options: SSRConfig): void;
214
+ }
215
+
110
216
  /**
111
217
  * Base class for creating custom elements with hyperHTML templating.
112
218
  * Extend this class and implement the render() method to create a custom element.
113
219
  *
220
+ * Can also be called as a factory function:
221
+ * - `hyperElement('tag-name', { render: ... })` - with auto-registration
222
+ * - `hyperElement({ render: ... })` - returns class for manual registration
223
+ *
114
224
  * @example
115
225
  * ```javascript
226
+ * // Class-based usage
116
227
  * class MyElement extends hyperElement {
117
228
  * render(Html) {
118
229
  * Html`<div>Hello ${this.attrs.name}!</div>`;
119
230
  * }
120
231
  * }
121
232
  * customElements.define('my-element', MyElement);
233
+ *
234
+ * // Functional usage with auto-registration
235
+ * hyperElement('my-element', {
236
+ * render: (Html, ctx) => Html`<div>Hello ${ctx.attrs.name}!</div>`
237
+ * });
238
+ *
239
+ * // Functional shorthand
240
+ * hyperElement('simple-elem', (Html, ctx) => Html`<div>Simple</div>`);
122
241
  * ```
123
242
  */
124
243
  export class hyperElement extends HTMLElement {
@@ -176,13 +295,343 @@ export class hyperElement extends HTMLElement {
176
295
  * ```
177
296
  */
178
297
  render(Html: HtmlFunction, ...data: any[]): void;
298
+
299
+ /**
300
+ * SSR hydration lifecycle hook. Called before buffered events are replayed.
301
+ * Override to filter or modify events before replay.
302
+ *
303
+ * @param events - Buffered events captured during SSR
304
+ * @returns Filtered events to replay
305
+ *
306
+ * @example
307
+ * ```javascript
308
+ * onBeforeHydrate(events) {
309
+ * return events.filter(e => e.type !== 'focus');
310
+ * }
311
+ * ```
312
+ */
313
+ onBeforeHydrate?(events: BufferedEvent[]): BufferedEvent[];
314
+
315
+ /**
316
+ * SSR hydration lifecycle hook. Called after event replay completes.
317
+ * Override to perform post-hydration setup.
318
+ *
319
+ * @example
320
+ * ```javascript
321
+ * onAfterHydrate() {
322
+ * console.log('Component hydrated');
323
+ * }
324
+ * ```
325
+ */
326
+ onAfterHydrate?(): void;
179
327
  }
180
328
 
329
+ /**
330
+ * The exported hyperElement is typed as an intersection:
331
+ * - HyperElementFactory: callable function signatures
332
+ * - typeof HyperElementBase: class for extension
333
+ */
334
+ type HyperElementType = HyperElementFactory &
335
+ (new () => hyperElement) & { prototype: hyperElement };
336
+
337
+ declare const hyperElement: HyperElementType;
338
+
181
339
  declare global {
182
340
  interface Window {
183
- hyperElement: typeof hyperElement;
341
+ hyperElement: HyperElementType;
184
342
  hyperHTML: any;
185
343
  }
186
344
  }
187
345
 
346
+ export { hyperElement };
188
347
  export default hyperElement;
348
+
349
+ // ============================================================================
350
+ // Signals API - Fine-grained reactivity primitives
351
+ // ============================================================================
352
+
353
+ /**
354
+ * A reactive signal that holds a value and notifies subscribers when it changes.
355
+ */
356
+ export interface Signal<T> {
357
+ /** Get or set the current value. Reading tracks dependencies. */
358
+ value: T;
359
+ /** Read the current value without tracking dependencies. */
360
+ peek(): T;
361
+ /** Subscribe to value changes. Returns an unsubscribe function. */
362
+ subscribe(fn: () => void): () => void;
363
+ }
364
+
365
+ /**
366
+ * A computed signal that derives its value from other signals.
367
+ */
368
+ export interface Computed<T> {
369
+ /** Get the computed value. Automatically tracks dependencies and recomputes when they change. */
370
+ readonly value: T;
371
+ /** Read the computed value without tracking dependencies. */
372
+ peek(): T;
373
+ }
374
+
375
+ /**
376
+ * Creates a reactive signal.
377
+ * @param initialValue - The initial value of the signal
378
+ * @returns A signal object with value getter/setter, peek(), and subscribe()
379
+ *
380
+ * @example
381
+ * ```javascript
382
+ * const count = signal(0);
383
+ * count.value; // 0
384
+ * count.value = 1; // Updates and notifies subscribers
385
+ * count.peek(); // Read without tracking
386
+ * ```
387
+ */
388
+ export function signal<T>(initialValue: T): Signal<T>;
389
+
390
+ /**
391
+ * Creates a computed signal that derives from other signals.
392
+ * The computation is lazy and cached until dependencies change.
393
+ * @param fn - Computation function that reads other signals
394
+ * @returns A computed signal object with value getter and peek()
395
+ *
396
+ * @example
397
+ * ```javascript
398
+ * const count = signal(0);
399
+ * const doubled = computed(() => count.value * 2);
400
+ * doubled.value; // 0
401
+ * count.value = 5;
402
+ * doubled.value; // 10
403
+ * ```
404
+ */
405
+ export function computed<T>(fn: () => T): Computed<T>;
406
+
407
+ /**
408
+ * Creates an effect that runs when its dependencies change.
409
+ * The effect runs immediately and re-runs whenever any signal it reads changes.
410
+ * @param fn - Effect function. Can return a cleanup function.
411
+ * @returns Cleanup function to stop the effect
412
+ *
413
+ * @example
414
+ * ```javascript
415
+ * const count = signal(0);
416
+ * const cleanup = effect(() => {
417
+ * console.log('Count:', count.value);
418
+ * return () => console.log('Cleanup');
419
+ * });
420
+ * // Logs: "Count: 0"
421
+ * count.value = 1;
422
+ * // Logs: "Cleanup", then "Count: 1"
423
+ * cleanup(); // Stop the effect
424
+ * ```
425
+ */
426
+ export function effect(fn: () => void | (() => void)): () => void;
427
+
428
+ /**
429
+ * Batches multiple signal updates into a single notification.
430
+ * Effects are deferred until the batch completes.
431
+ * @param fn - Function containing signal updates
432
+ *
433
+ * @example
434
+ * ```javascript
435
+ * const a = signal(0);
436
+ * const b = signal(0);
437
+ * batch(() => {
438
+ * a.value = 1;
439
+ * b.value = 2;
440
+ * }); // Effects run once after both updates
441
+ * ```
442
+ */
443
+ export function batch(fn: () => void): void;
444
+
445
+ /**
446
+ * Runs a function without tracking signal dependencies.
447
+ * Useful for reading signals without creating subscriptions.
448
+ * @param fn - Function to run untracked
449
+ * @returns The return value of fn
450
+ *
451
+ * @example
452
+ * ```javascript
453
+ * const count = signal(0);
454
+ * effect(() => {
455
+ * const val = untracked(() => count.value);
456
+ * // This effect won't re-run when count changes
457
+ * });
458
+ * ```
459
+ */
460
+ export function untracked<T>(fn: () => T): T;
461
+
462
+ // ============================================================================
463
+ // SSR Hydration API
464
+ // ============================================================================
465
+
466
+ /**
467
+ * Buffered event captured during SSR hydration.
468
+ */
469
+ export interface BufferedEvent {
470
+ /** Event type (e.g., 'click', 'input') */
471
+ type: string;
472
+ /** Path from custom element to target (e.g., "DIV:0/BUTTON:1") */
473
+ targetPath: string;
474
+ /** Timestamp when event was captured */
475
+ timestamp: number;
476
+ /** Event-specific details */
477
+ detail: Record<string, any>;
478
+ }
479
+
480
+ /**
481
+ * SSR configuration options.
482
+ */
483
+ export interface SSRConfig {
484
+ /** Event types to capture during SSR hydration (default: all interactive events) */
485
+ events?: string[];
486
+ /** Show visual indicator in development mode (default: false) */
487
+ devMode?: boolean;
488
+ }
489
+
490
+ /**
491
+ * Configure SSR hydration settings.
492
+ * Call this before any custom elements are defined to customize behavior.
493
+ *
494
+ * @param options - Configuration options
495
+ *
496
+ * @example
497
+ * ```javascript
498
+ * // Enable dev mode indicator
499
+ * hyperElement.configureSSR({ devMode: true });
500
+ *
501
+ * // Customize captured events
502
+ * hyperElement.configureSSR({
503
+ * events: ['click', 'input', 'submit'],
504
+ * devMode: true
505
+ * });
506
+ * ```
507
+ */
508
+ export function configureSSR(options: SSRConfig): void;
509
+
510
+ // ============================================================================
511
+ // SSR String Rendering API (Node.js/Server-side)
512
+ // ============================================================================
513
+
514
+ /**
515
+ * Options for renderElement function.
516
+ */
517
+ export interface RenderElementOptions<T = any> {
518
+ /** Attributes to pass to the component */
519
+ attrs?: Record<string, unknown>;
520
+ /** Store data for render context */
521
+ store?: T;
522
+ /** Wrap content in Declarative Shadow DOM template (default: false) */
523
+ shadowDOM?: boolean;
524
+ /** Fragment functions for the component */
525
+ fragments?: Record<string, (data: any) => any>;
526
+ /** Render function (Html, ctx) => void */
527
+ render: (Html: HtmlFunction, ctx: ElementContext) => void;
528
+ }
529
+
530
+ /**
531
+ * Renders a component definition to an HTML string.
532
+ * Use this in Node.js/Deno/Bun for server-side rendering.
533
+ *
534
+ * @param tagName - Custom element tag name (e.g., 'my-component')
535
+ * @param options - Render options
536
+ * @returns Promise resolving to HTML string
537
+ *
538
+ * @example
539
+ * ```javascript
540
+ * const { renderElement } = require('hyper-element/ssr/server');
541
+ *
542
+ * const html = await renderElement('my-greeting', {
543
+ * attrs: { name: 'World' },
544
+ * render: (Html, ctx) => Html`<div>Hello ${ctx.attrs.name}!</div>`
545
+ * });
546
+ * // Returns: '<my-greeting name="World"><div>Hello World!</div></my-greeting>'
547
+ *
548
+ * // With Declarative Shadow DOM
549
+ * const shadowHtml = await renderElement('my-greeting', {
550
+ * attrs: { name: 'World' },
551
+ * shadowDOM: true,
552
+ * render: (Html, ctx) => Html`<div>Hello ${ctx.attrs.name}!</div>`
553
+ * });
554
+ * // Returns: '<my-greeting name="World"><template shadowrootmode="open"><div>Hello World!</div></template></my-greeting>'
555
+ * ```
556
+ */
557
+ export function renderElement<T = any>(
558
+ tagName: string,
559
+ options: RenderElementOptions<T>
560
+ ): Promise<string>;
561
+
562
+ /**
563
+ * Renders multiple elements in parallel.
564
+ * Useful for batch SSR rendering.
565
+ *
566
+ * @param elements - Array of elements to render
567
+ * @returns Promise resolving to array of HTML strings
568
+ */
569
+ export function renderElements(
570
+ elements: Array<{ tagName: string; options: RenderElementOptions }>
571
+ ): Promise<string[]>;
572
+
573
+ /**
574
+ * Creates a reusable component renderer.
575
+ * Useful for rendering the same component multiple times with different data.
576
+ *
577
+ * @param tagName - Custom element tag name
578
+ * @param render - Render function
579
+ * @param baseOptions - Base options merged with each render
580
+ * @returns Renderer function
581
+ */
582
+ export function createRenderer<T = any>(
583
+ tagName: string,
584
+ render: (Html: HtmlFunction, ctx: ElementContext) => void,
585
+ baseOptions?: Partial<RenderElementOptions<T>>
586
+ ): (attrs?: Record<string, unknown>, store?: T) => Promise<string>;
587
+
588
+ /**
589
+ * Renders a tagged template literal to an HTML string.
590
+ * Lower-level API for direct template rendering.
591
+ *
592
+ * @param template - Template strings array
593
+ * @param values - Interpolated values
594
+ * @param xml - SVG/XML mode (default: false)
595
+ * @returns HTML string
596
+ */
597
+ export function renderToString(
598
+ template: TemplateStringsArray,
599
+ values: unknown[],
600
+ xml?: boolean
601
+ ): string;
602
+
603
+ /**
604
+ * Creates an SSR Html tagged template function.
605
+ * Use this for custom SSR rendering scenarios.
606
+ *
607
+ * @param context - Render context with attrs, store, fragments
608
+ * @returns Html function with wire, raw, lite methods
609
+ */
610
+ export function createSSRHtml(context?: {
611
+ attrs?: Record<string, unknown>;
612
+ store?: any;
613
+ fragments?: Record<string, (data: any) => any>;
614
+ }): HtmlFunction;
615
+
616
+ /**
617
+ * SSR HTML tagged template function.
618
+ * Renders directly to string without any component wrapper.
619
+ */
620
+ export const ssrHtml: (
621
+ strings: TemplateStringsArray,
622
+ ...values: unknown[]
623
+ ) => string;
624
+
625
+ /**
626
+ * Escapes HTML special characters to prevent XSS.
627
+ * @param str - String to escape
628
+ * @returns Escaped string
629
+ */
630
+ export function escapeHtml(str: string): string;
631
+
632
+ /**
633
+ * Marks a string as safe HTML that should not be escaped.
634
+ * @param html - HTML string to mark as safe
635
+ * @returns Safe HTML object
636
+ */
637
+ export function safeHtml(html: string): { value: string };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "1.0.3",
2
+ "version": "2.0.0",
3
3
  "name": "hyper-element",
4
4
  "author": "Brian Shannon (github.com/codemeasandwich)",
5
- "description": "hyperHTML + WebComponents",
5
+ "description": "Fast, lightweight web components",
6
6
  "main": "build/hyperElement.js",
7
7
  "types": "index.d.ts",
8
8
  "files": [
@@ -12,12 +12,15 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "build": "node scripts/build.js",
15
- "test": "npm run build && npm run test:coverage && npm run test:minified; rm -rf build",
16
- "test:coverage": "playwright test",
17
- "test:minified": "TEST_BUNDLE=min playwright test",
18
- "test:ui": "npm run build && playwright test --ui; rm -rf build",
19
- "test:headed": "npm run build && playwright test --headed; rm -rf build",
20
- "kitchensink": "npx serve .",
15
+ "test": "npm run test:src && npm run test:bundle",
16
+ "test:src": "rm -rf coverage && playwright test --project=source-coverage && npm run test:ssr-coverage && npm run test:coverage-report",
17
+ "test:bundle": "npm run build && playwright test --project=bundle-verify; rm -rf build",
18
+ "test:ssr": "node test/ssr.test.mjs",
19
+ "test:ssr-coverage": "node test/ssr-with-coverage.mjs",
20
+ "test:coverage-report": "node test/coverage-report.mjs",
21
+ "test:ui": "playwright test --project=source-coverage --ui",
22
+ "test:headed": "playwright test --project=source-coverage --headed",
23
+ "kitchensink": "npm run build && cp -r build kitchensink/ && (sleep 1 && open http://localhost:3000) & npx serve ./kitchensink",
21
24
  "release": "bash scripts/publish.sh",
22
25
  "hooks:install": "cp .hooks/pre-commit .git/hooks/ && cp .hooks/commit-msg .git/hooks/ && cp -r .hooks/pre-commit.d .git/hooks/ && chmod +x .git/hooks/pre-commit .git/hooks/commit-msg .git/hooks/pre-commit.d/*.sh",
23
26
  "prepare": "npm run hooks:install",
@@ -34,9 +37,10 @@
34
37
  "url": "https://github.com/codemeasandwich/hyper-element/issues"
35
38
  },
36
39
  "keywords": [
37
- "hyperhtml",
38
40
  "web-components",
39
- "webcomponents"
41
+ "webcomponents",
42
+ "signals",
43
+ "reactive"
40
44
  ],
41
45
  "devDependencies": {
42
46
  "@commitlint/cli": "^18.6.1",
@@ -49,15 +53,16 @@
49
53
  "eslint-config-prettier": "^9.1.2",
50
54
  "eslint-plugin-prettier": "^5.5.5",
51
55
  "husky": "^9.1.7",
52
- "hyperhtml": "^2.34.2",
56
+ "istanbul-lib-coverage": "^3.2.2",
57
+ "istanbul-lib-report": "^3.0.1",
58
+ "istanbul-reports": "^3.2.0",
53
59
  "lint-staged": "^15.5.2",
54
- "lodash": "^4.17.21",
55
60
  "minimist": "^1.2.8",
56
61
  "mkdirp": "^1.0.4",
57
62
  "prettier": "^3.8.0",
58
63
  "serve": "^14.2.5",
59
64
  "set-value": "^2.0.1",
60
- "typescript": "^5.9.3",
65
+ "source-map": "^0.7.6",
61
66
  "union-value": "^2.0.1",
62
67
  "v8-to-istanbul": "^9.3.0"
63
68
  }