Ramonda

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: slidein is yours to pick — is left alone. A url() or a quoted string in the value does not make it one: cursor: url(a.cur), pointerr is still reported, on pointerr.
  • A runtime value in a declaration is refused — color: $(this.brand) — and the message names the two things to use instead: a match for a value that is one of a few, and @@property for one that really comes from data.
  • An expression is checked in the scope where it was written, wherever $( … ) is allowed: a condition, a spread, a match subject. this.loud resolves 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, ships color:"yellow", and is dropped by every browser. content: "hi" and font-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:

writtenwhat happens
opacty: 1 in a framereported — a frame holds ordinary properties
form { … }reported — a frame is from, to or a percentage
opacity: 0 outside any framereported — it belongs to no time, so the browser drops it
@@font-face with no srcreported — the descriptor is required, and the face would load nothing
font-familly: "Brand"reported, with the descriptor you meant
@@property with no inheritsreported — the browser drops the whole rule without it
@@property with no initial-valuereported — the browser drops the whole rule, unless the syntax is "*"
initial-value its syntax does not acceptreported — the browser drops the whole rule for that too
a registered property set to a value its syntax refusesreported — the browser keeps the initial-value and says nothing
&:hover { … } in eitherreported — 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-ignore with 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.

rulereported when
unknown-propertya property name that is nearly one CSS has
unknown-valuea word this property does not take
repeated-declarationthe same property set twice to the same value
hole-out-of-placea hole where CSS needs text, like a property name
block-as-a-jsx-attributeclassName=@@( … ) — a block is a value, so it goes in the braces
block-in-a-templatea block inside a template literal's ${ … }, where nothing can see it — use mergeClassNames
run-on-declarationa missing ;, so the next line joined this value
line-commenta // comment, which CSS does not have
unknown-unita unit CSS does not have
at-rule-out-of-placean at-rule that names something for the whole stylesheet
unknown-framea keyframe selector that is not one
declaration-out-of-placea declaration where only a rule belongs
rule-out-of-placea nested rule where only declarations belong
override-out-of-ordera declaration written to override one that will win anyway
custom-property-set-by-another-namea var() reading a name set with different capitals
hole-as-a-custom-property-namea 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-declarationa token set with no range, or outside it — in a block, a project stylesheet, or a style attribute
unknown-custom-propertya custom property made up in a block or a style attribute, when unknownCustomProperties is false
url-not-founda relative url( … ) naming a file that is not there, next to the file that holds the block
styles-another-elementa selector on another element — a child or a sibling — when styleOtherElements is false
unknown-media-featurea media feature that will never match
unknown-media-valuea media feature's value it does not have, or a unit no engine has (40remx), so the condition never matches
malformed-media-conditiona 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-featurea container feature CSS does not have, or a media feature such as hover in @container, so the condition never matches
unknown-container-valuea container feature's value it does not take ((min-width: 40)), so the condition never matches
malformed-container-conditiona container condition no engine reads — (min-width 40rem) with no colon, 3e0m, a word that is not and, or or not
supports-a-media-featurea media feature asked of @supports, which is always true or never
supports-never-truea @supports condition false by a slip — (display: gird), (gap: 1), a missing colon — never a feature a browser may lack
container-without-a-typea container named without its type — container: card sets the type back to normal, so a size condition never matches
number-without-a-unita number with no unit where a length goes — a browser drops it
fraction-where-a-whole-number-goesa number not written as a whole one — 1.5, 1.0, 1e2 — where a whole number goes; a browser drops it
value-and-registered-syntaxa value a registered custom property cannot hold
unit-not-alloweda unit your ramonda.css.ts does not allow
value-not-alloweda value outside the closed list your ramonda.css.ts gave this property
shorthand-not-alloweda shorthand your ramonda.css.ts switched off — write its longhands
word-out-of-its-longhanda word one longhand of a shorthand has no place for — CSS drops the whole declaration
narrower-after-a-whole-shorthanda narrower shorthand after a wider one, both reaching the stylesheet whole — no order keeps it winning
resets-differ-across-enginesa shorthand browsers reset differently, like -webkit-mask — the line renders two ways; write mask
value-differs-across-enginesa value browsers read differently, like animation: auto — the line renders two ways
string-not-alloweda quoted value where the property takes a keyword
property-not-a-namea value that must name a property and does not
non-canonical-spellingone 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-blocka 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-reasonramonda-css-ignore with nothing after it
unknown-prefixa vendor prefix that is not one of the four
unknown-at-rulean at-rule name CSS does not have
unknown-functiona function CSS does not have (d(…)), so the declaration is dropped
custom-functiona CSS custom function (--name(…) or @function), which Firefox drops
invalid-hex-colora hex colour without 3, 4, 6 or 8 hex digits (#abcx), so the declaration is dropped
malformed-numbera decimal point with no digit after it (0.), so the declaration is dropped
unit-of-the-wrong-kinda real unit of a kind the property does not take (z-index: 2x, width: 2s), so the declaration is dropped
invalid-valuea 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-selectora pseudo-class or pseudo-element that is not one — the whole rule is dropped
malformed-selectora selector no engine reads — &not(…) 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-flaga ! at the end of a value that is not !important
unclosed-calla ( 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-handvar(--…) written by hand for a token your ramonda.css.ts declares — write $group.…
block-refusedwhat the build refuses, shown in the editor in the build's own words — it cannot be switched off
too-many-valuesmore values than the property takes — in CSS, or in your ramonda.css.ts
missing-semicolona declaration with no ;, which swallows the line written under it
hardcoded-not-alloweda value written out where your ramonda.css.ts takes that kind from tokens
declaration-does-nothinga declaration another one on the same element switches off — valid CSS the browser ignores
style-prop-never-useda prop that takes a style block and never puts it on an element — type check
style-prop-overriddena declaration below the spread that clears what a caller may send — type check
registered-never-seta @@property a block reads and nothing sets, so every element gets its initial value — type check
blocks-joined-not-mergedtwo style blocks joined into one string, where mergeClassNames was meant — every class from both lands — type check
allow-list-not-cssa value in an allow-list that is not CSS — every caller sending it is refused — type check
cast-to-a-blocka value cast to a style block, which skips every check a block prop makes — type check
state-is-a-tuplea state in an allow-list typed [{ … }], which constrains its first declaration and nothing under it — type check
allow-list-is-an-interfacean allow-list written with interface, which can never match a block shape — write it as a type — type check
hole-not-alloweda runtime value in a declaration — write it out with match, or declare it with @@property
hole-in-a-match-arman arm holding a value the render computes, where a class belongs
match-arm-repeatedan arm that can never run, because one above it answers first
match-with-no-armsa 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.