7.7 KiB
Project History
Phase 1: Core Node Types (initial scaffolding)
Commits: d7bb0b2 through c724094
The module was scaffolded with the Go module path forge.lthn.ai/core/go-html. The foundational work established:
d7bb0b2— Module scaffold,Nodeinterface withRender(ctx *Context) string.3e76e72—Textnode wired togo-i18ngrammar pipeline with HTML escaping.c724094— Conditional nodes (If,Unless), entitlement gating (Entitled, deny-by-default), runtime dispatch (Switch), and type-safe iteration (Each[T]).
The Raw escape hatch was present from the first commit. The decision to make Text always escape and Raw never escape was made at this stage and has not changed.
Phase 2: HLCRF Layout and Pipeline
Commits: 946ea8d through ef77793
946ea8d—Layouttype with HLCRF slot registry. Semantic HTML elements (<header>,<main>,<aside>,<footer>) and ARIA roles assigned per slot.d75988a— Nested layout path chains. Block IDs computed as{slot}-0at root, extended with{parent}-{slot}-0for nested layouts.40da0d8— Deterministic attribute sorting and thread-safe nested layout cloning (clone-on-render pattern).f49ddbf—Attr()helper for setting element attributes with chaining.e041f76—Responsivemulti-variant compositor withdata-variantcontainers.8ac5123—StripTagssingle-pass rune scanner for HTML-to-text stripping.76cef5a—Imprint()full render-reverse-imprint pipeline usinggo-i18n/reversal.ef77793—CompareVariants()pairwise semantic similarity scoring across responsive variants.
Phase 3: WASM Entry Point
Commits: 456adce through 9bc1fa7
456adce— Makefile withwasmtarget. Size gate:WASM_GZ_LIMIT = 1048576(1 MB). Initial measurement revealed the binary was already too large at this stage.5acf63c— WASM entry pointcmd/wasm/main.gowithrenderToStringexported towindow.gohtml.2fab89e— Integration tests refactored to useImprintpipeline.e34c5c9— WASM browser test harness added.18d2933— WASM binary size reporting improvements.9bc1fa7— Variant name escaping inResponsive, single-passStripTagsoptimisation, WASM security contract documented in source.
Phase 4: Codegen and Web Components
Commits: 937c08d through ab7ab92
37b50ab,496513e— Phase 4 design documents and implementation plan.937c08d—codegenpackage withGenerateClass,GenerateBundle,TagToClassName. Web Component classes with closed Shadow DOM.dcd55a4—registerComponentsexport added tocmd/wasm/main.go, bridging JSON slot config to WC bundle JS. This was the source of the subsequent binary size problem.ab7ab92— Transitivereplacedirective added forgo-inference.
WASM Binary Size Reduction
Commits: 6abda8b, 4c65737, aae5d21
The initial WASM binary measured 6.04 MB raw / 1.58 MB gzip — 58% over the 1 MB gzip limit set in the Makefile. The root causes were three heavyweight stdlib imports pulled in by registerComponents() in the WASM binary:
| Import | Approx. gzip contribution |
|---|---|
encoding/json |
~200 KB |
text/template |
~125 KB |
fmt (via layout.go) |
~50 KB |
go-i18n/reversal (via pipeline.go) |
~250 KB |
Total bloat: ~625 KB gzip over the core rendering requirement.
The fix was applied in three distinct steps:
Step 1: Remove registerComponents from WASM (4c65737)
cmd/wasm/register.go received a //go:build !js build tag, completely excluding it from the WASM compilation unit. The registerComponents entry on the gohtml JS object was removed from cmd/wasm/main.go. The codegen function was moved to a standalone build-time CLI at cmd/codegen/main.go. This eliminated encoding/json and text/template from the WASM import graph.
Step 2: Remove pipeline from WASM
pipeline.go received a //go:build !js build tag. The Imprint() and CompareVariants() functions depend on go-i18n/reversal, which is a heavyweight analysis library. These functions are server-side analysis tools and have no use in a client-side rendering module. The renderToString function in the WASM entry point never called them, so removal was non-breaking.
Step 3: Eliminate fmt from WASM
layout.go's blockID() method had used fmt.Sprintf for string construction. Replacing this with direct string concatenation (l.path + string(slot) + "-0") removed fmt from the WASM import graph entirely.
Result: 2.90 MB raw, 842 KB gzip. 47% reduction in gzip size. Well within the 1 MB limit.
Size gate test (aae5d21)
cmd/wasm/size_test.go was added to prevent regression. TestWASMBinarySize_Good builds the WASM binary in a temp directory, gzip-compresses it, and asserts:
- Gzip size < 1,048,576 bytes (1 MB).
- Raw size < 3,145,728 bytes (3 MB).
The test is skipped under go test -short and is guarded with //go:build !js.
Test Coverage Milestones
7efd2ab— Benchmarks added across all subsystems. Unicode edge case tests. Stress tests.ab7ab92— 53 passing tests across the package and sub-packages.aae5d21— 70+ tests passing (server-side); WASM size gate and codegen CLI tests added.
Known Limitations (as of current HEAD)
These are not regressions; they are design choices or deferred work recorded for future consideration.
-
Invalid layout variants are reported, not fatal.
NewLayout("XYZ")still produces empty output at render time, butValidateLayoutVariant()andLayout.VariantError()surface the invalid characters without changing the constructor signature. -
No WASM integration test.
cmd/wasm/size_test.gotests binary size only. TherenderToStringbehaviour is tested by building and running the WASM binary in a browser, not by an automated test. Asyscall/js-compatible test harness would be needed. -
Responsive accepts only Layout.
Responsive.Variant()takes*Layoutrather thanNode. The rationale is thatCompareVariantsin the pipeline needs access to the slot structure. AcceptingNodewould require a different approach to variant analysis. -
Context.service is private, but swappable through setters. The i18n service remains unexported, but
SetService()andWithService()let callers replace it while keeping the current locale applied. -
TypeScript definitions are generated.
codegen.GenerateTypeDefinitions()produces a.d.tscompanion for the generated Web Components. -
CSS scoping helper added.
VariantSelector()andScopeVariant()generate selectors fordata-variantcontainers, making responsive variants easier to target from CSS. -
Browser polyfill matrix not documented. Closed Shadow DOM is well-supported but older browsers require polyfills. The support matrix is not documented.
Future Considerations
These items were captured during the WASM size reduction work and expert review sessions. They are not committed work items.
- TypeScript type definitions alongside
GenerateBundle()for typed Web Component consumers. Implemented viaGenerateTypeDefinitions(). - Accessibility helpers —
aria-label,alt,aria-hidden, andtabindexhelpers. The layout has semantic HTML and ARIA roles, and the node layer now exposes common accessibility attribute shortcuts beyondAttr(). - Layout variant validation —
ValidateLayoutVariant()andLayout.VariantError()report unrecognised slot characters while preserving silent render-time skipping. - CSS scoping helper —
VariantSelector()andScopeVariant()generate selectors for responsive variants identified bydata-variantattributes. - Daemon mode for codegen — watch mode for regenerating the JS bundle when slot config changes, for development workflows.