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