@cmssy/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,106 +1,31 @@
1
1
  import chalk from "chalk";
2
- import { exec } from "child_process";
3
- import express from "express";
2
+ import { spawn } from "child_process";
4
3
  import fs from "fs-extra";
5
- import { GraphQLClient } from "graphql-request";
6
4
  import ora from "ora";
7
5
  import path from "path";
8
- import { fileURLToPath } from "url";
9
- import { createServer as createViteServer } from "vite";
10
- import react from "@vitejs/plugin-react";
11
- import tailwindcss from "@tailwindcss/postcss";
12
- // Custom Vite plugin to resolve @import "main.css" from styles folder
13
- // Inlines the imported CSS content to avoid Vite's postcss-import issues
14
- function cmssyStylesImportPlugin(projectRoot) {
15
- return {
16
- name: "cmssy-styles-import",
17
- enforce: "pre",
18
- async load(id) {
19
- // Strip query params for matching
20
- const cleanId = id.split("?")[0];
21
- // Only process CSS files in blocks/templates
22
- if (!cleanId.endsWith(".css"))
23
- return null;
24
- if (!cleanId.includes("/blocks/") && !cleanId.includes("/templates/"))
25
- return null;
26
- const content = await fs.readFile(cleanId, "utf-8");
27
- // Check if it has @import "main.css" or similar simple imports
28
- if (!content.includes('@import "') && !content.includes("@import '")) {
29
- return null;
30
- }
31
- // Replace @import "filename.css" with the actual file content (inline it)
32
- const stylesDir = path.join(projectRoot, "styles");
33
- let transformed = content;
34
- // Match @import "filename.css" or @import 'filename.css' (without path)
35
- const importRegex = /@import\s+["']([^"'\/]+\.css)["']\s*;/g;
36
- transformed = transformed.replace(importRegex, (match, filename) => {
37
- const fullPath = path.join(stylesDir, filename);
38
- if (fs.existsSync(fullPath)) {
39
- // Inline the CSS content instead of keeping the import
40
- const importedContent = fs.readFileSync(fullPath, "utf-8");
41
- return `/* Inlined from ${filename} */\n${importedContent}`;
42
- }
43
- return match; // Keep original if file doesn't exist
44
- });
45
- if (transformed !== content) {
46
- return transformed;
47
- }
48
- return null;
49
- },
50
- };
51
- }
52
- import { loadBlockConfig, validateSchema as validateBlockSchema } from "../utils/block-config.js";
53
- import { loadMetaCache, updateBlockInCache } from "../utils/blocks-meta-cache.js";
54
- import { isTemplateConfig } from "../types/block-config.js";
55
- import { loadConfig } from "../utils/cmssy-config.js";
56
- import { loadConfig as loadEnvConfig } from "../utils/config.js";
57
- import { getFieldTypes } from "../utils/field-schema.js";
6
+ import { generateDevApp, regeneratePreviewPages } from "../utils/dev-generator.js";
7
+ import { loadMetaCache } from "../utils/blocks-meta-cache.js";
58
8
  import { scanResources } from "../utils/scanner.js";
59
- import { generateTypes } from "../utils/type-generator.js";
60
- // Merge default values from schema into preview data
61
- // Preview data values take precedence over defaults
62
- function mergeDefaultsWithPreview(schema, previewData) {
63
- const merged = { ...previewData };
64
- for (const [key, field] of Object.entries(schema)) {
65
- // If field is missing or undefined, use defaultValue
66
- if (merged[key] === undefined || merged[key] === null) {
67
- if (field.defaultValue !== undefined) {
68
- merged[key] = field.defaultValue;
69
- }
70
- else if (field.type === "repeater") {
71
- // Repeaters default to empty array if no defaultValue
72
- merged[key] = [];
73
- }
74
- }
75
- // For repeaters with items, merge nested defaults
76
- if (field.type === "repeater" && field.schema && Array.isArray(merged[key])) {
77
- merged[key] = merged[key].map((item) => {
78
- const mergedItem = { ...item };
79
- for (const [nestedKey, nestedField] of Object.entries(field.schema)) {
80
- // Add default value if missing
81
- if (mergedItem[nestedKey] === undefined && nestedField.defaultValue !== undefined) {
82
- mergedItem[nestedKey] = nestedField.defaultValue;
83
- }
84
- }
85
- return mergedItem;
86
- });
87
- }
88
- }
89
- return merged;
90
- }
91
9
  export async function devCommand(options) {
92
- const spinner = ora("Starting development server...").start();
10
+ const spinner = ora("Starting Next.js dev server...").start();
93
11
  try {
94
- const config = await loadConfig();
95
- const port = parseInt(options.port, 10);
96
12
  const projectRoot = process.cwd();
97
- // Scan for blocks and templates - FAST: no config loading at startup
13
+ const port = parseInt(options.port, 10);
14
+ // Ensure next.config exists
15
+ const hasNextConfig = fs.existsSync(path.join(projectRoot, "next.config.mjs")) ||
16
+ fs.existsSync(path.join(projectRoot, "next.config.js")) ||
17
+ fs.existsSync(path.join(projectRoot, "next.config.ts"));
18
+ if (!hasNextConfig) {
19
+ spinner.fail("No next.config found. Run 'cmssy init' to create a new project.");
20
+ process.exit(1);
21
+ }
22
+ // Scan blocks
98
23
  spinner.text = "Scanning blocks...";
99
24
  const resources = await scanResources({
100
25
  strict: false,
101
- loadConfig: false, // Lazy load configs when needed
26
+ loadConfig: false,
102
27
  validateSchema: false,
103
- loadPreview: false, // Lazy load preview data
28
+ loadPreview: false,
104
29
  requirePackageJson: false,
105
30
  });
106
31
  if (resources.length === 0) {
@@ -109,851 +34,87 @@ export async function devCommand(options) {
109
34
  console.log(chalk.white(" npx cmssy create block my-block\n"));
110
35
  process.exit(0);
111
36
  }
112
- // Load metadata cache for instant filters
113
- spinner.text = "Loading metadata cache...";
37
+ // Load metadata cache
114
38
  const metaCache = loadMetaCache(projectRoot);
115
- let cachedCount = 0;
116
- // Merge cached metadata into resources
117
39
  resources.forEach((r) => {
118
40
  const cached = metaCache.blocks[r.name];
119
41
  if (cached) {
120
42
  r.category = cached.category;
121
43
  r.displayName = cached.displayName || r.name;
122
44
  r.description = cached.description;
123
- // Store tags in a temp property for API
124
- r.cachedTags = cached.tags;
125
- cachedCount++;
126
45
  }
127
46
  });
128
- if (cachedCount > 0) {
129
- spinner.text = `Loaded ${cachedCount} blocks from cache`;
130
- }
131
- // Fetch field types from backend (used for type generation)
132
- spinner.text = "Fetching field types...";
133
- let fieldTypes = [];
134
- try {
135
- fieldTypes = await getFieldTypes();
136
- }
137
- catch (error) {
138
- // Will use fallback types if backend is unreachable
139
- }
140
- spinner.text = "Starting Vite server...";
141
- // Dev UI paths (must be before Vite config)
142
- const __filename = fileURLToPath(import.meta.url);
143
- const __dirname = path.dirname(__filename);
144
- const devUiReactPath = path.join(__dirname, "../dev-ui-react");
145
- // Create Express app for API routes
146
- const app = express();
147
- app.use(express.json());
148
- // Create Vite server in middleware mode
149
- const vite = await createViteServer({
150
- root: projectRoot,
151
- server: {
152
- middlewareMode: true,
153
- hmr: { port: port + 1 },
154
- fs: {
155
- // Allow serving files from cmssy-cli package (dev-ui-react)
156
- allow: [projectRoot, path.dirname(__dirname)],
157
- },
158
- },
159
- appType: "custom",
160
- plugins: [cmssyStylesImportPlugin(projectRoot), react()],
161
- resolve: {
162
- alias: [
163
- // React packages must resolve from user's project, not cmssy-cli
164
- { find: "react", replacement: path.join(projectRoot, "node_modules/react") },
165
- { find: "react-dom", replacement: path.join(projectRoot, "node_modules/react-dom") },
166
- // Common @ alias for project root (shadcn/ui convention)
167
- { find: /^@\/(.*)/, replacement: path.join(projectRoot, "$1") },
168
- { find: "@blocks", replacement: path.join(projectRoot, "blocks") },
169
- { find: "@templates", replacement: path.join(projectRoot, "templates") },
170
- { find: "@styles", replacement: path.join(projectRoot, "styles") },
171
- { find: "@lib", replacement: path.join(projectRoot, "lib") },
172
- // Handle relative imports to lib from any depth
173
- { find: /^(\.\.\/)+lib/, replacement: path.join(projectRoot, "lib") },
174
- // Serve dev UI React files from cmssy-cli package
175
- { find: /^\/dev-ui-react\/(.*)/, replacement: path.join(devUiReactPath, "$1") },
176
- ],
47
+ // Generate the .cmssy/dev/ Next.js app
48
+ spinner.text = "Generating Next.js dev app...";
49
+ const devRoot = generateDevApp(projectRoot, resources);
50
+ // Symlink node_modules from project root into dev app
51
+ // so Next.js can resolve react, react-dom, next, etc.
52
+ const devNodeModules = path.join(devRoot, "node_modules");
53
+ const projectNodeModules = path.join(projectRoot, "node_modules");
54
+ if (!fs.existsSync(devNodeModules) && fs.existsSync(projectNodeModules)) {
55
+ fs.symlinkSync(projectNodeModules, devNodeModules, "junction");
56
+ }
57
+ // Find next binary from project's node_modules
58
+ const nextBin = path.join(projectRoot, "node_modules/.bin/next");
59
+ if (!fs.existsSync(nextBin)) {
60
+ spinner.fail("'next' not found in node_modules. Run: npm install next");
61
+ process.exit(1);
62
+ }
63
+ spinner.succeed("Next.js dev app generated");
64
+ console.log(chalk.green.bold("\n─────────────────────────────────────────"));
65
+ console.log(chalk.green.bold(" Cmssy Dev Server (Next.js)"));
66
+ console.log(chalk.green.bold("─────────────────────────────────────────\n"));
67
+ const blocks = resources.filter((r) => r.type === "block");
68
+ const templates = resources.filter((r) => r.type === "template");
69
+ console.log(chalk.cyan(` ${blocks.length} blocks, ${templates.length} templates`));
70
+ console.log(chalk.green(`\n Local: ${chalk.cyan(`http://localhost:${port}`)}`));
71
+ console.log(chalk.green(" Next.js Fast Refresh enabled"));
72
+ console.log(chalk.green(" Press Ctrl+C to stop"));
73
+ console.log(chalk.green.bold("\n─────────────────────────────────────────\n"));
74
+ // Spawn next dev from project root so the project's own PostCSS config,
75
+ // Tailwind setup, and node_modules resolution all work naturally.
76
+ // The dev app directory is passed as argument to next dev.
77
+ const nextProcess = spawn(nextBin, ["dev", devRoot, "--port", String(port)], {
78
+ cwd: projectRoot,
79
+ stdio: "inherit",
80
+ env: {
81
+ ...process.env,
82
+ CMSSY_PROJECT_ROOT: projectRoot,
177
83
  },
178
- css: {
179
- postcss: {
180
- plugins: [tailwindcss()],
181
- },
182
- },
183
- optimizeDeps: {
184
- include: ["react", "react-dom", "framer-motion"],
185
- },
186
- });
187
- // API: Get all blocks (uses cache for instant filters)
188
- app.get("/api/blocks", (_req, res) => {
189
- const blockList = resources.map((r) => ({
190
- type: r.type,
191
- name: r.name,
192
- displayName: r.displayName || r.name,
193
- version: r.packageJson?.version || "1.0.0",
194
- // Use cached or loaded metadata
195
- category: r.blockConfig?.category || r.category || "other",
196
- tags: r.blockConfig?.tags || r.cachedTags || [],
197
- description: r.blockConfig?.description || r.description,
198
- hasConfig: !!r.blockConfig,
199
- }));
200
- res.json(blockList);
201
- });
202
- // API: Lazy load block config (called when block is selected)
203
- app.get("/api/blocks/:name/config", async (req, res) => {
204
- const { name } = req.params;
205
- const resource = resources.find((r) => r.name === name);
206
- if (!resource) {
207
- res.status(404).json({ error: "Block not found" });
208
- return;
209
- }
210
- // Load config if not already loaded
211
- if (!resource.blockConfig) {
212
- try {
213
- const blockConfig = await loadBlockConfig(resource.path);
214
- if (blockConfig) {
215
- // Validate schema
216
- if (blockConfig.schema) {
217
- const validation = await validateBlockSchema(blockConfig.schema, resource.path);
218
- if (!validation.valid) {
219
- console.log(chalk.yellow(`\n⚠️ Schema warnings for ${name}:`));
220
- validation.errors.forEach((err) => console.log(chalk.yellow(` • ${err}`)));
221
- }
222
- }
223
- resource.blockConfig = blockConfig;
224
- resource.displayName = blockConfig.name || resource.name;
225
- resource.description = blockConfig.description;
226
- resource.category = blockConfig.category;
227
- // Update metadata cache
228
- updateBlockInCache(name, resource.type, blockConfig, resource.packageJson?.version, projectRoot);
229
- }
230
- }
231
- catch (error) {
232
- console.log(chalk.red(`\n❌ Failed to load config for ${name}: ${error.message}`));
233
- res.status(500).json({ error: error.message });
234
- return;
235
- }
236
- }
237
- // Always load preview data fresh from file (don't use stale cache)
238
- const previewPath = path.join(resource.path, "preview.json");
239
- if (fs.existsSync(previewPath)) {
240
- resource.previewData = fs.readJsonSync(previewPath);
241
- }
242
- else {
243
- resource.previewData = {};
244
- }
245
- const cfg = resource.blockConfig;
246
- // Merge default values from schema into previewData (preview.json values take precedence)
247
- const mergedPreviewData = mergeDefaultsWithPreview(cfg?.schema || {}, resource.previewData || {});
248
- // Build response with template-specific fields if applicable
249
- const response = {
250
- name: resource.name,
251
- displayName: cfg?.name || resource.displayName || resource.name,
252
- description: cfg?.description || resource.description,
253
- category: cfg?.category || "other",
254
- tags: cfg?.tags || [],
255
- schema: cfg?.schema || {},
256
- previewData: mergedPreviewData,
257
- version: resource.packageJson?.version || "1.0.0",
258
- };
259
- // Add template-specific fields
260
- if (cfg && isTemplateConfig(cfg)) {
261
- response.pages = cfg.pages;
262
- response.layoutPositions = cfg.layoutPositions || [];
263
- }
264
- res.json(response);
265
- });
266
- // API: Get user's workspaces
267
- app.get("/api/workspaces", async (_req, res) => {
268
- try {
269
- const envConfig = loadEnvConfig();
270
- if (!envConfig.apiToken) {
271
- res.status(401).json({
272
- error: "API token not configured",
273
- message: "Run 'cmssy configure' to set up your API credentials",
274
- });
275
- return;
276
- }
277
- const client = new GraphQLClient(envConfig.apiUrl, {
278
- headers: {
279
- "Content-Type": "application/json",
280
- Authorization: `Bearer ${envConfig.apiToken}`,
281
- },
282
- });
283
- const query = `
284
- query MyWorkspaces {
285
- myWorkspaces {
286
- id
287
- slug
288
- name
289
- myRole { name slug }
290
- }
291
- }
292
- `;
293
- const data = await client.request(query);
294
- res.json(data.myWorkspaces || []);
295
- }
296
- catch (error) {
297
- console.error("Failed to fetch workspaces:", error);
298
- res.status(500).json({
299
- error: "Failed to fetch workspaces",
300
- message: error.message || "Unknown error",
301
- });
302
- }
303
- });
304
- // API: Get preview data for a block (lazy loads if needed)
305
- app.get("/api/preview/:blockName", (req, res) => {
306
- const { blockName } = req.params;
307
- const resource = resources.find((r) => r.name === blockName);
308
- if (!resource) {
309
- res.status(404).json({ error: "Block not found" });
310
- return;
311
- }
312
- // Always load preview data fresh from file
313
- const previewPath = path.join(resource.path, "preview.json");
314
- if (fs.existsSync(previewPath)) {
315
- resource.previewData = fs.readJsonSync(previewPath);
316
- }
317
- else {
318
- resource.previewData = {};
319
- }
320
- res.json(resource.previewData);
321
- });
322
- // API: Save preview data for a block
323
- app.post("/api/preview/:blockName", (req, res) => {
324
- const { blockName } = req.params;
325
- const newPreviewData = req.body;
326
- const resource = resources.find((r) => r.name === blockName);
327
- if (!resource) {
328
- res.status(404).json({ error: "Block not found" });
329
- return;
330
- }
331
- resource.previewData = newPreviewData;
332
- const previewPath = path.join(resource.path, "preview.json");
333
- try {
334
- fs.writeJsonSync(previewPath, newPreviewData, { spaces: 2 });
335
- res.json({ success: true });
336
- }
337
- catch (error) {
338
- res.status(500).json({ error: error.message });
339
- }
340
- });
341
- // API: Get published version from backend
342
- app.get("/api/blocks/:name/published-version", async (req, res) => {
343
- const { name } = req.params;
344
- const { workspaceId } = req.query;
345
- const resource = resources.find((r) => r.name === name);
346
- if (!resource) {
347
- res.status(404).json({ error: "Block not found" });
348
- return;
349
- }
350
- if (!workspaceId) {
351
- res.status(400).json({ error: "workspaceId is required" });
352
- return;
353
- }
354
- try {
355
- const envConfig = loadEnvConfig();
356
- if (!envConfig.apiToken) {
357
- res.json({ version: null, published: false });
358
- return;
359
- }
360
- const client = new GraphQLClient(envConfig.apiUrl, {
361
- headers: {
362
- Authorization: `Bearer ${envConfig.apiToken}`,
363
- "x-workspace-id": workspaceId,
364
- },
365
- });
366
- const packageName = resource.packageJson?.name || "";
367
- const blockType = packageName.split(".").pop() || name;
368
- const query = `
369
- query GetPublishedVersion($blockType: String!) {
370
- workspaceBlockByType(blockType: $blockType) { version }
371
- }
372
- `;
373
- const data = await client.request(query, { blockType });
374
- const publishedVersion = data.workspaceBlockByType?.version || null;
375
- res.json({ version: publishedVersion, published: publishedVersion !== null });
376
- }
377
- catch (error) {
378
- res.json({ version: null, published: false, error: error.message });
379
- }
380
- });
381
- // API: Get block publish status
382
- app.get("/api/blocks/:name/status", (req, res) => {
383
- const { name } = req.params;
384
- const resource = resources.find((r) => r.name === name);
385
- if (!resource) {
386
- res.status(404).json({ error: "Block not found" });
387
- return;
388
- }
389
- res.json({
390
- name: resource.name,
391
- version: resource.packageJson?.version || "1.0.0",
392
- packageName: resource.packageJson?.name || `@local/${resource.type}s.${resource.name}`,
393
- published: false,
394
- lastPublished: null,
395
- });
396
- });
397
- // API: Publish block
398
- app.post("/api/blocks/:name/publish", async (req, res) => {
399
- const { name } = req.params;
400
- const { target, workspaceId, versionBump } = req.body;
401
- const resource = resources.find((r) => r.name === name);
402
- if (!resource) {
403
- res.status(404).json({ error: "Block not found" });
404
- return;
405
- }
406
- if (!target || target !== "workspace") {
407
- res.status(400).json({ error: "Invalid target. Only 'workspace' is supported." });
408
- return;
409
- }
410
- if (!workspaceId) {
411
- res.status(400).json({ error: "Workspace ID required" });
412
- return;
413
- }
414
- const args = ["publish", resource.name, "--workspace", workspaceId];
415
- if (versionBump && versionBump !== "none") {
416
- args.push(`--${versionBump}`);
417
- }
418
- else {
419
- args.push("--no-bump");
420
- }
421
- const command = `cmssy ${args.join(" ")}`;
422
- console.log("[PUBLISH] Executing:", command);
423
- exec(command, {
424
- cwd: projectRoot,
425
- timeout: 60000,
426
- maxBuffer: 10 * 1024 * 1024,
427
- env: { ...process.env, CI: "true", FORCE_COLOR: "0", NO_COLOR: "1" },
428
- }, (error, stdout, stderr) => {
429
- const output = `${stdout}\n${stderr}`;
430
- const success = output.includes("published successfully") ||
431
- output.includes("published to workspace");
432
- if (success) {
433
- const pkgPath = path.join(resource.path, "package.json");
434
- if (fs.existsSync(pkgPath)) {
435
- resource.packageJson = fs.readJsonSync(pkgPath);
436
- }
437
- res.json({
438
- success: true,
439
- message: "Published to workspace",
440
- version: resource.packageJson?.version,
441
- });
442
- }
443
- else {
444
- res.status(500).json({ success: false, error: stderr || error?.message || "Publish failed" });
445
- }
446
- });
447
- });
448
- // API: List resources (legacy)
449
- app.get("/api/resources", (_req, res) => {
450
- res.json(resources.map((r) => ({
451
- type: r.type,
452
- name: r.name,
453
- displayName: r.displayName,
454
- description: r.description,
455
- category: r.category,
456
- })));
457
84
  });
458
- // API: Get template pages (for template preview)
459
- app.get("/api/templates/:name/pages", async (req, res) => {
460
- const { name } = req.params;
461
- const resource = resources.find((r) => r.name === name && r.type === "template");
462
- if (!resource) {
463
- res.status(404).json({ error: "Template not found" });
464
- return;
465
- }
466
- // Lazy load config if needed
467
- if (!resource.blockConfig) {
468
- try {
469
- const blockConfig = await loadBlockConfig(resource.path);
470
- if (blockConfig) {
471
- resource.blockConfig = blockConfig;
472
- }
473
- }
474
- catch (error) {
475
- res.status(500).json({ error: error.message });
476
- return;
477
- }
478
- }
479
- const config = resource.blockConfig;
480
- if (!config || !isTemplateConfig(config)) {
481
- res.status(400).json({ error: "Not a valid template (missing pages)" });
482
- return;
483
- }
484
- res.json({
485
- name: resource.name,
486
- displayName: config.name || resource.name,
487
- pages: config.pages.map((p) => ({
488
- name: p.name,
489
- slug: p.slug,
490
- blocksCount: p.blocks.length,
491
- })),
492
- layoutPositions: config.layoutPositions || [],
85
+ nextProcess.on("error", (err) => {
86
+ console.error(chalk.red("Failed to start Next.js:"), err.message);
87
+ process.exit(1);
88
+ });
89
+ nextProcess.on("exit", (code) => {
90
+ process.exit(code || 0);
91
+ });
92
+ // Watch for new blocks
93
+ const chokidar = await import("chokidar");
94
+ const watcher = chokidar.watch([path.join(projectRoot, "blocks/*/package.json"), path.join(projectRoot, "templates/*/package.json")], { ignoreInitial: true });
95
+ watcher.on("add", async () => {
96
+ console.log(chalk.green("\n New block detected, regenerating preview pages..."));
97
+ const newResources = await scanResources({
98
+ strict: false,
99
+ loadConfig: false,
100
+ validateSchema: false,
101
+ loadPreview: false,
102
+ requirePackageJson: false,
493
103
  });
104
+ regeneratePreviewPages(projectRoot, newResources);
105
+ console.log(chalk.green(" Preview pages regenerated. Refresh browser.\n"));
494
106
  });
495
- // Template page preview - renders full page with all blocks
496
- app.get("/preview/template/:name/:pageSlug?", async (req, res) => {
497
- const { name, pageSlug } = req.params;
498
- const resource = resources.find((r) => r.name === name && r.type === "template");
499
- if (!resource) {
500
- res.status(404).send("Template not found");
501
- return;
502
- }
503
- // Lazy load config if needed
504
- if (!resource.blockConfig) {
505
- try {
506
- const blockConfig = await loadBlockConfig(resource.path);
507
- if (blockConfig) {
508
- resource.blockConfig = blockConfig;
509
- }
510
- }
511
- catch (error) {
512
- res.status(500).send(`Failed to load template: ${error.message}`);
513
- return;
514
- }
515
- }
516
- const templateConfig = resource.blockConfig;
517
- if (!templateConfig || !isTemplateConfig(templateConfig)) {
518
- res.status(400).send("Not a valid template (missing pages)");
519
- return;
520
- }
521
- // Find page (default to first page)
522
- const page = pageSlug
523
- ? templateConfig.pages.find((p) => p.slug === pageSlug)
524
- : templateConfig.pages[0];
525
- if (!page) {
526
- res.status(404).send(`Page "${pageSlug}" not found in template`);
527
- return;
528
- }
529
- const html = generateTemplatePreviewHTML(resource, templateConfig, page, resources, port);
530
- const transformed = await vite.transformIndexHtml(req.url, html);
531
- res.send(transformed);
532
- });
533
- // Preview page - serves HTML that loads block via Vite
534
- app.get("/preview/:name", async (req, res) => {
535
- const { name } = req.params;
536
- const resource = resources.find((r) => r.name === name);
537
- if (!resource) {
538
- res.status(404).send("Resource not found");
539
- return;
540
- }
541
- // Always load preview data fresh from file
542
- const previewPath = path.join(resource.path, "preview.json");
543
- if (fs.existsSync(previewPath)) {
544
- resource.previewData = fs.readJsonSync(previewPath);
545
- }
546
- else {
547
- resource.previewData = {};
548
- }
549
- const html = generatePreviewHTML(resource, config, port);
550
- const transformed = await vite.transformIndexHtml(req.url, html);
551
- res.send(transformed);
552
- });
553
- // Legacy preview route
554
- app.get("/preview/:type/:name", async (req, res) => {
555
- const { name } = req.params;
556
- const resource = resources.find((r) => r.name === name);
557
- if (!resource) {
558
- res.status(404).send("Resource not found");
559
- return;
560
- }
561
- // Always load preview data fresh from file
562
- const previewPath2 = path.join(resource.path, "preview.json");
563
- if (fs.existsSync(previewPath2)) {
564
- resource.previewData = fs.readJsonSync(previewPath2);
565
- }
566
- else {
567
- resource.previewData = {};
568
- }
569
- const html = generatePreviewHTML(resource, config, port);
570
- const transformed = await vite.transformIndexHtml(req.url, html);
571
- res.send(transformed);
572
- });
573
- // Home page - serve React dev UI
574
- app.get("/", async (req, res) => {
575
- const indexPath = path.join(devUiReactPath, "index.html");
576
- let html = fs.readFileSync(indexPath, "utf-8");
577
- // Transform HTML through Vite for HMR support
578
- html = await vite.transformIndexHtml(req.url, html);
579
- res.send(html);
580
- });
581
- // Use Vite's middleware for JS/TS/CSS transforms (handles /dev-ui-react/ via alias)
582
- app.use(vite.middlewares);
583
- // Start server
584
- const server = app.listen(port, () => {
585
- spinner.succeed("Development server started (Vite)");
586
- console.log(chalk.green.bold("\n─────────────────────────────────────────"));
587
- console.log(chalk.green.bold(" Cmssy Dev Server (Vite HMR)"));
588
- console.log(chalk.green.bold("─────────────────────────────────────────\n"));
589
- const blocks = resources.filter((r) => r.type === "block");
590
- const templates = resources.filter((r) => r.type === "template");
591
- console.log(chalk.cyan(` ${blocks.length} blocks, ${templates.length} templates`));
592
- console.log(chalk.green(`\n Local: ${chalk.cyan(`http://localhost:${port}`)}`));
593
- console.log(chalk.green(" Vite HMR enabled ✓"));
594
- console.log(chalk.green(" Press Ctrl+C to stop"));
595
- console.log(chalk.green.bold("\n─────────────────────────────────────────\n"));
596
- // Listen for Ctrl+C directly on stdin (works even if SIGINT is blocked)
597
- if (process.stdin.isTTY) {
598
- process.stdin.setRawMode(true);
599
- process.stdin.resume();
600
- process.stdin.on("data", (data) => {
601
- // Ctrl+C = \x03, Ctrl+D = \x04
602
- if (data[0] === 0x03 || data[0] === 0x04) {
603
- console.log(chalk.yellow("\n\nShutting down..."));
604
- process.exit(0);
605
- }
606
- });
607
- }
608
- // Also register SIGINT as fallback
609
- process.removeAllListeners("SIGINT");
610
- process.on("SIGINT", () => {
611
- console.log(chalk.yellow("\n\nShutting down..."));
612
- process.exit(0);
613
- });
107
+ // Handle Ctrl+C
108
+ process.on("SIGINT", () => {
109
+ nextProcess.kill("SIGINT");
110
+ watcher.close();
111
+ process.exit(0);
614
112
  });
615
- // Watch for new blocks/config changes
616
- setupConfigWatcher({ resources, vite, fieldTypes });
617
113
  }
618
114
  catch (error) {
619
- spinner.fail("Failed to start development server");
115
+ spinner.fail("Failed to start Next.js dev server");
620
116
  console.error(chalk.red("Error:"), error);
621
117
  process.exit(1);
622
118
  }
623
119
  }
624
- function setupConfigWatcher(options) {
625
- const { resources, vite, fieldTypes } = options;
626
- const projectRoot = process.cwd();
627
- // Watch for block.config.ts changes to regenerate types
628
- vite.watcher.on("change", async (filePath) => {
629
- if (filePath.endsWith("block.config.ts")) {
630
- const relativePath = path.relative(projectRoot, filePath);
631
- const parts = relativePath.split(path.sep);
632
- const resourceName = parts[1]; // blocks/hero/block.config.ts -> hero
633
- const resource = resources.find((r) => r.name === resourceName);
634
- if (resource) {
635
- console.log(chalk.blue(`\n⚙️ Config changed: ${resourceName}`));
636
- try {
637
- const blockConfig = await loadBlockConfig(resource.path);
638
- if (blockConfig) {
639
- // Validate schema and show errors
640
- if (blockConfig.schema) {
641
- const validation = await validateBlockSchema(blockConfig.schema, resource.path);
642
- if (!validation.valid) {
643
- console.log(chalk.red(`\n❌ Schema validation errors in ${resourceName}:`));
644
- validation.errors.forEach((err) => {
645
- console.log(chalk.red(` • ${err}`));
646
- });
647
- console.log(chalk.yellow(`\nFix the errors above in block.config.ts\n`));
648
- }
649
- }
650
- resource.blockConfig = blockConfig;
651
- resource.displayName = blockConfig.name || resource.name;
652
- resource.description = blockConfig.description;
653
- resource.category = blockConfig.category;
654
- if (blockConfig.schema) {
655
- await generateTypes({
656
- blockPath: resource.path,
657
- schema: blockConfig.schema,
658
- fieldTypes,
659
- });
660
- }
661
- // Update metadata cache
662
- updateBlockInCache(resourceName, resource.type, blockConfig, resource.packageJson?.version);
663
- console.log(chalk.green(`✓ Types regenerated for ${resourceName}\n`));
664
- }
665
- }
666
- catch (error) {
667
- console.log(chalk.red(`\n❌ Failed to load config for ${resourceName}:`));
668
- console.log(chalk.red(` ${error.message}\n`));
669
- // Show hint for common errors
670
- if (error.message.includes('SyntaxError') || error.message.includes('Unexpected')) {
671
- console.log(chalk.yellow(` Hint: Check for syntax errors in block.config.ts\n`));
672
- }
673
- }
674
- }
675
- }
676
- // Watch for new package.json (new block detection)
677
- if (filePath.endsWith("package.json") && !filePath.includes("node_modules")) {
678
- const relativePath = path.relative(projectRoot, filePath);
679
- const parts = relativePath.split(path.sep);
680
- if ((parts[0] === "blocks" || parts[0] === "templates") && parts.length === 3) {
681
- const resourceName = parts[1];
682
- if (!resources.find((r) => r.name === resourceName)) {
683
- console.log(chalk.green(`\n✨ New block detected: ${resourceName}`));
684
- // Re-scan resources
685
- try {
686
- const newResources = await scanResources({
687
- strict: false,
688
- loadConfig: true,
689
- validateSchema: true,
690
- loadPreview: true,
691
- requirePackageJson: true,
692
- });
693
- const newResource = newResources.find((r) => r.name === resourceName);
694
- if (newResource) {
695
- resources.push(newResource);
696
- console.log(chalk.green(`✓ ${resourceName} added\n`));
697
- }
698
- }
699
- catch (error) {
700
- console.error(chalk.red(`Failed to scan new block ${resourceName}:`), error);
701
- }
702
- }
703
- }
704
- }
705
- });
706
- }
707
- function generatePreviewHTML(resource, config, port) {
708
- const blockPath = `/${resource.type}s/${resource.name}/src/index.tsx`;
709
- const cssPath = `/${resource.type}s/${resource.name}/src/index.css`;
710
- return `
711
- <!DOCTYPE html>
712
- <html lang="en">
713
- <head>
714
- <meta charset="UTF-8">
715
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
716
- <title>${resource.displayName} - Preview</title>
717
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%23667eea'/%3E%3Ctext x='50' y='70' font-size='60' font-weight='bold' text-anchor='middle' fill='white' font-family='system-ui'%3EC%3C/text%3E%3C/svg%3E">
718
- <script type="module" src="/@vite/client"></script>
719
- <link rel="stylesheet" href="${cssPath}">
720
- <style>
721
- body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
722
- .preview-header {
723
- position: fixed; top: 0; left: 0; right: 0;
724
- background: white; border-bottom: 1px solid #e0e0e0;
725
- padding: 1rem 2rem; z-index: 1000;
726
- display: flex; justify-content: space-between; align-items: center;
727
- }
728
- .preview-title { font-size: 1.25rem; font-weight: 600; margin: 0; }
729
- .preview-back { color: #667eea; text-decoration: none; font-weight: 500; }
730
- .preview-container { margin-top: 60px; min-height: calc(100vh - 60px); }
731
- </style>
732
- </head>
733
- <body>
734
- <div class="preview-header">
735
- <div class="preview-title">${resource.displayName}</div>
736
- <a href="/" class="preview-back" target="_parent">← Back to Home</a>
737
- </div>
738
- <div class="preview-container">
739
- <div id="preview-root"></div>
740
- </div>
741
-
742
- <script type="module">
743
- import module from '${blockPath}';
744
- const element = document.getElementById('preview-root');
745
- let props = ${JSON.stringify(resource.previewData || {})};
746
- let context = module.mount(element, props);
747
-
748
- // Listen for prop updates from parent
749
- window.addEventListener('message', (event) => {
750
- if (event.data.type === 'UPDATE_PROPS') {
751
- props = event.data.props;
752
- if (module.update && context) {
753
- module.update(element, props, context);
754
- } else {
755
- if (context && module.unmount) module.unmount(element, context);
756
- context = module.mount(element, props);
757
- }
758
- }
759
- });
760
-
761
- // Vite HMR
762
- if (import.meta.hot) {
763
- import.meta.hot.accept('${blockPath}', (newModule) => {
764
- if (newModule) {
765
- console.log('🔄 HMR update');
766
- if (context && module.unmount) module.unmount(element, context);
767
- context = newModule.default.mount(element, props);
768
- }
769
- });
770
- }
771
- </script>
772
- </body>
773
- </html>
774
- `;
775
- }
776
- function generateTemplatePreviewHTML(resource, templateConfig, page, allResources, port) {
777
- // Find all blocks used in this page
778
- const blockImports = [];
779
- const blockMounts = [];
780
- // Generate imports and mounts for each block in the page
781
- page.blocks.forEach((blockInstance, index) => {
782
- // Block type can be "hero" or "@vendor/blocks.hero" - extract the block name
783
- const blockName = blockInstance.type.includes('.')
784
- ? blockInstance.type.split('.').pop()
785
- : blockInstance.type;
786
- // Find the block resource
787
- const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
788
- if (blockResource) {
789
- const blockPath = `/blocks/${blockName}/src/index.tsx`;
790
- const cssPath = `/blocks/${blockName}/src/index.css`;
791
- const varName = `block_${index}`;
792
- const containerId = `block-${index}`;
793
- blockImports.push(`import ${varName} from '${blockPath}';`);
794
- blockImports.push(`import '${cssPath}';`);
795
- const props = JSON.stringify(blockInstance.content || {});
796
- blockMounts.push(`
797
- {
798
- const el = document.getElementById('${containerId}');
799
- if (el && ${varName}.mount) {
800
- ${varName}.mount(el, ${props});
801
- }
802
- }
803
- `);
804
- }
805
- });
806
- // Generate layout slot imports/mounts
807
- const layoutPositions = templateConfig.layoutPositions || [];
808
- const headerSlot = layoutPositions.find((s) => s.position === "header");
809
- const footerSlot = layoutPositions.find((s) => s.position === "footer");
810
- if (headerSlot) {
811
- const blockName = headerSlot.type.includes('.')
812
- ? headerSlot.type.split('.').pop()
813
- : headerSlot.type;
814
- const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
815
- if (blockResource) {
816
- blockImports.push(`import headerBlock from '/blocks/${blockName}/src/index.tsx';`);
817
- blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
818
- blockMounts.push(`
819
- {
820
- const el = document.getElementById('layout-header');
821
- if (el && headerBlock.mount) {
822
- headerBlock.mount(el, ${JSON.stringify(headerSlot.content || {})});
823
- }
824
- }
825
- `);
826
- }
827
- }
828
- if (footerSlot) {
829
- const blockName = footerSlot.type.includes('.')
830
- ? footerSlot.type.split('.').pop()
831
- : footerSlot.type;
832
- const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
833
- if (blockResource) {
834
- blockImports.push(`import footerBlock from '/blocks/${blockName}/src/index.tsx';`);
835
- blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
836
- blockMounts.push(`
837
- {
838
- const el = document.getElementById('layout-footer');
839
- if (el && footerBlock.mount) {
840
- footerBlock.mount(el, ${JSON.stringify(footerSlot.content || {})});
841
- }
842
- }
843
- `);
844
- }
845
- }
846
- // Generate page navigation tabs
847
- const pageTabs = templateConfig.pages.map((p) => {
848
- const isActive = p.slug === page.slug;
849
- return `<a href="/preview/template/${resource.name}/${p.slug}" class="page-tab ${isActive ? 'active' : ''}">${p.name}</a>`;
850
- }).join('');
851
- // Generate block containers HTML
852
- const blockContainers = page.blocks.map((_, index) => {
853
- return `<div id="block-${index}" class="template-block"></div>`;
854
- }).join('\n ');
855
- return `
856
- <!DOCTYPE html>
857
- <html lang="en">
858
- <head>
859
- <meta charset="UTF-8">
860
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
861
- <title>${templateConfig.name} - ${page.name}</title>
862
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%23667eea'/%3E%3Ctext x='50' y='70' font-size='60' font-weight='bold' text-anchor='middle' fill='white' font-family='system-ui'%3EC%3C/text%3E%3C/svg%3E">
863
- <script type="module" src="/@vite/client"></script>
864
- <style>
865
- * { box-sizing: border-box; }
866
- body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
867
-
868
- .template-header {
869
- position: fixed; top: 0; left: 0; right: 0;
870
- background: #1a1a2e; color: white;
871
- padding: 0.75rem 1.5rem; z-index: 1000;
872
- display: flex; justify-content: space-between; align-items: center;
873
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
874
- }
875
- .template-header-left {
876
- display: flex; align-items: center; gap: 1.5rem;
877
- }
878
- .template-title {
879
- font-size: 1rem; font-weight: 600; margin: 0;
880
- display: flex; align-items: center; gap: 0.5rem;
881
- }
882
- .template-badge {
883
- background: #667eea; color: white;
884
- padding: 0.15rem 0.5rem; border-radius: 4px;
885
- font-size: 0.7rem; font-weight: 500;
886
- }
887
- .page-tabs {
888
- display: flex; gap: 0.25rem;
889
- }
890
- .page-tab {
891
- color: rgba(255,255,255,0.7); text-decoration: none;
892
- padding: 0.4rem 0.75rem; border-radius: 6px;
893
- font-size: 0.85rem; font-weight: 500;
894
- transition: all 0.2s;
895
- }
896
- .page-tab:hover { color: white; background: rgba(255,255,255,0.1); }
897
- .page-tab.active { color: white; background: #667eea; }
898
-
899
- .template-back {
900
- color: rgba(255,255,255,0.8); text-decoration: none;
901
- font-size: 0.85rem; font-weight: 500;
902
- padding: 0.4rem 0.75rem; border-radius: 6px;
903
- transition: all 0.2s;
904
- }
905
- .template-back:hover { color: white; background: rgba(255,255,255,0.1); }
906
-
907
- .template-content {
908
- margin-top: 52px;
909
- min-height: calc(100vh - 52px);
910
- }
911
- .template-block {
912
- /* Blocks render their own styles */
913
- }
914
- #layout-header, #layout-footer {
915
- /* Layout slots */
916
- }
917
-
918
- .block-error {
919
- padding: 2rem;
920
- background: #fff3cd;
921
- border: 1px solid #ffc107;
922
- color: #856404;
923
- text-align: center;
924
- }
925
- </style>
926
- </head>
927
- <body>
928
- <div class="template-header">
929
- <div class="template-header-left">
930
- <h1 class="template-title">
931
- <span class="template-badge">Template</span>
932
- ${templateConfig.name}
933
- </h1>
934
- <div class="page-tabs">
935
- ${pageTabs}
936
- </div>
937
- </div>
938
- <a href="/" class="template-back" target="_parent">← Back to Dev</a>
939
- </div>
940
-
941
- <div class="template-content">
942
- ${headerSlot ? '<div id="layout-header"></div>' : ''}
943
- <main>
944
- ${blockContainers || '<div class="block-error">No blocks defined for this page</div>'}
945
- </main>
946
- ${footerSlot ? '<div id="layout-footer"></div>' : ''}
947
- </div>
948
-
949
- <script type="module">
950
- ${blockImports.join('\n ')}
951
-
952
- // Mount all blocks
953
- ${blockMounts.join('\n ')}
954
- </script>
955
- </body>
956
- </html>
957
- `;
958
- }
959
120
  //# sourceMappingURL=dev.js.map