@agiflowai/style-system 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +318 -0
- package/dist/cli.cjs +184 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +183 -0
- package/dist/index.cjs +20 -0
- package/dist/index.d.cts +1368 -0
- package/dist/index.d.ts +1368 -0
- package/dist/index.js +3 -0
- package/dist/stdio-BlNvX94v.cjs +3251 -0
- package/dist/stdio-CGaoEmM8.js +3099 -0
- package/package.json +75 -0
|
@@ -0,0 +1,3251 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let __modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
|
|
25
|
+
__modelcontextprotocol_sdk_server_index_js = __toESM(__modelcontextprotocol_sdk_server_index_js);
|
|
26
|
+
let __modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
|
|
27
|
+
__modelcontextprotocol_sdk_types_js = __toESM(__modelcontextprotocol_sdk_types_js);
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
node_path = __toESM(node_path);
|
|
30
|
+
let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
|
|
31
|
+
__agiflowai_aicode_utils = __toESM(__agiflowai_aicode_utils);
|
|
32
|
+
let node_fs = require("node:fs");
|
|
33
|
+
node_fs = __toESM(node_fs);
|
|
34
|
+
let js_yaml = require("js-yaml");
|
|
35
|
+
js_yaml = __toESM(js_yaml);
|
|
36
|
+
let postcss = require("postcss");
|
|
37
|
+
postcss = __toESM(postcss);
|
|
38
|
+
let glob = require("glob");
|
|
39
|
+
glob = __toESM(glob);
|
|
40
|
+
let node_crypto = require("node:crypto");
|
|
41
|
+
node_crypto = __toESM(node_crypto);
|
|
42
|
+
let __storybook_csf_tools = require("@storybook/csf-tools");
|
|
43
|
+
__storybook_csf_tools = __toESM(__storybook_csf_tools);
|
|
44
|
+
let __tailwindcss_vite = require("@tailwindcss/vite");
|
|
45
|
+
__tailwindcss_vite = __toESM(__tailwindcss_vite);
|
|
46
|
+
let vite_plugin_singlefile = require("vite-plugin-singlefile");
|
|
47
|
+
vite_plugin_singlefile = __toESM(vite_plugin_singlefile);
|
|
48
|
+
let node_os = require("node:os");
|
|
49
|
+
node_os = __toESM(node_os);
|
|
50
|
+
let playwright = require("playwright");
|
|
51
|
+
playwright = __toESM(playwright);
|
|
52
|
+
let sharp = require("sharp");
|
|
53
|
+
sharp = __toESM(sharp);
|
|
54
|
+
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
55
|
+
__modelcontextprotocol_sdk_server_stdio_js = __toESM(__modelcontextprotocol_sdk_server_stdio_js);
|
|
56
|
+
|
|
57
|
+
//#region src/config.ts
|
|
58
|
+
/**
|
|
59
|
+
* Default tags for identifying shared/design system components
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_SHARED_COMPONENT_TAGS = ["style-system"];
|
|
62
|
+
/**
|
|
63
|
+
* Default configuration for apps without style-system config
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_CONFIG = {
|
|
66
|
+
type: "tailwind",
|
|
67
|
+
themeProvider: "@agimonai/web-ui",
|
|
68
|
+
sharedComponentTags: DEFAULT_SHARED_COMPONENT_TAGS
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Validate style-system configuration from project.json.
|
|
72
|
+
* Ensures required fields are present and have correct types.
|
|
73
|
+
*
|
|
74
|
+
* @param config - The config object to validate
|
|
75
|
+
* @param projectName - Project name for error messages
|
|
76
|
+
* @returns Validated DesignSystemConfig
|
|
77
|
+
* @throws Error if validation fails
|
|
78
|
+
*/
|
|
79
|
+
function validateDesignSystemConfig(config, projectName) {
|
|
80
|
+
if (typeof config !== "object" || config === null) throw new Error(`[${projectName}] style-system config must be an object`);
|
|
81
|
+
const cfg = config;
|
|
82
|
+
if (!cfg.type || cfg.type !== "tailwind" && cfg.type !== "shadcn") throw new Error(`[${projectName}] style-system.type must be 'tailwind' or 'shadcn'`);
|
|
83
|
+
if (!cfg.themeProvider || typeof cfg.themeProvider !== "string") throw new Error(`[${projectName}] style-system.themeProvider is required and must be a string`);
|
|
84
|
+
if (cfg.tailwindConfig !== void 0 && typeof cfg.tailwindConfig !== "string") throw new Error(`[${projectName}] style-system.tailwindConfig must be a string`);
|
|
85
|
+
if (cfg.rootComponent !== void 0 && typeof cfg.rootComponent !== "string") throw new Error(`[${projectName}] style-system.rootComponent must be a string`);
|
|
86
|
+
if (cfg.cssFiles !== void 0) {
|
|
87
|
+
if (!Array.isArray(cfg.cssFiles) || !cfg.cssFiles.every((f) => typeof f === "string")) throw new Error(`[${projectName}] style-system.cssFiles must be an array of strings`);
|
|
88
|
+
}
|
|
89
|
+
if (cfg.componentLibrary !== void 0 && typeof cfg.componentLibrary !== "string") throw new Error(`[${projectName}] style-system.componentLibrary must be a string`);
|
|
90
|
+
if (cfg.themePath !== void 0 && typeof cfg.themePath !== "string") throw new Error(`[${projectName}] style-system.themePath must be a string`);
|
|
91
|
+
if (cfg.sharedComponentTags !== void 0) {
|
|
92
|
+
if (!Array.isArray(cfg.sharedComponentTags) || !cfg.sharedComponentTags.every((t) => typeof t === "string")) throw new Error(`[${projectName}] style-system.sharedComponentTags must be an array of strings`);
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read design system configuration from an app's project.json.
|
|
98
|
+
*
|
|
99
|
+
* @param appPath - Path to the app directory (relative or absolute)
|
|
100
|
+
* @returns Validated DesignSystemConfig
|
|
101
|
+
* @throws Error if appPath is invalid, project.json cannot be read, or config validation fails
|
|
102
|
+
*/
|
|
103
|
+
async function getAppDesignSystemConfig(appPath) {
|
|
104
|
+
if (!appPath || typeof appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
|
|
105
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
106
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(monorepoRoot, appPath);
|
|
107
|
+
const projectJsonPath = node_path.default.join(resolvedAppPath, "project.json");
|
|
108
|
+
try {
|
|
109
|
+
const content = await node_fs.promises.readFile(projectJsonPath, "utf-8");
|
|
110
|
+
let projectJson;
|
|
111
|
+
try {
|
|
112
|
+
projectJson = JSON.parse(content);
|
|
113
|
+
} catch (parseError) {
|
|
114
|
+
throw new Error(`Invalid JSON in ${projectJsonPath}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof projectJson !== "object" || projectJson === null) throw new Error(`${projectJsonPath} must contain a JSON object`);
|
|
117
|
+
const project = projectJson;
|
|
118
|
+
const projectName = typeof project.name === "string" ? project.name : node_path.default.basename(resolvedAppPath);
|
|
119
|
+
if (project["style-system"]) {
|
|
120
|
+
const validatedConfig = validateDesignSystemConfig(project["style-system"], projectName);
|
|
121
|
+
__agiflowai_aicode_utils.log.info(`[Config] Loaded and validated style-system config for ${projectName}`);
|
|
122
|
+
return validatedConfig;
|
|
123
|
+
}
|
|
124
|
+
__agiflowai_aicode_utils.log.info(`[Config] No style-system config found for ${projectName}, using defaults`);
|
|
125
|
+
return DEFAULT_CONFIG;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Failed to read style-system config from ${projectJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get shared component tags from toolkit.yaml or use defaults.
|
|
132
|
+
*
|
|
133
|
+
* Reads configuration from toolkit.yaml at workspace root.
|
|
134
|
+
* Falls back to DEFAULT_SHARED_COMPONENT_TAGS if not configured.
|
|
135
|
+
*
|
|
136
|
+
* @returns Array of tag names that identify shared components
|
|
137
|
+
*/
|
|
138
|
+
async function getSharedComponentTags() {
|
|
139
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
140
|
+
const toolkitYamlPath = node_path.default.join(monorepoRoot, "toolkit.yaml");
|
|
141
|
+
try {
|
|
142
|
+
const content = await node_fs.promises.readFile(toolkitYamlPath, "utf-8");
|
|
143
|
+
const config = js_yaml.default.load(content);
|
|
144
|
+
if (config?.["style-system"]?.sharedComponentTags?.length) {
|
|
145
|
+
const tags = config["style-system"].sharedComponentTags;
|
|
146
|
+
if (Array.isArray(tags) && tags.every((tag) => typeof tag === "string")) {
|
|
147
|
+
__agiflowai_aicode_utils.log.info(`[Config] Loaded sharedComponentTags from toolkit.yaml: ${tags.join(", ")}`);
|
|
148
|
+
return tags;
|
|
149
|
+
}
|
|
150
|
+
__agiflowai_aicode_utils.log.warn("[Config] sharedComponentTags in toolkit.yaml is not a valid string array, using defaults");
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT") __agiflowai_aicode_utils.log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
__agiflowai_aicode_utils.log.info(`[Config] Using default sharedComponentTags: ${DEFAULT_SHARED_COMPONENT_TAGS.join(", ")}`);
|
|
156
|
+
return DEFAULT_SHARED_COMPONENT_TAGS;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get getCssClasses tool configuration from toolkit.yaml.
|
|
160
|
+
*
|
|
161
|
+
* Reads configuration from toolkit.yaml at workspace root under
|
|
162
|
+
* style-system.getCssClasses key.
|
|
163
|
+
*
|
|
164
|
+
* @returns GetCssClassesConfig or undefined if not configured
|
|
165
|
+
*/
|
|
166
|
+
async function getGetCssClassesConfig() {
|
|
167
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
168
|
+
const toolkitYamlPath = node_path.default.join(monorepoRoot, "toolkit.yaml");
|
|
169
|
+
try {
|
|
170
|
+
const content = await node_fs.promises.readFile(toolkitYamlPath, "utf-8");
|
|
171
|
+
const config = js_yaml.default.load(content);
|
|
172
|
+
if (config?.["style-system"]?.getCssClasses) {
|
|
173
|
+
const getCssClassesConfig = config["style-system"].getCssClasses;
|
|
174
|
+
if (getCssClassesConfig.customService !== void 0 && typeof getCssClassesConfig.customService !== "string") {
|
|
175
|
+
__agiflowai_aicode_utils.log.warn("[Config] style-system.getCssClasses.customService must be a string, ignoring");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
__agiflowai_aicode_utils.log.info(`[Config] Loaded getCssClasses config from toolkit.yaml`);
|
|
179
|
+
return getCssClassesConfig;
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT") __agiflowai_aicode_utils.log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get bundler service configuration from toolkit.yaml.
|
|
187
|
+
*
|
|
188
|
+
* Reads configuration from toolkit.yaml at workspace root under
|
|
189
|
+
* style-system.bundler key.
|
|
190
|
+
*
|
|
191
|
+
* @returns BundlerConfig or undefined if not configured
|
|
192
|
+
*/
|
|
193
|
+
async function getBundlerConfig() {
|
|
194
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
195
|
+
const toolkitYamlPath = node_path.default.join(monorepoRoot, "toolkit.yaml");
|
|
196
|
+
try {
|
|
197
|
+
const content = await node_fs.promises.readFile(toolkitYamlPath, "utf-8");
|
|
198
|
+
const config = js_yaml.default.load(content);
|
|
199
|
+
if (config?.["style-system"]?.bundler) {
|
|
200
|
+
const bundlerConfig = config["style-system"].bundler;
|
|
201
|
+
if (bundlerConfig.customService !== void 0 && typeof bundlerConfig.customService !== "string") {
|
|
202
|
+
__agiflowai_aicode_utils.log.warn("[Config] style-system.bundler.customService must be a string, ignoring");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
__agiflowai_aicode_utils.log.info(`[Config] Loaded bundler config from toolkit.yaml`);
|
|
206
|
+
return bundlerConfig;
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT") __agiflowai_aicode_utils.log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/services/CssClasses/BaseCSSClassesService.ts
|
|
215
|
+
/**
|
|
216
|
+
* Abstract base class for CSS class extraction services.
|
|
217
|
+
*
|
|
218
|
+
* Subclasses must implement the `extractClasses` method to provide
|
|
219
|
+
* framework-specific CSS class extraction logic.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* class MyCustomCSSService extends BaseCSSClassesService {
|
|
224
|
+
* async extractClasses(category: CSSClassCategory, themePath: string): Promise<CSSClassesResult> {
|
|
225
|
+
* // Custom extraction logic
|
|
226
|
+
* }
|
|
227
|
+
* }
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
var BaseCSSClassesService = class {
|
|
231
|
+
config;
|
|
232
|
+
/**
|
|
233
|
+
* Creates a new CSS classes service instance
|
|
234
|
+
* @param config - Style system configuration from toolkit.yaml
|
|
235
|
+
*/
|
|
236
|
+
constructor(config) {
|
|
237
|
+
this.config = config;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Validate that the theme path exists and is readable.
|
|
241
|
+
* Can be overridden by subclasses for custom validation.
|
|
242
|
+
*
|
|
243
|
+
* @param themePath - Path to validate
|
|
244
|
+
* @throws Error if path is invalid or unreadable
|
|
245
|
+
*/
|
|
246
|
+
async validateThemePath(themePath) {
|
|
247
|
+
try {
|
|
248
|
+
await node_fs.promises.access(themePath);
|
|
249
|
+
} catch {
|
|
250
|
+
throw new Error(`Theme file not found or not readable: ${themePath}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/services/CssClasses/TailwindCSSClassesService.ts
|
|
257
|
+
/**
|
|
258
|
+
* Tailwind CSS class extraction service.
|
|
259
|
+
*
|
|
260
|
+
* Extracts CSS classes from Tailwind theme files by parsing CSS variables
|
|
261
|
+
* using postcss AST and generating corresponding utility class names.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* const service = new TailwindCSSClassesService(config);
|
|
266
|
+
* const result = await service.extractClasses('colors', '/path/to/theme.css');
|
|
267
|
+
* console.log(result.classes.colors); // Array of color utility classes
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
var TailwindCSSClassesService = class extends BaseCSSClassesService {
|
|
271
|
+
/**
|
|
272
|
+
* Creates a new TailwindCSSClassesService instance
|
|
273
|
+
* @param config - Style system configuration from toolkit.yaml
|
|
274
|
+
*/
|
|
275
|
+
constructor(config) {
|
|
276
|
+
super(config);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the CSS framework identifier
|
|
280
|
+
* @returns Framework identifier string 'tailwind'
|
|
281
|
+
*/
|
|
282
|
+
getFrameworkId() {
|
|
283
|
+
return "tailwind";
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract Tailwind CSS classes from a theme file.
|
|
287
|
+
*
|
|
288
|
+
* Uses postcss to parse the CSS AST and safely extract variable declarations,
|
|
289
|
+
* then generates corresponding Tailwind utility classes (e.g., bg-*, text-*, border-*).
|
|
290
|
+
*
|
|
291
|
+
* Note: If an unrecognized category is passed, the method returns a result with
|
|
292
|
+
* empty classes object. Use valid categories: 'colors', 'typography', 'spacing', 'effects', 'all'.
|
|
293
|
+
*
|
|
294
|
+
* @param category - Category filter ('colors', 'typography', 'spacing', 'effects', 'all')
|
|
295
|
+
* @param themePath - Absolute path to the theme CSS file
|
|
296
|
+
* @returns Promise resolving to extracted CSS classes organized by category
|
|
297
|
+
* @throws Error if theme file cannot be read or parsed
|
|
298
|
+
*/
|
|
299
|
+
async extractClasses(category, themePath) {
|
|
300
|
+
try {
|
|
301
|
+
await this.validateThemePath(themePath);
|
|
302
|
+
const resolvedThemePath = node_path.default.resolve(themePath);
|
|
303
|
+
const themeContent = await node_fs.promises.readFile(resolvedThemePath, "utf-8");
|
|
304
|
+
const variables = await this.extractVariablesWithPostCSS(themeContent);
|
|
305
|
+
return this.generateClassesFromVariables(variables, category);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw new Error(`Failed to extract classes from theme file ${themePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Extract CSS variables with their values from theme content using postcss AST.
|
|
312
|
+
*
|
|
313
|
+
* Walks the CSS AST to find all custom property declarations (--*),
|
|
314
|
+
* handling multi-line values, comments, and any CSS formatting.
|
|
315
|
+
*
|
|
316
|
+
* @param themeContent - Raw CSS content from theme file
|
|
317
|
+
* @returns Promise resolving to Map of variable names (without --) to their values
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```typescript
|
|
321
|
+
* // Handles standard declarations
|
|
322
|
+
* // --color-primary: #3b82f6;
|
|
323
|
+
*
|
|
324
|
+
* // Handles multi-line declarations
|
|
325
|
+
* // --shadow-lg:
|
|
326
|
+
* // 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
327
|
+
* // 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
328
|
+
*
|
|
329
|
+
* // Handles compressed CSS
|
|
330
|
+
* // --color-primary:#3b82f6;--color-secondary:#10b981;
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
async extractVariablesWithPostCSS(themeContent) {
|
|
334
|
+
const variables = /* @__PURE__ */ new Map();
|
|
335
|
+
try {
|
|
336
|
+
postcss.default.parse(themeContent).walkDecls((decl) => {
|
|
337
|
+
if (decl.prop.startsWith("--")) {
|
|
338
|
+
const varName = decl.prop.slice(2);
|
|
339
|
+
const varValue = decl.value.trim();
|
|
340
|
+
if (!variables.has(varName)) variables.set(varName, varValue);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new Error(`Failed to parse CSS content: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
|
+
}
|
|
346
|
+
return variables;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Generate utility classes with actual values from CSS variables.
|
|
350
|
+
*
|
|
351
|
+
* Maps CSS variable naming conventions to Tailwind utility classes:
|
|
352
|
+
* - color-* → bg-*, text-*, border-*, ring-*
|
|
353
|
+
* - sidebar* → bg-*, text-*, border-*
|
|
354
|
+
* - text-* → text-* (typography)
|
|
355
|
+
* - font-* → font-* (typography)
|
|
356
|
+
* - space-* → p-*, m-*, gap-*
|
|
357
|
+
* - shadow-* → shadow-*
|
|
358
|
+
*
|
|
359
|
+
* Note: If the variables Map is empty, returns a result with empty arrays
|
|
360
|
+
* for all requested categories.
|
|
361
|
+
*
|
|
362
|
+
* @param variables - Map of CSS variable names to values
|
|
363
|
+
* @param category - Category filter for which classes to generate
|
|
364
|
+
* @returns CSSClassesResult with organized classes by category
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const variables = new Map([
|
|
369
|
+
* ['color-primary', '#3b82f6'],
|
|
370
|
+
* ['shadow-md', '0 4px 6px rgba(0,0,0,0.1)']
|
|
371
|
+
* ]);
|
|
372
|
+
* const result = generateClassesFromVariables(variables, 'colors');
|
|
373
|
+
* // Returns:
|
|
374
|
+
* // {
|
|
375
|
+
* // category: 'colors',
|
|
376
|
+
* // classes: {
|
|
377
|
+
* // colors: [
|
|
378
|
+
* // { class: 'bg-primary', value: '#3b82f6' },
|
|
379
|
+
* // { class: 'text-primary', value: '#3b82f6' },
|
|
380
|
+
* // { class: 'border-primary', value: '#3b82f6' },
|
|
381
|
+
* // { class: 'ring-primary', value: '#3b82f6' }
|
|
382
|
+
* // ]
|
|
383
|
+
* // },
|
|
384
|
+
* // totalClasses: 4
|
|
385
|
+
* // }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
generateClassesFromVariables(variables, category) {
|
|
389
|
+
const colorClasses = [];
|
|
390
|
+
const typographyClasses = [];
|
|
391
|
+
const spacingClasses = [];
|
|
392
|
+
const effectsClasses = [];
|
|
393
|
+
for (const [varName, varValue] of variables.entries()) {
|
|
394
|
+
if (category === "all" || category === "colors") {
|
|
395
|
+
if (varName.startsWith("color-")) {
|
|
396
|
+
const colorName = varName.replace("color-", "");
|
|
397
|
+
colorClasses.push({
|
|
398
|
+
class: `bg-${colorName}`,
|
|
399
|
+
value: varValue
|
|
400
|
+
}, {
|
|
401
|
+
class: `text-${colorName}`,
|
|
402
|
+
value: varValue
|
|
403
|
+
}, {
|
|
404
|
+
class: `border-${colorName}`,
|
|
405
|
+
value: varValue
|
|
406
|
+
}, {
|
|
407
|
+
class: `ring-${colorName}`,
|
|
408
|
+
value: varValue
|
|
409
|
+
});
|
|
410
|
+
} else if (varName.startsWith("sidebar")) colorClasses.push({
|
|
411
|
+
class: `bg-${varName}`,
|
|
412
|
+
value: varValue
|
|
413
|
+
}, {
|
|
414
|
+
class: `text-${varName}`,
|
|
415
|
+
value: varValue
|
|
416
|
+
}, {
|
|
417
|
+
class: `border-${varName}`,
|
|
418
|
+
value: varValue
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (category === "all" || category === "typography") {
|
|
422
|
+
if (varName.startsWith("text-")) typographyClasses.push({
|
|
423
|
+
class: varName,
|
|
424
|
+
value: varValue
|
|
425
|
+
});
|
|
426
|
+
if (varName.startsWith("font-")) {
|
|
427
|
+
const fontName = varName.replace("font-", "");
|
|
428
|
+
if (fontName.startsWith("weight-")) typographyClasses.push({
|
|
429
|
+
class: `font-${fontName.replace("weight-", "")}`,
|
|
430
|
+
value: varValue
|
|
431
|
+
});
|
|
432
|
+
else typographyClasses.push({
|
|
433
|
+
class: `font-${fontName}`,
|
|
434
|
+
value: varValue
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (category === "all" || category === "spacing") {
|
|
439
|
+
if (varName.startsWith("space-")) {
|
|
440
|
+
const spaceName = varName.replace("space-", "");
|
|
441
|
+
const calcValue = `calc(var(--${varName}) * 1)`;
|
|
442
|
+
spacingClasses.push({
|
|
443
|
+
class: `p-${spaceName}`,
|
|
444
|
+
value: calcValue
|
|
445
|
+
}, {
|
|
446
|
+
class: `m-${spaceName}`,
|
|
447
|
+
value: calcValue
|
|
448
|
+
}, {
|
|
449
|
+
class: `gap-${spaceName}`,
|
|
450
|
+
value: calcValue
|
|
451
|
+
});
|
|
452
|
+
} else if (varName === "spacing") spacingClasses.push({
|
|
453
|
+
class: "space",
|
|
454
|
+
value: varValue
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (category === "all" || category === "effects") {
|
|
458
|
+
if (varName.startsWith("shadow-")) {
|
|
459
|
+
const shadowName = varName.replace("shadow-", "");
|
|
460
|
+
effectsClasses.push({
|
|
461
|
+
class: `shadow-${shadowName}`,
|
|
462
|
+
value: varValue
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const result = {
|
|
468
|
+
category,
|
|
469
|
+
classes: {},
|
|
470
|
+
totalClasses: colorClasses.length + typographyClasses.length + spacingClasses.length + effectsClasses.length
|
|
471
|
+
};
|
|
472
|
+
if (category === "all" || category === "colors") result.classes.colors = colorClasses;
|
|
473
|
+
if (category === "all" || category === "typography") result.classes.typography = typographyClasses;
|
|
474
|
+
if (category === "all" || category === "spacing") result.classes.spacing = spacingClasses;
|
|
475
|
+
if (category === "all" || category === "effects") result.classes.effects = effectsClasses;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/services/CssClasses/types.ts
|
|
482
|
+
/**
|
|
483
|
+
* Default configuration values
|
|
484
|
+
*/
|
|
485
|
+
const DEFAULT_STYLE_SYSTEM_CONFIG = { cssFramework: "tailwind" };
|
|
486
|
+
|
|
487
|
+
//#endregion
|
|
488
|
+
//#region src/services/CssClasses/CSSClassesServiceFactory.ts
|
|
489
|
+
/** Valid file extensions for custom service modules */
|
|
490
|
+
const VALID_SERVICE_EXTENSIONS$1 = [
|
|
491
|
+
".ts",
|
|
492
|
+
".js",
|
|
493
|
+
".mjs",
|
|
494
|
+
".cjs"
|
|
495
|
+
];
|
|
496
|
+
/**
|
|
497
|
+
* Factory for creating CSS classes service instances.
|
|
498
|
+
*
|
|
499
|
+
* Supports built-in frameworks (tailwind) and custom service implementations
|
|
500
|
+
* loaded dynamically from user-specified paths.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```typescript
|
|
504
|
+
* const factory = new CSSClassesServiceFactory();
|
|
505
|
+
* const service = await factory.createService({ cssFramework: 'tailwind' });
|
|
506
|
+
* const classes = await service.extractClasses('colors', '/path/to/theme.css');
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
var CSSClassesServiceFactory = class {
|
|
510
|
+
/**
|
|
511
|
+
* Create a CSS classes service based on configuration.
|
|
512
|
+
*
|
|
513
|
+
* @param config - Style system configuration (defaults to tailwind)
|
|
514
|
+
* @returns Promise resolving to a CSS classes service instance
|
|
515
|
+
* @throws Error if framework is unknown or custom service cannot be loaded
|
|
516
|
+
*/
|
|
517
|
+
async createService(config = {}) {
|
|
518
|
+
const resolvedConfig = {
|
|
519
|
+
...DEFAULT_STYLE_SYSTEM_CONFIG,
|
|
520
|
+
...config
|
|
521
|
+
};
|
|
522
|
+
if (resolvedConfig.customServicePath) return this.loadCustomService(resolvedConfig);
|
|
523
|
+
return this.createBuiltInService(resolvedConfig);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Create a built-in CSS classes service based on framework identifier.
|
|
527
|
+
*
|
|
528
|
+
* @param config - Resolved style system configuration
|
|
529
|
+
* @returns CSS classes service instance
|
|
530
|
+
* @throws Error if framework is not supported
|
|
531
|
+
*/
|
|
532
|
+
createBuiltInService(config) {
|
|
533
|
+
switch (config.cssFramework) {
|
|
534
|
+
case "tailwind": return new TailwindCSSClassesService(config);
|
|
535
|
+
default: throw new Error(`Unsupported CSS framework: ${config.cssFramework}. Supported frameworks: tailwind. Use customServicePath to provide a custom implementation.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Load a custom CSS classes service from user-specified path.
|
|
540
|
+
*
|
|
541
|
+
* The custom service must export a class that extends BaseCSSClassesService.
|
|
542
|
+
*
|
|
543
|
+
* @param config - Configuration with customServicePath set
|
|
544
|
+
* @returns Promise resolving to custom service instance
|
|
545
|
+
* @throws Error if service cannot be loaded or is invalid
|
|
546
|
+
*/
|
|
547
|
+
async loadCustomService(config) {
|
|
548
|
+
const servicePath = config.customServicePath;
|
|
549
|
+
if (!servicePath) throw new Error("customServicePath is required for custom service loading");
|
|
550
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
551
|
+
const resolvedPath = node_path.default.resolve(monorepoRoot, servicePath);
|
|
552
|
+
const normalizedWorkspaceRoot = node_path.default.resolve(monorepoRoot);
|
|
553
|
+
if (!resolvedPath.startsWith(normalizedWorkspaceRoot + node_path.default.sep)) throw new Error(`Security error: customServicePath "${servicePath}" resolves outside workspace root`);
|
|
554
|
+
const ext = node_path.default.extname(resolvedPath).toLowerCase();
|
|
555
|
+
if (!VALID_SERVICE_EXTENSIONS$1.includes(ext)) throw new Error(`Invalid file extension "${ext}" for customServicePath. Expected one of: ${VALID_SERVICE_EXTENSIONS$1.join(", ")}`);
|
|
556
|
+
__agiflowai_aicode_utils.log.info(`[CSSClassesServiceFactory] Loading custom CSS service from: ${resolvedPath}`);
|
|
557
|
+
try {
|
|
558
|
+
const customModule = await import(resolvedPath);
|
|
559
|
+
const ServiceClass = customModule.default || customModule.CSSClassesService || customModule.CustomCSSClassesService;
|
|
560
|
+
if (!ServiceClass) throw new Error(`Custom service module at ${resolvedPath} must export a default class, CSSClassesService, or CustomCSSClassesService that extends BaseCSSClassesService`);
|
|
561
|
+
const instance = new ServiceClass(config);
|
|
562
|
+
if (!(instance instanceof BaseCSSClassesService)) throw new Error(`Custom service at ${resolvedPath} must extend BaseCSSClassesService`);
|
|
563
|
+
__agiflowai_aicode_utils.log.info(`[CSSClassesServiceFactory] Custom CSS service loaded successfully`);
|
|
564
|
+
return instance;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (error instanceof Error && error.message.includes("BaseCSSClassesService")) throw error;
|
|
567
|
+
throw new Error(`Failed to load custom CSS classes service from ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/tools/GetCSSClassesTool.ts
|
|
574
|
+
/**
|
|
575
|
+
* Valid CSS class category values
|
|
576
|
+
*/
|
|
577
|
+
const VALID_CATEGORIES = [
|
|
578
|
+
"colors",
|
|
579
|
+
"typography",
|
|
580
|
+
"spacing",
|
|
581
|
+
"effects",
|
|
582
|
+
"all"
|
|
583
|
+
];
|
|
584
|
+
/**
|
|
585
|
+
* Type guard to validate category input
|
|
586
|
+
* @param value - Value to check
|
|
587
|
+
* @returns True if value is a valid CSSClassCategory
|
|
588
|
+
*/
|
|
589
|
+
function isValidCategory(value) {
|
|
590
|
+
return VALID_CATEGORIES.includes(value);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* MCP Tool for extracting CSS classes from theme files.
|
|
594
|
+
*
|
|
595
|
+
* Uses the CSSClassesServiceFactory to create the appropriate service
|
|
596
|
+
* based on configuration, supporting Tailwind and custom CSS frameworks.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```typescript
|
|
600
|
+
* const tool = new GetCSSClassesTool();
|
|
601
|
+
* const result = await tool.execute({ category: 'colors' });
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
604
|
+
var GetCSSClassesTool = class GetCSSClassesTool {
|
|
605
|
+
static TOOL_NAME = "get_css_classes";
|
|
606
|
+
static CSS_REUSE_INSTRUCTION = "IMPORTANT: Always reuse these existing CSS classes from the theme as much as possible instead of creating custom styles. This ensures design consistency and reduces CSS bloat.";
|
|
607
|
+
serviceFactory;
|
|
608
|
+
service = null;
|
|
609
|
+
defaultThemePath;
|
|
610
|
+
/**
|
|
611
|
+
* Creates a new GetCSSClassesTool instance
|
|
612
|
+
* @param defaultThemePath - Default path to theme CSS file (relative to workspace root)
|
|
613
|
+
*/
|
|
614
|
+
constructor(defaultThemePath = "packages/frontend/web-theme/src/agimon-theme.css") {
|
|
615
|
+
this.serviceFactory = new CSSClassesServiceFactory();
|
|
616
|
+
this.defaultThemePath = defaultThemePath;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Returns the tool definition for MCP registration
|
|
620
|
+
* @returns Tool definition with name, description, and input schema
|
|
621
|
+
*/
|
|
622
|
+
getDefinition() {
|
|
623
|
+
return {
|
|
624
|
+
name: GetCSSClassesTool.TOOL_NAME,
|
|
625
|
+
description: "Extract and return all supported CSS classes from the theme file. Call this tool BEFORE writing any component styles or class names to ensure you use existing theme classes.",
|
|
626
|
+
inputSchema: {
|
|
627
|
+
type: "object",
|
|
628
|
+
properties: {
|
|
629
|
+
category: {
|
|
630
|
+
type: "string",
|
|
631
|
+
enum: [
|
|
632
|
+
"colors",
|
|
633
|
+
"typography",
|
|
634
|
+
"spacing",
|
|
635
|
+
"effects",
|
|
636
|
+
"all"
|
|
637
|
+
],
|
|
638
|
+
description: "Category filter: 'colors', 'typography', 'spacing', 'effects', 'all' (default)"
|
|
639
|
+
},
|
|
640
|
+
appPath: {
|
|
641
|
+
type: "string",
|
|
642
|
+
description: "Optional app path (relative or absolute) to read theme path from project.json style-system config (e.g., \"apps/agiflow-app\")"
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
additionalProperties: false
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Executes the CSS class extraction
|
|
651
|
+
* @param input - Tool input parameters
|
|
652
|
+
* @returns CallToolResult with extracted CSS classes or error
|
|
653
|
+
*/
|
|
654
|
+
async execute(input) {
|
|
655
|
+
try {
|
|
656
|
+
const category = input.category || "all";
|
|
657
|
+
if (!isValidCategory(category)) throw new Error(`Invalid category: '${category}'. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
|
|
658
|
+
const themePath = await this.resolveThemePath(input.appPath);
|
|
659
|
+
if (!this.service) {
|
|
660
|
+
const toolkitConfig = await getGetCssClassesConfig();
|
|
661
|
+
this.service = await this.serviceFactory.createService({ customServicePath: toolkitConfig?.customService });
|
|
662
|
+
}
|
|
663
|
+
const result = await this.service.extractClasses(category, themePath);
|
|
664
|
+
return { content: [{
|
|
665
|
+
type: "text",
|
|
666
|
+
text: `${GetCSSClassesTool.CSS_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
|
|
667
|
+
}] };
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return {
|
|
670
|
+
content: [{
|
|
671
|
+
type: "text",
|
|
672
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
673
|
+
}],
|
|
674
|
+
isError: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Resolves the theme file path based on app configuration or defaults.
|
|
680
|
+
*
|
|
681
|
+
* Resolution strategy:
|
|
682
|
+
* 1. If appPath provided, read themePath from project.json style-system config
|
|
683
|
+
* 2. themePath is resolved relative to the app directory (where project.json is)
|
|
684
|
+
* 3. Fall back to default theme path if not configured
|
|
685
|
+
*
|
|
686
|
+
* @param appPath - Optional app path to read config from
|
|
687
|
+
* @returns Absolute path to the theme file
|
|
688
|
+
*/
|
|
689
|
+
async resolveThemePath(appPath) {
|
|
690
|
+
const workspaceRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
691
|
+
if (appPath) {
|
|
692
|
+
const config = await getAppDesignSystemConfig(appPath);
|
|
693
|
+
if (config.themePath) {
|
|
694
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(workspaceRoot, appPath);
|
|
695
|
+
return node_path.default.resolve(resolvedAppPath, config.themePath);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return node_path.default.resolve(workspaceRoot, this.defaultThemePath);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/services/StoriesIndexService/StoriesIndexService.ts
|
|
704
|
+
var StoriesIndexService = class {
|
|
705
|
+
componentIndex = /* @__PURE__ */ new Map();
|
|
706
|
+
monorepoRoot;
|
|
707
|
+
initialized = false;
|
|
708
|
+
/** Last initialization result for error reporting */
|
|
709
|
+
lastInitResult = null;
|
|
710
|
+
/**
|
|
711
|
+
* Creates a new StoriesIndexService instance
|
|
712
|
+
*/
|
|
713
|
+
constructor() {
|
|
714
|
+
this.monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Initialize the index by scanning all .stories files.
|
|
718
|
+
* @returns Initialization result with success/failure statistics
|
|
719
|
+
*/
|
|
720
|
+
async initialize() {
|
|
721
|
+
if (this.initialized && this.lastInitResult) return this.lastInitResult;
|
|
722
|
+
__agiflowai_aicode_utils.log.info("[StoriesIndexService] Initializing story index...");
|
|
723
|
+
const storyFiles = await (0, glob.glob)("**/*.stories.{ts,tsx}", {
|
|
724
|
+
cwd: this.monorepoRoot,
|
|
725
|
+
ignore: [
|
|
726
|
+
"**/node_modules/**",
|
|
727
|
+
"**/dist/**",
|
|
728
|
+
"**/.next/**",
|
|
729
|
+
"**/build/**"
|
|
730
|
+
],
|
|
731
|
+
absolute: true
|
|
732
|
+
});
|
|
733
|
+
__agiflowai_aicode_utils.log.info(`[StoriesIndexService] Found ${storyFiles.length} story files`);
|
|
734
|
+
const failures = [];
|
|
735
|
+
let successCount = 0;
|
|
736
|
+
for (const filePath of storyFiles) try {
|
|
737
|
+
await this.indexStoryFile(filePath);
|
|
738
|
+
successCount++;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
741
|
+
__agiflowai_aicode_utils.log.error(`[StoriesIndexService] Error indexing ${filePath}: ${errorMessage}`);
|
|
742
|
+
failures.push({
|
|
743
|
+
filePath,
|
|
744
|
+
error: errorMessage
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
this.initialized = true;
|
|
748
|
+
this.lastInitResult = {
|
|
749
|
+
totalFiles: storyFiles.length,
|
|
750
|
+
successCount,
|
|
751
|
+
failureCount: failures.length,
|
|
752
|
+
failures
|
|
753
|
+
};
|
|
754
|
+
if (failures.length > 0) __agiflowai_aicode_utils.log.warn(`[StoriesIndexService] Indexed ${successCount}/${storyFiles.length} files successfully. ${failures.length} files failed.`);
|
|
755
|
+
else __agiflowai_aicode_utils.log.info(`[StoriesIndexService] Indexed ${this.componentIndex.size} components successfully`);
|
|
756
|
+
return this.lastInitResult;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Get the last initialization result.
|
|
760
|
+
* @returns Initialization result or null if not initialized
|
|
761
|
+
*/
|
|
762
|
+
getLastInitResult() {
|
|
763
|
+
return this.lastInitResult;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Index a single story file using @storybook/csf-tools.
|
|
767
|
+
*
|
|
768
|
+
* Uses the official Storybook CSF parser which handles all CSF formats
|
|
769
|
+
* including TypeScript satisfies/as expressions.
|
|
770
|
+
*/
|
|
771
|
+
async indexStoryFile(filePath) {
|
|
772
|
+
const content = await node_fs.promises.readFile(filePath, "utf-8");
|
|
773
|
+
const fileHash = this.hashContent(content);
|
|
774
|
+
const csf = (0, __storybook_csf_tools.loadCsf)(content, {
|
|
775
|
+
fileName: filePath,
|
|
776
|
+
makeTitle: (title) => title
|
|
777
|
+
});
|
|
778
|
+
await csf.parse();
|
|
779
|
+
if (!csf.meta?.title) {
|
|
780
|
+
__agiflowai_aicode_utils.log.warn(`[StoriesIndexService] No valid meta title in ${filePath}`);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const stories = csf.stories.map((story) => story.name).filter((name) => !!name);
|
|
784
|
+
const tags = Array.isArray(csf.meta.tags) ? csf.meta.tags.filter((t) => typeof t === "string") : [];
|
|
785
|
+
const description = this.extractDescription(content, csf.meta);
|
|
786
|
+
const meta = {
|
|
787
|
+
title: csf.meta.title,
|
|
788
|
+
tags
|
|
789
|
+
};
|
|
790
|
+
const componentInfo = {
|
|
791
|
+
title: meta.title,
|
|
792
|
+
filePath,
|
|
793
|
+
fileHash,
|
|
794
|
+
tags,
|
|
795
|
+
stories,
|
|
796
|
+
meta,
|
|
797
|
+
description
|
|
798
|
+
};
|
|
799
|
+
this.componentIndex.set(meta.title, componentInfo);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Extract component description from file header JSDoc or meta.parameters.docs.description.
|
|
803
|
+
*
|
|
804
|
+
* Priority:
|
|
805
|
+
* 1. meta.parameters.docs.description.component (Storybook standard)
|
|
806
|
+
* 2. File header JSDoc comment (first block comment in file)
|
|
807
|
+
*
|
|
808
|
+
* @param content - Raw file content
|
|
809
|
+
* @param meta - Parsed meta object from csf-tools
|
|
810
|
+
* @returns Description string or undefined
|
|
811
|
+
*/
|
|
812
|
+
extractDescription(content, meta) {
|
|
813
|
+
const docsDescription = (((meta?.parameters)?.docs)?.description)?.component;
|
|
814
|
+
if (typeof docsDescription === "string" && docsDescription.trim()) return docsDescription.trim();
|
|
815
|
+
const jsDocMatch = content.match(/^\s*\/\*\*\s*([\s\S]*?)\s*\*\//);
|
|
816
|
+
if (jsDocMatch?.[1]) {
|
|
817
|
+
const description = jsDocMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => !line.startsWith("@")).join(" ").replace(/\s+/g, " ").trim();
|
|
818
|
+
if (description) return description;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Hash file content for cache invalidation
|
|
823
|
+
*/
|
|
824
|
+
hashContent(content) {
|
|
825
|
+
return (0, node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get all components filtered by tags
|
|
829
|
+
* @param tags - Optional array of tags to filter by
|
|
830
|
+
* @returns Array of matching components
|
|
831
|
+
*/
|
|
832
|
+
getComponentsByTags(tags) {
|
|
833
|
+
const components = Array.from(this.componentIndex.values());
|
|
834
|
+
if (!tags || tags.length === 0) return components;
|
|
835
|
+
return components.filter((component) => tags.some((tag) => component.tags.includes(tag)));
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Get component by title
|
|
839
|
+
* @param title - Exact title to match (e.g., "Components/Button")
|
|
840
|
+
* @returns Component info or undefined
|
|
841
|
+
*/
|
|
842
|
+
getComponentByTitle(title) {
|
|
843
|
+
return this.componentIndex.get(title);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Find component by partial name match
|
|
847
|
+
* @param name - Partial name to search for
|
|
848
|
+
* @returns First matching component or undefined
|
|
849
|
+
*/
|
|
850
|
+
findComponentByName(name) {
|
|
851
|
+
const lowerName = name.toLowerCase();
|
|
852
|
+
for (const component of this.componentIndex.values()) if ((component.title.split("/").pop() || component.title).toLowerCase() === lowerName) return component;
|
|
853
|
+
for (const component of this.componentIndex.values()) if ((component.title.split("/").pop() || component.title).toLowerCase().includes(lowerName)) return component;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Refresh a specific file if it has changed
|
|
857
|
+
* @param filePath - Absolute path to story file
|
|
858
|
+
* @returns True if file was updated, false if unchanged
|
|
859
|
+
*/
|
|
860
|
+
async refreshFile(filePath) {
|
|
861
|
+
const content = await node_fs.promises.readFile(filePath, "utf-8");
|
|
862
|
+
const newHash = this.hashContent(content);
|
|
863
|
+
const existingComponent = Array.from(this.componentIndex.values()).find((c) => c.filePath === filePath);
|
|
864
|
+
if (existingComponent && existingComponent.fileHash === newHash) return false;
|
|
865
|
+
await this.indexStoryFile(filePath);
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get all indexed components
|
|
870
|
+
* @returns Array of all component info objects
|
|
871
|
+
*/
|
|
872
|
+
getAllComponents() {
|
|
873
|
+
return Array.from(this.componentIndex.values());
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Clear the index (useful for testing)
|
|
877
|
+
*/
|
|
878
|
+
clear() {
|
|
879
|
+
this.componentIndex.clear();
|
|
880
|
+
this.initialized = false;
|
|
881
|
+
this.lastInitResult = null;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get all unique tags from indexed components
|
|
885
|
+
* @returns Sorted array of unique tag names
|
|
886
|
+
*/
|
|
887
|
+
getAllTags() {
|
|
888
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
889
|
+
for (const component of this.componentIndex.values()) for (const tag of component.tags) tagSet.add(tag);
|
|
890
|
+
return Array.from(tagSet).sort();
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/services/AppComponentsService/types.ts
|
|
896
|
+
/**
|
|
897
|
+
* Default configuration values.
|
|
898
|
+
*/
|
|
899
|
+
const DEFAULT_APP_COMPONENTS_CONFIG = { pageSize: 50 };
|
|
900
|
+
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/services/AppComponentsService/AppComponentsService.ts
|
|
903
|
+
/**
|
|
904
|
+
* AppComponentsService handles listing app-specific and package components.
|
|
905
|
+
*
|
|
906
|
+
* Detects components by file path (within app directory) and resolves
|
|
907
|
+
* workspace dependencies to find package components.
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```typescript
|
|
911
|
+
* const service = new AppComponentsService();
|
|
912
|
+
* const result = await service.listComponents({ appPath: 'apps/my-app' });
|
|
913
|
+
* // Returns: { app: 'my-app', appComponents: ['Button'], packageComponents: {...}, pagination: {...} }
|
|
914
|
+
* ```
|
|
915
|
+
*/
|
|
916
|
+
var AppComponentsService = class {
|
|
917
|
+
config;
|
|
918
|
+
/**
|
|
919
|
+
* Creates a new AppComponentsService instance.
|
|
920
|
+
* @param config - Service configuration options
|
|
921
|
+
*/
|
|
922
|
+
constructor(config = {}) {
|
|
923
|
+
this.config = {
|
|
924
|
+
...DEFAULT_APP_COMPONENTS_CONFIG,
|
|
925
|
+
...config
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* List app-specific and package components for a given application.
|
|
930
|
+
* @param input - Object containing appPath and optional cursor for pagination
|
|
931
|
+
* @returns Promise resolving to paginated component list
|
|
932
|
+
* @throws Error if input validation fails, app path does not exist, or stories index fails to initialize
|
|
933
|
+
*/
|
|
934
|
+
async listComponents(input) {
|
|
935
|
+
if (!input.appPath || typeof input.appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
|
|
936
|
+
if (input.cursor !== void 0 && typeof input.cursor !== "string") throw new Error("cursor must be a string");
|
|
937
|
+
const { appPath, cursor } = input;
|
|
938
|
+
const { offset } = cursor ? this.decodeCursor(cursor) : { offset: 0 };
|
|
939
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
940
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(monorepoRoot, appPath);
|
|
941
|
+
try {
|
|
942
|
+
await node_fs.promises.access(resolvedAppPath);
|
|
943
|
+
} catch {
|
|
944
|
+
throw new Error(`App path does not exist: ${resolvedAppPath}`);
|
|
945
|
+
}
|
|
946
|
+
const appName = await this.getAppName(resolvedAppPath);
|
|
947
|
+
const workspaceDependencies = await this.getWorkspaceDependencies(resolvedAppPath);
|
|
948
|
+
__agiflowai_aicode_utils.log.info(`[AppComponentsService] Found ${workspaceDependencies.length} workspace dependencies for ${appName}`);
|
|
949
|
+
const packageMap = await this.buildPackageMap(monorepoRoot);
|
|
950
|
+
const storiesIndex = new StoriesIndexService();
|
|
951
|
+
await storiesIndex.initialize();
|
|
952
|
+
const allComponents = storiesIndex.getAllComponents();
|
|
953
|
+
const { appComponentsArray, packageComponents, totalPackageComponents } = this.categorizeComponents(allComponents, resolvedAppPath, workspaceDependencies, packageMap);
|
|
954
|
+
const totalComponents = appComponentsArray.length + totalPackageComponents;
|
|
955
|
+
const { paginatedAppComponents, paginatedPackageComponents, totalReturned } = this.paginateComponents(appComponentsArray, packageComponents, offset);
|
|
956
|
+
const hasMore = offset + totalReturned < totalComponents;
|
|
957
|
+
const result = {
|
|
958
|
+
app: appName,
|
|
959
|
+
appComponents: paginatedAppComponents,
|
|
960
|
+
packageComponents: paginatedPackageComponents,
|
|
961
|
+
pagination: {
|
|
962
|
+
offset,
|
|
963
|
+
pageSize: this.config.pageSize,
|
|
964
|
+
totalComponents,
|
|
965
|
+
hasMore
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
if (hasMore) result.nextCursor = this.encodeCursor(offset + totalReturned);
|
|
969
|
+
__agiflowai_aicode_utils.log.info(`[AppComponentsService] Page ${Math.floor(offset / this.config.pageSize) + 1}: Returned ${totalReturned} of ${totalComponents} total components (hasMore: ${hasMore})`);
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Get app name from project.json.
|
|
974
|
+
* @param resolvedAppPath - Absolute path to the app directory
|
|
975
|
+
* @returns App name from project.json or directory basename as fallback
|
|
976
|
+
*/
|
|
977
|
+
async getAppName(resolvedAppPath) {
|
|
978
|
+
const projectJsonPath = node_path.default.join(resolvedAppPath, "project.json");
|
|
979
|
+
let appName = node_path.default.basename(resolvedAppPath);
|
|
980
|
+
try {
|
|
981
|
+
appName = JSON.parse(await node_fs.promises.readFile(projectJsonPath, "utf-8")).name || appName;
|
|
982
|
+
} catch (error) {
|
|
983
|
+
__agiflowai_aicode_utils.log.warn(`[AppComponentsService] Could not read project.json for ${resolvedAppPath}:`, error);
|
|
984
|
+
}
|
|
985
|
+
return appName;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Get workspace dependencies from package.json.
|
|
989
|
+
* @param resolvedAppPath - Absolute path to the app directory
|
|
990
|
+
* @returns Array of workspace dependency package names
|
|
991
|
+
*/
|
|
992
|
+
async getWorkspaceDependencies(resolvedAppPath) {
|
|
993
|
+
const packageJsonPath = node_path.default.join(resolvedAppPath, "package.json");
|
|
994
|
+
try {
|
|
995
|
+
const packageJson = JSON.parse(await node_fs.promises.readFile(packageJsonPath, "utf-8"));
|
|
996
|
+
const allDeps = {
|
|
997
|
+
...packageJson.dependencies,
|
|
998
|
+
...packageJson.devDependencies
|
|
999
|
+
};
|
|
1000
|
+
return Object.entries(allDeps).filter(([_name, version]) => typeof version === "string" && version.startsWith("workspace:")).map(([name]) => name);
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
__agiflowai_aicode_utils.log.warn(`[AppComponentsService] Could not read package.json for ${resolvedAppPath}:`, error);
|
|
1003
|
+
return [];
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Find all package.json files in the monorepo and build a map of package name → directory path.
|
|
1008
|
+
* @param monorepoRoot - The root directory of the monorepo
|
|
1009
|
+
* @returns Promise resolving to a Map where keys are package names and values are directory paths
|
|
1010
|
+
* @throws Error if scanning for package.json files fails
|
|
1011
|
+
*/
|
|
1012
|
+
async buildPackageMap(monorepoRoot) {
|
|
1013
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
1014
|
+
let packageJsonFiles;
|
|
1015
|
+
try {
|
|
1016
|
+
packageJsonFiles = await (0, glob.glob)("**/package.json", {
|
|
1017
|
+
cwd: monorepoRoot,
|
|
1018
|
+
ignore: [
|
|
1019
|
+
"**/node_modules/**",
|
|
1020
|
+
"**/dist/**",
|
|
1021
|
+
"**/.next/**",
|
|
1022
|
+
"**/build/**"
|
|
1023
|
+
],
|
|
1024
|
+
absolute: true
|
|
1025
|
+
});
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
throw new Error(`Failed to scan for package.json files in ${monorepoRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1028
|
+
}
|
|
1029
|
+
for (const pkgJsonPath of packageJsonFiles) try {
|
|
1030
|
+
const content = await node_fs.promises.readFile(pkgJsonPath, "utf-8");
|
|
1031
|
+
const pkgJson = JSON.parse(content);
|
|
1032
|
+
if (pkgJson.name) {
|
|
1033
|
+
const pkgDir = node_path.default.dirname(pkgJsonPath);
|
|
1034
|
+
packageMap.set(pkgJson.name, pkgDir);
|
|
1035
|
+
}
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
__agiflowai_aicode_utils.log.debug(`[AppComponentsService] Skipping invalid package.json at ${pkgJsonPath}:`, error);
|
|
1038
|
+
}
|
|
1039
|
+
return packageMap;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Categorize components into app-specific and package components.
|
|
1043
|
+
* App components are detected by file path (within app directory).
|
|
1044
|
+
* Package components are matched to workspace dependencies.
|
|
1045
|
+
*
|
|
1046
|
+
* @param allComponents - All components from stories index
|
|
1047
|
+
* @param resolvedAppPath - Absolute path to the app directory
|
|
1048
|
+
* @param workspaceDependencies - List of workspace dependency package names
|
|
1049
|
+
* @param packageMap - Map of package name to directory path
|
|
1050
|
+
* @returns Categorized components with totals
|
|
1051
|
+
*/
|
|
1052
|
+
categorizeComponents(allComponents, resolvedAppPath, workspaceDependencies, packageMap) {
|
|
1053
|
+
const appComponentsMap = /* @__PURE__ */ new Map();
|
|
1054
|
+
const packageComponentsMap = {};
|
|
1055
|
+
for (const component of allComponents) {
|
|
1056
|
+
const componentName = component.title.split("/").pop() || component.title;
|
|
1057
|
+
const componentBrief = {
|
|
1058
|
+
name: componentName,
|
|
1059
|
+
...component.description && { description: component.description }
|
|
1060
|
+
};
|
|
1061
|
+
if (component.filePath.startsWith(resolvedAppPath)) appComponentsMap.set(componentName, componentBrief);
|
|
1062
|
+
for (const dep of workspaceDependencies) {
|
|
1063
|
+
const packageDir = packageMap.get(dep);
|
|
1064
|
+
if (packageDir && component.filePath.startsWith(packageDir)) {
|
|
1065
|
+
if (!packageComponentsMap[dep]) packageComponentsMap[dep] = /* @__PURE__ */ new Map();
|
|
1066
|
+
packageComponentsMap[dep].set(componentName, componentBrief);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const packageComponents = {};
|
|
1071
|
+
for (const [pkg, componentsMap] of Object.entries(packageComponentsMap)) packageComponents[pkg] = Array.from(componentsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
1072
|
+
return {
|
|
1073
|
+
appComponentsArray: Array.from(appComponentsMap.values()).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1074
|
+
packageComponents,
|
|
1075
|
+
totalPackageComponents: Object.values(packageComponents).reduce((sum, arr) => sum + arr.length, 0)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Apply pagination to component lists.
|
|
1080
|
+
*
|
|
1081
|
+
* Pagination strategy:
|
|
1082
|
+
* 1. First, fill page with app components starting from offset
|
|
1083
|
+
* 2. Then, fill remaining page space with package components in order
|
|
1084
|
+
* 3. Track total returned for cursor calculation
|
|
1085
|
+
*
|
|
1086
|
+
* @param appComponentsArray - Sorted array of app component briefs
|
|
1087
|
+
* @param packageComponents - Record of package name to component briefs
|
|
1088
|
+
* @param offset - Current pagination offset (0-indexed)
|
|
1089
|
+
* @returns Paginated components and total returned count
|
|
1090
|
+
*/
|
|
1091
|
+
paginateComponents(appComponentsArray, packageComponents, offset) {
|
|
1092
|
+
const totalPackageComponents = Object.values(packageComponents).reduce((sum, arr) => sum + arr.length, 0);
|
|
1093
|
+
const totalComponents = appComponentsArray.length + totalPackageComponents;
|
|
1094
|
+
const paginatedAppComponents = appComponentsArray.slice(offset, offset + this.config.pageSize);
|
|
1095
|
+
const remainingSpace = this.config.pageSize - paginatedAppComponents.length;
|
|
1096
|
+
const paginatedPackageComponents = {};
|
|
1097
|
+
let packageComponentsConsumed = 0;
|
|
1098
|
+
if (remainingSpace > 0 && offset < totalComponents) {
|
|
1099
|
+
const packageOffset = Math.max(0, offset - appComponentsArray.length);
|
|
1100
|
+
let itemsToTake = remainingSpace;
|
|
1101
|
+
let currentOffset = packageOffset;
|
|
1102
|
+
for (const [pkg, components] of Object.entries(packageComponents)) {
|
|
1103
|
+
if (itemsToTake <= 0) break;
|
|
1104
|
+
if (currentOffset < components.length) {
|
|
1105
|
+
const sliced = components.slice(currentOffset, currentOffset + itemsToTake);
|
|
1106
|
+
paginatedPackageComponents[pkg] = sliced;
|
|
1107
|
+
packageComponentsConsumed += sliced.length;
|
|
1108
|
+
itemsToTake -= sliced.length;
|
|
1109
|
+
currentOffset = 0;
|
|
1110
|
+
} else currentOffset -= components.length;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
paginatedAppComponents,
|
|
1115
|
+
paginatedPackageComponents,
|
|
1116
|
+
totalReturned: paginatedAppComponents.length + packageComponentsConsumed
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Encode pagination state into a base64 cursor string.
|
|
1121
|
+
* @param offset - The current offset position in the component list
|
|
1122
|
+
* @returns Base64-encoded cursor string for the next page
|
|
1123
|
+
*/
|
|
1124
|
+
encodeCursor(offset) {
|
|
1125
|
+
return Buffer.from(JSON.stringify({ offset })).toString("base64");
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Decode cursor string into pagination state.
|
|
1129
|
+
* @param cursor - Base64-encoded cursor string from previous response
|
|
1130
|
+
* @returns Object with offset position; defaults to 0 if cursor is invalid
|
|
1131
|
+
*/
|
|
1132
|
+
decodeCursor(cursor) {
|
|
1133
|
+
try {
|
|
1134
|
+
const decoded = Buffer.from(cursor, "base64").toString("utf-8");
|
|
1135
|
+
return { offset: JSON.parse(decoded).offset || 0 };
|
|
1136
|
+
} catch {
|
|
1137
|
+
__agiflowai_aicode_utils.log.debug("[AppComponentsService] Invalid cursor, resetting to offset 0");
|
|
1138
|
+
return { offset: 0 };
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/services/BundlerService/BaseBundlerService.ts
|
|
1145
|
+
/**
|
|
1146
|
+
* Abstract base class for bundler service implementations.
|
|
1147
|
+
*
|
|
1148
|
+
* Subclasses must implement the abstract methods to provide
|
|
1149
|
+
* bundler-specific (Vite, Webpack, etc.) and framework-specific
|
|
1150
|
+
* (React, Vue, etc.) component rendering logic.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```typescript
|
|
1154
|
+
* class MyCustomBundlerService extends BaseBundlerService {
|
|
1155
|
+
* async startDevServer(appPath: string): Promise<DevServerResult> {
|
|
1156
|
+
* // Custom dev server logic
|
|
1157
|
+
* }
|
|
1158
|
+
* // ... implement other abstract methods
|
|
1159
|
+
* }
|
|
1160
|
+
* ```
|
|
1161
|
+
*/
|
|
1162
|
+
var BaseBundlerService = class {
|
|
1163
|
+
config;
|
|
1164
|
+
/**
|
|
1165
|
+
* Creates a new bundler service instance.
|
|
1166
|
+
* @param config - Service configuration options
|
|
1167
|
+
*/
|
|
1168
|
+
constructor(config = {}) {
|
|
1169
|
+
this.config = config;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/services/BundlerService/ViteReactBundlerService.ts
|
|
1175
|
+
/**
|
|
1176
|
+
* Maximum age for story configs in milliseconds (5 minutes).
|
|
1177
|
+
* Configs older than this are cleaned up to prevent memory leaks.
|
|
1178
|
+
*/
|
|
1179
|
+
const STORY_CONFIG_MAX_AGE_MS = 300 * 1e3;
|
|
1180
|
+
/**
|
|
1181
|
+
* Maximum number of story configs to keep in memory.
|
|
1182
|
+
* Oldest configs are removed when this limit is exceeded.
|
|
1183
|
+
*/
|
|
1184
|
+
const STORY_CONFIG_MAX_COUNT = 100;
|
|
1185
|
+
/**
|
|
1186
|
+
* Maximum size for serialized args in bytes (1MB).
|
|
1187
|
+
* Prevents memory issues with extremely large payloads.
|
|
1188
|
+
*/
|
|
1189
|
+
const MAX_ARGS_SIZE_BYTES = 1024 * 1024;
|
|
1190
|
+
/**
|
|
1191
|
+
* Valid pattern for story names.
|
|
1192
|
+
* Allows alphanumeric characters, underscores, hyphens, and spaces.
|
|
1193
|
+
*/
|
|
1194
|
+
const VALID_STORY_NAME_PATTERN = /^[a-zA-Z0-9_\- ]+$/;
|
|
1195
|
+
/**
|
|
1196
|
+
* Valid pattern for component paths.
|
|
1197
|
+
* Must end with .stories.tsx, .stories.ts, .stories.jsx, or .stories.js
|
|
1198
|
+
*/
|
|
1199
|
+
const VALID_COMPONENT_PATH_PATTERN = /\.stories\.(tsx?|jsx?)$/;
|
|
1200
|
+
/**
|
|
1201
|
+
* Validates a story name to prevent code injection.
|
|
1202
|
+
* @param storyName - The story name to validate
|
|
1203
|
+
* @throws Error if the story name contains invalid characters
|
|
1204
|
+
*/
|
|
1205
|
+
function validateStoryName(storyName) {
|
|
1206
|
+
if (!storyName || typeof storyName !== "string") throw new Error("Story name is required and must be a string");
|
|
1207
|
+
if (!VALID_STORY_NAME_PATTERN.test(storyName)) throw new Error(`Story name "${storyName}" contains invalid characters. Only alphanumeric characters, underscores, hyphens, and spaces are allowed.`);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Validates a component path to prevent code injection.
|
|
1211
|
+
* @param componentPath - The component path to validate
|
|
1212
|
+
* @throws Error if the component path is invalid
|
|
1213
|
+
*/
|
|
1214
|
+
function validateComponentPath(componentPath) {
|
|
1215
|
+
if (!componentPath || typeof componentPath !== "string") throw new Error("Component path is required and must be a string");
|
|
1216
|
+
if (!VALID_COMPONENT_PATH_PATTERN.test(componentPath)) throw new Error(`Component path "${componentPath}" must be a valid Storybook story file (*.stories.tsx, *.stories.ts, *.stories.jsx, or *.stories.js)`);
|
|
1217
|
+
if (componentPath.includes("..")) throw new Error("Component path must not contain path traversal sequences (..)");
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Validates args object size to prevent memory issues.
|
|
1221
|
+
* @param args - The args object to validate
|
|
1222
|
+
* @throws Error if args exceed size limit
|
|
1223
|
+
*/
|
|
1224
|
+
function validateArgsSize(args) {
|
|
1225
|
+
const serialized = JSON.stringify(args);
|
|
1226
|
+
const sizeBytes = Buffer.byteLength(serialized, "utf-8");
|
|
1227
|
+
if (sizeBytes > MAX_ARGS_SIZE_BYTES) throw new Error(`Args payload too large: ${sizeBytes} bytes exceeds maximum of ${MAX_ARGS_SIZE_BYTES} bytes (1MB)`);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Validates CSS file paths to prevent path traversal attacks.
|
|
1231
|
+
* @param cssFiles - Array of CSS file paths to validate
|
|
1232
|
+
* @param workspaceRoot - The workspace root directory
|
|
1233
|
+
* @throws Error if any path is invalid or escapes workspace
|
|
1234
|
+
*/
|
|
1235
|
+
function validateCssFiles(cssFiles, workspaceRoot) {
|
|
1236
|
+
for (const cssFile of cssFiles) {
|
|
1237
|
+
if (typeof cssFile !== "string") throw new Error("CSS file path must be a string");
|
|
1238
|
+
if (cssFile.includes("..")) throw new Error(`CSS file path "${cssFile}" must not contain path traversal sequences (..)`);
|
|
1239
|
+
if (node_path.default.isAbsolute(cssFile)) {
|
|
1240
|
+
if (!node_path.default.normalize(cssFile).startsWith(workspaceRoot)) throw new Error(`CSS file path "${cssFile}" must be within workspace boundaries`);
|
|
1241
|
+
}
|
|
1242
|
+
const ext = node_path.default.extname(cssFile).toLowerCase();
|
|
1243
|
+
if (![
|
|
1244
|
+
".css",
|
|
1245
|
+
".scss",
|
|
1246
|
+
".sass",
|
|
1247
|
+
".less",
|
|
1248
|
+
".pcss",
|
|
1249
|
+
".postcss"
|
|
1250
|
+
].includes(ext)) throw new Error(`CSS file "${cssFile}" must have a valid CSS extension (.css, .scss, .sass, .less, .pcss)`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Helper to create a Vite plugin that serves story entry files from memory.
|
|
1255
|
+
*/
|
|
1256
|
+
function createStoryEntryPlugin(getStoryConfig, generateCode) {
|
|
1257
|
+
return {
|
|
1258
|
+
name: "vite-plugin-story-entry",
|
|
1259
|
+
enforce: "pre",
|
|
1260
|
+
resolveId(id) {
|
|
1261
|
+
if (id.includes("virtual:story-entry")) {
|
|
1262
|
+
__agiflowai_aicode_utils.log.debug(`[vite-plugin-story-entry] resolveId: ${id}`);
|
|
1263
|
+
return `\0${id.replace(/^\//, "")}`;
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
load(id) {
|
|
1267
|
+
if (id.includes("virtual:story-entry")) {
|
|
1268
|
+
__agiflowai_aicode_utils.log.debug(`[vite-plugin-story-entry] load: ${id}`);
|
|
1269
|
+
const storyId = id.match(/id=([^&]+)/)?.[1];
|
|
1270
|
+
if (storyId) {
|
|
1271
|
+
const config = getStoryConfig(storyId);
|
|
1272
|
+
if (config) return generateCode(config);
|
|
1273
|
+
}
|
|
1274
|
+
throw new Error(`[Vite] Story config not found for id: ${storyId}`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* ViteReactBundlerService provides Vite + React bundling for component rendering.
|
|
1281
|
+
*
|
|
1282
|
+
* This is the default implementation of BaseBundlerService that uses Vite
|
|
1283
|
+
* as the bundler and React as the framework for rendering components.
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* ```typescript
|
|
1287
|
+
* const service = ViteReactBundlerService.getInstance();
|
|
1288
|
+
* await service.startDevServer('apps/my-app');
|
|
1289
|
+
* const { url } = await service.serveComponent({
|
|
1290
|
+
* componentPath: '/path/to/Button.stories.tsx',
|
|
1291
|
+
* storyName: 'Primary',
|
|
1292
|
+
* appPath: 'apps/my-app'
|
|
1293
|
+
* });
|
|
1294
|
+
* ```
|
|
1295
|
+
*/
|
|
1296
|
+
var ViteReactBundlerService = class ViteReactBundlerService extends BaseBundlerService {
|
|
1297
|
+
static instance = null;
|
|
1298
|
+
server = null;
|
|
1299
|
+
monorepoRoot;
|
|
1300
|
+
serverUrl = null;
|
|
1301
|
+
serverPort = null;
|
|
1302
|
+
currentAppPath = null;
|
|
1303
|
+
storyConfigs = /* @__PURE__ */ new Map();
|
|
1304
|
+
/** Timestamps for when each story config was created, used for cleanup */
|
|
1305
|
+
storyConfigTimestamps = /* @__PURE__ */ new Map();
|
|
1306
|
+
/** Promise that resolves when server startup completes, prevents race conditions */
|
|
1307
|
+
serverStartPromise = null;
|
|
1308
|
+
/**
|
|
1309
|
+
* Creates a new ViteReactBundlerService instance.
|
|
1310
|
+
* Use getInstance() for singleton access.
|
|
1311
|
+
* @param config - Service configuration options
|
|
1312
|
+
*/
|
|
1313
|
+
constructor(config = {}) {
|
|
1314
|
+
super(config);
|
|
1315
|
+
this.monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Get the singleton instance of ViteReactBundlerService.
|
|
1319
|
+
* Singleton pattern ensures only one dev server runs at a time,
|
|
1320
|
+
* preventing port conflicts and resource duplication.
|
|
1321
|
+
* @returns The singleton ViteReactBundlerService instance
|
|
1322
|
+
*/
|
|
1323
|
+
static getInstance() {
|
|
1324
|
+
if (!ViteReactBundlerService.instance) ViteReactBundlerService.instance = new ViteReactBundlerService();
|
|
1325
|
+
return ViteReactBundlerService.instance;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Reset the singleton instance.
|
|
1329
|
+
* This is primarily used in testing to ensure a fresh instance.
|
|
1330
|
+
* @example
|
|
1331
|
+
* ```typescript
|
|
1332
|
+
* afterEach(() => {
|
|
1333
|
+
* ViteReactBundlerService.resetInstance();
|
|
1334
|
+
* });
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
static resetInstance() {
|
|
1338
|
+
ViteReactBundlerService.instance = null;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get the bundler identifier.
|
|
1342
|
+
* @returns The bundler ID string ('vite')
|
|
1343
|
+
*/
|
|
1344
|
+
getBundlerId() {
|
|
1345
|
+
return "vite";
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Get the framework identifier.
|
|
1349
|
+
* @returns The framework ID string ('react')
|
|
1350
|
+
*/
|
|
1351
|
+
getFrameworkId() {
|
|
1352
|
+
return "react";
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Get the current server URL.
|
|
1356
|
+
* @returns Server URL or null if not running
|
|
1357
|
+
*/
|
|
1358
|
+
getServerUrl() {
|
|
1359
|
+
return this.serverUrl;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Get the current server port.
|
|
1363
|
+
* @returns Server port or null if not running
|
|
1364
|
+
*/
|
|
1365
|
+
getServerPort() {
|
|
1366
|
+
return this.serverPort;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Check if the dev server is running.
|
|
1370
|
+
* @returns True if server is running
|
|
1371
|
+
*/
|
|
1372
|
+
isServerRunning() {
|
|
1373
|
+
return this.server !== null && this.serverUrl !== null;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Get the current app path being served.
|
|
1377
|
+
* @returns App path or null if not running
|
|
1378
|
+
*/
|
|
1379
|
+
getCurrentAppPath() {
|
|
1380
|
+
return this.currentAppPath;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Start a Vite dev server for hot reload and caching.
|
|
1384
|
+
* Handles concurrent calls by returning the same promise if server is already starting.
|
|
1385
|
+
* @param appPath - Absolute or relative path to the app directory
|
|
1386
|
+
* @returns Promise resolving to server URL and port
|
|
1387
|
+
* @throws Error if server fails to start
|
|
1388
|
+
*/
|
|
1389
|
+
async startDevServer(appPath) {
|
|
1390
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(this.monorepoRoot, appPath);
|
|
1391
|
+
if (this.isServerRunning() && this.currentAppPath === resolvedAppPath) {
|
|
1392
|
+
__agiflowai_aicode_utils.log.info(`[ViteReactBundlerService] Server already running for ${resolvedAppPath}`);
|
|
1393
|
+
return {
|
|
1394
|
+
url: this.serverUrl,
|
|
1395
|
+
port: this.serverPort
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
if (this.serverStartPromise) {
|
|
1399
|
+
__agiflowai_aicode_utils.log.info("[ViteReactBundlerService] Server start already in progress, waiting...");
|
|
1400
|
+
return this.serverStartPromise;
|
|
1401
|
+
}
|
|
1402
|
+
this.serverStartPromise = this.doStartDevServer(resolvedAppPath);
|
|
1403
|
+
try {
|
|
1404
|
+
return await this.serverStartPromise;
|
|
1405
|
+
} finally {
|
|
1406
|
+
this.serverStartPromise = null;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Internal method that performs the actual server startup.
|
|
1411
|
+
* @param resolvedAppPath - Resolved absolute path to the app directory
|
|
1412
|
+
* @returns Promise resolving to server URL and port
|
|
1413
|
+
*/
|
|
1414
|
+
async doStartDevServer(resolvedAppPath) {
|
|
1415
|
+
const tmpDir = node_path.default.join(resolvedAppPath, ".tmp");
|
|
1416
|
+
try {
|
|
1417
|
+
await node_fs.promises.mkdir(tmpDir, { recursive: true });
|
|
1418
|
+
const { createServer: createServer$1 } = await import("vite");
|
|
1419
|
+
this.server = await createServer$1({
|
|
1420
|
+
root: tmpDir,
|
|
1421
|
+
base: "/",
|
|
1422
|
+
configFile: false,
|
|
1423
|
+
plugins: [(0, __tailwindcss_vite.default)(), createStoryEntryPlugin((id) => this.storyConfigs.get(id), (opts) => this.generateEntryFile(opts))],
|
|
1424
|
+
resolve: { alias: { "@": node_path.default.join(resolvedAppPath, "src") } },
|
|
1425
|
+
esbuild: {
|
|
1426
|
+
jsx: "automatic",
|
|
1427
|
+
jsxImportSource: "react"
|
|
1428
|
+
},
|
|
1429
|
+
server: {
|
|
1430
|
+
strictPort: false,
|
|
1431
|
+
open: false
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
this.server.middlewares.use(async (req, res, next) => {
|
|
1435
|
+
const url$1 = req.url || "/";
|
|
1436
|
+
const match = url$1.match(/^\/preview\/([^/?]+)/);
|
|
1437
|
+
if (match) {
|
|
1438
|
+
const storyId = match[1];
|
|
1439
|
+
const config = this.storyConfigs.get(storyId);
|
|
1440
|
+
if (config) try {
|
|
1441
|
+
const htmlTemplate = this.generateHtmlTemplate(`@virtual:story-entry?id=${storyId}`, config.darkMode);
|
|
1442
|
+
const transformedHtml = await this.server.transformIndexHtml(url$1, htmlTemplate);
|
|
1443
|
+
res.statusCode = 200;
|
|
1444
|
+
res.setHeader("Content-Type", "text/html");
|
|
1445
|
+
res.end(transformedHtml);
|
|
1446
|
+
return;
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
const err = e;
|
|
1449
|
+
__agiflowai_aicode_utils.log.error(`[ViteMiddleware] Error serving preview: ${err.message}`);
|
|
1450
|
+
next(err);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
next();
|
|
1455
|
+
});
|
|
1456
|
+
await this.server.listen();
|
|
1457
|
+
const address = this.server.httpServer?.address();
|
|
1458
|
+
if (!address || typeof address === "string") throw new Error("Failed to start Vite dev server. Ensure no other process is using the port.");
|
|
1459
|
+
const port = address.port;
|
|
1460
|
+
const url = `http://localhost:${port}`;
|
|
1461
|
+
this.serverUrl = url;
|
|
1462
|
+
this.serverPort = port;
|
|
1463
|
+
this.currentAppPath = resolvedAppPath;
|
|
1464
|
+
__agiflowai_aicode_utils.log.info(`[ViteReactBundlerService] Vite dev server started at ${url}`);
|
|
1465
|
+
return {
|
|
1466
|
+
url,
|
|
1467
|
+
port
|
|
1468
|
+
};
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
const err = /* @__PURE__ */ new Error(`Failed to start dev server for ${resolvedAppPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1471
|
+
err.cause = error;
|
|
1472
|
+
throw err;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Serve a component dynamically through the dev server.
|
|
1477
|
+
* @param options - Component rendering options
|
|
1478
|
+
* @returns Promise resolving to the component URL and HTML file path
|
|
1479
|
+
* @throws Error if dev server is not running or file operations fail
|
|
1480
|
+
*/
|
|
1481
|
+
async serveComponent(options) {
|
|
1482
|
+
if (!this.isServerRunning()) throw new Error("Dev server is not running. Start it first using startDevServer().");
|
|
1483
|
+
const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent } = options;
|
|
1484
|
+
validateStoryName(storyName);
|
|
1485
|
+
validateComponentPath(componentPath);
|
|
1486
|
+
validateArgsSize(args);
|
|
1487
|
+
validateCssFiles(cssFiles, this.monorepoRoot);
|
|
1488
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(this.monorepoRoot, appPath);
|
|
1489
|
+
if (this.currentAppPath !== resolvedAppPath) throw new Error(`Dev server is running for ${this.currentAppPath} but requested ${resolvedAppPath}.`);
|
|
1490
|
+
const tmpDir = node_path.default.join(resolvedAppPath, ".tmp");
|
|
1491
|
+
try {
|
|
1492
|
+
this.cleanupStaleStoryConfigs();
|
|
1493
|
+
await node_fs.promises.mkdir(tmpDir, { recursive: true });
|
|
1494
|
+
const wrapperCssPath = node_path.default.join(tmpDir, "tailwind-wrapper.css");
|
|
1495
|
+
const wrapperCssContent = this.generateWrapperCss(resolvedAppPath, cssFiles);
|
|
1496
|
+
await node_fs.promises.writeFile(wrapperCssPath, wrapperCssContent, "utf-8");
|
|
1497
|
+
const timestamp = Date.now();
|
|
1498
|
+
const storyId = `${timestamp}-${Math.random().toString(36).slice(2)}`;
|
|
1499
|
+
this.storyConfigs.set(storyId, {
|
|
1500
|
+
componentPath,
|
|
1501
|
+
storyName,
|
|
1502
|
+
args,
|
|
1503
|
+
appPath: resolvedAppPath,
|
|
1504
|
+
darkMode,
|
|
1505
|
+
cssFiles,
|
|
1506
|
+
rootComponent,
|
|
1507
|
+
tmpDir
|
|
1508
|
+
});
|
|
1509
|
+
this.storyConfigTimestamps.set(storyId, timestamp);
|
|
1510
|
+
const url = `${this.serverUrl}/preview/${storyId}`;
|
|
1511
|
+
const htmlContent = this.generateHtmlTemplate(`@virtual:story-entry?id=${storyId}`, darkMode);
|
|
1512
|
+
__agiflowai_aicode_utils.log.info(`[ViteReactBundlerService] Component served at: ${url}`);
|
|
1513
|
+
return {
|
|
1514
|
+
url,
|
|
1515
|
+
htmlContent
|
|
1516
|
+
};
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
const err = /* @__PURE__ */ new Error(`Failed to serve component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1519
|
+
err.cause = error;
|
|
1520
|
+
throw err;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Pre-render a component to a static HTML file.
|
|
1525
|
+
* @param options - Component rendering options
|
|
1526
|
+
* @returns Promise resolving to the HTML file path
|
|
1527
|
+
* @throws Error if build fails
|
|
1528
|
+
*/
|
|
1529
|
+
async prerenderComponent(options) {
|
|
1530
|
+
const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent } = options;
|
|
1531
|
+
validateStoryName(storyName);
|
|
1532
|
+
validateComponentPath(componentPath);
|
|
1533
|
+
validateArgsSize(args);
|
|
1534
|
+
validateCssFiles(cssFiles, this.monorepoRoot);
|
|
1535
|
+
const resolvedAppPath = node_path.default.isAbsolute(appPath) ? appPath : node_path.default.join(this.monorepoRoot, appPath);
|
|
1536
|
+
const tmpDir = node_path.default.join(resolvedAppPath, ".tmp");
|
|
1537
|
+
try {
|
|
1538
|
+
await node_fs.promises.mkdir(tmpDir, { recursive: true });
|
|
1539
|
+
return { htmlFilePath: await this.buildComponent({
|
|
1540
|
+
componentPath,
|
|
1541
|
+
storyName,
|
|
1542
|
+
args,
|
|
1543
|
+
appPath: resolvedAppPath,
|
|
1544
|
+
darkMode,
|
|
1545
|
+
cssFiles,
|
|
1546
|
+
rootComponent,
|
|
1547
|
+
tmpDir
|
|
1548
|
+
}) };
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
const err = /* @__PURE__ */ new Error(`Failed to prerender component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1551
|
+
err.cause = error;
|
|
1552
|
+
throw err;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Clean up server resources and reset state.
|
|
1557
|
+
* Closes the Vite dev server if running.
|
|
1558
|
+
*
|
|
1559
|
+
* Note: Errors during server close are intentionally logged but not re-thrown.
|
|
1560
|
+
* This ensures cleanup always completes and state is reset, even if the server
|
|
1561
|
+
* is in an unexpected state. Callers should not depend on cleanup failure detection.
|
|
1562
|
+
*/
|
|
1563
|
+
async cleanup() {
|
|
1564
|
+
if (this.server) {
|
|
1565
|
+
__agiflowai_aicode_utils.log.info("[ViteReactBundlerService] Closing Vite dev server...");
|
|
1566
|
+
try {
|
|
1567
|
+
await this.server.close();
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
__agiflowai_aicode_utils.log.error(`[ViteReactBundlerService] Error closing server: ${error instanceof Error ? error.message : String(error)}`);
|
|
1570
|
+
}
|
|
1571
|
+
this.server = null;
|
|
1572
|
+
this.serverUrl = null;
|
|
1573
|
+
this.serverPort = null;
|
|
1574
|
+
this.currentAppPath = null;
|
|
1575
|
+
this.serverStartPromise = null;
|
|
1576
|
+
this.storyConfigs.clear();
|
|
1577
|
+
this.storyConfigTimestamps.clear();
|
|
1578
|
+
__agiflowai_aicode_utils.log.info("[ViteReactBundlerService] Vite dev server closed");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Clean up stale story configs to prevent memory leaks.
|
|
1583
|
+
* Removes configs that are older than STORY_CONFIG_MAX_AGE_MS or
|
|
1584
|
+
* when the number of configs exceeds STORY_CONFIG_MAX_COUNT.
|
|
1585
|
+
*/
|
|
1586
|
+
cleanupStaleStoryConfigs() {
|
|
1587
|
+
const now = Date.now();
|
|
1588
|
+
const entriesToDelete = [];
|
|
1589
|
+
for (const [storyId, timestamp] of this.storyConfigTimestamps) if (now - timestamp > STORY_CONFIG_MAX_AGE_MS) entriesToDelete.push(storyId);
|
|
1590
|
+
for (const storyId of entriesToDelete) {
|
|
1591
|
+
this.storyConfigs.delete(storyId);
|
|
1592
|
+
this.storyConfigTimestamps.delete(storyId);
|
|
1593
|
+
}
|
|
1594
|
+
if (entriesToDelete.length > 0) __agiflowai_aicode_utils.log.debug(`[ViteReactBundlerService] Cleaned up ${entriesToDelete.length} stale story configs`);
|
|
1595
|
+
if (this.storyConfigs.size > STORY_CONFIG_MAX_COUNT) {
|
|
1596
|
+
const toRemove = Array.from(this.storyConfigTimestamps.entries()).sort((a, b) => a[1] - b[1]).slice(0, this.storyConfigs.size - STORY_CONFIG_MAX_COUNT);
|
|
1597
|
+
for (const [storyId] of toRemove) {
|
|
1598
|
+
this.storyConfigs.delete(storyId);
|
|
1599
|
+
this.storyConfigTimestamps.delete(storyId);
|
|
1600
|
+
}
|
|
1601
|
+
__agiflowai_aicode_utils.log.debug(`[ViteReactBundlerService] Removed ${toRemove.length} oldest story configs to stay under limit`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Generate a wrapper CSS file with @source directive for Tailwind v4.
|
|
1606
|
+
* This tells Tailwind where to scan for class names when building from .tmp directory.
|
|
1607
|
+
* @param appPath - Absolute path to the app directory
|
|
1608
|
+
* @param cssFiles - Array of CSS file paths to import
|
|
1609
|
+
* @returns Generated CSS content with @source directive
|
|
1610
|
+
*/
|
|
1611
|
+
generateWrapperCss(appPath, cssFiles) {
|
|
1612
|
+
const cssImportStatements = cssFiles.map((cssFile) => {
|
|
1613
|
+
if (cssFile.startsWith("@") || cssFile.startsWith("tailwindcss/")) return `@import '${cssFile}';`;
|
|
1614
|
+
if (cssFile.startsWith("packages/") || cssFile.startsWith("apps/")) return `@import '${node_path.default.join(this.monorepoRoot, cssFile)}';`;
|
|
1615
|
+
return `@import '${node_path.default.join(appPath, cssFile)}';`;
|
|
1616
|
+
}).join("\n");
|
|
1617
|
+
return `/* Tailwind v4 source configuration for component scanning */
|
|
1618
|
+
@source "${node_path.default.join(appPath, "src")}";
|
|
1619
|
+
|
|
1620
|
+
${cssImportStatements}
|
|
1621
|
+
`;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Generate the React entry file content for rendering a story.
|
|
1625
|
+
* @param options - Build options including component path and story name
|
|
1626
|
+
* @returns Generated TypeScript/JSX entry file content
|
|
1627
|
+
*/
|
|
1628
|
+
generateEntryFile(options) {
|
|
1629
|
+
const { componentPath, storyName, args, appPath, darkMode, rootComponent, tmpDir } = options;
|
|
1630
|
+
const argsJson = JSON.stringify(args, null, 2);
|
|
1631
|
+
return `${`import '${node_path.default.join(tmpDir, "tailwind-wrapper.css").replace(/\\/g, "/")}';`}
|
|
1632
|
+
|
|
1633
|
+
import React from 'react';
|
|
1634
|
+
import { createRoot } from 'react-dom/client';
|
|
1635
|
+
import * as Stories from '${componentPath}';
|
|
1636
|
+
${rootComponent ? `import { RootDocument } from '${node_path.default.join(appPath, rootComponent).replace(/\\/g, "/")}';` : ""}
|
|
1637
|
+
|
|
1638
|
+
// Extract story metadata and the specific story to render
|
|
1639
|
+
const meta = Stories.default;
|
|
1640
|
+
const Story = Stories['${storyName}'];
|
|
1641
|
+
const storyArgs = Story?.args || {};
|
|
1642
|
+
// Merge story's default args with any custom args passed in
|
|
1643
|
+
const args = { ...storyArgs, ...${argsJson} };
|
|
1644
|
+
|
|
1645
|
+
// Create the story element - Storybook stories can define rendering in two ways:
|
|
1646
|
+
// 1. A render() function that receives args and context
|
|
1647
|
+
// 2. A component reference in meta.component
|
|
1648
|
+
let element;
|
|
1649
|
+
if (Story?.render) {
|
|
1650
|
+
// Story has custom render function - call it with args and minimal context
|
|
1651
|
+
const RenderComponent = () => Story.render(args, { loaded: {}, args });
|
|
1652
|
+
element = React.createElement(RenderComponent);
|
|
1653
|
+
} else {
|
|
1654
|
+
// Story uses component from meta - render with merged args as props
|
|
1655
|
+
const Component = meta.component;
|
|
1656
|
+
element = React.createElement(Component, args);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// Wrap element with root component (theme provider, etc.) if specified
|
|
1660
|
+
const Wrapper = ${rootComponent ? "RootDocument" : "React.Fragment"};
|
|
1661
|
+
const wrappedElement = React.createElement(Wrapper, ${rootComponent ? `{ darkMode: ${darkMode} }` : "{}"}, element);
|
|
1662
|
+
|
|
1663
|
+
// Mount to DOM
|
|
1664
|
+
const rootEl = document.getElementById('root');
|
|
1665
|
+
if (!rootEl) throw new Error('Root element not found');
|
|
1666
|
+
const root = createRoot(rootEl);
|
|
1667
|
+
root.render(wrappedElement);
|
|
1668
|
+
`;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Generate the HTML template for component preview.
|
|
1672
|
+
* @param entryFileName - Name of the entry file to include
|
|
1673
|
+
* @param darkMode - Whether to add dark mode class to HTML
|
|
1674
|
+
* @returns Generated HTML template string
|
|
1675
|
+
*/
|
|
1676
|
+
generateHtmlTemplate(entryFileName, darkMode) {
|
|
1677
|
+
return `<!DOCTYPE html>
|
|
1678
|
+
<html class="${darkMode ? "dark" : ""}">
|
|
1679
|
+
<head>
|
|
1680
|
+
<meta charset="UTF-8">
|
|
1681
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1682
|
+
<title>Component Preview</title>
|
|
1683
|
+
<style>
|
|
1684
|
+
body { margin: 0; padding: 0; }
|
|
1685
|
+
#root { display: inline-block; }
|
|
1686
|
+
</style>
|
|
1687
|
+
</head>
|
|
1688
|
+
<body>
|
|
1689
|
+
<div id="root"></div>
|
|
1690
|
+
<script type="module" src="/${entryFileName}"><\/script>
|
|
1691
|
+
</body>
|
|
1692
|
+
</html>`;
|
|
1693
|
+
}
|
|
1694
|
+
async buildComponent(options) {
|
|
1695
|
+
const { appPath, darkMode, tmpDir, cssFiles = [] } = options;
|
|
1696
|
+
const timestamp = Date.now();
|
|
1697
|
+
try {
|
|
1698
|
+
const wrapperCssPath = node_path.default.join(tmpDir, "tailwind-wrapper.css");
|
|
1699
|
+
const wrapperCssContent = this.generateWrapperCss(appPath, cssFiles);
|
|
1700
|
+
await node_fs.promises.writeFile(wrapperCssPath, wrapperCssContent, "utf-8");
|
|
1701
|
+
const storyId = `build-${timestamp}`;
|
|
1702
|
+
const virtualModuleId = `@virtual:story-entry?id=${storyId}`;
|
|
1703
|
+
const htmlTemplate = this.generateHtmlTemplate(virtualModuleId, darkMode);
|
|
1704
|
+
const htmlTemplateFileName = `index-${timestamp}.html`;
|
|
1705
|
+
const htmlTemplatePath = node_path.default.join(tmpDir, htmlTemplateFileName);
|
|
1706
|
+
await node_fs.promises.writeFile(htmlTemplatePath, htmlTemplate, "utf-8");
|
|
1707
|
+
const { build } = await import("vite");
|
|
1708
|
+
const outDir = node_path.default.join(tmpDir, `dist-${timestamp}`);
|
|
1709
|
+
const buildStoryConfigs = /* @__PURE__ */ new Map();
|
|
1710
|
+
buildStoryConfigs.set(storyId, options);
|
|
1711
|
+
await build({
|
|
1712
|
+
root: tmpDir,
|
|
1713
|
+
base: "./",
|
|
1714
|
+
configFile: false,
|
|
1715
|
+
plugins: [
|
|
1716
|
+
(0, __tailwindcss_vite.default)(),
|
|
1717
|
+
(0, vite_plugin_singlefile.viteSingleFile)(),
|
|
1718
|
+
createStoryEntryPlugin((id) => buildStoryConfigs.get(id), (opts) => this.generateEntryFile(opts))
|
|
1719
|
+
],
|
|
1720
|
+
resolve: { alias: { "@": node_path.default.join(appPath, "src") } },
|
|
1721
|
+
esbuild: {
|
|
1722
|
+
jsx: "automatic",
|
|
1723
|
+
jsxImportSource: "react"
|
|
1724
|
+
},
|
|
1725
|
+
build: {
|
|
1726
|
+
outDir,
|
|
1727
|
+
emptyOutDir: false,
|
|
1728
|
+
cssCodeSplit: false,
|
|
1729
|
+
rollupOptions: {
|
|
1730
|
+
input: htmlTemplatePath,
|
|
1731
|
+
output: { inlineDynamicImports: true }
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
const builtHtmlPath = node_path.default.join(outDir, htmlTemplateFileName);
|
|
1736
|
+
__agiflowai_aicode_utils.log.info(`[ViteReactBundlerService] Component built to: ${builtHtmlPath}`);
|
|
1737
|
+
await node_fs.promises.unlink(htmlTemplatePath).catch((err) => __agiflowai_aicode_utils.log.debug(`[ViteReactBundlerService] Failed to cleanup temp file: ${err.message}`));
|
|
1738
|
+
return builtHtmlPath;
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
const err = /* @__PURE__ */ new Error(`Failed to build component: ${error instanceof Error ? error.message : String(error)}`);
|
|
1741
|
+
err.cause = error;
|
|
1742
|
+
throw err;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
//#endregion
|
|
1748
|
+
//#region src/services/BundlerService/BundlerServiceFactory.ts
|
|
1749
|
+
/** Valid file extensions for custom service modules */
|
|
1750
|
+
const VALID_SERVICE_EXTENSIONS = [
|
|
1751
|
+
".ts",
|
|
1752
|
+
".js",
|
|
1753
|
+
".mjs",
|
|
1754
|
+
".cjs"
|
|
1755
|
+
];
|
|
1756
|
+
/**
|
|
1757
|
+
* Default factory that creates a ViteReactBundlerService instance.
|
|
1758
|
+
* Uses the singleton pattern to ensure only one dev server runs at a time.
|
|
1759
|
+
*
|
|
1760
|
+
* @returns The singleton ViteReactBundlerService instance
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```typescript
|
|
1764
|
+
* import { createDefaultBundlerService } from './BundlerServiceFactory';
|
|
1765
|
+
*
|
|
1766
|
+
* const bundler = createDefaultBundlerService();
|
|
1767
|
+
* await bundler.startDevServer('apps/my-app');
|
|
1768
|
+
* ```
|
|
1769
|
+
*/
|
|
1770
|
+
function createDefaultBundlerService() {
|
|
1771
|
+
return ViteReactBundlerService.getInstance();
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Registry of available bundler service factories.
|
|
1775
|
+
* Allows registration of custom bundler implementations by key.
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```typescript
|
|
1779
|
+
* // Register a custom bundler
|
|
1780
|
+
* bundlerRegistry.set('webpack-react', () => new WebpackReactBundlerService());
|
|
1781
|
+
*
|
|
1782
|
+
* // Get a bundler by key
|
|
1783
|
+
* const factory = bundlerRegistry.get('webpack-react');
|
|
1784
|
+
* const bundler = factory?.() ?? createDefaultBundlerService();
|
|
1785
|
+
* ```
|
|
1786
|
+
*/
|
|
1787
|
+
const bundlerRegistry = /* @__PURE__ */ new Map();
|
|
1788
|
+
bundlerRegistry.set("vite-react", createDefaultBundlerService);
|
|
1789
|
+
/** Cached bundler service instance loaded from config */
|
|
1790
|
+
let cachedBundlerService = null;
|
|
1791
|
+
/**
|
|
1792
|
+
* Get bundler service based on toolkit.yaml configuration.
|
|
1793
|
+
*
|
|
1794
|
+
* If a custom service is configured in toolkit.yaml under style-system.bundler.customService,
|
|
1795
|
+
* it will be dynamically loaded. Otherwise, returns the default ViteReactBundlerService.
|
|
1796
|
+
*
|
|
1797
|
+
* The custom service module must:
|
|
1798
|
+
* - Export a class that extends BaseBundlerService as default export, OR
|
|
1799
|
+
* - Export an instance of BaseBundlerService as default export, OR
|
|
1800
|
+
* - Export a getInstance() function that returns a BaseBundlerService
|
|
1801
|
+
*
|
|
1802
|
+
* @returns Promise resolving to a bundler service instance
|
|
1803
|
+
*
|
|
1804
|
+
* @example
|
|
1805
|
+
* ```typescript
|
|
1806
|
+
* // In toolkit.yaml:
|
|
1807
|
+
* // style-system:
|
|
1808
|
+
* // bundler:
|
|
1809
|
+
* // customService: packages/my-app/src/bundler/CustomBundlerService.ts
|
|
1810
|
+
*
|
|
1811
|
+
* const bundler = await getBundlerServiceFromConfig();
|
|
1812
|
+
* await bundler.startDevServer('apps/my-app');
|
|
1813
|
+
* ```
|
|
1814
|
+
*/
|
|
1815
|
+
async function getBundlerServiceFromConfig() {
|
|
1816
|
+
if (cachedBundlerService) return cachedBundlerService;
|
|
1817
|
+
const config = await getBundlerConfig();
|
|
1818
|
+
if (!config?.customService) {
|
|
1819
|
+
cachedBundlerService = createDefaultBundlerService();
|
|
1820
|
+
return cachedBundlerService;
|
|
1821
|
+
}
|
|
1822
|
+
const monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
1823
|
+
const customServicePath = node_path.default.resolve(monorepoRoot, config.customService);
|
|
1824
|
+
const normalizedWorkspaceRoot = node_path.default.resolve(monorepoRoot);
|
|
1825
|
+
if (!customServicePath.startsWith(normalizedWorkspaceRoot + node_path.default.sep)) {
|
|
1826
|
+
__agiflowai_aicode_utils.log.error(`[BundlerServiceFactory] Security error: customService path "${config.customService}" resolves outside workspace root`);
|
|
1827
|
+
cachedBundlerService = createDefaultBundlerService();
|
|
1828
|
+
return cachedBundlerService;
|
|
1829
|
+
}
|
|
1830
|
+
const ext = node_path.default.extname(customServicePath).toLowerCase();
|
|
1831
|
+
if (!VALID_SERVICE_EXTENSIONS.includes(ext)) {
|
|
1832
|
+
__agiflowai_aicode_utils.log.error(`[BundlerServiceFactory] Invalid file extension "${ext}" for customService. Expected one of: ${VALID_SERVICE_EXTENSIONS.join(", ")}`);
|
|
1833
|
+
cachedBundlerService = createDefaultBundlerService();
|
|
1834
|
+
return cachedBundlerService;
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
__agiflowai_aicode_utils.log.info(`[BundlerServiceFactory] Loading custom bundler service from: ${customServicePath}`);
|
|
1838
|
+
const module$1 = await import(customServicePath);
|
|
1839
|
+
if (module$1.default) {
|
|
1840
|
+
if (typeof module$1.default === "function" && module$1.default.prototype) if (typeof module$1.default.getInstance === "function") cachedBundlerService = module$1.default.getInstance();
|
|
1841
|
+
else cachedBundlerService = new module$1.default();
|
|
1842
|
+
else if (typeof module$1.default === "object") cachedBundlerService = module$1.default;
|
|
1843
|
+
} else if (typeof module$1.getInstance === "function") cachedBundlerService = module$1.getInstance();
|
|
1844
|
+
if (!cachedBundlerService) throw new Error("Custom bundler service module must export a class extending BaseBundlerService as default, an instance as default, or a getInstance() function");
|
|
1845
|
+
const missingMethods = [
|
|
1846
|
+
"getBundlerId",
|
|
1847
|
+
"getFrameworkId",
|
|
1848
|
+
"startDevServer",
|
|
1849
|
+
"serveComponent",
|
|
1850
|
+
"prerenderComponent",
|
|
1851
|
+
"isServerRunning",
|
|
1852
|
+
"getServerUrl",
|
|
1853
|
+
"getServerPort",
|
|
1854
|
+
"getCurrentAppPath",
|
|
1855
|
+
"cleanup"
|
|
1856
|
+
].filter((method) => typeof cachedBundlerService[method] !== "function");
|
|
1857
|
+
if (missingMethods.length > 0) throw new Error(`Custom bundler service must implement BaseBundlerService interface. Missing methods: ${missingMethods.join(", ")}`);
|
|
1858
|
+
__agiflowai_aicode_utils.log.info(`[BundlerServiceFactory] Custom bundler service loaded successfully`);
|
|
1859
|
+
return cachedBundlerService;
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1862
|
+
__agiflowai_aicode_utils.log.error(`[BundlerServiceFactory] Failed to load custom bundler service: ${message}`);
|
|
1863
|
+
__agiflowai_aicode_utils.log.info("[BundlerServiceFactory] Falling back to default ViteReactBundlerService");
|
|
1864
|
+
cachedBundlerService = createDefaultBundlerService();
|
|
1865
|
+
return cachedBundlerService;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
//#endregion
|
|
1870
|
+
//#region src/utils/screenshot.ts
|
|
1871
|
+
const browsers = {
|
|
1872
|
+
chromium: playwright.chromium,
|
|
1873
|
+
firefox: playwright.firefox,
|
|
1874
|
+
webkit: playwright.webkit
|
|
1875
|
+
};
|
|
1876
|
+
/**
|
|
1877
|
+
* Browser launch configurations to try in order of preference.
|
|
1878
|
+
* Prefers system-installed Chrome before falling back to Playwright's bundled browsers.
|
|
1879
|
+
*/
|
|
1880
|
+
const browserLaunchConfigs = [
|
|
1881
|
+
{
|
|
1882
|
+
browserType: playwright.chromium,
|
|
1883
|
+
channel: "chrome",
|
|
1884
|
+
name: "System Chrome"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
browserType: playwright.chromium,
|
|
1888
|
+
channel: void 0,
|
|
1889
|
+
name: "Playwright Chromium"
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
browserType: playwright.firefox,
|
|
1893
|
+
channel: void 0,
|
|
1894
|
+
name: "Playwright Firefox"
|
|
1895
|
+
}
|
|
1896
|
+
];
|
|
1897
|
+
/**
|
|
1898
|
+
* Attempts to launch a browser, trying multiple configurations in order of preference.
|
|
1899
|
+
* @returns Launched browser instance
|
|
1900
|
+
* @throws Error if no browser can be launched
|
|
1901
|
+
*/
|
|
1902
|
+
async function launchBrowserWithFallback() {
|
|
1903
|
+
const errors = [];
|
|
1904
|
+
for (const config of browserLaunchConfigs) try {
|
|
1905
|
+
const browser = await config.browserType.launch({
|
|
1906
|
+
headless: true,
|
|
1907
|
+
channel: config.channel
|
|
1908
|
+
});
|
|
1909
|
+
console.log(`[screenshot] Using ${config.name}`);
|
|
1910
|
+
return browser;
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1913
|
+
errors.push(`${config.name}: ${message}`);
|
|
1914
|
+
}
|
|
1915
|
+
throw new Error(`No browser available. Tried:\n${errors.join("\n")}\n\nPlease install Chrome, or run 'npx playwright install chromium'`);
|
|
1916
|
+
}
|
|
1917
|
+
async function takeScreenshot(options) {
|
|
1918
|
+
const { url, output, width = 1280, height = 800, fullPage = false, browser: browserName = "chromium", waitTime = 1e3, darkMode = false, mobile = false, generateThumbnail = false, thumbnailWidth = 400, thumbnailQuality = 80, base64 = false } = options;
|
|
1919
|
+
let browser = null;
|
|
1920
|
+
try {
|
|
1921
|
+
if (browserName === "chromium") browser = await launchBrowserWithFallback();
|
|
1922
|
+
else {
|
|
1923
|
+
const browserType = browsers[browserName];
|
|
1924
|
+
if (!browserType) throw new Error(`Unsupported browser: ${browserName}`);
|
|
1925
|
+
browser = await browserType.launch({ headless: true });
|
|
1926
|
+
}
|
|
1927
|
+
const page = await (await browser.newContext({
|
|
1928
|
+
viewport: {
|
|
1929
|
+
width,
|
|
1930
|
+
height
|
|
1931
|
+
},
|
|
1932
|
+
colorScheme: darkMode ? "dark" : "light",
|
|
1933
|
+
isMobile: mobile
|
|
1934
|
+
})).newPage();
|
|
1935
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
1936
|
+
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
1937
|
+
const screenshotBuffer = await page.screenshot({
|
|
1938
|
+
path: output,
|
|
1939
|
+
fullPage
|
|
1940
|
+
});
|
|
1941
|
+
const result = { imagePath: output };
|
|
1942
|
+
if (generateThumbnail) {
|
|
1943
|
+
const ext = node_path.default.extname(output);
|
|
1944
|
+
const thumbnailPath = output.replace(ext, `-thumb${ext}`);
|
|
1945
|
+
await (0, sharp.default)(screenshotBuffer).resize(thumbnailWidth).jpeg({ quality: thumbnailQuality }).toFile(thumbnailPath.replace(ext, ".jpg"));
|
|
1946
|
+
result.thumbnailPath = thumbnailPath.replace(ext, ".jpg");
|
|
1947
|
+
}
|
|
1948
|
+
if (base64) result.base64 = screenshotBuffer.toString("base64");
|
|
1949
|
+
return result;
|
|
1950
|
+
} finally {
|
|
1951
|
+
if (browser) await browser.close();
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/services/ThemeService/BaseThemeService.ts
|
|
1957
|
+
/**
|
|
1958
|
+
* Abstract base class for theme listing services.
|
|
1959
|
+
*
|
|
1960
|
+
* Subclasses must implement the `listThemes` method to provide
|
|
1961
|
+
* source-specific theme extraction logic (CSS files, JSON configs, etc.).
|
|
1962
|
+
*
|
|
1963
|
+
* @example
|
|
1964
|
+
* ```typescript
|
|
1965
|
+
* class MyCustomThemeService extends BaseThemeService {
|
|
1966
|
+
* async listThemes(): Promise<AvailableThemesResult> {
|
|
1967
|
+
* // Custom theme extraction logic
|
|
1968
|
+
* }
|
|
1969
|
+
* }
|
|
1970
|
+
* ```
|
|
1971
|
+
*/
|
|
1972
|
+
var BaseThemeService = class {
|
|
1973
|
+
config;
|
|
1974
|
+
/**
|
|
1975
|
+
* Creates a new theme service instance
|
|
1976
|
+
* @param config - Theme service configuration
|
|
1977
|
+
*/
|
|
1978
|
+
constructor(config) {
|
|
1979
|
+
this.config = config;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Validate that a file path exists and is readable.
|
|
1983
|
+
* Can be overridden by subclasses for custom validation.
|
|
1984
|
+
*
|
|
1985
|
+
* @param filePath - Path to validate
|
|
1986
|
+
* @throws Error if path is invalid or unreadable
|
|
1987
|
+
*/
|
|
1988
|
+
async validatePath(filePath) {
|
|
1989
|
+
try {
|
|
1990
|
+
await node_fs.promises.access(filePath);
|
|
1991
|
+
} catch {
|
|
1992
|
+
throw new Error(`File not found or not readable: ${filePath}`);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
//#endregion
|
|
1998
|
+
//#region src/services/ThemeService/CSSThemeService.ts
|
|
1999
|
+
/**
|
|
2000
|
+
* CSS-based theme service implementation.
|
|
2001
|
+
*
|
|
2002
|
+
* Extracts themes from CSS files by parsing class selectors that contain
|
|
2003
|
+
* CSS custom properties (variables). Each top-level class selector with
|
|
2004
|
+
* color variables is treated as a theme.
|
|
2005
|
+
*
|
|
2006
|
+
* @example
|
|
2007
|
+
* ```typescript
|
|
2008
|
+
* const service = new CSSThemeService({ themePath: 'apps/my-app/src/styles/colors.css' });
|
|
2009
|
+
* const result = await service.listThemes();
|
|
2010
|
+
* console.log(result.themes); // [{ name: 'slate', ... }, { name: 'blue', ... }]
|
|
2011
|
+
* ```
|
|
2012
|
+
*/
|
|
2013
|
+
var CSSThemeService = class extends BaseThemeService {
|
|
2014
|
+
monorepoRoot;
|
|
2015
|
+
/**
|
|
2016
|
+
* Creates a new CSSThemeService instance
|
|
2017
|
+
* @param config - Theme service configuration with themePath or cssFiles
|
|
2018
|
+
*/
|
|
2019
|
+
constructor(config) {
|
|
2020
|
+
super(config);
|
|
2021
|
+
this.monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Get the source identifier for this service
|
|
2025
|
+
* @returns 'css-file' identifier
|
|
2026
|
+
*/
|
|
2027
|
+
getSourceId() {
|
|
2028
|
+
return "css-file";
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* List available themes by parsing CSS files.
|
|
2032
|
+
*
|
|
2033
|
+
* Scans the configured CSS files for class selectors that define
|
|
2034
|
+
* CSS custom properties (--color-*, etc.) and returns them as themes.
|
|
2035
|
+
*
|
|
2036
|
+
* @returns Promise resolving to available themes result
|
|
2037
|
+
* @throws Error if no theme files are configured or files cannot be read
|
|
2038
|
+
*/
|
|
2039
|
+
async listThemes() {
|
|
2040
|
+
const cssFilePaths = this.resolveCSSFilePaths();
|
|
2041
|
+
if (cssFilePaths.length === 0) throw new Error("No theme CSS files configured. Set themePath or cssFiles in project.json style-system config.");
|
|
2042
|
+
const themes = [];
|
|
2043
|
+
for (const cssFilePath of cssFilePaths) try {
|
|
2044
|
+
await this.validatePath(cssFilePath);
|
|
2045
|
+
const fileThemes = await this.extractThemesFromCSS(cssFilePath);
|
|
2046
|
+
themes.push(...fileThemes);
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
__agiflowai_aicode_utils.log.warn(`[CSSThemeService] Could not process ${cssFilePath}:`, error);
|
|
2049
|
+
}
|
|
2050
|
+
const uniqueThemes = this.deduplicateThemes(themes);
|
|
2051
|
+
return {
|
|
2052
|
+
themes: uniqueThemes.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2053
|
+
activeTheme: uniqueThemes.length > 0 ? uniqueThemes[0].name : void 0,
|
|
2054
|
+
source: "css-file"
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Resolve configured CSS file paths to absolute paths.
|
|
2059
|
+
* @returns Array of absolute CSS file paths
|
|
2060
|
+
*/
|
|
2061
|
+
resolveCSSFilePaths() {
|
|
2062
|
+
const paths = [];
|
|
2063
|
+
if (this.config.themePath) {
|
|
2064
|
+
const themePath = node_path.default.isAbsolute(this.config.themePath) ? this.config.themePath : node_path.default.join(this.monorepoRoot, this.config.themePath);
|
|
2065
|
+
paths.push(themePath);
|
|
2066
|
+
}
|
|
2067
|
+
if (this.config.cssFiles && this.config.cssFiles.length > 0) for (const cssFile of this.config.cssFiles) {
|
|
2068
|
+
const cssPath = node_path.default.isAbsolute(cssFile) ? cssFile : node_path.default.join(this.monorepoRoot, cssFile);
|
|
2069
|
+
if (!paths.includes(cssPath)) paths.push(cssPath);
|
|
2070
|
+
}
|
|
2071
|
+
return paths;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Extract themes from a CSS file by parsing class selectors.
|
|
2075
|
+
*
|
|
2076
|
+
* Identifies class selectors that contain CSS custom property declarations
|
|
2077
|
+
* (--color-*, --primary-*, etc.) and treats each as a theme definition.
|
|
2078
|
+
*
|
|
2079
|
+
* @param cssFilePath - Absolute path to CSS file
|
|
2080
|
+
* @returns Array of ThemeInfo objects
|
|
2081
|
+
*/
|
|
2082
|
+
async extractThemesFromCSS(cssFilePath) {
|
|
2083
|
+
const content = await node_fs.promises.readFile(cssFilePath, "utf-8");
|
|
2084
|
+
const fileName = node_path.default.basename(cssFilePath);
|
|
2085
|
+
const themes = [];
|
|
2086
|
+
try {
|
|
2087
|
+
postcss.default.parse(content).walkRules((rule) => {
|
|
2088
|
+
const selector = rule.selector.trim();
|
|
2089
|
+
if (!selector.startsWith(".") || selector.includes(" ") || selector.includes(",")) return;
|
|
2090
|
+
const className = selector.slice(1);
|
|
2091
|
+
const colorVariables = {};
|
|
2092
|
+
let hasColorVariables = false;
|
|
2093
|
+
rule.walkDecls((decl) => {
|
|
2094
|
+
if (decl.prop.startsWith("--color-") || decl.prop.startsWith("--primary-")) {
|
|
2095
|
+
colorVariables[decl.prop] = decl.value;
|
|
2096
|
+
hasColorVariables = true;
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
if (hasColorVariables) themes.push({
|
|
2100
|
+
name: className,
|
|
2101
|
+
fileName,
|
|
2102
|
+
path: cssFilePath,
|
|
2103
|
+
colorVariables,
|
|
2104
|
+
shadeCount: Object.keys(colorVariables).length
|
|
2105
|
+
});
|
|
2106
|
+
});
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
throw new Error(`Failed to parse CSS file ${cssFilePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2109
|
+
}
|
|
2110
|
+
__agiflowai_aicode_utils.log.info(`[CSSThemeService] Found ${themes.length} themes in ${fileName}`);
|
|
2111
|
+
return themes;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Deduplicate themes by name, keeping the first occurrence.
|
|
2115
|
+
* @param themes - Array of themes to deduplicate
|
|
2116
|
+
* @returns Deduplicated array of themes
|
|
2117
|
+
*/
|
|
2118
|
+
deduplicateThemes(themes) {
|
|
2119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2120
|
+
return themes.filter((theme) => {
|
|
2121
|
+
if (seen.has(theme.name)) return false;
|
|
2122
|
+
seen.add(theme.name);
|
|
2123
|
+
return true;
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
//#endregion
|
|
2129
|
+
//#region src/services/ThemeService/ThemeService.ts
|
|
2130
|
+
/**
|
|
2131
|
+
* ThemeService handles theme configuration and CSS generation.
|
|
2132
|
+
*
|
|
2133
|
+
* Provides methods for accessing theme CSS, generating theme wrappers,
|
|
2134
|
+
* and listing available theme configurations.
|
|
2135
|
+
*
|
|
2136
|
+
* @example
|
|
2137
|
+
* ```typescript
|
|
2138
|
+
* const service = new ThemeService(designConfig);
|
|
2139
|
+
* const cssFiles = await service.getThemeCSS();
|
|
2140
|
+
* const themes = await service.listAvailableThemes();
|
|
2141
|
+
* ```
|
|
2142
|
+
*/
|
|
2143
|
+
var ThemeService = class {
|
|
2144
|
+
monorepoRoot;
|
|
2145
|
+
config;
|
|
2146
|
+
/**
|
|
2147
|
+
* Creates a new ThemeService instance
|
|
2148
|
+
* @param config - Design system configuration
|
|
2149
|
+
*/
|
|
2150
|
+
constructor(config) {
|
|
2151
|
+
this.monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
2152
|
+
this.config = config;
|
|
2153
|
+
__agiflowai_aicode_utils.log.info(`[ThemeService] Using theme provider: ${this.config.themeProvider}`);
|
|
2154
|
+
__agiflowai_aicode_utils.log.info(`[ThemeService] Design system type: ${this.config.type}`);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Get design system configuration
|
|
2158
|
+
* @returns Current design system configuration
|
|
2159
|
+
*/
|
|
2160
|
+
getConfig() {
|
|
2161
|
+
return this.config;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Get theme CSS imports from config or common locations
|
|
2165
|
+
* @returns Array of absolute paths to CSS files
|
|
2166
|
+
*/
|
|
2167
|
+
async getThemeCSS() {
|
|
2168
|
+
if (this.config.cssFiles && this.config.cssFiles.length > 0) return this.config.cssFiles.map((cssFile) => node_path.default.isAbsolute(cssFile) ? cssFile : node_path.default.join(this.monorepoRoot, cssFile));
|
|
2169
|
+
const cssPatterns = ["**/packages/frontend/web-theme/src/**/*.css", "**/packages/frontend/shared-theme/**/*.css"];
|
|
2170
|
+
const cssFiles = [];
|
|
2171
|
+
for (const pattern of cssPatterns) {
|
|
2172
|
+
const files = await (0, glob.glob)(pattern, {
|
|
2173
|
+
cwd: this.monorepoRoot,
|
|
2174
|
+
absolute: true,
|
|
2175
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2176
|
+
});
|
|
2177
|
+
cssFiles.push(...files);
|
|
2178
|
+
}
|
|
2179
|
+
return cssFiles;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Generate theme provider wrapper code
|
|
2183
|
+
* Uses default export as specified in config
|
|
2184
|
+
* @param componentCode - Component code to wrap
|
|
2185
|
+
* @param darkMode - Whether to use dark mode theme
|
|
2186
|
+
* @returns Generated wrapper code string
|
|
2187
|
+
*/
|
|
2188
|
+
generateThemeWrapper(componentCode, darkMode = false) {
|
|
2189
|
+
const { themeProvider } = this.config;
|
|
2190
|
+
return `
|
|
2191
|
+
import React from 'react';
|
|
2192
|
+
import ThemeProvider from '${themeProvider}';
|
|
2193
|
+
|
|
2194
|
+
const WrappedComponent = () => {
|
|
2195
|
+
return React.createElement(
|
|
2196
|
+
ThemeProvider,
|
|
2197
|
+
{ theme: ${darkMode ? "'dark'" : "'light'"} },
|
|
2198
|
+
${componentCode}
|
|
2199
|
+
);
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
export default WrappedComponent;
|
|
2203
|
+
`.trim();
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Get inline theme styles for SSR
|
|
2207
|
+
* @param darkMode - Whether to use dark mode styles
|
|
2208
|
+
* @returns Combined CSS content as string
|
|
2209
|
+
*/
|
|
2210
|
+
async getInlineStyles(darkMode = false) {
|
|
2211
|
+
const cssFiles = await this.getThemeCSS();
|
|
2212
|
+
let styles = "";
|
|
2213
|
+
for (const cssFile of cssFiles) try {
|
|
2214
|
+
const content = await node_fs.promises.readFile(cssFile, "utf-8");
|
|
2215
|
+
styles += content + "\n";
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
__agiflowai_aicode_utils.log.warn(`[ThemeService] Could not read CSS file ${cssFile}:`, error);
|
|
2218
|
+
}
|
|
2219
|
+
if (darkMode && styles) styles = `.dark { ${styles} }`;
|
|
2220
|
+
return styles;
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Get Tailwind CSS classes for theming
|
|
2224
|
+
* @param darkMode - Whether to use dark mode classes
|
|
2225
|
+
* @returns Array of Tailwind class names
|
|
2226
|
+
*/
|
|
2227
|
+
getTailwindClasses(darkMode = false) {
|
|
2228
|
+
const baseClasses = ["font-sans", "antialiased"];
|
|
2229
|
+
if (darkMode) return [
|
|
2230
|
+
...baseClasses,
|
|
2231
|
+
"dark",
|
|
2232
|
+
"bg-gray-900",
|
|
2233
|
+
"text-white"
|
|
2234
|
+
];
|
|
2235
|
+
return [
|
|
2236
|
+
...baseClasses,
|
|
2237
|
+
"bg-white",
|
|
2238
|
+
"text-gray-900"
|
|
2239
|
+
];
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Validate that the theme provider path exists
|
|
2243
|
+
* @returns True if theme provider is valid
|
|
2244
|
+
*/
|
|
2245
|
+
async validateThemeProvider() {
|
|
2246
|
+
try {
|
|
2247
|
+
if (this.config.themeProvider.startsWith("@") || !this.config.themeProvider.startsWith("/")) return true;
|
|
2248
|
+
return (await node_fs.promises.stat(this.config.themeProvider)).isFile();
|
|
2249
|
+
} catch {
|
|
2250
|
+
return false;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* List all available theme configurations
|
|
2255
|
+
* @returns Object containing themes array and active brand
|
|
2256
|
+
* @throws Error if themes directory cannot be read
|
|
2257
|
+
*/
|
|
2258
|
+
async listAvailableThemes() {
|
|
2259
|
+
const configsPath = node_path.default.join(this.monorepoRoot, "packages/frontend/shared-theme/configs");
|
|
2260
|
+
try {
|
|
2261
|
+
const themeFiles = (await node_fs.promises.readdir(configsPath)).filter((file) => file.endsWith(".json"));
|
|
2262
|
+
const themes = [];
|
|
2263
|
+
let activeBrand;
|
|
2264
|
+
for (const file of themeFiles) {
|
|
2265
|
+
const filePath = node_path.default.join(configsPath, file);
|
|
2266
|
+
const content = await node_fs.promises.readFile(filePath, "utf-8");
|
|
2267
|
+
const themeData = JSON.parse(content);
|
|
2268
|
+
const themeName = file.replace(".json", "");
|
|
2269
|
+
themes.push({
|
|
2270
|
+
name: themeName,
|
|
2271
|
+
fileName: file,
|
|
2272
|
+
path: filePath,
|
|
2273
|
+
colors: themeData.colors || themeData
|
|
2274
|
+
});
|
|
2275
|
+
if (themeName === "lightTheme" || themeName === "agimonTheme") activeBrand = themeName;
|
|
2276
|
+
}
|
|
2277
|
+
return {
|
|
2278
|
+
themes: themes.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2279
|
+
activeBrand
|
|
2280
|
+
};
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
throw new Error(`Failed to list themes: ${error instanceof Error ? error.message : String(error)}`);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
//#endregion
|
|
2288
|
+
//#region src/services/ThemeService/types.ts
|
|
2289
|
+
/**
|
|
2290
|
+
* Default theme service configuration
|
|
2291
|
+
*/
|
|
2292
|
+
const DEFAULT_THEME_SERVICE_CONFIG = {
|
|
2293
|
+
themePath: void 0,
|
|
2294
|
+
cssFiles: [],
|
|
2295
|
+
customServicePath: void 0
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region src/services/ThemeService/ThemeServiceFactory.ts
|
|
2300
|
+
/**
|
|
2301
|
+
* Factory for creating theme service instances.
|
|
2302
|
+
*
|
|
2303
|
+
* Supports the built-in CSSThemeService and custom service implementations
|
|
2304
|
+
* loaded dynamically from user-specified paths.
|
|
2305
|
+
*
|
|
2306
|
+
* @example
|
|
2307
|
+
* ```typescript
|
|
2308
|
+
* const factory = new ThemeServiceFactory();
|
|
2309
|
+
*
|
|
2310
|
+
* // Create default CSS-based service
|
|
2311
|
+
* const service = await factory.createService({
|
|
2312
|
+
* themePath: 'apps/my-app/src/styles/colors.css'
|
|
2313
|
+
* });
|
|
2314
|
+
*
|
|
2315
|
+
* // Create with custom service override
|
|
2316
|
+
* const customService = await factory.createService({
|
|
2317
|
+
* customServicePath: './my-custom-theme-service.ts'
|
|
2318
|
+
* });
|
|
2319
|
+
*
|
|
2320
|
+
* const themes = await service.listThemes();
|
|
2321
|
+
* ```
|
|
2322
|
+
*/
|
|
2323
|
+
var ThemeServiceFactory = class {
|
|
2324
|
+
/**
|
|
2325
|
+
* Create a theme service based on configuration.
|
|
2326
|
+
*
|
|
2327
|
+
* Resolution order:
|
|
2328
|
+
* 1. If customServicePath is provided, load custom service dynamically
|
|
2329
|
+
* 2. Otherwise, create the default CSSThemeService
|
|
2330
|
+
*
|
|
2331
|
+
* @param config - Theme service configuration
|
|
2332
|
+
* @returns Promise resolving to a theme service instance
|
|
2333
|
+
* @throws Error if custom service cannot be loaded or is invalid
|
|
2334
|
+
*/
|
|
2335
|
+
async createService(config = {}) {
|
|
2336
|
+
const resolvedConfig = {
|
|
2337
|
+
...DEFAULT_THEME_SERVICE_CONFIG,
|
|
2338
|
+
...config
|
|
2339
|
+
};
|
|
2340
|
+
if (resolvedConfig.customServicePath) return this.loadCustomService(resolvedConfig);
|
|
2341
|
+
return new CSSThemeService(resolvedConfig);
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Load a custom theme service from user-specified path.
|
|
2345
|
+
*
|
|
2346
|
+
* The custom service must export a class that extends BaseThemeService.
|
|
2347
|
+
*
|
|
2348
|
+
* @param config - Configuration with customServicePath set
|
|
2349
|
+
* @returns Promise resolving to custom service instance
|
|
2350
|
+
* @throws Error if service cannot be loaded or is invalid
|
|
2351
|
+
*/
|
|
2352
|
+
async loadCustomService(config) {
|
|
2353
|
+
const servicePath = config.customServicePath;
|
|
2354
|
+
if (!servicePath) throw new Error("customServicePath is required for custom service loading");
|
|
2355
|
+
try {
|
|
2356
|
+
const customModule = await import(servicePath);
|
|
2357
|
+
const ServiceClass = customModule.default || customModule.ThemeService || customModule.CustomThemeService;
|
|
2358
|
+
if (!ServiceClass) throw new Error(`Custom service module at ${servicePath} must export a default class, ThemeService, or CustomThemeService that extends BaseThemeService`);
|
|
2359
|
+
const instance = new ServiceClass(config);
|
|
2360
|
+
if (!(instance instanceof BaseThemeService)) throw new Error(`Custom service at ${servicePath} must extend BaseThemeService`);
|
|
2361
|
+
return instance;
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
if (error instanceof Error && error.message.includes("BaseThemeService")) throw error;
|
|
2364
|
+
throw new Error(`Failed to load custom theme service from ${servicePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
//#endregion
|
|
2370
|
+
//#region src/services/ComponentRendererService/ComponentRendererService.ts
|
|
2371
|
+
/**
|
|
2372
|
+
* ComponentRendererService handles rendering React components to images.
|
|
2373
|
+
*
|
|
2374
|
+
* Uses BundlerService for building/serving components and ThemeService
|
|
2375
|
+
* for theme configuration. Supports both dev server (fast) and static
|
|
2376
|
+
* build (fallback) rendering modes.
|
|
2377
|
+
*
|
|
2378
|
+
* The bundler service can be customized by providing a bundlerFactory
|
|
2379
|
+
* to support different bundlers (Vite, Webpack) and frameworks (React, Vue).
|
|
2380
|
+
*
|
|
2381
|
+
* @example
|
|
2382
|
+
* ```typescript
|
|
2383
|
+
* // Using default bundler (Vite + React)
|
|
2384
|
+
* const service = new ComponentRendererService(designConfig, 'apps/my-app');
|
|
2385
|
+
*
|
|
2386
|
+
* // Using custom bundler
|
|
2387
|
+
* const service = new ComponentRendererService(
|
|
2388
|
+
* designConfig,
|
|
2389
|
+
* 'apps/my-app',
|
|
2390
|
+
* { bundlerFactory: () => new MyCustomBundlerService() }
|
|
2391
|
+
* );
|
|
2392
|
+
*
|
|
2393
|
+
* const result = await service.renderComponent(componentInfo, {
|
|
2394
|
+
* storyName: 'Primary',
|
|
2395
|
+
* darkMode: true,
|
|
2396
|
+
* width: 1280,
|
|
2397
|
+
* height: 800
|
|
2398
|
+
* });
|
|
2399
|
+
* console.log(result.imagePath);
|
|
2400
|
+
* ```
|
|
2401
|
+
*/
|
|
2402
|
+
var ComponentRendererService = class ComponentRendererService {
|
|
2403
|
+
monorepoRoot;
|
|
2404
|
+
tmpDir;
|
|
2405
|
+
themeService;
|
|
2406
|
+
appPath;
|
|
2407
|
+
bundlerFactory;
|
|
2408
|
+
/**
|
|
2409
|
+
* Creates a new ComponentRendererService instance
|
|
2410
|
+
* @param designSystemConfig - Design system configuration
|
|
2411
|
+
* @param appPath - Path to the app directory (relative or absolute)
|
|
2412
|
+
* @param options - Optional configuration including custom bundler factory
|
|
2413
|
+
*/
|
|
2414
|
+
constructor(designSystemConfig, appPath, options = {}) {
|
|
2415
|
+
if (!appPath) throw new Error("appPath is required for ComponentRendererService");
|
|
2416
|
+
this.monorepoRoot = __agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRootSync();
|
|
2417
|
+
this.appPath = appPath;
|
|
2418
|
+
this.tmpDir = node_path.default.join(node_os.default.tmpdir(), "style-system");
|
|
2419
|
+
this.themeService = new ThemeService(designSystemConfig);
|
|
2420
|
+
this.bundlerFactory = options.bundlerFactory ?? createDefaultBundlerService;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Get the bundler service instance.
|
|
2424
|
+
* Uses the factory to create/retrieve the bundler.
|
|
2425
|
+
* @returns The bundler service instance
|
|
2426
|
+
*/
|
|
2427
|
+
getBundlerService() {
|
|
2428
|
+
return this.bundlerFactory();
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Render a component to an image
|
|
2432
|
+
* @param componentInfo - Component metadata from StoriesIndexService
|
|
2433
|
+
* @param options - Render options (story name, args, dimensions, etc.)
|
|
2434
|
+
* @returns Rendered image path, HTML content, and component info
|
|
2435
|
+
* @throws Error if rendering fails
|
|
2436
|
+
*/
|
|
2437
|
+
async renderComponent(componentInfo, options = {}) {
|
|
2438
|
+
const { storyName = componentInfo.stories[0] || "Default", args = {}, darkMode = false, width = 1280, height = 800 } = options;
|
|
2439
|
+
try {
|
|
2440
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] Rendering ${componentInfo.title} - ${storyName}`);
|
|
2441
|
+
await node_fs.promises.mkdir(this.tmpDir, { recursive: true });
|
|
2442
|
+
const designSystemConfig = this.themeService.getConfig();
|
|
2443
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] Using theme provider: ${designSystemConfig.themeProvider}`);
|
|
2444
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] Design system type: ${designSystemConfig.type}`);
|
|
2445
|
+
if (!await this.themeService.validateThemeProvider()) __agiflowai_aicode_utils.log.warn(`[ComponentRendererService] Theme provider path may not exist: ${designSystemConfig.themeProvider}`);
|
|
2446
|
+
const bundlerService = this.getBundlerService();
|
|
2447
|
+
let componentUrl;
|
|
2448
|
+
let htmlFilePath;
|
|
2449
|
+
if (bundlerService.isServerRunning()) {
|
|
2450
|
+
__agiflowai_aicode_utils.log.info("[ComponentRendererService] Using dev server for fast rendering");
|
|
2451
|
+
try {
|
|
2452
|
+
const result = await bundlerService.serveComponent({
|
|
2453
|
+
componentPath: componentInfo.filePath,
|
|
2454
|
+
storyName,
|
|
2455
|
+
args,
|
|
2456
|
+
themePath: designSystemConfig.themeProvider,
|
|
2457
|
+
darkMode,
|
|
2458
|
+
appPath: this.appPath,
|
|
2459
|
+
cssFiles: designSystemConfig.cssFiles || [],
|
|
2460
|
+
rootComponent: designSystemConfig.rootComponent
|
|
2461
|
+
});
|
|
2462
|
+
componentUrl = result.url;
|
|
2463
|
+
htmlFilePath = result.htmlFilePath;
|
|
2464
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] Component served at: ${componentUrl}`);
|
|
2465
|
+
} catch (devServerError) {
|
|
2466
|
+
__agiflowai_aicode_utils.log.warn(`[ComponentRendererService] Dev server failed, falling back to static build: ${devServerError instanceof Error ? devServerError.message : String(devServerError)}`);
|
|
2467
|
+
const result = await bundlerService.prerenderComponent({
|
|
2468
|
+
componentPath: componentInfo.filePath,
|
|
2469
|
+
storyName,
|
|
2470
|
+
args,
|
|
2471
|
+
themePath: designSystemConfig.themeProvider,
|
|
2472
|
+
darkMode,
|
|
2473
|
+
appPath: this.appPath,
|
|
2474
|
+
cssFiles: designSystemConfig.cssFiles || [],
|
|
2475
|
+
rootComponent: designSystemConfig.rootComponent
|
|
2476
|
+
});
|
|
2477
|
+
componentUrl = `file://${result.htmlFilePath}`;
|
|
2478
|
+
htmlFilePath = result.htmlFilePath;
|
|
2479
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] HTML built to: ${htmlFilePath}`);
|
|
2480
|
+
}
|
|
2481
|
+
} else {
|
|
2482
|
+
__agiflowai_aicode_utils.log.info("[ComponentRendererService] No dev server running, using static build");
|
|
2483
|
+
const result = await bundlerService.prerenderComponent({
|
|
2484
|
+
componentPath: componentInfo.filePath,
|
|
2485
|
+
storyName,
|
|
2486
|
+
args,
|
|
2487
|
+
themePath: designSystemConfig.themeProvider,
|
|
2488
|
+
darkMode,
|
|
2489
|
+
appPath: this.appPath,
|
|
2490
|
+
cssFiles: designSystemConfig.cssFiles || [],
|
|
2491
|
+
rootComponent: designSystemConfig.rootComponent
|
|
2492
|
+
});
|
|
2493
|
+
componentUrl = `file://${result.htmlFilePath}`;
|
|
2494
|
+
htmlFilePath = result.htmlFilePath;
|
|
2495
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] HTML built to: ${htmlFilePath}`);
|
|
2496
|
+
}
|
|
2497
|
+
const timestamp = Date.now();
|
|
2498
|
+
const imageName = `component-${componentInfo.title.split("/").pop()}-${timestamp}.png`;
|
|
2499
|
+
const imagePath = node_path.default.join(this.tmpDir, imageName);
|
|
2500
|
+
await takeScreenshot({
|
|
2501
|
+
url: componentUrl,
|
|
2502
|
+
output: imagePath,
|
|
2503
|
+
width,
|
|
2504
|
+
height,
|
|
2505
|
+
fullPage: true,
|
|
2506
|
+
browser: "chromium",
|
|
2507
|
+
waitTime: 2e3,
|
|
2508
|
+
darkMode,
|
|
2509
|
+
mobile: false,
|
|
2510
|
+
generateThumbnail: true,
|
|
2511
|
+
thumbnailWidth: 900,
|
|
2512
|
+
thumbnailQuality: 80,
|
|
2513
|
+
base64: false
|
|
2514
|
+
});
|
|
2515
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] Screenshot saved to: ${imagePath}`);
|
|
2516
|
+
let html = "";
|
|
2517
|
+
if (htmlFilePath) {
|
|
2518
|
+
html = await node_fs.promises.readFile(htmlFilePath, "utf-8");
|
|
2519
|
+
__agiflowai_aicode_utils.log.info(`[ComponentRendererService] HTML file kept at: ${htmlFilePath}`);
|
|
2520
|
+
} else __agiflowai_aicode_utils.log.warn("[ComponentRendererService] No HTML file path available, returning empty HTML content");
|
|
2521
|
+
return {
|
|
2522
|
+
imagePath,
|
|
2523
|
+
html,
|
|
2524
|
+
componentInfo
|
|
2525
|
+
};
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2528
|
+
throw new Error(`Failed to render component ${componentInfo.title} (${storyName}): ${errorMessage}`);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Maximum number of screenshot files to keep in temp directory.
|
|
2533
|
+
* Prevents disk space issues on long-running servers.
|
|
2534
|
+
*/
|
|
2535
|
+
static MAX_TEMP_FILES = 100;
|
|
2536
|
+
/**
|
|
2537
|
+
* Clean up old rendered files.
|
|
2538
|
+
* Removes files older than the specified duration and enforces a max file count.
|
|
2539
|
+
* @param olderThanMs - Remove files older than this duration (default: 1 hour)
|
|
2540
|
+
*/
|
|
2541
|
+
async cleanup(olderThanMs = 36e5) {
|
|
2542
|
+
try {
|
|
2543
|
+
const files = await node_fs.promises.readdir(this.tmpDir);
|
|
2544
|
+
const now = Date.now();
|
|
2545
|
+
const componentFiles = [];
|
|
2546
|
+
for (const file of files) if (file.startsWith("component-")) {
|
|
2547
|
+
const filePath = node_path.default.join(this.tmpDir, file);
|
|
2548
|
+
try {
|
|
2549
|
+
const stats = await node_fs.promises.stat(filePath);
|
|
2550
|
+
componentFiles.push({
|
|
2551
|
+
name: file,
|
|
2552
|
+
path: filePath,
|
|
2553
|
+
mtime: stats.mtimeMs
|
|
2554
|
+
});
|
|
2555
|
+
} catch {}
|
|
2556
|
+
}
|
|
2557
|
+
let deletedCount = 0;
|
|
2558
|
+
for (const file of componentFiles) if (now - file.mtime > olderThanMs) {
|
|
2559
|
+
await node_fs.promises.unlink(file.path).catch((err) => __agiflowai_aicode_utils.log.warn("[ComponentRendererService] Failed to delete file:", file.path, err));
|
|
2560
|
+
deletedCount++;
|
|
2561
|
+
}
|
|
2562
|
+
const remainingFiles = componentFiles.filter((f) => now - f.mtime <= olderThanMs);
|
|
2563
|
+
if (remainingFiles.length > ComponentRendererService.MAX_TEMP_FILES) {
|
|
2564
|
+
remainingFiles.sort((a, b) => a.mtime - b.mtime);
|
|
2565
|
+
const toDelete = remainingFiles.slice(0, remainingFiles.length - ComponentRendererService.MAX_TEMP_FILES);
|
|
2566
|
+
for (const file of toDelete) {
|
|
2567
|
+
await node_fs.promises.unlink(file.path).catch((err) => __agiflowai_aicode_utils.log.warn("[ComponentRendererService] Failed to delete file:", file.path, err));
|
|
2568
|
+
deletedCount++;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (deletedCount > 0) __agiflowai_aicode_utils.log.info(`[ComponentRendererService] Cleaned up ${deletedCount} temp files`);
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
__agiflowai_aicode_utils.log.error("[ComponentRendererService] Cleanup error:", error);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Cleanup bundler server and temp files.
|
|
2578
|
+
* Called on service shutdown.
|
|
2579
|
+
*/
|
|
2580
|
+
async dispose() {
|
|
2581
|
+
try {
|
|
2582
|
+
await this.cleanup(0);
|
|
2583
|
+
const bundlerService = this.getBundlerService();
|
|
2584
|
+
if (!bundlerService.isServerRunning()) await bundlerService.cleanup();
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
__agiflowai_aicode_utils.log.error("[ComponentRendererService] Dispose error:", error);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
//#endregion
|
|
2592
|
+
//#region src/services/GetUiComponentService/types.ts
|
|
2593
|
+
/**
|
|
2594
|
+
* Default configuration values.
|
|
2595
|
+
*/
|
|
2596
|
+
const DEFAULT_GET_UI_COMPONENT_CONFIG = {
|
|
2597
|
+
defaultStoryName: "Playground",
|
|
2598
|
+
defaultDarkMode: true,
|
|
2599
|
+
defaultWidth: 1280,
|
|
2600
|
+
defaultHeight: 800
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
//#endregion
|
|
2604
|
+
//#region src/services/GetUiComponentService/GetUiComponentService.ts
|
|
2605
|
+
/**
|
|
2606
|
+
* GetUiComponentService handles rendering UI component previews.
|
|
2607
|
+
*
|
|
2608
|
+
* Locates components in the stories index, renders them with app-specific
|
|
2609
|
+
* design system configuration, and returns screenshot results.
|
|
2610
|
+
*
|
|
2611
|
+
* @example
|
|
2612
|
+
* ```typescript
|
|
2613
|
+
* const service = new GetUiComponentService();
|
|
2614
|
+
* const result = await service.getComponent({
|
|
2615
|
+
* componentName: 'Button',
|
|
2616
|
+
* appPath: 'apps/my-app',
|
|
2617
|
+
* });
|
|
2618
|
+
* console.log(result.imagePath);
|
|
2619
|
+
* ```
|
|
2620
|
+
*/
|
|
2621
|
+
var GetUiComponentService = class {
|
|
2622
|
+
config;
|
|
2623
|
+
storiesIndexFactory;
|
|
2624
|
+
rendererFactory;
|
|
2625
|
+
/**
|
|
2626
|
+
* Creates a new GetUiComponentService instance.
|
|
2627
|
+
* @param config - Service configuration options
|
|
2628
|
+
* @param storiesIndexFactory - Factory for creating StoriesIndexService (for DI/testing)
|
|
2629
|
+
* @param rendererFactory - Factory for creating ComponentRendererService (for DI/testing)
|
|
2630
|
+
*/
|
|
2631
|
+
constructor(config = {}, storiesIndexFactory = () => new StoriesIndexService(), rendererFactory = (c, p) => new ComponentRendererService(c, p)) {
|
|
2632
|
+
this.config = {
|
|
2633
|
+
...DEFAULT_GET_UI_COMPONENT_CONFIG,
|
|
2634
|
+
...config
|
|
2635
|
+
};
|
|
2636
|
+
this.storiesIndexFactory = storiesIndexFactory;
|
|
2637
|
+
this.rendererFactory = rendererFactory;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Get a UI component preview image.
|
|
2641
|
+
*
|
|
2642
|
+
* @param input - Component input parameters
|
|
2643
|
+
* @returns Result with image path and component metadata
|
|
2644
|
+
* @throws Error if input validation fails, component not found, or rendering fails
|
|
2645
|
+
*/
|
|
2646
|
+
async getComponent(input) {
|
|
2647
|
+
if (!input.componentName || typeof input.componentName !== "string") throw new Error("componentName is required and must be a non-empty string");
|
|
2648
|
+
if (!input.appPath || typeof input.appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
|
|
2649
|
+
if (input.storyName !== void 0 && typeof input.storyName !== "string") throw new Error("storyName must be a string");
|
|
2650
|
+
if (input.darkMode !== void 0 && typeof input.darkMode !== "boolean") throw new Error("darkMode must be a boolean");
|
|
2651
|
+
if (input.selector !== void 0) {
|
|
2652
|
+
if (typeof input.selector !== "string") throw new Error("selector must be a string");
|
|
2653
|
+
if (!/^[a-zA-Z0-9_\-#.\[\]=":' ]+$/.test(input.selector)) throw new Error("selector contains invalid characters");
|
|
2654
|
+
if (/javascript:|expression\(|url\(/i.test(input.selector)) throw new Error("selector contains potentially malicious content");
|
|
2655
|
+
}
|
|
2656
|
+
const { componentName, appPath, storyName = this.config.defaultStoryName, darkMode = this.config.defaultDarkMode } = input;
|
|
2657
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Starting for component: ${componentName}, appPath: ${appPath}, storyName: ${storyName}`);
|
|
2658
|
+
const storiesIndex = this.storiesIndexFactory();
|
|
2659
|
+
try {
|
|
2660
|
+
await storiesIndex.initialize();
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
throw new Error(`Failed to initialize stories index: ${error instanceof Error ? error.message : String(error)}`);
|
|
2663
|
+
}
|
|
2664
|
+
const componentInfo = storiesIndex.findComponentByName(componentName);
|
|
2665
|
+
if (!componentInfo) throw new Error(`Component "${componentName}" not found in stories index. Ensure the component has a .stories.tsx file and has been indexed.`);
|
|
2666
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Found component: ${componentInfo.title}`);
|
|
2667
|
+
const validStoryName = this.resolveStoryName(storyName, componentInfo.stories);
|
|
2668
|
+
const designSystemConfig = await getAppDesignSystemConfig(appPath);
|
|
2669
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Using theme provider: ${designSystemConfig.themeProvider}`);
|
|
2670
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Design system type: ${designSystemConfig.type}`);
|
|
2671
|
+
const renderer = this.rendererFactory(designSystemConfig, appPath);
|
|
2672
|
+
try {
|
|
2673
|
+
const renderResult = await renderer.renderComponent(componentInfo, {
|
|
2674
|
+
storyName: validStoryName,
|
|
2675
|
+
width: this.config.defaultWidth,
|
|
2676
|
+
height: this.config.defaultHeight,
|
|
2677
|
+
darkMode
|
|
2678
|
+
});
|
|
2679
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Component rendered to: ${renderResult.imagePath}`);
|
|
2680
|
+
const storyFileContent = await this.readStoryFile(componentInfo.filePath);
|
|
2681
|
+
__agiflowai_aicode_utils.log.info("[GetUiComponentService] Completed successfully");
|
|
2682
|
+
return {
|
|
2683
|
+
imagePath: renderResult.imagePath,
|
|
2684
|
+
format: "png",
|
|
2685
|
+
dimensions: "900px width, 80% quality",
|
|
2686
|
+
storyFilePath: componentInfo.filePath,
|
|
2687
|
+
storyFileContent,
|
|
2688
|
+
componentTitle: componentInfo.title,
|
|
2689
|
+
availableStories: componentInfo.stories,
|
|
2690
|
+
renderedStory: validStoryName
|
|
2691
|
+
};
|
|
2692
|
+
} finally {
|
|
2693
|
+
await renderer.dispose();
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Resolve and validate the story name.
|
|
2698
|
+
*
|
|
2699
|
+
* @param requestedStory - The requested story name
|
|
2700
|
+
* @param availableStories - List of available stories for the component
|
|
2701
|
+
* @returns Valid story name (requested or fallback)
|
|
2702
|
+
*/
|
|
2703
|
+
resolveStoryName(requestedStory, availableStories) {
|
|
2704
|
+
if (availableStories.includes(requestedStory)) return requestedStory;
|
|
2705
|
+
__agiflowai_aicode_utils.log.warn(`[GetUiComponentService] Story "${requestedStory}" not found, available stories: ${availableStories.join(", ")}`);
|
|
2706
|
+
const fallbackStory = availableStories[0] || "Default";
|
|
2707
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Using fallback story: ${fallbackStory}`);
|
|
2708
|
+
return fallbackStory;
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Read the story file content.
|
|
2712
|
+
*
|
|
2713
|
+
* @param filePath - Path to the story file
|
|
2714
|
+
* @returns File content or error message
|
|
2715
|
+
*/
|
|
2716
|
+
async readStoryFile(filePath) {
|
|
2717
|
+
try {
|
|
2718
|
+
const content = await node_fs.promises.readFile(filePath, "utf-8");
|
|
2719
|
+
__agiflowai_aicode_utils.log.info(`[GetUiComponentService] Story file read successfully (${content.length} chars)`);
|
|
2720
|
+
return content;
|
|
2721
|
+
} catch (error) {
|
|
2722
|
+
__agiflowai_aicode_utils.log.warn(`[GetUiComponentService] Warning: Could not read story file: ${error instanceof Error ? error.message : String(error)}`);
|
|
2723
|
+
return `// Could not read file: ${filePath}\n// Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
|
|
2728
|
+
//#endregion
|
|
2729
|
+
//#region src/tools/GetComponentVisualTool.ts
|
|
2730
|
+
/**
|
|
2731
|
+
* Template for visual review instructions returned with component screenshots.
|
|
2732
|
+
* Guides the LLM to verify visual correctness of the rendered component.
|
|
2733
|
+
* @param imagePath - Path to the component screenshot image
|
|
2734
|
+
* @returns Formatted markdown string with visual review instructions
|
|
2735
|
+
*/
|
|
2736
|
+
const REVIEW_INSTRUCTIONS_TEMPLATE = (imagePath) => `
|
|
2737
|
+
## Visual Review Instructions
|
|
2738
|
+
|
|
2739
|
+
Please review the component screenshot and story code to verify visual correctness:
|
|
2740
|
+
|
|
2741
|
+
1. **Read the screenshot** at: ${imagePath}
|
|
2742
|
+
2. **Compare with the story code** to ensure the rendered output matches expectations
|
|
2743
|
+
3. **Check for visual issues**:
|
|
2744
|
+
- Are all variants rendering correctly?
|
|
2745
|
+
- Are colors, spacing, and typography as expected?
|
|
2746
|
+
- Are interactive states (hover, disabled, loading) properly styled?
|
|
2747
|
+
- Is the layout and alignment correct?
|
|
2748
|
+
|
|
2749
|
+
If you notice any visual discrepancies between the code and the rendered output, please report them.
|
|
2750
|
+
`;
|
|
2751
|
+
var GetComponentVisualTool = class GetComponentVisualTool {
|
|
2752
|
+
static TOOL_NAME = "get_component_visual";
|
|
2753
|
+
service;
|
|
2754
|
+
/**
|
|
2755
|
+
* Creates a new GetComponentVisualTool instance.
|
|
2756
|
+
* @param service - Optional service instance for dependency injection (useful for testing)
|
|
2757
|
+
*/
|
|
2758
|
+
constructor(service) {
|
|
2759
|
+
this.service = service ?? new GetUiComponentService();
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Returns the tool definition including name, description, and input schema.
|
|
2763
|
+
* @returns Tool definition with JSON Schema for input validation
|
|
2764
|
+
*/
|
|
2765
|
+
getDefinition() {
|
|
2766
|
+
return {
|
|
2767
|
+
name: GetComponentVisualTool.TOOL_NAME,
|
|
2768
|
+
description: "Get a image preview of a UI component with app-specific design system configuration. Useful when work on the frontend design to review the UI quickly without running the full app.",
|
|
2769
|
+
inputSchema: {
|
|
2770
|
+
type: "object",
|
|
2771
|
+
properties: {
|
|
2772
|
+
componentName: {
|
|
2773
|
+
type: "string",
|
|
2774
|
+
minLength: 1,
|
|
2775
|
+
description: "The name of the component to capture (e.g., \"Button\", \"Card\", etc.)"
|
|
2776
|
+
},
|
|
2777
|
+
appPath: {
|
|
2778
|
+
type: "string",
|
|
2779
|
+
minLength: 1,
|
|
2780
|
+
description: "The app path (relative or absolute) to load design system configuration from (e.g., \"apps/agiflow-app\"). The design system config is read from {appPath}/project.json"
|
|
2781
|
+
},
|
|
2782
|
+
storyName: {
|
|
2783
|
+
type: "string",
|
|
2784
|
+
minLength: 1,
|
|
2785
|
+
description: "The story name to render (e.g., \"Playground\", \"Default\"). Defaults to \"Playground\"."
|
|
2786
|
+
},
|
|
2787
|
+
darkMode: {
|
|
2788
|
+
type: "boolean",
|
|
2789
|
+
description: "Whether to render the component in dark mode. Defaults to true."
|
|
2790
|
+
},
|
|
2791
|
+
selector: {
|
|
2792
|
+
type: "string",
|
|
2793
|
+
minLength: 1,
|
|
2794
|
+
description: "CSS selector to target specific element for screenshot. When provided, screenshot will auto-resize to element dimensions. Defaults to \"#root\"."
|
|
2795
|
+
}
|
|
2796
|
+
},
|
|
2797
|
+
required: ["componentName", "appPath"],
|
|
2798
|
+
additionalProperties: false
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Executes the tool to get a UI component preview.
|
|
2804
|
+
* @param input - The input parameters for getting the component
|
|
2805
|
+
* @returns Promise resolving to CallToolResult with component info or error
|
|
2806
|
+
*/
|
|
2807
|
+
async execute(input) {
|
|
2808
|
+
try {
|
|
2809
|
+
const result = await this.service.getComponent(input);
|
|
2810
|
+
const reviewInstructions = REVIEW_INSTRUCTIONS_TEMPLATE(result.imagePath);
|
|
2811
|
+
return { content: [{
|
|
2812
|
+
type: "text",
|
|
2813
|
+
text: JSON.stringify(result, null, 2)
|
|
2814
|
+
}, {
|
|
2815
|
+
type: "text",
|
|
2816
|
+
text: reviewInstructions
|
|
2817
|
+
}] };
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
return {
|
|
2820
|
+
content: [{
|
|
2821
|
+
type: "text",
|
|
2822
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2823
|
+
}],
|
|
2824
|
+
isError: true
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
|
|
2830
|
+
//#endregion
|
|
2831
|
+
//#region src/tools/ListAppComponentsTool.ts
|
|
2832
|
+
/**
|
|
2833
|
+
* Tool to list app-specific components and package components used by an app.
|
|
2834
|
+
*
|
|
2835
|
+
* Detects app components by file path (within app directory) and resolves
|
|
2836
|
+
* workspace dependencies to find package components from Storybook stories.
|
|
2837
|
+
*
|
|
2838
|
+
* @example
|
|
2839
|
+
* ```typescript
|
|
2840
|
+
* const tool = new ListAppComponentsTool();
|
|
2841
|
+
* const result = await tool.execute({ appPath: 'apps/my-app' });
|
|
2842
|
+
* // Returns: { app: 'my-app', appComponents: ['Button'], packageComponents: {...}, pagination: {...} }
|
|
2843
|
+
* ```
|
|
2844
|
+
*/
|
|
2845
|
+
var ListAppComponentsTool = class ListAppComponentsTool {
|
|
2846
|
+
static TOOL_NAME = "list_app_components";
|
|
2847
|
+
static COMPONENT_REUSE_INSTRUCTION = "IMPORTANT: Before creating new components, check if a similar component already exists in the list above. Always reuse existing components to maintain consistency and reduce code duplication.";
|
|
2848
|
+
service;
|
|
2849
|
+
constructor() {
|
|
2850
|
+
this.service = new AppComponentsService();
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Gets the tool definition including name, description, and input schema.
|
|
2854
|
+
* @returns Tool definition for MCP registration
|
|
2855
|
+
*/
|
|
2856
|
+
getDefinition() {
|
|
2857
|
+
return {
|
|
2858
|
+
name: ListAppComponentsTool.TOOL_NAME,
|
|
2859
|
+
description: "List app-specific components and package components used by an app. Call this tool BEFORE creating new components to check if a similar component already exists. Reads the app's package.json to find workspace dependencies and returns components from both the app and its dependent packages.",
|
|
2860
|
+
inputSchema: {
|
|
2861
|
+
type: "object",
|
|
2862
|
+
properties: {
|
|
2863
|
+
appPath: {
|
|
2864
|
+
type: "string",
|
|
2865
|
+
description: "The app path (relative or absolute) to list components for (e.g., \"apps/my-app\")",
|
|
2866
|
+
minLength: 1
|
|
2867
|
+
},
|
|
2868
|
+
cursor: {
|
|
2869
|
+
type: "string",
|
|
2870
|
+
description: "Optional pagination cursor to fetch the next page of results. Omit to fetch the first page."
|
|
2871
|
+
}
|
|
2872
|
+
},
|
|
2873
|
+
required: ["appPath"],
|
|
2874
|
+
additionalProperties: false
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Lists app-specific and package components for a given application.
|
|
2880
|
+
* @param input - Object containing appPath and optional cursor for pagination
|
|
2881
|
+
* @returns CallToolResult with component list or error
|
|
2882
|
+
*/
|
|
2883
|
+
async execute(input) {
|
|
2884
|
+
try {
|
|
2885
|
+
const result = await this.service.listComponents({
|
|
2886
|
+
appPath: input.appPath,
|
|
2887
|
+
cursor: input.cursor
|
|
2888
|
+
});
|
|
2889
|
+
return { content: [{
|
|
2890
|
+
type: "text",
|
|
2891
|
+
text: `${ListAppComponentsTool.COMPONENT_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
|
|
2892
|
+
}] };
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
return {
|
|
2895
|
+
content: [{
|
|
2896
|
+
type: "text",
|
|
2897
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2898
|
+
}],
|
|
2899
|
+
isError: true
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
|
|
2905
|
+
//#endregion
|
|
2906
|
+
//#region src/tools/ListThemesTool.ts
|
|
2907
|
+
/**
|
|
2908
|
+
* Tool to list all available theme configurations.
|
|
2909
|
+
*
|
|
2910
|
+
* Reads themes from CSS files configured in the app's project.json
|
|
2911
|
+
* style-system config. Themes are extracted by parsing CSS class selectors
|
|
2912
|
+
* that contain color variable definitions.
|
|
2913
|
+
*
|
|
2914
|
+
* @example
|
|
2915
|
+
* ```typescript
|
|
2916
|
+
* const tool = new ListThemesTool();
|
|
2917
|
+
* const result = await tool.execute({ appPath: 'apps/my-app' });
|
|
2918
|
+
* // Returns: { themes: [{ name: 'slate', ... }, { name: 'blue', ... }], source: 'css-file' }
|
|
2919
|
+
* ```
|
|
2920
|
+
*/
|
|
2921
|
+
var ListThemesTool = class ListThemesTool {
|
|
2922
|
+
static TOOL_NAME = "list_themes";
|
|
2923
|
+
serviceFactory;
|
|
2924
|
+
constructor() {
|
|
2925
|
+
this.serviceFactory = new ThemeServiceFactory();
|
|
2926
|
+
}
|
|
2927
|
+
getDefinition() {
|
|
2928
|
+
return {
|
|
2929
|
+
name: ListThemesTool.TOOL_NAME,
|
|
2930
|
+
description: "List all available theme configurations. Reads themes from CSS files configured in the app's project.json style-system config.",
|
|
2931
|
+
inputSchema: {
|
|
2932
|
+
type: "object",
|
|
2933
|
+
properties: { appPath: {
|
|
2934
|
+
type: "string",
|
|
2935
|
+
description: "App path (relative or absolute) to read theme config from project.json (e.g., \"apps/my-app\")"
|
|
2936
|
+
} },
|
|
2937
|
+
additionalProperties: false
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
async execute(input) {
|
|
2942
|
+
try {
|
|
2943
|
+
let themePath;
|
|
2944
|
+
let cssFiles;
|
|
2945
|
+
if (input.appPath) {
|
|
2946
|
+
const appConfig = await getAppDesignSystemConfig(input.appPath);
|
|
2947
|
+
themePath = appConfig.themePath;
|
|
2948
|
+
cssFiles = appConfig.cssFiles;
|
|
2949
|
+
}
|
|
2950
|
+
const result = await (await this.serviceFactory.createService({
|
|
2951
|
+
themePath,
|
|
2952
|
+
cssFiles
|
|
2953
|
+
})).listThemes();
|
|
2954
|
+
return { content: [{
|
|
2955
|
+
type: "text",
|
|
2956
|
+
text: JSON.stringify(result, null, 2)
|
|
2957
|
+
}] };
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
return {
|
|
2960
|
+
content: [{
|
|
2961
|
+
type: "text",
|
|
2962
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2963
|
+
}],
|
|
2964
|
+
isError: true
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
//#endregion
|
|
2971
|
+
//#region src/tools/ListSharedComponentsTool.ts
|
|
2972
|
+
var ListSharedComponentsTool = class ListSharedComponentsTool {
|
|
2973
|
+
static TOOL_NAME = "list_shared_components";
|
|
2974
|
+
static PAGE_SIZE = 50;
|
|
2975
|
+
static COMPONENT_REUSE_INSTRUCTION = "IMPORTANT: Before creating new components, check if a similar component already exists in the list above. Always reuse existing shared components to maintain consistency and reduce code duplication.";
|
|
2976
|
+
/**
|
|
2977
|
+
* Encode pagination state into an opaque cursor string
|
|
2978
|
+
* @param offset - The current offset in the component list
|
|
2979
|
+
* @returns Base64 encoded cursor string
|
|
2980
|
+
*/
|
|
2981
|
+
encodeCursor(offset) {
|
|
2982
|
+
return Buffer.from(JSON.stringify({ offset })).toString("base64");
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Decode cursor string into pagination state
|
|
2986
|
+
* @param cursor - Base64 encoded cursor string
|
|
2987
|
+
* @returns Object containing the offset
|
|
2988
|
+
*/
|
|
2989
|
+
decodeCursor(cursor) {
|
|
2990
|
+
try {
|
|
2991
|
+
const decoded = Buffer.from(cursor, "base64").toString("utf-8");
|
|
2992
|
+
return { offset: JSON.parse(decoded).offset || 0 };
|
|
2993
|
+
} catch {
|
|
2994
|
+
return { offset: 0 };
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Gets the tool definition including name, description, and input schema.
|
|
2999
|
+
* @returns Tool definition for MCP registration
|
|
3000
|
+
*/
|
|
3001
|
+
getDefinition() {
|
|
3002
|
+
return {
|
|
3003
|
+
name: ListSharedComponentsTool.TOOL_NAME,
|
|
3004
|
+
description: "List shared UI components from the design system. Call this tool BEFORE creating new components to check if a similar component already exists. Use the \"tags\" parameter to filter by specific tags. If no tags provided, uses configured sharedComponentTags from toolkit.yaml (default: style-system). The response includes \"availableTags\" showing all tags in the codebase for filtering.",
|
|
3005
|
+
inputSchema: {
|
|
3006
|
+
type: "object",
|
|
3007
|
+
properties: {
|
|
3008
|
+
tags: {
|
|
3009
|
+
type: "array",
|
|
3010
|
+
items: { type: "string" },
|
|
3011
|
+
description: "Optional array of tags to filter components by. If not provided, uses configured sharedComponentTags from toolkit.yaml. Pass an empty array [] to list ALL components."
|
|
3012
|
+
},
|
|
3013
|
+
cursor: {
|
|
3014
|
+
type: "string",
|
|
3015
|
+
description: "Optional pagination cursor to fetch the next page of results. Omit to fetch the first page."
|
|
3016
|
+
}
|
|
3017
|
+
},
|
|
3018
|
+
additionalProperties: false
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Lists shared UI components from the design system.
|
|
3024
|
+
* @param input - Object containing optional tags filter and cursor for pagination
|
|
3025
|
+
* @returns CallToolResult with component list or error
|
|
3026
|
+
*/
|
|
3027
|
+
async execute(input) {
|
|
3028
|
+
try {
|
|
3029
|
+
const { cursor, tags: inputTags } = input;
|
|
3030
|
+
const { offset } = cursor ? this.decodeCursor(cursor) : { offset: 0 };
|
|
3031
|
+
const storiesIndex = new StoriesIndexService();
|
|
3032
|
+
await storiesIndex.initialize();
|
|
3033
|
+
const availableTags = storiesIndex.getAllTags();
|
|
3034
|
+
let filterTags;
|
|
3035
|
+
if (inputTags !== void 0) filterTags = inputTags;
|
|
3036
|
+
else filterTags = await getSharedComponentTags();
|
|
3037
|
+
const allComponentNames = storiesIndex.getComponentsByTags(filterTags.length > 0 ? filterTags : void 0).map((component) => component.title.split("/").pop() || component.title).filter((name, index, self) => self.indexOf(name) === index).sort();
|
|
3038
|
+
const totalComponents = allComponentNames.length;
|
|
3039
|
+
const paginatedComponents = allComponentNames.slice(offset, offset + ListSharedComponentsTool.PAGE_SIZE);
|
|
3040
|
+
const hasMore = offset + paginatedComponents.length < totalComponents;
|
|
3041
|
+
const result = {
|
|
3042
|
+
filteredByTags: filterTags,
|
|
3043
|
+
availableTags,
|
|
3044
|
+
components: paginatedComponents,
|
|
3045
|
+
pagination: {
|
|
3046
|
+
offset,
|
|
3047
|
+
pageSize: ListSharedComponentsTool.PAGE_SIZE,
|
|
3048
|
+
totalComponents,
|
|
3049
|
+
hasMore
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
if (hasMore) result.nextCursor = this.encodeCursor(offset + paginatedComponents.length);
|
|
3053
|
+
__agiflowai_aicode_utils.log.info(`[ListSharedComponentsTool] Tags: [${filterTags.join(", ")}], Page ${Math.floor(offset / ListSharedComponentsTool.PAGE_SIZE) + 1}: Returned ${paginatedComponents.length} of ${totalComponents} total components (hasMore: ${hasMore})`);
|
|
3054
|
+
return { content: [{
|
|
3055
|
+
type: "text",
|
|
3056
|
+
text: `${ListSharedComponentsTool.COMPONENT_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
|
|
3057
|
+
}] };
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
return {
|
|
3060
|
+
content: [{
|
|
3061
|
+
type: "text",
|
|
3062
|
+
text: `Failed to list shared components: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3063
|
+
}],
|
|
3064
|
+
isError: true
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
3069
|
+
|
|
3070
|
+
//#endregion
|
|
3071
|
+
//#region src/server/index.ts
|
|
3072
|
+
function createServer(themePath = "packages/frontend/web-theme/src/agimon-theme.css") {
|
|
3073
|
+
const server = new __modelcontextprotocol_sdk_server_index_js.Server({
|
|
3074
|
+
name: "style-system-mcp",
|
|
3075
|
+
version: "0.1.0"
|
|
3076
|
+
}, {
|
|
3077
|
+
capabilities: { tools: {} },
|
|
3078
|
+
instructions: "Use this MCP when you work on the frontend or design. You can use this to list supported themes, get the correct tailwind classes supported by the repo, list design system components, and get visual + detailed implementation of components."
|
|
3079
|
+
});
|
|
3080
|
+
const listThemesTool = new ListThemesTool();
|
|
3081
|
+
const getCSSClassesTool = new GetCSSClassesTool(themePath);
|
|
3082
|
+
const getComponentVisualTool = new GetComponentVisualTool();
|
|
3083
|
+
const listSharedComponentsTool = new ListSharedComponentsTool();
|
|
3084
|
+
const listAppComponentsTool = new ListAppComponentsTool();
|
|
3085
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => {
|
|
3086
|
+
return { tools: [
|
|
3087
|
+
listThemesTool.getDefinition(),
|
|
3088
|
+
getCSSClassesTool.getDefinition(),
|
|
3089
|
+
getComponentVisualTool.getDefinition(),
|
|
3090
|
+
listSharedComponentsTool.getDefinition(),
|
|
3091
|
+
listAppComponentsTool.getDefinition()
|
|
3092
|
+
] };
|
|
3093
|
+
});
|
|
3094
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
3095
|
+
const { name, arguments: args } = request.params;
|
|
3096
|
+
if (name === ListThemesTool.TOOL_NAME) return await listThemesTool.execute({});
|
|
3097
|
+
if (name === GetCSSClassesTool.TOOL_NAME) return await getCSSClassesTool.execute(args);
|
|
3098
|
+
if (name === GetComponentVisualTool.TOOL_NAME) return await getComponentVisualTool.execute(args);
|
|
3099
|
+
if (name === ListSharedComponentsTool.TOOL_NAME) return await listSharedComponentsTool.execute({});
|
|
3100
|
+
if (name === ListAppComponentsTool.TOOL_NAME) return await listAppComponentsTool.execute(args);
|
|
3101
|
+
return {
|
|
3102
|
+
content: [{
|
|
3103
|
+
type: "text",
|
|
3104
|
+
text: `Error: Unknown tool: ${name}`
|
|
3105
|
+
}],
|
|
3106
|
+
isError: true
|
|
3107
|
+
};
|
|
3108
|
+
});
|
|
3109
|
+
return server;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
//#endregion
|
|
3113
|
+
//#region src/transports/stdio.ts
|
|
3114
|
+
/**
|
|
3115
|
+
* Stdio transport handler for MCP server
|
|
3116
|
+
* Used for command-line and direct integrations
|
|
3117
|
+
*/
|
|
3118
|
+
var StdioTransportHandler = class {
|
|
3119
|
+
server;
|
|
3120
|
+
transport = null;
|
|
3121
|
+
constructor(server) {
|
|
3122
|
+
this.server = server;
|
|
3123
|
+
}
|
|
3124
|
+
async start() {
|
|
3125
|
+
this.transport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
3126
|
+
await this.server.connect(this.transport);
|
|
3127
|
+
__agiflowai_aicode_utils.log.info("style-system-mcp MCP server started on stdio");
|
|
3128
|
+
}
|
|
3129
|
+
async stop() {
|
|
3130
|
+
if (this.transport) {
|
|
3131
|
+
await this.transport.close();
|
|
3132
|
+
this.transport = null;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
};
|
|
3136
|
+
|
|
3137
|
+
//#endregion
|
|
3138
|
+
Object.defineProperty(exports, 'BaseBundlerService', {
|
|
3139
|
+
enumerable: true,
|
|
3140
|
+
get: function () {
|
|
3141
|
+
return BaseBundlerService;
|
|
3142
|
+
}
|
|
3143
|
+
});
|
|
3144
|
+
Object.defineProperty(exports, 'BaseCSSClassesService', {
|
|
3145
|
+
enumerable: true,
|
|
3146
|
+
get: function () {
|
|
3147
|
+
return BaseCSSClassesService;
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
Object.defineProperty(exports, 'CSSClassesServiceFactory', {
|
|
3151
|
+
enumerable: true,
|
|
3152
|
+
get: function () {
|
|
3153
|
+
return CSSClassesServiceFactory;
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
Object.defineProperty(exports, 'ComponentRendererService', {
|
|
3157
|
+
enumerable: true,
|
|
3158
|
+
get: function () {
|
|
3159
|
+
return ComponentRendererService;
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
Object.defineProperty(exports, 'DEFAULT_STYLE_SYSTEM_CONFIG', {
|
|
3163
|
+
enumerable: true,
|
|
3164
|
+
get: function () {
|
|
3165
|
+
return DEFAULT_STYLE_SYSTEM_CONFIG;
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
Object.defineProperty(exports, 'GetCSSClassesTool', {
|
|
3169
|
+
enumerable: true,
|
|
3170
|
+
get: function () {
|
|
3171
|
+
return GetCSSClassesTool;
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
Object.defineProperty(exports, 'GetComponentVisualTool', {
|
|
3175
|
+
enumerable: true,
|
|
3176
|
+
get: function () {
|
|
3177
|
+
return GetComponentVisualTool;
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
3180
|
+
Object.defineProperty(exports, 'ListAppComponentsTool', {
|
|
3181
|
+
enumerable: true,
|
|
3182
|
+
get: function () {
|
|
3183
|
+
return ListAppComponentsTool;
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
Object.defineProperty(exports, 'ListSharedComponentsTool', {
|
|
3187
|
+
enumerable: true,
|
|
3188
|
+
get: function () {
|
|
3189
|
+
return ListSharedComponentsTool;
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
Object.defineProperty(exports, 'ListThemesTool', {
|
|
3193
|
+
enumerable: true,
|
|
3194
|
+
get: function () {
|
|
3195
|
+
return ListThemesTool;
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3198
|
+
Object.defineProperty(exports, 'StdioTransportHandler', {
|
|
3199
|
+
enumerable: true,
|
|
3200
|
+
get: function () {
|
|
3201
|
+
return StdioTransportHandler;
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
Object.defineProperty(exports, 'StoriesIndexService', {
|
|
3205
|
+
enumerable: true,
|
|
3206
|
+
get: function () {
|
|
3207
|
+
return StoriesIndexService;
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
Object.defineProperty(exports, 'TailwindCSSClassesService', {
|
|
3211
|
+
enumerable: true,
|
|
3212
|
+
get: function () {
|
|
3213
|
+
return TailwindCSSClassesService;
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
Object.defineProperty(exports, 'ThemeService', {
|
|
3217
|
+
enumerable: true,
|
|
3218
|
+
get: function () {
|
|
3219
|
+
return ThemeService;
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
Object.defineProperty(exports, 'ViteReactBundlerService', {
|
|
3223
|
+
enumerable: true,
|
|
3224
|
+
get: function () {
|
|
3225
|
+
return ViteReactBundlerService;
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
Object.defineProperty(exports, '__toESM', {
|
|
3229
|
+
enumerable: true,
|
|
3230
|
+
get: function () {
|
|
3231
|
+
return __toESM;
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
Object.defineProperty(exports, 'createDefaultBundlerService', {
|
|
3235
|
+
enumerable: true,
|
|
3236
|
+
get: function () {
|
|
3237
|
+
return createDefaultBundlerService;
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
Object.defineProperty(exports, 'createServer', {
|
|
3241
|
+
enumerable: true,
|
|
3242
|
+
get: function () {
|
|
3243
|
+
return createServer;
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
Object.defineProperty(exports, 'getBundlerServiceFromConfig', {
|
|
3247
|
+
enumerable: true,
|
|
3248
|
+
get: function () {
|
|
3249
|
+
return getBundlerServiceFromConfig;
|
|
3250
|
+
}
|
|
3251
|
+
});
|