@instruments/materia-ui 0.4.0 → 0.4.2
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 +2 -4
- package/agent-docs.md +9 -9
- package/dist/components/accordion/accordion-root.d.ts +1 -0
- package/dist/components/accordion/accordion-root.js +1 -0
- package/dist/components/alert/alert-root.d.ts +1 -0
- package/dist/components/alert/alert-root.js +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.d.ts +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.js +1 -0
- package/dist/components/avatar/avatar-root.d.ts +1 -0
- package/dist/components/avatar/avatar-root.js +1 -0
- package/dist/components/badge/badge-root.d.ts +1 -0
- package/dist/components/badge/badge-root.js +1 -0
- package/dist/components/calendar/calendar-root.js +1 -0
- package/dist/components/carousel/carousel-root.d.ts +1 -0
- package/dist/components/carousel/carousel-root.js +1 -0
- package/dist/components/chart/chart-root.d.ts +1 -0
- package/dist/components/chart/chart-root.js +1 -0
- package/dist/components/collapsible/collapsible-root.d.ts +1 -0
- package/dist/components/collapsible/collapsible-root.js +1 -0
- package/dist/components/combobox/combobox-root.d.ts +1 -0
- package/dist/components/combobox/combobox-root.js +1 -0
- package/dist/components/command/command-dialog.d.ts +5 -1
- package/dist/components/command/command-dialog.js +1 -1
- package/dist/components/command/command-input.js +5 -2
- package/dist/components/command/command-root.d.ts +1 -0
- package/dist/components/command/command-root.js +1 -0
- package/dist/components/dialog/dialog-content.d.ts +2 -0
- package/dist/components/dialog/dialog-content.js +3 -2
- package/dist/components/dialog/dialog-footer.d.ts +2 -0
- package/dist/components/dialog/dialog-footer.js +8 -8
- package/dist/components/dialog/dialog-root.d.ts +1 -0
- package/dist/components/dialog/dialog-root.js +1 -0
- package/dist/components/dialog/dialog-title.d.ts +2 -0
- package/dist/components/dialog/dialog-title.js +3 -1
- package/dist/components/drawer/drawer-root.d.ts +1 -0
- package/dist/components/drawer/drawer-root.js +1 -0
- package/dist/components/heading/heading-root.d.ts +1 -0
- package/dist/components/heading/heading-root.js +1 -0
- package/dist/components/input/input-root.d.ts +1 -0
- package/dist/components/input/input-root.js +1 -0
- package/dist/components/kbd/kbd-root.d.ts +1 -0
- package/dist/components/kbd/kbd-root.js +1 -0
- package/dist/components/logo/logo-mark.d.ts +1 -1
- package/dist/components/logo/logo-mark.js +1 -1
- package/dist/components/logotype/logotype-paths.d.ts +1 -1
- package/dist/components/logotype/logotype-paths.js +1 -1
- package/dist/components/month-year-picker/month-year-picker-root.d.ts +1 -0
- package/dist/components/month-year-picker/month-year-picker-root.js +1 -0
- package/dist/components/native-select/native-select-root.d.ts +1 -0
- package/dist/components/native-select/native-select-root.js +1 -0
- package/dist/components/pill/pill-root.d.ts +1 -0
- package/dist/components/pill/pill-root.js +1 -0
- package/dist/components/popover/popover-root.d.ts +1 -0
- package/dist/components/popover/popover-root.js +1 -0
- package/dist/components/quantity-control/quantity-control-root.d.ts +1 -0
- package/dist/components/quantity-control/quantity-control-root.js +1 -0
- package/dist/components/segmented-control/segmented-control-root.js +6 -3
- package/dist/components/segmented-control/segmented-control-variants.js +4 -1
- package/dist/components/select/select-root.d.ts +1 -0
- package/dist/components/select/select-root.js +1 -0
- package/dist/components/separator/separator-root.d.ts +1 -0
- package/dist/components/separator/separator-root.js +1 -0
- package/dist/components/sheet/sheet-root.d.ts +1 -0
- package/dist/components/sheet/sheet-root.js +1 -0
- package/dist/components/spinner/spinner-root.d.ts +1 -0
- package/dist/components/spinner/spinner-root.js +1 -0
- package/dist/components/stepper/stepper-root.d.ts +1 -0
- package/dist/components/stepper/stepper-root.js +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.d.ts +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.js +1 -0
- package/dist/components/swatch-selector/swatch-selector-item.js +4 -1
- package/dist/components/switch/switch-root.d.ts +1 -0
- package/dist/components/switch/switch-root.js +1 -0
- package/dist/components/tag/tag-root.d.ts +1 -0
- package/dist/components/tag/tag-root.js +1 -0
- package/dist/components/textarea/textarea-root.d.ts +1 -0
- package/dist/components/textarea/textarea-root.js +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.d.ts +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.js +1 -0
- package/dist/components/toast/toast-root.d.ts +1 -0
- package/dist/components/toast/toast-root.js +1 -0
- package/dist/components/tooltip/tooltip-root.d.ts +1 -0
- package/dist/components/tooltip/tooltip-root.js +1 -0
- package/dist/compositions/empty-state/empty-state-root.d.ts +1 -0
- package/dist/compositions/empty-state/empty-state-root.js +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.d.ts +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.js +1 -0
- package/dist/data/decision-trees.d.ts +1 -1
- package/dist/data/decision-trees.js +1 -1
- package/dist/data/docs-manifest.d.ts +2 -0
- package/dist/data/docs-manifest.js +235 -68
- package/dist/styles/index.css +0 -1
- package/dist/tokens/segmented.d.ts +8 -0
- package/dist/tokens/segmented.js +26 -0
- package/dist/utils/cn-theme.js +1 -1
- package/lint/README.md +6 -2
- package/lint/doctrine-rules.mjs +4 -4
- package/lint/oxlint/index.d.mts +13 -5
- package/lint/oxlint/index.mjs +26 -0
- package/lint/oxlint/no-naked-element.mjs +123 -65
- package/lint/oxlint/no-render-layout-as.mjs +61 -56
- package/lint/scan-classes.d.mts +58 -5
- package/lint/scan-classes.mjs +274 -38
- package/lint/tokens.d.mts +16 -8
- package/lint/tokens.mjs +13 -3
- package/lint/type-case.d.mts +16 -8
- package/lint/type-case.mjs +17 -0
- package/package.json +27 -14
- package/lint/oxlint/fixtures/allowed.tsx +0 -12
- package/lint/oxlint/fixtures/fixture.config.mjs +0 -29
- package/lint/oxlint/fixtures/invalid.tsx +0 -24
- package/lint/oxlint/fixtures/layout-only.tsx +0 -26
- package/lint/oxlint/fixtures/render-as-invalid.tsx +0 -11
- package/lint/oxlint/fixtures/render-as-valid.tsx +0 -14
- package/lint/oxlint/fixtures/valid.tsx +0 -18
- package/lint/oxlint/no-naked-element.test.ts +0 -155
- package/lint/oxlint/no-render-layout-as.test.ts +0 -81
package/lint/scan-classes.d.mts
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/scan-classes.mjs - do not edit.
|
|
2
|
+
// Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
|
|
3
|
+
|
|
4
|
+
export interface Options {
|
|
5
|
+
/**
|
|
6
|
+
* Files and directories to scan.
|
|
7
|
+
*/
|
|
8
|
+
paths: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Path fragments to skip.
|
|
11
|
+
*/
|
|
12
|
+
excludes: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Whether the `hex` rule is on.
|
|
15
|
+
*/
|
|
16
|
+
hex: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the `arbitrary` rule is on.
|
|
19
|
+
*/
|
|
20
|
+
arbitrary: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to print a JSON document instead of text lines.
|
|
23
|
+
*/
|
|
24
|
+
json: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to print usage and stop.
|
|
27
|
+
*/
|
|
28
|
+
help: boolean;
|
|
29
|
+
}
|
|
30
|
+
export type RuleName = "hex" | "arbitrary";
|
|
31
|
+
export interface Region {
|
|
32
|
+
/**
|
|
33
|
+
* Offset of the region in the file.
|
|
34
|
+
*/
|
|
35
|
+
start: number;
|
|
36
|
+
/**
|
|
37
|
+
* The region's text.
|
|
38
|
+
*/
|
|
39
|
+
body: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Violation {
|
|
42
|
+
/**
|
|
43
|
+
* The rule that matched.
|
|
44
|
+
*/
|
|
45
|
+
rule: RuleName;
|
|
46
|
+
/**
|
|
47
|
+
* The offending text.
|
|
48
|
+
*/
|
|
49
|
+
value: string;
|
|
50
|
+
/**
|
|
51
|
+
* One-based line of the match.
|
|
52
|
+
*/
|
|
53
|
+
line: number;
|
|
54
|
+
/**
|
|
55
|
+
* One-based column of the match.
|
|
56
|
+
*/
|
|
57
|
+
column: number;
|
|
58
|
+
}
|
package/lint/scan-classes.mjs
CHANGED
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
* @doctrine Class strings carry no raw hex/oklch/oklab colours and no arbitrary Tailwind values - only design tokens and theme utilities.
|
|
16
16
|
*
|
|
17
17
|
* Usage:
|
|
18
|
-
*
|
|
18
|
+
* materia-ui-lint <path...> [--arbitrary] [--all] [--no-hex]
|
|
19
|
+
* [--exclude <substr>] [--json]
|
|
20
|
+
* materia-ui-lint --help
|
|
21
|
+
*
|
|
22
|
+
* Exit codes:
|
|
23
|
+
* 0 no violations (or --help)
|
|
24
|
+
* 1 violations found
|
|
25
|
+
* 2 usage error (unknown flag, missing value, no paths)
|
|
26
|
+
* 3 a path could not be read
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
@@ -36,14 +44,106 @@ const HEX_PATTERN = /#[0-9a-fA-F]{3,8}\b/gu;
|
|
|
36
44
|
const COLOR_FUNCTION = /\bokl(?:ch|ab)\(/giu;
|
|
37
45
|
const ARBITRARY_PATTERN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\[[^\]]+\]/gu;
|
|
38
46
|
|
|
47
|
+
const EXIT_VIOLATIONS = 1;
|
|
48
|
+
const EXIT_USAGE = 2;
|
|
49
|
+
const EXIT_IO = 3;
|
|
50
|
+
|
|
51
|
+
/** @type {Record<RuleName, string>} */
|
|
52
|
+
const MESSAGES = {
|
|
53
|
+
hex: "raw hex/oklch colour in class string - use a design token",
|
|
54
|
+
arbitrary: "arbitrary Tailwind value - use a token or theme utility",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const USAGE = `Usage: materia-ui-lint <path...> [options]
|
|
58
|
+
|
|
59
|
+
Scan class strings for raw colours and arbitrary Tailwind values.
|
|
60
|
+
Files (.ts .tsx .js .jsx .mjs .cjs) are found by walking each path.
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--arbitrary Also flag arbitrary Tailwind values such as mt-[13px]
|
|
64
|
+
--all Turn on every rule (hex and arbitrary)
|
|
65
|
+
--no-hex Turn the hex rule off
|
|
66
|
+
--exclude <substr> Skip paths containing <substr> (repeatable;
|
|
67
|
+
--exclude=<substr> also works)
|
|
68
|
+
--json Print one JSON document instead of text lines:
|
|
69
|
+
{ "violations": [{ file, line, column, rule, value,
|
|
70
|
+
message }], "rules": [...], "files": <count> }
|
|
71
|
+
-h, --help Show this help
|
|
72
|
+
|
|
73
|
+
Exit codes:
|
|
74
|
+
0 no violations
|
|
75
|
+
1 violations found
|
|
76
|
+
2 usage error
|
|
77
|
+
3 a path could not be read
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
/** A failure that ends the run with a message and a specific exit code. */
|
|
81
|
+
class ScanError extends Error {
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} message What to print to stderr.
|
|
84
|
+
* @param {number} exitCode The process exit code.
|
|
85
|
+
* @param {ErrorOptions} [options] The underlying cause, if any.
|
|
86
|
+
*/
|
|
87
|
+
constructor(message, exitCode, options) {
|
|
88
|
+
super(message, options);
|
|
89
|
+
this.name = "ScanError";
|
|
90
|
+
this.exitCode = exitCode;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} message What was wrong with the command line.
|
|
96
|
+
* @returns {ScanError} An exit-2 error that points at `--help`.
|
|
97
|
+
*/
|
|
98
|
+
const usageError = (message) =>
|
|
99
|
+
new ScanError(`${message} (see --help)`, EXIT_USAGE);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} Options
|
|
103
|
+
* @property {string[]} paths Files and directories to scan.
|
|
104
|
+
* @property {string[]} excludes Path fragments to skip.
|
|
105
|
+
* @property {boolean} hex Whether the `hex` rule is on.
|
|
106
|
+
* @property {boolean} arbitrary Whether the `arbitrary` rule is on.
|
|
107
|
+
* @property {boolean} json Whether to print a JSON document instead of text lines.
|
|
108
|
+
* @property {boolean} help Whether to print usage and stop.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/** @typedef {"hex" | "arbitrary"} RuleName */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {object} Region
|
|
115
|
+
* @property {number} start Offset of the region in the file.
|
|
116
|
+
* @property {string} body The region's text.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} Violation
|
|
121
|
+
* @property {RuleName} rule The rule that matched.
|
|
122
|
+
* @property {string} value The offending text.
|
|
123
|
+
* @property {number} line One-based line of the match.
|
|
124
|
+
* @property {number} column One-based column of the match.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string[]} argv The command-line arguments after the script name.
|
|
129
|
+
* @returns {Options} What to scan and which rules are on.
|
|
130
|
+
* @throws {ScanError} On an unknown flag or an `--exclude` without a value.
|
|
131
|
+
*/
|
|
39
132
|
const parseArgs = (argv) => {
|
|
133
|
+
/** @type {string[]} */
|
|
40
134
|
const paths = [];
|
|
135
|
+
/** @type {string[]} */
|
|
41
136
|
const excludes = [];
|
|
42
137
|
let hex = true;
|
|
43
138
|
let arbitrary = false;
|
|
139
|
+
let json = false;
|
|
44
140
|
|
|
45
|
-
|
|
46
|
-
|
|
141
|
+
// `--exclude <substr>` takes the next argument, so walk an iterator.
|
|
142
|
+
const args = argv.values();
|
|
143
|
+
for (const arg of args) {
|
|
144
|
+
if (arg === "--help" || arg === "-h") {
|
|
145
|
+
return { paths, excludes, hex, arbitrary, json, help: true };
|
|
146
|
+
}
|
|
47
147
|
if (arg === "--no-hex") {
|
|
48
148
|
hex = false;
|
|
49
149
|
} else if (arg === "--arbitrary") {
|
|
@@ -51,32 +151,94 @@ const parseArgs = (argv) => {
|
|
|
51
151
|
} else if (arg === "--all") {
|
|
52
152
|
hex = true;
|
|
53
153
|
arbitrary = true;
|
|
154
|
+
} else if (arg === "--json") {
|
|
155
|
+
json = true;
|
|
54
156
|
} else if (arg === "--exclude") {
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
157
|
+
const next = args.next();
|
|
158
|
+
if (next.done === true || next.value.startsWith("-")) {
|
|
159
|
+
throw usageError("--exclude needs a value");
|
|
58
160
|
}
|
|
161
|
+
excludes.push(next.value);
|
|
59
162
|
} else if (arg.startsWith("--exclude=")) {
|
|
60
|
-
|
|
61
|
-
|
|
163
|
+
const value = arg.slice("--exclude=".length);
|
|
164
|
+
if (value === "") {
|
|
165
|
+
throw usageError("--exclude needs a value");
|
|
166
|
+
}
|
|
167
|
+
excludes.push(value);
|
|
168
|
+
} else if (arg.startsWith("-")) {
|
|
169
|
+
throw usageError(`unknown flag ${arg}`);
|
|
170
|
+
} else {
|
|
62
171
|
paths.push(arg);
|
|
63
172
|
}
|
|
64
173
|
}
|
|
65
174
|
|
|
66
|
-
return { paths, excludes, hex, arbitrary };
|
|
175
|
+
return { paths, excludes, hex, arbitrary, json, help: false };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {unknown} error Whatever a filesystem call threw.
|
|
180
|
+
* @returns {string} Its error code, or its message when it has none.
|
|
181
|
+
*/
|
|
182
|
+
const describeFsError = (error) => {
|
|
183
|
+
if (error instanceof Error && "code" in error) {
|
|
184
|
+
return String(error.code);
|
|
185
|
+
}
|
|
186
|
+
return error instanceof Error ? error.message : String(error);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Run a filesystem call, naming the path if it fails.
|
|
191
|
+
*
|
|
192
|
+
* @template T
|
|
193
|
+
* @param {string} target The path the call touches.
|
|
194
|
+
* @param {() => T} action The filesystem call.
|
|
195
|
+
* @returns {T} What the call returned.
|
|
196
|
+
* @throws {ScanError} When the call throws.
|
|
197
|
+
*/
|
|
198
|
+
const reading = (target, action) => {
|
|
199
|
+
try {
|
|
200
|
+
return action();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new ScanError(
|
|
203
|
+
`cannot read ${target} (${describeFsError(error)})`,
|
|
204
|
+
EXIT_IO,
|
|
205
|
+
{ cause: error }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
67
208
|
};
|
|
68
209
|
|
|
69
|
-
|
|
70
|
-
|
|
210
|
+
/**
|
|
211
|
+
* @param {string[]} fragments Path fragments to skip.
|
|
212
|
+
* @returns {RegExp | undefined} A pattern matching any of them, if there are any.
|
|
213
|
+
*/
|
|
214
|
+
const excludePattern = (fragments) =>
|
|
215
|
+
fragments.length === 0
|
|
216
|
+
? undefined
|
|
217
|
+
: new RegExp(
|
|
218
|
+
fragments
|
|
219
|
+
.map((fragment) =>
|
|
220
|
+
fragment.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`)
|
|
221
|
+
)
|
|
222
|
+
.join("|"),
|
|
223
|
+
"u"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string} target A file or directory.
|
|
228
|
+
* @param {RegExp | undefined} excluded Matches the paths to skip.
|
|
229
|
+
* @param {string[]} out Collects the scannable files.
|
|
230
|
+
*/
|
|
231
|
+
const walk = (target, excluded, out) => {
|
|
232
|
+
if (excluded?.test(target) === true) {
|
|
71
233
|
return;
|
|
72
234
|
}
|
|
73
|
-
const stats = statSync(target);
|
|
235
|
+
const stats = reading(target, () => statSync(target));
|
|
74
236
|
if (stats.isDirectory()) {
|
|
75
|
-
for (const entry of readdirSync(target)) {
|
|
237
|
+
for (const entry of reading(target, () => readdirSync(target))) {
|
|
76
238
|
if (entry === "node_modules" || entry.startsWith(".")) {
|
|
77
239
|
continue;
|
|
78
240
|
}
|
|
79
|
-
walk(path.join(target, entry),
|
|
241
|
+
walk(path.join(target, entry), excluded, out);
|
|
80
242
|
}
|
|
81
243
|
return;
|
|
82
244
|
}
|
|
@@ -86,6 +248,12 @@ const walk = (target, excludes, out) => {
|
|
|
86
248
|
}
|
|
87
249
|
};
|
|
88
250
|
|
|
251
|
+
/**
|
|
252
|
+
* @param {string} text The file text.
|
|
253
|
+
* @param {number} openIndex Offset of the opening quote.
|
|
254
|
+
* @param {string} quote The quote character that closes the string.
|
|
255
|
+
* @returns {number} Offset of the closing quote, or the last offset if unclosed.
|
|
256
|
+
*/
|
|
89
257
|
const findStringEnd = (text, openIndex, quote) => {
|
|
90
258
|
for (let index = openIndex + 1; index < text.length; index += 1) {
|
|
91
259
|
const char = text[index];
|
|
@@ -100,6 +268,13 @@ const findStringEnd = (text, openIndex, quote) => {
|
|
|
100
268
|
return text.length - 1;
|
|
101
269
|
};
|
|
102
270
|
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} text The file text.
|
|
273
|
+
* @param {number} openIndex Offset of the opening bracket.
|
|
274
|
+
* @param {string} open The opening bracket character.
|
|
275
|
+
* @param {string} close The closing bracket character.
|
|
276
|
+
* @returns {number} Offset of the matching close, or the last offset if unbalanced.
|
|
277
|
+
*/
|
|
103
278
|
const findMatching = (text, openIndex, open, close) => {
|
|
104
279
|
let depth = 0;
|
|
105
280
|
for (let index = openIndex; index < text.length; index += 1) {
|
|
@@ -121,8 +296,12 @@ const findMatching = (text, openIndex, open, close) => {
|
|
|
121
296
|
* attribute values plus the argument lists of the common class-composition
|
|
122
297
|
* helpers. Scanning only these ranges keeps hex detection off inline styles and
|
|
123
298
|
* ordinary colour constants.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} text The file text.
|
|
301
|
+
* @returns {Region[]} The class-string ranges, in source order within each kind.
|
|
124
302
|
*/
|
|
125
303
|
const extractClassRegions = (text) => {
|
|
304
|
+
/** @type {Region[]} */
|
|
126
305
|
const regions = [];
|
|
127
306
|
|
|
128
307
|
CLASS_ATTRIBUTE.lastIndex = 0;
|
|
@@ -159,6 +338,11 @@ const extractClassRegions = (text) => {
|
|
|
159
338
|
return regions;
|
|
160
339
|
};
|
|
161
340
|
|
|
341
|
+
/**
|
|
342
|
+
* @param {string} text The file text.
|
|
343
|
+
* @param {number} offset A character offset into it.
|
|
344
|
+
* @returns {{ line: number, column: number }} The one-based line and column.
|
|
345
|
+
*/
|
|
162
346
|
const lineColumnAt = (text, offset) => {
|
|
163
347
|
let line = 1;
|
|
164
348
|
let lineStart = 0;
|
|
@@ -171,6 +355,14 @@ const lineColumnAt = (text, offset) => {
|
|
|
171
355
|
return { line, column: offset - lineStart + 1 };
|
|
172
356
|
};
|
|
173
357
|
|
|
358
|
+
/**
|
|
359
|
+
* @param {Region} region The class-string range to search.
|
|
360
|
+
* @param {RegExp} pattern The global pattern to match.
|
|
361
|
+
* @param {RuleName} rule The rule a match counts against.
|
|
362
|
+
* @param {string} text The whole file, for line and column.
|
|
363
|
+
* @param {Set<string>} seen Offset and rule pairs already reported.
|
|
364
|
+
* @param {Violation[]} violations Collects the matches.
|
|
365
|
+
*/
|
|
174
366
|
const collectMatches = (region, pattern, rule, text, seen, violations) => {
|
|
175
367
|
pattern.lastIndex = 0;
|
|
176
368
|
let match = pattern.exec(region.body);
|
|
@@ -186,10 +378,17 @@ const collectMatches = (region, pattern, rule, text, seen, violations) => {
|
|
|
186
378
|
}
|
|
187
379
|
};
|
|
188
380
|
|
|
381
|
+
/**
|
|
382
|
+
* @param {string} file The file to scan.
|
|
383
|
+
* @param {{ hex: boolean, arbitrary: boolean }} rules Which rules are on.
|
|
384
|
+
* @returns {Violation[]} The violations, ordered by position.
|
|
385
|
+
*/
|
|
189
386
|
const scanFile = (file, rules) => {
|
|
190
|
-
const text = readFileSync(file, "utf-8");
|
|
387
|
+
const text = reading(file, () => readFileSync(file, "utf-8"));
|
|
191
388
|
const regions = extractClassRegions(text);
|
|
389
|
+
/** @type {Violation[]} */
|
|
192
390
|
const violations = [];
|
|
391
|
+
/** @type {Set<string>} */
|
|
193
392
|
const seen = new Set();
|
|
194
393
|
|
|
195
394
|
for (const region of regions) {
|
|
@@ -213,42 +412,79 @@ const scanFile = (file, rules) => {
|
|
|
213
412
|
return violations;
|
|
214
413
|
};
|
|
215
414
|
|
|
216
|
-
|
|
217
|
-
|
|
415
|
+
/**
|
|
416
|
+
* @param {Options} options The parsed command line.
|
|
417
|
+
* @returns {number} The exit code.
|
|
418
|
+
* @throws {ScanError} When no paths are given or a path cannot be read.
|
|
419
|
+
*/
|
|
420
|
+
const run = (options) => {
|
|
421
|
+
const { paths, excludes, hex, arbitrary, json } = options;
|
|
218
422
|
if (paths.length === 0) {
|
|
219
|
-
|
|
220
|
-
process.exit(2);
|
|
423
|
+
throw usageError("no paths supplied");
|
|
221
424
|
}
|
|
222
425
|
|
|
426
|
+
/** @type {string[]} */
|
|
223
427
|
const files = [];
|
|
428
|
+
const excluded = excludePattern(excludes);
|
|
224
429
|
for (const target of paths) {
|
|
225
|
-
walk(target,
|
|
430
|
+
walk(target, excluded, files);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** @type {RuleName[]} */
|
|
434
|
+
const rules = [];
|
|
435
|
+
if (hex) {
|
|
436
|
+
rules.push("hex");
|
|
437
|
+
}
|
|
438
|
+
if (arbitrary) {
|
|
439
|
+
rules.push("arbitrary");
|
|
226
440
|
}
|
|
227
441
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
442
|
+
const reported = files.flatMap((file) =>
|
|
443
|
+
scanFile(file, { hex, arbitrary }).map((violation) => ({
|
|
444
|
+
file,
|
|
445
|
+
line: violation.line,
|
|
446
|
+
column: violation.column,
|
|
447
|
+
rule: violation.rule,
|
|
448
|
+
value: violation.value,
|
|
449
|
+
message: MESSAGES[violation.rule],
|
|
450
|
+
}))
|
|
451
|
+
);
|
|
232
452
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
453
|
+
if (json) {
|
|
454
|
+
process.stdout.write(
|
|
455
|
+
`${JSON.stringify({ violations: reported, rules, files: files.length })}\n`
|
|
456
|
+
);
|
|
457
|
+
} else {
|
|
458
|
+
for (const v of reported) {
|
|
238
459
|
process.stdout.write(
|
|
239
|
-
`${file}:${
|
|
460
|
+
`${v.file}:${v.line}:${v.column} ${v.rule} ${v.value} ${v.message}\n`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (reported.length > 0) {
|
|
464
|
+
process.stderr.write(
|
|
465
|
+
`\nscan-classes: ${reported.length} violation(s) [rules: ${rules.join(", ")}]\n`
|
|
240
466
|
);
|
|
241
467
|
}
|
|
242
468
|
}
|
|
243
469
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
|
|
470
|
+
return reported.length > 0 ? EXIT_VIOLATIONS : 0;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const main = () => {
|
|
474
|
+
try {
|
|
475
|
+
const options = parseArgs(process.argv.slice(2));
|
|
476
|
+
if (options.help) {
|
|
477
|
+
process.stdout.write(USAGE);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Set rather than `process.exit()` so piped output is flushed first.
|
|
481
|
+
process.exitCode = run(options);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (!(error instanceof ScanError)) {
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
process.stderr.write(`scan-classes: ${error.message}\n`);
|
|
487
|
+
process.exitCode = error.exitCode;
|
|
252
488
|
}
|
|
253
489
|
};
|
|
254
490
|
|
package/lint/tokens.d.mts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/tokens.mjs - do not edit.
|
|
2
|
+
// Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Every `--color-<name>` the stylesheet declares, as `<name>`.
|
|
6
|
+
*/
|
|
7
|
+
export const colorTokens: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Every `--text-<name>` type role the stylesheet declares, as `<name>`.
|
|
10
|
+
*/
|
|
5
11
|
export const textRoles: readonly string[];
|
|
6
|
-
|
|
7
12
|
/**
|
|
8
|
-
* `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a
|
|
9
|
-
*
|
|
13
|
+
* `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a declared
|
|
14
|
+
* colour (`bg-primary`, `border-muted-foreground`) and every `text-<role>`,
|
|
15
|
+
* which the rule would otherwise read as an undeclared colour.
|
|
10
16
|
*/
|
|
11
17
|
export const rawColorsRule: {
|
|
12
|
-
readonly "shadcn/no-raw-colors":
|
|
18
|
+
readonly "shadcn/no-raw-colors": [
|
|
13
19
|
"error",
|
|
14
|
-
{
|
|
20
|
+
{
|
|
21
|
+
readonly allow: readonly string[];
|
|
22
|
+
},
|
|
15
23
|
];
|
|
16
24
|
};
|
package/lint/tokens.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
// GENERATED by scripts/generate-lint-tokens.
|
|
1
|
+
// GENERATED by scripts/generate-lint-tokens.ts from src/styles/index.css - do not edit.
|
|
2
2
|
// Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-tokens`.
|
|
3
3
|
//
|
|
4
4
|
// Published as `@instruments/materia-ui/lint/tokens`. `shadcn/no-raw-colors`
|
|
5
5
|
// skips stylesheets under node_modules, so it cannot see the tokens this package
|
|
6
6
|
// declares; spread `rawColorsRule` into an app's oxlint `rules` to allow them.
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Every `--color-<name>` the stylesheet declares, as `<name>`.
|
|
10
|
+
*
|
|
11
|
+
* @type {readonly string[]}
|
|
12
|
+
*/
|
|
9
13
|
export const colorTokens = [
|
|
10
14
|
"accent",
|
|
11
15
|
"accent-foreground",
|
|
@@ -95,7 +99,11 @@ export const colorTokens = [
|
|
|
95
99
|
"warning-soft",
|
|
96
100
|
];
|
|
97
101
|
|
|
98
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Every `--text-<name>` type role the stylesheet declares, as `<name>`.
|
|
104
|
+
*
|
|
105
|
+
* @type {readonly string[]}
|
|
106
|
+
*/
|
|
99
107
|
export const textRoles = [
|
|
100
108
|
"2xs",
|
|
101
109
|
"calendar",
|
|
@@ -117,6 +125,8 @@ export const textRoles = [
|
|
|
117
125
|
* `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a declared
|
|
118
126
|
* colour (`bg-primary`, `border-muted-foreground`) and every `text-<role>`,
|
|
119
127
|
* which the rule would otherwise read as an undeclared colour.
|
|
128
|
+
*
|
|
129
|
+
* @type {{ readonly "shadcn/no-raw-colors": ["error", { readonly allow: readonly string[] }] }}
|
|
120
130
|
*/
|
|
121
131
|
export const rawColorsRule = {
|
|
122
132
|
"shadcn/no-raw-colors": [
|
package/lint/type-case.d.mts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
readonly allow: readonly string[];
|
|
4
|
-
}
|
|
1
|
+
// GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/type-case.mjs - do not edit.
|
|
2
|
+
// Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
|
|
5
3
|
|
|
6
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The modifiers that may sit beside a role: weights, mono, italic and the like.
|
|
6
|
+
*/
|
|
7
7
|
export const typeCase: TypeCase;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `howells/no-raw-type-utilities` with the type case. Spread it into an oxlint
|
|
10
|
+
* `rules`.
|
|
11
|
+
*/
|
|
10
12
|
export const typeCaseRule: {
|
|
11
|
-
readonly "howells/no-raw-type-utilities":
|
|
13
|
+
readonly "howells/no-raw-type-utilities": ["error", TypeCase];
|
|
12
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The `no-raw-type-utilities` options: the modifiers allowed beside a type role.
|
|
17
|
+
*/
|
|
18
|
+
export interface TypeCase {
|
|
19
|
+
readonly allow: readonly string[];
|
|
20
|
+
}
|
package/lint/type-case.mjs
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
//
|
|
16
16
|
// @doctrine Text takes its size, leading and tracking from a role on the type ramp (text-caption to text-title) or the Text and Heading size props, never from raw Tailwind typography such as text-sm, leading-* or tracking-*.
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The `no-raw-type-utilities` options: the modifiers allowed beside a type role.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {{ readonly allow: readonly string[] }} TypeCase
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The modifiers that may sit beside a role: weights, mono, italic and the like.
|
|
26
|
+
*
|
|
27
|
+
* @type {TypeCase}
|
|
28
|
+
*/
|
|
18
29
|
export const typeCase = {
|
|
19
30
|
allow: [
|
|
20
31
|
"font-normal",
|
|
@@ -28,6 +39,12 @@ export const typeCase = {
|
|
|
28
39
|
],
|
|
29
40
|
};
|
|
30
41
|
|
|
42
|
+
/**
|
|
43
|
+
* `howells/no-raw-type-utilities` with the type case. Spread it into an oxlint
|
|
44
|
+
* `rules`.
|
|
45
|
+
*
|
|
46
|
+
* @type {{ readonly "howells/no-raw-type-utilities": ["error", TypeCase] }}
|
|
47
|
+
*/
|
|
31
48
|
export const typeCaseRule = {
|
|
32
49
|
"howells/no-raw-type-utilities": ["error", typeCase],
|
|
33
50
|
};
|