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