When running cem commands against the example projects, you don't need to cd, you can cem ${cmd} -p examples/${example}.
example:
$ make
$ dist/cem -p examples/kitchen-sink validateGetter methods should be named Foo(), not GetFoo().
All diagnostic logging (debug, info, warning, error, success, trace) must go through bennypowers.dev/cem/internal/logging. This ensures correct routing in CLI vs LSP modes and respects -v/-vv/-q flags. Never call pterm.Warning, pterm.Info, etc. directly for log messages. Direct pterm usage is reserved for terminal UI primitives only: spinners, live areas, colored/styled display formatting.
Verbosity levels: -q (quiet: warnings/errors only), default (+ success), -v (+ info), -vv (+ debug), -vvv (+ trace per-file detail). Use logging.AtLevel(level) as a guard before expensive debug computations.
Use platform.FileSystem for all filesystem I/O in library code. Never call os.ReadFile, os.WriteFile, os.Stat, os.MkdirAll, os.Create, os.Open, os.Remove, os.Rename, os.ReadDir, filepath.Glob, or os.DirFS directly -- thread platform.FileSystem through function parameters instead. Use platform.DirFS(fsys, dir) instead of os.DirFS(dir). For os.Getwd/os.UserHomeDir, pass the result as a parameter from CLI entry points rather than calling from internal packages. Exception: cmd/ package entry points may call os.Getwd()/os.UserHomeDir() since they are CLI-specific and pass results downward. Build tools with //go:build ignore (e.g., serve/generate_elements.go, mcp/tools/gen-adapters/) are also exempt.
Use platform.WalkDir for all directory traversals. It silently prunes .git directories and accepts additional dirs to skip via set.Set[string]. Never use filepath.WalkDir or fs.WalkDir directly -- those bypass .git pruning.
Run go vet to surface gopls suggestions. Common examples:
- replace
interface{}withany - replace
if/elsewithmin - replace
m[k]=vloop withmaps.Copy[mapsloop] - Loop can be simplified using slices.Contains [slicescontains]
In cobra command RunE handlers, use cmd.Println / cmd.PrintErrln instead of fmt.Println / fmt.Fprintln(os.Stderr, ...). This respects cobra's configured output streams and makes commands testable.
When debugging Go code, always use the logger logger.Debug, etc. Don't use fmt.Printf, which pollutes stdio, breaking the LSP and MCP commands.
To print debug logs to the console, pass -vv (debug) or -vvv (trace)
Practice TDD. See new tests fail first, before implementing the fix/feature.
IMPORTANT: When writing tests, always use fixture/golden pattern. We always prefer goldens, STRONG justification needed to permit inline assertions. If using inline assertions, comment why in test func.
- Fixtures: Input test data - the content directories/files your code operates on (e.g.,
serve/testdata/transforms/http-typescript/simple-greeting.ts). - Goldens: Expected output files to compare against (e.g.,
serve/middleware/routes/testdata/chrome-rendering/expected-basic.html). Tests should support--updateflag to regenerate golden files when intentional changes occur. - Always use MapFS for test data (unit/integration tests): Load all fixture and golden files into
platform.MapFileSystemviatestutil.NewFixtureFS()ortestutil.LoadTestdataFS(). Never callos.ReadFile()oros.Open()on test data in test code. Frontloading disk I/O into MapFS at setup time ensures consistency across packages and improves CI performance. - Exception: E2E tests (
//go:build e2eincmd/): These run the built binary as a subprocess and compare stdout against golden files on disk. Directos.ReadFile/os.WriteFileis expected for golden comparison and--updateregeneration since MapFS cannot cross process boundaries. - Always use Makefile targets for running tests or builds, since they export the necessary env vars.
- For LSP tests, ALWAYS use
testutil.RunLSPFixtures()- NEVER use directos.ReadFile()calls. - If frontend visual regression tests fail consistently, check if it's merely an issue of font rendering.
-
Fixture Structure: Each test scenario is a subdirectory containing:
input.htmlorinput.ts(required)manifest.json(optional)expected.jsonorexpected-*.json(optional, for assertions)package.jsonpointingcustomElementsblock tomanifest.json
-
Cursor Positions via
^cursorMarker: Position-dependent LSP tests (hover, completion, definition, references) embed cursor position directly in the fixture input file using a comment marker. The^column is the cursor character; the line above is the cursor line. Use language-appropriate comment syntax:HTML:
<test-element .testAttr="hello"></test-element> <!-- ^cursor -->
TypeScript (inside Lit template literals, use HTML comments):
const tpl = html` <test-element .testAttr="hello"></test-element> <!-- ^cursor --> `;
TypeScript (outside template literals, use JS comments):
import { html } from 'lit'; /* ^cursor */
CSS:
my-element::part(button) { /* ^cursor */
For HTML fixtures, the loader extracts cursors automatically. For TS fixtures, tests call
fixture.ParseCursor(testhelpers.TSCursorParser)since tree-sitter is needed to find template literal boundaries. For CSS, usefixture.ParseCursor(testhelpers.CSSCursorParser).The fixture loader strips the marker line from
InputContentand populatesfixture.Cursor *protocol.Position. Tests usefixture.Cursordirectly instead of maintaining a separate Go map of cursor positions. When no marker is present,Cursoris nil.Fallback: When a comment marker isn't practical (cursor inside a value, ambiguous column), use YAML frontmatter. Avoid frontmatter in
.tsfiles as it causes biome/eslint issues:--- cursor: line: 0 character: 14 --- <test-element .testAttr="hello"></test-element>
-
RunLSPFixtures Pattern:
testutil.RunLSPFixtures(t, "testdata/my-test-suite", func(t *testing.T, fixture *testutil.LSPFixture) { // Setup context ctx := testhelpers.NewMockServerContext() // Use fixture.InputContent, fixture.Manifest, fixture.Cursor, etc. // Load expected data with fixture.GetExpected("key", &expected) })
-
Multiple Expected Files: Use
expected-variant.json,expected-size.jsonpattern for testing multiple positions/cases in one fixture -
Regression Test Isolation: Keep regression test fixtures in separate directories (e.g.,
testdata-regression/) to avoid interference with standard test discovery -
Filesystem in Tests: Test code follows the same
platform.FileSystemrules as production code. Load fixture data viatestutil.NewFixtureFS(),testutil.LoadTestdataFS(), ortestutil.LoadFixtureFile()-- never via directos.ReadFile/os.Opencalls. When constructingMCPContextor similar objects that accept aplatform.FileSystem, pass the workspace'sMapFileSysteminstead ofnilto exercise the injected FS path. Two exceptions: (1) OS integration subtests (e.g., filewatcher tests) that must write to real temp dirs viat.TempDir(), and (2) E2E tests (//go:build e2eincmd/) that run the built binary as a subprocess and compare stdout against golden files on disk. -
Error Path Testing: When testing filesystem error paths that rely on OS permissions (e.g., unreadable files), use an error-injecting
FileSystemwrapper instead ofos.Chmod. MapFS does not enforce permissions, so permission-based error tests need a wrapper that returns errors for specific paths.
See docs/TESTING.md for the full testing convention. In brief:
- Tier 1 (pure functions): exhaustive table-driven tests, in-package. A pure function is one where all effects depend on its formal parameters.
- Tier 2 (logic with effects): fixture-based tests with real dependencies (e.g., QueryManager). No percentage target.
- Tier 3 (thin wiring): covered by integration/e2e tests. No separate unit tests required.
New LSP methods or MCP tools must include at least Tier 2 tests before merging.
- Docs: When working on the docs site, refer to
docs/CLAUDE.mdfor docs-specific guidelines. - Generate and Validate: When touching code related to the custom-elements manifest schema, double-check with the canonical TypeScript schema.d.ts at https://github.com/webcomponents/custom-elements-manifest/. This is to avoid hallucinations. Note that different schema versions may have different types, and we'll have to deal with that if they do.
- Tree-sitter concurrency: Never share
QueryMatcherbetween goroutines. Cache queries, create fresh cursors per operation. - Nil safety: Check for nil
DocumentManagerbefore use; test with nil parameters. - Protocol compliance: Match LSP return types exactly; declare all capabilities in
initialize. - Testing: Use
package lsp_testfor public API tests. Usetesthelpers.NewMockServerContext()for test context. Fixtures intestdata/dirs, regression fixtures intestdata-regression/. - When adding LSP methods, implement diagnostics + code actions together and update server capabilities.
- Design principle: Data + context + LLM decision-making. Provide rich manifest data and contextual templates; let the LLM decide. No hardcoded suggestions.
- Respect element authors: Use their documented constraints and intent. Follow documented accessibility patterns, don't add generic ARIA advice.
- Template naming: Use dashes (e.g.,
element-attributes.md), not underscores. Reference subresources (cem://element/{tagName}/attributes), not tool names.
When adding or modifying configuration fields, update all three locations:
- Config struct in
internal/config/load.go-- add the field with proper struct tags - Init wizard in
cmd/config_init.go-- add form fields, detection, and apply logic - Docs in
docs/content/docs/reference/configuration.md-- document the new option
If the field is detectable from the project (e.g. file patterns, git remote), add detection in cmd/config_init_detect.go. If it needs validation, add it to internal/config/validate.go and wire it into the wizard's ValidateFn.
- Always run
make lintand address issues before committing - Always check go LSP diagnostics and address them before committing
- When commit messages mention AI agents, always use
Assisted-By, never useCo-Authored-By.
When writing CSS, use modern native syntax including nesting, :has, layers, and light-dark()
When writing web components (e.g. for the dev server)
- prefer ids to classes in shadow roots for unique elements
- don't use
classon the host as a public api, use attributes instead - don't dispatch new CustomEvent with details, instead create custom classes which extend Event, and have class fields for attached state.
Format attributes vertically aligned and sort by importance, e.g.:
<button id="button"
part="button"
class="${classMap({ loading })}"
aria-disabled="${String(disabled)}"
@click="${this.#onClick}">When manipulating HTML via JavaScript, prefer template cloning and the DOM API to the innerHTML setter.
- Use native CSS nesting syntax.
- but don't nest under :host except when using host attrs like
:host([disabled]) { button { ... } } - Prefer
light-dark()toprefers-color-scheme - When writing stylesheets for shadow roots, prefer ID selectors to classes