@fragments-sdk/core 0.1.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.
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { analyzeComposition } from "./composition.js";
3
+ import type { CompiledFragment } from "./types.js";
4
+
5
+ function makeFragment(overrides: Partial<CompiledFragment> & { meta: CompiledFragment["meta"] }): CompiledFragment {
6
+ return {
7
+ filePath: "src/components/Test.tsx",
8
+ usage: { when: [], whenNot: [] },
9
+ props: {},
10
+ variants: [],
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ const baseFragments: Record<string, CompiledFragment> = {
16
+ Button: makeFragment({
17
+ meta: { name: "Button", description: "A button", category: "actions", status: "stable" },
18
+ relations: [
19
+ { component: "IconButton", relationship: "alternative", note: "Use for icon-only actions" },
20
+ { component: "ButtonGroup", relationship: "composition", note: "Wrap multiple buttons" },
21
+ ],
22
+ usage: {
23
+ when: ["User needs to trigger an action"],
24
+ whenNot: ["Use Link for navigation"],
25
+ },
26
+ }),
27
+ IconButton: makeFragment({
28
+ meta: { name: "IconButton", description: "Icon-only button", category: "actions", status: "stable" },
29
+ relations: [
30
+ { component: "Button", relationship: "alternative", note: "Use for labeled actions" },
31
+ ],
32
+ }),
33
+ ButtonGroup: makeFragment({
34
+ meta: { name: "ButtonGroup", description: "Groups buttons", category: "actions", status: "stable" },
35
+ relations: [
36
+ { component: "Button", relationship: "child", note: "Contains buttons" },
37
+ ],
38
+ }),
39
+ TextField: makeFragment({
40
+ meta: { name: "TextField", description: "Text input", category: "forms", status: "stable" },
41
+ relations: [
42
+ { component: "Form", relationship: "parent", note: "Should be inside a Form" },
43
+ { component: "Label", relationship: "sibling", note: "Pair with a Label" },
44
+ ],
45
+ usage: {
46
+ when: ["User needs to enter text"],
47
+ whenNot: ["Use TextArea for multiline input"],
48
+ },
49
+ }),
50
+ Form: makeFragment({
51
+ meta: { name: "Form", description: "Form container", category: "forms", status: "stable" },
52
+ }),
53
+ Label: makeFragment({
54
+ meta: { name: "Label", description: "Form label", category: "forms", status: "stable" },
55
+ }),
56
+ Alert: makeFragment({
57
+ meta: { name: "Alert", description: "Feedback alert", category: "feedback", status: "stable" },
58
+ }),
59
+ Toast: makeFragment({
60
+ meta: { name: "Toast", description: "Toast notification", category: "feedback", status: "experimental" },
61
+ }),
62
+ OldButton: makeFragment({
63
+ meta: { name: "OldButton", description: "Use Button instead", category: "actions", status: "deprecated" },
64
+ }),
65
+ Link: makeFragment({
66
+ meta: { name: "Link", description: "Navigation link", category: "navigation", status: "stable" },
67
+ usage: {
68
+ when: ["User needs to navigate"],
69
+ whenNot: ["Use Button for actions"],
70
+ },
71
+ }),
72
+ };
73
+
74
+ describe("analyzeComposition", () => {
75
+ describe("name validation", () => {
76
+ it("should split components into found and unknown", () => {
77
+ const result = analyzeComposition(baseFragments, ["Button", "NonExistent", "TextField", "AlsoFake"]);
78
+ expect(result.components).toEqual(["Button", "TextField"]);
79
+ expect(result.unknown).toEqual(["NonExistent", "AlsoFake"]);
80
+ });
81
+
82
+ it("should handle all valid names", () => {
83
+ const result = analyzeComposition(baseFragments, ["Button", "TextField"]);
84
+ expect(result.components).toEqual(["Button", "TextField"]);
85
+ expect(result.unknown).toEqual([]);
86
+ });
87
+
88
+ it("should handle all unknown names", () => {
89
+ const result = analyzeComposition(baseFragments, ["Foo", "Bar"]);
90
+ expect(result.components).toEqual([]);
91
+ expect(result.unknown).toEqual(["Foo", "Bar"]);
92
+ });
93
+
94
+ it("should handle empty input", () => {
95
+ const result = analyzeComposition(baseFragments, []);
96
+ expect(result.components).toEqual([]);
97
+ expect(result.unknown).toEqual([]);
98
+ expect(result.warnings).toEqual([]);
99
+ expect(result.suggestions).toEqual([]);
100
+ expect(result.guidelines).toEqual([]);
101
+ });
102
+ });
103
+
104
+ describe("relation checks", () => {
105
+ it("should warn about missing parent", () => {
106
+ const result = analyzeComposition(baseFragments, ["TextField"]);
107
+ const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
108
+ expect(parentWarning).toBeDefined();
109
+ expect(parentWarning!.component).toBe("TextField");
110
+ expect(parentWarning!.relatedComponent).toBe("Form");
111
+ });
112
+
113
+ it("should not warn about parent when parent is selected", () => {
114
+ const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
115
+ const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
116
+ expect(parentWarning).toBeUndefined();
117
+ });
118
+
119
+ it("should suggest missing children", () => {
120
+ const result = analyzeComposition(baseFragments, ["ButtonGroup"]);
121
+ const childSuggestion = result.suggestions.find(
122
+ (s) => s.component === "Button" && s.relationship === "child"
123
+ );
124
+ expect(childSuggestion).toBeDefined();
125
+ expect(childSuggestion!.sourceComponent).toBe("ButtonGroup");
126
+ });
127
+
128
+ it("should warn about missing composition peers", () => {
129
+ const result = analyzeComposition(baseFragments, ["Button"]);
130
+ const compWarning = result.warnings.find((w) => w.type === "missing_composition");
131
+ expect(compWarning).toBeDefined();
132
+ expect(compWarning!.component).toBe("Button");
133
+ expect(compWarning!.relatedComponent).toBe("ButtonGroup");
134
+ });
135
+
136
+ it("should not warn when composition peer is selected", () => {
137
+ const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup"]);
138
+ const compWarning = result.warnings.find((w) => w.type === "missing_composition");
139
+ expect(compWarning).toBeUndefined();
140
+ });
141
+
142
+ it("should suggest missing siblings", () => {
143
+ const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
144
+ const siblingSuggestion = result.suggestions.find(
145
+ (s) => s.component === "Label" && s.relationship === "sibling"
146
+ );
147
+ expect(siblingSuggestion).toBeDefined();
148
+ expect(siblingSuggestion!.sourceComponent).toBe("TextField");
149
+ });
150
+
151
+ it("should warn about redundant alternatives", () => {
152
+ const result = analyzeComposition(baseFragments, ["Button", "IconButton"]);
153
+ const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
154
+ expect(altWarning).toBeDefined();
155
+ expect(altWarning!.component).toBe("Button");
156
+ expect(altWarning!.relatedComponent).toBe("IconButton");
157
+ });
158
+
159
+ it("should not warn when only one alternative is selected", () => {
160
+ const result = analyzeComposition(baseFragments, ["Button"]);
161
+ const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
162
+ expect(altWarning).toBeUndefined();
163
+ });
164
+ });
165
+
166
+ describe("usage conflict checks", () => {
167
+ it("should emit guideline when whenNot mentions another selected component", () => {
168
+ const result = analyzeComposition(baseFragments, ["Button", "Link"]);
169
+ const conflict = result.guidelines.find(
170
+ (g) => g.component === "Link" && g.guideline.includes("Button")
171
+ );
172
+ expect(conflict).toBeDefined();
173
+ });
174
+
175
+ it("should not emit guideline when no conflict exists", () => {
176
+ const result = analyzeComposition(baseFragments, ["Button", "Alert"]);
177
+ expect(result.guidelines).toEqual([]);
178
+ });
179
+ });
180
+
181
+ describe("status warnings", () => {
182
+ it("should warn about deprecated components", () => {
183
+ const result = analyzeComposition(baseFragments, ["OldButton"]);
184
+ const depWarning = result.warnings.find((w) => w.type === "deprecated");
185
+ expect(depWarning).toBeDefined();
186
+ expect(depWarning!.component).toBe("OldButton");
187
+ expect(depWarning!.message).toContain("deprecated");
188
+ });
189
+
190
+ it("should warn about experimental components", () => {
191
+ const result = analyzeComposition(baseFragments, ["Toast"]);
192
+ const expWarning = result.warnings.find((w) => w.type === "experimental");
193
+ expect(expWarning).toBeDefined();
194
+ expect(expWarning!.component).toBe("Toast");
195
+ expect(expWarning!.message).toContain("experimental");
196
+ });
197
+
198
+ it("should not warn about stable components", () => {
199
+ const result = analyzeComposition(baseFragments, ["Button"]);
200
+ const statusWarnings = result.warnings.filter(
201
+ (w) => w.type === "deprecated" || w.type === "experimental"
202
+ );
203
+ expect(statusWarnings).toEqual([]);
204
+ });
205
+ });
206
+
207
+ describe("category gap analysis", () => {
208
+ it("should suggest feedback component when forms are selected without feedback", () => {
209
+ const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
210
+ const gapSuggestion = result.suggestions.find(
211
+ (s) => s.relationship === "category_gap"
212
+ );
213
+ expect(gapSuggestion).toBeDefined();
214
+ expect(gapSuggestion!.component).toBe("Alert");
215
+ expect(gapSuggestion!.reason).toContain("feedback");
216
+ });
217
+
218
+ it("should suggest feedback component when actions are selected without feedback", () => {
219
+ const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup"]);
220
+ const gapSuggestion = result.suggestions.find(
221
+ (s) => s.relationship === "category_gap"
222
+ );
223
+ expect(gapSuggestion).toBeDefined();
224
+ expect(gapSuggestion!.component).toBe("Alert");
225
+ });
226
+
227
+ it("should not suggest category gap when feedback is already present", () => {
228
+ const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup", "Alert"]);
229
+ const gapSuggestion = result.suggestions.find(
230
+ (s) => s.relationship === "category_gap"
231
+ );
232
+ expect(gapSuggestion).toBeUndefined();
233
+ });
234
+
235
+ it("should prefer stable components over experimental in category gap suggestions", () => {
236
+ // Alert is stable, Toast is experimental — should pick Alert
237
+ const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
238
+ const gapSuggestion = result.suggestions.find(
239
+ (s) => s.relationship === "category_gap"
240
+ );
241
+ expect(gapSuggestion?.component).toBe("Alert");
242
+ });
243
+ });
244
+
245
+ describe("deduplication", () => {
246
+ it("should not suggest the same component twice", () => {
247
+ const result = analyzeComposition(baseFragments, ["ButtonGroup", "Button"]);
248
+ const buttonSuggestions = result.suggestions.filter(
249
+ (s) => s.component === "Button"
250
+ );
251
+ // Button is already selected so it shouldn't be suggested
252
+ expect(buttonSuggestions).toEqual([]);
253
+ });
254
+ });
255
+
256
+ describe("context parameter", () => {
257
+ it("should accept optional context without error", () => {
258
+ const result = analyzeComposition(baseFragments, ["Button"], "building a form page");
259
+ expect(result.components).toEqual(["Button"]);
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,318 @@
1
+ import type { CompiledFragment, RelationshipType } from "./types.js";
2
+ import type { ComponentGraph } from "@fragments-sdk/context/graph";
3
+ import { ComponentGraphEngine } from "@fragments-sdk/context/graph";
4
+
5
+ // --- Public types ---
6
+
7
+ export interface CompositionWarning {
8
+ type:
9
+ | "missing_parent"
10
+ | "missing_child"
11
+ | "missing_composition"
12
+ | "redundant_alternative"
13
+ | "deprecated"
14
+ | "experimental";
15
+ component: string;
16
+ message: string;
17
+ relatedComponent?: string;
18
+ }
19
+
20
+ export interface CompositionSuggestion {
21
+ component: string;
22
+ reason: string;
23
+ relationship: RelationshipType | "category_gap";
24
+ sourceComponent: string;
25
+ }
26
+
27
+ export interface CompositionGuideline {
28
+ component: string;
29
+ guideline: string;
30
+ }
31
+
32
+ export interface CompositionAnalysis {
33
+ /** The validated component names (filtered to those that exist) */
34
+ components: string[];
35
+
36
+ /** Components requested but not found in the registry */
37
+ unknown: string[];
38
+
39
+ /** Issues with the current selection */
40
+ warnings: CompositionWarning[];
41
+
42
+ /** Components to consider adding */
43
+ suggestions: CompositionSuggestion[];
44
+
45
+ /** Relevant usage guidelines for the selected components */
46
+ guidelines: CompositionGuideline[];
47
+ }
48
+
49
+ // --- Category affinities ---
50
+
51
+ const CATEGORY_AFFINITIES: Record<string, string[]> = {
52
+ forms: ["feedback"],
53
+ actions: ["feedback"],
54
+ };
55
+
56
+ // --- Main function ---
57
+
58
+ /**
59
+ * Analyzes a set of components as a composition group.
60
+ * Returns warnings about missing relations, usage conflicts,
61
+ * and suggestions for additional components.
62
+ *
63
+ * When a ComponentGraph is provided via `options.graph`, the analysis is
64
+ * enhanced with graph-based dependency detection and block-based suggestions.
65
+ *
66
+ * Browser-safe: no Node.js APIs used.
67
+ */
68
+ export function analyzeComposition(
69
+ fragments: Record<string, CompiledFragment>,
70
+ componentNames: string[],
71
+ _context?: string,
72
+ options?: { graph?: ComponentGraph },
73
+ ): CompositionAnalysis {
74
+ const allNames = new Set(Object.keys(fragments));
75
+
76
+ // 1. Validate names
77
+ const components: string[] = [];
78
+ const unknown: string[] = [];
79
+ for (const name of componentNames) {
80
+ if (allNames.has(name)) {
81
+ components.push(name);
82
+ } else {
83
+ unknown.push(name);
84
+ }
85
+ }
86
+
87
+ const selectedSet = new Set(components);
88
+ const warnings: CompositionWarning[] = [];
89
+ const suggestions: CompositionSuggestion[] = [];
90
+ const guidelines: CompositionGuideline[] = [];
91
+
92
+ // Track suggestions to avoid duplicates
93
+ const suggestedSet = new Set<string>();
94
+
95
+ for (const name of components) {
96
+ const fragment = fragments[name];
97
+
98
+ // 2. Relation checks
99
+ if (fragment.relations) {
100
+ for (const rel of fragment.relations) {
101
+ switch (rel.relationship) {
102
+ case "parent":
103
+ if (!selectedSet.has(rel.component)) {
104
+ warnings.push({
105
+ type: "missing_parent",
106
+ component: name,
107
+ message: `"${name}" expects to be wrapped by "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
108
+ relatedComponent: rel.component,
109
+ });
110
+ }
111
+ break;
112
+
113
+ case "child":
114
+ if (!selectedSet.has(rel.component) && !suggestedSet.has(rel.component)) {
115
+ suggestions.push({
116
+ component: rel.component,
117
+ reason: `"${name}" typically contains "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
118
+ relationship: "child",
119
+ sourceComponent: name,
120
+ });
121
+ suggestedSet.add(rel.component);
122
+ }
123
+ break;
124
+
125
+ case "composition":
126
+ if (!selectedSet.has(rel.component)) {
127
+ warnings.push({
128
+ type: "missing_composition",
129
+ component: name,
130
+ message: `"${name}" is typically used together with "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
131
+ relatedComponent: rel.component,
132
+ });
133
+ }
134
+ break;
135
+
136
+ case "sibling":
137
+ if (!selectedSet.has(rel.component) && !suggestedSet.has(rel.component)) {
138
+ suggestions.push({
139
+ component: rel.component,
140
+ reason: `"${rel.component}" is a sibling of "${name}"${rel.note ? `: ${rel.note}` : ""}`,
141
+ relationship: "sibling",
142
+ sourceComponent: name,
143
+ });
144
+ suggestedSet.add(rel.component);
145
+ }
146
+ break;
147
+
148
+ case "alternative":
149
+ if (selectedSet.has(rel.component)) {
150
+ warnings.push({
151
+ type: "redundant_alternative",
152
+ component: name,
153
+ message: `"${name}" and "${rel.component}" are alternatives — using both may be redundant${rel.note ? `: ${rel.note}` : ""}`,
154
+ relatedComponent: rel.component,
155
+ });
156
+ }
157
+ break;
158
+ }
159
+ }
160
+ }
161
+
162
+ // 3. Usage conflict checks (whenNot)
163
+ if (fragment.usage?.whenNot) {
164
+ for (const whenNotEntry of fragment.usage.whenNot) {
165
+ const lower = whenNotEntry.toLowerCase();
166
+ for (const other of components) {
167
+ if (other !== name && lower.includes(other.toLowerCase())) {
168
+ guidelines.push({
169
+ component: name,
170
+ guideline: `Potential conflict with "${other}": ${whenNotEntry}`,
171
+ });
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // 4. Status warnings
178
+ if (fragment.meta.status === "deprecated") {
179
+ warnings.push({
180
+ type: "deprecated",
181
+ component: name,
182
+ message: fragment.meta.description
183
+ ? `"${name}" is deprecated: ${fragment.meta.description}`
184
+ : `"${name}" is deprecated`,
185
+ });
186
+ } else if (fragment.meta.status === "experimental") {
187
+ warnings.push({
188
+ type: "experimental",
189
+ component: name,
190
+ message: `"${name}" is experimental and may change without notice`,
191
+ });
192
+ }
193
+ }
194
+
195
+ // 5. Category gap analysis
196
+ const selectedCategories = new Set(
197
+ components.map((name) => fragments[name].meta.category)
198
+ );
199
+
200
+ for (const [category, affinities] of Object.entries(CATEGORY_AFFINITIES)) {
201
+ if (!selectedCategories.has(category)) continue;
202
+
203
+ for (const neededCategory of affinities) {
204
+ if (selectedCategories.has(neededCategory)) continue;
205
+
206
+ // Find the best component from the needed category
207
+ const candidate = findBestCategoryCandidate(
208
+ fragments,
209
+ neededCategory,
210
+ selectedSet,
211
+ suggestedSet
212
+ );
213
+ if (candidate) {
214
+ suggestions.push({
215
+ component: candidate,
216
+ reason: `Compositions using "${category}" components often benefit from a "${neededCategory}" component`,
217
+ relationship: "category_gap",
218
+ sourceComponent: components.find(
219
+ (n) => fragments[n].meta.category === category
220
+ )!,
221
+ });
222
+ suggestedSet.add(candidate);
223
+ }
224
+ }
225
+ }
226
+
227
+ // 6. Graph-enhanced analysis (when graph data is available)
228
+ if (options?.graph) {
229
+ const engine = new ComponentGraphEngine(options.graph);
230
+
231
+ // Add graph-based dependency warnings
232
+ for (const name of components) {
233
+ const deps = engine.dependencies(name, ["imports", "hook-depends"]);
234
+ for (const dep of deps) {
235
+ if (
236
+ !selectedSet.has(dep.target) &&
237
+ !suggestedSet.has(dep.target) &&
238
+ allNames.has(dep.target)
239
+ ) {
240
+ suggestions.push({
241
+ component: dep.target,
242
+ reason: `"${name}" ${dep.type === "hook-depends" ? "uses a hook from" : "imports"} "${dep.target}"`,
243
+ relationship: "composition",
244
+ sourceComponent: name,
245
+ });
246
+ suggestedSet.add(dep.target);
247
+ }
248
+ }
249
+ }
250
+
251
+ // Add block-based suggestions
252
+ for (const name of components) {
253
+ const blocks = engine.blocksUsing(name);
254
+ for (const blockName of blocks) {
255
+ // Find other components in this block that aren't selected
256
+ const blockComps = options.graph.edges
257
+ .filter(
258
+ (e) =>
259
+ e.type === "composes" &&
260
+ e.provenance === `block:${blockName}` &&
261
+ (e.source === name || e.target === name)
262
+ )
263
+ .map((e) => (e.source === name ? e.target : e.source));
264
+
265
+ for (const comp of blockComps) {
266
+ if (
267
+ !selectedSet.has(comp) &&
268
+ !suggestedSet.has(comp) &&
269
+ allNames.has(comp)
270
+ ) {
271
+ suggestions.push({
272
+ component: comp,
273
+ reason: `"${name}" and "${comp}" are used together in the "${blockName}" block`,
274
+ relationship: "composition",
275
+ sourceComponent: name,
276
+ });
277
+ suggestedSet.add(comp);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ return { components, unknown, warnings, suggestions, guidelines };
285
+ }
286
+
287
+ /**
288
+ * Find the best candidate component from a given category.
289
+ * Prefers stable components and avoids already-selected or already-suggested ones.
290
+ */
291
+ function findBestCategoryCandidate(
292
+ fragments: Record<string, CompiledFragment>,
293
+ category: string,
294
+ selectedSet: Set<string>,
295
+ suggestedSet: Set<string>
296
+ ): string | null {
297
+ let best: string | null = null;
298
+ let bestScore = -1;
299
+
300
+ for (const [name, fragment] of Object.entries(fragments)) {
301
+ if (fragment.meta.category !== category) continue;
302
+ if (selectedSet.has(name) || suggestedSet.has(name)) continue;
303
+
304
+ const status = fragment.meta.status ?? "stable";
305
+ let score = 0;
306
+ if (status === "stable") score = 3;
307
+ else if (status === "beta") score = 2;
308
+ else if (status === "experimental") score = 1;
309
+ // deprecated gets 0
310
+
311
+ if (score > bestScore) {
312
+ bestScore = score;
313
+ best = name;
314
+ }
315
+ }
316
+
317
+ return best;
318
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Brand constants for easy rebranding if domain availability requires it.
3
+ * All naming throughout the codebase should reference these constants.
4
+ */
5
+ export const BRAND = {
6
+ /** Display name (e.g., "Fragments") */
7
+ name: "Fragments",
8
+
9
+ /** Lowercase name for file paths and CLI (e.g., "fragments") */
10
+ nameLower: "fragments",
11
+
12
+ /** File extension for fragment definition files (e.g., ".fragment.tsx") */
13
+ fileExtension: ".fragment.tsx",
14
+
15
+ /** Legacy file extension for segments (still supported for migration) */
16
+ legacyFileExtension: ".segment.tsx",
17
+
18
+ /** JSON file extension for compiled output */
19
+ jsonExtension: ".fragment.json",
20
+
21
+ /** Default output file name (e.g., "fragments.json") */
22
+ outFile: "fragments.json",
23
+
24
+ /** Config file name (e.g., "fragments.config.ts") */
25
+ configFile: "fragments.config.ts",
26
+
27
+ /** Legacy config file name (still supported for migration) */
28
+ legacyConfigFile: "segments.config.ts",
29
+
30
+ /** CLI command name (e.g., "fragments") */
31
+ cliCommand: "fragments",
32
+
33
+ /** Package scope (e.g., "@fragments") */
34
+ packageScope: "@fragments",
35
+
36
+ /** Directory for storing fragments, registry, and cache */
37
+ dataDir: ".fragments",
38
+
39
+ /** Components subdirectory within .fragments/ */
40
+ componentsDir: "components",
41
+
42
+ /** Registry file name */
43
+ registryFile: "registry.json",
44
+
45
+ /** Context file name (AI-ready markdown) */
46
+ contextFile: "context.md",
47
+
48
+ /** Screenshots subdirectory */
49
+ screenshotsDir: "screenshots",
50
+
51
+ /** Cache subdirectory (gitignored) */
52
+ cacheDir: "cache",
53
+
54
+ /** Diff output subdirectory (gitignored) */
55
+ diffDir: "diff",
56
+
57
+ /** Manifest filename */
58
+ manifestFile: "manifest.json",
59
+
60
+ /** Prefix for localStorage keys (e.g., "fragments-") */
61
+ storagePrefix: "fragments-",
62
+
63
+ /** Static viewer HTML file name */
64
+ viewerHtmlFile: "fragments-viewer.html",
65
+
66
+ /** MCP tool name prefix (e.g., "fragments_") */
67
+ mcpToolPrefix: "fragments_",
68
+
69
+ /** File extension for block definition files */
70
+ blockFileExtension: ".block.ts",
71
+
72
+ /** @deprecated Use blockFileExtension instead */
73
+ recipeFileExtension: ".recipe.ts",
74
+
75
+ /** Vite plugin namespace */
76
+ vitePluginNamespace: "fragments-core-shim",
77
+ } as const;
78
+
79
+ export type Brand = typeof BRAND;
80
+
81
+ /**
82
+ * Default configuration values for the service.
83
+ * These can be overridden in fragments.config.ts
84
+ */
85
+ export const DEFAULTS = {
86
+ /** Default viewport dimensions */
87
+ viewport: {
88
+ width: 1280,
89
+ height: 800,
90
+ },
91
+
92
+ /** Default diff threshold (percentage) */
93
+ diffThreshold: 5,
94
+
95
+ /** Browser pool size */
96
+ poolSize: 3,
97
+
98
+ /** Idle timeout before browser shutdown (ms) - 5 minutes */
99
+ idleTimeoutMs: 5 * 60 * 1000,
100
+
101
+ /** Delay after render before capture (ms) */
102
+ captureDelayMs: 100,
103
+
104
+ /** Font loading timeout (ms) */
105
+ fontTimeoutMs: 3000,
106
+
107
+ /** Default theme */
108
+ theme: "light" as const,
109
+
110
+ /** Dev server port */
111
+ port: 6006,
112
+ } as const;
113
+
114
+ export type Defaults = typeof DEFAULTS;
package/src/context.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { generateContext, filterPlaceholders, PLACEHOLDER_PATTERNS } from '@fragments-sdk/context/generate';
2
+ export type { ContextOptions, ContextResult } from '@fragments-sdk/context/generate';