@see-ms/converter 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -202
- package/dist/cli.mjs +2792 -457
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +246 -12
- package/dist/index.mjs +2783 -356
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -4,20 +4,79 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import pc4 from "picocolors";
|
|
6
6
|
import * as readline2 from "readline";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs14 from "fs-extra";
|
|
8
|
+
import path18 from "path";
|
|
9
9
|
|
|
10
10
|
// src/converter.ts
|
|
11
11
|
import pc3 from "picocolors";
|
|
12
|
-
import
|
|
13
|
-
import fs10 from "fs-extra";
|
|
12
|
+
import path16 from "path";
|
|
14
13
|
|
|
15
14
|
// src/filesystem.ts
|
|
16
15
|
import fs from "fs-extra";
|
|
17
|
-
import
|
|
16
|
+
import path2 from "path";
|
|
18
17
|
import { glob } from "glob";
|
|
19
18
|
import { execSync } from "child_process";
|
|
20
19
|
import pc from "picocolors";
|
|
20
|
+
|
|
21
|
+
// src/assets.ts
|
|
22
|
+
import path from "path";
|
|
23
|
+
var RESPONSIVE_VARIANT_RE = /-p-(?:\d+(?:x\d+q\d+)?)(?=\.[^.]+$)/i;
|
|
24
|
+
function isResponsiveImageVariant(filePath) {
|
|
25
|
+
return RESPONSIVE_VARIANT_RE.test(path.basename(filePath));
|
|
26
|
+
}
|
|
27
|
+
function toOriginalImageCandidate(filePath) {
|
|
28
|
+
return filePath.replace(RESPONSIVE_VARIANT_RE, "");
|
|
29
|
+
}
|
|
30
|
+
function normalizeAssetUrl(value) {
|
|
31
|
+
try {
|
|
32
|
+
return decodeURI(value);
|
|
33
|
+
} catch {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function normalizeImageSeedPath(imageSrc) {
|
|
38
|
+
if (!imageSrc) return "";
|
|
39
|
+
if (/^(https?:)?\/\//i.test(imageSrc) || imageSrc.startsWith("data:")) return imageSrc;
|
|
40
|
+
const [pathPart] = imageSrc.split(/[?#]/);
|
|
41
|
+
const decoded = normalizeAssetUrl(pathPart).replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
42
|
+
const withoutLeadingSlash = decoded.replace(/^\/+/, "");
|
|
43
|
+
const withoutAssetsPrefix = withoutLeadingSlash.replace(/^assets\/images\//, "images/");
|
|
44
|
+
const imageIndex = withoutAssetsPrefix.indexOf("images/");
|
|
45
|
+
const imagePath = imageIndex >= 0 ? withoutAssetsPrefix.slice(imageIndex) : `images/${path.basename(withoutAssetsPrefix)}`;
|
|
46
|
+
return `/${toOriginalImageCandidate(imagePath)}`;
|
|
47
|
+
}
|
|
48
|
+
function mediaLookupKeys(value) {
|
|
49
|
+
if (!value) return [];
|
|
50
|
+
const decoded = normalizeAssetUrl(value);
|
|
51
|
+
const withoutLeadingSlash = decoded.replace(/^\/+/, "");
|
|
52
|
+
const withoutAssetsPrefix = withoutLeadingSlash.replace(/^assets\/images\//, "images/");
|
|
53
|
+
const withoutImagesPrefix = withoutAssetsPrefix.replace(/^images\//, "");
|
|
54
|
+
const original = toOriginalImageCandidate(withoutImagesPrefix);
|
|
55
|
+
const basename = path.basename(original);
|
|
56
|
+
const basenameHyphenated = basename.replace(/\s+/g, "-");
|
|
57
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
58
|
+
decoded,
|
|
59
|
+
withoutLeadingSlash,
|
|
60
|
+
withoutAssetsPrefix,
|
|
61
|
+
withoutImagesPrefix,
|
|
62
|
+
original,
|
|
63
|
+
basename,
|
|
64
|
+
basenameHyphenated,
|
|
65
|
+
`/images/${withoutImagesPrefix}`,
|
|
66
|
+
`images/${withoutImagesPrefix}`,
|
|
67
|
+
`/images/${original}`,
|
|
68
|
+
`images/${original}`,
|
|
69
|
+
`/assets/images/${withoutImagesPrefix}`,
|
|
70
|
+
`assets/images/${withoutImagesPrefix}`,
|
|
71
|
+
`/assets/images/${original}`,
|
|
72
|
+
`assets/images/${original}`
|
|
73
|
+
]));
|
|
74
|
+
}
|
|
75
|
+
function isLikelyImagePath(value) {
|
|
76
|
+
return /\.(?:jpe?g|png|gif|webp|avif|svg)(?:[?#].*)?$/i.test(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/filesystem.ts
|
|
21
80
|
async function scanAssets(webflowDir) {
|
|
22
81
|
const assets = {
|
|
23
82
|
css: [],
|
|
@@ -27,47 +86,55 @@ async function scanAssets(webflowDir) {
|
|
|
27
86
|
};
|
|
28
87
|
const cssFiles = await glob("css/**/*.css", { cwd: webflowDir });
|
|
29
88
|
assets.css = cssFiles;
|
|
30
|
-
const imageFiles = await glob("images/**/*", { cwd: webflowDir });
|
|
31
|
-
assets.images = imageFiles;
|
|
32
|
-
const fontFiles = await glob("fonts/**/*", { cwd: webflowDir });
|
|
89
|
+
const imageFiles = await glob("images/**/*", { cwd: webflowDir, nodir: true });
|
|
90
|
+
assets.images = imageFiles.filter((file) => !isResponsiveImageVariant(file));
|
|
91
|
+
const fontFiles = await glob("fonts/**/*", { cwd: webflowDir, nodir: true });
|
|
33
92
|
assets.fonts = fontFiles;
|
|
34
93
|
const jsFiles = await glob("js/**/*.js", { cwd: webflowDir });
|
|
35
94
|
assets.js = jsFiles;
|
|
36
95
|
return assets;
|
|
37
96
|
}
|
|
38
97
|
async function copyCSSFiles(webflowDir, outputDir, cssFiles) {
|
|
39
|
-
const targetDir =
|
|
98
|
+
const targetDir = path2.join(outputDir, "assets", "css");
|
|
40
99
|
await fs.ensureDir(targetDir);
|
|
41
100
|
for (const file of cssFiles) {
|
|
42
|
-
const source =
|
|
43
|
-
const
|
|
101
|
+
const source = path2.join(webflowDir, file);
|
|
102
|
+
const relative = path2.relative("css", file);
|
|
103
|
+
const target = path2.join(targetDir, relative);
|
|
104
|
+
await fs.ensureDir(path2.dirname(target));
|
|
44
105
|
await fs.copy(source, target);
|
|
45
106
|
}
|
|
46
107
|
}
|
|
47
108
|
async function copyImages(webflowDir, outputDir, imageFiles) {
|
|
48
|
-
const targetDir =
|
|
109
|
+
const targetDir = path2.join(outputDir, "public", "assets", "images");
|
|
49
110
|
await fs.ensureDir(targetDir);
|
|
50
111
|
for (const file of imageFiles) {
|
|
51
|
-
const source =
|
|
52
|
-
const
|
|
112
|
+
const source = path2.join(webflowDir, file);
|
|
113
|
+
const relative = path2.relative("images", file);
|
|
114
|
+
const target = path2.join(targetDir, relative);
|
|
115
|
+
await fs.ensureDir(path2.dirname(target));
|
|
53
116
|
await fs.copy(source, target);
|
|
54
117
|
}
|
|
55
118
|
}
|
|
56
119
|
async function copyFonts(webflowDir, outputDir, fontFiles) {
|
|
57
|
-
const targetDir =
|
|
120
|
+
const targetDir = path2.join(outputDir, "public", "assets", "fonts");
|
|
58
121
|
await fs.ensureDir(targetDir);
|
|
59
122
|
for (const file of fontFiles) {
|
|
60
|
-
const source =
|
|
61
|
-
const
|
|
123
|
+
const source = path2.join(webflowDir, file);
|
|
124
|
+
const relative = path2.relative("fonts", file);
|
|
125
|
+
const target = path2.join(targetDir, relative);
|
|
126
|
+
await fs.ensureDir(path2.dirname(target));
|
|
62
127
|
await fs.copy(source, target);
|
|
63
128
|
}
|
|
64
129
|
}
|
|
65
130
|
async function copyJSFiles(webflowDir, outputDir, jsFiles) {
|
|
66
|
-
const targetDir =
|
|
131
|
+
const targetDir = path2.join(outputDir, "public", "assets", "js");
|
|
67
132
|
await fs.ensureDir(targetDir);
|
|
68
133
|
for (const file of jsFiles) {
|
|
69
|
-
const source =
|
|
70
|
-
const
|
|
134
|
+
const source = path2.join(webflowDir, file);
|
|
135
|
+
const relative = path2.relative("js", file);
|
|
136
|
+
const target = path2.join(targetDir, relative);
|
|
137
|
+
await fs.ensureDir(path2.dirname(target));
|
|
71
138
|
await fs.copy(source, target);
|
|
72
139
|
}
|
|
73
140
|
}
|
|
@@ -78,26 +145,53 @@ async function copyAllAssets(webflowDir, outputDir, assets) {
|
|
|
78
145
|
await copyJSFiles(webflowDir, outputDir, assets.js);
|
|
79
146
|
}
|
|
80
147
|
async function findHTMLFiles(webflowDir) {
|
|
81
|
-
const htmlFiles = await glob("**/*.html", { cwd: webflowDir });
|
|
148
|
+
const htmlFiles = await glob("**/*.html", { cwd: webflowDir, nodir: true });
|
|
82
149
|
return htmlFiles;
|
|
83
150
|
}
|
|
84
151
|
async function readHTMLFile(webflowDir, fileName) {
|
|
85
|
-
const filePath =
|
|
152
|
+
const filePath = path2.join(webflowDir, fileName);
|
|
86
153
|
return await fs.readFile(filePath, "utf-8");
|
|
87
154
|
}
|
|
88
|
-
async function writeVueComponent(outputDir, fileName, content) {
|
|
89
|
-
|
|
155
|
+
async function writeVueComponent(outputDir, fileName, content, target = "nuxt", cssFiles = [], editorEnabled = false) {
|
|
156
|
+
if (target === "astro-vue") {
|
|
157
|
+
const componentDir = path2.join(outputDir, "src", "components", "pages");
|
|
158
|
+
const astroPagesDir = path2.join(outputDir, "src", "pages");
|
|
159
|
+
const vueName2 = fileName.replace(".html", ".vue");
|
|
160
|
+
const astroName = fileName.replace(".html", ".astro");
|
|
161
|
+
const vuePath = path2.join(componentDir, vueName2);
|
|
162
|
+
const astroPath = path2.join(astroPagesDir, astroName);
|
|
163
|
+
const relativeVueImport = ensureRelativeImport(path2.relative(path2.dirname(astroPath), vuePath));
|
|
164
|
+
const cssImports = cssFiles.map((file) => `import '${ensureRelativeImport(path2.relative(path2.dirname(astroPath), path2.join(outputDir, "assets", "css", path2.relative("css", file))))}';`).join("\n");
|
|
165
|
+
const editorScript = editorEnabled ? "\n<script>\n import '../cms-editor';\n</script>\n" : "";
|
|
166
|
+
await fs.ensureDir(path2.dirname(vuePath));
|
|
167
|
+
await fs.ensureDir(path2.dirname(astroPath));
|
|
168
|
+
await fs.writeFile(vuePath, content, "utf-8");
|
|
169
|
+
await fs.writeFile(astroPath, `---
|
|
170
|
+
import Page from '${relativeVueImport}';
|
|
171
|
+
${cssImports}
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
<Page client:load />
|
|
175
|
+
${editorScript}
|
|
176
|
+
`, "utf-8");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const pagesDir = path2.join(outputDir, "pages");
|
|
90
180
|
const vueName = fileName.replace(".html", ".vue");
|
|
91
|
-
const targetPath =
|
|
92
|
-
await fs.ensureDir(
|
|
181
|
+
const targetPath = path2.join(pagesDir, vueName);
|
|
182
|
+
await fs.ensureDir(path2.dirname(targetPath));
|
|
93
183
|
await fs.writeFile(targetPath, content, "utf-8");
|
|
94
184
|
}
|
|
95
|
-
|
|
96
|
-
const
|
|
185
|
+
function ensureRelativeImport(importPath) {
|
|
186
|
+
const normalized = importPath.split(path2.sep).join("/");
|
|
187
|
+
return normalized.startsWith(".") ? normalized : `./${normalized}`;
|
|
188
|
+
}
|
|
189
|
+
async function formatVueFiles(outputDir, target = "nuxt") {
|
|
190
|
+
const pagesDir = target === "astro-vue" ? path2.join(outputDir, "src", "components", "pages") : path2.join(outputDir, "pages");
|
|
97
191
|
try {
|
|
98
192
|
console.log(pc.blue("\n\u2728 Formatting Vue files with Prettier..."));
|
|
99
|
-
execSync("
|
|
100
|
-
execSync(`
|
|
193
|
+
execSync("prettier --version", { stdio: "ignore" });
|
|
194
|
+
execSync(`prettier --write "${pagesDir}/**/*.vue"`, {
|
|
101
195
|
cwd: outputDir,
|
|
102
196
|
stdio: "inherit"
|
|
103
197
|
});
|
|
@@ -109,30 +203,35 @@ async function formatVueFiles(outputDir) {
|
|
|
109
203
|
|
|
110
204
|
// src/parser.ts
|
|
111
205
|
import * as cheerio from "cheerio";
|
|
112
|
-
import
|
|
113
|
-
function normalizeRoute(href) {
|
|
114
|
-
|
|
206
|
+
import path3 from "path";
|
|
207
|
+
function normalizeRoute(href, currentFile) {
|
|
208
|
+
const [pathPart, suffix = ""] = href.split(/(?=[?#])/);
|
|
209
|
+
let route = pathPart.replace(/\.html$/i, "");
|
|
115
210
|
if (route === "index" || route === "/index" || route.endsWith("/index")) {
|
|
116
|
-
|
|
211
|
+
const parent = route.replace(/(^|\/)index$/, "");
|
|
212
|
+
return `${parent ? parent.startsWith("/") ? parent : `/${parent}` : "/"}${suffix}`;
|
|
117
213
|
}
|
|
118
214
|
if (route === ".." || route === "../" || route === "/.." || route === "../index") {
|
|
119
|
-
return
|
|
215
|
+
return `/${suffix}`;
|
|
216
|
+
}
|
|
217
|
+
if (currentFile && !route.startsWith("/")) {
|
|
218
|
+
route = path3.posix.join(path3.posix.dirname(currentFile.replace(/\\/g, "/")), route);
|
|
120
219
|
}
|
|
121
|
-
|
|
122
|
-
const normalized = path2.posix.normalize(route);
|
|
220
|
+
const normalized = path3.posix.normalize(route);
|
|
123
221
|
if (!normalized.startsWith("/")) {
|
|
124
|
-
return
|
|
222
|
+
return `/${normalized}${suffix}`;
|
|
125
223
|
}
|
|
126
224
|
if (normalized === "." || normalized === "") {
|
|
127
|
-
return
|
|
225
|
+
return `/${suffix}`;
|
|
128
226
|
}
|
|
129
|
-
return normalized
|
|
227
|
+
return `${normalized}${suffix}`;
|
|
130
228
|
}
|
|
131
229
|
function normalizeAssetPath(src) {
|
|
132
230
|
if (!src || src.startsWith("http") || src.startsWith("https")) {
|
|
133
231
|
return src;
|
|
134
232
|
}
|
|
135
|
-
let normalized = src.replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
233
|
+
let normalized = normalizeAssetUrl(src).replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
234
|
+
normalized = toOriginalImageCandidate(normalized);
|
|
136
235
|
if (normalized.startsWith("/assets/")) {
|
|
137
236
|
normalized = normalized.replace(/\/\.\.\//g, "/");
|
|
138
237
|
return normalized;
|
|
@@ -153,11 +252,15 @@ function parseHTML(html, fileName) {
|
|
|
153
252
|
$(".global-embed style").each((_, el) => {
|
|
154
253
|
embeddedStyles += $(el).html() + "\n";
|
|
155
254
|
});
|
|
255
|
+
$(".w-embed > style").each((_, el) => {
|
|
256
|
+
embeddedStyles += $(el).html() + "\n";
|
|
257
|
+
});
|
|
156
258
|
$("body > style").each((_, el) => {
|
|
157
259
|
embeddedStyles += $(el).html() + "\n";
|
|
158
260
|
});
|
|
159
261
|
$(".global-embed").remove();
|
|
160
262
|
$("body > style").remove();
|
|
263
|
+
$(".w-embed > style").remove();
|
|
161
264
|
$("body script").remove();
|
|
162
265
|
const images = [];
|
|
163
266
|
$("img").each((_, el) => {
|
|
@@ -184,7 +287,7 @@ function parseHTML(html, fileName) {
|
|
|
184
287
|
links
|
|
185
288
|
};
|
|
186
289
|
}
|
|
187
|
-
function transformForNuxt(html) {
|
|
290
|
+
function transformForNuxt(html, currentFile) {
|
|
188
291
|
const $ = cheerio.load(html);
|
|
189
292
|
$("html, head, body").each((_, el) => {
|
|
190
293
|
const $el = $(el);
|
|
@@ -197,12 +300,13 @@ function transformForNuxt(html) {
|
|
|
197
300
|
if (!href) return;
|
|
198
301
|
const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#");
|
|
199
302
|
if (!isExternal) {
|
|
200
|
-
const route = normalizeRoute(href);
|
|
201
|
-
$el.attr("to", route);
|
|
202
|
-
$el.removeAttr("href");
|
|
303
|
+
const route = normalizeRoute(href, currentFile);
|
|
203
304
|
const content = $el.html();
|
|
204
|
-
const
|
|
205
|
-
|
|
305
|
+
const attrs = { ...$el.attr() };
|
|
306
|
+
delete attrs.href;
|
|
307
|
+
attrs.to = route;
|
|
308
|
+
const attrString = Object.entries(attrs).map(([name, value]) => `${name}="${escapeAttribute(value ?? "")}"`).join(" ");
|
|
309
|
+
$el.replaceWith(`<nuxt-link ${attrString}>${content}</nuxt-link>`);
|
|
206
310
|
}
|
|
207
311
|
});
|
|
208
312
|
$("img").each((_, el) => {
|
|
@@ -217,10 +321,18 @@ function transformForNuxt(html) {
|
|
|
217
321
|
});
|
|
218
322
|
return $.html();
|
|
219
323
|
}
|
|
220
|
-
function
|
|
221
|
-
return
|
|
222
|
-
|
|
324
|
+
function escapeAttribute(value) {
|
|
325
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
326
|
+
}
|
|
327
|
+
function htmlToVueComponent(html, pageName, componentImports, componentImportBase = "~/components") {
|
|
328
|
+
let importsSection = "";
|
|
329
|
+
if (componentImports && componentImports.length > 0) {
|
|
330
|
+
importsSection = componentImports.map((name) => `import ${name} from '${componentImportBase}/${name}.vue';`).join("\n");
|
|
331
|
+
html = restoreComponentTags(replaceComponentMarkers(html), componentImports);
|
|
332
|
+
}
|
|
333
|
+
return `<script setup lang="ts">
|
|
223
334
|
// Page: ${pageName}
|
|
335
|
+
${importsSection}
|
|
224
336
|
</script>
|
|
225
337
|
|
|
226
338
|
<template>
|
|
@@ -230,6 +342,17 @@ function htmlToVueComponent(html, pageName) {
|
|
|
230
342
|
</template>
|
|
231
343
|
`;
|
|
232
344
|
}
|
|
345
|
+
function replaceComponentMarkers(html) {
|
|
346
|
+
return html.replace(/<!--COMPONENT:(\w+)-->/g, "<$1 />");
|
|
347
|
+
}
|
|
348
|
+
function restoreComponentTags(html, componentImports) {
|
|
349
|
+
let restored = html;
|
|
350
|
+
for (const name of componentImports) {
|
|
351
|
+
const lowered = name.toLowerCase();
|
|
352
|
+
restored = restored.replace(new RegExp(`<${lowered}\\s*><\\/${lowered}>`, "g"), `<${name} />`).replace(new RegExp(`<${lowered}\\s*\\/>`, "g"), `<${name} />`);
|
|
353
|
+
}
|
|
354
|
+
return restored;
|
|
355
|
+
}
|
|
233
356
|
function deduplicateStyles(styles) {
|
|
234
357
|
if (!styles.trim()) return "";
|
|
235
358
|
const sections = styles.split(/\/\* From .+ \*\//);
|
|
@@ -245,9 +368,9 @@ function deduplicateStyles(styles) {
|
|
|
245
368
|
|
|
246
369
|
// src/config-updater.ts
|
|
247
370
|
import fs2 from "fs-extra";
|
|
248
|
-
import
|
|
371
|
+
import path4 from "path";
|
|
249
372
|
function generateWebflowAssetPlugin(cssFiles) {
|
|
250
|
-
const webflowFiles = cssFiles.map((file) => `/assets/css/${
|
|
373
|
+
const webflowFiles = cssFiles.map((file) => `/assets/css/${path4.basename(file)}`);
|
|
251
374
|
return `import type { Plugin } from 'vite'
|
|
252
375
|
|
|
253
376
|
const webflowFiles = [${webflowFiles.map((f) => `'${f}'`).join(", ")}]
|
|
@@ -280,20 +403,20 @@ export default webflowURLReset
|
|
|
280
403
|
`;
|
|
281
404
|
}
|
|
282
405
|
async function writeWebflowAssetPlugin(outputDir, cssFiles) {
|
|
283
|
-
const utilsDir =
|
|
406
|
+
const utilsDir = path4.join(outputDir, "utils");
|
|
284
407
|
await fs2.ensureDir(utilsDir);
|
|
285
408
|
const content = generateWebflowAssetPlugin(cssFiles);
|
|
286
|
-
const targetPath =
|
|
409
|
+
const targetPath = path4.join(utilsDir, "webflow-assets.ts");
|
|
287
410
|
await fs2.writeFile(targetPath, content, "utf-8");
|
|
288
411
|
}
|
|
289
412
|
async function updateNuxtConfig(outputDir, cssFiles) {
|
|
290
|
-
const configPath =
|
|
413
|
+
const configPath = path4.join(outputDir, "nuxt.config.ts");
|
|
291
414
|
const configExists = await fs2.pathExists(configPath);
|
|
292
415
|
if (!configExists) {
|
|
293
416
|
throw new Error("nuxt.config.ts not found in output directory");
|
|
294
417
|
}
|
|
295
418
|
let config = await fs2.readFile(configPath, "utf-8");
|
|
296
|
-
const cssEntries = cssFiles.map((file) => ` '~/assets/css/${
|
|
419
|
+
const cssEntries = cssFiles.map((file) => ` '~/assets/css/${path4.basename(file)}'`);
|
|
297
420
|
if (config.includes("css:")) {
|
|
298
421
|
config = config.replace(
|
|
299
422
|
/css:\s*\[/,
|
|
@@ -313,9 +436,9 @@ ${cssEntries.join(",\n")}
|
|
|
313
436
|
}
|
|
314
437
|
async function writeEmbeddedStyles(outputDir, styles) {
|
|
315
438
|
if (!styles.trim()) return;
|
|
316
|
-
const cssDir =
|
|
439
|
+
const cssDir = path4.join(outputDir, "assets", "css");
|
|
317
440
|
await fs2.ensureDir(cssDir);
|
|
318
|
-
const mainCssPath =
|
|
441
|
+
const mainCssPath = path4.join(cssDir, "main.css");
|
|
319
442
|
const exists = await fs2.pathExists(mainCssPath);
|
|
320
443
|
if (exists) {
|
|
321
444
|
const existing = await fs2.readFile(mainCssPath, "utf-8");
|
|
@@ -329,7 +452,7 @@ ${styles}`, "utf-8");
|
|
|
329
452
|
}
|
|
330
453
|
}
|
|
331
454
|
async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:1337") {
|
|
332
|
-
const configPath =
|
|
455
|
+
const configPath = path4.join(outputDir, "nuxt.config.ts");
|
|
333
456
|
const configExists = await fs2.pathExists(configPath);
|
|
334
457
|
if (!configExists) {
|
|
335
458
|
throw new Error("nuxt.config.ts not found in output directory");
|
|
@@ -367,9 +490,18 @@ async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:133
|
|
|
367
490
|
|
|
368
491
|
// src/editor-integration.ts
|
|
369
492
|
import fs3 from "fs-extra";
|
|
370
|
-
import
|
|
493
|
+
import path5 from "path";
|
|
494
|
+
function getPageCollections(manifest) {
|
|
495
|
+
if (!manifest) return {};
|
|
496
|
+
return Object.fromEntries(
|
|
497
|
+
Object.entries(manifest.pages).map(([pageName, page]) => [
|
|
498
|
+
pageName,
|
|
499
|
+
Object.entries(page.collections || {}).filter(([, collection]) => collection.storage !== "page-repeatable").map(([collectionName]) => collectionName)
|
|
500
|
+
])
|
|
501
|
+
);
|
|
502
|
+
}
|
|
371
503
|
async function createEditorContentComposable(outputDir) {
|
|
372
|
-
const composablesDir =
|
|
504
|
+
const composablesDir = path5.join(outputDir, "composables");
|
|
373
505
|
await fs3.ensureDir(composablesDir);
|
|
374
506
|
const composableContent = `/**
|
|
375
507
|
* Global state for editor content in preview mode
|
|
@@ -468,21 +600,25 @@ export function useEditorContent(pageName?: string) {
|
|
|
468
600
|
};
|
|
469
601
|
}
|
|
470
602
|
`;
|
|
471
|
-
const composablePath =
|
|
603
|
+
const composablePath = path5.join(composablesDir, "useEditorContent.ts");
|
|
472
604
|
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
473
605
|
}
|
|
474
|
-
async function createStrapiContentComposable(outputDir) {
|
|
475
|
-
const composablesDir =
|
|
606
|
+
async function createStrapiContentComposable(outputDir, manifest) {
|
|
607
|
+
const composablesDir = path5.join(outputDir, "composables");
|
|
476
608
|
await fs3.ensureDir(composablesDir);
|
|
609
|
+
const pageCollections = JSON.stringify(getPageCollections(manifest), null, 2);
|
|
477
610
|
const composableContent = `/**
|
|
478
611
|
* Composable to fetch content from Strapi based on CMS manifest
|
|
479
612
|
* Integrates with editor state for preview mode
|
|
480
613
|
*/
|
|
481
614
|
|
|
615
|
+
const PAGE_COLLECTIONS: Record<string, string[]> = ${pageCollections};
|
|
616
|
+
|
|
482
617
|
export function useStrapiContent(pageName: string) {
|
|
483
618
|
const config = useRuntimeConfig();
|
|
484
619
|
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
485
620
|
const editorContent = useEditorContent(pageName);
|
|
621
|
+
const collectionNames = PAGE_COLLECTIONS[pageName] || [];
|
|
486
622
|
|
|
487
623
|
// Helper to transform Strapi image objects to URL strings
|
|
488
624
|
const transformStrapiImages = (data: any, baseUrl: string): any => {
|
|
@@ -541,6 +677,25 @@ export function useStrapiContent(pageName: string) {
|
|
|
541
677
|
}
|
|
542
678
|
);
|
|
543
679
|
|
|
680
|
+
const collectionFetches = collectionNames.map((collectionName) =>
|
|
681
|
+
useFetch<any>(
|
|
682
|
+
\`\${strapiUrl}/api/\${collectionName}\`,
|
|
683
|
+
{
|
|
684
|
+
key: \`strapi-\${pageName}-collection-\${collectionName}\`,
|
|
685
|
+
query: {
|
|
686
|
+
populate: '*',
|
|
687
|
+
},
|
|
688
|
+
transform: (response) => {
|
|
689
|
+
const data = response?.data || response || [];
|
|
690
|
+
if (Array.isArray(data)) {
|
|
691
|
+
return data.map((item) => transformStrapiImages(item, strapiUrl));
|
|
692
|
+
}
|
|
693
|
+
return transformStrapiImages(data, strapiUrl);
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
|
|
544
699
|
// Initialize editor state with Strapi data when fetched
|
|
545
700
|
// This runs in both normal AND preview mode to ensure initial content is available
|
|
546
701
|
watch(
|
|
@@ -558,12 +713,25 @@ export function useStrapiContent(pageName: string) {
|
|
|
558
713
|
// In preview mode: use editor state
|
|
559
714
|
// In normal mode: use Strapi data (and sync to editor state)
|
|
560
715
|
const content = computed(() => {
|
|
716
|
+
const collections = Object.fromEntries(
|
|
717
|
+
collectionNames.map((collectionName, index) => [
|
|
718
|
+
collectionName,
|
|
719
|
+
collectionFetches[index]?.data.value || []
|
|
720
|
+
])
|
|
721
|
+
);
|
|
722
|
+
|
|
561
723
|
if (editorContent.isPreviewMode.value) {
|
|
562
724
|
// Use editor state in preview mode
|
|
563
|
-
return
|
|
725
|
+
return {
|
|
726
|
+
...editorContent.getPageContent(pageName),
|
|
727
|
+
...collections,
|
|
728
|
+
};
|
|
564
729
|
} else {
|
|
565
730
|
// Use Strapi data in normal mode
|
|
566
|
-
return
|
|
731
|
+
return {
|
|
732
|
+
...(strapiData.value || editorContent.getPageContent(pageName)),
|
|
733
|
+
...collections,
|
|
734
|
+
};
|
|
567
735
|
}
|
|
568
736
|
});
|
|
569
737
|
|
|
@@ -572,11 +740,11 @@ export function useStrapiContent(pageName: string) {
|
|
|
572
740
|
};
|
|
573
741
|
}
|
|
574
742
|
`;
|
|
575
|
-
const composablePath =
|
|
743
|
+
const composablePath = path5.join(composablesDir, "useStrapiContent.ts");
|
|
576
744
|
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
577
745
|
}
|
|
578
746
|
async function createEditorPlugin(outputDir) {
|
|
579
|
-
const pluginsDir =
|
|
747
|
+
const pluginsDir = path5.join(outputDir, "plugins");
|
|
580
748
|
await fs3.ensureDir(pluginsDir);
|
|
581
749
|
const pluginContent = `/**
|
|
582
750
|
* CMS Editor Overlay Plugin
|
|
@@ -585,6 +753,8 @@ async function createEditorPlugin(outputDir) {
|
|
|
585
753
|
|
|
586
754
|
/**
|
|
587
755
|
* Disable Lenis smooth scroll to allow native scrolling in edit mode
|
|
756
|
+
* Note: The primary approach is to conditionally render <VueLenis> in the layout.
|
|
757
|
+
* This function serves as a fallback for existing projects that haven't been updated.
|
|
588
758
|
*/
|
|
589
759
|
function disableLenisInEditMode() {
|
|
590
760
|
try {
|
|
@@ -592,28 +762,48 @@ function disableLenisInEditMode() {
|
|
|
592
762
|
const lenisInstances = [
|
|
593
763
|
(window as any).lenis,
|
|
594
764
|
(window as any).__lenis,
|
|
595
|
-
|
|
765
|
+
(window as any).Lenis,
|
|
596
766
|
];
|
|
597
767
|
|
|
598
768
|
for (const lenis of lenisInstances) {
|
|
769
|
+
if (lenis && typeof lenis.stop === 'function') {
|
|
770
|
+
lenis.stop();
|
|
771
|
+
}
|
|
599
772
|
if (lenis && typeof lenis.destroy === 'function') {
|
|
600
773
|
lenis.destroy();
|
|
601
774
|
return;
|
|
602
775
|
}
|
|
603
776
|
}
|
|
604
777
|
|
|
605
|
-
// Check for Vue Lenis component instances
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
778
|
+
// Check for Vue Lenis component instances via refs
|
|
779
|
+
// VueLenis stores the instance in the component's exposed properties
|
|
780
|
+
const lenisElements = document.querySelectorAll('[data-lenis], .lenis, [data-lenis-prevent]');
|
|
781
|
+
lenisElements.forEach((el: any) => {
|
|
782
|
+
// Try various ways Vue Lenis might store the instance
|
|
783
|
+
const possibleInstances = [
|
|
784
|
+
el.__lenis,
|
|
785
|
+
el._lenis,
|
|
786
|
+
el.$lenis,
|
|
787
|
+
el.__vue__?.exposed?.lenis,
|
|
788
|
+
el.__vueParentComponent?.exposed?.lenis,
|
|
789
|
+
];
|
|
790
|
+
|
|
791
|
+
for (const instance of possibleInstances) {
|
|
792
|
+
if (instance && typeof instance.stop === 'function') {
|
|
793
|
+
instance.stop();
|
|
612
794
|
}
|
|
613
|
-
|
|
614
|
-
|
|
795
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
796
|
+
instance.destroy();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Also remove lenis-related classes from html/body that might affect scrolling
|
|
803
|
+
document.documentElement.classList.remove('lenis', 'lenis-smooth');
|
|
804
|
+
document.body.classList.remove('lenis', 'lenis-smooth');
|
|
615
805
|
} catch (error) {
|
|
616
|
-
// Silently fail - Lenis may not be present
|
|
806
|
+
// Silently fail - Lenis may not be present or already disabled via layout
|
|
617
807
|
}
|
|
618
808
|
}
|
|
619
809
|
|
|
@@ -747,11 +937,11 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
747
937
|
});
|
|
748
938
|
});
|
|
749
939
|
`;
|
|
750
|
-
const pluginPath =
|
|
940
|
+
const pluginPath = path5.join(pluginsDir, "cms-editor.client.ts");
|
|
751
941
|
await fs3.writeFile(pluginPath, pluginContent, "utf-8");
|
|
752
942
|
}
|
|
753
943
|
async function addEditorDependency(outputDir) {
|
|
754
|
-
const packageJsonPath =
|
|
944
|
+
const packageJsonPath = path5.join(outputDir, "package.json");
|
|
755
945
|
if (await fs3.pathExists(packageJsonPath)) {
|
|
756
946
|
const packageJson = await fs3.readJson(packageJsonPath);
|
|
757
947
|
if (!packageJson.dependencies) {
|
|
@@ -762,7 +952,7 @@ async function addEditorDependency(outputDir) {
|
|
|
762
952
|
}
|
|
763
953
|
}
|
|
764
954
|
async function createSaveEndpoint(outputDir) {
|
|
765
|
-
const serverDir =
|
|
955
|
+
const serverDir = path5.join(outputDir, "server", "api", "cms");
|
|
766
956
|
await fs3.ensureDir(serverDir);
|
|
767
957
|
const endpointContent = `/**
|
|
768
958
|
* API endpoint for saving CMS changes
|
|
@@ -824,7 +1014,7 @@ export default defineEventHandler(async (event) => {
|
|
|
824
1014
|
}
|
|
825
1015
|
|
|
826
1016
|
// Load manifest to understand field mappings
|
|
827
|
-
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1017
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
828
1018
|
let manifest;
|
|
829
1019
|
try {
|
|
830
1020
|
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
@@ -959,11 +1149,11 @@ export default defineEventHandler(async (event) => {
|
|
|
959
1149
|
}
|
|
960
1150
|
});
|
|
961
1151
|
`;
|
|
962
|
-
const endpointPath =
|
|
1152
|
+
const endpointPath = path5.join(serverDir, "save.post.ts");
|
|
963
1153
|
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
964
1154
|
}
|
|
965
1155
|
async function createStrapiBootstrap(outputDir) {
|
|
966
|
-
const strapiBootstrapDir =
|
|
1156
|
+
const strapiBootstrapDir = path5.join(outputDir, "strapi-bootstrap");
|
|
967
1157
|
await fs3.ensureDir(strapiBootstrapDir);
|
|
968
1158
|
const bootstrapContent = `/**
|
|
969
1159
|
* Strapi Bootstrap File
|
|
@@ -1078,7 +1268,7 @@ export default {
|
|
|
1078
1268
|
},
|
|
1079
1269
|
};
|
|
1080
1270
|
`;
|
|
1081
|
-
const bootstrapPath =
|
|
1271
|
+
const bootstrapPath = path5.join(strapiBootstrapDir, "index.ts");
|
|
1082
1272
|
await fs3.writeFile(bootstrapPath, bootstrapContent, "utf-8");
|
|
1083
1273
|
const readmeContent = `# Strapi Bootstrap File
|
|
1084
1274
|
|
|
@@ -1128,12 +1318,12 @@ If you prefer to set permissions manually:
|
|
|
1128
1318
|
- Only affects the "Public" role (unauthenticated users)
|
|
1129
1319
|
- Safe to run multiple times (idempotent)
|
|
1130
1320
|
`;
|
|
1131
|
-
const readmePath =
|
|
1321
|
+
const readmePath = path5.join(strapiBootstrapDir, "README.md");
|
|
1132
1322
|
await fs3.writeFile(readmePath, readmeContent, "utf-8");
|
|
1133
1323
|
console.log(" \u2713 Generated Strapi bootstrap file");
|
|
1134
1324
|
}
|
|
1135
1325
|
async function createPublishEndpoint(outputDir) {
|
|
1136
|
-
const serverDir =
|
|
1326
|
+
const serverDir = path5.join(outputDir, "server", "api", "cms");
|
|
1137
1327
|
await fs3.ensureDir(serverDir);
|
|
1138
1328
|
const endpointContent = `/**
|
|
1139
1329
|
* API endpoint for batch publishing CMS changes
|
|
@@ -1195,7 +1385,7 @@ export default defineEventHandler(async (event) => {
|
|
|
1195
1385
|
}
|
|
1196
1386
|
|
|
1197
1387
|
// Load manifest to understand field mappings
|
|
1198
|
-
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1388
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
1199
1389
|
let manifest;
|
|
1200
1390
|
try {
|
|
1201
1391
|
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
@@ -1352,13 +1542,335 @@ export default defineEventHandler(async (event) => {
|
|
|
1352
1542
|
}
|
|
1353
1543
|
});
|
|
1354
1544
|
`;
|
|
1355
|
-
const endpointPath =
|
|
1545
|
+
const endpointPath = path5.join(serverDir, "publish.post.ts");
|
|
1356
1546
|
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
1357
1547
|
}
|
|
1548
|
+
async function createAstroStrapiContentComposable(outputDir, manifest) {
|
|
1549
|
+
const composablesDir = path5.join(outputDir, "src", "composables");
|
|
1550
|
+
await fs3.ensureDir(composablesDir);
|
|
1551
|
+
const pageCollections = JSON.stringify(getPageCollections(manifest), null, 2);
|
|
1552
|
+
const content = `import { computed, reactive, ref, onMounted } from 'vue';
|
|
1553
|
+
|
|
1554
|
+
const PAGE_COLLECTIONS: Record<string, string[]> = ${pageCollections};
|
|
1555
|
+
|
|
1556
|
+
const editorState = reactive<{
|
|
1557
|
+
content: Record<string, Record<string, any>>;
|
|
1558
|
+
hasChanges: Record<string, boolean>;
|
|
1559
|
+
}>({
|
|
1560
|
+
content: {},
|
|
1561
|
+
hasChanges: {},
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
function getStrapiUrl() {
|
|
1565
|
+
return import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function transformStrapiImages(data: any, baseUrl: string): any {
|
|
1569
|
+
if (!data || typeof data !== 'object') return data;
|
|
1570
|
+
if (Array.isArray(data)) return data.map((item) => transformStrapiImages(item, baseUrl));
|
|
1571
|
+
if ('url' in data && ('mime' in data || 'formats' in data)) {
|
|
1572
|
+
return data.url.startsWith('http') ? data.url : \`\${baseUrl}\${data.url}\`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const transformed: Record<string, any> = {};
|
|
1576
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1577
|
+
transformed[key] = transformStrapiImages(value, baseUrl);
|
|
1578
|
+
}
|
|
1579
|
+
return transformed;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
export function useStrapiContent(pageName: string) {
|
|
1583
|
+
const strapiUrl = getStrapiUrl();
|
|
1584
|
+
const strapiData = ref<Record<string, any>>({});
|
|
1585
|
+
const isPreviewMode = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('preview') === 'true';
|
|
1586
|
+
const collectionNames = PAGE_COLLECTIONS[pageName] || [];
|
|
1587
|
+
|
|
1588
|
+
function initializePageContent(page: string, content: Record<string, any>) {
|
|
1589
|
+
if (!editorState.content[page]) {
|
|
1590
|
+
editorState.content[page] = { ...content };
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function getPageContent(page: string) {
|
|
1595
|
+
return editorState.content[page] || {};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function updateField(page: string, fieldName: string, value: any) {
|
|
1599
|
+
if (!editorState.content[page]) editorState.content[page] = {};
|
|
1600
|
+
editorState.content[page][fieldName] = value;
|
|
1601
|
+
editorState.hasChanges[page] = true;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
onMounted(async () => {
|
|
1605
|
+
try {
|
|
1606
|
+
const response = await fetch(\`\${strapiUrl}/api/\${pageName}?populate=*\`);
|
|
1607
|
+
if (!response.ok) return;
|
|
1608
|
+
const json = await response.json();
|
|
1609
|
+
const data = transformStrapiImages(json?.data || json, strapiUrl);
|
|
1610
|
+
const collections = Object.fromEntries(
|
|
1611
|
+
await Promise.all(collectionNames.map(async (collectionName) => {
|
|
1612
|
+
const collectionResponse = await fetch(\`\${strapiUrl}/api/\${collectionName}?populate=*\`);
|
|
1613
|
+
if (!collectionResponse.ok) return [collectionName, []];
|
|
1614
|
+
const collectionJson = await collectionResponse.json();
|
|
1615
|
+
const collectionData = collectionJson?.data || collectionJson || [];
|
|
1616
|
+
return [
|
|
1617
|
+
collectionName,
|
|
1618
|
+
Array.isArray(collectionData)
|
|
1619
|
+
? collectionData.map((item) => transformStrapiImages(item, strapiUrl))
|
|
1620
|
+
: transformStrapiImages(collectionData, strapiUrl)
|
|
1621
|
+
];
|
|
1622
|
+
}))
|
|
1623
|
+
);
|
|
1624
|
+
strapiData.value = { ...(data || {}), ...collections };
|
|
1625
|
+
initializePageContent(pageName, strapiData.value);
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
console.error('[SeeMS] Failed to fetch Strapi content', error);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
(window as any).__editorState = {
|
|
1631
|
+
...editorState,
|
|
1632
|
+
getPageContent,
|
|
1633
|
+
updateField,
|
|
1634
|
+
initializePageContent,
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
const content = computed(() => {
|
|
1639
|
+
return isPreviewMode ? getPageContent(pageName) : (strapiData.value || getPageContent(pageName));
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
return { content };
|
|
1643
|
+
}
|
|
1644
|
+
`;
|
|
1645
|
+
await fs3.writeFile(path5.join(composablesDir, "useStrapiContent.ts"), content, "utf-8");
|
|
1646
|
+
}
|
|
1647
|
+
async function createAstroEditorClient(outputDir) {
|
|
1648
|
+
const srcDir = path5.join(outputDir, "src");
|
|
1649
|
+
await fs3.ensureDir(srcDir);
|
|
1650
|
+
const content = `/**
|
|
1651
|
+
* Astro client entry for the SeeMS inline editor.
|
|
1652
|
+
*/
|
|
1653
|
+
async function initSeeMSEditor() {
|
|
1654
|
+
const params = new URLSearchParams(window.location.search);
|
|
1655
|
+
if (params.get('preview') !== 'true') return;
|
|
1656
|
+
|
|
1657
|
+
const {
|
|
1658
|
+
initEditor,
|
|
1659
|
+
createAuthManager,
|
|
1660
|
+
showLoginModal,
|
|
1661
|
+
createDraftStorage,
|
|
1662
|
+
createURLStateManager,
|
|
1663
|
+
createManifestLoader,
|
|
1664
|
+
createNavigationGuard,
|
|
1665
|
+
getCurrentPageFromRoute,
|
|
1666
|
+
createToolbar,
|
|
1667
|
+
} = await import('@see-ms/editor-overlay');
|
|
1668
|
+
|
|
1669
|
+
const strapiUrl = import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1670
|
+
const urlState = createURLStateManager();
|
|
1671
|
+
urlState.setState({ preview: true });
|
|
1672
|
+
|
|
1673
|
+
const authManager = createAuthManager({
|
|
1674
|
+
strapiUrl,
|
|
1675
|
+
storageKey: 'cms_editor_token',
|
|
1676
|
+
});
|
|
1677
|
+
const draftStorage = createDraftStorage();
|
|
1678
|
+
const manifestLoader = createManifestLoader();
|
|
1679
|
+
|
|
1680
|
+
try {
|
|
1681
|
+
await manifestLoader.load();
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
console.error('[CMS Editor] Failed to load manifest:', error);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
let currentPage = getCurrentPageFromRoute();
|
|
1688
|
+
if (!currentPage) {
|
|
1689
|
+
currentPage = manifestLoader.getPageFromRoute(window.location.pathname);
|
|
1690
|
+
}
|
|
1691
|
+
if (!currentPage) {
|
|
1692
|
+
console.error('[CMS Editor] Could not determine current page');
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
let token = authManager.getToken();
|
|
1697
|
+
if (!token || !await authManager.verifyToken(token)) {
|
|
1698
|
+
try {
|
|
1699
|
+
token = await showLoginModal(authManager);
|
|
1700
|
+
} catch {
|
|
1701
|
+
urlState.clearPreviewMode();
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const navigationGuard = createNavigationGuard({
|
|
1707
|
+
showToast: true,
|
|
1708
|
+
toastMessage: 'Navigation disabled in edit mode',
|
|
1709
|
+
});
|
|
1710
|
+
navigationGuard.enable();
|
|
1711
|
+
|
|
1712
|
+
const editor = initEditor({
|
|
1713
|
+
apiEndpoint: '/api/cms/save',
|
|
1714
|
+
authToken: token,
|
|
1715
|
+
richText: true,
|
|
1716
|
+
manifestLoader,
|
|
1717
|
+
draftStorage,
|
|
1718
|
+
currentPage,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
await editor.enable();
|
|
1722
|
+
|
|
1723
|
+
const toolbar = await createToolbar(editor, {
|
|
1724
|
+
draftStorage,
|
|
1725
|
+
urlState,
|
|
1726
|
+
navigationGuard,
|
|
1727
|
+
manifestLoader,
|
|
1728
|
+
currentPage,
|
|
1729
|
+
});
|
|
1730
|
+
document.body.appendChild(toolbar);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (document.readyState === 'loading') {
|
|
1734
|
+
document.addEventListener('DOMContentLoaded', initSeeMSEditor, { once: true });
|
|
1735
|
+
} else {
|
|
1736
|
+
initSeeMSEditor();
|
|
1737
|
+
}
|
|
1738
|
+
`;
|
|
1739
|
+
await fs3.writeFile(path5.join(srcDir, "cms-editor.ts"), content, "utf-8");
|
|
1740
|
+
}
|
|
1741
|
+
async function createAstroSaveEndpoint(outputDir) {
|
|
1742
|
+
const endpointDir = path5.join(outputDir, "src", "pages", "api", "cms");
|
|
1743
|
+
await fs3.ensureDir(endpointDir);
|
|
1744
|
+
await fs3.writeFile(path5.join(endpointDir, "save.ts"), astroEndpointContent(false), "utf-8");
|
|
1745
|
+
await fs3.writeFile(path5.join(endpointDir, "publish.ts"), astroEndpointContent(true), "utf-8");
|
|
1746
|
+
}
|
|
1747
|
+
function astroEndpointContent(batchPublish) {
|
|
1748
|
+
return `import type { APIRoute } from 'astro';
|
|
1749
|
+
import fs from 'node:fs';
|
|
1750
|
+
import path from 'node:path';
|
|
1751
|
+
|
|
1752
|
+
export const prerender = false;
|
|
1753
|
+
|
|
1754
|
+
const strapiUrl = import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1755
|
+
|
|
1756
|
+
function json(status: number, body: unknown) {
|
|
1757
|
+
return new Response(JSON.stringify(body), {
|
|
1758
|
+
status,
|
|
1759
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
async function verifyToken(token: string) {
|
|
1764
|
+
const adminResponse = await fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
1765
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
1766
|
+
});
|
|
1767
|
+
if (adminResponse.ok) return { user: await adminResponse.json(), isAdminToken: true };
|
|
1768
|
+
|
|
1769
|
+
const userResponse = await fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
1770
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
1771
|
+
});
|
|
1772
|
+
if (userResponse.ok) return { user: await userResponse.json(), isAdminToken: false };
|
|
1773
|
+
|
|
1774
|
+
throw new Error('Invalid or expired token');
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function loadManifest() {
|
|
1778
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
1779
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function filterFields(pageConfig: any, fields: Record<string, any>) {
|
|
1783
|
+
const data: Record<string, any> = {};
|
|
1784
|
+
for (const [fieldName, value] of Object.entries(fields || {})) {
|
|
1785
|
+
if (pageConfig?.fields?.[fieldName]) data[fieldName] = value;
|
|
1786
|
+
}
|
|
1787
|
+
return data;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
async function writePage(page: string, fields: Record<string, any>, token: string, isAdminToken: boolean, publish: boolean) {
|
|
1791
|
+
const manifest = loadManifest();
|
|
1792
|
+
const pageConfig = manifest.pages[page];
|
|
1793
|
+
if (!pageConfig) throw new Error(\`Page "\${page}" not found in manifest\`);
|
|
1794
|
+
const data = filterFields(pageConfig, fields);
|
|
1795
|
+
|
|
1796
|
+
if (isAdminToken) {
|
|
1797
|
+
const endpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
1798
|
+
const update = await fetch(endpoint, {
|
|
1799
|
+
method: 'PUT',
|
|
1800
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1801
|
+
body: JSON.stringify(data),
|
|
1802
|
+
});
|
|
1803
|
+
if (!update.ok) throw new Error(await update.text());
|
|
1804
|
+
|
|
1805
|
+
if (publish) {
|
|
1806
|
+
const publishResponse = await fetch(\`\${endpoint}/actions/publish\`, {
|
|
1807
|
+
method: 'POST',
|
|
1808
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1809
|
+
body: JSON.stringify({}),
|
|
1810
|
+
});
|
|
1811
|
+
if (!publishResponse.ok) throw new Error(await publishResponse.text());
|
|
1812
|
+
}
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
const update = await fetch(\`\${strapiUrl}/api/\${page}\`, {
|
|
1817
|
+
method: 'PUT',
|
|
1818
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1819
|
+
body: JSON.stringify({ data }),
|
|
1820
|
+
});
|
|
1821
|
+
if (!update.ok) throw new Error(await update.text());
|
|
1822
|
+
|
|
1823
|
+
if (publish) {
|
|
1824
|
+
const publishResponse = await fetch(\`\${strapiUrl}/api/\${page}/publish\`, {
|
|
1825
|
+
method: 'POST',
|
|
1826
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1827
|
+
body: JSON.stringify({}),
|
|
1828
|
+
});
|
|
1829
|
+
if (!publishResponse.ok) throw new Error(await publishResponse.text());
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
1834
|
+
const authHeader = request.headers.get('authorization');
|
|
1835
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
1836
|
+
return json(401, { success: false, message: 'Missing authorization header' });
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const token = authHeader.slice(7);
|
|
1840
|
+
try {
|
|
1841
|
+
const { user, isAdminToken } = await verifyToken(token);
|
|
1842
|
+
const body = await request.json();
|
|
1843
|
+
${batchPublish ? ` const pages = Array.isArray(body.pages) ? body.pages : [];
|
|
1844
|
+
const results = await Promise.allSettled(
|
|
1845
|
+
pages.map(({ page, fields }: any) => writePage(page, fields, token, isAdminToken, true))
|
|
1846
|
+
);
|
|
1847
|
+
const failed = results
|
|
1848
|
+
.map((result, index) => ({ result, page: pages[index]?.page }))
|
|
1849
|
+
.filter(({ result }) => result.status === 'rejected')
|
|
1850
|
+
.map(({ result, page }) => ({ page, error: result.status === 'rejected' ? result.reason?.message : 'Unknown error' }));
|
|
1851
|
+
return json(200, {
|
|
1852
|
+
success: failed.length === 0,
|
|
1853
|
+
message: \`Published \${pages.length - failed.length} of \${pages.length} pages\`,
|
|
1854
|
+
failed,
|
|
1855
|
+
user,
|
|
1856
|
+
});` : ` if (!body.page || !body.fields) {
|
|
1857
|
+
return json(400, { success: false, message: 'Missing page or fields' });
|
|
1858
|
+
}
|
|
1859
|
+
await writePage(body.page, body.fields, token, isAdminToken, body.isDraft === false);
|
|
1860
|
+
return json(200, { success: true, message: 'Changes saved successfully', page: body.page, isDraft: body.isDraft !== false, user });`}
|
|
1861
|
+
} catch (error: any) {
|
|
1862
|
+
return json(error.message === 'Invalid or expired token' ? 401 : 500, {
|
|
1863
|
+
success: false,
|
|
1864
|
+
message: error.message || 'Failed to save CMS changes',
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
`;
|
|
1869
|
+
}
|
|
1358
1870
|
|
|
1359
1871
|
// src/boilerplate.ts
|
|
1360
1872
|
import fs4 from "fs-extra";
|
|
1361
|
-
import
|
|
1873
|
+
import path6 from "path";
|
|
1362
1874
|
import { execSync as execSync2 } from "child_process";
|
|
1363
1875
|
import pc2 from "picocolors";
|
|
1364
1876
|
function isGitHubURL(source) {
|
|
@@ -1368,7 +1880,7 @@ async function cloneFromGitHub(repoUrl, outputDir) {
|
|
|
1368
1880
|
console.log(pc2.blue(" Cloning from GitHub..."));
|
|
1369
1881
|
try {
|
|
1370
1882
|
execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
|
|
1371
|
-
const gitDir =
|
|
1883
|
+
const gitDir = path6.join(outputDir, ".git");
|
|
1372
1884
|
await fs4.remove(gitDir);
|
|
1373
1885
|
console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
|
|
1374
1886
|
} catch (error) {
|
|
@@ -1383,30 +1895,88 @@ async function copyFromLocal(sourcePath, outputDir) {
|
|
|
1383
1895
|
}
|
|
1384
1896
|
await fs4.copy(sourcePath, outputDir, {
|
|
1385
1897
|
filter: (src) => {
|
|
1386
|
-
const name =
|
|
1898
|
+
const name = path6.basename(src);
|
|
1387
1899
|
return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
|
|
1388
1900
|
}
|
|
1389
1901
|
});
|
|
1390
1902
|
console.log(pc2.green(" \u2713 Boilerplate copied successfully"));
|
|
1391
1903
|
}
|
|
1392
|
-
async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
1904
|
+
async function setupBoilerplate(boilerplateSource, outputDir, target = "nuxt") {
|
|
1393
1905
|
if (!boilerplateSource) {
|
|
1394
|
-
console.log(pc2.blue(
|
|
1906
|
+
console.log(pc2.blue(`
|
|
1907
|
+
\u{1F4E6} Creating minimal ${target === "astro-vue" ? "Astro + Vue" : "Nuxt"} structure...`));
|
|
1395
1908
|
await fs4.ensureDir(outputDir);
|
|
1396
|
-
await fs4.ensureDir(
|
|
1397
|
-
await fs4.ensureDir(
|
|
1398
|
-
await fs4.ensureDir(
|
|
1399
|
-
await fs4.ensureDir(
|
|
1400
|
-
const configPath =
|
|
1909
|
+
await fs4.ensureDir(target === "astro-vue" ? path6.join(outputDir, "src", "pages") : path6.join(outputDir, "pages"));
|
|
1910
|
+
await fs4.ensureDir(path6.join(outputDir, "assets"));
|
|
1911
|
+
await fs4.ensureDir(path6.join(outputDir, "public"));
|
|
1912
|
+
await fs4.ensureDir(path6.join(outputDir, "utils"));
|
|
1913
|
+
const configPath = path6.join(outputDir, target === "astro-vue" ? "astro.config.mjs" : "nuxt.config.ts");
|
|
1401
1914
|
const configExists = await fs4.pathExists(configPath);
|
|
1402
1915
|
if (!configExists) {
|
|
1403
|
-
const basicConfig = `
|
|
1916
|
+
const basicConfig = target === "astro-vue" ? `import { defineConfig } from 'astro/config';
|
|
1917
|
+
import vue from '@astrojs/vue';
|
|
1918
|
+
import path from 'node:path';
|
|
1919
|
+
import { fileURLToPath } from 'node:url';
|
|
1920
|
+
|
|
1921
|
+
export default defineConfig({
|
|
1922
|
+
integrations: [vue()],
|
|
1923
|
+
vite: {
|
|
1924
|
+
resolve: {
|
|
1925
|
+
alias: {
|
|
1926
|
+
'~': path.dirname(fileURLToPath(import.meta.url)),
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
},
|
|
1930
|
+
});
|
|
1931
|
+
` : `export default defineNuxtConfig({
|
|
1404
1932
|
devtools: { enabled: true },
|
|
1405
1933
|
css: [],
|
|
1406
1934
|
})
|
|
1407
1935
|
`;
|
|
1408
1936
|
await fs4.writeFile(configPath, basicConfig, "utf-8");
|
|
1409
1937
|
}
|
|
1938
|
+
const packageJsonPath = path6.join(outputDir, "package.json");
|
|
1939
|
+
const packageJsonExists = await fs4.pathExists(packageJsonPath);
|
|
1940
|
+
if (!packageJsonExists) {
|
|
1941
|
+
const packageName = path6.basename(outputDir) || (target === "astro-vue" ? "see-ms-astro-site" : "see-ms-nuxt-site");
|
|
1942
|
+
await fs4.writeJson(packageJsonPath, target === "astro-vue" ? {
|
|
1943
|
+
name: packageName,
|
|
1944
|
+
private: true,
|
|
1945
|
+
type: "module",
|
|
1946
|
+
scripts: {
|
|
1947
|
+
dev: "astro dev",
|
|
1948
|
+
build: "astro build",
|
|
1949
|
+
preview: "astro preview"
|
|
1950
|
+
},
|
|
1951
|
+
dependencies: {
|
|
1952
|
+
"@astrojs/vue": "^5.0.0",
|
|
1953
|
+
astro: "^5.0.0",
|
|
1954
|
+
vue: "^3.5.14"
|
|
1955
|
+
},
|
|
1956
|
+
devDependencies: {
|
|
1957
|
+
typescript: "^5.8.3"
|
|
1958
|
+
}
|
|
1959
|
+
} : {
|
|
1960
|
+
name: packageName,
|
|
1961
|
+
private: true,
|
|
1962
|
+
type: "module",
|
|
1963
|
+
scripts: {
|
|
1964
|
+
dev: "nuxt dev",
|
|
1965
|
+
build: "nuxt build",
|
|
1966
|
+
generate: "nuxt generate",
|
|
1967
|
+
preview: "nuxt preview",
|
|
1968
|
+
postinstall: "nuxt prepare"
|
|
1969
|
+
},
|
|
1970
|
+
dependencies: {
|
|
1971
|
+
nuxt: "^3.17.4",
|
|
1972
|
+
vue: "^3.5.14",
|
|
1973
|
+
"vue-router": "^4.5.1"
|
|
1974
|
+
},
|
|
1975
|
+
devDependencies: {
|
|
1976
|
+
typescript: "^5.8.3"
|
|
1977
|
+
}
|
|
1978
|
+
}, { spaces: 2 });
|
|
1979
|
+
}
|
|
1410
1980
|
console.log(pc2.green(" \u2713 Structure created"));
|
|
1411
1981
|
return;
|
|
1412
1982
|
}
|
|
@@ -1424,44 +1994,108 @@ async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
|
1424
1994
|
|
|
1425
1995
|
// src/manifest.ts
|
|
1426
1996
|
import fs6 from "fs-extra";
|
|
1427
|
-
import
|
|
1997
|
+
import path9 from "path";
|
|
1428
1998
|
|
|
1429
1999
|
// src/detector.ts
|
|
1430
2000
|
import * as cheerio2 from "cheerio";
|
|
1431
2001
|
import fs5 from "fs-extra";
|
|
1432
|
-
import
|
|
1433
|
-
|
|
1434
|
-
|
|
2002
|
+
import path8 from "path";
|
|
2003
|
+
import { glob as glob2 } from "glob";
|
|
2004
|
+
|
|
2005
|
+
// src/routes.ts
|
|
2006
|
+
import path7 from "path";
|
|
2007
|
+
function htmlPathToPageId(htmlPath) {
|
|
2008
|
+
const withoutExt = htmlPath.replace(/\.html$/i, "");
|
|
2009
|
+
return withoutExt.replace(/[\\/]/g, "-");
|
|
1435
2010
|
}
|
|
1436
|
-
function
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
return {
|
|
2011
|
+
function htmlPathToRoute(htmlPath) {
|
|
2012
|
+
const normalized = htmlPath.replace(/\\/g, "/").replace(/\.html$/i, "");
|
|
2013
|
+
if (normalized === "index" || normalized.endsWith("/index")) {
|
|
2014
|
+
const parent = normalized.replace(/(^|\/)index$/, "");
|
|
2015
|
+
return parent ? `/${parent}` : "/";
|
|
2016
|
+
}
|
|
2017
|
+
return `/${normalized}`;
|
|
2018
|
+
}
|
|
2019
|
+
function htmlPathToVuePath(htmlPath) {
|
|
2020
|
+
return htmlPath.replace(/\.html$/i, ".vue");
|
|
2021
|
+
}
|
|
2022
|
+
function getPageRouteInfo(htmlPath) {
|
|
2023
|
+
return {
|
|
2024
|
+
sourcePath: htmlPath,
|
|
2025
|
+
pageId: htmlPathToPageId(htmlPath),
|
|
2026
|
+
route: htmlPathToRoute(htmlPath),
|
|
2027
|
+
outputPath: path7.posix.join("pages", htmlPathToVuePath(htmlPath).replace(/\\/g, "/"))
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/detector.ts
|
|
2032
|
+
var TEXT_SELECTORS = [
|
|
2033
|
+
"h1",
|
|
2034
|
+
"h2",
|
|
2035
|
+
"h3",
|
|
2036
|
+
"h4",
|
|
2037
|
+
"h5",
|
|
2038
|
+
"h6",
|
|
2039
|
+
"p",
|
|
2040
|
+
"span",
|
|
2041
|
+
"li",
|
|
2042
|
+
"blockquote",
|
|
2043
|
+
"figcaption",
|
|
2044
|
+
"label",
|
|
2045
|
+
"td",
|
|
2046
|
+
"th",
|
|
2047
|
+
"dt",
|
|
2048
|
+
"dd",
|
|
2049
|
+
"cite",
|
|
2050
|
+
"q"
|
|
2051
|
+
// div is NOT included here - handled separately to only detect text-only divs
|
|
2052
|
+
];
|
|
2053
|
+
var IGNORE_PATTERNS = [
|
|
2054
|
+
".sr-only",
|
|
2055
|
+
".visually-hidden",
|
|
2056
|
+
'[aria-hidden="true"]',
|
|
2057
|
+
"script",
|
|
2058
|
+
"style",
|
|
2059
|
+
"noscript",
|
|
2060
|
+
"template"
|
|
2061
|
+
];
|
|
2062
|
+
var DECORATIVE_CLASS_PATTERNS = [
|
|
2063
|
+
"icon",
|
|
2064
|
+
"arrow",
|
|
2065
|
+
"pagination",
|
|
2066
|
+
"breadcrumb",
|
|
2067
|
+
"loader",
|
|
2068
|
+
"spinner",
|
|
2069
|
+
"skeleton",
|
|
2070
|
+
"placeholder"
|
|
2071
|
+
];
|
|
2072
|
+
function isCollectionClass(className, customClasses) {
|
|
2073
|
+
if (!customClasses || customClasses.length === 0) return false;
|
|
2074
|
+
const normalizedName = className.toLowerCase().replace(/-/g, "_");
|
|
2075
|
+
for (const customClass of customClasses) {
|
|
2076
|
+
const normalizedCustom = customClass.toLowerCase().replace(/-/g, "_");
|
|
2077
|
+
if (normalizedName === normalizedCustom || normalizedName.includes(normalizedCustom)) {
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
function cleanClassName(className) {
|
|
2084
|
+
return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
|
|
2085
|
+
}
|
|
2086
|
+
function getPrimaryClass(classAttr) {
|
|
2087
|
+
if (!classAttr) return null;
|
|
2088
|
+
const cleaned = cleanClassName(classAttr);
|
|
2089
|
+
const classes = cleaned.split(" ").filter((c) => c.length > 0);
|
|
2090
|
+
if (classes.length === 0) return null;
|
|
2091
|
+
const original = classes[0];
|
|
2092
|
+
return {
|
|
1443
2093
|
selector: original,
|
|
1444
2094
|
// Keep original with dashes for CSS selector
|
|
1445
2095
|
fieldName: original.replace(/-/g, "_")
|
|
1446
2096
|
// Normalize for field name
|
|
1447
2097
|
};
|
|
1448
2098
|
}
|
|
1449
|
-
function getContextModifier(_$, $el) {
|
|
1450
|
-
let $current = $el.parent();
|
|
1451
|
-
let depth = 0;
|
|
1452
|
-
while ($current.length > 0 && depth < 5) {
|
|
1453
|
-
const classes = $current.attr("class");
|
|
1454
|
-
if (classes) {
|
|
1455
|
-
const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
|
|
1456
|
-
if (ccClass) {
|
|
1457
|
-
return ccClass.replace("cc-", "").replace(/-/g, "_");
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
$current = $current.parent();
|
|
1461
|
-
depth++;
|
|
1462
|
-
}
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1465
2099
|
function isDecorativeImage(_$, $img) {
|
|
1466
2100
|
const $parent = $img.parent();
|
|
1467
2101
|
const parentClass = $parent.attr("class") || "";
|
|
@@ -1482,9 +2116,136 @@ function isDecorativeImage(_$, $img) {
|
|
|
1482
2116
|
}
|
|
1483
2117
|
function isInsideButton($, el) {
|
|
1484
2118
|
const $el = $(el);
|
|
1485
|
-
const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
|
|
2119
|
+
const $button = $el.closest("button, a, NuxtLink, nuxt-link, .c_button, .c_icon_button");
|
|
1486
2120
|
return $button.length > 0;
|
|
1487
2121
|
}
|
|
2122
|
+
function shouldIgnoreElement(_$, $el, options = {}) {
|
|
2123
|
+
if ($el.attr("data-cms-ignore") !== void 0) return true;
|
|
2124
|
+
for (const pattern of IGNORE_PATTERNS) {
|
|
2125
|
+
if ($el.is(pattern)) return true;
|
|
2126
|
+
if ($el.closest(pattern).length > 0) return true;
|
|
2127
|
+
}
|
|
2128
|
+
for (const selector of options.ignoreSelectors || []) {
|
|
2129
|
+
if ($el.is(selector)) return true;
|
|
2130
|
+
if ($el.closest(selector).length > 0) return true;
|
|
2131
|
+
}
|
|
2132
|
+
const className = $el.attr("class") || "";
|
|
2133
|
+
for (const ignoredClass of options.ignoreClasses || []) {
|
|
2134
|
+
if (className.split(/\s+/).includes(ignoredClass)) return true;
|
|
2135
|
+
}
|
|
2136
|
+
for (const pattern of DECORATIVE_CLASS_PATTERNS) {
|
|
2137
|
+
if (className.toLowerCase().includes(pattern)) return true;
|
|
2138
|
+
}
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
2141
|
+
function isEditableLeaf($el) {
|
|
2142
|
+
if ($el.children().length > 0) {
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
const text = $el.text().trim();
|
|
2146
|
+
if (text.length === 0) {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
var globalFieldIndex = 0;
|
|
2152
|
+
function resetGlobalFieldIndex() {
|
|
2153
|
+
globalFieldIndex = 0;
|
|
2154
|
+
}
|
|
2155
|
+
function generateFieldName(_$, $el, elementType, _index) {
|
|
2156
|
+
const dataCms = $el.attr("data-cms");
|
|
2157
|
+
if (dataCms) return dataCms.replace(/-/g, "_");
|
|
2158
|
+
const id = $el.attr("id");
|
|
2159
|
+
if (id) return id.replace(/-/g, "_");
|
|
2160
|
+
const ariaLabel = $el.attr("aria-label");
|
|
2161
|
+
if (ariaLabel) return ariaLabel.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase();
|
|
2162
|
+
const classInfo = getPrimaryClass($el.attr("class"));
|
|
2163
|
+
if (classInfo && !classInfo.fieldName.startsWith("w_") && !classInfo.fieldName.startsWith("c_")) {
|
|
2164
|
+
return classInfo.fieldName;
|
|
2165
|
+
}
|
|
2166
|
+
const $parent = $el.parent();
|
|
2167
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
2168
|
+
if (parentClassInfo && !parentClassInfo.fieldName.startsWith("w_") && !parentClassInfo.fieldName.startsWith("c_")) {
|
|
2169
|
+
return `${parentClassInfo.fieldName}_${elementType}`;
|
|
2170
|
+
}
|
|
2171
|
+
const $section = $el.closest('section, [class*="section"], [class*="hero"], [class*="cta"], [class*="about"]').first();
|
|
2172
|
+
const sectionClassInfo = getPrimaryClass($section.attr("class"));
|
|
2173
|
+
if (sectionClassInfo && $section.length > 0) {
|
|
2174
|
+
return `${sectionClassInfo.fieldName}_${elementType}`;
|
|
2175
|
+
}
|
|
2176
|
+
const text = $el.text().trim();
|
|
2177
|
+
if (text.length > 0 && text.length < 50) {
|
|
2178
|
+
const words = text.split(/\s+/).slice(0, 3);
|
|
2179
|
+
const contentName = words.join("_").toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
2180
|
+
if (contentName.length > 2 && contentName.length < 30) {
|
|
2181
|
+
return `${elementType}_${contentName}`;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return `${elementType}_${globalFieldIndex++}`;
|
|
2185
|
+
}
|
|
2186
|
+
function buildUniqueSelector($, $el) {
|
|
2187
|
+
const tag = ($el.prop("tagName") || "div").toLowerCase();
|
|
2188
|
+
const id = $el.attr("id");
|
|
2189
|
+
if (id) {
|
|
2190
|
+
const selector = `#${id}`;
|
|
2191
|
+
if ($(selector).length === 1) return selector;
|
|
2192
|
+
}
|
|
2193
|
+
const dataCms = $el.attr("data-cms");
|
|
2194
|
+
if (dataCms) {
|
|
2195
|
+
const selector = `[data-cms="${dataCms}"]`;
|
|
2196
|
+
if ($(selector).length === 1) return selector;
|
|
2197
|
+
}
|
|
2198
|
+
const className = $el.attr("class");
|
|
2199
|
+
if (className) {
|
|
2200
|
+
const classes = className.split(" ").filter((c) => c.length > 2 && !c.startsWith("w-"));
|
|
2201
|
+
for (const cls of classes) {
|
|
2202
|
+
const selector = `.${cls}`;
|
|
2203
|
+
if ($(selector).length === 1) return selector;
|
|
2204
|
+
}
|
|
2205
|
+
for (const cls of classes) {
|
|
2206
|
+
const selector = `${tag}.${cls}`;
|
|
2207
|
+
if ($(selector).length === 1) return selector;
|
|
2208
|
+
}
|
|
2209
|
+
for (let i = 2; i <= Math.min(classes.length, 3); i++) {
|
|
2210
|
+
const combo = classes.slice(0, i).map((c) => `.${c}`).join("");
|
|
2211
|
+
if ($(combo).length === 1) return combo;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return buildFullPath($, $el);
|
|
2215
|
+
}
|
|
2216
|
+
function buildFullPath(_$, $el) {
|
|
2217
|
+
const parts = [];
|
|
2218
|
+
let current = $el;
|
|
2219
|
+
while (current.length && current.prop("tagName")) {
|
|
2220
|
+
const tag = (current.prop("tagName") || "").toLowerCase();
|
|
2221
|
+
if (!tag || tag === "html" || tag === "body") break;
|
|
2222
|
+
const $parent = current.parent();
|
|
2223
|
+
const $siblings = $parent.children(tag);
|
|
2224
|
+
let part = tag;
|
|
2225
|
+
if ($siblings.length > 1) {
|
|
2226
|
+
const index = $siblings.index(current) + 1;
|
|
2227
|
+
part = `${tag}:nth-of-type(${index})`;
|
|
2228
|
+
}
|
|
2229
|
+
parts.unshift(part);
|
|
2230
|
+
current = $parent;
|
|
2231
|
+
if (parts.length >= 4) break;
|
|
2232
|
+
}
|
|
2233
|
+
return parts.join(" > ");
|
|
2234
|
+
}
|
|
2235
|
+
function determineFieldType($el, tagName) {
|
|
2236
|
+
const dataCmsType = $el.attr("data-cms-type");
|
|
2237
|
+
if (dataCmsType) return dataCmsType;
|
|
2238
|
+
const hasFormatting = $el.find("strong, em, b, i, br, a").length > 0;
|
|
2239
|
+
const innerHTML = $el.html() || "";
|
|
2240
|
+
const hasHtmlTags = /<[^>]+>/.test(innerHTML);
|
|
2241
|
+
if (hasFormatting || hasHtmlTags) {
|
|
2242
|
+
return "rich";
|
|
2243
|
+
}
|
|
2244
|
+
if (tagName === "a" || tagName === "nuxt-link" || $el.is("NuxtLink")) {
|
|
2245
|
+
return "link";
|
|
2246
|
+
}
|
|
2247
|
+
return "plain";
|
|
2248
|
+
}
|
|
1488
2249
|
function extractTemplateFromVue(vueContent) {
|
|
1489
2250
|
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1490
2251
|
if (!templateMatch) {
|
|
@@ -1492,27 +2253,68 @@ function extractTemplateFromVue(vueContent) {
|
|
|
1492
2253
|
}
|
|
1493
2254
|
return templateMatch[1];
|
|
1494
2255
|
}
|
|
1495
|
-
function detectEditableFields(templateHtml) {
|
|
2256
|
+
function detectEditableFields(templateHtml, options = {}) {
|
|
1496
2257
|
const $ = cheerio2.load(templateHtml);
|
|
1497
2258
|
const detectedFields = {};
|
|
1498
2259
|
const detectedCollections = {};
|
|
2260
|
+
const { collectionClasses, collectionMin = 2, universalDetection = true } = options;
|
|
2261
|
+
resetGlobalFieldIndex();
|
|
1499
2262
|
const collectionElements = /* @__PURE__ */ new Set();
|
|
1500
|
-
const
|
|
2263
|
+
const processedElements = /* @__PURE__ */ new Set();
|
|
2264
|
+
const usedFieldNames = /* @__PURE__ */ new Set();
|
|
2265
|
+
const getUniqueFieldName = (baseName) => {
|
|
2266
|
+
let name = baseName;
|
|
2267
|
+
let counter = 1;
|
|
2268
|
+
while (usedFieldNames.has(name)) {
|
|
2269
|
+
name = `${baseName}_${counter++}`;
|
|
2270
|
+
}
|
|
2271
|
+
usedFieldNames.add(name);
|
|
2272
|
+
return name;
|
|
2273
|
+
};
|
|
2274
|
+
$("[data-cms]").each((_, el) => {
|
|
2275
|
+
const $el = $(el);
|
|
2276
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2277
|
+
const fieldName = $el.attr("data-cms").replace(/-/g, "_");
|
|
2278
|
+
const tagName = ($el.prop("tagName") || "div").toLowerCase();
|
|
2279
|
+
const fieldType = determineFieldType($el, tagName);
|
|
2280
|
+
const selector = buildUniqueSelector($, $el);
|
|
2281
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2282
|
+
selector,
|
|
2283
|
+
type: fieldType,
|
|
2284
|
+
editable: true,
|
|
2285
|
+
source: "attribute"
|
|
2286
|
+
};
|
|
2287
|
+
processedElements.add(el);
|
|
2288
|
+
});
|
|
1501
2289
|
const potentialCollections = /* @__PURE__ */ new Map();
|
|
1502
|
-
$("[
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
potentialCollections.get(primaryClass.fieldName)?.push(el);
|
|
2290
|
+
$("[data-cms-collection]").each((_, el) => {
|
|
2291
|
+
const $el = $(el);
|
|
2292
|
+
const collectionName = $el.attr("data-cms-collection");
|
|
2293
|
+
const normalizedName = collectionName.replace(/-/g, "_");
|
|
2294
|
+
if (!potentialCollections.has(normalizedName)) {
|
|
2295
|
+
potentialCollections.set(normalizedName, []);
|
|
1509
2296
|
}
|
|
2297
|
+
potentialCollections.get(normalizedName)?.push(el);
|
|
1510
2298
|
});
|
|
2299
|
+
if (collectionClasses && collectionClasses.length > 0) {
|
|
2300
|
+
$("[class]").each((_, el) => {
|
|
2301
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
2302
|
+
if (!primaryClass) return;
|
|
2303
|
+
if (primaryClass.fieldName.includes("image")) return;
|
|
2304
|
+
if (primaryClass.fieldName.includes("inner")) return;
|
|
2305
|
+
if (primaryClass.fieldName.includes("wrapper") && !primaryClass.fieldName.includes("card")) return;
|
|
2306
|
+
if (isCollectionClass(primaryClass.fieldName, collectionClasses)) {
|
|
2307
|
+
if (!potentialCollections.has(primaryClass.fieldName)) {
|
|
2308
|
+
potentialCollections.set(primaryClass.fieldName, []);
|
|
2309
|
+
}
|
|
2310
|
+
potentialCollections.get(primaryClass.fieldName)?.push(el);
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
1511
2314
|
potentialCollections.forEach((elements, className) => {
|
|
1512
|
-
if (elements.length >=
|
|
2315
|
+
if (elements.length >= collectionMin) {
|
|
1513
2316
|
const $first = $(elements[0]);
|
|
1514
2317
|
const collectionFields = {};
|
|
1515
|
-
processedCollectionClasses.add(className);
|
|
1516
2318
|
elements.forEach((el) => {
|
|
1517
2319
|
collectionElements.add(el);
|
|
1518
2320
|
$(el).find("*").each((_, child) => {
|
|
@@ -1527,36 +2329,35 @@ function detectEditableFields(templateHtml) {
|
|
|
1527
2329
|
const $parent = $img.parent();
|
|
1528
2330
|
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1529
2331
|
if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
|
|
1530
|
-
collectionFields.image = `.${parentClassInfo.selector}
|
|
2332
|
+
collectionFields.image = { selector: `.${parentClassInfo.selector}`, type: "image", attribute: "src" };
|
|
2333
|
+
return false;
|
|
2334
|
+
} else {
|
|
2335
|
+
collectionFields.image = { selector: "img", type: "image", attribute: "src" };
|
|
1531
2336
|
return false;
|
|
1532
2337
|
}
|
|
1533
2338
|
});
|
|
1534
|
-
$first.find("
|
|
2339
|
+
$first.find('[class*="tag"]').not('[class*="container"]').first().each((_, el) => {
|
|
1535
2340
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1536
|
-
if (classInfo
|
|
1537
|
-
collectionFields.tag = `.${classInfo.selector}
|
|
1538
|
-
return false;
|
|
2341
|
+
if (classInfo) {
|
|
2342
|
+
collectionFields.tag = { selector: `.${classInfo.selector}`, type: "plain" };
|
|
1539
2343
|
}
|
|
1540
2344
|
});
|
|
1541
2345
|
$first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
|
|
1542
2346
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
2347
|
+
const selector = classInfo ? `.${classInfo.selector}` : el.tagName?.toLowerCase() || "h2";
|
|
2348
|
+
collectionFields.title = { selector, type: "plain" };
|
|
1546
2349
|
});
|
|
1547
2350
|
$first.find("p").first().each((_, el) => {
|
|
1548
2351
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
2352
|
+
const selector = classInfo ? `.${classInfo.selector}` : "p";
|
|
2353
|
+
collectionFields.description = { selector, type: "plain" };
|
|
1552
2354
|
});
|
|
1553
|
-
$first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
|
|
2355
|
+
$first.find("a, NuxtLink, nuxt-link").not(".c_button, .c_icon_button").first().each((_, el) => {
|
|
1554
2356
|
const $link = $(el);
|
|
1555
2357
|
const linkText = $link.text().trim();
|
|
1556
2358
|
if (linkText) {
|
|
1557
2359
|
const classInfo = getPrimaryClass($link.attr("class"));
|
|
1558
|
-
collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
|
|
1559
|
-
return false;
|
|
2360
|
+
collectionFields.link = { selector: classInfo ? `.${classInfo.selector}` : "a", type: "link", attribute: "href" };
|
|
1560
2361
|
}
|
|
1561
2362
|
});
|
|
1562
2363
|
if (Object.keys(collectionFields).length > 0) {
|
|
@@ -1571,103 +2372,102 @@ function detectEditableFields(templateHtml) {
|
|
|
1571
2372
|
}
|
|
1572
2373
|
}
|
|
1573
2374
|
});
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
} else if (modifier) {
|
|
1594
|
-
fieldName = `${modifier}_heading`;
|
|
1595
|
-
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1596
|
-
} else {
|
|
1597
|
-
fieldName = `heading_${index}`;
|
|
1598
|
-
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
detectedFields[fieldName] = {
|
|
2375
|
+
if (universalDetection) {
|
|
2376
|
+
const $body = $("body");
|
|
2377
|
+
let textIndex = 0;
|
|
2378
|
+
let imageIndex = 0;
|
|
2379
|
+
let linkIndex = 0;
|
|
2380
|
+
const allTextSelectors = [...TEXT_SELECTORS, "div"].join(", ");
|
|
2381
|
+
$body.find(allTextSelectors).each((_, el) => {
|
|
2382
|
+
if (collectionElements.has(el)) return;
|
|
2383
|
+
if (processedElements.has(el)) return;
|
|
2384
|
+
const $el = $(el);
|
|
2385
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2386
|
+
const tagName = ($el.prop("tagName") || "div").toLowerCase();
|
|
2387
|
+
if (tagName === "a" || tagName === "nuxt-link" || $el.is("NuxtLink")) return;
|
|
2388
|
+
if (isInsideButton($, el)) return;
|
|
2389
|
+
if (!isEditableLeaf($el)) return;
|
|
2390
|
+
const fieldName = generateFieldName($, $el, tagName, textIndex++);
|
|
2391
|
+
const fieldType = determineFieldType($el, tagName);
|
|
2392
|
+
const selector = buildUniqueSelector($, $el);
|
|
2393
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
1602
2394
|
selector,
|
|
1603
|
-
type:
|
|
1604
|
-
editable: true
|
|
2395
|
+
type: fieldType,
|
|
2396
|
+
editable: true,
|
|
2397
|
+
source: "auto"
|
|
1605
2398
|
};
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
editable: true
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
$body.find("img").each((_index, el) => {
|
|
1623
|
-
if (collectionElements.has(el)) return;
|
|
1624
|
-
if (isInsideButton($, el)) return;
|
|
1625
|
-
const $el = $(el);
|
|
1626
|
-
if (isDecorativeImage($, $el)) return;
|
|
1627
|
-
const $parent = $el.parent();
|
|
1628
|
-
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1629
|
-
if (parentClassInfo) {
|
|
1630
|
-
const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
|
|
1631
|
-
detectedFields[fieldName] = {
|
|
1632
|
-
selector: `.${parentClassInfo.selector}`,
|
|
2399
|
+
processedElements.add(el);
|
|
2400
|
+
});
|
|
2401
|
+
$body.find("img").each((_, el) => {
|
|
2402
|
+
if (collectionElements.has(el)) return;
|
|
2403
|
+
if (processedElements.has(el)) return;
|
|
2404
|
+
const $el = $(el);
|
|
2405
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2406
|
+
if (isDecorativeImage($, $el)) return;
|
|
2407
|
+
const fieldName = generateFieldName($, $el, "image", imageIndex++);
|
|
2408
|
+
const selector = buildUniqueSelector($, $el);
|
|
2409
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2410
|
+
selector,
|
|
1633
2411
|
type: "image",
|
|
1634
|
-
editable: true
|
|
2412
|
+
editable: true,
|
|
2413
|
+
source: "auto",
|
|
2414
|
+
attribute: "src"
|
|
1635
2415
|
};
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
1648
|
-
detectedFields[fieldName] = {
|
|
1649
|
-
selector
|
|
2416
|
+
processedElements.add(el);
|
|
2417
|
+
});
|
|
2418
|
+
$body.find("a, NuxtLink, nuxt-link").each((_, el) => {
|
|
2419
|
+
if (collectionElements.has(el)) return;
|
|
2420
|
+
if (processedElements.has(el)) return;
|
|
2421
|
+
const $el = $(el);
|
|
2422
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2423
|
+
const hasOnlyImage = $el.children().length === 1 && $el.find("img").length === 1;
|
|
2424
|
+
const linkText = $el.text().trim();
|
|
2425
|
+
if (!hasOnlyImage && (!linkText || linkText.length < 2)) return;
|
|
2426
|
+
const fieldName = generateFieldName($, $el, "link", linkIndex++);
|
|
2427
|
+
const selector = buildUniqueSelector($, $el);
|
|
2428
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2429
|
+
selector,
|
|
2430
|
+
type: "link",
|
|
2431
|
+
editable: true,
|
|
2432
|
+
source: "auto",
|
|
2433
|
+
attribute: "href"
|
|
2434
|
+
};
|
|
2435
|
+
processedElements.add(el);
|
|
2436
|
+
});
|
|
2437
|
+
$body.find('button, .c_button, [class*="button"]').each((_, el) => {
|
|
2438
|
+
if (collectionElements.has(el)) return;
|
|
2439
|
+
if (processedElements.has(el)) return;
|
|
2440
|
+
const $el = $(el);
|
|
2441
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2442
|
+
const text = $el.clone().children().remove().end().text().trim();
|
|
2443
|
+
if (!text || text.length < 2) return;
|
|
2444
|
+
const fieldName = generateFieldName($, $el, "button", textIndex++);
|
|
2445
|
+
const selector = buildUniqueSelector($, $el);
|
|
2446
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2447
|
+
selector,
|
|
1650
2448
|
type: "plain",
|
|
1651
|
-
editable: true
|
|
2449
|
+
editable: true,
|
|
2450
|
+
source: "auto"
|
|
1652
2451
|
};
|
|
1653
|
-
|
|
1654
|
-
|
|
2452
|
+
processedElements.add(el);
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
1655
2455
|
return {
|
|
1656
2456
|
fields: detectedFields,
|
|
1657
2457
|
collections: detectedCollections
|
|
1658
2458
|
};
|
|
1659
2459
|
}
|
|
1660
|
-
async function analyzeVuePages(pagesDir) {
|
|
2460
|
+
async function analyzeVuePages(pagesDir, options = {}) {
|
|
1661
2461
|
const results = {};
|
|
1662
|
-
const vueFiles = await
|
|
2462
|
+
const vueFiles = await glob2("**/*.vue", { cwd: pagesDir, nodir: true });
|
|
1663
2463
|
for (const file of vueFiles) {
|
|
1664
2464
|
if (file.endsWith(".vue")) {
|
|
1665
|
-
const filePath =
|
|
2465
|
+
const filePath = path8.join(pagesDir, file);
|
|
1666
2466
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
1667
2467
|
const template = extractTemplateFromVue(content);
|
|
1668
2468
|
if (template) {
|
|
1669
|
-
const pageName = file.replace(
|
|
1670
|
-
results[pageName] = detectEditableFields(template);
|
|
2469
|
+
const pageName = htmlPathToPageId(file.replace(/\.vue$/i, ".html"));
|
|
2470
|
+
results[pageName] = detectEditableFields(template, options);
|
|
1671
2471
|
}
|
|
1672
2472
|
}
|
|
1673
2473
|
}
|
|
@@ -1675,49 +2475,190 @@ async function analyzeVuePages(pagesDir) {
|
|
|
1675
2475
|
}
|
|
1676
2476
|
|
|
1677
2477
|
// src/manifest.ts
|
|
1678
|
-
async function generateManifest(pagesDir) {
|
|
1679
|
-
const
|
|
2478
|
+
async function generateManifest(pagesDir, options = {}) {
|
|
2479
|
+
const collectionItemSelectors = options.sharedComponents?.filter((component) => component.role === "collection-item").map((component) => component.selector) || [];
|
|
2480
|
+
const detectionOptions = {
|
|
2481
|
+
collectionClasses: options.collectionClasses,
|
|
2482
|
+
ignoreSelectors: [
|
|
2483
|
+
...options.ignoreSelectors || [],
|
|
2484
|
+
...collectionItemSelectors
|
|
2485
|
+
],
|
|
2486
|
+
ignoreClasses: options.ignoreClasses
|
|
2487
|
+
};
|
|
2488
|
+
const componentDetectionOptions = {
|
|
2489
|
+
collectionClasses: options.collectionClasses,
|
|
2490
|
+
ignoreSelectors: options.ignoreSelectors,
|
|
2491
|
+
ignoreClasses: options.ignoreClasses
|
|
2492
|
+
};
|
|
2493
|
+
const analyzed = await analyzeVuePages(pagesDir, detectionOptions);
|
|
1680
2494
|
const pages = {};
|
|
1681
2495
|
for (const [pageName, detection] of Object.entries(analyzed)) {
|
|
2496
|
+
let collections = detection.collections;
|
|
2497
|
+
if (options.collectionNames && Object.keys(options.collectionNames).length > 0) {
|
|
2498
|
+
collections = {};
|
|
2499
|
+
for (const [collectionKey, collection] of Object.entries(detection.collections)) {
|
|
2500
|
+
let newName = collectionKey;
|
|
2501
|
+
for (const [className, displayName] of Object.entries(options.collectionNames)) {
|
|
2502
|
+
const normalizedClassName = className.replace(/-/g, "_");
|
|
2503
|
+
if (collectionKey.includes(normalizedClassName) || collectionKey === normalizedClassName) {
|
|
2504
|
+
newName = displayName;
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
collections[newName] = collection;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
1682
2511
|
pages[pageName] = {
|
|
1683
2512
|
fields: detection.fields,
|
|
1684
|
-
collections
|
|
2513
|
+
collections,
|
|
1685
2514
|
meta: {
|
|
1686
|
-
route: pageName === "index" ? "/" : `/${pageName}`
|
|
2515
|
+
route: options.pageRoutes?.[pageName] || (pageName === "index" ? "/" : `/${pageName}`)
|
|
1687
2516
|
}
|
|
1688
2517
|
};
|
|
1689
2518
|
}
|
|
1690
2519
|
const manifest = {
|
|
1691
2520
|
version: "1.0",
|
|
1692
|
-
pages
|
|
2521
|
+
pages,
|
|
2522
|
+
global: options.sharedComponents && options.sharedComponents.length > 0 ? {
|
|
2523
|
+
components: Object.fromEntries(
|
|
2524
|
+
options.sharedComponents.map((component) => [component.name, component])
|
|
2525
|
+
)
|
|
2526
|
+
} : void 0,
|
|
2527
|
+
providers: {
|
|
2528
|
+
[options.provider || "strapi"]: {
|
|
2529
|
+
version: "1"
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
1693
2532
|
};
|
|
2533
|
+
if (options.sharedComponents?.length && options.componentsDir) {
|
|
2534
|
+
const globalFields = {};
|
|
2535
|
+
const components = manifest.global?.components || {};
|
|
2536
|
+
for (const component of options.sharedComponents) {
|
|
2537
|
+
const componentPath = path9.join(options.componentsDir, `${component.name}.vue`);
|
|
2538
|
+
if (!await fs6.pathExists(componentPath)) continue;
|
|
2539
|
+
const content = await fs6.readFile(componentPath, "utf-8");
|
|
2540
|
+
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
|
2541
|
+
if (!templateMatch) continue;
|
|
2542
|
+
const detection = detectEditableFields(templateMatch[1], componentDetectionOptions);
|
|
2543
|
+
const prefixedFields = Object.fromEntries(
|
|
2544
|
+
Object.entries(detection.fields).map(([fieldName, field]) => [
|
|
2545
|
+
`${component.name}_${fieldName}`,
|
|
2546
|
+
field
|
|
2547
|
+
])
|
|
2548
|
+
);
|
|
2549
|
+
const collectionFields = Object.fromEntries(
|
|
2550
|
+
Object.entries(detection.fields).map(([fieldName, field]) => [
|
|
2551
|
+
fieldName,
|
|
2552
|
+
{
|
|
2553
|
+
selector: field.selector,
|
|
2554
|
+
type: field.type,
|
|
2555
|
+
attribute: field.attribute
|
|
2556
|
+
}
|
|
2557
|
+
])
|
|
2558
|
+
);
|
|
2559
|
+
const contentMode = component.contentMode || "shared-global";
|
|
2560
|
+
const role = component.role || "shared-section";
|
|
2561
|
+
components[component.name] = {
|
|
2562
|
+
...component,
|
|
2563
|
+
role,
|
|
2564
|
+
contentMode,
|
|
2565
|
+
fields: role === "collection-item" ? detection.fields : prefixedFields
|
|
2566
|
+
};
|
|
2567
|
+
if (role === "collection-item") {
|
|
2568
|
+
for (const pageId of component.pages || []) {
|
|
2569
|
+
if (!manifest.pages[pageId]) continue;
|
|
2570
|
+
const collectionName = resolveCollectionName(component.collectionName || toCollectionName(component.name), pageId);
|
|
2571
|
+
manifest.pages[pageId].collections = {
|
|
2572
|
+
...manifest.pages[pageId].collections,
|
|
2573
|
+
[collectionName]: {
|
|
2574
|
+
selector: component.selector,
|
|
2575
|
+
fields: collectionFields,
|
|
2576
|
+
componentName: component.name,
|
|
2577
|
+
storage: component.collectionStorage || "collection-type"
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
} else if (contentMode === "per-page") {
|
|
2582
|
+
for (const pageId of component.pages || []) {
|
|
2583
|
+
if (!manifest.pages[pageId]) continue;
|
|
2584
|
+
manifest.pages[pageId].fields = {
|
|
2585
|
+
...manifest.pages[pageId].fields,
|
|
2586
|
+
...prefixedFields
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
} else if (contentMode === "shared-global" || contentMode === "auto") {
|
|
2590
|
+
Object.assign(globalFields, prefixedFields);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (manifest.global) {
|
|
2594
|
+
manifest.global.components = components;
|
|
2595
|
+
if (Object.keys(globalFields || {}).length > 0) {
|
|
2596
|
+
manifest.global.fields = globalFields;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
1694
2600
|
return manifest;
|
|
1695
2601
|
}
|
|
2602
|
+
function toCollectionName(name) {
|
|
2603
|
+
const base = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
2604
|
+
if (base.endsWith("s")) return base;
|
|
2605
|
+
return `${base}s`;
|
|
2606
|
+
}
|
|
2607
|
+
function resolveCollectionName(collectionName, pageId) {
|
|
2608
|
+
return collectionName === pageId ? `${collectionName}_items` : collectionName;
|
|
2609
|
+
}
|
|
1696
2610
|
async function writeManifest(outputDir, manifest) {
|
|
1697
2611
|
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
1698
|
-
const
|
|
1699
|
-
await fs6.writeFile(manifestPath, manifestContent, "utf-8");
|
|
1700
|
-
const publicDir = path7.join(outputDir, "public");
|
|
2612
|
+
const publicDir = path9.join(outputDir, "public");
|
|
1701
2613
|
await fs6.ensureDir(publicDir);
|
|
1702
|
-
const publicManifestPath =
|
|
2614
|
+
const publicManifestPath = path9.join(publicDir, "cms-manifest.json");
|
|
1703
2615
|
await fs6.writeFile(publicManifestPath, manifestContent, "utf-8");
|
|
1704
2616
|
}
|
|
1705
2617
|
|
|
1706
2618
|
// src/vue-transformer.ts
|
|
1707
2619
|
import * as cheerio3 from "cheerio";
|
|
1708
2620
|
import fs7 from "fs-extra";
|
|
1709
|
-
import
|
|
2621
|
+
import path10 from "path";
|
|
2622
|
+
import { glob as glob3 } from "glob";
|
|
2623
|
+
function isSafeToEmpty($el) {
|
|
2624
|
+
return $el.children().length === 0;
|
|
2625
|
+
}
|
|
1710
2626
|
function replaceWithBinding(_$, $el, fieldName, type) {
|
|
1711
2627
|
if (type === "image") {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
$
|
|
1715
|
-
|
|
2628
|
+
if ($el.is("img")) {
|
|
2629
|
+
$el.attr(":src", `content.${fieldName}`);
|
|
2630
|
+
$el.removeAttr("src");
|
|
2631
|
+
} else {
|
|
2632
|
+
const $img = $el.find("img").first();
|
|
2633
|
+
if ($img.length) {
|
|
2634
|
+
$img.attr(":src", `content.${fieldName}`);
|
|
2635
|
+
$img.removeAttr("src");
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
} else if (type === "link") {
|
|
2639
|
+
const $link = $el.is("a") || $el.is("NuxtLink") || $el.is("nuxt-link") ? $el : $el.find("a, NuxtLink, nuxt-link").first();
|
|
2640
|
+
if ($link.length) {
|
|
2641
|
+
const isNuxtLink = $link.is("NuxtLink") || $link.is("nuxt-link");
|
|
2642
|
+
$link.attr(isNuxtLink ? ":to" : ":href", `content.${fieldName}?.url`);
|
|
2643
|
+
$link.attr(":target", `content.${fieldName}?.newTab ? '_blank' : undefined`);
|
|
2644
|
+
$link.removeAttr("href");
|
|
2645
|
+
if (isNuxtLink) $link.removeAttr("to");
|
|
2646
|
+
$link.removeAttr("target");
|
|
2647
|
+
if (isSafeToEmpty($link)) {
|
|
2648
|
+
$link.empty();
|
|
2649
|
+
$link.text(`{{ content.${fieldName}?.text }}`);
|
|
2650
|
+
}
|
|
1716
2651
|
}
|
|
1717
2652
|
} else if (type === "rich") {
|
|
2653
|
+
if (!isSafeToEmpty($el)) {
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
1718
2656
|
$el.attr("v-html", `content.${fieldName}`);
|
|
1719
2657
|
$el.empty();
|
|
1720
2658
|
} else {
|
|
2659
|
+
if (!isSafeToEmpty($el)) {
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
1721
2662
|
$el.empty();
|
|
1722
2663
|
$el.text(`{{ content.${fieldName} }}`);
|
|
1723
2664
|
}
|
|
@@ -1726,21 +2667,43 @@ function transformCollection($, collectionName, collection) {
|
|
|
1726
2667
|
const $items = $(collection.selector);
|
|
1727
2668
|
if ($items.length === 0) return;
|
|
1728
2669
|
const $first = $items.first();
|
|
2670
|
+
if (collection.componentName) {
|
|
2671
|
+
$first.replaceWith(`<!--COLLECTION_COMPONENT:${collection.componentName}:${collectionName}-->`);
|
|
2672
|
+
$items.slice(1).remove();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
1729
2675
|
$first.attr("v-for", `(item, index) in content.${collectionName}`);
|
|
1730
2676
|
$first.attr(":key", "index");
|
|
1731
|
-
Object.entries(collection.fields).forEach(([fieldName,
|
|
2677
|
+
Object.entries(collection.fields).forEach(([fieldName, fieldConfig]) => {
|
|
2678
|
+
const selector = typeof fieldConfig === "string" ? fieldConfig : fieldConfig.selector || fieldConfig;
|
|
2679
|
+
const fieldType = typeof fieldConfig === "object" ? fieldConfig.type : void 0;
|
|
1732
2680
|
const $fieldEl = $first.find(selector);
|
|
1733
2681
|
if ($fieldEl.length) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
$
|
|
2682
|
+
const isImage = fieldType === "image" || fieldName === "image" || fieldName.includes("image");
|
|
2683
|
+
const isLink = fieldType === "link" || fieldName === "link" || fieldName === "url";
|
|
2684
|
+
if (isImage) {
|
|
2685
|
+
if ($fieldEl.is("img")) {
|
|
2686
|
+
$fieldEl.attr(":src", `item.${fieldName}`);
|
|
2687
|
+
$fieldEl.removeAttr("src");
|
|
2688
|
+
} else {
|
|
2689
|
+
const $img = $fieldEl.find("img").first();
|
|
2690
|
+
if ($img.length) {
|
|
2691
|
+
$img.attr(":src", `item.${fieldName}`);
|
|
2692
|
+
$img.removeAttr("src");
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
} else if (isLink) {
|
|
2696
|
+
const $link = $fieldEl.is("a") || $fieldEl.is("NuxtLink") || $fieldEl.is("nuxt-link") ? $fieldEl : $fieldEl.find("a, NuxtLink, nuxt-link").first();
|
|
2697
|
+
if ($link.length) {
|
|
2698
|
+
const isNuxtLink = $link.is("NuxtLink") || $link.is("nuxt-link");
|
|
2699
|
+
$link.attr(isNuxtLink ? ":to" : ":href", `item.${fieldName}?.url`);
|
|
2700
|
+
$link.attr(":target", `item.${fieldName}?.newTab ? '_blank' : undefined`);
|
|
2701
|
+
$link.removeAttr("href");
|
|
2702
|
+
$link.removeAttr("target");
|
|
2703
|
+
$link.removeAttr("to");
|
|
2704
|
+
$link.empty();
|
|
2705
|
+
$link.text(`{{ item.${fieldName}?.text }}`);
|
|
1739
2706
|
}
|
|
1740
|
-
} else if (fieldName === "link") {
|
|
1741
|
-
$fieldEl.attr(":to", "item.link");
|
|
1742
|
-
$fieldEl.removeAttr("to");
|
|
1743
|
-
$fieldEl.removeAttr("href");
|
|
1744
2707
|
} else {
|
|
1745
2708
|
$fieldEl.empty();
|
|
1746
2709
|
$fieldEl.text(`{{ item.${fieldName} }}`);
|
|
@@ -1749,7 +2712,7 @@ function transformCollection($, collectionName, collection) {
|
|
|
1749
2712
|
});
|
|
1750
2713
|
$items.slice(1).remove();
|
|
1751
2714
|
}
|
|
1752
|
-
async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
2715
|
+
async function transformVueToReactive(vueFilePath, pageName, manifest, options = {}) {
|
|
1753
2716
|
const pageManifest = manifest.pages[pageName];
|
|
1754
2717
|
if (!pageManifest) return;
|
|
1755
2718
|
const vueContent = await fs7.readFile(vueFilePath, "utf-8");
|
|
@@ -1759,7 +2722,8 @@ async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
|
1759
2722
|
}
|
|
1760
2723
|
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1761
2724
|
if (!templateMatch) return;
|
|
1762
|
-
const
|
|
2725
|
+
const componentNames = Object.keys(manifest.global?.components || {});
|
|
2726
|
+
const templateContent = maskComponentTags(templateMatch[1], componentNames);
|
|
1763
2727
|
const $ = cheerio3.load(templateContent, { xmlMode: false });
|
|
1764
2728
|
if (pageManifest.collections) {
|
|
1765
2729
|
Object.entries(pageManifest.collections).forEach(([collectionName, collection]) => {
|
|
@@ -1785,8 +2749,19 @@ async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
|
1785
2749
|
if (wrapperDivMatch) {
|
|
1786
2750
|
transformedTemplate = wrapperDivMatch[1].trim();
|
|
1787
2751
|
}
|
|
2752
|
+
const perPageComponentNames = componentNames.filter((name) => {
|
|
2753
|
+
const component = manifest.global?.components?.[name];
|
|
2754
|
+
return component?.contentMode === "per-page" && component.pages.includes(pageName);
|
|
2755
|
+
});
|
|
2756
|
+
transformedTemplate = restoreCollectionComponentTags(transformedTemplate);
|
|
2757
|
+
transformedTemplate = restoreComponentTags2(transformedTemplate, componentNames, perPageComponentNames);
|
|
2758
|
+
const explicitImports = options.target === "astro-vue" ? [
|
|
2759
|
+
`import { useStrapiContent } from '~/src/composables/useStrapiContent';`,
|
|
2760
|
+
...componentNames.map((name) => `import ${name} from '~/components/${name}.vue';`)
|
|
2761
|
+
].join("\n") : "";
|
|
1788
2762
|
const scriptSetup = `<script setup lang="ts">
|
|
1789
2763
|
// Auto-generated reactive content from Strapi
|
|
2764
|
+
${explicitImports}
|
|
1790
2765
|
const { content } = useStrapiContent('${pageName}');
|
|
1791
2766
|
</script>`;
|
|
1792
2767
|
const finalTemplate = transformedTemplate.split("\n").map((line) => " " + line).join("\n");
|
|
@@ -1798,29 +2773,129 @@ ${finalTemplate}
|
|
|
1798
2773
|
`;
|
|
1799
2774
|
await fs7.writeFile(vueFilePath, newVueContent, "utf-8");
|
|
1800
2775
|
}
|
|
1801
|
-
async function
|
|
1802
|
-
const
|
|
2776
|
+
async function transformSharedComponentsToReactive(componentsDir, manifest, options = {}) {
|
|
2777
|
+
const components = manifest.global?.components || {};
|
|
2778
|
+
for (const [componentName, component] of Object.entries(components)) {
|
|
2779
|
+
const fields = component.fields || {};
|
|
2780
|
+
if (Object.keys(fields).length === 0) continue;
|
|
2781
|
+
const filePath = path10.join(componentsDir, `${componentName}.vue`);
|
|
2782
|
+
if (!await fs7.pathExists(filePath)) continue;
|
|
2783
|
+
const vueContent = await fs7.readFile(filePath, "utf-8");
|
|
2784
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
2785
|
+
if (!templateMatch) continue;
|
|
2786
|
+
const $ = cheerio3.load(templateMatch[1], { xmlMode: false });
|
|
2787
|
+
const isCollectionItem = component.role === "collection-item";
|
|
2788
|
+
const isPerPage = component.contentMode === "per-page";
|
|
2789
|
+
const contentSource = isCollectionItem ? "item" : isPerPage ? "componentContent" : "content";
|
|
2790
|
+
Object.entries(fields).forEach(([fieldName, field]) => {
|
|
2791
|
+
const originalName = fieldName.startsWith(`${componentName}_`) ? fieldName.slice(componentName.length + 1) : fieldName;
|
|
2792
|
+
const selector = field.selector;
|
|
2793
|
+
$(selector).each((_, el) => {
|
|
2794
|
+
replaceWithBinding($, $(el), fieldName, field.type);
|
|
2795
|
+
});
|
|
2796
|
+
if (originalName !== fieldName) {
|
|
2797
|
+
$(selector).each((_, el) => {
|
|
2798
|
+
replaceWithBinding($, $(el), fieldName, field.type);
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
let transformedTemplate = $.html();
|
|
2803
|
+
const bodyMatch = transformedTemplate.match(/<body>([\s\S]*)<\/body>/);
|
|
2804
|
+
if (bodyMatch) transformedTemplate = bodyMatch[1];
|
|
2805
|
+
transformedTemplate = transformedTemplate.replace(/<\/?html[^>]*>/gi, "").replace(/<head><\/head>/gi, "").trim();
|
|
2806
|
+
for (const fieldName of Object.keys(fields)) {
|
|
2807
|
+
transformedTemplate = transformedTemplate.replaceAll(`content.${fieldName}`, `${contentSource}.${fieldName}`);
|
|
2808
|
+
}
|
|
2809
|
+
const importLine = !isCollectionItem && !isPerPage && options.target === "astro-vue" ? `import { useStrapiContent } from '~/src/composables/useStrapiContent';
|
|
2810
|
+
` : "";
|
|
2811
|
+
const contentSetup = isCollectionItem ? `defineProps<{ item: Record<string, any> }>();` : isPerPage ? `const props = defineProps<{ content: Record<string, any> }>();
|
|
2812
|
+
const componentContent = props.content || {};` : `const { content } = useStrapiContent('global');`;
|
|
2813
|
+
const scriptSetup = `<script setup lang="ts">
|
|
2814
|
+
${importLine}${contentSetup}
|
|
2815
|
+
</script>`;
|
|
2816
|
+
await fs7.writeFile(filePath, `${scriptSetup}
|
|
2817
|
+
|
|
2818
|
+
<template>
|
|
2819
|
+
${transformedTemplate}
|
|
2820
|
+
</template>
|
|
2821
|
+
`, "utf-8");
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function restoreComponentTags2(html, componentNames, perPageComponentNames = []) {
|
|
2825
|
+
let restored = html;
|
|
2826
|
+
for (const name of componentNames) {
|
|
2827
|
+
const lowered = name.toLowerCase();
|
|
2828
|
+
const tag = perPageComponentNames.includes(name) ? `<${name} :content="content" />` : `<${name} />`;
|
|
2829
|
+
restored = restored.replace(new RegExp(`<!--COMPONENT:${name}-->`, "g"), tag).replace(new RegExp(`<${lowered}\\s*><\\/${lowered}>`, "g"), tag).replace(new RegExp(`<${lowered}\\s*\\/>`, "g"), tag);
|
|
2830
|
+
}
|
|
2831
|
+
return restored;
|
|
2832
|
+
}
|
|
2833
|
+
function restoreCollectionComponentTags(html) {
|
|
2834
|
+
return html.replace(
|
|
2835
|
+
/<!--COLLECTION_COMPONENT:(\w+):([\w-]+)-->/g,
|
|
2836
|
+
(_match, componentName, collectionName) => `<${componentName} v-for="(item, index) in content.${collectionName}" :key="index" :item="item" />`
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
function maskComponentTags(html, componentNames) {
|
|
2840
|
+
let masked = html;
|
|
2841
|
+
for (const name of componentNames) {
|
|
2842
|
+
masked = masked.replace(new RegExp(`<${name}\\s*\\/>`, "g"), `<!--COMPONENT:${name}-->`).replace(new RegExp(`<${name}\\s*>\\s*<\\/${name}>`, "g"), `<!--COMPONENT:${name}-->`);
|
|
2843
|
+
}
|
|
2844
|
+
return masked;
|
|
2845
|
+
}
|
|
2846
|
+
async function transformAllVuePages(pagesDir, manifest, options = {}) {
|
|
2847
|
+
const vueFiles = await glob3("**/*.vue", { cwd: pagesDir, nodir: true });
|
|
1803
2848
|
for (const file of vueFiles) {
|
|
1804
2849
|
if (file.endsWith(".vue")) {
|
|
1805
|
-
const pageName = file.replace(
|
|
1806
|
-
const vueFilePath =
|
|
1807
|
-
await transformVueToReactive(vueFilePath, pageName, manifest);
|
|
2850
|
+
const pageName = htmlPathToPageId(file.replace(/\.vue$/i, ".html"));
|
|
2851
|
+
const vueFilePath = path10.join(pagesDir, file);
|
|
2852
|
+
await transformVueToReactive(vueFilePath, pageName, manifest, options);
|
|
1808
2853
|
}
|
|
1809
2854
|
}
|
|
1810
2855
|
}
|
|
1811
2856
|
|
|
1812
2857
|
// src/transformer.ts
|
|
2858
|
+
var LINK_COMPONENT_SCHEMA = {
|
|
2859
|
+
collectionName: "components_shared_links",
|
|
2860
|
+
info: {
|
|
2861
|
+
displayName: "Link",
|
|
2862
|
+
icon: "link",
|
|
2863
|
+
description: "A link with URL and text"
|
|
2864
|
+
},
|
|
2865
|
+
options: {},
|
|
2866
|
+
attributes: {
|
|
2867
|
+
url: {
|
|
2868
|
+
type: "string",
|
|
2869
|
+
required: true
|
|
2870
|
+
},
|
|
2871
|
+
text: {
|
|
2872
|
+
type: "string",
|
|
2873
|
+
required: true
|
|
2874
|
+
},
|
|
2875
|
+
newTab: {
|
|
2876
|
+
type: "boolean",
|
|
2877
|
+
default: false
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
1813
2881
|
function mapFieldTypeToStrapi(fieldType) {
|
|
2882
|
+
if (fieldType === "link") {
|
|
2883
|
+
return {
|
|
2884
|
+
type: "component",
|
|
2885
|
+
isComponent: true,
|
|
2886
|
+
component: "shared.link"
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
1814
2889
|
const typeMap = {
|
|
1815
2890
|
plain: "string",
|
|
1816
2891
|
rich: "richtext",
|
|
1817
2892
|
html: "richtext",
|
|
1818
2893
|
image: "media",
|
|
1819
|
-
|
|
2894
|
+
icon: "media",
|
|
1820
2895
|
email: "email",
|
|
1821
2896
|
phone: "string"
|
|
1822
2897
|
};
|
|
1823
|
-
return typeMap[fieldType] || "string";
|
|
2898
|
+
return { type: typeMap[fieldType] || "string" };
|
|
1824
2899
|
}
|
|
1825
2900
|
function pluralize(word) {
|
|
1826
2901
|
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
|
|
@@ -1837,12 +2912,21 @@ function pluralize(word) {
|
|
|
1837
2912
|
function pageToStrapiSchema(pageName, fields) {
|
|
1838
2913
|
const attributes = {};
|
|
1839
2914
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2915
|
+
const strapiType = mapFieldTypeToStrapi(field.type);
|
|
2916
|
+
if (strapiType.isComponent) {
|
|
2917
|
+
attributes[fieldName] = {
|
|
2918
|
+
type: "component",
|
|
2919
|
+
component: strapiType.component,
|
|
2920
|
+
repeatable: false
|
|
2921
|
+
};
|
|
2922
|
+
} else {
|
|
2923
|
+
attributes[fieldName] = {
|
|
2924
|
+
type: strapiType.type,
|
|
2925
|
+
required: field.required || false
|
|
2926
|
+
};
|
|
2927
|
+
if (field.default && typeof field.default === "string") {
|
|
2928
|
+
attributes[fieldName].default = field.default;
|
|
2929
|
+
}
|
|
1846
2930
|
}
|
|
1847
2931
|
}
|
|
1848
2932
|
const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
@@ -1864,20 +2948,44 @@ function pageToStrapiSchema(pageName, fields) {
|
|
|
1864
2948
|
}
|
|
1865
2949
|
function collectionToStrapiSchema(collectionName, collection) {
|
|
1866
2950
|
const attributes = {};
|
|
1867
|
-
for (const [fieldName,
|
|
1868
|
-
let
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2951
|
+
for (const [fieldName, fieldConfig] of Object.entries(collection.fields)) {
|
|
2952
|
+
let fieldType;
|
|
2953
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null && "type" in fieldConfig) {
|
|
2954
|
+
fieldType = fieldConfig.type;
|
|
2955
|
+
}
|
|
2956
|
+
if (fieldType) {
|
|
2957
|
+
const strapiType = mapFieldTypeToStrapi(fieldType);
|
|
2958
|
+
if (strapiType.isComponent) {
|
|
2959
|
+
attributes[fieldName] = {
|
|
2960
|
+
type: "component",
|
|
2961
|
+
component: strapiType.component,
|
|
2962
|
+
repeatable: false
|
|
2963
|
+
};
|
|
2964
|
+
} else {
|
|
2965
|
+
attributes[fieldName] = {
|
|
2966
|
+
type: strapiType.type
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
} else {
|
|
2970
|
+
let type = "string";
|
|
2971
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
2972
|
+
type = "media";
|
|
2973
|
+
} else if (fieldName === "description" || fieldName === "content") {
|
|
2974
|
+
type = "richtext";
|
|
2975
|
+
} else if (fieldName === "link" || fieldName === "url") {
|
|
2976
|
+
attributes[fieldName] = {
|
|
2977
|
+
type: "component",
|
|
2978
|
+
component: "shared.link",
|
|
2979
|
+
repeatable: false
|
|
2980
|
+
};
|
|
2981
|
+
continue;
|
|
2982
|
+
} else if (fieldName === "title" || fieldName === "tag") {
|
|
2983
|
+
type = "string";
|
|
2984
|
+
}
|
|
2985
|
+
attributes[fieldName] = {
|
|
2986
|
+
type
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
1881
2989
|
}
|
|
1882
2990
|
const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1883
2991
|
const kebabCaseName = collectionName.replace(/_/g, "-");
|
|
@@ -1896,6 +3004,37 @@ function collectionToStrapiSchema(collectionName, collection) {
|
|
|
1896
3004
|
attributes
|
|
1897
3005
|
};
|
|
1898
3006
|
}
|
|
3007
|
+
function hasLinkFields(manifest) {
|
|
3008
|
+
for (const page of Object.values(manifest.pages)) {
|
|
3009
|
+
if (page.fields) {
|
|
3010
|
+
for (const field of Object.values(page.fields)) {
|
|
3011
|
+
if (field.type === "link") return true;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (page.collections) {
|
|
3015
|
+
for (const collection of Object.values(page.collections)) {
|
|
3016
|
+
for (const fieldConfig of Object.values(collection.fields)) {
|
|
3017
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
3018
|
+
if (fieldConfig.type === "link") return true;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (manifest.global?.fields) {
|
|
3025
|
+
for (const field of Object.values(manifest.global.fields)) {
|
|
3026
|
+
if (field.type === "link") return true;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
if (manifest.global?.components) {
|
|
3030
|
+
for (const component of Object.values(manifest.global.components)) {
|
|
3031
|
+
for (const field of Object.values(component.fields || {})) {
|
|
3032
|
+
if (field.type === "link") return true;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return false;
|
|
3037
|
+
}
|
|
1899
3038
|
function manifestToSchemas(manifest) {
|
|
1900
3039
|
const schemas = {};
|
|
1901
3040
|
for (const [pageName, page] of Object.entries(manifest.pages)) {
|
|
@@ -1908,16 +3047,22 @@ function manifestToSchemas(manifest) {
|
|
|
1908
3047
|
}
|
|
1909
3048
|
}
|
|
1910
3049
|
}
|
|
3050
|
+
if (manifest.global?.fields && Object.keys(manifest.global.fields).length > 0) {
|
|
3051
|
+
schemas["global"] = pageToStrapiSchema("global", manifest.global.fields);
|
|
3052
|
+
}
|
|
1911
3053
|
return schemas;
|
|
1912
3054
|
}
|
|
3055
|
+
function getLinkComponentSchema(manifest) {
|
|
3056
|
+
return hasLinkFields(manifest) ? LINK_COMPONENT_SCHEMA : null;
|
|
3057
|
+
}
|
|
1913
3058
|
|
|
1914
3059
|
// src/schema-writer.ts
|
|
1915
3060
|
import fs8 from "fs-extra";
|
|
1916
|
-
import
|
|
3061
|
+
import path11 from "path";
|
|
1917
3062
|
async function writeStrapiSchema(outputDir, name, schema) {
|
|
1918
|
-
const schemasDir =
|
|
3063
|
+
const schemasDir = path11.join(outputDir, "cms-schemas");
|
|
1919
3064
|
await fs8.ensureDir(schemasDir);
|
|
1920
|
-
const schemaPath =
|
|
3065
|
+
const schemaPath = path11.join(schemasDir, `${name}.json`);
|
|
1921
3066
|
await fs8.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
1922
3067
|
}
|
|
1923
3068
|
async function writeAllSchemas(outputDir, schemas) {
|
|
@@ -1925,8 +3070,14 @@ async function writeAllSchemas(outputDir, schemas) {
|
|
|
1925
3070
|
await writeStrapiSchema(outputDir, name, schema);
|
|
1926
3071
|
}
|
|
1927
3072
|
}
|
|
3073
|
+
async function writeLinkComponentSchema(outputDir) {
|
|
3074
|
+
const componentsDir = path11.join(outputDir, "cms-schemas", "components", "shared");
|
|
3075
|
+
await fs8.ensureDir(componentsDir);
|
|
3076
|
+
const schemaPath = path11.join(componentsDir, "link.json");
|
|
3077
|
+
await fs8.writeFile(schemaPath, JSON.stringify(LINK_COMPONENT_SCHEMA, null, 2), "utf-8");
|
|
3078
|
+
}
|
|
1928
3079
|
async function createStrapiReadme(outputDir) {
|
|
1929
|
-
const readmePath =
|
|
3080
|
+
const readmePath = path11.join(outputDir, "cms-schemas", "README.md");
|
|
1930
3081
|
const content = `# CMS Schemas
|
|
1931
3082
|
|
|
1932
3083
|
Auto-generated Strapi content type schemas from your Webflow export.
|
|
@@ -2001,7 +3152,17 @@ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
|
|
|
2001
3152
|
|
|
2002
3153
|
// src/content-extractor.ts
|
|
2003
3154
|
import * as cheerio4 from "cheerio";
|
|
2004
|
-
|
|
3155
|
+
function extractLinkValue($element) {
|
|
3156
|
+
const href = $element.attr("href") || $element.attr("to") || "";
|
|
3157
|
+
const text = $element.text().trim();
|
|
3158
|
+
const target = $element.attr("target");
|
|
3159
|
+
const newTab = target === "_blank";
|
|
3160
|
+
return {
|
|
3161
|
+
url: href,
|
|
3162
|
+
text,
|
|
3163
|
+
newTab: newTab || void 0
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
2005
3166
|
function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
2006
3167
|
const $ = cheerio4.load(html);
|
|
2007
3168
|
const content = {
|
|
@@ -2016,6 +3177,11 @@ function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
|
2016
3177
|
if (field.type === "image") {
|
|
2017
3178
|
const src = element.attr("src") || element.find("img").attr("src") || "";
|
|
2018
3179
|
content.fields[fieldName] = src;
|
|
3180
|
+
} else if (field.type === "link") {
|
|
3181
|
+
const linkElement = element.is("a") || element.is("NuxtLink") || element.is("nuxt-link") ? element : element.find("a, NuxtLink, nuxt-link").first();
|
|
3182
|
+
if (linkElement.length > 0) {
|
|
3183
|
+
content.fields[fieldName] = extractLinkValue(linkElement);
|
|
3184
|
+
}
|
|
2019
3185
|
} else {
|
|
2020
3186
|
const text = element.text().trim();
|
|
2021
3187
|
content.fields[fieldName] = text;
|
|
@@ -2030,15 +3196,19 @@ function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
|
2030
3196
|
collectionElements.each((_, elem) => {
|
|
2031
3197
|
const item = {};
|
|
2032
3198
|
const $elem = $(elem);
|
|
2033
|
-
for (const [fieldName,
|
|
3199
|
+
for (const [fieldName, fieldConfig] of Object.entries(collection.fields)) {
|
|
3200
|
+
const fieldSelector = typeof fieldConfig === "string" ? fieldConfig : fieldConfig.selector || fieldConfig;
|
|
3201
|
+
const fieldType = typeof fieldConfig === "object" ? fieldConfig.type : void 0;
|
|
2034
3202
|
const fieldElement = $elem.find(fieldSelector).first();
|
|
2035
3203
|
if (fieldElement.length > 0) {
|
|
2036
|
-
if (fieldName === "image" || fieldName.includes("image")) {
|
|
3204
|
+
if (fieldType === "image" || fieldName === "image" || fieldName.includes("image")) {
|
|
2037
3205
|
const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
|
|
2038
3206
|
item[fieldName] = src;
|
|
2039
|
-
} else if (fieldName === "link" || fieldName === "url") {
|
|
2040
|
-
const
|
|
2041
|
-
|
|
3207
|
+
} else if (fieldType === "link" || fieldName === "link" || fieldName === "url") {
|
|
3208
|
+
const linkElement = fieldElement.is("a") || fieldElement.is("NuxtLink") || fieldElement.is("nuxt-link") ? fieldElement : fieldElement.find("a, NuxtLink, nuxt-link").first();
|
|
3209
|
+
if (linkElement.length > 0) {
|
|
3210
|
+
item[fieldName] = extractLinkValue(linkElement);
|
|
3211
|
+
}
|
|
2042
3212
|
} else {
|
|
2043
3213
|
const text = fieldElement.text().trim();
|
|
2044
3214
|
item[fieldName] = text;
|
|
@@ -2067,16 +3237,23 @@ function extractAllContent(htmlFiles, manifest) {
|
|
|
2067
3237
|
extractedContent.pages[pageName] = content;
|
|
2068
3238
|
}
|
|
2069
3239
|
}
|
|
2070
|
-
|
|
3240
|
+
if (manifest.global?.fields) {
|
|
3241
|
+
const firstPage = Object.keys(manifest.pages)[0];
|
|
3242
|
+
const firstHtml = firstPage ? htmlFiles.get(firstPage) : void 0;
|
|
3243
|
+
if (firstHtml) {
|
|
3244
|
+
extractedContent.global = extractContentFromHTML(firstHtml, "global", {
|
|
3245
|
+
fields: manifest.global.fields,
|
|
3246
|
+
collections: {}
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return { ...extractedContent, manifest };
|
|
2071
3251
|
}
|
|
2072
3252
|
function normalizeImagePath(imageSrc) {
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
return `/images/${filename}`;
|
|
2078
|
-
}
|
|
2079
|
-
return `/${filename}`;
|
|
3253
|
+
return normalizeImageSeedPath(imageSrc);
|
|
3254
|
+
}
|
|
3255
|
+
function isLinkValue(value) {
|
|
3256
|
+
return typeof value === "object" && value !== null && "url" in value && "text" in value;
|
|
2080
3257
|
}
|
|
2081
3258
|
function formatForStrapi(extracted) {
|
|
2082
3259
|
const seedData = {};
|
|
@@ -2084,7 +3261,9 @@ function formatForStrapi(extracted) {
|
|
|
2084
3261
|
if (Object.keys(content.fields).length > 0) {
|
|
2085
3262
|
const formattedFields = {};
|
|
2086
3263
|
for (const [fieldName, value] of Object.entries(content.fields)) {
|
|
2087
|
-
if (
|
|
3264
|
+
if (isLinkValue(value)) {
|
|
3265
|
+
formattedFields[fieldName] = value;
|
|
3266
|
+
} else if (fieldName.includes("image") || fieldName.includes("img") || fieldName.includes("bg") || isLikelyImagePath(value)) {
|
|
2088
3267
|
formattedFields[fieldName] = normalizeImagePath(value);
|
|
2089
3268
|
} else {
|
|
2090
3269
|
formattedFields[fieldName] = value;
|
|
@@ -2096,7 +3275,9 @@ function formatForStrapi(extracted) {
|
|
|
2096
3275
|
const formattedItems = items.map((item) => {
|
|
2097
3276
|
const formattedItem = {};
|
|
2098
3277
|
for (const [fieldName, value] of Object.entries(item)) {
|
|
2099
|
-
if (
|
|
3278
|
+
if (isLinkValue(value)) {
|
|
3279
|
+
formattedItem[fieldName] = value;
|
|
3280
|
+
} else if (fieldName === "image" || fieldName.includes("image") || fieldName.includes("img") || isLikelyImagePath(value)) {
|
|
2100
3281
|
formattedItem[fieldName] = normalizeImagePath(value);
|
|
2101
3282
|
} else {
|
|
2102
3283
|
formattedItem[fieldName] = value;
|
|
@@ -2107,20 +3288,33 @@ function formatForStrapi(extracted) {
|
|
|
2107
3288
|
seedData[collectionName] = formattedItems;
|
|
2108
3289
|
}
|
|
2109
3290
|
}
|
|
3291
|
+
if (extracted.global && Object.keys(extracted.global.fields).length > 0) {
|
|
3292
|
+
const formattedFields = {};
|
|
3293
|
+
for (const [fieldName, value] of Object.entries(extracted.global.fields)) {
|
|
3294
|
+
if (isLinkValue(value)) {
|
|
3295
|
+
formattedFields[fieldName] = value;
|
|
3296
|
+
} else if (fieldName.includes("image") || fieldName.includes("img") || fieldName.includes("bg") || isLikelyImagePath(value)) {
|
|
3297
|
+
formattedFields[fieldName] = normalizeImagePath(value);
|
|
3298
|
+
} else {
|
|
3299
|
+
formattedFields[fieldName] = value;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
seedData.global = formattedFields;
|
|
3303
|
+
}
|
|
2110
3304
|
return seedData;
|
|
2111
3305
|
}
|
|
2112
3306
|
|
|
2113
3307
|
// src/seed-writer.ts
|
|
2114
3308
|
import fs9 from "fs-extra";
|
|
2115
|
-
import
|
|
3309
|
+
import path12 from "path";
|
|
2116
3310
|
async function writeSeedData(outputDir, seedData) {
|
|
2117
|
-
const seedDir =
|
|
3311
|
+
const seedDir = path12.join(outputDir, "cms-seed");
|
|
2118
3312
|
await fs9.ensureDir(seedDir);
|
|
2119
|
-
const seedPath =
|
|
3313
|
+
const seedPath = path12.join(seedDir, "seed-data.json");
|
|
2120
3314
|
await fs9.writeJson(seedPath, seedData, { spaces: 2 });
|
|
2121
3315
|
}
|
|
2122
3316
|
async function createSeedReadme(outputDir) {
|
|
2123
|
-
const readmePath =
|
|
3317
|
+
const readmePath = path12.join(outputDir, "cms-seed", "README.md");
|
|
2124
3318
|
const content = `# CMS Seed Data
|
|
2125
3319
|
|
|
2126
3320
|
Auto-extracted content from your Webflow export, ready to seed into Strapi.
|
|
@@ -2179,20 +3373,574 @@ When seeding Strapi, these images will be uploaded to Strapi's media library.
|
|
|
2179
3373
|
await fs9.writeFile(readmePath, content, "utf-8");
|
|
2180
3374
|
}
|
|
2181
3375
|
|
|
3376
|
+
// src/component-extractor.ts
|
|
3377
|
+
import * as cheerio5 from "cheerio";
|
|
3378
|
+
import * as crypto from "crypto";
|
|
3379
|
+
import fs10 from "fs-extra";
|
|
3380
|
+
import path13 from "path";
|
|
3381
|
+
import { glob as glob4 } from "glob";
|
|
3382
|
+
var COMPONENT_NAME_PATTERNS = {
|
|
3383
|
+
TheNav: [/nav/i, /navbar/i, /navigation/i, /header.*nav/i, /main.*menu/i],
|
|
3384
|
+
TheFooter: [/footer/i, /site.*footer/i],
|
|
3385
|
+
TheHeader: [/header/i, /site.*header/i, /page.*header/i],
|
|
3386
|
+
TheSidebar: [/sidebar/i, /side.*bar/i, /aside/i]
|
|
3387
|
+
};
|
|
3388
|
+
var DEFAULT_MIN_SECTION_SIZE = 200;
|
|
3389
|
+
async function parseAllPages(inputDir, options = {}) {
|
|
3390
|
+
const pages = [];
|
|
3391
|
+
const htmlFiles = await glob4("**/*.html", { cwd: inputDir, nodir: true });
|
|
3392
|
+
for (const file of htmlFiles) {
|
|
3393
|
+
if (!file.endsWith(".html")) continue;
|
|
3394
|
+
const filePath = path13.join(inputDir, file);
|
|
3395
|
+
const html = await fs10.readFile(filePath, "utf-8");
|
|
3396
|
+
const $ = cheerio5.load(html);
|
|
3397
|
+
const pageName = htmlPathToPageId(file);
|
|
3398
|
+
const sections = extractSections($, options.minSectionSize ?? DEFAULT_MIN_SECTION_SIZE);
|
|
3399
|
+
pages.push({
|
|
3400
|
+
name: pageName,
|
|
3401
|
+
filePath,
|
|
3402
|
+
sourcePath: file,
|
|
3403
|
+
$,
|
|
3404
|
+
sections
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
return pages;
|
|
3408
|
+
}
|
|
3409
|
+
function extractSections($, minSectionSize) {
|
|
3410
|
+
const sections = [];
|
|
3411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3412
|
+
let $container = $("body");
|
|
3413
|
+
const $bodyChildren = $container.children();
|
|
3414
|
+
if ($bodyChildren.length === 1 && $bodyChildren.first().is("div")) {
|
|
3415
|
+
const $wrapper = $bodyChildren.first();
|
|
3416
|
+
if ($wrapper.children().length > 1) {
|
|
3417
|
+
$container = $wrapper;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
$container.children().each((_, el) => {
|
|
3421
|
+
const $element = $(el);
|
|
3422
|
+
const tagName = ($element.prop("tagName") || "").toLowerCase();
|
|
3423
|
+
if (["script", "style", "link", "meta", "noscript", "template"].includes(tagName)) {
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
const className = $element.attr("class") || "";
|
|
3427
|
+
if (className.includes("global-embed") || className.includes("globalembed")) {
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
const html = $.html($element);
|
|
3431
|
+
const elementId = getElementIdentifier($, $element);
|
|
3432
|
+
if (seen.has(elementId)) return;
|
|
3433
|
+
seen.add(elementId);
|
|
3434
|
+
const fingerprint = createFingerprint($, $element);
|
|
3435
|
+
const exactFingerprint = createExactFingerprint(html);
|
|
3436
|
+
const suggestedName = suggestComponentName($element);
|
|
3437
|
+
const semanticName = ["TheNav", "TheFooter", "TheHeader", "TheSidebar", "Nav", "Footer", "Header", "Sidebar"].includes(suggestedName);
|
|
3438
|
+
if (!semanticName && html.length < minSectionSize) return;
|
|
3439
|
+
const uniqueSelector = buildUniqueSelector2($, $element);
|
|
3440
|
+
sections.push({
|
|
3441
|
+
selector: uniqueSelector,
|
|
3442
|
+
fingerprint,
|
|
3443
|
+
exactFingerprint,
|
|
3444
|
+
$element,
|
|
3445
|
+
html,
|
|
3446
|
+
suggestedName
|
|
3447
|
+
});
|
|
3448
|
+
});
|
|
3449
|
+
return sections;
|
|
3450
|
+
}
|
|
3451
|
+
function createExactFingerprint(html) {
|
|
3452
|
+
const normalized = html.replace(/\s*data-w-id="[^"]*"/g, "").replace(/\s*data-wf-page="[^"]*"/g, "").replace(/\s*data-wf-site="[^"]*"/g, "").replace(/\s+/g, " ").trim();
|
|
3453
|
+
return crypto.createHash("md5").update(normalized).digest("hex").substring(0, 12);
|
|
3454
|
+
}
|
|
3455
|
+
function getElementIdentifier(_$, $element) {
|
|
3456
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3457
|
+
const className = $element.attr("class") || "";
|
|
3458
|
+
const id = $element.attr("id") || "";
|
|
3459
|
+
return `${tag}#${id}.${className}`;
|
|
3460
|
+
}
|
|
3461
|
+
function createFingerprint($, $element) {
|
|
3462
|
+
const structure = getStructure($, $element);
|
|
3463
|
+
return crypto.createHash("md5").update(structure).digest("hex").substring(0, 12);
|
|
3464
|
+
}
|
|
3465
|
+
function getStructure($, $element, depth = 0) {
|
|
3466
|
+
if (depth > 10) return "";
|
|
3467
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3468
|
+
const classNames = normalizeClasses($element.attr("class") || "");
|
|
3469
|
+
let structure = `${tag}`;
|
|
3470
|
+
if (classNames) {
|
|
3471
|
+
structure += `.${classNames}`;
|
|
3472
|
+
}
|
|
3473
|
+
const children = [];
|
|
3474
|
+
$element.children().each((_, child) => {
|
|
3475
|
+
const $child = $(child);
|
|
3476
|
+
const childStructure = getStructure($, $child, depth + 1);
|
|
3477
|
+
if (childStructure) {
|
|
3478
|
+
children.push(childStructure);
|
|
3479
|
+
}
|
|
3480
|
+
});
|
|
3481
|
+
if (children.length > 0) {
|
|
3482
|
+
const childCounts = countIdentical(children);
|
|
3483
|
+
const compressedChildren = childCounts.map(({ item, count }) => count > 1 ? `${item}*${count}` : item).join(",");
|
|
3484
|
+
structure += `[${compressedChildren}]`;
|
|
3485
|
+
}
|
|
3486
|
+
return structure;
|
|
3487
|
+
}
|
|
3488
|
+
function normalizeClasses(classes) {
|
|
3489
|
+
return classes.split(/\s+/).filter((c) => {
|
|
3490
|
+
if (c.startsWith("w-")) return false;
|
|
3491
|
+
if (c.match(/^(p|m|pt|pb|pl|pr|px|py|mt|mb|ml|mr|mx|my)-\d/)) return false;
|
|
3492
|
+
return c.length > 0;
|
|
3493
|
+
}).sort().join(".");
|
|
3494
|
+
}
|
|
3495
|
+
function countIdentical(items) {
|
|
3496
|
+
const result = [];
|
|
3497
|
+
for (const item of items) {
|
|
3498
|
+
const last = result[result.length - 1];
|
|
3499
|
+
if (last && last.item === item) {
|
|
3500
|
+
last.count++;
|
|
3501
|
+
} else {
|
|
3502
|
+
result.push({ item, count: 1 });
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return result;
|
|
3506
|
+
}
|
|
3507
|
+
function suggestComponentName($element) {
|
|
3508
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "";
|
|
3509
|
+
const className = $element.attr("class") || "";
|
|
3510
|
+
const id = $element.attr("id") || "";
|
|
3511
|
+
if (tag === "nav") return "TheNav";
|
|
3512
|
+
if (tag === "footer") return "TheFooter";
|
|
3513
|
+
if (tag === "header") return "TheHeader";
|
|
3514
|
+
if (tag === "aside") return "TheSidebar";
|
|
3515
|
+
const normalizedClassName = className.replace(/\bc-/g, "");
|
|
3516
|
+
const searchText = `${className} ${normalizedClassName} ${id}`.toLowerCase();
|
|
3517
|
+
for (const [name, patterns] of Object.entries(COMPONENT_NAME_PATTERNS)) {
|
|
3518
|
+
for (const pattern of patterns) {
|
|
3519
|
+
if (pattern.test(searchText)) {
|
|
3520
|
+
return name;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const primaryClass = className.split(" ").find((c) => !c.startsWith("w-") && c.length > 2);
|
|
3525
|
+
if (primaryClass) {
|
|
3526
|
+
const cleanName = primaryClass.replace(/^c-/, "");
|
|
3527
|
+
return pascalCase(cleanName);
|
|
3528
|
+
}
|
|
3529
|
+
return "SharedSection";
|
|
3530
|
+
}
|
|
3531
|
+
function pascalCase(str) {
|
|
3532
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(str)) {
|
|
3533
|
+
return str;
|
|
3534
|
+
}
|
|
3535
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3536
|
+
}
|
|
3537
|
+
function buildUniqueSelector2($, $element) {
|
|
3538
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3539
|
+
const id = $element.attr("id");
|
|
3540
|
+
const className = $element.attr("class");
|
|
3541
|
+
if (id) {
|
|
3542
|
+
return `#${id}`;
|
|
3543
|
+
}
|
|
3544
|
+
if (className) {
|
|
3545
|
+
const primaryClasses = className.split(" ").filter((c) => !c.startsWith("w-") && c.length > 2).slice(0, 2);
|
|
3546
|
+
if (primaryClasses.length > 0) {
|
|
3547
|
+
const selector = `${tag}.${primaryClasses.join(".")}`;
|
|
3548
|
+
if ($(selector).length === 1) {
|
|
3549
|
+
return selector;
|
|
3550
|
+
}
|
|
3551
|
+
return `.${primaryClasses.join(".")}`;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
const $parent = $element.parent();
|
|
3555
|
+
const siblings = $parent.children(tag);
|
|
3556
|
+
if (siblings.length > 1) {
|
|
3557
|
+
const index = siblings.index($element) + 1;
|
|
3558
|
+
return `${tag}:nth-child(${index})`;
|
|
3559
|
+
}
|
|
3560
|
+
return tag;
|
|
3561
|
+
}
|
|
3562
|
+
function findSharedSections(pages, options = {}) {
|
|
3563
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
3564
|
+
const match = options.match || "exact";
|
|
3565
|
+
for (const page of pages) {
|
|
3566
|
+
for (const section of page.sections) {
|
|
3567
|
+
const fingerprint = match === "structure" ? section.fingerprint : section.exactFingerprint;
|
|
3568
|
+
const existing = fingerprintMap.get(fingerprint) || [];
|
|
3569
|
+
existing.push({ section, page });
|
|
3570
|
+
fingerprintMap.set(fingerprint, existing);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
const sharedComponents = [];
|
|
3574
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
3575
|
+
for (const occurrences of fingerprintMap.values()) {
|
|
3576
|
+
const uniquePages = new Set(occurrences.map((o) => o.page.name));
|
|
3577
|
+
if (occurrences.length < (options.minOccurrences ?? 2)) continue;
|
|
3578
|
+
if (uniquePages.size < (options.minPages ?? options.minOccurrences ?? 2)) continue;
|
|
3579
|
+
const template = occurrences[0];
|
|
3580
|
+
let name = template.section.suggestedName;
|
|
3581
|
+
if (options.include?.length && !options.include.some((item) => name.toLowerCase().includes(item.toLowerCase()))) {
|
|
3582
|
+
const semanticNames = ["TheNav", "TheFooter", "TheHeader", "TheSidebar"];
|
|
3583
|
+
if (!semanticNames.includes(name)) continue;
|
|
3584
|
+
}
|
|
3585
|
+
if (options.exclude?.some((item) => name.toLowerCase().includes(item.toLowerCase()))) continue;
|
|
3586
|
+
let counter = 1;
|
|
3587
|
+
while (usedNames.has(name)) {
|
|
3588
|
+
name = `${template.section.suggestedName}${counter++}`;
|
|
3589
|
+
}
|
|
3590
|
+
usedNames.add(name);
|
|
3591
|
+
const confidence = getComponentConfidence(name, uniquePages.size, pages.length);
|
|
3592
|
+
sharedComponents.push({
|
|
3593
|
+
name,
|
|
3594
|
+
selector: template.section.selector,
|
|
3595
|
+
pages: [...uniquePages],
|
|
3596
|
+
html: template.section.html,
|
|
3597
|
+
fingerprint: template.section.fingerprint,
|
|
3598
|
+
confidence,
|
|
3599
|
+
reason: confidence === "high" ? "Semantic or repeated site-wide section" : match === "exact" ? "Exact repeated HTML section" : "Repeated section with matching DOM structure"
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
return sharedComponents;
|
|
3603
|
+
}
|
|
3604
|
+
function getComponentConfidence(name, pageCount, totalPages) {
|
|
3605
|
+
const semanticNames = ["thenav", "thefooter", "theheader", "nav", "footer", "header"];
|
|
3606
|
+
if (semanticNames.includes(name.toLowerCase())) return "high";
|
|
3607
|
+
if (pageCount === totalPages || pageCount >= 3) return "medium";
|
|
3608
|
+
return "low";
|
|
3609
|
+
}
|
|
3610
|
+
function createVueComponent(component) {
|
|
3611
|
+
let html = component.html;
|
|
3612
|
+
html = html.replace(/\s*data-w-id="[^"]*"/g, "");
|
|
3613
|
+
html = html.replace(/\s*data-wf-page="[^"]*"/g, "");
|
|
3614
|
+
html = html.replace(/\s*data-wf-site="[^"]*"/g, "");
|
|
3615
|
+
return `<script setup lang="ts">
|
|
3616
|
+
/**
|
|
3617
|
+
* ${component.name} Component
|
|
3618
|
+
* Shared across pages: ${component.pages.join(", ")}
|
|
3619
|
+
*
|
|
3620
|
+
* To make content editable, add fields to the 'global' section in cms-manifest.json
|
|
3621
|
+
*/
|
|
3622
|
+
const { content } = useStrapiContent('global')
|
|
3623
|
+
</script>
|
|
3624
|
+
|
|
3625
|
+
<template>
|
|
3626
|
+
${html}
|
|
3627
|
+
</template>
|
|
3628
|
+
|
|
3629
|
+
<style scoped>
|
|
3630
|
+
/* Component-specific styles if needed */
|
|
3631
|
+
</style>
|
|
3632
|
+
`;
|
|
3633
|
+
}
|
|
3634
|
+
function replaceWithComponent($, selector, componentName) {
|
|
3635
|
+
const $element = $(selector);
|
|
3636
|
+
if ($element.length > 0) {
|
|
3637
|
+
$element.replaceWith(`<!--COMPONENT:${componentName}-->`);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
async function writeComponents(outputDir, components) {
|
|
3641
|
+
const componentsDir = path13.join(outputDir, "components");
|
|
3642
|
+
await fs10.ensureDir(componentsDir);
|
|
3643
|
+
const sharedComponents = [];
|
|
3644
|
+
for (const component of components) {
|
|
3645
|
+
const vueContent = createVueComponent(component);
|
|
3646
|
+
const filePath = path13.join(componentsDir, `${component.name}.vue`);
|
|
3647
|
+
await fs10.writeFile(filePath, vueContent, "utf-8");
|
|
3648
|
+
sharedComponents.push({
|
|
3649
|
+
name: component.name,
|
|
3650
|
+
selector: component.selector,
|
|
3651
|
+
pages: component.pages,
|
|
3652
|
+
confidence: component.confidence,
|
|
3653
|
+
reason: component.reason,
|
|
3654
|
+
role: component.role || "shared-section",
|
|
3655
|
+
collectionName: component.collectionName,
|
|
3656
|
+
collectionStorage: component.collectionStorage,
|
|
3657
|
+
contentMode: component.contentMode || "shared-global"
|
|
3658
|
+
// Fields will be detected separately
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
return sharedComponents;
|
|
3662
|
+
}
|
|
3663
|
+
async function extractSharedComponents(inputDir, outputDir, options = {}) {
|
|
3664
|
+
const pages = await parseAllPages(inputDir, options);
|
|
3665
|
+
if (pages.length < 2) {
|
|
3666
|
+
return [];
|
|
3667
|
+
}
|
|
3668
|
+
const rules = (options.rules || []).map((rule) => ({
|
|
3669
|
+
...rule,
|
|
3670
|
+
minOccurrences: rule.minOccurrences ?? options.minOccurrences,
|
|
3671
|
+
minPages: rule.minPages ?? options.minPages
|
|
3672
|
+
}));
|
|
3673
|
+
const ruleSections = findRuleSections(pages, rules);
|
|
3674
|
+
const sharedSections = [...ruleSections, ...findSharedSections(pages, options)];
|
|
3675
|
+
if (sharedSections.length === 0) {
|
|
3676
|
+
return [];
|
|
3677
|
+
}
|
|
3678
|
+
const componentsToWrite = sharedSections.filter((component) => meetsConfidence(component.confidence, options.writeConfidence || "medium"));
|
|
3679
|
+
const components = await writeComponents(outputDir, componentsToWrite);
|
|
3680
|
+
return components;
|
|
3681
|
+
}
|
|
3682
|
+
function findRuleSections(pages, rules) {
|
|
3683
|
+
const components = [];
|
|
3684
|
+
for (const rule of rules) {
|
|
3685
|
+
const occurrences = [];
|
|
3686
|
+
for (const page of pages) {
|
|
3687
|
+
const $elements = page.$(rule.selector);
|
|
3688
|
+
if ($elements.length === 0) continue;
|
|
3689
|
+
$elements.each((index, element) => {
|
|
3690
|
+
if (rule.role !== "collection-item" && index > 0) return false;
|
|
3691
|
+
occurrences.push({ page, html: page.$.html(element) });
|
|
3692
|
+
return void 0;
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
const uniquePages = new Set(occurrences.map(({ page }) => page.name));
|
|
3696
|
+
if (occurrences.length < (rule.minOccurrences ?? 2)) continue;
|
|
3697
|
+
if (uniquePages.size < (rule.minPages ?? rule.minOccurrences ?? 2)) continue;
|
|
3698
|
+
components.push({
|
|
3699
|
+
name: pascalCase(rule.name),
|
|
3700
|
+
selector: rule.selector,
|
|
3701
|
+
pages: [...uniquePages],
|
|
3702
|
+
html: occurrences[0].html,
|
|
3703
|
+
fingerprint: crypto.createHash("md5").update(occurrences[0].html).digest("hex").substring(0, 12),
|
|
3704
|
+
confidence: "high",
|
|
3705
|
+
reason: "User-defined component rule",
|
|
3706
|
+
role: rule.role || "shared-section",
|
|
3707
|
+
collectionName: rule.collectionName || toCollectionName2(rule.name),
|
|
3708
|
+
collectionStorage: rule.collectionStorage || "collection-type",
|
|
3709
|
+
contentMode: rule.contentMode || "auto"
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
return components;
|
|
3713
|
+
}
|
|
3714
|
+
function toCollectionName2(name) {
|
|
3715
|
+
const base = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
3716
|
+
if (base.endsWith("s")) return base;
|
|
3717
|
+
return `${base}s`;
|
|
3718
|
+
}
|
|
3719
|
+
function meetsConfidence(confidence, minimum) {
|
|
3720
|
+
const rank = { low: 1, medium: 2, high: 3 };
|
|
3721
|
+
return rank[confidence] >= rank[minimum];
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
// src/converter.ts
|
|
3725
|
+
import * as cheerio6 from "cheerio";
|
|
3726
|
+
|
|
3727
|
+
// src/analyzer.ts
|
|
3728
|
+
import path14 from "path";
|
|
3729
|
+
import fs11 from "fs-extra";
|
|
3730
|
+
async function analyzeWebflowExport(inputDir, config = {}) {
|
|
3731
|
+
const inputExists = await fs11.pathExists(inputDir);
|
|
3732
|
+
if (!inputExists) {
|
|
3733
|
+
throw new Error(`Input directory not found: ${inputDir}`);
|
|
3734
|
+
}
|
|
3735
|
+
const [assets, htmlFiles] = await Promise.all([
|
|
3736
|
+
scanAssets(inputDir),
|
|
3737
|
+
findHTMLFiles(inputDir)
|
|
3738
|
+
]);
|
|
3739
|
+
const pages = htmlFiles.sort().map(getPageRouteInfo);
|
|
3740
|
+
const warnings = [];
|
|
3741
|
+
if (pages.length === 0) {
|
|
3742
|
+
warnings.push("No HTML pages were found in the input directory.");
|
|
3743
|
+
}
|
|
3744
|
+
const componentConfig = config.components || {};
|
|
3745
|
+
const parsedPages = await parseAllPages(inputDir, {
|
|
3746
|
+
minSectionSize: componentConfig.minSectionSize
|
|
3747
|
+
});
|
|
3748
|
+
const componentCandidates = findSharedSections(parsedPages, {
|
|
3749
|
+
match: componentConfig.match,
|
|
3750
|
+
minOccurrences: componentConfig.minOccurrences,
|
|
3751
|
+
minPages: componentConfig.minPages,
|
|
3752
|
+
include: componentConfig.include,
|
|
3753
|
+
exclude: componentConfig.exclude,
|
|
3754
|
+
rules: componentConfig.rules
|
|
3755
|
+
}).map((component) => ({
|
|
3756
|
+
name: component.name,
|
|
3757
|
+
selector: component.selector,
|
|
3758
|
+
pages: component.pages,
|
|
3759
|
+
confidence: component.confidence,
|
|
3760
|
+
reason: component.reason
|
|
3761
|
+
}));
|
|
3762
|
+
return {
|
|
3763
|
+
inputDir,
|
|
3764
|
+
pages,
|
|
3765
|
+
assets,
|
|
3766
|
+
componentCandidates,
|
|
3767
|
+
warnings
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
function createConversionReport(input) {
|
|
3771
|
+
return {
|
|
3772
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3773
|
+
stages: input.stages,
|
|
3774
|
+
pages: input.analysis.pages.map((page) => ({
|
|
3775
|
+
source: page.sourcePath,
|
|
3776
|
+
pageId: page.pageId,
|
|
3777
|
+
route: page.route,
|
|
3778
|
+
output: page.outputPath
|
|
3779
|
+
})),
|
|
3780
|
+
assets: {
|
|
3781
|
+
css: input.analysis.assets.css.length,
|
|
3782
|
+
images: input.analysis.assets.images.length,
|
|
3783
|
+
fonts: input.analysis.assets.fonts.length,
|
|
3784
|
+
js: input.analysis.assets.js.length,
|
|
3785
|
+
preservedStructure: true
|
|
3786
|
+
},
|
|
3787
|
+
components: input.components.map((component) => ({
|
|
3788
|
+
name: component.name,
|
|
3789
|
+
selector: component.selector,
|
|
3790
|
+
pages: component.pages,
|
|
3791
|
+
confidence: component.confidence,
|
|
3792
|
+
reason: component.reason
|
|
3793
|
+
})),
|
|
3794
|
+
cms: {
|
|
3795
|
+
provider: input.provider,
|
|
3796
|
+
fields: input.fields,
|
|
3797
|
+
collections: input.collections,
|
|
3798
|
+
schemas: input.schemas,
|
|
3799
|
+
seedPages: input.seedPages
|
|
3800
|
+
},
|
|
3801
|
+
warnings: [...input.analysis.warnings, ...input.warnings || []]
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
async function writeConversionReport(outputDir, report) {
|
|
3805
|
+
const jsonPath = path14.join(outputDir, "see-ms-report.json");
|
|
3806
|
+
const mdPath = path14.join(outputDir, "see-ms-report.md");
|
|
3807
|
+
await fs11.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
3808
|
+
await fs11.writeFile(mdPath, renderReportMarkdown(report), "utf-8");
|
|
3809
|
+
}
|
|
3810
|
+
function renderReportMarkdown(report) {
|
|
3811
|
+
const lines = [
|
|
3812
|
+
"# SeeMS Conversion Report",
|
|
3813
|
+
"",
|
|
3814
|
+
`Generated: ${report.generatedAt}`,
|
|
3815
|
+
"",
|
|
3816
|
+
"## Stages",
|
|
3817
|
+
report.stages.map((stage) => `- ${stage}`).join("\n") || "- none",
|
|
3818
|
+
"",
|
|
3819
|
+
"## Pages",
|
|
3820
|
+
...report.pages.map((page) => `- ${page.source} -> ${page.output} (${page.route}, id: ${page.pageId})`),
|
|
3821
|
+
"",
|
|
3822
|
+
"## Assets",
|
|
3823
|
+
`- CSS: ${report.assets.css}`,
|
|
3824
|
+
`- Images: ${report.assets.images}`,
|
|
3825
|
+
`- Fonts: ${report.assets.fonts}`,
|
|
3826
|
+
`- JS: ${report.assets.js}`,
|
|
3827
|
+
`- Preserved folder structure: ${report.assets.preservedStructure ? "yes" : "no"}`,
|
|
3828
|
+
"",
|
|
3829
|
+
"## Components",
|
|
3830
|
+
...report.components.length ? report.components.map((component) => `- ${component.name} (${component.confidence || "unknown"}): ${component.pages.join(", ")}`) : ["- none"],
|
|
3831
|
+
"",
|
|
3832
|
+
"## CMS",
|
|
3833
|
+
`- Provider: ${report.cms.provider}`,
|
|
3834
|
+
`- Editable fields: ${report.cms.fields}`,
|
|
3835
|
+
`- Collections: ${report.cms.collections}`,
|
|
3836
|
+
`- Schemas: ${report.cms.schemas}`,
|
|
3837
|
+
`- Seeded pages: ${report.cms.seedPages}`,
|
|
3838
|
+
"",
|
|
3839
|
+
"## Warnings",
|
|
3840
|
+
...report.warnings.length ? report.warnings.map((warning) => `- ${warning}`) : ["- none"],
|
|
3841
|
+
""
|
|
3842
|
+
];
|
|
3843
|
+
return lines.join("\n");
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
// src/config.ts
|
|
3847
|
+
import fs12 from "fs-extra";
|
|
3848
|
+
import path15 from "path";
|
|
3849
|
+
var DEFAULT_SEEMS_CONFIG = {
|
|
3850
|
+
target: "nuxt",
|
|
3851
|
+
cms: { provider: "strapi", strapi: { scaffold: false, packageManager: "npm", install: true } },
|
|
3852
|
+
components: {
|
|
3853
|
+
enabled: true,
|
|
3854
|
+
match: "structure",
|
|
3855
|
+
minOccurrences: 2,
|
|
3856
|
+
minPages: 2,
|
|
3857
|
+
minSectionSize: 200,
|
|
3858
|
+
writeConfidence: "medium",
|
|
3859
|
+
include: ["nav", "header", "footer"],
|
|
3860
|
+
exclude: [],
|
|
3861
|
+
rules: []
|
|
3862
|
+
},
|
|
3863
|
+
ignore: {
|
|
3864
|
+
selectors: [],
|
|
3865
|
+
classes: []
|
|
3866
|
+
},
|
|
3867
|
+
editor: {
|
|
3868
|
+
enabled: true,
|
|
3869
|
+
previewParam: "preview"
|
|
3870
|
+
},
|
|
3871
|
+
assets: {
|
|
3872
|
+
excludeResponsiveVariants: true
|
|
3873
|
+
}
|
|
3874
|
+
};
|
|
3875
|
+
function mergeConfig(base = {}, override = {}) {
|
|
3876
|
+
return {
|
|
3877
|
+
...base,
|
|
3878
|
+
...override,
|
|
3879
|
+
cms: {
|
|
3880
|
+
...base.cms,
|
|
3881
|
+
...override.cms,
|
|
3882
|
+
strapi: { ...base.cms?.strapi, ...override.cms?.strapi }
|
|
3883
|
+
},
|
|
3884
|
+
components: { ...base.components, ...override.components },
|
|
3885
|
+
ignore: { ...base.ignore, ...override.ignore },
|
|
3886
|
+
editor: { ...base.editor, ...override.editor },
|
|
3887
|
+
assets: { ...base.assets, ...override.assets },
|
|
3888
|
+
collections: override.collections ?? base.collections,
|
|
3889
|
+
fields: { ...base.fields, ...override.fields }
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
async function loadSeeMSConfig(configPath) {
|
|
3893
|
+
if (!configPath) return {};
|
|
3894
|
+
const absolutePath = path15.resolve(configPath);
|
|
3895
|
+
if (!await fs12.pathExists(absolutePath)) {
|
|
3896
|
+
throw new Error(`Config file not found: ${absolutePath}`);
|
|
3897
|
+
}
|
|
3898
|
+
if (absolutePath.endsWith(".json")) {
|
|
3899
|
+
return JSON.parse(await fs12.readFile(absolutePath, "utf-8"));
|
|
3900
|
+
}
|
|
3901
|
+
const content = await fs12.readFile(absolutePath, "utf-8");
|
|
3902
|
+
const objectMatch = content.match(/export\s+default\s+(\{[\s\S]*\})\s*(?:satisfies\s+\w+)?\s*;?\s*$/) || content.match(/const\s+\w+(?::\s*[\w<>]+)?\s*=\s*(\{[\s\S]*\})\s*;\s*export\s+default\s+\w+\s*;?\s*$/);
|
|
3903
|
+
if (!objectMatch) {
|
|
3904
|
+
throw new Error("see-ms config must export an object literal or be JSON");
|
|
3905
|
+
}
|
|
3906
|
+
return Function(`"use strict"; return (${objectMatch[1]});`)();
|
|
3907
|
+
}
|
|
3908
|
+
async function writeSeeMSConfig(outputDir, config) {
|
|
3909
|
+
const target = path15.join(outputDir, "see-ms.config.ts");
|
|
3910
|
+
const content = `import type { SeeMSConfig } from "@see-ms/types";
|
|
3911
|
+
|
|
3912
|
+
const config: SeeMSConfig = ${JSON.stringify(config, null, 2)};
|
|
3913
|
+
|
|
3914
|
+
export default config;
|
|
3915
|
+
`;
|
|
3916
|
+
await fs12.writeFile(target, content, "utf-8");
|
|
3917
|
+
}
|
|
3918
|
+
function normalizeConfig(config = {}) {
|
|
3919
|
+
return mergeConfig(DEFAULT_SEEMS_CONFIG, config);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
2182
3922
|
// src/converter.ts
|
|
2183
3923
|
async function convertWebflowExport(options) {
|
|
2184
3924
|
const { inputDir, outputDir, boilerplate } = options;
|
|
2185
|
-
|
|
3925
|
+
const loadedConfig = options.configPath ? await loadSeeMSConfig(options.configPath) : {};
|
|
3926
|
+
const config = normalizeConfig(mergeConfig(loadedConfig, options.config || {}));
|
|
3927
|
+
const target = options.target || config.target || "nuxt";
|
|
3928
|
+
const provider = options.cmsBackend || config.cms?.provider || "strapi";
|
|
3929
|
+
const editorEnabled = options.editor ?? config.editor?.enabled ?? true;
|
|
3930
|
+
const shouldGenerateContent = options.generateContent !== false;
|
|
3931
|
+
const collectionClasses = options.collectionClasses || config.collections?.map((collection) => collection.className);
|
|
3932
|
+
const collectionNames = options.collectionNames || Object.fromEntries(
|
|
3933
|
+
(config.collections || []).map((collection) => [collection.className, collection.name || collection.className])
|
|
3934
|
+
);
|
|
3935
|
+
console.log(pc3.cyan(`\u{1F680} Starting Webflow to ${target === "astro-vue" ? "Astro + Vue" : "Nuxt"} conversion...`));
|
|
2186
3936
|
console.log(pc3.dim(`Input: ${inputDir}`));
|
|
2187
3937
|
console.log(pc3.dim(`Output: ${outputDir}`));
|
|
2188
3938
|
try {
|
|
2189
|
-
await
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
throw new Error(`Input directory not found: ${inputDir}`);
|
|
2193
|
-
}
|
|
3939
|
+
const analysis = await analyzeWebflowExport(inputDir, config);
|
|
3940
|
+
await setupBoilerplate(boilerplate, outputDir, target);
|
|
3941
|
+
await writeSeeMSConfig(outputDir, config);
|
|
2194
3942
|
console.log(pc3.blue("\n\u{1F4C2} Scanning assets..."));
|
|
2195
|
-
const assets =
|
|
3943
|
+
const assets = analysis.assets;
|
|
2196
3944
|
console.log(pc3.green(` \u2713 Found ${assets.css.length} CSS files`));
|
|
2197
3945
|
console.log(pc3.green(` \u2713 Found ${assets.images.length} images`));
|
|
2198
3946
|
console.log(pc3.green(` \u2713 Found ${assets.fonts.length} fonts`));
|
|
@@ -2201,19 +3949,62 @@ async function convertWebflowExport(options) {
|
|
|
2201
3949
|
await copyAllAssets(inputDir, outputDir, assets);
|
|
2202
3950
|
console.log(pc3.green(" \u2713 Assets copied successfully"));
|
|
2203
3951
|
console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
|
|
2204
|
-
const htmlFiles =
|
|
3952
|
+
const htmlFiles = analysis.pages.map((page) => page.sourcePath);
|
|
2205
3953
|
console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
|
|
2206
3954
|
const htmlContentMap = /* @__PURE__ */ new Map();
|
|
3955
|
+
const originalHtmlContentMap = /* @__PURE__ */ new Map();
|
|
2207
3956
|
for (const htmlFile of htmlFiles) {
|
|
2208
3957
|
const html = await readHTMLFile(inputDir, htmlFile);
|
|
2209
|
-
const pageName = htmlFile
|
|
3958
|
+
const pageName = htmlPathToPageId(htmlFile);
|
|
2210
3959
|
htmlContentMap.set(pageName, html);
|
|
3960
|
+
originalHtmlContentMap.set(pageName, html);
|
|
2211
3961
|
console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
|
|
2212
3962
|
}
|
|
3963
|
+
console.log(pc3.blue("\n\u{1F9E9} Extracting shared components..."));
|
|
3964
|
+
const sharedComponents = config.components?.enabled === false ? [] : await extractSharedComponents(inputDir, outputDir, {
|
|
3965
|
+
minOccurrences: config.components?.minOccurrences,
|
|
3966
|
+
minPages: config.components?.minPages,
|
|
3967
|
+
minSectionSize: config.components?.minSectionSize,
|
|
3968
|
+
match: config.components?.match,
|
|
3969
|
+
writeConfidence: config.components?.writeConfidence,
|
|
3970
|
+
include: config.components?.include,
|
|
3971
|
+
exclude: config.components?.exclude,
|
|
3972
|
+
rules: config.components?.rules
|
|
3973
|
+
});
|
|
3974
|
+
const pageComponentMap = /* @__PURE__ */ new Map();
|
|
3975
|
+
if (sharedComponents.length > 0) {
|
|
3976
|
+
console.log(pc3.green(` \u2713 Extracted ${sharedComponents.length} shared components:`));
|
|
3977
|
+
for (const component of sharedComponents) {
|
|
3978
|
+
console.log(pc3.dim(` - ${component.name} (found in ${component.pages.length} pages)`));
|
|
3979
|
+
}
|
|
3980
|
+
for (const [pageName, html] of htmlContentMap.entries()) {
|
|
3981
|
+
const $ = cheerio6.load(html);
|
|
3982
|
+
let modified = false;
|
|
3983
|
+
const usedComponents = [];
|
|
3984
|
+
for (const component of sharedComponents) {
|
|
3985
|
+
if (component.role === "collection-item") {
|
|
3986
|
+
continue;
|
|
3987
|
+
}
|
|
3988
|
+
if (component.pages.includes(pageName)) {
|
|
3989
|
+
replaceWithComponent($, component.selector, component.name);
|
|
3990
|
+
usedComponents.push(component.name);
|
|
3991
|
+
modified = true;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
if (modified) {
|
|
3995
|
+
const serializedHtml = $.html();
|
|
3996
|
+
htmlContentMap.set(pageName, serializedHtml);
|
|
3997
|
+
pageComponentMap.set(pageName, usedComponents);
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
} else {
|
|
4001
|
+
console.log(pc3.dim(" No shared components detected across pages"));
|
|
4002
|
+
}
|
|
2213
4003
|
console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
|
|
2214
4004
|
let allEmbeddedStyles = "";
|
|
2215
4005
|
for (const htmlFile of htmlFiles) {
|
|
2216
|
-
const
|
|
4006
|
+
const pageName = htmlPathToPageId(htmlFile);
|
|
4007
|
+
const html = htmlContentMap.get(pageName);
|
|
2217
4008
|
const parsed = parseHTML(html, htmlFile);
|
|
2218
4009
|
if (parsed.embeddedStyles) {
|
|
2219
4010
|
allEmbeddedStyles += `
|
|
@@ -2221,16 +4012,31 @@ async function convertWebflowExport(options) {
|
|
|
2221
4012
|
${parsed.embeddedStyles}
|
|
2222
4013
|
`;
|
|
2223
4014
|
}
|
|
2224
|
-
const transformed = transformForNuxt(parsed.htmlContent);
|
|
2225
|
-
const
|
|
2226
|
-
const vueComponent = htmlToVueComponent(transformed, pageName);
|
|
2227
|
-
await writeVueComponent(outputDir, htmlFile, vueComponent);
|
|
2228
|
-
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
|
|
4015
|
+
const transformed = transformForNuxt(parsed.htmlContent, htmlFile);
|
|
4016
|
+
const componentImports = pageComponentMap.get(pageName);
|
|
4017
|
+
const vueComponent = htmlToVueComponent(transformed, pageName, componentImports);
|
|
4018
|
+
await writeVueComponent(outputDir, htmlFile, vueComponent, target, assets.css, editorEnabled);
|
|
4019
|
+
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", target === "astro-vue" ? ".astro + .vue" : ".vue")}`));
|
|
2229
4020
|
}
|
|
2230
|
-
await formatVueFiles(outputDir);
|
|
4021
|
+
await formatVueFiles(outputDir, target);
|
|
2231
4022
|
console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
|
|
2232
|
-
const pagesDir =
|
|
2233
|
-
const
|
|
4023
|
+
const pagesDir = target === "astro-vue" ? path16.join(outputDir, "src", "components", "pages") : path16.join(outputDir, "pages");
|
|
4024
|
+
const pageRoutes = Object.fromEntries(
|
|
4025
|
+
htmlFiles.map((htmlFile) => {
|
|
4026
|
+
const info = getPageRouteInfo(htmlFile);
|
|
4027
|
+
return [info.pageId, info.route];
|
|
4028
|
+
})
|
|
4029
|
+
);
|
|
4030
|
+
const manifest = await generateManifest(pagesDir, {
|
|
4031
|
+
collectionClasses,
|
|
4032
|
+
collectionNames,
|
|
4033
|
+
sharedComponents,
|
|
4034
|
+
componentsDir: path16.join(outputDir, "components"),
|
|
4035
|
+
ignoreSelectors: config.ignore?.selectors,
|
|
4036
|
+
ignoreClasses: config.ignore?.classes,
|
|
4037
|
+
provider,
|
|
4038
|
+
pageRoutes
|
|
4039
|
+
});
|
|
2234
4040
|
await writeManifest(outputDir, manifest);
|
|
2235
4041
|
const totalFields = Object.values(manifest.pages).reduce(
|
|
2236
4042
|
(sum, page) => sum + Object.keys(page.fields || {}).length,
|
|
@@ -2243,27 +4049,50 @@ ${parsed.embeddedStyles}
|
|
|
2243
4049
|
console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
|
|
2244
4050
|
console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
|
|
2245
4051
|
console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
|
|
4052
|
+
console.log(pc3.blue("\n\u{1F50C} Generating content runtime..."));
|
|
4053
|
+
if (target === "nuxt") {
|
|
4054
|
+
await createEditorContentComposable(outputDir);
|
|
4055
|
+
await createStrapiContentComposable(outputDir, manifest);
|
|
4056
|
+
await addStrapiUrlToConfig(outputDir);
|
|
4057
|
+
} else {
|
|
4058
|
+
await createAstroStrapiContentComposable(outputDir, manifest);
|
|
4059
|
+
}
|
|
4060
|
+
console.log(pc3.green(" \u2713 Content runtime generated"));
|
|
2246
4061
|
console.log(pc3.blue("\n\u26A1 Transforming Vue files to reactive templates..."));
|
|
2247
|
-
await transformAllVuePages(pagesDir, manifest);
|
|
4062
|
+
await transformAllVuePages(pagesDir, manifest, { target });
|
|
4063
|
+
await transformSharedComponentsToReactive(path16.join(outputDir, "components"), manifest, { target });
|
|
2248
4064
|
console.log(pc3.green(` \u2713 Transformed ${Object.keys(manifest.pages).length} pages to use Vue template syntax`));
|
|
2249
4065
|
console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
|
|
2250
4066
|
console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
|
|
2251
4067
|
console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
4068
|
+
let seedData = {};
|
|
4069
|
+
if (shouldGenerateContent) {
|
|
4070
|
+
const extractedContent = extractAllContent(originalHtmlContentMap, manifest);
|
|
4071
|
+
seedData = formatForStrapi(extractedContent);
|
|
4072
|
+
await writeSeedData(outputDir, seedData);
|
|
4073
|
+
await createSeedReadme(outputDir);
|
|
4074
|
+
}
|
|
4075
|
+
const pagesWithContent = Object.keys(manifest.pages).filter((key) => {
|
|
2257
4076
|
const data = seedData[key];
|
|
4077
|
+
if (!data) return false;
|
|
2258
4078
|
if (Array.isArray(data)) return data.length > 0;
|
|
2259
4079
|
return Object.keys(data).length > 0;
|
|
2260
4080
|
}).length;
|
|
2261
|
-
|
|
2262
|
-
|
|
4081
|
+
if (shouldGenerateContent) {
|
|
4082
|
+
console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
|
|
4083
|
+
console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
|
|
4084
|
+
} else {
|
|
4085
|
+
console.log(pc3.dim(" Skipped initial CMS content generation"));
|
|
4086
|
+
}
|
|
2263
4087
|
console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
2264
4088
|
const schemas = manifestToSchemas(manifest);
|
|
2265
4089
|
await writeAllSchemas(outputDir, schemas);
|
|
2266
4090
|
await createStrapiReadme(outputDir);
|
|
4091
|
+
const linkSchema = getLinkComponentSchema(manifest);
|
|
4092
|
+
if (linkSchema) {
|
|
4093
|
+
await writeLinkComponentSchema(outputDir);
|
|
4094
|
+
console.log(pc3.dim(" \u2713 Generated shared.link component schema"));
|
|
4095
|
+
}
|
|
2267
4096
|
console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
|
|
2268
4097
|
console.log(pc3.dim(" View schemas in: cms-schemas/"));
|
|
2269
4098
|
if (allEmbeddedStyles.trim()) {
|
|
@@ -2272,34 +4101,55 @@ ${parsed.embeddedStyles}
|
|
|
2272
4101
|
await writeEmbeddedStyles(outputDir, dedupedStyles);
|
|
2273
4102
|
console.log(pc3.green(" \u2713 Embedded styles added to main.css"));
|
|
2274
4103
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
4104
|
+
if (target === "nuxt") {
|
|
4105
|
+
console.log(pc3.blue("\n\u{1F527} Generating webflow-assets.ts plugin..."));
|
|
4106
|
+
await writeWebflowAssetPlugin(outputDir, assets.css);
|
|
4107
|
+
console.log(pc3.green(" \u2713 Plugin generated (existing file overwritten)"));
|
|
4108
|
+
console.log(pc3.blue("\n\u2699\uFE0F Updating nuxt.config.ts..."));
|
|
4109
|
+
try {
|
|
4110
|
+
await updateNuxtConfig(outputDir, assets.css);
|
|
4111
|
+
console.log(pc3.green(" \u2713 Config updated"));
|
|
4112
|
+
} catch (error) {
|
|
4113
|
+
console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
|
|
4114
|
+
console.log(pc3.dim(" Please add CSS files manually"));
|
|
4115
|
+
}
|
|
4116
|
+
} else {
|
|
4117
|
+
console.log(pc3.dim("\n\u{1F527} Skipped Nuxt asset plugin; Astro pages import CSS directly"));
|
|
4118
|
+
}
|
|
4119
|
+
if (editorEnabled) {
|
|
4120
|
+
console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
|
|
4121
|
+
if (target === "nuxt") {
|
|
4122
|
+
await createEditorPlugin(outputDir);
|
|
4123
|
+
await createSaveEndpoint(outputDir);
|
|
4124
|
+
await createPublishEndpoint(outputDir);
|
|
4125
|
+
console.log(pc3.green(" \u2713 Nuxt editor plugin created"));
|
|
4126
|
+
console.log(pc3.green(" \u2713 Nuxt save/publish endpoints created"));
|
|
4127
|
+
} else {
|
|
4128
|
+
await createAstroEditorClient(outputDir);
|
|
4129
|
+
await createAstroSaveEndpoint(outputDir);
|
|
4130
|
+
console.log(pc3.green(" \u2713 Astro editor client created"));
|
|
4131
|
+
console.log(pc3.green(" \u2713 Astro save/publish endpoints created"));
|
|
4132
|
+
}
|
|
4133
|
+
await addEditorDependency(outputDir);
|
|
4134
|
+
await createStrapiBootstrap(outputDir);
|
|
4135
|
+
console.log(pc3.green(" \u2713 Editor dependency added"));
|
|
4136
|
+
console.log(pc3.green(" \u2713 Strapi bootstrap file generated"));
|
|
4137
|
+
} else {
|
|
4138
|
+
console.log(pc3.dim("\n\u{1F3A8} Editor overlay disabled by config"));
|
|
4139
|
+
}
|
|
4140
|
+
const report = createConversionReport({
|
|
4141
|
+
analysis,
|
|
4142
|
+
provider,
|
|
4143
|
+
stages: ["scan", "analyze", "plan", "convert", "cms", ...editorEnabled ? ["editor"] : []],
|
|
4144
|
+
components: sharedComponents,
|
|
4145
|
+
fields: totalFields,
|
|
4146
|
+
collections: totalCollections,
|
|
4147
|
+
schemas: Object.keys(schemas).length,
|
|
4148
|
+
seedPages: pagesWithContent,
|
|
4149
|
+
warnings: []
|
|
4150
|
+
});
|
|
4151
|
+
await writeConversionReport(outputDir, report);
|
|
4152
|
+
console.log(pc3.green(" \u2713 Generated see-ms-report.md and see-ms-report.json"));
|
|
2303
4153
|
console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
|
|
2304
4154
|
console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
|
|
2305
4155
|
console.log(pc3.dim(` 1. cd ${outputDir}`));
|
|
@@ -2318,16 +4168,17 @@ ${parsed.embeddedStyles}
|
|
|
2318
4168
|
}
|
|
2319
4169
|
|
|
2320
4170
|
// src/strapi-setup.ts
|
|
2321
|
-
import
|
|
2322
|
-
import
|
|
2323
|
-
import { glob as
|
|
4171
|
+
import fs13 from "fs-extra";
|
|
4172
|
+
import path17 from "path";
|
|
4173
|
+
import { glob as glob5 } from "glob";
|
|
2324
4174
|
import * as readline from "readline";
|
|
4175
|
+
import { spawn } from "child_process";
|
|
2325
4176
|
var ENV_FILE = ".env";
|
|
2326
4177
|
async function loadConfig(projectDir) {
|
|
2327
|
-
const envPath =
|
|
2328
|
-
if (await
|
|
4178
|
+
const envPath = path17.join(projectDir, ENV_FILE);
|
|
4179
|
+
if (await fs13.pathExists(envPath)) {
|
|
2329
4180
|
try {
|
|
2330
|
-
const content = await
|
|
4181
|
+
const content = await fs13.readFile(envPath, "utf-8");
|
|
2331
4182
|
const config = {};
|
|
2332
4183
|
for (const line of content.split("\n")) {
|
|
2333
4184
|
const trimmed = line.trim();
|
|
@@ -2348,10 +4199,10 @@ async function loadConfig(projectDir) {
|
|
|
2348
4199
|
return {};
|
|
2349
4200
|
}
|
|
2350
4201
|
async function saveConfig(projectDir, config) {
|
|
2351
|
-
const envPath =
|
|
4202
|
+
const envPath = path17.join(projectDir, ENV_FILE);
|
|
2352
4203
|
let content = "";
|
|
2353
|
-
if (await
|
|
2354
|
-
content = await
|
|
4204
|
+
if (await fs13.pathExists(envPath)) {
|
|
4205
|
+
content = await fs13.readFile(envPath, "utf-8");
|
|
2355
4206
|
content = content.split("\n").filter((line) => !line.startsWith("STRAPI_API_TOKEN=") && !line.startsWith("STRAPI_URL=")).join("\n");
|
|
2356
4207
|
if (content && !content.endsWith("\n")) {
|
|
2357
4208
|
content += "\n";
|
|
@@ -2365,10 +4216,19 @@ async function saveConfig(projectDir, config) {
|
|
|
2365
4216
|
content += `STRAPI_API_TOKEN=${config.apiToken}
|
|
2366
4217
|
`;
|
|
2367
4218
|
}
|
|
2368
|
-
await
|
|
4219
|
+
await fs13.writeFile(envPath, content);
|
|
2369
4220
|
}
|
|
2370
4221
|
async function completeSetup(options) {
|
|
2371
4222
|
const { projectDir, strapiDir, strapiUrl: optionUrl, apiToken: optionToken, ignoreSavedToken } = options;
|
|
4223
|
+
if (!await fs13.pathExists(strapiDir)) {
|
|
4224
|
+
if (!options.scaffold) {
|
|
4225
|
+
throw new Error(`Strapi directory not found: ${strapiDir}`);
|
|
4226
|
+
}
|
|
4227
|
+
await scaffoldStrapiProject({
|
|
4228
|
+
strapiDir,
|
|
4229
|
+
...options.scaffoldOptions
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
2372
4232
|
const savedConfig = await loadConfig(projectDir);
|
|
2373
4233
|
const strapiUrl = optionUrl || savedConfig.strapiUrl || "http://localhost:1337";
|
|
2374
4234
|
console.log("\u{1F680} Starting complete Strapi setup...\n");
|
|
@@ -2408,7 +4268,7 @@ async function completeSetup(options) {
|
|
|
2408
4268
|
}
|
|
2409
4269
|
console.log("\u{1F4F8} Step 5: Uploading images...");
|
|
2410
4270
|
const mediaMap = await uploadAllImages(projectDir, strapiUrl, token);
|
|
2411
|
-
console.log(`\u2713
|
|
4271
|
+
console.log(`\u2713 Mapped ${mediaMap.size} media lookup keys
|
|
2412
4272
|
`);
|
|
2413
4273
|
console.log("\u{1F4DD} Step 6: Seeding content...");
|
|
2414
4274
|
await seedContent(projectDir, strapiUrl, token, mediaMap);
|
|
@@ -2419,21 +4279,94 @@ async function completeSetup(options) {
|
|
|
2419
4279
|
console.log(" 2. Check Content Manager - your content should be there!");
|
|
2420
4280
|
console.log(" 3. Connect your Nuxt app to Strapi API");
|
|
2421
4281
|
}
|
|
4282
|
+
async function scaffoldStrapiProject(options) {
|
|
4283
|
+
const {
|
|
4284
|
+
strapiDir,
|
|
4285
|
+
packageManager = "npm",
|
|
4286
|
+
install = true,
|
|
4287
|
+
run = false,
|
|
4288
|
+
gitInit = false,
|
|
4289
|
+
typescript = true
|
|
4290
|
+
} = options;
|
|
4291
|
+
const resolvedDir = path17.resolve(strapiDir);
|
|
4292
|
+
if (await fs13.pathExists(resolvedDir)) {
|
|
4293
|
+
const entries = await fs13.readdir(resolvedDir);
|
|
4294
|
+
if (entries.length > 0) {
|
|
4295
|
+
throw new Error(`Cannot scaffold Strapi into a non-empty directory: ${resolvedDir}`);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
await fs13.ensureDir(path17.dirname(resolvedDir));
|
|
4299
|
+
const args = [
|
|
4300
|
+
"create-strapi@latest",
|
|
4301
|
+
resolvedDir,
|
|
4302
|
+
typescript ? "--typescript" : "--javascript",
|
|
4303
|
+
"--skip-cloud",
|
|
4304
|
+
"--skip-db",
|
|
4305
|
+
"--no-example",
|
|
4306
|
+
install ? "--install" : "--no-install",
|
|
4307
|
+
gitInit ? "--git-init" : "--no-git-init",
|
|
4308
|
+
`--use-${packageManager}`
|
|
4309
|
+
];
|
|
4310
|
+
if (!run) {
|
|
4311
|
+
args.push("--no-run");
|
|
4312
|
+
}
|
|
4313
|
+
console.log("\u{1F3D7}\uFE0F Scaffolding Strapi project...");
|
|
4314
|
+
console.log(` npx ${args.join(" ")}`);
|
|
4315
|
+
await runCommand("npx", args, process.cwd());
|
|
4316
|
+
if (!await fs13.pathExists(path17.join(resolvedDir, "package.json"))) {
|
|
4317
|
+
throw new Error(`Strapi scaffold did not create a project at ${resolvedDir}`);
|
|
4318
|
+
}
|
|
4319
|
+
console.log(`\u2713 Strapi project scaffolded at ${resolvedDir}`);
|
|
4320
|
+
}
|
|
4321
|
+
function runCommand(command, args, cwd) {
|
|
4322
|
+
return new Promise((resolve, reject) => {
|
|
4323
|
+
const child = spawn(command, args, {
|
|
4324
|
+
cwd,
|
|
4325
|
+
stdio: "inherit",
|
|
4326
|
+
shell: process.platform === "win32"
|
|
4327
|
+
});
|
|
4328
|
+
child.on("error", reject);
|
|
4329
|
+
child.on("close", (code) => {
|
|
4330
|
+
if (code === 0) {
|
|
4331
|
+
resolve();
|
|
4332
|
+
} else {
|
|
4333
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
4334
|
+
}
|
|
4335
|
+
});
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
2422
4338
|
async function installSchemas(projectDir, strapiDir) {
|
|
2423
|
-
if (!await
|
|
4339
|
+
if (!await fs13.pathExists(strapiDir)) {
|
|
2424
4340
|
console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
|
|
2425
|
-
console.error(` Resolved to: ${
|
|
4341
|
+
console.error(` Resolved to: ${path17.resolve(strapiDir)}`);
|
|
2426
4342
|
throw new Error(`Strapi directory not found: ${strapiDir}`);
|
|
2427
4343
|
}
|
|
2428
|
-
const packageJsonPath =
|
|
2429
|
-
if (await
|
|
2430
|
-
const pkg = await
|
|
4344
|
+
const packageJsonPath = path17.join(strapiDir, "package.json");
|
|
4345
|
+
if (await fs13.pathExists(packageJsonPath)) {
|
|
4346
|
+
const pkg = await fs13.readJson(packageJsonPath);
|
|
2431
4347
|
if (!pkg.dependencies?.["@strapi/strapi"]) {
|
|
2432
4348
|
console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
|
|
2433
4349
|
}
|
|
2434
4350
|
}
|
|
2435
|
-
const schemaDir =
|
|
2436
|
-
const
|
|
4351
|
+
const schemaDir = path17.join(projectDir, "cms-schemas");
|
|
4352
|
+
const componentsDir = path17.join(schemaDir, "components");
|
|
4353
|
+
if (await fs13.pathExists(componentsDir)) {
|
|
4354
|
+
const componentFiles = await glob5("**/*.json", {
|
|
4355
|
+
cwd: componentsDir,
|
|
4356
|
+
absolute: false
|
|
4357
|
+
});
|
|
4358
|
+
if (componentFiles.length > 0) {
|
|
4359
|
+
console.log(` Found ${componentFiles.length} component(s)`);
|
|
4360
|
+
for (const file of componentFiles) {
|
|
4361
|
+
const sourcePath = path17.join(componentsDir, file);
|
|
4362
|
+
const targetPath = path17.join(strapiDir, "src", "components", file);
|
|
4363
|
+
await fs13.ensureDir(path17.dirname(targetPath));
|
|
4364
|
+
await fs13.copy(sourcePath, targetPath);
|
|
4365
|
+
console.log(` \u2713 Component: ${file}`);
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
const schemaFiles = await glob5("*.json", {
|
|
2437
4370
|
cwd: schemaDir,
|
|
2438
4371
|
absolute: false
|
|
2439
4372
|
});
|
|
@@ -2443,42 +4376,42 @@ async function installSchemas(projectDir, strapiDir) {
|
|
|
2443
4376
|
}
|
|
2444
4377
|
console.log(` Found ${schemaFiles.length} schema file(s)`);
|
|
2445
4378
|
for (const file of schemaFiles) {
|
|
2446
|
-
const schemaPath =
|
|
2447
|
-
const schema = await
|
|
2448
|
-
const singularName = schema.info?.singularName ||
|
|
4379
|
+
const schemaPath = path17.join(schemaDir, file);
|
|
4380
|
+
const schema = await fs13.readJson(schemaPath);
|
|
4381
|
+
const singularName = schema.info?.singularName || path17.basename(file, ".json");
|
|
2449
4382
|
console.log(` Installing ${singularName}...`);
|
|
2450
4383
|
try {
|
|
2451
|
-
const apiPath =
|
|
2452
|
-
const contentTypesPath =
|
|
4384
|
+
const apiPath = path17.join(strapiDir, "src", "api", singularName);
|
|
4385
|
+
const contentTypesPath = path17.join(
|
|
2453
4386
|
apiPath,
|
|
2454
4387
|
"content-types",
|
|
2455
4388
|
singularName
|
|
2456
4389
|
);
|
|
2457
|
-
const targetPath =
|
|
2458
|
-
await
|
|
2459
|
-
await
|
|
2460
|
-
await
|
|
2461
|
-
await
|
|
2462
|
-
await
|
|
4390
|
+
const targetPath = path17.join(contentTypesPath, "schema.json");
|
|
4391
|
+
await fs13.ensureDir(contentTypesPath);
|
|
4392
|
+
await fs13.ensureDir(path17.join(apiPath, "routes"));
|
|
4393
|
+
await fs13.ensureDir(path17.join(apiPath, "controllers"));
|
|
4394
|
+
await fs13.ensureDir(path17.join(apiPath, "services"));
|
|
4395
|
+
await fs13.writeJson(targetPath, schema, { spaces: 2 });
|
|
2463
4396
|
const routeContent = `import { factories } from '@strapi/strapi';
|
|
2464
4397
|
export default factories.createCoreRouter('api::${singularName}.${singularName}');
|
|
2465
4398
|
`;
|
|
2466
|
-
await
|
|
2467
|
-
|
|
4399
|
+
await fs13.writeFile(
|
|
4400
|
+
path17.join(apiPath, "routes", `${singularName}.ts`),
|
|
2468
4401
|
routeContent
|
|
2469
4402
|
);
|
|
2470
4403
|
const controllerContent = `import { factories } from '@strapi/strapi';
|
|
2471
4404
|
export default factories.createCoreController('api::${singularName}.${singularName}');
|
|
2472
4405
|
`;
|
|
2473
|
-
await
|
|
2474
|
-
|
|
4406
|
+
await fs13.writeFile(
|
|
4407
|
+
path17.join(apiPath, "controllers", `${singularName}.ts`),
|
|
2475
4408
|
controllerContent
|
|
2476
4409
|
);
|
|
2477
4410
|
const serviceContent = `import { factories } from '@strapi/strapi';
|
|
2478
4411
|
export default factories.createCoreService('api::${singularName}.${singularName}');
|
|
2479
4412
|
`;
|
|
2480
|
-
await
|
|
2481
|
-
|
|
4413
|
+
await fs13.writeFile(
|
|
4414
|
+
path17.join(apiPath, "services", `${singularName}.ts`),
|
|
2482
4415
|
serviceContent
|
|
2483
4416
|
);
|
|
2484
4417
|
} catch (error) {
|
|
@@ -2561,12 +4494,12 @@ async function getExistingMedia(strapiUrl, apiToken) {
|
|
|
2561
4494
|
}
|
|
2562
4495
|
async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
2563
4496
|
const mediaMap = /* @__PURE__ */ new Map();
|
|
2564
|
-
const imagesDir =
|
|
2565
|
-
if (!await
|
|
4497
|
+
const imagesDir = path17.join(projectDir, "public", "assets", "images");
|
|
4498
|
+
if (!await fs13.pathExists(imagesDir)) {
|
|
2566
4499
|
console.log(" No images directory found");
|
|
2567
4500
|
return mediaMap;
|
|
2568
4501
|
}
|
|
2569
|
-
const imageFiles = await
|
|
4502
|
+
const imageFiles = await glob5("**/*.{jpg,jpeg,png,gif,webp,avif,svg}", {
|
|
2570
4503
|
cwd: imagesDir,
|
|
2571
4504
|
absolute: false
|
|
2572
4505
|
});
|
|
@@ -2576,19 +4509,17 @@ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
|
2576
4509
|
let skippedCount = 0;
|
|
2577
4510
|
console.log(` Processing ${imageFiles.length} images...`);
|
|
2578
4511
|
for (const imageFile of imageFiles) {
|
|
2579
|
-
const fileName =
|
|
4512
|
+
const fileName = path17.basename(imageFile);
|
|
2580
4513
|
const existingId = existingMedia.get(fileName);
|
|
2581
4514
|
if (existingId) {
|
|
2582
|
-
mediaMap
|
|
2583
|
-
mediaMap.set(imageFile, existingId);
|
|
4515
|
+
addMediaMapEntries(mediaMap, imageFile, existingId);
|
|
2584
4516
|
skippedCount++;
|
|
2585
4517
|
continue;
|
|
2586
4518
|
}
|
|
2587
|
-
const imagePath =
|
|
4519
|
+
const imagePath = path17.join(imagesDir, imageFile);
|
|
2588
4520
|
const mediaId = await uploadImage(imagePath, imageFile, strapiUrl, apiToken);
|
|
2589
4521
|
if (mediaId) {
|
|
2590
|
-
mediaMap
|
|
2591
|
-
mediaMap.set(imageFile, mediaId);
|
|
4522
|
+
addMediaMapEntries(mediaMap, imageFile, mediaId);
|
|
2592
4523
|
uploadedCount++;
|
|
2593
4524
|
console.log(` \u2713 ${imageFile}`);
|
|
2594
4525
|
}
|
|
@@ -2598,7 +4529,7 @@ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
|
2598
4529
|
}
|
|
2599
4530
|
async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
|
|
2600
4531
|
try {
|
|
2601
|
-
const fileBuffer = await
|
|
4532
|
+
const fileBuffer = await fs13.readFile(filePath);
|
|
2602
4533
|
const mimeType = getMimeType(fileName);
|
|
2603
4534
|
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
2604
4535
|
const formData = new globalThis.FormData();
|
|
@@ -2625,30 +4556,31 @@ async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
|
|
|
2625
4556
|
}
|
|
2626
4557
|
}
|
|
2627
4558
|
function getMimeType(fileName) {
|
|
2628
|
-
const ext =
|
|
4559
|
+
const ext = path17.extname(fileName).toLowerCase();
|
|
2629
4560
|
const mimeTypes = {
|
|
2630
4561
|
".jpg": "image/jpeg",
|
|
2631
4562
|
".jpeg": "image/jpeg",
|
|
2632
4563
|
".png": "image/png",
|
|
2633
4564
|
".gif": "image/gif",
|
|
2634
4565
|
".webp": "image/webp",
|
|
4566
|
+
".avif": "image/avif",
|
|
2635
4567
|
".svg": "image/svg+xml"
|
|
2636
4568
|
};
|
|
2637
4569
|
return mimeTypes[ext] || "application/octet-stream";
|
|
2638
4570
|
}
|
|
2639
4571
|
async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
|
|
2640
|
-
const seedPath =
|
|
2641
|
-
if (!await
|
|
4572
|
+
const seedPath = path17.join(projectDir, "cms-seed", "seed-data.json");
|
|
4573
|
+
if (!await fs13.pathExists(seedPath)) {
|
|
2642
4574
|
console.log(" No seed data found");
|
|
2643
4575
|
return;
|
|
2644
4576
|
}
|
|
2645
|
-
const seedData = await
|
|
2646
|
-
const schemasDir =
|
|
4577
|
+
const seedData = await fs13.readJson(seedPath);
|
|
4578
|
+
const schemasDir = path17.join(projectDir, "cms-schemas");
|
|
2647
4579
|
const schemas = /* @__PURE__ */ new Map();
|
|
2648
|
-
const schemaFiles = await
|
|
4580
|
+
const schemaFiles = await glob5("*.json", { cwd: schemasDir });
|
|
2649
4581
|
for (const file of schemaFiles) {
|
|
2650
|
-
const schema = await
|
|
2651
|
-
const name =
|
|
4582
|
+
const schema = await fs13.readJson(path17.join(schemasDir, file));
|
|
4583
|
+
const name = path17.basename(file, ".json");
|
|
2652
4584
|
schemas.set(name, schema);
|
|
2653
4585
|
}
|
|
2654
4586
|
let successCount = 0;
|
|
@@ -2693,12 +4625,12 @@ function processMediaFields(data, mediaMap) {
|
|
|
2693
4625
|
const processed = {};
|
|
2694
4626
|
for (const [key, value] of Object.entries(data)) {
|
|
2695
4627
|
if (typeof value === "string") {
|
|
2696
|
-
if (key.includes("image") || key.includes("bg") || value.startsWith("/images/")) {
|
|
2697
|
-
const mediaId = mediaMap
|
|
4628
|
+
if (key.includes("image") || key.includes("img") || key.includes("bg") || value.startsWith("/images/") || value.startsWith("images/") || value.startsWith("/assets/images/") || value.startsWith("assets/images/") || isLikelyImagePath(value)) {
|
|
4629
|
+
const mediaId = findMediaId(mediaMap, value);
|
|
2698
4630
|
if (mediaId) {
|
|
2699
4631
|
processed[key] = mediaId;
|
|
2700
4632
|
} else {
|
|
2701
|
-
processed[key] =
|
|
4633
|
+
processed[key] = null;
|
|
2702
4634
|
}
|
|
2703
4635
|
} else {
|
|
2704
4636
|
processed[key] = value;
|
|
@@ -2709,6 +4641,18 @@ function processMediaFields(data, mediaMap) {
|
|
|
2709
4641
|
}
|
|
2710
4642
|
return processed;
|
|
2711
4643
|
}
|
|
4644
|
+
function addMediaMapEntries(mediaMap, imageFile, mediaId) {
|
|
4645
|
+
for (const key of mediaLookupKeys(imageFile)) {
|
|
4646
|
+
mediaMap.set(key, mediaId);
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
function findMediaId(mediaMap, value) {
|
|
4650
|
+
for (const key of mediaLookupKeys(value)) {
|
|
4651
|
+
const mediaId = mediaMap.get(key);
|
|
4652
|
+
if (mediaId) return mediaId;
|
|
4653
|
+
}
|
|
4654
|
+
return void 0;
|
|
4655
|
+
}
|
|
2712
4656
|
async function createEntry(contentType, data, strapiUrl, apiToken) {
|
|
2713
4657
|
try {
|
|
2714
4658
|
const response = await fetch(`${strapiUrl}/api/${contentType}`, {
|
|
@@ -2799,77 +4743,452 @@ async function prompt(question) {
|
|
|
2799
4743
|
});
|
|
2800
4744
|
});
|
|
2801
4745
|
}
|
|
2802
|
-
async function confirm(question) {
|
|
2803
|
-
const
|
|
4746
|
+
async function confirm(question, defaultYes = true) {
|
|
4747
|
+
const hint = defaultYes ? "(Y/n)" : "(y/N)";
|
|
4748
|
+
const answer = await prompt(`${question} ${hint}: `);
|
|
4749
|
+
if (answer === "") return defaultYes;
|
|
2804
4750
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
2805
4751
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
4752
|
+
function classToCollectionName(className) {
|
|
4753
|
+
let name = className.replace(/^c[-_]/, "").replace(/^cc[-_]/, "").replace(/[-_]card$/, "").replace(/[-_]item$/, "").replace(/[-_]wrapper$/, "");
|
|
4754
|
+
name = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase();
|
|
4755
|
+
if (!name.endsWith("s")) {
|
|
4756
|
+
name += "s";
|
|
4757
|
+
}
|
|
4758
|
+
return name;
|
|
4759
|
+
}
|
|
4760
|
+
function toPackageManager(value) {
|
|
4761
|
+
if (value === "pnpm" || value === "yarn") return value;
|
|
4762
|
+
return "npm";
|
|
4763
|
+
}
|
|
4764
|
+
async function select(question, choices, defaultValue) {
|
|
4765
|
+
console.log(question);
|
|
4766
|
+
choices.forEach((choice, index2) => {
|
|
4767
|
+
const marker = choice.value === defaultValue ? " default" : "";
|
|
4768
|
+
console.log(pc4.dim(` ${index2 + 1}. ${choice.label}${marker}`));
|
|
4769
|
+
});
|
|
4770
|
+
const answer = await prompt(pc4.cyan(" > "));
|
|
4771
|
+
if (!answer) return defaultValue;
|
|
4772
|
+
const index = Number(answer) - 1;
|
|
4773
|
+
if (Number.isInteger(index) && choices[index]) return choices[index].value;
|
|
4774
|
+
const direct = choices.find((choice) => choice.value === answer || choice.label.toLowerCase() === answer.toLowerCase());
|
|
4775
|
+
return direct?.value || defaultValue;
|
|
4776
|
+
}
|
|
4777
|
+
function toProjectTarget(value) {
|
|
4778
|
+
return value === "astro-vue" || value === "astro" ? "astro-vue" : "nuxt";
|
|
4779
|
+
}
|
|
4780
|
+
async function promptForCollections() {
|
|
4781
|
+
console.log("");
|
|
4782
|
+
console.log(pc4.cyan("\u{1F4CB} Collection Types Configuration"));
|
|
4783
|
+
console.log(pc4.dim(" Collections are repeating items like blog posts, team members, FAQs, etc."));
|
|
4784
|
+
console.log("");
|
|
4785
|
+
const classesInput = await prompt(
|
|
4786
|
+
pc4.white("Enter collection element classes (comma-separated, or press enter to skip):\n") + pc4.dim(" Example: c-blogpost, team-member_card, c-faq-item\n") + pc4.cyan(" > ")
|
|
4787
|
+
);
|
|
4788
|
+
if (!classesInput) {
|
|
4789
|
+
return [];
|
|
4790
|
+
}
|
|
4791
|
+
const classes = classesInput.split(",").map((c) => c.trim()).filter(Boolean);
|
|
4792
|
+
const collections = [];
|
|
4793
|
+
console.log("");
|
|
4794
|
+
console.log(pc4.dim(" Now let's name each collection type:"));
|
|
4795
|
+
console.log("");
|
|
4796
|
+
for (const className of classes) {
|
|
4797
|
+
const suggestedName = classToCollectionName(className);
|
|
4798
|
+
const nameInput = await prompt(
|
|
4799
|
+
pc4.white(` "${className}" \u2192 Collection name `) + pc4.dim(`(default: ${suggestedName}): `)
|
|
4800
|
+
);
|
|
4801
|
+
collections.push({
|
|
4802
|
+
className,
|
|
4803
|
+
collectionName: nameInput || suggestedName
|
|
4804
|
+
});
|
|
4805
|
+
}
|
|
4806
|
+
return collections;
|
|
4807
|
+
}
|
|
4808
|
+
async function promptForComponentRules(existing = []) {
|
|
4809
|
+
console.log("");
|
|
4810
|
+
console.log(pc4.cyan("\u{1F9E9} Component Rules"));
|
|
4811
|
+
console.log(pc4.dim(" Add named selectors for reusable blocks you definitely want componentized."));
|
|
4812
|
+
console.log(pc4.dim(" Example: AnnouncementBar = .quantum-zenith-design-system--c-announcement"));
|
|
4813
|
+
console.log("");
|
|
4814
|
+
const rules = [...existing];
|
|
4815
|
+
if (rules.length > 0) {
|
|
4816
|
+
console.log(pc4.dim(`Using ${rules.length} existing component rule(s) from config.`));
|
|
4817
|
+
}
|
|
4818
|
+
const addRules = await confirm(pc4.white("Add a component rule by name and selector?"), false);
|
|
4819
|
+
if (!addRules) return rules;
|
|
4820
|
+
while (true) {
|
|
4821
|
+
const name = await prompt(pc4.cyan(" Component name: "));
|
|
4822
|
+
if (!name) break;
|
|
4823
|
+
const selector = await prompt(pc4.cyan(" CSS selector: "));
|
|
4824
|
+
if (!selector) break;
|
|
4825
|
+
const minPagesInput = await prompt(pc4.cyan(" Minimum pages (2): "));
|
|
4826
|
+
const minPages = Number(minPagesInput) || 2;
|
|
4827
|
+
const role = await select(pc4.cyan(" What kind of component is this?"), [
|
|
4828
|
+
{ label: "Shared section/block", value: "shared-section" },
|
|
4829
|
+
{ label: "Repeated item rendered with v-for", value: "collection-item" }
|
|
4830
|
+
], "shared-section");
|
|
4831
|
+
let collectionName;
|
|
4832
|
+
let collectionStorage;
|
|
4833
|
+
if (role === "collection-item") {
|
|
4834
|
+
const suggestedCollection = classToCollectionName(name);
|
|
4835
|
+
const collectionInput = await prompt(
|
|
4836
|
+
pc4.cyan(` Collection name (${suggestedCollection}): `)
|
|
4837
|
+
);
|
|
4838
|
+
collectionName = collectionInput || suggestedCollection;
|
|
4839
|
+
collectionStorage = await select(pc4.cyan(" How should these repeated items be stored?"), [
|
|
4840
|
+
{ label: "Strapi collection type", value: "collection-type" },
|
|
4841
|
+
{ label: "Page repeatable field (coming next)", value: "page-repeatable" },
|
|
4842
|
+
{ label: "Global repeatable field (coming next)", value: "global-repeatable" }
|
|
4843
|
+
], "collection-type");
|
|
4844
|
+
if (collectionStorage !== "collection-type") {
|
|
4845
|
+
console.log(pc4.yellow(" Repeatable fields are not fully wired yet; using Strapi collection type for this run."));
|
|
4846
|
+
collectionStorage = "collection-type";
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
const contentMode = await select(pc4.cyan(" How should this component's editable content be stored?"), [
|
|
4850
|
+
{ label: "Auto/default: shared global for now", value: "auto" },
|
|
4851
|
+
{ label: "Shared global content, same everywhere", value: "shared-global" },
|
|
4852
|
+
{ label: "Per-page instances, same structure with different content", value: "per-page" }
|
|
4853
|
+
], "auto");
|
|
4854
|
+
rules.push({
|
|
4855
|
+
name,
|
|
4856
|
+
selector,
|
|
4857
|
+
role,
|
|
4858
|
+
collectionName,
|
|
4859
|
+
collectionStorage,
|
|
4860
|
+
contentMode,
|
|
4861
|
+
minOccurrences: minPages,
|
|
4862
|
+
minPages
|
|
4863
|
+
});
|
|
4864
|
+
const another = await confirm(pc4.white(" Add another component rule?"), false);
|
|
4865
|
+
if (!another) break;
|
|
4866
|
+
}
|
|
4867
|
+
return rules;
|
|
4868
|
+
}
|
|
4869
|
+
program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.3");
|
|
4870
|
+
program.command("convert").description("Convert Webflow export to a framework project with CMS integration").argument("[input]", "Path to Webflow export directory").argument("[output]", "Path to output project directory").option("--target <target>", "Output target (nuxt|astro-vue)").option(
|
|
2808
4871
|
"-b, --boilerplate <source>",
|
|
2809
4872
|
"Boilerplate source (GitHub URL or local path)"
|
|
2810
|
-
).option("-o, --overrides <path>", "Path to overrides JSON file").option("--
|
|
4873
|
+
).option("-o, --overrides <path>", "Path to overrides JSON file").option("--config <path>", "Path to see-ms config file").option(
|
|
2811
4874
|
"--cms <type>",
|
|
2812
|
-
"CMS backend type (strapi|contentful|sanity)"
|
|
2813
|
-
|
|
2814
|
-
).option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
|
|
4875
|
+
"CMS backend type (strapi|contentful|sanity)"
|
|
4876
|
+
).option("--skip-prompts", "Skip interactive prompts (for CI/CD)").option("--collection-classes <classes>", "Comma-separated collection class patterns").option("--no-content", "Skip generating initial CMS content").option("--no-editor", "Skip installing and wiring the inline editor").option("--scaffold-strapi <dir>", "Scaffold a new Strapi project after conversion").option("--strapi-dir <dir>", "Existing Strapi project to set up after conversion").option("--strapi-package-manager <manager>", "Package manager for new Strapi project (npm|pnpm|yarn)", "npm").option("--no-strapi-install", "Scaffold Strapi without installing dependencies").action(async (input, output, options) => {
|
|
2815
4877
|
try {
|
|
4878
|
+
const skipPrompts = options.skipPrompts || false;
|
|
4879
|
+
if (!input) {
|
|
4880
|
+
if (skipPrompts) throw new Error("Input path is required when --skip-prompts is used");
|
|
4881
|
+
input = await prompt(pc4.cyan("\u{1F4C1} Webflow export directory: "));
|
|
4882
|
+
}
|
|
4883
|
+
if (!output) {
|
|
4884
|
+
if (skipPrompts) throw new Error("Output project directory is required when --skip-prompts is used");
|
|
4885
|
+
const defaultOutput = path18.resolve(process.cwd(), `${path18.basename(path18.resolve(input))}-seems`);
|
|
4886
|
+
const answer = await prompt(pc4.cyan(`\u{1F4C1} Output project directory (${defaultOutput}): `));
|
|
4887
|
+
output = answer || defaultOutput;
|
|
4888
|
+
}
|
|
4889
|
+
console.log("");
|
|
4890
|
+
console.log(pc4.cyan(pc4.bold("\u{1F680} SeeMS Converter")));
|
|
4891
|
+
console.log(pc4.dim(` Converting: ${input} \u2192 ${output}`));
|
|
4892
|
+
console.log("");
|
|
4893
|
+
const loadedConfig = options.config ? await loadSeeMSConfig(options.config) : {};
|
|
4894
|
+
let collections = (loadedConfig.collections || []).map((collection) => ({
|
|
4895
|
+
className: collection.className,
|
|
4896
|
+
collectionName: collection.name || classToCollectionName(collection.className)
|
|
4897
|
+
}));
|
|
4898
|
+
let generateContent = true;
|
|
4899
|
+
let enableEditor = options.editor !== false && loadedConfig.editor?.enabled !== false;
|
|
4900
|
+
let target = toProjectTarget(options.target || loadedConfig.target);
|
|
4901
|
+
let cmsProvider = options.cms || loadedConfig.cms?.provider || "strapi";
|
|
4902
|
+
let detectComponents = loadedConfig.components?.enabled !== false;
|
|
4903
|
+
let componentMatch = loadedConfig.components?.match || "structure";
|
|
4904
|
+
let componentMinOccurrences = loadedConfig.components?.minOccurrences || 2;
|
|
4905
|
+
let componentMinPages = loadedConfig.components?.minPages || componentMinOccurrences;
|
|
4906
|
+
let componentRules = loadedConfig.components?.rules || [];
|
|
4907
|
+
if (!skipPrompts) {
|
|
4908
|
+
target = toProjectTarget(await select(pc4.cyan("\u{1F3AF} What are you converting to?"), [
|
|
4909
|
+
{ label: "Nuxt 3", value: "nuxt" },
|
|
4910
|
+
{ label: "Astro + Vue", value: "astro-vue" }
|
|
4911
|
+
], target));
|
|
4912
|
+
cmsProvider = await select(pc4.cyan("\u{1F9E0} Which CMS provider?"), [
|
|
4913
|
+
{ label: "Strapi", value: "strapi" }
|
|
4914
|
+
], cmsProvider);
|
|
4915
|
+
detectComponents = await confirm(pc4.cyan("Extract shared components from repeated sections?"), detectComponents);
|
|
4916
|
+
if (detectComponents) {
|
|
4917
|
+
componentMatch = await select(pc4.cyan("\u{1F9E9} How strict should component matching be?"), [
|
|
4918
|
+
{ label: "Exact repeated HTML blocks", value: "exact" },
|
|
4919
|
+
{ label: "Matching DOM structure", value: "structure" }
|
|
4920
|
+
], componentMatch);
|
|
4921
|
+
componentMinOccurrences = Number(await prompt(pc4.cyan(`Minimum total occurrences (${componentMinOccurrences}): `))) || componentMinOccurrences;
|
|
4922
|
+
componentMinPages = Number(await prompt(pc4.cyan(`Minimum pages (${componentMinPages}): `))) || componentMinPages;
|
|
4923
|
+
componentRules = await promptForComponentRules(componentRules);
|
|
4924
|
+
}
|
|
4925
|
+
if (options.collectionClasses) {
|
|
4926
|
+
const classes = options.collectionClasses.split(",").map((c) => c.trim());
|
|
4927
|
+
collections = classes.map((className) => ({
|
|
4928
|
+
className,
|
|
4929
|
+
collectionName: classToCollectionName(className)
|
|
4930
|
+
}));
|
|
4931
|
+
} else if (collections.length === 0) {
|
|
4932
|
+
collections = await promptForCollections();
|
|
4933
|
+
} else {
|
|
4934
|
+
console.log(pc4.dim(`Using ${collections.length} collection hint(s) from config.`));
|
|
4935
|
+
}
|
|
4936
|
+
console.log("");
|
|
4937
|
+
const previewConfig = normalizeConfig(mergeConfig(loadedConfig, {
|
|
4938
|
+
collections: collections.map((collection) => ({
|
|
4939
|
+
className: collection.className,
|
|
4940
|
+
name: collection.collectionName
|
|
4941
|
+
}))
|
|
4942
|
+
}));
|
|
4943
|
+
const analysis = await analyzeWebflowExport(input, previewConfig);
|
|
4944
|
+
console.log(pc4.cyan("\u{1F50E} Analysis Preview"));
|
|
4945
|
+
console.log(pc4.dim(` \u2022 Pages: ${analysis.pages.length}`));
|
|
4946
|
+
analysis.pages.slice(0, 8).forEach((page) => {
|
|
4947
|
+
console.log(pc4.dim(` - ${page.sourcePath} \u2192 ${page.route}`));
|
|
4948
|
+
});
|
|
4949
|
+
if (analysis.pages.length > 8) {
|
|
4950
|
+
console.log(pc4.dim(` \u2026 ${analysis.pages.length - 8} more`));
|
|
4951
|
+
}
|
|
4952
|
+
console.log(pc4.dim(` \u2022 Component candidates: ${analysis.componentCandidates.length}`));
|
|
4953
|
+
analysis.componentCandidates.slice(0, 8).forEach((component) => {
|
|
4954
|
+
console.log(pc4.dim(` - ${component.name} (${component.confidence}) on ${component.pages.length} pages`));
|
|
4955
|
+
});
|
|
4956
|
+
generateContent = await confirm(
|
|
4957
|
+
pc4.white("Generate initial CMS content from HTML?")
|
|
4958
|
+
);
|
|
4959
|
+
enableEditor = await confirm(
|
|
4960
|
+
pc4.white("Install and wire the inline editor overlay?"),
|
|
4961
|
+
true
|
|
4962
|
+
);
|
|
4963
|
+
} else if (options.collectionClasses) {
|
|
4964
|
+
const classes = options.collectionClasses.split(",").map((c) => c.trim());
|
|
4965
|
+
collections = classes.map((className) => ({
|
|
4966
|
+
className,
|
|
4967
|
+
collectionName: classToCollectionName(className)
|
|
4968
|
+
}));
|
|
4969
|
+
}
|
|
4970
|
+
console.log("");
|
|
4971
|
+
console.log(pc4.green("\u2713 Configuration:"));
|
|
4972
|
+
console.log(pc4.dim(` \u2022 Target: ${target === "astro-vue" ? "Astro + Vue" : "Nuxt 3"}`));
|
|
4973
|
+
console.log(pc4.dim(` \u2022 CMS: ${cmsProvider}`));
|
|
4974
|
+
console.log(pc4.dim(` \u2022 Component matching: ${componentMatch}, ${componentMinOccurrences}+ occurrences on ${componentMinPages}+ pages`));
|
|
4975
|
+
if (componentRules.length > 0) {
|
|
4976
|
+
console.log(pc4.dim(` \u2022 Component rules: ${componentRules.map((rule) => {
|
|
4977
|
+
const role = rule.role || "shared-section";
|
|
4978
|
+
const contentMode = rule.contentMode || "auto";
|
|
4979
|
+
const collection = rule.role === "collection-item" && rule.collectionName ? `, collection: ${rule.collectionName}` : "";
|
|
4980
|
+
return `${rule.name} (${rule.selector}, ${role}, ${contentMode}${collection})`;
|
|
4981
|
+
}).join(", ")}`));
|
|
4982
|
+
}
|
|
4983
|
+
if (collections.length > 0) {
|
|
4984
|
+
console.log(pc4.dim(` \u2022 Collections: ${collections.map((c) => `${c.className} \u2192 ${c.collectionName}`).join(", ")}`));
|
|
4985
|
+
} else {
|
|
4986
|
+
console.log(pc4.dim(" \u2022 Collections: none (auto-detect disabled)"));
|
|
4987
|
+
}
|
|
4988
|
+
console.log(pc4.dim(` \u2022 Generate content: ${generateContent}`));
|
|
4989
|
+
console.log(pc4.dim(` \u2022 Inline editor: ${enableEditor ? "enabled" : "disabled"}`));
|
|
4990
|
+
console.log("");
|
|
4991
|
+
console.log(pc4.blue("\u{1F4E6} Running conversion..."));
|
|
4992
|
+
console.log("");
|
|
4993
|
+
const cliConfig = {
|
|
4994
|
+
target,
|
|
4995
|
+
cms: { provider: cmsProvider },
|
|
4996
|
+
collections: collections.map((collection) => ({
|
|
4997
|
+
className: collection.className,
|
|
4998
|
+
name: collection.collectionName
|
|
4999
|
+
})),
|
|
5000
|
+
editor: {
|
|
5001
|
+
enabled: enableEditor,
|
|
5002
|
+
previewParam: "preview"
|
|
5003
|
+
},
|
|
5004
|
+
components: {
|
|
5005
|
+
...loadedConfig.components,
|
|
5006
|
+
enabled: detectComponents,
|
|
5007
|
+
match: componentMatch,
|
|
5008
|
+
minOccurrences: componentMinOccurrences,
|
|
5009
|
+
minPages: componentMinPages,
|
|
5010
|
+
rules: componentRules
|
|
5011
|
+
}
|
|
5012
|
+
};
|
|
2816
5013
|
await convertWebflowExport({
|
|
2817
5014
|
inputDir: input,
|
|
2818
5015
|
outputDir: output,
|
|
2819
5016
|
boilerplate: options.boilerplate,
|
|
2820
5017
|
overridesPath: options.overrides,
|
|
2821
|
-
|
|
2822
|
-
|
|
5018
|
+
configPath: options.config,
|
|
5019
|
+
config: mergeConfig(loadedConfig, cliConfig),
|
|
5020
|
+
generateStrapi: true,
|
|
5021
|
+
cmsBackend: cmsProvider,
|
|
5022
|
+
target,
|
|
5023
|
+
collectionClasses: collections.map((c) => c.className),
|
|
5024
|
+
collectionNames: Object.fromEntries(collections.map((c) => [c.className, c.collectionName])),
|
|
5025
|
+
extractComponents: true,
|
|
5026
|
+
skipPrompts: true,
|
|
5027
|
+
generateContent: generateContent && !options.noContent,
|
|
5028
|
+
editor: enableEditor
|
|
2823
5029
|
});
|
|
2824
|
-
|
|
5030
|
+
console.log(pc4.green("\n\u{1F389} Conversion complete!"));
|
|
5031
|
+
const configStrapi = loadedConfig.cms?.strapi;
|
|
5032
|
+
const requestedStrapiDir = options.scaffoldStrapi || options.strapiDir || configStrapi?.directory;
|
|
5033
|
+
const shouldScaffoldFromConfig = Boolean(configStrapi?.scaffold && requestedStrapiDir);
|
|
5034
|
+
if (cmsProvider === "strapi" && skipPrompts && requestedStrapiDir) {
|
|
5035
|
+
await completeSetup({
|
|
5036
|
+
projectDir: output,
|
|
5037
|
+
strapiDir: requestedStrapiDir,
|
|
5038
|
+
scaffold: Boolean(options.scaffoldStrapi) || shouldScaffoldFromConfig,
|
|
5039
|
+
scaffoldOptions: {
|
|
5040
|
+
strapiDir: requestedStrapiDir,
|
|
5041
|
+
packageManager: toPackageManager(options.strapiPackageManager || configStrapi?.packageManager || "npm"),
|
|
5042
|
+
install: options.strapiInstall !== false && configStrapi?.install !== false,
|
|
5043
|
+
run: false,
|
|
5044
|
+
gitInit: false,
|
|
5045
|
+
typescript: true
|
|
5046
|
+
}
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
if (cmsProvider === "strapi" && !skipPrompts) {
|
|
2825
5050
|
console.log("");
|
|
2826
5051
|
const shouldSetup = await confirm(
|
|
2827
|
-
pc4.cyan("\u{1F3AF} Would you like to
|
|
5052
|
+
pc4.cyan("\u{1F3AF} Would you like to set up Strapi now?"),
|
|
5053
|
+
false
|
|
2828
5054
|
);
|
|
2829
5055
|
if (shouldSetup) {
|
|
2830
5056
|
const strapiDir = await prompt(
|
|
2831
|
-
pc4.cyan(
|
|
2832
|
-
"\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): "
|
|
2833
|
-
)
|
|
5057
|
+
pc4.cyan("\u{1F4C1} Enter path to your Strapi directory: ")
|
|
2834
5058
|
);
|
|
2835
5059
|
if (strapiDir) {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
5060
|
+
const strapiExists = await fs14.pathExists(strapiDir);
|
|
5061
|
+
const shouldScaffold = !strapiExists ? await confirm(
|
|
5062
|
+
pc4.cyan("That Strapi directory does not exist. Scaffold a new Strapi project there?"),
|
|
5063
|
+
true
|
|
5064
|
+
) : false;
|
|
5065
|
+
console.log(pc4.cyan("\n\u{1F680} Starting Strapi setup..."));
|
|
2839
5066
|
try {
|
|
2840
5067
|
await completeSetup({
|
|
2841
5068
|
projectDir: output,
|
|
2842
|
-
strapiDir
|
|
5069
|
+
strapiDir,
|
|
5070
|
+
scaffold: shouldScaffold,
|
|
5071
|
+
scaffoldOptions: {
|
|
5072
|
+
strapiDir,
|
|
5073
|
+
packageManager: toPackageManager(options.strapiPackageManager || configStrapi?.packageManager || "npm"),
|
|
5074
|
+
install: options.strapiInstall !== false && configStrapi?.install !== false,
|
|
5075
|
+
run: false,
|
|
5076
|
+
gitInit: false,
|
|
5077
|
+
typescript: true
|
|
5078
|
+
}
|
|
2843
5079
|
});
|
|
2844
5080
|
} catch (error) {
|
|
2845
5081
|
console.error(pc4.red("\n\u274C Strapi setup failed"));
|
|
2846
5082
|
console.error(pc4.dim("You can run setup manually later with:"));
|
|
2847
|
-
console.error(
|
|
2848
|
-
pc4.dim(` cms setup-strapi ${output} ${strapiDir}`)
|
|
2849
|
-
);
|
|
5083
|
+
console.error(pc4.dim(` cms setup-strapi ${output} ${strapiDir}`));
|
|
2850
5084
|
}
|
|
2851
5085
|
}
|
|
2852
5086
|
} else {
|
|
2853
|
-
console.log("");
|
|
2854
|
-
console.log(pc4.dim(
|
|
2855
|
-
console.log(
|
|
2856
|
-
pc4.dim(` cms setup-strapi ${output} <strapi-directory>`)
|
|
2857
|
-
);
|
|
5087
|
+
console.log(pc4.dim("\n\u{1F4A1} You can setup Strapi later with:"));
|
|
5088
|
+
console.log(pc4.dim(` cms setup-strapi ${output} <strapi-directory>`));
|
|
2858
5089
|
}
|
|
2859
5090
|
}
|
|
2860
5091
|
} catch (error) {
|
|
2861
|
-
console.error(pc4.red("
|
|
5092
|
+
console.error(pc4.red("\nConversion failed:"));
|
|
5093
|
+
console.error(pc4.red(error instanceof Error ? error.message : String(error)));
|
|
2862
5094
|
process.exit(1);
|
|
2863
5095
|
}
|
|
2864
5096
|
});
|
|
2865
|
-
program.command("
|
|
5097
|
+
program.command("analyze").description("Analyze a Webflow export and preview pages, assets, and component candidates").argument("[input]", "Path to Webflow export directory").option("--config <path>", "Path to see-ms config file").action(async (input, options) => {
|
|
2866
5098
|
try {
|
|
5099
|
+
if (!input) {
|
|
5100
|
+
input = await prompt(pc4.cyan("\u{1F4C1} Webflow export directory to analyze: "));
|
|
5101
|
+
}
|
|
5102
|
+
const config = options.config ? await loadSeeMSConfig(options.config) : {};
|
|
5103
|
+
const analysis = await analyzeWebflowExport(input, normalizeConfig(config));
|
|
5104
|
+
const report = {
|
|
5105
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5106
|
+
stages: ["scan", "analyze", "plan"],
|
|
5107
|
+
pages: analysis.pages.map((page) => ({
|
|
5108
|
+
source: page.sourcePath,
|
|
5109
|
+
pageId: page.pageId,
|
|
5110
|
+
route: page.route,
|
|
5111
|
+
output: page.outputPath
|
|
5112
|
+
})),
|
|
5113
|
+
assets: {
|
|
5114
|
+
css: analysis.assets.css.length,
|
|
5115
|
+
images: analysis.assets.images.length,
|
|
5116
|
+
fonts: analysis.assets.fonts.length,
|
|
5117
|
+
js: analysis.assets.js.length,
|
|
5118
|
+
preservedStructure: true
|
|
5119
|
+
},
|
|
5120
|
+
components: analysis.componentCandidates,
|
|
5121
|
+
cms: {
|
|
5122
|
+
provider: config.cms?.provider || "strapi",
|
|
5123
|
+
fields: 0,
|
|
5124
|
+
collections: 0,
|
|
5125
|
+
schemas: 0,
|
|
5126
|
+
seedPages: 0
|
|
5127
|
+
},
|
|
5128
|
+
warnings: analysis.warnings
|
|
5129
|
+
};
|
|
5130
|
+
console.log(renderReportMarkdown(report));
|
|
5131
|
+
} catch (error) {
|
|
5132
|
+
console.error(pc4.red("\nAnalysis failed:"));
|
|
5133
|
+
console.error(pc4.red(error instanceof Error ? error.message : String(error)));
|
|
5134
|
+
process.exit(1);
|
|
5135
|
+
}
|
|
5136
|
+
});
|
|
5137
|
+
program.command("scaffold-strapi").description("Scaffold a new Strapi project for a converted SeeMS site").argument("[strapi-dir]", "Path where the new Strapi project should be created").option("--package-manager <manager>", "Package manager (npm|pnpm|yarn)", "npm").option("--no-install", "Create project files without installing dependencies").option("--run", "Start Strapi after scaffolding").option("--git-init", "Initialize a git repository for the Strapi project").option("--javascript", "Use JavaScript instead of TypeScript").action(async (strapiDir, options) => {
|
|
5138
|
+
try {
|
|
5139
|
+
if (!strapiDir) {
|
|
5140
|
+
strapiDir = await prompt(pc4.cyan("\u{1F4C1} New Strapi project directory: "));
|
|
5141
|
+
}
|
|
5142
|
+
const install = options.install !== false && await confirm(
|
|
5143
|
+
pc4.cyan("Install Strapi dependencies after scaffolding?"),
|
|
5144
|
+
true
|
|
5145
|
+
);
|
|
5146
|
+
await scaffoldStrapiProject({
|
|
5147
|
+
strapiDir,
|
|
5148
|
+
packageManager: toPackageManager(options.packageManager),
|
|
5149
|
+
install,
|
|
5150
|
+
run: Boolean(options.run),
|
|
5151
|
+
gitInit: Boolean(options.gitInit),
|
|
5152
|
+
typescript: !options.javascript
|
|
5153
|
+
});
|
|
5154
|
+
} catch (error) {
|
|
5155
|
+
console.error(pc4.red("Strapi scaffold failed"));
|
|
5156
|
+
console.error(error);
|
|
5157
|
+
process.exit(1);
|
|
5158
|
+
}
|
|
5159
|
+
});
|
|
5160
|
+
program.command("setup-strapi").description("Setup Strapi with schemas and seed data").argument("[project-dir]", "Path to converted project directory").argument("[strapi-dir]", "Path to Strapi directory").option("--url <url>", "Strapi URL", "http://localhost:1337").option("--token <token>", "Strapi API token (optional)").option("--new-token", "Ignore saved token and prompt for a new one").option("--scaffold", "Create the Strapi project if the target directory does not exist").option("--package-manager <manager>", "Package manager for scaffolding (npm|pnpm|yarn)", "npm").option("--no-install", "Scaffold without installing dependencies").action(async (projectDir, strapiDir, options) => {
|
|
5161
|
+
try {
|
|
5162
|
+
if (!projectDir) {
|
|
5163
|
+
projectDir = await prompt(pc4.cyan("\u{1F4C1} Converted project directory: "));
|
|
5164
|
+
}
|
|
5165
|
+
if (!strapiDir) {
|
|
5166
|
+
strapiDir = await prompt(pc4.cyan("\u{1F4C1} Strapi directory: "));
|
|
5167
|
+
}
|
|
5168
|
+
const strapiExists = strapiDir ? await fs14.pathExists(strapiDir) : false;
|
|
5169
|
+
const scaffold = Boolean(options.scaffold) || !strapiExists && await confirm(
|
|
5170
|
+
pc4.cyan("That Strapi directory does not exist. Scaffold it now?"),
|
|
5171
|
+
true
|
|
5172
|
+
);
|
|
5173
|
+
const install = options.install !== false && (!scaffold || await confirm(
|
|
5174
|
+
pc4.cyan("Install Strapi dependencies after scaffolding?"),
|
|
5175
|
+
true
|
|
5176
|
+
));
|
|
2867
5177
|
await completeSetup({
|
|
2868
5178
|
projectDir,
|
|
2869
5179
|
strapiDir,
|
|
2870
5180
|
strapiUrl: options.url,
|
|
2871
5181
|
apiToken: options.token,
|
|
2872
|
-
ignoreSavedToken: options.newToken
|
|
5182
|
+
ignoreSavedToken: options.newToken,
|
|
5183
|
+
scaffold,
|
|
5184
|
+
scaffoldOptions: {
|
|
5185
|
+
strapiDir,
|
|
5186
|
+
packageManager: toPackageManager(options.packageManager),
|
|
5187
|
+
install,
|
|
5188
|
+
run: false,
|
|
5189
|
+
gitInit: false,
|
|
5190
|
+
typescript: true
|
|
5191
|
+
}
|
|
2873
5192
|
});
|
|
2874
5193
|
} catch (error) {
|
|
2875
5194
|
console.error(pc4.red("Strapi setup failed"));
|
|
@@ -2877,19 +5196,30 @@ program.command("setup-strapi").description("Setup Strapi with schemas and seed
|
|
|
2877
5196
|
process.exit(1);
|
|
2878
5197
|
}
|
|
2879
5198
|
});
|
|
2880
|
-
program.command("generate").description("Generate CMS schemas from manifest").argument("
|
|
5199
|
+
program.command("generate").description("Generate CMS schemas from manifest").argument("[manifest]", "Path to cms-manifest.json").option("-t, --type <cms>", "CMS type (strapi|contentful|sanity)", "strapi").option("-o, --output <dir>", "Output directory for schemas").action(async (manifestPath, options) => {
|
|
2881
5200
|
try {
|
|
5201
|
+
if (!manifestPath) {
|
|
5202
|
+
manifestPath = await prompt(pc4.cyan("\u{1F4C4} Path to cms-manifest.json: "));
|
|
5203
|
+
}
|
|
2882
5204
|
console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
|
|
2883
5205
|
console.log(pc4.dim(`Reading manifest from: ${manifestPath}`));
|
|
2884
|
-
const manifestExists = await
|
|
5206
|
+
const manifestExists = await fs14.pathExists(manifestPath);
|
|
2885
5207
|
if (!manifestExists) {
|
|
2886
5208
|
throw new Error(`Manifest file not found: ${manifestPath}`);
|
|
2887
5209
|
}
|
|
2888
|
-
const manifestContent = await
|
|
5210
|
+
const manifestContent = await fs14.readFile(manifestPath, "utf-8");
|
|
2889
5211
|
const manifest = JSON.parse(manifestContent);
|
|
2890
5212
|
console.log(pc4.green(` \u2713 Manifest loaded successfully`));
|
|
2891
|
-
const
|
|
2892
|
-
|
|
5213
|
+
const type = options.type || await select(pc4.cyan("\u{1F9E0} Generate schemas for which CMS?"), [
|
|
5214
|
+
{ label: "Strapi", value: "strapi" }
|
|
5215
|
+
], "strapi");
|
|
5216
|
+
let outputDir = options.output;
|
|
5217
|
+
if (!outputDir) {
|
|
5218
|
+
const defaultOutput = path18.dirname(manifestPath);
|
|
5219
|
+
const answer = await prompt(pc4.cyan(`\u{1F4C1} Schema output directory (${defaultOutput}): `));
|
|
5220
|
+
outputDir = answer || defaultOutput;
|
|
5221
|
+
}
|
|
5222
|
+
if (type !== "strapi") {
|
|
2893
5223
|
console.log(
|
|
2894
5224
|
pc4.yellow(
|
|
2895
5225
|
`\u26A0\uFE0F Only Strapi is currently supported. Using Strapi schema format.`
|
|
@@ -2899,13 +5229,18 @@ program.command("generate").description("Generate CMS schemas from manifest").ar
|
|
|
2899
5229
|
console.log(pc4.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
2900
5230
|
const schemas = manifestToSchemas(manifest);
|
|
2901
5231
|
await writeAllSchemas(outputDir, schemas);
|
|
5232
|
+
const linkSchema = getLinkComponentSchema(manifest);
|
|
5233
|
+
if (linkSchema) {
|
|
5234
|
+
await writeLinkComponentSchema(outputDir);
|
|
5235
|
+
console.log(pc4.dim(" \u2713 Generated shared.link component"));
|
|
5236
|
+
}
|
|
2902
5237
|
await createStrapiReadme(outputDir);
|
|
2903
5238
|
console.log(
|
|
2904
5239
|
pc4.green(
|
|
2905
5240
|
` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`
|
|
2906
5241
|
)
|
|
2907
5242
|
);
|
|
2908
|
-
console.log(pc4.dim(` \u2713 Schemas written to: ${
|
|
5243
|
+
console.log(pc4.dim(` \u2713 Schemas written to: ${path18.join(outputDir, "cms-schemas")}/`));
|
|
2909
5244
|
console.log(pc4.green("\n\u2705 Schema generation completed successfully!"));
|
|
2910
5245
|
} catch (error) {
|
|
2911
5246
|
console.error(pc4.red("\n\u274C Schema generation failed:"));
|