Custom component sub-tokens already work: they parse, their token references resolve, and they survive into the resolved model, so a project can build on them. design.md lint exits 0. The gap is that there's no way to tell the linter which ones are intentional, so broken-ref warns once per recipe, forever.
Our design system records two sub-tokens per recipe that the exporter schema doesn't define: owner, the canonical code module implementing the recipe, and gap, a resolved spacing value for compositions that own the spacing between their children. Our build step reads the linter's resolved model and generates a typed contract from both.
That's 49 of our 62 warnings today, growing with every component. The cost isn't the volume, it's that a misspelled sub-token produces a warning that reads identically to the 48 expected ones — the check's real value is buried by its own false positives.
Blanket suppression isn't what we want, and it isn't really available anyway: the sub-token check lives inside broken-ref alongside dangling-reference errors, so deselecting the rule through LintOptions.rules would silence genuine errors. Since 0.4.0 we post-process the report instead, matching finding.rule plus the token path against a list of extensions we declare ourselves — which works, but means we maintain the typo defense the linter could be providing.
What would solve it, in order of preference:
- Per-rule options. A way to pass configuration to a rule — for broken-ref, the set of sub-token names the project owns. Validation then runs against the union, and anything unlisted still warns. This generalizes past our case; sub-tokens just happen to be where we hit it first.
- A frontmatter declaration, so the extension names live next to the recipes that use them and travel with the document.
- A documented extension prefix such as x-owner, treated as always-valid. Cheapest to implement, but it renames keys in every recipe and in the resolved model.
Any of these lets a project keep the typo detection instead of reimplementing it. Happy to send a PR in whichever direction you prefer.
Checked in 0.3.0 and 0.4.0: VALID_COMPONENT_SUB_TOKENS, the broken-ref implementation, and LintOptions are identical. loadSpecConfig(filePath?) accepts a path but getSpecConfig() calls it with no argument and it isn't exported. The 0.4.0 omitted: key covers section-level omissions, not sub-tokens.
Custom component sub-tokens already work: they parse, their token references resolve, and they survive into the resolved model, so a project can build on them. design.md lint exits 0. The gap is that there's no way to tell the linter which ones are intentional, so broken-ref warns once per recipe, forever.
Our design system records two sub-tokens per recipe that the exporter schema doesn't define: owner, the canonical code module implementing the recipe, and gap, a resolved spacing value for compositions that own the spacing between their children. Our build step reads the linter's resolved model and generates a typed contract from both.
That's 49 of our 62 warnings today, growing with every component. The cost isn't the volume, it's that a misspelled sub-token produces a warning that reads identically to the 48 expected ones — the check's real value is buried by its own false positives.
Blanket suppression isn't what we want, and it isn't really available anyway: the sub-token check lives inside broken-ref alongside dangling-reference errors, so deselecting the rule through LintOptions.rules would silence genuine errors. Since 0.4.0 we post-process the report instead, matching finding.rule plus the token path against a list of extensions we declare ourselves — which works, but means we maintain the typo defense the linter could be providing.
What would solve it, in order of preference:
Any of these lets a project keep the typo detection instead of reimplementing it. Happy to send a PR in whichever direction you prefer.
Checked in 0.3.0 and 0.4.0: VALID_COMPONENT_SUB_TOKENS, the broken-ref implementation, and LintOptions are identical. loadSpecConfig(filePath?) accepts a path but getSpecConfig() calls it with no argument and it isn't exported. The 0.4.0 omitted: key covers section-level omissions, not sub-tokens.