@fragments-sdk/cli 0.15.0 → 0.15.2
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/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +565 -548
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
- 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-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
- package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
- package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
- package/dist/core/index.js +0 -1
- package/dist/create-JVAU3YKN.js +852 -0
- package/dist/create-JVAU3YKN.js.map +1 -0
- package/dist/doctor-BDPMYYE6.js +385 -0
- package/dist/doctor-BDPMYYE6.js.map +1 -0
- package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
- package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +2 -22
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
- package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
- package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
- package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +1 -2
- 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-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
- package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
- package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
- package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
- package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
- package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
- package/dist/{test-UQYUCZIS.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-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
- package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
- package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +18 -16
- package/src/bin.ts +94 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- 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 +9 -1
- package/src/commands/__tests__/scan-generate.test.ts +1 -1
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +590 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/govern-scan.ts +187 -8
- package/src/commands/govern.ts +65 -2
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +1 -1
- package/src/commands/scan.ts +13 -0
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-push.ts +199 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +1 -1
- package/src/core/drift-verifier.ts +1 -1
- package/src/core/extractor-adapter.ts +1 -1
- package/src/index.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +2 -2
- package/src/service/index.ts +8 -0
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +30 -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/dist/chunk-65WSVDV5.js.map +0 -1
- package/dist/chunk-7WHVW72L.js +0 -2664
- package/dist/chunk-7WHVW72L.js.map +0 -1
- package/dist/chunk-CZD3AD4Q.js.map +0 -1
- package/dist/chunk-MN3TJ3D5.js +0 -695
- package/dist/chunk-MN3TJ3D5.js.map +0 -1
- package/dist/chunk-XJQ5BIWI.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/govern-scan-UCBZR6D6.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-generate-TWRHNU5M.js.map +0 -1
- package/src/build.ts +0 -736
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1121
- package/src/core/token-resolver.ts +0 -155
- package/src/viewer/preview-adapter.ts +0 -116
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
- /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
- /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"license": "FSL-1.1-MIT",
|
|
5
5
|
"description": "CLI, MCP server, and dev tools for Fragments design system",
|
|
6
6
|
"author": "Conan McNicholl",
|
|
@@ -50,16 +50,31 @@
|
|
|
50
50
|
"dist",
|
|
51
51
|
"src"
|
|
52
52
|
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"lint": "eslint src",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"clean": "rm -rf dist"
|
|
60
|
+
},
|
|
53
61
|
"scarfSettings": {
|
|
54
62
|
"defaultOptIn": true
|
|
55
63
|
},
|
|
56
64
|
"dependencies": {
|
|
57
65
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
66
|
+
"@fragments-sdk/compiler": "workspace:*",
|
|
67
|
+
"@fragments-sdk/extract": "workspace:*",
|
|
58
68
|
"@babel/generator": "^7.23.6",
|
|
59
69
|
"@babel/parser": "^7.23.6",
|
|
60
70
|
"@babel/traverse": "^7.23.6",
|
|
61
71
|
"@babel/types": "^7.23.6",
|
|
62
72
|
"@figma/rest-api-spec": "^0.35.0",
|
|
73
|
+
"@fragments-sdk/context": "workspace:*",
|
|
74
|
+
"@fragments-sdk/core": "workspace:*",
|
|
75
|
+
"@fragments-sdk/govern": "workspace:^",
|
|
76
|
+
"@fragments-sdk/viewer": "workspace:*",
|
|
77
|
+
"@fragments-sdk/webmcp": "workspace:*",
|
|
63
78
|
"@inquirer/prompts": "^7.2.1",
|
|
64
79
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
65
80
|
"@phosphor-icons/react": "^2.1.10",
|
|
@@ -80,12 +95,7 @@
|
|
|
80
95
|
"shiki": "^3.21.0",
|
|
81
96
|
"vite": "^6.0.0",
|
|
82
97
|
"vite-plugin-svgr": "^4.5.0",
|
|
83
|
-
"zod": "^3.24.1"
|
|
84
|
-
"@fragments-sdk/core": "2.0.0",
|
|
85
|
-
"@fragments-sdk/webmcp": "3.0.0",
|
|
86
|
-
"@fragments-sdk/viewer": "0.2.7",
|
|
87
|
-
"@fragments-sdk/context": "0.6.0",
|
|
88
|
-
"@fragments-sdk/govern": "^0.3.0"
|
|
98
|
+
"zod": "^3.24.1"
|
|
89
99
|
},
|
|
90
100
|
"devDependencies": {
|
|
91
101
|
"@types/babel__generator": "^7.6.8",
|
|
@@ -118,13 +128,5 @@
|
|
|
118
128
|
"react-dom": {
|
|
119
129
|
"optional": true
|
|
120
130
|
}
|
|
121
|
-
},
|
|
122
|
-
"scripts": {
|
|
123
|
-
"build": "tsup",
|
|
124
|
-
"dev": "tsup --watch",
|
|
125
|
-
"lint": "eslint src",
|
|
126
|
-
"test": "vitest run",
|
|
127
|
-
"typecheck": "tsc --noEmit",
|
|
128
|
-
"clean": "rm -rf dist"
|
|
129
131
|
}
|
|
130
|
-
}
|
|
132
|
+
}
|
package/src/bin.ts
CHANGED
|
@@ -40,7 +40,9 @@ import { graph } from './commands/graph.js';
|
|
|
40
40
|
import { inspect } from './commands/inspect.js';
|
|
41
41
|
import { discover } from './commands/discover.js';
|
|
42
42
|
import { perf } from './commands/perf.js';
|
|
43
|
-
|
|
43
|
+
// doctor is imported lazily — it pulls @fragments-sdk/viewer/docs-data which
|
|
44
|
+
// ships as raw TS source and cannot be resolved by Node at startup.
|
|
45
|
+
type DoctorFn = typeof import('./commands/doctor.js')['doctor'];
|
|
44
46
|
import { setup } from './commands/setup.js';
|
|
45
47
|
import { sync } from './commands/sync.js';
|
|
46
48
|
import { governCheck, governInit, governReport, governConnect } from './commands/govern.js';
|
|
@@ -123,6 +125,8 @@ program
|
|
|
123
125
|
.option('--registry', `Also generate ${BRAND.dataDir}/${BRAND.registryFile} and ${BRAND.contextFile}`)
|
|
124
126
|
.option('--registry-only', `Only generate ${BRAND.dataDir}/ directory (skip ${BRAND.outFile})`)
|
|
125
127
|
.option('--from-source', 'Build from source code (zero-config, no fragment files needed)')
|
|
128
|
+
.option('--if-needed', `Skip rebuilding when ${BRAND.outFile} is already fresh`)
|
|
129
|
+
.option('--check', `Check whether ${BRAND.outFile} is fresh and exit non-zero if it is stale`)
|
|
126
130
|
.option('--skip-usage', 'Skip usage analysis when building from source')
|
|
127
131
|
.option('--skip-storybook', 'Skip Storybook parsing when building from source')
|
|
128
132
|
.option('-v, --verbose', 'Verbose output')
|
|
@@ -134,6 +138,8 @@ program
|
|
|
134
138
|
registry: options.registry,
|
|
135
139
|
registryOnly: options.registryOnly,
|
|
136
140
|
fromSource: options.fromSource,
|
|
141
|
+
ifNeeded: options.ifNeeded,
|
|
142
|
+
check: options.check,
|
|
137
143
|
skipUsage: options.skipUsage,
|
|
138
144
|
skipStorybook: options.skipStorybook,
|
|
139
145
|
verbose: options.verbose,
|
|
@@ -791,6 +797,47 @@ program
|
|
|
791
797
|
}
|
|
792
798
|
});
|
|
793
799
|
|
|
800
|
+
// ============================================================================
|
|
801
|
+
// CREATE COMMAND
|
|
802
|
+
// ============================================================================
|
|
803
|
+
program
|
|
804
|
+
.command('create')
|
|
805
|
+
.argument('[name]', 'Project name')
|
|
806
|
+
.description('Create a new project with Fragments UI and your custom theme')
|
|
807
|
+
.option('-t, --template <template>', 'Framework template (nextjs, vite)', 'nextjs')
|
|
808
|
+
.option('--pm <manager>', 'Package manager (npm, pnpm, yarn, bun)')
|
|
809
|
+
.option('--theme <encoded>', 'Encoded theme string')
|
|
810
|
+
.option('--preset <id>', 'Theme preset ID from usefragments.com/create')
|
|
811
|
+
.option('--brand <color>', 'Brand color hex (e.g., #6366f1)')
|
|
812
|
+
.option('--scss', 'Use SCSS output (installs sass)')
|
|
813
|
+
.option('--mcp', 'Configure MCP server for AI tooling')
|
|
814
|
+
.option('-y, --yes', 'Skip interactive prompts')
|
|
815
|
+
.option('--no-git', 'Skip git initialization')
|
|
816
|
+
.action(async (name, options) => {
|
|
817
|
+
try {
|
|
818
|
+
const { create } = await import('./commands/create.js');
|
|
819
|
+
const result = await create({
|
|
820
|
+
name,
|
|
821
|
+
template: options.template,
|
|
822
|
+
packageManager: options.pm,
|
|
823
|
+
theme: options.theme,
|
|
824
|
+
preset: options.preset,
|
|
825
|
+
brand: options.brand,
|
|
826
|
+
scss: options.scss,
|
|
827
|
+
mcp: options.mcp,
|
|
828
|
+
yes: options.yes,
|
|
829
|
+
noGit: !options.git,
|
|
830
|
+
});
|
|
831
|
+
if (!result.success) {
|
|
832
|
+
if (result.error) console.error(pc.red(`Error: ${result.error}`));
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
} catch (error) {
|
|
836
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
794
841
|
// ============================================================================
|
|
795
842
|
// SETUP COMMAND
|
|
796
843
|
// ============================================================================
|
|
@@ -989,6 +1036,28 @@ tokensCmd
|
|
|
989
1036
|
}
|
|
990
1037
|
});
|
|
991
1038
|
|
|
1039
|
+
tokensCmd
|
|
1040
|
+
.command('push')
|
|
1041
|
+
.description('Push code tokens to Fragments Cloud for drift comparison')
|
|
1042
|
+
.option('-c, --config <path>', 'Path to fragments config file')
|
|
1043
|
+
.option('--tailwind-v4 <path>', 'Path to Tailwind v4 CSS file with @theme block')
|
|
1044
|
+
.option('--dry-run', 'Parse and display tokens without pushing')
|
|
1045
|
+
.option('--verbose', 'Show detailed output')
|
|
1046
|
+
.action(async (options) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const { tokensPush } = await import('./commands/tokens-push.js');
|
|
1049
|
+
await tokensPush({
|
|
1050
|
+
config: options.config,
|
|
1051
|
+
tailwindV4: options.tailwindV4,
|
|
1052
|
+
dryRun: options.dryRun,
|
|
1053
|
+
verbose: options.verbose,
|
|
1054
|
+
});
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
992
1061
|
// ============================================================================
|
|
993
1062
|
// GENERATE COMMAND
|
|
994
1063
|
// ============================================================================
|
|
@@ -1229,6 +1298,7 @@ program
|
|
|
1229
1298
|
.option('--fix', 'Auto-fix issues where possible')
|
|
1230
1299
|
.action(async (options) => {
|
|
1231
1300
|
try {
|
|
1301
|
+
const { doctor } = await import('./commands/doctor.js');
|
|
1232
1302
|
const result = await doctor({
|
|
1233
1303
|
root: options.root,
|
|
1234
1304
|
json: options.json,
|
|
@@ -1361,5 +1431,28 @@ governCmd
|
|
|
1361
1431
|
}
|
|
1362
1432
|
});
|
|
1363
1433
|
|
|
1434
|
+
governCmd
|
|
1435
|
+
.command('push-contracts')
|
|
1436
|
+
.description('Push component contracts to Fragments Cloud')
|
|
1437
|
+
.option('-i, --input <path>', 'Path to fragments.json (default: ./fragments.json)')
|
|
1438
|
+
.option('--url <url>', 'Fragments Cloud URL')
|
|
1439
|
+
.option('--api-key <key>', 'API key (default: FRAGMENTS_API_KEY env var)')
|
|
1440
|
+
.option('-q, --quiet', 'Suppress non-error output')
|
|
1441
|
+
.action(async (options) => {
|
|
1442
|
+
try {
|
|
1443
|
+
const { pushContracts } = await import('./commands/push-contracts.js');
|
|
1444
|
+
const { exitCode } = await pushContracts({
|
|
1445
|
+
input: options.input,
|
|
1446
|
+
url: options.url,
|
|
1447
|
+
apiKey: options.apiKey,
|
|
1448
|
+
quiet: options.quiet,
|
|
1449
|
+
});
|
|
1450
|
+
process.exit(exitCode);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1364
1457
|
// Parse command line arguments
|
|
1365
1458
|
program.parse();
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { getFragmentsBuildInputs, getFragmentsJsonStatus, getGeneratorVersion } from '@fragments-sdk/compiler';
|
|
7
|
+
import type { FragmentsConfig } from '@fragments-sdk/core';
|
|
8
|
+
|
|
9
|
+
describe('getFragmentsJsonStatus', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'build-freshness-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function createProject(projectName: string) {
|
|
21
|
+
const projectDir = join(tmpDir, projectName);
|
|
22
|
+
const srcDir = join(projectDir, 'src', 'components');
|
|
23
|
+
await mkdir(srcDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const configPath = join(projectDir, 'fragments.config.ts');
|
|
26
|
+
const fragmentPath = join(srcDir, 'Button.contract.json');
|
|
27
|
+
const componentPath = join(srcDir, 'Button.tsx');
|
|
28
|
+
const outputPath = join(projectDir, 'fragments.json');
|
|
29
|
+
|
|
30
|
+
await writeFile(
|
|
31
|
+
configPath,
|
|
32
|
+
`export default {
|
|
33
|
+
include: ['src/**/*.contract.json'],
|
|
34
|
+
exclude: [],
|
|
35
|
+
components: ['src/**/*.tsx'],
|
|
36
|
+
framework: 'react',
|
|
37
|
+
outFile: 'fragments.json',
|
|
38
|
+
};
|
|
39
|
+
`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await writeFile(
|
|
43
|
+
fragmentPath,
|
|
44
|
+
JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
name: 'Button',
|
|
47
|
+
description: 'Button component',
|
|
48
|
+
category: 'actions',
|
|
49
|
+
variants: [
|
|
50
|
+
{
|
|
51
|
+
name: 'Default',
|
|
52
|
+
code: '<Button>Press</Button>',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
sourcePath: 'src/components/Button.tsx',
|
|
56
|
+
exportName: 'Button',
|
|
57
|
+
},
|
|
58
|
+
null,
|
|
59
|
+
2
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await writeFile(
|
|
64
|
+
componentPath,
|
|
65
|
+
`export function Button() {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const config: FragmentsConfig = {
|
|
72
|
+
include: ['src/**/*.contract.json'],
|
|
73
|
+
exclude: [],
|
|
74
|
+
components: ['src/**/*.tsx'],
|
|
75
|
+
framework: 'react',
|
|
76
|
+
outFile: 'fragments.json',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
config,
|
|
81
|
+
projectDir,
|
|
82
|
+
configPath,
|
|
83
|
+
fragmentPath,
|
|
84
|
+
componentPath,
|
|
85
|
+
outputPath,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it('reports missing output files', async () => {
|
|
90
|
+
const project = await createProject('missing-output');
|
|
91
|
+
|
|
92
|
+
const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
|
|
93
|
+
configPath: project.configPath,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(status.missing).toBe(true);
|
|
97
|
+
expect(status.stale).toBe(false);
|
|
98
|
+
expect(status.reason).toBe('missing output file');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('accepts a fresh fragments.json with matching inputs and generator version', async () => {
|
|
102
|
+
const project = await createProject('fresh-output');
|
|
103
|
+
const generatorVersion = await getGeneratorVersion();
|
|
104
|
+
const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
|
|
105
|
+
configPath: project.configPath,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await writeFile(
|
|
109
|
+
project.outputPath,
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
version: '1.0.0',
|
|
113
|
+
generatedAt: new Date().toISOString(),
|
|
114
|
+
generatorVersion,
|
|
115
|
+
buildInputs: buildInputs.relativePaths,
|
|
116
|
+
fragments: {},
|
|
117
|
+
},
|
|
118
|
+
null,
|
|
119
|
+
2
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const earlier = new Date(now.getTime() - 10_000);
|
|
125
|
+
await utimes(project.configPath, earlier, earlier);
|
|
126
|
+
await utimes(project.fragmentPath, earlier, earlier);
|
|
127
|
+
await utimes(project.componentPath, earlier, earlier);
|
|
128
|
+
await utimes(project.outputPath, now, now);
|
|
129
|
+
|
|
130
|
+
const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
|
|
131
|
+
configPath: project.configPath,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(status.missing).toBe(false);
|
|
135
|
+
expect(status.stale).toBe(false);
|
|
136
|
+
expect(status.reason).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('marks fragments.json stale when a new input file is added', async () => {
|
|
140
|
+
const project = await createProject('added-input');
|
|
141
|
+
const generatorVersion = await getGeneratorVersion();
|
|
142
|
+
const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
|
|
143
|
+
configPath: project.configPath,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Write fragments.json with current inputs
|
|
147
|
+
await writeFile(
|
|
148
|
+
project.outputPath,
|
|
149
|
+
JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
version: '1.0.0',
|
|
152
|
+
generatedAt: new Date().toISOString(),
|
|
153
|
+
generatorVersion,
|
|
154
|
+
buildInputs: buildInputs.relativePaths,
|
|
155
|
+
fragments: {},
|
|
156
|
+
},
|
|
157
|
+
null,
|
|
158
|
+
2
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Add a new component file — buildInputs list will differ
|
|
163
|
+
const newComponentPath = join(project.projectDir, 'src', 'components', 'Input.tsx');
|
|
164
|
+
await writeFile(newComponentPath, 'export function Input() { return null; }\n');
|
|
165
|
+
|
|
166
|
+
const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
|
|
167
|
+
configPath: project.configPath,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(status.stale).toBe(true);
|
|
171
|
+
expect(status.reason).toContain('build inputs changed');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('marks legacy fragments.json without build metadata as stale', async () => {
|
|
175
|
+
const project = await createProject('legacy-output');
|
|
176
|
+
const generatorVersion = await getGeneratorVersion();
|
|
177
|
+
|
|
178
|
+
await writeFile(
|
|
179
|
+
project.outputPath,
|
|
180
|
+
JSON.stringify(
|
|
181
|
+
{
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
generatedAt: new Date().toISOString(),
|
|
184
|
+
generatorVersion,
|
|
185
|
+
fragments: {},
|
|
186
|
+
},
|
|
187
|
+
null,
|
|
188
|
+
2
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
|
|
193
|
+
configPath: project.configPath,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(status.stale).toBe(true);
|
|
197
|
+
expect(status.reason).toBe('missing build input metadata');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('marks fragments.json stale when an input file disappears', async () => {
|
|
201
|
+
const project = await createProject('deleted-input');
|
|
202
|
+
const generatorVersion = await getGeneratorVersion();
|
|
203
|
+
const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
|
|
204
|
+
configPath: project.configPath,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await writeFile(
|
|
208
|
+
project.outputPath,
|
|
209
|
+
JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
version: '1.0.0',
|
|
212
|
+
generatedAt: new Date().toISOString(),
|
|
213
|
+
generatorVersion,
|
|
214
|
+
buildInputs: buildInputs.relativePaths,
|
|
215
|
+
fragments: {},
|
|
216
|
+
},
|
|
217
|
+
null,
|
|
218
|
+
2
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await rm(project.componentPath);
|
|
223
|
+
|
|
224
|
+
const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
|
|
225
|
+
configPath: project.configPath,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(status.stale).toBe(true);
|
|
229
|
+
expect(status.reason).toContain('build inputs changed');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
addNextTranspilePackages,
|
|
8
|
+
generateNextjsLayout,
|
|
9
|
+
generateNextjsPage,
|
|
10
|
+
generateNextjsProviders,
|
|
11
|
+
} from "../create.js";
|
|
12
|
+
|
|
13
|
+
describe("create command templates", () => {
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
tmpDir = await mkdtemp(join(tmpdir(), "fragments-create-test-"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("generates a Next.js layout that routes providers through a client wrapper", () => {
|
|
25
|
+
const layout = generateNextjsLayout("../styles/theme.css");
|
|
26
|
+
|
|
27
|
+
expect(layout).toContain("import { Providers } from './providers';");
|
|
28
|
+
expect(layout).toContain("<Providers>{children}</Providers>");
|
|
29
|
+
expect(layout).not.toContain("ThemeProvider");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("generates a client providers module for Next.js", () => {
|
|
33
|
+
const providers = generateNextjsProviders();
|
|
34
|
+
|
|
35
|
+
expect(providers).toContain("'use client';");
|
|
36
|
+
expect(providers).toContain("ThemeProvider");
|
|
37
|
+
expect(providers).toContain("ToastProvider");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("marks the sample Next.js page as a client component", () => {
|
|
41
|
+
const page = generateNextjsPage();
|
|
42
|
+
|
|
43
|
+
expect(page).toContain("'use client';");
|
|
44
|
+
expect(page).toContain("import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("adds transpilePackages for @fragments-sdk/ui to next.config.ts", async () => {
|
|
48
|
+
const projectDir = join(tmpDir, "next-app");
|
|
49
|
+
await mkdir(projectDir, { recursive: true });
|
|
50
|
+
const configPath = join(projectDir, "next.config.ts");
|
|
51
|
+
|
|
52
|
+
await writeFile(
|
|
53
|
+
configPath,
|
|
54
|
+
[
|
|
55
|
+
'import type { NextConfig } from "next";',
|
|
56
|
+
"",
|
|
57
|
+
"const nextConfig: NextConfig = {",
|
|
58
|
+
" /* config options here */",
|
|
59
|
+
"};",
|
|
60
|
+
"",
|
|
61
|
+
"export default nextConfig;",
|
|
62
|
+
"",
|
|
63
|
+
].join("\n"),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
addNextTranspilePackages(projectDir);
|
|
67
|
+
|
|
68
|
+
const updated = await readFile(configPath, "utf-8");
|
|
69
|
+
expect(updated).toContain("transpilePackages: ['@fragments-sdk/ui']");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, writeFile, mkdir, rm, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { diffProps, validateDrift, type DriftItem } from '../../validators.js';
|
|
6
|
-
import type { PropMeta } from '
|
|
6
|
+
import type { PropMeta } from '@fragments-sdk/extract';
|
|
7
7
|
import type { FragmentsConfig } from '@fragments-sdk/core';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|