diff --git a/CLAUDE.md b/CLAUDE.md index d6f6b5f..0821cfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ See `docs/architecture.md` for full detail. Summary: - **Responsive**: Multi-variant breakpoint wrapper (`data-variant` attributes), renders all variants in insertion order - **Pipeline**: Render → StripTags → go-i18n/reversal Tokenise → GrammarImprint (server-side only) - **Codegen**: Web Component classes with closed Shadow DOM, generated at build time by `cmd/codegen/` -- **WASM**: `cmd/wasm/` exports `renderToString()` only — size gate: < 3 MB raw, < 1 MB gzip +- **WASM**: `cmd/wasm/` exports `renderToString()` only — size gate: < 3.5 MB raw, < 1 MB gzip ## Server/Client Split @@ -40,9 +40,10 @@ Files guarded with `//go:build !js` are excluded from WASM: ## Dependencies -- `forge.lthn.ai/core/go-i18n` (replace directive → `../go-i18n`) +- `forge.lthn.ai/core/go-i18n` — internationalisation, text reversal pipeline +- `forge.lthn.ai/core/go-io` — file I/O abstraction (`coreio.Local`) +- `forge.lthn.ai/core/go-log` — structured logging and error wrapping (`log.E()`) - `forge.lthn.ai/core/go-inference` (indirect, via go-i18n) -- Both `go-i18n` and `go-inference` must be cloned alongside this repo for builds - Go 1.26+ required (uses `range` over integers, `iter.Seq`, `maps.Keys`, `slices.Collect`) ## Coding Standards @@ -53,6 +54,8 @@ Files guarded with `//go:build !js` are excluded from WASM: - Licence: EUPL-1.2 — add `// SPDX-Licence-Identifier: EUPL-1.2` to new files - Safe-by-default: HTML escaping via `html.EscapeString()` on Text nodes and attribute values, void element handling, entitlement deny-by-default - Deterministic output: sorted attributes on El nodes, reproducible block ID paths +- Error handling: use `log.E(scope, message, err)` from `go-log`, never `fmt.Errorf` +- File I/O: use `coreio.Local` from `go-io`, never `os.ReadFile`/`os.WriteFile` - Commits: conventional commits + `Co-Authored-By: Virgil ` ## Test Conventions diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go index edacf19..6a12c3c 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "strings" "testing" @@ -9,6 +10,12 @@ import ( "github.com/stretchr/testify/require" ) +type errReader struct{} + +func (errReader) Read([]byte) (int, error) { + return 0, errors.New("read failed") +} + func TestRun_Good(t *testing.T) { input := strings.NewReader(`{"H":"nav-bar","C":"main-content"}`) var output bytes.Buffer @@ -49,3 +56,11 @@ func TestRun_Good_Empty(t *testing.T) { require.NoError(t, err) assert.Empty(t, output.String()) } + +func TestRun_Bad_ReadError(t *testing.T) { + var output bytes.Buffer + + err := run(errReader{}, &output) + assert.Error(t, err) + assert.Contains(t, err.Error(), "reading stdin") +} diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 28a6aa2..517649c 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -52,3 +52,23 @@ func TestGenerateBundle_Good(t *testing.T) { assert.Contains(t, js, "MainContent") assert.Equal(t, 2, strings.Count(js, "extends HTMLElement")) } + +func TestGenerateBundle_Good_DeduplicatesTags(t *testing.T) { + slots := map[string]string{ + "H": "nav-bar", + "F": "nav-bar", + } + js, err := GenerateBundle(slots) + require.NoError(t, err) + assert.Equal(t, 1, strings.Count(js, "extends HTMLElement"), + "duplicate tag should produce only one class definition") +} + +func TestGenerateBundle_Bad_InvalidTag(t *testing.T) { + slots := map[string]string{ + "H": "notag", + } + _, err := GenerateBundle(slots) + assert.Error(t, err) + assert.Contains(t, err.Error(), "hyphen") +}