@usefragments/core 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MLRDNSSA.js → chunk-CBSNXFOD.js} +1 -1
- package/dist/chunk-CBSNXFOD.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DYSirwJu.d.ts → governance-DxdJV6lx.d.ts} +10 -0
- package/dist/index.d.ts +97 -75
- package/dist/index.js +163 -30
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -0
- package/src/facts/builders.ts +1 -1
- package/src/facts/types.ts +14 -1
- package/src/rules/components-prefer-library.test.ts +148 -0
- package/src/rules/components-prefer-library.ts +52 -1
- package/src/rules/styles-no-raw-color.test.ts +213 -0
- package/src/rules/styles-no-raw-color.ts +125 -0
- package/src/schema.ts +6 -2
- package/src/token-types.ts +12 -0
- package/src/tokens/categories.ts +54 -24
- package/src/tokens/scss.test.ts +94 -0
- package/src/tokens/scss.ts +11 -4
- package/src/types.ts +11 -0
- package/dist/chunk-MLRDNSSA.js.map +0 -1
- package/dist/{index-h_yWj15D.d.ts → index-0lmh0Lbo.d.ts} +8 -8
package/dist/index.js
CHANGED
|
@@ -109,7 +109,7 @@ import {
|
|
|
109
109
|
ownedImportsEqual,
|
|
110
110
|
resolveComponentGovernance,
|
|
111
111
|
scaleGovernanceRecordSchema
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-CBSNXFOD.js";
|
|
113
113
|
import {
|
|
114
114
|
AGENT_FORMAT_SCHEMA_VERSION,
|
|
115
115
|
agentErrorEnvelopeSchema,
|
|
@@ -185,6 +185,8 @@ var BRAND = {
|
|
|
185
185
|
legacyConfigFile: "segments.config.ts",
|
|
186
186
|
/** CLI command name (e.g., "fragments") */
|
|
187
187
|
cliCommand: "fragments",
|
|
188
|
+
/** Full user-facing CLI invocation for copy printed to end users (e.g., "npx @usefragments/cli") */
|
|
189
|
+
cliInvocation: "npx @usefragments/cli",
|
|
188
190
|
/** Directory for storing fragments, registry, and cache */
|
|
189
191
|
dataDir: ".fragments",
|
|
190
192
|
/** Components subdirectory within .fragments/ */
|
|
@@ -1679,16 +1681,20 @@ var tokenSourceSchema = z4.object({
|
|
|
1679
1681
|
var tokenConfigSchema = z4.object({
|
|
1680
1682
|
include: repoRelativePathsSchema.optional(),
|
|
1681
1683
|
sources: z4.array(tokenSourceSchema).min(1).optional(),
|
|
1684
|
+
// Declared design-system packages whose shipped fragments.json token
|
|
1685
|
+
// vocabulary seeds the known-token set (#7c). A packages-only tokens block
|
|
1686
|
+
// is valid — see the superRefine below.
|
|
1687
|
+
packages: z4.array(z4.string().min(1)).optional(),
|
|
1682
1688
|
exclude: z4.array(repoRelativePathSchema).optional(),
|
|
1683
1689
|
themeSelectors: z4.record(z4.string()).optional(),
|
|
1684
1690
|
enabled: z4.boolean().optional(),
|
|
1685
1691
|
format: z4.enum(["auto", "css", "scss", "dtcg", "tailwind"]).optional(),
|
|
1686
1692
|
namespace: z4.string().min(1).optional()
|
|
1687
1693
|
}).passthrough().superRefine((tokens, ctx) => {
|
|
1688
|
-
if (!tokens.include?.length && !tokens.sources?.length) {
|
|
1694
|
+
if (!tokens.include?.length && !tokens.sources?.length && !tokens.packages?.length) {
|
|
1689
1695
|
ctx.addIssue({
|
|
1690
1696
|
code: z4.ZodIssueCode.custom,
|
|
1691
|
-
message: "Add tokens.include or tokens.
|
|
1697
|
+
message: "Add tokens.include, tokens.sources, or tokens.packages",
|
|
1692
1698
|
path: ["sources"]
|
|
1693
1699
|
});
|
|
1694
1700
|
}
|
|
@@ -2931,30 +2937,41 @@ function inferTokenGroup(name) {
|
|
|
2931
2937
|
return category;
|
|
2932
2938
|
}
|
|
2933
2939
|
}
|
|
2940
|
+
var TOKEN_GROUP_COMMENT_MAPPINGS = {
|
|
2941
|
+
"base configuration": "base",
|
|
2942
|
+
typography: "typography",
|
|
2943
|
+
"spacing (micro)": "spacing",
|
|
2944
|
+
spacing: "spacing",
|
|
2945
|
+
"density padding": "spacing",
|
|
2946
|
+
"border radius": "radius",
|
|
2947
|
+
transitions: "transitions",
|
|
2948
|
+
colors: "colors",
|
|
2949
|
+
surfaces: "surfaces",
|
|
2950
|
+
text: "text",
|
|
2951
|
+
borders: "borders",
|
|
2952
|
+
shadows: "shadows",
|
|
2953
|
+
focus: "focus",
|
|
2954
|
+
scrollbar: "scrollbar",
|
|
2955
|
+
"component heights": "component-sizing",
|
|
2956
|
+
"appshell layout": "layout",
|
|
2957
|
+
codeblock: "code",
|
|
2958
|
+
tooltip: "tooltip",
|
|
2959
|
+
"hero/marketing gradient": "marketing"
|
|
2960
|
+
};
|
|
2961
|
+
function looksLikeCleanHeading(text) {
|
|
2962
|
+
if (!/^[a-z0-9][a-z0-9 -]*$/.test(text)) return false;
|
|
2963
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
2964
|
+
return wordCount > 0 && wordCount <= 3;
|
|
2965
|
+
}
|
|
2934
2966
|
function normalizeTokenGroupComment(comment) {
|
|
2935
2967
|
const text = comment.trim().replace(/^\/\/\s*/, "").replace(/^\/\*+\s*/, "").replace(/\s*\*+\/$/, "").trim().toLowerCase();
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
transitions: "transitions",
|
|
2944
|
-
colors: "colors",
|
|
2945
|
-
surfaces: "surfaces",
|
|
2946
|
-
text: "text",
|
|
2947
|
-
borders: "borders",
|
|
2948
|
-
shadows: "shadows",
|
|
2949
|
-
focus: "focus",
|
|
2950
|
-
scrollbar: "scrollbar",
|
|
2951
|
-
"component heights": "component-sizing",
|
|
2952
|
-
"appshell layout": "layout",
|
|
2953
|
-
codeblock: "code",
|
|
2954
|
-
tooltip: "tooltip",
|
|
2955
|
-
"hero/marketing gradient": "marketing"
|
|
2956
|
-
};
|
|
2957
|
-
return mappings[text] ?? text.replace(/\s+/g, "-");
|
|
2968
|
+
if (text in TOKEN_GROUP_COMMENT_MAPPINGS) {
|
|
2969
|
+
return TOKEN_GROUP_COMMENT_MAPPINGS[text];
|
|
2970
|
+
}
|
|
2971
|
+
if (!looksLikeCleanHeading(text)) {
|
|
2972
|
+
return null;
|
|
2973
|
+
}
|
|
2974
|
+
return text.replace(/\s+/g, "-");
|
|
2958
2975
|
}
|
|
2959
2976
|
|
|
2960
2977
|
// src/tokens/scss.ts
|
|
@@ -3021,7 +3038,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
|
|
|
3021
3038
|
const tokens = [];
|
|
3022
3039
|
const seenNames = /* @__PURE__ */ new Set();
|
|
3023
3040
|
let currentCategory = "other";
|
|
3024
|
-
let
|
|
3041
|
+
let currentCategoryIsCurated = false;
|
|
3025
3042
|
const scssVars = parseScssVariables(content);
|
|
3026
3043
|
const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
|
|
3027
3044
|
const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
|
|
@@ -3031,7 +3048,9 @@ function parseScssTokens(content, filePath = "tokens.scss") {
|
|
|
3031
3048
|
const normalized = normalizeTokenGroupComment(commentMatch[0]);
|
|
3032
3049
|
if (normalized) {
|
|
3033
3050
|
currentCategory = normalized;
|
|
3034
|
-
|
|
3051
|
+
currentCategoryIsCurated = true;
|
|
3052
|
+
} else {
|
|
3053
|
+
currentCategoryIsCurated = false;
|
|
3035
3054
|
}
|
|
3036
3055
|
continue;
|
|
3037
3056
|
}
|
|
@@ -3047,7 +3066,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
|
|
|
3047
3066
|
tokens.push({
|
|
3048
3067
|
name,
|
|
3049
3068
|
value: cleanValue || void 0,
|
|
3050
|
-
category:
|
|
3069
|
+
category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name),
|
|
3051
3070
|
description
|
|
3052
3071
|
});
|
|
3053
3072
|
}
|
|
@@ -3058,7 +3077,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
|
|
|
3058
3077
|
tokens.push({
|
|
3059
3078
|
name,
|
|
3060
3079
|
value,
|
|
3061
|
-
category:
|
|
3080
|
+
category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name)
|
|
3062
3081
|
});
|
|
3063
3082
|
}
|
|
3064
3083
|
const cssVarValues = /* @__PURE__ */ new Map();
|
|
@@ -5057,11 +5076,13 @@ function ruleComponentsPreferLibrary(ix) {
|
|
|
5057
5076
|
const textByNode = indexTextChildrenByNodeId(ix);
|
|
5058
5077
|
const classTokensByNode = indexClassTokensByNode(ix);
|
|
5059
5078
|
const reimplTargets = canonicalReimplTargets(sources, mappings);
|
|
5079
|
+
const canonicalDirectoryResolvedNodes = indexCanonicalDirectoryResolvedNodes(ix, sources);
|
|
5060
5080
|
const findings = [];
|
|
5061
5081
|
const seenImportFixes = /* @__PURE__ */ new Set();
|
|
5062
5082
|
for (const node of ix.byKind("usage_node")) {
|
|
5063
5083
|
if (isCanonicalSourceImplementationFile(node.file, sources)) continue;
|
|
5064
5084
|
if (node.element.includes(".")) continue;
|
|
5085
|
+
if (canonicalDirectoryResolvedNodes.has(node.id)) continue;
|
|
5065
5086
|
const props = propsByNode.get(node.id) ?? [];
|
|
5066
5087
|
const textChildren = textByNode.get(node.id) ?? [];
|
|
5067
5088
|
const importsForFile = importsByFileAndLocal.get(node.file);
|
|
@@ -5587,7 +5608,27 @@ function sourceLabel(source) {
|
|
|
5587
5608
|
function canonicalImportPath(source) {
|
|
5588
5609
|
if (source.kind === "npm") return source.specifier;
|
|
5589
5610
|
if (source.kind === "registry") return source.importPath;
|
|
5590
|
-
return void 0;
|
|
5611
|
+
return source.include?.length ? source.path : void 0;
|
|
5612
|
+
}
|
|
5613
|
+
function indexCanonicalDirectoryResolvedNodes(ix, sources) {
|
|
5614
|
+
const out = /* @__PURE__ */ new Set();
|
|
5615
|
+
const directoryPaths = sources.filter(
|
|
5616
|
+
(source) => source.kind === "directory"
|
|
5617
|
+
).map((source) => normalizePath(source.path));
|
|
5618
|
+
if (directoryPaths.length === 0) return out;
|
|
5619
|
+
for (const fact of ix.byKind("usage_component")) {
|
|
5620
|
+
const module = componentIdModule(fact.componentId);
|
|
5621
|
+
if (module === void 0) continue;
|
|
5622
|
+
const normalizedModule = normalizePath(module);
|
|
5623
|
+
if (directoryPaths.some((path) => isPathInSource(normalizedModule, path))) {
|
|
5624
|
+
out.add(fact.nodeId);
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
return out;
|
|
5628
|
+
}
|
|
5629
|
+
function componentIdModule(componentId2) {
|
|
5630
|
+
const hash = componentId2.indexOf("#");
|
|
5631
|
+
return hash > 0 ? componentId2.slice(0, hash) : void 0;
|
|
5591
5632
|
}
|
|
5592
5633
|
function isCanonicalSourceImplementationFile(file, sources) {
|
|
5593
5634
|
const normalizedFile = normalizePath(file);
|
|
@@ -6228,6 +6269,7 @@ function ruleStylesNoRawColor(ix) {
|
|
|
6228
6269
|
const colorTokens = ix.tokens.byCategory("color");
|
|
6229
6270
|
const preferLabel = policy.prefer === "token" ? "design token" : "CSS variable";
|
|
6230
6271
|
const findings = [];
|
|
6272
|
+
const advisorySeverity = policy.severity === "error" ? "warn" : policy.severity;
|
|
6231
6273
|
for (const decl of ix.byKind("style_declaration")) {
|
|
6232
6274
|
if (decl.property.startsWith("--") && decl.declaredTokenSource === true) continue;
|
|
6233
6275
|
const color = detectRawColor(decl.value);
|
|
@@ -6267,6 +6309,37 @@ function ruleStylesNoRawColor(ix) {
|
|
|
6267
6309
|
}
|
|
6268
6310
|
const componentByNode = indexComponentByNodeId(ix);
|
|
6269
6311
|
for (const inline of ix.byKind("usage_inline_style")) {
|
|
6312
|
+
if (inline.valueKind === "dynamic-raw") {
|
|
6313
|
+
const node2 = readUsageNode(ix, inline.nodeId);
|
|
6314
|
+
if (!node2) continue;
|
|
6315
|
+
const componentEvidenceId2 = componentByNode.get(node2.id)?.id;
|
|
6316
|
+
const evidenceIds2 = componentEvidenceId2 ? [node2.id, componentEvidenceId2, inline.id, policy.id] : [node2.id, inline.id, policy.id];
|
|
6317
|
+
findings.push(
|
|
6318
|
+
makeFinding({
|
|
6319
|
+
ruleId: RULE_ID8,
|
|
6320
|
+
ruleVersion: RULE_VERSION9,
|
|
6321
|
+
severity: advisorySeverity,
|
|
6322
|
+
message: `Runtime-constructed raw color \`${inline.value}\` on inline \`${inline.property}\`. Use a ${preferLabel} instead.`,
|
|
6323
|
+
location: node2.location,
|
|
6324
|
+
evidence: ix.evidence(evidenceIds2),
|
|
6325
|
+
fingerprintIdentity: {
|
|
6326
|
+
source: "usage_inline_style_dynamic",
|
|
6327
|
+
file: node2.file,
|
|
6328
|
+
nodePath: node2.nodePath,
|
|
6329
|
+
element: node2.element,
|
|
6330
|
+
property: inline.property,
|
|
6331
|
+
value: inline.value
|
|
6332
|
+
},
|
|
6333
|
+
attributes: {
|
|
6334
|
+
property: inline.property,
|
|
6335
|
+
rawValue: inline.value,
|
|
6336
|
+
source: "jsx",
|
|
6337
|
+
dynamic: true
|
|
6338
|
+
}
|
|
6339
|
+
})
|
|
6340
|
+
);
|
|
6341
|
+
continue;
|
|
6342
|
+
}
|
|
6270
6343
|
if (inline.valueKind === "css-variable") continue;
|
|
6271
6344
|
const color = detectRawColor(inline.value);
|
|
6272
6345
|
if (!color) continue;
|
|
@@ -6307,8 +6380,68 @@ function ruleStylesNoRawColor(ix) {
|
|
|
6307
6380
|
})
|
|
6308
6381
|
);
|
|
6309
6382
|
}
|
|
6383
|
+
for (const prop of ix.byKind("usage_prop_resolved")) {
|
|
6384
|
+
if (prop.resolution !== "static") continue;
|
|
6385
|
+
if (typeof prop.value !== "string") continue;
|
|
6386
|
+
if (!SVG_PAINT_ATTRS.has(prop.prop)) continue;
|
|
6387
|
+
const color = detectRawColor(prop.value);
|
|
6388
|
+
if (!color) continue;
|
|
6389
|
+
if (isExemptColor(color, policy.except)) continue;
|
|
6390
|
+
const node = readUsageNode(ix, prop.nodeId);
|
|
6391
|
+
if (!node) continue;
|
|
6392
|
+
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
6393
|
+
const cssProperty = SVG_PAINT_CSS_PROPERTY[prop.prop] ?? prop.prop;
|
|
6394
|
+
const resolution = resolveColorToken(cssProperty, color, colorTokens);
|
|
6395
|
+
const fixEmission = resolution?.kind === "exact" ? buildTokenFix(cssProperty, prop.value, color, resolution, ix) : void 0;
|
|
6396
|
+
const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, prop.id, policy.id] : [node.id, prop.id, policy.id];
|
|
6397
|
+
const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
|
|
6398
|
+
findings.push(
|
|
6399
|
+
makeFinding({
|
|
6400
|
+
ruleId: RULE_ID8,
|
|
6401
|
+
ruleVersion: RULE_VERSION9,
|
|
6402
|
+
severity: advisorySeverity,
|
|
6403
|
+
message: colorMessage(color, `\`${prop.prop}\``, resolution, preferLabel),
|
|
6404
|
+
location: prop.location ?? node.location,
|
|
6405
|
+
evidence: ix.evidence(evidenceIds),
|
|
6406
|
+
fingerprintIdentity: {
|
|
6407
|
+
source: "usage_prop_resolved",
|
|
6408
|
+
file: node.file,
|
|
6409
|
+
nodePath: node.nodePath,
|
|
6410
|
+
element: node.element,
|
|
6411
|
+
property: prop.prop,
|
|
6412
|
+
value: color
|
|
6413
|
+
},
|
|
6414
|
+
fix: fixEmission?.fix,
|
|
6415
|
+
attributes: {
|
|
6416
|
+
property: prop.prop,
|
|
6417
|
+
rawValue: prop.value,
|
|
6418
|
+
color,
|
|
6419
|
+
source: "jsx",
|
|
6420
|
+
suggestedToken: resolution?.token.name,
|
|
6421
|
+
...resolution ? { tokenMatch: resolution.kind } : {},
|
|
6422
|
+
...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
|
|
6423
|
+
}
|
|
6424
|
+
})
|
|
6425
|
+
);
|
|
6426
|
+
}
|
|
6310
6427
|
return findings;
|
|
6311
6428
|
}
|
|
6429
|
+
var SVG_PAINT_ATTRS = /* @__PURE__ */ new Set([
|
|
6430
|
+
"fill",
|
|
6431
|
+
"stroke",
|
|
6432
|
+
"color",
|
|
6433
|
+
"stop-color",
|
|
6434
|
+
"stopColor",
|
|
6435
|
+
"flood-color",
|
|
6436
|
+
"floodColor",
|
|
6437
|
+
"lighting-color",
|
|
6438
|
+
"lightingColor"
|
|
6439
|
+
]);
|
|
6440
|
+
var SVG_PAINT_CSS_PROPERTY = {
|
|
6441
|
+
stopColor: "stop-color",
|
|
6442
|
+
floodColor: "flood-color",
|
|
6443
|
+
lightingColor: "lighting-color"
|
|
6444
|
+
};
|
|
6312
6445
|
function resolveColorToken(property, raw, tokens) {
|
|
6313
6446
|
const exact = findTokenMatch(property, raw, tokens);
|
|
6314
6447
|
if (exact) return { kind: "exact", token: exact.token, ambiguous: exact.ambiguous };
|