@bamboocss/generator 1.53.0 → 1.54.0
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 +234 -29
- package/dist/index.d.cts +36 -1
- package/dist/index.d.mts +36 -1
- package/dist/index.mjs +234 -29
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -2450,41 +2450,162 @@ const OS_DARK_QUERY = "@media (prefers-color-scheme: dark)";
|
|
|
2450
2450
|
/** Compare a condition against a known query without tripping over spacing. */
|
|
2451
2451
|
const normalize = (condition) => typeof condition === "string" ? condition.replace(/\s+/g, " ").trim() : void 0;
|
|
2452
2452
|
/**
|
|
2453
|
-
*
|
|
2453
|
+
* Split a value on its top-level `separator`, ignoring one nested inside a function or a
|
|
2454
|
+
* string.
|
|
2454
2455
|
*
|
|
2455
|
-
* `
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
* declaration. That failure is silent and total — a `--shadows-sm` carrying two shadows
|
|
2460
|
-
* emitted `light-dark(a, b, c)` and every element referencing it rendered with no shadow at
|
|
2461
|
-
* all, while the class naming it looked perfectly correct.
|
|
2462
|
-
*
|
|
2463
|
-
* Multi-part `box-shadow` is the shape that bites, since a realistic elevation token is
|
|
2464
|
-
* almost always two shadows; `transition` and `background` lists are the same story.
|
|
2465
|
-
*
|
|
2466
|
-
* Depth-aware, so `rgb(16, 19, 26)` still folds — its commas are the function's own.
|
|
2467
|
-
* Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a separator.
|
|
2456
|
+
* Depth-aware, so `rgb(16, 19, 26)` is one part rather than three — its commas are the
|
|
2457
|
+
* function's own. Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a
|
|
2458
|
+
* separator. Splitting on `' '` collapses runs of whitespace, which is what makes
|
|
2459
|
+
* `0 1px 2px red` and `0 1px 2px red` compare component for component.
|
|
2468
2460
|
*/
|
|
2469
|
-
|
|
2461
|
+
function splitTopLevel(value, separator) {
|
|
2462
|
+
const parts = [];
|
|
2463
|
+
let current = "";
|
|
2470
2464
|
let depth = 0;
|
|
2471
2465
|
let quote;
|
|
2466
|
+
const isSeparator = (char) => separator === "," ? char === "," : char === " " || char === " " || char === "\n" || char === "\r";
|
|
2472
2467
|
for (let index = 0; index < value.length; index++) {
|
|
2473
2468
|
const char = value[index];
|
|
2474
2469
|
if (quote) {
|
|
2475
|
-
|
|
2476
|
-
|
|
2470
|
+
current += char;
|
|
2471
|
+
if (char === "\\") {
|
|
2472
|
+
current += value[index + 1] ?? "";
|
|
2473
|
+
index++;
|
|
2474
|
+
} else if (char === quote) quote = void 0;
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
if (char === "\"" || char === "'") {
|
|
2478
|
+
quote = char;
|
|
2479
|
+
current += char;
|
|
2477
2480
|
continue;
|
|
2478
2481
|
}
|
|
2479
|
-
if (char === "
|
|
2480
|
-
else if (char === "(") depth++;
|
|
2482
|
+
if (char === "(") depth++;
|
|
2481
2483
|
else if (char === ")") depth--;
|
|
2482
|
-
else if (
|
|
2484
|
+
else if (depth === 0 && isSeparator(char)) {
|
|
2485
|
+
if (separator === "," || current.trim()) parts.push(current.trim());
|
|
2486
|
+
current = "";
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
current += char;
|
|
2483
2490
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2491
|
+
if (separator === "," || current.trim()) parts.push(current.trim());
|
|
2492
|
+
return parts;
|
|
2493
|
+
}
|
|
2494
|
+
/** Functions whose result is a `<color>`. */
|
|
2495
|
+
const COLOR_FUNCTIONS = new Set([
|
|
2496
|
+
"color",
|
|
2497
|
+
"color-mix",
|
|
2498
|
+
"device-cmyk",
|
|
2499
|
+
"hsl",
|
|
2500
|
+
"hsla",
|
|
2501
|
+
"hwb",
|
|
2502
|
+
"lab",
|
|
2503
|
+
"lch",
|
|
2504
|
+
"light-dark",
|
|
2505
|
+
"oklab",
|
|
2506
|
+
"oklch",
|
|
2507
|
+
"rgb",
|
|
2508
|
+
"rgba"
|
|
2509
|
+
]);
|
|
2510
|
+
/** Keywords that are a `<color>`: the named set, the system set, and the two specials. */
|
|
2511
|
+
const COLOR_KEYWORDS = new Set(`transparent currentcolor
|
|
2512
|
+
aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue
|
|
2513
|
+
blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk
|
|
2514
|
+
crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki
|
|
2515
|
+
darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen
|
|
2516
|
+
darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue
|
|
2517
|
+
dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite
|
|
2518
|
+
gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki
|
|
2519
|
+
lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan
|
|
2520
|
+
lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen
|
|
2521
|
+
lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen
|
|
2522
|
+
magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen
|
|
2523
|
+
mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream
|
|
2524
|
+
mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid
|
|
2525
|
+
palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum
|
|
2526
|
+
powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown
|
|
2527
|
+
seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen
|
|
2528
|
+
steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow
|
|
2529
|
+
yellowgreen
|
|
2530
|
+
accentcolor accentcolortext activetext buttonborder buttonface buttontext canvas canvastext
|
|
2531
|
+
field fieldtext graytext highlight highlighttext linktext mark marktext selecteditem
|
|
2532
|
+
selecteditemtext visitedtext`.trim().split(/\s+/));
|
|
2533
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2534
|
+
/** One value, rather than a list or a space-separated shorthand. */
|
|
2535
|
+
const isSingleComponent = (value) => splitTopLevel(value, ",").length === 1 && splitTopLevel(value, " ").length === 1;
|
|
2536
|
+
/**
|
|
2537
|
+
* Is this single component provably a `<color>`?
|
|
2538
|
+
*
|
|
2539
|
+
* Provably, not plausibly. A false negative costs a fold; a false positive emits CSS the
|
|
2540
|
+
* browser drops on the floor without a word, which is the failure this whole file is written
|
|
2541
|
+
* around. So anything unrecognised is not a color.
|
|
2542
|
+
*
|
|
2543
|
+
* A `var()` is only a color when it names a token bamboo emitted from the `colors` category —
|
|
2544
|
+
* the reference itself says nothing about its type, and guessing wrong is exactly the
|
|
2545
|
+
* silent-drop case.
|
|
2546
|
+
*/
|
|
2547
|
+
function isColorValue(value, colorVars) {
|
|
2548
|
+
if (value.startsWith("#")) return HEX_COLOR.test(value);
|
|
2549
|
+
const open = value.indexOf("(");
|
|
2550
|
+
if (open === -1) return COLOR_KEYWORDS.has(value.toLowerCase());
|
|
2551
|
+
if (!value.endsWith(")")) return false;
|
|
2552
|
+
const fn = value.slice(0, open).toLowerCase();
|
|
2553
|
+
if (fn !== "var") return COLOR_FUNCTIONS.has(fn);
|
|
2554
|
+
const [name] = splitTopLevel(value.slice(open + 1, -1), ",");
|
|
2555
|
+
return name !== void 0 && colorVars.has(name);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Merge a token's light and dark values into one, folding only the parts that differ.
|
|
2559
|
+
*
|
|
2560
|
+
* `light-dark()` is a `<color>` function — `light-dark() = light-dark(<color>, <color>)` in
|
|
2561
|
+
* CSS Color 5 — and that is the whole constraint here. It cannot carry a shadow, a border
|
|
2562
|
+
* shorthand or a length, and a browser handed one drops the declaration outright: verified in
|
|
2563
|
+
* Chrome, `--t: light-dark(0 1px 2px red, 0 1px 2px black)` computes `box-shadow: none`,
|
|
2564
|
+
* `light-dark(4px, 8px)` computes `padding-top: 0px`. Nothing warns. The token is still in the
|
|
2565
|
+
* sheet and the class naming it still looks correct.
|
|
2566
|
+
*
|
|
2567
|
+
* So a value is folded component by component rather than whole. `0 1px 2px red` against
|
|
2568
|
+
* `0 1px 2px black` differs in one place, that place is a color, and the result keeps the
|
|
2569
|
+
* geometry outside the function where it parses:
|
|
2570
|
+
*
|
|
2571
|
+
* 0 1px 2px light-dark(red, black)
|
|
2572
|
+
*
|
|
2573
|
+
* That is also what makes a list foldable, which folding whole never could: `light-dark()`
|
|
2574
|
+
* takes exactly two arguments and CSS offers no way to group a comma-separated value into one
|
|
2575
|
+
* of them, so a two-shadow token splatted into `light-dark(a, b, c)` and was dropped. Per-item
|
|
2576
|
+
* folding sidesteps the arity problem entirely, because the commas stay in the value where
|
|
2577
|
+
* they belong. A list of whole `light-dark()` calls does *not* work and was measured not to —
|
|
2578
|
+
* the arms have to be colors either way.
|
|
2579
|
+
*
|
|
2580
|
+
* Bails to `undefined` — keep the `@media` block, which expresses anything — whenever the two
|
|
2581
|
+
* arms do not line up part for part, or a differing part is not provably a color.
|
|
2582
|
+
*/
|
|
2583
|
+
function foldValue(light, dark, colorVars) {
|
|
2584
|
+
const lightItems = splitTopLevel(light, ",");
|
|
2585
|
+
const darkItems = splitTopLevel(dark, ",");
|
|
2586
|
+
if (lightItems.length !== darkItems.length) return;
|
|
2587
|
+
const items = [];
|
|
2588
|
+
for (let index = 0; index < lightItems.length; index++) {
|
|
2589
|
+
const lightParts = splitTopLevel(lightItems[index], " ");
|
|
2590
|
+
const darkParts = splitTopLevel(darkItems[index], " ");
|
|
2591
|
+
if (lightParts.length !== darkParts.length) return;
|
|
2592
|
+
const merged = [];
|
|
2593
|
+
for (let part = 0; part < lightParts.length; part++) {
|
|
2594
|
+
const lightPart = lightParts[part];
|
|
2595
|
+
const darkPart = darkParts[part];
|
|
2596
|
+
if (lightPart === darkPart) {
|
|
2597
|
+
merged.push(lightPart);
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
if (!isColorValue(lightPart, colorVars) || !isColorValue(darkPart, colorVars)) return;
|
|
2601
|
+
merged.push(`light-dark(${lightPart}, ${darkPart})`);
|
|
2602
|
+
}
|
|
2603
|
+
items.push(merged.join(" "));
|
|
2604
|
+
}
|
|
2605
|
+
return items.join(", ");
|
|
2606
|
+
}
|
|
2486
2607
|
/**
|
|
2487
|
-
* Collapse a token's `base`/`_osDark` pair into a single
|
|
2608
|
+
* Collapse a token's `base`/`_osDark` pair into a single declaration.
|
|
2488
2609
|
*
|
|
2489
2610
|
* Two declarations and a whole `@media (prefers-color-scheme: dark)` block become one line,
|
|
2490
2611
|
* which for a design system carrying a few hundred `_osDark` semantic tokens is the largest
|
|
@@ -2494,7 +2615,9 @@ const hasTopLevelComma = (value) => {
|
|
|
2494
2615
|
* `[data-theme=dark]` selector is not, so the two are independent mechanisms that resolve
|
|
2495
2616
|
* against each other by source order. `light-dark()` reads `color-scheme`, which is an
|
|
2496
2617
|
* ordinary inherited property — so a toggle is `color-scheme: dark` on a subtree rather than
|
|
2497
|
-
* a second copy of every token.
|
|
2618
|
+
* a second copy of every token. That argument is why the fold has to reach past colors: a
|
|
2619
|
+
* sheet that folds its colors and leaves its shadows on the media query gives a subtree
|
|
2620
|
+
* toggle half a theme, with the shadows still following the OS.
|
|
2498
2621
|
*
|
|
2499
2622
|
* A var carrying `_osLight` as well is left alone. Folding it would put the light arm of
|
|
2500
2623
|
* `light-dark()` and an `@media (prefers-color-scheme: light)` block in play for the same
|
|
@@ -2503,7 +2626,7 @@ const hasTopLevelComma = (value) => {
|
|
|
2503
2626
|
*
|
|
2504
2627
|
* `view.vars` is shared with the JS theme artifacts, so this copies rather than mutates.
|
|
2505
2628
|
*/
|
|
2506
|
-
function foldLightDark(vars, conditions) {
|
|
2629
|
+
function foldLightDark(vars, conditions, colorVars) {
|
|
2507
2630
|
const base = vars.get(BASE);
|
|
2508
2631
|
const osDark = vars.get(OS_DARK);
|
|
2509
2632
|
if (!base || !osDark) return {
|
|
@@ -2520,8 +2643,9 @@ function foldLightDark(vars, conditions) {
|
|
|
2520
2643
|
for (const [name, darkValue] of osDark) {
|
|
2521
2644
|
const lightValue = base.get(name);
|
|
2522
2645
|
if (lightValue === void 0 || osLight?.has(name)) continue;
|
|
2523
|
-
|
|
2524
|
-
|
|
2646
|
+
const folded = colorVars.has(name) && isSingleComponent(lightValue) && isSingleComponent(darkValue) ? `light-dark(${lightValue}, ${darkValue})` : foldValue(lightValue, darkValue, colorVars);
|
|
2647
|
+
if (folded === void 0) continue;
|
|
2648
|
+
nextBase.set(name, folded);
|
|
2525
2649
|
nextDark.delete(name);
|
|
2526
2650
|
}
|
|
2527
2651
|
if (nextDark.size === osDark.size) return {
|
|
@@ -2537,12 +2661,18 @@ function foldLightDark(vars, conditions) {
|
|
|
2537
2661
|
folded: true
|
|
2538
2662
|
};
|
|
2539
2663
|
}
|
|
2664
|
+
/** The custom properties bamboo emitted for `colors` tokens, by var name. */
|
|
2665
|
+
function getColorVars(tokens) {
|
|
2666
|
+
const names = /* @__PURE__ */ new Set();
|
|
2667
|
+
for (const token of tokens.view.categoryMap.get("colors")?.values() ?? []) names.add(token.extensions.var);
|
|
2668
|
+
return names;
|
|
2669
|
+
}
|
|
2540
2670
|
function generateTokenCss(ctx, sheet) {
|
|
2541
2671
|
const { config, conditions, tokens } = ctx;
|
|
2542
2672
|
const { cssVarRoot, staticCss } = config;
|
|
2543
2673
|
const root = cssVarRoot;
|
|
2544
2674
|
const results = [];
|
|
2545
|
-
const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions);
|
|
2675
|
+
const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions, getColorVars(tokens));
|
|
2546
2676
|
/**
|
|
2547
2677
|
* `light-dark()` returns the light value unless `color-scheme` names both, and a stylesheet
|
|
2548
2678
|
* that never sets it looks exactly like one where dark mode is broken. This rides with the
|
|
@@ -3943,14 +4073,32 @@ var Generator = class extends _bamboocss_core.Context {
|
|
|
3943
4073
|
getParserCss = (decoder) => {
|
|
3944
4074
|
return generateParserCss(this, decoder);
|
|
3945
4075
|
};
|
|
4076
|
+
/**
|
|
4077
|
+
* Each atom's first call site, by the class name the sheet writes it under.
|
|
4078
|
+
*
|
|
4079
|
+
* Only what the encoder recorded — see `StyleEncoder.recordOrigins` — so empty unless an
|
|
4080
|
+
* integration asked for origins. Several hashes can decode to one class; the first wins.
|
|
4081
|
+
*/
|
|
4082
|
+
getAtomOrigins = () => {
|
|
4083
|
+
const byHash = this.encoder.atomOrigins();
|
|
4084
|
+
const origins = /* @__PURE__ */ new Map();
|
|
4085
|
+
if (!byHash.size) return origins;
|
|
4086
|
+
for (const atom of this.decoder.collect(this.encoder).atomic) {
|
|
4087
|
+
const origin = byHash.get(atom.hash);
|
|
4088
|
+
if (origin && !origins.has(atom.className)) origins.set(atom.className, origin);
|
|
4089
|
+
}
|
|
4090
|
+
return origins;
|
|
4091
|
+
};
|
|
3946
4092
|
getCss = (stylesheet) => {
|
|
3947
|
-
|
|
4093
|
+
const sheet = stylesheet ?? this.createSheet();
|
|
4094
|
+
let css = sheet.toCss({ minify: this.config.minify });
|
|
3948
4095
|
if (this.hooks["cssgen:done"]) css = this.hooks["cssgen:done"]({
|
|
3949
4096
|
artifact: "styles.css",
|
|
3950
4097
|
content: css
|
|
3951
4098
|
}) ?? css;
|
|
3952
4099
|
this.assertNoUnresolvedTokens();
|
|
3953
4100
|
this.reportRawValues();
|
|
4101
|
+
this.reportInvalidDeclarations(sheet);
|
|
3954
4102
|
return css;
|
|
3955
4103
|
};
|
|
3956
4104
|
/**
|
|
@@ -4057,6 +4205,63 @@ var Generator = class extends _bamboocss_core.Context {
|
|
|
4057
4205
|
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.`);
|
|
4058
4206
|
};
|
|
4059
4207
|
/**
|
|
4208
|
+
* The findings `warn` has already printed, keyed `property:value`.
|
|
4209
|
+
*
|
|
4210
|
+
* A dev server emits the sheet on every edit, and extraction is additive within a watch, so
|
|
4211
|
+
* a finding that warned on every rebuild would bury the edit that introduced the next one.
|
|
4212
|
+
* Per process, like the sheet it describes.
|
|
4213
|
+
*/
|
|
4214
|
+
reportedInvalidDeclarations = /* @__PURE__ */ new Set();
|
|
4215
|
+
/**
|
|
4216
|
+
* Report every declaration in the sheet just emitted that its property's grammar rejects.
|
|
4217
|
+
*
|
|
4218
|
+
* The other side of `assertNoUnresolvedTokens`. That one reads the *values* the source asked
|
|
4219
|
+
* for; this reads what the sheet *contains*, after every utility transform, mixin and recipe
|
|
4220
|
+
* has had its say — which is the only place a transform that handed a value through
|
|
4221
|
+
* unchanged, or a `[…]` literal that was never valid CSS, can be seen at all. Collected by
|
|
4222
|
+
* `Stylesheet.toCss`, where the finished tree exists; graded here, where the sheet is
|
|
4223
|
+
* emitted.
|
|
4224
|
+
*
|
|
4225
|
+
* A finding the unresolved-token pass owns is left to it, whatever that pass is set to, so
|
|
4226
|
+
* one mistake is one report and `unresolvedToken: 'off'` means silence rather than the same
|
|
4227
|
+
* value reported in this check's voice: `display: 'flexx'` is that pass's grammar half, and
|
|
4228
|
+
* would otherwise be rejected again here as the declaration it became. Keyed on the
|
|
4229
|
+
* property the utility *emits*, because that is how the sheet spells it.
|
|
4230
|
+
*
|
|
4231
|
+
* `warn` reports each distinct declaration once per process. `error` lists everything the
|
|
4232
|
+
* sheet holds each time, because each time the build is failing on it.
|
|
4233
|
+
*/
|
|
4234
|
+
reportInvalidDeclarations = (sheet) => {
|
|
4235
|
+
const severity = this.utility.invalidDeclaration;
|
|
4236
|
+
if (severity === "off") return;
|
|
4237
|
+
const owned = /* @__PURE__ */ new Set();
|
|
4238
|
+
for (const ref of this.utility.unresolvedTokens.values()) owned.add(`${this.utility.cssPropertyOf(ref.prop)}:${ref.value}`);
|
|
4239
|
+
const id = (finding) => `${finding.prop}:${finding.value}`;
|
|
4240
|
+
const findings = sheet.invalidDeclarations.filter((finding) => !owned.has(id(finding)));
|
|
4241
|
+
if (!findings.length) return;
|
|
4242
|
+
const describe = ({ prop, value, selector, layer, count }) => {
|
|
4243
|
+
return `- \`${prop}: ${value}\`${selector ? ` in \`${selector}\`` : ""}${layer ? `, \`@layer ${layer}\`` : ""}${count > 1 ? ` (${count} rules)` : ""}`;
|
|
4244
|
+
};
|
|
4245
|
+
const dropped = "Each 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.";
|
|
4246
|
+
if (severity === "error") {
|
|
4247
|
+
const detail = (0, _bamboocss_shared.truncateList)(findings.map(describe), {
|
|
4248
|
+
limit: 25,
|
|
4249
|
+
unit: "declaration",
|
|
4250
|
+
separator: "\n"
|
|
4251
|
+
});
|
|
4252
|
+
throw new _bamboocss_shared.BambooError("INVALID_DECLARATION", `${findings.length} declaration(s) in the stylesheet are not valid CSS for their property:\n\n${detail}\n\n${dropped} Fix the value, or set \`invalidDeclaration: 'warn'\` to report these without failing.`);
|
|
4253
|
+
}
|
|
4254
|
+
const fresh = findings.filter((finding) => !this.reportedInvalidDeclarations.has(id(finding)));
|
|
4255
|
+
if (!fresh.length) return;
|
|
4256
|
+
for (const finding of fresh) this.reportedInvalidDeclarations.add(id(finding));
|
|
4257
|
+
const detail = (0, _bamboocss_shared.truncateList)(fresh.map(describe), {
|
|
4258
|
+
limit: 25,
|
|
4259
|
+
unit: "declaration",
|
|
4260
|
+
separator: "\n"
|
|
4261
|
+
});
|
|
4262
|
+
_bamboocss_logger.logger.warn("sheet", `${fresh.length} declaration(s) in the stylesheet are not valid CSS for their property:\n\n${detail}\n\n${dropped} Set \`invalidDeclaration: 'error'\` to fail the build on these, or \`'off'\` to stop reporting them.`);
|
|
4263
|
+
};
|
|
4264
|
+
/**
|
|
4060
4265
|
* Get CSS for a specific layer from the stylesheet
|
|
4061
4266
|
*/
|
|
4062
4267
|
getLayerCss = (sheet, layer) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, StyleDecoder, Stylesheet } from "@bamboocss/core";
|
|
1
|
+
import { AtomOrigin, Context, StyleDecoder, Stylesheet } from "@bamboocss/core";
|
|
2
2
|
import { ArtifactId, CssArtifactType, LoadConfigResult, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
|
|
3
3
|
|
|
4
4
|
//#region src/generator.d.ts
|
|
@@ -131,6 +131,13 @@ declare class Generator extends Context {
|
|
|
131
131
|
*/
|
|
132
132
|
private getAlwaysKeptTokenVars;
|
|
133
133
|
getParserCss: (decoder: StyleDecoder) => string;
|
|
134
|
+
/**
|
|
135
|
+
* Each atom's first call site, by the class name the sheet writes it under.
|
|
136
|
+
*
|
|
137
|
+
* Only what the encoder recorded — see `StyleEncoder.recordOrigins` — so empty unless an
|
|
138
|
+
* integration asked for origins. Several hashes can decode to one class; the first wins.
|
|
139
|
+
*/
|
|
140
|
+
getAtomOrigins: () => Map<string, AtomOrigin>;
|
|
134
141
|
getCss: (stylesheet?: Stylesheet) => string;
|
|
135
142
|
/**
|
|
136
143
|
* Fail on a style value shaped like a token path that names no token.
|
|
@@ -192,6 +199,34 @@ declare class Generator extends Context {
|
|
|
192
199
|
*/
|
|
193
200
|
reportRawValues: () => void;
|
|
194
201
|
assertNoUnresolvedTokens: () => void;
|
|
202
|
+
/**
|
|
203
|
+
* The findings `warn` has already printed, keyed `property:value`.
|
|
204
|
+
*
|
|
205
|
+
* A dev server emits the sheet on every edit, and extraction is additive within a watch, so
|
|
206
|
+
* a finding that warned on every rebuild would bury the edit that introduced the next one.
|
|
207
|
+
* Per process, like the sheet it describes.
|
|
208
|
+
*/
|
|
209
|
+
private reportedInvalidDeclarations;
|
|
210
|
+
/**
|
|
211
|
+
* Report every declaration in the sheet just emitted that its property's grammar rejects.
|
|
212
|
+
*
|
|
213
|
+
* The other side of `assertNoUnresolvedTokens`. That one reads the *values* the source asked
|
|
214
|
+
* for; this reads what the sheet *contains*, after every utility transform, mixin and recipe
|
|
215
|
+
* has had its say — which is the only place a transform that handed a value through
|
|
216
|
+
* unchanged, or a `[…]` literal that was never valid CSS, can be seen at all. Collected by
|
|
217
|
+
* `Stylesheet.toCss`, where the finished tree exists; graded here, where the sheet is
|
|
218
|
+
* emitted.
|
|
219
|
+
*
|
|
220
|
+
* A finding the unresolved-token pass owns is left to it, whatever that pass is set to, so
|
|
221
|
+
* one mistake is one report and `unresolvedToken: 'off'` means silence rather than the same
|
|
222
|
+
* value reported in this check's voice: `display: 'flexx'` is that pass's grammar half, and
|
|
223
|
+
* would otherwise be rejected again here as the declaration it became. Keyed on the
|
|
224
|
+
* property the utility *emits*, because that is how the sheet spells it.
|
|
225
|
+
*
|
|
226
|
+
* `warn` reports each distinct declaration once per process. `error` lists everything the
|
|
227
|
+
* sheet holds each time, because each time the build is failing on it.
|
|
228
|
+
*/
|
|
229
|
+
reportInvalidDeclarations: (sheet: Stylesheet) => void;
|
|
195
230
|
/**
|
|
196
231
|
* Get CSS for a specific layer from the stylesheet
|
|
197
232
|
*/
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, StyleDecoder, Stylesheet } from "@bamboocss/core";
|
|
1
|
+
import { AtomOrigin, Context, StyleDecoder, Stylesheet } from "@bamboocss/core";
|
|
2
2
|
import { ArtifactId, CssArtifactType, LoadConfigResult, SpecFile, SpecType, SpecTypeMap } from "@bamboocss/types";
|
|
3
3
|
|
|
4
4
|
//#region src/generator.d.ts
|
|
@@ -131,6 +131,13 @@ declare class Generator extends Context {
|
|
|
131
131
|
*/
|
|
132
132
|
private getAlwaysKeptTokenVars;
|
|
133
133
|
getParserCss: (decoder: StyleDecoder) => string;
|
|
134
|
+
/**
|
|
135
|
+
* Each atom's first call site, by the class name the sheet writes it under.
|
|
136
|
+
*
|
|
137
|
+
* Only what the encoder recorded — see `StyleEncoder.recordOrigins` — so empty unless an
|
|
138
|
+
* integration asked for origins. Several hashes can decode to one class; the first wins.
|
|
139
|
+
*/
|
|
140
|
+
getAtomOrigins: () => Map<string, AtomOrigin>;
|
|
134
141
|
getCss: (stylesheet?: Stylesheet) => string;
|
|
135
142
|
/**
|
|
136
143
|
* Fail on a style value shaped like a token path that names no token.
|
|
@@ -192,6 +199,34 @@ declare class Generator extends Context {
|
|
|
192
199
|
*/
|
|
193
200
|
reportRawValues: () => void;
|
|
194
201
|
assertNoUnresolvedTokens: () => void;
|
|
202
|
+
/**
|
|
203
|
+
* The findings `warn` has already printed, keyed `property:value`.
|
|
204
|
+
*
|
|
205
|
+
* A dev server emits the sheet on every edit, and extraction is additive within a watch, so
|
|
206
|
+
* a finding that warned on every rebuild would bury the edit that introduced the next one.
|
|
207
|
+
* Per process, like the sheet it describes.
|
|
208
|
+
*/
|
|
209
|
+
private reportedInvalidDeclarations;
|
|
210
|
+
/**
|
|
211
|
+
* Report every declaration in the sheet just emitted that its property's grammar rejects.
|
|
212
|
+
*
|
|
213
|
+
* The other side of `assertNoUnresolvedTokens`. That one reads the *values* the source asked
|
|
214
|
+
* for; this reads what the sheet *contains*, after every utility transform, mixin and recipe
|
|
215
|
+
* has had its say — which is the only place a transform that handed a value through
|
|
216
|
+
* unchanged, or a `[…]` literal that was never valid CSS, can be seen at all. Collected by
|
|
217
|
+
* `Stylesheet.toCss`, where the finished tree exists; graded here, where the sheet is
|
|
218
|
+
* emitted.
|
|
219
|
+
*
|
|
220
|
+
* A finding the unresolved-token pass owns is left to it, whatever that pass is set to, so
|
|
221
|
+
* one mistake is one report and `unresolvedToken: 'off'` means silence rather than the same
|
|
222
|
+
* value reported in this check's voice: `display: 'flexx'` is that pass's grammar half, and
|
|
223
|
+
* would otherwise be rejected again here as the declaration it became. Keyed on the
|
|
224
|
+
* property the utility *emits*, because that is how the sheet spells it.
|
|
225
|
+
*
|
|
226
|
+
* `warn` reports each distinct declaration once per process. `error` lists everything the
|
|
227
|
+
* sheet holds each time, because each time the build is failing on it.
|
|
228
|
+
*/
|
|
229
|
+
reportInvalidDeclarations: (sheet: Stylesheet) => void;
|
|
195
230
|
/**
|
|
196
231
|
* Get CSS for a specific layer from the stylesheet
|
|
197
232
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -2424,41 +2424,162 @@ const OS_DARK_QUERY = "@media (prefers-color-scheme: dark)";
|
|
|
2424
2424
|
/** Compare a condition against a known query without tripping over spacing. */
|
|
2425
2425
|
const normalize = (condition) => typeof condition === "string" ? condition.replace(/\s+/g, " ").trim() : void 0;
|
|
2426
2426
|
/**
|
|
2427
|
-
*
|
|
2427
|
+
* Split a value on its top-level `separator`, ignoring one nested inside a function or a
|
|
2428
|
+
* string.
|
|
2428
2429
|
*
|
|
2429
|
-
* `
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2433
|
-
* declaration. That failure is silent and total — a `--shadows-sm` carrying two shadows
|
|
2434
|
-
* emitted `light-dark(a, b, c)` and every element referencing it rendered with no shadow at
|
|
2435
|
-
* all, while the class naming it looked perfectly correct.
|
|
2436
|
-
*
|
|
2437
|
-
* Multi-part `box-shadow` is the shape that bites, since a realistic elevation token is
|
|
2438
|
-
* almost always two shadows; `transition` and `background` lists are the same story.
|
|
2439
|
-
*
|
|
2440
|
-
* Depth-aware, so `rgb(16, 19, 26)` still folds — its commas are the function's own.
|
|
2441
|
-
* Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a separator.
|
|
2430
|
+
* Depth-aware, so `rgb(16, 19, 26)` is one part rather than three — its commas are the
|
|
2431
|
+
* function's own. Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a
|
|
2432
|
+
* separator. Splitting on `' '` collapses runs of whitespace, which is what makes
|
|
2433
|
+
* `0 1px 2px red` and `0 1px 2px red` compare component for component.
|
|
2442
2434
|
*/
|
|
2443
|
-
|
|
2435
|
+
function splitTopLevel(value, separator) {
|
|
2436
|
+
const parts = [];
|
|
2437
|
+
let current = "";
|
|
2444
2438
|
let depth = 0;
|
|
2445
2439
|
let quote;
|
|
2440
|
+
const isSeparator = (char) => separator === "," ? char === "," : char === " " || char === " " || char === "\n" || char === "\r";
|
|
2446
2441
|
for (let index = 0; index < value.length; index++) {
|
|
2447
2442
|
const char = value[index];
|
|
2448
2443
|
if (quote) {
|
|
2449
|
-
|
|
2450
|
-
|
|
2444
|
+
current += char;
|
|
2445
|
+
if (char === "\\") {
|
|
2446
|
+
current += value[index + 1] ?? "";
|
|
2447
|
+
index++;
|
|
2448
|
+
} else if (char === quote) quote = void 0;
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
if (char === "\"" || char === "'") {
|
|
2452
|
+
quote = char;
|
|
2453
|
+
current += char;
|
|
2451
2454
|
continue;
|
|
2452
2455
|
}
|
|
2453
|
-
if (char === "
|
|
2454
|
-
else if (char === "(") depth++;
|
|
2456
|
+
if (char === "(") depth++;
|
|
2455
2457
|
else if (char === ")") depth--;
|
|
2456
|
-
else if (
|
|
2458
|
+
else if (depth === 0 && isSeparator(char)) {
|
|
2459
|
+
if (separator === "," || current.trim()) parts.push(current.trim());
|
|
2460
|
+
current = "";
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
current += char;
|
|
2457
2464
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2465
|
+
if (separator === "," || current.trim()) parts.push(current.trim());
|
|
2466
|
+
return parts;
|
|
2467
|
+
}
|
|
2468
|
+
/** Functions whose result is a `<color>`. */
|
|
2469
|
+
const COLOR_FUNCTIONS = new Set([
|
|
2470
|
+
"color",
|
|
2471
|
+
"color-mix",
|
|
2472
|
+
"device-cmyk",
|
|
2473
|
+
"hsl",
|
|
2474
|
+
"hsla",
|
|
2475
|
+
"hwb",
|
|
2476
|
+
"lab",
|
|
2477
|
+
"lch",
|
|
2478
|
+
"light-dark",
|
|
2479
|
+
"oklab",
|
|
2480
|
+
"oklch",
|
|
2481
|
+
"rgb",
|
|
2482
|
+
"rgba"
|
|
2483
|
+
]);
|
|
2484
|
+
/** Keywords that are a `<color>`: the named set, the system set, and the two specials. */
|
|
2485
|
+
const COLOR_KEYWORDS = new Set(`transparent currentcolor
|
|
2486
|
+
aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue
|
|
2487
|
+
blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk
|
|
2488
|
+
crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki
|
|
2489
|
+
darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen
|
|
2490
|
+
darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue
|
|
2491
|
+
dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite
|
|
2492
|
+
gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki
|
|
2493
|
+
lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan
|
|
2494
|
+
lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen
|
|
2495
|
+
lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen
|
|
2496
|
+
magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen
|
|
2497
|
+
mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream
|
|
2498
|
+
mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid
|
|
2499
|
+
palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum
|
|
2500
|
+
powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown
|
|
2501
|
+
seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen
|
|
2502
|
+
steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow
|
|
2503
|
+
yellowgreen
|
|
2504
|
+
accentcolor accentcolortext activetext buttonborder buttonface buttontext canvas canvastext
|
|
2505
|
+
field fieldtext graytext highlight highlighttext linktext mark marktext selecteditem
|
|
2506
|
+
selecteditemtext visitedtext`.trim().split(/\s+/));
|
|
2507
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2508
|
+
/** One value, rather than a list or a space-separated shorthand. */
|
|
2509
|
+
const isSingleComponent = (value) => splitTopLevel(value, ",").length === 1 && splitTopLevel(value, " ").length === 1;
|
|
2510
|
+
/**
|
|
2511
|
+
* Is this single component provably a `<color>`?
|
|
2512
|
+
*
|
|
2513
|
+
* Provably, not plausibly. A false negative costs a fold; a false positive emits CSS the
|
|
2514
|
+
* browser drops on the floor without a word, which is the failure this whole file is written
|
|
2515
|
+
* around. So anything unrecognised is not a color.
|
|
2516
|
+
*
|
|
2517
|
+
* A `var()` is only a color when it names a token bamboo emitted from the `colors` category —
|
|
2518
|
+
* the reference itself says nothing about its type, and guessing wrong is exactly the
|
|
2519
|
+
* silent-drop case.
|
|
2520
|
+
*/
|
|
2521
|
+
function isColorValue(value, colorVars) {
|
|
2522
|
+
if (value.startsWith("#")) return HEX_COLOR.test(value);
|
|
2523
|
+
const open = value.indexOf("(");
|
|
2524
|
+
if (open === -1) return COLOR_KEYWORDS.has(value.toLowerCase());
|
|
2525
|
+
if (!value.endsWith(")")) return false;
|
|
2526
|
+
const fn = value.slice(0, open).toLowerCase();
|
|
2527
|
+
if (fn !== "var") return COLOR_FUNCTIONS.has(fn);
|
|
2528
|
+
const [name] = splitTopLevel(value.slice(open + 1, -1), ",");
|
|
2529
|
+
return name !== void 0 && colorVars.has(name);
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Merge a token's light and dark values into one, folding only the parts that differ.
|
|
2533
|
+
*
|
|
2534
|
+
* `light-dark()` is a `<color>` function — `light-dark() = light-dark(<color>, <color>)` in
|
|
2535
|
+
* CSS Color 5 — and that is the whole constraint here. It cannot carry a shadow, a border
|
|
2536
|
+
* shorthand or a length, and a browser handed one drops the declaration outright: verified in
|
|
2537
|
+
* Chrome, `--t: light-dark(0 1px 2px red, 0 1px 2px black)` computes `box-shadow: none`,
|
|
2538
|
+
* `light-dark(4px, 8px)` computes `padding-top: 0px`. Nothing warns. The token is still in the
|
|
2539
|
+
* sheet and the class naming it still looks correct.
|
|
2540
|
+
*
|
|
2541
|
+
* So a value is folded component by component rather than whole. `0 1px 2px red` against
|
|
2542
|
+
* `0 1px 2px black` differs in one place, that place is a color, and the result keeps the
|
|
2543
|
+
* geometry outside the function where it parses:
|
|
2544
|
+
*
|
|
2545
|
+
* 0 1px 2px light-dark(red, black)
|
|
2546
|
+
*
|
|
2547
|
+
* That is also what makes a list foldable, which folding whole never could: `light-dark()`
|
|
2548
|
+
* takes exactly two arguments and CSS offers no way to group a comma-separated value into one
|
|
2549
|
+
* of them, so a two-shadow token splatted into `light-dark(a, b, c)` and was dropped. Per-item
|
|
2550
|
+
* folding sidesteps the arity problem entirely, because the commas stay in the value where
|
|
2551
|
+
* they belong. A list of whole `light-dark()` calls does *not* work and was measured not to —
|
|
2552
|
+
* the arms have to be colors either way.
|
|
2553
|
+
*
|
|
2554
|
+
* Bails to `undefined` — keep the `@media` block, which expresses anything — whenever the two
|
|
2555
|
+
* arms do not line up part for part, or a differing part is not provably a color.
|
|
2556
|
+
*/
|
|
2557
|
+
function foldValue(light, dark, colorVars) {
|
|
2558
|
+
const lightItems = splitTopLevel(light, ",");
|
|
2559
|
+
const darkItems = splitTopLevel(dark, ",");
|
|
2560
|
+
if (lightItems.length !== darkItems.length) return;
|
|
2561
|
+
const items = [];
|
|
2562
|
+
for (let index = 0; index < lightItems.length; index++) {
|
|
2563
|
+
const lightParts = splitTopLevel(lightItems[index], " ");
|
|
2564
|
+
const darkParts = splitTopLevel(darkItems[index], " ");
|
|
2565
|
+
if (lightParts.length !== darkParts.length) return;
|
|
2566
|
+
const merged = [];
|
|
2567
|
+
for (let part = 0; part < lightParts.length; part++) {
|
|
2568
|
+
const lightPart = lightParts[part];
|
|
2569
|
+
const darkPart = darkParts[part];
|
|
2570
|
+
if (lightPart === darkPart) {
|
|
2571
|
+
merged.push(lightPart);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (!isColorValue(lightPart, colorVars) || !isColorValue(darkPart, colorVars)) return;
|
|
2575
|
+
merged.push(`light-dark(${lightPart}, ${darkPart})`);
|
|
2576
|
+
}
|
|
2577
|
+
items.push(merged.join(" "));
|
|
2578
|
+
}
|
|
2579
|
+
return items.join(", ");
|
|
2580
|
+
}
|
|
2460
2581
|
/**
|
|
2461
|
-
* Collapse a token's `base`/`_osDark` pair into a single
|
|
2582
|
+
* Collapse a token's `base`/`_osDark` pair into a single declaration.
|
|
2462
2583
|
*
|
|
2463
2584
|
* Two declarations and a whole `@media (prefers-color-scheme: dark)` block become one line,
|
|
2464
2585
|
* which for a design system carrying a few hundred `_osDark` semantic tokens is the largest
|
|
@@ -2468,7 +2589,9 @@ const hasTopLevelComma = (value) => {
|
|
|
2468
2589
|
* `[data-theme=dark]` selector is not, so the two are independent mechanisms that resolve
|
|
2469
2590
|
* against each other by source order. `light-dark()` reads `color-scheme`, which is an
|
|
2470
2591
|
* ordinary inherited property — so a toggle is `color-scheme: dark` on a subtree rather than
|
|
2471
|
-
* a second copy of every token.
|
|
2592
|
+
* a second copy of every token. That argument is why the fold has to reach past colors: a
|
|
2593
|
+
* sheet that folds its colors and leaves its shadows on the media query gives a subtree
|
|
2594
|
+
* toggle half a theme, with the shadows still following the OS.
|
|
2472
2595
|
*
|
|
2473
2596
|
* A var carrying `_osLight` as well is left alone. Folding it would put the light arm of
|
|
2474
2597
|
* `light-dark()` and an `@media (prefers-color-scheme: light)` block in play for the same
|
|
@@ -2477,7 +2600,7 @@ const hasTopLevelComma = (value) => {
|
|
|
2477
2600
|
*
|
|
2478
2601
|
* `view.vars` is shared with the JS theme artifacts, so this copies rather than mutates.
|
|
2479
2602
|
*/
|
|
2480
|
-
function foldLightDark(vars, conditions) {
|
|
2603
|
+
function foldLightDark(vars, conditions, colorVars) {
|
|
2481
2604
|
const base = vars.get(BASE);
|
|
2482
2605
|
const osDark = vars.get(OS_DARK);
|
|
2483
2606
|
if (!base || !osDark) return {
|
|
@@ -2494,8 +2617,9 @@ function foldLightDark(vars, conditions) {
|
|
|
2494
2617
|
for (const [name, darkValue] of osDark) {
|
|
2495
2618
|
const lightValue = base.get(name);
|
|
2496
2619
|
if (lightValue === void 0 || osLight?.has(name)) continue;
|
|
2497
|
-
|
|
2498
|
-
|
|
2620
|
+
const folded = colorVars.has(name) && isSingleComponent(lightValue) && isSingleComponent(darkValue) ? `light-dark(${lightValue}, ${darkValue})` : foldValue(lightValue, darkValue, colorVars);
|
|
2621
|
+
if (folded === void 0) continue;
|
|
2622
|
+
nextBase.set(name, folded);
|
|
2499
2623
|
nextDark.delete(name);
|
|
2500
2624
|
}
|
|
2501
2625
|
if (nextDark.size === osDark.size) return {
|
|
@@ -2511,12 +2635,18 @@ function foldLightDark(vars, conditions) {
|
|
|
2511
2635
|
folded: true
|
|
2512
2636
|
};
|
|
2513
2637
|
}
|
|
2638
|
+
/** The custom properties bamboo emitted for `colors` tokens, by var name. */
|
|
2639
|
+
function getColorVars(tokens) {
|
|
2640
|
+
const names = /* @__PURE__ */ new Set();
|
|
2641
|
+
for (const token of tokens.view.categoryMap.get("colors")?.values() ?? []) names.add(token.extensions.var);
|
|
2642
|
+
return names;
|
|
2643
|
+
}
|
|
2514
2644
|
function generateTokenCss(ctx, sheet) {
|
|
2515
2645
|
const { config, conditions, tokens } = ctx;
|
|
2516
2646
|
const { cssVarRoot, staticCss } = config;
|
|
2517
2647
|
const root = cssVarRoot;
|
|
2518
2648
|
const results = [];
|
|
2519
|
-
const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions);
|
|
2649
|
+
const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions, getColorVars(tokens));
|
|
2520
2650
|
/**
|
|
2521
2651
|
* `light-dark()` returns the light value unless `color-scheme` names both, and a stylesheet
|
|
2522
2652
|
* that never sets it looks exactly like one where dark mode is broken. This rides with the
|
|
@@ -3917,14 +4047,32 @@ var Generator = class extends Context {
|
|
|
3917
4047
|
getParserCss = (decoder) => {
|
|
3918
4048
|
return generateParserCss(this, decoder);
|
|
3919
4049
|
};
|
|
4050
|
+
/**
|
|
4051
|
+
* Each atom's first call site, by the class name the sheet writes it under.
|
|
4052
|
+
*
|
|
4053
|
+
* Only what the encoder recorded — see `StyleEncoder.recordOrigins` — so empty unless an
|
|
4054
|
+
* integration asked for origins. Several hashes can decode to one class; the first wins.
|
|
4055
|
+
*/
|
|
4056
|
+
getAtomOrigins = () => {
|
|
4057
|
+
const byHash = this.encoder.atomOrigins();
|
|
4058
|
+
const origins = /* @__PURE__ */ new Map();
|
|
4059
|
+
if (!byHash.size) return origins;
|
|
4060
|
+
for (const atom of this.decoder.collect(this.encoder).atomic) {
|
|
4061
|
+
const origin = byHash.get(atom.hash);
|
|
4062
|
+
if (origin && !origins.has(atom.className)) origins.set(atom.className, origin);
|
|
4063
|
+
}
|
|
4064
|
+
return origins;
|
|
4065
|
+
};
|
|
3920
4066
|
getCss = (stylesheet) => {
|
|
3921
|
-
|
|
4067
|
+
const sheet = stylesheet ?? this.createSheet();
|
|
4068
|
+
let css = sheet.toCss({ minify: this.config.minify });
|
|
3922
4069
|
if (this.hooks["cssgen:done"]) css = this.hooks["cssgen:done"]({
|
|
3923
4070
|
artifact: "styles.css",
|
|
3924
4071
|
content: css
|
|
3925
4072
|
}) ?? css;
|
|
3926
4073
|
this.assertNoUnresolvedTokens();
|
|
3927
4074
|
this.reportRawValues();
|
|
4075
|
+
this.reportInvalidDeclarations(sheet);
|
|
3928
4076
|
return css;
|
|
3929
4077
|
};
|
|
3930
4078
|
/**
|
|
@@ -4031,6 +4179,63 @@ var Generator = class extends Context {
|
|
|
4031
4179
|
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.`);
|
|
4032
4180
|
};
|
|
4033
4181
|
/**
|
|
4182
|
+
* The findings `warn` has already printed, keyed `property:value`.
|
|
4183
|
+
*
|
|
4184
|
+
* A dev server emits the sheet on every edit, and extraction is additive within a watch, so
|
|
4185
|
+
* a finding that warned on every rebuild would bury the edit that introduced the next one.
|
|
4186
|
+
* Per process, like the sheet it describes.
|
|
4187
|
+
*/
|
|
4188
|
+
reportedInvalidDeclarations = /* @__PURE__ */ new Set();
|
|
4189
|
+
/**
|
|
4190
|
+
* Report every declaration in the sheet just emitted that its property's grammar rejects.
|
|
4191
|
+
*
|
|
4192
|
+
* The other side of `assertNoUnresolvedTokens`. That one reads the *values* the source asked
|
|
4193
|
+
* for; this reads what the sheet *contains*, after every utility transform, mixin and recipe
|
|
4194
|
+
* has had its say — which is the only place a transform that handed a value through
|
|
4195
|
+
* unchanged, or a `[…]` literal that was never valid CSS, can be seen at all. Collected by
|
|
4196
|
+
* `Stylesheet.toCss`, where the finished tree exists; graded here, where the sheet is
|
|
4197
|
+
* emitted.
|
|
4198
|
+
*
|
|
4199
|
+
* A finding the unresolved-token pass owns is left to it, whatever that pass is set to, so
|
|
4200
|
+
* one mistake is one report and `unresolvedToken: 'off'` means silence rather than the same
|
|
4201
|
+
* value reported in this check's voice: `display: 'flexx'` is that pass's grammar half, and
|
|
4202
|
+
* would otherwise be rejected again here as the declaration it became. Keyed on the
|
|
4203
|
+
* property the utility *emits*, because that is how the sheet spells it.
|
|
4204
|
+
*
|
|
4205
|
+
* `warn` reports each distinct declaration once per process. `error` lists everything the
|
|
4206
|
+
* sheet holds each time, because each time the build is failing on it.
|
|
4207
|
+
*/
|
|
4208
|
+
reportInvalidDeclarations = (sheet) => {
|
|
4209
|
+
const severity = this.utility.invalidDeclaration;
|
|
4210
|
+
if (severity === "off") return;
|
|
4211
|
+
const owned = /* @__PURE__ */ new Set();
|
|
4212
|
+
for (const ref of this.utility.unresolvedTokens.values()) owned.add(`${this.utility.cssPropertyOf(ref.prop)}:${ref.value}`);
|
|
4213
|
+
const id = (finding) => `${finding.prop}:${finding.value}`;
|
|
4214
|
+
const findings = sheet.invalidDeclarations.filter((finding) => !owned.has(id(finding)));
|
|
4215
|
+
if (!findings.length) return;
|
|
4216
|
+
const describe = ({ prop, value, selector, layer, count }) => {
|
|
4217
|
+
return `- \`${prop}: ${value}\`${selector ? ` in \`${selector}\`` : ""}${layer ? `, \`@layer ${layer}\`` : ""}${count > 1 ? ` (${count} rules)` : ""}`;
|
|
4218
|
+
};
|
|
4219
|
+
const dropped = "Each 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.";
|
|
4220
|
+
if (severity === "error") {
|
|
4221
|
+
const detail = truncateList(findings.map(describe), {
|
|
4222
|
+
limit: 25,
|
|
4223
|
+
unit: "declaration",
|
|
4224
|
+
separator: "\n"
|
|
4225
|
+
});
|
|
4226
|
+
throw new BambooError("INVALID_DECLARATION", `${findings.length} declaration(s) in the stylesheet are not valid CSS for their property:\n\n${detail}\n\n${dropped} Fix the value, or set \`invalidDeclaration: 'warn'\` to report these without failing.`);
|
|
4227
|
+
}
|
|
4228
|
+
const fresh = findings.filter((finding) => !this.reportedInvalidDeclarations.has(id(finding)));
|
|
4229
|
+
if (!fresh.length) return;
|
|
4230
|
+
for (const finding of fresh) this.reportedInvalidDeclarations.add(id(finding));
|
|
4231
|
+
const detail = truncateList(fresh.map(describe), {
|
|
4232
|
+
limit: 25,
|
|
4233
|
+
unit: "declaration",
|
|
4234
|
+
separator: "\n"
|
|
4235
|
+
});
|
|
4236
|
+
logger.warn("sheet", `${fresh.length} declaration(s) in the stylesheet are not valid CSS for their property:\n\n${detail}\n\n${dropped} Set \`invalidDeclaration: 'error'\` to fail the build on these, or \`'off'\` to stop reporting them.`);
|
|
4237
|
+
};
|
|
4238
|
+
/**
|
|
4034
4239
|
* Get CSS for a specific layer from the stylesheet
|
|
4035
4240
|
*/
|
|
4036
4241
|
getLayerCss = (sheet, layer) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/generator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.0",
|
|
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.
|
|
42
|
-
"@bamboocss/is-valid-prop": "^1.
|
|
43
|
-
"@bamboocss/logger": "1.
|
|
44
|
-
"@bamboocss/shared": "1.
|
|
45
|
-
"@bamboocss/token-dictionary": "1.
|
|
46
|
-
"@bamboocss/types": "1.
|
|
41
|
+
"@bamboocss/core": "1.54.0",
|
|
42
|
+
"@bamboocss/is-valid-prop": "^1.54.0",
|
|
43
|
+
"@bamboocss/logger": "1.54.0",
|
|
44
|
+
"@bamboocss/shared": "1.54.0",
|
|
45
|
+
"@bamboocss/token-dictionary": "1.54.0",
|
|
46
|
+
"@bamboocss/types": "1.54.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/pluralize": "0.0.33"
|