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