microtags 0.0.14 → 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.
Files changed (85) hide show
  1. package/README.md +554 -20
  2. package/dist/build.cjs +412 -0
  3. package/dist/build.cjs.map +7 -0
  4. package/dist/build.d.ts +17 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +395 -0
  7. package/dist/build.js.map +7 -0
  8. package/dist/builders.d.ts +70 -4
  9. package/dist/builders.d.ts.map +1 -1
  10. package/dist/{chunk-CCYWRJW6.js → chunk-46UEUDVX.js} +4 -26
  11. package/dist/chunk-46UEUDVX.js.map +7 -0
  12. package/dist/chunk-LS4L5QRD.js +26 -0
  13. package/dist/chunk-LS4L5QRD.js.map +7 -0
  14. package/dist/chunk-RKWPTT6P.js +69 -0
  15. package/dist/chunk-RKWPTT6P.js.map +7 -0
  16. package/dist/chunk-ZZ4ZLOQY.js +33 -0
  17. package/dist/chunk-ZZ4ZLOQY.js.map +7 -0
  18. package/dist/cli.cjs +536 -0
  19. package/dist/cli.cjs.map +7 -0
  20. package/dist/context.js +2 -1
  21. package/dist/esbuild.cjs +451 -0
  22. package/dist/esbuild.cjs.map +7 -0
  23. package/dist/esbuild.d.ts +7 -0
  24. package/dist/esbuild.d.ts.map +1 -0
  25. package/dist/esbuild.js +434 -0
  26. package/dist/esbuild.js.map +7 -0
  27. package/dist/factory.d.ts +7 -1
  28. package/dist/factory.d.ts.map +1 -1
  29. package/dist/htm.browser.cjs +591 -0
  30. package/dist/htm.browser.cjs.map +7 -0
  31. package/dist/htm.browser.d.ts +30 -0
  32. package/dist/htm.browser.d.ts.map +1 -0
  33. package/dist/htm.browser.js +498 -0
  34. package/dist/htm.browser.js.map +7 -0
  35. package/dist/htm.browser.min.js +3 -0
  36. package/dist/htm.browser.min.js.map +7 -0
  37. package/dist/htm.cjs +284 -0
  38. package/dist/htm.cjs.map +7 -0
  39. package/dist/htm.d.ts +14 -0
  40. package/dist/htm.d.ts.map +1 -0
  41. package/dist/htm.js +196 -0
  42. package/dist/htm.js.map +7 -0
  43. package/dist/htm.min.js +3 -0
  44. package/dist/htm.min.js.map +7 -0
  45. package/dist/index.cjs +125 -11
  46. package/dist/index.cjs.map +2 -2
  47. package/dist/index.d.ts +3 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +133 -17
  50. package/dist/index.js.map +2 -2
  51. package/dist/index.min.js +1 -1
  52. package/dist/index.min.js.map +3 -3
  53. package/dist/meta.json +411 -26
  54. package/dist/preact-shared.d.ts +61 -0
  55. package/dist/preact-shared.d.ts.map +1 -0
  56. package/dist/preact.browser.d.ts +8 -0
  57. package/dist/preact.browser.d.ts.map +1 -0
  58. package/dist/preact.browser.js +45 -0
  59. package/dist/preact.browser.js.map +7 -0
  60. package/dist/preact.browser.min.js +2 -0
  61. package/dist/preact.browser.min.js.map +7 -0
  62. package/dist/preact.cjs +128 -0
  63. package/dist/preact.cjs.map +7 -0
  64. package/dist/preact.d.ts +9 -0
  65. package/dist/preact.d.ts.map +1 -0
  66. package/dist/preact.js +35 -0
  67. package/dist/preact.js.map +7 -0
  68. package/dist/render-support.cjs +86 -0
  69. package/dist/render-support.cjs.map +7 -0
  70. package/dist/render-support.d.ts +23 -0
  71. package/dist/render-support.d.ts.map +1 -0
  72. package/dist/render-support.js +12 -0
  73. package/dist/render-support.js.map +7 -0
  74. package/dist/render-support.min.js +2 -0
  75. package/dist/render-support.min.js.map +7 -0
  76. package/dist/types.d.ts +8 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/vite.cjs +448 -0
  79. package/dist/vite.cjs.map +7 -0
  80. package/dist/vite.d.ts +7 -0
  81. package/dist/vite.d.ts.map +1 -0
  82. package/dist/vite.js +429 -0
  83. package/dist/vite.js.map +7 -0
  84. package/package.json +92 -13
  85. package/dist/chunk-CCYWRJW6.js.map +0 -7
package/README.md CHANGED
@@ -11,12 +11,16 @@
11
11
 
12
12
  Web component helper with reactive props, automatic cleanup, and
13
13
  [no Shadow DOM](https://gomakethings.com/the-shadow-dom-is-an-antipattern/).
14
- Reactive via [alien-signals](https://github.com/stackblitz/alien-signals).
14
+ Reactive via [`alien-signals`](https://github.com/stackblitz/alien-signals),
15
+ inspired by [`nanotags`](https://github.com/psd-coder/nanotags).
15
16
 
16
- **Size: <!-- size -->3.48 KB<!-- /size -->** with all dependencies, minified and brotlied
17
+ This package adds some provisions for serverside rendering.
17
18
 
18
- Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
19
- **alien-signals** instead of nanostores atoms.
19
+ | Entry | Size (min + brotli) |
20
+ |-------|---------------------|
21
+ | `microtags` | <!-- size -->3.99 KB<!-- /size --> |
22
+ | `microtags/htm` | <!-- size:htm -->1.99 KB<!-- /size:htm --> |
23
+ | `microtags/preact` | <!-- size:preact -->0.58 KB<!-- /size:preact --> |
20
24
 
21
25
  <details><summary><h2>Contents</h2></summary>
22
26
 
@@ -37,6 +41,7 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
37
41
  - [Serverside Example](#serverside-example)
38
42
  * [`.TAG`](#tag)
39
43
  * [`.refs`](#refs)
44
+ - [Hydration](#hydration)
40
45
  - [API](#api)
41
46
  * [`define`](#define)
42
47
  + [`ComponentBuilder`](#componentbuilder)
@@ -46,8 +51,12 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
46
51
  * [`builder.withRefs(fn)`](#builderwithrefsfn)
47
52
  * [`builder.withContexts`](#builderwithcontexts)
48
53
  + [`.withContexts` Example](#withcontexts-example)
54
+ * [`builder.render(fn, options?)`](#builderrenderfn-options)
49
55
  * [`builder.setup(ctx)`](#buildersetupctx)
50
56
  + [`ctx`](#ctx)
57
+ - [`ctx` Methods](#ctx-methods)
58
+ - [`ctx.bind`](#ctxbind)
59
+ * [`bindProp`](#bindprop)
51
60
  - [Missing refs](#missing-refs)
52
61
  * [Checking refs in tests](#checking-refs-in-tests)
53
62
  - [Typed Events](#typed-events)
@@ -56,11 +65,22 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
56
65
  * [`microtags/context`](#microtagscontext)
57
66
  * [`microtags/render`](#microtagsrender)
58
67
  + [`renderList`](#renderlist)
68
+ - [`renderList` Example](#renderlist-example)
59
69
  + [`render`](#render)
70
+ - [`render` Example](#render-example)
60
71
  * [`microtags/util`](#microtagsutil)
61
72
  + [Coercion](#coercion)
62
73
  + [`runAll`](#runall)
63
74
  + [`toAttributes`](#toattributes)
75
+ * [`microtags/htm`](#microtagshtm)
76
+ + [What escaping does not cover](#what-escaping-does-not-cover)
77
+ * [`microtags/render-support`](#microtagsrender-support)
78
+ * [`microtags/preact`](#microtagspreact)
79
+ * [`microtags/build`](#microtagsbuild)
80
+ * [`microtags/esbuild`](#microtagsesbuild)
81
+ * [`microtags/vite`](#microtagsvite)
82
+ * [`microbuild`](#microbuild)
83
+ * [`client:true` vs `--keep`](#clienttrue-vs---keep)
64
84
  - [Differences from `nanotags`](#differences-from-nanotags)
65
85
  * [Reactivity primitive](#reactivity-primitive)
66
86
  * [Effects + Subscriptions](#effects--subscriptions)
@@ -151,7 +171,8 @@ myCounter
151
171
 
152
172
  ### 3. Call `.setup`
153
173
 
154
- Method `.setup` takes a function that gets called with `context` object.
174
+ Method `.setup` is the terminal step in the method chain. It takes a function
175
+ that gets called with a `context` object. This is where "hydration" happens.
155
176
 
156
177
  ```ts
157
178
  myCounter
@@ -190,10 +211,10 @@ myCounter
190
211
  ## Full Example
191
212
 
192
213
  This example expects the full HTML to already be in the DOM.
193
- Defining the components is about "hydrating" the page, or adding behavior.
214
+ Defining the components "hydrates" the page (adds behavior).
194
215
 
195
216
  If you want to client-side render the components, that is possible too.
196
- You can update the DOM inside `.setup`.
217
+ You would update the DOM inside `.setup`.
197
218
 
198
219
  >
199
220
  > [!NOTE]
@@ -371,7 +392,7 @@ define('subscribe-form')
371
392
  const value = signal('')
372
393
  const touched = signal<boolean>(false)
373
394
 
374
- // capture the live value (two-way; clears the field on reset)
395
+ // capture the live value (clears the field on reset)
375
396
  ctx.bind(value, ctx.refs.input)
376
397
 
377
398
  // only reveal errors after the first blur
@@ -416,6 +437,9 @@ and the submit button is disabled until valid. See `example/subscribe-form.ts`.
416
437
  See [./example/ssr.ts](./example/ssr.ts) for an example of rendering an HTML
417
438
  string and then "hydrating" the page on the client.
418
439
 
440
+ > [!NOTE]
441
+ > The server must know how to render the HTML that the components depend on.
442
+
419
443
  ### `.TAG`
420
444
 
421
445
  Each tag name is exposed as a property `.TAG`. You can import the web
@@ -461,6 +485,12 @@ export function render ():string {
461
485
  }
462
486
  ```
463
487
 
488
+ > [!IMPORTANT]
489
+ > Custom elements must always have a closing tag, even when empty, like
490
+ > `<copy-btn></copy-btn>` above. HTML has no self-closing custom
491
+ > elements. The browser parses `<copy-btn />` as an open tag and treats
492
+ > everything after it as its children.
493
+
464
494
  ### `.refs`
465
495
 
466
496
  Web components created with `microtags` have a property `.refs`. It is an
@@ -482,6 +512,24 @@ export function render () {
482
512
 
483
513
  ---------------------------------------------------------------------
484
514
 
515
+ ## Hydration
516
+
517
+ In the [preceding serverside example](#serverside-example), the
518
+ server must know how to render the HTML that the client components depend on.
519
+ That means we have an unspecified dependency between HTML and JS.
520
+ Hydration is one way to render the correct HTML.
521
+
522
+ You can write a `.render` method, and then use a build tool
523
+ to strip the `.render` method from the JS sent to the client. That way
524
+ the clientside bundle stays small, but on the server, every component can
525
+ render itself, and the components shipped to the client deal with behavior only.
526
+
527
+ See [`example-hydration/`](./example-hydration/) for a working example that
528
+ uses Vite, and more docs.
529
+
530
+
531
+ ---------------------------------------------------------------
532
+
485
533
 
486
534
  ## API
487
535
 
@@ -504,6 +552,7 @@ The object returned by [`define`](#define). Includes methods:
504
552
  | `.withProps(fn)` | `ComponentBuilder` | Declare reactive attributes. See [Props](#props). |
505
553
  | `.withRefs(fn)` | `ComponentBuilder` | Declare DOM refs resolved once the element connects. See [Refs](#refs-1). |
506
554
  | `.withContexts(fn)` | `ComponentBuilder` | Consume context values published by an ancestor. See [`withContexts`](#withcontexts). |
555
+ | `.render(fn, opts?)` | `ComponentBuilder` | Attach a server-render function. See [`.render`](#builderrenderfn-options) below. Not to be confused with the DOM helper [`microtags/render`](#microtagsrender). |
507
556
  | `.setup(fn)` | `MicrotagElementClass` | Registers the custom element and returns its class. See [`setup`](#setup) below. |
508
557
 
509
558
  Methods `withProps`, `withRefs`, and `withContexts` each return a new builder,
@@ -517,15 +566,18 @@ Declare reactive attributes via `withProps`. Each prop becomes:
517
566
 
518
567
  * An observed HTML attribute (auto-synced via `attributeChangedCallback`)
519
568
  * A signal at `ctx.props.propName`
520
- * A typed getter/setter on the element instance
521
- * Four built-in validators coerce raw attribute strings to typed values:
569
+ * Four built-in coercers turn raw attribute strings into typed values:
570
+
571
+ | Coercer | Attribute present | Attribute absent |
572
+ |---------|-------------------|------------------|
573
+ | `p.string()` | the raw string | `""` |
574
+ | `p.number()` | `Number(val)`, so `NaN` if not numeric | `NaN` |
575
+ | `p.boolean()` | `true`, whatever the value, including `""` and `"false"` | `false` |
576
+ | `p.json<T>()` | `JSON.parse(val)`, or `undefined` if it does not parse | `undefined` |
522
577
 
523
- | Validator | Coercion | `null` attr |
524
- |-----------|----------|-------------|
525
- | `p.string()` | `String(val)` | `""` |
526
- | `p.number()` | `Number(val)` | `0` |
527
- | `p.boolean()` | `"true"` / `""` &rarr; `true`, `"false"` &rarr; `false` | `false` |
528
- | `p.oneOf(opts)` | Picklist enum, throws on invalid | throws |
578
+ `p.boolean()` follows HTML boolean-attribute semantics: presence means
579
+ `true`, like `disabled` or `checked`. To turn a boolean prop off, remove the
580
+ attribute rather than setting it to `"false"`.
529
581
 
530
582
  ```ts
531
583
  .withProps(p => ({
@@ -715,6 +767,105 @@ define('themed-card')
715
767
  ---------------------------------------------------------
716
768
 
717
769
 
770
+ ### `builder.render(fn, options?)`
771
+
772
+ Attach a render function that produces the component's inner HTML as a
773
+ string. The render function receives a context with `props` (plain
774
+ resolved values, not signals), `refs`, `TAG`, `children` (inner HTML
775
+ from the caller), and `attrs` (extra attributes from the caller).
776
+
777
+ ```ts
778
+ define('my-counter')
779
+ .withProps(p => ({
780
+ start: p.number(),
781
+ }))
782
+ .withRefs(r => ({
783
+ display: r.one<HTMLDivElement>(),
784
+ inc: r.one(),
785
+ dec: r.one(),
786
+ }))
787
+ .render(({ props, refs }) => {
788
+ return `
789
+ <div data-ref="${refs.display}">
790
+ ${props.start}
791
+ </div>
792
+ <button data-ref="${refs.dec}">-</button>
793
+ <button data-ref="${refs.inc}">+</button>
794
+ `
795
+ })
796
+ .setup(ctx => {
797
+ // hydration code only
798
+ })
799
+ ```
800
+
801
+ The returned element class gains three statics: `render(props,
802
+ options?)` which returns complete host markup, `renderParts(props,
803
+ options?)` which returns the same render as parts, and `props`, the list
804
+ of declared prop names. `options` accepts `children` (a string of inner
805
+ HTML) and `attrs` (an object of extra attributes, all strings).
806
+
807
+ `renderParts` returns `{ tag, attrs, html }`: the host tag name, the
808
+ host attributes as serialized (not yet escaped) strings, and the inner
809
+ HTML. `attrs` lists the declared props first, then the extra `attrs`,
810
+ skipping prop-only props, null or false values, and extra names that are
811
+ not valid attribute names. A true boolean is the empty string. Use it to
812
+ build the host with another renderer, such as a UI framework, while
813
+ `render` stays the string form:
814
+
815
+ ```ts
816
+ import 'microtags/render-support'
817
+ import type { RenderParts } from 'microtags'
818
+
819
+ const { tag, attrs, html }:RenderParts = MyCounter.renderParts({
820
+ start: 5,
821
+ })
822
+ // tag: 'my-counter', attrs: { start: '5' }, html: '<div ...'
823
+ ```
824
+
825
+ `props` are the names declared in `.withProps`. They are serialized onto
826
+ the host tag with the prop's serializer, so `.setup` can coerce them back
827
+ on the client. `attrs` are any other attributes the caller put on the
828
+ tag, such as `class`, `id`, or `aria-*`. They are written to the host tag
829
+ as-is, and are available to the render function so it can forward them
830
+ inside. See [Hydration](#hydration) for an example.
831
+
832
+ The static `render` (and `renderParts`) needs the server render
833
+ support module, which builds the host tag and serializes props. The `microtags/htm`
834
+ templater loads it, on the server and in the browser. Code that calls
835
+ a component's `render` static directly, without the templater, must
836
+ import [`microtags/render-support`](#microtagsrender-support) once.
837
+ Without it, `render` and `renderParts` throw an error naming that
838
+ module.
839
+
840
+ ```ts
841
+ import 'microtags/render-support'
842
+ import { MyCounter } from './components.js'
843
+
844
+ const markup = MyCounter.render({ start: 5 })
845
+ ```
846
+
847
+ > [!IMPORTANT]
848
+ > This is a breaking change in 0.0.x. Before it, `render` worked with
849
+ > no extra import.
850
+
851
+ Pass `{ client: true }` to keep the render function in the client
852
+ bundle after [`microbuild`](#microbuild) strips it:
853
+
854
+ ```ts
855
+ .render(renderFn, { client: true })
856
+ ```
857
+
858
+ See [`client:true` vs `--keep`](#clienttrue-vs---keep) for when to
859
+ use each.
860
+
861
+ > [!NOTE]
862
+ > `.render` on the builder is unrelated to
863
+ > [`microtags/render`](#microtagsrender), which is a DOM reconciler
864
+ > for lists and single items.
865
+
866
+ ---------------------------------------------------------
867
+
868
+
718
869
  ### `builder.setup(ctx)`
719
870
 
720
871
  Registers the custom element via `customElements.define` and returns the
@@ -736,6 +887,14 @@ class ComponentBuilder {
736
887
  The callback function runs once per element instance, and is called with the
737
888
  following `context` object.
738
889
 
890
+ Teardown waits a microtask. When the element is removed, its cleanups
891
+ (listeners, effects, bindings, `onCleanup` callbacks) run on the next
892
+ microtask, and only if it is still disconnected then. So an element that is
893
+ removed and re-inserted in the same tick (a move) keeps its setup and
894
+ state, even in browsers without `moveBefore`. After a real removal, cleanup has
895
+ happened once `await Promise.resolve()` returns. If the element is inserted
896
+ again later, setup runs again.
897
+
739
898
 
740
899
  #### `ctx`
741
900
 
@@ -750,11 +909,11 @@ ctx:SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>
750
909
  | Method | Returns | Description |
751
910
  |--------|---------|-------------|
752
911
  | `.peek(signal)` | `T` | Read a signal's current value without subscribing, even inside `ctx.effect`. |
753
- | `.on(target, type, handler, options?)` | `void` | Add a DOM event listener, auto-removed on disconnect. |
912
+ | `.on(target, type, handler, options?)` | `void` | Add a DOM event listener, auto-removed on teardown (a microtask after disconnect). |
754
913
  | `.emit(event)` / `.emit(name, detail?, options?)` | `boolean` | Dispatch a pre-built `Event` as-is, or construct and dispatch a bubbling `CustomEvent` from a name. Returns `dispatchEvent`'s result: `false` if a cancelable event had `preventDefault()` called. |
755
- | `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on disconnect. |
756
- | `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on disconnect. |
757
- | `.onCleanup(fn)` | `void` | Register custom teardown to run on disconnect. |
914
+ | `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on teardown. |
915
+ | `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on teardown. |
916
+ | `.onCleanup(fn)` | `void` | Register custom teardown, run a microtask after disconnect. |
758
917
  | `.consume(token)` | `T \| undefined` | Read an optional context value published by an ancestor. See [`withContexts`](#withcontexts). |
759
918
 
760
919
  See [Typed Events](#typed-events) for more about `.on` and `.emit`.
@@ -798,6 +957,36 @@ ctx.bind(() => `${count()} items`, ctx.refs.label, { prop: 'textContent' })
798
957
 
799
958
  ---
800
959
 
960
+ ### `bindProp`
961
+
962
+ Bind a signal (or computed) to a declared prop of another host, so the
963
+ prop reads through to it. This is the same as assigning the signal to
964
+ the host's prop accessor, but typed: the signal's value type must match
965
+ the prop's type, and only declared props are accepted.
966
+
967
+ ```ts
968
+ function bindProp (
969
+ host:HTMLElement,
970
+ name:string, // a declared prop of `host`
971
+ source:() => T // T is the prop's type
972
+ ):void
973
+ ```
974
+
975
+ ```ts
976
+ import { define, bindProp } from 'microtags'
977
+ import { computed } from 'alien-signals'
978
+
979
+ // in a parent's setup, drive a nested host's `label` prop
980
+ const label = computed(() => String(ctx.props.count()))
981
+ bindProp(ctx.refs.badge, 'label', label)
982
+ ```
983
+
984
+ While bound, the prop is read-only inside the nested component, and the
985
+ host's HTML attribute is no longer updated. Assigning a plain value to
986
+ the accessor removes the binding.
987
+
988
+ ---
989
+
801
990
 
802
991
  ## Missing refs
803
992
 
@@ -1217,6 +1406,351 @@ type Attrs = Record<
1217
1406
  function toAttributes (attrs:Attrs):string
1218
1407
  ```
1219
1408
 
1409
+ ### `microtags/htm`
1410
+
1411
+ A tagged-template string renderer that will expand any microtags
1412
+ instances if finds. Exports `html`, `h`, `raw`, and the `Markup` type.
1413
+ `html`, `h`, and `raw` return `Markup`, not a string. Convert it with
1414
+ `String(m)` where a string is required, for example `innerHTML`. In the
1415
+ browser the same import resolves to a build that also exports `mount`;
1416
+ see [ADR-007](./docs/adr/ADR-007-client-templater.md).
1417
+
1418
+ ```ts
1419
+ import { html, raw } from 'microtags/htm'
1420
+ import { MyCounter } from './components.js'
1421
+
1422
+ const page = html`<div>
1423
+ <${MyCounter} start=${5}>
1424
+ <p>Extra children</p>
1425
+ </${MyCounter}>
1426
+ ${raw('<b>trusted markup</b>')}
1427
+ </div>`
1428
+ ```
1429
+
1430
+ When `html` meets a microtags element class, it splits props from
1431
+ passthrough attributes using the class's static `props` list, joins
1432
+ children into one string, and calls the static `render`. Plain string
1433
+ tag names emit ordinary HTML.
1434
+
1435
+ **Escaping.** Every interpolated string is escaped (`<`, `>`, `&`, `"`,
1436
+ `'`) before the parser sees it. Component output and `raw()` output
1437
+ are trusted and never re-escaped. Static template text is trusted.
1438
+
1439
+ **Arrays.** Array children (from `.map()`) are flattened automatically.
1440
+
1441
+ **Void elements.** Write void tags in the self-closing form, like
1442
+ `<br />`, `<img src="x" />`, and `<input value=${v} />`. The output has
1443
+ no closing tag and no trailing slash, eg `<br>`. The `htm` parser does
1444
+ not know which tags are void, so an unclosed `<br>` would be treated
1445
+ as an open tag and swallow the content after it. Instead of dropping
1446
+ that content, the templater throws, on the server and in the browser,
1447
+ and the error names the tag and its self-closing form.
1448
+
1449
+ **Custom elements.** A component tag always gets a closing tag in the
1450
+ output, eg `<my-counter start="5">...</my-counter>`, because HTML has no
1451
+ self-closing custom elements. In a template, close a component with
1452
+ `</${MyComponent}>` or with the `htm` shorthand `<//>`, which closes
1453
+ the nearest open tag without repeating its name.
1454
+
1455
+ #### What escaping does not cover
1456
+
1457
+ Escaping keeps an interpolated string from becoming markup. It does not
1458
+ make every value safe in every place it can go.
1459
+
1460
+ **URL schemes.** Attribute escaping keeps a value inside its quotes,
1461
+ but `href=${url}` with `url` set to `javascript:alert(1)` is still a
1462
+ working link. The same goes for `src`, `action`, `formaction`, and
1463
+ other attributes that take a URL. Check the scheme yourself (for
1464
+ example, allow only `http:`, `https:` and `mailto:`) before a URL from
1465
+ user data reaches the template.
1466
+
1467
+ **`<script>` and `<style>`.** HTML escaping is the wrong encoding inside
1468
+ these elements. The browser does not decode entities there, so an
1469
+ escaped value comes out mangled, and HTML escaping does not stop a
1470
+ value from breaking out of a JavaScript string or a CSS rule. Do not
1471
+ interpolate user data into them. To pass data to a script, serialize it
1472
+ as JSON in a `<script type="application/json">` and escape `<` in the
1473
+ JSON, the way the hydration example's state script does.
1474
+
1475
+ **`raw`.** `raw()` skips escaping by construction. Pass it only markup
1476
+ you wrote or trust, never user data.
1477
+
1478
+ **Attribute names.** Escaping covers values, not names. A name must
1479
+ start with a letter, `_` or `:`, followed by letters, digits, `-`,
1480
+ `_`, `:` or `.`. Any other name, for example a key from a spread of
1481
+ user data such as `x" onload="...`, is skipped without an error, on the
1482
+ server, in the browser, and on a component's host tag. Valid keys in the
1483
+ same spread are still written.
1484
+
1485
+ **Void elements.** A void element with children throws instead of
1486
+ rendering, on the server and in the browser. Write void tags in the
1487
+ self-closing form, `<input />`, so an unclosed tag cannot swallow the
1488
+ markup after it.
1489
+
1490
+ ---
1491
+
1492
+ ### `microtags/render-support`
1493
+
1494
+ The host-tag builder, prop serializers and attribute escaping that a
1495
+ component's static `render` and `renderParts` use. Import it for its
1496
+ side effect:
1497
+
1498
+ ```ts
1499
+ import 'microtags/render-support'
1500
+ ```
1501
+
1502
+ It is separate from core so that a client bundle with every
1503
+ `.render` link stripped leaves this code out. The `microtags/htm`
1504
+ templater imports it already, and the [`microtags/build`](#microtagsbuild)
1505
+ transform adds it to any module where it keeps a `.render` link, so
1506
+ only code that calls a `render` or `renderParts` static without
1507
+ either needs this import. See
1508
+ [ADR-009](./docs/adr/ADR-009-server-render-support-through-render-link.md).
1509
+
1510
+ ---
1511
+
1512
+ ### `microtags/preact`
1513
+
1514
+ Use a microtags component inside a Preact app, rendered on the server
1515
+ and hydrated in the browser, or rendered client-only. `toPreact(El,
1516
+ { events })` takes an element class and returns a Preact component that
1517
+ renders the host tag. Preact owns the host and its attributes, and the
1518
+ element owns the host's children. See
1519
+ [ADR-010](./docs/adr/ADR-010-preact-adapter-ownership-split.md).
1520
+
1521
+ ```ts
1522
+ import { toPreact } from 'microtags/preact'
1523
+ import { NumberInput } from './number-input.js'
1524
+
1525
+ // callback prop name -> DOM event name
1526
+ export const PreactNumberInput = toPreact(NumberInput, {
1527
+ events: { onChange: 'number-input:change' },
1528
+ })
1529
+ ```
1530
+
1531
+ The component takes the element's declared props (typed from the
1532
+ class), one callback prop per entry in `events`, and `class`, `id`,
1533
+ `aria-*` and `data-*` props, which pass through to the host.
1534
+
1535
+ **Server render, then hydrate.** On the server, the adapter fills the
1536
+ host with the component's own markup, so `renderToString` gives the
1537
+ same HTML as the component's `render` static. Server code must import
1538
+ a variant of the component that keeps its `.render` link; a class with
1539
+ no render link throws an error naming its tag.
1540
+
1541
+ ```ts
1542
+ // server
1543
+ import { h } from 'preact'
1544
+ import { renderToString } from 'preact-render-to-string'
1545
+
1546
+ const body = renderToString(h(App, null))
1547
+ ```
1548
+
1549
+ ```ts
1550
+ // browser
1551
+ import { h, hydrate } from 'preact'
1552
+
1553
+ hydrate(h(App, null), document.getElementById('root')!)
1554
+ ```
1555
+
1556
+ In `App`, use the component like any other:
1557
+
1558
+ ```ts
1559
+ h(PreactNumberInput, {
1560
+ min: 1,
1561
+ class: 'qty',
1562
+ onChange: (ev:CustomEvent) => setQty(ev.detail),
1563
+ })
1564
+ ```
1565
+
1566
+ In the browser, the host gets a `dangerouslySetInnerHTML` that never
1567
+ changes once the host is in the document. While hydrating, Preact
1568
+ leaves that element's children alone, so the element adopts the server
1569
+ markup. Later renders never write `innerHTML` either. Declared props
1570
+ reach the element as properties, including after `hydrate`, where
1571
+ Preact itself sets no props: the adapter assigns them once the host is
1572
+ committed, so a prop-only value or an object arrives too.
1573
+
1574
+ **Client-only render.** With no server markup, call Preact's `render`.
1575
+ On that first render the adapter fills the host with the component's
1576
+ inner HTML from its `renderParts` static, before the element connects,
1577
+ so the element finds its markup and refs. This needs the variant of
1578
+ the component that keeps its render link, with
1579
+ [`microtags/render-support`](#microtagsrender-support) loaded (the
1580
+ build transform imports it for a kept render link). A component whose
1581
+ render link was stripped mounts as an empty host.
1582
+
1583
+ ```ts
1584
+ import { h, render } from 'preact'
1585
+
1586
+ render(h(PreactNumberInput, { min: 1 }), document.body)
1587
+ ```
1588
+
1589
+ **Two builds.** The import resolves by export condition, the same way
1590
+ as [`microtags/htm`](#microtagshtm). The `browser` condition selects a
1591
+ build that contains no render code. The default build, for Node and
1592
+ SSR, imports [`microtags/render-support`](#microtagsrender-support).
1593
+
1594
+ **Peer dependency.** `preact` is an optional peer dependency. Install
1595
+ it yourself. Only `microtags/preact` imports it.
1596
+
1597
+ **No children.** The adapter does not accept Preact children. Passing
1598
+ them is a type error, and the adapter throws.
1599
+
1600
+ **Props before definition.** If the element is not defined yet when
1601
+ Preact first sets a prop, the element has no property by that name, so
1602
+ Preact sets an attribute instead. The component reads its declared
1603
+ props from attributes when it connects, so a value that survives
1604
+ conversion to an attribute string still arrives. The adapter does not work around this.
1605
+
1606
+ **Other host renderers.** The adapter is built on the
1607
+ [`renderParts`](#builderrenderfn-options) static, which returns the
1608
+ host tag, its serialized attributes and its inner HTML separately. An
1609
+ adapter for another framework can build on the same parts.
1610
+
1611
+ ---
1612
+
1613
+ ### `microtags/build`
1614
+
1615
+ Node-only. Strips [`.render()`](#builderrenderfn-options) calls from
1616
+ define chains so the client bundle contains only hydration code.
1617
+ Requires `typescript` as a peer dependency.
1618
+
1619
+ ```ts
1620
+ import { transform } from 'microtags/build'
1621
+
1622
+ const result = transform(source, {
1623
+ keep: ['my-notice'], // keep these tags' render in the bundle
1624
+ cascade: true, // dead-import removal, default false
1625
+ filename: 'app.ts', // shown in warnings
1626
+ })
1627
+
1628
+ console.log(result.code)
1629
+ for (const w of result.warnings) {
1630
+ console.warn(`${w.file}:${w.line} ${w.message}`)
1631
+ }
1632
+ ```
1633
+
1634
+ **Strip rule:** a `.render(...)` call is removed when it is a link in a
1635
+ call chain rooted at a `define(...)` call imported from `microtags`,
1636
+ with import aliases tracked. Nothing else named `render` is touched.
1637
+
1638
+ **Keep rule (union):** the call is kept when its options argument is an
1639
+ object literal containing `client: true`, or when the string literal
1640
+ passed to `define` is listed in `keep`.
1641
+
1642
+ **Output fidelity:** removed spans are replaced with the same number of
1643
+ newlines. Every other byte is unchanged, apart from the support import
1644
+ below. TS in, TS out.
1645
+
1646
+ **Support import:** when a module keeps at least one `.render` link,
1647
+ the transform appends `import 'microtags/render-support'` after the
1648
+ last line, unless the module already imports it, so kept components
1649
+ can render in the browser. A module where every link was stripped gets
1650
+ no such import, and a bundle with no kept link and no templater import
1651
+ contains none of the server render code. A bundler config that aliases
1652
+ `microtags` to source should alias `microtags/render-support` too.
1653
+
1654
+ **Unsupported:** `const b = define('x'); b.render(fn)` -- the
1655
+ non-chained form is not matched and is left unchanged.
1656
+
1657
+ **`client: true` detection:** only an object literal is recognised. A
1658
+ variable or spread in the options position is not.
1659
+
1660
+ **Type gap:** `MicrotagElementClass` declares `render` and `props` as
1661
+ always present. After stripping, `render` is `undefined` at runtime.
1662
+
1663
+ **Warnings** (each with file and line):
1664
+
1665
+ | Kind | Meaning |
1666
+ |------|---------|
1667
+ | `outside-chain` | `.render(` call not in a define chain |
1668
+ | `stripped-static` | `X.render(` after X's render was stripped |
1669
+ | `non-literal-tag` | `define(variable)` with `keep` set |
1670
+
1671
+ ---
1672
+
1673
+ ### `microtags/esbuild`
1674
+
1675
+ Node-only esbuild plugin that calls [`transform`](#microtagsbuild) on
1676
+ every matching file during the build.
1677
+
1678
+ ```ts
1679
+ import { microtags } from 'microtags/esbuild'
1680
+
1681
+ // in your esbuild config
1682
+ plugins: [
1683
+ microtags({
1684
+ cascade: true,
1685
+ keep: ['my-notice'],
1686
+ }),
1687
+ ]
1688
+ ```
1689
+
1690
+ The plugin accepts the same options as `transform` plus a file `filter`.
1691
+ Warnings are forwarded to esbuild's warning channel.
1692
+
1693
+ ---
1694
+
1695
+ ### `microtags/vite`
1696
+
1697
+ Node-only Vite plugin, same interface as the
1698
+ [esbuild plugin](#microtagsesbuild).
1699
+
1700
+ ```ts
1701
+ import { microtags } from 'microtags/vite'
1702
+
1703
+ // in vite.config.ts
1704
+ plugins: [
1705
+ microtags({
1706
+ cascade: true,
1707
+ keep: ['my-notice'],
1708
+ }),
1709
+ ]
1710
+ ```
1711
+
1712
+ ---
1713
+
1714
+ ### `microbuild`
1715
+
1716
+ CLI for stripping `.render()` calls outside a bundler.
1717
+
1718
+ ```sh
1719
+ # stdin/stdout -- one file
1720
+ cat src/counter.ts | microbuild --cascade > dist/counter.ts
1721
+
1722
+ # glob mode -- requires --outdir
1723
+ microbuild 'src/**/*.ts' --outdir dist --cascade --keep my-notice
1724
+ ```
1725
+
1726
+ `--keep` is repeatable and accepts comma-separated tag names. `--root`
1727
+ sets the base for output paths (defaults to the current directory).
1728
+ Files are never rewritten in place.
1729
+
1730
+ stdin mode transforms one file only. A multi-file app should use the
1731
+ [esbuild](#microtagsesbuild) or [vite](#microtagsvite) plugin, or
1732
+ glob mode.
1733
+
1734
+ ---
1735
+
1736
+ ### `client:true` vs `--keep`
1737
+
1738
+ Both prevent [`microbuild`](#microbuild) from stripping a component's
1739
+ `.render()` call, but they work at different levels:
1740
+
1741
+ - **`{ client: true }`** is set in source, in the `.render()` options.
1742
+ Use it for components that must render themselves in the browser --
1743
+ for example, a notice inserted dynamically after page load. The
1744
+ decision travels with the component and applies in every build.
1745
+
1746
+ - **`--keep tag-name`** (or the `keep` option in the plugin) is set at
1747
+ build time. Use it to override stripping for specific tags without
1748
+ changing source, for example during development or in a build
1749
+ variant that needs client rendering.
1750
+
1751
+ Both can be combined. A component is kept when either condition is met
1752
+ (union semantics).
1753
+
1220
1754
  ---
1221
1755
 
1222
1756
  ## Differences from `nanotags`