This document is the human- and agent-readable summary of the lint rules enforced across the Magma monorepo. The canonical sources of truth are the config files; if anything here ever conflicts with them, the config wins.
| Layer | Root config | Project overrides |
|---|---|---|
| TypeScript / JS | eslint.config.mjs |
projects/stencil/eslint.config.mjs, projects/design-tokens/eslint.config.mjs |
| CSS | stylelint.config.mjs |
projects/stencil/stylelint.config.mjs |
Enforcement runs in three places:
- On save - if your editor has ESLint and Stylelint extensions installed
- On commit - via the
.husky/pre-commithook (lint + format on staged files) - In CI - the
lintworkflow (.github/workflows/lint.yml) runsnpm run linton every pull request and on every push todev,mainandbeta; a lint error fails the check
The root config extends:
@eslint/jsrecommendedtypescript-eslintrecommended
Common settings:
- Target: ECMAScript 2021 (
ecmaVersion: 12),modulesource type - Globals: browser + node
- Ignored:
**/node_modules,**/dist,**/.cache,**/*.config.*js, plus everything in.gitignore
projects/stencil/eslint.config.mjs layers additional rules on top of the base:
-
JSX pragmas not unused -
handFragmentimports are not flagged as unused. They're required for Stencil's TSX compilation.// ✅ allowed even if `h` looks unused - Stencil's JSX needs it import { h, Fragment, Component } from '@stencil/core';
-
@typescript-eslint/no-unused-varsiswarn, not error - drift won't block commits, but should be cleaned up. -
Stencil plugin (
@stencil/eslint-plugin) - itsflat/recommendedrule set applies tosrc/components/**/*.tsx(excluding.storybook/**and*.stories.*). -
stencil/strict-mutableis off - mutable@Prop()is allowed when needed. -
stencil/reserved-member-namesis off - theautofocus/autoFocusprops of mds-button, mds-button-dropdown, mds-input, mds-input-select and mds-input-switch mirror the native attribute on purpose; renaming them would break the public API. -
Storybook plugin (
eslint-plugin-storybook) -flat/recommendedapplies to story files. -
Ignored directories:
react,angular(these are auto-generated framework adapters and are not linted).
projects/design-tokens/eslint.config.mjs adds Jest globals (the token build has test fixtures) and ignores node_modules, dist, build.
# In projects/stencil/
npm run lint.ts # check
npm run lint:fix # auto-fix what's fixable
# Monorepo-wide
nx run-many --target=lint --allstylelint.config.mjs extends:
stylelint-config-standardstylelint-config-recess-orderstylelint-order
The rules below are the ones that constrain authoring decisions. Many additional internal-correctness rules are also enforced - see the config for the full list.
| Rule | Effect |
|---|---|
order/order |
Custom properties (--foo: …) come before regular declarations within a block |
order/properties-alphabetical-order |
Declarations inside a block are sorted A→Z |
declaration-block-single-line-max-declarations: 1 |
One declaration per line |
block-no-empty |
Empty blocks are forbidden |
declaration-empty-line-before |
Blank line before declarations, with exceptions for first-nested / after-comment / after-declaration |
custom-property-empty-line-before |
Blank line before custom properties, with same exceptions plus after-custom-property |
rule-empty-line-before |
Blank line before multi-line rules, except first-nested |
/* ✅ correct */
.foo {
--foo-bg: rgb(0 0 0);
background-color: rgb(255 255 255);
color: rgb(0 0 0);
display: flex;
}
/* 🚫 wrong: property order, multiple decls on one line, empty block */
.bar {
display: flex;
color: rgb(0 0 0);
background-color: rgb(255 255 255);
}
.baz {
}| Rule | Effect |
|---|---|
color-function-notation: 'modern' |
Use rgb(0 0 0 / 0.5), not rgba(0, 0, 0, 0.5) |
color-hex-length: 'long' |
Use #ffffff, not #fff |
color-named: 'never' |
No CSS named colors (red, white, aliceblue, …) |
alpha-value-notation: 'number' |
Use / 0.5, not / 50% |
hue-degree-notation: 'number' |
Use hsl(120 …), not hsl(120deg …) - though hsl is also disallowed (below) |
function-disallowed-list: ['hsl', 'lch'] |
Use rgb() (typically with token vars), not hsl() or lch() |
function-no-unknown |
Unknown color functions are rejected |
/* ✅ correct */
.foo {
background-color: rgb(255 255 255);
color: rgb(var(--tone-neutral-03) / 0.5);
}
/* 🚫 wrong */
.foo {
background-color: white; /* named color */
background-color: #fff; /* short hex */
color: rgba(0, 0, 0, 0.5); /* legacy notation */
color: rgb(0 0 0 / 50%); /* percentage alpha */
color: hsl(0 0% 0%); /* hsl disallowed */
}| Rule | Effect |
|---|---|
unit-disallowed-list: ['cm','em','ex','in','mm','pc','pt'] |
Only px, rem, %, vh, vw, fr, s, ms, etc. Never em |
number-max-precision: 4 |
Max 4 decimal places (units % and unitless are exempt) |
time-min-milliseconds: 100 |
Animation/transition durations must be ≥ 100 ms |
length-zero-no-unit: null |
0px is allowed (rule is disabled) |
/* ✅ correct */
.foo {
font-size: 1rem;
padding: 16px;
transition: opacity 200ms ease;
}
/* 🚫 wrong */
.foo {
font-size: 1em; /* em disallowed */
padding: 1in; /* in disallowed */
transition: opacity 50ms; /* below 100ms minimum */
}| Rule | Effect |
|---|---|
selector-class-pattern: '^[a-z-_]+' |
Class names are lowercase, kebab- or snake-case |
selector-attribute-quotes: 'always' |
Always quote attribute selectors: [type="button"], not [type=button] |
selector-type-case: 'lower' |
Element selectors lowercase |
selector-pseudo-class-no-unknown |
Unknown pseudo-classes rejected |
selector-pseudo-element-no-unknown |
Unknown pseudo-elements rejected |
selector-type-no-unknown |
Unknown element selectors rejected, except custom elements (mds-*) and namespaces |
no-duplicate-selectors |
Same selector cannot appear twice in a file |
/* ✅ correct */
.my-button { … }
mds-button[type="primary"] { … }
/* 🚫 wrong */
.MyButton { … } /* PascalCase */
[type=button] { … } /* unquoted attribute */| Rule | Effect |
|---|---|
property-disallowed-list: ['background'] |
Don't use the background shorthand. Use background-color, background-image, etc. |
property-no-vendor-prefix |
No -webkit-, -moz-, etc. Exception: -webkit-overflow-scrolling |
value-no-vendor-prefix |
No vendor-prefixed values either |
value-keyword-case: 'lower' |
Keyword values lowercase: display: flex, not display: Flex |
font-weight-notation: 'numeric' |
Use font-weight: 400, not font-weight: normal |
shorthand-property-no-redundant-values |
padding: 0 0 0 0 is rejected - use padding: 0 |
declaration-block-no-shorthand-property-overrides |
Don't override a longhand with a later shorthand (and vice versa) in the same block |
at-rule-no-vendor-prefix |
No vendor-prefixed at-rules |
/* ✅ correct */
.foo {
background-color: rgb(255 255 255);
background-image: url('hero.png');
font-weight: 400;
}
/* 🚫 wrong */
.foo {
background: white url('hero.png'); /* shorthand disallowed */
-webkit-transform: scale(1); /* vendor prefix */
font-weight: bold; /* must be numeric */
}@import, @layer, @apply, @include, @tailwind, @reference, @use, @container, @custom-variant, @function, etc. are allowed and recognised. The full list of permitted custom at-rules is in at-rule-no-unknown in the config. Stick to recognised at-rules; if you need a new one, add it to the config first.
/* ✅ correct */
@import 'normalize.css' layer(reset);
@layer base {
body {
color: rgb(0 0 0);
}
}import-notation: 'string' requires the bare-string form (@import 'foo.css'), not the url() form (@import url('foo.css')). This is reiterated in projects/stencil/stylelint.config.mjs.
| Rule | Effect |
|---|---|
comment-word-disallowed-list: ['/A-Z/', 'todo:'] |
Comments cannot start with an uppercase letter and cannot contain todo: |
comment-empty-line-before: 'always' |
Blank line before comments |
comment-no-empty |
No empty comments |
comment-whitespace-inside: 'always' |
Whitespace inside the delimiters: /* foo */, not /*foo*/ |
/* ✅ correct */
/* base layout overrides */
.foo {
display: flex;
}
/* 🚫 wrong: leading uppercase, todo, no whitespace */
/* Base layout */
/* todo: change later */
/*foo*/- Durations must be ≥
100ms(time-min-milliseconds) keyframe-declaration-no-important- never!importantinside@keyframesfunction-linear-gradient-no-nonstandard-direction- gradients use modern direction syntax
projects/stencil/stylelint.config.mjs extends the root and ignores compiled output (**/dist/*.css) and non-CSS files (*.js, *.jsx, *.svg). The only rule override is import-notation: 'string'.
# In projects/stencil/
npm run lint.css
# Auto-fix where possible
npx stylelint --fix 'src/**/*.css'When writing or editing code in this monorepo:
- TypeScript: keep
handFragmentimports even if unused; respect Stencil plugin rules in.tsxfiles - CSS authoring:
- Use
rgb(r g b / a)modern syntax with numeric alpha; never named colors, neverhsl/lch, never short hex - Use
px,rem,%,vh/vwfor sizing; neverem/cm/in/mm/pc/pt - Sort declarations alphabetically; custom props first; one declaration per line
- Use specific
background-*properties, never thebackgroundshorthand - No vendor prefixes (except
-webkit-overflow-scrolling) - Class names lowercase kebab/snake; quote attribute selectors
- Numeric
font-weight(400, notnormal) - No
todo:comments; comments must start lowercase - Animation/transition durations ≥ 100 ms
- Use
- Before committing: the pre-commit hook will run lint + format on staged files; fix the issues it reports rather than bypassing the hook.