@fragments-sdk/cli 0.14.3 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +4745 -3817
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/codebase-scanner-MQHUZC2G.js +21 -0
- package/dist/converter-7XM3Y6NJ.js +33 -0
- package/dist/converter-7XM3Y6NJ.js.map +1 -0
- package/dist/core/index.js +43 -2
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
- package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/govern-scan-OYFZYOQW.js +413 -0
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
- package/dist/init-SSGUSP7Z.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
- package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +5 -37
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/scan-PKSYSTRR.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/scanner-4KZNOXAK.js +12 -0
- package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
- package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
- package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
- package/dist/static-viewer-7QIBQZRC.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
- package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
- package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
- package/dist/tokens-generate-5JQSJ27E.js +85 -0
- package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +8 -6
- package/src/bin.ts +300 -48
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +189 -70
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +565 -0
- package/src/commands/govern.ts +67 -4
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +439 -51
- package/src/commands/scan.ts +14 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/tokens-push.ts +199 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +40 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/index.ts +3 -3
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/index.ts +8 -0
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-normalizer.ts +510 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js +0 -626
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js +0 -2453
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/build.ts +0 -612
- package/src/commands/dev.ts +0 -107
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1030
- package/src/core/token-resolver.ts +0 -155
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
package/src/service/ast-utils.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* accurate patches for token replacement.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { parse
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
10
10
|
import traverse, { type NodePath } from "@babel/traverse";
|
|
11
11
|
import generate from "@babel/generator";
|
|
12
12
|
import * as t from "@babel/types";
|
|
13
|
+
import { BABEL_PARSER_OPTIONS } from "./babel-config.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Style location information
|
|
@@ -43,26 +44,6 @@ export interface StyleLocation {
|
|
|
43
44
|
context?: string;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
* Babel parser options for React/TypeScript files
|
|
48
|
-
*/
|
|
49
|
-
const PARSER_OPTIONS: ParserOptions = {
|
|
50
|
-
sourceType: "module",
|
|
51
|
-
plugins: [
|
|
52
|
-
"jsx",
|
|
53
|
-
"typescript",
|
|
54
|
-
["decorators", { decoratorsBeforeExport: true }],
|
|
55
|
-
"classProperties",
|
|
56
|
-
"classPrivateProperties",
|
|
57
|
-
"classPrivateMethods",
|
|
58
|
-
"exportDefaultFrom",
|
|
59
|
-
"exportNamespaceFrom",
|
|
60
|
-
"dynamicImport",
|
|
61
|
-
"nullishCoalescingOperator",
|
|
62
|
-
"optionalChaining",
|
|
63
|
-
"objectRestSpread",
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
47
|
|
|
67
48
|
/**
|
|
68
49
|
* Extract style locations from a React/TypeScript source file
|
|
@@ -81,7 +62,7 @@ export function extractStyleLocations(
|
|
|
81
62
|
|
|
82
63
|
let ast: t.File;
|
|
83
64
|
try {
|
|
84
|
-
ast = parse(sourceCode,
|
|
65
|
+
ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
|
|
85
66
|
} catch (error) {
|
|
86
67
|
console.error(`Failed to parse ${filePath}:`, error);
|
|
87
68
|
return locations;
|
|
@@ -267,7 +248,7 @@ export function applyPatch(
|
|
|
267
248
|
): PatchResult {
|
|
268
249
|
let ast: t.File;
|
|
269
250
|
try {
|
|
270
|
-
ast = parse(sourceCode,
|
|
251
|
+
ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
|
|
271
252
|
} catch (error) {
|
|
272
253
|
return {
|
|
273
254
|
success: false,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Babel parser configuration for React/TypeScript files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ParserOptions } from "@babel/parser";
|
|
6
|
+
|
|
7
|
+
export const BABEL_PARSER_OPTIONS: ParserOptions = {
|
|
8
|
+
sourceType: "module",
|
|
9
|
+
plugins: [
|
|
10
|
+
"jsx",
|
|
11
|
+
"typescript",
|
|
12
|
+
["decorators", { decoratorsBeforeExport: true }],
|
|
13
|
+
"classProperties",
|
|
14
|
+
"classPrivateProperties",
|
|
15
|
+
"classPrivateMethods",
|
|
16
|
+
"exportDefaultFrom",
|
|
17
|
+
"exportNamespaceFrom",
|
|
18
|
+
"dynamicImport",
|
|
19
|
+
"nullishCoalescingOperator",
|
|
20
|
+
"optionalChaining",
|
|
21
|
+
"objectRestSpread",
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converter — transforms scanner output (ComponentUsage[]) to governance engine input (UISpec)
|
|
3
|
+
*
|
|
4
|
+
* Bridge between the codebase scanner (Babel AST analysis) and the govern engine.
|
|
5
|
+
* Each file's usages become a UISpec with one UINode per component usage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { relative } from 'node:path';
|
|
9
|
+
import type { ComponentUsage } from './types.js';
|
|
10
|
+
import type { UISpec, UINode } from '@fragments-sdk/govern';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert a list of component usages from a single file into a UISpec
|
|
14
|
+
* suitable for the governance engine.
|
|
15
|
+
*
|
|
16
|
+
* Each ComponentUsage becomes a UINode:
|
|
17
|
+
* - id: `${relativePath}:${line}:${column}` (unique, traceable back to source)
|
|
18
|
+
* - type: componentName (e.g. "Button", "Card.Header")
|
|
19
|
+
* - props: merged static + dynamic markers + spread markers
|
|
20
|
+
*/
|
|
21
|
+
export function usagesToSpec(
|
|
22
|
+
usages: ComponentUsage[],
|
|
23
|
+
filePath: string,
|
|
24
|
+
rootDir: string,
|
|
25
|
+
): UISpec {
|
|
26
|
+
const relativePath = relative(rootDir, filePath);
|
|
27
|
+
|
|
28
|
+
const nodes: UINode[] = usages.map((usage) => ({
|
|
29
|
+
id: `${relativePath}:${usage.line}:${usage.column}`,
|
|
30
|
+
type: usage.componentName,
|
|
31
|
+
props: mergeProps(usage),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
nodes,
|
|
36
|
+
metadata: {
|
|
37
|
+
agent: 'scan',
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Merge static props, dynamic prop markers, and spread markers into
|
|
45
|
+
* a single Record<string, unknown> for the UINode.
|
|
46
|
+
*/
|
|
47
|
+
function mergeProps(usage: ComponentUsage): Record<string, unknown> {
|
|
48
|
+
const props: Record<string, unknown> = { ...usage.props.static };
|
|
49
|
+
|
|
50
|
+
// Mark dynamic props so governance rules can see them
|
|
51
|
+
for (const name of usage.props.dynamic) {
|
|
52
|
+
props[name] = '(dynamic)';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mark spread props
|
|
56
|
+
for (const name of usage.props.spreads) {
|
|
57
|
+
props[`...${name}`] = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return props;
|
|
61
|
+
}
|
|
@@ -586,13 +586,23 @@ function extractPropsFromInlineParams(
|
|
|
586
586
|
// const ComponentName = (...) => ...
|
|
587
587
|
if (ts.isVariableStatement(node)) {
|
|
588
588
|
for (const decl of node.declarationList.declarations) {
|
|
589
|
-
if (
|
|
590
|
-
ts.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
589
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === componentName && decl.initializer) {
|
|
590
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
591
|
+
targetFunc = decl.initializer;
|
|
592
|
+
}
|
|
593
|
+
// Handle React.forwardRef(...) or forwardRef(...)
|
|
594
|
+
else if (ts.isCallExpression(decl.initializer)) {
|
|
595
|
+
const callee = decl.initializer.expression;
|
|
596
|
+
const isForwardRef =
|
|
597
|
+
(ts.isPropertyAccessExpression(callee) && callee.name.text === 'forwardRef') ||
|
|
598
|
+
(ts.isIdentifier(callee) && callee.text === 'forwardRef');
|
|
599
|
+
if (isForwardRef && decl.initializer.arguments.length > 0) {
|
|
600
|
+
const firstArg = decl.initializer.arguments[0];
|
|
601
|
+
if (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg)) {
|
|
602
|
+
targetFunc = firstArg;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
596
606
|
}
|
|
597
607
|
}
|
|
598
608
|
}
|
|
@@ -777,7 +787,12 @@ function inferComponentName(filePath: string): string {
|
|
|
777
787
|
if (name === "index") {
|
|
778
788
|
name = basename(dirname(filePath));
|
|
779
789
|
}
|
|
780
|
-
|
|
790
|
+
|
|
791
|
+
return name
|
|
792
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
793
|
+
.filter(Boolean)
|
|
794
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
795
|
+
.join("");
|
|
781
796
|
}
|
|
782
797
|
|
|
783
798
|
/**
|
|
@@ -850,6 +865,7 @@ export async function extractPropsForComponent(
|
|
|
850
865
|
if (existsSync(fullPath)) {
|
|
851
866
|
return extractPropsFromFile(fullPath, {
|
|
852
867
|
propsTypeName: `${componentName}Props`,
|
|
868
|
+
componentName,
|
|
853
869
|
});
|
|
854
870
|
}
|
|
855
871
|
}
|
|
@@ -870,6 +886,7 @@ export async function extractAllComponentProps(
|
|
|
870
886
|
try {
|
|
871
887
|
const extraction = await extractPropsFromFile(filePath, {
|
|
872
888
|
propsTypeName: `${componentName}Props`,
|
|
889
|
+
componentName,
|
|
873
890
|
});
|
|
874
891
|
results.set(componentName, extraction);
|
|
875
892
|
} catch (error) {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - Component usages in JSX with props
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { parse
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
10
|
+
import { BABEL_PARSER_OPTIONS } from "../babel-config.js";
|
|
10
11
|
import _traverse from "@babel/traverse";
|
|
11
12
|
import * as t from "@babel/types";
|
|
12
13
|
|
|
@@ -19,26 +20,6 @@ import type {
|
|
|
19
20
|
UsageProps,
|
|
20
21
|
} from "./types.js";
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Babel parser options for React/TypeScript files
|
|
24
|
-
*/
|
|
25
|
-
const PARSER_OPTIONS: ParserOptions = {
|
|
26
|
-
sourceType: "module",
|
|
27
|
-
plugins: [
|
|
28
|
-
"jsx",
|
|
29
|
-
"typescript",
|
|
30
|
-
["decorators", { decoratorsBeforeExport: true }],
|
|
31
|
-
"classProperties",
|
|
32
|
-
"classPrivateProperties",
|
|
33
|
-
"classPrivateMethods",
|
|
34
|
-
"exportDefaultFrom",
|
|
35
|
-
"exportNamespaceFrom",
|
|
36
|
-
"dynamicImport",
|
|
37
|
-
"nullishCoalescingOperator",
|
|
38
|
-
"optionalChaining",
|
|
39
|
-
"objectRestSpread",
|
|
40
|
-
],
|
|
41
|
-
};
|
|
42
23
|
|
|
43
24
|
/**
|
|
44
25
|
* Scan a file for component imports
|
|
@@ -56,7 +37,7 @@ export async function scanFileForImports(
|
|
|
56
37
|
|
|
57
38
|
try {
|
|
58
39
|
const sourceCode = await readFile(filePath, "utf-8");
|
|
59
|
-
const ast = parse(sourceCode,
|
|
40
|
+
const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
|
|
60
41
|
|
|
61
42
|
traverse(ast, {
|
|
62
43
|
ImportDeclaration(path) {
|
|
@@ -128,7 +109,7 @@ export async function scanFileForUsages(
|
|
|
128
109
|
try {
|
|
129
110
|
const sourceCode = await readFile(filePath, "utf-8");
|
|
130
111
|
const lines = sourceCode.split("\n");
|
|
131
|
-
const ast = parse(sourceCode,
|
|
112
|
+
const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
|
|
132
113
|
|
|
133
114
|
traverse(ast, {
|
|
134
115
|
JSXOpeningElement(path) {
|
|
@@ -189,7 +170,7 @@ export async function scanFile(
|
|
|
189
170
|
try {
|
|
190
171
|
const sourceCode = await readFile(filePath, "utf-8");
|
|
191
172
|
const lines = sourceCode.split("\n");
|
|
192
|
-
const ast = parse(sourceCode,
|
|
173
|
+
const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
|
|
193
174
|
|
|
194
175
|
// Track local names to component names mapping within this file
|
|
195
176
|
const localToComponent = new Map<string, string>();
|
package/src/service/index.ts
CHANGED
|
@@ -97,12 +97,20 @@ export { generateHtmlReport } from "./report.js";
|
|
|
97
97
|
export {
|
|
98
98
|
parseTokenFile,
|
|
99
99
|
parseTokenFiles,
|
|
100
|
+
parseTailwindV4File,
|
|
101
|
+
containsTailwindV4Theme,
|
|
100
102
|
hexToRgb,
|
|
101
103
|
rgbToHex,
|
|
102
104
|
parseRgb,
|
|
103
105
|
normalizeColor,
|
|
104
106
|
} from "./token-parser.js";
|
|
105
107
|
|
|
108
|
+
// Tailwind v4 @theme parser
|
|
109
|
+
export {
|
|
110
|
+
parseTailwindV4Theme,
|
|
111
|
+
type ThemeParserResult,
|
|
112
|
+
} from "./tailwind-v4-parser.js";
|
|
113
|
+
|
|
106
114
|
export {
|
|
107
115
|
TokenRegistryManager,
|
|
108
116
|
getSharedTokenRegistry,
|
|
@@ -494,8 +494,11 @@ function sortAndFilterBatch(
|
|
|
494
494
|
const getComponentName = (relativePath: string): string => {
|
|
495
495
|
const normalized = relativePath.replace(/\\/g, '/');
|
|
496
496
|
const fileName = normalized.split('/').pop() ?? normalized;
|
|
497
|
-
|
|
498
|
-
|
|
497
|
+
// Handle both V2 (.contract.json) and legacy (.fragment.tsx/.fragment.ts)
|
|
498
|
+
for (const ext of [BRAND.fileExtension, '.fragment.tsx', '.fragment.ts']) {
|
|
499
|
+
if (fileName.endsWith(ext)) {
|
|
500
|
+
return fileName.slice(0, -ext.length);
|
|
501
|
+
}
|
|
499
502
|
}
|
|
500
503
|
return extractComponentName(relativePath);
|
|
501
504
|
};
|
|
@@ -572,7 +575,10 @@ export async function validateSnippetPolicy(
|
|
|
572
575
|
const issues: FileIssue[] = [];
|
|
573
576
|
|
|
574
577
|
const discovered = await discoverFragmentFiles(config, configDir);
|
|
575
|
-
|
|
578
|
+
// Snippet validation applies to TSX fragment files (not .contract.json which has no render code)
|
|
579
|
+
const fragmentFiles = discovered.filter((file) =>
|
|
580
|
+
file.relativePath.endsWith('.fragment.tsx') || file.relativePath.endsWith('.fragment.ts')
|
|
581
|
+
);
|
|
576
582
|
|
|
577
583
|
const batchResult = sortAndFilterBatch(fragmentFiles, policy.componentStart, policy.componentLimit);
|
|
578
584
|
if (batchResult.warning) {
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind v4 @theme Block Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts CSS custom properties (--name: value;) from Tailwind v4 @theme {}
|
|
5
|
+
* blocks. Tailwind v4 moves token definitions from tailwind.config.js into
|
|
6
|
+
* CSS @theme blocks, declaring design tokens as standard custom properties.
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Multiple @theme blocks in one file
|
|
10
|
+
* - @theme inline { ... } variant
|
|
11
|
+
* - Empty @theme {} blocks
|
|
12
|
+
* - Comments (// single-line and multi-line)
|
|
13
|
+
* - Nested blocks inside @theme (tracks brace depth correctly)
|
|
14
|
+
* - Lines without semicolons (skipped with warning)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
DesignToken,
|
|
19
|
+
TokenCategory,
|
|
20
|
+
} from "../core/index.js";
|
|
21
|
+
|
|
22
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface ThemeParserResult {
|
|
25
|
+
tokens: DesignToken[];
|
|
26
|
+
errors: string[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
parseTimeMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Category Inference ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Category inference patterns — same logic as token-parser.ts CATEGORY_PATTERNS
|
|
35
|
+
* to keep categorization consistent across all parsers.
|
|
36
|
+
*/
|
|
37
|
+
const CATEGORY_PATTERNS: Array<{
|
|
38
|
+
pattern: RegExp;
|
|
39
|
+
category: TokenCategory;
|
|
40
|
+
}> = [
|
|
41
|
+
{ pattern: /color|bg|background|border-color|fill|stroke/i, category: "color" },
|
|
42
|
+
{ pattern: /spacing|margin|padding|gap|space|inset/i, category: "spacing" },
|
|
43
|
+
{ pattern: /font|text|line-height|letter-spacing|typography/i, category: "typography" },
|
|
44
|
+
{ pattern: /radius|rounded|corner/i, category: "radius" },
|
|
45
|
+
{ pattern: /shadow|elevation/i, category: "shadow" },
|
|
46
|
+
{ pattern: /size|width|height|min|max/i, category: "sizing" },
|
|
47
|
+
{ pattern: /border(?!-color)|stroke-width|outline/i, category: "border" },
|
|
48
|
+
{ pattern: /animation|transition|duration|timing|delay/i, category: "animation" },
|
|
49
|
+
{ pattern: /z-index|layer|stack/i, category: "z-index" },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Infer token category from a CSS custom property name.
|
|
54
|
+
*/
|
|
55
|
+
function inferCategory(name: string): TokenCategory {
|
|
56
|
+
const lowerName = name.toLowerCase();
|
|
57
|
+
|
|
58
|
+
for (const { pattern, category } of CATEGORY_PATTERNS) {
|
|
59
|
+
if (pattern.test(lowerName)) {
|
|
60
|
+
return category;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return "other";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Infer token level:
|
|
69
|
+
* - 1 (base) for raw values (hex colors, numbers, named CSS values)
|
|
70
|
+
* - 2 (semantic) for var() references
|
|
71
|
+
*/
|
|
72
|
+
function inferLevel(rawValue: string): 1 | 2 | 3 {
|
|
73
|
+
if (/var\(/.test(rawValue)) {
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Comment Stripping ───────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove block comments from a CSS string, handling multi-line spans.
|
|
83
|
+
* Returns the cleaned string.
|
|
84
|
+
*/
|
|
85
|
+
function stripBlockComments(css: string): string {
|
|
86
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Parser ──────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** Regex to match a CSS custom property declaration inside a @theme block */
|
|
92
|
+
const CUSTOM_PROPERTY_PATTERN = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse Tailwind v4 @theme blocks from CSS content and extract design tokens.
|
|
96
|
+
*
|
|
97
|
+
* @param cssContent - Raw CSS file content (may contain @theme blocks)
|
|
98
|
+
* @param filePath - Optional source file path for diagnostics
|
|
99
|
+
* @returns ThemeParserResult with extracted tokens, errors, and warnings
|
|
100
|
+
*/
|
|
101
|
+
export function parseTailwindV4Theme(
|
|
102
|
+
cssContent: string,
|
|
103
|
+
filePath?: string,
|
|
104
|
+
): ThemeParserResult {
|
|
105
|
+
const startTime = performance.now();
|
|
106
|
+
const tokens: DesignToken[] = [];
|
|
107
|
+
const errors: string[] = [];
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
const source = filePath ?? "unknown";
|
|
110
|
+
|
|
111
|
+
// Strip block comments first so they don't interfere with parsing
|
|
112
|
+
const cleaned = stripBlockComments(cssContent);
|
|
113
|
+
|
|
114
|
+
const lines = cleaned.split("\n");
|
|
115
|
+
|
|
116
|
+
let insideTheme = false;
|
|
117
|
+
let themeDepth = 0; // brace depth relative to the @theme block
|
|
118
|
+
let pendingTheme = false; // saw @theme keyword, waiting for opening brace
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
let line = lines[i];
|
|
122
|
+
const lineNumber = i + 1;
|
|
123
|
+
|
|
124
|
+
// Strip single-line comments (// ...)
|
|
125
|
+
const singleCommentIdx = line.indexOf("//");
|
|
126
|
+
if (singleCommentIdx !== -1) {
|
|
127
|
+
// Make sure // is not inside a string (simple heuristic: not inside quotes)
|
|
128
|
+
const beforeComment = line.slice(0, singleCommentIdx);
|
|
129
|
+
const singleQuotes = (beforeComment.match(/'/g) || []).length;
|
|
130
|
+
const doubleQuotes = (beforeComment.match(/"/g) || []).length;
|
|
131
|
+
// If both quote counts are even, the // is not inside a string
|
|
132
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
|
|
133
|
+
line = beforeComment;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
|
|
139
|
+
// Skip empty lines
|
|
140
|
+
if (trimmed === "") {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Detect @theme keyword (with optional modifiers like "inline")
|
|
145
|
+
if (!insideTheme && !pendingTheme) {
|
|
146
|
+
// Match @theme, @theme inline, @theme { ... }, etc.
|
|
147
|
+
if (/^@theme\b/.test(trimmed)) {
|
|
148
|
+
pendingTheme = true;
|
|
149
|
+
// Check if the opening brace is on the same line
|
|
150
|
+
const braceIdx = trimmed.indexOf("{");
|
|
151
|
+
if (braceIdx !== -1) {
|
|
152
|
+
pendingTheme = false;
|
|
153
|
+
insideTheme = true;
|
|
154
|
+
themeDepth = 1;
|
|
155
|
+
|
|
156
|
+
// Count any additional braces on this same line
|
|
157
|
+
const afterBrace = trimmed.slice(braceIdx + 1);
|
|
158
|
+
for (const ch of afterBrace) {
|
|
159
|
+
if (ch === "{") themeDepth++;
|
|
160
|
+
else if (ch === "}") themeDepth--;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If there are declarations on the same line as @theme {
|
|
164
|
+
// (e.g., @theme { --color-primary: #000; })
|
|
165
|
+
const inlineContent = afterBrace.trim();
|
|
166
|
+
if (inlineContent && themeDepth > 0) {
|
|
167
|
+
processThemeLine(inlineContent, lineNumber, source, tokens, warnings);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (themeDepth <= 0) {
|
|
171
|
+
insideTheme = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If we saw @theme but haven't found the opening brace yet
|
|
179
|
+
if (pendingTheme) {
|
|
180
|
+
const braceIdx = trimmed.indexOf("{");
|
|
181
|
+
if (braceIdx !== -1) {
|
|
182
|
+
pendingTheme = false;
|
|
183
|
+
insideTheme = true;
|
|
184
|
+
themeDepth = 1;
|
|
185
|
+
|
|
186
|
+
// Count additional braces on this line
|
|
187
|
+
const afterBrace = trimmed.slice(braceIdx + 1);
|
|
188
|
+
for (const ch of afterBrace) {
|
|
189
|
+
if (ch === "{") themeDepth++;
|
|
190
|
+
else if (ch === "}") themeDepth--;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Process any inline content after the brace
|
|
194
|
+
const inlineContent = afterBrace.trim();
|
|
195
|
+
if (inlineContent && themeDepth > 0) {
|
|
196
|
+
processThemeLine(inlineContent, lineNumber, source, tokens, warnings);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (themeDepth <= 0) {
|
|
200
|
+
insideTheme = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// If no brace found, keep waiting (could be multi-line @theme declaration)
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Inside a @theme block — track braces and extract declarations
|
|
208
|
+
if (insideTheme) {
|
|
209
|
+
// Count braces to track depth
|
|
210
|
+
for (const ch of trimmed) {
|
|
211
|
+
if (ch === "{") {
|
|
212
|
+
themeDepth++;
|
|
213
|
+
} else if (ch === "}") {
|
|
214
|
+
themeDepth--;
|
|
215
|
+
if (themeDepth <= 0) {
|
|
216
|
+
insideTheme = false;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Only extract declarations at depth 1 (directly inside @theme)
|
|
223
|
+
// or if we're still inside the theme block
|
|
224
|
+
if (insideTheme || themeDepth > 0) {
|
|
225
|
+
// Remove any closing braces from the line content before matching
|
|
226
|
+
const contentLine = trimmed.replace(/\}/g, "").trim();
|
|
227
|
+
if (contentLine) {
|
|
228
|
+
processThemeLine(contentLine, lineNumber, source, tokens, warnings);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If we ended while still expecting a @theme block, warn
|
|
235
|
+
if (pendingTheme) {
|
|
236
|
+
warnings.push(
|
|
237
|
+
`@theme keyword found but no opening brace — incomplete block in ${source}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (insideTheme) {
|
|
242
|
+
warnings.push(
|
|
243
|
+
`Unclosed @theme block — missing closing brace in ${source}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
tokens,
|
|
249
|
+
errors,
|
|
250
|
+
warnings,
|
|
251
|
+
parseTimeMs: performance.now() - startTime,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Process a single line inside a @theme block, attempting to extract a
|
|
257
|
+
* CSS custom property declaration.
|
|
258
|
+
*/
|
|
259
|
+
function processThemeLine(
|
|
260
|
+
line: string,
|
|
261
|
+
lineNumber: number,
|
|
262
|
+
source: string,
|
|
263
|
+
tokens: DesignToken[],
|
|
264
|
+
warnings: string[],
|
|
265
|
+
): void {
|
|
266
|
+
const match = line.match(CUSTOM_PROPERTY_PATTERN);
|
|
267
|
+
|
|
268
|
+
if (!match) {
|
|
269
|
+
// If the line looks like it has a custom property but is missing a semicolon
|
|
270
|
+
const partialMatch = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*$/);
|
|
271
|
+
if (partialMatch) {
|
|
272
|
+
// It matched without semicolon — treat as valid (CSS tolerates missing trailing semicolons)
|
|
273
|
+
const [, name, rawValue] = partialMatch;
|
|
274
|
+
tokens.push(buildToken(name, rawValue, lineNumber, source));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Lines that contain -- but couldn't be parsed
|
|
279
|
+
if (/^\s*--/.test(line)) {
|
|
280
|
+
warnings.push(
|
|
281
|
+
`Line ${lineNumber}: Could not parse custom property declaration: ${line.trim()}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const [, name, rawValue] = match;
|
|
288
|
+
tokens.push(buildToken(name, rawValue, lineNumber, source));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Build a DesignToken from a parsed custom property declaration.
|
|
293
|
+
*/
|
|
294
|
+
function buildToken(
|
|
295
|
+
name: string,
|
|
296
|
+
rawValue: string,
|
|
297
|
+
lineNumber: number,
|
|
298
|
+
source: string,
|
|
299
|
+
): DesignToken {
|
|
300
|
+
const value = rawValue.trim();
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
name,
|
|
304
|
+
rawValue: value,
|
|
305
|
+
resolvedValue: value,
|
|
306
|
+
category: inferCategory(name),
|
|
307
|
+
level: inferLevel(value),
|
|
308
|
+
referenceChain: [],
|
|
309
|
+
sourceFile: source,
|
|
310
|
+
lineNumber,
|
|
311
|
+
theme: "default",
|
|
312
|
+
selector: "@theme",
|
|
313
|
+
};
|
|
314
|
+
}
|