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