@fragments-sdk/cli 0.3.0 → 0.3.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.
Files changed (28) hide show
  1. package/dist/bin.js +8 -8
  2. package/dist/{chunk-7H2MMGYG.js → chunk-3OTEW66K.js} +2 -2
  3. package/dist/{chunk-4FDQSGKX.js → chunk-ACIDZOYW.js} +2 -2
  4. package/dist/{chunk-OAENNG3G.js → chunk-PMGI7ATF.js} +27 -1
  5. package/dist/chunk-PMGI7ATF.js.map +1 -0
  6. package/dist/{generate-4LQNJ7SX.js → generate-3LBZANQ3.js} +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/{init-EMVI47QG.js → init-NKIUCYTG.js} +2 -2
  9. package/dist/{scan-4YPRF7FV.js → scan-3ZAOVO4U.js} +3 -3
  10. package/dist/{test-SQ5ZHXWU.js → test-ZCTR4LBB.js} +2 -2
  11. package/dist/{tokens-HSGMYK64.js → tokens-5JQ5IOR2.js} +2 -2
  12. package/dist/{viewer-7ICROM7Q.js → viewer-D7QC4GM2.js} +20 -19
  13. package/dist/viewer-D7QC4GM2.js.map +1 -0
  14. package/package.json +1 -1
  15. package/src/core/discovery.ts +40 -0
  16. package/src/core/node.ts +1 -0
  17. package/src/viewer/__tests__/viewer-integration.test.ts +245 -0
  18. package/src/viewer/server.ts +22 -7
  19. package/src/viewer/tailwind.config.js +1 -1
  20. package/dist/chunk-OAENNG3G.js.map +0 -1
  21. package/dist/viewer-7ICROM7Q.js.map +0 -1
  22. /package/dist/{chunk-7H2MMGYG.js.map → chunk-3OTEW66K.js.map} +0 -0
  23. /package/dist/{chunk-4FDQSGKX.js.map → chunk-ACIDZOYW.js.map} +0 -0
  24. /package/dist/{generate-4LQNJ7SX.js.map → generate-3LBZANQ3.js.map} +0 -0
  25. /package/dist/{init-EMVI47QG.js.map → init-NKIUCYTG.js.map} +0 -0
  26. /package/dist/{scan-4YPRF7FV.js.map → scan-3ZAOVO4U.js.map} +0 -0
  27. /package/dist/{test-SQ5ZHXWU.js.map → test-ZCTR4LBB.js.map} +0 -0
  28. /package/dist/{tokens-HSGMYK64.js.map → tokens-5JQ5IOR2.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI, MCP server, and dev tools for Fragments design system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -296,6 +296,46 @@ export async function discoverComponentsFromBarrel(
296
296
  return components;
297
297
  }
298
298
 
299
+ /**
300
+ * Discover fragment files from installed packages that declare a "fragments" field
301
+ * in their package.json. This allows consumer projects to see components from
302
+ * installed packages (e.g. @fragments-sdk/ui) in the dev viewer.
303
+ */
304
+ export async function discoverInstalledFragments(
305
+ projectRoot: string
306
+ ): Promise<DiscoveredFile[]> {
307
+ const pkgJsonPath = resolve(projectRoot, 'package.json');
308
+ if (!existsSync(pkgJsonPath)) return [];
309
+
310
+ const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
311
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
312
+ const results: DiscoveredFile[] = [];
313
+
314
+ for (const depName of Object.keys(allDeps)) {
315
+ const depDir = resolve(projectRoot, 'node_modules', depName);
316
+ const depPkgPath = resolve(depDir, 'package.json');
317
+ if (!existsSync(depPkgPath)) continue;
318
+
319
+ const depPkg = JSON.parse(await readFile(depPkgPath, 'utf-8'));
320
+ if (!depPkg.fragments) continue;
321
+
322
+ // Package declares fragments — scan for source fragment files
323
+ const files = await fg(
324
+ [`src/**/*${BRAND.fileExtension}`, 'src/**/*.stories.tsx'],
325
+ { cwd: depDir, ignore: ['**/node_modules/**'], absolute: false }
326
+ );
327
+
328
+ for (const rel of files) {
329
+ results.push({
330
+ relativePath: `${depName}/${rel}`,
331
+ absolutePath: resolve(depDir, rel),
332
+ });
333
+ }
334
+ }
335
+
336
+ return results;
337
+ }
338
+
299
339
  /**
300
340
  * Discover all components using multiple strategies
301
341
  */
package/src/core/node.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  discoverSegmentFiles,
12
12
  discoverRecipeFiles,
13
13
  discoverComponentFiles,
14
+ discoverInstalledFragments,
14
15
  extractComponentName,
15
16
  // New: Component discovery from source files (no segment files needed)
16
17
  discoverComponentsFromSource,
@@ -0,0 +1,245 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync } from "node:fs";
5
+ import { readFile, mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { discoverInstalledFragments } from "../../core/discovery.js";
8
+
9
+ /**
10
+ * Integration tests for the consolidated viewer.
11
+ *
12
+ * After packages were merged into @fragments-sdk/cli, the viewer's
13
+ * path resolution changed. These tests verify that:
14
+ * - Viewer assets (HTML, TSX entry points) are found at the correct paths
15
+ * - The @fragments/core alias resolves to the consolidated core source
16
+ * - The virtual module generates valid import statements
17
+ * - The Vite config references correct file system locations
18
+ */
19
+
20
+ // Simulate the same path resolution used in server.ts and vite-plugin.ts
21
+ // At runtime, __dirname is dist/. In tests (vitest), it's the source dir.
22
+ const testDir = dirname(fileURLToPath(import.meta.url));
23
+ const viewerDir = resolve(testDir, "..");
24
+ const cliPackageRoot = resolve(viewerDir, "../..");
25
+
26
+ describe("viewer path resolution", () => {
27
+ it("viewerRoot resolves to a directory containing index.html", () => {
28
+ // server.ts: viewerRoot = resolve(cliPackageRoot, "src/viewer")
29
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
30
+ expect(existsSync(resolve(viewerRoot, "index.html"))).toBe(true);
31
+ });
32
+
33
+ it("viewerRoot contains entry.tsx", () => {
34
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
35
+ expect(existsSync(resolve(viewerRoot, "entry.tsx"))).toBe(true);
36
+ });
37
+
38
+ it("viewerRoot contains preview-frame.html", () => {
39
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
40
+ expect(existsSync(resolve(viewerRoot, "preview-frame.html"))).toBe(true);
41
+ });
42
+
43
+ it("viewerRoot contains preview-frame-entry.tsx", () => {
44
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
45
+ expect(existsSync(resolve(viewerRoot, "preview-frame-entry.tsx"))).toBe(true);
46
+ });
47
+
48
+ it("viewerRoot contains render-template.html", () => {
49
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
50
+ expect(existsSync(resolve(viewerRoot, "render-template.html"))).toBe(true);
51
+ });
52
+
53
+ it("viewerRoot contains tailwind.config.js", () => {
54
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
55
+ expect(existsSync(resolve(viewerRoot, "tailwind.config.js"))).toBe(true);
56
+ });
57
+
58
+ it("viewerRoot contains postcss.config.js", () => {
59
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
60
+ expect(existsSync(resolve(viewerRoot, "postcss.config.js"))).toBe(true);
61
+ });
62
+
63
+ it("viewerRoot contains styles/globals.css", () => {
64
+ const viewerRoot = resolve(cliPackageRoot, "src/viewer");
65
+ expect(existsSync(resolve(viewerRoot, "styles/globals.css"))).toBe(true);
66
+ });
67
+ });
68
+
69
+ describe("@fragments/core alias resolution", () => {
70
+ it("core/index.ts exists at the expected path", () => {
71
+ // server.ts: "@fragments/core": resolve(cliPackageRoot, "src/core/index.ts")
72
+ const corePath = resolve(cliPackageRoot, "src/core/index.ts");
73
+ expect(existsSync(corePath)).toBe(true);
74
+ });
75
+
76
+ it("core/index.ts exports storyModuleToSegment", async () => {
77
+ const corePath = resolve(cliPackageRoot, "src/core/index.ts");
78
+ const content = await readFile(corePath, "utf-8");
79
+ // The virtual module imports { storyModuleToSegment, setPreviewConfig }
80
+ expect(content).toContain("storyModuleToSegment");
81
+ });
82
+
83
+ it("core/index.ts exports setPreviewConfig", async () => {
84
+ const corePath = resolve(cliPackageRoot, "src/core/index.ts");
85
+ const content = await readFile(corePath, "utf-8");
86
+ expect(content).toContain("setPreviewConfig");
87
+ });
88
+ });
89
+
90
+ describe("virtual module @fragments/core import", () => {
91
+ it("vite-plugin generates import from @fragments/core (resolved via alias)", async () => {
92
+ // The virtual module template in vite-plugin.ts must import from @fragments/core
93
+ // which is resolved by the Vite alias to the consolidated core source
94
+ const pluginPath = resolve(viewerDir, "vite-plugin.ts");
95
+ const content = await readFile(pluginPath, "utf-8");
96
+
97
+ // The generated virtual module string should reference @fragments/core
98
+ expect(content).toContain(
99
+ 'import { storyModuleToSegment, setPreviewConfig } from "@fragments/core"'
100
+ );
101
+ });
102
+
103
+ it("server.ts sets up @fragments/core alias to src/core/index.ts", async () => {
104
+ const serverPath = resolve(viewerDir, "server.ts");
105
+ const content = await readFile(serverPath, "utf-8");
106
+
107
+ // The alias must resolve @fragments/core to the consolidated core
108
+ expect(content).toContain('"@fragments/core": resolve(cliPackageRoot, "src/core/index.ts")');
109
+ });
110
+
111
+ it("server.ts sets up @fragments/viewer alias", async () => {
112
+ const serverPath = resolve(viewerDir, "server.ts");
113
+ const content = await readFile(serverPath, "utf-8");
114
+
115
+ expect(content).toContain('"@fragments/viewer": viewerRoot');
116
+ });
117
+ });
118
+
119
+ describe("viewer HTML templates", () => {
120
+ it("index.html contains entry.tsx script reference", async () => {
121
+ const htmlPath = resolve(viewerDir, "index.html");
122
+ const content = await readFile(htmlPath, "utf-8");
123
+
124
+ // The HTML references /src/entry.tsx which gets rewritten to an absolute path
125
+ expect(content).toContain('src="/src/entry.tsx"');
126
+ });
127
+
128
+ it("preview-frame.html contains preview-frame-entry.tsx reference", async () => {
129
+ const htmlPath = resolve(viewerDir, "preview-frame.html");
130
+ const content = await readFile(htmlPath, "utf-8");
131
+
132
+ expect(content).toContain('src="/src/preview-frame-entry.tsx"');
133
+ });
134
+
135
+ it("vite-plugin rewrites entry paths using viewerAssetsRoot", async () => {
136
+ const pluginPath = resolve(viewerDir, "vite-plugin.ts");
137
+ const content = await readFile(pluginPath, "utf-8");
138
+
139
+ // The plugin must use viewerAssetsRoot (not __dirname or resolve(__dirname, ".."))
140
+ // to find viewer HTML and entry files
141
+ expect(content).toContain("const viewerAssetsRoot = resolve(__dirname,");
142
+ expect(content).toContain('const viewerRoot = viewerAssetsRoot');
143
+ });
144
+ });
145
+
146
+ describe("discoverInstalledFragments", () => {
147
+ let tmpDir: string;
148
+
149
+ beforeAll(async () => {
150
+ tmpDir = await mkdtemp(resolve(tmpdir(), "fragments-test-"));
151
+
152
+ // Create a fake project with a dependency that has fragments
153
+ await writeFile(
154
+ resolve(tmpDir, "package.json"),
155
+ JSON.stringify({
156
+ dependencies: { "@acme/ui": "1.0.0", "some-lib": "2.0.0" },
157
+ })
158
+ );
159
+
160
+ // @acme/ui declares fragments
161
+ const acmeDir = resolve(tmpDir, "node_modules/@acme/ui");
162
+ await mkdir(resolve(acmeDir, "src/components"), { recursive: true });
163
+ await writeFile(
164
+ resolve(acmeDir, "package.json"),
165
+ JSON.stringify({ name: "@acme/ui", fragments: { src: "src" } })
166
+ );
167
+ await writeFile(
168
+ resolve(acmeDir, "src/components/Button.fragment.tsx"),
169
+ "export default {};"
170
+ );
171
+ await writeFile(
172
+ resolve(acmeDir, "src/components/Card.fragment.tsx"),
173
+ "export default {};"
174
+ );
175
+
176
+ // some-lib does NOT declare fragments
177
+ const someLibDir = resolve(tmpDir, "node_modules/some-lib");
178
+ await mkdir(resolve(someLibDir, "src"), { recursive: true });
179
+ await writeFile(
180
+ resolve(someLibDir, "package.json"),
181
+ JSON.stringify({ name: "some-lib" })
182
+ );
183
+ await writeFile(
184
+ resolve(someLibDir, "src/Foo.fragment.tsx"),
185
+ "export default {};"
186
+ );
187
+ });
188
+
189
+ afterAll(async () => {
190
+ await rm(tmpDir, { recursive: true, force: true });
191
+ });
192
+
193
+ it("finds fragment files from packages with a fragments field", async () => {
194
+ const results = await discoverInstalledFragments(tmpDir);
195
+ const paths = results.map((r) => r.relativePath).sort();
196
+ expect(paths).toEqual([
197
+ "@acme/ui/src/components/Button.fragment.tsx",
198
+ "@acme/ui/src/components/Card.fragment.tsx",
199
+ ]);
200
+ });
201
+
202
+ it("ignores packages without a fragments field", async () => {
203
+ const results = await discoverInstalledFragments(tmpDir);
204
+ const hasLib = results.some((r) => r.relativePath.includes("some-lib"));
205
+ expect(hasLib).toBe(false);
206
+ });
207
+
208
+ it("returns absolutePath that exists on disk", async () => {
209
+ const results = await discoverInstalledFragments(tmpDir);
210
+ for (const r of results) {
211
+ expect(existsSync(r.absolutePath)).toBe(true);
212
+ }
213
+ });
214
+
215
+ it("returns empty array when no package.json exists", async () => {
216
+ const emptyDir = await mkdtemp(resolve(tmpdir(), "fragments-empty-"));
217
+ const results = await discoverInstalledFragments(emptyDir);
218
+ expect(results).toEqual([]);
219
+ await rm(emptyDir, { recursive: true, force: true });
220
+ });
221
+ });
222
+
223
+ describe("no stale @fragments/* package references", () => {
224
+ it("server.ts does not reference old resolveFragmentsPackage for core", async () => {
225
+ const serverPath = resolve(viewerDir, "server.ts");
226
+ const content = await readFile(serverPath, "utf-8");
227
+
228
+ // Should NOT try to resolve @fragments/core from node_modules
229
+ expect(content).not.toContain('resolveFragmentsPackage("core"');
230
+ });
231
+
232
+ it("no source files import from @fragments/core as a runtime dependency", async () => {
233
+ // All source-level imports should use relative paths (../core/).
234
+ // Only the generated virtual module string uses @fragments/core (resolved via alias).
235
+ const serverPath = resolve(viewerDir, "server.ts");
236
+ const content = await readFile(serverPath, "utf-8");
237
+
238
+ // server.ts should import from relative paths, not @fragments/core
239
+ const lines = content.split("\n");
240
+ const importLines = lines.filter(
241
+ (l) => l.startsWith("import") && l.includes("@fragments/core")
242
+ );
243
+ expect(importLines).toHaveLength(0);
244
+ });
245
+ });
@@ -23,7 +23,7 @@ import autoprefixer from "autoprefixer";
23
23
  import { resolve, dirname, join } from "node:path";
24
24
  import { existsSync, realpathSync } from "node:fs";
25
25
  import { fileURLToPath } from "node:url";
26
- import { loadConfig, discoverSegmentFiles } from "../core/node.js";
26
+ import { loadConfig, discoverSegmentFiles, discoverInstalledFragments } from "../core/node.js";
27
27
  import { segmentsPlugin } from "./vite-plugin.js";
28
28
 
29
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -69,9 +69,11 @@ export async function createDevServer(
69
69
  // Load segments config
70
70
  const { config, configDir } = await loadConfig(configPath);
71
71
 
72
- // Discover segment files
72
+ // Discover segment files (local + installed packages)
73
73
  const segmentFiles = await discoverSegmentFiles(config, configDir);
74
- console.log(`📦 Found ${segmentFiles.length} fragment file(s)`);
74
+ const installedFiles = await discoverInstalledFragments(projectRoot);
75
+ const allSegmentFiles = [...segmentFiles, ...installedFiles];
76
+ console.log(`📦 Found ${segmentFiles.length} local + ${installedFiles.length} installed fragment file(s)`);
75
77
 
76
78
  // Try to load project's Vite config
77
79
  let projectViteConfig: InlineConfig = {};
@@ -99,6 +101,19 @@ export async function createDevServer(
99
101
  const nodeModulesPath = findNodeModules(projectRoot);
100
102
  console.log(`📁 Using node_modules: ${nodeModulesPath}`);
101
103
 
104
+ // Collect installed package roots so Vite can serve files from node_modules
105
+ const installedPkgRoots = [...new Set(
106
+ installedFiles.map(f => {
107
+ const idx = f.absolutePath.indexOf('/node_modules/');
108
+ if (idx === -1) return dirname(f.absolutePath);
109
+ const afterNm = f.absolutePath.slice(idx + '/node_modules/'.length);
110
+ const pkgName = afterNm.startsWith('@')
111
+ ? afterNm.split('/').slice(0, 2).join('/')
112
+ : afterNm.split('/')[0];
113
+ return resolve(projectRoot, 'node_modules', pkgName);
114
+ })
115
+ )];
116
+
102
117
  // Our Segments-specific configuration
103
118
  const segmentsConfig: InlineConfig = {
104
119
  configFile: false, // Don't load config again
@@ -110,7 +125,7 @@ export async function createDevServer(
110
125
  open: open ? "/fragments/" : false,
111
126
  fs: {
112
127
  // Allow serving files from viewer package, project, and node_modules root
113
- allow: [viewerRoot, projectRoot, configDir, dirname(nodeModulesPath)],
128
+ allow: [viewerRoot, projectRoot, configDir, dirname(nodeModulesPath), ...installedPkgRoots],
114
129
  },
115
130
  },
116
131
 
@@ -120,7 +135,7 @@ export async function createDevServer(
120
135
 
121
136
  // Segments plugins (array including SVGR)
122
137
  ...segmentsPlugin({
123
- segmentFiles,
138
+ segmentFiles: allSegmentFiles,
124
139
  config,
125
140
  projectRoot,
126
141
  }),
@@ -150,14 +165,14 @@ export async function createDevServer(
150
165
  alias: {
151
166
  // Allow importing from viewer package
152
167
  "@fragments/viewer": viewerRoot,
168
+ // Resolve @fragments/core to the consolidated core source
169
+ "@fragments/core": resolve(cliPackageRoot, "src/core/index.ts"),
153
170
  // Ensure ALL react imports resolve to project's node_modules
154
171
  // This is critical for viewer files loaded from outside project root
155
172
  "react": safeRealpath(join(nodeModulesPath, "react")),
156
173
  "react-dom": safeRealpath(join(nodeModulesPath, "react-dom")),
157
174
  "react/jsx-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-runtime")),
158
175
  "react/jsx-dev-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-dev-runtime")),
159
- // Resolve @fragments packages - try project's node_modules first, fall back to monorepo
160
- "@fragments/core": resolveFragmentsPackage("core", nodeModulesPath),
161
176
  },
162
177
  },
163
178
  };
@@ -7,7 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  export default {
8
8
  content: [
9
9
  resolve(__dirname, 'index.html'),
10
- resolve(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
10
+ resolve(__dirname, '**/*.{js,ts,jsx,tsx}'),
11
11
  ],
12
12
  darkMode: 'class',
13
13
  theme: {