@campo-design/lint 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.
- package/LICENSE +21 -0
- package/README.md +294 -2
- package/bin/campo-lint.js +74 -0
- package/package.json +46 -3
- package/src/core.js +253 -0
- package/src/eslint/index.js +354 -0
- package/src/setup.js +100 -0
- package/src/stylelint/index.js +49 -0
- package/src/stylelint/plugin.js +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joshua Litchfield
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,295 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @campo-design/lint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Campo's rules, as lint, for products built with Campo: a Stylelint config for
|
|
4
|
+
CSS, and an ESLint plugin for JavaScript, JSX and templates.
|
|
5
|
+
|
|
6
|
+
Campo's rules only hold where something checks them. Inside the Campo repo,
|
|
7
|
+
`pnpm check` and `pnpm lint` do. A product that installs Campo's packages
|
|
8
|
+
used to get the rules as documentation, with nothing checking them. This
|
|
9
|
+
package moves the checks to where product code is written: the editor, a
|
|
10
|
+
pre-commit hook, CI, and an agent's own session.
|
|
11
|
+
|
|
12
|
+
It holds no list of tokens, icons or components of its own. It reads them
|
|
13
|
+
from the Campo packages the product has installed (they're peer
|
|
14
|
+
dependencies), so a product's lint always matches its Campo version: a token
|
|
15
|
+
added in the next release is allowed as soon as the product upgrades, and one
|
|
16
|
+
that's removed fails.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install --save-dev @campo-design/lint stylelint eslint
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`@campo-design/tokens`, `@campo-design/icons`, `@campo-design/webawesome` and
|
|
25
|
+
`@awesome.me/webawesome` are peer dependencies: a product building with Campo
|
|
26
|
+
has them already. Install only the linter you use; both are optional peers.
|
|
27
|
+
It needs `@campo-design/tokens` and `@campo-design/webawesome` 0.3.0 or
|
|
28
|
+
later: the contract check (`checkUsage`) and the manifest's `useInstead`,
|
|
29
|
+
which names the token to use instead of a primitive, first shipped there.
|
|
30
|
+
|
|
31
|
+
Tested with Stylelint 17 and 18, and ESLint 9 and 10 (flat config), in a
|
|
32
|
+
product installed from the published packages. The peer ranges allow
|
|
33
|
+
Stylelint 16 and later; versions outside the tested ones may work, but
|
|
34
|
+
nothing has checked them.
|
|
35
|
+
|
|
36
|
+
## Stylelint
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// stylelint.config.js
|
|
40
|
+
export default {
|
|
41
|
+
extends: ['stylelint-config-recommended', '@campo-design/lint/stylelint'],
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Put `@campo-design/lint/stylelint` last in `extends`. A later config
|
|
46
|
+
overrides an earlier one rule by rule, so a config after Campo's could turn a
|
|
47
|
+
Campo rule off or loosen it without anyone noticing. Last, Campo's rules win;
|
|
48
|
+
a deliberate exception goes in your own `rules` or an `overrides` entry,
|
|
49
|
+
where it shows.
|
|
50
|
+
|
|
51
|
+
The config holds Campo's rules only. `stylelint-config-recommended` adds the
|
|
52
|
+
bug-catching ones (invalid properties, unknown units); it's the product's
|
|
53
|
+
choice. To lint the `<style>` blocks and `style=""` attributes of HTML pages
|
|
54
|
+
too, add [`postcss-html`](https://github.com/ota-meshi/postcss-html):
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
overrides: [{ files: ['**/*.html'], customSyntax: 'postcss-html' }],
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
What the config turns on:
|
|
61
|
+
|
|
62
|
+
| Rule | Fails on |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| [`campo/semantic-tokens`](#camposemantic-tokens) | `var(--campo-color-neutral-900)`, a primitive, or `var(--campo-spacing-gap-medium)`, a token that doesn't exist |
|
|
65
|
+
| `color-no-hex`, `color-named`, `function-disallowed-list` | `#1a1a1a`, `white`, `oklch(…)` |
|
|
66
|
+
| `declaration-property-unit-disallowed-list` | `padding: 16px`, `transition-duration: 200ms`, for every property a token family exists for |
|
|
67
|
+
| [`campo/no-raw-through-custom-property`](#campono-raw-through-custom-property) | `--gap: 16px; gap: var(--gap)` |
|
|
68
|
+
| `declaration-property-value-disallowed-list` | `font-weight: 600` |
|
|
69
|
+
| `declaration-no-important` | `!important` |
|
|
70
|
+
| [`campo/no-wa-override`](#campono-wa-override) | `--wa-color-brand-fill-loud: …` |
|
|
71
|
+
| [`campo/no-color-scheme-query`](#campono-color-scheme-query) | `@media (prefers-color-scheme: dark)` |
|
|
72
|
+
|
|
73
|
+
It also requires a reason on every `stylelint-disable`, after `--`:
|
|
74
|
+
`/* stylelint-disable-next-line color-no-hex -- A specimen of the raw value. */`.
|
|
75
|
+
|
|
76
|
+
What stays allowed on purpose: layout values (`max-width: 65ch`, grid
|
|
77
|
+
templates, breakpoints), `em` values (they scale with the text), `0`, `%`,
|
|
78
|
+
keywords (`transparent`, `currentColor`, `inherit`), reading a `--wa-*` token,
|
|
79
|
+
and a local custom property for a size two rules share.
|
|
80
|
+
|
|
81
|
+
## ESLint
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// eslint.config.js
|
|
85
|
+
import campo from '@campo-design/lint/eslint';
|
|
86
|
+
|
|
87
|
+
export default [
|
|
88
|
+
{ files: ['**/*.{js,jsx}'], ...campo.configs.recommended },
|
|
89
|
+
];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Put Campo's entry after the others, for the same reason as in Stylelint: in
|
|
93
|
+
a flat config, a later entry overrides an earlier one for the files both
|
|
94
|
+
match, so Campo's rules stay on unless you turn one off where it shows.
|
|
95
|
+
|
|
96
|
+
JavaScript holds Campo's rules in its strings: CSS in template literals and
|
|
97
|
+
style objects, markup in lit's `html` templates, JSX or `innerHTML`, and import
|
|
98
|
+
paths. The rules read those. For JSX, turn it on in the parser
|
|
99
|
+
(`languageOptions.parserOptions.ecmaFeatures.jsx`); for TypeScript, use
|
|
100
|
+
[typescript-eslint](https://typescript-eslint.io)'s parser. The rules are the
|
|
101
|
+
same.
|
|
102
|
+
|
|
103
|
+
| Rule | Fails on |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| [`campo/semantic-tokens`](#camposemantic-tokens) | A primitive or an invented `--campo-*` token in a string, template or style object |
|
|
106
|
+
| [`campo/no-raw-values`](#campono-raw-values) | `style={{ padding: 8 }}`, `el.style.gap = '16px'`, ``css`.x { color: #333; }` `` |
|
|
107
|
+
| [`campo/no-important`](#campono-important) | `!important` in CSS written in script |
|
|
108
|
+
| [`campo/no-wa-override`](#campono-wa-override) | `setProperty('--wa-…')`, `{ '--wa-…': … }`, `--wa-…:` in a template |
|
|
109
|
+
| [`campo/no-color-scheme-query`](#campono-color-scheme-query) | `matchMedia('(prefers-color-scheme: dark)')` |
|
|
110
|
+
| [`campo/phosphor-icons`](#campophosphor-icons) | `<wa-icon name="circle-check">` or `<WaIcon name="circle-check">`, a Font Awesome name |
|
|
111
|
+
| [`campo/contract-gaps`](#campocontract-gaps) | `<wa-callout variant="info">`, or a combination the contract lists as a gap, on `<wa-*>` or on React's `<WaButton>` |
|
|
112
|
+
| [`campo/no-webawesome-stylesheet`](#campono-webawesome-stylesheet) | `import '@awesome.me/webawesome/dist/styles/webawesome.css'` |
|
|
113
|
+
| [`campo/no-autoloader`](#campono-autoloader) | `import '@awesome.me/webawesome'` or the autoloader |
|
|
114
|
+
| [`campo/no-palette-class`](#campono-palette-class) | `wa-palette-*` and `wa-brand-*` classes |
|
|
115
|
+
|
|
116
|
+
## The rules
|
|
117
|
+
|
|
118
|
+
Each rule is a mistake that renders fine today and breaks at the next theme
|
|
119
|
+
change or redesign. `AGENTS.md`'s *Building with Campo* explains each one,
|
|
120
|
+
with its reason.
|
|
121
|
+
|
|
122
|
+
### `campo/semantic-tokens`
|
|
123
|
+
|
|
124
|
+
Stylelint and ESLint. A `var(--campo-*)` must be a semantic token. A
|
|
125
|
+
primitive reaches past the tier, so a theme or redesign doesn't reach it; the
|
|
126
|
+
message names the semantic tokens that point at it, so you can pick by role.
|
|
127
|
+
A name Campo doesn't define looks right and resolves to nothing: check it in
|
|
128
|
+
`@campo-design/tokens/manifest.json`.
|
|
129
|
+
|
|
130
|
+
### `campo/no-raw-through-custom-property`
|
|
131
|
+
|
|
132
|
+
Stylelint. The unit rule stops `gap: 16px` but can't see
|
|
133
|
+
`--gap: 16px; gap: var(--gap)`, because a custom property has no type. This
|
|
134
|
+
rule follows a local custom property declared in the same file to where it's
|
|
135
|
+
used, and reports a raw unit that lands in a property a token owns. A local
|
|
136
|
+
custom property for a size is fine where it lands in `inline-size` or `top`.
|
|
137
|
+
|
|
138
|
+
### `campo/no-raw-values`
|
|
139
|
+
|
|
140
|
+
ESLint. Raw colors, `px`/`rem` lengths, `ms`/`s` durations and numeric
|
|
141
|
+
weights in style objects (`style={{ }}`, `Object.assign(el.style, { })`,
|
|
142
|
+
`el.style.x = …`) and in CSS written in a string or template. A bare number
|
|
143
|
+
in a style object counts as `px` where React renders it as one
|
|
144
|
+
(`padding: 8`), not where it's unitless (`lineHeight: 1.5`). A string is only
|
|
145
|
+
read as CSS when it holds a whole declaration (`gap: 16px;`) or an inline
|
|
146
|
+
style, so `'Time: 5s'` isn't flagged.
|
|
147
|
+
|
|
148
|
+
### `campo/no-important`
|
|
149
|
+
|
|
150
|
+
ESLint. `!important` in CSS written in script, the counterpart of
|
|
151
|
+
Stylelint's `declaration-no-important`. Campo's CSS is in layers, so product
|
|
152
|
+
CSS wins without it.
|
|
153
|
+
|
|
154
|
+
### `campo/no-wa-override`
|
|
155
|
+
|
|
156
|
+
Stylelint and ESLint. Declaring or setting a `--wa-*` token. Campo's bridge
|
|
157
|
+
maps every one to a Campo token; an override anywhere else bypasses Campo and
|
|
158
|
+
breaks at the next theme change. Get as close as the component's variants and
|
|
159
|
+
appearances allow, then name the gap as a Campo token decision. Reading a
|
|
160
|
+
`--wa-*` token is fine.
|
|
161
|
+
|
|
162
|
+
### `campo/no-color-scheme-query`
|
|
163
|
+
|
|
164
|
+
Stylelint and ESLint. `prefers-color-scheme`, in a media query or in script.
|
|
165
|
+
Campo's themes repoint the semantic tokens, so a screen built from them is
|
|
166
|
+
right in both; a query adds a second, unchecked version of the theme.
|
|
167
|
+
|
|
168
|
+
### `campo/phosphor-icons`
|
|
169
|
+
|
|
170
|
+
ESLint. `<wa-icon name>` takes a [Phosphor](https://phosphoricons.com) name.
|
|
171
|
+
Web Awesome's docs use Font Awesome names, and one of those draws nothing,
|
|
172
|
+
with no error. Common ones that differ: `check-circle` (not `circle-check`),
|
|
173
|
+
`x-circle` (not `circle-xmark`), `warning` (not `triangle-exclamation`),
|
|
174
|
+
`info` (not `circle-info`). In React, the same goes for `<WaIcon name>` from
|
|
175
|
+
`@campo-design/react/icon`, under whatever name it's imported as.
|
|
176
|
+
|
|
177
|
+
### `campo/contract-gaps`
|
|
178
|
+
|
|
179
|
+
ESLint. A Web Awesome component used with a combination its Campo contract
|
|
180
|
+
lists as a gap (it renders in stock Web Awesome colours, not Campo's), or a
|
|
181
|
+
value the component doesn't have, such as `variant="info"`. The message says
|
|
182
|
+
what to use instead. A value only known at run time (`variant=${v}`,
|
|
183
|
+
`variant={v}`) is skipped, not guessed, with every bridge release from lint's
|
|
184
|
+
floor on, including those before 0.4.1, whose own check read `${v}` as a
|
|
185
|
+
value. The contracts are in
|
|
186
|
+
`@campo-design/webawesome/contracts`.
|
|
187
|
+
|
|
188
|
+
In React, a component imported from `@campo-design/react` is read as the
|
|
189
|
+
element it renders: `<WaButton variant="danger" appearance="accent">` is
|
|
190
|
+
checked like `<wa-button variant="danger" appearance="accent">`, and the
|
|
191
|
+
message names it as you wrote it (`<WaButton> (<wa-button>) … is a gap`).
|
|
192
|
+
That's also true under a renamed import (`WaButton as Button`), and for
|
|
193
|
+
components imported from Web Awesome's own React wrappers
|
|
194
|
+
(`@awesome.me/webawesome/dist/react/<name>`). A component of your own that
|
|
195
|
+
happens to be called `WaButton` isn't checked.
|
|
196
|
+
|
|
197
|
+
`@campo-design/react`'s types already reject a gap in TypeScript. This rule
|
|
198
|
+
is the same check for code the types don't stop: plain JavaScript, a line
|
|
199
|
+
silenced with `// @ts-expect-error` or `// @ts-ignore`, and Web Awesome's
|
|
200
|
+
wrappers. Both read the same contracts, so they can't disagree. A value cast
|
|
201
|
+
inside an expression (`variant={'danger' as any}`) is a run-time value to
|
|
202
|
+
this rule, so it isn't checked.
|
|
203
|
+
|
|
204
|
+
### `campo/no-webawesome-stylesheet`
|
|
205
|
+
|
|
206
|
+
ESLint. Web Awesome's `webawesome.css`. Import
|
|
207
|
+
`@campo-design/webawesome/theme.css`, and `@campo-design/css` for native
|
|
208
|
+
elements.
|
|
209
|
+
|
|
210
|
+
### `campo/no-autoloader`
|
|
211
|
+
|
|
212
|
+
ESLint. Web Awesome's autoloader, or the package's root entry, which starts
|
|
213
|
+
it. Import each component you use from
|
|
214
|
+
`@awesome.me/webawesome/dist/components/<name>/<name>.js`.
|
|
215
|
+
|
|
216
|
+
### `campo/no-palette-class`
|
|
217
|
+
|
|
218
|
+
ESLint. `wa-palette-*` and `wa-brand-*` classes. Campo decides color through
|
|
219
|
+
its tokens; `<html>` takes `wa-theme-default campo-wa` only.
|
|
220
|
+
|
|
221
|
+
## Setup across a product
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
npx campo-lint setup # the current folder
|
|
225
|
+
npx campo-lint setup src public # or the folders you name
|
|
226
|
+
npx campo-lint setup --json # for tools and agents
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Campo's setup is spread over a product's files: the theme classes on
|
|
230
|
+
`<html>` in a page, `@campo-design/webawesome/theme.css` in a stylesheet or
|
|
231
|
+
script, `connectColorScheme()` and `registerCampoIcons()` (or, in React,
|
|
232
|
+
`setupCampo()`) in a script, and an import for each Web Awesome component
|
|
233
|
+
used. A linter reads one file at a time, so it can't see a step that's
|
|
234
|
+
missing from all of them. `campo-lint setup` reads them together. It
|
|
235
|
+
reports each problem at its file and line, each missing step with what to
|
|
236
|
+
add, and exits 1 when it finds any, so it can run in CI next to lint:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
index.html:3 <wa-icon> (setupImport)
|
|
240
|
+
Import it from @awesome.me/webawesome/dist/components/<name>/<name>.js (in React, @campo-design/react/<name>).
|
|
241
|
+
Missing from the product (2 files checked):
|
|
242
|
+
@campo-design/webawesome/theme.css (setupThemeCss)
|
|
243
|
+
Import @campo-design/webawesome/theme.css once, in the product's entry stylesheet or script.
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
It reads stylesheets, pages, templates and scripts (`.css`, `.scss`,
|
|
247
|
+
`.html`, `.njk`, `.md`, `.mdx`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`,
|
|
248
|
+
`.svelte`, `.astro`), skipping `node_modules`, build output (`dist`, `build`,
|
|
249
|
+
`out`, `_site`) and hidden folders. Code shown as text, in `<code>`, `<pre>`
|
|
250
|
+
or a Markdown code block, is skipped, so a page that teaches the setup can
|
|
251
|
+
quote it. A product that uses no Web Awesome has nothing to check.
|
|
252
|
+
|
|
253
|
+
| Rule | Fails on |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `setupThemeClass` | `<html>` without both `wa-theme-default` and `campo-wa` |
|
|
256
|
+
| `setupThemeCss` | `@campo-design/webawesome/theme.css` never imported |
|
|
257
|
+
| `setupColorScheme` | `connectColorScheme()` never called (or `setupCampo()` in React) |
|
|
258
|
+
| `setupIcons` | `registerCampoIcons()` never called (or `setupCampo()` in React) |
|
|
259
|
+
| `setupImport` | a `<wa-*>` element used but never imported |
|
|
260
|
+
| `setupStylesheet` | Web Awesome's `webawesome.css` imported |
|
|
261
|
+
| `setupAutoloader` | the autoloader, or the package's root entry, which starts it |
|
|
262
|
+
| `setupPalette` | a `wa-palette-*` or `wa-brand-*` class |
|
|
263
|
+
| `setupReactImport` | a component from Web Awesome's own React wrappers |
|
|
264
|
+
|
|
265
|
+
## What it doesn't check
|
|
266
|
+
|
|
267
|
+
- **Setup, in the editor.** The Stylelint and ESLint rules can't see a step
|
|
268
|
+
missing from every file; `campo-lint setup` (above) checks that, as a
|
|
269
|
+
command rather than in the editor.
|
|
270
|
+
- **Picking a token by its role.** `gap.related` against `gap.flow`, or
|
|
271
|
+
`text.subtle` against `text.disabled`, is judgment. The token
|
|
272
|
+
`$description`s in the manifest are what to read.
|
|
273
|
+
- **Whether a recipe fits.** Run `npx @campo-design/recipes find "<what
|
|
274
|
+
you're building>"`.
|
|
275
|
+
|
|
276
|
+
## Exceptions
|
|
277
|
+
|
|
278
|
+
When an exception is deliberate, say why at the place it's made:
|
|
279
|
+
`/* stylelint-disable-next-line <rule> -- <reason> */`, or
|
|
280
|
+
`// eslint-disable-next-line campo/<rule> -- <reason>`. If you need the
|
|
281
|
+
exception because Campo has no token or variant for what you're building,
|
|
282
|
+
that's a gap for Campo to decide; check *Not mapped yet* in
|
|
283
|
+
`@campo-design/webawesome`'s README, then raise it.
|
|
284
|
+
|
|
285
|
+
## In the Campo repo
|
|
286
|
+
|
|
287
|
+
Campo lints itself with this package: the root `stylelint.config.mjs`
|
|
288
|
+
extends `@campo-design/lint/stylelint`, and `eslint.config.mjs` applies the
|
|
289
|
+
recommended ESLint config to the docs' and site's browser scripts. Campo's
|
|
290
|
+
scoring (`scripts/token-score.mjs`, behind `pnpm metrics`, the bench grader
|
|
291
|
+
and the docs and site check) reads the same definitions from
|
|
292
|
+
`@campo-design/lint/core`. So a rule changes in one place, `src/core.js`, and
|
|
293
|
+
means the same thing in a product's lint, Campo's own checks and the bench.
|
|
294
|
+
|
|
295
|
+
Tests: `pnpm --filter @campo-design/lint test`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// campo-lint: checks for a whole product, beside the Stylelint config and
|
|
3
|
+
// ESLint plugin, which read one file at a time.
|
|
4
|
+
//
|
|
5
|
+
// npx campo-lint setup [folder…] Campo's fixed setup across the product
|
|
6
|
+
// npx campo-lint setup --json the same, as JSON
|
|
7
|
+
//
|
|
8
|
+
// It exits 1 when it finds a problem, so it can gate CI.
|
|
9
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, relative, resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { SETUP_FIXES, setupFindings } from '../src/setup.js';
|
|
13
|
+
|
|
14
|
+
// Files Campo's setup can appear in: stylesheets, pages, templates, scripts.
|
|
15
|
+
const EXTENSIONS = /\.(css|scss|html|njk|md|mdx|js|mjs|cjs|jsx|ts|tsx|vue|svelte|astro)$/;
|
|
16
|
+
// Dependencies and build output: not the product's own source.
|
|
17
|
+
const SKIP = new Set(['node_modules', 'dist', 'build', 'out', 'coverage', '_site', 'storybook-static']);
|
|
18
|
+
|
|
19
|
+
const USAGE = `Usage: campo-lint setup [folder…] [--json]
|
|
20
|
+
|
|
21
|
+
Checks Campo's fixed setup across a product's files: the theme classes on
|
|
22
|
+
<html>, @campo-design/webawesome/theme.css, connectColorScheme() and
|
|
23
|
+
registerCampoIcons() (or setupCampo() in React), an import for each Web
|
|
24
|
+
Awesome component used, and none of webawesome.css, the autoloader, palette
|
|
25
|
+
classes or Web Awesome's own React wrappers. Folders default to the current
|
|
26
|
+
one; node_modules, build output and hidden folders are skipped.`;
|
|
27
|
+
|
|
28
|
+
/** Every source file under the given paths. */
|
|
29
|
+
export function productFiles(paths) {
|
|
30
|
+
const walk = (path) => {
|
|
31
|
+
if (!statSync(path).isDirectory()) return EXTENSIONS.test(path) ? [path] : [];
|
|
32
|
+
return readdirSync(path)
|
|
33
|
+
.filter((name) => !name.startsWith('.') && !SKIP.has(name))
|
|
34
|
+
.flatMap((name) => walk(join(path, name)));
|
|
35
|
+
};
|
|
36
|
+
return paths.flatMap((p) => walk(resolve(p)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The report a person reads: problems in files first, then missing steps. */
|
|
40
|
+
export function formatReport(findings, fileCount, base = process.cwd()) {
|
|
41
|
+
if (!findings.length) return `No setup problems (${fileCount} files checked).`;
|
|
42
|
+
const lines = [];
|
|
43
|
+
const inFiles = findings.filter((f) => f.file);
|
|
44
|
+
const missing = findings.filter((f) => !f.file);
|
|
45
|
+
for (const f of inFiles) lines.push(`${relative(base, f.file)}:${f.line} ${f.detail} (${f.rule})\n ${SETUP_FIXES[f.rule]}`);
|
|
46
|
+
if (missing.length) {
|
|
47
|
+
lines.push(`Missing from the product (${fileCount} files checked):`);
|
|
48
|
+
for (const f of missing) lines.push(` ${f.detail} (${f.rule})\n ${SETUP_FIXES[f.rule]}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(`\n${findings.length} setup problem${findings.length === 1 ? '' : 's'}. The setup is under "Use" in @campo-design/webawesome's README.`);
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Run as a command, directly or through npx's link in node_modules/.bin;
|
|
55
|
+
// not when a test imports productFiles or formatReport.
|
|
56
|
+
const invoked = process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
|
|
57
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
58
|
+
if (invoked) {
|
|
59
|
+
if (command !== 'setup' || rest.includes('--help')) {
|
|
60
|
+
console.log(USAGE);
|
|
61
|
+
process.exit(command === 'setup' || command === '--help' || !command ? 0 : 2);
|
|
62
|
+
}
|
|
63
|
+
const json = rest.includes('--json');
|
|
64
|
+
const folders = rest.filter((a) => !a.startsWith('--'));
|
|
65
|
+
const files = productFiles(folders.length ? folders : ['.']);
|
|
66
|
+
const findings = setupFindings(files.map((file) => ({ file, text: readFileSync(file, 'utf8') })));
|
|
67
|
+
if (json) {
|
|
68
|
+
const base = process.cwd();
|
|
69
|
+
console.log(JSON.stringify({ files: files.length, findings: findings.map((f) => ({ ...f, file: f.file && relative(base, f.file), fix: SETUP_FIXES[f.rule] })) }, null, 2));
|
|
70
|
+
} else {
|
|
71
|
+
console.log(formatReport(findings, files.length));
|
|
72
|
+
}
|
|
73
|
+
process.exit(findings.length ? 1 : 0);
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@campo-design/lint",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Campo's lint rules for products: a Stylelint config and an ESLint plugin that fail on raw values, primitives, invented tokens, Web Awesome overrides, hand-rolled dark mode, non-Phosphor icons and contract gaps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"campo-lint": "./bin/campo-lint.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
"./stylelint": "./src/stylelint/index.js",
|
|
16
|
+
"./stylelint/plugin": "./src/stylelint/plugin.js",
|
|
17
|
+
"./eslint": "./src/eslint/index.js",
|
|
18
|
+
"./core": "./src/core.js",
|
|
19
|
+
"./setup": "./src/setup.js"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@awesome.me/webawesome": "^3.14.0",
|
|
24
|
+
"@campo-design/icons": ">=0.1.0 <1.0.0",
|
|
25
|
+
"@campo-design/tokens": ">=0.3.0 <1.0.0",
|
|
26
|
+
"@campo-design/webawesome": ">=0.3.0 <1.0.0",
|
|
27
|
+
"eslint": ">=9.0.0",
|
|
28
|
+
"stylelint": ">=16.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"eslint": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"stylelint": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@awesome.me/webawesome": "3.14.0",
|
|
40
|
+
"eslint": "^10.14.0",
|
|
41
|
+
"stylelint": "^17.16.0",
|
|
42
|
+
"@campo-design/icons": "^0.1.0",
|
|
43
|
+
"@campo-design/tokens": "^0.3.1",
|
|
44
|
+
"@campo-design/webawesome": "^0.4.1"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "node --test test/*.test.js"
|
|
48
|
+
}
|
|
6
49
|
}
|