@usefragments/core 1.0.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.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,171 @@
1
+ import { inferTokenGroup, normalizeTokenGroupComment } from "./categories.js";
2
+ import type { ParsedToken, TokenParseOutput } from "./types.js";
3
+
4
+ export function detectTokenPrefix(names: string[]): string {
5
+ if (names.length === 0) return "--";
6
+
7
+ const stripped = names.map((name) => name.slice(2));
8
+ let prefix = "";
9
+ const first = stripped[0] ?? "";
10
+
11
+ for (let i = 0; i < first.length; i++) {
12
+ const ch = first[i];
13
+ if (stripped.every((s) => s[i] === ch)) {
14
+ prefix += ch;
15
+ } else {
16
+ break;
17
+ }
18
+ }
19
+
20
+ const lastHyphen = prefix.lastIndexOf("-");
21
+ if (lastHyphen > 0) {
22
+ prefix = prefix.slice(0, lastHyphen + 1);
23
+ }
24
+
25
+ return `--${prefix}`;
26
+ }
27
+
28
+ export function parseScssVariables(content: string): Map<string, string> {
29
+ const vars = new Map<string, string>();
30
+ const scssVarRegex = /^\s*(\$[\w-]+)\s*:\s*(.+?)\s*(?:!default\s*)?;/gm;
31
+
32
+ let match: RegExpExecArray | null;
33
+ while ((match = scssVarRegex.exec(content)) !== null) {
34
+ const name = match[1];
35
+ const value = match[2].replace(/\s*\/\/.*$/, "").trim();
36
+ if (!vars.has(name)) {
37
+ vars.set(name, value);
38
+ }
39
+ }
40
+
41
+ return vars;
42
+ }
43
+
44
+ export function resolveTokenValue(
45
+ rawValue: string,
46
+ scssVars: Map<string, string>,
47
+ cssVarValues: Map<string, string>,
48
+ depth = 0,
49
+ ): string {
50
+ if (depth > 5) return rawValue;
51
+
52
+ let resolved = rawValue;
53
+
54
+ resolved = resolved.replace(/#\{(\$[\w-]+)\}/g, (_, varName) => {
55
+ const val = scssVars.get(varName);
56
+ return val !== undefined
57
+ ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1)
58
+ : `#{${varName}}`;
59
+ });
60
+
61
+ resolved = resolved.replace(/(?<![#\{])(\$[\w-]+)/g, (_, varName) => {
62
+ const val = scssVars.get(varName);
63
+ return val !== undefined
64
+ ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1)
65
+ : varName;
66
+ });
67
+
68
+ resolved = resolved.replace(
69
+ /var\((--[\w-]+)(?:\s*,\s*(.+?))?\)/g,
70
+ (original, tokenName, fallback) => {
71
+ const tokenVal = cssVarValues.get(tokenName);
72
+ if (tokenVal !== undefined) {
73
+ return resolveTokenValue(tokenVal, scssVars, cssVarValues, depth + 1);
74
+ }
75
+ if (fallback) {
76
+ return resolveTokenValue(fallback.trim(), scssVars, cssVarValues, depth + 1);
77
+ }
78
+ return original;
79
+ },
80
+ );
81
+
82
+ return resolved;
83
+ }
84
+
85
+ export function parseScssTokens(content: string, filePath = "tokens.scss"): TokenParseOutput {
86
+ const lines = content.split("\n");
87
+ const tokens: ParsedToken[] = [];
88
+ const seenNames = new Set<string>();
89
+ let currentCategory = "other";
90
+ let hasCommentCategories = false;
91
+
92
+ const scssVars = parseScssVariables(content);
93
+ const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
94
+ const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
95
+
96
+ for (const line of lines) {
97
+ const commentMatch = line.match(sectionCommentRegex);
98
+ if (commentMatch) {
99
+ const normalized = normalizeTokenGroupComment(commentMatch[0]);
100
+ if (normalized) {
101
+ currentCategory = normalized;
102
+ hasCommentCategories = true;
103
+ }
104
+ continue;
105
+ }
106
+
107
+ const varMatch = line.match(varDeclRegex);
108
+ if (varMatch) {
109
+ const name = varMatch[1];
110
+ const rawValue = varMatch[2];
111
+
112
+ if (seenNames.has(name)) continue;
113
+ seenNames.add(name);
114
+
115
+ const inlineComment = line.match(/\/\/\s*(.+)$/);
116
+ const description = inlineComment ? inlineComment[1].trim() : undefined;
117
+ const cleanValue = rawValue.replace(/\s*\/\/.*$/, "").trim();
118
+
119
+ tokens.push({
120
+ name,
121
+ value: cleanValue || undefined,
122
+ category: hasCommentCategories ? currentCategory : inferTokenGroup(name),
123
+ description,
124
+ });
125
+ }
126
+ }
127
+
128
+ for (const [name, value] of scssVars) {
129
+ if (seenNames.has(name)) continue;
130
+ seenNames.add(name);
131
+ tokens.push({
132
+ name,
133
+ value,
134
+ category: hasCommentCategories ? currentCategory : inferTokenGroup(name),
135
+ });
136
+ }
137
+
138
+ const cssVarValues = new Map<string, string>();
139
+ for (const token of tokens) {
140
+ if (token.value) {
141
+ cssVarValues.set(token.name, token.value);
142
+ }
143
+ }
144
+
145
+ for (const token of tokens) {
146
+ if (token.value) {
147
+ const resolved = resolveTokenValue(token.value, scssVars, cssVarValues);
148
+ if (resolved !== token.value && !resolved.includes("#{") && !resolved.includes("$")) {
149
+ token.resolvedValue = resolved;
150
+ }
151
+ }
152
+ }
153
+
154
+ const categories: Record<string, ParsedToken[]> = {};
155
+ for (const token of tokens) {
156
+ categories[token.category] ??= [];
157
+ categories[token.category].push(token);
158
+ }
159
+
160
+ return {
161
+ prefix: detectTokenPrefix(tokens.map((token) => token.name)),
162
+ categories,
163
+ total: tokens.length,
164
+ };
165
+ }
166
+
167
+ export function parseCssTokens(content: string, filePath = "tokens.css"): TokenParseOutput {
168
+ return parseScssTokens(content, filePath);
169
+ }
170
+
171
+ export const parseTokenFile = parseScssTokens;
@@ -0,0 +1,187 @@
1
+ import type { DesignToken } from "../token-types.js";
2
+ import { inferTokenCategory } from "./categories.js";
3
+ import type { ThemeParserResult } from "./types.js";
4
+
5
+ const CUSTOM_PROPERTY_PATTERN = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/;
6
+
7
+ export function containsTailwindV4Theme(content: string): boolean {
8
+ return /^@theme\b/m.test(content);
9
+ }
10
+
11
+ export function parseTailwindV4Theme(
12
+ cssContent: string,
13
+ filePath?: string,
14
+ ): ThemeParserResult {
15
+ const startTime = performance.now();
16
+ const tokens: DesignToken[] = [];
17
+ const errors: string[] = [];
18
+ const warnings: string[] = [];
19
+ const source = filePath ?? "unknown";
20
+
21
+ const cleaned = stripBlockComments(cssContent);
22
+ const lines = cleaned.split("\n");
23
+
24
+ let insideTheme = false;
25
+ let themeDepth = 0;
26
+ let pendingTheme = false;
27
+
28
+ for (let i = 0; i < lines.length; i++) {
29
+ let line = lines[i];
30
+ const lineNumber = i + 1;
31
+
32
+ const singleCommentIdx = line.indexOf("//");
33
+ if (singleCommentIdx !== -1) {
34
+ const beforeComment = line.slice(0, singleCommentIdx);
35
+ const singleQuotes = (beforeComment.match(/'/g) || []).length;
36
+ const doubleQuotes = (beforeComment.match(/"/g) || []).length;
37
+ if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
38
+ line = beforeComment;
39
+ }
40
+ }
41
+
42
+ const trimmed = line.trim();
43
+ if (trimmed === "") continue;
44
+
45
+ if (!insideTheme && !pendingTheme && /^@theme\b/.test(trimmed)) {
46
+ pendingTheme = true;
47
+ const braceIdx = trimmed.indexOf("{");
48
+ if (braceIdx !== -1) {
49
+ pendingTheme = false;
50
+ insideTheme = true;
51
+ themeDepth = 1;
52
+
53
+ const afterBrace = trimmed.slice(braceIdx + 1);
54
+ for (const ch of afterBrace) {
55
+ if (ch === "{") themeDepth++;
56
+ else if (ch === "}") themeDepth--;
57
+ }
58
+
59
+ const inlineContent = afterBrace.trim();
60
+ if (inlineContent && themeDepth > 0) {
61
+ processThemeLine(inlineContent, lineNumber, source, tokens, warnings);
62
+ }
63
+
64
+ if (themeDepth <= 0) {
65
+ insideTheme = false;
66
+ }
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (pendingTheme) {
72
+ const braceIdx = trimmed.indexOf("{");
73
+ if (braceIdx !== -1) {
74
+ pendingTheme = false;
75
+ insideTheme = true;
76
+ themeDepth = 1;
77
+
78
+ const afterBrace = trimmed.slice(braceIdx + 1);
79
+ for (const ch of afterBrace) {
80
+ if (ch === "{") themeDepth++;
81
+ else if (ch === "}") themeDepth--;
82
+ }
83
+
84
+ const inlineContent = afterBrace.trim();
85
+ if (inlineContent && themeDepth > 0) {
86
+ processThemeLine(inlineContent, lineNumber, source, tokens, warnings);
87
+ }
88
+
89
+ if (themeDepth <= 0) {
90
+ insideTheme = false;
91
+ }
92
+ }
93
+ continue;
94
+ }
95
+
96
+ if (insideTheme) {
97
+ for (const ch of trimmed) {
98
+ if (ch === "{") {
99
+ themeDepth++;
100
+ } else if (ch === "}") {
101
+ themeDepth--;
102
+ if (themeDepth <= 0) {
103
+ insideTheme = false;
104
+ break;
105
+ }
106
+ }
107
+ }
108
+
109
+ if (insideTheme || themeDepth > 0) {
110
+ const contentLine = trimmed.replace(/\}/g, "").trim();
111
+ if (contentLine) {
112
+ processThemeLine(contentLine, lineNumber, source, tokens, warnings);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ if (pendingTheme) {
119
+ warnings.push(`@theme keyword found but no opening brace - incomplete block in ${source}`);
120
+ }
121
+
122
+ if (insideTheme) {
123
+ warnings.push(`Unclosed @theme block - missing closing brace in ${source}`);
124
+ }
125
+
126
+ return {
127
+ tokens,
128
+ errors,
129
+ warnings,
130
+ parseTimeMs: performance.now() - startTime,
131
+ };
132
+ }
133
+
134
+ function stripBlockComments(css: string): string {
135
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
136
+ }
137
+
138
+ function processThemeLine(
139
+ line: string,
140
+ lineNumber: number,
141
+ source: string,
142
+ tokens: DesignToken[],
143
+ warnings: string[],
144
+ ): void {
145
+ const match = line.match(CUSTOM_PROPERTY_PATTERN);
146
+
147
+ if (!match) {
148
+ const partialMatch = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*$/);
149
+ if (partialMatch) {
150
+ const [, name, rawValue] = partialMatch;
151
+ tokens.push(buildToken(name, rawValue, lineNumber, source));
152
+ return;
153
+ }
154
+
155
+ if (/^\s*--/.test(line)) {
156
+ warnings.push(
157
+ `Line ${lineNumber}: Could not parse custom property declaration: ${line.trim()}`,
158
+ );
159
+ }
160
+ return;
161
+ }
162
+
163
+ const [, name, rawValue] = match;
164
+ tokens.push(buildToken(name, rawValue, lineNumber, source));
165
+ }
166
+
167
+ function buildToken(
168
+ name: string,
169
+ rawValue: string,
170
+ lineNumber: number,
171
+ source: string,
172
+ ): DesignToken {
173
+ const value = rawValue.trim();
174
+
175
+ return {
176
+ name,
177
+ rawValue: value,
178
+ resolvedValue: value,
179
+ category: inferTokenCategory(name),
180
+ level: /var\(/.test(value) ? 2 : 1,
181
+ referenceChain: [],
182
+ sourceFile: source,
183
+ lineNumber,
184
+ theme: "default",
185
+ selector: "@theme",
186
+ };
187
+ }
@@ -0,0 +1,43 @@
1
+ import type { DesignToken } from "../token-types.js";
2
+
3
+ export interface ParsedToken {
4
+ /** Full CSS variable name (e.g., "--fui-color-accent") */
5
+ name: string;
6
+ /** Raw value from the declaration (e.g., "#{$fui-space-4}" or "16px") */
7
+ value?: string;
8
+ /** Resolved value after SCSS variable substitution (e.g., "16px") */
9
+ resolvedValue?: string;
10
+ /** Category inferred from comment or naming convention */
11
+ category: string;
12
+ /** Description from inline comment, if any */
13
+ description?: string;
14
+ }
15
+
16
+ export interface TokenParseOutput {
17
+ /** Detected prefix (e.g., "--fui-") */
18
+ prefix: string;
19
+ /** Tokens grouped by category */
20
+ categories: Record<string, ParsedToken[]>;
21
+ /** Total number of tokens found */
22
+ total: number;
23
+ }
24
+
25
+ export type TokenFormat = "auto" | "css" | "scss" | "dtcg" | "tailwind-v4";
26
+
27
+ export interface ParseTokensOptions {
28
+ format?: TokenFormat;
29
+ filePath?: string;
30
+ }
31
+
32
+ export interface DesignTokenParseOptions {
33
+ filePath?: string;
34
+ themeSelectors?: Record<string, string>;
35
+ includeTailwindV4?: boolean;
36
+ }
37
+
38
+ export interface ThemeParserResult {
39
+ tokens: DesignToken[];
40
+ errors: string[];
41
+ warnings: string[];
42
+ parseTimeMs: number;
43
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Topology layer — barrel exports.
3
+ *
4
+ * Maps repo-relative file paths to product areas (checkout, dashboard, …) for
5
+ * area-scoped governance. See `apps/cloud/docs/topology/`.
6
+ */
7
+
8
+ export { resolveArea } from "./resolve-area.js";
9
+ export type { Area, AreaCriticality, AreaMatch, Topology } from "./resolve-area.js";
@@ -0,0 +1,174 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveArea, type Topology } from "./resolve-area.js";
3
+
4
+ // Mirrors apps/cloud/docs/examples/fragments.config.example.ts, normalized to
5
+ // the array shape the architecture doc locks in.
6
+ function makeTopology(overrides?: Partial<Topology>): Topology {
7
+ return {
8
+ version: 1,
9
+ areas: [
10
+ {
11
+ id: "checkout",
12
+ name: "Checkout",
13
+ criticality: "revenue",
14
+ owners: ["checkout-platform"],
15
+ files: ["src/app/checkout/**", "src/features/checkout/**", "src/app/payment/**"],
16
+ routes: ["/checkout/**"],
17
+ },
18
+ {
19
+ id: "dashboard",
20
+ name: "Dashboard",
21
+ criticality: "high",
22
+ owners: ["dashboard-platform"],
23
+ files: ["src/app/dashboard/**"],
24
+ },
25
+ {
26
+ id: "marketing",
27
+ name: "Marketing",
28
+ criticality: "medium",
29
+ owners: ["growth"],
30
+ files: ["src/app/(marketing)/**", "src/pages/{index,pricing}.tsx"],
31
+ },
32
+ ],
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe("resolveArea", () => {
38
+ it("matches a file to its area and records the winning glob", () => {
39
+ const match = resolveArea("src/app/checkout/page.tsx", makeTopology());
40
+ expect(match).toEqual({
41
+ areaId: "checkout",
42
+ areaName: "Checkout",
43
+ criticality: "revenue",
44
+ owners: ["checkout-platform"],
45
+ matchedGlob: "src/app/checkout/**",
46
+ });
47
+ });
48
+
49
+ it("matches nested files under a globstar", () => {
50
+ const match = resolveArea("src/features/checkout/cart/Total.tsx", makeTopology());
51
+ expect(match?.areaId).toBe("checkout");
52
+ expect(match?.matchedGlob).toBe("src/features/checkout/**");
53
+ });
54
+
55
+ it("returns null for an unmatched path (becomes Unassigned downstream)", () => {
56
+ expect(resolveArea("src/lib/utils.ts", makeTopology())).toBeNull();
57
+ });
58
+
59
+ it("treats Next.js route-group parens as literal path segments", () => {
60
+ const match = resolveArea("src/app/(marketing)/home.tsx", makeTopology());
61
+ expect(match?.areaId).toBe("marketing");
62
+ // The parens must not behave like a regex/extglob group.
63
+ expect(resolveArea("src/app/marketing/home.tsx", makeTopology())).toBeNull();
64
+ });
65
+
66
+ it("expands brace alternation", () => {
67
+ const topology = makeTopology();
68
+ expect(resolveArea("src/pages/index.tsx", topology)?.areaId).toBe("marketing");
69
+ expect(resolveArea("src/pages/pricing.tsx", topology)?.areaId).toBe("marketing");
70
+ // The literal dot must not act as a wildcard.
71
+ expect(resolveArea("src/pages/indexXtsx", topology)).toBeNull();
72
+ });
73
+
74
+ it("matches a single `*` within one segment only (no slash crossing)", () => {
75
+ const topology: Topology = {
76
+ version: 1,
77
+ areas: [
78
+ {
79
+ id: "features",
80
+ name: "Features",
81
+ criticality: "medium",
82
+ owners: [],
83
+ files: ["src/features/*/index.ts"],
84
+ },
85
+ ],
86
+ };
87
+ expect(resolveArea("src/features/billing/index.ts", topology)?.areaId).toBe("features");
88
+ // `*` is single-segment, so a nested path must not match.
89
+ expect(resolveArea("src/features/billing/nested/index.ts", topology)).toBeNull();
90
+ });
91
+
92
+ it("uses first-match by declaration order when priorities are equal", () => {
93
+ const topology: Topology = {
94
+ version: 1,
95
+ areas: [
96
+ { id: "first", name: "First", criticality: "low", owners: [], files: ["src/**"] },
97
+ { id: "second", name: "Second", criticality: "low", owners: [], files: ["src/app/**"] },
98
+ ],
99
+ };
100
+ expect(resolveArea("src/app/x.tsx", topology)?.areaId).toBe("first");
101
+ });
102
+
103
+ it("honors priority as a tie-break (higher wins regardless of order)", () => {
104
+ const topology: Topology = {
105
+ version: 1,
106
+ areas: [
107
+ { id: "broad", name: "Broad", criticality: "low", owners: [], files: ["src/**"] },
108
+ {
109
+ id: "specific",
110
+ name: "Specific",
111
+ criticality: "high",
112
+ owners: [],
113
+ files: ["src/app/**"],
114
+ priority: 10,
115
+ },
116
+ ],
117
+ };
118
+ expect(resolveArea("src/app/x.tsx", topology)?.areaId).toBe("specific");
119
+ });
120
+
121
+ it("normalizes Windows separators and leading ./ before matching", () => {
122
+ const topology = makeTopology();
123
+ expect(resolveArea("src\\app\\checkout\\page.tsx", topology)?.areaId).toBe("checkout");
124
+ expect(resolveArea("./src/app/checkout/page.tsx", topology)?.areaId).toBe("checkout");
125
+ });
126
+
127
+ it("matches an area's own root directory path, not just descendants", () => {
128
+ const topology = makeTopology();
129
+ // `src/app/checkout/**` should match the directory itself, not only files
130
+ // under it (picomatch `a/**` semantics).
131
+ expect(resolveArea("src/app/checkout", topology)?.areaId).toBe("checkout");
132
+ expect(resolveArea("src/app/checkout/page.tsx", topology)?.areaId).toBe("checkout");
133
+ });
134
+
135
+ it("matches a mid-path globstar against zero or more segments", () => {
136
+ const topology: Topology = {
137
+ version: 1,
138
+ areas: [
139
+ { id: "api", name: "API", criticality: "high", owners: [], files: ["src/**/route.ts"] },
140
+ ],
141
+ };
142
+ expect(resolveArea("src/route.ts", topology)?.areaId).toBe("api");
143
+ expect(resolveArea("src/app/api/route.ts", topology)?.areaId).toBe("api");
144
+ });
145
+
146
+ it("never throws on a malformed glob — the bad glob is skipped, not the scan", () => {
147
+ const topology: Topology = {
148
+ version: 1,
149
+ areas: [
150
+ {
151
+ id: "broken",
152
+ name: "Broken",
153
+ criticality: "low",
154
+ owners: [],
155
+ // Unbalanced brace → invalid RegExp. Must not abort resolution.
156
+ files: ["src/{a,b/x.ts", "src/good/**"],
157
+ },
158
+ ],
159
+ };
160
+ expect(() => resolveArea("src/whatever.ts", topology)).not.toThrow();
161
+ // The malformed glob is skipped; the valid sibling still matches.
162
+ expect(resolveArea("src/good/file.ts", topology)?.areaId).toBe("broken");
163
+ // A path only the broken glob could have matched falls through to null.
164
+ expect(resolveArea("src/a", topology)).toBeNull();
165
+ });
166
+
167
+ it("returns the same result across repeated calls (cache correctness)", () => {
168
+ const topology = makeTopology();
169
+ const a = resolveArea("src/app/dashboard/Home.tsx", topology);
170
+ const b = resolveArea("src/app/dashboard/Home.tsx", topology);
171
+ expect(a).toEqual(b);
172
+ expect(a?.areaId).toBe("dashboard");
173
+ });
174
+ });