@see-ms/converter 1.0.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/README.md +308 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2919 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.mjs +2330 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2330 @@
|
|
|
1
|
+
// src/converter.ts
|
|
2
|
+
import pc3 from "picocolors";
|
|
3
|
+
import path12 from "path";
|
|
4
|
+
import fs10 from "fs-extra";
|
|
5
|
+
|
|
6
|
+
// src/filesystem.ts
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { glob } from "glob";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
async function scanAssets(webflowDir) {
|
|
13
|
+
const assets = {
|
|
14
|
+
css: [],
|
|
15
|
+
images: [],
|
|
16
|
+
fonts: [],
|
|
17
|
+
js: []
|
|
18
|
+
};
|
|
19
|
+
const cssFiles = await glob("css/**/*.css", { cwd: webflowDir });
|
|
20
|
+
assets.css = cssFiles;
|
|
21
|
+
const imageFiles = await glob("images/**/*", { cwd: webflowDir });
|
|
22
|
+
assets.images = imageFiles;
|
|
23
|
+
const fontFiles = await glob("fonts/**/*", { cwd: webflowDir });
|
|
24
|
+
assets.fonts = fontFiles;
|
|
25
|
+
const jsFiles = await glob("js/**/*.js", { cwd: webflowDir });
|
|
26
|
+
assets.js = jsFiles;
|
|
27
|
+
return assets;
|
|
28
|
+
}
|
|
29
|
+
async function copyCSSFiles(webflowDir, outputDir, cssFiles) {
|
|
30
|
+
const targetDir = path.join(outputDir, "assets", "css");
|
|
31
|
+
await fs.ensureDir(targetDir);
|
|
32
|
+
for (const file of cssFiles) {
|
|
33
|
+
const source = path.join(webflowDir, file);
|
|
34
|
+
const target = path.join(targetDir, path.basename(file));
|
|
35
|
+
await fs.copy(source, target);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function copyImages(webflowDir, outputDir, imageFiles) {
|
|
39
|
+
const targetDir = path.join(outputDir, "public", "assets", "images");
|
|
40
|
+
await fs.ensureDir(targetDir);
|
|
41
|
+
for (const file of imageFiles) {
|
|
42
|
+
const source = path.join(webflowDir, file);
|
|
43
|
+
const target = path.join(targetDir, path.basename(file));
|
|
44
|
+
await fs.copy(source, target);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function copyFonts(webflowDir, outputDir, fontFiles) {
|
|
48
|
+
const targetDir = path.join(outputDir, "public", "assets", "fonts");
|
|
49
|
+
await fs.ensureDir(targetDir);
|
|
50
|
+
for (const file of fontFiles) {
|
|
51
|
+
const source = path.join(webflowDir, file);
|
|
52
|
+
const target = path.join(targetDir, path.basename(file));
|
|
53
|
+
await fs.copy(source, target);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function copyJSFiles(webflowDir, outputDir, jsFiles) {
|
|
57
|
+
const targetDir = path.join(outputDir, "public", "assets", "js");
|
|
58
|
+
await fs.ensureDir(targetDir);
|
|
59
|
+
for (const file of jsFiles) {
|
|
60
|
+
const source = path.join(webflowDir, file);
|
|
61
|
+
const target = path.join(targetDir, path.basename(file));
|
|
62
|
+
await fs.copy(source, target);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function copyAllAssets(webflowDir, outputDir, assets) {
|
|
66
|
+
await copyCSSFiles(webflowDir, outputDir, assets.css);
|
|
67
|
+
await copyImages(webflowDir, outputDir, assets.images);
|
|
68
|
+
await copyFonts(webflowDir, outputDir, assets.fonts);
|
|
69
|
+
await copyJSFiles(webflowDir, outputDir, assets.js);
|
|
70
|
+
}
|
|
71
|
+
async function findHTMLFiles(webflowDir) {
|
|
72
|
+
const htmlFiles = await glob("**/*.html", { cwd: webflowDir });
|
|
73
|
+
return htmlFiles;
|
|
74
|
+
}
|
|
75
|
+
async function readHTMLFile(webflowDir, fileName) {
|
|
76
|
+
const filePath = path.join(webflowDir, fileName);
|
|
77
|
+
return await fs.readFile(filePath, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
async function writeVueComponent(outputDir, fileName, content) {
|
|
80
|
+
const pagesDir = path.join(outputDir, "pages");
|
|
81
|
+
const vueName = fileName.replace(".html", ".vue");
|
|
82
|
+
const targetPath = path.join(pagesDir, vueName);
|
|
83
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
84
|
+
await fs.writeFile(targetPath, content, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
async function formatVueFiles(outputDir) {
|
|
87
|
+
const pagesDir = path.join(outputDir, "pages");
|
|
88
|
+
try {
|
|
89
|
+
console.log(pc.blue("\n\u2728 Formatting Vue files with Prettier..."));
|
|
90
|
+
execSync("npx prettier --version", { stdio: "ignore" });
|
|
91
|
+
execSync(`npx prettier --write "${pagesDir}/**/*.vue"`, {
|
|
92
|
+
cwd: outputDir,
|
|
93
|
+
stdio: "inherit"
|
|
94
|
+
});
|
|
95
|
+
console.log(pc.green(" \u2713 Vue files formatted"));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.log(pc.yellow(" \u26A0 Prettier not available, skipping formatting"));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/parser.ts
|
|
102
|
+
import * as cheerio from "cheerio";
|
|
103
|
+
import path2 from "path";
|
|
104
|
+
function normalizeRoute(href) {
|
|
105
|
+
let route = href.replace(".html", "");
|
|
106
|
+
if (route === "index" || route === "/index" || route.endsWith("/index")) {
|
|
107
|
+
return "/";
|
|
108
|
+
}
|
|
109
|
+
if (route === ".." || route === "../" || route === "/.." || route === "../index") {
|
|
110
|
+
return "/";
|
|
111
|
+
}
|
|
112
|
+
route = route.replace(/\.\.\//g, "").replace(/\.\//g, "");
|
|
113
|
+
const normalized = path2.posix.normalize(route);
|
|
114
|
+
if (!normalized.startsWith("/")) {
|
|
115
|
+
return "/" + normalized;
|
|
116
|
+
}
|
|
117
|
+
if (normalized === "." || normalized === "") {
|
|
118
|
+
return "/";
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
function normalizeAssetPath(src) {
|
|
123
|
+
if (!src || src.startsWith("http") || src.startsWith("https")) {
|
|
124
|
+
return src;
|
|
125
|
+
}
|
|
126
|
+
let normalized = src.replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
127
|
+
if (normalized.startsWith("/assets/")) {
|
|
128
|
+
normalized = normalized.replace(/\/\.\.\//g, "/");
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
return `/assets/${normalized}`;
|
|
132
|
+
}
|
|
133
|
+
function parseHTML(html, fileName) {
|
|
134
|
+
const $ = cheerio.load(html);
|
|
135
|
+
const title = $("title").text() || fileName.replace(".html", "");
|
|
136
|
+
const cssFiles = [];
|
|
137
|
+
$('link[rel="stylesheet"]').each((_, el) => {
|
|
138
|
+
const href = $(el).attr("href");
|
|
139
|
+
if (href) {
|
|
140
|
+
cssFiles.push(href);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
let embeddedStyles = "";
|
|
144
|
+
$(".global-embed style").each((_, el) => {
|
|
145
|
+
embeddedStyles += $(el).html() + "\n";
|
|
146
|
+
});
|
|
147
|
+
$("body > style").each((_, el) => {
|
|
148
|
+
embeddedStyles += $(el).html() + "\n";
|
|
149
|
+
});
|
|
150
|
+
$(".global-embed").remove();
|
|
151
|
+
$("body > style").remove();
|
|
152
|
+
$("body script").remove();
|
|
153
|
+
const images = [];
|
|
154
|
+
$("img").each((_, el) => {
|
|
155
|
+
const src = $(el).attr("src");
|
|
156
|
+
if (src) {
|
|
157
|
+
images.push(src);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const links = [];
|
|
161
|
+
$("a").each((_, el) => {
|
|
162
|
+
const href = $(el).attr("href");
|
|
163
|
+
if (href) {
|
|
164
|
+
links.push(href);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const htmlContent = $("body").html() || "";
|
|
168
|
+
return {
|
|
169
|
+
fileName,
|
|
170
|
+
title,
|
|
171
|
+
htmlContent,
|
|
172
|
+
cssFiles,
|
|
173
|
+
embeddedStyles,
|
|
174
|
+
images,
|
|
175
|
+
links
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function transformForNuxt(html) {
|
|
179
|
+
const $ = cheerio.load(html);
|
|
180
|
+
$("html, head, body").each((_, el) => {
|
|
181
|
+
const $el = $(el);
|
|
182
|
+
$el.replaceWith($el.html() || "");
|
|
183
|
+
});
|
|
184
|
+
$("script").remove();
|
|
185
|
+
$("a").each((_, el) => {
|
|
186
|
+
const $el = $(el);
|
|
187
|
+
const href = $el.attr("href");
|
|
188
|
+
if (!href) return;
|
|
189
|
+
const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#");
|
|
190
|
+
if (!isExternal) {
|
|
191
|
+
const route = normalizeRoute(href);
|
|
192
|
+
$el.attr("to", route);
|
|
193
|
+
$el.removeAttr("href");
|
|
194
|
+
const content = $el.html();
|
|
195
|
+
const classes = $el.attr("class") || "";
|
|
196
|
+
$el.replaceWith(`<nuxt-link to="${route}" class="${classes}">${content}</nuxt-link>`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
$("img").each((_, el) => {
|
|
200
|
+
const $el = $(el);
|
|
201
|
+
const src = $el.attr("src");
|
|
202
|
+
if (src) {
|
|
203
|
+
const normalizedSrc = normalizeAssetPath(src);
|
|
204
|
+
$el.attr("src", normalizedSrc);
|
|
205
|
+
}
|
|
206
|
+
$el.removeAttr("srcset");
|
|
207
|
+
$el.removeAttr("sizes");
|
|
208
|
+
});
|
|
209
|
+
return $.html();
|
|
210
|
+
}
|
|
211
|
+
function htmlToVueComponent(html, pageName) {
|
|
212
|
+
return `
|
|
213
|
+
<script setup lang="ts">
|
|
214
|
+
// Page: ${pageName}
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<div>
|
|
219
|
+
${html}
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
function deduplicateStyles(styles) {
|
|
225
|
+
if (!styles.trim()) return "";
|
|
226
|
+
const sections = styles.split(/\/\* From .+ \*\//);
|
|
227
|
+
const uniqueStyles = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const section of sections) {
|
|
229
|
+
const trimmed = section.trim();
|
|
230
|
+
if (trimmed) {
|
|
231
|
+
uniqueStyles.add(trimmed);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return Array.from(uniqueStyles).join("\n\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/config-updater.ts
|
|
238
|
+
import fs2 from "fs-extra";
|
|
239
|
+
import path3 from "path";
|
|
240
|
+
function generateWebflowAssetPlugin(cssFiles) {
|
|
241
|
+
const webflowFiles = cssFiles.map((file) => `/assets/css/${path3.basename(file)}`);
|
|
242
|
+
return `import type { Plugin } from 'vite'
|
|
243
|
+
|
|
244
|
+
const webflowFiles = [${webflowFiles.map((f) => `'${f}'`).join(", ")}]
|
|
245
|
+
const replacements = [
|
|
246
|
+
['../images/', '/assets/images/'],
|
|
247
|
+
['../fonts/', '/assets/fonts/']
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
const webflowURLReset = (): Plugin => ({
|
|
251
|
+
name: 'webflowURLReset',
|
|
252
|
+
config: () => ({
|
|
253
|
+
build: {
|
|
254
|
+
rollupOptions: {
|
|
255
|
+
external: [/\\.\\.\\/fonts\\//, /\\.\\.\\/images\\//]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
transform: (code, id) => {
|
|
260
|
+
if (webflowFiles.some((path) => id.includes(path))) {
|
|
261
|
+
replacements.forEach(([search, replace]) => {
|
|
262
|
+
code = code.replaceAll(search, replace)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { code, id, map: null }
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
export default webflowURLReset
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
async function writeWebflowAssetPlugin(outputDir, cssFiles) {
|
|
274
|
+
const utilsDir = path3.join(outputDir, "utils");
|
|
275
|
+
await fs2.ensureDir(utilsDir);
|
|
276
|
+
const content = generateWebflowAssetPlugin(cssFiles);
|
|
277
|
+
const targetPath = path3.join(utilsDir, "webflow-assets.ts");
|
|
278
|
+
await fs2.writeFile(targetPath, content, "utf-8");
|
|
279
|
+
}
|
|
280
|
+
async function updateNuxtConfig(outputDir, cssFiles) {
|
|
281
|
+
const configPath = path3.join(outputDir, "nuxt.config.ts");
|
|
282
|
+
const configExists = await fs2.pathExists(configPath);
|
|
283
|
+
if (!configExists) {
|
|
284
|
+
throw new Error("nuxt.config.ts not found in output directory");
|
|
285
|
+
}
|
|
286
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
287
|
+
const cssEntries = cssFiles.map((file) => ` '~/assets/css/${path3.basename(file)}'`);
|
|
288
|
+
if (config.includes("css:")) {
|
|
289
|
+
config = config.replace(
|
|
290
|
+
/css:\s*\[/,
|
|
291
|
+
`css: [
|
|
292
|
+
${cssEntries.join(",\n")},`
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
config = config.replace(
|
|
296
|
+
/export default defineNuxtConfig\(\{/,
|
|
297
|
+
`export default defineNuxtConfig({
|
|
298
|
+
css: [
|
|
299
|
+
${cssEntries.join(",\n")}
|
|
300
|
+
],`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
await fs2.writeFile(configPath, config, "utf-8");
|
|
304
|
+
}
|
|
305
|
+
async function writeEmbeddedStyles(outputDir, styles) {
|
|
306
|
+
if (!styles.trim()) return;
|
|
307
|
+
const cssDir = path3.join(outputDir, "assets", "css");
|
|
308
|
+
await fs2.ensureDir(cssDir);
|
|
309
|
+
const mainCssPath = path3.join(cssDir, "main.css");
|
|
310
|
+
const exists = await fs2.pathExists(mainCssPath);
|
|
311
|
+
if (exists) {
|
|
312
|
+
const existing = await fs2.readFile(mainCssPath, "utf-8");
|
|
313
|
+
await fs2.writeFile(mainCssPath, `${existing}
|
|
314
|
+
|
|
315
|
+
/* Webflow Embedded Styles */
|
|
316
|
+
${styles}`, "utf-8");
|
|
317
|
+
} else {
|
|
318
|
+
await fs2.writeFile(mainCssPath, `/* Webflow Embedded Styles */
|
|
319
|
+
${styles}`, "utf-8");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:1337") {
|
|
323
|
+
const configPath = path3.join(outputDir, "nuxt.config.ts");
|
|
324
|
+
const configExists = await fs2.pathExists(configPath);
|
|
325
|
+
if (!configExists) {
|
|
326
|
+
throw new Error("nuxt.config.ts not found in output directory");
|
|
327
|
+
}
|
|
328
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
329
|
+
if (config.includes("runtimeConfig:")) {
|
|
330
|
+
if (config.includes("public:")) {
|
|
331
|
+
config = config.replace(
|
|
332
|
+
/public:\s*\{/,
|
|
333
|
+
`public: {
|
|
334
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}',`
|
|
335
|
+
);
|
|
336
|
+
} else {
|
|
337
|
+
config = config.replace(
|
|
338
|
+
/runtimeConfig:\s*\{/,
|
|
339
|
+
`runtimeConfig: {
|
|
340
|
+
public: {
|
|
341
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
|
|
342
|
+
},`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
config = config.replace(
|
|
347
|
+
/export default defineNuxtConfig\(\{/,
|
|
348
|
+
`export default defineNuxtConfig({
|
|
349
|
+
runtimeConfig: {
|
|
350
|
+
public: {
|
|
351
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
|
|
352
|
+
}
|
|
353
|
+
},`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
await fs2.writeFile(configPath, config, "utf-8");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/editor-integration.ts
|
|
360
|
+
import fs3 from "fs-extra";
|
|
361
|
+
import path4 from "path";
|
|
362
|
+
async function createEditorContentComposable(outputDir) {
|
|
363
|
+
const composablesDir = path4.join(outputDir, "composables");
|
|
364
|
+
await fs3.ensureDir(composablesDir);
|
|
365
|
+
const composableContent = `/**
|
|
366
|
+
* Global state for editor content in preview mode
|
|
367
|
+
* This allows the editor overlay to update content reactively
|
|
368
|
+
*/
|
|
369
|
+
|
|
370
|
+
// Global reactive state
|
|
371
|
+
const editorState = reactive<{
|
|
372
|
+
isPreviewMode: boolean;
|
|
373
|
+
currentPage: string | null;
|
|
374
|
+
content: Record<string, Record<string, any>>; // page -> field -> value
|
|
375
|
+
hasChanges: Record<string, boolean>; // page -> hasChanges
|
|
376
|
+
}>({
|
|
377
|
+
isPreviewMode: false,
|
|
378
|
+
currentPage: null,
|
|
379
|
+
content: {},
|
|
380
|
+
hasChanges: {},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
export function useEditorContent(pageName?: string) {
|
|
384
|
+
const route = useRoute();
|
|
385
|
+
|
|
386
|
+
// Check if we're in preview mode
|
|
387
|
+
const isPreviewMode = computed(() => route.query.preview === 'true');
|
|
388
|
+
|
|
389
|
+
// Update global state
|
|
390
|
+
if (import.meta.client) {
|
|
391
|
+
editorState.isPreviewMode = isPreviewMode.value;
|
|
392
|
+
if (pageName) {
|
|
393
|
+
editorState.currentPage = pageName;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Get content for specific page
|
|
398
|
+
const getPageContent = (page: string) => {
|
|
399
|
+
return editorState.content[page] || {};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Update a field's value
|
|
403
|
+
const updateField = (page: string, fieldName: string, value: any) => {
|
|
404
|
+
if (!editorState.content[page]) {
|
|
405
|
+
editorState.content[page] = {};
|
|
406
|
+
}
|
|
407
|
+
editorState.content[page][fieldName] = value;
|
|
408
|
+
editorState.hasChanges[page] = true;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Clear all changes for a page
|
|
412
|
+
const clearPageChanges = (page: string) => {
|
|
413
|
+
delete editorState.content[page];
|
|
414
|
+
editorState.hasChanges[page] = false;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Initialize page content from Strapi data
|
|
418
|
+
const initializePageContent = (page: string, content: Record<string, any>) => {
|
|
419
|
+
if (!editorState.content[page]) {
|
|
420
|
+
editorState.content[page] = { ...content };
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Get content for current page (reactive)
|
|
425
|
+
const content = computed(() => {
|
|
426
|
+
const page = pageName || editorState.currentPage;
|
|
427
|
+
if (!page) return {};
|
|
428
|
+
return editorState.content[page] || {};
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Check if page has unsaved changes
|
|
432
|
+
const hasChanges = computed(() => {
|
|
433
|
+
const page = pageName || editorState.currentPage;
|
|
434
|
+
if (!page) return false;
|
|
435
|
+
return editorState.hasChanges[page] || false;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Get all pages with changes
|
|
439
|
+
const pagesWithChanges = computed(() => {
|
|
440
|
+
return Object.keys(editorState.hasChanges).filter(
|
|
441
|
+
(page) => editorState.hasChanges[page]
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Expose state for window object (for editor overlay to access)
|
|
446
|
+
if (import.meta.client) {
|
|
447
|
+
(window as any).__editorState = editorState;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
isPreviewMode,
|
|
452
|
+
content,
|
|
453
|
+
hasChanges,
|
|
454
|
+
pagesWithChanges,
|
|
455
|
+
getPageContent,
|
|
456
|
+
updateField,
|
|
457
|
+
clearPageChanges,
|
|
458
|
+
initializePageContent,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
`;
|
|
462
|
+
const composablePath = path4.join(composablesDir, "useEditorContent.ts");
|
|
463
|
+
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
464
|
+
}
|
|
465
|
+
async function createStrapiContentComposable(outputDir) {
|
|
466
|
+
const composablesDir = path4.join(outputDir, "composables");
|
|
467
|
+
await fs3.ensureDir(composablesDir);
|
|
468
|
+
const composableContent = `/**
|
|
469
|
+
* Composable to fetch content from Strapi based on CMS manifest
|
|
470
|
+
* Integrates with editor state for preview mode
|
|
471
|
+
*/
|
|
472
|
+
|
|
473
|
+
export function useStrapiContent(pageName: string) {
|
|
474
|
+
const config = useRuntimeConfig();
|
|
475
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
476
|
+
const editorContent = useEditorContent(pageName);
|
|
477
|
+
|
|
478
|
+
// Helper to transform Strapi image objects to URL strings
|
|
479
|
+
const transformStrapiImages = (data: any, baseUrl: string): any => {
|
|
480
|
+
if (!data || typeof data !== 'object') return data;
|
|
481
|
+
|
|
482
|
+
const transformed: any = {};
|
|
483
|
+
|
|
484
|
+
for (const [key, value] of Object.entries(data)) {
|
|
485
|
+
if (value && typeof value === 'object') {
|
|
486
|
+
// Check if it's a Strapi media object
|
|
487
|
+
if ('url' in value && ('mime' in value || 'formats' in value)) {
|
|
488
|
+
// It's an image - extract the URL
|
|
489
|
+
transformed[key] = value.url.startsWith('http')
|
|
490
|
+
? value.url
|
|
491
|
+
: \`\${baseUrl}\${value.url}\`;
|
|
492
|
+
} else if (Array.isArray(value)) {
|
|
493
|
+
// Handle arrays (collections of images)
|
|
494
|
+
transformed[key] = value.map((item) =>
|
|
495
|
+
item && typeof item === 'object' && 'url' in item
|
|
496
|
+
? item.url.startsWith('http')
|
|
497
|
+
? item.url
|
|
498
|
+
: \`\${baseUrl}\${item.url}\`
|
|
499
|
+
: item
|
|
500
|
+
);
|
|
501
|
+
} else {
|
|
502
|
+
// Recursively transform nested objects
|
|
503
|
+
transformed[key] = transformStrapiImages(value, baseUrl);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
transformed[key] = value;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return transformed;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Fetch content from Strapi with populated media fields
|
|
514
|
+
const { data: strapiData } = useFetch<any>(
|
|
515
|
+
\`\${strapiUrl}/api/\${pageName}\`,
|
|
516
|
+
{
|
|
517
|
+
key: \`strapi-\${pageName}\`,
|
|
518
|
+
query: {
|
|
519
|
+
populate: '*', // Strapi v5: Populate all fields including images
|
|
520
|
+
},
|
|
521
|
+
transform: (response) => {
|
|
522
|
+
// Strapi v5 returns data in response.data
|
|
523
|
+
const data = response?.data || response;
|
|
524
|
+
|
|
525
|
+
// Transform image fields from Strapi objects to URL strings
|
|
526
|
+
if (data && typeof data === 'object') {
|
|
527
|
+
return transformStrapiImages(data, strapiUrl);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return data;
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// Initialize editor state with Strapi data when fetched
|
|
536
|
+
// This runs in both normal AND preview mode to ensure initial content is available
|
|
537
|
+
watch(
|
|
538
|
+
strapiData,
|
|
539
|
+
(newData) => {
|
|
540
|
+
if (newData) {
|
|
541
|
+
// Always initialize from Strapi on first load
|
|
542
|
+
// Drafts will override this when they load in the editor
|
|
543
|
+
editorContent.initializePageContent(pageName, newData);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
{ immediate: true }
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// In preview mode: use editor state
|
|
550
|
+
// In normal mode: use Strapi data (and sync to editor state)
|
|
551
|
+
const content = computed(() => {
|
|
552
|
+
if (editorContent.isPreviewMode.value) {
|
|
553
|
+
// Use editor state in preview mode
|
|
554
|
+
return editorContent.getPageContent(pageName);
|
|
555
|
+
} else {
|
|
556
|
+
// Use Strapi data in normal mode
|
|
557
|
+
return strapiData.value || editorContent.getPageContent(pageName);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
content,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
`;
|
|
566
|
+
const composablePath = path4.join(composablesDir, "useStrapiContent.ts");
|
|
567
|
+
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
568
|
+
}
|
|
569
|
+
async function createEditorPlugin(outputDir) {
|
|
570
|
+
const pluginsDir = path4.join(outputDir, "plugins");
|
|
571
|
+
await fs3.ensureDir(pluginsDir);
|
|
572
|
+
const pluginContent = `/**
|
|
573
|
+
* CMS Editor Overlay Plugin
|
|
574
|
+
* Loads the inline editor when ?preview=true with full state management
|
|
575
|
+
*/
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Disable Lenis smooth scroll to allow native scrolling in edit mode
|
|
579
|
+
*/
|
|
580
|
+
function disableLenisInEditMode() {
|
|
581
|
+
try {
|
|
582
|
+
// Check for Lenis in common locations
|
|
583
|
+
const lenisInstances = [
|
|
584
|
+
(window as any).lenis,
|
|
585
|
+
(window as any).__lenis,
|
|
586
|
+
document.querySelector('.lenis'),
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
for (const lenis of lenisInstances) {
|
|
590
|
+
if (lenis && typeof lenis.destroy === 'function') {
|
|
591
|
+
lenis.destroy();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check for Vue Lenis component instances
|
|
597
|
+
const lenisElements = document.querySelectorAll('[data-lenis], .lenis');
|
|
598
|
+
if (lenisElements.length > 0) {
|
|
599
|
+
// Try to find and destroy via data attributes or component instances
|
|
600
|
+
lenisElements.forEach((el: any) => {
|
|
601
|
+
if (el.__lenis && typeof el.__lenis.destroy === 'function') {
|
|
602
|
+
el.__lenis.destroy();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
// Silently fail - Lenis may not be present
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
612
|
+
// Only run on client side
|
|
613
|
+
if (process.server) return;
|
|
614
|
+
|
|
615
|
+
// Import editor overlay modules
|
|
616
|
+
const {
|
|
617
|
+
initEditor,
|
|
618
|
+
createAuthManager,
|
|
619
|
+
showLoginModal,
|
|
620
|
+
createDraftStorage,
|
|
621
|
+
createURLStateManager,
|
|
622
|
+
createManifestLoader,
|
|
623
|
+
createNavigationGuard,
|
|
624
|
+
getCurrentPageFromRoute,
|
|
625
|
+
} = await import('@see-ms/editor-overlay');
|
|
626
|
+
|
|
627
|
+
// Initialize URL state manager
|
|
628
|
+
const urlState = createURLStateManager();
|
|
629
|
+
const state = urlState.getState();
|
|
630
|
+
|
|
631
|
+
// Only proceed if in preview mode
|
|
632
|
+
if (!state.preview) return;
|
|
633
|
+
|
|
634
|
+
// Get Strapi URL from runtime config
|
|
635
|
+
const config = useRuntimeConfig();
|
|
636
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
637
|
+
|
|
638
|
+
// Initialize components
|
|
639
|
+
const authManager = createAuthManager({
|
|
640
|
+
strapiUrl,
|
|
641
|
+
storageKey: 'cms_editor_token',
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const draftStorage = createDraftStorage();
|
|
645
|
+
const manifestLoader = createManifestLoader();
|
|
646
|
+
|
|
647
|
+
// Load manifest
|
|
648
|
+
try {
|
|
649
|
+
await manifestLoader.load();
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error('[CMS Editor] Failed to load manifest:', error);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Get current page from route
|
|
656
|
+
let currentPage = getCurrentPageFromRoute();
|
|
657
|
+
if (!currentPage) {
|
|
658
|
+
currentPage = manifestLoader.getPageFromRoute(window.location.pathname);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (!currentPage) {
|
|
662
|
+
console.error('[CMS Editor] Could not determine current page');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// URL state only manages preview mode (page is derived from route)
|
|
667
|
+
urlState.setState({ preview: true });
|
|
668
|
+
|
|
669
|
+
// Auth flow
|
|
670
|
+
let token = authManager.getToken();
|
|
671
|
+
if (!token || !await authManager.verifyToken(token)) {
|
|
672
|
+
try {
|
|
673
|
+
token = await showLoginModal(authManager);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
// Login cancelled - exit preview mode
|
|
676
|
+
console.log('[CMS Editor] Login cancelled');
|
|
677
|
+
urlState.clearPreviewMode();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Disable Lenis smooth scroll in edit mode (allows native scrolling)
|
|
683
|
+
disableLenisInEditMode();
|
|
684
|
+
|
|
685
|
+
// Initialize navigation guard
|
|
686
|
+
const navigationGuard = createNavigationGuard({
|
|
687
|
+
showToast: true,
|
|
688
|
+
toastMessage: 'Navigation disabled in edit mode',
|
|
689
|
+
});
|
|
690
|
+
navigationGuard.enable();
|
|
691
|
+
|
|
692
|
+
// Initialize editor with full context
|
|
693
|
+
const editor = initEditor({
|
|
694
|
+
apiEndpoint: '/api/cms/save',
|
|
695
|
+
authToken: token,
|
|
696
|
+
richText: true,
|
|
697
|
+
manifestLoader,
|
|
698
|
+
draftStorage,
|
|
699
|
+
currentPage,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Enable editor (will auto-load drafts)
|
|
703
|
+
await editor.enable();
|
|
704
|
+
|
|
705
|
+
// Create toolbar with navigation
|
|
706
|
+
const { createToolbar } = await import('@see-ms/editor-overlay');
|
|
707
|
+
const toolbar = await createToolbar(editor, {
|
|
708
|
+
draftStorage,
|
|
709
|
+
urlState,
|
|
710
|
+
navigationGuard,
|
|
711
|
+
manifestLoader,
|
|
712
|
+
currentPage,
|
|
713
|
+
});
|
|
714
|
+
document.body.appendChild(toolbar);
|
|
715
|
+
|
|
716
|
+
// Watch for route changes
|
|
717
|
+
const router = useRouter();
|
|
718
|
+
router.afterEach(async (to) => {
|
|
719
|
+
const newPage = manifestLoader.getPageFromRoute(to.path);
|
|
720
|
+
if (newPage && newPage !== currentPage) {
|
|
721
|
+
currentPage = newPage;
|
|
722
|
+
await editor.setPage(newPage);
|
|
723
|
+
|
|
724
|
+
// Update toolbar if it has an update method
|
|
725
|
+
if (typeof (toolbar as any).updateCurrentPage === 'function') {
|
|
726
|
+
await (toolbar as any).updateCurrentPage(newPage);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Cleanup on navigation away from preview mode
|
|
732
|
+
nuxtApp.hook('page:finish', () => {
|
|
733
|
+
const currentState = urlState.getState();
|
|
734
|
+
if (!currentState.preview) {
|
|
735
|
+
navigationGuard.disable();
|
|
736
|
+
editor.destroy();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
`;
|
|
741
|
+
const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
|
|
742
|
+
await fs3.writeFile(pluginPath, pluginContent, "utf-8");
|
|
743
|
+
}
|
|
744
|
+
async function addEditorDependency(outputDir) {
|
|
745
|
+
const packageJsonPath = path4.join(outputDir, "package.json");
|
|
746
|
+
if (await fs3.pathExists(packageJsonPath)) {
|
|
747
|
+
const packageJson = await fs3.readJson(packageJsonPath);
|
|
748
|
+
if (!packageJson.dependencies) {
|
|
749
|
+
packageJson.dependencies = {};
|
|
750
|
+
}
|
|
751
|
+
packageJson.dependencies["@see-ms/editor-overlay"] = "^1.0.0";
|
|
752
|
+
await fs3.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async function createSaveEndpoint(outputDir) {
|
|
756
|
+
const serverDir = path4.join(outputDir, "server", "api", "cms");
|
|
757
|
+
await fs3.ensureDir(serverDir);
|
|
758
|
+
const endpointContent = `/**
|
|
759
|
+
* API endpoint for saving CMS changes
|
|
760
|
+
* Handles draft and final saving to Strapi
|
|
761
|
+
*/
|
|
762
|
+
|
|
763
|
+
import fs from 'fs';
|
|
764
|
+
import path from 'path';
|
|
765
|
+
|
|
766
|
+
export default defineEventHandler(async (event) => {
|
|
767
|
+
// Get Strapi URL from runtime config
|
|
768
|
+
const config = useRuntimeConfig();
|
|
769
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
770
|
+
|
|
771
|
+
// Extract Authorization header
|
|
772
|
+
const authHeader = getHeader(event, 'authorization');
|
|
773
|
+
|
|
774
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
775
|
+
throw createError({
|
|
776
|
+
statusCode: 401,
|
|
777
|
+
statusMessage: 'Unauthorized: Missing or invalid authorization header',
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
782
|
+
|
|
783
|
+
// Verify token with Strapi and determine if it's an admin or user token
|
|
784
|
+
let userResponse: any;
|
|
785
|
+
let isAdminToken = false;
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
// Try admin token verification first
|
|
789
|
+
try {
|
|
790
|
+
userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
791
|
+
headers: {
|
|
792
|
+
Authorization: \`Bearer \${token}\`,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
isAdminToken = true;
|
|
796
|
+
} catch (adminError) {
|
|
797
|
+
// Fallback to regular user token verification
|
|
798
|
+
userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
799
|
+
headers: {
|
|
800
|
+
Authorization: \`Bearer \${token}\`,
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
isAdminToken = false;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Get the request body
|
|
807
|
+
const body = await readBody(event);
|
|
808
|
+
const { page, fields, isDraft = true } = body;
|
|
809
|
+
|
|
810
|
+
if (!page || !fields) {
|
|
811
|
+
throw createError({
|
|
812
|
+
statusCode: 400,
|
|
813
|
+
statusMessage: 'Bad Request: Missing page or fields',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Load manifest to understand field mappings
|
|
818
|
+
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
819
|
+
let manifest;
|
|
820
|
+
try {
|
|
821
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
822
|
+
manifest = JSON.parse(manifestContent);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
console.error('Failed to load manifest:', error);
|
|
825
|
+
throw createError({
|
|
826
|
+
statusCode: 500,
|
|
827
|
+
statusMessage: 'Failed to load CMS manifest',
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Get page configuration from manifest
|
|
832
|
+
const pageConfig = manifest.pages[page];
|
|
833
|
+
if (!pageConfig) {
|
|
834
|
+
throw createError({
|
|
835
|
+
statusCode: 404,
|
|
836
|
+
statusMessage: \`Page "\${page}" not found in manifest\`,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Transform fields to Strapi format
|
|
841
|
+
const strapiData: Record<string, any> = {};
|
|
842
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
843
|
+
const fieldConfig = pageConfig.fields[fieldName];
|
|
844
|
+
if (!fieldConfig) {
|
|
845
|
+
console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Handle different field types
|
|
850
|
+
if (fieldConfig.type === 'image') {
|
|
851
|
+
// TODO: Handle image uploads - for now just store the value
|
|
852
|
+
strapiData[fieldName] = value;
|
|
853
|
+
} else {
|
|
854
|
+
strapiData[fieldName] = value;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Update Strapi v5 content - use different endpoints for admin vs user tokens
|
|
859
|
+
if (isAdminToken) {
|
|
860
|
+
// Admin tokens use the content-manager API (Strapi v5)
|
|
861
|
+
const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
862
|
+
|
|
863
|
+
// Step 1: Update the content
|
|
864
|
+
await $fetch(contentEndpoint, {
|
|
865
|
+
method: 'PUT',
|
|
866
|
+
headers: {
|
|
867
|
+
'Authorization': \`Bearer \${token}\`,
|
|
868
|
+
'Content-Type': 'application/json',
|
|
869
|
+
},
|
|
870
|
+
body: strapiData,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// Step 2: Publish if not a draft (Strapi v5)
|
|
874
|
+
if (!isDraft) {
|
|
875
|
+
const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
|
|
876
|
+
await $fetch(publishEndpoint, {
|
|
877
|
+
method: 'POST',
|
|
878
|
+
headers: {
|
|
879
|
+
'Authorization': \`Bearer \${token}\`,
|
|
880
|
+
'Content-Type': 'application/json',
|
|
881
|
+
},
|
|
882
|
+
body: {},
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
// User tokens use the regular REST API
|
|
887
|
+
const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
|
|
888
|
+
|
|
889
|
+
await $fetch(strapiEndpoint, {
|
|
890
|
+
method: 'PUT',
|
|
891
|
+
headers: {
|
|
892
|
+
'Authorization': \`Bearer \${token}\`,
|
|
893
|
+
'Content-Type': 'application/json',
|
|
894
|
+
},
|
|
895
|
+
body: {
|
|
896
|
+
data: strapiData,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Publish if not a draft (Strapi v5)
|
|
901
|
+
if (!isDraft) {
|
|
902
|
+
const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
|
|
903
|
+
await $fetch(publishEndpoint, {
|
|
904
|
+
method: 'POST',
|
|
905
|
+
headers: {
|
|
906
|
+
'Authorization': \`Bearer \${token}\`,
|
|
907
|
+
'Content-Type': 'application/json',
|
|
908
|
+
},
|
|
909
|
+
body: {},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
console.log(\`[CMS Save] Updated "\${page}" in Strapi (draft: \${isDraft})\`);
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
success: true,
|
|
918
|
+
message: 'Changes saved successfully',
|
|
919
|
+
page,
|
|
920
|
+
isDraft,
|
|
921
|
+
user: {
|
|
922
|
+
id: userResponse.id,
|
|
923
|
+
username: userResponse.username || userResponse.firstname || 'Unknown',
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
} catch (error: any) {
|
|
927
|
+
console.error('[CMS Save] Error:', error);
|
|
928
|
+
|
|
929
|
+
// Token verification failed
|
|
930
|
+
if (error.statusCode === 401 || error.status === 401) {
|
|
931
|
+
throw createError({
|
|
932
|
+
statusCode: 401,
|
|
933
|
+
statusMessage: 'Unauthorized: Invalid or expired token',
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Strapi error
|
|
938
|
+
if (error.statusCode || error.status) {
|
|
939
|
+
throw createError({
|
|
940
|
+
statusCode: error.statusCode || error.status,
|
|
941
|
+
statusMessage: error.statusMessage || error.message || 'Failed to save to Strapi',
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Generic error
|
|
946
|
+
throw createError({
|
|
947
|
+
statusCode: 500,
|
|
948
|
+
statusMessage: 'Internal server error while saving changes',
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
`;
|
|
953
|
+
const endpointPath = path4.join(serverDir, "save.post.ts");
|
|
954
|
+
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
955
|
+
}
|
|
956
|
+
async function createStrapiBootstrap(outputDir) {
|
|
957
|
+
const strapiBootstrapDir = path4.join(outputDir, "strapi-bootstrap");
|
|
958
|
+
await fs3.ensureDir(strapiBootstrapDir);
|
|
959
|
+
const bootstrapContent = `/**
|
|
960
|
+
* Strapi Bootstrap File
|
|
961
|
+
* Auto-enables public read permissions for all single types
|
|
962
|
+
*
|
|
963
|
+
* Place this file in your Strapi project at: src/index.ts
|
|
964
|
+
*/
|
|
965
|
+
|
|
966
|
+
export default {
|
|
967
|
+
/**
|
|
968
|
+
* Bootstrap function runs when Strapi starts
|
|
969
|
+
*/
|
|
970
|
+
async bootstrap({ strapi }: { strapi: any }) {
|
|
971
|
+
try {
|
|
972
|
+
console.log('[Bootstrap] Configuring public permissions for CMS...');
|
|
973
|
+
|
|
974
|
+
// Get the public role
|
|
975
|
+
const publicRole = await strapi
|
|
976
|
+
.query('plugin::users-permissions.role')
|
|
977
|
+
.findOne({ where: { type: 'public' } });
|
|
978
|
+
|
|
979
|
+
if (!publicRole) {
|
|
980
|
+
console.error('[Bootstrap] Public role not found');
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Get all content types
|
|
985
|
+
const contentTypes = Object.keys(strapi.contentTypes).filter(
|
|
986
|
+
(uid) => uid.startsWith('api::')
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
// Enable find and findOne for each content type
|
|
990
|
+
const permissions = await strapi
|
|
991
|
+
.query('plugin::users-permissions.permission')
|
|
992
|
+
.findMany({
|
|
993
|
+
where: {
|
|
994
|
+
role: publicRole.id,
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
let updatedCount = 0;
|
|
999
|
+
|
|
1000
|
+
for (const contentType of contentTypes) {
|
|
1001
|
+
const [, apiName] = contentType.split('::');
|
|
1002
|
+
const [controllerName] = apiName.split('.');
|
|
1003
|
+
|
|
1004
|
+
// Find or create find permission
|
|
1005
|
+
const findPermission = permissions.find(
|
|
1006
|
+
(p: any) =>
|
|
1007
|
+
p.action === \`api::\${apiName}.find\` ||
|
|
1008
|
+
p.action === 'find' && p.controller === controllerName
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
const findOnePermission = permissions.find(
|
|
1012
|
+
(p: any) =>
|
|
1013
|
+
p.action === \`api::\${apiName}.findOne\` ||
|
|
1014
|
+
p.action === 'findOne' && p.controller === controllerName
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
// Enable find
|
|
1018
|
+
if (findPermission && !findPermission.enabled) {
|
|
1019
|
+
await strapi
|
|
1020
|
+
.query('plugin::users-permissions.permission')
|
|
1021
|
+
.update({
|
|
1022
|
+
where: { id: findPermission.id },
|
|
1023
|
+
data: { enabled: true },
|
|
1024
|
+
});
|
|
1025
|
+
updatedCount++;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Enable findOne
|
|
1029
|
+
if (findOnePermission && !findOnePermission.enabled) {
|
|
1030
|
+
await strapi
|
|
1031
|
+
.query('plugin::users-permissions.permission')
|
|
1032
|
+
.update({
|
|
1033
|
+
where: { id: findOnePermission.id },
|
|
1034
|
+
data: { enabled: true },
|
|
1035
|
+
});
|
|
1036
|
+
updatedCount++;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// If permissions don't exist, create them
|
|
1040
|
+
if (!findPermission) {
|
|
1041
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
1042
|
+
data: {
|
|
1043
|
+
action: \`api::\${apiName}.find\`,
|
|
1044
|
+
role: publicRole.id,
|
|
1045
|
+
enabled: true,
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
updatedCount++;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!findOnePermission) {
|
|
1052
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
1053
|
+
data: {
|
|
1054
|
+
action: \`api::\${apiName}.findOne\`,
|
|
1055
|
+
role: publicRole.id,
|
|
1056
|
+
enabled: true,
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
updatedCount++;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
console.log(
|
|
1064
|
+
\`[Bootstrap] \u2705 Enabled \${updatedCount} public permissions for \${contentTypes.length} content types\`
|
|
1065
|
+
);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
console.error('[Bootstrap] Error enabling public permissions:', error);
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
`;
|
|
1072
|
+
const bootstrapPath = path4.join(strapiBootstrapDir, "index.ts");
|
|
1073
|
+
await fs3.writeFile(bootstrapPath, bootstrapContent, "utf-8");
|
|
1074
|
+
const readmeContent = `# Strapi Bootstrap File
|
|
1075
|
+
|
|
1076
|
+
This file automatically enables public read permissions for all CMS content types when Strapi starts.
|
|
1077
|
+
|
|
1078
|
+
## Installation
|
|
1079
|
+
|
|
1080
|
+
1. Copy the \`index.ts\` file to your Strapi project:
|
|
1081
|
+
\`\`\`bash
|
|
1082
|
+
cp strapi-bootstrap/index.ts <your-strapi-project>/src/index.ts
|
|
1083
|
+
\`\`\`
|
|
1084
|
+
|
|
1085
|
+
2. Restart Strapi:
|
|
1086
|
+
\`\`\`bash
|
|
1087
|
+
cd <your-strapi-project>
|
|
1088
|
+
npm run develop
|
|
1089
|
+
\`\`\`
|
|
1090
|
+
|
|
1091
|
+
3. Check the console logs - you should see:
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
[Bootstrap] \u2705 Enabled X public permissions for Y content types
|
|
1094
|
+
\`\`\`
|
|
1095
|
+
|
|
1096
|
+
## What It Does
|
|
1097
|
+
|
|
1098
|
+
- Runs automatically when Strapi starts
|
|
1099
|
+
- Finds the "Public" role
|
|
1100
|
+
- Enables \`find\` and \`findOne\` permissions for all API content types
|
|
1101
|
+
- Allows unauthenticated users to read published content
|
|
1102
|
+
- Fixes 403 Forbidden errors from \`useStrapiContent\`
|
|
1103
|
+
|
|
1104
|
+
## Manual Alternative
|
|
1105
|
+
|
|
1106
|
+
If you prefer to set permissions manually:
|
|
1107
|
+
|
|
1108
|
+
1. Open Strapi admin: http://localhost:1337/admin
|
|
1109
|
+
2. Go to: Settings \u2192 Users & Permissions Plugin \u2192 Roles \u2192 Public
|
|
1110
|
+
3. For each content type, check:
|
|
1111
|
+
- \u2705 find
|
|
1112
|
+
- \u2705 findOne
|
|
1113
|
+
4. Click Save
|
|
1114
|
+
|
|
1115
|
+
## Notes
|
|
1116
|
+
|
|
1117
|
+
- Only enables READ permissions (find, findOne)
|
|
1118
|
+
- Does NOT enable write permissions (create, update, delete)
|
|
1119
|
+
- Only affects the "Public" role (unauthenticated users)
|
|
1120
|
+
- Safe to run multiple times (idempotent)
|
|
1121
|
+
`;
|
|
1122
|
+
const readmePath = path4.join(strapiBootstrapDir, "README.md");
|
|
1123
|
+
await fs3.writeFile(readmePath, readmeContent, "utf-8");
|
|
1124
|
+
console.log(" \u2713 Generated Strapi bootstrap file");
|
|
1125
|
+
}
|
|
1126
|
+
async function createPublishEndpoint(outputDir) {
|
|
1127
|
+
const serverDir = path4.join(outputDir, "server", "api", "cms");
|
|
1128
|
+
await fs3.ensureDir(serverDir);
|
|
1129
|
+
const endpointContent = `/**
|
|
1130
|
+
* API endpoint for batch publishing CMS changes
|
|
1131
|
+
* Publishes all drafts at once
|
|
1132
|
+
*/
|
|
1133
|
+
|
|
1134
|
+
import fs from 'fs';
|
|
1135
|
+
import path from 'path';
|
|
1136
|
+
|
|
1137
|
+
export default defineEventHandler(async (event) => {
|
|
1138
|
+
// Get Strapi URL from runtime config
|
|
1139
|
+
const config = useRuntimeConfig();
|
|
1140
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
1141
|
+
|
|
1142
|
+
// Extract Authorization header
|
|
1143
|
+
const authHeader = getHeader(event, 'authorization');
|
|
1144
|
+
|
|
1145
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
1146
|
+
throw createError({
|
|
1147
|
+
statusCode: 401,
|
|
1148
|
+
statusMessage: 'Unauthorized: Missing or invalid authorization header',
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
1153
|
+
|
|
1154
|
+
// Verify token with Strapi and determine if it's an admin or user token
|
|
1155
|
+
let userResponse: any;
|
|
1156
|
+
let isAdminToken = false;
|
|
1157
|
+
|
|
1158
|
+
try {
|
|
1159
|
+
// Try admin token verification first
|
|
1160
|
+
try {
|
|
1161
|
+
userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
1162
|
+
headers: {
|
|
1163
|
+
Authorization: \`Bearer \${token}\`,
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
isAdminToken = true;
|
|
1167
|
+
} catch (adminError) {
|
|
1168
|
+
// Fallback to regular user token verification
|
|
1169
|
+
userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
1170
|
+
headers: {
|
|
1171
|
+
Authorization: \`Bearer \${token}\`,
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
isAdminToken = false;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Get the request body
|
|
1178
|
+
const body = await readBody(event);
|
|
1179
|
+
const { pages } = body;
|
|
1180
|
+
|
|
1181
|
+
if (!pages || !Array.isArray(pages)) {
|
|
1182
|
+
throw createError({
|
|
1183
|
+
statusCode: 400,
|
|
1184
|
+
statusMessage: 'Bad Request: Missing or invalid pages array',
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Load manifest to understand field mappings
|
|
1189
|
+
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1190
|
+
let manifest;
|
|
1191
|
+
try {
|
|
1192
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
1193
|
+
manifest = JSON.parse(manifestContent);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
console.error('Failed to load manifest:', error);
|
|
1196
|
+
throw createError({
|
|
1197
|
+
statusCode: 500,
|
|
1198
|
+
statusMessage: 'Failed to load CMS manifest',
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Process all pages - call Strapi directly
|
|
1203
|
+
const results = await Promise.allSettled(
|
|
1204
|
+
pages.map(async ({ page, fields }) => {
|
|
1205
|
+
try {
|
|
1206
|
+
// Get page configuration from manifest
|
|
1207
|
+
const pageConfig = manifest.pages[page];
|
|
1208
|
+
if (!pageConfig) {
|
|
1209
|
+
throw new Error(\`Page "\${page}" not found in manifest\`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Transform fields to Strapi format
|
|
1213
|
+
const strapiData: Record<string, any> = {};
|
|
1214
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
1215
|
+
const fieldConfig = pageConfig.fields[fieldName];
|
|
1216
|
+
if (!fieldConfig) {
|
|
1217
|
+
console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Handle different field types
|
|
1222
|
+
if (fieldConfig.type === 'image') {
|
|
1223
|
+
// TODO: Handle image uploads - for now just store the value
|
|
1224
|
+
strapiData[fieldName] = value;
|
|
1225
|
+
} else {
|
|
1226
|
+
strapiData[fieldName] = value;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Update Strapi v5 content - use different endpoints for admin vs user tokens
|
|
1231
|
+
if (isAdminToken) {
|
|
1232
|
+
// Admin tokens use the content-manager API (Strapi v5)
|
|
1233
|
+
const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
1234
|
+
|
|
1235
|
+
// Step 1: Update the content
|
|
1236
|
+
await $fetch(contentEndpoint, {
|
|
1237
|
+
method: 'PUT',
|
|
1238
|
+
headers: {
|
|
1239
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1240
|
+
'Content-Type': 'application/json',
|
|
1241
|
+
},
|
|
1242
|
+
body: strapiData,
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// Step 2: Publish the content (Strapi v5)
|
|
1246
|
+
const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
|
|
1247
|
+
await $fetch(publishEndpoint, {
|
|
1248
|
+
method: 'POST',
|
|
1249
|
+
headers: {
|
|
1250
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1251
|
+
'Content-Type': 'application/json',
|
|
1252
|
+
},
|
|
1253
|
+
body: {},
|
|
1254
|
+
});
|
|
1255
|
+
} else {
|
|
1256
|
+
// User tokens use the regular REST API
|
|
1257
|
+
const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
|
|
1258
|
+
|
|
1259
|
+
await $fetch(strapiEndpoint, {
|
|
1260
|
+
method: 'PUT',
|
|
1261
|
+
headers: {
|
|
1262
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1263
|
+
'Content-Type': 'application/json',
|
|
1264
|
+
},
|
|
1265
|
+
body: {
|
|
1266
|
+
data: strapiData,
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// Publish using the publish endpoint (Strapi v5)
|
|
1271
|
+
const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
|
|
1272
|
+
await $fetch(publishEndpoint, {
|
|
1273
|
+
method: 'POST',
|
|
1274
|
+
headers: {
|
|
1275
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1276
|
+
'Content-Type': 'application/json',
|
|
1277
|
+
},
|
|
1278
|
+
body: {},
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
console.log(\`[CMS Publish] Published "\${page}" to Strapi\`);
|
|
1283
|
+
return { page, success: true };
|
|
1284
|
+
} catch (error: any) {
|
|
1285
|
+
console.error(\`[CMS Publish] Failed to publish "\${page}":\`, error);
|
|
1286
|
+
return {
|
|
1287
|
+
page,
|
|
1288
|
+
success: false,
|
|
1289
|
+
error: error.message || 'Unknown error',
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
})
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
// Separate successful and failed publications
|
|
1296
|
+
const successful: string[] = [];
|
|
1297
|
+
const failed: Array<{ page: string; error: string }> = [];
|
|
1298
|
+
|
|
1299
|
+
results.forEach((result, index) => {
|
|
1300
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
1301
|
+
successful.push(result.value.page);
|
|
1302
|
+
} else if (result.status === 'fulfilled' && !result.value.success) {
|
|
1303
|
+
failed.push({
|
|
1304
|
+
page: result.value.page,
|
|
1305
|
+
error: result.value.error || 'Unknown error',
|
|
1306
|
+
});
|
|
1307
|
+
} else if (result.status === 'rejected') {
|
|
1308
|
+
failed.push({
|
|
1309
|
+
page: pages[index].page,
|
|
1310
|
+
error: result.reason?.message || 'Unknown error',
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
console.log(\`[CMS Publish] Published \${successful.length} pages, \${failed.length} failed\`);
|
|
1316
|
+
|
|
1317
|
+
return {
|
|
1318
|
+
success: failed.length === 0,
|
|
1319
|
+
message: \`Published \${successful.length} of \${pages.length} pages\`,
|
|
1320
|
+
successful,
|
|
1321
|
+
failed,
|
|
1322
|
+
user: {
|
|
1323
|
+
id: userResponse.id,
|
|
1324
|
+
username: userResponse.username || userResponse.firstname || 'Unknown',
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
} catch (error: any) {
|
|
1328
|
+
console.error('[CMS Publish] Error:', error);
|
|
1329
|
+
|
|
1330
|
+
// Token verification failed
|
|
1331
|
+
if (error.statusCode === 401 || error.status === 401) {
|
|
1332
|
+
throw createError({
|
|
1333
|
+
statusCode: 401,
|
|
1334
|
+
statusMessage: 'Unauthorized: Invalid or expired token',
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Generic error
|
|
1339
|
+
throw createError({
|
|
1340
|
+
statusCode: 500,
|
|
1341
|
+
statusMessage: 'Internal server error while publishing changes',
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
`;
|
|
1346
|
+
const endpointPath = path4.join(serverDir, "publish.post.ts");
|
|
1347
|
+
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/boilerplate.ts
|
|
1351
|
+
import fs4 from "fs-extra";
|
|
1352
|
+
import path5 from "path";
|
|
1353
|
+
import { execSync as execSync2 } from "child_process";
|
|
1354
|
+
import pc2 from "picocolors";
|
|
1355
|
+
function isGitHubURL(source) {
|
|
1356
|
+
return source.startsWith("https://github.com/") || source.startsWith("git@github.com:") || source.includes("github.com");
|
|
1357
|
+
}
|
|
1358
|
+
async function cloneFromGitHub(repoUrl, outputDir) {
|
|
1359
|
+
console.log(pc2.blue(" Cloning from GitHub..."));
|
|
1360
|
+
try {
|
|
1361
|
+
execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
|
|
1362
|
+
const gitDir = path5.join(outputDir, ".git");
|
|
1363
|
+
await fs4.remove(gitDir);
|
|
1364
|
+
console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async function copyFromLocal(sourcePath, outputDir) {
|
|
1370
|
+
console.log(pc2.blue(" Copying from local path..."));
|
|
1371
|
+
const sourceExists = await fs4.pathExists(sourcePath);
|
|
1372
|
+
if (!sourceExists) {
|
|
1373
|
+
throw new Error(`Local boilerplate not found: ${sourcePath}`);
|
|
1374
|
+
}
|
|
1375
|
+
await fs4.copy(sourcePath, outputDir, {
|
|
1376
|
+
filter: (src) => {
|
|
1377
|
+
const name = path5.basename(src);
|
|
1378
|
+
return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
console.log(pc2.green(" \u2713 Boilerplate copied successfully"));
|
|
1382
|
+
}
|
|
1383
|
+
async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
1384
|
+
if (!boilerplateSource) {
|
|
1385
|
+
console.log(pc2.blue("\n\u{1F4E6} Creating minimal Nuxt structure..."));
|
|
1386
|
+
await fs4.ensureDir(outputDir);
|
|
1387
|
+
await fs4.ensureDir(path5.join(outputDir, "pages"));
|
|
1388
|
+
await fs4.ensureDir(path5.join(outputDir, "assets"));
|
|
1389
|
+
await fs4.ensureDir(path5.join(outputDir, "public"));
|
|
1390
|
+
await fs4.ensureDir(path5.join(outputDir, "utils"));
|
|
1391
|
+
const configPath = path5.join(outputDir, "nuxt.config.ts");
|
|
1392
|
+
const configExists = await fs4.pathExists(configPath);
|
|
1393
|
+
if (!configExists) {
|
|
1394
|
+
const basicConfig = `export default defineNuxtConfig({
|
|
1395
|
+
devtools: { enabled: true },
|
|
1396
|
+
css: [],
|
|
1397
|
+
})
|
|
1398
|
+
`;
|
|
1399
|
+
await fs4.writeFile(configPath, basicConfig, "utf-8");
|
|
1400
|
+
}
|
|
1401
|
+
console.log(pc2.green(" \u2713 Structure created"));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const outputExists = await fs4.pathExists(outputDir);
|
|
1405
|
+
if (outputExists) {
|
|
1406
|
+
throw new Error(`Output directory already exists: ${outputDir}. Please choose a different path or remove it first.`);
|
|
1407
|
+
}
|
|
1408
|
+
console.log(pc2.blue("\n\u{1F4E6} Setting up boilerplate..."));
|
|
1409
|
+
if (isGitHubURL(boilerplateSource)) {
|
|
1410
|
+
await cloneFromGitHub(boilerplateSource, outputDir);
|
|
1411
|
+
} else {
|
|
1412
|
+
await copyFromLocal(boilerplateSource, outputDir);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/manifest.ts
|
|
1417
|
+
import fs6 from "fs-extra";
|
|
1418
|
+
import path7 from "path";
|
|
1419
|
+
|
|
1420
|
+
// src/detector.ts
|
|
1421
|
+
import * as cheerio2 from "cheerio";
|
|
1422
|
+
import fs5 from "fs-extra";
|
|
1423
|
+
import path6 from "path";
|
|
1424
|
+
function cleanClassName(className) {
|
|
1425
|
+
return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
|
|
1426
|
+
}
|
|
1427
|
+
function getPrimaryClass(classAttr) {
|
|
1428
|
+
if (!classAttr) return null;
|
|
1429
|
+
const cleaned = cleanClassName(classAttr);
|
|
1430
|
+
const classes = cleaned.split(" ").filter((c) => c.length > 0);
|
|
1431
|
+
if (classes.length === 0) return null;
|
|
1432
|
+
const original = classes[0];
|
|
1433
|
+
return {
|
|
1434
|
+
selector: original,
|
|
1435
|
+
// Keep original with dashes for CSS selector
|
|
1436
|
+
fieldName: original.replace(/-/g, "_")
|
|
1437
|
+
// Normalize for field name
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function getContextModifier(_$, $el) {
|
|
1441
|
+
let $current = $el.parent();
|
|
1442
|
+
let depth = 0;
|
|
1443
|
+
while ($current.length > 0 && depth < 5) {
|
|
1444
|
+
const classes = $current.attr("class");
|
|
1445
|
+
if (classes) {
|
|
1446
|
+
const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
|
|
1447
|
+
if (ccClass) {
|
|
1448
|
+
return ccClass.replace("cc-", "").replace(/-/g, "_");
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
$current = $current.parent();
|
|
1452
|
+
depth++;
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
function isDecorativeImage(_$, $img) {
|
|
1457
|
+
const $parent = $img.parent();
|
|
1458
|
+
const parentClass = $parent.attr("class") || "";
|
|
1459
|
+
const decorativePatterns = [
|
|
1460
|
+
"nav",
|
|
1461
|
+
"logo",
|
|
1462
|
+
"icon",
|
|
1463
|
+
"arrow",
|
|
1464
|
+
"button",
|
|
1465
|
+
"quote",
|
|
1466
|
+
"pagination",
|
|
1467
|
+
"footer",
|
|
1468
|
+
"link"
|
|
1469
|
+
];
|
|
1470
|
+
return decorativePatterns.some(
|
|
1471
|
+
(pattern) => parentClass.includes(pattern) || parentClass.includes(`${pattern}_`)
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function isInsideButton($, el) {
|
|
1475
|
+
const $el = $(el);
|
|
1476
|
+
const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
|
|
1477
|
+
return $button.length > 0;
|
|
1478
|
+
}
|
|
1479
|
+
function extractTemplateFromVue(vueContent) {
|
|
1480
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1481
|
+
if (!templateMatch) {
|
|
1482
|
+
return "";
|
|
1483
|
+
}
|
|
1484
|
+
return templateMatch[1];
|
|
1485
|
+
}
|
|
1486
|
+
function detectEditableFields(templateHtml) {
|
|
1487
|
+
const $ = cheerio2.load(templateHtml);
|
|
1488
|
+
const detectedFields = {};
|
|
1489
|
+
const detectedCollections = {};
|
|
1490
|
+
const collectionElements = /* @__PURE__ */ new Set();
|
|
1491
|
+
const processedCollectionClasses = /* @__PURE__ */ new Set();
|
|
1492
|
+
const potentialCollections = /* @__PURE__ */ new Map();
|
|
1493
|
+
$("[class]").each((_, el) => {
|
|
1494
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
1495
|
+
if (primaryClass && (primaryClass.fieldName.includes("card") || primaryClass.fieldName.includes("item") || primaryClass.fieldName.includes("post") || primaryClass.fieldName.includes("feature")) && !primaryClass.fieldName.includes("image") && !primaryClass.fieldName.includes("inner")) {
|
|
1496
|
+
if (!potentialCollections.has(primaryClass.fieldName)) {
|
|
1497
|
+
potentialCollections.set(primaryClass.fieldName, []);
|
|
1498
|
+
}
|
|
1499
|
+
potentialCollections.get(primaryClass.fieldName)?.push(el);
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
potentialCollections.forEach((elements, className) => {
|
|
1503
|
+
if (elements.length >= 2) {
|
|
1504
|
+
const $first = $(elements[0]);
|
|
1505
|
+
const collectionFields = {};
|
|
1506
|
+
processedCollectionClasses.add(className);
|
|
1507
|
+
elements.forEach((el) => {
|
|
1508
|
+
collectionElements.add(el);
|
|
1509
|
+
$(el).find("*").each((_, child) => {
|
|
1510
|
+
collectionElements.add(child);
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
const collectionClassInfo = getPrimaryClass($(elements[0]).attr("class"));
|
|
1514
|
+
const collectionSelector = collectionClassInfo ? `.${collectionClassInfo.selector}` : `.${className}`;
|
|
1515
|
+
$first.find("img").each((_, img) => {
|
|
1516
|
+
if (isInsideButton($, img)) return;
|
|
1517
|
+
const $img = $(img);
|
|
1518
|
+
const $parent = $img.parent();
|
|
1519
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1520
|
+
if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
|
|
1521
|
+
collectionFields.image = `.${parentClassInfo.selector}`;
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
$first.find("div").each((_, el) => {
|
|
1526
|
+
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1527
|
+
if (classInfo && classInfo.fieldName.includes("tag") && !classInfo.fieldName.includes("container")) {
|
|
1528
|
+
collectionFields.tag = `.${classInfo.selector}`;
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
$first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
|
|
1533
|
+
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1534
|
+
if (classInfo) {
|
|
1535
|
+
collectionFields.title = `.${classInfo.selector}`;
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
$first.find("p").first().each((_, el) => {
|
|
1539
|
+
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1540
|
+
if (classInfo) {
|
|
1541
|
+
collectionFields.description = `.${classInfo.selector}`;
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
$first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
|
|
1545
|
+
const $link = $(el);
|
|
1546
|
+
const linkText = $link.text().trim();
|
|
1547
|
+
if (linkText) {
|
|
1548
|
+
const classInfo = getPrimaryClass($link.attr("class"));
|
|
1549
|
+
collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
if (Object.keys(collectionFields).length > 0) {
|
|
1554
|
+
let collectionName = className;
|
|
1555
|
+
if (!collectionName.endsWith("s")) {
|
|
1556
|
+
collectionName += "s";
|
|
1557
|
+
}
|
|
1558
|
+
detectedCollections[collectionName] = {
|
|
1559
|
+
selector: collectionSelector,
|
|
1560
|
+
fields: collectionFields
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
const $body = $("body");
|
|
1566
|
+
$body.find("h1, h2, h3, h4, h5, h6").each((index, el) => {
|
|
1567
|
+
if (collectionElements.has(el)) return;
|
|
1568
|
+
const $el = $(el);
|
|
1569
|
+
const text = $el.text().trim();
|
|
1570
|
+
const classInfo = getPrimaryClass($el.attr("class"));
|
|
1571
|
+
if (text) {
|
|
1572
|
+
let fieldName;
|
|
1573
|
+
let selector;
|
|
1574
|
+
if (classInfo && !classInfo.fieldName.startsWith("heading_")) {
|
|
1575
|
+
fieldName = classInfo.fieldName;
|
|
1576
|
+
selector = `.${classInfo.selector}`;
|
|
1577
|
+
} else {
|
|
1578
|
+
const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
|
|
1579
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1580
|
+
const modifier = getContextModifier($, $el);
|
|
1581
|
+
if (parentClassInfo) {
|
|
1582
|
+
fieldName = modifier ? `${modifier}_${parentClassInfo.fieldName}` : parentClassInfo.fieldName;
|
|
1583
|
+
selector = classInfo ? `.${classInfo.selector}` : `.${parentClassInfo.selector}`;
|
|
1584
|
+
} else if (modifier) {
|
|
1585
|
+
fieldName = `${modifier}_heading`;
|
|
1586
|
+
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1587
|
+
} else {
|
|
1588
|
+
fieldName = `heading_${index}`;
|
|
1589
|
+
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
detectedFields[fieldName] = {
|
|
1593
|
+
selector,
|
|
1594
|
+
type: "plain",
|
|
1595
|
+
editable: true
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
$body.find("p").each((_index, el) => {
|
|
1600
|
+
if (collectionElements.has(el)) return;
|
|
1601
|
+
const $el = $(el);
|
|
1602
|
+
const text = $el.text().trim();
|
|
1603
|
+
const classInfo = getPrimaryClass($el.attr("class"));
|
|
1604
|
+
if (text && text.length > 20 && classInfo) {
|
|
1605
|
+
const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
|
|
1606
|
+
detectedFields[classInfo.fieldName] = {
|
|
1607
|
+
selector: `.${classInfo.selector}`,
|
|
1608
|
+
type: hasFormatting ? "rich" : "plain",
|
|
1609
|
+
editable: true
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
$body.find("img").each((_index, el) => {
|
|
1614
|
+
if (collectionElements.has(el)) return;
|
|
1615
|
+
if (isInsideButton($, el)) return;
|
|
1616
|
+
const $el = $(el);
|
|
1617
|
+
if (isDecorativeImage($, $el)) return;
|
|
1618
|
+
const $parent = $el.parent();
|
|
1619
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1620
|
+
if (parentClassInfo) {
|
|
1621
|
+
const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
|
|
1622
|
+
detectedFields[fieldName] = {
|
|
1623
|
+
selector: `.${parentClassInfo.selector}`,
|
|
1624
|
+
type: "image",
|
|
1625
|
+
editable: true
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
$body.find("NuxtLink.c_button, a.c_button, .c_button").each((_index, el) => {
|
|
1630
|
+
if (collectionElements.has(el)) return;
|
|
1631
|
+
const $el = $(el);
|
|
1632
|
+
const text = $el.contents().filter(function() {
|
|
1633
|
+
return this.type === "text" || this.type === "tag" && this.name === "div";
|
|
1634
|
+
}).first().text().trim();
|
|
1635
|
+
if (text && text.length > 2) {
|
|
1636
|
+
const $parent = $el.closest('[class*="cta"]').first();
|
|
1637
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1638
|
+
const fieldName = parentClassInfo ? `${parentClassInfo.fieldName}_button_text` : "button_text";
|
|
1639
|
+
detectedFields[fieldName] = {
|
|
1640
|
+
selector: `.c_button`,
|
|
1641
|
+
type: "plain",
|
|
1642
|
+
editable: true
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
return {
|
|
1647
|
+
fields: detectedFields,
|
|
1648
|
+
collections: detectedCollections
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async function analyzeVuePages(pagesDir) {
|
|
1652
|
+
const results = {};
|
|
1653
|
+
const vueFiles = await fs5.readdir(pagesDir);
|
|
1654
|
+
for (const file of vueFiles) {
|
|
1655
|
+
if (file.endsWith(".vue")) {
|
|
1656
|
+
const filePath = path6.join(pagesDir, file);
|
|
1657
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
1658
|
+
const template = extractTemplateFromVue(content);
|
|
1659
|
+
if (template) {
|
|
1660
|
+
const pageName = file.replace(".vue", "");
|
|
1661
|
+
results[pageName] = detectEditableFields(template);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return results;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/manifest.ts
|
|
1669
|
+
async function generateManifest(pagesDir) {
|
|
1670
|
+
const analyzed = await analyzeVuePages(pagesDir);
|
|
1671
|
+
const pages = {};
|
|
1672
|
+
for (const [pageName, detection] of Object.entries(analyzed)) {
|
|
1673
|
+
pages[pageName] = {
|
|
1674
|
+
fields: detection.fields,
|
|
1675
|
+
collections: detection.collections,
|
|
1676
|
+
meta: {
|
|
1677
|
+
route: pageName === "index" ? "/" : `/${pageName}`
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
const manifest = {
|
|
1682
|
+
version: "1.0",
|
|
1683
|
+
pages
|
|
1684
|
+
};
|
|
1685
|
+
return manifest;
|
|
1686
|
+
}
|
|
1687
|
+
async function writeManifest(outputDir, manifest) {
|
|
1688
|
+
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
1689
|
+
const manifestPath = path7.join(outputDir, "cms-manifest.json");
|
|
1690
|
+
await fs6.writeFile(manifestPath, manifestContent, "utf-8");
|
|
1691
|
+
const publicDir = path7.join(outputDir, "public");
|
|
1692
|
+
await fs6.ensureDir(publicDir);
|
|
1693
|
+
const publicManifestPath = path7.join(publicDir, "cms-manifest.json");
|
|
1694
|
+
await fs6.writeFile(publicManifestPath, manifestContent, "utf-8");
|
|
1695
|
+
}
|
|
1696
|
+
async function readManifest(outputDir) {
|
|
1697
|
+
const manifestPath = path7.join(outputDir, "cms-manifest.json");
|
|
1698
|
+
const content = await fs6.readFile(manifestPath, "utf-8");
|
|
1699
|
+
return JSON.parse(content);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/vue-transformer.ts
|
|
1703
|
+
import * as cheerio3 from "cheerio";
|
|
1704
|
+
import fs7 from "fs-extra";
|
|
1705
|
+
import path8 from "path";
|
|
1706
|
+
function replaceWithBinding(_$, $el, fieldName, type) {
|
|
1707
|
+
if (type === "image") {
|
|
1708
|
+
const $img = $el.find("img").first();
|
|
1709
|
+
if ($img.length) {
|
|
1710
|
+
$img.attr(":src", `content.${fieldName}`);
|
|
1711
|
+
$img.removeAttr("src");
|
|
1712
|
+
}
|
|
1713
|
+
} else if (type === "rich") {
|
|
1714
|
+
$el.attr("v-html", `content.${fieldName}`);
|
|
1715
|
+
$el.empty();
|
|
1716
|
+
} else {
|
|
1717
|
+
$el.empty();
|
|
1718
|
+
$el.text(`{{ content.${fieldName} }}`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function transformCollection($, collectionName, collection) {
|
|
1722
|
+
const $items = $(collection.selector);
|
|
1723
|
+
if ($items.length === 0) return;
|
|
1724
|
+
const $first = $items.first();
|
|
1725
|
+
$first.attr("v-for", `(item, index) in content.${collectionName}`);
|
|
1726
|
+
$first.attr(":key", "index");
|
|
1727
|
+
Object.entries(collection.fields).forEach(([fieldName, selector]) => {
|
|
1728
|
+
const $fieldEl = $first.find(selector);
|
|
1729
|
+
if ($fieldEl.length) {
|
|
1730
|
+
if (fieldName === "image") {
|
|
1731
|
+
const $img = $fieldEl.find("img").first();
|
|
1732
|
+
if ($img.length) {
|
|
1733
|
+
$img.attr(":src", "item.image");
|
|
1734
|
+
$img.removeAttr("src");
|
|
1735
|
+
}
|
|
1736
|
+
} else if (fieldName === "link") {
|
|
1737
|
+
$fieldEl.attr(":to", "item.link");
|
|
1738
|
+
$fieldEl.removeAttr("to");
|
|
1739
|
+
$fieldEl.removeAttr("href");
|
|
1740
|
+
} else {
|
|
1741
|
+
$fieldEl.empty();
|
|
1742
|
+
$fieldEl.text(`{{ item.${fieldName} }}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
$items.slice(1).remove();
|
|
1747
|
+
}
|
|
1748
|
+
async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
1749
|
+
const pageManifest = manifest.pages[pageName];
|
|
1750
|
+
if (!pageManifest) return;
|
|
1751
|
+
const vueContent = await fs7.readFile(vueFilePath, "utf-8");
|
|
1752
|
+
if (vueContent.includes("useStrapiContent")) {
|
|
1753
|
+
console.log(` Skipping ${pageName} - already transformed`);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1757
|
+
if (!templateMatch) return;
|
|
1758
|
+
const templateContent = templateMatch[1];
|
|
1759
|
+
const $ = cheerio3.load(templateContent, { xmlMode: false });
|
|
1760
|
+
if (pageManifest.collections) {
|
|
1761
|
+
Object.entries(pageManifest.collections).forEach(([collectionName, collection]) => {
|
|
1762
|
+
transformCollection($, collectionName, collection);
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
if (pageManifest.fields) {
|
|
1766
|
+
Object.entries(pageManifest.fields).forEach(([fieldName, field]) => {
|
|
1767
|
+
const $elements = $(field.selector);
|
|
1768
|
+
$elements.each((_, el) => {
|
|
1769
|
+
const $el = $(el);
|
|
1770
|
+
replaceWithBinding($, $el, fieldName, field.type);
|
|
1771
|
+
});
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
let transformedTemplate = $.html();
|
|
1775
|
+
const bodyMatch = transformedTemplate.match(/<body>([\s\S]*)<\/body>/);
|
|
1776
|
+
if (bodyMatch) {
|
|
1777
|
+
transformedTemplate = bodyMatch[1];
|
|
1778
|
+
}
|
|
1779
|
+
transformedTemplate = transformedTemplate.replace(/<\/?html[^>]*>/gi, "").replace(/<head><\/head>/gi, "").trim();
|
|
1780
|
+
const wrapperDivMatch = transformedTemplate.match(/^<div>\s*([\s\S]*?)\s*<\/div>$/);
|
|
1781
|
+
if (wrapperDivMatch) {
|
|
1782
|
+
transformedTemplate = wrapperDivMatch[1].trim();
|
|
1783
|
+
}
|
|
1784
|
+
const scriptSetup = `<script setup lang="ts">
|
|
1785
|
+
// Auto-generated reactive content from Strapi
|
|
1786
|
+
const { content } = useStrapiContent('${pageName}');
|
|
1787
|
+
</script>`;
|
|
1788
|
+
const finalTemplate = transformedTemplate.split("\n").map((line) => " " + line).join("\n");
|
|
1789
|
+
const newVueContent = `${scriptSetup}
|
|
1790
|
+
|
|
1791
|
+
<template>
|
|
1792
|
+
${finalTemplate}
|
|
1793
|
+
</template>
|
|
1794
|
+
`;
|
|
1795
|
+
await fs7.writeFile(vueFilePath, newVueContent, "utf-8");
|
|
1796
|
+
}
|
|
1797
|
+
async function transformAllVuePages(pagesDir, manifest) {
|
|
1798
|
+
const vueFiles = await fs7.readdir(pagesDir);
|
|
1799
|
+
for (const file of vueFiles) {
|
|
1800
|
+
if (file.endsWith(".vue")) {
|
|
1801
|
+
const pageName = file.replace(".vue", "");
|
|
1802
|
+
const vueFilePath = path8.join(pagesDir, file);
|
|
1803
|
+
await transformVueToReactive(vueFilePath, pageName, manifest);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/transformer.ts
|
|
1809
|
+
function mapFieldTypeToStrapi(fieldType) {
|
|
1810
|
+
const typeMap = {
|
|
1811
|
+
plain: "string",
|
|
1812
|
+
rich: "richtext",
|
|
1813
|
+
html: "richtext",
|
|
1814
|
+
image: "media",
|
|
1815
|
+
link: "string",
|
|
1816
|
+
email: "email",
|
|
1817
|
+
phone: "string"
|
|
1818
|
+
};
|
|
1819
|
+
return typeMap[fieldType] || "string";
|
|
1820
|
+
}
|
|
1821
|
+
function pluralize(word) {
|
|
1822
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
|
|
1823
|
+
return word + "es";
|
|
1824
|
+
}
|
|
1825
|
+
if (word.endsWith("y") && word.length > 1) {
|
|
1826
|
+
const secondLast = word[word.length - 2];
|
|
1827
|
+
if (!"aeiou".includes(secondLast.toLowerCase())) {
|
|
1828
|
+
return word.slice(0, -1) + "ies";
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
return word + "s";
|
|
1832
|
+
}
|
|
1833
|
+
function pageToStrapiSchema(pageName, fields) {
|
|
1834
|
+
const attributes = {};
|
|
1835
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1836
|
+
attributes[fieldName] = {
|
|
1837
|
+
type: mapFieldTypeToStrapi(field.type),
|
|
1838
|
+
required: field.required || false
|
|
1839
|
+
};
|
|
1840
|
+
if (field.default) {
|
|
1841
|
+
attributes[fieldName].default = field.default;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1845
|
+
const kebabCaseName = pageName;
|
|
1846
|
+
const pluralName = pluralize(kebabCaseName);
|
|
1847
|
+
return {
|
|
1848
|
+
kind: "singleType",
|
|
1849
|
+
collectionName: kebabCaseName,
|
|
1850
|
+
info: {
|
|
1851
|
+
singularName: kebabCaseName,
|
|
1852
|
+
pluralName,
|
|
1853
|
+
displayName
|
|
1854
|
+
},
|
|
1855
|
+
options: {
|
|
1856
|
+
draftAndPublish: true
|
|
1857
|
+
},
|
|
1858
|
+
attributes
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function collectionToStrapiSchema(collectionName, collection) {
|
|
1862
|
+
const attributes = {};
|
|
1863
|
+
for (const [fieldName, _selector] of Object.entries(collection.fields)) {
|
|
1864
|
+
let type = "string";
|
|
1865
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
1866
|
+
type = "media";
|
|
1867
|
+
} else if (fieldName === "description" || fieldName === "content") {
|
|
1868
|
+
type = "richtext";
|
|
1869
|
+
} else if (fieldName === "link" || fieldName === "url") {
|
|
1870
|
+
type = "string";
|
|
1871
|
+
} else if (fieldName === "title" || fieldName === "tag") {
|
|
1872
|
+
type = "string";
|
|
1873
|
+
}
|
|
1874
|
+
attributes[fieldName] = {
|
|
1875
|
+
type
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1879
|
+
const kebabCaseName = collectionName.replace(/_/g, "-");
|
|
1880
|
+
const singularName = kebabCaseName.endsWith("s") ? kebabCaseName.slice(0, -1) : kebabCaseName;
|
|
1881
|
+
return {
|
|
1882
|
+
kind: "collectionType",
|
|
1883
|
+
collectionName: kebabCaseName,
|
|
1884
|
+
info: {
|
|
1885
|
+
singularName,
|
|
1886
|
+
pluralName: kebabCaseName,
|
|
1887
|
+
displayName
|
|
1888
|
+
},
|
|
1889
|
+
options: {
|
|
1890
|
+
draftAndPublish: true
|
|
1891
|
+
},
|
|
1892
|
+
attributes
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function manifestToSchemas(manifest) {
|
|
1896
|
+
const schemas = {};
|
|
1897
|
+
for (const [pageName, page] of Object.entries(manifest.pages)) {
|
|
1898
|
+
if (page.fields && Object.keys(page.fields).length > 0) {
|
|
1899
|
+
schemas[pageName] = pageToStrapiSchema(pageName, page.fields);
|
|
1900
|
+
}
|
|
1901
|
+
if (page.collections) {
|
|
1902
|
+
for (const [collectionName, collection] of Object.entries(page.collections)) {
|
|
1903
|
+
schemas[collectionName] = collectionToStrapiSchema(collectionName, collection);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return schemas;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/schema-writer.ts
|
|
1911
|
+
import fs8 from "fs-extra";
|
|
1912
|
+
import path9 from "path";
|
|
1913
|
+
async function writeStrapiSchema(outputDir, name, schema) {
|
|
1914
|
+
const schemasDir = path9.join(outputDir, "cms-schemas");
|
|
1915
|
+
await fs8.ensureDir(schemasDir);
|
|
1916
|
+
const schemaPath = path9.join(schemasDir, `${name}.json`);
|
|
1917
|
+
await fs8.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
1918
|
+
}
|
|
1919
|
+
async function writeAllSchemas(outputDir, schemas) {
|
|
1920
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
1921
|
+
await writeStrapiSchema(outputDir, name, schema);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
async function createStrapiReadme(outputDir) {
|
|
1925
|
+
const readmePath = path9.join(outputDir, "cms-schemas", "README.md");
|
|
1926
|
+
const content = `# CMS Schemas
|
|
1927
|
+
|
|
1928
|
+
Auto-generated Strapi content type schemas from your Webflow export.
|
|
1929
|
+
|
|
1930
|
+
## What's in this folder?
|
|
1931
|
+
|
|
1932
|
+
Each \`.json\` file is a Strapi content type schema:
|
|
1933
|
+
|
|
1934
|
+
- **Pages** (single types) - Unique pages like \`index.json\`, \`about.json\`
|
|
1935
|
+
- **Collections** (collection types) - Repeating content like \`portfolio_cards.json\`
|
|
1936
|
+
|
|
1937
|
+
## How to use with Strapi
|
|
1938
|
+
|
|
1939
|
+
### Option 1: Manual Setup (Recommended for learning)
|
|
1940
|
+
|
|
1941
|
+
1. Start your Strapi project
|
|
1942
|
+
2. In Strapi admin, go to **Content-Type Builder**
|
|
1943
|
+
3. Create each content type manually using these schemas as reference
|
|
1944
|
+
4. Match the field names and types
|
|
1945
|
+
|
|
1946
|
+
### Option 2: Automated Setup (Advanced)
|
|
1947
|
+
|
|
1948
|
+
Copy schemas to your Strapi project structure:
|
|
1949
|
+
|
|
1950
|
+
\`\`\`bash
|
|
1951
|
+
# For each schema file, create the Strapi directory structure
|
|
1952
|
+
# Example for index.json (single type):
|
|
1953
|
+
mkdir -p strapi/src/api/index/content-types/index
|
|
1954
|
+
cp cms-schemas/index.json strapi/src/api/index/content-types/index/schema.json
|
|
1955
|
+
|
|
1956
|
+
# Example for portfolio_cards.json (collection type):
|
|
1957
|
+
mkdir -p strapi/src/api/portfolio-cards/content-types/portfolio-card
|
|
1958
|
+
cp cms-schemas/portfolio_cards.json strapi/src/api/portfolio-cards/content-types/portfolio-card/schema.json
|
|
1959
|
+
\`\`\`
|
|
1960
|
+
|
|
1961
|
+
Then restart Strapi - it will auto-create the content types.
|
|
1962
|
+
|
|
1963
|
+
## Schema Structure
|
|
1964
|
+
|
|
1965
|
+
Each schema defines:
|
|
1966
|
+
- \`kind\`: "singleType" (unique page) or "collectionType" (repeating)
|
|
1967
|
+
- \`attributes\`: Fields and their types (string, richtext, media, etc.)
|
|
1968
|
+
- \`displayName\`: How it appears in Strapi admin
|
|
1969
|
+
|
|
1970
|
+
## Field Types
|
|
1971
|
+
|
|
1972
|
+
- \`string\` - Plain text
|
|
1973
|
+
- \`richtext\` - Formatted text with HTML
|
|
1974
|
+
- \`media\` - Image uploads
|
|
1975
|
+
|
|
1976
|
+
## Next Steps
|
|
1977
|
+
|
|
1978
|
+
1. Set up a Strapi project: \`npx create-strapi-app@latest my-strapi\`
|
|
1979
|
+
2. Use these schemas to create content types
|
|
1980
|
+
3. Populate content in Strapi admin
|
|
1981
|
+
4. Connect your Nuxt app to Strapi API
|
|
1982
|
+
|
|
1983
|
+
## API Usage in Nuxt
|
|
1984
|
+
|
|
1985
|
+
Once Strapi is running with these content types:
|
|
1986
|
+
|
|
1987
|
+
\`\`\`typescript
|
|
1988
|
+
// Fetch single type (e.g., home page)
|
|
1989
|
+
const { data } = await $fetch('http://localhost:1337/api/index')
|
|
1990
|
+
|
|
1991
|
+
// Fetch collection type (e.g., portfolio cards)
|
|
1992
|
+
const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
|
|
1993
|
+
\`\`\`
|
|
1994
|
+
`;
|
|
1995
|
+
await fs8.writeFile(readmePath, content, "utf-8");
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/content-extractor.ts
|
|
1999
|
+
import * as cheerio4 from "cheerio";
|
|
2000
|
+
import path10 from "path";
|
|
2001
|
+
function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
2002
|
+
const $ = cheerio4.load(html);
|
|
2003
|
+
const content = {
|
|
2004
|
+
fields: {},
|
|
2005
|
+
collections: {}
|
|
2006
|
+
};
|
|
2007
|
+
if (pageManifest.fields) {
|
|
2008
|
+
for (const [fieldName, field] of Object.entries(pageManifest.fields)) {
|
|
2009
|
+
const selector = field.selector;
|
|
2010
|
+
const element = $(selector).first();
|
|
2011
|
+
if (element.length > 0) {
|
|
2012
|
+
if (field.type === "image") {
|
|
2013
|
+
const src = element.attr("src") || element.find("img").attr("src") || "";
|
|
2014
|
+
content.fields[fieldName] = src;
|
|
2015
|
+
} else {
|
|
2016
|
+
const text = element.text().trim();
|
|
2017
|
+
content.fields[fieldName] = text;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (pageManifest.collections) {
|
|
2023
|
+
for (const [collectionName, collection] of Object.entries(pageManifest.collections)) {
|
|
2024
|
+
const items = [];
|
|
2025
|
+
const collectionElements = $(collection.selector);
|
|
2026
|
+
collectionElements.each((_, elem) => {
|
|
2027
|
+
const item = {};
|
|
2028
|
+
const $elem = $(elem);
|
|
2029
|
+
for (const [fieldName, fieldSelector] of Object.entries(collection.fields)) {
|
|
2030
|
+
const fieldElement = $elem.find(fieldSelector).first();
|
|
2031
|
+
if (fieldElement.length > 0) {
|
|
2032
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
2033
|
+
const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
|
|
2034
|
+
item[fieldName] = src;
|
|
2035
|
+
} else if (fieldName === "link" || fieldName === "url") {
|
|
2036
|
+
const href = fieldElement.attr("href") || "";
|
|
2037
|
+
item[fieldName] = href;
|
|
2038
|
+
} else {
|
|
2039
|
+
const text = fieldElement.text().trim();
|
|
2040
|
+
item[fieldName] = text;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (Object.keys(item).length > 0) {
|
|
2045
|
+
items.push(item);
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
if (items.length > 0) {
|
|
2049
|
+
content.collections[collectionName] = items;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return content;
|
|
2054
|
+
}
|
|
2055
|
+
function extractAllContent(htmlFiles, manifest) {
|
|
2056
|
+
const extractedContent = {
|
|
2057
|
+
pages: {}
|
|
2058
|
+
};
|
|
2059
|
+
for (const [pageName, pageManifest] of Object.entries(manifest.pages)) {
|
|
2060
|
+
const html = htmlFiles.get(pageName);
|
|
2061
|
+
if (html) {
|
|
2062
|
+
const content = extractContentFromHTML(html, pageName, pageManifest);
|
|
2063
|
+
extractedContent.pages[pageName] = content;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return extractedContent;
|
|
2067
|
+
}
|
|
2068
|
+
function normalizeImagePath(imageSrc) {
|
|
2069
|
+
if (!imageSrc) return "";
|
|
2070
|
+
if (imageSrc.startsWith("/")) return imageSrc;
|
|
2071
|
+
const filename = path10.basename(imageSrc);
|
|
2072
|
+
if (imageSrc.includes("images/")) {
|
|
2073
|
+
return `/images/${filename}`;
|
|
2074
|
+
}
|
|
2075
|
+
return `/${filename}`;
|
|
2076
|
+
}
|
|
2077
|
+
function formatForStrapi(extracted) {
|
|
2078
|
+
const seedData = {};
|
|
2079
|
+
for (const [pageName, content] of Object.entries(extracted.pages)) {
|
|
2080
|
+
if (Object.keys(content.fields).length > 0) {
|
|
2081
|
+
const formattedFields = {};
|
|
2082
|
+
for (const [fieldName, value] of Object.entries(content.fields)) {
|
|
2083
|
+
if (fieldName.includes("image") || fieldName.includes("bg")) {
|
|
2084
|
+
formattedFields[fieldName] = normalizeImagePath(value);
|
|
2085
|
+
} else {
|
|
2086
|
+
formattedFields[fieldName] = value;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
seedData[pageName] = formattedFields;
|
|
2090
|
+
}
|
|
2091
|
+
for (const [collectionName, items] of Object.entries(content.collections)) {
|
|
2092
|
+
const formattedItems = items.map((item) => {
|
|
2093
|
+
const formattedItem = {};
|
|
2094
|
+
for (const [fieldName, value] of Object.entries(item)) {
|
|
2095
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
2096
|
+
formattedItem[fieldName] = normalizeImagePath(value);
|
|
2097
|
+
} else {
|
|
2098
|
+
formattedItem[fieldName] = value;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return formattedItem;
|
|
2102
|
+
});
|
|
2103
|
+
seedData[collectionName] = formattedItems;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return seedData;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/seed-writer.ts
|
|
2110
|
+
import fs9 from "fs-extra";
|
|
2111
|
+
import path11 from "path";
|
|
2112
|
+
async function writeSeedData(outputDir, seedData) {
|
|
2113
|
+
const seedDir = path11.join(outputDir, "cms-seed");
|
|
2114
|
+
await fs9.ensureDir(seedDir);
|
|
2115
|
+
const seedPath = path11.join(seedDir, "seed-data.json");
|
|
2116
|
+
await fs9.writeJson(seedPath, seedData, { spaces: 2 });
|
|
2117
|
+
}
|
|
2118
|
+
async function createSeedReadme(outputDir) {
|
|
2119
|
+
const readmePath = path11.join(outputDir, "cms-seed", "README.md");
|
|
2120
|
+
const content = `# CMS Seed Data
|
|
2121
|
+
|
|
2122
|
+
Auto-extracted content from your Webflow export, ready to seed into Strapi.
|
|
2123
|
+
|
|
2124
|
+
## What's in this folder?
|
|
2125
|
+
|
|
2126
|
+
\`seed-data.json\` contains the actual content extracted from your HTML:
|
|
2127
|
+
- **Single types** - Page-specific content (homepage, about page, etc.)
|
|
2128
|
+
- **Collection types** - Repeating items (portfolio cards, team members, etc.)
|
|
2129
|
+
|
|
2130
|
+
## Structure
|
|
2131
|
+
|
|
2132
|
+
\`\`\`json
|
|
2133
|
+
{
|
|
2134
|
+
"index": {
|
|
2135
|
+
"hero_heading_container": "Actual heading from HTML",
|
|
2136
|
+
"hero_bg_image": "/images/hero.jpg",
|
|
2137
|
+
...
|
|
2138
|
+
},
|
|
2139
|
+
"portfolio_cards": [
|
|
2140
|
+
{
|
|
2141
|
+
"image": "/images/card1.jpg",
|
|
2142
|
+
"tag": "Technology",
|
|
2143
|
+
"description": "Card description"
|
|
2144
|
+
}
|
|
2145
|
+
]
|
|
2146
|
+
}
|
|
2147
|
+
\`\`\`
|
|
2148
|
+
|
|
2149
|
+
## How to Seed Strapi
|
|
2150
|
+
|
|
2151
|
+
### Option 1: Manual Entry
|
|
2152
|
+
1. Open Strapi admin panel
|
|
2153
|
+
2. Go to Content Manager
|
|
2154
|
+
3. Create entries using the data from \`seed-data.json\`
|
|
2155
|
+
|
|
2156
|
+
### Option 2: Automated Seeding (Coming Soon)
|
|
2157
|
+
We'll provide a seeding script that:
|
|
2158
|
+
1. Uploads images to Strapi media library
|
|
2159
|
+
2. Creates content entries via Strapi API
|
|
2160
|
+
3. Handles relationships between content types
|
|
2161
|
+
|
|
2162
|
+
## Image Paths
|
|
2163
|
+
|
|
2164
|
+
Image paths in the seed data reference files in your Nuxt \`public/\` directory:
|
|
2165
|
+
- \`/images/hero.jpg\` \u2192 \`public/images/hero.jpg\`
|
|
2166
|
+
|
|
2167
|
+
When seeding Strapi, these images will be uploaded to Strapi's media library.
|
|
2168
|
+
|
|
2169
|
+
## Next Steps
|
|
2170
|
+
|
|
2171
|
+
1. Review the extracted data for accuracy
|
|
2172
|
+
2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
|
|
2173
|
+
3. Use this seed data to populate your CMS
|
|
2174
|
+
`;
|
|
2175
|
+
await fs9.writeFile(readmePath, content, "utf-8");
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/converter.ts
|
|
2179
|
+
async function convertWebflowExport(options) {
|
|
2180
|
+
const { inputDir, outputDir, boilerplate } = options;
|
|
2181
|
+
console.log(pc3.cyan("\u{1F680} Starting Webflow to Nuxt conversion..."));
|
|
2182
|
+
console.log(pc3.dim(`Input: ${inputDir}`));
|
|
2183
|
+
console.log(pc3.dim(`Output: ${outputDir}`));
|
|
2184
|
+
try {
|
|
2185
|
+
await setupBoilerplate(boilerplate, outputDir);
|
|
2186
|
+
const inputExists = await fs10.pathExists(inputDir);
|
|
2187
|
+
if (!inputExists) {
|
|
2188
|
+
throw new Error(`Input directory not found: ${inputDir}`);
|
|
2189
|
+
}
|
|
2190
|
+
console.log(pc3.blue("\n\u{1F4C2} Scanning assets..."));
|
|
2191
|
+
const assets = await scanAssets(inputDir);
|
|
2192
|
+
console.log(pc3.green(` \u2713 Found ${assets.css.length} CSS files`));
|
|
2193
|
+
console.log(pc3.green(` \u2713 Found ${assets.images.length} images`));
|
|
2194
|
+
console.log(pc3.green(` \u2713 Found ${assets.fonts.length} fonts`));
|
|
2195
|
+
console.log(pc3.green(` \u2713 Found ${assets.js.length} JS files`));
|
|
2196
|
+
console.log(pc3.blue("\n\u{1F4E6} Copying assets..."));
|
|
2197
|
+
await copyAllAssets(inputDir, outputDir, assets);
|
|
2198
|
+
console.log(pc3.green(" \u2713 Assets copied successfully"));
|
|
2199
|
+
console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
|
|
2200
|
+
const htmlFiles = await findHTMLFiles(inputDir);
|
|
2201
|
+
console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
|
|
2202
|
+
const htmlContentMap = /* @__PURE__ */ new Map();
|
|
2203
|
+
for (const htmlFile of htmlFiles) {
|
|
2204
|
+
const html = await readHTMLFile(inputDir, htmlFile);
|
|
2205
|
+
const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
|
|
2206
|
+
htmlContentMap.set(pageName, html);
|
|
2207
|
+
console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
|
|
2208
|
+
}
|
|
2209
|
+
console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
|
|
2210
|
+
let allEmbeddedStyles = "";
|
|
2211
|
+
for (const htmlFile of htmlFiles) {
|
|
2212
|
+
const html = htmlContentMap.get(htmlFile.replace(".html", "").replace(/\//g, "-"));
|
|
2213
|
+
const parsed = parseHTML(html, htmlFile);
|
|
2214
|
+
if (parsed.embeddedStyles) {
|
|
2215
|
+
allEmbeddedStyles += `
|
|
2216
|
+
/* From ${htmlFile} */
|
|
2217
|
+
${parsed.embeddedStyles}
|
|
2218
|
+
`;
|
|
2219
|
+
}
|
|
2220
|
+
const transformed = transformForNuxt(parsed.htmlContent);
|
|
2221
|
+
const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
|
|
2222
|
+
const vueComponent = htmlToVueComponent(transformed, pageName);
|
|
2223
|
+
await writeVueComponent(outputDir, htmlFile, vueComponent);
|
|
2224
|
+
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
|
|
2225
|
+
}
|
|
2226
|
+
await formatVueFiles(outputDir);
|
|
2227
|
+
console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
|
|
2228
|
+
const pagesDir = path12.join(outputDir, "pages");
|
|
2229
|
+
const manifest = await generateManifest(pagesDir);
|
|
2230
|
+
await writeManifest(outputDir, manifest);
|
|
2231
|
+
const totalFields = Object.values(manifest.pages).reduce(
|
|
2232
|
+
(sum, page) => sum + Object.keys(page.fields || {}).length,
|
|
2233
|
+
0
|
|
2234
|
+
);
|
|
2235
|
+
const totalCollections = Object.values(manifest.pages).reduce(
|
|
2236
|
+
(sum, page) => sum + Object.keys(page.collections || {}).length,
|
|
2237
|
+
0
|
|
2238
|
+
);
|
|
2239
|
+
console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
|
|
2240
|
+
console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
|
|
2241
|
+
console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
|
|
2242
|
+
console.log(pc3.blue("\n\u26A1 Transforming Vue files to reactive templates..."));
|
|
2243
|
+
await transformAllVuePages(pagesDir, manifest);
|
|
2244
|
+
console.log(pc3.green(` \u2713 Transformed ${Object.keys(manifest.pages).length} pages to use Vue template syntax`));
|
|
2245
|
+
console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
|
|
2246
|
+
console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
|
|
2247
|
+
console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
|
|
2248
|
+
const extractedContent = extractAllContent(htmlContentMap, manifest);
|
|
2249
|
+
const seedData = formatForStrapi(extractedContent);
|
|
2250
|
+
await writeSeedData(outputDir, seedData);
|
|
2251
|
+
await createSeedReadme(outputDir);
|
|
2252
|
+
const pagesWithContent = Object.keys(seedData).filter((key) => {
|
|
2253
|
+
const data = seedData[key];
|
|
2254
|
+
if (Array.isArray(data)) return data.length > 0;
|
|
2255
|
+
return Object.keys(data).length > 0;
|
|
2256
|
+
}).length;
|
|
2257
|
+
console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
|
|
2258
|
+
console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
|
|
2259
|
+
console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
2260
|
+
const schemas = manifestToSchemas(manifest);
|
|
2261
|
+
await writeAllSchemas(outputDir, schemas);
|
|
2262
|
+
await createStrapiReadme(outputDir);
|
|
2263
|
+
console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
|
|
2264
|
+
console.log(pc3.dim(" View schemas in: cms-schemas/"));
|
|
2265
|
+
if (allEmbeddedStyles.trim()) {
|
|
2266
|
+
console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
|
|
2267
|
+
const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
|
|
2268
|
+
await writeEmbeddedStyles(outputDir, dedupedStyles);
|
|
2269
|
+
console.log(pc3.green(" \u2713 Embedded styles added to main.css"));
|
|
2270
|
+
}
|
|
2271
|
+
console.log(pc3.blue("\n\u{1F527} Generating webflow-assets.ts plugin..."));
|
|
2272
|
+
await writeWebflowAssetPlugin(outputDir, assets.css);
|
|
2273
|
+
console.log(pc3.green(" \u2713 Plugin generated (existing file overwritten)"));
|
|
2274
|
+
console.log(pc3.blue("\n\u2699\uFE0F Updating nuxt.config.ts..."));
|
|
2275
|
+
try {
|
|
2276
|
+
await updateNuxtConfig(outputDir, assets.css);
|
|
2277
|
+
console.log(pc3.green(" \u2713 Config updated"));
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
|
|
2280
|
+
console.log(pc3.dim(" Please add CSS files manually"));
|
|
2281
|
+
}
|
|
2282
|
+
console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
|
|
2283
|
+
await createEditorPlugin(outputDir);
|
|
2284
|
+
await createEditorContentComposable(outputDir);
|
|
2285
|
+
await createStrapiContentComposable(outputDir);
|
|
2286
|
+
await addEditorDependency(outputDir);
|
|
2287
|
+
await createSaveEndpoint(outputDir);
|
|
2288
|
+
await createPublishEndpoint(outputDir);
|
|
2289
|
+
await createStrapiBootstrap(outputDir);
|
|
2290
|
+
await addStrapiUrlToConfig(outputDir);
|
|
2291
|
+
console.log(pc3.green(" \u2713 Editor plugin created"));
|
|
2292
|
+
console.log(pc3.green(" \u2713 Editor content composable created"));
|
|
2293
|
+
console.log(pc3.green(" \u2713 Strapi content composable created"));
|
|
2294
|
+
console.log(pc3.green(" \u2713 Editor dependency added"));
|
|
2295
|
+
console.log(pc3.green(" \u2713 Save endpoint created"));
|
|
2296
|
+
console.log(pc3.green(" \u2713 Publish endpoint created"));
|
|
2297
|
+
console.log(pc3.green(" \u2713 Strapi bootstrap file generated"));
|
|
2298
|
+
console.log(pc3.green(" \u2713 Strapi config added"));
|
|
2299
|
+
console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
|
|
2300
|
+
console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
|
|
2301
|
+
console.log(pc3.dim(` 1. cd ${outputDir}`));
|
|
2302
|
+
console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
|
|
2303
|
+
console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
|
|
2304
|
+
console.log(pc3.dim(" 4. Copy strapi-bootstrap/index.ts to your Strapi project at src/index.ts"));
|
|
2305
|
+
console.log(pc3.dim(" (This auto-enables public read permissions on Strapi startup)"));
|
|
2306
|
+
console.log(pc3.dim(" 5. Seed Strapi with data from cms-seed/"));
|
|
2307
|
+
console.log(pc3.dim(" 6. pnpm install && pnpm dev"));
|
|
2308
|
+
console.log(pc3.dim(" 7. Visit http://localhost:3000?preview=true to edit inline!"));
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
console.error(pc3.red("\n\u274C Conversion failed:"));
|
|
2311
|
+
console.error(pc3.red(error instanceof Error ? error.message : String(error)));
|
|
2312
|
+
throw error;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/generator.ts
|
|
2317
|
+
async function generateSchemas(_manifestPath, _cmsType) {
|
|
2318
|
+
throw new Error("Not yet implemented");
|
|
2319
|
+
}
|
|
2320
|
+
export {
|
|
2321
|
+
convertWebflowExport,
|
|
2322
|
+
detectEditableFields,
|
|
2323
|
+
generateManifest,
|
|
2324
|
+
generateSchemas,
|
|
2325
|
+
manifestToSchemas,
|
|
2326
|
+
readManifest,
|
|
2327
|
+
setupBoilerplate,
|
|
2328
|
+
transformAllVuePages
|
|
2329
|
+
};
|
|
2330
|
+
//# sourceMappingURL=index.mjs.map
|