Skip to content

Commit 77a1f04

Browse files
timphandevclaude
andcommitted
feat(native-recipe): warn about names that a config does not declare
The recipe creators warn, through @lynstack/recipe, about a default, a compound variant, or a style that names a variant, an option, or a slot that the config does not declare. Tests cover a config declared before the call, and a themed recipe, whose config function TypeScript checks no better and which warns once whatever the number of themes. The docs give the rule one place, "Names the config does not declare" in the createStyleRecipe reference. Typing recipes, the slot recipe reference, and the FAQ say it in one sentence and link to it, and the skill says to fix every such warning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2524eea commit 77a1f04

9 files changed

Lines changed: 161 additions & 25 deletions

File tree

‎docs/src/content/docs/native-recipe/create-slot-style-recipe.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ an object of styles keyed by slot name.
5353
| `cache` | Optional | Whether the recipe caches its styles. Defaults to `true`. With `false`, every call returns a new object, so leave it on in components. |
5454

5555
An option or a compound variant gives styles only to the slots it names.
56-
A style for a slot that `slots` does not declare is a type error.
56+
A style for a slot that `slots` does not declare is a type error. It is
57+
ignored, and the recipe warns about it when it is created (see
58+
[Names the config does not declare](/recipe/native-recipe/create-style-recipe/#names-the-config-does-not-declare)).
5759
Variants, defaults, boolean variants, and undeclared options work as in a
5860
style recipe (see [Variants](/recipe/native-recipe/variants/)).
5961

‎docs/src/content/docs/native-recipe/create-style-recipe.mdx‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,35 @@ recipe throws a `TypeError` that names what is wrong: no `variants`, a
5656
style that is not an object, or a compound variant without `variants` or
5757
`style`.
5858

59+
### Names the config does not declare
60+
61+
A default or a compound variant can name a variant or an option that the
62+
config does not declare, such as a misspelled one. TypeScript rejects it
63+
in a config written in the call, but a config declared before the call
64+
lets some through (see
65+
[A config declared before the call](/recipe/native-recipe/typescript/#a-config-declared-before-the-call)),
66+
and so does JavaScript. The recipe then adds no style for that name:
67+
68+
- A default of an undeclared variant, or an option that its variant does
69+
not declare, is ignored.
70+
- A compound variant's condition on an undeclared variant never matches.
71+
An undeclared option in its list is ignored, so a condition left
72+
without a declared option never matches either.
73+
- In a slot recipe, a style for a slot that `slots` does not name is
74+
ignored.
75+
76+
When it is created, the recipe warns about each such name with
77+
`console.warn`, in one message:
78+
79+
```text
80+
A recipe's config names variants, options, or slots that it does not declare, so they add nothing:
81+
- Compound variant 0 names the variant "tonne".
82+
```
83+
84+
The names of the recipes it composes count as declared. A recipe created
85+
again from a config with the same mistakes, such as a themed recipe for
86+
each theme, does not repeat the warning.
87+
5988
A style that never changes does not need a recipe, though: keep it in
6089
`StyleSheet.create` (see
6190
[Building components](/recipe/native-recipe/building-components/#keep-static-styles-in-stylesheetcreate)).
@@ -80,9 +109,10 @@ compoundVariants: [
80109
],
81110
```
82111

83-
The condition is checked after defaults apply. A compound variant that
84-
names an undeclared variant, or lists no declared option for a variant,
85-
never matches.
112+
The condition is checked after defaults apply. A condition on an
113+
undeclared variant never matches, and an undeclared option in a list is
114+
ignored; the recipe warns about both (see
115+
[Names the config does not declare](#names-the-config-does-not-declare)).
86116

87117
## The result
88118

‎docs/src/content/docs/native-recipe/faq.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ TypeScript reports `Property 'variants' is missing` (TS2741). In
2121
JavaScript, creating the recipe throws a `TypeError` that says so. See
2222
[createStyleRecipe](/recipe/native-recipe/create-style-recipe/#the-config).
2323

24+
## Why does the console warn that a recipe's config names what it does not declare?
25+
26+
A default, a compound variant, or a slot's style names a variant, an
27+
option, or a slot that the config does not declare, usually a typo in a
28+
config declared before the call, or returned by a themed recipe's
29+
function, where TypeScript does not catch every one. The recipe adds no
30+
style for that name. The message lists each one; fix the name, or
31+
declare it. See
32+
[Names the config does not declare](/recipe/native-recipe/create-style-recipe/#names-the-config-does-not-declare).
33+
2434
## Can I pass extra styles into a recipe call?
2535

2636
No. A recipe takes only variants. Merge extra styles in the `style` prop,

‎docs/src/content/docs/native-recipe/typescript.md‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ that exports its recipes, or sets `isolatedDeclarations`, also needs
1212

1313
## A config declared before the call
1414

15-
Declare a config before the call `as const`, so that the options named
16-
in its compound and default variants stay literal types. This holds for the `slots` of a slot recipe
17-
too: a list declared before the call without `as const` is a `string[]`,
18-
so the slot recipe takes any slot name, and a misspelled slot in `base`,
19-
in the variants, or in the result is not an error.
20-
21-
TypeScript checks a declared config less than one written in the call:
22-
it reports a name that a type does not declare only in an object written
23-
where that type is expected. In a declared config, a misspelled variant
15+
Declare a config before the call `as const`, so that the options named in
16+
its compound and default variants stay literal types. The same holds for
17+
the `slots` of a slot recipe: a list declared without `as const` is a
18+
`string[]`, so the slot recipe takes any slot name, and a misspelled slot
19+
in `base`, in the variants, or in the result is not a type error.
20+
21+
TypeScript also checks a declared config less than one written in the
22+
call, since it reports a name that a type does not declare only in an
23+
object written where that type is expected. In a declared config, and in
24+
the config that a themed recipe's function returns, a misspelled variant
2425
name next to a correct one in the variants that a compound variant names
25-
is not an error, and the compound variant never applies. Write the
26-
config in the call when nothing needs it declared, and test the styles of
27-
a declared config.
26+
is not a type error. The recipe warns about it when it is created, and
27+
the compound variant never matches; see
28+
[Names the config does not declare](/recipe/native-recipe/create-style-recipe/#names-the-config-does-not-declare).
2829

2930
## Checked styles
3031

‎packages/native-recipe/CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ published on npm and as a
55
[GitHub release](https://github.com/lynstack/recipe/releases) tagged
66
`native-recipe@<version>`.
77

8+
## Unreleased
9+
10+
- The recipe creators warn, once, with `console.warn`, about a default, a
11+
compound variant, or a style that names a variant, an option, or a slot
12+
that the config does not declare, such as a misspelled name in a config
13+
declared before the call or returned by a themed recipe's function,
14+
which TypeScript does not always report. Such a name still adds no
15+
style.
16+
817
## 1.6.0 — 2026-10-08
918

1019
A library of components can export and wrap its recipes: their

‎packages/native-recipe/skills/native-recipe/SKILL.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ type BadgeProps = VariantsOf<typeof badge>; // also works on themed recipes
9898
the variants' styles.
9999
- A recipe ignores props that are not variants, so it can take all of a
100100
component's props.
101-
- Write the config inside the call. A config declared before the call
102-
must be `as const`, and TypeScript checks it less: a misspelled variant
103-
name next to a correct one in a compound variant is not an error.
101+
- Write the config inside the call, where TypeScript checks every name.
102+
A config declared before the call must be `as const`, and TypeScript
103+
lets some misspelled names through; the recipe then warns, when it is
104+
created, that its config names what it does not declare. Fix every
105+
such warning.
104106
- `composes: [other]` adds the config of another recipe of the same kind
105107
(style recipe or slot style recipe) before its own; see
106108
[Compose a shared recipe](#compose-a-shared-recipe-instead-of-copying-its-config).
@@ -332,4 +334,5 @@ no variants.
332334
size that a token exists for?
333335
- Is every theme object created once, or memoized, and passed unchanged?
334336
- Is any config declared before its call where it could be written in
335-
the call, or declared without `as const`?
337+
the call, or declared without `as const`? Does creating any recipe warn
338+
that its config names what it does not declare?

‎packages/native-recipe/src/slot-style-recipe.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ type ComposedSlotStyleRecipe<
172172
* variant, whose only options are `"true"` and `"false"` and which defaults
173173
* to `false`. An option that the config does not declare adds no style, and
174174
* its styles are built on every call. Properties of the selection that are
175-
* not variants are ignored. Its `variantKeys`, `variantOptions`, and
175+
* not variants are ignored. Creating it warns once, with `console.warn`,
176+
* about a default, a compound variant, or a style that names a variant, an
177+
* option, or a slot that the config does not declare, which adds no style.
178+
* Its `variantKeys`, `variantOptions`, and
176179
* `defaultVariants` properties list its variants as those of
177180
* `createStyleRecipe` do.
178181
*

‎packages/native-recipe/src/style-recipe.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@ type ComposedStyleRecipe<
185185
* only options are `"true"` and `"false"` and which defaults to `false`. An
186186
* option that the config does not declare adds no style, and its style is
187187
* built on every call. Properties of the selection that are not variants
188-
* are ignored. The recipe's `variantKeys` property lists the names of its
189-
* variants, `variantOptions` the names of the options of each, and
190-
* `defaultVariants` the option each uses when the recipe is called without
191-
* it.
188+
* are ignored. Creating the recipe warns once, with `console.warn`, about a
189+
* default or a compound variant that names a variant or an option that the
190+
* config does not declare, which adds no style. The recipe's `variantKeys`
191+
* property lists the names of its variants, `variantOptions` the names of
192+
* the options of each, and `defaultVariants` the option each uses when the
193+
* recipe is called without it.
192194
*
193195
* A recipe composes the recipes listed in its config's `composes` as if
194196
* their configs and its own were one: their base styles first, then the
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { createStyleRecipe } from "./style-recipe.js";
4+
import { createThemedRecipes } from "./themed-recipes.js";
5+
6+
const heading =
7+
"A recipe's config names variants, options, or slots that it does not declare, so they add nothing:";
8+
9+
/** Returns what `create` warns about, without printing it. */
10+
function warningsOf(create: () => unknown): readonly string[] {
11+
const messages: string[] = [];
12+
const warn = vi
13+
.spyOn(console, "warn")
14+
.mockImplementation((message: unknown) => {
15+
messages.push(String(message));
16+
});
17+
try {
18+
create();
19+
} finally {
20+
warn.mockRestore();
21+
}
22+
return messages;
23+
}
24+
25+
interface Theme {
26+
readonly gap: number;
27+
}
28+
29+
describe("a config declared before the call", () => {
30+
it("warns about a compound variant that names an undeclared variant", () => {
31+
const boxConfig = {
32+
compoundVariants: [
33+
{
34+
style: { borderWidth: 2 },
35+
variants: { size: "sm", tonne: "danger" },
36+
},
37+
],
38+
variants: {
39+
size: { md: { padding: 8 }, sm: { padding: 4 } },
40+
tone: { danger: { opacity: 1 }, neutral: { opacity: 0.6 } },
41+
},
42+
} as const;
43+
44+
expect(warningsOf(() => createStyleRecipe(boxConfig))).toStrictEqual([
45+
`${heading}\n- Compound variant 0 names the variant "tonne".`,
46+
]);
47+
expect(
48+
createStyleRecipe(boxConfig)({ size: "sm", tone: "danger" }),
49+
).toStrictEqual({ opacity: 1, padding: 4 });
50+
});
51+
});
52+
53+
describe("a themed recipe", () => {
54+
it("warns once, whatever the number of themes", () => {
55+
const { createStyleRecipe: createThemedStyleRecipe } =
56+
createThemedRecipes<Theme>();
57+
const chip = createThemedStyleRecipe((theme) => ({
58+
compoundVariants: [
59+
{ style: { borderWidth: 1 }, variants: { dense: true, sise: "sm" } },
60+
],
61+
variants: {
62+
dense: { true: { padding: theme.gap } },
63+
size: { sm: { height: 24 } },
64+
},
65+
}));
66+
67+
expect(
68+
warningsOf(() => [
69+
chip({ gap: 4 }, { size: "sm" }),
70+
chip({ gap: 8 }, { size: "sm" }),
71+
]),
72+
).toStrictEqual([
73+
`${heading}\n- Compound variant 0 names the variant "sise".`,
74+
]);
75+
});
76+
});

0 commit comments

Comments
 (0)