What is checked
The syntax is not TypeScript, so the package owns a parser and a virtual-file layer — the same way
JSX is usable because somebody wrote the parser for it. That layer is what buys the checking: a fault
in a block arrives as an ordinary tsc diagnostic, on the character you wrote, in your own file.
Running it
The checker takes a project, not a file — every block becomes a virtual file and the whole program
goes to tsc once, which is what makes a report land on your own character:
npx ramonda-css # the `tsconfig.json` beside you
npx ramonda-css app/tsconfig.json
There is no check verb: the bare command IS the check, and format, lint, codegen and
explain are the ones that are not. It exits non-zero when it reports anything, so it is a CI step
as it stands.
A build stops on every CSS rule below, and it does not run the type check. What only the types
catch — a match key its subject can never be, or a value of the subject with no arm — is reported
by this command and by your editor, not by Vite or esbuild. Run it in CI beside the build.
What your editor tells you
- A property that does not exist is TypeScript's own did you mean, on the property.
- A value the property does not take is reported the same way, with the values it does take.
A property whose grammar admits a name nobody can judge —
animation-name: slideinis yours to pick — is left alone. Aurl()or a quoted string in the value does not make it one:cursor: url(a.cur), pointerris still reported, onpointerr. - A runtime value in a declaration is refused —
color: $(this.brand)— and the message names the two things to use instead: amatchfor a value that is one of a few, and@@propertyfor one that really comes from data. - An expression is checked in the scope where it was written, wherever
$( … )is allowed: a condition, a spread, amatchsubject.this.loudresolves to the field beside it, because the expression stays where you put it. - A condition or a selector spelled two ways is reported, with the spelling to use.
@media (min-width:40rem)and@media (min-width: 40rem)are the same CSS, so writing both is writing two of what should be one thing. - A quoted value on a property with no place for a string is reported. Your editor completes a
value from a real union the way it completes any string literal, so
color: "yellow"is an easy thing to end up with — and it compiles, shipscolor:"yellow", and is dropped by every browser.content: "hi"andfont-family: "Brand"are correct CSS and stay silent, because the question is asked of each property's own grammar.
One spelling
Two spellings of the same CSS are two things to search for and two to keep in step. Where a
declaration, a condition or a selector has a canonical form, the other one is reported and
ramonda-css format writes the canonical one:
const card = @@(
COLOR: red;
@media (min-width:40rem) { color: blue; }
);
Both of those are correct CSS — a browser reads COLOR and color as one property, and a media
query with no space after the colon is the same query. What they cost is a second thing to grep for.
CSS does not mind either. Your repository does.
Inside a named block, the vocabulary is its own
@@keyframes, @@font-face and @@property each hold something different from an element's rule,
and the check follows that:
| written | what happens |
|---|---|
opacty: 1 in a frame | reported — a frame holds ordinary properties |
form { … } | reported — a frame is from, to or a percentage |
opacity: 0 outside any frame | reported — it belongs to no time, so the browser drops it |
@@font-face with no src | reported — the descriptor is required, and the face would load nothing |
font-familly: "Brand" | reported, with the descriptor you meant |
@@property with no inherits | reported — the browser drops the whole rule without it |
@@property with no initial-value | reported — the browser drops the whole rule, unless the syntax is "*" |
initial-value its syntax does not accept | reported — the browser drops the whole rule for that too |
a registered property set to a value its syntax refuses | reported — the browser keeps the initial-value and says nothing |
&:hover { … } in either | reported — a descriptor list has no element to select against |
An expression may not go in one of these at all: these name something the whole stylesheet uses, and there is no element for a value to come from.
A media feature that will never match is the row worth pausing on, because it is not invalid
CSS. A browser reads @media (min-widht: 40rem), keeps the rule, and never matches it — so every
declaration inside is inert and nothing else has any reason to complain.
When a rule is wrong
Every rule here fails the build. There is no warning level, and that is deliberate: a warning nobody must act on is a warning nobody reads.
So there is one way to say I looked at this and it stays:
/* ramonda-css-ignore a vendor stylesheet defines this one */
display: flexx;
It covers the next line only, so it cannot creep past what you looked at. It works in a //
comment too, for a finding about the block itself rather than about a line in it.
A reason is required. A directive with nothing after it is refused, and does not exempt the line below it either:
a
ramonda-css-ignorewith no reason after it is a silence, not a record.
And every one is printed on every run, whether or not anything failed:
[ramonda-css] 1 `ramonda-css-ignore`, honoured:
src/Card.tsx:12 a vendor stylesheet defines this one
That is what makes it a record rather than a silence. A reason that has stopped being true is one somebody meets, instead of one they would have to go looking for.
What your project adds to that
Everything above is CSS being checked against itself, and it needs no config. A
ramonda.css.ts adds reports that are yours rather than CSS's — a unit this
project does not use, a value outside a list it gave, a shorthand it switched off, a colour written
out where it takes colours from tokens, a $ path it does not declare. Each is a row in the
table below, and each fails the build the same way.
If that file cannot be read, your editor says so on the block. The completions keep working, but nothing is being checked against rules that did not load, and a green file would be a claim the tool cannot support:
[config-not-read]… does not parse: TS1136: Property assignment expected. Until it reads, none of this project's rules are running — this block is not being checked against them, and a build will refuse before it gets here.
Every rule
A typo in one of these ids is caught too: writing unknown-unti in
ramonda.css.ts tells you so, and names the one you meant.
82 rules. Every rule that reads a block fails the build. The 8 marked type check need TypeScript's answer about a type, so the editor and ramonda-check report them and the build — which does not type-check — does not. Each is a key you can switch off, except block-in-a-template and block-refused: the build refuses those whatever a config says, so switching one off would only quiet the editor.
| rule | reported when |
|---|---|
unknown-property | a property name that is nearly one CSS has |
unknown-value | a word this property does not take |
repeated-declaration | the same property set twice to the same value |
hole-out-of-place | a hole where CSS needs text, like a property name |
block-as-a-jsx-attribute | className=@@( … ) — a block is a value, so it goes in the braces |
block-in-a-template | a block inside a template literal's ${ … }, where nothing can see it — use mergeClassNames |
run-on-declaration | a missing ;, so the next line joined this value |
line-comment | a // comment, which CSS does not have |
unknown-unit | a unit CSS does not have |
at-rule-out-of-place | an at-rule that names something for the whole stylesheet |
unknown-frame | a keyframe selector that is not one |
declaration-out-of-place | a declaration where only a rule belongs |
rule-out-of-place | a nested rule where only declarations belong |
override-out-of-order | a declaration written to override one that will win anyway |
custom-property-set-by-another-name | a var() reading a name set with different capitals |
hole-as-a-custom-property-name | a hole naming a custom property rather than holding a value |
initial-value-and-syntax | @@property with no initial-value, or one its syntax does not accept — the browser drops the rule |
property-descriptor-missing | @@property with no syntax or no inherits — the browser drops the rule; the build's own words for what the editor's type says |
token-set-against-its-declaration | a token set with no range, or outside it — in a block, a project stylesheet, or a style attribute |
unknown-custom-property | a custom property made up in a block or a style attribute, when unknownCustomProperties is false |
url-not-found | a relative url( … ) naming a file that is not there, next to the file that holds the block |
styles-another-element | a selector on another element — a child or a sibling — when styleOtherElements is false |
unknown-media-feature | a media feature that will never match |
unknown-media-value | a media feature's value it does not have, or a unit no engine has (40remx), so the condition never matches |
malformed-media-condition | a media condition no engine reads — (min-width 40rem) with no colon, 4r0em, a word between conditions that is not and or or — kept and never matching |
unknown-container-feature | a container feature CSS does not have, or a media feature such as hover in @container, so the condition never matches |
unknown-container-value | a container feature's value it does not take ((min-width: 40)), so the condition never matches |
malformed-container-condition | a container condition no engine reads — (min-width 40rem) with no colon, 3e0m, a word that is not and, or or not |
supports-a-media-feature | a media feature asked of @supports, which is always true or never |
supports-never-true | a @supports condition false by a slip — (display: gird), (gap: 1), a missing colon — never a feature a browser may lack |
container-without-a-type | a container named without its type — container: card sets the type back to normal, so a size condition never matches |
number-without-a-unit | a number with no unit where a length goes — a browser drops it |
fraction-where-a-whole-number-goes | a number not written as a whole one — 1.5, 1.0, 1e2 — where a whole number goes; a browser drops it |
value-and-registered-syntax | a value a registered custom property cannot hold |
unit-not-allowed | a unit your ramonda.css.ts does not allow |
value-not-allowed | a value outside the closed list your ramonda.css.ts gave this property |
shorthand-not-allowed | a shorthand your ramonda.css.ts switched off — write its longhands |
word-out-of-its-longhand | a word one longhand of a shorthand has no place for — CSS drops the whole declaration |
narrower-after-a-whole-shorthand | a narrower shorthand after a wider one, both reaching the stylesheet whole — no order keeps it winning |
resets-differ-across-engines | a shorthand browsers reset differently, like -webkit-mask — the line renders two ways; write mask |
value-differs-across-engines | a value browsers read differently, like animation: auto — the line renders two ways |
string-not-allowed | a quoted value where the property takes a keyword |
property-not-a-name | a value that must name a property and does not |
non-canonical-spelling | one CSS written two ways — ramonda-css format fixes it |
layer-in-a-block | @layer inside a block, which the sheet already decides |
root-in-a-block | :root or html inside a block, which puts the root under the element — it applies nowhere |
spread-out-of-place | ...$(block) somewhere a whole block cannot go |
hole-in-a-named-block | a hole in @@keyframes( … ) and its kind, which have no element |
unknown-named-block | @@name( … ) where the name is not a site this compiles |
composition-in-a-named-block | ...$(block) or when inside a named site, which composes nothing |
ignore-without-a-reason | ramonda-css-ignore with nothing after it |
unknown-prefix | a vendor prefix that is not one of the four |
unknown-at-rule | an at-rule name CSS does not have |
unknown-function | a function CSS does not have (d(…)), so the declaration is dropped |
custom-function | a CSS custom function (--name(…) or @function), which Firefox drops |
invalid-hex-color | a hex colour without 3, 4, 6 or 8 hex digits (#abcx), so the declaration is dropped |
malformed-number | a decimal point with no digit after it (0.), so the declaration is dropped |
unit-of-the-wrong-kind | a real unit of a kind the property does not take (z-index: 2x, width: 2s), so the declaration is dropped |
invalid-value | a value its property's grammar cannot read — rgb(1x 2 3), linear-gradient(redx, blue), aspect-ratio: 16 9 — so the declaration is dropped |
unknown-selector | a pseudo-class or pseudo-element that is not one — the whole rule is dropped |
malformed-selector | a selector no engine reads — ¬(…) with no colon, :nth-child(2+1), a bracket left open, an attribute flag other than i or s — so the whole rule is dropped |
unknown-flag | a ! at the end of a value that is not !important |
unclosed-call | a ( in a value that no ) closes — the value runs past the end of the block |
unknown-token | $group.… naming a token your ramonda.css.ts does not declare |
token-by-hand | var(--…) written by hand for a token your ramonda.css.ts declares — write $group.… |
block-refused | what the build refuses, shown in the editor in the build's own words — it cannot be switched off |
too-many-values | more values than the property takes — in CSS, or in your ramonda.css.ts |
missing-semicolon | a declaration with no ;, which swallows the line written under it |
hardcoded-not-allowed | a value written out where your ramonda.css.ts takes that kind from tokens |
declaration-does-nothing | a declaration another one on the same element switches off — valid CSS the browser ignores |
style-prop-never-used | a prop that takes a style block and never puts it on an element — type check |
style-prop-overridden | a declaration below the spread that clears what a caller may send — type check |
registered-never-set | a @@property a block reads and nothing sets, so every element gets its initial value — type check |
blocks-joined-not-merged | two style blocks joined into one string, where mergeClassNames was meant — every class from both lands — type check |
allow-list-not-css | a value in an allow-list that is not CSS — every caller sending it is refused — type check |
cast-to-a-block | a value cast to a style block, which skips every check a block prop makes — type check |
state-is-a-tuple | a state in an allow-list typed [{ … }], which constrains its first declaration and nothing under it — type check |
allow-list-is-an-interface | an allow-list written with interface, which can never match a block shape — write it as a type — type check |
hole-not-allowed | a runtime value in a declaration — write it out with match, or declare it with @@property |
hole-in-a-match-arm | an arm holding a value the render computes, where a class belongs |
match-arm-repeated | an arm that can never run, because one above it answers first |
match-with-no-arms | a match that sets nothing whatever its subject is |
Next
- The config file — switching a rule off, and making the rest stricter than CSS is.
- Tooling — what your formatter and linter can and cannot read.