@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/index.js CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  ownedImportsEqual,
110
110
  resolveComponentGovernance,
111
111
  scaleGovernanceRecordSchema
112
- } from "./chunk-MLRDNSSA.js";
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.sources",
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
- const mappings = {
2937
- "base configuration": "base",
2938
- typography: "typography",
2939
- "spacing (micro)": "spacing",
2940
- spacing: "spacing",
2941
- "density padding": "spacing",
2942
- "border radius": "radius",
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 hasCommentCategories = false;
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
- hasCommentCategories = true;
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: hasCommentCategories ? currentCategory : inferTokenGroup(name),
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: hasCommentCategories ? currentCategory : inferTokenGroup(name)
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 };