cssints 0.0.0-stage → 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -2
  3. package/dist/author-types-C1AhsnrT.d.mts +80 -0
  4. package/dist/check.d.mts +22 -0
  5. package/dist/check.mjs +37 -0
  6. package/dist/color-BYIxVLlS.mjs +29 -0
  7. package/dist/color-C8OzjAeg.mjs +2 -0
  8. package/dist/contrast-BUKVao74.mjs +83 -0
  9. package/dist/derived.d.mts +14 -0
  10. package/dist/derived.mjs +391 -0
  11. package/dist/errors-D4ZbcwRo.d.mts +32 -0
  12. package/dist/frame-BoBKjgBB.mjs +92 -0
  13. package/dist/frame.d.mts +72 -0
  14. package/dist/frame.mjs +2 -0
  15. package/dist/index.d.mts +34 -0
  16. package/dist/index.mjs +1132 -0
  17. package/dist/instrument-Bdtk2INz.mjs +172 -0
  18. package/dist/lib-2OEIfPAE.d.mts +9 -0
  19. package/dist/lowered-CeoXbm0P.mjs +28 -0
  20. package/dist/node-CdhAGmF3.d.mts +229 -0
  21. package/dist/parent-vars.d.mts +13 -0
  22. package/dist/parent-vars.mjs +65 -0
  23. package/dist/plugin-types.d.mts +377 -0
  24. package/dist/plugin.d.mts +3 -0
  25. package/dist/plugin.mjs +30 -0
  26. package/dist/registry-n9JCX4pl.mjs +72 -0
  27. package/dist/runtime-CTvuM-Hm.d.mts +5 -0
  28. package/dist/runtime.d.mts +18 -0
  29. package/dist/runtime.mjs +21 -0
  30. package/dist/scope-oWEaWE9n.d.mts +2095 -0
  31. package/dist/sheet-CO7a-mdA.mjs +756 -0
  32. package/dist/surface.d.mts +228 -0
  33. package/dist/surface.mjs +101 -0
  34. package/dist/theme-BGEbFAYh.mjs +906 -0
  35. package/dist/theme-types-DHdtTZhz.d.mts +1803 -0
  36. package/dist/trace-otlp-CbKFDsn9.mjs +14 -0
  37. package/dist/tui-types.d.mts +181 -0
  38. package/dist/tui.d.mts +30 -0
  39. package/dist/tui.mjs +399 -0
  40. package/dist/types.d.mts +2 -0
  41. package/dist/vite.d.mts +118 -0
  42. package/dist/vite.mjs +980 -0
  43. package/docs/API.md +1148 -0
  44. package/package.json +81 -4
  45. package/skills/cssints/SKILL.md +200 -0
  46. package/skills/cssints/references/errors.md +85 -0
  47. package/skills/cssints/references/plugins.md +91 -0
  48. package/skills/cssints/references/tokens.md +91 -0
  49. package/skills/cssints-plugin/SKILL.md +177 -0
  50. package/skills/cssints-plugin/references/table.md +124 -0
  51. package/skills/cssints-plugin/references/testing.md +122 -0
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: cssints-plugin
3
+ description: >-
4
+ Write or fix a cssints plugin: a module or package that exports members of scope() from "cssints/plugin" (kernels,
5
+ chain/call/value members, conflicts, runtimes with polyfills/css/script/parentVars, lacking(), group markers, token
6
+ themes). Load it when code imports scope, fn, arg or lacking from "cssints/plugin", when you design a new
7
+ @cssints/* package or a member like stack(), motion() or fluid(), when a build says "kernel ... sets",
8
+ "wraps ... the marker of kernel", "scope ... is defined twice", "share a name", "kernels ... both set", or a
9
+ declaration file fails with TS7056/TS2742, and when you write its fixtures, type tests or browser tests.
10
+ ---
11
+
12
+ # cssints plugins on `scope()`
13
+
14
+ A plugin is a module that exports the members of `scope(name, table).build(impl).export()` from `cssints/plugin`.
15
+ The table says what the plugin has; the members are typed from it (no types file). User code imports the plugin with
16
+ `with { type: "cssints" }`; there is no registration. In user code a member is a class string (or a CSS value), so it
17
+ goes into `cn()`, `cx()` and conditions. Everything runs at build time in Vite's Module Runner. Read the user skill
18
+ `cssints` first for the styles themselves; the full reference is the "Plugins" section of `docs/API.md`.
19
+
20
+ If the cssints MCP server is configured, prefer `compile` to see a fixture's classes, sheet, errors and warnings,
21
+ `check_declaration` for a kernel's declarations, `check_support` for the BCD keys of `requires`/`polyfills`, and
22
+ `search_docs` for the API text.
23
+
24
+ ## The shape
25
+
26
+ ```ts
27
+ import { scope } from "cssints/plugin";
28
+
29
+ export const { ring } = scope("ring", {
30
+ "#core": {
31
+ css: (css) => [css.outline("var(--_w)", "solid", "var(--_c)"), css.outlineOffset("2px")],
32
+ },
33
+ ring: {
34
+ uses: ["core"],
35
+ chain: {
36
+ width: { args: "<length [0,∞]> | thin | thick", spacing: true, emit: (v) => [["--_w", v]] },
37
+ color: { args: "<color>", emit: (v) => [["--_c", v]] },
38
+ },
39
+ },
40
+ }).export();
41
+ // user code: cn(ring().width(1).color("red")) -> "_ring-core _a _b"; ring().width("red") is a type error
42
+ ```
43
+
44
+ - **Names.** The scope name is lowercase letters, digits and `-`, unique in the process (`cssints`, `derived` are
45
+ taken). `--_x` anywhere in the scope is `--<scope>-x`; `&` in raw CSS is the kernel's class `_<scope>-<entry>`;
46
+ `&(mod.key)` is the class of a kernel of an embedded scope. Your CSS never writes the long names.
47
+ - **Private keys** start with `#`: used by other entries, not exported. `uses` names them without the `#`, so `#trim`
48
+ beside a public `trim` is an error (`#trim and trim share a name: rename one`).
49
+ - **A key is a name** (no dots). A member shown as `fluid.container` is a key `fluidContainer` with
50
+ `label: "fluid.container"`, attached by hand (`Object.assign`).
51
+ - `.build({ member: fn })` holds the functions of call members; a scope with only chains and values skips it.
52
+
53
+ The table entries by shape (syntax names, typed `-props`, kernels, globals, layers, runtimes, conflicts, token sets,
54
+ embedded scopes, chain, call and value members): [references/table.md](references/table.md).
55
+
56
+ ## Kernels: styles or raw CSS
57
+
58
+ A kernel is global CSS printed once in layer `_.k`, keyed to its marker class. Write it **as styles** by default:
59
+ `css: (css) => [...]` gets the `css.*` members, checked by the grammar like a user's declarations, plus:
60
+
61
+ - conditions: `css.hover(...)`, `css.media("(...)")(...)`, `attr()`, `within()`, `container()`, `supports()`;
62
+ - `css.nest(selector)(...)` for other elements: one complex selector with `&` (the kernel's class) anywhere:
63
+ `"& > *"`, `"& > * + *"`, `".dark &"`, `":root:has(&)"` (a list is an error);
64
+ - `css.vars({ "--_gap": "1rem" })` for custom properties (values not checked);
65
+ - `keyframes()`/`positionTry()` names as values (`css.vars({ "--_k": fade })`): their rules go with the kernel; a
66
+ `css.keyframes({ from: css.vars({ "--_p": "100%" }) })` made in the kernel animates the scope's `--<scope>-p`.
67
+
68
+ A styles kernel **derives its conflicts** (every property that meets one it sets, as `cn()` decides overlap) and its
69
+ support warnings are **covered** by the `polyfills` of a runtime in the same `uses`. Raw CSS (`css: "&{...}"`) is still
70
+ needed for:
71
+
72
+ - at-rules the styles cannot write: `@container style(...)` alternation (frame, surface), `&&` specificity tricks;
73
+ - a value the grammar does not know yet (write it raw, with a test that fails once the grammar has it);
74
+ - a declaration whose support warning nothing should cover and nothing should print (balance's `pretty()`).
75
+
76
+ Runtime fallback CSS (`@supports not (...) { & {...} }`) is a raw string in the runtime entry, not a kernel.
77
+
78
+ A raw kernel derives no conflicts and is not compared with others: list a `conflicts` entry. Two styles kernels of
79
+ one member that set one element property are an error (`kernels zap and view both set animation, ...: write those in
80
+ one kernel`). `atProperty` holds `@property` rules; `requires` holds BCD keys (warnings, never errors);
81
+ `{ key, exact: true }` counts a partial implementation as lacking.
82
+
83
+ A kernel cannot be conditional per site. A member whose style is only markers (no declarations: no method, no
84
+ arguments) under `hover()` or `media()` is a build error (`:hover wraps _x-core, the marker of kernel x.core, with no
85
+ declaration of its own`). Give the member methods that set custom properties; the kernel reads them, and a condition
86
+ can wrap a method.
87
+
88
+ ## Members
89
+
90
+ - **Chain** `{ uses, chain: { m: { args, spacing?, excludes?, emit } } }`: `x().m(v)`, each method once. `args` is a
91
+ grammar of the parts (`""` for no value); `spacing: true` makes a bare number a 0.25rem step; `emit(v)` returns
92
+ `[prop, value][]` (custom properties unchecked, others checked). Write grammars as literals (`as const` on a
93
+ `const` string).
94
+ - **Call** `{ args, uses? }` plus `build({ x: (...args) => ({ decls?, uses?, globals?, themes?, layers? }) })`. `args`:
95
+ a grammar, `arg<T>()` (a TypeScript value, as is), or a tuple of positions: grammars, `"$tokens"`, `"$overrides"`
96
+ (checked against the `$tokens` before it), `"$selector"`, `arg<T>()`, and at the end one object of named leaves
97
+ (`{ "size?": "<length>" }`). Use `args: []` for a member with no arguments.
98
+ - **Value** `{ args, value, label? }`: returns a CSS value, not a class. `value` gets the parts as text (a token as
99
+ its `var(--name)`). With a tuple or `arg<T>()` write `fn({ args, value })`. Return a Typed OM value to keep the
100
+ type (`new CSSColorValue(...)`, `CSS.rem()`, `class X extends CSSStyleValue<"length">`, `CSSImageValue`, all from
101
+ `"cssints"` without the attribute in a plugin module); a string (a template literal too) is opaque to the types
102
+ and checked where it lands.
103
+ - **`group: true`** on a chain or call member adds the group marker `_g-<scope>`, so
104
+ `within(attr("data-stuck", "top", "~="), "<scope>")(...)` reads that element. One group per scope.
105
+ - **Typed properties**: a key that starts with `-` and a grammar (`"-webkit-font-smoothing": "auto | antialiased"`)
106
+ is exported camel-cased (`webkitFontSmoothing`). A property mdn-data knows is an error: use `css.<name>`.
107
+ - **Plain functions** the plugin exports (wrapping the user's styles, as `stuck(...styles)`) are fine; anything they
108
+ throw while a site runs is located at that site.
109
+
110
+ ## Conflicts
111
+
112
+ - **Derived** from styles kernels: `cn(stack(), display("grid"))` gives
113
+ `layout: kernel stack-core sets display, flex-direction, gap (this style also sets display)`. Custom properties and
114
+ `nest()` rules on another subject are left out (a child's margin is not the element's; `.dark &` and `&.on` are
115
+ the element and count).
116
+ - **Explicit** `{ props: ["outline", /^outline-/], message: "ring() sets the outline: use its methods" }` in `uses`
117
+ **replaces** what the kernels derive; `props: []` allows everything. Needed for raw kernels, and when a member
118
+ emits a property its own kernel sets (the check reads the member's emits too).
119
+
120
+ ## Runtimes: fallbacks for targets that lack a feature
121
+
122
+ `"#fb": { polyfills: [bcdKeys], css?, script?, parentVars? }` in a member's `uses`. A client module that uses the
123
+ member imports `virtual:cssints/runtime/<scope>.<entry>`; it is empty (`export {};`) unless a browserslist target
124
+ lacks a key.
125
+
126
+ - `css`: fallback rules, added to `_.k` only then (usually `@supports not (...) { & {...} }`); reaches every target.
127
+ - `script`: a module specifier the runtime module imports (`"@cssints/reveal/runtime"`), never evaluated at build
128
+ time. Ship it as a package subpath with a `development` condition for `src/`; it probes the feature itself
129
+ (`CSS.supports`) and does nothing where it is native. Reaches every target.
130
+ - `parentVars: { marker: "&", read, write, watch? }`: the shared `cssints/parent-vars` script copies custom
131
+ properties from parent to child; it stands on `@property`, so it reaches only targets that have it.
132
+ - Coverage: a `requires` key or a styles-kernel warning whose BCD key is in `polyfills` warns only for targets the
133
+ runtime does not reach. Match keys exactly (the key is in parentheses at the end of the warning).
134
+ - `lacking(...keys)` returns the build's targets that lack a key (`["firefox 140"]`, `[]` when none): emit fallback
135
+ rules or styles only when needed (`stuck()` adds its attribute rules only then).
136
+ - `dependsOn(path)`: call it for every file the plugin reads (an image, an SVG, a font). In dev an edit of the file
137
+ runs the modules that read it again; in `vite build --watch` it is a watched file.
138
+
139
+ ## Errors
140
+
141
+ What `build`, `value`, `emit` or a kernel's styles throw is a located `MemberError` at the call, prefixed with the
142
+ scope. Throw `new Error("cssints: <scope>: <what>, <what to do>")`; name the value and the fix. The grammar errors of
143
+ `args` come for free (`ring.width: unexpected "red" ...`, a tuple's `tint.shade(2).by: ...`).
144
+
145
+ ## Exporting, packaging
146
+
147
+ When the inferred type is too long to print (TS7056) or names an internal file (TS2742) in `vp pack`/`dts`:
148
+
149
+ ```ts
150
+ const balanceScope = scope("balance", { ... }).build({ balance: () => ({}) });
151
+ type Members = ReturnType<typeof balanceScope.export>;
152
+ const members: Members = balanceScope.export();
153
+ export const balance: Members["balance"] = members.balance;
154
+ ```
155
+
156
+ A package: `cssints` in `peerDependencies`; `exports` with `development` (src) before `types`/`default` (dist); a
157
+ `./runtime` subpath for a script (with `sideEffects` listing it); no `.css` file. Export the scope itself
158
+ (`export const motionScope = ...`) when another plugin should embed it.
159
+
160
+ ## Testing
161
+
162
+ Fixtures through the engine (output, errors, a re-run per `BROWSERSLIST`), a type test with `@ts-expect-error`, and
163
+ `test:browsers` with a control that must fail: [references/testing.md](references/testing.md).
164
+
165
+ ## Checklist
166
+
167
+ - [ ] Scope name unique and lowercase; private keys for everything that is not a member.
168
+ - [ ] Kernels as styles unless one of the raw cases above; raw kernels have a `conflicts` entry.
169
+ - [ ] Every member that may go under a condition has a method or argument that emits a declaration.
170
+ - [ ] Defaults of custom properties set on the kernel (`vars()`), so a nested member does not inherit its parent's.
171
+ - [ ] `requires` lists the BCD keys; each has a runtime in `polyfills`, or the warning is intended.
172
+ - [ ] Fallback CSS and scripts only when `lacking()` or a runtime says so; a script probes and stays off natively.
173
+ - [ ] Errors name the scope, the value and the fix; grammars are literals.
174
+ - [ ] Fixtures: output, each error, `BROWSERSLIST` modern and old, the runtime module empty or not.
175
+ - [ ] Type test: valid calls typed `string` (or the value type), invalid ones under `@ts-expect-error`.
176
+ - [ ] Browser test (if there is a runtime or a visual claim): native, fallback forced, and a control that fails.
177
+ - [ ] README: example, the CSS it prints, rules, browsers, limits; `docs/API.md` table row if in the cssints repo.
@@ -0,0 +1,124 @@
1
+ # The entries of a `scope()` table
2
+
3
+ Entries are told apart by their shape. A key that starts with `#` is private (not exported, invisible to an embedding
4
+ scope); `uses` names it without the `#`. `--_x` is `--<scope>-x` everywhere in the scope; in raw CSS `&` is the
5
+ kernel's class and `&(mod.key)` the class of a kernel of an embedded scope.
6
+
7
+ | Shape | What it is |
8
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
9
+ | `width: "<length [0,∞]> \| thin"` | A local syntax name: `<width>` in this scope's grammars |
10
+ | `"-webkit-font-smoothing": "auto \| antialiased"` | A typed property not in mdn-data, exported camel-cased (`webkitFontSmoothing`) |
11
+ | `{ css, atProperty?, requires? }` | A kernel: `css` a string (raw) or `(css) => style \| style[]` |
12
+ | `{ rule, layer? }` | A global rule (no class), identified by its content |
13
+ | `{ layers: ["app", "viewer"], before? }` | Top-level layers, after the engine's (or before with `before: true`) |
14
+ | `{ polyfills, css?, script?, parentVars? }` | A runtime: the fallback for targets that lack a BCD key |
15
+ | `{ props, message }` | Conflicts: a `cn()` with the member and one of `props` (names or RegExps) is an error |
16
+ | `{ tokens }` | A token set from `createTokens`: `<tokens.color.accent>` in grammars is that token's type |
17
+ | `mod: otherScope` | An embedded scope: `uses: ["mod.key"]`, `<mod.name>` in grammars, `&(mod.key)` in raw CSS |
18
+ | `{ uses?, group?, chain: { ... } }` | A chain member: `x().m(v).n(w)` |
19
+ | `{ args, uses?, group? }` | A call member: its function is the entry of the same name in `.build({ ... })` |
20
+ | `{ args, value, label? }` / `fn({ ... })` | A value member: returns a CSS value (`fn()` when `args` is a tuple or `arg<T>()`) |
21
+
22
+ ## Kernels
23
+
24
+ ```ts
25
+ "#core": {
26
+ css: (css) => [
27
+ css.vars({ "--_gap": "1rem", "--_fade": fade }), // defaults on the element; a keyframes() name carries its rule
28
+ css.display("flex"),
29
+ css.animation("var(--_fade) 200ms"),
30
+ css.hover(css.opacity(0.8)),
31
+ css.media("(width >= 40rem)")(css.padding("1rem")),
32
+ css.nest("& > * + *")(css.marginInlineStart("var(--_gap)")), // another subject: & anywhere
33
+ ],
34
+ requires: ["css.at-rules.property", { key: "css.types.color.oklch.relative_syntax", exact: true }],
35
+ },
36
+ "#raw": {
37
+ css: "& { --_x: 1 } &(base.mark) > & { --_y: 2 }",
38
+ atProperty: '@property --_w { syntax: "<length>"; inherits: false; initial-value: 2px; }',
39
+ },
40
+ ```
41
+
42
+ - Printed once in `_.k`, sorted by key (`<scope>.<entry>`), whatever the order of `uses`; at-rule kernels
43
+ (`keyframes()`) before the kernel that names them. A styles kernel prints in the sheet's order (a shorthand before
44
+ its longhands; fewer conditions first), so the order you write does not matter.
45
+ - Not allowed in a styles kernel: `firstThatWorks()`, a member's marker (another plugin member), `nest()` without an
46
+ `&` or with a selector list.
47
+ - Build errors: two keys that declare one custom property, two keys that print one marker, two styles kernels of one
48
+ member that set one element property.
49
+
50
+ ## Chain members
51
+
52
+ ```ts
53
+ ring: {
54
+ uses: ["core", "fallback"],
55
+ group: true, // adds _g-<scope> after the kernel markers
56
+ chain: {
57
+ width: { args: "<width>", spacing: true, emit: (v) => [["--_w", v]] },
58
+ color: { args: "<color>", emit: (v) => [["--_c", v]] },
59
+ plain: { args: "", excludes: ["color"], emit: () => [["--_c", "currentColor"]] }, // no value; color() no longer offered
60
+ },
61
+ },
62
+ ```
63
+
64
+ `emit` returns `[property, value][]`: a custom property's value is not checked, any other property is checked like a
65
+ `css.*` call (and counts for conflicts).
66
+
67
+ ## Call members
68
+
69
+ ```ts
70
+ look: { args: ["<color>", "$tokens", "$overrides", { "size?": "<base.gap>", "in?": "app | viewer" }], uses: ["order"] },
71
+ icon: { args: arg<"home" | "star">() },
72
+ reset: { args: [{ "media?": arg<boolean>(), "prose?": arg<boolean>() }] },
73
+ // ...
74
+ .build({
75
+ look: (color, tokens, overrides, { size, in: source }) => ({
76
+ decls: [["--_c", color], ["--_s", size ?? "0"]],
77
+ uses: ["base.mark"],
78
+ themes: [{ tokens, overrides, selector: ":root", layer: source ?? "app" }],
79
+ }),
80
+ icon: (name) => ({ decls: [["--_icon", name]] }),
81
+ reset: ({ media }) => ({
82
+ layers: { layers: ["reset"], before: true },
83
+ globals: media ? [{ css: "img { display: block }", layer: "reset" }] : [],
84
+ }),
85
+ })
86
+ ```
87
+
88
+ `build` returns `{ decls?, uses?, globals?, themes?, layers? }`:
89
+
90
+ - `decls`: declarations of the member's own class (in `_.a`, or under the user's condition).
91
+ - `uses`: more kernels, globals, layers or runtimes, by key or `mod.key`.
92
+ - `globals: { css, layer? }[]`: rules with no class, identified by content; may set one custom property in several
93
+ layers (looks).
94
+ - `themes: { tokens, overrides, selector, layer?, scope? }[]`: `createGlobalTheme` made by a member, checked against
95
+ the token set and read by `check.contrast`.
96
+ - `layers`: a list, or `{ layers, before: true }`.
97
+
98
+ The positional form puts a type error on the call, with the position in the message (`look(2): ...`), not on the
99
+ argument. A grammar part arrives as checked text; a token as its `var(--name)`.
100
+
101
+ ## Value members
102
+
103
+ ```ts
104
+ tint: { args: "<color> <percentage>", value: ([color, share]) => new CSSColorValue(`color-mix(in oklab, ${color}, white ${share})`) },
105
+ shade: fn({
106
+ args: ["<color>", { "by?": "<percentage>", "dark?": arg<boolean>() }],
107
+ label: "tint.shade",
108
+ value: ([c, { by = "10%", dark }]): string => `color-mix(in oklab, ${c}, ${dark ? "black" : "white"} ${by})`,
109
+ }),
110
+ ```
111
+
112
+ - With a grammar `args`, the parts are joined by a space, so `"<color> <color>"` cannot tell `"red", "blue"` from
113
+ `"red blue"`: check `parts.length` or use a tuple.
114
+ - `value` runs at build time, once per call; the call is replaced by its CSS text. It throws for values the grammar
115
+ cannot reject (units it cannot compute, a failed accessibility rule): `fluid` rejects `em` and fails WCAG 1.4.4.
116
+ - What `value` returns must be a string or a Typed OM value (`cssints: tint() value must return a string or a Typed OM
117
+ value`). A result that does not fit the property is that property's error at the same site.
118
+
119
+ ## Identity and HMR
120
+
121
+ A scope is its content (the table, the source of its functions, the scopes it embeds), hashed. Running the module
122
+ again with the same content is nothing; another content under the same name replaces it (an edit in dev). Two
123
+ modules that define one name with different content: `scope "x" is defined twice with different content`. A closure
124
+ the hash does not see (icons' collection) is fine as long as the table and code are the same.
@@ -0,0 +1,122 @@
1
+ # Testing a plugin
2
+
3
+ Three layers, as every `@cssints/*` package does them (`packages/balance`, `packages/stuck`, `packages/squircle` in
4
+ the cssints repo are the models): fixtures through the engine, a type test, and a browser test when there is a
5
+ runtime or a visual claim. Tests are plain `node` scripts with `node:assert`; no test runner.
6
+
7
+ ```
8
+ packages/<name>/
9
+ src/index.ts the scope
10
+ src/runtime.ts a browser script, if any (no imports)
11
+ test/<name>.mjs fixtures through the engine
12
+ test/fixtures/*.ts modules that use the members (some invalid on purpose)
13
+ test/types/<name>.ts the type test
14
+ test/browsers.mjs Chrome, Firefox and WebKit (optional)
15
+ ```
16
+
17
+ ## Fixtures through the engine
18
+
19
+ A Vite dev server in middleware mode with the plugin, a client transform per fixture, and the sheet of a module:
20
+
21
+ ```js
22
+ import assert from "node:assert/strict";
23
+ import { execFileSync } from "node:child_process";
24
+ import cssints from "cssints/vite";
25
+ import { createLogger, createServer } from "vite";
26
+
27
+ const root = `${import.meta.dirname}/fixtures`;
28
+ const mode = process.argv[2];
29
+ const warnings = [];
30
+ const plugins = cssints();
31
+ const server = await createServer({
32
+ root,
33
+ configFile: false,
34
+ customLogger: { ...createLogger("warn"), warn: (m) => warnings.push(m) },
35
+ server: { middlewareMode: true, watch: null, ws: false },
36
+ plugins,
37
+ });
38
+ const client = server.environments.client;
39
+ const code = async (f) => (await client.transformRequest(`/${f}`)).code;
40
+ const sheet = (f) => plugins[0].api.sheet([`${root}/${f}`]);
41
+ const flat = (css) => css.replace(/\s+/g, " ").trim();
42
+
43
+ const use = await code("use.ts"); // class strings in place of the calls
44
+ const out = flat(sheet("use.ts")); // the CSS of that module
45
+ const runtime = (await client.transformRequest("virtual:cssints/runtime/x.fb")).code;
46
+
47
+ assert.match(use, /export const a = "_x-core _\w+";/);
48
+ assert.ok(out.includes("._x-core{display:flex}"), out);
49
+ assert.deepEqual(warnings, []); // no support warning left uncovered
50
+ await assert.rejects(
51
+ code("conflict.ts"),
52
+ /x: kernel core sets display \(this style also sets display\)/,
53
+ );
54
+ await assert.rejects(code("condition.ts"), /:hover wraps _x-core, the marker of kernel x\.core/);
55
+ ```
56
+
57
+ Then the targets: run the same file again per browserslist, in a child process (`BROWSERSLIST` is read once per
58
+ process; a Vite root with its own `.browserslistrc` works in one process too):
59
+
60
+ ```js
61
+ if (mode) {
62
+ assert.equal(runtime === "export {};", mode === "native"); // empty where every target has the feature
63
+ assert.equal(out.includes("@supports not"), mode === "old"); // fallback CSS only where needed
64
+ await server.close();
65
+ process.exit(0);
66
+ }
67
+ await server.close();
68
+ for (const [m, targets] of [
69
+ ["native", "chrome 130, safari 18, firefox 130"],
70
+ ["old", "chrome 110, safari 17.3, firefox 120"],
71
+ ])
72
+ execFileSync(process.execPath, [import.meta.filename, m], {
73
+ env: { ...process.env, BROWSERSLIST: targets },
74
+ stdio: "inherit",
75
+ });
76
+ ```
77
+
78
+ What to cover: the output of each member and method; each build error (`assert.rejects` with the message); the
79
+ warnings for the default targets (none, or the intended ones); the runtime module and the fallback CSS for a modern
80
+ and an old target set; a member under a condition (a method: fine; marker only: the error); nesting (a member inside
81
+ another one does not inherit its custom properties).
82
+
83
+ To look at one fixture by hand inside the cssints repo: `cd packages/cssints/test && node run.mjs fixtures/x.ts`
84
+ prints the code and the CSS (a plugin fixture must live where `cssints` resolves). With the MCP server, `compile`.
85
+
86
+ ## The type test
87
+
88
+ `test/types/<name>.ts`, checked by the package's `typecheck` (`tsgo -p tsconfig.json --noEmit`, with
89
+ `test/types/*.ts` in `include`):
90
+
91
+ ```ts
92
+ import { ring } from "../../src/index.ts";
93
+
94
+ export const a: string = ring().width("2px").color("red");
95
+ // @ts-expect-error not a width
96
+ export const e1 = ring().width("red");
97
+ // @ts-expect-error a method is used once
98
+ export const e2 = ring().width("1px").width("2px");
99
+ ```
100
+
101
+ A value member's result goes into a property in the type test (`color(tint("red", "20%"))`), to catch a result type
102
+ the grammar rejects. Some errors only the build sees (a range in some positions, `em` where px is computed): test
103
+ those in the fixtures, not with `@ts-expect-error`.
104
+
105
+ ## Browser tests
106
+
107
+ `test/browsers.mjs`, run by `bun run test:browsers` (not in `test`: it needs the browsers). The shape:
108
+
109
+ 1. Build a fixture app with `vite build` (`write: false`) and a `BROWSERSLIST` that makes the fallback ship; set
110
+ `NODE_ENV=development` so the runtime resolves to `src/`.
111
+ 2. Assert on the built CSS and JS first (the `@supports not` rule is there, the script is in the bundle once).
112
+ 3. Open it in Chrome (`chromium.launch({ channel: "chrome" })`), Firefox and WebKit from `playwright-core`.
113
+ 4. For each browser, three runs:
114
+ - **native**: the feature works by itself, the fallback stays off;
115
+ - **fallback forced**: the native feature switched off (an `!important` override, or the declaration cut out of
116
+ the CSS), the runtime or fallback CSS must produce the same result;
117
+ - **control**: the same, with the fallback disabled (its observers replaced by no-ops, its rules removed). This
118
+ run **must fail** the check: it proves that the test can see the difference.
119
+ 5. Measure something visible: line widths, pixels of a screenshot, computed styles, the phase of an animation.
120
+
121
+ A check without a control that fails is not a test of the fallback. `BROWSERS=chrome,webkit node test/browsers.mjs`
122
+ runs some.