@cmssy/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +649 -0
- package/config.d.ts +2 -0
- package/config.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +236 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add-source.d.ts +7 -0
- package/dist/commands/add-source.d.ts.map +1 -0
- package/dist/commands/add-source.js +238 -0
- package/dist/commands/add-source.js.map +1 -0
- package/dist/commands/build.d.ts +7 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +105 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/configure.d.ts +6 -0
- package/dist/commands/configure.d.ts.map +1 -0
- package/dist/commands/configure.js +42 -0
- package/dist/commands/configure.js.map +1 -0
- package/dist/commands/create.d.ts +18 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +444 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +962 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +362 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +227 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/package.d.ts +7 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +136 -0
- package/dist/commands/package.js.map +1 -0
- package/dist/commands/publish.d.ts +13 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +910 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +208 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/upload.d.ts +7 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +126 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/commands/workspaces.d.ts +2 -0
- package/dist/commands/workspaces.d.ts.map +1 -0
- package/dist/commands/workspaces.js +67 -0
- package/dist/commands/workspaces.js.map +1 -0
- package/dist/dev-ui/app.js +1284 -0
- package/dist/dev-ui/index.html +1511 -0
- package/dist/dev-ui-react/App.tsx +164 -0
- package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
- package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
- package/dist/dev-ui-react/components/Editor.tsx +469 -0
- package/dist/dev-ui-react/components/Preview.tsx +146 -0
- package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
- package/dist/dev-ui-react/index.html +13 -0
- package/dist/dev-ui-react/main.tsx +8 -0
- package/dist/dev-ui-react/styles.css +856 -0
- package/dist/dev-ui-react/types.ts +45 -0
- package/dist/types/block-config.d.ts +315 -0
- package/dist/types/block-config.d.ts.map +1 -0
- package/dist/types/block-config.js +8 -0
- package/dist/types/block-config.js.map +1 -0
- package/dist/utils/block-config.d.ts +10 -0
- package/dist/utils/block-config.d.ts.map +1 -0
- package/dist/utils/block-config.js +199 -0
- package/dist/utils/block-config.js.map +1 -0
- package/dist/utils/blocks-meta-cache.d.ts +28 -0
- package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
- package/dist/utils/blocks-meta-cache.js +72 -0
- package/dist/utils/blocks-meta-cache.js.map +1 -0
- package/dist/utils/builder.d.ts +34 -0
- package/dist/utils/builder.d.ts.map +1 -0
- package/dist/utils/builder.js +140 -0
- package/dist/utils/builder.js.map +1 -0
- package/dist/utils/cmssy-config.d.ts +16 -0
- package/dist/utils/cmssy-config.d.ts.map +1 -0
- package/dist/utils/cmssy-config.js +19 -0
- package/dist/utils/cmssy-config.js.map +1 -0
- package/dist/utils/config.d.ts +9 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +46 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/field-schema.d.ts +12 -0
- package/dist/utils/field-schema.d.ts.map +1 -0
- package/dist/utils/field-schema.js +202 -0
- package/dist/utils/field-schema.js.map +1 -0
- package/dist/utils/graphql.d.ts +8 -0
- package/dist/utils/graphql.d.ts.map +1 -0
- package/dist/utils/graphql.js +118 -0
- package/dist/utils/graphql.js.map +1 -0
- package/dist/utils/publish-helpers.d.ts +35 -0
- package/dist/utils/publish-helpers.d.ts.map +1 -0
- package/dist/utils/publish-helpers.js +141 -0
- package/dist/utils/publish-helpers.js.map +1 -0
- package/dist/utils/scanner.d.ts +36 -0
- package/dist/utils/scanner.d.ts.map +1 -0
- package/dist/utils/scanner.js +140 -0
- package/dist/utils/scanner.js.map +1 -0
- package/dist/utils/type-generator.d.ts +9 -0
- package/dist/utils/type-generator.d.ts.map +1 -0
- package/dist/utils/type-generator.js +85 -0
- package/dist/utils/type-generator.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import { GraphQLClient } from "graphql-request";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
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";
|
|
58
|
+
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
|
+
export async function devCommand(options) {
|
|
92
|
+
const spinner = ora("Starting development server...").start();
|
|
93
|
+
try {
|
|
94
|
+
const config = await loadConfig();
|
|
95
|
+
const port = parseInt(options.port, 10);
|
|
96
|
+
const projectRoot = process.cwd();
|
|
97
|
+
// Scan for blocks and templates - FAST: no config loading at startup
|
|
98
|
+
spinner.text = "Scanning blocks...";
|
|
99
|
+
const resources = await scanResources({
|
|
100
|
+
strict: false,
|
|
101
|
+
loadConfig: false, // Lazy load configs when needed
|
|
102
|
+
validateSchema: false,
|
|
103
|
+
loadPreview: false, // Lazy load preview data
|
|
104
|
+
requirePackageJson: false,
|
|
105
|
+
});
|
|
106
|
+
if (resources.length === 0) {
|
|
107
|
+
spinner.warn("No blocks or templates found");
|
|
108
|
+
console.log(chalk.yellow("\nCreate your first block:\n"));
|
|
109
|
+
console.log(chalk.white(" npx cmssy create block my-block\n"));
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
// Load metadata cache for instant filters
|
|
113
|
+
spinner.text = "Loading metadata cache...";
|
|
114
|
+
const metaCache = loadMetaCache(projectRoot);
|
|
115
|
+
let cachedCount = 0;
|
|
116
|
+
// Merge cached metadata into resources
|
|
117
|
+
resources.forEach((r) => {
|
|
118
|
+
const cached = metaCache.blocks[r.name];
|
|
119
|
+
if (cached) {
|
|
120
|
+
r.category = cached.category;
|
|
121
|
+
r.displayName = cached.displayName || r.name;
|
|
122
|
+
r.description = cached.description;
|
|
123
|
+
// Store tags in a temp property for API
|
|
124
|
+
r.cachedTags = cached.tags;
|
|
125
|
+
cachedCount++;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
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
|
+
],
|
|
177
|
+
},
|
|
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.layoutSlots = cfg.layoutSlots || [];
|
|
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 !== "marketplace" && target !== "workspace")) {
|
|
407
|
+
res.status(400).json({ error: "Invalid target" });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (target === "workspace" && !workspaceId) {
|
|
411
|
+
res.status(400).json({ error: "Workspace ID required" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const args = ["publish", resource.name, `--${target}`];
|
|
415
|
+
if (target === "workspace" && workspaceId)
|
|
416
|
+
args.push(workspaceId);
|
|
417
|
+
if (versionBump && versionBump !== "none") {
|
|
418
|
+
args.push(`--${versionBump}`);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
args.push("--no-bump");
|
|
422
|
+
}
|
|
423
|
+
const command = `cmssy ${args.join(" ")}`;
|
|
424
|
+
console.log("[PUBLISH] Executing:", command);
|
|
425
|
+
exec(command, {
|
|
426
|
+
cwd: projectRoot,
|
|
427
|
+
timeout: 60000,
|
|
428
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
429
|
+
env: { ...process.env, CI: "true", FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
430
|
+
}, (error, stdout, stderr) => {
|
|
431
|
+
const output = `${stdout}\n${stderr}`;
|
|
432
|
+
const success = output.includes("published successfully") ||
|
|
433
|
+
output.includes("published to workspace") ||
|
|
434
|
+
output.includes("submitted for review");
|
|
435
|
+
if (success) {
|
|
436
|
+
const pkgPath = path.join(resource.path, "package.json");
|
|
437
|
+
if (fs.existsSync(pkgPath)) {
|
|
438
|
+
resource.packageJson = fs.readJsonSync(pkgPath);
|
|
439
|
+
}
|
|
440
|
+
res.json({
|
|
441
|
+
success: true,
|
|
442
|
+
message: target === "marketplace" ? "Submitted for review" : "Published to workspace",
|
|
443
|
+
version: resource.packageJson?.version,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
res.status(500).json({ success: false, error: stderr || error?.message || "Publish failed" });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
// API: List resources (legacy)
|
|
452
|
+
app.get("/api/resources", (_req, res) => {
|
|
453
|
+
res.json(resources.map((r) => ({
|
|
454
|
+
type: r.type,
|
|
455
|
+
name: r.name,
|
|
456
|
+
displayName: r.displayName,
|
|
457
|
+
description: r.description,
|
|
458
|
+
category: r.category,
|
|
459
|
+
})));
|
|
460
|
+
});
|
|
461
|
+
// API: Get template pages (for template preview)
|
|
462
|
+
app.get("/api/templates/:name/pages", async (req, res) => {
|
|
463
|
+
const { name } = req.params;
|
|
464
|
+
const resource = resources.find((r) => r.name === name && r.type === "template");
|
|
465
|
+
if (!resource) {
|
|
466
|
+
res.status(404).json({ error: "Template not found" });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// Lazy load config if needed
|
|
470
|
+
if (!resource.blockConfig) {
|
|
471
|
+
try {
|
|
472
|
+
const blockConfig = await loadBlockConfig(resource.path);
|
|
473
|
+
if (blockConfig) {
|
|
474
|
+
resource.blockConfig = blockConfig;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
res.status(500).json({ error: error.message });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const config = resource.blockConfig;
|
|
483
|
+
if (!config || !isTemplateConfig(config)) {
|
|
484
|
+
res.status(400).json({ error: "Not a valid template (missing pages)" });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
res.json({
|
|
488
|
+
name: resource.name,
|
|
489
|
+
displayName: config.name || resource.name,
|
|
490
|
+
pages: config.pages.map((p) => ({
|
|
491
|
+
name: p.name,
|
|
492
|
+
slug: p.slug,
|
|
493
|
+
blocksCount: p.blocks.length,
|
|
494
|
+
})),
|
|
495
|
+
layoutSlots: config.layoutSlots || [],
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
// Template page preview - renders full page with all blocks
|
|
499
|
+
app.get("/preview/template/:name/:pageSlug?", async (req, res) => {
|
|
500
|
+
const { name, pageSlug } = req.params;
|
|
501
|
+
const resource = resources.find((r) => r.name === name && r.type === "template");
|
|
502
|
+
if (!resource) {
|
|
503
|
+
res.status(404).send("Template not found");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// Lazy load config if needed
|
|
507
|
+
if (!resource.blockConfig) {
|
|
508
|
+
try {
|
|
509
|
+
const blockConfig = await loadBlockConfig(resource.path);
|
|
510
|
+
if (blockConfig) {
|
|
511
|
+
resource.blockConfig = blockConfig;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
res.status(500).send(`Failed to load template: ${error.message}`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const templateConfig = resource.blockConfig;
|
|
520
|
+
if (!templateConfig || !isTemplateConfig(templateConfig)) {
|
|
521
|
+
res.status(400).send("Not a valid template (missing pages)");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Find page (default to first page)
|
|
525
|
+
const page = pageSlug
|
|
526
|
+
? templateConfig.pages.find((p) => p.slug === pageSlug)
|
|
527
|
+
: templateConfig.pages[0];
|
|
528
|
+
if (!page) {
|
|
529
|
+
res.status(404).send(`Page "${pageSlug}" not found in template`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const html = generateTemplatePreviewHTML(resource, templateConfig, page, resources, port);
|
|
533
|
+
const transformed = await vite.transformIndexHtml(req.url, html);
|
|
534
|
+
res.send(transformed);
|
|
535
|
+
});
|
|
536
|
+
// Preview page - serves HTML that loads block via Vite
|
|
537
|
+
app.get("/preview/:name", async (req, res) => {
|
|
538
|
+
const { name } = req.params;
|
|
539
|
+
const resource = resources.find((r) => r.name === name);
|
|
540
|
+
if (!resource) {
|
|
541
|
+
res.status(404).send("Resource not found");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Always load preview data fresh from file
|
|
545
|
+
const previewPath = path.join(resource.path, "preview.json");
|
|
546
|
+
if (fs.existsSync(previewPath)) {
|
|
547
|
+
resource.previewData = fs.readJsonSync(previewPath);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
resource.previewData = {};
|
|
551
|
+
}
|
|
552
|
+
const html = generatePreviewHTML(resource, config, port);
|
|
553
|
+
const transformed = await vite.transformIndexHtml(req.url, html);
|
|
554
|
+
res.send(transformed);
|
|
555
|
+
});
|
|
556
|
+
// Legacy preview route
|
|
557
|
+
app.get("/preview/:type/:name", async (req, res) => {
|
|
558
|
+
const { name } = req.params;
|
|
559
|
+
const resource = resources.find((r) => r.name === name);
|
|
560
|
+
if (!resource) {
|
|
561
|
+
res.status(404).send("Resource not found");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// Always load preview data fresh from file
|
|
565
|
+
const previewPath2 = path.join(resource.path, "preview.json");
|
|
566
|
+
if (fs.existsSync(previewPath2)) {
|
|
567
|
+
resource.previewData = fs.readJsonSync(previewPath2);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
resource.previewData = {};
|
|
571
|
+
}
|
|
572
|
+
const html = generatePreviewHTML(resource, config, port);
|
|
573
|
+
const transformed = await vite.transformIndexHtml(req.url, html);
|
|
574
|
+
res.send(transformed);
|
|
575
|
+
});
|
|
576
|
+
// Home page - serve React dev UI
|
|
577
|
+
app.get("/", async (req, res) => {
|
|
578
|
+
const indexPath = path.join(devUiReactPath, "index.html");
|
|
579
|
+
let html = fs.readFileSync(indexPath, "utf-8");
|
|
580
|
+
// Transform HTML through Vite for HMR support
|
|
581
|
+
html = await vite.transformIndexHtml(req.url, html);
|
|
582
|
+
res.send(html);
|
|
583
|
+
});
|
|
584
|
+
// Use Vite's middleware for JS/TS/CSS transforms (handles /dev-ui-react/ via alias)
|
|
585
|
+
app.use(vite.middlewares);
|
|
586
|
+
// Start server
|
|
587
|
+
const server = app.listen(port, () => {
|
|
588
|
+
spinner.succeed("Development server started (Vite)");
|
|
589
|
+
console.log(chalk.green.bold("\n─────────────────────────────────────────"));
|
|
590
|
+
console.log(chalk.green.bold(" Cmssy Dev Server (Vite HMR)"));
|
|
591
|
+
console.log(chalk.green.bold("─────────────────────────────────────────\n"));
|
|
592
|
+
const blocks = resources.filter((r) => r.type === "block");
|
|
593
|
+
const templates = resources.filter((r) => r.type === "template");
|
|
594
|
+
console.log(chalk.cyan(` ${blocks.length} blocks, ${templates.length} templates`));
|
|
595
|
+
console.log(chalk.green(`\n Local: ${chalk.cyan(`http://localhost:${port}`)}`));
|
|
596
|
+
console.log(chalk.green(" Vite HMR enabled ✓"));
|
|
597
|
+
console.log(chalk.green(" Press Ctrl+C to stop"));
|
|
598
|
+
console.log(chalk.green.bold("\n─────────────────────────────────────────\n"));
|
|
599
|
+
// Listen for Ctrl+C directly on stdin (works even if SIGINT is blocked)
|
|
600
|
+
if (process.stdin.isTTY) {
|
|
601
|
+
process.stdin.setRawMode(true);
|
|
602
|
+
process.stdin.resume();
|
|
603
|
+
process.stdin.on("data", (data) => {
|
|
604
|
+
// Ctrl+C = \x03, Ctrl+D = \x04
|
|
605
|
+
if (data[0] === 0x03 || data[0] === 0x04) {
|
|
606
|
+
console.log(chalk.yellow("\n\nShutting down..."));
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
// Also register SIGINT as fallback
|
|
612
|
+
process.removeAllListeners("SIGINT");
|
|
613
|
+
process.on("SIGINT", () => {
|
|
614
|
+
console.log(chalk.yellow("\n\nShutting down..."));
|
|
615
|
+
process.exit(0);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
// Watch for new blocks/config changes
|
|
619
|
+
setupConfigWatcher({ resources, vite, fieldTypes });
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
spinner.fail("Failed to start development server");
|
|
623
|
+
console.error(chalk.red("Error:"), error);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function setupConfigWatcher(options) {
|
|
628
|
+
const { resources, vite, fieldTypes } = options;
|
|
629
|
+
const projectRoot = process.cwd();
|
|
630
|
+
// Watch for block.config.ts changes to regenerate types
|
|
631
|
+
vite.watcher.on("change", async (filePath) => {
|
|
632
|
+
if (filePath.endsWith("block.config.ts")) {
|
|
633
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
634
|
+
const parts = relativePath.split(path.sep);
|
|
635
|
+
const resourceName = parts[1]; // blocks/hero/block.config.ts -> hero
|
|
636
|
+
const resource = resources.find((r) => r.name === resourceName);
|
|
637
|
+
if (resource) {
|
|
638
|
+
console.log(chalk.blue(`\n⚙️ Config changed: ${resourceName}`));
|
|
639
|
+
try {
|
|
640
|
+
const blockConfig = await loadBlockConfig(resource.path);
|
|
641
|
+
if (blockConfig) {
|
|
642
|
+
// Validate schema and show errors
|
|
643
|
+
if (blockConfig.schema) {
|
|
644
|
+
const validation = await validateBlockSchema(blockConfig.schema, resource.path);
|
|
645
|
+
if (!validation.valid) {
|
|
646
|
+
console.log(chalk.red(`\n❌ Schema validation errors in ${resourceName}:`));
|
|
647
|
+
validation.errors.forEach((err) => {
|
|
648
|
+
console.log(chalk.red(` • ${err}`));
|
|
649
|
+
});
|
|
650
|
+
console.log(chalk.yellow(`\nFix the errors above in block.config.ts\n`));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
resource.blockConfig = blockConfig;
|
|
654
|
+
resource.displayName = blockConfig.name || resource.name;
|
|
655
|
+
resource.description = blockConfig.description;
|
|
656
|
+
resource.category = blockConfig.category;
|
|
657
|
+
if (blockConfig.schema) {
|
|
658
|
+
await generateTypes({
|
|
659
|
+
blockPath: resource.path,
|
|
660
|
+
schema: blockConfig.schema,
|
|
661
|
+
fieldTypes,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
// Update metadata cache
|
|
665
|
+
updateBlockInCache(resourceName, resource.type, blockConfig, resource.packageJson?.version);
|
|
666
|
+
console.log(chalk.green(`✓ Types regenerated for ${resourceName}\n`));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
console.log(chalk.red(`\n❌ Failed to load config for ${resourceName}:`));
|
|
671
|
+
console.log(chalk.red(` ${error.message}\n`));
|
|
672
|
+
// Show hint for common errors
|
|
673
|
+
if (error.message.includes('SyntaxError') || error.message.includes('Unexpected')) {
|
|
674
|
+
console.log(chalk.yellow(` Hint: Check for syntax errors in block.config.ts\n`));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Watch for new package.json (new block detection)
|
|
680
|
+
if (filePath.endsWith("package.json") && !filePath.includes("node_modules")) {
|
|
681
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
682
|
+
const parts = relativePath.split(path.sep);
|
|
683
|
+
if ((parts[0] === "blocks" || parts[0] === "templates") && parts.length === 3) {
|
|
684
|
+
const resourceName = parts[1];
|
|
685
|
+
if (!resources.find((r) => r.name === resourceName)) {
|
|
686
|
+
console.log(chalk.green(`\n✨ New block detected: ${resourceName}`));
|
|
687
|
+
// Re-scan resources
|
|
688
|
+
try {
|
|
689
|
+
const newResources = await scanResources({
|
|
690
|
+
strict: false,
|
|
691
|
+
loadConfig: true,
|
|
692
|
+
validateSchema: true,
|
|
693
|
+
loadPreview: true,
|
|
694
|
+
requirePackageJson: true,
|
|
695
|
+
});
|
|
696
|
+
const newResource = newResources.find((r) => r.name === resourceName);
|
|
697
|
+
if (newResource) {
|
|
698
|
+
resources.push(newResource);
|
|
699
|
+
console.log(chalk.green(`✓ ${resourceName} added\n`));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
console.error(chalk.red(`Failed to scan new block ${resourceName}:`), error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function generatePreviewHTML(resource, config, port) {
|
|
711
|
+
const blockPath = `/${resource.type}s/${resource.name}/src/index.tsx`;
|
|
712
|
+
const cssPath = `/${resource.type}s/${resource.name}/src/index.css`;
|
|
713
|
+
return `
|
|
714
|
+
<!DOCTYPE html>
|
|
715
|
+
<html lang="en">
|
|
716
|
+
<head>
|
|
717
|
+
<meta charset="UTF-8">
|
|
718
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
719
|
+
<title>${resource.displayName} - Preview</title>
|
|
720
|
+
<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">
|
|
721
|
+
<script type="module" src="/@vite/client"></script>
|
|
722
|
+
<link rel="stylesheet" href="${cssPath}">
|
|
723
|
+
<style>
|
|
724
|
+
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
725
|
+
.preview-header {
|
|
726
|
+
position: fixed; top: 0; left: 0; right: 0;
|
|
727
|
+
background: white; border-bottom: 1px solid #e0e0e0;
|
|
728
|
+
padding: 1rem 2rem; z-index: 1000;
|
|
729
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
730
|
+
}
|
|
731
|
+
.preview-title { font-size: 1.25rem; font-weight: 600; margin: 0; }
|
|
732
|
+
.preview-back { color: #667eea; text-decoration: none; font-weight: 500; }
|
|
733
|
+
.preview-container { margin-top: 60px; min-height: calc(100vh - 60px); }
|
|
734
|
+
</style>
|
|
735
|
+
</head>
|
|
736
|
+
<body>
|
|
737
|
+
<div class="preview-header">
|
|
738
|
+
<div class="preview-title">${resource.displayName}</div>
|
|
739
|
+
<a href="/" class="preview-back" target="_parent">← Back to Home</a>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="preview-container">
|
|
742
|
+
<div id="preview-root"></div>
|
|
743
|
+
</div>
|
|
744
|
+
|
|
745
|
+
<script type="module">
|
|
746
|
+
import module from '${blockPath}';
|
|
747
|
+
const element = document.getElementById('preview-root');
|
|
748
|
+
let props = ${JSON.stringify(resource.previewData || {})};
|
|
749
|
+
let context = module.mount(element, props);
|
|
750
|
+
|
|
751
|
+
// Listen for prop updates from parent
|
|
752
|
+
window.addEventListener('message', (event) => {
|
|
753
|
+
if (event.data.type === 'UPDATE_PROPS') {
|
|
754
|
+
props = event.data.props;
|
|
755
|
+
if (module.update && context) {
|
|
756
|
+
module.update(element, props, context);
|
|
757
|
+
} else {
|
|
758
|
+
if (context && module.unmount) module.unmount(element, context);
|
|
759
|
+
context = module.mount(element, props);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Vite HMR
|
|
765
|
+
if (import.meta.hot) {
|
|
766
|
+
import.meta.hot.accept('${blockPath}', (newModule) => {
|
|
767
|
+
if (newModule) {
|
|
768
|
+
console.log('🔄 HMR update');
|
|
769
|
+
if (context && module.unmount) module.unmount(element, context);
|
|
770
|
+
context = newModule.default.mount(element, props);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
</script>
|
|
775
|
+
</body>
|
|
776
|
+
</html>
|
|
777
|
+
`;
|
|
778
|
+
}
|
|
779
|
+
function generateTemplatePreviewHTML(resource, templateConfig, page, allResources, port) {
|
|
780
|
+
// Find all blocks used in this page
|
|
781
|
+
const blockImports = [];
|
|
782
|
+
const blockMounts = [];
|
|
783
|
+
// Generate imports and mounts for each block in the page
|
|
784
|
+
page.blocks.forEach((blockInstance, index) => {
|
|
785
|
+
// Block type can be "hero" or "@vendor/blocks.hero" - extract the block name
|
|
786
|
+
const blockName = blockInstance.type.includes('.')
|
|
787
|
+
? blockInstance.type.split('.').pop()
|
|
788
|
+
: blockInstance.type;
|
|
789
|
+
// Find the block resource
|
|
790
|
+
const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
|
|
791
|
+
if (blockResource) {
|
|
792
|
+
const blockPath = `/blocks/${blockName}/src/index.tsx`;
|
|
793
|
+
const cssPath = `/blocks/${blockName}/src/index.css`;
|
|
794
|
+
const varName = `block_${index}`;
|
|
795
|
+
const containerId = `block-${index}`;
|
|
796
|
+
blockImports.push(`import ${varName} from '${blockPath}';`);
|
|
797
|
+
blockImports.push(`import '${cssPath}';`);
|
|
798
|
+
const props = JSON.stringify(blockInstance.content || {});
|
|
799
|
+
blockMounts.push(`
|
|
800
|
+
{
|
|
801
|
+
const el = document.getElementById('${containerId}');
|
|
802
|
+
if (el && ${varName}.mount) {
|
|
803
|
+
${varName}.mount(el, ${props});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
`);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
// Generate layout slot imports/mounts
|
|
810
|
+
const layoutSlots = templateConfig.layoutSlots || [];
|
|
811
|
+
const headerSlot = layoutSlots.find((s) => s.slot === "header");
|
|
812
|
+
const footerSlot = layoutSlots.find((s) => s.slot === "footer");
|
|
813
|
+
if (headerSlot) {
|
|
814
|
+
const blockName = headerSlot.type.includes('.')
|
|
815
|
+
? headerSlot.type.split('.').pop()
|
|
816
|
+
: headerSlot.type;
|
|
817
|
+
const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
|
|
818
|
+
if (blockResource) {
|
|
819
|
+
blockImports.push(`import headerBlock from '/blocks/${blockName}/src/index.tsx';`);
|
|
820
|
+
blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
|
|
821
|
+
blockMounts.push(`
|
|
822
|
+
{
|
|
823
|
+
const el = document.getElementById('layout-header');
|
|
824
|
+
if (el && headerBlock.mount) {
|
|
825
|
+
headerBlock.mount(el, ${JSON.stringify(headerSlot.content || {})});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (footerSlot) {
|
|
832
|
+
const blockName = footerSlot.type.includes('.')
|
|
833
|
+
? footerSlot.type.split('.').pop()
|
|
834
|
+
: footerSlot.type;
|
|
835
|
+
const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
|
|
836
|
+
if (blockResource) {
|
|
837
|
+
blockImports.push(`import footerBlock from '/blocks/${blockName}/src/index.tsx';`);
|
|
838
|
+
blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
|
|
839
|
+
blockMounts.push(`
|
|
840
|
+
{
|
|
841
|
+
const el = document.getElementById('layout-footer');
|
|
842
|
+
if (el && footerBlock.mount) {
|
|
843
|
+
footerBlock.mount(el, ${JSON.stringify(footerSlot.content || {})});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Generate page navigation tabs
|
|
850
|
+
const pageTabs = templateConfig.pages.map((p) => {
|
|
851
|
+
const isActive = p.slug === page.slug;
|
|
852
|
+
return `<a href="/preview/template/${resource.name}/${p.slug}" class="page-tab ${isActive ? 'active' : ''}">${p.name}</a>`;
|
|
853
|
+
}).join('');
|
|
854
|
+
// Generate block containers HTML
|
|
855
|
+
const blockContainers = page.blocks.map((_, index) => {
|
|
856
|
+
return `<div id="block-${index}" class="template-block"></div>`;
|
|
857
|
+
}).join('\n ');
|
|
858
|
+
return `
|
|
859
|
+
<!DOCTYPE html>
|
|
860
|
+
<html lang="en">
|
|
861
|
+
<head>
|
|
862
|
+
<meta charset="UTF-8">
|
|
863
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
864
|
+
<title>${templateConfig.name} - ${page.name}</title>
|
|
865
|
+
<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">
|
|
866
|
+
<script type="module" src="/@vite/client"></script>
|
|
867
|
+
<style>
|
|
868
|
+
* { box-sizing: border-box; }
|
|
869
|
+
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
870
|
+
|
|
871
|
+
.template-header {
|
|
872
|
+
position: fixed; top: 0; left: 0; right: 0;
|
|
873
|
+
background: #1a1a2e; color: white;
|
|
874
|
+
padding: 0.75rem 1.5rem; z-index: 1000;
|
|
875
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
876
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
877
|
+
}
|
|
878
|
+
.template-header-left {
|
|
879
|
+
display: flex; align-items: center; gap: 1.5rem;
|
|
880
|
+
}
|
|
881
|
+
.template-title {
|
|
882
|
+
font-size: 1rem; font-weight: 600; margin: 0;
|
|
883
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
884
|
+
}
|
|
885
|
+
.template-badge {
|
|
886
|
+
background: #667eea; color: white;
|
|
887
|
+
padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
888
|
+
font-size: 0.7rem; font-weight: 500;
|
|
889
|
+
}
|
|
890
|
+
.page-tabs {
|
|
891
|
+
display: flex; gap: 0.25rem;
|
|
892
|
+
}
|
|
893
|
+
.page-tab {
|
|
894
|
+
color: rgba(255,255,255,0.7); text-decoration: none;
|
|
895
|
+
padding: 0.4rem 0.75rem; border-radius: 6px;
|
|
896
|
+
font-size: 0.85rem; font-weight: 500;
|
|
897
|
+
transition: all 0.2s;
|
|
898
|
+
}
|
|
899
|
+
.page-tab:hover { color: white; background: rgba(255,255,255,0.1); }
|
|
900
|
+
.page-tab.active { color: white; background: #667eea; }
|
|
901
|
+
|
|
902
|
+
.template-back {
|
|
903
|
+
color: rgba(255,255,255,0.8); text-decoration: none;
|
|
904
|
+
font-size: 0.85rem; font-weight: 500;
|
|
905
|
+
padding: 0.4rem 0.75rem; border-radius: 6px;
|
|
906
|
+
transition: all 0.2s;
|
|
907
|
+
}
|
|
908
|
+
.template-back:hover { color: white; background: rgba(255,255,255,0.1); }
|
|
909
|
+
|
|
910
|
+
.template-content {
|
|
911
|
+
margin-top: 52px;
|
|
912
|
+
min-height: calc(100vh - 52px);
|
|
913
|
+
}
|
|
914
|
+
.template-block {
|
|
915
|
+
/* Blocks render their own styles */
|
|
916
|
+
}
|
|
917
|
+
#layout-header, #layout-footer {
|
|
918
|
+
/* Layout slots */
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.block-error {
|
|
922
|
+
padding: 2rem;
|
|
923
|
+
background: #fff3cd;
|
|
924
|
+
border: 1px solid #ffc107;
|
|
925
|
+
color: #856404;
|
|
926
|
+
text-align: center;
|
|
927
|
+
}
|
|
928
|
+
</style>
|
|
929
|
+
</head>
|
|
930
|
+
<body>
|
|
931
|
+
<div class="template-header">
|
|
932
|
+
<div class="template-header-left">
|
|
933
|
+
<h1 class="template-title">
|
|
934
|
+
<span class="template-badge">Template</span>
|
|
935
|
+
${templateConfig.name}
|
|
936
|
+
</h1>
|
|
937
|
+
<div class="page-tabs">
|
|
938
|
+
${pageTabs}
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
<a href="/" class="template-back" target="_parent">← Back to Dev</a>
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
<div class="template-content">
|
|
945
|
+
${headerSlot ? '<div id="layout-header"></div>' : ''}
|
|
946
|
+
<main>
|
|
947
|
+
${blockContainers || '<div class="block-error">No blocks defined for this page</div>'}
|
|
948
|
+
</main>
|
|
949
|
+
${footerSlot ? '<div id="layout-footer"></div>' : ''}
|
|
950
|
+
</div>
|
|
951
|
+
|
|
952
|
+
<script type="module">
|
|
953
|
+
${blockImports.join('\n ')}
|
|
954
|
+
|
|
955
|
+
// Mount all blocks
|
|
956
|
+
${blockMounts.join('\n ')}
|
|
957
|
+
</script>
|
|
958
|
+
</body>
|
|
959
|
+
</html>
|
|
960
|
+
`;
|
|
961
|
+
}
|
|
962
|
+
//# sourceMappingURL=dev.js.map
|