onsystem 0.9.1 → 0.10.1
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 +16 -1
- package/dist/cli.js +152 -56
- package/dist/{config-B98w8bcg.d.ts → config-BbTW1jKU.d.ts} +12 -2
- package/dist/{design-system-DZm2aP1z.js → design-system-CPH4C7-1.js} +389 -170
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +2 -2
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/{stdio-BJvjEnKW.js → stdio-BK2kMN1x.js} +681 -339
- package/dist/{workspace-Dqu3nkYM.js → workspace-BINqxi7p.js} +8 -7
- package/package.json +1 -1
- package/schema.json +5 -1
|
@@ -7,7 +7,7 @@ import ts from "typescript";
|
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import postcss from "postcss";
|
|
9
9
|
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
import fsp from "node:fs/promises";
|
|
12
12
|
import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
|
|
13
13
|
import { stemmer } from "stemmer";
|
|
@@ -122,6 +122,74 @@ function isProjectFile(file) {
|
|
|
122
122
|
return !/[\\/]node_modules[\\/]/.test(file);
|
|
123
123
|
}
|
|
124
124
|
//#endregion
|
|
125
|
+
//#region src/util/strings.ts
|
|
126
|
+
/** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
|
|
127
|
+
function pascalCase(input) {
|
|
128
|
+
return input.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
129
|
+
}
|
|
130
|
+
/** `AlertDialog` → `["Alert", "Dialog"]`, `isHTMLSafe` → `["is", "HTML", "Safe"]`. */
|
|
131
|
+
function splitWords(input) {
|
|
132
|
+
return input.split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
function isPascalCase(name) {
|
|
135
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
136
|
+
}
|
|
137
|
+
/** Optimal string alignment distance (Levenshtein plus adjacent transpositions). */
|
|
138
|
+
function editDistance(a, b) {
|
|
139
|
+
if (a === b) return 0;
|
|
140
|
+
if (!a.length) return b.length;
|
|
141
|
+
if (!b.length) return a.length;
|
|
142
|
+
const rows = a.length + 1;
|
|
143
|
+
const cols = b.length + 1;
|
|
144
|
+
const d = new Array(rows * cols).fill(0);
|
|
145
|
+
const at = (i, j) => d[i * cols + j] ?? 0;
|
|
146
|
+
for (let i = 0; i < rows; i++) d[i * cols] = i;
|
|
147
|
+
for (let j = 0; j < cols; j++) d[j] = j;
|
|
148
|
+
for (let i = 1; i < rows; i++) for (let j = 1; j < cols; j++) {
|
|
149
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
150
|
+
let v = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + cost);
|
|
151
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, at(i - 2, j - 2) + 1);
|
|
152
|
+
d[i * cols + j] = v;
|
|
153
|
+
}
|
|
154
|
+
return at(a.length, b.length);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The candidate closest to `input` by case-insensitive edit distance, if it is
|
|
158
|
+
* close enough to be a plausible typo (at most a third of the length, min 1).
|
|
159
|
+
*/
|
|
160
|
+
function closest(input, candidates, maxRatio = 1 / 3) {
|
|
161
|
+
const needle = input.toLowerCase();
|
|
162
|
+
const limit = Math.max(1, Math.floor(needle.length * maxRatio));
|
|
163
|
+
let best;
|
|
164
|
+
let bestScore = Infinity;
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
const gap = Math.abs(candidate.length - needle.length);
|
|
167
|
+
if (gap > limit || gap >= bestScore) continue;
|
|
168
|
+
const score = editDistance(needle, candidate.toLowerCase());
|
|
169
|
+
if (score < bestScore) {
|
|
170
|
+
best = candidate;
|
|
171
|
+
bestScore = score;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return bestScore <= limit ? best : void 0;
|
|
175
|
+
}
|
|
176
|
+
function truncate(text, max) {
|
|
177
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* `text` as a JSON string, with U+0085, U+2028 and U+2029 escaped too, so it stays on one line
|
|
181
|
+
* (JSON escapes the other line breaks).
|
|
182
|
+
*/
|
|
183
|
+
function json(text) {
|
|
184
|
+
return JSON.stringify(text).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
185
|
+
}
|
|
186
|
+
function plural$1(count, word, pluralWord = `${word}s`) {
|
|
187
|
+
return `${count} ${count === 1 ? word : pluralWord}`;
|
|
188
|
+
}
|
|
189
|
+
function unique(items) {
|
|
190
|
+
return [...new Set(items)];
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
125
193
|
//#region src/util/paths.ts
|
|
126
194
|
function toPosix(p) {
|
|
127
195
|
return p.split(path.sep).join("/");
|
|
@@ -271,6 +339,14 @@ function splitTopLevel$2(text) {
|
|
|
271
339
|
function escapeRegExp(text) {
|
|
272
340
|
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
273
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* A file, folder or import path, as written when it holds only the characters of a path (letters,
|
|
344
|
+
* digits and `_ @ $ . + ~ / \ : ( ) [ ] { } # % , = ! -`, as in `app/(site)/[id]/page.tsx`);
|
|
345
|
+
* otherwise, as for a path that holds a space or a line break (git allows one), JSON-quoted.
|
|
346
|
+
*/
|
|
347
|
+
function pathOf(file) {
|
|
348
|
+
return /^[\p{L}\p{N}_@$.+~/\\:()[\]{}#%,=!-]+$/u.test(file) ? file : json(file);
|
|
349
|
+
}
|
|
274
350
|
//#endregion
|
|
275
351
|
//#region src/package-source.ts
|
|
276
352
|
/**
|
|
@@ -567,67 +643,6 @@ function importedStylesheets(files, resolve) {
|
|
|
567
643
|
return out;
|
|
568
644
|
}
|
|
569
645
|
//#endregion
|
|
570
|
-
//#region src/util/strings.ts
|
|
571
|
-
/** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
|
|
572
|
-
function pascalCase(input) {
|
|
573
|
-
return input.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
574
|
-
}
|
|
575
|
-
/** `AlertDialog` → `["Alert", "Dialog"]`, `isHTMLSafe` → `["is", "HTML", "Safe"]`. */
|
|
576
|
-
function splitWords(input) {
|
|
577
|
-
return input.split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean);
|
|
578
|
-
}
|
|
579
|
-
function isPascalCase(name) {
|
|
580
|
-
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
581
|
-
}
|
|
582
|
-
/** Optimal string alignment distance (Levenshtein plus adjacent transpositions). */
|
|
583
|
-
function editDistance(a, b) {
|
|
584
|
-
if (a === b) return 0;
|
|
585
|
-
if (!a.length) return b.length;
|
|
586
|
-
if (!b.length) return a.length;
|
|
587
|
-
const rows = a.length + 1;
|
|
588
|
-
const cols = b.length + 1;
|
|
589
|
-
const d = new Array(rows * cols).fill(0);
|
|
590
|
-
const at = (i, j) => d[i * cols + j] ?? 0;
|
|
591
|
-
for (let i = 0; i < rows; i++) d[i * cols] = i;
|
|
592
|
-
for (let j = 0; j < cols; j++) d[j] = j;
|
|
593
|
-
for (let i = 1; i < rows; i++) for (let j = 1; j < cols; j++) {
|
|
594
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
595
|
-
let v = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + cost);
|
|
596
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, at(i - 2, j - 2) + 1);
|
|
597
|
-
d[i * cols + j] = v;
|
|
598
|
-
}
|
|
599
|
-
return at(a.length, b.length);
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* The candidate closest to `input` by case-insensitive edit distance, if it is
|
|
603
|
-
* close enough to be a plausible typo (at most a third of the length, min 1).
|
|
604
|
-
*/
|
|
605
|
-
function closest(input, candidates, maxRatio = 1 / 3) {
|
|
606
|
-
const needle = input.toLowerCase();
|
|
607
|
-
const limit = Math.max(1, Math.floor(needle.length * maxRatio));
|
|
608
|
-
let best;
|
|
609
|
-
let bestScore = Infinity;
|
|
610
|
-
for (const candidate of candidates) {
|
|
611
|
-
const gap = Math.abs(candidate.length - needle.length);
|
|
612
|
-
if (gap > limit || gap >= bestScore) continue;
|
|
613
|
-
const score = editDistance(needle, candidate.toLowerCase());
|
|
614
|
-
if (score < bestScore) {
|
|
615
|
-
best = candidate;
|
|
616
|
-
bestScore = score;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return bestScore <= limit ? best : void 0;
|
|
620
|
-
}
|
|
621
|
-
function truncate(text, max) {
|
|
622
|
-
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
623
|
-
}
|
|
624
|
-
function plural$1(count, word, pluralWord = `${word}s`) {
|
|
625
|
-
return `${count} ${count === 1 ? word : pluralWord}`;
|
|
626
|
-
}
|
|
627
|
-
function unique(items) {
|
|
628
|
-
return [...new Set(items)];
|
|
629
|
-
}
|
|
630
|
-
//#endregion
|
|
631
646
|
//#region src/detect.ts
|
|
632
647
|
const ignore = () => void 0;
|
|
633
648
|
const COMPONENT_FILE = /\.[jt]sx$/;
|
|
@@ -1588,11 +1603,11 @@ const PACKAGE_FIELD = "onsystem";
|
|
|
1588
1603
|
function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
|
|
1589
1604
|
const value = pkg?.[PACKAGE_FIELD];
|
|
1590
1605
|
if (value === void 0) return void 0;
|
|
1591
|
-
const field = `"${PACKAGE_FIELD}" in ${path.join(dir, "package.json")}`;
|
|
1606
|
+
const field = `"${PACKAGE_FIELD}" in ${pathOf(path.join(dir, "package.json"))}`;
|
|
1592
1607
|
if (typeof value !== "string" || !value.endsWith(".json")) return { problem: `${field} is ${JSON.stringify(value)}: it names a JSON config file in the package, such as "./onsystem.config.json".` };
|
|
1593
1608
|
const file = path.resolve(dir, value);
|
|
1594
|
-
if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${value}, which is outside the package.` };
|
|
1595
|
-
if (!isFile(file)) return { problem: `${field} names ${value}, which does not exist.` };
|
|
1609
|
+
if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${JSON.stringify(value)}, which is outside the package.` };
|
|
1610
|
+
if (!isFile(file)) return { problem: `${field} names ${JSON.stringify(value)}, which does not exist.` };
|
|
1596
1611
|
return { file };
|
|
1597
1612
|
}
|
|
1598
1613
|
/**
|
|
@@ -3530,7 +3545,7 @@ function unknownKeys(what, known) {
|
|
|
3530
3545
|
if (issue.code !== "unrecognized_keys") return void 0;
|
|
3531
3546
|
return issue.keys.map((key) => {
|
|
3532
3547
|
const guess = closest(key, known, .5);
|
|
3533
|
-
return `Unknown ${what}
|
|
3548
|
+
return `Unknown ${what} ${JSON.stringify(key)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${known.join(", ")}.`}`;
|
|
3534
3549
|
}).join("\n");
|
|
3535
3550
|
};
|
|
3536
3551
|
}
|
|
@@ -3544,7 +3559,7 @@ const ruleSettingSchema = z.union([
|
|
|
3544
3559
|
if (typeof issue.input !== "string") return void 0;
|
|
3545
3560
|
const input = issue.input;
|
|
3546
3561
|
const guess = SEVERITIES.find((s) => input && (s.startsWith(input) || input.startsWith(s)));
|
|
3547
|
-
return `Invalid severity
|
|
3562
|
+
return `Invalid severity ${JSON.stringify(input)}: use "off", "warn" or "error"${guess ? ` (did you mean "${guess}"?)` : ""}.`;
|
|
3548
3563
|
} });
|
|
3549
3564
|
const globsSchema = z.union([z.string(), z.array(z.string())]);
|
|
3550
3565
|
const rulesSchema = z.partialRecord(z.enum(RULE_IDS), ruleSettingSchema, { error: unknownKeys("rule", RULE_IDS) });
|
|
@@ -3562,18 +3577,19 @@ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))]
|
|
|
3562
3577
|
const unknown = Array.isArray(issue.input) ? issue.input.find((id) => !LIBRARY_IDS.includes(id)) : void 0;
|
|
3563
3578
|
if (typeof unknown === "string") {
|
|
3564
3579
|
const guess = closest(unknown, LIBRARY_IDS, .5);
|
|
3565
|
-
return `Unknown library
|
|
3580
|
+
return `Unknown library ${JSON.stringify(unknown)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
|
|
3566
3581
|
}
|
|
3567
3582
|
return `"libraries" is false, to read no library, or a list of the ones to read: ${JSON.stringify(LIBRARY_IDS)}.`;
|
|
3568
3583
|
} });
|
|
3569
3584
|
const configShape = {
|
|
3570
3585
|
$schema: z.string().optional(),
|
|
3586
|
+
extends: z.boolean({ error: (issue) => `"extends" is true or false, not ${JSON.stringify(issue.input)}: the design-system package is found from the dependencies, the one whose package.json declares its config ("onsystem"). Leave "extends" out to build on it, or set it to false to read this config alone.` }).optional().describe("Whether this config builds on the one a design-system package declares (\"onsystem\" in the package.json of a dependency, found from the dependencies): its rules apply over the package's, and, unless it sets components of its own, what it leaves out comes from the package's (components with their importPath, docs, elements, tokens and libraries). Default: true. false reads this config alone."),
|
|
3571
3587
|
components: globsSchema.optional().describe("Globs for component source files, relative to the root."),
|
|
3572
3588
|
exclude: z.array(z.string()).optional().describe("Globs for files to leave out, relative to the root: `check` in folders and globs, the ESLint plugin, the Claude Code hook and check_ui leave them out, and finding components skips them. A folder (`app/legacy`) covers everything in it. Adds to the defaults (node_modules, .d.ts files, tests and stories); a pattern that starts with `!` takes out the default it names, such as `!**/*.d.ts`."),
|
|
3573
3589
|
tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json), CSS custom properties (*.css) or a DESIGN.md's front matter (*.md)."),
|
|
3574
3590
|
docs: globsSchema.optional().describe("Globs for per-component Markdown/MDX docs."),
|
|
3575
3591
|
tsconfig: z.string().optional().describe("tsconfig used to resolve types and path aliases."),
|
|
3576
|
-
importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. Default: inferred from package exports and tsconfig paths."),
|
|
3592
|
+
importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. An installed dependency it names exactly, whose component sources the project does not read, is read from its type declarations (.d.ts). Default: inferred from package exports and tsconfig paths."),
|
|
3577
3593
|
elements: z.record(z.string(), z.string()).optional().describe("Extra native element → component mappings, e.g. { \"a\": \"Link\" }."),
|
|
3578
3594
|
libraries: librariesSchema.optional().describe(`Published component libraries to read from their type declarations when the project depends on them. Omitted: every one onsystem reads (${LIBRARY_IDS.join(", ")}). false: none. A list: only those, e.g. ["mui"].`),
|
|
3579
3595
|
includeDesignSystem: z.boolean().optional().describe("check: lint the design system's own component files too. Off by default: they implement the scale and primitives the rules enforce."),
|
|
@@ -3653,9 +3669,11 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3653
3669
|
let packages = context.packages?.root === searchDir ? context.packages : void 0;
|
|
3654
3670
|
const finder = (dir) => packages?.root === dir ? packages : packages = new PackageFinder(dir);
|
|
3655
3671
|
let lookup = context.lookup;
|
|
3672
|
+
/**
|
|
3673
|
+
* The project's own config: a config file in the folder searched, `--config`, or a workspace
|
|
3674
|
+
* root's that a project in it takes on.
|
|
3675
|
+
*/
|
|
3656
3676
|
let configFile;
|
|
3657
|
-
/** The design-system package whose config this project takes on, having none of its own. */
|
|
3658
|
-
let declared;
|
|
3659
3677
|
if (options.config) configFile = path.resolve(cwd, options.config);
|
|
3660
3678
|
else {
|
|
3661
3679
|
const find = (names) => names.map((name) => path.join(searchDir, name)).find((file) => fs.existsSync(file));
|
|
@@ -3663,120 +3681,134 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3663
3681
|
const legacy = find(LEGACY_CONFIG_FILES);
|
|
3664
3682
|
if (legacy) deprecations.push(legacyConfigNotice(legacy, configFile));
|
|
3665
3683
|
configFile ??= legacy ?? ownDeclaredConfig(searchDir);
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
if (lookup?.problems.length) throw declarationError(lookup, searchDir);
|
|
3669
|
-
declared = lookup?.found;
|
|
3670
|
-
configFile = declared?.config;
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
for (const { name, problem } of lookup?.skipped ?? []) warnings.push(`${name}'s onsystem config was skipped: ${problem} ${name} is installed in node_modules, so this project is read as if it declared none.`);
|
|
3674
|
-
let raw = {};
|
|
3675
|
-
if (configFile) {
|
|
3676
|
-
if (!fs.existsSync(configFile)) throw new ConfigError(`Config file not found: ${configFile}`);
|
|
3677
|
-
if (options.jsonConfigOnly && !configFile.endsWith(".json")) throw new ConfigError(`${configFile} was not run: the server runs a JavaScript or TypeScript config only in a project it was started in (its working directory, --root or the client's roots), not in one found from the path a tool call names. Start the server in the project, or use onsystem.config.json.`);
|
|
3678
|
-
raw = await readConfigFile(configFile);
|
|
3679
|
-
}
|
|
3680
|
-
const parsed = configSchema.safeParse(raw);
|
|
3681
|
-
if (!parsed.success) throw new ConfigError(`Invalid config in ${configFile ?? "(defaults)"}:\n${z.prettifyError(parsed.error)}`);
|
|
3682
|
-
const config = parsed.data;
|
|
3684
|
+
}
|
|
3685
|
+
const config = configFile ? await readConfig(configFile, options) : {};
|
|
3683
3686
|
const root = options.root ? path.resolve(cwd, options.root) : options.config && configFile ? path.dirname(configFile) : searchDir;
|
|
3684
3687
|
if (!isDirectory(root)) throw new ConfigError(`Project root not found: ${root}`);
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
+
let declared;
|
|
3689
|
+
if (config.extends !== false) {
|
|
3690
|
+
lookup ??= findDesignSystemPackage(root, finder(root));
|
|
3691
|
+
if (!lookup?.problems.length) declared = lookup?.found;
|
|
3692
|
+
else if (!configFile) throw declarationError(lookup, root);
|
|
3693
|
+
else warnings.push(`${problemLines(lookup.problems)}\nSo ${configFile} is read alone, without the config a design-system package declares. Fix the "${PACKAGE_FIELD}" field in that package, or set "extends": false in ${path.basename(configFile)}.`);
|
|
3694
|
+
}
|
|
3695
|
+
for (const { name, problem } of lookup?.skipped ?? []) {
|
|
3696
|
+
const quoted = json(name);
|
|
3697
|
+
warnings.push(`The onsystem config of ${quoted} was skipped: ${problem} ${quoted} is installed in node_modules, so this project is read as if it declared none.`);
|
|
3698
|
+
}
|
|
3699
|
+
const pkg = declared ? await readConfig(declared.config, options) : void 0;
|
|
3700
|
+
const base = options.base ? path.resolve(cwd, options.base) : root;
|
|
3701
|
+
const rebase = (from) => (p) => from === root ? p : toPosix(path.relative(root, path.resolve(from, slashGlob(p)))) || ".";
|
|
3702
|
+
const fromBase = rebase(base);
|
|
3703
|
+
const fromPackage = rebase(declared?.dir ?? root);
|
|
3688
3704
|
const toProject = toPosix(path.relative(base, root));
|
|
3689
3705
|
const excludeFromBase = (e) => {
|
|
3690
|
-
if (
|
|
3691
|
-
if (declared) return fromBase(e);
|
|
3706
|
+
if (base === root || e.startsWith("**/")) return e;
|
|
3692
3707
|
if (e === toProject) return "**";
|
|
3693
3708
|
return e.startsWith(`${toProject}/`) ? e.slice(toProject.length + 1) : void 0;
|
|
3694
3709
|
};
|
|
3710
|
+
const excludeFromPackage = (e) => e.startsWith("**/") ? e : fromPackage(e);
|
|
3695
3711
|
const outside = [];
|
|
3696
3712
|
const within = (field) => (p) => {
|
|
3697
3713
|
if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
|
|
3698
|
-
outside.push(`${field} ${p}`);
|
|
3714
|
+
outside.push(`${field} ${json(p)}`);
|
|
3699
3715
|
return false;
|
|
3700
3716
|
};
|
|
3701
|
-
const
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
3717
|
+
const pathOf = (t) => typeof t === "string" ? t : t.path;
|
|
3718
|
+
const tokensFrom = (move) => (t) => typeof t === "string" ? { path: move(t) } : {
|
|
3719
|
+
...t,
|
|
3720
|
+
path: move(t.path)
|
|
3721
|
+
};
|
|
3722
|
+
const packageComponents = pkg?.components === void 0 ? void 0 : toArray(pkg.components).filter(within("components")).map(fromPackage);
|
|
3723
|
+
const packageTokens = pkg?.tokens === void 0 ? void 0 : toArray(pkg.tokens).filter((t) => within("tokens")(pathOf(t))).map(tokensFrom(fromPackage));
|
|
3724
|
+
const packageDocs = pkg?.docs === void 0 ? void 0 : toArray(pkg.docs).filter(within("docs")).map(fromPackage);
|
|
3725
|
+
if (declared && outside.length) warnings.push(`The onsystem config of ${json(declared.name)} names paths outside ${declared.installed ? "the package" : "the workspace"}, which were ignored: ${outside.join(", ")}.`);
|
|
3726
|
+
const inherits = pkg !== void 0 && config.components === void 0;
|
|
3727
|
+
const configComponents = config.components === void 0 ? packageComponents : toArray(config.components).map(fromBase);
|
|
3728
|
+
const configTokens = config.tokens === void 0 ? inherits ? packageTokens : void 0 : toArray(config.tokens).map(tokensFrom(fromBase));
|
|
3729
|
+
const configDocs = config.docs === void 0 ? inherits ? packageDocs : void 0 : toArray(config.docs).map(fromBase);
|
|
3730
|
+
const importPath = config.importPath ?? (inherits ? pkg.importPath : void 0);
|
|
3731
|
+
const librariesSetting = config.libraries ?? (inherits ? pkg.libraries : void 0);
|
|
3705
3732
|
const rules = {};
|
|
3706
3733
|
for (const id of RULE_IDS) {
|
|
3707
|
-
const setting = config.rules?.[id];
|
|
3734
|
+
const setting = config.rules?.[id] ?? pkg?.rules?.[id];
|
|
3708
3735
|
rules[id] = setting === void 0 ? {
|
|
3709
3736
|
severity: DEFAULT_SEVERITY[id],
|
|
3710
3737
|
options: {}
|
|
3711
3738
|
} : resolveRule(setting);
|
|
3712
3739
|
}
|
|
3713
|
-
const overrides = (
|
|
3740
|
+
const overrides = (config.overrides ?? []).map((override) => ({
|
|
3714
3741
|
files: toArray(override.files).map(slashGlob),
|
|
3715
3742
|
rules: Object.fromEntries(Object.entries(override.rules).map(([id, setting]) => [id, resolveRule(setting)]))
|
|
3716
3743
|
}));
|
|
3717
|
-
const tsconfig = config.tsconfig === void 0
|
|
3744
|
+
const tsconfig = config.tsconfig === void 0 ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
|
|
3718
3745
|
if (tsconfig !== void 0 && !fs.existsSync(path.resolve(root, tsconfig))) throw new ConfigError(`tsconfig not found: ${path.resolve(root, tsconfig)} (from "tsconfig" in ${configFile ?? "the config"})`);
|
|
3719
|
-
const componentsSet = Boolean(options.components?.length) ||
|
|
3746
|
+
const componentsSet = Boolean(options.components?.length) || configComponents !== void 0;
|
|
3720
3747
|
const detection = componentsSet ? void 0 : detectProject(root, tsconfig, void 0, declared, finder(root));
|
|
3721
3748
|
const explicitTokens = options.tokens?.length ? {
|
|
3722
3749
|
paths: options.tokens,
|
|
3723
3750
|
from: "--tokens"
|
|
3724
|
-
} : config.tokens !== void 0
|
|
3725
|
-
paths: toArray(config.tokens).map((t) => fromBase(
|
|
3751
|
+
} : config.tokens !== void 0 ? {
|
|
3752
|
+
paths: toArray(config.tokens).map((t) => fromBase(pathOf(t))),
|
|
3726
3753
|
from: `"tokens" in ${configFile ?? "the config"}`
|
|
3727
3754
|
} : void 0;
|
|
3728
3755
|
for (const file of explicitTokens?.paths ?? []) {
|
|
3729
3756
|
const absolute = path.resolve(root, slashGlob(file));
|
|
3730
3757
|
if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
|
|
3731
3758
|
}
|
|
3732
|
-
const components = (options.components?.length ? options.components : configComponents
|
|
3733
|
-
const libraries = detectLibraries(root,
|
|
3734
|
-
importPath
|
|
3759
|
+
const components = (options.components?.length ? options.components : configComponents ?? (detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS)).map(slashGlob);
|
|
3760
|
+
const libraries = detectLibraries(root, librariesSetting, {
|
|
3761
|
+
importPath,
|
|
3735
3762
|
components
|
|
3736
3763
|
});
|
|
3737
|
-
const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens
|
|
3738
|
-
...t,
|
|
3739
|
-
path: fromBase(t.path)
|
|
3740
|
-
}) : [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
|
|
3764
|
+
const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens ?? [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
|
|
3741
3765
|
path: p,
|
|
3742
3766
|
fallback: true
|
|
3743
3767
|
}))];
|
|
3744
3768
|
const tidy = (e) => slashGlob(e).replace(/^(?:\.\/)+/, "").replace(/(.)\/+$/, "$1");
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3769
|
+
const excludeOf = (listed, move) => {
|
|
3770
|
+
const dropped = new Set(listed.filter((e) => e.startsWith("!")).map((e) => tidy(e.slice(1))));
|
|
3771
|
+
for (const pattern of dropped) {
|
|
3772
|
+
if (DEFAULT_EXCLUDE.includes(pattern)) continue;
|
|
3773
|
+
warnings.push(`"!${pattern}" in "exclude" takes out none of the defaults, which are ${DEFAULT_EXCLUDE.map((d) => `"${d}"`).join(", ")}: a pattern that starts with "!" names one of them.`);
|
|
3774
|
+
}
|
|
3775
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE.filter((d) => !dropped.has(d)), ...listed.filter((e) => !e.startsWith("!")).map(tidy)])].map(move).filter((e) => e !== void 0);
|
|
3776
|
+
};
|
|
3777
|
+
const exclude = excludeOf(config.exclude ?? [], excludeFromBase);
|
|
3778
|
+
const packageExclude = pkg?.exclude ? excludeOf(pkg.exclude, excludeFromPackage) : void 0;
|
|
3752
3779
|
return {
|
|
3753
3780
|
root,
|
|
3754
|
-
configFile,
|
|
3781
|
+
configFile: configFile ?? declared?.config,
|
|
3755
3782
|
components,
|
|
3756
|
-
exclude:
|
|
3757
|
-
...
|
|
3783
|
+
exclude: packageExclude ? [.../* @__PURE__ */ new Set([...exclude, ...packageExclude.filter((e) => !DEFAULT_EXCLUDE.includes(e))])] : exclude,
|
|
3784
|
+
...packageExclude && inherits ? { designSystemExclude: packageExclude } : {},
|
|
3758
3785
|
tokens: tokenSources.map((t) => ({
|
|
3759
3786
|
...t,
|
|
3760
3787
|
path: slashGlob(t.path)
|
|
3761
3788
|
})),
|
|
3762
3789
|
...!options.tokens?.length && configTokens === void 0 ? { tokensDetected: true } : {},
|
|
3763
|
-
docs: (options.docs?.length ? options.docs : configDocs
|
|
3790
|
+
docs: (options.docs?.length ? options.docs : configDocs ?? [.../* @__PURE__ */ new Set([...DEFAULT_DOCS, ...detection?.docs ?? []])]).map(slashGlob),
|
|
3764
3791
|
tsconfig,
|
|
3765
|
-
importPath
|
|
3766
|
-
imports: detection?.imports ?? (declared && componentsSet ? packageImports(root, declared.dir) : []),
|
|
3792
|
+
importPath,
|
|
3793
|
+
imports: detection?.imports ?? (declared && inherits && componentsSet ? packageImports(root, declared.dir) : []),
|
|
3767
3794
|
detected: detection?.source,
|
|
3768
3795
|
...detection?.designSystems ? { designSystems: detection.designSystems } : {},
|
|
3769
3796
|
tailwindConfig: findTailwindConfig(root),
|
|
3770
3797
|
libraries: libraries.read,
|
|
3771
|
-
libraryIds:
|
|
3798
|
+
libraryIds: librariesSetting,
|
|
3772
3799
|
librariesOff: libraries.off,
|
|
3773
3800
|
librariesUnread: libraries.unread,
|
|
3774
|
-
elements:
|
|
3775
|
-
|
|
3801
|
+
elements: {
|
|
3802
|
+
...inherits ? pkg.elements : {},
|
|
3803
|
+
...config.elements
|
|
3804
|
+
},
|
|
3805
|
+
includeDesignSystem: options.includeDesignSystem ?? config.includeDesignSystem ?? false,
|
|
3776
3806
|
rules,
|
|
3777
3807
|
overrides,
|
|
3778
|
-
...
|
|
3808
|
+
...base !== root ? { base } : {},
|
|
3779
3809
|
...declared ? { designSystemPackage: declared } : {},
|
|
3810
|
+
...inherits ? { inherits: true } : {},
|
|
3811
|
+
...config.extends === false ? { extendsOff: true } : {},
|
|
3780
3812
|
configured: componentsSet,
|
|
3781
3813
|
...deprecations.length ? { deprecations } : {},
|
|
3782
3814
|
...warnings.length ? { warnings } : {}
|
|
@@ -3785,7 +3817,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3785
3817
|
/**
|
|
3786
3818
|
* Whether `dir` has a config of its own: a config file under its current or old name, or one its
|
|
3787
3819
|
* package.json names in its `onsystem` field. Without one, a project takes on its design-system
|
|
3788
|
-
* package's config,
|
|
3820
|
+
* package's config, and, from a workspace root, the root's over it.
|
|
3789
3821
|
*/
|
|
3790
3822
|
function hasOwnConfig(dir) {
|
|
3791
3823
|
if ([...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((name) => fs.existsSync(path.join(dir, name)))) return true;
|
|
@@ -3797,9 +3829,17 @@ function ownDeclaredConfig(dir) {
|
|
|
3797
3829
|
if (declared && "problem" in declared) throw new ConfigError(declared.problem);
|
|
3798
3830
|
return declared?.file;
|
|
3799
3831
|
}
|
|
3832
|
+
/**
|
|
3833
|
+
* Each declaration that cannot be read, on a line of its own. The package's name is the
|
|
3834
|
+
* repository's text (any string can name a dependency in package.json), so it is JSON-quoted:
|
|
3835
|
+
* a line break or a sentence in it stays inside the quotes, on that line.
|
|
3836
|
+
*/
|
|
3837
|
+
function problemLines(problems) {
|
|
3838
|
+
return problems.map((p) => `${json(p.name)}: ${p.problem}`).join("\n");
|
|
3839
|
+
}
|
|
3800
3840
|
/** The error for workspace packages whose declaration cannot be read: their team can fix them. */
|
|
3801
3841
|
function declarationError(lookup, dir) {
|
|
3802
|
-
return new ConfigError(`${lookup.problems
|
|
3842
|
+
return new ConfigError(`${problemLines(lookup.problems)}\nFix the "${PACKAGE_FIELD}" field in that package, or give ${dir} a config of its own (onsystem.config.json), which is read instead.`);
|
|
3803
3843
|
}
|
|
3804
3844
|
/**
|
|
3805
3845
|
* Whether a path or glob in a dependency's config, relative to its folder `dir`, stays in
|
|
@@ -3863,6 +3903,14 @@ function nodeRunsTypeScript(version = process.versions.node) {
|
|
|
3863
3903
|
const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number);
|
|
3864
3904
|
return major > 23 || major === 23 && minor >= 6 || major === 22 && minor >= 18;
|
|
3865
3905
|
}
|
|
3906
|
+
/** A config file, read and validated: a JSON one only with `jsonConfigOnly`. */
|
|
3907
|
+
async function readConfig(file, options) {
|
|
3908
|
+
if (!fs.existsSync(file)) throw new ConfigError(`Config file not found: ${file}`);
|
|
3909
|
+
if (options.jsonConfigOnly && !file.endsWith(".json")) throw new ConfigError(`${file} was not run: the server runs a JavaScript or TypeScript config only in a project it was started in (its working directory, --root or the client's roots), not in one found from the path a tool call names. Start the server in the project, or use onsystem.config.json.`);
|
|
3910
|
+
const parsed = configSchema.safeParse(await readConfigFile(file));
|
|
3911
|
+
if (!parsed.success) throw new ConfigError(`Invalid config in ${file}:\n${z.prettifyError(parsed.error)}`);
|
|
3912
|
+
return parsed.data;
|
|
3913
|
+
}
|
|
3866
3914
|
async function readConfigFile(file) {
|
|
3867
3915
|
if (/\.[cm]?ts$/.test(file) && !nodeRunsTypeScript()) throw new ConfigError(`${file} is TypeScript, which Node.js runs from 22.18; this is ${process.version}. Use onsystem.config.json or .mjs, or Node.js 22.18 or later.`);
|
|
3868
3916
|
if (file.endsWith(".json")) try {
|
|
@@ -4913,6 +4961,8 @@ function dedent(code) {
|
|
|
4913
4961
|
}
|
|
4914
4962
|
//#endregion
|
|
4915
4963
|
//#region src/libraries/declarations.ts
|
|
4964
|
+
/** A component's name: a capitalised identifier (`Button`, `ButtonBase`, `Unstable_Grid2`). */
|
|
4965
|
+
const COMPONENT_NAME = /^[A-Z][\w$]*$/;
|
|
4916
4966
|
/** Packages contributing more props than this are summarised, as in source extraction. */
|
|
4917
4967
|
const MAX_LISTED_INHERITED = 40;
|
|
4918
4968
|
/**
|
|
@@ -4984,9 +5034,9 @@ function extractLibrary(options) {
|
|
|
4984
5034
|
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
4985
5035
|
const isDefault = exported.getName() === "default";
|
|
4986
5036
|
const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
|
|
4987
|
-
if (isDefault &&
|
|
5037
|
+
if (isDefault && !COMPONENT_NAME.test(name)) continue;
|
|
4988
5038
|
exportNames.add(name);
|
|
4989
|
-
if (
|
|
5039
|
+
if (!COMPONENT_NAME.test(name)) continue;
|
|
4990
5040
|
const known = bySymbol.get(symbol);
|
|
4991
5041
|
if (known) {
|
|
4992
5042
|
if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
|
|
@@ -8179,7 +8229,8 @@ function table(tokens, keys) {
|
|
|
8179
8229
|
use(token),
|
|
8180
8230
|
...described ? [token.description ?? ""] : []
|
|
8181
8231
|
]);
|
|
8182
|
-
const
|
|
8232
|
+
const oneLine = (text) => text.split(/\r\n?|\n/).map((part) => part.trim()).filter(Boolean).join(" ");
|
|
8233
|
+
const line = (cells) => `| ${cells.map((c) => oneLine(c).replace(/\|/g, "\\|")).join(" | ")} |`;
|
|
8183
8234
|
return [
|
|
8184
8235
|
line(header),
|
|
8185
8236
|
line(header.map(() => "---")),
|
|
@@ -10411,7 +10462,7 @@ function maskReferencedGradientIds(elements) {
|
|
|
10411
10462
|
for (const element of elements) if (element.tag === "mask") refsFrom(element.node);
|
|
10412
10463
|
return ids;
|
|
10413
10464
|
}
|
|
10414
|
-
const noHardcodedColor = {
|
|
10465
|
+
const noHardcodedColor = shortQuotes({
|
|
10415
10466
|
id: "no-hardcoded-color",
|
|
10416
10467
|
description: "Colors must come from design tokens: no hex/rgb/oklch literals, arbitrary Tailwind colors or default-palette classes.",
|
|
10417
10468
|
run(context) {
|
|
@@ -10622,7 +10673,31 @@ const noHardcodedColor = {
|
|
|
10622
10673
|
}
|
|
10623
10674
|
reportChartTokens(context, allow);
|
|
10624
10675
|
}
|
|
10625
|
-
};
|
|
10676
|
+
});
|
|
10677
|
+
/** The longest code a message quotes. */
|
|
10678
|
+
const QUOTED_CODE = 120;
|
|
10679
|
+
/**
|
|
10680
|
+
* The rule, with each `code` span in its messages cut to QUOTED_CODE characters. A message quotes
|
|
10681
|
+
* the class, its fix and the value whole, so one class with 33,000 `hover:` variants made a
|
|
10682
|
+
* message of 400,000 characters for the agent to read.
|
|
10683
|
+
*/
|
|
10684
|
+
function shortQuotes(rule) {
|
|
10685
|
+
const cut = (span, code) => code.length > QUOTED_CODE ? `\`${truncate(code, QUOTED_CODE)}\`` : span;
|
|
10686
|
+
return {
|
|
10687
|
+
...rule,
|
|
10688
|
+
run(context) {
|
|
10689
|
+
rule.run({
|
|
10690
|
+
...context,
|
|
10691
|
+
report(report) {
|
|
10692
|
+
context.report({
|
|
10693
|
+
...report,
|
|
10694
|
+
message: report.message.replace(/`([^`]+)`/g, cut)
|
|
10695
|
+
});
|
|
10696
|
+
}
|
|
10697
|
+
});
|
|
10698
|
+
}
|
|
10699
|
+
};
|
|
10700
|
+
}
|
|
10626
10701
|
/** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
|
|
10627
10702
|
const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
|
|
10628
10703
|
/** Roles of an element that is a drawing: a chart drawn by hand is an image. */
|
|
@@ -11211,25 +11286,69 @@ function setsColor(utility, prefix) {
|
|
|
11211
11286
|
*/
|
|
11212
11287
|
function keepStatesApart(list) {
|
|
11213
11288
|
const after = (listed) => listed.report?.fix?.[0]?.text ?? listed.value;
|
|
11289
|
+
const order = new Map(list.map((listed, i) => [listed, i]));
|
|
11290
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
11291
|
+
const parse = (text) => {
|
|
11292
|
+
let utility = parsed.get(text);
|
|
11293
|
+
if (!utility) {
|
|
11294
|
+
utility = parseUtility(text);
|
|
11295
|
+
parsed.set(text, utility);
|
|
11296
|
+
}
|
|
11297
|
+
return utility;
|
|
11298
|
+
};
|
|
11299
|
+
const byText = /* @__PURE__ */ new Map();
|
|
11300
|
+
const byUtility = /* @__PURE__ */ new Map();
|
|
11301
|
+
const utilityKey = (text) => `${parse(text).base}\0${parse(text).modifier}`;
|
|
11302
|
+
const place = (listed) => {
|
|
11303
|
+
const text = after(listed);
|
|
11304
|
+
const same = byText.get(text) ?? [];
|
|
11305
|
+
byText.set(text, same);
|
|
11306
|
+
const index = order.get(listed) ?? 0;
|
|
11307
|
+
same.splice(same.findLastIndex((other) => (order.get(other) ?? 0) < index) + 1, 0, listed);
|
|
11308
|
+
const texts = byUtility.get(utilityKey(text)) ?? /* @__PURE__ */ new Set();
|
|
11309
|
+
byUtility.set(utilityKey(text), texts.add(text));
|
|
11310
|
+
};
|
|
11311
|
+
const unplace = (listed) => {
|
|
11312
|
+
const text = after(listed);
|
|
11313
|
+
const same = byText.get(text) ?? [];
|
|
11314
|
+
same.splice(same.indexOf(listed), 1);
|
|
11315
|
+
if (!same.length) byUtility.get(utilityKey(text))?.delete(text);
|
|
11316
|
+
};
|
|
11317
|
+
for (const listed of list) place(listed);
|
|
11214
11318
|
for (const pass of ["state", "base"]) for (const listed of list) {
|
|
11215
11319
|
const { report } = listed;
|
|
11216
11320
|
if (!report?.fix?.[0]) continue;
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
const base =
|
|
11321
|
+
const text = after(listed);
|
|
11322
|
+
let found;
|
|
11323
|
+
for (const otherText of byUtility.get(utilityKey(text)) ?? []) {
|
|
11324
|
+
if (otherText === text) continue;
|
|
11325
|
+
const [state, base] = pass === "state" ? [parse(text), parse(otherText)] : [parse(otherText), parse(text)];
|
|
11222
11326
|
const conditions = conditionsBetween(state, base);
|
|
11223
|
-
if (!conditions
|
|
11224
|
-
const
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11327
|
+
if (!conditions) continue;
|
|
11328
|
+
for (const other of byText.get(otherText) ?? []) {
|
|
11329
|
+
if (found && (order.get(other) ?? 0) > (order.get(found.other) ?? 0)) break;
|
|
11330
|
+
if (oneColor(listed.color, other.color)) continue;
|
|
11331
|
+
const [stateListed, baseListed] = pass === "state" ? [listed, other] : [other, listed];
|
|
11332
|
+
if (hasDarkBase(list, stateListed, baseListed, state, base, after)) continue;
|
|
11333
|
+
found = {
|
|
11334
|
+
other,
|
|
11335
|
+
state,
|
|
11336
|
+
conditions
|
|
11337
|
+
};
|
|
11338
|
+
break;
|
|
11339
|
+
}
|
|
11232
11340
|
}
|
|
11341
|
+
if (!found) continue;
|
|
11342
|
+
const { other, state, conditions } = found;
|
|
11343
|
+
const had = pass === "base" ? `\`${other.value}\` is in the class list too` : after(other) === other.value ? `The class list already has \`${other.value}\`` : `\`${other.value}\` becomes \`${after(other)}\` too`;
|
|
11344
|
+
const apart = state.modifier ? "such as with another opacity modifier" : `such as with \`${withBase({
|
|
11345
|
+
...state,
|
|
11346
|
+
modifier: "/90"
|
|
11347
|
+
}, state.base)}\``;
|
|
11348
|
+
report.message += ` ${had}, so with this fix \`${conditions}\` would change nothing: it is not applied automatically. Keep the two apart, ${apart}.`;
|
|
11349
|
+
unplace(listed);
|
|
11350
|
+
report.fix = void 0;
|
|
11351
|
+
place(listed);
|
|
11233
11352
|
}
|
|
11234
11353
|
}
|
|
11235
11354
|
/**
|
|
@@ -11409,7 +11528,8 @@ function scopes(context, element, classes) {
|
|
|
11409
11528
|
if (name && !/^Sidebar(?:Provider|Inset)$/.test(name)) tags.push(name);
|
|
11410
11529
|
}
|
|
11411
11530
|
const { tokens, components } = context.target;
|
|
11412
|
-
const found = scopesIn(tokens.scopedFamilies, context.file,
|
|
11531
|
+
const found = scopesIn(tokens.scopedFamilies, context.file, ...tags);
|
|
11532
|
+
if (classes) for (const family of classScopes(context, classes)) found.add(family);
|
|
11413
11533
|
const files = /* @__PURE__ */ new Set([context.file]);
|
|
11414
11534
|
for (const tag of tags) {
|
|
11415
11535
|
const file = components.get(tag)?.source.file;
|
|
@@ -11421,6 +11541,25 @@ function scopes(context, element, classes) {
|
|
|
11421
11541
|
}
|
|
11422
11542
|
return found;
|
|
11423
11543
|
}
|
|
11544
|
+
/** The scoped families each class string of a check names, by its text. */
|
|
11545
|
+
const classScopeCache = /* @__PURE__ */ new WeakMap();
|
|
11546
|
+
/**
|
|
11547
|
+
* The scoped families a class string names, read once per string and check: each color in it
|
|
11548
|
+
* asks, and reading the whole string again for each made a list of thousands quadratic.
|
|
11549
|
+
*/
|
|
11550
|
+
function classScopes(context, classes) {
|
|
11551
|
+
let cache = classScopeCache.get(context.analysis);
|
|
11552
|
+
if (!cache) {
|
|
11553
|
+
cache = /* @__PURE__ */ new Map();
|
|
11554
|
+
classScopeCache.set(context.analysis, cache);
|
|
11555
|
+
}
|
|
11556
|
+
let found = cache.get(classes);
|
|
11557
|
+
if (!found) {
|
|
11558
|
+
found = scopesIn(context.target.tokens.scopedFamilies, classes);
|
|
11559
|
+
cache.set(classes, found);
|
|
11560
|
+
}
|
|
11561
|
+
return found;
|
|
11562
|
+
}
|
|
11424
11563
|
/** The variant prop of the element's component that already sets `prefix-*` classes, if any. */
|
|
11425
11564
|
function variantProp(context, element, prefix) {
|
|
11426
11565
|
const resolution = context.resolve(element);
|
|
@@ -11443,7 +11582,7 @@ function numberValue(init) {
|
|
|
11443
11582
|
if (ts.isPrefixUnaryExpression(init) && ts.isNumericLiteral(init.operand) && (init.operator === ts.SyntaxKind.MinusToken || init.operator === ts.SyntaxKind.PlusToken)) return `${init.operator === ts.SyntaxKind.MinusToken ? "-" : ""}${init.operand.text}`;
|
|
11444
11583
|
}
|
|
11445
11584
|
function lengthRule(id, description, spec) {
|
|
11446
|
-
return {
|
|
11585
|
+
return shortQuotes({
|
|
11447
11586
|
id,
|
|
11448
11587
|
description,
|
|
11449
11588
|
run(context) {
|
|
@@ -11515,7 +11654,7 @@ function lengthRule(id, description, spec) {
|
|
|
11515
11654
|
}
|
|
11516
11655
|
}
|
|
11517
11656
|
}
|
|
11518
|
-
};
|
|
11657
|
+
});
|
|
11519
11658
|
}
|
|
11520
11659
|
/**
|
|
11521
11660
|
* A value on the scale whose token code cannot name, with neither a Tailwind class nor a
|
|
@@ -11998,7 +12137,7 @@ function isObject(value) {
|
|
|
11998
12137
|
var name = "onsystem";
|
|
11999
12138
|
//#endregion
|
|
12000
12139
|
//#region src/version.ts
|
|
12001
|
-
const VERSION = "0.
|
|
12140
|
+
const VERSION = "0.10.1";
|
|
12002
12141
|
const NAME = name;
|
|
12003
12142
|
//#endregion
|
|
12004
12143
|
//#region src/lint/sarif.ts
|
|
@@ -12221,7 +12360,7 @@ function checkSource(code, target, options) {
|
|
|
12221
12360
|
try {
|
|
12222
12361
|
return checkParsed(code, target, options, file);
|
|
12223
12362
|
} catch (error) {
|
|
12224
|
-
if (!isStackOverflow(error)) throw error;
|
|
12363
|
+
if (!isStackOverflow$1(error)) throw error;
|
|
12225
12364
|
return {
|
|
12226
12365
|
file,
|
|
12227
12366
|
diagnostics: [{
|
|
@@ -12239,7 +12378,7 @@ function checkSource(code, target, options) {
|
|
|
12239
12378
|
};
|
|
12240
12379
|
}
|
|
12241
12380
|
}
|
|
12242
|
-
function isStackOverflow(error) {
|
|
12381
|
+
function isStackOverflow$1(error) {
|
|
12243
12382
|
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
12244
12383
|
}
|
|
12245
12384
|
function checkParsed(code, target, options, file) {
|
|
@@ -12363,7 +12502,7 @@ function uncheckedNotice(results) {
|
|
|
12363
12502
|
const list = [...names];
|
|
12364
12503
|
const shown = list.slice(0, 4).map((n) => `<${n}>`);
|
|
12365
12504
|
const more = list.length > shown.length ? ` and ${list.length - shown.length} more` : "";
|
|
12366
|
-
const from = [...modules].slice(0, 3).map((m) =>
|
|
12505
|
+
const from = [...modules].slice(0, 3).map((m) => JSON.stringify(m));
|
|
12367
12506
|
const moreModules = modules.size > from.length ? ` and ${modules.size - from.length} more` : "";
|
|
12368
12507
|
const where = results.length > 1 ? ` (in ${files} ${files === 1 ? "file" : "files"})` : "";
|
|
12369
12508
|
return `${shown.join(", ")}${more} ${list.length === 1 ? "comes" : "come"} from ${from.join(", ")}${moreModules}, which the design-system model does not include, so ${list.length === 1 ? "it was" : "they were"} not checked${where}. Extraction looks incomplete: \`inspect\` lists what was found; add the missing files to "components" in the config.`;
|
|
@@ -13080,6 +13219,38 @@ function repositoryOf(dir) {
|
|
|
13080
13219
|
if (current === path.dirname(current)) return void 0;
|
|
13081
13220
|
}
|
|
13082
13221
|
}
|
|
13222
|
+
/**
|
|
13223
|
+
* `extractComponents`, without the component files nested too deeply for TypeScript's recursive
|
|
13224
|
+
* parser (thousands of levels), each named in a warning: one such file would otherwise fail the
|
|
13225
|
+
* whole design system with a RangeError that names no file.
|
|
13226
|
+
*/
|
|
13227
|
+
function extractReadable(options) {
|
|
13228
|
+
try {
|
|
13229
|
+
return extractComponents(options);
|
|
13230
|
+
} catch (error) {
|
|
13231
|
+
if (!isStackOverflow(error)) throw error;
|
|
13232
|
+
const tooDeep = new Set(options.files.filter(overflowsParser));
|
|
13233
|
+
if (!tooDeep.size) throw error;
|
|
13234
|
+
const extracted = extractComponents({
|
|
13235
|
+
...options,
|
|
13236
|
+
files: options.files.filter((file) => !tooDeep.has(file))
|
|
13237
|
+
});
|
|
13238
|
+
for (const file of tooDeep) extracted.warnings.push(`${relativePath(options.root, file)}: nested too deeply to read (thousands of levels); skipped.`);
|
|
13239
|
+
return extracted;
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
/** Whether parsing `file` on its own runs out of stack. */
|
|
13243
|
+
function overflowsParser(file) {
|
|
13244
|
+
try {
|
|
13245
|
+
ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest);
|
|
13246
|
+
return false;
|
|
13247
|
+
} catch (error) {
|
|
13248
|
+
return isStackOverflow(error);
|
|
13249
|
+
}
|
|
13250
|
+
}
|
|
13251
|
+
function isStackOverflow(error) {
|
|
13252
|
+
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
13253
|
+
}
|
|
13083
13254
|
/** What `inspect` and the server's log say about files that lead outside the project. */
|
|
13084
13255
|
function outsideWarning(root, files) {
|
|
13085
13256
|
const shown = files.map((file) => relativePath(root, file)).sort();
|
|
@@ -13227,11 +13398,54 @@ async function cacheKey(config, files, dependencies, stats = new FileStats(confi
|
|
|
13227
13398
|
stats: await stats.of(all)
|
|
13228
13399
|
})).digest("hex");
|
|
13229
13400
|
}
|
|
13401
|
+
/**
|
|
13402
|
+
* Where the model is cached: the project's `node_modules/.cache`, else (`temp`) a folder of the
|
|
13403
|
+
* user's own in the system temp folder, `onsystem-<uid>`.
|
|
13404
|
+
*/
|
|
13230
13405
|
function cacheFile(root) {
|
|
13231
13406
|
const nodeModules = path.join(root, "node_modules");
|
|
13232
|
-
if (fs.existsSync(nodeModules)) return
|
|
13407
|
+
if (fs.existsSync(nodeModules)) return {
|
|
13408
|
+
file: path.join(nodeModules, ".cache", "onsystem", "model.json"),
|
|
13409
|
+
temp: false
|
|
13410
|
+
};
|
|
13233
13411
|
const id = createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
13234
|
-
|
|
13412
|
+
const user = process.getuid ? `-${process.getuid()}` : "";
|
|
13413
|
+
return {
|
|
13414
|
+
file: path.join(os.tmpdir(), `onsystem${user}`, `${id}.json`),
|
|
13415
|
+
temp: true
|
|
13416
|
+
};
|
|
13417
|
+
}
|
|
13418
|
+
/**
|
|
13419
|
+
* Why the cache folder in the system temp folder cannot be trusted, or undefined when it can: it
|
|
13420
|
+
* must be a folder, not a link, that the user owns and no one else can write to. On a multi-user
|
|
13421
|
+
* host the temp folder is shared, and a model planted in it would reach the agent. With `create`,
|
|
13422
|
+
* it is created (or closed) for the user first. Windows gives each user a temp folder of their own.
|
|
13423
|
+
*/
|
|
13424
|
+
async function untrustedCacheFolder(folder, create) {
|
|
13425
|
+
if (!process.getuid) return void 0;
|
|
13426
|
+
if (create) await fsp.mkdir(folder, {
|
|
13427
|
+
recursive: true,
|
|
13428
|
+
mode: 448
|
|
13429
|
+
});
|
|
13430
|
+
const stat = await fsp.lstat(folder);
|
|
13431
|
+
if (!stat.isDirectory()) return `${folder} is not a folder`;
|
|
13432
|
+
if (stat.uid !== process.getuid()) return `${folder} belongs to another user`;
|
|
13433
|
+
if (!create && stat.mode & 18) return `other users can write to ${folder}`;
|
|
13434
|
+
if (create && stat.mode & 63) await fsp.chmod(folder, 448);
|
|
13435
|
+
}
|
|
13436
|
+
/**
|
|
13437
|
+
* Writes `text` beside `file`, then renames it into place: a run reading it never sees half. With
|
|
13438
|
+
* `own`, only the user can read it (the temp folder's cache); without, it has the default mode, so
|
|
13439
|
+
* a cache in `node_modules` built by one user (a Docker build as root) is still read by another.
|
|
13440
|
+
*/
|
|
13441
|
+
async function writeWhole(file, text, own) {
|
|
13442
|
+
const partial = `${file}.${process.pid}-${randomUUID()}.tmp`;
|
|
13443
|
+
try {
|
|
13444
|
+
await fsp.writeFile(partial, text, own ? { mode: 384 } : {});
|
|
13445
|
+
await fsp.rename(partial, file);
|
|
13446
|
+
} finally {
|
|
13447
|
+
await fsp.rm(partial, { force: true });
|
|
13448
|
+
}
|
|
13235
13449
|
}
|
|
13236
13450
|
/**
|
|
13237
13451
|
* Fingerprint of the files a config tracks (paths, sizes, mtimes), plus the
|
|
@@ -13245,10 +13459,10 @@ async function buildModel(config, options = {}) {
|
|
|
13245
13459
|
const started = performance.now();
|
|
13246
13460
|
const files = await resolveFiles(config);
|
|
13247
13461
|
const useCache = options.cache !== false;
|
|
13248
|
-
const cachePath = cacheFile(config.root);
|
|
13462
|
+
const { file: cachePath, temp } = cacheFile(config.root);
|
|
13249
13463
|
let cached;
|
|
13250
13464
|
if (useCache && !options.oldProgram) try {
|
|
13251
|
-
cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
|
|
13465
|
+
if (!(temp && await untrustedCacheFolder(path.dirname(cachePath), false))) cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
|
|
13252
13466
|
} catch {}
|
|
13253
13467
|
const known = Array.isArray(cached?.dependencies) ? cached.dependencies : options.oldProgram ? projectFiles(options.oldProgram) : [];
|
|
13254
13468
|
const stats = new FileStats(config.root);
|
|
@@ -13262,7 +13476,7 @@ async function buildModel(config, options = {}) {
|
|
|
13262
13476
|
dependencies: cached.dependencies
|
|
13263
13477
|
};
|
|
13264
13478
|
}
|
|
13265
|
-
const extracted =
|
|
13479
|
+
const extracted = extractReadable({
|
|
13266
13480
|
root: config.root,
|
|
13267
13481
|
files: files.components,
|
|
13268
13482
|
tsconfig: config.tsconfig,
|
|
@@ -13324,12 +13538,16 @@ async function buildModel(config, options = {}) {
|
|
|
13324
13538
|
]);
|
|
13325
13539
|
const key = await cacheKey(config, files, dependencies, stats);
|
|
13326
13540
|
if (useCache) try {
|
|
13327
|
-
|
|
13328
|
-
|
|
13541
|
+
const folder = path.dirname(cachePath);
|
|
13542
|
+
let untrusted;
|
|
13543
|
+
if (temp) untrusted = await untrustedCacheFolder(folder, true);
|
|
13544
|
+
else await fsp.mkdir(folder, { recursive: true });
|
|
13545
|
+
if (untrusted) logger.warn(`not caching the model: ${untrusted}. Install the project's dependencies, so the cache goes in node_modules, or set TMPDIR to a folder of your own.`);
|
|
13546
|
+
else await writeWhole(cachePath, JSON.stringify({
|
|
13329
13547
|
key,
|
|
13330
13548
|
dependencies,
|
|
13331
13549
|
model
|
|
13332
|
-
}));
|
|
13550
|
+
}), temp);
|
|
13333
13551
|
} catch (error) {
|
|
13334
13552
|
logger.warn(`could not write cache ${cachePath}: ${error.message}`);
|
|
13335
13553
|
}
|
|
@@ -13406,15 +13624,16 @@ var DesignSystemHost = class {
|
|
|
13406
13624
|
this.watchConfigFiles();
|
|
13407
13625
|
}
|
|
13408
13626
|
/**
|
|
13409
|
-
* Watches the config file, the
|
|
13410
|
-
*
|
|
13411
|
-
*
|
|
13412
|
-
* `extends` target is picked up.
|
|
13627
|
+
* Watches the config file, the config a design-system package declares, which a project with
|
|
13628
|
+
* a config of its own reads under that one, the tsconfig and the tsconfigs it extends through
|
|
13629
|
+
* their directories: editors save by renaming a new file over the old one, which ends a watch
|
|
13630
|
+
* on the file. Called again after each build, so a new `extends` target is picked up.
|
|
13413
13631
|
*/
|
|
13414
13632
|
watchConfigFiles() {
|
|
13415
13633
|
const { root, configFile, tsconfig, designSystemPackage } = this.config;
|
|
13416
13634
|
const files = [
|
|
13417
13635
|
configFile,
|
|
13636
|
+
designSystemPackage?.config,
|
|
13418
13637
|
designSystemPackage && path.join(designSystemPackage.dir, "package.json"),
|
|
13419
13638
|
...DETECTION_FILES.map((f) => path.join(root, f)),
|
|
13420
13639
|
...tsconfig ? [path.resolve(root, tsconfig)] : PROJECT_CONFIG_FILES.map((f) => path.join(root, f)),
|
|
@@ -13472,7 +13691,7 @@ var DesignSystemHost = class {
|
|
|
13472
13691
|
const { root, configFile, designSystemPackage, warnings } = this.config;
|
|
13473
13692
|
const load = this.options.loadConfig ?? (() => loadConfig({
|
|
13474
13693
|
root,
|
|
13475
|
-
config: designSystemPackage ? void 0 : configFile
|
|
13694
|
+
config: configFile === designSystemPackage?.config ? void 0 : configFile
|
|
13476
13695
|
}));
|
|
13477
13696
|
try {
|
|
13478
13697
|
this.config = await load();
|
|
@@ -13511,4 +13730,4 @@ var DesignSystemHost = class {
|
|
|
13511
13730
|
}
|
|
13512
13731
|
};
|
|
13513
13732
|
//#endregion
|
|
13514
|
-
export {
|
|
13733
|
+
export { DEFAULT_TOKENS as $, unrecordedRules as A, toPosix as At, statusKinds as B, countBaseline as C, workspacePackageDirs as Ct, pruneBaseline as D, matchesGlob as Dt, onLines as E, isInside as Et, loadTokens as F, unique as Ft, parseDoc as G, parseDesignMd as H, mergeTokens as I, defaultProjectConfig as It, ConfigError as J, extractComponents as K, toDesignMd as L, readProjectConfig as Lt, writeBaseline as M, json as Mt, RULES as N, plural$1 as Nt, readBaseline as O, pathOf as Ot, TokenIndex as P, truncate as Pt, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, isWorkspaceRoot as St, goneFiles as T, SCRIPT_FILE$2 as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, PackageFinder as _t, fingerprint as a, excludedBy as at, BASELINE_FILE as b, findDesignSystemPackage as bt, silentLogger as c, loadConfigWith as ct, descriptionOf as d, findVariantDefinitions as dt, FALLBACK_TOKENS as et, searchComponents as f, LIBRARIES as ft, escapeData as g, PACKAGE_FIELD as gt, uncheckedNotice as h, notReadYet as ht, componentFiles as i, defineConfig as it, updateBaseline as j, closest as jt, rulesRun as k, relativePath as kt, stderrLogger as l, rulesFor as lt, checkSource as m, libraryTitle as mt, DesignSystemHost as n, RULE_IDS as nt, loadDesignSystem as o, hasOwnConfig as ot, applyFixes as p, LIBRARY_IDS as pt, CONFIG_FILES as q, buildModel as r, configJsonSchema as rt, tokenFiles as s, loadConfig as st, DesignSystem as t, LEGACY_CONFIG_FILES as tt, buildSearchIndex as u, formatPx as ut, NAME as v, declaredConfig as vt, findMoves as w, workspacePatterns as wt, LEGACY_BASELINE_FILE as x, isDesignSystemName as xt, VERSION as y, detectProject as yt, TOKEN_CATEGORIES as z };
|