onsystem 0.10.0 → 0.10.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 +16 -1
- package/dist/cli.js +8 -10
- package/dist/{config-BbTW1jKU.d.ts → config-JaNAiR9Z.d.ts} +6 -8
- package/dist/{design-system-D53Jxybo.js → design-system-rl__N6tw.js} +399 -204
- package/dist/eslint-worker.d.ts +2 -4
- package/dist/eslint-worker.js +3 -4
- package/dist/eslint.d.ts +0 -2
- package/dist/eslint.js +3 -5
- package/dist/index.d.ts +18 -52
- package/dist/index.js +2 -2
- package/dist/{stdio-ha4IF0CU.js → stdio-CkczPt_1.js} +810 -380
- package/dist/{workspace-DAa_08uB.js → workspace-INef0Yqt.js} +10 -19
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -7,11 +9,10 @@ import ts from "typescript";
|
|
|
7
9
|
import os from "node:os";
|
|
8
10
|
import postcss from "postcss";
|
|
9
11
|
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
12
|
import fsp from "node:fs/promises";
|
|
12
13
|
import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
|
|
13
14
|
import { stemmer } from "stemmer";
|
|
14
|
-
|
|
15
|
+
// src/extract/program.ts
|
|
15
16
|
const DEFAULT_OPTIONS = {
|
|
16
17
|
target: ts.ScriptTarget.ES2022,
|
|
17
18
|
module: ts.ModuleKind.ESNext,
|
|
@@ -121,8 +122,74 @@ function projectFiles(program) {
|
|
|
121
122
|
function isProjectFile(file) {
|
|
122
123
|
return !/[\\/]node_modules[\\/]/.test(file);
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// 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
|
+
// src/util/paths.ts
|
|
126
193
|
function toPosix(p) {
|
|
127
194
|
return p.split(path.sep).join("/");
|
|
128
195
|
}
|
|
@@ -271,8 +338,15 @@ function splitTopLevel$2(text) {
|
|
|
271
338
|
function escapeRegExp(text) {
|
|
272
339
|
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
273
340
|
}
|
|
274
|
-
|
|
275
|
-
|
|
341
|
+
/**
|
|
342
|
+
* A file, folder or import path, as written when it holds only the characters of a path (letters,
|
|
343
|
+
* digits and `_ @ $ . + ~ / \ : ( ) [ ] { } # % , = ! -`, as in `app/(site)/[id]/page.tsx`);
|
|
344
|
+
* otherwise, as for a path that holds a space or a line break (git allows one), JSON-quoted.
|
|
345
|
+
*/
|
|
346
|
+
function pathOf(file) {
|
|
347
|
+
return /^[\p{L}\p{N}_@$.+~/\\:()[\]{}#%,=!-]+$/u.test(file) ? file : json(file);
|
|
348
|
+
}
|
|
349
|
+
// src/package-source.ts
|
|
276
350
|
/**
|
|
277
351
|
* A package's sources behind its build output: `exports` that point at `dist/` mapped
|
|
278
352
|
* back to `src/`, and barrels (`index.ts` files that re-export) followed to the files
|
|
@@ -513,8 +587,7 @@ function isFile$1(file) {
|
|
|
513
587
|
return false;
|
|
514
588
|
}
|
|
515
589
|
}
|
|
516
|
-
|
|
517
|
-
//#region src/tokens/imports.ts
|
|
590
|
+
// src/tokens/imports.ts
|
|
518
591
|
/** Enough stylesheets for a theme split over files, without reading a whole design system. */
|
|
519
592
|
const MAX_IMPORTED = 50;
|
|
520
593
|
/**
|
|
@@ -566,69 +639,7 @@ function importedStylesheets(files, resolve) {
|
|
|
566
639
|
for (const file of files) visit(file);
|
|
567
640
|
return out;
|
|
568
641
|
}
|
|
569
|
-
|
|
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
|
-
//#region src/detect.ts
|
|
642
|
+
// src/detect.ts
|
|
632
643
|
const ignore = () => void 0;
|
|
633
644
|
const COMPONENT_FILE = /\.[jt]sx$/;
|
|
634
645
|
/** A module a barrel can lead to: TypeScript or JavaScript, not a declaration file. */
|
|
@@ -1588,11 +1599,11 @@ const PACKAGE_FIELD = "onsystem";
|
|
|
1588
1599
|
function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
|
|
1589
1600
|
const value = pkg?.[PACKAGE_FIELD];
|
|
1590
1601
|
if (value === void 0) return void 0;
|
|
1591
|
-
const field = `"${PACKAGE_FIELD}" in ${path.join(dir, "package.json")}`;
|
|
1602
|
+
const field = `"${PACKAGE_FIELD}" in ${pathOf(path.join(dir, "package.json"))}`;
|
|
1592
1603
|
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
1604
|
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.` };
|
|
1605
|
+
if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${JSON.stringify(value)}, which is outside the package.` };
|
|
1606
|
+
if (!isFile(file)) return { problem: `${field} names ${JSON.stringify(value)}, which does not exist.` };
|
|
1596
1607
|
return { file };
|
|
1597
1608
|
}
|
|
1598
1609
|
/**
|
|
@@ -2064,8 +2075,7 @@ function isDirectory$1(dir) {
|
|
|
2064
2075
|
return false;
|
|
2065
2076
|
}
|
|
2066
2077
|
}
|
|
2067
|
-
|
|
2068
|
-
//#region src/tokens/color.ts
|
|
2078
|
+
// src/tokens/color.ts
|
|
2069
2079
|
const toOklch$1 = converter("oklch");
|
|
2070
2080
|
/**
|
|
2071
2081
|
* Euclidean distance in OKLCH with hue handled as an angle, which is ΔE in
|
|
@@ -2236,8 +2246,7 @@ function formatDistance(color, token) {
|
|
|
2236
2246
|
if (deltaE < 5e-4 && formatOpacityGap(color, token, "")) return "same color";
|
|
2237
2247
|
return formatDeltaE(deltaE);
|
|
2238
2248
|
}
|
|
2239
|
-
|
|
2240
|
-
//#region src/libraries/mui.ts
|
|
2249
|
+
// src/libraries/mui.ts
|
|
2241
2250
|
/** The palette groups MUI's components take as a `color` value (`<Button color="primary">`). */
|
|
2242
2251
|
const ROLES = /* @__PURE__ */ new Set([
|
|
2243
2252
|
"primary",
|
|
@@ -2573,8 +2582,7 @@ function themeTokens(options, source) {
|
|
|
2573
2582
|
for (const token of tokens) token.origin = "library";
|
|
2574
2583
|
return tokens;
|
|
2575
2584
|
}
|
|
2576
|
-
|
|
2577
|
-
//#region src/libraries/index.ts
|
|
2585
|
+
// src/libraries/index.ts
|
|
2578
2586
|
/** The ids of the libraries onsystem can read, for the config's `libraries` list. */
|
|
2579
2587
|
const LIBRARY_IDS = ["mui"];
|
|
2580
2588
|
/** The libraries onsystem can read, by package. */
|
|
@@ -2703,8 +2711,7 @@ function readJson(file) {
|
|
|
2703
2711
|
return;
|
|
2704
2712
|
}
|
|
2705
2713
|
}
|
|
2706
|
-
|
|
2707
|
-
//#region src/extract/cva.ts
|
|
2714
|
+
// src/extract/cva.ts
|
|
2708
2715
|
const FACTORIES = /* @__PURE__ */ new Set(["cva", "tv"]);
|
|
2709
2716
|
/** Finds every `const x = cva(base, { variants, defaultVariants })` in a file. */
|
|
2710
2717
|
function findVariantDefinitions(sourceFile) {
|
|
@@ -2946,8 +2953,7 @@ function evaluateCalc(expression) {
|
|
|
2946
2953
|
const result = sum();
|
|
2947
2954
|
return result && Number.isFinite(result.n) ? result.n : void 0;
|
|
2948
2955
|
}
|
|
2949
|
-
|
|
2950
|
-
//#region src/tokens/usage.ts
|
|
2956
|
+
// src/tokens/usage.ts
|
|
2951
2957
|
/**
|
|
2952
2958
|
* Ready-to-paste ways to use a token, most idiomatic first. With a Tailwind
|
|
2953
2959
|
* theme key that is the utility class; otherwise the custom property.
|
|
@@ -3005,8 +3011,7 @@ function utilities(namespace, key) {
|
|
|
3005
3011
|
default: return [];
|
|
3006
3012
|
}
|
|
3007
3013
|
}
|
|
3008
|
-
|
|
3009
|
-
//#region src/tokens/tailwind-config.ts
|
|
3014
|
+
// src/tokens/tailwind-config.ts
|
|
3010
3015
|
/** A preset chain is a few files deep; anything beyond is not a Tailwind config. */
|
|
3011
3016
|
const MAX_FILES = 12;
|
|
3012
3017
|
/** A config or preset module, which is read for its theme. */
|
|
@@ -3506,8 +3511,7 @@ function findTailwindConfig(root) {
|
|
|
3506
3511
|
"mts"
|
|
3507
3512
|
].map((ext) => path.join(root, `tailwind.config.${ext}`)).find((file) => fs.existsSync(file));
|
|
3508
3513
|
}
|
|
3509
|
-
|
|
3510
|
-
//#region src/config.ts
|
|
3514
|
+
// src/config.ts
|
|
3511
3515
|
const RULE_IDS = [
|
|
3512
3516
|
"no-hardcoded-color",
|
|
3513
3517
|
"no-hardcoded-spacing",
|
|
@@ -3530,7 +3534,7 @@ function unknownKeys(what, known) {
|
|
|
3530
3534
|
if (issue.code !== "unrecognized_keys") return void 0;
|
|
3531
3535
|
return issue.keys.map((key) => {
|
|
3532
3536
|
const guess = closest(key, known, .5);
|
|
3533
|
-
return `Unknown ${what}
|
|
3537
|
+
return `Unknown ${what} ${JSON.stringify(key)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${known.join(", ")}.`}`;
|
|
3534
3538
|
}).join("\n");
|
|
3535
3539
|
};
|
|
3536
3540
|
}
|
|
@@ -3544,7 +3548,7 @@ const ruleSettingSchema = z.union([
|
|
|
3544
3548
|
if (typeof issue.input !== "string") return void 0;
|
|
3545
3549
|
const input = issue.input;
|
|
3546
3550
|
const guess = SEVERITIES.find((s) => input && (s.startsWith(input) || input.startsWith(s)));
|
|
3547
|
-
return `Invalid severity
|
|
3551
|
+
return `Invalid severity ${JSON.stringify(input)}: use "off", "warn" or "error"${guess ? ` (did you mean "${guess}"?)` : ""}.`;
|
|
3548
3552
|
} });
|
|
3549
3553
|
const globsSchema = z.union([z.string(), z.array(z.string())]);
|
|
3550
3554
|
const rulesSchema = z.partialRecord(z.enum(RULE_IDS), ruleSettingSchema, { error: unknownKeys("rule", RULE_IDS) });
|
|
@@ -3562,7 +3566,7 @@ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))]
|
|
|
3562
3566
|
const unknown = Array.isArray(issue.input) ? issue.input.find((id) => !LIBRARY_IDS.includes(id)) : void 0;
|
|
3563
3567
|
if (typeof unknown === "string") {
|
|
3564
3568
|
const guess = closest(unknown, LIBRARY_IDS, .5);
|
|
3565
|
-
return `Unknown library
|
|
3569
|
+
return `Unknown library ${JSON.stringify(unknown)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
|
|
3566
3570
|
}
|
|
3567
3571
|
return `"libraries" is false, to read no library, or a list of the ones to read: ${JSON.stringify(LIBRARY_IDS)}.`;
|
|
3568
3572
|
} });
|
|
@@ -3599,6 +3603,19 @@ const CONFIG_FILES = [
|
|
|
3599
3603
|
* They are still read, when no `onsystem.config.*` exists, with a notice to rename the file.
|
|
3600
3604
|
*/
|
|
3601
3605
|
const LEGACY_CONFIG_FILES = CONFIG_FILES.map((name) => name.replace(/^onsystem\./, "design-system-mcp."));
|
|
3606
|
+
/**
|
|
3607
|
+
* What marks the folder a file belongs to: the nearest one at or above it with a config,
|
|
3608
|
+
* a components.json, a package.json or an Nx project.json. The Claude Code hook finds a
|
|
3609
|
+
* file's project the same way, so `check` from a workspace root and the hook agree on
|
|
3610
|
+
* every file.
|
|
3611
|
+
*/
|
|
3612
|
+
const PROJECT_MARKERS = [
|
|
3613
|
+
...CONFIG_FILES,
|
|
3614
|
+
...LEGACY_CONFIG_FILES,
|
|
3615
|
+
"components.json",
|
|
3616
|
+
"package.json",
|
|
3617
|
+
"project.json"
|
|
3618
|
+
];
|
|
3602
3619
|
const DEFAULT_COMPONENTS = ["components/ui/**/*.{tsx,jsx}", "src/components/ui/**/*.{tsx,jsx}"];
|
|
3603
3620
|
/** Tests and stories: neither component sources nor UI that `check` lints by default. */
|
|
3604
3621
|
const DEFAULT_TEST_EXCLUDE = "**/*.{test,spec,stories}.{ts,tsx,js,jsx}";
|
|
@@ -3675,9 +3692,12 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3675
3692
|
lookup ??= findDesignSystemPackage(root, finder(root));
|
|
3676
3693
|
if (!lookup?.problems.length) declared = lookup?.found;
|
|
3677
3694
|
else if (!configFile) throw declarationError(lookup, root);
|
|
3678
|
-
else warnings.push(`${lookup.problems
|
|
3695
|
+
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)}.`);
|
|
3696
|
+
}
|
|
3697
|
+
for (const { name, problem } of lookup?.skipped ?? []) {
|
|
3698
|
+
const quoted = json(name);
|
|
3699
|
+
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.`);
|
|
3679
3700
|
}
|
|
3680
|
-
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.`);
|
|
3681
3701
|
const pkg = declared ? await readConfig(declared.config, options) : void 0;
|
|
3682
3702
|
const base = options.base ? path.resolve(cwd, options.base) : root;
|
|
3683
3703
|
const rebase = (from) => (p) => from === root ? p : toPosix(path.relative(root, path.resolve(from, slashGlob(p)))) || ".";
|
|
@@ -3693,7 +3713,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3693
3713
|
const outside = [];
|
|
3694
3714
|
const within = (field) => (p) => {
|
|
3695
3715
|
if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
|
|
3696
|
-
outside.push(`${field} ${p}`);
|
|
3716
|
+
outside.push(`${field} ${json(p)}`);
|
|
3697
3717
|
return false;
|
|
3698
3718
|
};
|
|
3699
3719
|
const pathOf = (t) => typeof t === "string" ? t : t.path;
|
|
@@ -3704,7 +3724,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3704
3724
|
const packageComponents = pkg?.components === void 0 ? void 0 : toArray(pkg.components).filter(within("components")).map(fromPackage);
|
|
3705
3725
|
const packageTokens = pkg?.tokens === void 0 ? void 0 : toArray(pkg.tokens).filter((t) => within("tokens")(pathOf(t))).map(tokensFrom(fromPackage));
|
|
3706
3726
|
const packageDocs = pkg?.docs === void 0 ? void 0 : toArray(pkg.docs).filter(within("docs")).map(fromPackage);
|
|
3707
|
-
if (declared && outside.length) warnings.push(
|
|
3727
|
+
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(", ")}.`);
|
|
3708
3728
|
const inherits = pkg !== void 0 && config.components === void 0;
|
|
3709
3729
|
const configComponents = config.components === void 0 ? packageComponents : toArray(config.components).map(fromBase);
|
|
3710
3730
|
const configTokens = config.tokens === void 0 ? inherits ? packageTokens : void 0 : toArray(config.tokens).map(tokensFrom(fromBase));
|
|
@@ -3811,9 +3831,17 @@ function ownDeclaredConfig(dir) {
|
|
|
3811
3831
|
if (declared && "problem" in declared) throw new ConfigError(declared.problem);
|
|
3812
3832
|
return declared?.file;
|
|
3813
3833
|
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Each declaration that cannot be read, on a line of its own. The package's name is the
|
|
3836
|
+
* repository's text (any string can name a dependency in package.json), so it is JSON-quoted:
|
|
3837
|
+
* a line break or a sentence in it stays inside the quotes, on that line.
|
|
3838
|
+
*/
|
|
3839
|
+
function problemLines(problems) {
|
|
3840
|
+
return problems.map((p) => `${json(p.name)}: ${p.problem}`).join("\n");
|
|
3841
|
+
}
|
|
3814
3842
|
/** The error for workspace packages whose declaration cannot be read: their team can fix them. */
|
|
3815
3843
|
function declarationError(lookup, dir) {
|
|
3816
|
-
return new ConfigError(`${lookup.problems
|
|
3844
|
+
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.`);
|
|
3817
3845
|
}
|
|
3818
3846
|
/**
|
|
3819
3847
|
* Whether a path or glob in a dependency's config, relative to its folder `dir`, stays in
|
|
@@ -3880,11 +3908,17 @@ function nodeRunsTypeScript(version = process.versions.node) {
|
|
|
3880
3908
|
/** A config file, read and validated: a JSON one only with `jsonConfigOnly`. */
|
|
3881
3909
|
async function readConfig(file, options) {
|
|
3882
3910
|
if (!fs.existsSync(file)) throw new ConfigError(`Config file not found: ${file}`);
|
|
3883
|
-
if (options.jsonConfigOnly && !file.endsWith(".json"))
|
|
3911
|
+
if (options.jsonConfigOnly && !file.endsWith(".json")) {
|
|
3912
|
+
const within = options.jsonConfigWithin;
|
|
3913
|
+
const project = options.root ? path.resolve(options.cwd ?? process.cwd(), options.root) : path.dirname(file);
|
|
3914
|
+
throw new ConfigError(within ? `${pathOf(file)} was not run: the server was started for ${pathOf(within)} (with --root or the client's root), which holds no project of its own, only projects in its folders, and a project found in it from the path a tool call names reads only a JSON config. Start the server with --root ${pathOf(project)} to use this config, or use onsystem.config.json.` : `${pathOf(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.`);
|
|
3915
|
+
}
|
|
3884
3916
|
const parsed = configSchema.safeParse(await readConfigFile(file));
|
|
3885
3917
|
if (!parsed.success) throw new ConfigError(`Invalid config in ${file}:\n${z.prettifyError(parsed.error)}`);
|
|
3886
3918
|
return parsed.data;
|
|
3887
3919
|
}
|
|
3920
|
+
/** Where require keeps the CommonJS modules it has loaded, by path. */
|
|
3921
|
+
const requireCache = createRequire(import.meta.url).cache;
|
|
3888
3922
|
async function readConfigFile(file) {
|
|
3889
3923
|
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.`);
|
|
3890
3924
|
if (file.endsWith(".json")) try {
|
|
@@ -3893,7 +3927,9 @@ async function readConfigFile(file) {
|
|
|
3893
3927
|
throw new ConfigError(`Could not parse ${file}: ${error.message}`);
|
|
3894
3928
|
}
|
|
3895
3929
|
try {
|
|
3896
|
-
const
|
|
3930
|
+
const version = createHash("sha256").update(fs.readFileSync(file)).digest("hex").slice(0, 16);
|
|
3931
|
+
delete requireCache[fs.realpathSync(file)];
|
|
3932
|
+
const mod = await import(`${pathToFileURL(file).href}?v=${version}`);
|
|
3897
3933
|
return mod.default ?? mod;
|
|
3898
3934
|
} catch (error) {
|
|
3899
3935
|
const message = error.message;
|
|
@@ -3917,8 +3953,7 @@ function configJsonSchema() {
|
|
|
3917
3953
|
io: "input"
|
|
3918
3954
|
});
|
|
3919
3955
|
}
|
|
3920
|
-
|
|
3921
|
-
//#region src/extract/components.ts
|
|
3956
|
+
// src/extract/components.ts
|
|
3922
3957
|
/**
|
|
3923
3958
|
* Extracts React components from source with the TypeScript checker: props
|
|
3924
3959
|
* (types, required, defaults, JSDoc), `cva()`/`tv()` variants, the native
|
|
@@ -4764,8 +4799,7 @@ function valueExports(checker, sourceFile) {
|
|
|
4764
4799
|
return ((symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol).flags & ts.SymbolFlags.Value) !== 0;
|
|
4765
4800
|
}).map((symbol) => symbol.name);
|
|
4766
4801
|
}
|
|
4767
|
-
|
|
4768
|
-
//#region src/extract/docs.ts
|
|
4802
|
+
// src/extract/docs.ts
|
|
4769
4803
|
const EXAMPLE_LANGS = /* @__PURE__ */ new Set([
|
|
4770
4804
|
"tsx",
|
|
4771
4805
|
"jsx",
|
|
@@ -4933,8 +4967,9 @@ function dedent(code) {
|
|
|
4933
4967
|
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)?.[0].length ?? 0));
|
|
4934
4968
|
return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
|
|
4935
4969
|
}
|
|
4936
|
-
|
|
4937
|
-
|
|
4970
|
+
// src/libraries/declarations.ts
|
|
4971
|
+
/** A component's name: a capitalised identifier (`Button`, `ButtonBase`, `Unstable_Grid2`). */
|
|
4972
|
+
const COMPONENT_NAME = /^[A-Z][\w$]*$/;
|
|
4938
4973
|
/** Packages contributing more props than this are summarised, as in source extraction. */
|
|
4939
4974
|
const MAX_LISTED_INHERITED = 40;
|
|
4940
4975
|
/**
|
|
@@ -5006,9 +5041,9 @@ function extractLibrary(options) {
|
|
|
5006
5041
|
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
5007
5042
|
const isDefault = exported.getName() === "default";
|
|
5008
5043
|
const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
|
|
5009
|
-
if (isDefault &&
|
|
5044
|
+
if (isDefault && !COMPONENT_NAME.test(name)) continue;
|
|
5010
5045
|
exportNames.add(name);
|
|
5011
|
-
if (
|
|
5046
|
+
if (!COMPONENT_NAME.test(name)) continue;
|
|
5012
5047
|
const known = bySymbol.get(symbol);
|
|
5013
5048
|
if (known) {
|
|
5014
5049
|
if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
|
|
@@ -5470,8 +5505,7 @@ function linkSlots(components, icons) {
|
|
|
5470
5505
|
}
|
|
5471
5506
|
}
|
|
5472
5507
|
}
|
|
5473
|
-
|
|
5474
|
-
//#region src/libraries/load.ts
|
|
5508
|
+
// src/libraries/load.ts
|
|
5475
5509
|
/** App sources read for module augmentations and themes, at most. */
|
|
5476
5510
|
const MAX_SOURCES = 5e3;
|
|
5477
5511
|
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
@@ -5584,8 +5618,7 @@ async function appSources(root, project, patterns) {
|
|
|
5584
5618
|
}
|
|
5585
5619
|
return found;
|
|
5586
5620
|
}
|
|
5587
|
-
|
|
5588
|
-
//#region src/lint/analyze.ts
|
|
5621
|
+
// src/lint/analyze.ts
|
|
5589
5622
|
const CLASS_FUNCTIONS = /^(cn|clsx|cx|twMerge|twJoin|classNames|classnames|cva|tv)$/;
|
|
5590
5623
|
/** The node's children in source order. */
|
|
5591
5624
|
function childrenOf(node) {
|
|
@@ -5939,8 +5972,7 @@ function styleObjects(expr) {
|
|
|
5939
5972
|
const fn = asFunction(expr);
|
|
5940
5973
|
return (fn ? returnedExpressions(fn).flatMap(valueBranches) : valueBranches(expr)).filter(ts.isObjectLiteralExpression);
|
|
5941
5974
|
}
|
|
5942
|
-
|
|
5943
|
-
//#region src/lint/context.ts
|
|
5975
|
+
// src/lint/context.ts
|
|
5944
5976
|
/**
|
|
5945
5977
|
* Resolves a JSX tag to a design-system component, following imports and
|
|
5946
5978
|
* aliases. `file` is the checked file relative to the root, for relative imports.
|
|
@@ -6035,8 +6067,7 @@ function fromLibrary(target, library, name, source, member) {
|
|
|
6035
6067
|
source
|
|
6036
6068
|
};
|
|
6037
6069
|
}
|
|
6038
|
-
|
|
6039
|
-
//#region src/lint/tailwind-palette.ts
|
|
6070
|
+
// src/lint/tailwind-palette.ts
|
|
6040
6071
|
/** Tailwind's default color palette: name → shade → OKLCH value. */
|
|
6041
6072
|
const TAILWIND_PALETTE = {
|
|
6042
6073
|
red: {
|
|
@@ -6383,8 +6414,7 @@ const TAILWIND_KEYWORD_COLORS = {
|
|
|
6383
6414
|
black: "#000",
|
|
6384
6415
|
white: "#fff"
|
|
6385
6416
|
};
|
|
6386
|
-
|
|
6387
|
-
//#region src/tokens/css.ts
|
|
6417
|
+
// src/tokens/css.ts
|
|
6388
6418
|
/** Tailwind v4 theme namespaces → token category. Longest prefixes first. */
|
|
6389
6419
|
const NAMESPACES = [
|
|
6390
6420
|
["--inset-shadow-", "shadow"],
|
|
@@ -6765,8 +6795,7 @@ function trails(comment, before) {
|
|
|
6765
6795
|
const line = before?.source?.end?.line;
|
|
6766
6796
|
return line !== void 0 && comment.source?.start?.line === line;
|
|
6767
6797
|
}
|
|
6768
|
-
|
|
6769
|
-
//#region src/tokens/design-md.ts
|
|
6798
|
+
// src/tokens/design-md.ts
|
|
6770
6799
|
/**
|
|
6771
6800
|
* Reads the tokens in a DESIGN.md's YAML front matter (https://github.com/google-labs-code/design.md):
|
|
6772
6801
|
* `colors`, `typography`, `rounded` and `spacing`. Tokens are named by their path
|
|
@@ -6902,8 +6931,7 @@ function reference(value) {
|
|
|
6902
6931
|
function isReference(value) {
|
|
6903
6932
|
return reference(value) !== void 0;
|
|
6904
6933
|
}
|
|
6905
|
-
|
|
6906
|
-
//#region src/tokens/dtcg.ts
|
|
6934
|
+
// src/tokens/dtcg.ts
|
|
6907
6935
|
/**
|
|
6908
6936
|
* The `$extensions` key under which onsystem keeps what DTCG has no field for: the token's
|
|
6909
6937
|
* own name, the custom property code uses, the Tailwind theme variable and the values in
|
|
@@ -7265,8 +7293,7 @@ function lineIndex(text) {
|
|
|
7265
7293
|
return lineAt(offset);
|
|
7266
7294
|
};
|
|
7267
7295
|
}
|
|
7268
|
-
|
|
7269
|
-
//#region src/tokens/roles.ts
|
|
7296
|
+
// src/tokens/roles.ts
|
|
7270
7297
|
/** The role of a Tailwind color prefix (`bg`, `text`, `border-t`); undefined for shadows and the like. */
|
|
7271
7298
|
function colorRole(prefix) {
|
|
7272
7299
|
if (!prefix) return void 0;
|
|
@@ -7500,8 +7527,7 @@ function roleFit(token, role) {
|
|
|
7500
7527
|
};
|
|
7501
7528
|
}
|
|
7502
7529
|
}
|
|
7503
|
-
|
|
7504
|
-
//#region src/types.ts
|
|
7530
|
+
// src/types.ts
|
|
7505
7531
|
/**
|
|
7506
7532
|
* The design-system model the server exposes. Everything here is plain JSON so
|
|
7507
7533
|
* it can be cached on disk and returned as MCP structured content.
|
|
@@ -7514,8 +7540,7 @@ const TOKEN_CATEGORIES = [
|
|
|
7514
7540
|
"shadow",
|
|
7515
7541
|
"other"
|
|
7516
7542
|
];
|
|
7517
|
-
|
|
7518
|
-
//#region src/tokens/export.ts
|
|
7543
|
+
// src/tokens/export.ts
|
|
7519
7544
|
/** Group names a DTCG or DESIGN.md token path may already start with, per category. */
|
|
7520
7545
|
const GROUP_NAMES = {
|
|
7521
7546
|
color: [
|
|
@@ -8213,8 +8238,7 @@ function table(tokens, keys) {
|
|
|
8213
8238
|
function use(token) {
|
|
8214
8239
|
return [token.usage.find((u) => !u.includes("(")), cssReference(token)].filter((u) => Boolean(u)).map((u) => `\`${u}\``).join(", ");
|
|
8215
8240
|
}
|
|
8216
|
-
|
|
8217
|
-
//#region src/tokens/index.ts
|
|
8241
|
+
// src/tokens/index.ts
|
|
8218
8242
|
async function loadTokens(root, sources) {
|
|
8219
8243
|
const warnings = [];
|
|
8220
8244
|
const all = [];
|
|
@@ -8537,8 +8561,7 @@ function preferTailwind(a, b) {
|
|
|
8537
8561
|
function byDistance(a, b) {
|
|
8538
8562
|
return Math.round(a.distance * 1e4) - Math.round(b.distance * 1e4) || Number(b.inScope) - Number(a.inScope) || a.fit.rank - b.fit.rank || preferTailwind(a.candidate.token, b.candidate.token) || a.index - b.index;
|
|
8539
8563
|
}
|
|
8540
|
-
|
|
8541
|
-
//#region src/lint/tailwind.ts
|
|
8564
|
+
// src/lint/tailwind.ts
|
|
8542
8565
|
function splitClasses(text, offset) {
|
|
8543
8566
|
const tokens = [];
|
|
8544
8567
|
for (const match of text.matchAll(/\S+/g)) tokens.push({
|
|
@@ -8816,8 +8839,7 @@ const STYLE_RADIUS_PROPERTIES = {
|
|
|
8816
8839
|
function isColorAttribute(name) {
|
|
8817
8840
|
return /^(color|fill|stroke|stopColor|stop-color|floodColor|flood-color|lightingColor|bgcolor)$|Color$/.test(name);
|
|
8818
8841
|
}
|
|
8819
|
-
|
|
8820
|
-
//#region src/lint/target.ts
|
|
8842
|
+
// src/lint/target.ts
|
|
8821
8843
|
/**
|
|
8822
8844
|
* Elements a design system typically replaces. Containers (`div`, `span`,
|
|
8823
8845
|
* `p`) are deliberately absent: `Card` renders a `div`, but not every `div`
|
|
@@ -9222,8 +9244,7 @@ function modulePaths(file) {
|
|
|
9222
9244
|
const module = stripExtension(file);
|
|
9223
9245
|
return module.endsWith("/index") ? [module, module.slice(0, -6)] : [module];
|
|
9224
9246
|
}
|
|
9225
|
-
|
|
9226
|
-
//#region src/lint/rules/components.ts
|
|
9247
|
+
// src/lint/rules/components.ts
|
|
9227
9248
|
function renameTag(context, element, name) {
|
|
9228
9249
|
const edits = [{
|
|
9229
9250
|
range: [element.tagName.getStart(context.sourceFile), element.tagName.end],
|
|
@@ -10378,8 +10399,7 @@ const iconButtonAccessibleName = {
|
|
|
10378
10399
|
}
|
|
10379
10400
|
}
|
|
10380
10401
|
};
|
|
10381
|
-
|
|
10382
|
-
//#region src/lint/rules/hardcoded.ts
|
|
10402
|
+
// src/lint/rules/hardcoded.ts
|
|
10383
10403
|
/** `var(--color-green-50)`: a shade of Tailwind's palette through its CSS variable. */
|
|
10384
10404
|
const PALETTE_VARIABLE = /^var\(\s*--color-([a-z]+-(?:50|[1-9]00|950))\s*\)$/;
|
|
10385
10405
|
/** Luminance values, not design colors: `white`/`black` (and hex twins). */
|
|
@@ -10434,7 +10454,7 @@ function maskReferencedGradientIds(elements) {
|
|
|
10434
10454
|
for (const element of elements) if (element.tag === "mask") refsFrom(element.node);
|
|
10435
10455
|
return ids;
|
|
10436
10456
|
}
|
|
10437
|
-
const noHardcodedColor = {
|
|
10457
|
+
const noHardcodedColor = shortQuotes({
|
|
10438
10458
|
id: "no-hardcoded-color",
|
|
10439
10459
|
description: "Colors must come from design tokens: no hex/rgb/oklch literals, arbitrary Tailwind colors or default-palette classes.",
|
|
10440
10460
|
run(context) {
|
|
@@ -10645,7 +10665,31 @@ const noHardcodedColor = {
|
|
|
10645
10665
|
}
|
|
10646
10666
|
reportChartTokens(context, allow);
|
|
10647
10667
|
}
|
|
10648
|
-
};
|
|
10668
|
+
});
|
|
10669
|
+
/** The longest code a message quotes. */
|
|
10670
|
+
const QUOTED_CODE = 120;
|
|
10671
|
+
/**
|
|
10672
|
+
* The rule, with each `code` span in its messages cut to QUOTED_CODE characters. A message quotes
|
|
10673
|
+
* the class, its fix and the value whole, so one class with 33,000 `hover:` variants made a
|
|
10674
|
+
* message of 400,000 characters for the agent to read.
|
|
10675
|
+
*/
|
|
10676
|
+
function shortQuotes(rule) {
|
|
10677
|
+
const cut = (span, code) => code.length > QUOTED_CODE ? `\`${truncate(code, QUOTED_CODE)}\`` : span;
|
|
10678
|
+
return {
|
|
10679
|
+
...rule,
|
|
10680
|
+
run(context) {
|
|
10681
|
+
rule.run({
|
|
10682
|
+
...context,
|
|
10683
|
+
report(report) {
|
|
10684
|
+
context.report({
|
|
10685
|
+
...report,
|
|
10686
|
+
message: report.message.replace(/`([^`]+)`/g, cut)
|
|
10687
|
+
});
|
|
10688
|
+
}
|
|
10689
|
+
});
|
|
10690
|
+
}
|
|
10691
|
+
};
|
|
10692
|
+
}
|
|
10649
10693
|
/** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
|
|
10650
10694
|
const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
|
|
10651
10695
|
/** Roles of an element that is a drawing: a chart drawn by hand is an image. */
|
|
@@ -11234,25 +11278,69 @@ function setsColor(utility, prefix) {
|
|
|
11234
11278
|
*/
|
|
11235
11279
|
function keepStatesApart(list) {
|
|
11236
11280
|
const after = (listed) => listed.report?.fix?.[0]?.text ?? listed.value;
|
|
11281
|
+
const order = new Map(list.map((listed, i) => [listed, i]));
|
|
11282
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
11283
|
+
const parse = (text) => {
|
|
11284
|
+
let utility = parsed.get(text);
|
|
11285
|
+
if (!utility) {
|
|
11286
|
+
utility = parseUtility(text);
|
|
11287
|
+
parsed.set(text, utility);
|
|
11288
|
+
}
|
|
11289
|
+
return utility;
|
|
11290
|
+
};
|
|
11291
|
+
const byText = /* @__PURE__ */ new Map();
|
|
11292
|
+
const byUtility = /* @__PURE__ */ new Map();
|
|
11293
|
+
const utilityKey = (text) => `${parse(text).base}\0${parse(text).modifier}`;
|
|
11294
|
+
const place = (listed) => {
|
|
11295
|
+
const text = after(listed);
|
|
11296
|
+
const same = byText.get(text) ?? [];
|
|
11297
|
+
byText.set(text, same);
|
|
11298
|
+
const index = order.get(listed) ?? 0;
|
|
11299
|
+
same.splice(same.findLastIndex((other) => (order.get(other) ?? 0) < index) + 1, 0, listed);
|
|
11300
|
+
const texts = byUtility.get(utilityKey(text)) ?? /* @__PURE__ */ new Set();
|
|
11301
|
+
byUtility.set(utilityKey(text), texts.add(text));
|
|
11302
|
+
};
|
|
11303
|
+
const unplace = (listed) => {
|
|
11304
|
+
const text = after(listed);
|
|
11305
|
+
const same = byText.get(text) ?? [];
|
|
11306
|
+
same.splice(same.indexOf(listed), 1);
|
|
11307
|
+
if (!same.length) byUtility.get(utilityKey(text))?.delete(text);
|
|
11308
|
+
};
|
|
11309
|
+
for (const listed of list) place(listed);
|
|
11237
11310
|
for (const pass of ["state", "base"]) for (const listed of list) {
|
|
11238
11311
|
const { report } = listed;
|
|
11239
11312
|
if (!report?.fix?.[0]) continue;
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
const base =
|
|
11313
|
+
const text = after(listed);
|
|
11314
|
+
let found;
|
|
11315
|
+
for (const otherText of byUtility.get(utilityKey(text)) ?? []) {
|
|
11316
|
+
if (otherText === text) continue;
|
|
11317
|
+
const [state, base] = pass === "state" ? [parse(text), parse(otherText)] : [parse(otherText), parse(text)];
|
|
11245
11318
|
const conditions = conditionsBetween(state, base);
|
|
11246
|
-
if (!conditions
|
|
11247
|
-
const
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11319
|
+
if (!conditions) continue;
|
|
11320
|
+
for (const other of byText.get(otherText) ?? []) {
|
|
11321
|
+
if (found && (order.get(other) ?? 0) > (order.get(found.other) ?? 0)) break;
|
|
11322
|
+
if (oneColor(listed.color, other.color)) continue;
|
|
11323
|
+
const [stateListed, baseListed] = pass === "state" ? [listed, other] : [other, listed];
|
|
11324
|
+
if (hasDarkBase(list, stateListed, baseListed, state, base, after)) continue;
|
|
11325
|
+
found = {
|
|
11326
|
+
other,
|
|
11327
|
+
state,
|
|
11328
|
+
conditions
|
|
11329
|
+
};
|
|
11330
|
+
break;
|
|
11331
|
+
}
|
|
11255
11332
|
}
|
|
11333
|
+
if (!found) continue;
|
|
11334
|
+
const { other, state, conditions } = found;
|
|
11335
|
+
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`;
|
|
11336
|
+
const apart = state.modifier ? "such as with another opacity modifier" : `such as with \`${withBase({
|
|
11337
|
+
...state,
|
|
11338
|
+
modifier: "/90"
|
|
11339
|
+
}, state.base)}\``;
|
|
11340
|
+
report.message += ` ${had}, so with this fix \`${conditions}\` would change nothing: it is not applied automatically. Keep the two apart, ${apart}.`;
|
|
11341
|
+
unplace(listed);
|
|
11342
|
+
report.fix = void 0;
|
|
11343
|
+
place(listed);
|
|
11256
11344
|
}
|
|
11257
11345
|
}
|
|
11258
11346
|
/**
|
|
@@ -11432,7 +11520,8 @@ function scopes(context, element, classes) {
|
|
|
11432
11520
|
if (name && !/^Sidebar(?:Provider|Inset)$/.test(name)) tags.push(name);
|
|
11433
11521
|
}
|
|
11434
11522
|
const { tokens, components } = context.target;
|
|
11435
|
-
const found = scopesIn(tokens.scopedFamilies, context.file,
|
|
11523
|
+
const found = scopesIn(tokens.scopedFamilies, context.file, ...tags);
|
|
11524
|
+
if (classes) for (const family of classScopes(context, classes)) found.add(family);
|
|
11436
11525
|
const files = /* @__PURE__ */ new Set([context.file]);
|
|
11437
11526
|
for (const tag of tags) {
|
|
11438
11527
|
const file = components.get(tag)?.source.file;
|
|
@@ -11444,6 +11533,25 @@ function scopes(context, element, classes) {
|
|
|
11444
11533
|
}
|
|
11445
11534
|
return found;
|
|
11446
11535
|
}
|
|
11536
|
+
/** The scoped families each class string of a check names, by its text. */
|
|
11537
|
+
const classScopeCache = /* @__PURE__ */ new WeakMap();
|
|
11538
|
+
/**
|
|
11539
|
+
* The scoped families a class string names, read once per string and check: each color in it
|
|
11540
|
+
* asks, and reading the whole string again for each made a list of thousands quadratic.
|
|
11541
|
+
*/
|
|
11542
|
+
function classScopes(context, classes) {
|
|
11543
|
+
let cache = classScopeCache.get(context.analysis);
|
|
11544
|
+
if (!cache) {
|
|
11545
|
+
cache = /* @__PURE__ */ new Map();
|
|
11546
|
+
classScopeCache.set(context.analysis, cache);
|
|
11547
|
+
}
|
|
11548
|
+
let found = cache.get(classes);
|
|
11549
|
+
if (!found) {
|
|
11550
|
+
found = scopesIn(context.target.tokens.scopedFamilies, classes);
|
|
11551
|
+
cache.set(classes, found);
|
|
11552
|
+
}
|
|
11553
|
+
return found;
|
|
11554
|
+
}
|
|
11447
11555
|
/** The variant prop of the element's component that already sets `prefix-*` classes, if any. */
|
|
11448
11556
|
function variantProp(context, element, prefix) {
|
|
11449
11557
|
const resolution = context.resolve(element);
|
|
@@ -11466,7 +11574,7 @@ function numberValue(init) {
|
|
|
11466
11574
|
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}`;
|
|
11467
11575
|
}
|
|
11468
11576
|
function lengthRule(id, description, spec) {
|
|
11469
|
-
return {
|
|
11577
|
+
return shortQuotes({
|
|
11470
11578
|
id,
|
|
11471
11579
|
description,
|
|
11472
11580
|
run(context) {
|
|
@@ -11538,7 +11646,7 @@ function lengthRule(id, description, spec) {
|
|
|
11538
11646
|
}
|
|
11539
11647
|
}
|
|
11540
11648
|
}
|
|
11541
|
-
};
|
|
11649
|
+
});
|
|
11542
11650
|
}
|
|
11543
11651
|
/**
|
|
11544
11652
|
* A value on the scale whose token code cannot name, with neither a Tailwind class nor a
|
|
@@ -11582,8 +11690,7 @@ function lengthReplacement(context, category, px, prefix) {
|
|
|
11582
11690
|
tailwind: false
|
|
11583
11691
|
};
|
|
11584
11692
|
}
|
|
11585
|
-
|
|
11586
|
-
//#region src/lint/rules/index.ts
|
|
11693
|
+
// src/lint/rules/index.ts
|
|
11587
11694
|
/** Every rule, in the order they run. */
|
|
11588
11695
|
const RULES = [
|
|
11589
11696
|
noHardcodedColor,
|
|
@@ -11603,8 +11710,7 @@ const RULES = [
|
|
|
11603
11710
|
noUnknownVariant,
|
|
11604
11711
|
iconButtonAccessibleName
|
|
11605
11712
|
];
|
|
11606
|
-
|
|
11607
|
-
//#region src/lint/suppress.ts
|
|
11713
|
+
// src/lint/suppress.ts
|
|
11608
11714
|
/**
|
|
11609
11715
|
* The prefix of suppression comments, the one place it is spelled:
|
|
11610
11716
|
* `// onsystem-disable-next-line <rule>[, <rule>]`, `// onsystem-disable-line
|
|
@@ -11679,8 +11785,7 @@ function insideLiteral(sourceFile, offset) {
|
|
|
11679
11785
|
}
|
|
11680
11786
|
return ts.isStringLiteralLike(node) || ts.isTemplateLiteralToken(node) || ts.isJsxText(node) || ts.isRegularExpressionLiteral(node);
|
|
11681
11787
|
}
|
|
11682
|
-
|
|
11683
|
-
//#region src/baseline.ts
|
|
11788
|
+
// src/baseline.ts
|
|
11684
11789
|
const BASELINE_FILE = "onsystem.baseline.json";
|
|
11685
11790
|
/** The baseline's name from before the rename to onsystem: still read when the new one is absent. */
|
|
11686
11791
|
const LEGACY_BASELINE_FILE = "design-system-mcp.baseline.json";
|
|
@@ -12016,15 +12121,12 @@ function writeBaseline(file, entries, about = {}) {
|
|
|
12016
12121
|
function isObject(value) {
|
|
12017
12122
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12018
12123
|
}
|
|
12019
|
-
|
|
12020
|
-
//#region package.json
|
|
12124
|
+
// package.json
|
|
12021
12125
|
var name = "onsystem";
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
const VERSION = "0.10.0";
|
|
12126
|
+
// src/version.ts
|
|
12127
|
+
const VERSION = "0.10.2";
|
|
12025
12128
|
const NAME = name;
|
|
12026
|
-
|
|
12027
|
-
//#region src/lint/sarif.ts
|
|
12129
|
+
// src/lint/sarif.ts
|
|
12028
12130
|
const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
12029
12131
|
const REPO = "https://github.com/dgesteves/onsystem";
|
|
12030
12132
|
const RULES_PAGE = "https://onsystem.vercel.app/rules";
|
|
@@ -12132,8 +12234,7 @@ function sarifResult(d, uri, ruleIndex, fingerprint) {
|
|
|
12132
12234
|
function fingerprint$1(uri, identity, occurrence) {
|
|
12133
12235
|
return createHash("sha256").update(`${uri}\0${identity}\0${String(occurrence)}`).digest("hex");
|
|
12134
12236
|
}
|
|
12135
|
-
|
|
12136
|
-
//#region src/lint/format.ts
|
|
12237
|
+
// src/lint/format.ts
|
|
12137
12238
|
/** From this many findings, `pretty` ends with a summary: more than a screen to scroll back through. */
|
|
12138
12239
|
const SUMMARY_FROM = 50;
|
|
12139
12240
|
/** The palette classes the summary lists. */
|
|
@@ -12232,8 +12333,7 @@ function escapeData(value) {
|
|
|
12232
12333
|
function escapeProperty(value) {
|
|
12233
12334
|
return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
12234
12335
|
}
|
|
12235
|
-
|
|
12236
|
-
//#region src/lint/index.ts
|
|
12336
|
+
// src/lint/index.ts
|
|
12237
12337
|
/**
|
|
12238
12338
|
* Lints a TSX/JSX snippet or file against the design system. Purely
|
|
12239
12339
|
* syntactic: no type-checking of the snippet, so it works on fragments an
|
|
@@ -12244,7 +12344,7 @@ function checkSource(code, target, options) {
|
|
|
12244
12344
|
try {
|
|
12245
12345
|
return checkParsed(code, target, options, file);
|
|
12246
12346
|
} catch (error) {
|
|
12247
|
-
if (!isStackOverflow(error)) throw error;
|
|
12347
|
+
if (!isStackOverflow$1(error)) throw error;
|
|
12248
12348
|
return {
|
|
12249
12349
|
file,
|
|
12250
12350
|
diagnostics: [{
|
|
@@ -12262,7 +12362,7 @@ function checkSource(code, target, options) {
|
|
|
12262
12362
|
};
|
|
12263
12363
|
}
|
|
12264
12364
|
}
|
|
12265
|
-
function isStackOverflow(error) {
|
|
12365
|
+
function isStackOverflow$1(error) {
|
|
12266
12366
|
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
12267
12367
|
}
|
|
12268
12368
|
function checkParsed(code, target, options, file) {
|
|
@@ -12386,7 +12486,7 @@ function uncheckedNotice(results) {
|
|
|
12386
12486
|
const list = [...names];
|
|
12387
12487
|
const shown = list.slice(0, 4).map((n) => `<${n}>`);
|
|
12388
12488
|
const more = list.length > shown.length ? ` and ${list.length - shown.length} more` : "";
|
|
12389
|
-
const from = [...modules].slice(0, 3).map((m) =>
|
|
12489
|
+
const from = [...modules].slice(0, 3).map((m) => JSON.stringify(m));
|
|
12390
12490
|
const moreModules = modules.size > from.length ? ` and ${modules.size - from.length} more` : "";
|
|
12391
12491
|
const where = results.length > 1 ? ` (in ${files} ${files === 1 ? "file" : "files"})` : "";
|
|
12392
12492
|
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.`;
|
|
@@ -12447,8 +12547,7 @@ function applyFixes(code, diagnostics) {
|
|
|
12447
12547
|
}
|
|
12448
12548
|
return out;
|
|
12449
12549
|
}
|
|
12450
|
-
|
|
12451
|
-
//#region src/modules.ts
|
|
12550
|
+
// src/modules.ts
|
|
12452
12551
|
/**
|
|
12453
12552
|
* Finds the modules a checked file imports from the design system, and what
|
|
12454
12553
|
* they export, so the linter can tell a name the model left out (an excluded
|
|
@@ -12561,8 +12660,7 @@ function readExports(sourceFile, resolve) {
|
|
|
12561
12660
|
namespaces
|
|
12562
12661
|
};
|
|
12563
12662
|
}
|
|
12564
|
-
|
|
12565
|
-
//#region src/search/bm25.ts
|
|
12663
|
+
// src/search/bm25.ts
|
|
12566
12664
|
var Bm25Index = class {
|
|
12567
12665
|
docs = [];
|
|
12568
12666
|
df = /* @__PURE__ */ new Map();
|
|
@@ -12619,8 +12717,7 @@ var Bm25Index = class {
|
|
|
12619
12717
|
return hits.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
12620
12718
|
}
|
|
12621
12719
|
};
|
|
12622
|
-
|
|
12623
|
-
//#region src/search/tokenize.ts
|
|
12720
|
+
// src/search/tokenize.ts
|
|
12624
12721
|
const STOP_WORDS = new Set(`a an and are as at be but by can do does for from has have how i if in into is it its me my need
|
|
12625
12722
|
of on or our should so some that the their them then there these this to use used using want we
|
|
12626
12723
|
what when where which while who will with without would you your show make let lets get add want
|
|
@@ -12694,8 +12791,7 @@ function expandQuery(query) {
|
|
|
12694
12791
|
}
|
|
12695
12792
|
return weights;
|
|
12696
12793
|
}
|
|
12697
|
-
|
|
12698
|
-
//#region src/search/index.ts
|
|
12794
|
+
// src/search/index.ts
|
|
12699
12795
|
/** Field weights: names matter most, then descriptions and variant values. */
|
|
12700
12796
|
const WEIGHTS = {
|
|
12701
12797
|
name: 4,
|
|
@@ -12848,8 +12944,7 @@ function searchComponents(index, query, limit = 5) {
|
|
|
12848
12944
|
weak: true
|
|
12849
12945
|
}));
|
|
12850
12946
|
}
|
|
12851
|
-
|
|
12852
|
-
//#region src/util/log.ts
|
|
12947
|
+
// src/util/log.ts
|
|
12853
12948
|
const stderrLogger = {
|
|
12854
12949
|
info: (message) => process.stderr.write(`[onsystem] ${message}\n`),
|
|
12855
12950
|
warn: (message) => process.stderr.write(`[onsystem] warning: ${message}\n`),
|
|
@@ -12860,8 +12955,7 @@ const silentLogger = {
|
|
|
12860
12955
|
warn: () => void 0,
|
|
12861
12956
|
error: () => void 0
|
|
12862
12957
|
};
|
|
12863
|
-
|
|
12864
|
-
//#region src/design-system.ts
|
|
12958
|
+
// src/design-system.ts
|
|
12865
12959
|
/** A loaded design system: the model plus the indexes the tools query. */
|
|
12866
12960
|
var DesignSystem = class {
|
|
12867
12961
|
model;
|
|
@@ -13103,6 +13197,38 @@ function repositoryOf(dir) {
|
|
|
13103
13197
|
if (current === path.dirname(current)) return void 0;
|
|
13104
13198
|
}
|
|
13105
13199
|
}
|
|
13200
|
+
/**
|
|
13201
|
+
* `extractComponents`, without the component files nested too deeply for TypeScript's recursive
|
|
13202
|
+
* parser (thousands of levels), each named in a warning: one such file would otherwise fail the
|
|
13203
|
+
* whole design system with a RangeError that names no file.
|
|
13204
|
+
*/
|
|
13205
|
+
function extractReadable(options) {
|
|
13206
|
+
try {
|
|
13207
|
+
return extractComponents(options);
|
|
13208
|
+
} catch (error) {
|
|
13209
|
+
if (!isStackOverflow(error)) throw error;
|
|
13210
|
+
const tooDeep = new Set(options.files.filter(overflowsParser));
|
|
13211
|
+
if (!tooDeep.size) throw error;
|
|
13212
|
+
const extracted = extractComponents({
|
|
13213
|
+
...options,
|
|
13214
|
+
files: options.files.filter((file) => !tooDeep.has(file))
|
|
13215
|
+
});
|
|
13216
|
+
for (const file of tooDeep) extracted.warnings.push(`${relativePath(options.root, file)}: nested too deeply to read (thousands of levels); skipped.`);
|
|
13217
|
+
return extracted;
|
|
13218
|
+
}
|
|
13219
|
+
}
|
|
13220
|
+
/** Whether parsing `file` on its own runs out of stack. */
|
|
13221
|
+
function overflowsParser(file) {
|
|
13222
|
+
try {
|
|
13223
|
+
ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest);
|
|
13224
|
+
return false;
|
|
13225
|
+
} catch (error) {
|
|
13226
|
+
return isStackOverflow(error);
|
|
13227
|
+
}
|
|
13228
|
+
}
|
|
13229
|
+
function isStackOverflow(error) {
|
|
13230
|
+
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
13231
|
+
}
|
|
13106
13232
|
/** What `inspect` and the server's log say about files that lead outside the project. */
|
|
13107
13233
|
function outsideWarning(root, files) {
|
|
13108
13234
|
const shown = files.map((file) => relativePath(root, file)).sort();
|
|
@@ -13111,9 +13237,10 @@ function outsideWarning(root, files) {
|
|
|
13111
13237
|
/**
|
|
13112
13238
|
* Absolute paths matching `patterns` under the root. `node_modules` is skipped
|
|
13113
13239
|
* even when `exclude` replaces the defaults, except by patterns that name it
|
|
13114
|
-
* (tokens published as a package). Files that lead outside the project are left out.
|
|
13240
|
+
* (tokens published as a package). Files that lead outside the project are left out. Those that
|
|
13241
|
+
* `skip` accepts are left out before that check, so they are not named as leading outside.
|
|
13115
13242
|
*/
|
|
13116
|
-
async function find(config, patterns, contain = new Containment(config)) {
|
|
13243
|
+
async function find(config, patterns, contain = new Containment(config), skip = () => false) {
|
|
13117
13244
|
const options = {
|
|
13118
13245
|
cwd: config.root,
|
|
13119
13246
|
absolute: true,
|
|
@@ -13146,7 +13273,7 @@ async function find(config, patterns, contain = new Containment(config)) {
|
|
|
13146
13273
|
});
|
|
13147
13274
|
})
|
|
13148
13275
|
]);
|
|
13149
|
-
return (await contain.filter(unique(found.flat()))).sort();
|
|
13276
|
+
return (await contain.filter(unique(found.flat()).filter((file) => !skip(file)))).sort();
|
|
13150
13277
|
}
|
|
13151
13278
|
/** The design system's own component files, as absolute paths. */
|
|
13152
13279
|
function componentFiles(config) {
|
|
@@ -13168,8 +13295,9 @@ async function resolveFiles(config) {
|
|
|
13168
13295
|
seen.add(real);
|
|
13169
13296
|
return true;
|
|
13170
13297
|
};
|
|
13298
|
+
const others = config.tokensDetected && !config.configFile && !holdsProject(config.root) ? (file) => inProjectBelow(config.root, file) : () => false;
|
|
13171
13299
|
const add = async (sources) => {
|
|
13172
|
-
for (const source of sources) for (const file of await find(config, [source.path], contain)) if (await isNew(file)) tokens.push({
|
|
13300
|
+
for (const source of sources) for (const file of await find(config, [source.path], contain, others)) if (await isNew(file)) tokens.push({
|
|
13173
13301
|
file,
|
|
13174
13302
|
prefix: source.prefix
|
|
13175
13303
|
});
|
|
@@ -13187,6 +13315,17 @@ async function resolveFiles(config) {
|
|
|
13187
13315
|
outside: [...contain.outside]
|
|
13188
13316
|
};
|
|
13189
13317
|
}
|
|
13318
|
+
function holdsProject(dir) {
|
|
13319
|
+
return PROJECT_MARKERS.some((name) => fs.existsSync(path.join(dir, name)));
|
|
13320
|
+
}
|
|
13321
|
+
/** Whether a folder between `root` and `file` (in it) holds a project. */
|
|
13322
|
+
function inProjectBelow(root, file) {
|
|
13323
|
+
for (let dir = path.dirname(path.resolve(file)); isInside(root, dir); dir = path.dirname(dir)) {
|
|
13324
|
+
if (path.relative(root, dir) === "") return false;
|
|
13325
|
+
if (holdsProject(dir)) return true;
|
|
13326
|
+
}
|
|
13327
|
+
return false;
|
|
13328
|
+
}
|
|
13190
13329
|
/**
|
|
13191
13330
|
* The stylesheets zero config reads besides the ones it found: those they `@import` (by a
|
|
13192
13331
|
* path, or from a workspace package: `@import "@acme/theme"`), then those that set the colors
|
|
@@ -13250,11 +13389,54 @@ async function cacheKey(config, files, dependencies, stats = new FileStats(confi
|
|
|
13250
13389
|
stats: await stats.of(all)
|
|
13251
13390
|
})).digest("hex");
|
|
13252
13391
|
}
|
|
13392
|
+
/**
|
|
13393
|
+
* Where the model is cached: the project's `node_modules/.cache`, else (`temp`) a folder of the
|
|
13394
|
+
* user's own in the system temp folder, `onsystem-<uid>`.
|
|
13395
|
+
*/
|
|
13253
13396
|
function cacheFile(root) {
|
|
13254
13397
|
const nodeModules = path.join(root, "node_modules");
|
|
13255
|
-
if (fs.existsSync(nodeModules)) return
|
|
13398
|
+
if (fs.existsSync(nodeModules)) return {
|
|
13399
|
+
file: path.join(nodeModules, ".cache", "onsystem", "model.json"),
|
|
13400
|
+
temp: false
|
|
13401
|
+
};
|
|
13256
13402
|
const id = createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
13257
|
-
|
|
13403
|
+
const user = process.getuid ? `-${process.getuid()}` : "";
|
|
13404
|
+
return {
|
|
13405
|
+
file: path.join(os.tmpdir(), `onsystem${user}`, `${id}.json`),
|
|
13406
|
+
temp: true
|
|
13407
|
+
};
|
|
13408
|
+
}
|
|
13409
|
+
/**
|
|
13410
|
+
* Why the cache folder in the system temp folder cannot be trusted, or undefined when it can: it
|
|
13411
|
+
* must be a folder, not a link, that the user owns and no one else can write to. On a multi-user
|
|
13412
|
+
* host the temp folder is shared, and a model planted in it would reach the agent. With `create`,
|
|
13413
|
+
* it is created (or closed) for the user first. Windows gives each user a temp folder of their own.
|
|
13414
|
+
*/
|
|
13415
|
+
async function untrustedCacheFolder(folder, create) {
|
|
13416
|
+
if (!process.getuid) return void 0;
|
|
13417
|
+
if (create) await fsp.mkdir(folder, {
|
|
13418
|
+
recursive: true,
|
|
13419
|
+
mode: 448
|
|
13420
|
+
});
|
|
13421
|
+
const stat = await fsp.lstat(folder);
|
|
13422
|
+
if (!stat.isDirectory()) return `${folder} is not a folder`;
|
|
13423
|
+
if (stat.uid !== process.getuid()) return `${folder} belongs to another user`;
|
|
13424
|
+
if (!create && stat.mode & 18) return `other users can write to ${folder}`;
|
|
13425
|
+
if (create && stat.mode & 63) await fsp.chmod(folder, 448);
|
|
13426
|
+
}
|
|
13427
|
+
/**
|
|
13428
|
+
* Writes `text` beside `file`, then renames it into place: a run reading it never sees half. With
|
|
13429
|
+
* `own`, only the user can read it (the temp folder's cache); without, it has the default mode, so
|
|
13430
|
+
* a cache in `node_modules` built by one user (a Docker build as root) is still read by another.
|
|
13431
|
+
*/
|
|
13432
|
+
async function writeWhole(file, text, own) {
|
|
13433
|
+
const partial = `${file}.${process.pid}-${randomUUID()}.tmp`;
|
|
13434
|
+
try {
|
|
13435
|
+
await fsp.writeFile(partial, text, own ? { mode: 384 } : {});
|
|
13436
|
+
await fsp.rename(partial, file);
|
|
13437
|
+
} finally {
|
|
13438
|
+
await fsp.rm(partial, { force: true });
|
|
13439
|
+
}
|
|
13258
13440
|
}
|
|
13259
13441
|
/**
|
|
13260
13442
|
* Fingerprint of the files a config tracks (paths, sizes, mtimes), plus the
|
|
@@ -13268,10 +13450,10 @@ async function buildModel(config, options = {}) {
|
|
|
13268
13450
|
const started = performance.now();
|
|
13269
13451
|
const files = await resolveFiles(config);
|
|
13270
13452
|
const useCache = options.cache !== false;
|
|
13271
|
-
const cachePath = cacheFile(config.root);
|
|
13453
|
+
const { file: cachePath, temp } = cacheFile(config.root);
|
|
13272
13454
|
let cached;
|
|
13273
13455
|
if (useCache && !options.oldProgram) try {
|
|
13274
|
-
cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
|
|
13456
|
+
if (!(temp && await untrustedCacheFolder(path.dirname(cachePath), false))) cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
|
|
13275
13457
|
} catch {}
|
|
13276
13458
|
const known = Array.isArray(cached?.dependencies) ? cached.dependencies : options.oldProgram ? projectFiles(options.oldProgram) : [];
|
|
13277
13459
|
const stats = new FileStats(config.root);
|
|
@@ -13285,7 +13467,7 @@ async function buildModel(config, options = {}) {
|
|
|
13285
13467
|
dependencies: cached.dependencies
|
|
13286
13468
|
};
|
|
13287
13469
|
}
|
|
13288
|
-
const extracted =
|
|
13470
|
+
const extracted = extractReadable({
|
|
13289
13471
|
root: config.root,
|
|
13290
13472
|
files: files.components,
|
|
13291
13473
|
tsconfig: config.tsconfig,
|
|
@@ -13347,12 +13529,16 @@ async function buildModel(config, options = {}) {
|
|
|
13347
13529
|
]);
|
|
13348
13530
|
const key = await cacheKey(config, files, dependencies, stats);
|
|
13349
13531
|
if (useCache) try {
|
|
13350
|
-
|
|
13351
|
-
|
|
13532
|
+
const folder = path.dirname(cachePath);
|
|
13533
|
+
let untrusted;
|
|
13534
|
+
if (temp) untrusted = await untrustedCacheFolder(folder, true);
|
|
13535
|
+
else await fsp.mkdir(folder, { recursive: true });
|
|
13536
|
+
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.`);
|
|
13537
|
+
else await writeWhole(cachePath, JSON.stringify({
|
|
13352
13538
|
key,
|
|
13353
13539
|
dependencies,
|
|
13354
13540
|
model
|
|
13355
|
-
}));
|
|
13541
|
+
}), temp);
|
|
13356
13542
|
} catch (error) {
|
|
13357
13543
|
logger.warn(`could not write cache ${cachePath}: ${error.message}`);
|
|
13358
13544
|
}
|
|
@@ -13391,19 +13577,29 @@ var DesignSystemHost = class {
|
|
|
13391
13577
|
this.options = options;
|
|
13392
13578
|
}
|
|
13393
13579
|
get() {
|
|
13394
|
-
this.current ??= this.build(false);
|
|
13580
|
+
this.current ??= this.kept(this.build(false));
|
|
13395
13581
|
return this.current;
|
|
13396
13582
|
}
|
|
13397
13583
|
reload() {
|
|
13398
13584
|
const next = this.build(true);
|
|
13399
13585
|
const previous = this.current;
|
|
13400
|
-
this.current = next.catch(async (error) => {
|
|
13586
|
+
this.current = this.kept(next.catch(async (error) => {
|
|
13401
13587
|
(this.options.logger ?? silentLogger).error(`reload failed: ${error.message}`);
|
|
13402
13588
|
if (previous) return previous;
|
|
13403
13589
|
throw error;
|
|
13404
|
-
});
|
|
13590
|
+
}));
|
|
13405
13591
|
return this.current;
|
|
13406
13592
|
}
|
|
13593
|
+
/**
|
|
13594
|
+
* `build`, dropped if it fails with no earlier model to serve instead (a first build that
|
|
13595
|
+
* fails), so the next `get()` builds again.
|
|
13596
|
+
*/
|
|
13597
|
+
kept(build) {
|
|
13598
|
+
build.catch(() => {
|
|
13599
|
+
if (this.current === build) this.current = void 0;
|
|
13600
|
+
});
|
|
13601
|
+
return build;
|
|
13602
|
+
}
|
|
13407
13603
|
onChange(listener) {
|
|
13408
13604
|
this.listeners.add(listener);
|
|
13409
13605
|
return () => this.listeners.delete(listener);
|
|
@@ -13534,5 +13730,4 @@ var DesignSystemHost = class {
|
|
|
13534
13730
|
return ds;
|
|
13535
13731
|
}
|
|
13536
13732
|
};
|
|
13537
|
-
|
|
13538
|
-
export { DEFAULT_TOKENS as $, unrecordedRules as A, isInside as At, statusKinds as B, countBaseline as C, workspacePackageDirs as Ct, pruneBaseline as D, truncate as Dt, onLines as E, plural$1 as Et, loadTokens as F, readProjectConfig as Ft, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, relativePath as Mt, RULES as N, toPosix as Nt, readBaseline as O, unique as Ot, TokenIndex as P, defaultProjectConfig as Pt, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, isWorkspaceRoot as St, goneFiles as T, closest 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, matchesGlob as jt, rulesRun as k, SCRIPT_FILE$2 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 };
|
|
13733
|
+
export { DEFAULT_TOKENS as $, unrecordedRules as A, relativePath as At, statusKinds as B, countBaseline as C, isWorkspaceRoot as Ct, pruneBaseline as D, isInside as Dt, onLines as E, SCRIPT_FILE$2 as Et, loadTokens as F, truncate as Ft, parseDoc as G, parseDesignMd as H, mergeTokens as I, unique as It, ConfigError as J, extractComponents as K, toDesignMd as L, defaultProjectConfig as Lt, writeBaseline as M, closest as Mt, RULES as N, json as Nt, readBaseline as O, matchesGlob as Ot, TokenIndex as P, plural$1 as Pt, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, readProjectConfig as Rt, applyBaseline as S, isDesignSystemName as St, goneFiles as T, workspacePatterns 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 _, PACKAGE_FIELD as _t, fingerprint as a, defineConfig as at, BASELINE_FILE as b, detectProject as bt, silentLogger as c, loadConfig as ct, descriptionOf as d, formatPx as dt, FALLBACK_TOKENS as et, searchComponents as f, findVariantDefinitions as ft, escapeData as g, notReadYet as gt, uncheckedNotice as h, libraryTitle as ht, componentFiles as i, configJsonSchema as it, updateBaseline as j, toPosix as jt, rulesRun as k, pathOf as kt, stderrLogger as l, loadConfigWith as lt, checkSource as m, LIBRARY_IDS as mt, DesignSystemHost as n, PROJECT_MARKERS as nt, loadDesignSystem as o, excludedBy as ot, applyFixes as p, LIBRARIES as pt, CONFIG_FILES as q, buildModel as r, RULE_IDS as rt, tokenFiles as s, hasOwnConfig as st, DesignSystem as t, LEGACY_CONFIG_FILES as tt, buildSearchIndex as u, rulesFor as ut, NAME as v, PackageFinder as vt, findMoves as w, workspacePackageDirs as wt, LEGACY_BASELINE_FILE as x, findDesignSystemPackage as xt, VERSION as y, declaredConfig as yt, TOKEN_CATEGORIES as z };
|