@fragments-sdk/cli 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +18 -13
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-MUZ6CM66.js → chunk-5JNME72P.js} +3 -3
- package/dist/{chunk-MUZ6CM66.js.map → chunk-5JNME72P.js.map} +1 -1
- package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
- package/dist/chunk-AW7MWOUH.js.map +1 -0
- package/dist/{chunk-LY2CFFPY.js → chunk-FYIYMXGA.js} +2 -2
- package/dist/{chunk-3OTEW66K.js → chunk-LDKNZ55O.js} +4 -4
- package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
- package/dist/{chunk-ACIDZOYW.js → chunk-ODXQAQQX.js} +21 -8
- package/dist/chunk-ODXQAQQX.js.map +1 -0
- package/dist/{chunk-PMGI7ATF.js → chunk-OZQ7Z6C3.js} +31 -2
- package/dist/chunk-OZQ7Z6C3.js.map +1 -0
- package/dist/{core-DWKLGY4N.js → core-F3VT277E.js} +5 -3
- package/dist/{generate-3LBZANQ3.js → generate-PNIUR75D.js} +4 -4
- package/dist/index.d.ts +18 -0
- package/dist/index.js +6 -6
- package/dist/{init-NKIUCYTG.js → init-ON6WYG66.js} +4 -4
- package/dist/mcp-bin.js +8 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-E6U644RS.js +12 -0
- package/dist/{service-QSZMZJBJ.js → service-U7AR2PC2.js} +4 -4
- package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-QL2SCWYB.js} +4 -4
- package/dist/{test-ZCTR4LBB.js → test-PBPKJ4WJ.js} +3 -3
- package/dist/{tokens-5JQ5IOR2.js → tokens-4J4PRIGT.js} +5 -5
- package/dist/{viewer-D7QC4GM2.js → viewer-6VCZMA3T.js} +13 -13
- package/package.json +1 -1
- package/src/bin.ts +7 -1
- package/src/build.ts +16 -0
- package/src/core/index.ts +4 -0
- package/src/core/parser.ts +54 -1
- package/src/core/schema.ts +11 -0
- package/src/core/types.ts +27 -0
- package/src/mcp/server.ts +11 -1
- package/src/migrate/bin.ts +7 -1
- package/src/migrate/report.ts +1 -1
- package/src/service/report.ts +1 -1
- package/src/theme/__tests__/generator.test.ts +412 -0
- package/src/theme/__tests__/presets.test.ts +169 -0
- package/src/theme/__tests__/schema.test.ts +463 -0
- package/src/theme/__tests__/serializer.test.ts +326 -0
- package/src/theme/generator.ts +355 -0
- package/src/theme/index.ts +61 -0
- package/src/theme/presets.ts +189 -0
- package/src/theme/schema.ts +193 -0
- package/src/theme/serializer.ts +123 -0
- package/src/theme/types.ts +210 -0
- package/src/viewer/styles/globals.css +1 -1
- package/dist/chunk-ACIDZOYW.js.map +0 -1
- package/dist/chunk-PMGI7ATF.js.map +0 -1
- package/dist/chunk-XHNKNI6J.js.map +0 -1
- package/dist/scan-3ZAOVO4U.js +0 -12
- /package/dist/{chunk-LY2CFFPY.js.map → chunk-FYIYMXGA.js.map} +0 -0
- /package/dist/{chunk-3OTEW66K.js.map → chunk-LDKNZ55O.js.map} +0 -0
- /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
- /package/dist/{core-DWKLGY4N.js.map → core-F3VT277E.js.map} +0 -0
- /package/dist/{generate-3LBZANQ3.js.map → generate-PNIUR75D.js.map} +0 -0
- /package/dist/{init-NKIUCYTG.js.map → init-ON6WYG66.js.map} +0 -0
- /package/dist/{scan-3ZAOVO4U.js.map → scan-E6U644RS.js.map} +0 -0
- /package/dist/{service-QSZMZJBJ.js.map → service-U7AR2PC2.js.map} +0 -0
- /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-QL2SCWYB.js.map} +0 -0
- /package/dist/{test-ZCTR4LBB.js.map → test-PBPKJ4WJ.js.map} +0 -0
- /package/dist/{tokens-5JQ5IOR2.js.map → tokens-4J4PRIGT.js.map} +0 -0
- /package/dist/{viewer-D7QC4GM2.js.map → viewer-6VCZMA3T.js.map} +0 -0
package/src/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { resolve, join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import type {
|
|
4
5
|
SegmentsConfig,
|
|
5
6
|
CompiledSegmentsFile,
|
|
@@ -110,6 +111,8 @@ export async function buildSegments(
|
|
|
110
111
|
...(v.figma && { figma: v.figma }),
|
|
111
112
|
...(v.args && { args: v.args }),
|
|
112
113
|
})),
|
|
114
|
+
// Include AI metadata if present
|
|
115
|
+
...(parsed.ai && { ai: parsed.ai }),
|
|
113
116
|
};
|
|
114
117
|
|
|
115
118
|
segments[parsed.meta.name] = compiled;
|
|
@@ -150,9 +153,22 @@ export async function buildSegments(
|
|
|
150
153
|
// Recipe discovery failure is non-fatal
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
// Read package name for import statements
|
|
157
|
+
let packageName: string | undefined;
|
|
158
|
+
const pkgJsonPath = resolve(configDir, "package.json");
|
|
159
|
+
if (existsSync(pkgJsonPath)) {
|
|
160
|
+
try {
|
|
161
|
+
const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
162
|
+
if (pkg.name) packageName = pkg.name;
|
|
163
|
+
} catch {
|
|
164
|
+
// Non-fatal
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
const output: CompiledSegmentsFile = {
|
|
154
169
|
version: "1.0.0",
|
|
155
170
|
generatedAt: new Date().toISOString(),
|
|
171
|
+
...(packageName && { packageName }),
|
|
156
172
|
segments,
|
|
157
173
|
...(Object.keys(recipes).length > 0 && { recipes }),
|
|
158
174
|
};
|
package/src/core/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type {
|
|
|
32
32
|
// Contract and provenance types
|
|
33
33
|
SegmentContract,
|
|
34
34
|
SegmentGenerated,
|
|
35
|
+
// AI metadata type
|
|
36
|
+
AIMetadata,
|
|
35
37
|
// Screenshot types
|
|
36
38
|
ScreenshotConfig,
|
|
37
39
|
ServiceConfig,
|
|
@@ -87,6 +89,8 @@ export {
|
|
|
87
89
|
segmentGeneratedSchema,
|
|
88
90
|
segmentBanSchema,
|
|
89
91
|
recipeDefinitionSchema,
|
|
92
|
+
// AI metadata schema
|
|
93
|
+
aiMetadataSchema,
|
|
90
94
|
} from "./schema.js";
|
|
91
95
|
|
|
92
96
|
// Main API
|
package/src/core/parser.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import ts from "typescript";
|
|
11
|
-
import type { SegmentMeta, SegmentUsage, PropDefinition } from "./types.js";
|
|
11
|
+
import type { SegmentMeta, SegmentUsage, PropDefinition, AIMetadata } from "./types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Parsed segment metadata (extracted statically from AST)
|
|
@@ -45,6 +45,9 @@ export interface ParsedSegmentMetadata {
|
|
|
45
45
|
note: string;
|
|
46
46
|
}>;
|
|
47
47
|
|
|
48
|
+
/** AI-specific metadata for playground context generation */
|
|
49
|
+
ai?: AIMetadata;
|
|
50
|
+
|
|
48
51
|
/** Parse warnings */
|
|
49
52
|
warnings: string[];
|
|
50
53
|
}
|
|
@@ -129,6 +132,9 @@ export function parseSegmentFile(
|
|
|
129
132
|
// Extract relations
|
|
130
133
|
const relations = extractRelations(arg, warnings);
|
|
131
134
|
|
|
135
|
+
// Extract AI metadata
|
|
136
|
+
const ai = extractAIMetadata(arg, warnings);
|
|
137
|
+
|
|
132
138
|
return {
|
|
133
139
|
componentImport,
|
|
134
140
|
componentName,
|
|
@@ -137,6 +143,7 @@ export function parseSegmentFile(
|
|
|
137
143
|
props,
|
|
138
144
|
variants,
|
|
139
145
|
relations,
|
|
146
|
+
ai,
|
|
140
147
|
warnings,
|
|
141
148
|
};
|
|
142
149
|
}
|
|
@@ -460,6 +467,52 @@ function extractRelations(
|
|
|
460
467
|
return relations;
|
|
461
468
|
}
|
|
462
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Extract AI metadata from defineSegment call.
|
|
472
|
+
*/
|
|
473
|
+
function extractAIMetadata(
|
|
474
|
+
arg: ts.ObjectLiteralExpression,
|
|
475
|
+
warnings: string[]
|
|
476
|
+
): AIMetadata | undefined {
|
|
477
|
+
const aiProp = findProperty(arg, "ai");
|
|
478
|
+
if (!aiProp || !ts.isObjectLiteralExpression(aiProp)) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const ai: AIMetadata = {};
|
|
483
|
+
|
|
484
|
+
// Extract compositionPattern
|
|
485
|
+
const compositionPattern = extractStringProperty(aiProp, "compositionPattern");
|
|
486
|
+
if (compositionPattern && ['compound', 'simple', 'controlled'].includes(compositionPattern)) {
|
|
487
|
+
ai.compositionPattern = compositionPattern as AIMetadata['compositionPattern'];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Extract subComponents array
|
|
491
|
+
const subComponents = extractStringArray(aiProp, "subComponents");
|
|
492
|
+
if (subComponents.length > 0) {
|
|
493
|
+
ai.subComponents = subComponents;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Extract requiredChildren array
|
|
497
|
+
const requiredChildren = extractStringArray(aiProp, "requiredChildren");
|
|
498
|
+
if (requiredChildren.length > 0) {
|
|
499
|
+
ai.requiredChildren = requiredChildren;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Extract commonPatterns array
|
|
503
|
+
const commonPatterns = extractStringArray(aiProp, "commonPatterns");
|
|
504
|
+
if (commonPatterns.length > 0) {
|
|
505
|
+
ai.commonPatterns = commonPatterns;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Only return if we have any fields
|
|
509
|
+
if (Object.keys(ai).length > 0) {
|
|
510
|
+
return ai;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
463
516
|
/**
|
|
464
517
|
* Extract a string property from an object literal.
|
|
465
518
|
*/
|
package/src/core/schema.ts
CHANGED
|
@@ -138,6 +138,16 @@ export const segmentGeneratedSchema = z.object({
|
|
|
138
138
|
timestamp: z.string().datetime().optional(),
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Schema for AI-specific metadata for playground context generation
|
|
143
|
+
*/
|
|
144
|
+
export const aiMetadataSchema = z.object({
|
|
145
|
+
compositionPattern: z.enum(['compound', 'simple', 'controlled']).optional(),
|
|
146
|
+
subComponents: z.array(z.string()).optional(),
|
|
147
|
+
requiredChildren: z.array(z.string()).optional(),
|
|
148
|
+
commonPatterns: z.array(z.string()).optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
141
151
|
/**
|
|
142
152
|
* Schema for recipe definitions
|
|
143
153
|
*/
|
|
@@ -158,6 +168,7 @@ export const segmentDefinitionSchema = z.object({
|
|
|
158
168
|
relations: z.array(componentRelationSchema).optional(),
|
|
159
169
|
variants: z.array(segmentVariantSchema), // Allow empty variants array
|
|
160
170
|
contract: segmentContractSchema.optional(),
|
|
171
|
+
ai: aiMetadataSchema.optional(),
|
|
161
172
|
_generated: segmentGeneratedSchema.optional(),
|
|
162
173
|
});
|
|
163
174
|
|
package/src/core/types.ts
CHANGED
|
@@ -314,6 +314,24 @@ export interface SegmentGenerated {
|
|
|
314
314
|
timestamp?: string;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/**
|
|
318
|
+
* AI-specific metadata for playground context generation
|
|
319
|
+
* Provides hints for AI code generation about component composition
|
|
320
|
+
*/
|
|
321
|
+
export interface AIMetadata {
|
|
322
|
+
/** How this component is composed with others */
|
|
323
|
+
compositionPattern?: "compound" | "simple" | "controlled";
|
|
324
|
+
|
|
325
|
+
/** Sub-component names (without parent prefix, e.g., "Header" not "Card.Header") */
|
|
326
|
+
subComponents?: string[];
|
|
327
|
+
|
|
328
|
+
/** Sub-components that must be present for valid composition */
|
|
329
|
+
requiredChildren?: string[];
|
|
330
|
+
|
|
331
|
+
/** Common usage patterns as JSX strings for AI reference */
|
|
332
|
+
commonPatterns?: string[];
|
|
333
|
+
}
|
|
334
|
+
|
|
317
335
|
/**
|
|
318
336
|
* Complete segment definition
|
|
319
337
|
*/
|
|
@@ -339,6 +357,9 @@ export interface SegmentDefinition<TProps = unknown> {
|
|
|
339
357
|
/** Agent-optimized contract metadata */
|
|
340
358
|
contract?: SegmentContract;
|
|
341
359
|
|
|
360
|
+
/** AI-specific metadata for playground context generation */
|
|
361
|
+
ai?: AIMetadata;
|
|
362
|
+
|
|
342
363
|
/** Provenance tracking (for generated segments) */
|
|
343
364
|
_generated?: SegmentGenerated;
|
|
344
365
|
}
|
|
@@ -706,6 +727,9 @@ export interface CompiledSegment {
|
|
|
706
727
|
/** Agent-optimized contract metadata */
|
|
707
728
|
contract?: SegmentContract;
|
|
708
729
|
|
|
730
|
+
/** AI-specific metadata for playground context generation */
|
|
731
|
+
ai?: AIMetadata;
|
|
732
|
+
|
|
709
733
|
/** Provenance tracking (for generated segments) */
|
|
710
734
|
_generated?: SegmentGenerated;
|
|
711
735
|
}
|
|
@@ -746,6 +770,9 @@ export interface CompiledSegmentsFile {
|
|
|
746
770
|
/** When this file was generated */
|
|
747
771
|
generatedAt: string;
|
|
748
772
|
|
|
773
|
+
/** Package name for import statements (read from package.json at build time) */
|
|
774
|
+
packageName?: string;
|
|
775
|
+
|
|
749
776
|
/** All compiled segments indexed by component name */
|
|
750
777
|
segments: Record<string, CompiledSegment>;
|
|
751
778
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -366,13 +366,23 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
/**
|
|
369
|
-
* Get the package name
|
|
369
|
+
* Get the package name for import statements.
|
|
370
|
+
* Prefers packageName from fragments.json (set at build time),
|
|
371
|
+
* falls back to the project's package.json name.
|
|
370
372
|
*/
|
|
371
373
|
async function getPackageName(): Promise<string> {
|
|
372
374
|
if (packageName) {
|
|
373
375
|
return packageName;
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
// Prefer packageName from compiled fragments.json
|
|
379
|
+
const data = await loadSegments();
|
|
380
|
+
if (data.packageName) {
|
|
381
|
+
packageName = data.packageName;
|
|
382
|
+
return packageName;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Fallback to project package.json
|
|
376
386
|
const packageJsonPath = join(config.projectRoot, 'package.json');
|
|
377
387
|
if (existsSync(packageJsonPath)) {
|
|
378
388
|
try {
|
package/src/migrate/bin.ts
CHANGED
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import pc from "picocolors";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
10
13
|
import { BRAND } from "../core/index.js";
|
|
11
14
|
import { migrate } from "./migrate.js";
|
|
12
15
|
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8")) as { version: string };
|
|
18
|
+
|
|
13
19
|
const program = new Command();
|
|
14
20
|
|
|
15
21
|
program
|
|
16
22
|
.name("segments-migrate")
|
|
17
23
|
.description(`${BRAND.name} Storybook Migration Tool`)
|
|
18
|
-
.version(
|
|
24
|
+
.version(pkg.version);
|
|
19
25
|
|
|
20
26
|
program
|
|
21
27
|
.command("migrate")
|
package/src/migrate/report.ts
CHANGED
package/src/service/report.ts
CHANGED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, rm, readFile, access } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
generateScssTokens,
|
|
7
|
+
generateCssTokens,
|
|
8
|
+
generateTokenFiles,
|
|
9
|
+
} from "../generator.js";
|
|
10
|
+
import type { ThemeConfig } from "../types.js";
|
|
11
|
+
|
|
12
|
+
describe("Theme Generator", () => {
|
|
13
|
+
let testDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
testDir = join(
|
|
17
|
+
tmpdir(),
|
|
18
|
+
`theme-generator-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
19
|
+
);
|
|
20
|
+
await mkdir(testDir, { recursive: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
try {
|
|
25
|
+
await rm(testDir, { recursive: true, force: true });
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore cleanup errors
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("SCSS generation", () => {
|
|
32
|
+
it("should generate valid SCSS with custom accent color", () => {
|
|
33
|
+
const theme: ThemeConfig = {
|
|
34
|
+
name: "Test Theme",
|
|
35
|
+
colors: {
|
|
36
|
+
accent: "#ff6600",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const scss = generateScssTokens(theme);
|
|
41
|
+
|
|
42
|
+
expect(scss).toContain("$fui-color-accent: #ff6600 !default;");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should include !default flag for all variables", () => {
|
|
46
|
+
const theme: ThemeConfig = {
|
|
47
|
+
name: "Test Theme",
|
|
48
|
+
colors: {
|
|
49
|
+
accent: "#6366f1",
|
|
50
|
+
danger: "#ef4444",
|
|
51
|
+
},
|
|
52
|
+
radius: {
|
|
53
|
+
sm: "4px",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const scss = generateScssTokens(theme);
|
|
58
|
+
|
|
59
|
+
// All variable definitions should have !default
|
|
60
|
+
const lines = scss.split("\n").filter((l) => l.includes("$fui-"));
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
expect(line).toContain("!default;");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should generate dark mode variables", () => {
|
|
67
|
+
const theme: ThemeConfig = {
|
|
68
|
+
name: "Test Theme",
|
|
69
|
+
dark: {
|
|
70
|
+
surfaces: {
|
|
71
|
+
bgPrimary: "#0f172a",
|
|
72
|
+
},
|
|
73
|
+
text: {
|
|
74
|
+
primary: "#f8fafc",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const scss = generateScssTokens(theme);
|
|
80
|
+
|
|
81
|
+
expect(scss).toContain("$fui-dark-bg-primary: #0f172a !default;");
|
|
82
|
+
expect(scss).toContain("$fui-dark-text-primary: #f8fafc !default;");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should include header comment with theme name", () => {
|
|
86
|
+
const theme: ThemeConfig = {
|
|
87
|
+
name: "My Custom Theme",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const scss = generateScssTokens(theme);
|
|
91
|
+
|
|
92
|
+
expect(scss).toContain("// Theme: My Custom Theme");
|
|
93
|
+
expect(scss).toContain("// Auto-generated by @fragments-sdk/cli");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should generate all token categories", () => {
|
|
97
|
+
const theme: ThemeConfig = {
|
|
98
|
+
name: "Complete Theme",
|
|
99
|
+
colors: {
|
|
100
|
+
accent: "#6366f1",
|
|
101
|
+
},
|
|
102
|
+
surfaces: {
|
|
103
|
+
bgPrimary: "#ffffff",
|
|
104
|
+
},
|
|
105
|
+
text: {
|
|
106
|
+
primary: "#0f172a",
|
|
107
|
+
},
|
|
108
|
+
borders: {
|
|
109
|
+
default: "#e2e8f0",
|
|
110
|
+
},
|
|
111
|
+
typography: {
|
|
112
|
+
fontSans: "system-ui, sans-serif",
|
|
113
|
+
fontWeightNormal: 400,
|
|
114
|
+
},
|
|
115
|
+
radius: {
|
|
116
|
+
sm: "4px",
|
|
117
|
+
},
|
|
118
|
+
shadows: {
|
|
119
|
+
sm: "0 1px 2px rgba(0,0,0,0.05)",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const scss = generateScssTokens(theme);
|
|
124
|
+
|
|
125
|
+
// Check for section comments
|
|
126
|
+
expect(scss).toContain("// Colors");
|
|
127
|
+
expect(scss).toContain("// Surfaces");
|
|
128
|
+
expect(scss).toContain("// Text");
|
|
129
|
+
expect(scss).toContain("// Borders");
|
|
130
|
+
expect(scss).toContain("// Typography");
|
|
131
|
+
expect(scss).toContain("// Border Radius");
|
|
132
|
+
expect(scss).toContain("// Shadows");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should only include overridden tokens", () => {
|
|
136
|
+
const theme: ThemeConfig = {
|
|
137
|
+
name: "Minimal Theme",
|
|
138
|
+
colors: {
|
|
139
|
+
accent: "#ff0000",
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const scss = generateScssTokens(theme);
|
|
144
|
+
|
|
145
|
+
// Should include accent
|
|
146
|
+
expect(scss).toContain("$fui-color-accent: #ff0000");
|
|
147
|
+
|
|
148
|
+
// Should NOT include non-overridden tokens
|
|
149
|
+
expect(scss).not.toContain("$fui-color-danger:");
|
|
150
|
+
expect(scss).not.toContain("$fui-bg-primary:");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("CSS generation", () => {
|
|
155
|
+
it("should generate CSS custom properties", () => {
|
|
156
|
+
const theme: ThemeConfig = {
|
|
157
|
+
name: "Test Theme",
|
|
158
|
+
colors: {
|
|
159
|
+
accent: "#ff6600",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const css = generateCssTokens(theme);
|
|
164
|
+
|
|
165
|
+
expect(css).toContain("--fui-color-accent: #ff6600;");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should wrap in :root selector", () => {
|
|
169
|
+
const theme: ThemeConfig = {
|
|
170
|
+
name: "Test Theme",
|
|
171
|
+
colors: {
|
|
172
|
+
accent: "#6366f1",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const css = generateCssTokens(theme);
|
|
177
|
+
|
|
178
|
+
expect(css).toContain(":root {");
|
|
179
|
+
expect(css).toContain("}");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should generate dark mode with .dark and [data-theme] selectors", () => {
|
|
183
|
+
const theme: ThemeConfig = {
|
|
184
|
+
name: "Test Theme",
|
|
185
|
+
dark: {
|
|
186
|
+
surfaces: {
|
|
187
|
+
bgPrimary: "#0f172a",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const css = generateCssTokens(theme);
|
|
193
|
+
|
|
194
|
+
expect(css).toContain(':root.dark,\n:root[data-theme="dark"] {');
|
|
195
|
+
expect(css).toContain("--fui-bg-primary: #0f172a;");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should include header comment with theme name", () => {
|
|
199
|
+
const theme: ThemeConfig = {
|
|
200
|
+
name: "My CSS Theme",
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const css = generateCssTokens(theme);
|
|
204
|
+
|
|
205
|
+
expect(css).toContain("/* Theme: My CSS Theme */");
|
|
206
|
+
expect(css).toContain("/* Auto-generated by @fragments-sdk/cli */");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("file generation", () => {
|
|
211
|
+
it("should create SCSS file at specified path", async () => {
|
|
212
|
+
const theme: ThemeConfig = {
|
|
213
|
+
name: "Test Theme",
|
|
214
|
+
colors: {
|
|
215
|
+
accent: "#6366f1",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = await generateTokenFiles(theme, {
|
|
220
|
+
format: "scss",
|
|
221
|
+
outputDir: testDir,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(result.success).toBe(true);
|
|
225
|
+
expect(result.scssPath).toBeDefined();
|
|
226
|
+
|
|
227
|
+
// Verify file exists and contains content
|
|
228
|
+
const content = await readFile(result.scssPath!, "utf-8");
|
|
229
|
+
expect(content).toContain("$fui-color-accent: #6366f1");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should create CSS file at specified path", async () => {
|
|
233
|
+
const theme: ThemeConfig = {
|
|
234
|
+
name: "Test Theme",
|
|
235
|
+
colors: {
|
|
236
|
+
accent: "#6366f1",
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = await generateTokenFiles(theme, {
|
|
241
|
+
format: "css",
|
|
242
|
+
outputDir: testDir,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result.success).toBe(true);
|
|
246
|
+
expect(result.cssPath).toBeDefined();
|
|
247
|
+
|
|
248
|
+
// Verify file exists and contains content
|
|
249
|
+
const content = await readFile(result.cssPath!, "utf-8");
|
|
250
|
+
expect(content).toContain("--fui-color-accent: #6366f1");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should create both when format is "both"', async () => {
|
|
254
|
+
const theme: ThemeConfig = {
|
|
255
|
+
name: "Test Theme",
|
|
256
|
+
colors: {
|
|
257
|
+
accent: "#6366f1",
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const result = await generateTokenFiles(theme, {
|
|
262
|
+
format: "both",
|
|
263
|
+
outputDir: testDir,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(result.scssPath).toBeDefined();
|
|
268
|
+
expect(result.cssPath).toBeDefined();
|
|
269
|
+
|
|
270
|
+
// Verify both files exist
|
|
271
|
+
await expect(access(result.scssPath!)).resolves.toBeUndefined();
|
|
272
|
+
await expect(access(result.cssPath!)).resolves.toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should use custom file prefix", async () => {
|
|
276
|
+
const theme: ThemeConfig = {
|
|
277
|
+
name: "Test Theme",
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = await generateTokenFiles(theme, {
|
|
281
|
+
format: "scss",
|
|
282
|
+
outputDir: testDir,
|
|
283
|
+
filePrefix: "_custom-tokens",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(result.scssPath).toContain("_custom-tokens.scss");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should create output directory if needed", async () => {
|
|
291
|
+
const theme: ThemeConfig = {
|
|
292
|
+
name: "Test Theme",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const nestedDir = join(testDir, "nested", "path");
|
|
296
|
+
|
|
297
|
+
const result = await generateTokenFiles(theme, {
|
|
298
|
+
format: "scss",
|
|
299
|
+
outputDir: nestedDir,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(result.success).toBe(true);
|
|
303
|
+
await expect(access(nestedDir)).resolves.toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should use default file prefix when not specified", async () => {
|
|
307
|
+
const theme: ThemeConfig = {
|
|
308
|
+
name: "Test Theme",
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const result = await generateTokenFiles(theme, {
|
|
312
|
+
format: "scss",
|
|
313
|
+
outputDir: testDir,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.success).toBe(true);
|
|
317
|
+
expect(result.scssPath).toContain("_theme-tokens.scss");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("token mapping", () => {
|
|
322
|
+
it("should map colors.accent to $fui-color-accent", () => {
|
|
323
|
+
const theme: ThemeConfig = {
|
|
324
|
+
name: "Test",
|
|
325
|
+
colors: { accent: "#123456" },
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const scss = generateScssTokens(theme);
|
|
329
|
+
expect(scss).toContain("$fui-color-accent: #123456");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should map surfaces.bgPrimary to $fui-bg-primary", () => {
|
|
333
|
+
const theme: ThemeConfig = {
|
|
334
|
+
name: "Test",
|
|
335
|
+
surfaces: { bgPrimary: "#ffffff" },
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const scss = generateScssTokens(theme);
|
|
339
|
+
expect(scss).toContain("$fui-bg-primary: #ffffff");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should map text.primary to $fui-text-primary", () => {
|
|
343
|
+
const theme: ThemeConfig = {
|
|
344
|
+
name: "Test",
|
|
345
|
+
text: { primary: "#000000" },
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const scss = generateScssTokens(theme);
|
|
349
|
+
expect(scss).toContain("$fui-text-primary: #000000");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should map borders.default to $fui-border", () => {
|
|
353
|
+
const theme: ThemeConfig = {
|
|
354
|
+
name: "Test",
|
|
355
|
+
borders: { default: "#cccccc" },
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const scss = generateScssTokens(theme);
|
|
359
|
+
expect(scss).toContain("$fui-border: #cccccc");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should map typography.fontSans to $fui-font-sans", () => {
|
|
363
|
+
const theme: ThemeConfig = {
|
|
364
|
+
name: "Test",
|
|
365
|
+
typography: { fontSans: "Arial, sans-serif" },
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const scss = generateScssTokens(theme);
|
|
369
|
+
expect(scss).toContain("$fui-font-sans: Arial, sans-serif");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should map radius.sm to $fui-radius-sm", () => {
|
|
373
|
+
const theme: ThemeConfig = {
|
|
374
|
+
name: "Test",
|
|
375
|
+
radius: { sm: "2px" },
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const scss = generateScssTokens(theme);
|
|
379
|
+
expect(scss).toContain("$fui-radius-sm: 2px");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("should map shadows.md to $fui-shadow-md", () => {
|
|
383
|
+
const theme: ThemeConfig = {
|
|
384
|
+
name: "Test",
|
|
385
|
+
shadows: { md: "0 4px 8px rgba(0,0,0,0.1)" },
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const scss = generateScssTokens(theme);
|
|
389
|
+
expect(scss).toContain("$fui-shadow-md: 0 4px 8px rgba(0,0,0,0.1)");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should map dark.surfaces.bgPrimary to $fui-dark-bg-primary", () => {
|
|
393
|
+
const theme: ThemeConfig = {
|
|
394
|
+
name: "Test",
|
|
395
|
+
dark: { surfaces: { bgPrimary: "#1a1a1a" } },
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const scss = generateScssTokens(theme);
|
|
399
|
+
expect(scss).toContain("$fui-dark-bg-primary: #1a1a1a");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should map dark.dangerBg to $fui-dark-color-danger-bg", () => {
|
|
403
|
+
const theme: ThemeConfig = {
|
|
404
|
+
name: "Test",
|
|
405
|
+
dark: { dangerBg: "rgba(239, 68, 68, 0.15)" },
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const scss = generateScssTokens(theme);
|
|
409
|
+
expect(scss).toContain("$fui-dark-color-danger-bg: rgba(239, 68, 68, 0.15)");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|