@bamboocss/generator 1.33.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -111,6 +111,7 @@ function generateConditions(ctx) {
111
111
  function generateCssFn(ctx) {
112
112
  const { utility, hash, prefix } = ctx;
113
113
  const { separator } = utility;
114
+ const leafFallback = ctx.config.leafFallback ?? true;
114
115
  return {
115
116
  dts: outdent.outdent`
116
117
  ${ctx.file.importType("SystemStyleObject, ViewTransitionFn", "../types/index")}
@@ -204,12 +205,22 @@ function generateCssFn(ctx) {
204
205
 
205
206
  // Emitted for the source transform, which rewrites a single dynamic style leaf into a
206
207
  // call to this rather than leaving a \`css()\` behind. \`prefix\` is the class up to the
207
- // value, resolved at build time; \`prop\` is only used for the shapes \`leafClass\`
208
- // declines, which have to run the real thing.
209
- export const cssLeaf = (prefix, prop, value) => {
208
+ // value, resolved at build time; \`prop\` names the property for the shapes \`leafClass\`
209
+ // declines.
210
+ ${leafFallback ? `export const cssLeaf = (prefix, prop, value) => {
210
211
  const className = leafClass(prefix, value)
211
212
  return className === undefined ? css({ [prop]: value }) : className
212
- }
213
+ }` : `export const cssLeaf = (prefix, prop, value) => {
214
+ const className = leafClass(prefix, value)
215
+ if (className === undefined) {
216
+ throw new Error(
217
+ \`[bamboocss] \\\`\${prop}\\\` got a \${Array.isArray(value) ? 'responsive array' : 'condition object'} from a \` +
218
+ \`runtime value, and \\\`leafFallback\\\` is off. Write the conditions as a literal at the call site, so \` +
219
+ \`the build resolves each branch, or set \\\`leafFallback: true\\\` to keep the runtime.\`,
220
+ )
221
+ }
222
+ return className
223
+ }`}
213
224
 
214
225
  // Sugar for the string form, so the feature has an import to discover, a signature to
215
226
  // hover and a name the editor can complete. The extractor evaluates the call, so the
@@ -2353,13 +2364,36 @@ function generatePropTypes(ctx) {
2353
2364
  return outdent.outdent`
2354
2365
  ${result.join("\n")}
2355
2366
 
2356
- type WithColorOpacityModifier<T> = [T] extends [string] ? \`$\{T}/\${string}\` & { __colorOpacityModifier?: true } : never
2357
-
2358
2367
  type ImportantMark = "!" | "!important"
2359
2368
  type WhitespaceImportant = \` \${ImportantMark}\`
2360
2369
  type Important = ImportantMark | WhitespaceImportant
2361
2370
  type WithImportant<T> = [T] extends [string] ? \`\${T}\${Important}\` & { __important?: true } : never
2362
2371
 
2372
+ /**
2373
+ * The modifiers a token path may carry, as one open-ended tail rather than one closed
2374
+ * template per form.
2375
+ *
2376
+ * ⚠️ The \`& { __modifier?: true }\` is load-bearing, and nothing reads it. Deleting it as
2377
+ * dead weight costs **12.8x** on \`tsc\` — measured at 87.2s against 6.8s over 4,000 call
2378
+ * sites — because it is what stops TypeScript attempting subtype reduction across the
2379
+ * union these expand into. The same applies to \`__important\` above.
2380
+ *
2381
+ * A template literal distributes over a union in any placeholder, so \`\${T}\` against a
2382
+ * 258-token colour palette is 258 members, and the old \`\${T}\${Important}\` was four times
2383
+ * that. Between them the two modifier forms were 5N of a ~1,560-member union for \`color\`
2384
+ * alone, and half the cost of type-checking a \`css()\` call under \`strictTokens\`. Folding
2385
+ * them into one 3N tail is 14.5% off that — 7.09s against 8.29s over the same 4,000 call
2386
+ * sites, with a control repeat agreeing to 3.5%.
2387
+ *
2388
+ * What it gives up is the tail: \`red.300!nonsense\` type-checks now, where five exact
2389
+ * templates would have rejected it. \`unresolvedToken\` strips the mark and resolves the
2390
+ * path underneath, so the build still reports it — warning by default, failing under
2391
+ * \`'error'\`. The diagnostic moves rather than disappears, and only for a value nobody
2392
+ * writes on purpose.
2393
+ */
2394
+ type Modifier = "/" | "!" | " !"
2395
+ type WithModifier<T> = [T] extends [string] ? \`\${T}\${Modifier}\${string}\` & { __modifier?: true } : never
2396
+
2363
2397
  /**
2364
2398
  * A list of candidate values, most-preferred first, emitted as repeated declarations so the
2365
2399
  * browser keeps the last one it understands.
@@ -2391,8 +2425,7 @@ function generatePropTypes(ctx) {
2391
2425
  | \`[\${string}]\`
2392
2426
  | FallbackValue
2393
2427
  | WithImportant<FallbackValue>
2394
- | WithColorOpacityModifier<T>
2395
- | WithImportant<T>
2428
+ | WithModifier<T>
2396
2429
 
2397
2430
  /**
2398
2431
  * Will restrict the value of properties that have predefined values to those values only.
@@ -3934,8 +3967,21 @@ var Generator = class extends _bamboocss_core.Context {
3934
3967
  * unused for want of a utility to reference it.
3935
3968
  *
3936
3969
  * `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
3970
+ *
3971
+ * `reachableVars` is `pruneTokens`' answer about custom properties, which has to be handed
3972
+ * over rather than re-derived here. A token kept by a reader outside the stylesheet — a
3973
+ * `token()` call, a `prune.keepTokens` pattern, a theme, a `globalCss` export — is
3974
+ * reachable to that pass and invisible to this one, so deriving it again from the css
3975
+ * deletes the `@keyframes` out from under a declaration that ships. Every caller that
3976
+ * prunes both hands it over.
3977
+ *
3978
+ * Omitting it falls back to what `prune.tokens` implies. Under `off` that is `'all'`: no
3979
+ * token declaration is removable, so each one ships and keeps the keyframe it names —
3980
+ * and `off` is precisely the setting chosen because something outside the stylesheet
3981
+ * reads them. Otherwise it is the css alone, which is what a caller running this pass
3982
+ * without the other one is asking for.
3937
3983
  */
3938
- pruneKeyframes = (sheet, keep) => {
3984
+ pruneKeyframes = (sheet, keep, reachableVars) => {
3939
3985
  if (!this.config.prune?.keyframes) return;
3940
3986
  const layers = sheet.layers;
3941
3987
  const keyframeNames = new Set(Object.keys(this.config.theme?.keyframes ?? {}));
@@ -3953,7 +3999,8 @@ var Generator = class extends _bamboocss_core.Context {
3953
3999
  ],
3954
4000
  target: layers.tokens,
3955
4001
  keyframeNames,
3956
- keep: new Set([...this.getThemeKeyframeNames(keyframeNames), ...keep ?? []])
4002
+ keep: new Set([...this.getThemeKeyframeNames(keyframeNames), ...keep ?? []]),
4003
+ reachableVars: reachableVars ?? ((this.config.prune?.tokens ?? "reachable") === "off" ? "all" : void 0)
3957
4004
  });
3958
4005
  _bamboocss_logger.logger.debug("prune:keyframes", `Removed ${result.removed} unused keyframe(s)`);
3959
4006
  return result;
@@ -4041,9 +4088,79 @@ var Generator = class extends _bamboocss_core.Context {
4041
4088
  artifact: "styles.css",
4042
4089
  content: css
4043
4090
  }) ?? css;
4091
+ this.assertNoUnresolvedTokens();
4044
4092
  return css;
4045
4093
  };
4046
4094
  /**
4095
+ * Fail on a style value shaped like a token path that names no token.
4096
+ *
4097
+ * Only under `unresolvedToken: 'error'` — see that option for why this one is graded and a
4098
+ * dead binding is not.
4099
+ *
4100
+ * Two sources, because neither sees the whole build.
4101
+ *
4102
+ * **Atomic styles are read off the decoded sheet** rather than accumulated as `transform`
4103
+ * runs, and that is the load-bearing part for them. A `Context` outlives rebuilds while the
4104
+ * decoder memoizes each atom by hash, so on the second build of the same source `transform`
4105
+ * is never re-entered: an accumulating record either keeps a finding past the edit that
4106
+ * fixed it — wedging a dev server — or is cleared and then never refilled, which passes a
4107
+ * build whose source is still broken. That second one is the worse failure and is what an
4108
+ * earlier version of this did.
4109
+ *
4110
+ * `decoder.atomic` has neither problem, because it is not a record of what happened — it is
4111
+ * what the sheet is built from, and each result keeps the `prop` and `value` it was written
4112
+ * with. So the question asked is the one that matters: does the stylesheet *being emitted*
4113
+ * contain a declaration the browser will drop.
4114
+ *
4115
+ * Within a watch process that set is cumulative, and so is the css: extraction is additive,
4116
+ * so the rule for a style deleted from source is still in the sheet until the process
4117
+ * restarts. This reports the same way for the same reason — the declaration really is still
4118
+ * in the file being written, and saying otherwise would be a check that disagreed with its
4119
+ * own output. A production build is a fresh process and sees only what its source asked
4120
+ * for.
4121
+ *
4122
+ * **Config-derived styles are not in that set at all**, which is the gap this used to have.
4123
+ * `globalCss`, the reset, config recipes and compositions serialize through
4124
+ * `transformStyles`, and that clones the decoder — so their atoms land in a throwaway and
4125
+ * `decoder.atomic` never hears about them. Reading only the sheet made `'error'` *quieter*
4126
+ * than the default on exactly those styles: the warning was suppressed in favour of a check
4127
+ * that could not see them, so a bad token in `globalCss` warned with the option unset and
4128
+ * then passed silently with it set to `'error'`. `utility.unresolvedTokens` is the record of
4129
+ * what only `transform` can see; see it for why accumulating is right for that half.
4130
+ *
4131
+ * Both halves key on `property:path` with shorthands resolved, so a value that does reach
4132
+ * both — every atomic style is transformed once before it is memoized — is one finding.
4133
+ *
4134
+ * Here rather than beside the asserts in `BambooContext` because this is where the sheet
4135
+ * exists: those all run during extraction, before anything has been decoded. Every path
4136
+ * that emits css comes through `getCss`.
4137
+ */
4138
+ assertNoUnresolvedTokens = () => {
4139
+ if (this.config.unresolvedToken !== "error") return;
4140
+ const found = new Map(this.utility.unresolvedTokens);
4141
+ for (const atom of this.decoder.atomic) {
4142
+ const { prop, value } = atom.entry;
4143
+ if (typeof value !== "string" || typeof prop !== "string") continue;
4144
+ if (!this.utility.isUnresolvedTokenValue(prop, value)) continue;
4145
+ const key = this.utility.resolveShorthand(prop);
4146
+ const bare = this.utility.bareTokenPath(key, value);
4147
+ found.set(`${key}:${bare}`, {
4148
+ prop: key,
4149
+ value: bare,
4150
+ category: this.utility.getTokenCategory(key)
4151
+ });
4152
+ }
4153
+ if (!found.size) return;
4154
+ const detail = (0, _bamboocss_shared.truncateList)(Array.from(found.values(), ({ prop, value, category }) => {
4155
+ return `- \`${prop}: ${value}\`.${category ? ` Check the path against your \`${category}\` tokens.` : ""}`;
4156
+ }), {
4157
+ limit: 25,
4158
+ unit: "value",
4159
+ separator: "\n"
4160
+ });
4161
+ throw new _bamboocss_shared.BambooError("UNRESOLVED_TOKEN", `${found.size} style value(s) name a token that does not exist:\n\n${detail}\n\nEach is emitted as written, which parses — so the stylesheet is valid and nothing downstream objects. The browser drops the declaration at compute time and the style is simply absent from the element, which surfaces as "this never applied" a long way from the typo that caused it. Write \`[value]\` to mark one as a literal, or set \`unresolvedToken: 'warn'\` to report these without failing.`);
4162
+ };
4163
+ /**
4047
4164
  * Get CSS for a specific layer from the stylesheet
4048
4165
  */
4049
4166
  getLayerCss = (sheet, layer) => {
package/dist/index.d.cts CHANGED
@@ -37,10 +37,7 @@ declare class Generator extends Context {
37
37
  * `keep` carries references this cannot see for itself; see `collectTokenReferences`.
38
38
  */
39
39
  pruneTokens: (sheet: Stylesheet, keep?: Set<string>, tokensReachableFromJs?: boolean) => {
40
- removed: number;
41
- kept: number;
42
- removedProperties?: undefined;
43
- } | {
40
+ reachable: Set<string> | undefined;
44
41
  removed: number;
45
42
  removedProperties: number;
46
43
  kept: number;
@@ -63,8 +60,21 @@ declare class Generator extends Context {
63
60
  * unused for want of a utility to reference it.
64
61
  *
65
62
  * `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
63
+ *
64
+ * `reachableVars` is `pruneTokens`' answer about custom properties, which has to be handed
65
+ * over rather than re-derived here. A token kept by a reader outside the stylesheet — a
66
+ * `token()` call, a `prune.keepTokens` pattern, a theme, a `globalCss` export — is
67
+ * reachable to that pass and invisible to this one, so deriving it again from the css
68
+ * deletes the `@keyframes` out from under a declaration that ships. Every caller that
69
+ * prunes both hands it over.
70
+ *
71
+ * Omitting it falls back to what `prune.tokens` implies. Under `off` that is `'all'`: no
72
+ * token declaration is removable, so each one ships and keeps the keyframe it names —
73
+ * and `off` is precisely the setting chosen because something outside the stylesheet
74
+ * reads them. Otherwise it is the css alone, which is what a caller running this pass
75
+ * without the other one is asking for.
66
76
  */
67
- pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>) => {
77
+ pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>, reachableVars?: Set<string> | "all") => {
68
78
  removed: number;
69
79
  kept: number;
70
80
  } | undefined;
@@ -119,6 +129,51 @@ declare class Generator extends Context {
119
129
  private getAlwaysKeptTokenVars;
120
130
  getParserCss: (decoder: StyleDecoder) => string;
121
131
  getCss: (stylesheet?: Stylesheet) => string;
132
+ /**
133
+ * Fail on a style value shaped like a token path that names no token.
134
+ *
135
+ * Only under `unresolvedToken: 'error'` — see that option for why this one is graded and a
136
+ * dead binding is not.
137
+ *
138
+ * Two sources, because neither sees the whole build.
139
+ *
140
+ * **Atomic styles are read off the decoded sheet** rather than accumulated as `transform`
141
+ * runs, and that is the load-bearing part for them. A `Context` outlives rebuilds while the
142
+ * decoder memoizes each atom by hash, so on the second build of the same source `transform`
143
+ * is never re-entered: an accumulating record either keeps a finding past the edit that
144
+ * fixed it — wedging a dev server — or is cleared and then never refilled, which passes a
145
+ * build whose source is still broken. That second one is the worse failure and is what an
146
+ * earlier version of this did.
147
+ *
148
+ * `decoder.atomic` has neither problem, because it is not a record of what happened — it is
149
+ * what the sheet is built from, and each result keeps the `prop` and `value` it was written
150
+ * with. So the question asked is the one that matters: does the stylesheet *being emitted*
151
+ * contain a declaration the browser will drop.
152
+ *
153
+ * Within a watch process that set is cumulative, and so is the css: extraction is additive,
154
+ * so the rule for a style deleted from source is still in the sheet until the process
155
+ * restarts. This reports the same way for the same reason — the declaration really is still
156
+ * in the file being written, and saying otherwise would be a check that disagreed with its
157
+ * own output. A production build is a fresh process and sees only what its source asked
158
+ * for.
159
+ *
160
+ * **Config-derived styles are not in that set at all**, which is the gap this used to have.
161
+ * `globalCss`, the reset, config recipes and compositions serialize through
162
+ * `transformStyles`, and that clones the decoder — so their atoms land in a throwaway and
163
+ * `decoder.atomic` never hears about them. Reading only the sheet made `'error'` *quieter*
164
+ * than the default on exactly those styles: the warning was suppressed in favour of a check
165
+ * that could not see them, so a bad token in `globalCss` warned with the option unset and
166
+ * then passed silently with it set to `'error'`. `utility.unresolvedTokens` is the record of
167
+ * what only `transform` can see; see it for why accumulating is right for that half.
168
+ *
169
+ * Both halves key on `property:path` with shorthands resolved, so a value that does reach
170
+ * both — every atomic style is transformed once before it is memoized — is one finding.
171
+ *
172
+ * Here rather than beside the asserts in `BambooContext` because this is where the sheet
173
+ * exists: those all run during extraction, before anything has been decoded. Every path
174
+ * that emits css comes through `getCss`.
175
+ */
176
+ assertNoUnresolvedTokens: () => void;
122
177
  /**
123
178
  * Get CSS for a specific layer from the stylesheet
124
179
  */
package/dist/index.d.mts CHANGED
@@ -37,10 +37,7 @@ declare class Generator extends Context {
37
37
  * `keep` carries references this cannot see for itself; see `collectTokenReferences`.
38
38
  */
39
39
  pruneTokens: (sheet: Stylesheet, keep?: Set<string>, tokensReachableFromJs?: boolean) => {
40
- removed: number;
41
- kept: number;
42
- removedProperties?: undefined;
43
- } | {
40
+ reachable: Set<string> | undefined;
44
41
  removed: number;
45
42
  removedProperties: number;
46
43
  kept: number;
@@ -63,8 +60,21 @@ declare class Generator extends Context {
63
60
  * unused for want of a utility to reference it.
64
61
  *
65
62
  * `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
63
+ *
64
+ * `reachableVars` is `pruneTokens`' answer about custom properties, which has to be handed
65
+ * over rather than re-derived here. A token kept by a reader outside the stylesheet — a
66
+ * `token()` call, a `prune.keepTokens` pattern, a theme, a `globalCss` export — is
67
+ * reachable to that pass and invisible to this one, so deriving it again from the css
68
+ * deletes the `@keyframes` out from under a declaration that ships. Every caller that
69
+ * prunes both hands it over.
70
+ *
71
+ * Omitting it falls back to what `prune.tokens` implies. Under `off` that is `'all'`: no
72
+ * token declaration is removable, so each one ships and keeps the keyframe it names —
73
+ * and `off` is precisely the setting chosen because something outside the stylesheet
74
+ * reads them. Otherwise it is the css alone, which is what a caller running this pass
75
+ * without the other one is asking for.
66
76
  */
67
- pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>) => {
77
+ pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>, reachableVars?: Set<string> | "all") => {
68
78
  removed: number;
69
79
  kept: number;
70
80
  } | undefined;
@@ -119,6 +129,51 @@ declare class Generator extends Context {
119
129
  private getAlwaysKeptTokenVars;
120
130
  getParserCss: (decoder: StyleDecoder) => string;
121
131
  getCss: (stylesheet?: Stylesheet) => string;
132
+ /**
133
+ * Fail on a style value shaped like a token path that names no token.
134
+ *
135
+ * Only under `unresolvedToken: 'error'` — see that option for why this one is graded and a
136
+ * dead binding is not.
137
+ *
138
+ * Two sources, because neither sees the whole build.
139
+ *
140
+ * **Atomic styles are read off the decoded sheet** rather than accumulated as `transform`
141
+ * runs, and that is the load-bearing part for them. A `Context` outlives rebuilds while the
142
+ * decoder memoizes each atom by hash, so on the second build of the same source `transform`
143
+ * is never re-entered: an accumulating record either keeps a finding past the edit that
144
+ * fixed it — wedging a dev server — or is cleared and then never refilled, which passes a
145
+ * build whose source is still broken. That second one is the worse failure and is what an
146
+ * earlier version of this did.
147
+ *
148
+ * `decoder.atomic` has neither problem, because it is not a record of what happened — it is
149
+ * what the sheet is built from, and each result keeps the `prop` and `value` it was written
150
+ * with. So the question asked is the one that matters: does the stylesheet *being emitted*
151
+ * contain a declaration the browser will drop.
152
+ *
153
+ * Within a watch process that set is cumulative, and so is the css: extraction is additive,
154
+ * so the rule for a style deleted from source is still in the sheet until the process
155
+ * restarts. This reports the same way for the same reason — the declaration really is still
156
+ * in the file being written, and saying otherwise would be a check that disagreed with its
157
+ * own output. A production build is a fresh process and sees only what its source asked
158
+ * for.
159
+ *
160
+ * **Config-derived styles are not in that set at all**, which is the gap this used to have.
161
+ * `globalCss`, the reset, config recipes and compositions serialize through
162
+ * `transformStyles`, and that clones the decoder — so their atoms land in a throwaway and
163
+ * `decoder.atomic` never hears about them. Reading only the sheet made `'error'` *quieter*
164
+ * than the default on exactly those styles: the warning was suppressed in favour of a check
165
+ * that could not see them, so a bad token in `globalCss` warned with the option unset and
166
+ * then passed silently with it set to `'error'`. `utility.unresolvedTokens` is the record of
167
+ * what only `transform` can see; see it for why accumulating is right for that half.
168
+ *
169
+ * Both halves key on `property:path` with shorthands resolved, so a value that does reach
170
+ * both — every atomic style is transformed once before it is memoized — is one finding.
171
+ *
172
+ * Here rather than beside the asserts in `BambooContext` because this is where the sheet
173
+ * exists: those all run during extraction, before anything has been decoded. Every path
174
+ * that emits css comes through `getCss`.
175
+ */
176
+ assertNoUnresolvedTokens: () => void;
122
177
  /**
123
178
  * Get CSS for a specific layer from the stylesheet
124
179
  */
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, Recipes, expandNestedCss, extractParentSelectors, extractTrailingPseudos, pruneKeyframes, prunePreflight, pruneTokenVars, prunesPreflight, stringify } from "@bamboocss/core";
2
2
  import { logger } from "@bamboocss/logger";
3
- import { BambooError, capitalize, compact, cssVarRefs, dashCase, isBoolean, isObject, mapEntries, unionType, walkObject } from "@bamboocss/shared";
3
+ import { BambooError, capitalize, compact, cssVarRefs, dashCase, isBoolean, isObject, mapEntries, truncateList, unionType, walkObject } from "@bamboocss/shared";
4
4
  import { match } from "ts-pattern";
5
5
  import outdent$1, { outdent } from "outdent";
6
6
  import { stringify as stringify$1 } from "javascript-stringify";
@@ -85,6 +85,7 @@ function generateConditions(ctx) {
85
85
  function generateCssFn(ctx) {
86
86
  const { utility, hash, prefix } = ctx;
87
87
  const { separator } = utility;
88
+ const leafFallback = ctx.config.leafFallback ?? true;
88
89
  return {
89
90
  dts: outdent`
90
91
  ${ctx.file.importType("SystemStyleObject, ViewTransitionFn", "../types/index")}
@@ -178,12 +179,22 @@ function generateCssFn(ctx) {
178
179
 
179
180
  // Emitted for the source transform, which rewrites a single dynamic style leaf into a
180
181
  // call to this rather than leaving a \`css()\` behind. \`prefix\` is the class up to the
181
- // value, resolved at build time; \`prop\` is only used for the shapes \`leafClass\`
182
- // declines, which have to run the real thing.
183
- export const cssLeaf = (prefix, prop, value) => {
182
+ // value, resolved at build time; \`prop\` names the property for the shapes \`leafClass\`
183
+ // declines.
184
+ ${leafFallback ? `export const cssLeaf = (prefix, prop, value) => {
184
185
  const className = leafClass(prefix, value)
185
186
  return className === undefined ? css({ [prop]: value }) : className
186
- }
187
+ }` : `export const cssLeaf = (prefix, prop, value) => {
188
+ const className = leafClass(prefix, value)
189
+ if (className === undefined) {
190
+ throw new Error(
191
+ \`[bamboocss] \\\`\${prop}\\\` got a \${Array.isArray(value) ? 'responsive array' : 'condition object'} from a \` +
192
+ \`runtime value, and \\\`leafFallback\\\` is off. Write the conditions as a literal at the call site, so \` +
193
+ \`the build resolves each branch, or set \\\`leafFallback: true\\\` to keep the runtime.\`,
194
+ )
195
+ }
196
+ return className
197
+ }`}
187
198
 
188
199
  // Sugar for the string form, so the feature has an import to discover, a signature to
189
200
  // hover and a name the editor can complete. The extractor evaluates the call, so the
@@ -2327,13 +2338,36 @@ function generatePropTypes(ctx) {
2327
2338
  return outdent`
2328
2339
  ${result.join("\n")}
2329
2340
 
2330
- type WithColorOpacityModifier<T> = [T] extends [string] ? \`$\{T}/\${string}\` & { __colorOpacityModifier?: true } : never
2331
-
2332
2341
  type ImportantMark = "!" | "!important"
2333
2342
  type WhitespaceImportant = \` \${ImportantMark}\`
2334
2343
  type Important = ImportantMark | WhitespaceImportant
2335
2344
  type WithImportant<T> = [T] extends [string] ? \`\${T}\${Important}\` & { __important?: true } : never
2336
2345
 
2346
+ /**
2347
+ * The modifiers a token path may carry, as one open-ended tail rather than one closed
2348
+ * template per form.
2349
+ *
2350
+ * ⚠️ The \`& { __modifier?: true }\` is load-bearing, and nothing reads it. Deleting it as
2351
+ * dead weight costs **12.8x** on \`tsc\` — measured at 87.2s against 6.8s over 4,000 call
2352
+ * sites — because it is what stops TypeScript attempting subtype reduction across the
2353
+ * union these expand into. The same applies to \`__important\` above.
2354
+ *
2355
+ * A template literal distributes over a union in any placeholder, so \`\${T}\` against a
2356
+ * 258-token colour palette is 258 members, and the old \`\${T}\${Important}\` was four times
2357
+ * that. Between them the two modifier forms were 5N of a ~1,560-member union for \`color\`
2358
+ * alone, and half the cost of type-checking a \`css()\` call under \`strictTokens\`. Folding
2359
+ * them into one 3N tail is 14.5% off that — 7.09s against 8.29s over the same 4,000 call
2360
+ * sites, with a control repeat agreeing to 3.5%.
2361
+ *
2362
+ * What it gives up is the tail: \`red.300!nonsense\` type-checks now, where five exact
2363
+ * templates would have rejected it. \`unresolvedToken\` strips the mark and resolves the
2364
+ * path underneath, so the build still reports it — warning by default, failing under
2365
+ * \`'error'\`. The diagnostic moves rather than disappears, and only for a value nobody
2366
+ * writes on purpose.
2367
+ */
2368
+ type Modifier = "/" | "!" | " !"
2369
+ type WithModifier<T> = [T] extends [string] ? \`\${T}\${Modifier}\${string}\` & { __modifier?: true } : never
2370
+
2337
2371
  /**
2338
2372
  * A list of candidate values, most-preferred first, emitted as repeated declarations so the
2339
2373
  * browser keeps the last one it understands.
@@ -2365,8 +2399,7 @@ function generatePropTypes(ctx) {
2365
2399
  | \`[\${string}]\`
2366
2400
  | FallbackValue
2367
2401
  | WithImportant<FallbackValue>
2368
- | WithColorOpacityModifier<T>
2369
- | WithImportant<T>
2402
+ | WithModifier<T>
2370
2403
 
2371
2404
  /**
2372
2405
  * Will restrict the value of properties that have predefined values to those values only.
@@ -3908,8 +3941,21 @@ var Generator = class extends Context {
3908
3941
  * unused for want of a utility to reference it.
3909
3942
  *
3910
3943
  * `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
3944
+ *
3945
+ * `reachableVars` is `pruneTokens`' answer about custom properties, which has to be handed
3946
+ * over rather than re-derived here. A token kept by a reader outside the stylesheet — a
3947
+ * `token()` call, a `prune.keepTokens` pattern, a theme, a `globalCss` export — is
3948
+ * reachable to that pass and invisible to this one, so deriving it again from the css
3949
+ * deletes the `@keyframes` out from under a declaration that ships. Every caller that
3950
+ * prunes both hands it over.
3951
+ *
3952
+ * Omitting it falls back to what `prune.tokens` implies. Under `off` that is `'all'`: no
3953
+ * token declaration is removable, so each one ships and keeps the keyframe it names —
3954
+ * and `off` is precisely the setting chosen because something outside the stylesheet
3955
+ * reads them. Otherwise it is the css alone, which is what a caller running this pass
3956
+ * without the other one is asking for.
3911
3957
  */
3912
- pruneKeyframes = (sheet, keep) => {
3958
+ pruneKeyframes = (sheet, keep, reachableVars) => {
3913
3959
  if (!this.config.prune?.keyframes) return;
3914
3960
  const layers = sheet.layers;
3915
3961
  const keyframeNames = new Set(Object.keys(this.config.theme?.keyframes ?? {}));
@@ -3927,7 +3973,8 @@ var Generator = class extends Context {
3927
3973
  ],
3928
3974
  target: layers.tokens,
3929
3975
  keyframeNames,
3930
- keep: new Set([...this.getThemeKeyframeNames(keyframeNames), ...keep ?? []])
3976
+ keep: new Set([...this.getThemeKeyframeNames(keyframeNames), ...keep ?? []]),
3977
+ reachableVars: reachableVars ?? ((this.config.prune?.tokens ?? "reachable") === "off" ? "all" : void 0)
3931
3978
  });
3932
3979
  logger.debug("prune:keyframes", `Removed ${result.removed} unused keyframe(s)`);
3933
3980
  return result;
@@ -4015,9 +4062,79 @@ var Generator = class extends Context {
4015
4062
  artifact: "styles.css",
4016
4063
  content: css
4017
4064
  }) ?? css;
4065
+ this.assertNoUnresolvedTokens();
4018
4066
  return css;
4019
4067
  };
4020
4068
  /**
4069
+ * Fail on a style value shaped like a token path that names no token.
4070
+ *
4071
+ * Only under `unresolvedToken: 'error'` — see that option for why this one is graded and a
4072
+ * dead binding is not.
4073
+ *
4074
+ * Two sources, because neither sees the whole build.
4075
+ *
4076
+ * **Atomic styles are read off the decoded sheet** rather than accumulated as `transform`
4077
+ * runs, and that is the load-bearing part for them. A `Context` outlives rebuilds while the
4078
+ * decoder memoizes each atom by hash, so on the second build of the same source `transform`
4079
+ * is never re-entered: an accumulating record either keeps a finding past the edit that
4080
+ * fixed it — wedging a dev server — or is cleared and then never refilled, which passes a
4081
+ * build whose source is still broken. That second one is the worse failure and is what an
4082
+ * earlier version of this did.
4083
+ *
4084
+ * `decoder.atomic` has neither problem, because it is not a record of what happened — it is
4085
+ * what the sheet is built from, and each result keeps the `prop` and `value` it was written
4086
+ * with. So the question asked is the one that matters: does the stylesheet *being emitted*
4087
+ * contain a declaration the browser will drop.
4088
+ *
4089
+ * Within a watch process that set is cumulative, and so is the css: extraction is additive,
4090
+ * so the rule for a style deleted from source is still in the sheet until the process
4091
+ * restarts. This reports the same way for the same reason — the declaration really is still
4092
+ * in the file being written, and saying otherwise would be a check that disagreed with its
4093
+ * own output. A production build is a fresh process and sees only what its source asked
4094
+ * for.
4095
+ *
4096
+ * **Config-derived styles are not in that set at all**, which is the gap this used to have.
4097
+ * `globalCss`, the reset, config recipes and compositions serialize through
4098
+ * `transformStyles`, and that clones the decoder — so their atoms land in a throwaway and
4099
+ * `decoder.atomic` never hears about them. Reading only the sheet made `'error'` *quieter*
4100
+ * than the default on exactly those styles: the warning was suppressed in favour of a check
4101
+ * that could not see them, so a bad token in `globalCss` warned with the option unset and
4102
+ * then passed silently with it set to `'error'`. `utility.unresolvedTokens` is the record of
4103
+ * what only `transform` can see; see it for why accumulating is right for that half.
4104
+ *
4105
+ * Both halves key on `property:path` with shorthands resolved, so a value that does reach
4106
+ * both — every atomic style is transformed once before it is memoized — is one finding.
4107
+ *
4108
+ * Here rather than beside the asserts in `BambooContext` because this is where the sheet
4109
+ * exists: those all run during extraction, before anything has been decoded. Every path
4110
+ * that emits css comes through `getCss`.
4111
+ */
4112
+ assertNoUnresolvedTokens = () => {
4113
+ if (this.config.unresolvedToken !== "error") return;
4114
+ const found = new Map(this.utility.unresolvedTokens);
4115
+ for (const atom of this.decoder.atomic) {
4116
+ const { prop, value } = atom.entry;
4117
+ if (typeof value !== "string" || typeof prop !== "string") continue;
4118
+ if (!this.utility.isUnresolvedTokenValue(prop, value)) continue;
4119
+ const key = this.utility.resolveShorthand(prop);
4120
+ const bare = this.utility.bareTokenPath(key, value);
4121
+ found.set(`${key}:${bare}`, {
4122
+ prop: key,
4123
+ value: bare,
4124
+ category: this.utility.getTokenCategory(key)
4125
+ });
4126
+ }
4127
+ if (!found.size) return;
4128
+ const detail = truncateList(Array.from(found.values(), ({ prop, value, category }) => {
4129
+ return `- \`${prop}: ${value}\`.${category ? ` Check the path against your \`${category}\` tokens.` : ""}`;
4130
+ }), {
4131
+ limit: 25,
4132
+ unit: "value",
4133
+ separator: "\n"
4134
+ });
4135
+ throw new BambooError("UNRESOLVED_TOKEN", `${found.size} style value(s) name a token that does not exist:\n\n${detail}\n\nEach is emitted as written, which parses — so the stylesheet is valid and nothing downstream objects. The browser drops the declaration at compute time and the style is simply absent from the element, which surfaces as "this never applied" a long way from the typo that caused it. Write \`[value]\` to mark one as a literal, or set \`unresolvedToken: 'warn'\` to report these without failing.`);
4136
+ };
4137
+ /**
4021
4138
  * Get CSS for a specific layer from the stylesheet
4022
4139
  */
4023
4140
  getLayerCss = (sheet, layer) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/generator",
3
- "version": "1.33.0",
3
+ "version": "1.34.1",
4
4
  "description": "The css generator for css bamboo",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -38,12 +38,12 @@
38
38
  "pluralize": "8.0.0",
39
39
  "postcss": "8.5.26",
40
40
  "ts-pattern": "5.9.0",
41
- "@bamboocss/core": "1.33.0",
42
- "@bamboocss/is-valid-prop": "^1.33.0",
43
- "@bamboocss/logger": "1.33.0",
44
- "@bamboocss/shared": "1.33.0",
45
- "@bamboocss/token-dictionary": "1.33.0",
46
- "@bamboocss/types": "1.33.0"
41
+ "@bamboocss/core": "1.34.1",
42
+ "@bamboocss/is-valid-prop": "^1.34.1",
43
+ "@bamboocss/logger": "1.34.1",
44
+ "@bamboocss/shared": "1.34.1",
45
+ "@bamboocss/token-dictionary": "1.34.1",
46
+ "@bamboocss/types": "1.34.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/pluralize": "0.0.33"