brandrail 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brandrail.mjs +1361 -1235
- package/package.json +3 -3
package/dist/brandrail.mjs
CHANGED
|
@@ -7013,10 +7013,10 @@ var require_formats = __commonJS({
|
|
|
7013
7013
|
function isLeapYear(year) {
|
|
7014
7014
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
7015
7015
|
}
|
|
7016
|
-
var
|
|
7016
|
+
var DATE2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
7017
7017
|
var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
7018
7018
|
function date5(str) {
|
|
7019
|
-
const matches2 =
|
|
7019
|
+
const matches2 = DATE2.exec(str);
|
|
7020
7020
|
if (!matches2)
|
|
7021
7021
|
return false;
|
|
7022
7022
|
const year = +matches2[1];
|
|
@@ -7810,6 +7810,634 @@ function contrastRatio(a, b) {
|
|
|
7810
7810
|
return (l1 + 0.05) / (l2 + 0.05);
|
|
7811
7811
|
}
|
|
7812
7812
|
|
|
7813
|
+
// ../engine/src/tokens.ts
|
|
7814
|
+
var EXT = "dev.brandrail";
|
|
7815
|
+
function slug(s) {
|
|
7816
|
+
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
7817
|
+
}
|
|
7818
|
+
var segs = (name) => name.split(/[/$.]+/).map(slug).filter(Boolean);
|
|
7819
|
+
var stripPrefix = (s, ...prefixes) => {
|
|
7820
|
+
for (const p of prefixes) if (s === p) return "";
|
|
7821
|
+
for (const p of prefixes) if (s.startsWith(`${p}-`)) return s.slice(p.length + 1);
|
|
7822
|
+
return s;
|
|
7823
|
+
};
|
|
7824
|
+
var WEIGHTS = {
|
|
7825
|
+
thin: "100",
|
|
7826
|
+
hairline: "100",
|
|
7827
|
+
"extra-light": "200",
|
|
7828
|
+
"ultra-light": "200",
|
|
7829
|
+
light: "300",
|
|
7830
|
+
regular: "400",
|
|
7831
|
+
regulare: "400",
|
|
7832
|
+
normal: "400",
|
|
7833
|
+
book: "400",
|
|
7834
|
+
medium: "500",
|
|
7835
|
+
"semi-bold": "600",
|
|
7836
|
+
semibold: "600",
|
|
7837
|
+
"demi-bold": "600",
|
|
7838
|
+
bold: "700",
|
|
7839
|
+
"extra-bold": "800",
|
|
7840
|
+
"ultra-bold": "800",
|
|
7841
|
+
black: "900",
|
|
7842
|
+
heavy: "900"
|
|
7843
|
+
};
|
|
7844
|
+
function fontWeight(v) {
|
|
7845
|
+
if (typeof v === "number") return String(v);
|
|
7846
|
+
return WEIGHTS[slug(v)] ?? (/^\d{3}$/.test(v.trim()) ? v.trim() : v);
|
|
7847
|
+
}
|
|
7848
|
+
function colorValue(v) {
|
|
7849
|
+
if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
|
|
7850
|
+
const c = v;
|
|
7851
|
+
const alpha = typeof c.alpha === "number" ? c.alpha : 1;
|
|
7852
|
+
const withAlpha = (hex4) => alpha < 1 ? `${hex4.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex4.slice(0, 7);
|
|
7853
|
+
if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
|
|
7854
|
+
const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
|
|
7855
|
+
if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
|
|
7856
|
+
return withAlpha(toHex({ r: comps[0] * 255, g: comps[1] * 255, b: comps[2] * 255, a: 1 }));
|
|
7857
|
+
}
|
|
7858
|
+
return null;
|
|
7859
|
+
}
|
|
7860
|
+
if (v && typeof v === "object" && "r" in v) {
|
|
7861
|
+
const c = v;
|
|
7862
|
+
const hex4 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
|
|
7863
|
+
const a = c.a ?? 1;
|
|
7864
|
+
return a < 1 ? `${hex4}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex4;
|
|
7865
|
+
}
|
|
7866
|
+
if (typeof v !== "string") return null;
|
|
7867
|
+
const s = v.trim();
|
|
7868
|
+
if (/^#[0-9a-f]{3,8}$/i.test(s)) {
|
|
7869
|
+
const h = s.slice(1).toLowerCase();
|
|
7870
|
+
return h.length === 3 || h.length === 4 ? `#${[...h].map((c) => c + c).join("")}` : `#${h}`;
|
|
7871
|
+
}
|
|
7872
|
+
const rgb = parseColor(s);
|
|
7873
|
+
if (!rgb) return null;
|
|
7874
|
+
const hex3 = toHex(rgb);
|
|
7875
|
+
return rgb.a < 1 ? `${hex3}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex3;
|
|
7876
|
+
}
|
|
7877
|
+
var dim = (v) => {
|
|
7878
|
+
if (v && typeof v === "object" && "value" in v) {
|
|
7879
|
+
const d = v;
|
|
7880
|
+
const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
|
|
7881
|
+
return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
|
|
7882
|
+
}
|
|
7883
|
+
return typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
|
|
7884
|
+
};
|
|
7885
|
+
function classify(name, kind, value, opts = {}) {
|
|
7886
|
+
const parts = segs(name);
|
|
7887
|
+
const hay = slug(`${opts.context ?? ""} ${name}`);
|
|
7888
|
+
const scopes = opts.scopes ?? [];
|
|
7889
|
+
const last = parts[parts.length - 1] ?? "";
|
|
7890
|
+
const rest = parts.slice(0, -1);
|
|
7891
|
+
if (kind === "boolean") return null;
|
|
7892
|
+
if (kind === "color") {
|
|
7893
|
+
const v2 = colorValue(value);
|
|
7894
|
+
if (!v2) return null;
|
|
7895
|
+
let p = parts[0] === "color" || parts[0] === "colors" ? parts.slice(1) : parts;
|
|
7896
|
+
if (p.length === 1) p = [stripPrefix(p[0], "colors", "color") || p[0]];
|
|
7897
|
+
const tail = p[p.length - 1];
|
|
7898
|
+
const m = tail.match(/^(.*[a-z])-(\d+)$/);
|
|
7899
|
+
if (m && p.length <= 2) p = [...p.slice(0, -1), m[1], m[2]];
|
|
7900
|
+
return { path: ["color", ...p].join("."), type: "color", value: v2 };
|
|
7901
|
+
}
|
|
7902
|
+
if (kind === "string") {
|
|
7903
|
+
const s = String(value);
|
|
7904
|
+
if (scopes.includes("FONT_FAMILY") || /font-family|typeface|font-name/.test(hay) || last === "font") {
|
|
7905
|
+
const n = stripPrefix(last, "font-family", "font", "family") || "sans";
|
|
7906
|
+
return { path: `font.family.${/mono|code/.test(s.toLowerCase()) && n === "sans" ? "mono" : n}`, type: "fontFamily", value: s };
|
|
7907
|
+
}
|
|
7908
|
+
if (scopes.includes("FONT_STYLE") || scopes.includes("FONT_WEIGHT") || /font-weight|weight/.test(hay)) {
|
|
7909
|
+
return { path: `font.weight.${stripPrefix(last, "font-weight", "weight") || last}`, type: "fontWeight", value: fontWeight(s) };
|
|
7910
|
+
}
|
|
7911
|
+
const c = colorValue(s);
|
|
7912
|
+
if (c && /^#|^rgb|^hsl/i.test(s.trim())) return classify(name, "color", s, opts);
|
|
7913
|
+
return null;
|
|
7914
|
+
}
|
|
7915
|
+
const v = dim(value);
|
|
7916
|
+
const has = (...scope) => scope.some((x2) => scopes.includes(x2));
|
|
7917
|
+
const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
|
|
7918
|
+
if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
|
|
7919
|
+
if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
|
|
7920
|
+
if (has("LINE_HEIGHT") || /line-height|leading/.test(hay)) return { path: `font.line-height.${nm("line-height", "leading")}`, type: "dimension", value: v };
|
|
7921
|
+
if (has("LETTER_SPACING") || /letter-spacing|tracking/.test(hay)) return { path: `font.letter-spacing.${nm("letter-spacing", "tracking")}`, type: "dimension", value: v };
|
|
7922
|
+
if (has("FONT_WEIGHT") || /font-weight/.test(hay)) return { path: `font.weight.${nm("font-weight", "weight")}`, type: "fontWeight", value: String(value) };
|
|
7923
|
+
if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
|
|
7924
|
+
if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
|
|
7925
|
+
if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
|
|
7926
|
+
if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
|
|
7927
|
+
return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
|
|
7928
|
+
}
|
|
7929
|
+
function rawLeaves(node2, prefix, out, studio, inherited) {
|
|
7930
|
+
if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
|
|
7931
|
+
const o = node2;
|
|
7932
|
+
const groupType = o.$type ?? inherited;
|
|
7933
|
+
const isLeaf = "$value" in o || studio && "value" in o && (typeof o.value !== "object" || o.value === null || "type" in o);
|
|
7934
|
+
if (isLeaf) {
|
|
7935
|
+
const ext = o.$extensions?.[EXT];
|
|
7936
|
+
out.push({
|
|
7937
|
+
path: prefix,
|
|
7938
|
+
type: o.$type ?? o.type ?? groupType,
|
|
7939
|
+
value: "$value" in o ? o.$value : o.value,
|
|
7940
|
+
description: o.$description ?? o.description,
|
|
7941
|
+
dark: ext?.modes?.dark
|
|
7942
|
+
});
|
|
7943
|
+
return;
|
|
7944
|
+
}
|
|
7945
|
+
for (const [k, v] of Object.entries(o)) if (!k.startsWith("$")) rawLeaves(v, prefix ? `${prefix}.${k}` : k, out, studio, groupType);
|
|
7946
|
+
}
|
|
7947
|
+
var REF = /^\{([^}]+)\}$/;
|
|
7948
|
+
function resolveRefs(raws) {
|
|
7949
|
+
const byPath = new Map(raws.map((r) => [r.path, r]));
|
|
7950
|
+
const deref = (v, mode, depth = 0) => {
|
|
7951
|
+
if (typeof v !== "string" || depth > 10) return v;
|
|
7952
|
+
const m = v.trim().match(REF);
|
|
7953
|
+
if (!m) return v.replace(/\{([^}]+)\}/g, (all, p) => String(byPath.get(p)?.value ?? all));
|
|
7954
|
+
const target = byPath.get(m[1]);
|
|
7955
|
+
if (!target) return v;
|
|
7956
|
+
return deref(mode === "dark" && target.dark !== void 0 ? target.dark : target.value, mode, depth + 1);
|
|
7957
|
+
};
|
|
7958
|
+
return raws.map((r) => ({ ...r, value: deref(r.value, "light"), dark: r.dark !== void 0 ? deref(r.dark, "dark") : void 0 }));
|
|
7959
|
+
}
|
|
7960
|
+
var TYPE_KIND = {
|
|
7961
|
+
color: "color",
|
|
7962
|
+
dimension: "number",
|
|
7963
|
+
spacing: "number",
|
|
7964
|
+
sizing: "number",
|
|
7965
|
+
borderradius: "number",
|
|
7966
|
+
fontsizes: "number",
|
|
7967
|
+
fontsize: "number",
|
|
7968
|
+
lineheights: "number",
|
|
7969
|
+
lineheight: "number",
|
|
7970
|
+
letterspacing: "number",
|
|
7971
|
+
number: "number",
|
|
7972
|
+
fontfamilies: "string",
|
|
7973
|
+
fontfamily: "string",
|
|
7974
|
+
fontweights: "string",
|
|
7975
|
+
fontweight: "string"
|
|
7976
|
+
};
|
|
7977
|
+
function flattenTokens(doc) {
|
|
7978
|
+
const raws = resolveRefs(collectRaw(doc).raws);
|
|
7979
|
+
const out = [];
|
|
7980
|
+
for (const r of raws) {
|
|
7981
|
+
const t = (r.type ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
7982
|
+
if (t === "color" || !t && r.path.startsWith("color")) {
|
|
7983
|
+
const v = colorValue(r.value);
|
|
7984
|
+
if (!v) continue;
|
|
7985
|
+
const d = r.dark !== void 0 ? colorValue(r.dark) : null;
|
|
7986
|
+
out.push({ path: r.path, type: "color", value: v, ...d ? { dark: d } : {}, ...r.description ? { description: r.description } : {} });
|
|
7987
|
+
} else if (typeof r.value === "string" || typeof r.value === "number" || r.value && typeof r.value === "object" && "unit" in r.value) {
|
|
7988
|
+
const type = typeof r.value === "object" ? "dimension" : t === "fontfamily" || t === "fontfamilies" ? "fontFamily" : t === "fontweight" || t === "fontweights" ? "fontWeight" : TYPE_KIND[t] === "number" ? "dimension" : t === "number" ? "number" : /^font\.family\./.test(r.path) ? "fontFamily" : typeof r.value === "number" ? "number" : /px$|rem$/.test(String(r.value)) ? "dimension" : "string";
|
|
7989
|
+
out.push({ path: r.path, type, value: type === "dimension" ? dim(r.value) : String(r.value), ...r.description ? { description: r.description } : {} });
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
return out;
|
|
7993
|
+
}
|
|
7994
|
+
function collectRaw(doc) {
|
|
7995
|
+
const o = doc ?? {};
|
|
7996
|
+
const studio = !hasDollarValue(o) && hasStudioValue(o);
|
|
7997
|
+
const order = o.$metadata?.tokenSetOrder;
|
|
7998
|
+
if (!studio) {
|
|
7999
|
+
const raws = [];
|
|
8000
|
+
rawLeaves(o, "", raws, false);
|
|
8001
|
+
return { raws, studio };
|
|
8002
|
+
}
|
|
8003
|
+
const setNames = (order ?? Object.keys(o).filter((k) => !k.startsWith("$"))).filter((k) => o[k] && typeof o[k] === "object");
|
|
8004
|
+
const looksLikeSets = setNames.length > 0 && setNames.every((k) => !hasLeafAtTop(o[k]));
|
|
8005
|
+
if (!looksLikeSets) {
|
|
8006
|
+
const raws = [];
|
|
8007
|
+
rawLeaves(o, "", raws, true);
|
|
8008
|
+
return { raws, studio };
|
|
8009
|
+
}
|
|
8010
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8011
|
+
const darkSets = setNames.filter((n) => /dark/i.test(n));
|
|
8012
|
+
for (const set2 of setNames.filter((n) => !/dark/i.test(n))) {
|
|
8013
|
+
const raws = [];
|
|
8014
|
+
rawLeaves(o[set2], "", raws, true);
|
|
8015
|
+
for (const r of raws) merged.set(r.path, r);
|
|
8016
|
+
}
|
|
8017
|
+
for (const set2 of darkSets) {
|
|
8018
|
+
const raws = [];
|
|
8019
|
+
rawLeaves(o[set2], "", raws, true);
|
|
8020
|
+
for (const r of raws) {
|
|
8021
|
+
const base = merged.get(r.path);
|
|
8022
|
+
if (base) base.dark = r.value;
|
|
8023
|
+
else merged.set(r.path, r);
|
|
8024
|
+
}
|
|
8025
|
+
}
|
|
8026
|
+
return { raws: [...merged.values()], studio };
|
|
8027
|
+
}
|
|
8028
|
+
function hasDollarValue(node2, depth = 0) {
|
|
8029
|
+
if (!node2 || typeof node2 !== "object" || depth > 12) return false;
|
|
8030
|
+
if ("$value" in node2) return true;
|
|
8031
|
+
return Object.values(node2).some((v) => hasDollarValue(v, depth + 1));
|
|
8032
|
+
}
|
|
8033
|
+
function hasStudioValue(node2, depth = 0) {
|
|
8034
|
+
if (!node2 || typeof node2 !== "object" || depth > 12) return false;
|
|
8035
|
+
const o = node2;
|
|
8036
|
+
if ("value" in o && "type" in o) return true;
|
|
8037
|
+
return Object.values(o).some((v) => hasStudioValue(v, depth + 1));
|
|
8038
|
+
}
|
|
8039
|
+
function hasLeafAtTop(node2) {
|
|
8040
|
+
return !!node2 && typeof node2 === "object" && ("value" in node2 || "$value" in node2);
|
|
8041
|
+
}
|
|
8042
|
+
function buildDtcg(tokens) {
|
|
8043
|
+
const tree = {};
|
|
8044
|
+
for (const t of tokens) {
|
|
8045
|
+
const parts = t.path.split(".");
|
|
8046
|
+
let node2 = tree;
|
|
8047
|
+
parts.forEach((p, i2) => {
|
|
8048
|
+
if (i2 === parts.length - 1) {
|
|
8049
|
+
node2[p] = {
|
|
8050
|
+
$type: t.type,
|
|
8051
|
+
$value: t.value,
|
|
8052
|
+
...t.description ? { $description: t.description } : {},
|
|
8053
|
+
...t.dark && t.dark !== t.value ? { $extensions: { [EXT]: { modes: { dark: t.dark } } } } : {}
|
|
8054
|
+
};
|
|
8055
|
+
} else {
|
|
8056
|
+
const next = node2[p];
|
|
8057
|
+
if (next && typeof next === "object" && "$value" in next) node2[p] = { default: next };
|
|
8058
|
+
node2 = node2[p] ??= {};
|
|
8059
|
+
}
|
|
8060
|
+
});
|
|
8061
|
+
}
|
|
8062
|
+
return tree;
|
|
8063
|
+
}
|
|
8064
|
+
var isAlias = (v) => !!v && typeof v === "object" && v.type === "VARIABLE_ALIAS";
|
|
8065
|
+
var DARK = /dark|night/i;
|
|
8066
|
+
var LIGHT = /light|default|day|base/i;
|
|
8067
|
+
function finish(format, all, notes, opts) {
|
|
8068
|
+
const PALETTE2 = /^(color\.[a-z-]+)\.\d{1,3}$/;
|
|
8069
|
+
const usedFamilies = new Set(all.filter((t) => t.referenced).map((t) => t.path.match(PALETTE2)?.[1]).filter(Boolean));
|
|
8070
|
+
if (usedFamilies.size) {
|
|
8071
|
+
const steps = /* @__PURE__ */ new Map();
|
|
8072
|
+
for (const t of all) {
|
|
8073
|
+
const f = t.path.match(PALETTE2)?.[1];
|
|
8074
|
+
if (f) steps.set(f, (steps.get(f) ?? 0) + 1);
|
|
8075
|
+
}
|
|
8076
|
+
for (const t of all) {
|
|
8077
|
+
const f = t.path.match(PALETTE2)?.[1];
|
|
8078
|
+
if (f && (usedFamilies.has(f) || (steps.get(f) ?? 0) >= 3)) t.referenced = true;
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
const hasSemantic = all.some((t) => !t.referenced) && all.some((t) => t.referenced);
|
|
8082
|
+
const keep = opts.includePrimitives || !hasSemantic ? all : all.filter((t) => !t.referenced);
|
|
8083
|
+
const primitives = hasSemantic ? all.filter((t) => t.referenced).length : 0;
|
|
8084
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8085
|
+
const tokens = [];
|
|
8086
|
+
for (const { referenced: _r, ...t } of keep) {
|
|
8087
|
+
if (seen.has(t.path)) continue;
|
|
8088
|
+
seen.add(t.path);
|
|
8089
|
+
tokens.push(t);
|
|
8090
|
+
}
|
|
8091
|
+
tokens.sort((a, b) => a.path.localeCompare(b.path, "en", { numeric: true }));
|
|
8092
|
+
return { format, tokens: buildDtcg(tokens), count: tokens.length, primitives, notes };
|
|
8093
|
+
}
|
|
8094
|
+
function fromFigmaVariables(meta3, opts = {}) {
|
|
8095
|
+
const vars = meta3.variables ?? {};
|
|
8096
|
+
const cols = meta3.variableCollections ?? {};
|
|
8097
|
+
const notes = [];
|
|
8098
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
8099
|
+
const modeFor = (col, want, likeName) => {
|
|
8100
|
+
if (likeName) {
|
|
8101
|
+
const same = col.modes.find((m) => m.name.toLowerCase() === likeName.toLowerCase());
|
|
8102
|
+
if (same) return same.modeId;
|
|
8103
|
+
}
|
|
8104
|
+
if (want === "dark") return col.modes.find((m) => DARK.test(m.name))?.modeId;
|
|
8105
|
+
return col.defaultModeId ?? col.modes[0]?.modeId;
|
|
8106
|
+
};
|
|
8107
|
+
const resolve4 = (v, want, modeName, depth = 0) => {
|
|
8108
|
+
const col = cols[v.variableCollectionId];
|
|
8109
|
+
if (!col) return void 0;
|
|
8110
|
+
const modeId = modeFor(col, want, modeName) ?? col.defaultModeId;
|
|
8111
|
+
const raw = v.valuesByMode[modeId] ?? Object.values(v.valuesByMode)[0];
|
|
8112
|
+
if (!isAlias(raw)) return raw;
|
|
8113
|
+
referenced.add(raw.id);
|
|
8114
|
+
const target = vars[raw.id];
|
|
8115
|
+
if (!target || depth > 10) return void 0;
|
|
8116
|
+
const name = col.modes.find((m) => m.modeId === modeId)?.name;
|
|
8117
|
+
return resolve4(target, want, name, depth + 1);
|
|
8118
|
+
};
|
|
8119
|
+
for (const v of Object.values(vars)) for (const val of Object.values(v.valuesByMode)) if (isAlias(val)) referenced.add(val.id);
|
|
8120
|
+
for (const col of Object.values(cols)) {
|
|
8121
|
+
if (col.remote) continue;
|
|
8122
|
+
const dark = col.modes.find((m) => DARK.test(m.name));
|
|
8123
|
+
const others = col.modes.filter((m) => m.modeId !== col.defaultModeId && m !== dark);
|
|
8124
|
+
if (others.length) {
|
|
8125
|
+
const def = col.modes.find((m) => m.modeId === col.defaultModeId)?.name;
|
|
8126
|
+
notes.push(`\u201C${col.name}\u201D has modes ${col.modes.map((m) => m.name).join(", ")}; used \u201C${def}\u201D${dark ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
const out = [];
|
|
8130
|
+
let hidden = 0;
|
|
8131
|
+
for (const v of Object.values(vars)) {
|
|
8132
|
+
const col = cols[v.variableCollectionId];
|
|
8133
|
+
if (!col || v.remote || col.remote) continue;
|
|
8134
|
+
if (v.hiddenFromPublishing) {
|
|
8135
|
+
hidden++;
|
|
8136
|
+
continue;
|
|
8137
|
+
}
|
|
8138
|
+
const kind = v.resolvedType === "COLOR" ? "color" : v.resolvedType === "FLOAT" ? "number" : v.resolvedType === "STRING" ? "string" : "boolean";
|
|
8139
|
+
const value = resolve4(v, "default");
|
|
8140
|
+
if (value === void 0) continue;
|
|
8141
|
+
const c = classify(v.name, kind, value, { scopes: v.scopes?.filter((s) => s !== "ALL_SCOPES"), context: col.name });
|
|
8142
|
+
if (!c) continue;
|
|
8143
|
+
let darkValue;
|
|
8144
|
+
if (c.type === "color") {
|
|
8145
|
+
const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
|
|
8146
|
+
if (hasDarkSomewhere) {
|
|
8147
|
+
const d = colorValue(resolve4(v, "dark"));
|
|
8148
|
+
if (d && d !== c.value) darkValue = d;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
out.push({ ...c, ...darkValue ? { dark: darkValue } : {}, ...v.description ? { description: v.description } : {}, referenced: referenced.has(v.id) });
|
|
8152
|
+
}
|
|
8153
|
+
if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
|
|
8154
|
+
return finish("figma-variables", out, notes, opts);
|
|
8155
|
+
}
|
|
8156
|
+
function fromFigmaStyles(styles, opts = {}) {
|
|
8157
|
+
const out = [];
|
|
8158
|
+
const families = /* @__PURE__ */ new Map();
|
|
8159
|
+
let skipped = 0;
|
|
8160
|
+
for (const s of styles) {
|
|
8161
|
+
if (s.styleType === "FILL") {
|
|
8162
|
+
const fill = s.node?.fills?.find((f) => f.type === "SOLID" && f.visible !== false && f.color);
|
|
8163
|
+
if (!fill?.color) {
|
|
8164
|
+
skipped++;
|
|
8165
|
+
continue;
|
|
8166
|
+
}
|
|
8167
|
+
const c = classify(s.name, "color", { ...fill.color, a: (fill.color.a ?? 1) * (fill.opacity ?? 1) });
|
|
8168
|
+
if (c) out.push({ ...c, ...s.description ? { description: s.description } : {} });
|
|
8169
|
+
} else if (s.styleType === "TEXT" && s.node?.style) {
|
|
8170
|
+
const st = s.node.style;
|
|
8171
|
+
const name = segs(s.name).join("-");
|
|
8172
|
+
if (st.fontSize) out.push({ path: `font.size.${name}`, type: "dimension", value: dim(st.fontSize) });
|
|
8173
|
+
if (st.lineHeightPx) out.push({ path: `font.line-height.${name}`, type: "dimension", value: dim(Math.round(st.lineHeightPx)) });
|
|
8174
|
+
if (st.fontWeight) out.push({ path: `font.weight.${name}`, type: "fontWeight", value: String(st.fontWeight) });
|
|
8175
|
+
if (st.fontFamily) families.set(st.fontFamily, (families.get(st.fontFamily) ?? 0) + 1);
|
|
8176
|
+
}
|
|
8177
|
+
}
|
|
8178
|
+
const fams = [...families.entries()].sort((a, b) => b[1] - a[1]);
|
|
8179
|
+
fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
|
|
8180
|
+
const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
|
|
8181
|
+
return finish("figma-styles", out, notes, opts);
|
|
8182
|
+
}
|
|
8183
|
+
function plainLeaves(node2, prefix, out) {
|
|
8184
|
+
if (node2 === null || node2 === void 0) return;
|
|
8185
|
+
if (typeof node2 !== "object" || typeof node2 === "object" && "r" in node2 && "g" in node2) {
|
|
8186
|
+
out.push({ key: prefix, value: node2 });
|
|
8187
|
+
return;
|
|
8188
|
+
}
|
|
8189
|
+
for (const [k, v] of Object.entries(node2)) plainLeaves(v, [...prefix, k], out);
|
|
8190
|
+
}
|
|
8191
|
+
function fromModeObject(obj, opts = {}) {
|
|
8192
|
+
const notes = [];
|
|
8193
|
+
const modes = Object.entries(obj).filter(([, v]) => v && typeof v === "object");
|
|
8194
|
+
const leafSets = modes.map(([name, v]) => {
|
|
8195
|
+
const l = [];
|
|
8196
|
+
plainLeaves(v, [], l);
|
|
8197
|
+
return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
|
|
8198
|
+
});
|
|
8199
|
+
const groups = [];
|
|
8200
|
+
for (const m of leafSets) {
|
|
8201
|
+
const g = groups.find((grp) => {
|
|
8202
|
+
const a = grp[0].keys;
|
|
8203
|
+
let same = 0;
|
|
8204
|
+
for (const k of m.keys) if (a.has(k)) same++;
|
|
8205
|
+
return same / Math.max(a.size, m.keys.size) >= 0.8;
|
|
8206
|
+
});
|
|
8207
|
+
if (g) g.push(m);
|
|
8208
|
+
else groups.push([m]);
|
|
8209
|
+
}
|
|
8210
|
+
const byLeaf = /* @__PURE__ */ new Map();
|
|
8211
|
+
groups.forEach((grp, gi) => {
|
|
8212
|
+
for (const m of grp) for (const l of m.leaves) {
|
|
8213
|
+
const k = l.key[l.key.length - 1];
|
|
8214
|
+
const e = byLeaf.get(k) ?? { group: gi, values: /* @__PURE__ */ new Map() };
|
|
8215
|
+
e.values.set(m.name, l.value);
|
|
8216
|
+
byLeaf.set(k, e);
|
|
8217
|
+
}
|
|
8218
|
+
});
|
|
8219
|
+
const pick2 = (grp, want) => want === "dark" ? grp.find((m) => DARK.test(m.name)) : grp.find((m) => LIGHT.test(m.name) && !DARK.test(m.name)) ?? grp.find((m) => !DARK.test(m.name)) ?? grp[0];
|
|
8220
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
8221
|
+
for (const m of leafSets) for (const l of m.leaves) if (typeof l.value === "string" && byLeaf.has(l.value)) referenced.add(l.value);
|
|
8222
|
+
const deref = (v, want, depth = 0) => {
|
|
8223
|
+
if (typeof v !== "string" || depth > 10) return v;
|
|
8224
|
+
const ref = byLeaf.get(v) ?? byLeaf.get(v.replace(REF, "$1"));
|
|
8225
|
+
if (!ref) return v;
|
|
8226
|
+
referenced.add(v);
|
|
8227
|
+
const grp = groups[ref.group];
|
|
8228
|
+
const mode = pick2(grp, want) ?? pick2(grp, "default");
|
|
8229
|
+
return deref(ref.values.get(mode.name) ?? [...ref.values.values()][0], want, depth + 1);
|
|
8230
|
+
};
|
|
8231
|
+
const out = [];
|
|
8232
|
+
const pending = [];
|
|
8233
|
+
for (const grp of groups) {
|
|
8234
|
+
const def = pick2(grp, "default");
|
|
8235
|
+
const dark = pick2(grp, "dark");
|
|
8236
|
+
if (grp.length > 1) notes.push(`${grp.map((m) => m.name).join(" / ")} look like modes of one collection; used \u201C${def.name}\u201D${dark && dark !== def ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
|
|
8237
|
+
for (const l of def.leaves) {
|
|
8238
|
+
const key = l.key[l.key.length - 1];
|
|
8239
|
+
const container = l.key.length > 1 ? l.key[l.key.length - 2].toLowerCase() : "";
|
|
8240
|
+
const value = deref(l.value, "default");
|
|
8241
|
+
const kind = container === "color" || colorValue(value) && /^#|^rgb|^hsl/i.test(String(value)) ? "color" : container === "string" || typeof value === "string" && !/^-?[\d.]+(px|rem|%)?$/.test(value) ? "string" : "number";
|
|
8242
|
+
const c = classify(key, kind, kind === "number" && typeof value === "string" && /px$/.test(value) ? parseFloat(value) : value, { context: def.name });
|
|
8243
|
+
if (!c) continue;
|
|
8244
|
+
let d;
|
|
8245
|
+
if (c.type === "color") {
|
|
8246
|
+
const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
|
|
8247
|
+
const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
|
|
8248
|
+
if (dv && dv !== c.value) d = dv;
|
|
8249
|
+
}
|
|
8250
|
+
pending.push({ key, t: { ...c, ...d ? { dark: d } : {} } });
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
for (const p of pending) out.push({ ...p.t, referenced: referenced.has(p.key) });
|
|
8254
|
+
return finish("mode-object", out, notes, opts);
|
|
8255
|
+
}
|
|
8256
|
+
var figmaExt = (node2, key) => {
|
|
8257
|
+
const ext = node2.$extensions;
|
|
8258
|
+
if (!ext) return void 0;
|
|
8259
|
+
if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
|
|
8260
|
+
const nested = ext["com.figma"];
|
|
8261
|
+
return nested?.[key];
|
|
8262
|
+
};
|
|
8263
|
+
function fileLeaves(node2, prefix, out, inherited) {
|
|
8264
|
+
if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
|
|
8265
|
+
const o = node2;
|
|
8266
|
+
const groupType = o.$type ?? inherited;
|
|
8267
|
+
if ("$value" in o) {
|
|
8268
|
+
const alias = figmaExt(o, "aliasData");
|
|
8269
|
+
const scopes = figmaExt(o, "scopes");
|
|
8270
|
+
out.push({
|
|
8271
|
+
path: prefix,
|
|
8272
|
+
type: o.$type ?? groupType,
|
|
8273
|
+
value: o.$value,
|
|
8274
|
+
description: o.$description,
|
|
8275
|
+
scopes: Array.isArray(scopes) ? scopes : void 0,
|
|
8276
|
+
aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
|
|
8277
|
+
hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
|
|
8278
|
+
});
|
|
8279
|
+
return;
|
|
8280
|
+
}
|
|
8281
|
+
for (const [k, v] of Object.entries(o)) {
|
|
8282
|
+
if (k.startsWith("$")) continue;
|
|
8283
|
+
fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
var hasFigmaExt = (node2, depth = 0) => {
|
|
8287
|
+
if (!node2 || typeof node2 !== "object" || depth > 12) return false;
|
|
8288
|
+
const ext = node2.$extensions;
|
|
8289
|
+
if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
|
|
8290
|
+
return Object.values(node2).some((v) => hasFigmaExt(v, depth + 1));
|
|
8291
|
+
};
|
|
8292
|
+
function modeOf(file2) {
|
|
8293
|
+
const root = file2.data ?? {};
|
|
8294
|
+
const parts = file2.name.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
8295
|
+
const base = (parts.pop() ?? "tokens").replace(/(\.tokens)?\.json$/i, "");
|
|
8296
|
+
const named = figmaExt(root, "modeName");
|
|
8297
|
+
const split = base.match(/^(.+?)[\s._-]+(light|dark|night|day|default|mode[\s_-]*\d+)$/i);
|
|
8298
|
+
return {
|
|
8299
|
+
collection: parts.join("/") || (split ? split[1] : ""),
|
|
8300
|
+
mode: typeof named === "string" ? named : split ? split[2] : base
|
|
8301
|
+
};
|
|
8302
|
+
}
|
|
8303
|
+
function importTokenFiles(files, opts = {}) {
|
|
8304
|
+
const parsed = files.filter((f) => f.data && typeof f.data === "object");
|
|
8305
|
+
if (!parsed.length) throw new Error("No JSON token files found.");
|
|
8306
|
+
const figma = parsed.some((f) => hasFigmaExt(f.data));
|
|
8307
|
+
if (parsed.length === 1 && !figma) return importTokens(parsed[0].data, opts);
|
|
8308
|
+
const notes = [];
|
|
8309
|
+
const withMode = parsed.map((f) => ({ ...f, ...modeOf(f) }));
|
|
8310
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
8311
|
+
for (const f of withMode) byCollection.set(f.collection, [...byCollection.get(f.collection) ?? [], f]);
|
|
8312
|
+
const light = /* @__PURE__ */ new Map();
|
|
8313
|
+
const darkOverrides = /* @__PURE__ */ new Map();
|
|
8314
|
+
for (const [collection, list] of byCollection) {
|
|
8315
|
+
const dark = list.find((f) => DARK.test(f.mode));
|
|
8316
|
+
const def = list.find((f) => LIGHT.test(f.mode) && !DARK.test(f.mode)) ?? list.find((f) => !DARK.test(f.mode)) ?? list[0];
|
|
8317
|
+
if (list.length > 1) {
|
|
8318
|
+
const label = collection ? `\u201C${collection}\u201D` : "The files";
|
|
8319
|
+
notes.push(`${label}: modes ${list.map((f) => f.mode).join(", ")}; used \u201C${def.mode}\u201D${dark && dark !== def ? ` (and \u201C${dark.mode}\u201D as dark)` : ""}.`);
|
|
8320
|
+
}
|
|
8321
|
+
const read2 = (f, into) => {
|
|
8322
|
+
const leaves3 = [];
|
|
8323
|
+
fileLeaves(f.data, "", leaves3);
|
|
8324
|
+
for (const l of leaves3) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
|
|
8325
|
+
};
|
|
8326
|
+
read2(def, light);
|
|
8327
|
+
if (dark && dark !== def) read2(dark, darkOverrides);
|
|
8328
|
+
}
|
|
8329
|
+
const lightRaws = resolveRefs([...light.values()]);
|
|
8330
|
+
const darkRaws = resolveRefs([...light.values()].map((r) => darkOverrides.get(r.path) ?? r));
|
|
8331
|
+
const darkByPath = new Map(darkRaws.map((r) => [r.path, r.value]));
|
|
8332
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
8333
|
+
for (const r of [...light.values(), ...darkOverrides.values()]) {
|
|
8334
|
+
if (r.leaf.aliasTarget) referenced.add(r.leaf.aliasTarget);
|
|
8335
|
+
const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
|
|
8336
|
+
if (m) referenced.add(m[1]);
|
|
8337
|
+
}
|
|
8338
|
+
const out = [];
|
|
8339
|
+
let hidden = 0;
|
|
8340
|
+
for (const r of lightRaws) {
|
|
8341
|
+
const leaf = light.get(r.path).leaf;
|
|
8342
|
+
if (leaf.hidden) {
|
|
8343
|
+
hidden++;
|
|
8344
|
+
continue;
|
|
8345
|
+
}
|
|
8346
|
+
const t = (r.type ?? "").toLowerCase();
|
|
8347
|
+
const kind = t === "color" ? "color" : t === "boolean" ? "boolean" : t === "string" || t === "fontfamily" || t === "fontweight" ? "string" : typeof r.value === "string" && !/^-?[\d.]+(px|rem)?$/.test(r.value) && !colorValue(r.value) ? "string" : colorValue(r.value) && t !== "number" && t !== "dimension" ? "color" : "number";
|
|
8348
|
+
let tok;
|
|
8349
|
+
if (figma) {
|
|
8350
|
+
const value = kind === "number" && r.value && typeof r.value === "object" ? parseFloat(dim(r.value)) : r.value;
|
|
8351
|
+
tok = classify(r.path.replace(/\./g, "/"), kind, value, { scopes: leaf.scopes?.filter((x2) => x2 !== "ALL_SCOPES") });
|
|
8352
|
+
} else {
|
|
8353
|
+
const flat = flattenTokens({ x: { $type: r.type, $value: r.value } })[0];
|
|
8354
|
+
tok = flat ? { path: flat.type === "color" && !r.path.startsWith("color.") ? `color.${r.path}` : r.path, type: flat.type, value: flat.value } : null;
|
|
8355
|
+
}
|
|
8356
|
+
if (!tok) continue;
|
|
8357
|
+
let darkValue;
|
|
8358
|
+
if (tok.type === "color" && darkOverrides.size) {
|
|
8359
|
+
const d = colorValue(darkByPath.get(r.path));
|
|
8360
|
+
if (d && d !== tok.value) darkValue = d;
|
|
8361
|
+
}
|
|
8362
|
+
out.push({ ...tok, ...darkValue ? { dark: darkValue } : {}, ...r.description ? { description: r.description } : {}, referenced: referenced.has(r.path) });
|
|
8363
|
+
}
|
|
8364
|
+
if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
|
|
8365
|
+
return finish(figma ? "figma-export" : "dtcg", out, notes, opts);
|
|
8366
|
+
}
|
|
8367
|
+
function importTokens(input2, opts = {}) {
|
|
8368
|
+
if (!input2 || typeof input2 !== "object") throw new Error("Expected a JSON object.");
|
|
8369
|
+
const o = input2;
|
|
8370
|
+
if (Array.isArray(o.$files)) return importTokenFiles(o.$files, opts);
|
|
8371
|
+
const meta3 = o.meta ?? o;
|
|
8372
|
+
if (meta3.variables && meta3.variableCollections) return fromFigmaVariables(meta3, opts);
|
|
8373
|
+
if (Array.isArray(o.styles) && o.styles.every((s) => s && "styleType" in s)) return fromFigmaStyles(o.styles, opts);
|
|
8374
|
+
if (hasDollarValue(o) && hasFigmaExt(o)) return importTokenFiles([{ name: "tokens.json", data: o }], opts);
|
|
8375
|
+
if (hasDollarValue(o) || hasStudioValue(o)) {
|
|
8376
|
+
const studio = !hasDollarValue(o);
|
|
8377
|
+
const flat = flattenTokens(o);
|
|
8378
|
+
if (!flat.length) throw new Error("No tokens found in that file.");
|
|
8379
|
+
const out = flat.map((t) => {
|
|
8380
|
+
if (t.type === "color" && !t.path.startsWith("color.")) return { ...t, path: `color.${t.path}` };
|
|
8381
|
+
return t;
|
|
8382
|
+
});
|
|
8383
|
+
const notes = studio ? [] : [];
|
|
8384
|
+
const res = finish(studio ? "tokens-studio" : "dtcg", out, notes, opts);
|
|
8385
|
+
return res;
|
|
8386
|
+
}
|
|
8387
|
+
const leaves3 = [];
|
|
8388
|
+
plainLeaves(o, [], leaves3);
|
|
8389
|
+
if (leaves3.length) return fromModeObject(o, opts);
|
|
8390
|
+
throw new Error("Couldn't find any tokens. Supported: DTCG, Tokens Studio, Figma variables JSON.");
|
|
8391
|
+
}
|
|
8392
|
+
function diffTokens(before, after) {
|
|
8393
|
+
const a = new Map(flattenTokens(before).map((t) => [t.path, t]));
|
|
8394
|
+
const b = new Map(flattenTokens(after).map((t) => [t.path, t]));
|
|
8395
|
+
const diff = { added: [], removed: [], changed: [], unchanged: 0 };
|
|
8396
|
+
for (const [p, t] of b) {
|
|
8397
|
+
const old = a.get(p);
|
|
8398
|
+
if (!old) diff.added.push(t);
|
|
8399
|
+
else if (old.value.toLowerCase() !== t.value.toLowerCase() || (old.dark ?? "").toLowerCase() !== (t.dark ?? "").toLowerCase()) diff.changed.push({ path: p, before: old, after: t });
|
|
8400
|
+
else diff.unchanged++;
|
|
8401
|
+
}
|
|
8402
|
+
for (const [p, t] of a) if (!b.has(p)) diff.removed.push(t);
|
|
8403
|
+
return diff;
|
|
8404
|
+
}
|
|
8405
|
+
function mergeTokens(base, incoming) {
|
|
8406
|
+
const m = new Map(flattenTokens(base).map((t) => [t.path, t]));
|
|
8407
|
+
for (const t of flattenTokens(incoming)) m.set(t.path, { ...m.get(t.path), ...t, description: t.description ?? m.get(t.path)?.description });
|
|
8408
|
+
return buildDtcg([...m.values()]);
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
// ../engine/src/governance.ts
|
|
8412
|
+
var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8413
|
+
var DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
8414
|
+
function tokenDeprecations(doc) {
|
|
8415
|
+
const out = [];
|
|
8416
|
+
const walk2 = (n, path) => {
|
|
8417
|
+
if (!isObj(n)) return;
|
|
8418
|
+
if ("$value" in n) {
|
|
8419
|
+
const d = n.$deprecated;
|
|
8420
|
+
if (d === void 0 || d === false) return;
|
|
8421
|
+
const ext = isObj(n.$extensions) && isObj(n.$extensions[EXT]) ? n.$extensions[EXT] : {};
|
|
8422
|
+
out.push({
|
|
8423
|
+
path,
|
|
8424
|
+
...typeof ext.replacement === "string" && ext.replacement ? { replacement: ext.replacement } : {},
|
|
8425
|
+
...typeof ext.removeAfter === "string" && DATE.test(ext.removeAfter) ? { removeAfter: ext.removeAfter } : {},
|
|
8426
|
+
...typeof d === "string" && d.trim() ? { reason: d.trim() } : {}
|
|
8427
|
+
});
|
|
8428
|
+
return;
|
|
8429
|
+
}
|
|
8430
|
+
for (const [k, v] of Object.entries(n)) if (!k.startsWith("$")) walk2(v, path ? `${path}.${k}` : k);
|
|
8431
|
+
};
|
|
8432
|
+
walk2(doc, "");
|
|
8433
|
+
return out;
|
|
8434
|
+
}
|
|
8435
|
+
var describe = (d, cssVars) => {
|
|
8436
|
+
const v = (p) => cssVars && p.includes(".") ? `\`var(--${p.replace(/\./g, "-")})\`` : `\`${p}\``;
|
|
8437
|
+
return `${v(d.path)}${d.replacement ? ` \u2192 use ${v(d.replacement)}` : ""}${d.removeAfter ? ` (removed after ${d.removeAfter})` : ""}${d.reason ? `. ${d.reason}` : ""}`;
|
|
8438
|
+
};
|
|
8439
|
+
var deprecationLine = describe;
|
|
8440
|
+
|
|
7813
8441
|
// ../engine/src/config.ts
|
|
7814
8442
|
var DEFAULT_IGNORE = [
|
|
7815
8443
|
"**/node_modules/**",
|
|
@@ -7937,6 +8565,7 @@ function resolveConfig(config2) {
|
|
|
7937
8565
|
const components = config2.components ?? [];
|
|
7938
8566
|
const replaces = /* @__PURE__ */ new Map();
|
|
7939
8567
|
for (const c of components) {
|
|
8568
|
+
if (c.status === "deprecated") continue;
|
|
7940
8569
|
const tags = c.replaces ?? NAME_TO_TAGS[c.name.toLowerCase()] ?? [];
|
|
7941
8570
|
for (const t of tags) {
|
|
7942
8571
|
const key = t.toLowerCase().replace(/\s+/g, "").replace(/["']/g, "");
|
|
@@ -7952,6 +8581,7 @@ function resolveConfig(config2) {
|
|
|
7952
8581
|
fontVars: fontVars.length ? fontVars : ["--font-family-sans"],
|
|
7953
8582
|
fontSizes: uniqSorted(all.filter((l) => /^font\.size\./.test(l.path)).map((l) => px(l.value)).filter((v) => v !== null)),
|
|
7954
8583
|
components,
|
|
8584
|
+
deprecatedTokens: new Map(tokenDeprecations(config2.tokens).map((d) => [`--${d.path.replace(/\./g, "-")}`, d])),
|
|
7955
8585
|
replaces,
|
|
7956
8586
|
rules: config2.rules ?? {},
|
|
7957
8587
|
ignore: [...DEFAULT_IGNORE, ...config2.ignore ?? []],
|
|
@@ -8677,1295 +9307,766 @@ var radiusScale = {
|
|
|
8677
9307
|
}
|
|
8678
9308
|
for (const m of matches(/(?<![\w-])rounded(?:-[a-z]{1,2})?-\[(\d*\.?\d+)px\]/g, code)) {
|
|
8679
9309
|
const n = parseFloat(m[1]);
|
|
8680
|
-
if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), n)}` });
|
|
8681
|
-
}
|
|
8682
|
-
for (const b of styleBindings(code)) {
|
|
8683
|
-
if (!RADIUS_PROP.test(b.prop)) continue;
|
|
8684
|
-
for (const v of pxValues(b.value)) {
|
|
8685
|
-
if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), v.n)}` });
|
|
8686
|
-
}
|
|
8687
|
-
}
|
|
8688
|
-
}
|
|
8689
|
-
return out;
|
|
8690
|
-
}
|
|
8691
|
-
};
|
|
8692
|
-
var fontFamily = {
|
|
8693
|
-
id: "font-family",
|
|
8694
|
-
title: "Unapproved font",
|
|
8695
|
-
description: "Only the Brand Kit's font families may be used.",
|
|
8696
|
-
defaultSeverity: "warning",
|
|
8697
|
-
kinds: ALL,
|
|
8698
|
-
check({ code, kind, config: config2 }) {
|
|
8699
|
-
if (config2.fonts.length === 0) return [];
|
|
8700
|
-
const out = [];
|
|
8701
|
-
const allowed = config2.fonts;
|
|
8702
|
-
const test = (value, offset) => {
|
|
8703
|
-
const fam = normalizeFontFamily(value);
|
|
8704
|
-
if (!fam || isGenericFont(fam) || allowed.includes(fam)) return;
|
|
8705
|
-
out.push({
|
|
8706
|
-
offset,
|
|
8707
|
-
message: `Font "${fam}" is not in the Brand Kit`,
|
|
8708
|
-
suggestion: `Use var(--font-family-sans) (${allowed[0]})`
|
|
8709
|
-
});
|
|
8710
|
-
};
|
|
8711
|
-
for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
|
|
8712
|
-
if (kind !== "css") {
|
|
8713
|
-
for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
|
|
8714
|
-
for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
|
|
8715
|
-
for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
|
|
8716
|
-
}
|
|
8717
|
-
return out;
|
|
8718
|
-
}
|
|
8719
|
-
};
|
|
8720
|
-
var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "password", "number", "search", "tel", "url"]);
|
|
8721
|
-
var preferOfficialComponent = {
|
|
8722
|
-
id: "prefer-official-component",
|
|
8723
|
-
title: "Re-invented component",
|
|
8724
|
-
description: "Use the design system component instead of a raw HTML element.",
|
|
8725
|
-
defaultSeverity: "error",
|
|
8726
|
-
kinds: MARKUP,
|
|
8727
|
-
check({ code, config: config2, framework }) {
|
|
8728
|
-
if (config2.replaces.size === 0) return [];
|
|
8729
|
-
const out = [];
|
|
8730
|
-
const baseTags = [...new Set([...config2.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
|
|
8731
|
-
for (const m of matches(new RegExp(`<(${baseTags.join("|")})(?=[\\s>/])([^>]*)>`, "g"), code)) {
|
|
8732
|
-
const tag = m[1];
|
|
8733
|
-
const attrs = m[2];
|
|
8734
|
-
let comp = config2.replaces.get(tag);
|
|
8735
|
-
if (tag === "input" || tag === "button") {
|
|
8736
|
-
const type = (attrs.match(/\btype\s*=\s*\{?\s*["'`]([\w-]+)["'`]/)?.[1] ?? "").toLowerCase();
|
|
8737
|
-
const specific = config2.replaces.get(`${tag}[type=${type}]`);
|
|
8738
|
-
if (specific) comp = specific;
|
|
8739
|
-
else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config2.replaces.get("button") : void 0;
|
|
8740
|
-
}
|
|
8741
|
-
if (!comp) continue;
|
|
8742
|
-
const usage = resolveUsage(comp, framework);
|
|
8743
|
-
if (!usage) continue;
|
|
8744
|
-
if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
|
|
8745
|
-
if (framework !== "react" && implementsTag(code, usage.tag)) continue;
|
|
8746
|
-
out.push({
|
|
8747
|
-
offset: m.index,
|
|
8748
|
-
message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
|
|
8749
|
-
suggestion: useSuggestion(comp, usage)
|
|
8750
|
-
});
|
|
8751
|
-
}
|
|
8752
|
-
return out;
|
|
8753
|
-
}
|
|
8754
|
-
};
|
|
8755
|
-
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8756
|
-
var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
|
|
8757
|
-
function useSuggestion(comp, u) {
|
|
8758
|
-
if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
|
|
8759
|
-
const parts = [`Use <${u.tag}>`];
|
|
8760
|
-
if (u.importLine) parts.push(`(${u.importLine})`);
|
|
8761
|
-
return parts.join(" ");
|
|
8762
|
-
}
|
|
8763
|
-
var forbiddenImport = {
|
|
8764
|
-
id: "forbidden-import",
|
|
8765
|
-
title: "Other UI library",
|
|
8766
|
-
description: "Only the design system's component library may be imported.",
|
|
8767
|
-
defaultSeverity: "error",
|
|
8768
|
-
kinds: ALL,
|
|
8769
|
-
check({ code, config: config2 }) {
|
|
8770
|
-
const out = [];
|
|
8771
|
-
const lib = config2.components[0]?.importPath;
|
|
8772
|
-
const re = /(?:\bimport\s[^'";]*?from\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\(\s*|@import\s+(?:url\(\s*)?)(['"])([^'"]+)\1/g;
|
|
8773
|
-
for (const m of matches(re, code)) {
|
|
8774
|
-
const spec = m[2];
|
|
8775
|
-
const bad = config2.forbiddenImports.find((p) => spec === p || spec.startsWith(`${p}/`));
|
|
8776
|
-
if (!bad) continue;
|
|
8777
|
-
out.push({
|
|
8778
|
-
offset: m.index + m[0].indexOf(spec),
|
|
8779
|
-
message: `Imports ${bad}, which isn't part of the design system`,
|
|
8780
|
-
suggestion: lib ? `Use components from "${lib}"` : "Use the design system's components"
|
|
8781
|
-
});
|
|
8782
|
-
}
|
|
8783
|
-
return out;
|
|
8784
|
-
}
|
|
8785
|
-
};
|
|
8786
|
-
var imgAlt = {
|
|
8787
|
-
id: "img-alt",
|
|
8788
|
-
title: "Image without alt text",
|
|
8789
|
-
description: "Every <img> needs an alt attribute (empty for decorative images).",
|
|
8790
|
-
defaultSeverity: "warning",
|
|
8791
|
-
kinds: MARKUP,
|
|
8792
|
-
check({ code }) {
|
|
8793
|
-
const out = [];
|
|
8794
|
-
for (const m of matches(/<img(?=[\s/>])([^>]*)>/g, code)) {
|
|
8795
|
-
if (/(?<![\w-])alt\s*=/.test(m[1]) || /\{\s*\.\.\./.test(m[1])) continue;
|
|
8796
|
-
out.push({ offset: m.index, message: "<img> has no alt text", suggestion: 'Add alt="\u2026" describing the image, or alt="" if decorative' });
|
|
8797
|
-
}
|
|
8798
|
-
return out;
|
|
8799
|
-
}
|
|
8800
|
-
};
|
|
8801
|
-
var NATIVE_ATTRS = new Set(
|
|
8802
|
-
"id className style title role tabIndex hidden lang dir name value defaultValue defaultChecked checked disabled required readOnly autoFocus autoComplete placeholder type min max step maxLength minLength pattern form accept multiple href target rel download children".split(" ")
|
|
8803
|
-
);
|
|
8804
|
-
var ALWAYS_OK = /* @__PURE__ */ new Set(["key", "ref"]);
|
|
8805
|
-
function importedComponents(code, components) {
|
|
8806
|
-
const out = /* @__PURE__ */ new Map();
|
|
8807
|
-
for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
|
|
8808
|
-
const from = m[3];
|
|
8809
|
-
for (const part of m[1].split(",")) {
|
|
8810
|
-
const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
|
|
8811
|
-
if (!orig) continue;
|
|
8812
|
-
const comp = components.find((c) => c.name === orig && c.importPath === from && c.props?.length);
|
|
8813
|
-
if (comp) out.set(alias || orig, comp);
|
|
8814
|
-
}
|
|
8815
|
-
}
|
|
8816
|
-
return out;
|
|
8817
|
-
}
|
|
8818
|
-
function skipBalanced(code, i2) {
|
|
8819
|
-
const open2 = code[i2];
|
|
8820
|
-
if (open2 === '"' || open2 === "'") {
|
|
8821
|
-
const end = code.indexOf(open2, i2 + 1);
|
|
8822
|
-
return end === -1 ? code.length : end + 1;
|
|
8823
|
-
}
|
|
8824
|
-
let depth = 0;
|
|
8825
|
-
let quote = null;
|
|
8826
|
-
for (let j = i2; j < code.length; j++) {
|
|
8827
|
-
const c = code[j];
|
|
8828
|
-
if (quote) {
|
|
8829
|
-
if (c === "\\") j++;
|
|
8830
|
-
else if (c === quote) quote = null;
|
|
8831
|
-
continue;
|
|
8832
|
-
}
|
|
8833
|
-
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
8834
|
-
else if (c === "{") depth++;
|
|
8835
|
-
else if (c === "}" && --depth === 0) return j + 1;
|
|
8836
|
-
}
|
|
8837
|
-
return code.length;
|
|
8838
|
-
}
|
|
8839
|
-
function parseAttributes(code, start) {
|
|
8840
|
-
const attrs = [];
|
|
8841
|
-
let spread = false;
|
|
8842
|
-
let i2 = start;
|
|
8843
|
-
while (i2 < code.length) {
|
|
8844
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8845
|
-
const c = code[i2];
|
|
8846
|
-
if (c === void 0) return null;
|
|
8847
|
-
if (c === ">") return { attrs, spread, selfClosing: false };
|
|
8848
|
-
if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
|
|
8849
|
-
if (c === "{") {
|
|
8850
|
-
spread = true;
|
|
8851
|
-
i2 = skipBalanced(code, i2);
|
|
8852
|
-
continue;
|
|
8853
|
-
}
|
|
8854
|
-
const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
|
|
8855
|
-
if (!name) return null;
|
|
8856
|
-
attrs.push({ name, offset: i2 });
|
|
8857
|
-
i2 += name.length;
|
|
8858
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8859
|
-
if (code[i2] === "=") {
|
|
8860
|
-
i2++;
|
|
8861
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8862
|
-
i2 = skipBalanced(code, i2);
|
|
8863
|
-
}
|
|
8864
|
-
}
|
|
8865
|
-
return null;
|
|
8866
|
-
}
|
|
8867
|
-
function parseHtmlAttributes(code, start) {
|
|
8868
|
-
const attrs = [];
|
|
8869
|
-
let i2 = start;
|
|
8870
|
-
while (i2 < code.length) {
|
|
8871
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8872
|
-
const c = code[i2];
|
|
8873
|
-
if (c === void 0) return null;
|
|
8874
|
-
if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
|
|
8875
|
-
if (c === "{" || c === "<" && code[i2 + 1] === "?") {
|
|
8876
|
-
const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
|
|
8877
|
-
const e = code.indexOf(close, i2 + 2);
|
|
8878
|
-
i2 = e === -1 ? code.length : e + close.length;
|
|
8879
|
-
continue;
|
|
8880
|
-
}
|
|
8881
|
-
const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
|
|
8882
|
-
if (!name) return null;
|
|
8883
|
-
attrs.push({ name, offset: i2 });
|
|
8884
|
-
i2 += name.length;
|
|
8885
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8886
|
-
if (code[i2] === "=") {
|
|
8887
|
-
i2++;
|
|
8888
|
-
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
8889
|
-
const q = code[i2];
|
|
8890
|
-
if (q === '"' || q === "'") {
|
|
8891
|
-
const e = code.indexOf(q, i2 + 1);
|
|
8892
|
-
i2 = e === -1 ? code.length : e + 1;
|
|
8893
|
-
} else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
|
|
8894
|
-
}
|
|
8895
|
-
}
|
|
8896
|
-
return null;
|
|
8897
|
-
}
|
|
8898
|
-
var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
|
|
8899
|
-
function propName(raw) {
|
|
8900
|
-
let n = raw;
|
|
8901
|
-
if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
|
|
8902
|
-
n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
|
|
8903
|
-
if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
|
|
8904
|
-
n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
|
|
8905
|
-
if (HTML_GLOBAL.test(n.toLowerCase())) return null;
|
|
8906
|
-
return n;
|
|
8907
|
-
}
|
|
8908
|
-
var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
|
|
8909
|
-
var NATIVE_EVENT = /^(?:click|dblclick|auxclick|contextmenu|focus|blur|focusin|focusout|key(?:down|up|press)|mouse\w+|pointer\w+|touch\w+|wheel|drag\w*|drop|scroll|animation\w+|transition\w+)$/;
|
|
8910
|
-
function eventName(raw) {
|
|
8911
|
-
const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
|
|
8912
|
-
return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
|
|
8913
|
-
}
|
|
8914
|
-
function templateProps({ code, config: config2, framework }) {
|
|
8915
|
-
const tagged = [];
|
|
8916
|
-
for (const comp of config2.components) {
|
|
8917
|
-
const u = resolveUsage(comp, framework);
|
|
8918
|
-
if (u && (u.props?.length || u.events?.length)) tagged.push({ comp, tag: u.tag, props: u.props, events: u.events, ce: Boolean(u.customElement || u.via) });
|
|
8919
|
-
}
|
|
8920
|
-
if (!tagged.length) return [];
|
|
8921
|
-
const out = [];
|
|
8922
|
-
const forms = /* @__PURE__ */ new Map();
|
|
8923
|
-
for (const t of tagged) {
|
|
8924
|
-
forms.set(t.tag, t);
|
|
8925
|
-
if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
|
|
8926
|
-
}
|
|
8927
|
-
const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
|
|
8928
|
-
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
8929
|
-
const t = forms.get(m[1]);
|
|
8930
|
-
const parsed = parseHtmlAttributes(code, m.index + m[0].length);
|
|
8931
|
-
if (!parsed) continue;
|
|
8932
|
-
const list = (t.props ?? []).map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
8933
|
-
const passed = [];
|
|
8934
|
-
for (const a of parsed.attrs) {
|
|
8935
|
-
const raw = a.name;
|
|
8936
|
-
const ev = eventName(raw);
|
|
8937
|
-
if (ev) {
|
|
8938
|
-
if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
|
|
8939
|
-
const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
|
|
8940
|
-
out.push({
|
|
8941
|
-
offset: a.offset,
|
|
8942
|
-
message: `<${t.tag}> has no "${ev}" event`,
|
|
8943
|
-
suggestion: `${like ? `Listen to ${framework === "angular" ? `(${like.name})` : framework === "vue" ? `@${like.name}` : `"${like.name}"`}${like.detail ? ` and read ${framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : "event.detail"} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
|
|
8944
|
-
});
|
|
8945
|
-
continue;
|
|
8946
|
-
}
|
|
8947
|
-
if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
|
|
8948
|
-
if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
|
|
8949
|
-
const n = propName(raw);
|
|
8950
|
-
if (!n || !t.props?.length) continue;
|
|
8951
|
-
passed.push(n);
|
|
8952
|
-
if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
|
|
8953
|
-
const camel2 = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8954
|
-
out.push({
|
|
8955
|
-
offset: a.offset,
|
|
8956
|
-
message: `${raw} binds a property that doesn't exist on <${t.tag}>`,
|
|
8957
|
-
suggestion: `Bind the camelCase property: ${framework === "vue" ? `:${camel2}` : `[${camel2}]`}="\u2026" (kebab-case is only for static attributes: ${n}="\u2026")`
|
|
8958
|
-
});
|
|
8959
|
-
continue;
|
|
8960
|
-
}
|
|
8961
|
-
if (t.props.some((p) => sameName(p.name, n))) continue;
|
|
8962
|
-
out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
|
|
8963
|
-
}
|
|
8964
|
-
for (const p of t.props ?? []) {
|
|
8965
|
-
if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
|
|
8966
|
-
if (p.name === "children" || p.name === "content") continue;
|
|
8967
|
-
out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
8968
|
-
}
|
|
8969
|
-
}
|
|
8970
|
-
return out;
|
|
8971
|
-
}
|
|
8972
|
-
var componentProps = {
|
|
8973
|
-
id: "component-props",
|
|
8974
|
-
title: "Unknown or missing prop",
|
|
8975
|
-
description: "Design system components only accept their documented props, and required props must be passed.",
|
|
8976
|
-
defaultSeverity: "error",
|
|
8977
|
-
kinds: MARKUP,
|
|
8978
|
-
check(ctx) {
|
|
8979
|
-
if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
|
|
8980
|
-
const { code, config: config2 } = ctx;
|
|
8981
|
-
if (!config2.components.some((c) => c.props?.length)) return [];
|
|
8982
|
-
const imported = importedComponents(code, config2.components);
|
|
8983
|
-
if (imported.size === 0) return [];
|
|
8984
|
-
const out = [];
|
|
8985
|
-
const names = [...imported.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|");
|
|
8986
|
-
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
8987
|
-
if (/[\w)\]]\s*$/.test(code.slice(Math.max(0, m.index - 20), m.index))) continue;
|
|
8988
|
-
const comp = imported.get(m[1]);
|
|
8989
|
-
const parsed = parseAttributes(code, m.index + m[0].length);
|
|
8990
|
-
if (!parsed) continue;
|
|
8991
|
-
const own2 = new Map(comp.props.map((p) => [p.name, p]));
|
|
8992
|
-
const list = comp.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
8993
|
-
for (const a of parsed.attrs) {
|
|
8994
|
-
if (own2.has(a.name) || ALWAYS_OK.has(a.name) || a.name.includes("-")) continue;
|
|
8995
|
-
if (comp.htmlAttributes && (NATIVE_ATTRS.has(a.name) || /^on[A-Z]/.test(a.name) || /^aria/.test(a.name))) continue;
|
|
8996
|
-
const sibling = config2.components.find(
|
|
8997
|
-
(c) => c !== comp && c.importPath === comp.importPath && (c.name.startsWith(comp.name) || comp.name.startsWith(c.name)) && c.props?.some((p) => p.name === a.name)
|
|
8998
|
-
);
|
|
8999
|
-
out.push({
|
|
9000
|
-
offset: a.offset,
|
|
9001
|
-
message: `<${comp.name}> has no "${a.name}" prop`,
|
|
9002
|
-
suggestion: sibling ? `Use <${sibling.name}>, which has "${a.name}", or remove it. ${comp.name} props: ${list}` : `Remove it. ${comp.name} props: ${list}`
|
|
9003
|
-
});
|
|
9004
|
-
}
|
|
9005
|
-
if (!parsed.spread) {
|
|
9006
|
-
const passed = new Set(parsed.attrs.map((a) => a.name));
|
|
9007
|
-
for (const p of comp.props) {
|
|
9008
|
-
if (!p.required || passed.has(p.name) || p.name === "children" && !parsed.selfClosing) continue;
|
|
9009
|
-
out.push({ offset: m.index, message: `<${comp.name}> is missing the required "${p.name}" prop`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
9310
|
+
if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), n)}` });
|
|
9311
|
+
}
|
|
9312
|
+
for (const b of styleBindings(code)) {
|
|
9313
|
+
if (!RADIUS_PROP.test(b.prop)) continue;
|
|
9314
|
+
for (const v of pxValues(b.value)) {
|
|
9315
|
+
if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), v.n)}` });
|
|
9010
9316
|
}
|
|
9011
9317
|
}
|
|
9012
9318
|
}
|
|
9013
9319
|
return out;
|
|
9014
9320
|
}
|
|
9015
9321
|
};
|
|
9016
|
-
var
|
|
9017
|
-
id: "
|
|
9018
|
-
title: "
|
|
9019
|
-
description: "
|
|
9322
|
+
var fontFamily = {
|
|
9323
|
+
id: "font-family",
|
|
9324
|
+
title: "Unapproved font",
|
|
9325
|
+
description: "Only the Brand Kit's font families may be used.",
|
|
9020
9326
|
defaultSeverity: "warning",
|
|
9021
|
-
kinds:
|
|
9022
|
-
check({ code, kind, config: config2
|
|
9327
|
+
kinds: ALL,
|
|
9328
|
+
check({ code, kind, config: config2 }) {
|
|
9329
|
+
if (config2.fonts.length === 0) return [];
|
|
9023
9330
|
const out = [];
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
const
|
|
9331
|
+
const allowed = config2.fonts;
|
|
9332
|
+
const test = (value, offset) => {
|
|
9333
|
+
const fam = normalizeFontFamily(value);
|
|
9334
|
+
if (!fam || isGenericFont(fam) || allowed.includes(fam)) return;
|
|
9027
9335
|
out.push({
|
|
9028
|
-
offset
|
|
9029
|
-
message:
|
|
9030
|
-
suggestion:
|
|
9336
|
+
offset,
|
|
9337
|
+
message: `Font "${fam}" is not in the Brand Kit`,
|
|
9338
|
+
suggestion: `Use var(--font-family-sans) (${allowed[0]})`
|
|
9031
9339
|
});
|
|
9340
|
+
};
|
|
9341
|
+
for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
|
|
9342
|
+
if (kind !== "css") {
|
|
9343
|
+
for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
|
|
9344
|
+
for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
|
|
9345
|
+
for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
|
|
9032
9346
|
}
|
|
9033
9347
|
return out;
|
|
9034
9348
|
}
|
|
9035
9349
|
};
|
|
9036
|
-
var
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
]
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
if (ext === "vue") return "vue";
|
|
9057
|
-
if (ext === "svelte") return "svelte";
|
|
9058
|
-
if (["html", "htm", "astro"].includes(ext)) return "html";
|
|
9059
|
-
if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
|
|
9060
|
-
return "other";
|
|
9061
|
-
}
|
|
9062
|
-
var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
|
|
9063
|
-
var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
|
|
9064
|
-
var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
|
|
9065
|
-
function fileFramework(path, kind, code, project) {
|
|
9066
|
-
const p = path.toLowerCase();
|
|
9067
|
-
switch (kind) {
|
|
9068
|
-
case "vue":
|
|
9069
|
-
return "vue";
|
|
9070
|
-
case "svelte":
|
|
9071
|
-
return "web-components";
|
|
9072
|
-
case "template":
|
|
9073
|
-
return "html";
|
|
9074
|
-
case "css":
|
|
9075
|
-
return project ?? "react";
|
|
9076
|
-
case "html":
|
|
9077
|
-
if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
|
|
9078
|
-
return project === "vue" || project === "web-components" ? project : "html";
|
|
9079
|
-
case "jsx":
|
|
9080
|
-
if (ANGULAR_COMPONENT.test(code)) return "angular";
|
|
9081
|
-
if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
|
|
9082
|
-
return project ?? "react";
|
|
9083
|
-
default:
|
|
9084
|
-
return project ?? "react";
|
|
9085
|
-
}
|
|
9086
|
-
}
|
|
9087
|
-
var blank = (s) => s.replace(/[^\n]/g, " ");
|
|
9088
|
-
function stripComments(code, kind) {
|
|
9089
|
-
let out = code;
|
|
9090
|
-
if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
|
|
9091
|
-
out = out.replace(/<!--[\s\S]*?-->/g, blank);
|
|
9092
|
-
}
|
|
9093
|
-
if (kind === "template") {
|
|
9094
|
-
out = out.replace(/\{\{!--[\s\S]*?--\}\}|\{\{![\s\S]*?\}\}|\{\{--[\s\S]*?--\}\}|\{#[\s\S]*?#\}|\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, blank).replace(/<\?(?:php|=)?[\s\S]*?(?:\?>|$)/g, (block) => block.replace(/\/\*[\s\S]*?\*\/|(?<![:"'])\/\/[^\n]*?(?=\?>|\n|$)|(?<=\s)#[^\n]*?(?=\?>|\n|$)/g, blank));
|
|
9095
|
-
return out;
|
|
9096
|
-
}
|
|
9097
|
-
if (kind === "css") {
|
|
9098
|
-
return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
|
|
9099
|
-
}
|
|
9100
|
-
const chars = out.split("");
|
|
9101
|
-
let i2 = 0;
|
|
9102
|
-
let quote = null;
|
|
9103
|
-
while (i2 < chars.length) {
|
|
9104
|
-
const c = chars[i2];
|
|
9105
|
-
const n = chars[i2 + 1];
|
|
9106
|
-
if (quote) {
|
|
9107
|
-
if (c === "\\") i2 += 2;
|
|
9108
|
-
else {
|
|
9109
|
-
if (c === quote) quote = null;
|
|
9110
|
-
i2++;
|
|
9350
|
+
var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "password", "number", "search", "tel", "url"]);
|
|
9351
|
+
var preferOfficialComponent = {
|
|
9352
|
+
id: "prefer-official-component",
|
|
9353
|
+
title: "Re-invented component",
|
|
9354
|
+
description: "Use the design system component instead of a raw HTML element.",
|
|
9355
|
+
defaultSeverity: "error",
|
|
9356
|
+
kinds: MARKUP,
|
|
9357
|
+
check({ code, config: config2, framework }) {
|
|
9358
|
+
if (config2.replaces.size === 0) return [];
|
|
9359
|
+
const out = [];
|
|
9360
|
+
const baseTags = [...new Set([...config2.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
|
|
9361
|
+
for (const m of matches(new RegExp(`<(${baseTags.join("|")})(?=[\\s>/])([^>]*)>`, "g"), code)) {
|
|
9362
|
+
const tag = m[1];
|
|
9363
|
+
const attrs = m[2];
|
|
9364
|
+
let comp = config2.replaces.get(tag);
|
|
9365
|
+
if (tag === "input" || tag === "button") {
|
|
9366
|
+
const type = (attrs.match(/\btype\s*=\s*\{?\s*["'`]([\w-]+)["'`]/)?.[1] ?? "").toLowerCase();
|
|
9367
|
+
const specific = config2.replaces.get(`${tag}[type=${type}]`);
|
|
9368
|
+
if (specific) comp = specific;
|
|
9369
|
+
else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config2.replaces.get("button") : void 0;
|
|
9111
9370
|
}
|
|
9112
|
-
continue;
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
|
|
9123
|
-
i2 = stop;
|
|
9124
|
-
continue;
|
|
9125
|
-
}
|
|
9126
|
-
if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
|
|
9127
|
-
while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
|
|
9128
|
-
continue;
|
|
9129
|
-
}
|
|
9130
|
-
i2++;
|
|
9131
|
-
}
|
|
9132
|
-
return chars.join("");
|
|
9133
|
-
}
|
|
9134
|
-
var LineIndex = class {
|
|
9135
|
-
starts = [0];
|
|
9136
|
-
text;
|
|
9137
|
-
constructor(text2) {
|
|
9138
|
-
this.text = text2;
|
|
9139
|
-
for (let i2 = 0; i2 < text2.length; i2++) if (text2.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
|
|
9140
|
-
}
|
|
9141
|
-
position(offset) {
|
|
9142
|
-
let lo = 0;
|
|
9143
|
-
let hi = this.starts.length - 1;
|
|
9144
|
-
while (lo < hi) {
|
|
9145
|
-
const mid = lo + hi + 1 >> 1;
|
|
9146
|
-
if (this.starts[mid] <= offset) lo = mid;
|
|
9147
|
-
else hi = mid - 1;
|
|
9371
|
+
if (!comp) continue;
|
|
9372
|
+
const usage = resolveUsage(comp, framework);
|
|
9373
|
+
if (!usage) continue;
|
|
9374
|
+
if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
|
|
9375
|
+
if (framework !== "react" && implementsTag(code, usage.tag)) continue;
|
|
9376
|
+
out.push({
|
|
9377
|
+
offset: m.index,
|
|
9378
|
+
message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
|
|
9379
|
+
suggestion: useSuggestion(comp, usage)
|
|
9380
|
+
});
|
|
9148
9381
|
}
|
|
9149
|
-
return
|
|
9150
|
-
}
|
|
9151
|
-
lineText(line) {
|
|
9152
|
-
const start = this.starts[line - 1] ?? 0;
|
|
9153
|
-
const end = this.starts[line] ?? this.text.length + 1;
|
|
9154
|
-
return this.text.slice(start, end - 1);
|
|
9155
|
-
}
|
|
9156
|
-
get lineCount() {
|
|
9157
|
-
return this.starts.length;
|
|
9382
|
+
return out;
|
|
9158
9383
|
}
|
|
9159
9384
|
};
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
re += slash ? "(?:.*/)?" : ".*";
|
|
9168
|
-
i2 += slash ? 2 : 1;
|
|
9169
|
-
} else re += "[^/]*";
|
|
9170
|
-
} else if (c === "?") re += "[^/]";
|
|
9171
|
-
else if (c === "{") {
|
|
9172
|
-
const end = glob.indexOf("}", i2);
|
|
9173
|
-
re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
9174
|
-
i2 = end;
|
|
9175
|
-
} else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
9176
|
-
}
|
|
9177
|
-
return new RegExp(`^${re}$`);
|
|
9178
|
-
}
|
|
9179
|
-
function makeIgnore(globs) {
|
|
9180
|
-
const res = globs.map(globToRegExp);
|
|
9181
|
-
return (path) => {
|
|
9182
|
-
const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9183
|
-
return res.some((r) => r.test(p) || r.test(`/${p}`));
|
|
9184
|
-
};
|
|
9185
|
-
}
|
|
9186
|
-
|
|
9187
|
-
// ../engine/src/check.ts
|
|
9188
|
-
var ENGINE_VERSION = "0.8.0";
|
|
9189
|
-
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
9190
|
-
function computeScore(counts, lines) {
|
|
9191
|
-
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
9192
|
-
if (penalty === 0) return 100;
|
|
9193
|
-
const perK = penalty / Math.max(1, lines / 1e3);
|
|
9194
|
-
return Math.max(0, Math.min(99, Math.round(100 / (1 + perK / 25))));
|
|
9385
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9386
|
+
var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
|
|
9387
|
+
function useSuggestion(comp, u) {
|
|
9388
|
+
if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
|
|
9389
|
+
const parts = [`Use <${u.tag}>`];
|
|
9390
|
+
if (u.importLine) parts.push(`(${u.importLine})`);
|
|
9391
|
+
return parts.join(" ");
|
|
9195
9392
|
}
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
const
|
|
9205
|
-
const
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
for (const rule of RULES) {
|
|
9216
|
-
const setting = config2.rules[rule.id];
|
|
9217
|
-
if (setting === "off" || !rule.kinds.includes(kind)) continue;
|
|
9218
|
-
for (const f of rule.check({ path, kind, code, config: config2, framework })) {
|
|
9219
|
-
const { line, column } = idx.position(f.offset);
|
|
9220
|
-
if (only && !only.has(line)) continue;
|
|
9221
|
-
const key = `${rule.id}:${line}:${column}`;
|
|
9222
|
-
if (seen.has(key)) continue;
|
|
9223
|
-
seen.add(key);
|
|
9224
|
-
violations.push({
|
|
9225
|
-
ruleId: rule.id,
|
|
9226
|
-
severity: setting ?? f.severity ?? rule.defaultSeverity,
|
|
9227
|
-
file: path,
|
|
9228
|
-
line,
|
|
9229
|
-
column,
|
|
9230
|
-
message: f.message,
|
|
9231
|
-
suggestion: f.suggestion,
|
|
9232
|
-
snippet: opts.snippets === false ? void 0 : idx.lineText(line).trim().slice(0, 160)
|
|
9233
|
-
});
|
|
9234
|
-
}
|
|
9393
|
+
var forbiddenImport = {
|
|
9394
|
+
id: "forbidden-import",
|
|
9395
|
+
title: "Other UI library",
|
|
9396
|
+
description: "Only the design system's component library may be imported.",
|
|
9397
|
+
defaultSeverity: "error",
|
|
9398
|
+
kinds: ALL,
|
|
9399
|
+
check({ code, config: config2 }) {
|
|
9400
|
+
const out = [];
|
|
9401
|
+
const lib = config2.components[0]?.importPath;
|
|
9402
|
+
const re = /(?:\bimport\s[^'";]*?from\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\(\s*|@import\s+(?:url\(\s*)?)(['"])([^'"]+)\1/g;
|
|
9403
|
+
for (const m of matches(re, code)) {
|
|
9404
|
+
const spec = m[2];
|
|
9405
|
+
const bad = config2.forbiddenImports.find((p) => spec === p || spec.startsWith(`${p}/`));
|
|
9406
|
+
if (!bad) continue;
|
|
9407
|
+
out.push({
|
|
9408
|
+
offset: m.index + m[0].indexOf(spec),
|
|
9409
|
+
message: `Imports ${bad}, which isn't part of the design system`,
|
|
9410
|
+
suggestion: lib ? `Use components from "${lib}"` : "Use the design system's components"
|
|
9411
|
+
});
|
|
9235
9412
|
}
|
|
9413
|
+
return out;
|
|
9236
9414
|
}
|
|
9237
|
-
violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
|
|
9238
|
-
const counts = { error: 0, warning: 0, info: 0 };
|
|
9239
|
-
const perRule = /* @__PURE__ */ new Map();
|
|
9240
|
-
for (const v of violations) {
|
|
9241
|
-
counts[v.severity]++;
|
|
9242
|
-
perRule.set(v.ruleId, (perRule.get(v.ruleId) ?? 0) + 1);
|
|
9243
|
-
}
|
|
9244
|
-
return {
|
|
9245
|
-
engineVersion: ENGINE_VERSION,
|
|
9246
|
-
tokenVersion: config2.tokenVersion,
|
|
9247
|
-
score: computeScore(counts, linesScanned),
|
|
9248
|
-
counts,
|
|
9249
|
-
filesScanned,
|
|
9250
|
-
linesScanned,
|
|
9251
|
-
violations,
|
|
9252
|
-
byRule: [...perRule].map(([ruleId, count]) => ({ ruleId, count })).sort((a, b) => b.count - a.count),
|
|
9253
|
-
durationMs: Date.now() - t0
|
|
9254
|
-
};
|
|
9255
|
-
}
|
|
9256
|
-
|
|
9257
|
-
// ../engine/src/report.ts
|
|
9258
|
-
var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
|
|
9259
|
-
var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
|
|
9260
|
-
var ansi = (on) => {
|
|
9261
|
-
const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
9262
|
-
return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
|
|
9263
9415
|
};
|
|
9264
|
-
var
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
const rendered2 = v.line === 0;
|
|
9276
|
-
lines.push(` ${c.dim(rendered2 ? "".padEnd(8) : `${v.line}:${v.column}`.padEnd(8))} ${sev[v.severity](v.severity.padEnd(7))} ${v.message} ${c.dim(v.ruleId)}`);
|
|
9277
|
-
if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
|
|
9278
|
-
if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
|
|
9279
|
-
}
|
|
9280
|
-
if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
|
|
9281
|
-
const s = report.score;
|
|
9282
|
-
const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
|
|
9283
|
-
lines.push(
|
|
9284
|
-
"",
|
|
9285
|
-
`${c.bold("Brand score")} ${scoreStr}/100 ${c.dim("\xB7")} ${c.red(`${report.counts.error} errors`)} ${c.dim("\xB7")} ${c.yellow(`${report.counts.warning} warnings`)} ${c.dim("\xB7")} ${c.cyan(`${report.counts.info} info`)}`,
|
|
9286
|
-
c.dim(`${report.filesScanned} ${rendered(report) ? "pages" : "files"}, ${report.linesScanned} ${rendered(report) ? "elements" : "lines"} in ${report.durationMs}ms${report.tokenVersion ? ` \xB7 Brand Kit v${report.tokenVersion}` : ""}`)
|
|
9287
|
-
);
|
|
9288
|
-
return lines.join("\n");
|
|
9289
|
-
}
|
|
9290
|
-
var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
9291
|
-
var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
9292
|
-
function formatGithubAnnotations(report, max2 = 50) {
|
|
9293
|
-
const level = { error: "error", warning: "warning", info: "notice" };
|
|
9294
|
-
return report.violations.slice(0, max2).map(
|
|
9295
|
-
(v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
|
|
9296
|
-
v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
|
|
9297
|
-
)}`
|
|
9298
|
-
).join("\n");
|
|
9299
|
-
}
|
|
9300
|
-
var COMMENT_MARKER = "<!-- brandrail-report -->";
|
|
9301
|
-
function formatMarkdown(report, opts = {}) {
|
|
9302
|
-
const { threshold, maxRows = 30 } = opts;
|
|
9303
|
-
const pass = threshold === void 0 || report.score >= threshold;
|
|
9304
|
-
const badge = report.violations.length === 0 ? "\u{1F7E2}" : pass ? "\u{1F7E1}" : "\u{1F534}";
|
|
9305
|
-
const out = [COMMENT_MARKER];
|
|
9306
|
-
out.push(`## ${badge} Brand score: **${report.score}**/100${threshold !== void 0 ? ` (threshold ${threshold})` : ""}`);
|
|
9307
|
-
out.push("");
|
|
9308
|
-
out.push(
|
|
9309
|
-
`**${report.counts.error}** errors \xB7 **${report.counts.warning}** warnings \xB7 **${report.counts.info}** info \u2014 ${report.filesScanned} files scanned${report.tokenVersion ? ` against Brand Kit v${report.tokenVersion}` : ""}`
|
|
9310
|
-
);
|
|
9311
|
-
if (report.violations.length === 0) {
|
|
9312
|
-
out.push("", "Everything uses approved tokens and components. \u2728");
|
|
9313
|
-
} else {
|
|
9314
|
-
if (report.byRule.length) {
|
|
9315
|
-
out.push("", "| Rule | Count |", "| --- | ---: |");
|
|
9316
|
-
for (const r of report.byRule) out.push(`| ${title(r.ruleId)} \`${r.ruleId}\` | ${r.count} |`);
|
|
9317
|
-
}
|
|
9318
|
-
out.push("", "<details><summary>Violations</summary>", "", "| | Location | Issue | Fix |", "| --- | --- | --- | --- |");
|
|
9319
|
-
const icon = { error: "\u{1F534}", warning: "\u{1F7E1}", info: "\u{1F535}" };
|
|
9320
|
-
for (const v of report.violations.slice(0, maxRows)) {
|
|
9321
|
-
const loc = v.line === 0 ? `${v.file} ${v.snippet ?? ""}`.trim() : `${v.file}:${v.line}`;
|
|
9322
|
-
const url2 = opts.fileUrl?.(v);
|
|
9323
|
-
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
9324
|
-
out.push(`| ${icon[v.severity]} | ${url2 ? `[\`${loc}\`](${url2})` : `\`${loc}\``} | ${cell(v.message)} | ${cell(v.suggestion ?? "")} |`);
|
|
9416
|
+
var imgAlt = {
|
|
9417
|
+
id: "img-alt",
|
|
9418
|
+
title: "Image without alt text",
|
|
9419
|
+
description: "Every <img> needs an alt attribute (empty for decorative images).",
|
|
9420
|
+
defaultSeverity: "warning",
|
|
9421
|
+
kinds: MARKUP,
|
|
9422
|
+
check({ code }) {
|
|
9423
|
+
const out = [];
|
|
9424
|
+
for (const m of matches(/<img(?=[\s/>])([^>]*)>/g, code)) {
|
|
9425
|
+
if (/(?<![\w-])alt\s*=/.test(m[1]) || /\{\s*\.\.\./.test(m[1])) continue;
|
|
9426
|
+
out.push({ offset: m.index, message: "<img> has no alt text", suggestion: 'Add alt="\u2026" describing the image, or alt="" if decorative' });
|
|
9325
9427
|
}
|
|
9326
|
-
|
|
9327
|
-
out.push("", "</details>");
|
|
9428
|
+
return out;
|
|
9328
9429
|
}
|
|
9329
|
-
out.push("", `<sub>Brandrail engine ${report.engineVersion}${opts.dashboardUrl ? ` \xB7 [Open in Brandrail](${opts.dashboardUrl})` : ""}</sub>`);
|
|
9330
|
-
return out.join("\n");
|
|
9331
|
-
}
|
|
9332
|
-
|
|
9333
|
-
// ../engine/src/defaults.ts
|
|
9334
|
-
var starterConfig = {
|
|
9335
|
-
$schema: "https://brandrail.dev/schema/config.json",
|
|
9336
|
-
tokenVersion: "0.0.0-local",
|
|
9337
|
-
tokens: {
|
|
9338
|
-
color: {
|
|
9339
|
-
action: { primary: { $type: "color", $value: "#1f883d", $description: "One primary action per view" } },
|
|
9340
|
-
accent: { default: { $type: "color", $value: "#0969da", $description: "Links, focus, selection" } },
|
|
9341
|
-
fg: {
|
|
9342
|
-
default: { $type: "color", $value: "#1f2328", $description: "Body text" },
|
|
9343
|
-
muted: { $type: "color", $value: "#59636e", $description: "Secondary text" }
|
|
9344
|
-
},
|
|
9345
|
-
canvas: {
|
|
9346
|
-
default: { $type: "color", $value: "#ffffff", $description: "Page background" },
|
|
9347
|
-
subtle: { $type: "color", $value: "#f6f8fa", $description: "Headers, wells" }
|
|
9348
|
-
},
|
|
9349
|
-
border: { default: { $type: "color", $value: "#d1d9e0", $description: "Borders and dividers" } },
|
|
9350
|
-
danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } },
|
|
9351
|
-
success: { default: { $type: "color", $value: "#1a7f37", $description: "Success messages, completed states" } },
|
|
9352
|
-
attention: { default: { $type: "color", $value: "#9a6700", $description: "Warnings, needs attention" } }
|
|
9353
|
-
},
|
|
9354
|
-
font: {
|
|
9355
|
-
family: { sans: { $type: "fontFamily", $value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif" } },
|
|
9356
|
-
size: { base: { $type: "dimension", $value: "14px" } }
|
|
9357
|
-
},
|
|
9358
|
-
radius: { md: { $type: "dimension", $value: "6px" } },
|
|
9359
|
-
space: { unit: { $type: "dimension", $value: "8px" } }
|
|
9360
|
-
},
|
|
9361
|
-
components: [
|
|
9362
|
-
{ name: "Button", importPath: "@acme/ui" },
|
|
9363
|
-
{ name: "TextInput", importPath: "@acme/ui" },
|
|
9364
|
-
{ name: "Select", importPath: "@acme/ui" },
|
|
9365
|
-
{ name: "Textarea", importPath: "@acme/ui" }
|
|
9366
|
-
],
|
|
9367
|
-
rules: {},
|
|
9368
|
-
ignore: []
|
|
9369
|
-
};
|
|
9370
|
-
|
|
9371
|
-
// ../engine/src/tokens.ts
|
|
9372
|
-
var EXT = "dev.brandrail";
|
|
9373
|
-
function slug(s) {
|
|
9374
|
-
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
9375
|
-
}
|
|
9376
|
-
var segs = (name) => name.split(/[/$.]+/).map(slug).filter(Boolean);
|
|
9377
|
-
var stripPrefix = (s, ...prefixes) => {
|
|
9378
|
-
for (const p of prefixes) if (s === p) return "";
|
|
9379
|
-
for (const p of prefixes) if (s.startsWith(`${p}-`)) return s.slice(p.length + 1);
|
|
9380
|
-
return s;
|
|
9381
|
-
};
|
|
9382
|
-
var WEIGHTS = {
|
|
9383
|
-
thin: "100",
|
|
9384
|
-
hairline: "100",
|
|
9385
|
-
"extra-light": "200",
|
|
9386
|
-
"ultra-light": "200",
|
|
9387
|
-
light: "300",
|
|
9388
|
-
regular: "400",
|
|
9389
|
-
regulare: "400",
|
|
9390
|
-
normal: "400",
|
|
9391
|
-
book: "400",
|
|
9392
|
-
medium: "500",
|
|
9393
|
-
"semi-bold": "600",
|
|
9394
|
-
semibold: "600",
|
|
9395
|
-
"demi-bold": "600",
|
|
9396
|
-
bold: "700",
|
|
9397
|
-
"extra-bold": "800",
|
|
9398
|
-
"ultra-bold": "800",
|
|
9399
|
-
black: "900",
|
|
9400
|
-
heavy: "900"
|
|
9401
9430
|
};
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
function
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
const
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
}
|
|
9416
|
-
return null;
|
|
9417
|
-
}
|
|
9418
|
-
if (v && typeof v === "object" && "r" in v) {
|
|
9419
|
-
const c = v;
|
|
9420
|
-
const hex4 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
|
|
9421
|
-
const a = c.a ?? 1;
|
|
9422
|
-
return a < 1 ? `${hex4}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex4;
|
|
9423
|
-
}
|
|
9424
|
-
if (typeof v !== "string") return null;
|
|
9425
|
-
const s = v.trim();
|
|
9426
|
-
if (/^#[0-9a-f]{3,8}$/i.test(s)) {
|
|
9427
|
-
const h = s.slice(1).toLowerCase();
|
|
9428
|
-
return h.length === 3 || h.length === 4 ? `#${[...h].map((c) => c + c).join("")}` : `#${h}`;
|
|
9429
|
-
}
|
|
9430
|
-
const rgb = parseColor(s);
|
|
9431
|
-
if (!rgb) return null;
|
|
9432
|
-
const hex3 = toHex(rgb);
|
|
9433
|
-
return rgb.a < 1 ? `${hex3}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex3;
|
|
9434
|
-
}
|
|
9435
|
-
var dim = (v) => {
|
|
9436
|
-
if (v && typeof v === "object" && "value" in v) {
|
|
9437
|
-
const d = v;
|
|
9438
|
-
const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
|
|
9439
|
-
return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
|
|
9431
|
+
var NATIVE_ATTRS = new Set(
|
|
9432
|
+
"id className style title role tabIndex hidden lang dir name value defaultValue defaultChecked checked disabled required readOnly autoFocus autoComplete placeholder type min max step maxLength minLength pattern form accept multiple href target rel download children".split(" ")
|
|
9433
|
+
);
|
|
9434
|
+
var ALWAYS_OK = /* @__PURE__ */ new Set(["key", "ref"]);
|
|
9435
|
+
function importedComponents(code, components) {
|
|
9436
|
+
const out = /* @__PURE__ */ new Map();
|
|
9437
|
+
for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
|
|
9438
|
+
const from = m[3];
|
|
9439
|
+
for (const part of m[1].split(",")) {
|
|
9440
|
+
const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
|
|
9441
|
+
if (!orig) continue;
|
|
9442
|
+
const comp = components.find((c) => c.name === orig && c.importPath === from && c.props?.length);
|
|
9443
|
+
if (comp) out.set(alias || orig, comp);
|
|
9444
|
+
}
|
|
9440
9445
|
}
|
|
9441
|
-
return
|
|
9442
|
-
}
|
|
9443
|
-
function
|
|
9444
|
-
const
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
const rest = parts.slice(0, -1);
|
|
9449
|
-
if (kind === "boolean") return null;
|
|
9450
|
-
if (kind === "color") {
|
|
9451
|
-
const v2 = colorValue(value);
|
|
9452
|
-
if (!v2) return null;
|
|
9453
|
-
let p = parts[0] === "color" || parts[0] === "colors" ? parts.slice(1) : parts;
|
|
9454
|
-
if (p.length === 1) p = [stripPrefix(p[0], "colors", "color") || p[0]];
|
|
9455
|
-
const tail = p[p.length - 1];
|
|
9456
|
-
const m = tail.match(/^(.*[a-z])-(\d+)$/);
|
|
9457
|
-
if (m && p.length <= 2) p = [...p.slice(0, -1), m[1], m[2]];
|
|
9458
|
-
return { path: ["color", ...p].join("."), type: "color", value: v2 };
|
|
9446
|
+
return out;
|
|
9447
|
+
}
|
|
9448
|
+
function skipBalanced(code, i2) {
|
|
9449
|
+
const open2 = code[i2];
|
|
9450
|
+
if (open2 === '"' || open2 === "'") {
|
|
9451
|
+
const end = code.indexOf(open2, i2 + 1);
|
|
9452
|
+
return end === -1 ? code.length : end + 1;
|
|
9459
9453
|
}
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9454
|
+
let depth = 0;
|
|
9455
|
+
let quote = null;
|
|
9456
|
+
for (let j = i2; j < code.length; j++) {
|
|
9457
|
+
const c = code[j];
|
|
9458
|
+
if (quote) {
|
|
9459
|
+
if (c === "\\") j++;
|
|
9460
|
+
else if (c === quote) quote = null;
|
|
9461
|
+
continue;
|
|
9465
9462
|
}
|
|
9466
|
-
if (
|
|
9467
|
-
|
|
9463
|
+
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
9464
|
+
else if (c === "{") depth++;
|
|
9465
|
+
else if (c === "}" && --depth === 0) return j + 1;
|
|
9466
|
+
}
|
|
9467
|
+
return code.length;
|
|
9468
|
+
}
|
|
9469
|
+
function parseAttributes(code, start) {
|
|
9470
|
+
const attrs = [];
|
|
9471
|
+
let spread = false;
|
|
9472
|
+
let i2 = start;
|
|
9473
|
+
while (i2 < code.length) {
|
|
9474
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9475
|
+
const c = code[i2];
|
|
9476
|
+
if (c === void 0) return null;
|
|
9477
|
+
if (c === ">") return { attrs, spread, selfClosing: false };
|
|
9478
|
+
if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
|
|
9479
|
+
if (c === "{") {
|
|
9480
|
+
spread = true;
|
|
9481
|
+
i2 = skipBalanced(code, i2);
|
|
9482
|
+
continue;
|
|
9483
|
+
}
|
|
9484
|
+
const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
|
|
9485
|
+
if (!name) return null;
|
|
9486
|
+
attrs.push({ name, offset: i2 });
|
|
9487
|
+
i2 += name.length;
|
|
9488
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9489
|
+
if (code[i2] === "=") {
|
|
9490
|
+
i2++;
|
|
9491
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9492
|
+
i2 = skipBalanced(code, i2);
|
|
9468
9493
|
}
|
|
9469
|
-
const c = colorValue(s);
|
|
9470
|
-
if (c && /^#|^rgb|^hsl/i.test(s.trim())) return classify(name, "color", s, opts);
|
|
9471
|
-
return null;
|
|
9472
9494
|
}
|
|
9473
|
-
|
|
9474
|
-
const has = (...scope) => scope.some((x2) => scopes.includes(x2));
|
|
9475
|
-
const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
|
|
9476
|
-
if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
|
|
9477
|
-
if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
|
|
9478
|
-
if (has("LINE_HEIGHT") || /line-height|leading/.test(hay)) return { path: `font.line-height.${nm("line-height", "leading")}`, type: "dimension", value: v };
|
|
9479
|
-
if (has("LETTER_SPACING") || /letter-spacing|tracking/.test(hay)) return { path: `font.letter-spacing.${nm("letter-spacing", "tracking")}`, type: "dimension", value: v };
|
|
9480
|
-
if (has("FONT_WEIGHT") || /font-weight/.test(hay)) return { path: `font.weight.${nm("font-weight", "weight")}`, type: "fontWeight", value: String(value) };
|
|
9481
|
-
if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
|
|
9482
|
-
if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
|
|
9483
|
-
if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
|
|
9484
|
-
if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
|
|
9485
|
-
return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
|
|
9495
|
+
return null;
|
|
9486
9496
|
}
|
|
9487
|
-
function
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
}
|
|
9501
|
-
|
|
9497
|
+
function parseHtmlAttributes(code, start) {
|
|
9498
|
+
const attrs = [];
|
|
9499
|
+
let i2 = start;
|
|
9500
|
+
while (i2 < code.length) {
|
|
9501
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9502
|
+
const c = code[i2];
|
|
9503
|
+
if (c === void 0) return null;
|
|
9504
|
+
if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
|
|
9505
|
+
if (c === "{" || c === "<" && code[i2 + 1] === "?") {
|
|
9506
|
+
const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
|
|
9507
|
+
const e = code.indexOf(close, i2 + 2);
|
|
9508
|
+
i2 = e === -1 ? code.length : e + close.length;
|
|
9509
|
+
continue;
|
|
9510
|
+
}
|
|
9511
|
+
const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
|
|
9512
|
+
if (!name) return null;
|
|
9513
|
+
attrs.push({ name, offset: i2 });
|
|
9514
|
+
i2 += name.length;
|
|
9515
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9516
|
+
if (code[i2] === "=") {
|
|
9517
|
+
i2++;
|
|
9518
|
+
while (/\s/.test(code[i2] ?? "")) i2++;
|
|
9519
|
+
const q = code[i2];
|
|
9520
|
+
if (q === '"' || q === "'") {
|
|
9521
|
+
const e = code.indexOf(q, i2 + 1);
|
|
9522
|
+
i2 = e === -1 ? code.length : e + 1;
|
|
9523
|
+
} else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
|
|
9524
|
+
}
|
|
9502
9525
|
}
|
|
9503
|
-
|
|
9526
|
+
return null;
|
|
9504
9527
|
}
|
|
9505
|
-
var
|
|
9506
|
-
function
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
return deref(mode === "dark" && target.dark !== void 0 ? target.dark : target.value, mode, depth + 1);
|
|
9515
|
-
};
|
|
9516
|
-
return raws.map((r) => ({ ...r, value: deref(r.value, "light"), dark: r.dark !== void 0 ? deref(r.dark, "dark") : void 0 }));
|
|
9528
|
+
var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
|
|
9529
|
+
function propName(raw) {
|
|
9530
|
+
let n = raw;
|
|
9531
|
+
if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
|
|
9532
|
+
n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
|
|
9533
|
+
if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
|
|
9534
|
+
n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
|
|
9535
|
+
if (HTML_GLOBAL.test(n.toLowerCase())) return null;
|
|
9536
|
+
return n;
|
|
9517
9537
|
}
|
|
9518
|
-
var
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
fontfamily: "string",
|
|
9532
|
-
fontweights: "string",
|
|
9533
|
-
fontweight: "string"
|
|
9534
|
-
};
|
|
9535
|
-
function flattenTokens(doc) {
|
|
9536
|
-
const raws = resolveRefs(collectRaw(doc).raws);
|
|
9538
|
+
var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
|
|
9539
|
+
var NATIVE_EVENT = /^(?:click|dblclick|auxclick|contextmenu|focus|blur|focusin|focusout|key(?:down|up|press)|mouse\w+|pointer\w+|touch\w+|wheel|drag\w*|drop|scroll|animation\w+|transition\w+)$/;
|
|
9540
|
+
function eventName(raw) {
|
|
9541
|
+
const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
|
|
9542
|
+
return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
|
|
9543
|
+
}
|
|
9544
|
+
function templateProps({ code, config: config2, framework }) {
|
|
9545
|
+
const tagged = [];
|
|
9546
|
+
for (const comp of config2.components) {
|
|
9547
|
+
const u = resolveUsage(comp, framework);
|
|
9548
|
+
if (u && (u.props?.length || u.events?.length)) tagged.push({ comp, tag: u.tag, props: u.props, events: u.events, ce: Boolean(u.customElement || u.via) });
|
|
9549
|
+
}
|
|
9550
|
+
if (!tagged.length) return [];
|
|
9537
9551
|
const out = [];
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9552
|
+
const forms = /* @__PURE__ */ new Map();
|
|
9553
|
+
for (const t of tagged) {
|
|
9554
|
+
forms.set(t.tag, t);
|
|
9555
|
+
if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
|
|
9556
|
+
}
|
|
9557
|
+
const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
|
|
9558
|
+
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
9559
|
+
const t = forms.get(m[1]);
|
|
9560
|
+
const parsed = parseHtmlAttributes(code, m.index + m[0].length);
|
|
9561
|
+
if (!parsed) continue;
|
|
9562
|
+
const list = (t.props ?? []).map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
9563
|
+
const passed = [];
|
|
9564
|
+
for (const a of parsed.attrs) {
|
|
9565
|
+
const raw = a.name;
|
|
9566
|
+
const ev = eventName(raw);
|
|
9567
|
+
if (ev) {
|
|
9568
|
+
if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
|
|
9569
|
+
const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
|
|
9570
|
+
out.push({
|
|
9571
|
+
offset: a.offset,
|
|
9572
|
+
message: `<${t.tag}> has no "${ev}" event`,
|
|
9573
|
+
suggestion: `${like ? `Listen to ${framework === "angular" ? `(${like.name})` : framework === "vue" ? `@${like.name}` : `"${like.name}"`}${like.detail ? ` and read ${framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : "event.detail"} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
|
|
9574
|
+
});
|
|
9575
|
+
continue;
|
|
9576
|
+
}
|
|
9577
|
+
if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
|
|
9578
|
+
if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
|
|
9579
|
+
const n = propName(raw);
|
|
9580
|
+
if (!n || !t.props?.length) continue;
|
|
9581
|
+
passed.push(n);
|
|
9582
|
+
if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
|
|
9583
|
+
const camel2 = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
9584
|
+
out.push({
|
|
9585
|
+
offset: a.offset,
|
|
9586
|
+
message: `${raw} binds a property that doesn't exist on <${t.tag}>`,
|
|
9587
|
+
suggestion: `Bind the camelCase property: ${framework === "vue" ? `:${camel2}` : `[${camel2}]`}="\u2026" (kebab-case is only for static attributes: ${n}="\u2026")`
|
|
9588
|
+
});
|
|
9589
|
+
continue;
|
|
9590
|
+
}
|
|
9591
|
+
if (t.props.some((p) => sameName(p.name, n))) continue;
|
|
9592
|
+
out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
|
|
9593
|
+
}
|
|
9594
|
+
for (const p of t.props ?? []) {
|
|
9595
|
+
if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
|
|
9596
|
+
if (p.name === "children" || p.name === "content") continue;
|
|
9597
|
+
out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
9548
9598
|
}
|
|
9549
9599
|
}
|
|
9550
9600
|
return out;
|
|
9551
9601
|
}
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
const
|
|
9565
|
-
|
|
9566
|
-
|
|
9602
|
+
var componentProps = {
|
|
9603
|
+
id: "component-props",
|
|
9604
|
+
title: "Unknown or missing prop",
|
|
9605
|
+
description: "Design system components only accept their documented props, and required props must be passed.",
|
|
9606
|
+
defaultSeverity: "error",
|
|
9607
|
+
kinds: MARKUP,
|
|
9608
|
+
check(ctx) {
|
|
9609
|
+
if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
|
|
9610
|
+
const { code, config: config2 } = ctx;
|
|
9611
|
+
if (!config2.components.some((c) => c.props?.length)) return [];
|
|
9612
|
+
const imported = importedComponents(code, config2.components);
|
|
9613
|
+
if (imported.size === 0) return [];
|
|
9614
|
+
const out = [];
|
|
9615
|
+
const names = [...imported.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|");
|
|
9616
|
+
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
9617
|
+
if (/[\w)\]]\s*$/.test(code.slice(Math.max(0, m.index - 20), m.index))) continue;
|
|
9618
|
+
const comp = imported.get(m[1]);
|
|
9619
|
+
const parsed = parseAttributes(code, m.index + m[0].length);
|
|
9620
|
+
if (!parsed) continue;
|
|
9621
|
+
const own2 = new Map(comp.props.map((p) => [p.name, p]));
|
|
9622
|
+
const list = comp.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
9623
|
+
for (const a of parsed.attrs) {
|
|
9624
|
+
if (own2.has(a.name) || ALWAYS_OK.has(a.name) || a.name.includes("-")) continue;
|
|
9625
|
+
if (comp.htmlAttributes && (NATIVE_ATTRS.has(a.name) || /^on[A-Z]/.test(a.name) || /^aria/.test(a.name))) continue;
|
|
9626
|
+
const sibling = config2.components.find(
|
|
9627
|
+
(c) => c !== comp && c.importPath === comp.importPath && (c.name.startsWith(comp.name) || comp.name.startsWith(c.name)) && c.props?.some((p) => p.name === a.name)
|
|
9628
|
+
);
|
|
9629
|
+
out.push({
|
|
9630
|
+
offset: a.offset,
|
|
9631
|
+
message: `<${comp.name}> has no "${a.name}" prop`,
|
|
9632
|
+
suggestion: sibling ? `Use <${sibling.name}>, which has "${a.name}", or remove it. ${comp.name} props: ${list}` : `Remove it. ${comp.name} props: ${list}`
|
|
9633
|
+
});
|
|
9634
|
+
}
|
|
9635
|
+
if (!parsed.spread) {
|
|
9636
|
+
const passed = new Set(parsed.attrs.map((a) => a.name));
|
|
9637
|
+
for (const p of comp.props) {
|
|
9638
|
+
if (!p.required || passed.has(p.name) || p.name === "children" && !parsed.selfClosing) continue;
|
|
9639
|
+
out.push({ offset: m.index, message: `<${comp.name}> is missing the required "${p.name}" prop`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
}
|
|
9643
|
+
return out;
|
|
9567
9644
|
}
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9645
|
+
};
|
|
9646
|
+
var legacyLibrary = {
|
|
9647
|
+
id: "legacy-library",
|
|
9648
|
+
title: "Other UI library in use",
|
|
9649
|
+
description: "Pieces of Material, MUI, Bootstrap, Ant Design, Chakra or PrimeNG that a design system component replaces. The count is the migration still to do.",
|
|
9650
|
+
defaultSeverity: "warning",
|
|
9651
|
+
kinds: ["jsx", "vue", "svelte", "html", "template"],
|
|
9652
|
+
check({ code, kind, config: config2, framework }) {
|
|
9653
|
+
const out = [];
|
|
9654
|
+
for (const u of findLegacyUsages(code, kind)) {
|
|
9655
|
+
const comp = componentForConcept(config2, u.concept);
|
|
9656
|
+
const usage = comp ? resolveUsage(comp, framework) : null;
|
|
9657
|
+
out.push({
|
|
9658
|
+
offset: u.offset,
|
|
9659
|
+
message: `${u.what} (${LIBRARY_LABEL[u.library]}) should be ${comp ? comp.name : `a design system ${u.concept}`}`,
|
|
9660
|
+
suggestion: comp && usage ? useSuggestion(comp, usage) : comp ? `${comp.name} has no ${FRAMEWORK_LABEL[framework]} version yet: add one in the registry, or build it with tokens` : `No ${u.concept} component in the registry yet: add one, or build it with tokens and semantic HTML`
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
9663
|
+
return out;
|
|
9574
9664
|
}
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9665
|
+
};
|
|
9666
|
+
var whenGone = (d) => d.removeAfter ? ` and will be removed after ${d.removeAfter}` : "";
|
|
9667
|
+
var deprecatedToken = {
|
|
9668
|
+
id: "deprecated-token",
|
|
9669
|
+
title: "Deprecated token",
|
|
9670
|
+
description: "Tokens the design system is retiring. They still work until their removal date; switch to the replacement.",
|
|
9671
|
+
defaultSeverity: "warning",
|
|
9672
|
+
kinds: ALL,
|
|
9673
|
+
check({ code, config: config2 }) {
|
|
9674
|
+
if (!config2.deprecatedTokens.size) return [];
|
|
9675
|
+
const out = [];
|
|
9676
|
+
for (const m of matches(/var\(\s*(--[\w-]+)/g, code)) {
|
|
9677
|
+
const d = config2.deprecatedTokens.get(m[1]);
|
|
9678
|
+
if (!d) continue;
|
|
9679
|
+
const repl = d.replacement ? `var(--${d.replacement.replace(/\./g, "-")})` : void 0;
|
|
9680
|
+
out.push({
|
|
9681
|
+
offset: m.index,
|
|
9682
|
+
message: `${m[1]} (${d.path}) is deprecated${whenGone(d)}`,
|
|
9683
|
+
suggestion: repl ? `Use ${repl}${d.reason ? `. ${d.reason}` : ""}` : d.reason || "Ask the design system team what replaces it"
|
|
9684
|
+
});
|
|
9582
9685
|
}
|
|
9686
|
+
return out;
|
|
9583
9687
|
}
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
}
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
}
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9688
|
+
};
|
|
9689
|
+
var deprecatedComponent = {
|
|
9690
|
+
id: "deprecated-component",
|
|
9691
|
+
title: "Deprecated component",
|
|
9692
|
+
description: "Components the design system is retiring. They still work until their removal date; switch to the replacement.",
|
|
9693
|
+
defaultSeverity: "warning",
|
|
9694
|
+
kinds: MARKUP,
|
|
9695
|
+
check({ code, kind, config: config2, framework }) {
|
|
9696
|
+
const dep = config2.components.filter((c) => c.status === "deprecated");
|
|
9697
|
+
if (!dep.length) return [];
|
|
9698
|
+
const out = [];
|
|
9699
|
+
const report = (offset, c) => {
|
|
9700
|
+
const d = c.deprecation ?? {};
|
|
9701
|
+
out.push({
|
|
9702
|
+
offset,
|
|
9703
|
+
message: `${c.name} is deprecated${whenGone(d)}`,
|
|
9704
|
+
suggestion: d.replacement ? `Use ${d.replacement}${d.reason ? `. ${d.reason}` : ""}` : d.reason || "Ask the design system team what replaces it"
|
|
9705
|
+
});
|
|
9706
|
+
};
|
|
9707
|
+
if (framework === "react" && kind === "jsx") {
|
|
9708
|
+
for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
|
|
9709
|
+
for (const part of m[1].split(",")) {
|
|
9710
|
+
const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
|
|
9711
|
+
const c = dep.find((x2) => x2.name === orig && x2.importPath === m[3]);
|
|
9712
|
+
if (!c) continue;
|
|
9713
|
+
const local = (alias || orig).replace(/[$]/g, "\\$");
|
|
9714
|
+
let used = false;
|
|
9715
|
+
for (const u of matches(new RegExp(`<${local}(?=[\\s/>])`, "g"), code)) {
|
|
9716
|
+
used = true;
|
|
9717
|
+
report(u.index, c);
|
|
9718
|
+
}
|
|
9719
|
+
if (!used) report(m.index, c);
|
|
9720
|
+
}
|
|
9617
9721
|
}
|
|
9618
|
-
|
|
9619
|
-
}
|
|
9620
|
-
return tree;
|
|
9621
|
-
}
|
|
9622
|
-
var isAlias = (v) => !!v && typeof v === "object" && v.type === "VARIABLE_ALIAS";
|
|
9623
|
-
var DARK = /dark|night/i;
|
|
9624
|
-
var LIGHT = /light|default|day|base/i;
|
|
9625
|
-
function finish(format, all, notes, opts) {
|
|
9626
|
-
const PALETTE2 = /^(color\.[a-z-]+)\.\d{1,3}$/;
|
|
9627
|
-
const usedFamilies = new Set(all.filter((t) => t.referenced).map((t) => t.path.match(PALETTE2)?.[1]).filter(Boolean));
|
|
9628
|
-
if (usedFamilies.size) {
|
|
9629
|
-
const steps = /* @__PURE__ */ new Map();
|
|
9630
|
-
for (const t of all) {
|
|
9631
|
-
const f = t.path.match(PALETTE2)?.[1];
|
|
9632
|
-
if (f) steps.set(f, (steps.get(f) ?? 0) + 1);
|
|
9722
|
+
return out;
|
|
9633
9723
|
}
|
|
9634
|
-
for (const
|
|
9635
|
-
const
|
|
9636
|
-
if (
|
|
9724
|
+
for (const c of dep) {
|
|
9725
|
+
const u = resolveUsage(c, framework);
|
|
9726
|
+
if (!u) continue;
|
|
9727
|
+
const tag = u.tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9728
|
+
for (const m of matches(new RegExp(`<${tag}(?=[\\s/>])`, "g"), code)) report(m.index, c);
|
|
9637
9729
|
}
|
|
9730
|
+
return out;
|
|
9638
9731
|
}
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9732
|
+
};
|
|
9733
|
+
var RULES = [
|
|
9734
|
+
noHardcodedColor,
|
|
9735
|
+
noRawPaletteClass,
|
|
9736
|
+
preferOfficialComponent,
|
|
9737
|
+
forbiddenImport,
|
|
9738
|
+
spacingScale,
|
|
9739
|
+
fontFamily,
|
|
9740
|
+
radiusScale,
|
|
9741
|
+
imgAlt,
|
|
9742
|
+
componentProps,
|
|
9743
|
+
legacyLibrary,
|
|
9744
|
+
deprecatedToken,
|
|
9745
|
+
deprecatedComponent
|
|
9746
|
+
];
|
|
9747
|
+
var RULE_IDS = RULES.map((r) => r.id);
|
|
9748
|
+
|
|
9749
|
+
// ../engine/src/source.ts
|
|
9750
|
+
var TEMPLATE_EXTENSIONS = ["mustache", "hbs", "handlebars", "php", "phtml", "twig", "njk", "liquid"];
|
|
9751
|
+
function fileKind(path) {
|
|
9752
|
+
const ext = path.toLowerCase().split(".").pop() ?? "";
|
|
9753
|
+
if (TEMPLATE_EXTENSIONS.includes(ext)) return "template";
|
|
9754
|
+
if (["tsx", "jsx", "ts", "js", "mjs", "cjs", "mdx"].includes(ext)) return "jsx";
|
|
9755
|
+
if (ext === "vue") return "vue";
|
|
9756
|
+
if (ext === "svelte") return "svelte";
|
|
9757
|
+
if (["html", "htm", "astro"].includes(ext)) return "html";
|
|
9758
|
+
if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
|
|
9759
|
+
return "other";
|
|
9760
|
+
}
|
|
9761
|
+
var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
|
|
9762
|
+
var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
|
|
9763
|
+
var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
|
|
9764
|
+
function fileFramework(path, kind, code, project) {
|
|
9765
|
+
const p = path.toLowerCase();
|
|
9766
|
+
switch (kind) {
|
|
9767
|
+
case "vue":
|
|
9768
|
+
return "vue";
|
|
9769
|
+
case "svelte":
|
|
9770
|
+
return "web-components";
|
|
9771
|
+
case "template":
|
|
9772
|
+
return "html";
|
|
9773
|
+
case "css":
|
|
9774
|
+
return project ?? "react";
|
|
9775
|
+
case "html":
|
|
9776
|
+
if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
|
|
9777
|
+
return project === "vue" || project === "web-components" ? project : "html";
|
|
9778
|
+
case "jsx":
|
|
9779
|
+
if (ANGULAR_COMPONENT.test(code)) return "angular";
|
|
9780
|
+
if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
|
|
9781
|
+
return project ?? "react";
|
|
9782
|
+
default:
|
|
9783
|
+
return project ?? "react";
|
|
9648
9784
|
}
|
|
9649
|
-
tokens.sort((a, b) => a.path.localeCompare(b.path, "en", { numeric: true }));
|
|
9650
|
-
return { format, tokens: buildDtcg(tokens), count: tokens.length, primitives, notes };
|
|
9651
9785
|
}
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
return
|
|
9664
|
-
}
|
|
9665
|
-
const
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
const
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
for (const col of Object.values(cols)) {
|
|
9679
|
-
if (col.remote) continue;
|
|
9680
|
-
const dark = col.modes.find((m) => DARK.test(m.name));
|
|
9681
|
-
const others = col.modes.filter((m) => m.modeId !== col.defaultModeId && m !== dark);
|
|
9682
|
-
if (others.length) {
|
|
9683
|
-
const def = col.modes.find((m) => m.modeId === col.defaultModeId)?.name;
|
|
9684
|
-
notes.push(`\u201C${col.name}\u201D has modes ${col.modes.map((m) => m.name).join(", ")}; used \u201C${def}\u201D${dark ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
|
|
9786
|
+
var blank = (s) => s.replace(/[^\n]/g, " ");
|
|
9787
|
+
function stripComments(code, kind) {
|
|
9788
|
+
let out = code;
|
|
9789
|
+
if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
|
|
9790
|
+
out = out.replace(/<!--[\s\S]*?-->/g, blank);
|
|
9791
|
+
}
|
|
9792
|
+
if (kind === "template") {
|
|
9793
|
+
out = out.replace(/\{\{!--[\s\S]*?--\}\}|\{\{![\s\S]*?\}\}|\{\{--[\s\S]*?--\}\}|\{#[\s\S]*?#\}|\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, blank).replace(/<\?(?:php|=)?[\s\S]*?(?:\?>|$)/g, (block) => block.replace(/\/\*[\s\S]*?\*\/|(?<![:"'])\/\/[^\n]*?(?=\?>|\n|$)|(?<=\s)#[^\n]*?(?=\?>|\n|$)/g, blank));
|
|
9794
|
+
return out;
|
|
9795
|
+
}
|
|
9796
|
+
if (kind === "css") {
|
|
9797
|
+
return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
|
|
9798
|
+
}
|
|
9799
|
+
const chars = out.split("");
|
|
9800
|
+
let i2 = 0;
|
|
9801
|
+
let quote = null;
|
|
9802
|
+
while (i2 < chars.length) {
|
|
9803
|
+
const c = chars[i2];
|
|
9804
|
+
const n = chars[i2 + 1];
|
|
9805
|
+
if (quote) {
|
|
9806
|
+
if (c === "\\") i2 += 2;
|
|
9807
|
+
else {
|
|
9808
|
+
if (c === quote) quote = null;
|
|
9809
|
+
i2++;
|
|
9810
|
+
}
|
|
9811
|
+
continue;
|
|
9685
9812
|
}
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
for (const v of Object.values(vars)) {
|
|
9690
|
-
const col = cols[v.variableCollectionId];
|
|
9691
|
-
if (!col || v.remote || col.remote) continue;
|
|
9692
|
-
if (v.hiddenFromPublishing) {
|
|
9693
|
-
hidden++;
|
|
9813
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
9814
|
+
quote = c;
|
|
9815
|
+
i2++;
|
|
9694
9816
|
continue;
|
|
9695
9817
|
}
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
if (c.type === "color") {
|
|
9703
|
-
const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
|
|
9704
|
-
if (hasDarkSomewhere) {
|
|
9705
|
-
const d = colorValue(resolve4(v, "dark"));
|
|
9706
|
-
if (d && d !== c.value) darkValue = d;
|
|
9707
|
-
}
|
|
9818
|
+
if (c === "/" && n === "*") {
|
|
9819
|
+
const end = out.indexOf("*/", i2 + 2);
|
|
9820
|
+
const stop = end === -1 ? chars.length : end + 2;
|
|
9821
|
+
for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
|
|
9822
|
+
i2 = stop;
|
|
9823
|
+
continue;
|
|
9708
9824
|
}
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
return finish("figma-variables", out, notes, opts);
|
|
9713
|
-
}
|
|
9714
|
-
function fromFigmaStyles(styles, opts = {}) {
|
|
9715
|
-
const out = [];
|
|
9716
|
-
const families = /* @__PURE__ */ new Map();
|
|
9717
|
-
let skipped = 0;
|
|
9718
|
-
for (const s of styles) {
|
|
9719
|
-
if (s.styleType === "FILL") {
|
|
9720
|
-
const fill = s.node?.fills?.find((f) => f.type === "SOLID" && f.visible !== false && f.color);
|
|
9721
|
-
if (!fill?.color) {
|
|
9722
|
-
skipped++;
|
|
9723
|
-
continue;
|
|
9724
|
-
}
|
|
9725
|
-
const c = classify(s.name, "color", { ...fill.color, a: (fill.color.a ?? 1) * (fill.opacity ?? 1) });
|
|
9726
|
-
if (c) out.push({ ...c, ...s.description ? { description: s.description } : {} });
|
|
9727
|
-
} else if (s.styleType === "TEXT" && s.node?.style) {
|
|
9728
|
-
const st = s.node.style;
|
|
9729
|
-
const name = segs(s.name).join("-");
|
|
9730
|
-
if (st.fontSize) out.push({ path: `font.size.${name}`, type: "dimension", value: dim(st.fontSize) });
|
|
9731
|
-
if (st.lineHeightPx) out.push({ path: `font.line-height.${name}`, type: "dimension", value: dim(Math.round(st.lineHeightPx)) });
|
|
9732
|
-
if (st.fontWeight) out.push({ path: `font.weight.${name}`, type: "fontWeight", value: String(st.fontWeight) });
|
|
9733
|
-
if (st.fontFamily) families.set(st.fontFamily, (families.get(st.fontFamily) ?? 0) + 1);
|
|
9825
|
+
if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
|
|
9826
|
+
while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
|
|
9827
|
+
continue;
|
|
9734
9828
|
}
|
|
9829
|
+
i2++;
|
|
9735
9830
|
}
|
|
9736
|
-
|
|
9737
|
-
fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
|
|
9738
|
-
const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
|
|
9739
|
-
return finish("figma-styles", out, notes, opts);
|
|
9740
|
-
}
|
|
9741
|
-
function plainLeaves(node2, prefix, out) {
|
|
9742
|
-
if (node2 === null || node2 === void 0) return;
|
|
9743
|
-
if (typeof node2 !== "object" || typeof node2 === "object" && "r" in node2 && "g" in node2) {
|
|
9744
|
-
out.push({ key: prefix, value: node2 });
|
|
9745
|
-
return;
|
|
9746
|
-
}
|
|
9747
|
-
for (const [k, v] of Object.entries(node2)) plainLeaves(v, [...prefix, k], out);
|
|
9831
|
+
return chars.join("");
|
|
9748
9832
|
}
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
|
|
9756
|
-
});
|
|
9757
|
-
const groups = [];
|
|
9758
|
-
for (const m of leafSets) {
|
|
9759
|
-
const g = groups.find((grp) => {
|
|
9760
|
-
const a = grp[0].keys;
|
|
9761
|
-
let same = 0;
|
|
9762
|
-
for (const k of m.keys) if (a.has(k)) same++;
|
|
9763
|
-
return same / Math.max(a.size, m.keys.size) >= 0.8;
|
|
9764
|
-
});
|
|
9765
|
-
if (g) g.push(m);
|
|
9766
|
-
else groups.push([m]);
|
|
9833
|
+
var LineIndex = class {
|
|
9834
|
+
starts = [0];
|
|
9835
|
+
text;
|
|
9836
|
+
constructor(text2) {
|
|
9837
|
+
this.text = text2;
|
|
9838
|
+
for (let i2 = 0; i2 < text2.length; i2++) if (text2.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
|
|
9767
9839
|
}
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
const
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
}
|
|
9776
|
-
});
|
|
9777
|
-
const pick2 = (grp, want) => want === "dark" ? grp.find((m) => DARK.test(m.name)) : grp.find((m) => LIGHT.test(m.name) && !DARK.test(m.name)) ?? grp.find((m) => !DARK.test(m.name)) ?? grp[0];
|
|
9778
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
9779
|
-
for (const m of leafSets) for (const l of m.leaves) if (typeof l.value === "string" && byLeaf.has(l.value)) referenced.add(l.value);
|
|
9780
|
-
const deref = (v, want, depth = 0) => {
|
|
9781
|
-
if (typeof v !== "string" || depth > 10) return v;
|
|
9782
|
-
const ref = byLeaf.get(v) ?? byLeaf.get(v.replace(REF, "$1"));
|
|
9783
|
-
if (!ref) return v;
|
|
9784
|
-
referenced.add(v);
|
|
9785
|
-
const grp = groups[ref.group];
|
|
9786
|
-
const mode = pick2(grp, want) ?? pick2(grp, "default");
|
|
9787
|
-
return deref(ref.values.get(mode.name) ?? [...ref.values.values()][0], want, depth + 1);
|
|
9788
|
-
};
|
|
9789
|
-
const out = [];
|
|
9790
|
-
const pending = [];
|
|
9791
|
-
for (const grp of groups) {
|
|
9792
|
-
const def = pick2(grp, "default");
|
|
9793
|
-
const dark = pick2(grp, "dark");
|
|
9794
|
-
if (grp.length > 1) notes.push(`${grp.map((m) => m.name).join(" / ")} look like modes of one collection; used \u201C${def.name}\u201D${dark && dark !== def ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
|
|
9795
|
-
for (const l of def.leaves) {
|
|
9796
|
-
const key = l.key[l.key.length - 1];
|
|
9797
|
-
const container = l.key.length > 1 ? l.key[l.key.length - 2].toLowerCase() : "";
|
|
9798
|
-
const value = deref(l.value, "default");
|
|
9799
|
-
const kind = container === "color" || colorValue(value) && /^#|^rgb|^hsl/i.test(String(value)) ? "color" : container === "string" || typeof value === "string" && !/^-?[\d.]+(px|rem|%)?$/.test(value) ? "string" : "number";
|
|
9800
|
-
const c = classify(key, kind, kind === "number" && typeof value === "string" && /px$/.test(value) ? parseFloat(value) : value, { context: def.name });
|
|
9801
|
-
if (!c) continue;
|
|
9802
|
-
let d;
|
|
9803
|
-
if (c.type === "color") {
|
|
9804
|
-
const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
|
|
9805
|
-
const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
|
|
9806
|
-
if (dv && dv !== c.value) d = dv;
|
|
9807
|
-
}
|
|
9808
|
-
pending.push({ key, t: { ...c, ...d ? { dark: d } : {} } });
|
|
9840
|
+
position(offset) {
|
|
9841
|
+
let lo = 0;
|
|
9842
|
+
let hi = this.starts.length - 1;
|
|
9843
|
+
while (lo < hi) {
|
|
9844
|
+
const mid = lo + hi + 1 >> 1;
|
|
9845
|
+
if (this.starts[mid] <= offset) lo = mid;
|
|
9846
|
+
else hi = mid - 1;
|
|
9809
9847
|
}
|
|
9848
|
+
return { line: lo + 1, column: offset - this.starts[lo] + 1 };
|
|
9810
9849
|
}
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
const ext = node2.$extensions;
|
|
9816
|
-
if (!ext) return void 0;
|
|
9817
|
-
if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
|
|
9818
|
-
const nested = ext["com.figma"];
|
|
9819
|
-
return nested?.[key];
|
|
9820
|
-
};
|
|
9821
|
-
function fileLeaves(node2, prefix, out, inherited) {
|
|
9822
|
-
if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
|
|
9823
|
-
const o = node2;
|
|
9824
|
-
const groupType = o.$type ?? inherited;
|
|
9825
|
-
if ("$value" in o) {
|
|
9826
|
-
const alias = figmaExt(o, "aliasData");
|
|
9827
|
-
const scopes = figmaExt(o, "scopes");
|
|
9828
|
-
out.push({
|
|
9829
|
-
path: prefix,
|
|
9830
|
-
type: o.$type ?? groupType,
|
|
9831
|
-
value: o.$value,
|
|
9832
|
-
description: o.$description,
|
|
9833
|
-
scopes: Array.isArray(scopes) ? scopes : void 0,
|
|
9834
|
-
aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
|
|
9835
|
-
hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
|
|
9836
|
-
});
|
|
9837
|
-
return;
|
|
9850
|
+
lineText(line) {
|
|
9851
|
+
const start = this.starts[line - 1] ?? 0;
|
|
9852
|
+
const end = this.starts[line] ?? this.text.length + 1;
|
|
9853
|
+
return this.text.slice(start, end - 1);
|
|
9838
9854
|
}
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
|
|
9855
|
+
get lineCount() {
|
|
9856
|
+
return this.starts.length;
|
|
9842
9857
|
}
|
|
9843
|
-
}
|
|
9844
|
-
var hasFigmaExt = (node2, depth = 0) => {
|
|
9845
|
-
if (!node2 || typeof node2 !== "object" || depth > 12) return false;
|
|
9846
|
-
const ext = node2.$extensions;
|
|
9847
|
-
if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
|
|
9848
|
-
return Object.values(node2).some((v) => hasFigmaExt(v, depth + 1));
|
|
9849
9858
|
};
|
|
9850
|
-
function
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
+
function globToRegExp(glob) {
|
|
9860
|
+
let re = "";
|
|
9861
|
+
for (let i2 = 0; i2 < glob.length; i2++) {
|
|
9862
|
+
const c = glob[i2];
|
|
9863
|
+
if (c === "*") {
|
|
9864
|
+
if (glob[i2 + 1] === "*") {
|
|
9865
|
+
const slash = glob[i2 + 2] === "/";
|
|
9866
|
+
re += slash ? "(?:.*/)?" : ".*";
|
|
9867
|
+
i2 += slash ? 2 : 1;
|
|
9868
|
+
} else re += "[^/]*";
|
|
9869
|
+
} else if (c === "?") re += "[^/]";
|
|
9870
|
+
else if (c === "{") {
|
|
9871
|
+
const end = glob.indexOf("}", i2);
|
|
9872
|
+
re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
9873
|
+
i2 = end;
|
|
9874
|
+
} else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
9875
|
+
}
|
|
9876
|
+
return new RegExp(`^${re}$`);
|
|
9877
|
+
}
|
|
9878
|
+
function makeIgnore(globs) {
|
|
9879
|
+
const res = globs.map(globToRegExp);
|
|
9880
|
+
return (path) => {
|
|
9881
|
+
const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9882
|
+
return res.some((r) => r.test(p) || r.test(`/${p}`));
|
|
9859
9883
|
};
|
|
9860
9884
|
}
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
const
|
|
9867
|
-
|
|
9868
|
-
const
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9885
|
+
|
|
9886
|
+
// ../engine/src/check.ts
|
|
9887
|
+
var ENGINE_VERSION = "0.9.0";
|
|
9888
|
+
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
9889
|
+
function computeScore(counts, lines) {
|
|
9890
|
+
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
9891
|
+
if (penalty === 0) return 100;
|
|
9892
|
+
const perK = penalty / Math.max(1, lines / 1e3);
|
|
9893
|
+
return Math.max(0, Math.min(99, Math.round(100 / (1 + perK / 25))));
|
|
9894
|
+
}
|
|
9895
|
+
function check(files, configInput, opts = {}) {
|
|
9896
|
+
const t0 = Date.now();
|
|
9897
|
+
const config2 = "replaces" in configInput ? configInput : resolveConfig(configInput);
|
|
9898
|
+
const ignored = makeIgnore(config2.ignore);
|
|
9899
|
+
const violations = [];
|
|
9900
|
+
let filesScanned = 0;
|
|
9901
|
+
let linesScanned = 0;
|
|
9902
|
+
for (const file2 of files) {
|
|
9903
|
+
const path = file2.path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9904
|
+
const kind = fileKind(path);
|
|
9905
|
+
if (!SCANNABLE.has(kind) || ignored(path)) continue;
|
|
9906
|
+
const only = opts.onlyLines?.get(path);
|
|
9907
|
+
if (opts.onlyLines && (!only || only.size === 0)) continue;
|
|
9908
|
+
filesScanned++;
|
|
9909
|
+
const idx = new LineIndex(file2.content);
|
|
9910
|
+
linesScanned += only ? only.size : file2.content.split("\n").filter((l) => l.trim()).length;
|
|
9911
|
+
const code = stripComments(file2.content, kind);
|
|
9912
|
+
const framework = fileFramework(path, kind, code, config2.framework);
|
|
9913
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9914
|
+
for (const rule of RULES) {
|
|
9915
|
+
const setting = config2.rules[rule.id];
|
|
9916
|
+
if (setting === "off" || !rule.kinds.includes(kind)) continue;
|
|
9917
|
+
for (const f of rule.check({ path, kind, code, config: config2, framework })) {
|
|
9918
|
+
const { line, column } = idx.position(f.offset);
|
|
9919
|
+
if (only && !only.has(line)) continue;
|
|
9920
|
+
const key = `${rule.id}:${line}:${column}`;
|
|
9921
|
+
if (seen.has(key)) continue;
|
|
9922
|
+
seen.add(key);
|
|
9923
|
+
violations.push({
|
|
9924
|
+
ruleId: rule.id,
|
|
9925
|
+
severity: setting ?? f.severity ?? rule.defaultSeverity,
|
|
9926
|
+
file: path,
|
|
9927
|
+
line,
|
|
9928
|
+
column,
|
|
9929
|
+
message: f.message,
|
|
9930
|
+
suggestion: f.suggestion,
|
|
9931
|
+
snippet: opts.snippets === false ? void 0 : idx.lineText(line).trim().slice(0, 160)
|
|
9932
|
+
});
|
|
9933
|
+
}
|
|
9878
9934
|
}
|
|
9879
|
-
const read2 = (f, into) => {
|
|
9880
|
-
const leaves3 = [];
|
|
9881
|
-
fileLeaves(f.data, "", leaves3);
|
|
9882
|
-
for (const l of leaves3) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
|
|
9883
|
-
};
|
|
9884
|
-
read2(def, light);
|
|
9885
|
-
if (dark && dark !== def) read2(dark, darkOverrides);
|
|
9886
9935
|
}
|
|
9887
|
-
|
|
9888
|
-
const
|
|
9889
|
-
const
|
|
9890
|
-
const
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
|
|
9894
|
-
if (m) referenced.add(m[1]);
|
|
9936
|
+
violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
|
|
9937
|
+
const counts = { error: 0, warning: 0, info: 0 };
|
|
9938
|
+
const perRule = /* @__PURE__ */ new Map();
|
|
9939
|
+
for (const v of violations) {
|
|
9940
|
+
counts[v.severity]++;
|
|
9941
|
+
perRule.set(v.ruleId, (perRule.get(v.ruleId) ?? 0) + 1);
|
|
9895
9942
|
}
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9943
|
+
return {
|
|
9944
|
+
engineVersion: ENGINE_VERSION,
|
|
9945
|
+
tokenVersion: config2.tokenVersion,
|
|
9946
|
+
score: computeScore(counts, linesScanned),
|
|
9947
|
+
counts,
|
|
9948
|
+
filesScanned,
|
|
9949
|
+
linesScanned,
|
|
9950
|
+
violations,
|
|
9951
|
+
byRule: [...perRule].map(([ruleId, count]) => ({ ruleId, count })).sort((a, b) => b.count - a.count),
|
|
9952
|
+
durationMs: Date.now() - t0
|
|
9953
|
+
};
|
|
9954
|
+
}
|
|
9955
|
+
|
|
9956
|
+
// ../engine/src/report.ts
|
|
9957
|
+
var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
|
|
9958
|
+
var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
|
|
9959
|
+
var ansi = (on) => {
|
|
9960
|
+
const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
9961
|
+
return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
|
|
9962
|
+
};
|
|
9963
|
+
var rendered = (r) => r.violations.length > 0 && r.violations.every((v) => v.line === 0);
|
|
9964
|
+
function formatPretty(report, { color = true, max: max2 = 200 } = {}) {
|
|
9965
|
+
const c = ansi(color);
|
|
9966
|
+
const sev = { error: c.red, warning: c.yellow, info: c.cyan };
|
|
9967
|
+
const lines = [];
|
|
9968
|
+
let lastFile = "";
|
|
9969
|
+
for (const v of report.violations.slice(0, max2)) {
|
|
9970
|
+
if (v.file !== lastFile) {
|
|
9971
|
+
lines.push("", c.bold(v.file));
|
|
9972
|
+
lastFile = v.file;
|
|
9919
9973
|
}
|
|
9920
|
-
|
|
9974
|
+
const rendered2 = v.line === 0;
|
|
9975
|
+
lines.push(` ${c.dim(rendered2 ? "".padEnd(8) : `${v.line}:${v.column}`.padEnd(8))} ${sev[v.severity](v.severity.padEnd(7))} ${v.message} ${c.dim(v.ruleId)}`);
|
|
9976
|
+
if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
|
|
9977
|
+
if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
|
|
9921
9978
|
}
|
|
9922
|
-
if (
|
|
9923
|
-
|
|
9979
|
+
if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
|
|
9980
|
+
const s = report.score;
|
|
9981
|
+
const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
|
|
9982
|
+
lines.push(
|
|
9983
|
+
"",
|
|
9984
|
+
`${c.bold("Brand score")} ${scoreStr}/100 ${c.dim("\xB7")} ${c.red(`${report.counts.error} errors`)} ${c.dim("\xB7")} ${c.yellow(`${report.counts.warning} warnings`)} ${c.dim("\xB7")} ${c.cyan(`${report.counts.info} info`)}`,
|
|
9985
|
+
c.dim(`${report.filesScanned} ${rendered(report) ? "pages" : "files"}, ${report.linesScanned} ${rendered(report) ? "elements" : "lines"} in ${report.durationMs}ms${report.tokenVersion ? ` \xB7 Brand Kit v${report.tokenVersion}` : ""}`)
|
|
9986
|
+
);
|
|
9987
|
+
return lines.join("\n");
|
|
9924
9988
|
}
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
const studio = !hasDollarValue(o);
|
|
9935
|
-
const flat = flattenTokens(o);
|
|
9936
|
-
if (!flat.length) throw new Error("No tokens found in that file.");
|
|
9937
|
-
const out = flat.map((t) => {
|
|
9938
|
-
if (t.type === "color" && !t.path.startsWith("color.")) return { ...t, path: `color.${t.path}` };
|
|
9939
|
-
return t;
|
|
9940
|
-
});
|
|
9941
|
-
const notes = studio ? [] : [];
|
|
9942
|
-
const res = finish(studio ? "tokens-studio" : "dtcg", out, notes, opts);
|
|
9943
|
-
return res;
|
|
9944
|
-
}
|
|
9945
|
-
const leaves3 = [];
|
|
9946
|
-
plainLeaves(o, [], leaves3);
|
|
9947
|
-
if (leaves3.length) return fromModeObject(o, opts);
|
|
9948
|
-
throw new Error("Couldn't find any tokens. Supported: DTCG, Tokens Studio, Figma variables JSON.");
|
|
9989
|
+
var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
9990
|
+
var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
9991
|
+
function formatGithubAnnotations(report, max2 = 50) {
|
|
9992
|
+
const level = { error: "error", warning: "warning", info: "notice" };
|
|
9993
|
+
return report.violations.slice(0, max2).map(
|
|
9994
|
+
(v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
|
|
9995
|
+
v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
|
|
9996
|
+
)}`
|
|
9997
|
+
).join("\n");
|
|
9949
9998
|
}
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
const
|
|
9953
|
-
const
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9999
|
+
var COMMENT_MARKER = "<!-- brandrail-report -->";
|
|
10000
|
+
function formatMarkdown(report, opts = {}) {
|
|
10001
|
+
const { threshold, maxRows = 30 } = opts;
|
|
10002
|
+
const pass = threshold === void 0 || report.score >= threshold;
|
|
10003
|
+
const badge = report.violations.length === 0 ? "\u{1F7E2}" : pass ? "\u{1F7E1}" : "\u{1F534}";
|
|
10004
|
+
const out = [COMMENT_MARKER];
|
|
10005
|
+
out.push(`## ${badge} Brand score: **${report.score}**/100${threshold !== void 0 ? ` (threshold ${threshold})` : ""}`);
|
|
10006
|
+
out.push("");
|
|
10007
|
+
out.push(
|
|
10008
|
+
`**${report.counts.error}** errors \xB7 **${report.counts.warning}** warnings \xB7 **${report.counts.info}** info \u2014 ${report.filesScanned} files scanned${report.tokenVersion ? ` against Brand Kit v${report.tokenVersion}` : ""}`
|
|
10009
|
+
);
|
|
10010
|
+
if (report.violations.length === 0) {
|
|
10011
|
+
out.push("", "Everything uses approved tokens and components. \u2728");
|
|
10012
|
+
} else {
|
|
10013
|
+
if (report.byRule.length) {
|
|
10014
|
+
out.push("", "| Rule | Count |", "| --- | ---: |");
|
|
10015
|
+
for (const r of report.byRule) out.push(`| ${title(r.ruleId)} \`${r.ruleId}\` | ${r.count} |`);
|
|
10016
|
+
}
|
|
10017
|
+
out.push("", "<details><summary>Violations</summary>", "", "| | Location | Issue | Fix |", "| --- | --- | --- | --- |");
|
|
10018
|
+
const icon = { error: "\u{1F534}", warning: "\u{1F7E1}", info: "\u{1F535}" };
|
|
10019
|
+
for (const v of report.violations.slice(0, maxRows)) {
|
|
10020
|
+
const loc = v.line === 0 ? `${v.file} ${v.snippet ?? ""}`.trim() : `${v.file}:${v.line}`;
|
|
10021
|
+
const url2 = opts.fileUrl?.(v);
|
|
10022
|
+
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
10023
|
+
out.push(`| ${icon[v.severity]} | ${url2 ? `[\`${loc}\`](${url2})` : `\`${loc}\``} | ${cell(v.message)} | ${cell(v.suggestion ?? "")} |`);
|
|
10024
|
+
}
|
|
10025
|
+
if (report.violations.length > maxRows) out.push("", `\u2026and ${report.violations.length - maxRows} more.`);
|
|
10026
|
+
out.push("", "</details>");
|
|
9959
10027
|
}
|
|
9960
|
-
|
|
9961
|
-
return
|
|
9962
|
-
}
|
|
9963
|
-
function mergeTokens(base, incoming) {
|
|
9964
|
-
const m = new Map(flattenTokens(base).map((t) => [t.path, t]));
|
|
9965
|
-
for (const t of flattenTokens(incoming)) m.set(t.path, { ...m.get(t.path), ...t, description: t.description ?? m.get(t.path)?.description });
|
|
9966
|
-
return buildDtcg([...m.values()]);
|
|
10028
|
+
out.push("", `<sub>Brandrail engine ${report.engineVersion}${opts.dashboardUrl ? ` \xB7 [Open in Brandrail](${opts.dashboardUrl})` : ""}</sub>`);
|
|
10029
|
+
return out.join("\n");
|
|
9967
10030
|
}
|
|
9968
10031
|
|
|
10032
|
+
// ../engine/src/defaults.ts
|
|
10033
|
+
var starterConfig = {
|
|
10034
|
+
$schema: "https://brandrail.dev/schema/config.json",
|
|
10035
|
+
tokenVersion: "0.0.0-local",
|
|
10036
|
+
tokens: {
|
|
10037
|
+
color: {
|
|
10038
|
+
action: { primary: { $type: "color", $value: "#1f883d", $description: "One primary action per view" } },
|
|
10039
|
+
accent: { default: { $type: "color", $value: "#0969da", $description: "Links, focus, selection" } },
|
|
10040
|
+
fg: {
|
|
10041
|
+
default: { $type: "color", $value: "#1f2328", $description: "Body text" },
|
|
10042
|
+
muted: { $type: "color", $value: "#59636e", $description: "Secondary text" }
|
|
10043
|
+
},
|
|
10044
|
+
canvas: {
|
|
10045
|
+
default: { $type: "color", $value: "#ffffff", $description: "Page background" },
|
|
10046
|
+
subtle: { $type: "color", $value: "#f6f8fa", $description: "Headers, wells" }
|
|
10047
|
+
},
|
|
10048
|
+
border: { default: { $type: "color", $value: "#d1d9e0", $description: "Borders and dividers" } },
|
|
10049
|
+
danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } },
|
|
10050
|
+
success: { default: { $type: "color", $value: "#1a7f37", $description: "Success messages, completed states" } },
|
|
10051
|
+
attention: { default: { $type: "color", $value: "#9a6700", $description: "Warnings, needs attention" } }
|
|
10052
|
+
},
|
|
10053
|
+
font: {
|
|
10054
|
+
family: { sans: { $type: "fontFamily", $value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif" } },
|
|
10055
|
+
size: { base: { $type: "dimension", $value: "14px" } }
|
|
10056
|
+
},
|
|
10057
|
+
radius: { md: { $type: "dimension", $value: "6px" } },
|
|
10058
|
+
space: { unit: { $type: "dimension", $value: "8px" } }
|
|
10059
|
+
},
|
|
10060
|
+
components: [
|
|
10061
|
+
{ name: "Button", importPath: "@acme/ui" },
|
|
10062
|
+
{ name: "TextInput", importPath: "@acme/ui" },
|
|
10063
|
+
{ name: "Select", importPath: "@acme/ui" },
|
|
10064
|
+
{ name: "Textarea", importPath: "@acme/ui" }
|
|
10065
|
+
],
|
|
10066
|
+
rules: {},
|
|
10067
|
+
ignore: []
|
|
10068
|
+
};
|
|
10069
|
+
|
|
9969
10070
|
// ../engine/src/instructions.ts
|
|
9970
10071
|
var KIT_PACKAGE = "@brandrail/kit";
|
|
9971
10072
|
var RULES_START = "<!-- brandrail:start -->";
|
|
@@ -9996,8 +10097,9 @@ function generateInstructions(config2, opts = {}) {
|
|
|
9996
10097
|
}
|
|
9997
10098
|
lines.push("");
|
|
9998
10099
|
}
|
|
9999
|
-
|
|
10000
|
-
|
|
10100
|
+
const active = r.components.filter((c) => c.status !== "deprecated");
|
|
10101
|
+
if (active.length) {
|
|
10102
|
+
const usable = active.map((c) => ({ c, u: resolveUsage(c, fw) }));
|
|
10001
10103
|
const here = usable.filter((x2) => x2.u);
|
|
10002
10104
|
const missing2 = usable.filter((x2) => !x2.u).map((x2) => x2.c.name);
|
|
10003
10105
|
lines.push("## Components");
|
|
@@ -10031,7 +10133,7 @@ function generateInstructions(config2, opts = {}) {
|
|
|
10031
10133
|
lines.push("## Colours");
|
|
10032
10134
|
lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
|
|
10033
10135
|
for (const c of r.colors) {
|
|
10034
|
-
if (c.mode) continue;
|
|
10136
|
+
if (c.mode || r.deprecatedTokens.has(c.cssVar)) continue;
|
|
10035
10137
|
const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
|
|
10036
10138
|
lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
|
|
10037
10139
|
}
|
|
@@ -10047,6 +10149,14 @@ function generateInstructions(config2, opts = {}) {
|
|
|
10047
10149
|
if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
|
|
10048
10150
|
if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
|
|
10049
10151
|
lines.push("");
|
|
10152
|
+
const depComps = r.components.filter((c) => c.status === "deprecated");
|
|
10153
|
+
if (r.deprecatedTokens.size || depComps.length) {
|
|
10154
|
+
lines.push("## Deprecated: don't use in new code");
|
|
10155
|
+
lines.push("These still work until their removal date. Use the replacement, and switch existing uses when you touch that code (the Checker warns on each).");
|
|
10156
|
+
for (const d of r.deprecatedTokens.values()) lines.push(`- ${deprecationLine(d, true)}`);
|
|
10157
|
+
for (const c of depComps) lines.push(`- ${deprecationLine({ path: c.name, ...c.deprecation }, false)}`);
|
|
10158
|
+
lines.push("");
|
|
10159
|
+
}
|
|
10050
10160
|
lines.push("## New pages");
|
|
10051
10161
|
lines.push("- Building a new page or screen? Start from a page template: MCP `get_template` (list, detail, form, settings, dashboard, sign-in) or `npx brandrail template <name>`. It's built from these components and the spacing scale, and lists the page's rules (states, layout, accessibility). Rename its sample data; keep its structure.");
|
|
10052
10162
|
lines.push("");
|
|
@@ -10803,7 +10913,8 @@ function mergeProjectConfig(prev, kit) {
|
|
|
10803
10913
|
rules: { ...prev.rules, ...kit.rules },
|
|
10804
10914
|
ignore: prev.ignore ?? kit.ignore,
|
|
10805
10915
|
tokensCss: prev.tokensCss,
|
|
10806
|
-
framework: prev.framework ?? kit.framework
|
|
10916
|
+
framework: prev.framework ?? kit.framework,
|
|
10917
|
+
brandKitVersion: prev.brandKitVersion
|
|
10807
10918
|
};
|
|
10808
10919
|
}
|
|
10809
10920
|
function tokensCssContent(config2, org) {
|
|
@@ -11740,8 +11851,8 @@ async function call(path, init) {
|
|
|
11740
11851
|
}
|
|
11741
11852
|
return body;
|
|
11742
11853
|
}
|
|
11743
|
-
function fetchBrandKit(key) {
|
|
11744
|
-
return call(
|
|
11854
|
+
function fetchBrandKit(key, version2) {
|
|
11855
|
+
return call(`/api/v1/brand-kit${version2 ? `?version=${encodeURIComponent(version2)}` : ""}`, { method: "GET", key });
|
|
11745
11856
|
}
|
|
11746
11857
|
function uploadReport(key, payload) {
|
|
11747
11858
|
const report = payload.uploadSnippets ? payload.report : { ...payload.report, violations: payload.report.violations.map(({ snippet: _snippet, ...v }) => v) };
|
|
@@ -12015,7 +12126,7 @@ __export(external_exports, {
|
|
|
12015
12126
|
decode: () => decode2,
|
|
12016
12127
|
decodeAsync: () => decodeAsync2,
|
|
12017
12128
|
deepPartial: () => deepPartial,
|
|
12018
|
-
describe: () =>
|
|
12129
|
+
describe: () => describe3,
|
|
12019
12130
|
discriminatedUnion: () => discriminatedUnion,
|
|
12020
12131
|
e164: () => e1642,
|
|
12021
12132
|
email: () => email2,
|
|
@@ -12421,7 +12532,7 @@ __export(core_exports2, {
|
|
|
12421
12532
|
createToJSONSchemaMethod: () => createToJSONSchemaMethod,
|
|
12422
12533
|
decode: () => decode,
|
|
12423
12534
|
decodeAsync: () => decodeAsync,
|
|
12424
|
-
describe: () =>
|
|
12535
|
+
describe: () => describe2,
|
|
12425
12536
|
encode: () => encode,
|
|
12426
12537
|
encodeAsync: () => encodeAsync,
|
|
12427
12538
|
extractDefs: () => extractDefs,
|
|
@@ -27411,7 +27522,7 @@ function _check(fn, params) {
|
|
|
27411
27522
|
return ch;
|
|
27412
27523
|
}
|
|
27413
27524
|
// @__NO_SIDE_EFFECTS__
|
|
27414
|
-
function
|
|
27525
|
+
function describe2(description) {
|
|
27415
27526
|
const ch = new $ZodCheck({ check: "describe" });
|
|
27416
27527
|
ch._zod.onattach = [
|
|
27417
27528
|
(inst) => {
|
|
@@ -28965,7 +29076,7 @@ __export(schemas_exports2, {
|
|
|
28965
29076
|
currencyCode: () => currencyCode2,
|
|
28966
29077
|
custom: () => custom,
|
|
28967
29078
|
date: () => date2,
|
|
28968
|
-
describe: () =>
|
|
29079
|
+
describe: () => describe3,
|
|
28969
29080
|
discriminatedUnion: () => discriminatedUnion,
|
|
28970
29081
|
e164: () => e1642,
|
|
28971
29082
|
email: () => email2,
|
|
@@ -30557,7 +30668,7 @@ function refine(fn, _params = {}) {
|
|
|
30557
30668
|
function superRefine(fn, params) {
|
|
30558
30669
|
return _superRefine(fn, params);
|
|
30559
30670
|
}
|
|
30560
|
-
var
|
|
30671
|
+
var describe3 = describe2;
|
|
30561
30672
|
var meta2 = meta;
|
|
30562
30673
|
var ZodInstanceOf = /* @__PURE__ */ $constructor("ZodInstanceOf", (inst, def) => {
|
|
30563
30674
|
ZodCustom.init(inst, def);
|
|
@@ -41315,6 +41426,15 @@ function componentDoc(c, fw) {
|
|
|
41315
41426
|
const u = resolveUsage(c, fw);
|
|
41316
41427
|
const others = availableFrameworks(c).filter((f) => f !== fw).map((f) => FRAMEWORK_LABEL[f]);
|
|
41317
41428
|
const title2 = `### ${c.name}${c.status && c.status !== "stable" ? ` (${c.status})` : ""}`;
|
|
41429
|
+
if (c.status === "deprecated") {
|
|
41430
|
+
const d = c.deprecation ?? {};
|
|
41431
|
+
return [
|
|
41432
|
+
title2,
|
|
41433
|
+
"",
|
|
41434
|
+
`Deprecated${d.removeAfter ? `: removed after ${d.removeAfter}` : ""}. Don't use it in new code${d.replacement ? `; use ${d.replacement} (get_component_docs "${d.replacement}")` : ""}.${d.reason ? ` ${d.reason}` : ""}`,
|
|
41435
|
+
"When you edit code that uses it, switch to the replacement."
|
|
41436
|
+
].join("\n");
|
|
41437
|
+
}
|
|
41318
41438
|
if (!u) {
|
|
41319
41439
|
return [
|
|
41320
41440
|
title2,
|
|
@@ -41382,7 +41502,11 @@ function createBrandrailMcp(ctx) {
|
|
|
41382
41502
|
const pick2 = category === "all" ? all : all.filter(CATEGORY[category]);
|
|
41383
41503
|
const lines = [`# ${ctx.org} design tokens${ctx.config.tokenVersion ? ` (Brand Kit v${ctx.config.tokenVersion})` : ""}`, ""];
|
|
41384
41504
|
lines.push("| Token | Use | Value | Meaning |", "| --- | --- | --- | --- |");
|
|
41385
|
-
for (const l of pick2)
|
|
41505
|
+
for (const l of pick2) {
|
|
41506
|
+
const dep = resolved.deprecatedTokens.get(`--${l.path.replace(/\./g, "-")}`);
|
|
41507
|
+
const note = dep ? `**Deprecated**${dep.replacement ? `: use \`${cssVar2(dep.replacement)}\`` : ""}${dep.removeAfter ? ` (removed after ${dep.removeAfter})` : ""}.` : l.description ?? "";
|
|
41508
|
+
lines.push(`| ${l.path} | \`${cssVar2(l.path)}\` | ${String(l.value)}${l.dark ? ` (dark ${String(l.dark)})` : ""} | ${note} |`);
|
|
41509
|
+
}
|
|
41386
41510
|
if (pick2.some((l) => l.dark)) lines.push("", "Colours with a dark value switch automatically: always use the variable, never the hex, so both themes work.");
|
|
41387
41511
|
if (category === "all" || category === "spacing") lines.push("", `Allowed spacing (px): ${resolved.spacing.filter((s) => s > 2).join(", ")}`);
|
|
41388
41512
|
if (category === "all" || category === "radius") lines.push(`Allowed radius (px): ${resolved.radii.filter((r) => r > 0 && r < 999).join(", ")}`);
|
|
@@ -41618,10 +41742,10 @@ function cachePath(key) {
|
|
|
41618
41742
|
const id = createHash2("sha256").update(`${apiBase()}|${key}`).digest("hex").slice(0, 16);
|
|
41619
41743
|
return join4(base, "brandrail", `brand-kit-${id}.json`);
|
|
41620
41744
|
}
|
|
41621
|
-
async function loadKit(key) {
|
|
41622
|
-
const p = cachePath(key);
|
|
41745
|
+
async function loadKit(key, version2) {
|
|
41746
|
+
const p = cachePath(`${key}${version2 ? `@${version2}` : ""}`);
|
|
41623
41747
|
try {
|
|
41624
|
-
const kit = await fetchBrandKit(key);
|
|
41748
|
+
const kit = await fetchBrandKit(key, version2);
|
|
41625
41749
|
mkdirSync2(dirname3(p), { recursive: true, mode: 448 });
|
|
41626
41750
|
writeFileSync2(p, JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), ...kit }), { mode: 384 });
|
|
41627
41751
|
return kit;
|
|
@@ -41639,7 +41763,7 @@ async function runLocalMcp(cwd, opts) {
|
|
|
41639
41763
|
const configPath = findConfig(cwd);
|
|
41640
41764
|
const local = configPath ? readConfig(configPath) : void 0;
|
|
41641
41765
|
const key = resolveKey();
|
|
41642
|
-
const kit = key ? await loadKit(key) : null;
|
|
41766
|
+
const kit = key ? await loadKit(key, local?.brandKitVersion) : null;
|
|
41643
41767
|
let config2;
|
|
41644
41768
|
let org;
|
|
41645
41769
|
let published;
|
|
@@ -42111,9 +42235,11 @@ async function cmdInit(cwd, flags) {
|
|
|
42111
42235
|
if (flags["from-api"]) {
|
|
42112
42236
|
const key = resolveKey();
|
|
42113
42237
|
if (!key) throw new Error(`--from-api: ${NO_KEY}`);
|
|
42114
|
-
const
|
|
42115
|
-
|
|
42238
|
+
const pinned = existsSync7(target) ? readConfig(target).brandKitVersion : void 0;
|
|
42239
|
+
const res = await fetchBrandKit(key, pinned);
|
|
42240
|
+
config2 = { $schema: starterConfig.$schema, ...res.config, ...pinned ? { brandKitVersion: pinned } : {} };
|
|
42116
42241
|
org = res.org;
|
|
42242
|
+
if (pinned && res.latestVersion && res.latestVersion !== pinned) log2(`Pinned to Brand Kit v${pinned} (v${res.latestVersion} is the latest). Remove "brandKitVersion" from ${CONFIG_FILE2} to follow the latest.`);
|
|
42117
42243
|
}
|
|
42118
42244
|
if (existsSync7(target) && !flags.force && !flags["from-api"]) {
|
|
42119
42245
|
log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);
|