@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/commands/scan.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
loadConfig,
|
|
24
24
|
discoverAllComponents,
|
|
25
|
+
findConfigFile,
|
|
25
26
|
type DiscoveredComponent,
|
|
26
27
|
} from "../core/node.js";
|
|
27
28
|
import {
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
type PropsExtractionResult,
|
|
39
40
|
type ParsedStoryFile,
|
|
40
41
|
} from "../service/index.js";
|
|
42
|
+
import { getGeneratorVersion } from '@fragments-sdk/compiler';
|
|
41
43
|
|
|
42
44
|
export interface ScanOptions {
|
|
43
45
|
/** Path to config file */
|
|
@@ -146,6 +148,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
|
|
|
146
148
|
try {
|
|
147
149
|
const extraction = await extractPropsFromFile(comp.sourcePath, {
|
|
148
150
|
propsTypeName: `${comp.name}Props`,
|
|
151
|
+
componentName: comp.name,
|
|
149
152
|
});
|
|
150
153
|
|
|
151
154
|
propsResults.set(comp.name, extraction);
|
|
@@ -270,10 +273,21 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
|
|
|
270
273
|
// Write output
|
|
271
274
|
const outputPath = resolve(configDir, outputFile);
|
|
272
275
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
276
|
+
const generatorVersion = await getGeneratorVersion();
|
|
277
|
+
const buildInputs = components
|
|
278
|
+
.map((component) => relative(configDir, component.sourcePath).split("\\").join("/"))
|
|
279
|
+
.sort();
|
|
280
|
+
const configPath = options.config ? resolve(process.cwd(), options.config) : findConfigFile(configDir);
|
|
281
|
+
if (configPath) {
|
|
282
|
+
buildInputs.push(relative(configDir, configPath).split("\\").join("/"));
|
|
283
|
+
buildInputs.sort();
|
|
284
|
+
}
|
|
273
285
|
|
|
274
286
|
const output: CompiledFragmentsFile = {
|
|
275
287
|
version: "1.0.0",
|
|
276
288
|
generatedAt: new Date().toISOString(),
|
|
289
|
+
generatorVersion,
|
|
290
|
+
buildInputs,
|
|
277
291
|
fragments,
|
|
278
292
|
};
|
|
279
293
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -456,6 +456,28 @@ function generateScssSeedImport(brand?: string): string {
|
|
|
456
456
|
`;
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
// ============================================
|
|
460
|
+
// Step runner
|
|
461
|
+
// ============================================
|
|
462
|
+
|
|
463
|
+
async function runSetupStep(
|
|
464
|
+
fn: () => Promise<{ modified: boolean; message: string }>,
|
|
465
|
+
failureLabel: string,
|
|
466
|
+
actions: string[],
|
|
467
|
+
errors: string[],
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
try {
|
|
470
|
+
const result = await fn();
|
|
471
|
+
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
472
|
+
console.log(` ${icon} ${result.message}`);
|
|
473
|
+
if (result.modified) actions.push(result.message);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
const msg = `${failureLabel}: ${error instanceof Error ? error.message : error}`;
|
|
476
|
+
console.log(` ${pc.red('✗')} ${msg}`);
|
|
477
|
+
errors.push(msg);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
459
481
|
// ============================================
|
|
460
482
|
// Main Setup Function
|
|
461
483
|
// ============================================
|
|
@@ -491,72 +513,27 @@ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
|
|
|
491
513
|
|
|
492
514
|
// 3. Add styles import
|
|
493
515
|
if (entryFile) {
|
|
494
|
-
|
|
495
|
-
const result = await addStylesImport(root, entryFile);
|
|
496
|
-
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
497
|
-
console.log(` ${icon} ${result.message}`);
|
|
498
|
-
if (result.modified) actions.push(result.message);
|
|
499
|
-
} catch (error) {
|
|
500
|
-
const msg = `Failed to add styles import: ${error instanceof Error ? error.message : error}`;
|
|
501
|
-
console.log(` ${pc.red('✗')} ${msg}`);
|
|
502
|
-
errors.push(msg);
|
|
503
|
-
}
|
|
516
|
+
await runSetupStep(() => addStylesImport(root, entryFile), 'Failed to add styles import', actions, errors);
|
|
504
517
|
}
|
|
505
518
|
|
|
506
519
|
// 4. Add ThemeProvider imports
|
|
507
520
|
if (entryFile) {
|
|
508
|
-
|
|
509
|
-
const result = await addThemeProvider(root, entryFile, framework);
|
|
510
|
-
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
511
|
-
console.log(` ${icon} ${result.message}`);
|
|
512
|
-
if (result.modified) actions.push(result.message);
|
|
513
|
-
} catch (error) {
|
|
514
|
-
const msg = `Failed to add ThemeProvider: ${error instanceof Error ? error.message : error}`;
|
|
515
|
-
console.log(` ${pc.red('✗')} ${msg}`);
|
|
516
|
-
errors.push(msg);
|
|
517
|
-
}
|
|
521
|
+
await runSetupStep(() => addThemeProvider(root, entryFile, framework), 'Failed to add ThemeProvider', actions, errors);
|
|
518
522
|
}
|
|
519
523
|
|
|
520
524
|
// 5. Next.js: add transpilePackages
|
|
521
525
|
if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
|
|
522
|
-
|
|
523
|
-
const result = await addTranspilePackages(root);
|
|
524
|
-
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
525
|
-
console.log(` ${icon} ${result.message}`);
|
|
526
|
-
if (result.modified) actions.push(result.message);
|
|
527
|
-
} catch (error) {
|
|
528
|
-
const msg = `Failed to update next.config: ${error instanceof Error ? error.message : error}`;
|
|
529
|
-
console.log(` ${pc.red('✗')} ${msg}`);
|
|
530
|
-
errors.push(msg);
|
|
531
|
-
}
|
|
526
|
+
await runSetupStep(() => addTranspilePackages(root), 'Failed to update next.config', actions, errors);
|
|
532
527
|
}
|
|
533
528
|
|
|
534
529
|
// 6. Create SCSS seeds file (if --scss flag or brand color specified)
|
|
535
530
|
if (options.scss || options.brand) {
|
|
536
|
-
|
|
537
|
-
const result = await createScssSeeds(root, options.brand);
|
|
538
|
-
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
539
|
-
console.log(` ${icon} ${result.message}`);
|
|
540
|
-
if (result.modified) actions.push(result.message);
|
|
541
|
-
} catch (error) {
|
|
542
|
-
const msg = `Failed to create SCSS seeds: ${error instanceof Error ? error.message : error}`;
|
|
543
|
-
console.log(` ${pc.red('✗')} ${msg}`);
|
|
544
|
-
errors.push(msg);
|
|
545
|
-
}
|
|
531
|
+
await runSetupStep(() => createScssSeeds(root, options.brand), 'Failed to create SCSS seeds', actions, errors);
|
|
546
532
|
}
|
|
547
533
|
|
|
548
534
|
// 7. Configure MCP server (if --mcp flag)
|
|
549
535
|
if (options.mcp) {
|
|
550
|
-
|
|
551
|
-
const result = await setupMcpConfig(root);
|
|
552
|
-
const icon = result.modified ? pc.green('+') : pc.dim('·');
|
|
553
|
-
console.log(` ${icon} ${result.message}`);
|
|
554
|
-
if (result.modified) actions.push(result.message);
|
|
555
|
-
} catch (error) {
|
|
556
|
-
const msg = `Failed to configure MCP: ${error instanceof Error ? error.message : error}`;
|
|
557
|
-
console.log(` ${pc.red('✗')} ${msg}`);
|
|
558
|
-
errors.push(msg);
|
|
559
|
-
}
|
|
536
|
+
await runSetupStep(() => setupMcpConfig(root), 'Failed to configure MCP', actions, errors);
|
|
560
537
|
}
|
|
561
538
|
|
|
562
539
|
// Summary
|
package/src/commands/sync.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { BRAND } from '../core/index.js';
|
|
|
13
13
|
import { loadConfig } from '../core/node.js';
|
|
14
14
|
import { discoverFragmentFiles, loadFragmentFile } from '../core/node.js';
|
|
15
15
|
import { parseFragmentFile } from '../core/parser.js';
|
|
16
|
-
import { resolveComponentSourcePath } from '
|
|
17
|
-
import { createComponentExtractor, type PropMeta, type CompositionMeta } from '
|
|
16
|
+
import { resolveComponentSourcePath } from '@fragments-sdk/extract';
|
|
17
|
+
import { createComponentExtractor, type PropMeta, type CompositionMeta } from '@fragments-sdk/extract';
|
|
18
18
|
import type { FragmentsConfig } from '@fragments-sdk/core';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments tokens generate — Generate CSS, SCSS, Tailwind, or Figma output
|
|
3
|
+
* from a DTCG .tokens.json source file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { resolve, dirname, basename, extname } from 'node:path';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import {
|
|
10
|
+
generateCSSCustomProperties,
|
|
11
|
+
generateSCSSVariables,
|
|
12
|
+
generateTailwindConfig,
|
|
13
|
+
generateFigmaVariables,
|
|
14
|
+
} from '../core/index.js';
|
|
15
|
+
import type { DTCGTokenFile } from '../core/index.js';
|
|
16
|
+
|
|
17
|
+
export interface TokensGenerateOptions {
|
|
18
|
+
/** Path to DTCG .tokens.json source file */
|
|
19
|
+
from: string;
|
|
20
|
+
/** Output formats (comma-separated: css, scss, tailwind, figma) */
|
|
21
|
+
format: string;
|
|
22
|
+
/** Output directory */
|
|
23
|
+
out?: string;
|
|
24
|
+
/** Token name prefix */
|
|
25
|
+
prefix?: string;
|
|
26
|
+
/** CSS selector for custom properties (default: ':root') */
|
|
27
|
+
selector?: string;
|
|
28
|
+
/** Verbose output */
|
|
29
|
+
verbose?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type OutputFormat = 'css' | 'scss' | 'tailwind' | 'figma';
|
|
33
|
+
|
|
34
|
+
const VALID_FORMATS = new Set<OutputFormat>(['css', 'scss', 'tailwind', 'figma']);
|
|
35
|
+
|
|
36
|
+
export async function tokensGenerate(options: TokensGenerateOptions): Promise<void> {
|
|
37
|
+
const { from, format, out, prefix, selector, verbose } = options;
|
|
38
|
+
|
|
39
|
+
// Parse formats
|
|
40
|
+
const formats = format.split(',').map((f) => f.trim().toLowerCase()) as OutputFormat[];
|
|
41
|
+
const invalidFormats = formats.filter((f) => !VALID_FORMATS.has(f));
|
|
42
|
+
if (invalidFormats.length > 0) {
|
|
43
|
+
console.error(pc.red(`Invalid format(s): ${invalidFormats.join(', ')}`));
|
|
44
|
+
console.error(`Valid formats: ${[...VALID_FORMATS].join(', ')}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read source file
|
|
49
|
+
const sourcePath = resolve(process.cwd(), from);
|
|
50
|
+
let content: string;
|
|
51
|
+
try {
|
|
52
|
+
content = await readFile(sourcePath, 'utf-8');
|
|
53
|
+
} catch {
|
|
54
|
+
console.error(pc.red(`Could not read file: ${sourcePath}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let tokens: DTCGTokenFile;
|
|
59
|
+
try {
|
|
60
|
+
tokens = JSON.parse(content) as DTCGTokenFile;
|
|
61
|
+
} catch {
|
|
62
|
+
console.error(pc.red(`Invalid JSON in: ${sourcePath}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Determine output directory
|
|
67
|
+
const outDir = out ? resolve(process.cwd(), out) : dirname(sourcePath);
|
|
68
|
+
await mkdir(outDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const baseName = basename(from, extname(from)).replace(/\.tokens$/, '');
|
|
71
|
+
|
|
72
|
+
// Generate each format
|
|
73
|
+
for (const fmt of formats) {
|
|
74
|
+
let output: string;
|
|
75
|
+
let fileName: string;
|
|
76
|
+
|
|
77
|
+
switch (fmt) {
|
|
78
|
+
case 'css': {
|
|
79
|
+
output = generateCSSCustomProperties(tokens, { prefix, selector });
|
|
80
|
+
fileName = `${baseName}.css`;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'scss': {
|
|
84
|
+
output = generateSCSSVariables(tokens, { prefix });
|
|
85
|
+
fileName = `_${baseName}.scss`;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'tailwind': {
|
|
89
|
+
const config = generateTailwindConfig(tokens);
|
|
90
|
+
output = `// Auto-generated Tailwind config from DTCG tokens\n// Do not edit directly\nexport default ${JSON.stringify(config, null, 2)};\n`;
|
|
91
|
+
fileName = `tailwind.tokens.js`;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'figma': {
|
|
95
|
+
const collections = generateFigmaVariables(tokens);
|
|
96
|
+
output = JSON.stringify(collections, null, 2);
|
|
97
|
+
fileName = `${baseName}.figma-variables.json`;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const outputPath = resolve(outDir, fileName);
|
|
105
|
+
await writeFile(outputPath, output, 'utf-8');
|
|
106
|
+
|
|
107
|
+
if (verbose) {
|
|
108
|
+
console.log(pc.green(` ✓ ${fmt}`), pc.dim(outputPath));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(pc.green(`Generated ${formats.length} output(s) from ${from}`));
|
|
113
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments tokens push — Push code tokens to Fragments Cloud for drift comparison.
|
|
3
|
+
*
|
|
4
|
+
* Extracts CSS custom properties from Tailwind v4 @theme blocks (or config-based
|
|
5
|
+
* token files) and POSTs them to the Fragments Cloud /api/ingest endpoint.
|
|
6
|
+
* Supports --dry-run for local inspection without pushing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Options
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface TokensPushOptions {
|
|
17
|
+
/** Path to fragments config file */
|
|
18
|
+
config?: string;
|
|
19
|
+
/** Path to Tailwind v4 CSS file with @theme block */
|
|
20
|
+
tailwindV4?: string;
|
|
21
|
+
/** Parse and display tokens without pushing */
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
/** Show detailed output */
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// tokensPush
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export async function tokensPush(options: TokensPushOptions): Promise<void> {
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
// 1. Resolve API credentials
|
|
35
|
+
const apiKey = process.env.FRAGMENTS_API_KEY;
|
|
36
|
+
const baseUrl = process.env.FRAGMENTS_URL || 'https://app.usefragments.com';
|
|
37
|
+
|
|
38
|
+
if (!apiKey && !options.dryRun) {
|
|
39
|
+
console.error(pc.red('Error: FRAGMENTS_API_KEY environment variable is required'));
|
|
40
|
+
console.error(pc.dim('Get your API key from https://app.usefragments.com/api-keys'));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Token extraction
|
|
45
|
+
const flatMap: Record<string, string> = {};
|
|
46
|
+
|
|
47
|
+
if (options.tailwindV4) {
|
|
48
|
+
// Use @theme parser for Tailwind v4 CSS files
|
|
49
|
+
const { parseTailwindV4File } = await import('../service/token-parser.js');
|
|
50
|
+
|
|
51
|
+
const filePath = resolve(process.cwd(), options.tailwindV4);
|
|
52
|
+
const result = parseTailwindV4File(filePath);
|
|
53
|
+
|
|
54
|
+
if (result.errors.length > 0) {
|
|
55
|
+
for (const err of result.errors) {
|
|
56
|
+
console.error(pc.red(` Error: ${err.message}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (options.verbose && result.warnings.length > 0) {
|
|
61
|
+
for (const warn of result.warnings) {
|
|
62
|
+
console.warn(pc.yellow(` Warning: ${warn}`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const token of result.tokens) {
|
|
67
|
+
flatMap[token.name] = token.resolvedValue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options.verbose) {
|
|
71
|
+
console.log(
|
|
72
|
+
pc.dim(` Parsed ${result.tokens.length} tokens from ${options.tailwindV4} (${result.parseTimeMs.toFixed(1)}ms)`),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} else if (options.config) {
|
|
76
|
+
// Use config-based token extraction
|
|
77
|
+
try {
|
|
78
|
+
const { loadConfig } = await import('../core/node.js');
|
|
79
|
+
const { parseTokenFiles } = await import('../service/index.js');
|
|
80
|
+
|
|
81
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
82
|
+
if (config.tokens?.include?.length) {
|
|
83
|
+
const result = await parseTokenFiles(config.tokens, configDir);
|
|
84
|
+
for (const token of result.tokens) {
|
|
85
|
+
flatMap[token.name] = token.resolvedValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.verbose) {
|
|
89
|
+
console.log(
|
|
90
|
+
pc.dim(` Parsed ${result.tokens.length} tokens from config (${result.parseTimeMs.toFixed(1)}ms)`),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
console.error(pc.red('Error: Config file has no tokens.include patterns'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(pc.red('Error loading config:'), error instanceof Error ? error.message : error);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// No source specified — try auto-detection
|
|
103
|
+
try {
|
|
104
|
+
const { loadConfig } = await import('../core/node.js');
|
|
105
|
+
const { parseTokenFiles } = await import('../service/index.js');
|
|
106
|
+
|
|
107
|
+
const { config, configDir } = await loadConfig();
|
|
108
|
+
if (config.tokens?.include?.length) {
|
|
109
|
+
const result = await parseTokenFiles(config.tokens, configDir);
|
|
110
|
+
for (const token of result.tokens) {
|
|
111
|
+
flatMap[token.name] = token.resolvedValue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
console.log(
|
|
116
|
+
pc.dim(` Parsed ${result.tokens.length} tokens from auto-detected config (${result.parseTimeMs.toFixed(1)}ms)`),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// No config found — fall through to error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Object.keys(flatMap).length === 0) {
|
|
125
|
+
console.error(pc.red('Error: No token source specified'));
|
|
126
|
+
console.error(pc.dim('Provide one of:'));
|
|
127
|
+
console.error(pc.dim(' --tailwind-v4 <path> Path to Tailwind v4 CSS file with @theme block'));
|
|
128
|
+
console.error(pc.dim(' --config <path> Path to fragments config with tokens.include'));
|
|
129
|
+
console.error(pc.dim('\nExample: fragments tokens push --tailwind-v4 ./app.css'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tokenCount = Object.keys(flatMap).length;
|
|
135
|
+
|
|
136
|
+
if (tokenCount === 0) {
|
|
137
|
+
console.log(pc.yellow('No tokens found.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(pc.cyan(`Found ${tokenCount} tokens`));
|
|
142
|
+
|
|
143
|
+
// 3. Dry run — display tokens and exit
|
|
144
|
+
if (options.dryRun) {
|
|
145
|
+
console.log(pc.dim('\nDry run — tokens that would be pushed:\n'));
|
|
146
|
+
const entries = Object.entries(flatMap);
|
|
147
|
+
for (const [name, value] of entries.slice(0, 20)) {
|
|
148
|
+
console.log(` ${pc.bold(name)}: ${value}`);
|
|
149
|
+
}
|
|
150
|
+
if (entries.length > 20) {
|
|
151
|
+
console.log(pc.dim(` ... and ${entries.length - 20} more`));
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. POST to Fragments Cloud
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(`${baseUrl}/api/ingest`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
codeTokens: JSON.stringify(flatMap),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorText = await response.text();
|
|
171
|
+
console.error(pc.red(`Error: API returned ${response.status}`));
|
|
172
|
+
console.error(pc.dim(errorText));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await response.json() as Record<string, unknown>;
|
|
177
|
+
|
|
178
|
+
console.log(pc.green(`\nPushed ${tokenCount} tokens to Fragments Cloud`));
|
|
179
|
+
|
|
180
|
+
if (result.tokenDrift) {
|
|
181
|
+
const drift = result.tokenDrift as Record<string, unknown>;
|
|
182
|
+
const summary = drift.summary as Record<string, number> | undefined;
|
|
183
|
+
if (summary) {
|
|
184
|
+
console.log(pc.cyan('\nDrift Summary:'));
|
|
185
|
+
if (summary.total !== undefined) console.log(pc.dim(` Total issues: ${summary.total}`));
|
|
186
|
+
if (summary.missingInCode > 0) console.log(pc.yellow(` Missing in code: ${summary.missingInCode}`));
|
|
187
|
+
if (summary.missingInFigma > 0) console.log(pc.yellow(` Missing in Figma: ${summary.missingInFigma}`));
|
|
188
|
+
if (summary.valueMismatch > 0) console.log(pc.yellow(` Value mismatches: ${summary.valueMismatch}`));
|
|
189
|
+
if (summary.score !== undefined) console.log(` Health score: ${summary.score}%`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(pc.dim(`\nView dashboard: ${baseUrl}/tokens`));
|
|
194
|
+
console.log(pc.dim(`Completed in ${Date.now() - startTime}ms`));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(pc.red('Error pushing tokens:'), error instanceof Error ? error.message : error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/commands/verify.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import pc from 'picocolors';
|
|
9
|
-
import {
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import {
|
|
13
|
+
BRAND,
|
|
14
|
+
type CompiledFragment,
|
|
15
|
+
type CompiledFragmentsFile,
|
|
16
|
+
} from '../core/index.js';
|
|
10
17
|
import { loadConfig } from '../core/node.js';
|
|
11
18
|
import {
|
|
12
19
|
createDevServerClient,
|
|
@@ -86,6 +93,10 @@ export async function verify(
|
|
|
86
93
|
console.log(pc.dim(`Minimum compliance: ${minCompliance}%\n`));
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
if (ci) {
|
|
97
|
+
return verifyFromLocalFragments(configPath, minCompliance, component);
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
// Check if dev server is reachable
|
|
90
101
|
const isReachable = await client.ping();
|
|
91
102
|
if (!isReachable) {
|
|
@@ -213,3 +224,186 @@ export async function verify(
|
|
|
213
224
|
|
|
214
225
|
return summary;
|
|
215
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compute metadata completeness score for a compiled fragment.
|
|
230
|
+
*
|
|
231
|
+
* Scoring breakdown (max 100):
|
|
232
|
+
* - Has description: +20
|
|
233
|
+
* - Has category that isn't "Components": +15
|
|
234
|
+
* - Has when/whenNot usage guidelines: +20
|
|
235
|
+
* - Has props with types: +20
|
|
236
|
+
* - Has variants: +15
|
|
237
|
+
* - Has relations: +10
|
|
238
|
+
*/
|
|
239
|
+
function computeMetadataScore(fragment: CompiledFragment): {
|
|
240
|
+
score: number;
|
|
241
|
+
violations: ViolationItem[];
|
|
242
|
+
} {
|
|
243
|
+
let score = 0;
|
|
244
|
+
const violations: ViolationItem[] = [];
|
|
245
|
+
|
|
246
|
+
// Description (+20)
|
|
247
|
+
if (fragment.meta.description && fragment.meta.description.trim().length > 0) {
|
|
248
|
+
score += 20;
|
|
249
|
+
} else {
|
|
250
|
+
violations.push({
|
|
251
|
+
property: 'meta.description',
|
|
252
|
+
issue: 'Missing component description',
|
|
253
|
+
severity: 'warning',
|
|
254
|
+
suggestion: 'Add a description to the fragment definition',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Category that isn't the default "Components" (+15)
|
|
259
|
+
if (fragment.meta.category && fragment.meta.category !== 'Components') {
|
|
260
|
+
score += 15;
|
|
261
|
+
} else {
|
|
262
|
+
violations.push({
|
|
263
|
+
property: 'meta.category',
|
|
264
|
+
issue: fragment.meta.category === 'Components'
|
|
265
|
+
? 'Using default category "Components"'
|
|
266
|
+
: 'Missing category',
|
|
267
|
+
severity: 'warning',
|
|
268
|
+
suggestion: 'Set a specific category (e.g., "forms", "layout", "feedback")',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Usage guidelines — when/whenNot (+20)
|
|
273
|
+
const hasWhen = fragment.usage?.when && fragment.usage.when.length > 0;
|
|
274
|
+
const hasWhenNot = fragment.usage?.whenNot && fragment.usage.whenNot.length > 0;
|
|
275
|
+
if (hasWhen || hasWhenNot) {
|
|
276
|
+
score += 20;
|
|
277
|
+
} else {
|
|
278
|
+
violations.push({
|
|
279
|
+
property: 'usage.when / usage.whenNot',
|
|
280
|
+
issue: 'Missing usage guidelines',
|
|
281
|
+
severity: 'warning',
|
|
282
|
+
suggestion: 'Add when[] and whenNot[] arrays to describe appropriate usage',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Props with types (+20)
|
|
287
|
+
const propKeys = Object.keys(fragment.props ?? {});
|
|
288
|
+
if (propKeys.length > 0) {
|
|
289
|
+
score += 20;
|
|
290
|
+
} else {
|
|
291
|
+
violations.push({
|
|
292
|
+
property: 'props',
|
|
293
|
+
issue: 'No props defined',
|
|
294
|
+
severity: 'warning',
|
|
295
|
+
suggestion: 'Define props with types in the fragment definition',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Variants (+15)
|
|
300
|
+
if (fragment.variants && fragment.variants.length > 0) {
|
|
301
|
+
score += 15;
|
|
302
|
+
} else {
|
|
303
|
+
violations.push({
|
|
304
|
+
property: 'variants',
|
|
305
|
+
issue: 'No variants defined',
|
|
306
|
+
severity: 'warning',
|
|
307
|
+
suggestion: 'Add at least one variant showing component usage',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Relations (+10)
|
|
312
|
+
if (fragment.relations && fragment.relations.length > 0) {
|
|
313
|
+
score += 10;
|
|
314
|
+
} else {
|
|
315
|
+
violations.push({
|
|
316
|
+
property: 'relations',
|
|
317
|
+
issue: 'No component relations defined',
|
|
318
|
+
severity: 'warning',
|
|
319
|
+
suggestion: 'Define relations to related components (parent, child, alternative)',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { score, violations };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* CI fallback: verify components from the local fragments.json file
|
|
328
|
+
* without requiring a running dev server.
|
|
329
|
+
*/
|
|
330
|
+
async function verifyFromLocalFragments(
|
|
331
|
+
configPath: string | undefined,
|
|
332
|
+
minCompliance: number,
|
|
333
|
+
component: string | undefined,
|
|
334
|
+
): Promise<VerifySummary> {
|
|
335
|
+
// Load config to resolve outFile
|
|
336
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
337
|
+
const outFile = config.outFile ?? 'fragments.json';
|
|
338
|
+
const fragmentsPath = resolve(configDir ?? process.cwd(), outFile);
|
|
339
|
+
|
|
340
|
+
if (!existsSync(fragmentsPath)) {
|
|
341
|
+
const error = {
|
|
342
|
+
error: `fragments.json not found at ${fragmentsPath}. Run "fragments build" first.`,
|
|
343
|
+
};
|
|
344
|
+
console.log(JSON.stringify(error));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const raw = await readFile(fragmentsPath, 'utf-8');
|
|
349
|
+
const data: CompiledFragmentsFile = JSON.parse(raw);
|
|
350
|
+
|
|
351
|
+
// Filter to only local fragments (skip anything from node_modules)
|
|
352
|
+
let entries = Object.entries(data.fragments).filter(
|
|
353
|
+
([, frag]) => !frag.filePath.includes('node_modules')
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Filter by specific component if requested
|
|
357
|
+
if (component) {
|
|
358
|
+
entries = entries.filter(
|
|
359
|
+
([name]) => name.toLowerCase() === component.toLowerCase()
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
if (entries.length === 0) {
|
|
363
|
+
const error = { error: `Component "${component}" not found in local fragments` };
|
|
364
|
+
console.log(JSON.stringify(error));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const results: VerifyResultItem[] = [];
|
|
370
|
+
let totalCompliance = 0;
|
|
371
|
+
|
|
372
|
+
for (const [name, fragment] of entries) {
|
|
373
|
+
const { score, violations } = computeMetadataScore(fragment);
|
|
374
|
+
const passed = score >= minCompliance;
|
|
375
|
+
|
|
376
|
+
results.push({
|
|
377
|
+
component: name,
|
|
378
|
+
compliance: score,
|
|
379
|
+
passed,
|
|
380
|
+
violations,
|
|
381
|
+
totalProperties: Object.keys(fragment.props ?? {}).length,
|
|
382
|
+
hardcoded: 0,
|
|
383
|
+
usingTokens: 0,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
totalCompliance += score;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const componentCount = entries.length;
|
|
390
|
+
const averageCompliance = componentCount > 0 ? totalCompliance / componentCount : 100;
|
|
391
|
+
const allPassed = results.every(r => r.passed);
|
|
392
|
+
|
|
393
|
+
const summary: VerifySummary = {
|
|
394
|
+
passed: allPassed,
|
|
395
|
+
compliance: Math.round(averageCompliance * 100) / 100,
|
|
396
|
+
threshold: minCompliance,
|
|
397
|
+
totalComponents: componentCount,
|
|
398
|
+
passedComponents: results.filter(r => r.passed).length,
|
|
399
|
+
failedComponents: results.filter(r => !r.passed).length,
|
|
400
|
+
results,
|
|
401
|
+
violations: results.flatMap(r => r.violations.map(v => ({
|
|
402
|
+
component: r.component,
|
|
403
|
+
...v,
|
|
404
|
+
}))),
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
408
|
+
return summary;
|
|
409
|
+
}
|