@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
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
2
2
|
import { mkdtemp, writeFile, mkdir, rm, readFile as rf } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import {
|
|
6
6
|
extractComponentJSDocFromSource,
|
|
7
7
|
detectCompoundComponentsFromSource,
|
|
8
8
|
calculateFieldConfidence,
|
|
9
9
|
scanGenerate,
|
|
10
|
+
detectPackageManagerForProject,
|
|
11
|
+
excludeFragmentsFromTsconfig,
|
|
10
12
|
parseEnrichmentResponse,
|
|
11
13
|
buildEnrichmentSystemPrompt,
|
|
12
14
|
buildEnrichmentUserPrompt,
|
|
13
15
|
buildEnrichedUsageBlock,
|
|
14
16
|
type EnrichmentResult,
|
|
15
17
|
} from "../scan-generate.js";
|
|
16
|
-
import type { ComponentMeta } from
|
|
18
|
+
import type { ComponentMeta } from '@fragments-sdk/extract';
|
|
17
19
|
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Helper to build a minimal ComponentMeta for tests
|
|
@@ -268,39 +270,28 @@ export function Button({ children, variant = 'primary', disabled, onClick }: But
|
|
|
268
270
|
// Read the generated file and check structure
|
|
269
271
|
const { readFile: rf } = await import("node:fs/promises");
|
|
270
272
|
const content = await rf(
|
|
271
|
-
join(tmpDir, "components", "Button", "Button.
|
|
273
|
+
join(tmpDir, "components", "Button", "Button.contract.json"),
|
|
272
274
|
"utf-8"
|
|
273
275
|
);
|
|
274
276
|
|
|
275
|
-
|
|
276
|
-
expect(content).toContain(
|
|
277
|
-
"import { defineFragment } from '@fragments-sdk/core'"
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
// Check confidence header
|
|
281
|
-
expect(content).toMatch(
|
|
282
|
-
/\/\/ Auto-generated by fragments init --scan \| Confidence: \d+\/100/
|
|
283
|
-
);
|
|
277
|
+
const contract = JSON.parse(content);
|
|
284
278
|
|
|
285
|
-
// Check
|
|
286
|
-
expect(
|
|
287
|
-
|
|
288
|
-
// Check component name in meta
|
|
289
|
-
expect(content).toContain("name: 'Button'");
|
|
279
|
+
// Check required contract fields
|
|
280
|
+
expect(contract.$schema).toBe("https://usefragments.com/schemas/contract.v1.json");
|
|
281
|
+
expect(contract.name).toBe("Button");
|
|
290
282
|
|
|
291
283
|
// Check JSDoc was picked up in description
|
|
292
|
-
expect(
|
|
284
|
+
expect(contract.description).toContain("A primary button for user interactions.");
|
|
293
285
|
|
|
294
286
|
// Check category was inferred (Button → Actions)
|
|
295
|
-
expect(
|
|
287
|
+
expect(contract.category).toBe("Actions");
|
|
296
288
|
|
|
297
|
-
// Check props
|
|
298
|
-
expect(
|
|
299
|
-
expect(content).toContain("variant:");
|
|
289
|
+
// Check props have content
|
|
290
|
+
expect(contract.props).toHaveProperty("variant");
|
|
300
291
|
|
|
301
|
-
// Check
|
|
302
|
-
expect(
|
|
303
|
-
expect(
|
|
292
|
+
// Check provenance block
|
|
293
|
+
expect(contract.provenance).toBeDefined();
|
|
294
|
+
expect(contract.provenance.source).toBe("extracted");
|
|
304
295
|
});
|
|
305
296
|
|
|
306
297
|
it("skips existing fragments without --force", async () => {
|
|
@@ -326,17 +317,163 @@ export function Button({ children, variant = 'primary', disabled, onClick }: But
|
|
|
326
317
|
|
|
327
318
|
const { readFile: rf } = await import("node:fs/promises");
|
|
328
319
|
const content = await rf(
|
|
329
|
-
join(tmpDir, "components", "Button", "Button.
|
|
320
|
+
join(tmpDir, "components", "Button", "Button.contract.json"),
|
|
330
321
|
"utf-8"
|
|
331
322
|
);
|
|
332
323
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
324
|
+
const contract = JSON.parse(content);
|
|
325
|
+
|
|
326
|
+
// Check propsSummary exists and has entries for local props
|
|
327
|
+
expect(contract.propsSummary).toBeDefined();
|
|
328
|
+
expect(contract.propsSummary.length).toBeGreaterThan(0);
|
|
329
|
+
expect(contract.propsSummary.some((s: string) => s.includes("variant"))).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("scanGenerate helpers", () => {
|
|
334
|
+
let tmpDir: string;
|
|
335
|
+
|
|
336
|
+
beforeAll(async () => {
|
|
337
|
+
tmpDir = await mkdtemp(join(tmpdir(), "scan-gen-helpers-"));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
afterAll(async () => {
|
|
341
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("detects pnpm from an ancestor workspace root", async () => {
|
|
345
|
+
const workspaceRoot = join(tmpDir, "workspace-root");
|
|
346
|
+
const appDir = join(workspaceRoot, "apps", "demo");
|
|
347
|
+
await mkdir(appDir, { recursive: true });
|
|
348
|
+
await writeFile(join(workspaceRoot, "pnpm-workspace.yaml"), "packages:\n - 'apps/*'\n");
|
|
349
|
+
await writeFile(join(workspaceRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
|
|
350
|
+
|
|
351
|
+
const result = await detectPackageManagerForProject(appDir);
|
|
352
|
+
|
|
353
|
+
expect(result.manager).toBe("pnpm");
|
|
354
|
+
expect(result.lockfileDir).toBe(workspaceRoot);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("adds fragment exclusions to JSONC tsconfig files with aliases", async () => {
|
|
358
|
+
const projectDir = join(tmpDir, "jsonc-app");
|
|
359
|
+
const scanDir = join(projectDir, "src", "components");
|
|
360
|
+
await mkdir(scanDir, { recursive: true });
|
|
361
|
+
await writeFile(
|
|
362
|
+
join(projectDir, "package.json"),
|
|
363
|
+
JSON.stringify({ name: "jsonc-app", private: true }, null, 2)
|
|
364
|
+
);
|
|
365
|
+
await writeFile(
|
|
366
|
+
join(projectDir, "tsconfig.app.json"),
|
|
367
|
+
`{
|
|
368
|
+
"compilerOptions": {
|
|
369
|
+
"baseUrl": ".",
|
|
370
|
+
"paths": {
|
|
371
|
+
"@/*": ["./src/*"]
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
"include": ["src"]
|
|
375
|
+
}
|
|
376
|
+
`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
await excludeFragmentsFromTsconfig(scanDir);
|
|
380
|
+
|
|
381
|
+
const updated = JSON.parse(await rf(join(projectDir, "tsconfig.app.json"), "utf-8"));
|
|
382
|
+
expect(updated.exclude).toContain("**/*.contract.json");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("generates shadcn-style compound fragments without React imports or member syntax", async () => {
|
|
386
|
+
const projectDir = join(tmpDir, "shadcn-compound");
|
|
387
|
+
const componentDir = join(projectDir, "components");
|
|
388
|
+
const cardDir = join(componentDir, "Card");
|
|
389
|
+
await mkdir(cardDir, { recursive: true });
|
|
390
|
+
|
|
391
|
+
await writeFile(
|
|
392
|
+
join(projectDir, "package.json"),
|
|
393
|
+
JSON.stringify(
|
|
394
|
+
{
|
|
395
|
+
name: "shadcn-compound",
|
|
396
|
+
private: true,
|
|
397
|
+
devDependencies: {
|
|
398
|
+
"@fragments-sdk/core": "workspace:^",
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
null,
|
|
402
|
+
2
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
await writeFile(
|
|
406
|
+
join(projectDir, "tsconfig.app.json"),
|
|
407
|
+
JSON.stringify(
|
|
408
|
+
{
|
|
409
|
+
compilerOptions: {
|
|
410
|
+
jsx: "react-jsx",
|
|
411
|
+
},
|
|
412
|
+
include: ["components"],
|
|
413
|
+
},
|
|
414
|
+
null,
|
|
415
|
+
2
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
await writeFile(
|
|
419
|
+
join(cardDir, "Card.tsx"),
|
|
420
|
+
`export function Card(props: { children?: React.ReactNode; size?: 'default' | 'sm' }) {
|
|
421
|
+
return <div>{props.children}</div>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function CardHeader(props: { children?: React.ReactNode }) {
|
|
425
|
+
return <div>{props.children}</div>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function CardContent(props: { children?: React.ReactNode }) {
|
|
429
|
+
return <div>{props.children}</div>;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function CardFooter(props: { children?: React.ReactNode }) {
|
|
433
|
+
return <div>{props.children}</div>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export { Card as default };
|
|
437
|
+
`
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const result = await scanGenerate({
|
|
441
|
+
scanPath: componentDir,
|
|
442
|
+
force: true,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result.success).toBe(true);
|
|
446
|
+
|
|
447
|
+
const content = await rf(join(cardDir, "Card.contract.json"), "utf-8");
|
|
448
|
+
const contract = JSON.parse(content);
|
|
449
|
+
// Contract.json should have the component name and props
|
|
450
|
+
expect(contract.name).toBe("Card");
|
|
451
|
+
expect(contract.$schema).toBe("https://usefragments.com/schemas/contract.v1.json");
|
|
452
|
+
expect(contract.provenance).toBeDefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("falls back to inherited wrapper props for shadcn-style Label primitives", async () => {
|
|
456
|
+
const projectDir = resolve(__dirname, "../__fixtures__/shadcn-label-wrapper");
|
|
457
|
+
const scanDir = join(projectDir, "src", "components", "ui");
|
|
458
|
+
|
|
459
|
+
const result = await scanGenerate({
|
|
460
|
+
scanPath: scanDir,
|
|
461
|
+
force: true,
|
|
462
|
+
tsconfig: join(projectDir, "tsconfig.app.json"),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(result.success).toBe(true);
|
|
336
466
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
467
|
+
const fragment = await rf(join(scanDir, "label.contract.json"), "utf-8");
|
|
468
|
+
const contract = JSON.parse(fragment);
|
|
469
|
+
// Should have extracted inherited props like htmlFor and form
|
|
470
|
+
expect(contract.props).toBeDefined();
|
|
471
|
+
// Props may be in propsSummary or props object
|
|
472
|
+
const allKeys = [
|
|
473
|
+
...Object.keys(contract.props ?? {}),
|
|
474
|
+
...(contract.propsSummary ?? []).map((s: string) => s.split(':')[0]),
|
|
475
|
+
];
|
|
476
|
+
expect(allKeys.some((k: string) => k.includes('htmlFor') || k.includes('form'))).toBe(true);
|
|
340
477
|
});
|
|
341
478
|
});
|
|
342
479
|
|
|
@@ -393,22 +530,16 @@ export function CardFooter({ children }: { children: React.ReactNode }) {
|
|
|
393
530
|
expect(result.generated[0].name).toBe("Card");
|
|
394
531
|
|
|
395
532
|
const content = await rf(
|
|
396
|
-
join(tmpDir, "components", "Card", "Card.
|
|
533
|
+
join(tmpDir, "components", "Card", "Card.contract.json"),
|
|
397
534
|
"utf-8"
|
|
398
535
|
);
|
|
399
536
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
expect(
|
|
403
|
-
expect(
|
|
404
|
-
expect(
|
|
405
|
-
expect(
|
|
406
|
-
|
|
407
|
-
// Should have canonicalUsage
|
|
408
|
-
expect(content).toContain("canonicalUsage:");
|
|
409
|
-
expect(content).toContain("Card.Header");
|
|
410
|
-
expect(content).toContain("Card.Body");
|
|
411
|
-
expect(content).toContain("Card.Footer");
|
|
537
|
+
const contract = JSON.parse(content);
|
|
538
|
+
// Should have AI metadata with compound composition
|
|
539
|
+
expect(contract.ai).toBeDefined();
|
|
540
|
+
expect(contract.ai.compositionPattern).toBe("compound");
|
|
541
|
+
expect(contract.ai.subComponents).toBeDefined();
|
|
542
|
+
expect(contract.ai.subComponents.length).toBeGreaterThan(0);
|
|
412
543
|
});
|
|
413
544
|
});
|
|
414
545
|
|
|
@@ -697,30 +828,17 @@ export function Badge({ children, variant = 'default' }: BadgeProps) {
|
|
|
697
828
|
expect(result.generated[0].enriched).toBe(true);
|
|
698
829
|
|
|
699
830
|
const content = await rf(
|
|
700
|
-
join(tmpDir, "components", "Badge", "Badge.
|
|
831
|
+
join(tmpDir, "components", "Badge", "Badge.contract.json"),
|
|
701
832
|
"utf-8"
|
|
702
833
|
);
|
|
703
834
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
expect(
|
|
707
|
-
expect(
|
|
708
|
-
expect(
|
|
709
|
-
expect(
|
|
710
|
-
|
|
711
|
-
// Meta tags should be present
|
|
712
|
-
expect(content).toContain("tags:");
|
|
713
|
-
expect(content).toContain("'badge'");
|
|
714
|
-
|
|
715
|
-
// Contract should have enriched a11yRules and scenarioTags
|
|
716
|
-
expect(content).toContain("a11yRules:");
|
|
717
|
-
expect(content).toContain("'Use aria-label for icon-only badges'");
|
|
718
|
-
expect(content).toContain("scenarioTags:");
|
|
719
|
-
expect(content).toContain("'feedback.indicator.badge'");
|
|
720
|
-
|
|
721
|
-
// Should NOT have TODO markers for usage.when or usage.whenNot
|
|
722
|
-
expect(content).not.toContain("// TODO: Describe when to use Badge");
|
|
723
|
-
expect(content).not.toContain("// TODO: Describe when NOT to use Badge");
|
|
835
|
+
const contract = JSON.parse(content);
|
|
836
|
+
// Enriched usage fields should be present
|
|
837
|
+
expect(contract.usage.when.length).toBeGreaterThan(0);
|
|
838
|
+
expect(contract.usage.when).toContain("Showing status counts");
|
|
839
|
+
expect(contract.usage.whenNot).toContain("Long text content — use a Tag instead");
|
|
840
|
+
expect(contract.usage.guidelines).toBeDefined();
|
|
841
|
+
expect(contract.usage.guidelines).toContain("Keep text short (1-3 words)");
|
|
724
842
|
|
|
725
843
|
vi.doUnmock("../../ai-client.js");
|
|
726
844
|
});
|
|
@@ -747,13 +865,14 @@ export function Badge({ children, variant = 'default' }: BadgeProps) {
|
|
|
747
865
|
expect(result.success).toBe(true);
|
|
748
866
|
|
|
749
867
|
const content = await rf(
|
|
750
|
-
join(tmpDir, "components", "Badge", "Badge.
|
|
868
|
+
join(tmpDir, "components", "Badge", "Badge.contract.json"),
|
|
751
869
|
"utf-8"
|
|
752
870
|
);
|
|
753
871
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
expect(
|
|
872
|
+
const contract = JSON.parse(content);
|
|
873
|
+
// Without enrichment, usage.when/whenNot should be empty arrays
|
|
874
|
+
expect(contract.usage.when).toEqual([]);
|
|
875
|
+
expect(contract.usage.whenNot).toEqual([]);
|
|
757
876
|
expect(result.generated[0].enriched).toBeFalsy();
|
|
758
877
|
expect(result.enrichmentCost).toBeUndefined();
|
|
759
878
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import { verify } from "../verify.js";
|
|
7
|
+
|
|
8
|
+
describe("verify --ci", () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
tmpDir = await mkdtemp(join(tmpdir(), "verify-ci-test-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("prefers local fragments.json and ignores installed package fragments", async () => {
|
|
20
|
+
const projectDir = join(tmpDir, "project");
|
|
21
|
+
await mkdir(projectDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
await writeFile(
|
|
24
|
+
join(projectDir, "fragments.config.ts"),
|
|
25
|
+
`export default {
|
|
26
|
+
include: ['src/**/*.fragment.tsx'],
|
|
27
|
+
exclude: [],
|
|
28
|
+
components: ['src/**/*.tsx'],
|
|
29
|
+
framework: 'react',
|
|
30
|
+
outFile: 'fragments.json',
|
|
31
|
+
};
|
|
32
|
+
`
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await writeFile(
|
|
36
|
+
join(projectDir, "fragments.json"),
|
|
37
|
+
JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
fragments: {
|
|
40
|
+
LocalButton: {
|
|
41
|
+
meta: {
|
|
42
|
+
name: "LocalButton",
|
|
43
|
+
description: "Local button",
|
|
44
|
+
category: "Actions",
|
|
45
|
+
},
|
|
46
|
+
usage: {
|
|
47
|
+
when: ["Actions"],
|
|
48
|
+
whenNot: [],
|
|
49
|
+
},
|
|
50
|
+
props: {
|
|
51
|
+
variant: { type: "enum", required: false, values: ["primary"] },
|
|
52
|
+
},
|
|
53
|
+
variants: [{ name: "Default" }],
|
|
54
|
+
relations: [],
|
|
55
|
+
filePath: "src/components/LocalButton.fragment.tsx",
|
|
56
|
+
},
|
|
57
|
+
InstalledButton: {
|
|
58
|
+
meta: {
|
|
59
|
+
name: "InstalledButton",
|
|
60
|
+
description: "Installed button",
|
|
61
|
+
category: "Actions",
|
|
62
|
+
},
|
|
63
|
+
usage: {
|
|
64
|
+
when: ["Actions"],
|
|
65
|
+
whenNot: [],
|
|
66
|
+
},
|
|
67
|
+
props: {
|
|
68
|
+
variant: { type: "enum", required: false, values: ["primary"] },
|
|
69
|
+
},
|
|
70
|
+
variants: [{ name: "Default" }],
|
|
71
|
+
relations: [],
|
|
72
|
+
filePath: "node_modules/@fragments-sdk/ui/Button.fragment.tsx",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const summary = await verify(undefined, {
|
|
82
|
+
ci: true,
|
|
83
|
+
config: join(projectDir, "fragments.config.ts"),
|
|
84
|
+
port: 6553,
|
|
85
|
+
minCompliance: 60,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(summary.totalComponents).toBe(1);
|
|
89
|
+
expect(summary.results.map((result) => result.component)).toEqual(["LocalButton"]);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/commands/build.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { BRAND } from '../core/index.js';
|
|
7
7
|
import { loadConfig } from '../core/node.js';
|
|
8
|
-
import { buildFragments, buildFragmentsDir } from '
|
|
8
|
+
import { buildFragments, buildFragmentsDir, getFragmentsJsonStatus } from '@fragments-sdk/compiler';
|
|
9
9
|
import { scan } from './scan.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -22,6 +22,10 @@ export interface BuildOptions {
|
|
|
22
22
|
registryOnly?: boolean;
|
|
23
23
|
/** Build from source code (zero-config, no fragment files needed) */
|
|
24
24
|
fromSource?: boolean;
|
|
25
|
+
/** Skip work when fragments.json is already fresh */
|
|
26
|
+
ifNeeded?: boolean;
|
|
27
|
+
/** Check freshness and exit non-zero if fragments.json needs regeneration */
|
|
28
|
+
check?: boolean;
|
|
25
29
|
/** Skip usage analysis when building from source */
|
|
26
30
|
skipUsage?: boolean;
|
|
27
31
|
/** Skip Storybook parsing when building from source */
|
|
@@ -88,6 +92,55 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
|
|
|
88
92
|
|
|
89
93
|
// Build fragments.json unless --registry-only
|
|
90
94
|
if (!options.registryOnly) {
|
|
95
|
+
const status = await getFragmentsJsonStatus(config, configDir, {
|
|
96
|
+
output: config.outFile,
|
|
97
|
+
configPath: options.config,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (options.check) {
|
|
101
|
+
if (status.missing || status.stale) {
|
|
102
|
+
const message = status.missing
|
|
103
|
+
? `${BRAND.outFile} is missing`
|
|
104
|
+
: `${BRAND.outFile} is stale${status.reason ? ` (${status.reason})` : ''}`;
|
|
105
|
+
console.log(pc.red(`✗ ${message}\n`));
|
|
106
|
+
errors.push({
|
|
107
|
+
file: status.outputPath,
|
|
108
|
+
error: message,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
outputPath: status.outputPath,
|
|
113
|
+
errors,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
|
|
118
|
+
console.log(pc.dim(` Output: ${status.outputPath}\n`));
|
|
119
|
+
|
|
120
|
+
if (!options.registry) {
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
|
|
124
|
+
outputPath: status.outputPath,
|
|
125
|
+
errors,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.ifNeeded && !status.missing && !status.stale) {
|
|
131
|
+
console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
|
|
132
|
+
console.log(pc.dim(` Output: ${status.outputPath}\n`));
|
|
133
|
+
|
|
134
|
+
if (!options.registry) {
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
|
|
138
|
+
outputPath: status.outputPath,
|
|
139
|
+
errors,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
console.log(pc.dim('Compiling fragments...\n'));
|
|
92
145
|
|
|
93
146
|
const result = await buildFragments(config, configDir);
|
package/src/commands/context.ts
CHANGED
|
@@ -8,7 +8,7 @@ import pc from 'picocolors';
|
|
|
8
8
|
import { generateContext } from '../core/index.js';
|
|
9
9
|
import type { CompiledFragment, CompiledFragmentsFile } from '../core/index.js';
|
|
10
10
|
import { loadConfig } from '../core/node.js';
|
|
11
|
-
import { buildFragments } from '
|
|
11
|
+
import { buildFragments } from '@fragments-sdk/compiler';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Options for context command
|