- lint `summary` uses the key `infos`, not `info`
- the `diff` JSON includes all token categories plus a `findings`
block (before/after/delta) and `regression`; the example previously
showed only two token categories and omitted `findings`
- the linter runs ten rules — add the missing `token-like-ignored` row
Co-authored-by: David East <deast@google.com>
Implement support for an optional omitted frontmatter configuration key in DESIGN.md, allowing design system authors to explicitly declare token categories that are intentionally skipped or absent (Issue #78).
* Update Parser: Add 'omitted' to the known schema keys and types. Implement frontmatter parsing supporting both bare strings (e.g. - spacing) and object mappings with reasons (e.g. section: rounded, reason: "No rounded corners").
* Update Model: Forward the parsed omitted sections to the compiled DesignSystemState. Add optional rule property to linter Findings.
* Implement Omission Validation & Suppression:
- Create omittedRule to validate the omitted configuration, warning on unknown or redundant sections (if tokens exist for a section listed in omitted).
- Update missing-sections and missing-typography rules to skip warnings if the targets are explicitly listed as omitted.
- Register the new rule in the default linter list.
* Test Coverage: Add test suites verifying frontmatter parsing, model mapping, linter warnings (declared-omission, redundant-omission, unknown-omission), and rule suppressions.
* Docs: Document the omitted frontmatter key in README.md and spec.mdx, update active rule counts, and regenerate docs/spec.md.
---------
Co-authored-by: David East <deast@google.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Sudarshan sunil hadmode <sudarshan.deve@gmail.com>
Use stdout.write because serializeTailwindV4 already ends with one
newline; console.log added a second byte that broke git diff --check.
Also exit cleanly on missing DESIGN.md with a friendly message instead
of dumping a Node stack trace after the JSON error.
Fixes#139Fixes#132
Co-authored-by: David East <deast@google.com>
* feat: data-driven Color and Dimension type definitions (fixes#97)
* fix(export): align export test expectations with structured error formats
* fix(ts): eliminate duplicate typeDefinitions identifiers and properties for clean CI compilation
---------
Co-authored-by: David East <deast@google.com>
A typography token's sub-properties outside the schema (fontFamily,
fontSize, fontWeight, lineHeight, letterSpacing, fontFeature, fontVariation)
were silently dropped by the model — never resolved, never exported, and
with no diagnostic, so a typo like `fontwight` or an unsupported property
like `textTransform` vanished without a trace. Emit a warning for each,
mirroring how unknown component sub-tokens are already reported.
cssStringLiteral escaped only backslash and double-quote. A font-family value
containing a raw newline, carriage return, or form feed (legal via a YAML
quoted or block scalar) was emitted verbatim inside the CSS string literal,
where raw line terminators are illegal and can break the value out of the
@theme token. Emit them as CSS hex escapes (e.g. `\a `).
The success branches (css-tailwind, json-tailwind, dtcg, css-vars) write
their output but never assign process.exitCode, so a successful export
left the code at its incoming value instead of an explicit 0. That is the
success-path counterpart to #126, which decoupled the exit code from
source lint findings; the subprocess test there only exercises json-tailwind
and a real process happens to exit 0 when exitCode is unset, so the gap went
unnoticed. Set process.exitCode = 0 after the format branches (every error
branch already returns first).
The in-process export.test.ts added in #109 asserts process.exitCode === 0
after a css-vars export, so it was failing on main. Also fix a companion
assertion in that file: it read error.error for the human message, but the
error envelope is { error: CODE, message: TEXT }, so the text lives on
error.message.
Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
The export command derived its exit code from the source's lint summary, so
exporting a file that had any lint *error* exited 1 even though the export
produced correct output. That conflates "the source has lint findings"
(which `lint` already reports by exiting 1) with "the export failed."
Decouple them: a successful export exits 0; only an invalid --format or an
emitter failure exits 1 (and an unreadable input exits 2, via readInput).
Document the export exit codes in the README.
On a missing or unreadable input file, readInput printed a structured
FILE_READ_ERROR JSON to stderr and then re-threw. The throw let the CLI
framework print a second, stack-trace error on top of the JSON and override
the exit code with 1 instead of the intended 2. Exit cleanly with code 2
right after the JSON, matching the function's documented "exits with error
JSON" contract.
Also unify the export command's stderr error envelope with readInput's
`{ error: <CODE>, message }` shape: an unknown --format now reports
`INVALID_FORMAT` (the human text moves to `message`), and emitter failures
forward the emitter's structured code (e.g. INVALID_TOKEN_NAME) instead of
discarding it.
When a user runs a command with "-" as the file path from an interactive
terminal (e.g. design.md lint -), the process blocks silently waiting
for EOF with no indication of what to do.
Add a TTY check before the stdin read loop. If stdin is attached to a
terminal, write to stderr:
Reading from stdin… Press Ctrl+D when done.
The stdin stream is accepted as an optional second parameter on readInput
(defaulting to process.stdin), making the TTY path fully testable via
dependency injection without touching process.stdin directly.
Also exports StdinStream so callers can type mock streams without
duplicating the definition.
* fix: replace ENOENT stack trace with FileReadError and human-readable stderr (#132)
Introduce a typed `FileReadError` class in `readInput` so the function
throws instead of calling `process.exit` directly. Each command handler
catches it and writes a plain-text error to stderr:
Error: "DESIGN.md" not found.
Create a DESIGN.md file or pass "-" to read from stdin.
This replaces the unhandled Node.js stack trace dump reported in #132.
`readInput` is now unit-testable without mocking `process.exit`, and
`FileReadError.filePath` identifies the specific missing file (important
for `diff`, which reads two files).
* fix: use error code to generate accurate FileReadError message
Replace the hardcoded "not found" string in all command handlers with
a friendlyMessage getter on FileReadError that checks the OS error code:
- ENOENT → "not found. Create a DESIGN.md file or pass '-' for stdin."
- EACCES → "could not be read: permission denied."
- other → "could not be read: <raw message>"
This prevents a misleading "not found" message when the file exists
but cannot be read due to permissions or other I/O errors.
Three small guards so a hostile DESIGN.md cannot pin CPU or exhaust the
call stack. All inputs are at the documented untrusted boundary (arbitrary
file/stdin), and none of the changes alter results for legitimate input.
- parseDimensionParts (and token-like-ignored's CSS_DIMENSION_RE) backtrack
quadratically on long all-digit strings. Cap value length to 64 chars
before matching; real CSS dimensions are far shorter.
- unknown-key runs an O(n*m) Levenshtein DP against every schema key for
each unknown key. Skip a schema key whose length differs by more than the
typo threshold — edit distance is at least the length difference, so the
set of suggestions is unchanged.
- parseCssColor recurses for nested color-mix() with no depth bound. Thread
a depth counter and stop at 32, so an over-deep value resolves to an
invalid color (a precise error finding) instead of a RangeError that
collapses the whole model build.
parseHue tested `endsWith('rad')` before `endsWith('grad')`, so a gradian
angle matched the radians branch first: `100grad` resolved to ~329.58deg
instead of 90deg, silently producing the wrong color, luminance, contrast
result, and exports. Test `grad` before `rad`; deg/rad/turn/unitless
behavior is unchanged.
parseColorWithWeight treated any bare number as a weight and multiplied it
by 100, so `color-mix(in srgb, red 20, blue)` produced a wildly wrong blend
instead of being rejected. CSS color-mix weights are percentages only;
require a `%` suffix and otherwise return null (invalid color).
serializeTailwindV4() already appends a trailing newline to the
@theme block. Using console.log() adds a second trailing newline,
making checked-in generated CSS fail strict whitespace gates like
git diff --check.
Fixes#139
Change-Id: I3a0150b2c0f8e6a7b9d4c3e2f1a0b9c8d7e6f5a4
`npx @google/design.md spec` failed with "Failed to load spec.md" in
installed/published packages. The bundler emits the CLI to dist/index.js,
so getSpecContent() resolves spec.md alongside it at dist/spec.md, but the
build only copied docs/spec.md to dist/linter/. The dev-path fallback then
resolves outside the package, so the command threw.
Copy spec.md to dist/ as well, mirroring how spec-config.yaml is already
copied to both dist/ and dist/linter/. Add a `spec` invocation to the
tarball smoke test so the bundled-spec.md resolution is exercised in CI.
Closes#100
Add a new `token-like-ignored` lint rule that warns when a top-level YAML
key is not part of the recognized export schema and its value looks like a
design-token map (hex colors, CSS dimensions, or typography property names).
These keys are silently dropped by `design.md export`, misleading users into
thinking their color palette or type scale was exported.
- Extend `ParsedDesignSystem` with `rawValues` so the parser carries all
raw YAML values through to the model layer
- Extend `DesignSystemState` with `unknownKeyValues` populated by `ModelHandler`
- New rule `token-like-ignored` in `linter/rules/token-like-ignored.ts`
- 10 tests covering hex color maps, font maps, dimension maps, flat scalars,
non-token objects, nested maps, and multi-key scenarios
- Register rule in `DEFAULT_RULE_DESCRIPTORS` and re-export from public API
- Update hardcoded rule-count assertions in `types.test.ts` and `spec.test.ts`
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* release: 0.3.0
* fix: remove hardcoded local path from DTCG conformance test
* test: skip DTCG conformance test (upstream @terrazzo/token-types removed from npm)
- Remove dead runtime dependencies with zero imports: ink, react,
@json-render/core, @json-render/ink, mdast (type-only import,
covered by @types/mdast)
- Remove @types/react from devDependencies
- Add package-level lint script (tsc --noEmit --skipLibCheck) so
turbo lint actually runs
- Fix 10 pre-existing type errors in handler.test.ts where
properties.get() returns ResolvedValue but tests compared against
raw primitives
- Pin CI bun version to 1.3.9 to match root packageManager field
instead of floating on latest
- Use root-level turbo commands in CI instead of cd-ing into
packages/cli
- Add lint step to CI pipeline
- Regenerate bun.lock after dependency removal
bun install now completes with no peer warnings.
bun run build, bun run test, and bun run lint all pass at the root.
Closes#30
The linter already supports oklch, oklab, lab, lch, rgb, hsl, hwb,
named colors, color-mix, and 8-digit hex alpha. But the spec, README,
and error messages all said only hex was accepted. This was blocking
adoption for teams using modern CSS color spaces (Issue #53).
Update the Color type definition in the spec, the token types table in
the README, the spec.mdx source, and the linter error message to
reflect the full range of supported formats.
Closes#53
* feat(linter): add unknown-key rule for unknown top-level keys
* refactor(linter): narrow unknown-key to typo detection via Levenshtein
DESIGN.md is intentionally extensible, so warning on every unknown
top-level key flags legitimate custom fields. Restrict the rule to
likely typos of known schema keys (edit distance ≤ 2, case-insensitive)
and stay silent for unrelated extension keys.
Per @davideast review feedback on #84:
- Add SCHEMA_KEYS / SchemaKey in parser/spec.ts as the single source
of truth and reference it from the model handler.
- Add a zero-dependency Levenshtein helper.
- Suggest the closest known key in the warning message.
* test(linter): add unit tests for levenshtein helper
Cover empty strings, single edit operations (insert/delete/substitute),
symmetry, the classic kitten/sitting case, and the exact distances
used by the unknown-key typo threshold.
formatAsMarkdown template-literaled obj.summary directly, which coerced
the lint summary object { errors, warnings, infos } to its toString
representation '[object Object]'.
Detect the lint output shape (findings array + numeric summary) and
render a proper markdown report with severity counts and a bulleted
findings list. The legacy string-summary path for fixer/diff shapes
is preserved.
Adds 6 tests covering the regression, findings with and without paths,
empty findings, the --format md alias, and the legacy fixer shape.
Fixes the bug originally reported in PR #56.
The component property loop only checked for typeof 'number' before
passing values to string-only helpers (isTokenReference, isValidColor,
isParseableDimension). Boolean YAML values (e.g. visible: true) fell
through to those helpers and only survived by accident due to the typeof
guards added in PR #79. This change handles booleans explicitly alongside
numbers so the intent is clear.
Adds three tests covering the exact reproducer from Issue #75
(opacity: 0.9), boolean properties (visible: true), and a mixed case
with numbers, booleans, and strings in the same component.
Rename export format flags to avoid 'Tailwind CSS' product name confusion
and restore backwards compatibility:
- css-tailwind: Tailwind v4 CSS @theme block (new)
- json-tailwind: Tailwind v3 theme.extend JSON
- tailwind: backwards-compatible alias for json-tailwind
The output-first naming (css-*, json-*) tells the user what they'll get
before which tool it targets. Bare 'tailwind' preserves existing behavior
(JSON output), preventing a breaking change from PR #45.
Updates README.md export section and interop docs to reflect new naming.
Adds a `--format tailwind` export that emits a CSS `@theme { ... }`
block using Tailwind v4's native CSS-variable token namespaces
(--color-*, --font-*, --text-*, --leading-*, --tracking-*,
--font-weight-*, --radius-*, --spacing-*).
The previous v3 JSON output is preserved under the explicit
`--format tailwind-v3` name. Since the project is pre-1.0 (0.1.1),
renaming `tailwind` to mean 'latest' follows the convention most
tooling uses for unversioned names.
New module: packages/cli/src/linter/tailwind/v4/
- spec.ts — types and Zod schema for v4 theme data
- handler.ts — DesignSystemState → v4 theme data
- serialize.ts — v4 theme data → CSS @theme string
Tests: 17 new tests (handler + serializer + fixture).
- Document quoting for scoped package in shells that treat @ specially
- Explain ENOVERSIONS and registry/.npmrc checks (addresses #55)
- Add Windows CI job that installs @google/design.md from npmjs.org
Made-with: Cursor
The diff command compared colors, typography, rounded, and spacing but
silently skipped components — changes to button styles, added/removed
component tokens, etc. were invisible in the output.
- Add `components` field to the tokens diff using a local serializer
that flattens ComponentDef.properties Maps into plain objects before
passing them to the existing diffMaps utility
- Remove the unused `serializeDesignSystem` import
- Add diff.test.ts with 4 cases: no-change, added, removed, modified
Numeric values such as fontWeight: 600 or borderWidth: 1 are valid per
the DESIGN.md spec, which states bare numbers and quoted strings are
equivalent. However, the component property loop in ModelHandler passed
all rawValues directly to isTokenReference / isValidColor, both of which
call .match() and crash when rawValue is a number.
Fix: add a typeof rawValue === 'number' guard at the top of the loop,
storing numeric values as-is — matching the pattern already used by
parseTypography for the same property.
Fixes#42
Closes#54.
The existing `design.md` bin entry produces a `node_modules/.bin/design.md`
shim file on Windows. Because the basename ends in `.md`, Windows command
resolution short-circuits to the Markdown file association before iterating
PATHEXT, so PowerShell opens the shim in the user's Markdown editor
(VS Code by default) instead of executing it.
Add a `designmd` alias bin that resolves to the same entrypoint. The dot-free
name lets the npm CMD/PowerShell shims resolve cleanly via PATHEXT on Windows
while leaving the original `design.md` bin in place for posix users and
existing scripts.
* `packages/cli/package.json`: add the alias to the bin map.
* `README.md`: add a "Windows tip" callout under CLI Installation explaining
when to reach for the alias.
* `packages/cli/scripts/check-package.ts`: new check #5c verifies the bin
map exposes at least one dot-free alias so future package edits don't
silently regress this.
Pre-existing failure #20 (`CLI spec command failed to load spec.md`)
reproduces on `main@8ecd464` with my changes stashed (23 pass / 1 fail);
adding 5c yields 24 pass / 1 fail, with no other deltas.
Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
Components that reference one MD3 token (e.g. `colors.primary`) imply
the rest of the family (`on-primary`, `primary-container`,
`primary-fixed-dim`, `inverse-primary`, etc.) is part of the same
in-use semantic group. The rule now derives a family root for each
color name and treats sibling tokens as referenced when any sibling is.
The MD3 baseline families (primary, secondary, tertiary, error,
surface, background, outline) are also exempt from the orphaned check
because they are part of the standard contract a design system ships,
not optional decoration. Custom tokens (e.g. `brand-blue`) still get
flagged when truly unused.
Result on the shipped examples:
- atmospheric-glass: 43 orphan findings -> 0
- paws-and-paths: 33 orphan findings -> 0
- totality-festival: 38 orphan findings -> 0
Fixes#46
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
- Replace outdated tailwind command with export command in root README
- Add spec:gen script to packages/cli/package.json
- Fix path to repo root in generate.ts so it can find docs/spec.md
- Ignore smoke-test/ and *.tgz in packages/cli/.gitignore
- Note: packages/cli/README.md is generated from root README and ignored.
* chore: remove unused TUI dependencies
* fix: remove debug logging and add lazy-loaded config singleton
- Remove console.log/console.error from loadSpecConfig() that corrupted
stdout JSON output for CLI and programmatic consumers
- Introduce getSpecConfig() lazy singleton to cache YAML file reads
- Add 3 new tests: no-stdout-pollution, lazy-load validity, cache identity
Fixes Critical Blockers #1 and #2 from pre-publish analysis.
* fix: address high-risk publishing issues
- Add engines field to package.json (node >= 18)
- Add prepublishOnly script to enforce build before publish
- Add Node.js smoke test to CI workflow
- Clean up unused SPEC_VERSION import in test
* fix: derive version from package.json and simplify spec.md resolution
Version:
- Create src/version.ts that reads ../package.json using import.meta.url
- Path is stable across source, bundle, installed, and npx contexts
because npm always includes package.json in the published tarball
- Replace hardcoded '0.1.0' in index.ts with dynamic VERSION import
- Add 3 tests: matches package.json, valid semver, not fallback
spec.md path resolution:
- Reduce 5 shotgun candidate paths to 2 deterministic strategies:
1. Bundle path: ./spec.md (build copies it alongside entry points)
2. Dev path: ../../../../../docs/spec.md (relative to source)
- Add explicit specPath parameter for callers who know the path
- Update build script to copy spec.md to both dist/ and dist/linter/
- Add 3 tests: consistency, content length, explicit path contract
* fix: pre-publish hardening — 5 items
1. Move build-only deps to devDependencies (keep zod for type compat)
- citty, mdast, remark-*, unified, unist-util-visit, yaml all bundled
- Zero runtime require() calls to any npm dependency
2. Add 'designmd' bin alias for Windows shell compatibility
- cmd.exe confuses .md extension with file association
3. Add ./package.json to exports map
- Prevents ERR_PACKAGE_PATH_NOT_EXPORTED in strict ESM bundlers
4. Remove stale @types/react from devDependencies
5. Add tarball smoke test to CI workflow
- Packs, installs in clean dir, runs CLI + programmatic import
- Catches 'works in monorepo, breaks when installed' bugs
* docs: update recommended tokens in spec
* feat: set up OSS boilerplate and license headers
* chore: update package.json for npm publish readiness
* chore: update build script and ignore copied files in packages/cli