@ncksnydr/config-js 0.2.0 → 0.4.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/README.md +101 -4
- package/dist/eslint-react.d.ts +2 -0
- package/dist/eslint-react.js +42 -0
- package/dist/eslint-react.js.map +1 -0
- package/dist/stylelint/layered.d.ts +2 -0
- package/dist/stylelint/layered.js +47 -0
- package/dist/stylelint/layered.js.map +1 -0
- package/dist/stylelint/mediaLast.d.ts +2 -0
- package/dist/stylelint/mediaLast.js +27 -0
- package/dist/stylelint/mediaLast.js.map +1 -0
- package/dist/stylelint/scaleOfThree.d.ts +2 -0
- package/dist/stylelint/scaleOfThree.js +34 -0
- package/dist/stylelint/scaleOfThree.js.map +1 -0
- package/dist/stylelint/varFallback.d.ts +2 -0
- package/dist/stylelint/varFallback.js +28 -0
- package/dist/stylelint/varFallback.js.map +1 -0
- package/dist/stylelint.d.ts +3 -0
- package/dist/stylelint.js +49 -0
- package/dist/stylelint.js.map +1 -0
- package/dist/vitest-react.d.ts +3 -0
- package/dist/vitest-react.js +15 -0
- package/dist/vitest-react.js.map +1 -0
- package/dist/vitest.d.ts +3 -2
- package/dist/vitest.js +3 -3
- package/dist/vitest.js.map +1 -1
- package/package.json +39 -2
- package/src/eslint-react.ts +45 -0
- package/src/stylelint/layered.ts +62 -0
- package/src/stylelint/mediaLast.ts +37 -0
- package/src/stylelint/scaleOfThree.ts +45 -0
- package/src/stylelint/varFallback.ts +39 -0
- package/src/stylelint.ts +56 -0
- package/src/vitest-react.ts +19 -0
- package/src/vitest.ts +5 -4
- package/tsconfig/react.json +8 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# @ncksnydr/config-js
|
|
2
2
|
|
|
3
|
-
Shared configuration for TypeScript, ESLint, Prettier and Vitest. Each configuration is a separate entry point, so a Consumer uses only the parts it needs.
|
|
3
|
+
Shared configuration for TypeScript, ESLint, Prettier, Stylelint and Vitest. Each configuration is a separate entry point, so a Consumer uses only the parts it needs.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pnpm add --save-dev @ncksnydr/config-js typescript eslint prettier vitest
|
|
8
|
+
pnpm add --save-dev @ncksnydr/config-js typescript eslint prettier stylelint vitest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## TypeScript
|
|
@@ -96,6 +96,41 @@ Checking formatting...
|
|
|
96
96
|
|
|
97
97
|
To fix the layout, run `prettier --write .`.
|
|
98
98
|
|
|
99
|
+
## Stylelint
|
|
100
|
+
|
|
101
|
+
Use the configuration as your `stylelint.config.js`:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
export { default } from "@ncksnydr/config-js/stylelint";
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The configuration needs Stylelint 18 or later. Then lint your CSS:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
stylelint "**/*.css"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The configuration holds these rules:
|
|
114
|
+
|
|
115
|
+
- Colors use `oklch()`. Hex colors, named colors and `rgb()`, `rgba()`, `hsl()` and `hsla()` fail.
|
|
116
|
+
- Primitive tokens, such as `var(--palette-blue-500)` or `var(--scale-3)`, are allowed only in files inside a `tokens/` folder. Other files use semantic tokens.
|
|
117
|
+
- Each `var()` has a fallback, such as `var(--space-md, 1.5rem)`. A custom property that the same file declares needs no fallback.
|
|
118
|
+
- `!important` and `brightness()` fail.
|
|
119
|
+
- Each rule sits inside one of the layers `reset`, `tokens`, `base`, `components` or `utilities`. A top-level `@media` block is the one exception.
|
|
120
|
+
- Top-level `@media` blocks come last in the file.
|
|
121
|
+
- Rules nest at most 3 levels deep.
|
|
122
|
+
- Class names follow BEM in kebab case: `block`, `block__element`, `block--modifier`.
|
|
123
|
+
- Units are limited to `rem`, `px`, `%`, `fr`, `vh`, `dvh`, `svh`, `vw`, `deg`, `ms` and `s`.
|
|
124
|
+
- Each `px` or `rem` length is `1px` or a multiple of `3px`. The check counts `1rem` as `16px`.
|
|
125
|
+
- Custom properties come first, then declarations, then nested rules, then nested at-rules. Declarations are in alphabetical order.
|
|
126
|
+
|
|
127
|
+
A CSS file that breaks a rule fails the check:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
card.css
|
|
131
|
+
3:12 ✖ Disallowed hex color "#fff" color-no-hex
|
|
132
|
+
```
|
|
133
|
+
|
|
99
134
|
## Vitest
|
|
100
135
|
|
|
101
136
|
Use the preset as your `vitest.config.ts`:
|
|
@@ -110,7 +145,69 @@ To change a setting, merge the preset with your own configuration:
|
|
|
110
145
|
import preset from "@ncksnydr/config-js/vitest";
|
|
111
146
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
112
147
|
|
|
113
|
-
export default mergeConfig(preset, defineConfig({ test: {
|
|
148
|
+
export default mergeConfig(preset, defineConfig({ test: { testTimeout: 10_000 } }));
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The preset runs tests in Node. It restores mocks between tests. If it finds no tests, the run fails.
|
|
152
|
+
|
|
153
|
+
The preset does not set `include`, so Vitest finds test files with its own pattern, such as `*.test.ts` and `*.test.tsx`. To use your own pattern, merge an `include` into the preset.
|
|
154
|
+
|
|
155
|
+
## React
|
|
156
|
+
|
|
157
|
+
A React Consumer uses the React presets in place of the plain ones. Each React preset holds the plain preset and adds to it.
|
|
158
|
+
|
|
159
|
+
### TypeScript for React
|
|
160
|
+
|
|
161
|
+
Extend the React base in your `tsconfig.json`:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"extends": "@ncksnydr/config-js/tsconfig-react"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The React base adds the DOM types. It compiles JSX with React's automatic runtime, so a component file does not import React to use JSX. Install `react` and `@types/react`.
|
|
170
|
+
|
|
171
|
+
### ESLint for React
|
|
172
|
+
|
|
173
|
+
Use the React configuration as your `eslint.config.js`:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
export { default } from "@ncksnydr/config-js/eslint-react";
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The React configuration adds these rules to `.ts` and `.tsx` files:
|
|
180
|
+
|
|
181
|
+
- The recommended rules of `eslint-plugin-react`, `eslint-plugin-react-hooks` and `eslint-plugin-jsx-a11y`.
|
|
182
|
+
- A missing dependency in a hook such as `useEffect` is an error, not a warning.
|
|
183
|
+
- `useContext` is banned. Put shared state in a Zustand store.
|
|
184
|
+
- `fireEvent` from `@testing-library/react` is banned. Use `userEvent`.
|
|
185
|
+
- Text cannot be written inside JSX. Put the text in a variable, and use the variable in the JSX. Text in a prop is allowed.
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
Welcome.tsx
|
|
189
|
+
2:13 error Strings not allowed in JSX files: "Welcome back" react/jsx-no-literals
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The configuration reads the React version from the `react` package that your code imports. Use `boundaries()` with it the same way as with the plain configuration.
|
|
193
|
+
|
|
194
|
+
### Vitest for React
|
|
195
|
+
|
|
196
|
+
Use the React preset as your `vitest.config.ts`:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export { default } from "@ncksnydr/config-js/vitest-react";
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The React preset holds the plain Vitest preset, and runs the tests in Vitest's browser mode, in Chromium through Playwright. The Storybook Vitest addon uses the same browser.
|
|
203
|
+
|
|
204
|
+
The React preset sets no `include`, so you can add the Storybook Vitest plugin to it. The plugin sets `include` to your stories. It prints a warning when the configuration already has an `include`.
|
|
205
|
+
|
|
206
|
+
Install Playwright and the Vitest Playwright provider. Then install Chromium once on each machine and in CI:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
pnpm add --save-dev playwright @vitest/browser-playwright
|
|
210
|
+
pnpm exec playwright install chromium
|
|
114
211
|
```
|
|
115
212
|
|
|
116
|
-
|
|
213
|
+
In CI on Linux, use `pnpm exec playwright install --with-deps chromium`. This command also installs the system libraries that Chromium needs.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fixupPluginRules } from "@eslint/compat";
|
|
2
|
+
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
3
|
+
import react from "eslint-plugin-react";
|
|
4
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
5
|
+
import { defineConfig } from "eslint/config";
|
|
6
|
+
import base from "./eslint.js";
|
|
7
|
+
const reactWithVersionDetection = fixupPluginRules(react);
|
|
8
|
+
function reactPluginConfig(name) {
|
|
9
|
+
const config = react.configs.flat[name];
|
|
10
|
+
if (config === undefined) {
|
|
11
|
+
throw new Error(`eslint-plugin-react has no flat configuration named "${name}".`);
|
|
12
|
+
}
|
|
13
|
+
return { ...config, plugins: { react: reactWithVersionDetection } };
|
|
14
|
+
}
|
|
15
|
+
export default defineConfig(base, {
|
|
16
|
+
extends: [
|
|
17
|
+
jsxA11y.flatConfigs.recommended,
|
|
18
|
+
reactPluginConfig("recommended"),
|
|
19
|
+
reactPluginConfig("jsx-runtime"),
|
|
20
|
+
reactHooks.configs.flat.recommended,
|
|
21
|
+
],
|
|
22
|
+
files: ["**/*.{ts,tsx}"],
|
|
23
|
+
rules: {
|
|
24
|
+
"no-restricted-imports": [
|
|
25
|
+
"error",
|
|
26
|
+
{
|
|
27
|
+
paths: [
|
|
28
|
+
{ importNames: ["useContext"], message: "Put shared state in a Zustand store.", name: "react" },
|
|
29
|
+
{ importNames: ["fireEvent"], message: "Use userEvent.", name: "@testing-library/react" },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
"react-hooks/exhaustive-deps": "error",
|
|
34
|
+
"react/jsx-no-literals": ["error", { ignoreProps: true, noStrings: true }],
|
|
35
|
+
},
|
|
36
|
+
settings: {
|
|
37
|
+
react: {
|
|
38
|
+
version: "detect",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=eslint-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eslint-react.js","sourceRoot":"","sources":["../src/eslint-react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,OAAO,MAAM,wBAAwB,CAAC;AAC7C,OAAO,KAAK,MAAM,qBAAqB,CAAC;AACxC,OAAO,UAAU,MAAM,2BAA2B,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,IAAI,MAAM,aAAa,CAAC;AAE/B,MAAM,yBAAyB,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;AAE1D,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,wDAAwD,IAAI,IAAI,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE,EAAE,CAAC;AACtE,CAAC;AAED,eAAe,YAAY,CAAC,IAAI,EAAE;IAChC,OAAO,EAAE;QACP,OAAO,CAAC,WAAW,CAAC,WAAW;QAC/B,iBAAiB,CAAC,aAAa,CAAC;QAChC,iBAAiB,CAAC,aAAa,CAAC;QAChC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW;KACpC;IACD,KAAK,EAAE,CAAC,eAAe,CAAC;IACxB,KAAK,EAAE;QACL,uBAAuB,EAAE;YACvB,OAAO;YACP;gBACE,KAAK,EAAE;oBACL,EAAE,WAAW,EAAE,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;oBAC/F,EAAE,WAAW,EAAE,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,wBAAwB,EAAE;iBAC1F;aACF;SACF;QACD,6BAA6B,EAAE,OAAO;QACtC,uBAAuB,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;KAC3E;IACD,QAAQ,EAAE;QACR,KAAK,EAAE;YACL,OAAO,EAAE,QAAQ;SAClB;KACF;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
const { createPlugin, utils: { report, ruleMessages, validateOptions }, } = stylelint;
|
|
3
|
+
const ruleName = "nks/layered";
|
|
4
|
+
const HOUSE_LAYERS = new Set(["reset", "tokens", "base", "components", "utilities"]);
|
|
5
|
+
const houseLayerList = new Intl.ListFormat("en-GB", { type: "disjunction" }).format(HOUSE_LAYERS);
|
|
6
|
+
const messages = ruleMessages(ruleName, {
|
|
7
|
+
outside: `Put this rule inside @layer ${houseLayerList}.`,
|
|
8
|
+
undeclared: (name) => `'${name}' is not a house layer. Use ${houseLayerList}.`,
|
|
9
|
+
});
|
|
10
|
+
function isAtRule(node) {
|
|
11
|
+
return node.type === "atrule";
|
|
12
|
+
}
|
|
13
|
+
function isLayeredOrExempt(styleRule) {
|
|
14
|
+
for (let parent = styleRule.parent; parent && parent.type !== "root"; parent = parent.parent) {
|
|
15
|
+
if (parent.type === "rule") {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (isAtRule(parent)) {
|
|
19
|
+
const isTopLevelMedia = parent.name === "media" && parent.parent?.type === "root";
|
|
20
|
+
if (parent.name === "layer" || parent.name.endsWith("keyframes") || isTopLevelMedia) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const rule = (primary) => (root, result) => {
|
|
28
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
root.walkAtRules("layer", (atRule) => {
|
|
32
|
+
const name = atRule.params.split(".")[0]?.trim() ?? "";
|
|
33
|
+
const isLayerBlock = atRule.nodes !== undefined;
|
|
34
|
+
if (isLayerBlock && !HOUSE_LAYERS.has(name)) {
|
|
35
|
+
report({ message: messages.undeclared(name), node: atRule, result, ruleName });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
root.walkRules((styleRule) => {
|
|
39
|
+
if (!isLayeredOrExempt(styleRule)) {
|
|
40
|
+
report({ message: messages.outside, node: styleRule, result, ruleName });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
rule.ruleName = ruleName;
|
|
45
|
+
rule.messages = messages;
|
|
46
|
+
export const layered = createPlugin(ruleName, rule);
|
|
47
|
+
//# sourceMappingURL=layered.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layered.js","sourceRoot":"","sources":["../../src/stylelint/layered.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
const { createPlugin, utils: { report, ruleMessages, validateOptions }, } = stylelint;
|
|
3
|
+
const ruleName = "nks/media-last";
|
|
4
|
+
const messages = ruleMessages(ruleName, {
|
|
5
|
+
rejected: "Move this above the first top-level @media. Media queries go at the bottom of the file.",
|
|
6
|
+
});
|
|
7
|
+
function isMedia(node) {
|
|
8
|
+
return node.type === "atrule" && node.name === "media";
|
|
9
|
+
}
|
|
10
|
+
const rule = (primary) => (root, result) => {
|
|
11
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let mediaSeen = false;
|
|
15
|
+
for (const node of root.nodes) {
|
|
16
|
+
if (isMedia(node)) {
|
|
17
|
+
mediaSeen = true;
|
|
18
|
+
}
|
|
19
|
+
else if (mediaSeen && (node.type === "rule" || node.type === "atrule")) {
|
|
20
|
+
report({ message: messages.rejected, node, result, ruleName });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
rule.ruleName = ruleName;
|
|
25
|
+
rule.messages = messages;
|
|
26
|
+
export const mediaLast = createPlugin(ruleName, rule);
|
|
27
|
+
//# sourceMappingURL=mediaLast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediaLast.js","sourceRoot":"","sources":["../../src/stylelint/mediaLast.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,MAAM,EACJ,YAAY,EACZ,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,GACjD,GAAG,SAAS,CAAC;AAEd,MAAM,QAAQ,GAAG,gBAAgB,CAAC;AAElC,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,yFAAyF;CACpG,CAAC,CAAC;AAEH,SAAS,OAAO,CAAC,IAAe;IAC9B,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;AACzD,CAAC;AAED,MAAM,IAAI,GAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;IACzD,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAC5D,OAAO;IACT,CAAC;IAED,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC;aAAM,IAAI,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;YACzE,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAEzB,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
const { createPlugin, utils: { report, ruleMessages, validateOptions }, } = stylelint;
|
|
3
|
+
const ruleName = "nks/scale-of-three";
|
|
4
|
+
const messages = ruleMessages(ruleName, {
|
|
5
|
+
rejected: (length) => `${length} is off the scale. Use 1px or a multiple of 3px (rem at 16px).`,
|
|
6
|
+
});
|
|
7
|
+
const LENGTH = /(-?\d*\.?\d+)(px|rem)\b/g;
|
|
8
|
+
const PX_PER_REM = 16;
|
|
9
|
+
const SCALE_STEP_PX = 3;
|
|
10
|
+
const HAIRLINE_PX = 1;
|
|
11
|
+
const ROUNDING_TOLERANCE = 1e-9;
|
|
12
|
+
function isOnScale(amount, unit) {
|
|
13
|
+
const px = Math.abs(unit === "rem" ? amount * PX_PER_REM : amount);
|
|
14
|
+
const steps = px / SCALE_STEP_PX;
|
|
15
|
+
const isWholeStep = Math.abs(steps - Math.round(steps)) < ROUNDING_TOLERANCE;
|
|
16
|
+
return px === HAIRLINE_PX || isWholeStep;
|
|
17
|
+
}
|
|
18
|
+
const rule = (primary) => (root, result) => {
|
|
19
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
root.walkDecls((decl) => {
|
|
23
|
+
for (const [length, amount, unit] of decl.value.matchAll(LENGTH)) {
|
|
24
|
+
const isOffScale = amount !== undefined && unit !== undefined && !isOnScale(Number(amount), unit);
|
|
25
|
+
if (isOffScale) {
|
|
26
|
+
report({ message: messages.rejected(length), node: decl, result, ruleName, word: length });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
rule.ruleName = ruleName;
|
|
32
|
+
rule.messages = messages;
|
|
33
|
+
export const scaleOfThree = createPlugin(ruleName, rule);
|
|
34
|
+
//# sourceMappingURL=scaleOfThree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaleOfThree.js","sourceRoot":"","sources":["../../src/stylelint/scaleOfThree.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,MAAM,EACJ,YAAY,EACZ,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,GACjD,GAAG,SAAS,CAAC;AAEd,MAAM,QAAQ,GAAG,oBAAoB,CAAC;AAEtC,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,GAAG,MAAM,gEAAgE;CACxG,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,0BAA0B,CAAC;AAC1C,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,SAAS,SAAS,CAAC,MAAc,EAAE,IAAY;IAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,EAAE,GAAG,aAAa,CAAC;IACjC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,kBAAkB,CAAC;IAC7E,OAAO,EAAE,KAAK,WAAW,IAAI,WAAW,CAAC;AAC3C,CAAC;AAED,MAAM,IAAI,GAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;IACzD,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAC5D,OAAO;IACT,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;QACtB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACjE,MAAM,UAAU,GAAG,MAAM,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;YAClG,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC7F,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAEzB,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
const { createPlugin, utils: { report, ruleMessages, validateOptions }, } = stylelint;
|
|
3
|
+
const ruleName = "nks/var-fallback";
|
|
4
|
+
const messages = ruleMessages(ruleName, {
|
|
5
|
+
rejected: (name) => `Give var(${name}) a fallback: var(${name}, value).`,
|
|
6
|
+
});
|
|
7
|
+
const BARE_VAR = /var\(\s*(--[\w-]+)\s*\)/g;
|
|
8
|
+
const rule = (primary) => (root, result) => {
|
|
9
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const declaredInFile = new Set();
|
|
13
|
+
root.walkDecls(/^--/, (decl) => {
|
|
14
|
+
declaredInFile.add(decl.prop);
|
|
15
|
+
});
|
|
16
|
+
root.walkDecls((decl) => {
|
|
17
|
+
for (const [bareVar, name] of decl.value.matchAll(BARE_VAR)) {
|
|
18
|
+
const needsFallback = name !== undefined && !declaredInFile.has(name);
|
|
19
|
+
if (needsFallback) {
|
|
20
|
+
report({ message: messages.rejected(name), node: decl, result, ruleName, word: bareVar });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
rule.ruleName = ruleName;
|
|
26
|
+
rule.messages = messages;
|
|
27
|
+
export const varFallback = createPlugin(ruleName, rule);
|
|
28
|
+
//# sourceMappingURL=varFallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"varFallback.js","sourceRoot":"","sources":["../../src/stylelint/varFallback.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,MAAM,EACJ,YAAY,EACZ,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,GACjD,GAAG,SAAS,CAAC;AAEd,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AAEpC,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,IAAI,qBAAqB,IAAI,WAAW;CACjF,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,0BAA0B,CAAC;AAE5C,MAAM,IAAI,GAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;IACzD,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAC5D,OAAO;IACT,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;IACzC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE;QAC7B,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;QACtB,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5D,MAAM,aAAa,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtE,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAEzB,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { layered } from "./stylelint/layered.js";
|
|
3
|
+
import { mediaLast } from "./stylelint/mediaLast.js";
|
|
4
|
+
import { scaleOfThree } from "./stylelint/scaleOfThree.js";
|
|
5
|
+
import { varFallback } from "./stylelint/varFallback.js";
|
|
6
|
+
const orderPlugin = fileURLToPath(import.meta.resolve("stylelint-order"));
|
|
7
|
+
const BEM_CLASS = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$/;
|
|
8
|
+
const BRIGHTNESS = /brightness\(/;
|
|
9
|
+
const disallowedEverywhere = { filter: [BRIGHTNESS] };
|
|
10
|
+
function disallowedValueMessage(_property, value) {
|
|
11
|
+
return BRIGHTNESS.test(value)
|
|
12
|
+
? "Do not use brightness()."
|
|
13
|
+
: "Use a semantic token here. Primitive tokens belong in the token files.";
|
|
14
|
+
}
|
|
15
|
+
const config = {
|
|
16
|
+
overrides: [
|
|
17
|
+
{
|
|
18
|
+
files: ["**/tokens/**/*.css"],
|
|
19
|
+
rules: {
|
|
20
|
+
"declaration-property-value-disallowed-list": [disallowedEverywhere, { message: disallowedValueMessage }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
plugins: [layered, mediaLast, orderPlugin, scaleOfThree, varFallback],
|
|
25
|
+
rules: {
|
|
26
|
+
"color-named": "never",
|
|
27
|
+
"color-no-hex": true,
|
|
28
|
+
"declaration-no-important": true,
|
|
29
|
+
"declaration-property-value-disallowed-list": [
|
|
30
|
+
{ ...disallowedEverywhere, "/.*/": [/var\(--(palette|scale)-/] },
|
|
31
|
+
{ message: disallowedValueMessage },
|
|
32
|
+
],
|
|
33
|
+
"function-disallowed-list": [["rgb", "rgba", "hsl", "hsla"], { message: "Use oklch()." }],
|
|
34
|
+
"max-nesting-depth": 3,
|
|
35
|
+
"nks/layered": true,
|
|
36
|
+
"nks/media-last": true,
|
|
37
|
+
"nks/scale-of-three": true,
|
|
38
|
+
"nks/var-fallback": true,
|
|
39
|
+
"order/order": ["custom-properties", "declarations", "rules", "at-rules"],
|
|
40
|
+
"order/properties-alphabetical-order": true,
|
|
41
|
+
"selector-class-pattern": [
|
|
42
|
+
BEM_CLASS,
|
|
43
|
+
{ message: "Expected BEM kebab-case: block, block__element, block--modifier" },
|
|
44
|
+
],
|
|
45
|
+
"unit-allowed-list": ["rem", "px", "%", "fr", "vh", "dvh", "svh", "vw", "deg", "ms", "s"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export default config;
|
|
49
|
+
//# sourceMappingURL=stylelint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stylelint.js","sourceRoot":"","sources":["../src/stylelint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAE1E,MAAM,SAAS,GAAG,qFAAqF,CAAC;AAExG,MAAM,UAAU,GAAG,cAAc,CAAC;AAElC,MAAM,oBAAoB,GAAG,EAAE,MAAM,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;AAEtD,SAAS,sBAAsB,CAAC,SAAiB,EAAE,KAAa;IAC9D,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;QAC3B,CAAC,CAAC,0BAA0B;QAC5B,CAAC,CAAC,wEAAwE,CAAC;AAC/E,CAAC;AAED,MAAM,MAAM,GAAW;IACrB,SAAS,EAAE;QACT;YACE,KAAK,EAAE,CAAC,oBAAoB,CAAC;YAC7B,KAAK,EAAE;gBACL,4CAA4C,EAAE,CAAC,oBAAoB,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;aAC1G;SACF;KACF;IACD,OAAO,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,CAAC;IACrE,KAAK,EAAE;QACL,aAAa,EAAE,OAAO;QACtB,cAAc,EAAE,IAAI;QACpB,0BAA0B,EAAE,IAAI;QAChC,4CAA4C,EAAE;YAC5C,EAAE,GAAG,oBAAoB,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE;YAChE,EAAE,OAAO,EAAE,sBAAsB,EAAE;SACpC;QACD,0BAA0B,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;QACzF,mBAAmB,EAAE,CAAC;QACtB,aAAa,EAAE,IAAI;QACnB,gBAAgB,EAAE,IAAI;QACtB,oBAAoB,EAAE,IAAI;QAC1B,kBAAkB,EAAE,IAAI;QACxB,aAAa,EAAE,CAAC,mBAAmB,EAAE,cAAc,EAAE,OAAO,EAAE,UAAU,CAAC;QACzE,qCAAqC,EAAE,IAAI;QAC3C,wBAAwB,EAAE;YACxB,SAAS;YACT,EAAE,OAAO,EAAE,iEAAiE,EAAE;SAC/E;QACD,mBAAmB,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC;KAC1F;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
2
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
3
|
+
import plain from "./vitest.js";
|
|
4
|
+
const preset = mergeConfig(plain, defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
browser: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
headless: true,
|
|
9
|
+
instances: [{ browser: "chromium" }],
|
|
10
|
+
provider: playwright(),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
export default preset;
|
|
15
|
+
//# sourceMappingURL=vitest-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest-react.js","sourceRoot":"","sources":["../src/vitest-react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAuB,MAAM,eAAe,CAAC;AAC/E,OAAO,KAAK,MAAM,aAAa,CAAC;AAEhC,MAAM,MAAM,GAAmB,WAAW,CACxC,KAAK,EACL,YAAY,CAAC;IACX,IAAI,EAAE;QACJ,OAAO,EAAE;YACP,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;YACpC,QAAQ,EAAE,UAAU,EAAE;SACvB;KACF;CACF,CAAC,CACH,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/vitest.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type ViteUserConfig } from "vitest/config";
|
|
2
|
+
declare const preset: ViteUserConfig;
|
|
3
|
+
export default preset;
|
package/dist/vitest.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
2
|
+
const preset = defineConfig({
|
|
3
3
|
test: {
|
|
4
4
|
environment: "node",
|
|
5
|
-
include: ["**/*.test.ts"],
|
|
6
|
-
restoreMocks: true,
|
|
7
5
|
passWithNoTests: false,
|
|
6
|
+
restoreMocks: true,
|
|
8
7
|
},
|
|
9
8
|
});
|
|
9
|
+
export default preset;
|
|
10
10
|
//# sourceMappingURL=vitest.js.map
|
package/dist/vitest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.js","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"vitest.js","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,eAAe,CAAC;AAElE,MAAM,MAAM,GAAmB,YAAY,CAAC;IAC1C,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM;QACnB,eAAe,EAAE,KAAK;QACtB,YAAY,EAAE,IAAI;KACnB;CACF,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ncksnydr/config-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared TypeScript, ESLint, Prettier and Vitest configuration for Nick Snyder's codebases",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Shared TypeScript, ESLint, Prettier, Stylelint and Vitest configuration for Nick Snyder's codebases",
|
|
5
5
|
"repository": {
|
|
6
6
|
"directory": "packages/config-js",
|
|
7
7
|
"type": "git",
|
|
@@ -13,14 +13,27 @@
|
|
|
13
13
|
"types": "./dist/eslint.d.ts",
|
|
14
14
|
"default": "./dist/eslint.js"
|
|
15
15
|
},
|
|
16
|
+
"./eslint-react": {
|
|
17
|
+
"types": "./dist/eslint-react.d.ts",
|
|
18
|
+
"default": "./dist/eslint-react.js"
|
|
19
|
+
},
|
|
16
20
|
"./prettier": {
|
|
17
21
|
"types": "./dist/prettier.d.ts",
|
|
18
22
|
"default": "./dist/prettier.js"
|
|
19
23
|
},
|
|
24
|
+
"./stylelint": {
|
|
25
|
+
"types": "./dist/stylelint.d.ts",
|
|
26
|
+
"default": "./dist/stylelint.js"
|
|
27
|
+
},
|
|
20
28
|
"./tsconfig": "./tsconfig/base.json",
|
|
29
|
+
"./tsconfig-react": "./tsconfig/react.json",
|
|
21
30
|
"./vitest": {
|
|
22
31
|
"types": "./dist/vitest.d.ts",
|
|
23
32
|
"default": "./dist/vitest.js"
|
|
33
|
+
},
|
|
34
|
+
"./vitest-react": {
|
|
35
|
+
"types": "./dist/vitest-react.d.ts",
|
|
36
|
+
"default": "./dist/vitest-react.js"
|
|
24
37
|
}
|
|
25
38
|
},
|
|
26
39
|
"files": [
|
|
@@ -32,18 +45,30 @@
|
|
|
32
45
|
"tsconfig"
|
|
33
46
|
],
|
|
34
47
|
"peerDependencies": {
|
|
48
|
+
"@vitest/browser-playwright": "^5.0.0",
|
|
35
49
|
"eslint": "^10.0.0",
|
|
50
|
+
"playwright": "^1.65.0",
|
|
36
51
|
"prettier": "^3.5.0",
|
|
52
|
+
"stylelint": "^18.0.0",
|
|
37
53
|
"typescript": "^6.0.0",
|
|
38
54
|
"vitest": "^5.0.0"
|
|
39
55
|
},
|
|
40
56
|
"peerDependenciesMeta": {
|
|
57
|
+
"@vitest/browser-playwright": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
41
60
|
"eslint": {
|
|
42
61
|
"optional": true
|
|
43
62
|
},
|
|
63
|
+
"playwright": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
44
66
|
"prettier": {
|
|
45
67
|
"optional": true
|
|
46
68
|
},
|
|
69
|
+
"stylelint": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
47
72
|
"typescript": {
|
|
48
73
|
"optional": true
|
|
49
74
|
},
|
|
@@ -52,16 +77,28 @@
|
|
|
52
77
|
}
|
|
53
78
|
},
|
|
54
79
|
"dependencies": {
|
|
80
|
+
"@eslint/compat": "^2.1.1",
|
|
55
81
|
"@eslint/js": "^10.0.1",
|
|
56
82
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
|
57
83
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
58
84
|
"eslint-plugin-import-x": "^4.18.1",
|
|
85
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
86
|
+
"eslint-plugin-react": "^7.37.5",
|
|
87
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
88
|
+
"stylelint-order": "^8.1.3",
|
|
59
89
|
"typescript-eslint": "^8.72.2"
|
|
60
90
|
},
|
|
61
91
|
"devDependencies": {
|
|
92
|
+
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
|
|
62
93
|
"@types/node": "^26.6.6",
|
|
94
|
+
"@types/react": "^19.3.0",
|
|
95
|
+
"@vitest/browser-playwright": "^5.1.0",
|
|
63
96
|
"eslint": "^10.14.0",
|
|
97
|
+
"playwright": "^1.65.0",
|
|
98
|
+
"postcss": "^8.5.32",
|
|
64
99
|
"prettier": "^3.10.0",
|
|
100
|
+
"react": "^19.3.1",
|
|
101
|
+
"stylelint": "^18.0.0",
|
|
65
102
|
"typescript": "~6.0.3",
|
|
66
103
|
"vitest": "^5.1.0"
|
|
67
104
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fixupPluginRules } from "@eslint/compat";
|
|
2
|
+
import type { Linter } from "eslint";
|
|
3
|
+
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
4
|
+
import react from "eslint-plugin-react";
|
|
5
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
6
|
+
import { defineConfig } from "eslint/config";
|
|
7
|
+
import base from "./eslint.js";
|
|
8
|
+
|
|
9
|
+
const reactWithVersionDetection = fixupPluginRules(react);
|
|
10
|
+
|
|
11
|
+
function reactPluginConfig(name: string): Linter.Config {
|
|
12
|
+
const config = react.configs.flat[name];
|
|
13
|
+
if (config === undefined) {
|
|
14
|
+
throw new Error(`eslint-plugin-react has no flat configuration named "${name}".`);
|
|
15
|
+
}
|
|
16
|
+
return { ...config, plugins: { react: reactWithVersionDetection } };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineConfig(base, {
|
|
20
|
+
extends: [
|
|
21
|
+
jsxA11y.flatConfigs.recommended,
|
|
22
|
+
reactPluginConfig("recommended"),
|
|
23
|
+
reactPluginConfig("jsx-runtime"),
|
|
24
|
+
reactHooks.configs.flat.recommended,
|
|
25
|
+
],
|
|
26
|
+
files: ["**/*.{ts,tsx}"],
|
|
27
|
+
rules: {
|
|
28
|
+
"no-restricted-imports": [
|
|
29
|
+
"error",
|
|
30
|
+
{
|
|
31
|
+
paths: [
|
|
32
|
+
{ importNames: ["useContext"], message: "Put shared state in a Zustand store.", name: "react" },
|
|
33
|
+
{ importNames: ["fireEvent"], message: "Use userEvent.", name: "@testing-library/react" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
"react-hooks/exhaustive-deps": "error",
|
|
38
|
+
"react/jsx-no-literals": ["error", { ignoreProps: true, noStrings: true }],
|
|
39
|
+
},
|
|
40
|
+
settings: {
|
|
41
|
+
react: {
|
|
42
|
+
version: "detect",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AtRule, Node, Rule } from "postcss";
|
|
2
|
+
import stylelint from "stylelint";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createPlugin,
|
|
6
|
+
utils: { report, ruleMessages, validateOptions },
|
|
7
|
+
} = stylelint;
|
|
8
|
+
|
|
9
|
+
const ruleName = "nks/layered";
|
|
10
|
+
|
|
11
|
+
const HOUSE_LAYERS = new Set(["reset", "tokens", "base", "components", "utilities"]);
|
|
12
|
+
|
|
13
|
+
const houseLayerList = new Intl.ListFormat("en-GB", { type: "disjunction" }).format(HOUSE_LAYERS);
|
|
14
|
+
|
|
15
|
+
const messages = ruleMessages(ruleName, {
|
|
16
|
+
outside: `Put this rule inside @layer ${houseLayerList}.`,
|
|
17
|
+
undeclared: (name: string) => `'${name}' is not a house layer. Use ${houseLayerList}.`,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function isAtRule(node: Node): node is AtRule {
|
|
21
|
+
return node.type === "atrule";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isLayeredOrExempt(styleRule: Rule) {
|
|
25
|
+
for (let parent = styleRule.parent; parent && parent.type !== "root"; parent = parent.parent) {
|
|
26
|
+
if (parent.type === "rule") {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (isAtRule(parent)) {
|
|
30
|
+
const isTopLevelMedia = parent.name === "media" && parent.parent?.type === "root";
|
|
31
|
+
if (parent.name === "layer" || parent.name.endsWith("keyframes") || isTopLevelMedia) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rule: stylelint.Rule = (primary) => (root, result) => {
|
|
40
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
root.walkAtRules("layer", (atRule) => {
|
|
45
|
+
const name = atRule.params.split(".")[0]?.trim() ?? "";
|
|
46
|
+
const isLayerBlock = atRule.nodes !== undefined;
|
|
47
|
+
if (isLayerBlock && !HOUSE_LAYERS.has(name)) {
|
|
48
|
+
report({ message: messages.undeclared(name), node: atRule, result, ruleName });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
root.walkRules((styleRule) => {
|
|
53
|
+
if (!isLayeredOrExempt(styleRule)) {
|
|
54
|
+
report({ message: messages.outside, node: styleRule, result, ruleName });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
rule.ruleName = ruleName;
|
|
60
|
+
rule.messages = messages;
|
|
61
|
+
|
|
62
|
+
export const layered = createPlugin(ruleName, rule);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ChildNode } from "postcss";
|
|
2
|
+
import stylelint from "stylelint";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createPlugin,
|
|
6
|
+
utils: { report, ruleMessages, validateOptions },
|
|
7
|
+
} = stylelint;
|
|
8
|
+
|
|
9
|
+
const ruleName = "nks/media-last";
|
|
10
|
+
|
|
11
|
+
const messages = ruleMessages(ruleName, {
|
|
12
|
+
rejected: "Move this above the first top-level @media. Media queries go at the bottom of the file.",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function isMedia(node: ChildNode) {
|
|
16
|
+
return node.type === "atrule" && node.name === "media";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rule: stylelint.Rule = (primary) => (root, result) => {
|
|
20
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let mediaSeen = false;
|
|
25
|
+
for (const node of root.nodes) {
|
|
26
|
+
if (isMedia(node)) {
|
|
27
|
+
mediaSeen = true;
|
|
28
|
+
} else if (mediaSeen && (node.type === "rule" || node.type === "atrule")) {
|
|
29
|
+
report({ message: messages.rejected, node, result, ruleName });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
rule.ruleName = ruleName;
|
|
35
|
+
rule.messages = messages;
|
|
36
|
+
|
|
37
|
+
export const mediaLast = createPlugin(ruleName, rule);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createPlugin,
|
|
5
|
+
utils: { report, ruleMessages, validateOptions },
|
|
6
|
+
} = stylelint;
|
|
7
|
+
|
|
8
|
+
const ruleName = "nks/scale-of-three";
|
|
9
|
+
|
|
10
|
+
const messages = ruleMessages(ruleName, {
|
|
11
|
+
rejected: (length: string) => `${length} is off the scale. Use 1px or a multiple of 3px (rem at 16px).`,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const LENGTH = /(-?\d*\.?\d+)(px|rem)\b/g;
|
|
15
|
+
const PX_PER_REM = 16;
|
|
16
|
+
const SCALE_STEP_PX = 3;
|
|
17
|
+
const HAIRLINE_PX = 1;
|
|
18
|
+
const ROUNDING_TOLERANCE = 1e-9;
|
|
19
|
+
|
|
20
|
+
function isOnScale(amount: number, unit: string) {
|
|
21
|
+
const px = Math.abs(unit === "rem" ? amount * PX_PER_REM : amount);
|
|
22
|
+
const steps = px / SCALE_STEP_PX;
|
|
23
|
+
const isWholeStep = Math.abs(steps - Math.round(steps)) < ROUNDING_TOLERANCE;
|
|
24
|
+
return px === HAIRLINE_PX || isWholeStep;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rule: stylelint.Rule = (primary) => (root, result) => {
|
|
28
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
root.walkDecls((decl) => {
|
|
33
|
+
for (const [length, amount, unit] of decl.value.matchAll(LENGTH)) {
|
|
34
|
+
const isOffScale = amount !== undefined && unit !== undefined && !isOnScale(Number(amount), unit);
|
|
35
|
+
if (isOffScale) {
|
|
36
|
+
report({ message: messages.rejected(length), node: decl, result, ruleName, word: length });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
rule.ruleName = ruleName;
|
|
43
|
+
rule.messages = messages;
|
|
44
|
+
|
|
45
|
+
export const scaleOfThree = createPlugin(ruleName, rule);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import stylelint from "stylelint";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createPlugin,
|
|
5
|
+
utils: { report, ruleMessages, validateOptions },
|
|
6
|
+
} = stylelint;
|
|
7
|
+
|
|
8
|
+
const ruleName = "nks/var-fallback";
|
|
9
|
+
|
|
10
|
+
const messages = ruleMessages(ruleName, {
|
|
11
|
+
rejected: (name: string) => `Give var(${name}) a fallback: var(${name}, value).`,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const BARE_VAR = /var\(\s*(--[\w-]+)\s*\)/g;
|
|
15
|
+
|
|
16
|
+
const rule: stylelint.Rule = (primary) => (root, result) => {
|
|
17
|
+
if (!validateOptions(result, ruleName, { actual: primary })) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const declaredInFile = new Set<string>();
|
|
22
|
+
root.walkDecls(/^--/, (decl) => {
|
|
23
|
+
declaredInFile.add(decl.prop);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
root.walkDecls((decl) => {
|
|
27
|
+
for (const [bareVar, name] of decl.value.matchAll(BARE_VAR)) {
|
|
28
|
+
const needsFallback = name !== undefined && !declaredInFile.has(name);
|
|
29
|
+
if (needsFallback) {
|
|
30
|
+
report({ message: messages.rejected(name), node: decl, result, ruleName, word: bareVar });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
rule.ruleName = ruleName;
|
|
37
|
+
rule.messages = messages;
|
|
38
|
+
|
|
39
|
+
export const varFallback = createPlugin(ruleName, rule);
|
package/src/stylelint.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { Config } from "stylelint";
|
|
3
|
+
import { layered } from "./stylelint/layered.js";
|
|
4
|
+
import { mediaLast } from "./stylelint/mediaLast.js";
|
|
5
|
+
import { scaleOfThree } from "./stylelint/scaleOfThree.js";
|
|
6
|
+
import { varFallback } from "./stylelint/varFallback.js";
|
|
7
|
+
|
|
8
|
+
const orderPlugin = fileURLToPath(import.meta.resolve("stylelint-order"));
|
|
9
|
+
|
|
10
|
+
const BEM_CLASS = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$/;
|
|
11
|
+
|
|
12
|
+
const BRIGHTNESS = /brightness\(/;
|
|
13
|
+
|
|
14
|
+
const disallowedEverywhere = { filter: [BRIGHTNESS] };
|
|
15
|
+
|
|
16
|
+
function disallowedValueMessage(_property: string, value: string) {
|
|
17
|
+
return BRIGHTNESS.test(value)
|
|
18
|
+
? "Do not use brightness()."
|
|
19
|
+
: "Use a semantic token here. Primitive tokens belong in the token files.";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const config: Config = {
|
|
23
|
+
overrides: [
|
|
24
|
+
{
|
|
25
|
+
files: ["**/tokens/**/*.css"],
|
|
26
|
+
rules: {
|
|
27
|
+
"declaration-property-value-disallowed-list": [disallowedEverywhere, { message: disallowedValueMessage }],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
plugins: [layered, mediaLast, orderPlugin, scaleOfThree, varFallback],
|
|
32
|
+
rules: {
|
|
33
|
+
"color-named": "never",
|
|
34
|
+
"color-no-hex": true,
|
|
35
|
+
"declaration-no-important": true,
|
|
36
|
+
"declaration-property-value-disallowed-list": [
|
|
37
|
+
{ ...disallowedEverywhere, "/.*/": [/var\(--(palette|scale)-/] },
|
|
38
|
+
{ message: disallowedValueMessage },
|
|
39
|
+
],
|
|
40
|
+
"function-disallowed-list": [["rgb", "rgba", "hsl", "hsla"], { message: "Use oklch()." }],
|
|
41
|
+
"max-nesting-depth": 3,
|
|
42
|
+
"nks/layered": true,
|
|
43
|
+
"nks/media-last": true,
|
|
44
|
+
"nks/scale-of-three": true,
|
|
45
|
+
"nks/var-fallback": true,
|
|
46
|
+
"order/order": ["custom-properties", "declarations", "rules", "at-rules"],
|
|
47
|
+
"order/properties-alphabetical-order": true,
|
|
48
|
+
"selector-class-pattern": [
|
|
49
|
+
BEM_CLASS,
|
|
50
|
+
{ message: "Expected BEM kebab-case: block, block__element, block--modifier" },
|
|
51
|
+
],
|
|
52
|
+
"unit-allowed-list": ["rem", "px", "%", "fr", "vh", "dvh", "svh", "vw", "deg", "ms", "s"],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default config;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
2
|
+
import { defineConfig, mergeConfig, type ViteUserConfig } from "vitest/config";
|
|
3
|
+
import plain from "./vitest.js";
|
|
4
|
+
|
|
5
|
+
const preset: ViteUserConfig = mergeConfig(
|
|
6
|
+
plain,
|
|
7
|
+
defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
browser: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
headless: true,
|
|
12
|
+
instances: [{ browser: "chromium" }],
|
|
13
|
+
provider: playwright(),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export default preset;
|
package/src/vitest.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
1
|
+
import { defineConfig, type ViteUserConfig } from "vitest/config";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const preset: ViteUserConfig = defineConfig({
|
|
4
4
|
test: {
|
|
5
5
|
environment: "node",
|
|
6
|
-
include: ["**/*.test.ts"],
|
|
7
|
-
restoreMocks: true,
|
|
8
6
|
passWithNoTests: false,
|
|
7
|
+
restoreMocks: true,
|
|
9
8
|
},
|
|
10
9
|
});
|
|
10
|
+
|
|
11
|
+
export default preset;
|