@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/index.mjs
CHANGED
|
@@ -1,14 +1,84 @@
|
|
|
1
1
|
// src/converter.ts
|
|
2
2
|
import pc3 from "picocolors";
|
|
3
|
-
import
|
|
4
|
-
import fs10 from "fs-extra";
|
|
3
|
+
import path16 from "path";
|
|
5
4
|
|
|
6
5
|
// src/filesystem.ts
|
|
7
6
|
import fs from "fs-extra";
|
|
8
|
-
import
|
|
7
|
+
import path2 from "path";
|
|
9
8
|
import { glob } from "glob";
|
|
10
9
|
import { execSync } from "child_process";
|
|
11
10
|
import pc from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/assets.ts
|
|
13
|
+
import path from "path";
|
|
14
|
+
var RESPONSIVE_VARIANT_RE = /-p-(?:\d+(?:x\d+q\d+)?)(?=\.[^.]+$)/i;
|
|
15
|
+
function isResponsiveImageVariant(filePath) {
|
|
16
|
+
return RESPONSIVE_VARIANT_RE.test(path.basename(filePath));
|
|
17
|
+
}
|
|
18
|
+
function toOriginalImageCandidate(filePath) {
|
|
19
|
+
return filePath.replace(RESPONSIVE_VARIANT_RE, "");
|
|
20
|
+
}
|
|
21
|
+
function normalizeAssetUrl(value) {
|
|
22
|
+
try {
|
|
23
|
+
return decodeURI(value);
|
|
24
|
+
} catch {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function normalizePublicAssetPath(src) {
|
|
29
|
+
if (!src || /^(https?:)?\/\//i.test(src) || src.startsWith("data:")) {
|
|
30
|
+
return src;
|
|
31
|
+
}
|
|
32
|
+
let normalized = normalizeAssetUrl(src).replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
33
|
+
normalized = toOriginalImageCandidate(normalized);
|
|
34
|
+
if (normalized.startsWith("/assets/")) {
|
|
35
|
+
return normalized.replace(/\/\.\.\//g, "/");
|
|
36
|
+
}
|
|
37
|
+
return `/assets/${normalized.replace(/^\/+/, "")}`;
|
|
38
|
+
}
|
|
39
|
+
function normalizeImageSeedPath(imageSrc) {
|
|
40
|
+
if (!imageSrc) return "";
|
|
41
|
+
if (/^(https?:)?\/\//i.test(imageSrc) || imageSrc.startsWith("data:")) return imageSrc;
|
|
42
|
+
const [pathPart] = imageSrc.split(/[?#]/);
|
|
43
|
+
const decoded = normalizeAssetUrl(pathPart).replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
44
|
+
const withoutLeadingSlash = decoded.replace(/^\/+/, "");
|
|
45
|
+
const withoutAssetsPrefix = withoutLeadingSlash.replace(/^assets\/images\//, "images/");
|
|
46
|
+
const imageIndex = withoutAssetsPrefix.indexOf("images/");
|
|
47
|
+
const imagePath = imageIndex >= 0 ? withoutAssetsPrefix.slice(imageIndex) : `images/${path.basename(withoutAssetsPrefix)}`;
|
|
48
|
+
return `/${toOriginalImageCandidate(imagePath)}`;
|
|
49
|
+
}
|
|
50
|
+
function mediaLookupKeys(value) {
|
|
51
|
+
if (!value) return [];
|
|
52
|
+
const decoded = normalizeAssetUrl(value);
|
|
53
|
+
const withoutLeadingSlash = decoded.replace(/^\/+/, "");
|
|
54
|
+
const withoutAssetsPrefix = withoutLeadingSlash.replace(/^assets\/images\//, "images/");
|
|
55
|
+
const withoutImagesPrefix = withoutAssetsPrefix.replace(/^images\//, "");
|
|
56
|
+
const original = toOriginalImageCandidate(withoutImagesPrefix);
|
|
57
|
+
const basename = path.basename(original);
|
|
58
|
+
const basenameHyphenated = basename.replace(/\s+/g, "-");
|
|
59
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
60
|
+
decoded,
|
|
61
|
+
withoutLeadingSlash,
|
|
62
|
+
withoutAssetsPrefix,
|
|
63
|
+
withoutImagesPrefix,
|
|
64
|
+
original,
|
|
65
|
+
basename,
|
|
66
|
+
basenameHyphenated,
|
|
67
|
+
`/images/${withoutImagesPrefix}`,
|
|
68
|
+
`images/${withoutImagesPrefix}`,
|
|
69
|
+
`/images/${original}`,
|
|
70
|
+
`images/${original}`,
|
|
71
|
+
`/assets/images/${withoutImagesPrefix}`,
|
|
72
|
+
`assets/images/${withoutImagesPrefix}`,
|
|
73
|
+
`/assets/images/${original}`,
|
|
74
|
+
`assets/images/${original}`
|
|
75
|
+
]));
|
|
76
|
+
}
|
|
77
|
+
function isLikelyImagePath(value) {
|
|
78
|
+
return /\.(?:jpe?g|png|gif|webp|avif|svg)(?:[?#].*)?$/i.test(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/filesystem.ts
|
|
12
82
|
async function scanAssets(webflowDir) {
|
|
13
83
|
const assets = {
|
|
14
84
|
css: [],
|
|
@@ -18,47 +88,55 @@ async function scanAssets(webflowDir) {
|
|
|
18
88
|
};
|
|
19
89
|
const cssFiles = await glob("css/**/*.css", { cwd: webflowDir });
|
|
20
90
|
assets.css = cssFiles;
|
|
21
|
-
const imageFiles = await glob("images/**/*", { cwd: webflowDir });
|
|
22
|
-
assets.images = imageFiles;
|
|
23
|
-
const fontFiles = await glob("fonts/**/*", { cwd: webflowDir });
|
|
91
|
+
const imageFiles = await glob("images/**/*", { cwd: webflowDir, nodir: true });
|
|
92
|
+
assets.images = imageFiles.filter((file) => !isResponsiveImageVariant(file));
|
|
93
|
+
const fontFiles = await glob("fonts/**/*", { cwd: webflowDir, nodir: true });
|
|
24
94
|
assets.fonts = fontFiles;
|
|
25
95
|
const jsFiles = await glob("js/**/*.js", { cwd: webflowDir });
|
|
26
96
|
assets.js = jsFiles;
|
|
27
97
|
return assets;
|
|
28
98
|
}
|
|
29
99
|
async function copyCSSFiles(webflowDir, outputDir, cssFiles) {
|
|
30
|
-
const targetDir =
|
|
100
|
+
const targetDir = path2.join(outputDir, "assets", "css");
|
|
31
101
|
await fs.ensureDir(targetDir);
|
|
32
102
|
for (const file of cssFiles) {
|
|
33
|
-
const source =
|
|
34
|
-
const
|
|
103
|
+
const source = path2.join(webflowDir, file);
|
|
104
|
+
const relative = path2.relative("css", file);
|
|
105
|
+
const target = path2.join(targetDir, relative);
|
|
106
|
+
await fs.ensureDir(path2.dirname(target));
|
|
35
107
|
await fs.copy(source, target);
|
|
36
108
|
}
|
|
37
109
|
}
|
|
38
110
|
async function copyImages(webflowDir, outputDir, imageFiles) {
|
|
39
|
-
const targetDir =
|
|
111
|
+
const targetDir = path2.join(outputDir, "public", "assets", "images");
|
|
40
112
|
await fs.ensureDir(targetDir);
|
|
41
113
|
for (const file of imageFiles) {
|
|
42
|
-
const source =
|
|
43
|
-
const
|
|
114
|
+
const source = path2.join(webflowDir, file);
|
|
115
|
+
const relative = path2.relative("images", file);
|
|
116
|
+
const target = path2.join(targetDir, relative);
|
|
117
|
+
await fs.ensureDir(path2.dirname(target));
|
|
44
118
|
await fs.copy(source, target);
|
|
45
119
|
}
|
|
46
120
|
}
|
|
47
121
|
async function copyFonts(webflowDir, outputDir, fontFiles) {
|
|
48
|
-
const targetDir =
|
|
122
|
+
const targetDir = path2.join(outputDir, "public", "assets", "fonts");
|
|
49
123
|
await fs.ensureDir(targetDir);
|
|
50
124
|
for (const file of fontFiles) {
|
|
51
|
-
const source =
|
|
52
|
-
const
|
|
125
|
+
const source = path2.join(webflowDir, file);
|
|
126
|
+
const relative = path2.relative("fonts", file);
|
|
127
|
+
const target = path2.join(targetDir, relative);
|
|
128
|
+
await fs.ensureDir(path2.dirname(target));
|
|
53
129
|
await fs.copy(source, target);
|
|
54
130
|
}
|
|
55
131
|
}
|
|
56
132
|
async function copyJSFiles(webflowDir, outputDir, jsFiles) {
|
|
57
|
-
const targetDir =
|
|
133
|
+
const targetDir = path2.join(outputDir, "public", "assets", "js");
|
|
58
134
|
await fs.ensureDir(targetDir);
|
|
59
135
|
for (const file of jsFiles) {
|
|
60
|
-
const source =
|
|
61
|
-
const
|
|
136
|
+
const source = path2.join(webflowDir, file);
|
|
137
|
+
const relative = path2.relative("js", file);
|
|
138
|
+
const target = path2.join(targetDir, relative);
|
|
139
|
+
await fs.ensureDir(path2.dirname(target));
|
|
62
140
|
await fs.copy(source, target);
|
|
63
141
|
}
|
|
64
142
|
}
|
|
@@ -69,26 +147,56 @@ async function copyAllAssets(webflowDir, outputDir, assets) {
|
|
|
69
147
|
await copyJSFiles(webflowDir, outputDir, assets.js);
|
|
70
148
|
}
|
|
71
149
|
async function findHTMLFiles(webflowDir) {
|
|
72
|
-
const htmlFiles = await glob("**/*.html", { cwd: webflowDir });
|
|
150
|
+
const htmlFiles = await glob("**/*.html", { cwd: webflowDir, nodir: true });
|
|
73
151
|
return htmlFiles;
|
|
74
152
|
}
|
|
75
153
|
async function readHTMLFile(webflowDir, fileName) {
|
|
76
|
-
const filePath =
|
|
154
|
+
const filePath = path2.join(webflowDir, fileName);
|
|
77
155
|
return await fs.readFile(filePath, "utf-8");
|
|
78
156
|
}
|
|
79
|
-
async function writeVueComponent(outputDir, fileName, content) {
|
|
80
|
-
|
|
157
|
+
async function writeVueComponent(outputDir, fileName, content, target = "nuxt", cssFiles = [], editorEnabled = false) {
|
|
158
|
+
if (target === "astro-vue") {
|
|
159
|
+
const componentDir = path2.join(outputDir, "src", "components", "pages");
|
|
160
|
+
const astroPagesDir = path2.join(outputDir, "src", "pages");
|
|
161
|
+
const vueName2 = fileName.replace(".html", ".vue");
|
|
162
|
+
const astroName = fileName.replace(".html", ".astro");
|
|
163
|
+
const vuePath = path2.join(componentDir, vueName2);
|
|
164
|
+
const astroPath = path2.join(astroPagesDir, astroName);
|
|
165
|
+
const relativeVueImport = ensureRelativeImport(path2.relative(path2.dirname(astroPath), vuePath));
|
|
166
|
+
const cssLinks = [
|
|
167
|
+
...cssFiles.map((file) => `/assets/css/${path2.basename(file)}`),
|
|
168
|
+
"/assets/css/main.css"
|
|
169
|
+
].map((href) => `<link rel="stylesheet" href="${href}" />`).join("\n");
|
|
170
|
+
const editorScript = editorEnabled ? "\n<script>\n import '../cms-editor';\n</script>\n" : "";
|
|
171
|
+
await fs.ensureDir(path2.dirname(vuePath));
|
|
172
|
+
await fs.ensureDir(path2.dirname(astroPath));
|
|
173
|
+
await fs.writeFile(vuePath, content, "utf-8");
|
|
174
|
+
await fs.writeFile(astroPath, `---
|
|
175
|
+
import Page from '${relativeVueImport}';
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
${cssLinks}
|
|
179
|
+
<Page client:only="vue" />
|
|
180
|
+
${editorScript}
|
|
181
|
+
`, "utf-8");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const pagesDir = path2.join(outputDir, "pages");
|
|
81
185
|
const vueName = fileName.replace(".html", ".vue");
|
|
82
|
-
const targetPath =
|
|
83
|
-
await fs.ensureDir(
|
|
186
|
+
const targetPath = path2.join(pagesDir, vueName);
|
|
187
|
+
await fs.ensureDir(path2.dirname(targetPath));
|
|
84
188
|
await fs.writeFile(targetPath, content, "utf-8");
|
|
85
189
|
}
|
|
86
|
-
|
|
87
|
-
const
|
|
190
|
+
function ensureRelativeImport(importPath) {
|
|
191
|
+
const normalized = importPath.split(path2.sep).join("/");
|
|
192
|
+
return normalized.startsWith(".") ? normalized : `./${normalized}`;
|
|
193
|
+
}
|
|
194
|
+
async function formatVueFiles(outputDir, target = "nuxt") {
|
|
195
|
+
const pagesDir = target === "astro-vue" ? path2.join(outputDir, "src", "components", "pages") : path2.join(outputDir, "pages");
|
|
88
196
|
try {
|
|
89
197
|
console.log(pc.blue("\n\u2728 Formatting Vue files with Prettier..."));
|
|
90
|
-
execSync("
|
|
91
|
-
execSync(`
|
|
198
|
+
execSync("prettier --version", { stdio: "ignore" });
|
|
199
|
+
execSync(`prettier --write "${pagesDir}/**/*.vue"`, {
|
|
92
200
|
cwd: outputDir,
|
|
93
201
|
stdio: "inherit"
|
|
94
202
|
});
|
|
@@ -100,35 +208,28 @@ async function formatVueFiles(outputDir) {
|
|
|
100
208
|
|
|
101
209
|
// src/parser.ts
|
|
102
210
|
import * as cheerio from "cheerio";
|
|
103
|
-
import
|
|
104
|
-
function normalizeRoute(href) {
|
|
105
|
-
|
|
211
|
+
import path3 from "path";
|
|
212
|
+
function normalizeRoute(href, currentFile) {
|
|
213
|
+
const [pathPart, suffix = ""] = href.split(/(?=[?#])/);
|
|
214
|
+
let route = pathPart.replace(/\.html$/i, "");
|
|
106
215
|
if (route === "index" || route === "/index" || route.endsWith("/index")) {
|
|
107
|
-
|
|
216
|
+
const parent = route.replace(/(^|\/)index$/, "");
|
|
217
|
+
return `${parent ? parent.startsWith("/") ? parent : `/${parent}` : "/"}${suffix}`;
|
|
108
218
|
}
|
|
109
219
|
if (route === ".." || route === "../" || route === "/.." || route === "../index") {
|
|
110
|
-
return
|
|
220
|
+
return `/${suffix}`;
|
|
221
|
+
}
|
|
222
|
+
if (currentFile && !route.startsWith("/")) {
|
|
223
|
+
route = path3.posix.join(path3.posix.dirname(currentFile.replace(/\\/g, "/")), route);
|
|
111
224
|
}
|
|
112
|
-
|
|
113
|
-
const normalized = path2.posix.normalize(route);
|
|
225
|
+
const normalized = path3.posix.normalize(route);
|
|
114
226
|
if (!normalized.startsWith("/")) {
|
|
115
|
-
return
|
|
227
|
+
return `/${normalized}${suffix}`;
|
|
116
228
|
}
|
|
117
229
|
if (normalized === "." || normalized === "") {
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
return normalized;
|
|
121
|
-
}
|
|
122
|
-
function normalizeAssetPath(src) {
|
|
123
|
-
if (!src || src.startsWith("http") || src.startsWith("https")) {
|
|
124
|
-
return src;
|
|
125
|
-
}
|
|
126
|
-
let normalized = src.replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
|
|
127
|
-
if (normalized.startsWith("/assets/")) {
|
|
128
|
-
normalized = normalized.replace(/\/\.\.\//g, "/");
|
|
129
|
-
return normalized;
|
|
230
|
+
return `/${suffix}`;
|
|
130
231
|
}
|
|
131
|
-
return
|
|
232
|
+
return `${normalized}${suffix}`;
|
|
132
233
|
}
|
|
133
234
|
function parseHTML(html, fileName) {
|
|
134
235
|
const $ = cheerio.load(html);
|
|
@@ -144,11 +245,15 @@ function parseHTML(html, fileName) {
|
|
|
144
245
|
$(".global-embed style").each((_, el) => {
|
|
145
246
|
embeddedStyles += $(el).html() + "\n";
|
|
146
247
|
});
|
|
248
|
+
$(".w-embed > style").each((_, el) => {
|
|
249
|
+
embeddedStyles += $(el).html() + "\n";
|
|
250
|
+
});
|
|
147
251
|
$("body > style").each((_, el) => {
|
|
148
252
|
embeddedStyles += $(el).html() + "\n";
|
|
149
253
|
});
|
|
150
254
|
$(".global-embed").remove();
|
|
151
255
|
$("body > style").remove();
|
|
256
|
+
$(".w-embed > style").remove();
|
|
152
257
|
$("body script").remove();
|
|
153
258
|
const images = [];
|
|
154
259
|
$("img").each((_, el) => {
|
|
@@ -175,8 +280,9 @@ function parseHTML(html, fileName) {
|
|
|
175
280
|
links
|
|
176
281
|
};
|
|
177
282
|
}
|
|
178
|
-
function transformForNuxt(html) {
|
|
283
|
+
function transformForNuxt(html, currentFile, options = {}) {
|
|
179
284
|
const $ = cheerio.load(html);
|
|
285
|
+
const linkMode = options.linkMode || "nuxt";
|
|
180
286
|
$("html, head, body").each((_, el) => {
|
|
181
287
|
const $el = $(el);
|
|
182
288
|
$el.replaceWith($el.html() || "");
|
|
@@ -187,20 +293,23 @@ function transformForNuxt(html) {
|
|
|
187
293
|
const href = $el.attr("href");
|
|
188
294
|
if (!href) return;
|
|
189
295
|
const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#");
|
|
190
|
-
if (!isExternal) {
|
|
191
|
-
const route = normalizeRoute(href);
|
|
192
|
-
$el.attr("to", route);
|
|
193
|
-
$el.removeAttr("href");
|
|
296
|
+
if (!isExternal && linkMode === "nuxt") {
|
|
297
|
+
const route = normalizeRoute(href, currentFile);
|
|
194
298
|
const content = $el.html();
|
|
195
|
-
const
|
|
196
|
-
|
|
299
|
+
const attrs = { ...$el.attr() };
|
|
300
|
+
delete attrs.href;
|
|
301
|
+
attrs.to = route;
|
|
302
|
+
const attrString = Object.entries(attrs).map(([name, value]) => `${name}="${escapeAttribute(value ?? "")}"`).join(" ");
|
|
303
|
+
$el.replaceWith(`<nuxt-link ${attrString}>${content}</nuxt-link>`);
|
|
304
|
+
} else if (!isExternal) {
|
|
305
|
+
$el.attr("href", normalizeRoute(href, currentFile));
|
|
197
306
|
}
|
|
198
307
|
});
|
|
199
308
|
$("img").each((_, el) => {
|
|
200
309
|
const $el = $(el);
|
|
201
310
|
const src = $el.attr("src");
|
|
202
311
|
if (src) {
|
|
203
|
-
const normalizedSrc =
|
|
312
|
+
const normalizedSrc = normalizePublicAssetPath(src);
|
|
204
313
|
$el.attr("src", normalizedSrc);
|
|
205
314
|
}
|
|
206
315
|
$el.removeAttr("srcset");
|
|
@@ -208,10 +317,18 @@ function transformForNuxt(html) {
|
|
|
208
317
|
});
|
|
209
318
|
return $.html();
|
|
210
319
|
}
|
|
211
|
-
function
|
|
212
|
-
return
|
|
213
|
-
|
|
320
|
+
function escapeAttribute(value) {
|
|
321
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
322
|
+
}
|
|
323
|
+
function htmlToVueComponent(html, pageName, componentImports, componentImportBase = "~/components") {
|
|
324
|
+
let importsSection = "";
|
|
325
|
+
if (componentImports && componentImports.length > 0) {
|
|
326
|
+
importsSection = componentImports.map((name) => `import ${name} from '${componentImportBase}/${name}.vue';`).join("\n");
|
|
327
|
+
html = restoreComponentTags(replaceComponentMarkers(html), componentImports);
|
|
328
|
+
}
|
|
329
|
+
return `<script setup lang="ts">
|
|
214
330
|
// Page: ${pageName}
|
|
331
|
+
${importsSection}
|
|
215
332
|
</script>
|
|
216
333
|
|
|
217
334
|
<template>
|
|
@@ -221,6 +338,17 @@ function htmlToVueComponent(html, pageName) {
|
|
|
221
338
|
</template>
|
|
222
339
|
`;
|
|
223
340
|
}
|
|
341
|
+
function replaceComponentMarkers(html) {
|
|
342
|
+
return html.replace(/<!--COMPONENT:(\w+)-->/g, "<$1 />");
|
|
343
|
+
}
|
|
344
|
+
function restoreComponentTags(html, componentImports) {
|
|
345
|
+
let restored = html;
|
|
346
|
+
for (const name of componentImports) {
|
|
347
|
+
const lowered = name.toLowerCase();
|
|
348
|
+
restored = restored.replace(new RegExp(`<${lowered}\\s*><\\/${lowered}>`, "g"), `<${name} />`).replace(new RegExp(`<${lowered}\\s*\\/>`, "g"), `<${name} />`);
|
|
349
|
+
}
|
|
350
|
+
return restored;
|
|
351
|
+
}
|
|
224
352
|
function deduplicateStyles(styles) {
|
|
225
353
|
if (!styles.trim()) return "";
|
|
226
354
|
const sections = styles.split(/\/\* From .+ \*\//);
|
|
@@ -236,9 +364,9 @@ function deduplicateStyles(styles) {
|
|
|
236
364
|
|
|
237
365
|
// src/config-updater.ts
|
|
238
366
|
import fs2 from "fs-extra";
|
|
239
|
-
import
|
|
367
|
+
import path4 from "path";
|
|
240
368
|
function generateWebflowAssetPlugin(cssFiles) {
|
|
241
|
-
const webflowFiles = cssFiles.map((file) => `/assets/css/${
|
|
369
|
+
const webflowFiles = cssFiles.map((file) => `/assets/css/${path4.basename(file)}`);
|
|
242
370
|
return `import type { Plugin } from 'vite'
|
|
243
371
|
|
|
244
372
|
const webflowFiles = [${webflowFiles.map((f) => `'${f}'`).join(", ")}]
|
|
@@ -271,20 +399,20 @@ export default webflowURLReset
|
|
|
271
399
|
`;
|
|
272
400
|
}
|
|
273
401
|
async function writeWebflowAssetPlugin(outputDir, cssFiles) {
|
|
274
|
-
const utilsDir =
|
|
402
|
+
const utilsDir = path4.join(outputDir, "utils");
|
|
275
403
|
await fs2.ensureDir(utilsDir);
|
|
276
404
|
const content = generateWebflowAssetPlugin(cssFiles);
|
|
277
|
-
const targetPath =
|
|
405
|
+
const targetPath = path4.join(utilsDir, "webflow-assets.ts");
|
|
278
406
|
await fs2.writeFile(targetPath, content, "utf-8");
|
|
279
407
|
}
|
|
280
408
|
async function updateNuxtConfig(outputDir, cssFiles) {
|
|
281
|
-
const configPath =
|
|
409
|
+
const configPath = path4.join(outputDir, "nuxt.config.ts");
|
|
282
410
|
const configExists = await fs2.pathExists(configPath);
|
|
283
411
|
if (!configExists) {
|
|
284
412
|
throw new Error("nuxt.config.ts not found in output directory");
|
|
285
413
|
}
|
|
286
414
|
let config = await fs2.readFile(configPath, "utf-8");
|
|
287
|
-
const cssEntries = cssFiles.map((file) => ` '~/assets/css/${
|
|
415
|
+
const cssEntries = cssFiles.map((file) => ` '~/assets/css/${path4.basename(file)}'`);
|
|
288
416
|
if (config.includes("css:")) {
|
|
289
417
|
config = config.replace(
|
|
290
418
|
/css:\s*\[/,
|
|
@@ -304,9 +432,9 @@ ${cssEntries.join(",\n")}
|
|
|
304
432
|
}
|
|
305
433
|
async function writeEmbeddedStyles(outputDir, styles) {
|
|
306
434
|
if (!styles.trim()) return;
|
|
307
|
-
const cssDir =
|
|
435
|
+
const cssDir = path4.join(outputDir, "assets", "css");
|
|
308
436
|
await fs2.ensureDir(cssDir);
|
|
309
|
-
const mainCssPath =
|
|
437
|
+
const mainCssPath = path4.join(cssDir, "main.css");
|
|
310
438
|
const exists = await fs2.pathExists(mainCssPath);
|
|
311
439
|
if (exists) {
|
|
312
440
|
const existing = await fs2.readFile(mainCssPath, "utf-8");
|
|
@@ -320,7 +448,7 @@ ${styles}`, "utf-8");
|
|
|
320
448
|
}
|
|
321
449
|
}
|
|
322
450
|
async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:1337") {
|
|
323
|
-
const configPath =
|
|
451
|
+
const configPath = path4.join(outputDir, "nuxt.config.ts");
|
|
324
452
|
const configExists = await fs2.pathExists(configPath);
|
|
325
453
|
if (!configExists) {
|
|
326
454
|
throw new Error("nuxt.config.ts not found in output directory");
|
|
@@ -358,9 +486,18 @@ async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:133
|
|
|
358
486
|
|
|
359
487
|
// src/editor-integration.ts
|
|
360
488
|
import fs3 from "fs-extra";
|
|
361
|
-
import
|
|
489
|
+
import path5 from "path";
|
|
490
|
+
function getPageCollections(manifest) {
|
|
491
|
+
if (!manifest) return {};
|
|
492
|
+
return Object.fromEntries(
|
|
493
|
+
Object.entries(manifest.pages).map(([pageName, page]) => [
|
|
494
|
+
pageName,
|
|
495
|
+
Object.entries(page.collections || {}).filter(([, collection]) => collection.storage !== "page-repeatable").map(([collectionName]) => collectionName)
|
|
496
|
+
])
|
|
497
|
+
);
|
|
498
|
+
}
|
|
362
499
|
async function createEditorContentComposable(outputDir) {
|
|
363
|
-
const composablesDir =
|
|
500
|
+
const composablesDir = path5.join(outputDir, "composables");
|
|
364
501
|
await fs3.ensureDir(composablesDir);
|
|
365
502
|
const composableContent = `/**
|
|
366
503
|
* Global state for editor content in preview mode
|
|
@@ -459,21 +596,25 @@ export function useEditorContent(pageName?: string) {
|
|
|
459
596
|
};
|
|
460
597
|
}
|
|
461
598
|
`;
|
|
462
|
-
const composablePath =
|
|
599
|
+
const composablePath = path5.join(composablesDir, "useEditorContent.ts");
|
|
463
600
|
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
464
601
|
}
|
|
465
|
-
async function createStrapiContentComposable(outputDir) {
|
|
466
|
-
const composablesDir =
|
|
602
|
+
async function createStrapiContentComposable(outputDir, manifest) {
|
|
603
|
+
const composablesDir = path5.join(outputDir, "composables");
|
|
467
604
|
await fs3.ensureDir(composablesDir);
|
|
605
|
+
const pageCollections = JSON.stringify(getPageCollections(manifest), null, 2);
|
|
468
606
|
const composableContent = `/**
|
|
469
607
|
* Composable to fetch content from Strapi based on CMS manifest
|
|
470
608
|
* Integrates with editor state for preview mode
|
|
471
609
|
*/
|
|
472
610
|
|
|
611
|
+
const PAGE_COLLECTIONS: Record<string, string[]> = ${pageCollections};
|
|
612
|
+
|
|
473
613
|
export function useStrapiContent(pageName: string) {
|
|
474
614
|
const config = useRuntimeConfig();
|
|
475
615
|
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
476
616
|
const editorContent = useEditorContent(pageName);
|
|
617
|
+
const collectionNames = PAGE_COLLECTIONS[pageName] || [];
|
|
477
618
|
|
|
478
619
|
// Helper to transform Strapi image objects to URL strings
|
|
479
620
|
const transformStrapiImages = (data: any, baseUrl: string): any => {
|
|
@@ -532,6 +673,25 @@ export function useStrapiContent(pageName: string) {
|
|
|
532
673
|
}
|
|
533
674
|
);
|
|
534
675
|
|
|
676
|
+
const collectionFetches = collectionNames.map((collectionName) =>
|
|
677
|
+
useFetch<any>(
|
|
678
|
+
\`\${strapiUrl}/api/\${collectionName}\`,
|
|
679
|
+
{
|
|
680
|
+
key: \`strapi-\${pageName}-collection-\${collectionName}\`,
|
|
681
|
+
query: {
|
|
682
|
+
populate: '*',
|
|
683
|
+
},
|
|
684
|
+
transform: (response) => {
|
|
685
|
+
const data = response?.data || response || [];
|
|
686
|
+
if (Array.isArray(data)) {
|
|
687
|
+
return data.map((item) => transformStrapiImages(item, strapiUrl));
|
|
688
|
+
}
|
|
689
|
+
return transformStrapiImages(data, strapiUrl);
|
|
690
|
+
},
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
|
|
535
695
|
// Initialize editor state with Strapi data when fetched
|
|
536
696
|
// This runs in both normal AND preview mode to ensure initial content is available
|
|
537
697
|
watch(
|
|
@@ -549,12 +709,25 @@ export function useStrapiContent(pageName: string) {
|
|
|
549
709
|
// In preview mode: use editor state
|
|
550
710
|
// In normal mode: use Strapi data (and sync to editor state)
|
|
551
711
|
const content = computed(() => {
|
|
712
|
+
const collections = Object.fromEntries(
|
|
713
|
+
collectionNames.map((collectionName, index) => [
|
|
714
|
+
collectionName,
|
|
715
|
+
collectionFetches[index]?.data.value || []
|
|
716
|
+
])
|
|
717
|
+
);
|
|
718
|
+
|
|
552
719
|
if (editorContent.isPreviewMode.value) {
|
|
553
720
|
// Use editor state in preview mode
|
|
554
|
-
return
|
|
721
|
+
return {
|
|
722
|
+
...editorContent.getPageContent(pageName),
|
|
723
|
+
...collections,
|
|
724
|
+
};
|
|
555
725
|
} else {
|
|
556
726
|
// Use Strapi data in normal mode
|
|
557
|
-
return
|
|
727
|
+
return {
|
|
728
|
+
...(strapiData.value || editorContent.getPageContent(pageName)),
|
|
729
|
+
...collections,
|
|
730
|
+
};
|
|
558
731
|
}
|
|
559
732
|
});
|
|
560
733
|
|
|
@@ -563,11 +736,11 @@ export function useStrapiContent(pageName: string) {
|
|
|
563
736
|
};
|
|
564
737
|
}
|
|
565
738
|
`;
|
|
566
|
-
const composablePath =
|
|
739
|
+
const composablePath = path5.join(composablesDir, "useStrapiContent.ts");
|
|
567
740
|
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
568
741
|
}
|
|
569
742
|
async function createEditorPlugin(outputDir) {
|
|
570
|
-
const pluginsDir =
|
|
743
|
+
const pluginsDir = path5.join(outputDir, "plugins");
|
|
571
744
|
await fs3.ensureDir(pluginsDir);
|
|
572
745
|
const pluginContent = `/**
|
|
573
746
|
* CMS Editor Overlay Plugin
|
|
@@ -576,6 +749,8 @@ async function createEditorPlugin(outputDir) {
|
|
|
576
749
|
|
|
577
750
|
/**
|
|
578
751
|
* Disable Lenis smooth scroll to allow native scrolling in edit mode
|
|
752
|
+
* Note: The primary approach is to conditionally render <VueLenis> in the layout.
|
|
753
|
+
* This function serves as a fallback for existing projects that haven't been updated.
|
|
579
754
|
*/
|
|
580
755
|
function disableLenisInEditMode() {
|
|
581
756
|
try {
|
|
@@ -583,28 +758,48 @@ function disableLenisInEditMode() {
|
|
|
583
758
|
const lenisInstances = [
|
|
584
759
|
(window as any).lenis,
|
|
585
760
|
(window as any).__lenis,
|
|
586
|
-
|
|
761
|
+
(window as any).Lenis,
|
|
587
762
|
];
|
|
588
763
|
|
|
589
764
|
for (const lenis of lenisInstances) {
|
|
765
|
+
if (lenis && typeof lenis.stop === 'function') {
|
|
766
|
+
lenis.stop();
|
|
767
|
+
}
|
|
590
768
|
if (lenis && typeof lenis.destroy === 'function') {
|
|
591
769
|
lenis.destroy();
|
|
592
770
|
return;
|
|
593
771
|
}
|
|
594
772
|
}
|
|
595
773
|
|
|
596
|
-
// Check for Vue Lenis component instances
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
774
|
+
// Check for Vue Lenis component instances via refs
|
|
775
|
+
// VueLenis stores the instance in the component's exposed properties
|
|
776
|
+
const lenisElements = document.querySelectorAll('[data-lenis], .lenis, [data-lenis-prevent]');
|
|
777
|
+
lenisElements.forEach((el: any) => {
|
|
778
|
+
// Try various ways Vue Lenis might store the instance
|
|
779
|
+
const possibleInstances = [
|
|
780
|
+
el.__lenis,
|
|
781
|
+
el._lenis,
|
|
782
|
+
el.$lenis,
|
|
783
|
+
el.__vue__?.exposed?.lenis,
|
|
784
|
+
el.__vueParentComponent?.exposed?.lenis,
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
for (const instance of possibleInstances) {
|
|
788
|
+
if (instance && typeof instance.stop === 'function') {
|
|
789
|
+
instance.stop();
|
|
603
790
|
}
|
|
604
|
-
|
|
605
|
-
|
|
791
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
792
|
+
instance.destroy();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Also remove lenis-related classes from html/body that might affect scrolling
|
|
799
|
+
document.documentElement.classList.remove('lenis', 'lenis-smooth');
|
|
800
|
+
document.body.classList.remove('lenis', 'lenis-smooth');
|
|
606
801
|
} catch (error) {
|
|
607
|
-
// Silently fail - Lenis may not be present
|
|
802
|
+
// Silently fail - Lenis may not be present or already disabled via layout
|
|
608
803
|
}
|
|
609
804
|
}
|
|
610
805
|
|
|
@@ -738,11 +933,11 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
738
933
|
});
|
|
739
934
|
});
|
|
740
935
|
`;
|
|
741
|
-
const pluginPath =
|
|
936
|
+
const pluginPath = path5.join(pluginsDir, "cms-editor.client.ts");
|
|
742
937
|
await fs3.writeFile(pluginPath, pluginContent, "utf-8");
|
|
743
938
|
}
|
|
744
939
|
async function addEditorDependency(outputDir) {
|
|
745
|
-
const packageJsonPath =
|
|
940
|
+
const packageJsonPath = path5.join(outputDir, "package.json");
|
|
746
941
|
if (await fs3.pathExists(packageJsonPath)) {
|
|
747
942
|
const packageJson = await fs3.readJson(packageJsonPath);
|
|
748
943
|
if (!packageJson.dependencies) {
|
|
@@ -753,7 +948,7 @@ async function addEditorDependency(outputDir) {
|
|
|
753
948
|
}
|
|
754
949
|
}
|
|
755
950
|
async function createSaveEndpoint(outputDir) {
|
|
756
|
-
const serverDir =
|
|
951
|
+
const serverDir = path5.join(outputDir, "server", "api", "cms");
|
|
757
952
|
await fs3.ensureDir(serverDir);
|
|
758
953
|
const endpointContent = `/**
|
|
759
954
|
* API endpoint for saving CMS changes
|
|
@@ -815,7 +1010,7 @@ export default defineEventHandler(async (event) => {
|
|
|
815
1010
|
}
|
|
816
1011
|
|
|
817
1012
|
// Load manifest to understand field mappings
|
|
818
|
-
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1013
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
819
1014
|
let manifest;
|
|
820
1015
|
try {
|
|
821
1016
|
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
@@ -950,11 +1145,11 @@ export default defineEventHandler(async (event) => {
|
|
|
950
1145
|
}
|
|
951
1146
|
});
|
|
952
1147
|
`;
|
|
953
|
-
const endpointPath =
|
|
1148
|
+
const endpointPath = path5.join(serverDir, "save.post.ts");
|
|
954
1149
|
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
955
1150
|
}
|
|
956
1151
|
async function createStrapiBootstrap(outputDir) {
|
|
957
|
-
const strapiBootstrapDir =
|
|
1152
|
+
const strapiBootstrapDir = path5.join(outputDir, "strapi-bootstrap");
|
|
958
1153
|
await fs3.ensureDir(strapiBootstrapDir);
|
|
959
1154
|
const bootstrapContent = `/**
|
|
960
1155
|
* Strapi Bootstrap File
|
|
@@ -1069,7 +1264,7 @@ export default {
|
|
|
1069
1264
|
},
|
|
1070
1265
|
};
|
|
1071
1266
|
`;
|
|
1072
|
-
const bootstrapPath =
|
|
1267
|
+
const bootstrapPath = path5.join(strapiBootstrapDir, "index.ts");
|
|
1073
1268
|
await fs3.writeFile(bootstrapPath, bootstrapContent, "utf-8");
|
|
1074
1269
|
const readmeContent = `# Strapi Bootstrap File
|
|
1075
1270
|
|
|
@@ -1119,12 +1314,12 @@ If you prefer to set permissions manually:
|
|
|
1119
1314
|
- Only affects the "Public" role (unauthenticated users)
|
|
1120
1315
|
- Safe to run multiple times (idempotent)
|
|
1121
1316
|
`;
|
|
1122
|
-
const readmePath =
|
|
1317
|
+
const readmePath = path5.join(strapiBootstrapDir, "README.md");
|
|
1123
1318
|
await fs3.writeFile(readmePath, readmeContent, "utf-8");
|
|
1124
1319
|
console.log(" \u2713 Generated Strapi bootstrap file");
|
|
1125
1320
|
}
|
|
1126
1321
|
async function createPublishEndpoint(outputDir) {
|
|
1127
|
-
const serverDir =
|
|
1322
|
+
const serverDir = path5.join(outputDir, "server", "api", "cms");
|
|
1128
1323
|
await fs3.ensureDir(serverDir);
|
|
1129
1324
|
const endpointContent = `/**
|
|
1130
1325
|
* API endpoint for batch publishing CMS changes
|
|
@@ -1186,7 +1381,7 @@ export default defineEventHandler(async (event) => {
|
|
|
1186
1381
|
}
|
|
1187
1382
|
|
|
1188
1383
|
// Load manifest to understand field mappings
|
|
1189
|
-
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1384
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
1190
1385
|
let manifest;
|
|
1191
1386
|
try {
|
|
1192
1387
|
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
@@ -1343,13 +1538,335 @@ export default defineEventHandler(async (event) => {
|
|
|
1343
1538
|
}
|
|
1344
1539
|
});
|
|
1345
1540
|
`;
|
|
1346
|
-
const endpointPath =
|
|
1541
|
+
const endpointPath = path5.join(serverDir, "publish.post.ts");
|
|
1347
1542
|
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
1348
1543
|
}
|
|
1544
|
+
async function createAstroStrapiContentComposable(outputDir, manifest) {
|
|
1545
|
+
const composablesDir = path5.join(outputDir, "src", "composables");
|
|
1546
|
+
await fs3.ensureDir(composablesDir);
|
|
1547
|
+
const pageCollections = JSON.stringify(getPageCollections(manifest), null, 2);
|
|
1548
|
+
const content = `import { computed, reactive, ref, onMounted } from 'vue';
|
|
1549
|
+
|
|
1550
|
+
const PAGE_COLLECTIONS: Record<string, string[]> = ${pageCollections};
|
|
1551
|
+
|
|
1552
|
+
const editorState = reactive<{
|
|
1553
|
+
content: Record<string, Record<string, any>>;
|
|
1554
|
+
hasChanges: Record<string, boolean>;
|
|
1555
|
+
}>({
|
|
1556
|
+
content: {},
|
|
1557
|
+
hasChanges: {},
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
function getStrapiUrl() {
|
|
1561
|
+
return import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function transformStrapiImages(data: any, baseUrl: string): any {
|
|
1565
|
+
if (!data || typeof data !== 'object') return data;
|
|
1566
|
+
if (Array.isArray(data)) return data.map((item) => transformStrapiImages(item, baseUrl));
|
|
1567
|
+
if ('url' in data && ('mime' in data || 'formats' in data)) {
|
|
1568
|
+
return data.url.startsWith('http') ? data.url : \`\${baseUrl}\${data.url}\`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const transformed: Record<string, any> = {};
|
|
1572
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1573
|
+
transformed[key] = transformStrapiImages(value, baseUrl);
|
|
1574
|
+
}
|
|
1575
|
+
return transformed;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
export function useStrapiContent(pageName: string) {
|
|
1579
|
+
const strapiUrl = getStrapiUrl();
|
|
1580
|
+
const strapiData = ref<Record<string, any>>({});
|
|
1581
|
+
const isPreviewMode = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('preview') === 'true';
|
|
1582
|
+
const collectionNames = PAGE_COLLECTIONS[pageName] || [];
|
|
1583
|
+
|
|
1584
|
+
function initializePageContent(page: string, content: Record<string, any>) {
|
|
1585
|
+
if (!editorState.content[page]) {
|
|
1586
|
+
editorState.content[page] = { ...content };
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function getPageContent(page: string) {
|
|
1591
|
+
return editorState.content[page] || {};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function updateField(page: string, fieldName: string, value: any) {
|
|
1595
|
+
if (!editorState.content[page]) editorState.content[page] = {};
|
|
1596
|
+
editorState.content[page][fieldName] = value;
|
|
1597
|
+
editorState.hasChanges[page] = true;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
onMounted(async () => {
|
|
1601
|
+
try {
|
|
1602
|
+
const response = await fetch(\`\${strapiUrl}/api/\${pageName}?populate=*\`);
|
|
1603
|
+
if (!response.ok) return;
|
|
1604
|
+
const json = await response.json();
|
|
1605
|
+
const data = transformStrapiImages(json?.data || json, strapiUrl);
|
|
1606
|
+
const collections = Object.fromEntries(
|
|
1607
|
+
await Promise.all(collectionNames.map(async (collectionName) => {
|
|
1608
|
+
const collectionResponse = await fetch(\`\${strapiUrl}/api/\${collectionName}?populate=*\`);
|
|
1609
|
+
if (!collectionResponse.ok) return [collectionName, []];
|
|
1610
|
+
const collectionJson = await collectionResponse.json();
|
|
1611
|
+
const collectionData = collectionJson?.data || collectionJson || [];
|
|
1612
|
+
return [
|
|
1613
|
+
collectionName,
|
|
1614
|
+
Array.isArray(collectionData)
|
|
1615
|
+
? collectionData.map((item) => transformStrapiImages(item, strapiUrl))
|
|
1616
|
+
: transformStrapiImages(collectionData, strapiUrl)
|
|
1617
|
+
];
|
|
1618
|
+
}))
|
|
1619
|
+
);
|
|
1620
|
+
strapiData.value = { ...(data || {}), ...collections };
|
|
1621
|
+
initializePageContent(pageName, strapiData.value);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
console.error('[SeeMS] Failed to fetch Strapi content', error);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
(window as any).__editorState = {
|
|
1627
|
+
...editorState,
|
|
1628
|
+
getPageContent,
|
|
1629
|
+
updateField,
|
|
1630
|
+
initializePageContent,
|
|
1631
|
+
};
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
const content = computed(() => {
|
|
1635
|
+
return isPreviewMode ? getPageContent(pageName) : (strapiData.value || getPageContent(pageName));
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
return { content };
|
|
1639
|
+
}
|
|
1640
|
+
`;
|
|
1641
|
+
await fs3.writeFile(path5.join(composablesDir, "useStrapiContent.ts"), content, "utf-8");
|
|
1642
|
+
}
|
|
1643
|
+
async function createAstroEditorClient(outputDir) {
|
|
1644
|
+
const srcDir = path5.join(outputDir, "src");
|
|
1645
|
+
await fs3.ensureDir(srcDir);
|
|
1646
|
+
const content = `/**
|
|
1647
|
+
* Astro client entry for the SeeMS inline editor.
|
|
1648
|
+
*/
|
|
1649
|
+
async function initSeeMSEditor() {
|
|
1650
|
+
const params = new URLSearchParams(window.location.search);
|
|
1651
|
+
if (params.get('preview') !== 'true') return;
|
|
1652
|
+
|
|
1653
|
+
const {
|
|
1654
|
+
initEditor,
|
|
1655
|
+
createAuthManager,
|
|
1656
|
+
showLoginModal,
|
|
1657
|
+
createDraftStorage,
|
|
1658
|
+
createURLStateManager,
|
|
1659
|
+
createManifestLoader,
|
|
1660
|
+
createNavigationGuard,
|
|
1661
|
+
getCurrentPageFromRoute,
|
|
1662
|
+
createToolbar,
|
|
1663
|
+
} = await import('@see-ms/editor-overlay');
|
|
1664
|
+
|
|
1665
|
+
const strapiUrl = import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1666
|
+
const urlState = createURLStateManager();
|
|
1667
|
+
urlState.setState({ preview: true });
|
|
1668
|
+
|
|
1669
|
+
const authManager = createAuthManager({
|
|
1670
|
+
strapiUrl,
|
|
1671
|
+
storageKey: 'cms_editor_token',
|
|
1672
|
+
});
|
|
1673
|
+
const draftStorage = createDraftStorage();
|
|
1674
|
+
const manifestLoader = createManifestLoader();
|
|
1675
|
+
|
|
1676
|
+
try {
|
|
1677
|
+
await manifestLoader.load();
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
console.error('[CMS Editor] Failed to load manifest:', error);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
let currentPage = getCurrentPageFromRoute();
|
|
1684
|
+
if (!currentPage) {
|
|
1685
|
+
currentPage = manifestLoader.getPageFromRoute(window.location.pathname);
|
|
1686
|
+
}
|
|
1687
|
+
if (!currentPage) {
|
|
1688
|
+
console.error('[CMS Editor] Could not determine current page');
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
let token = authManager.getToken();
|
|
1693
|
+
if (!token || !await authManager.verifyToken(token)) {
|
|
1694
|
+
try {
|
|
1695
|
+
token = await showLoginModal(authManager);
|
|
1696
|
+
} catch {
|
|
1697
|
+
urlState.clearPreviewMode();
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const navigationGuard = createNavigationGuard({
|
|
1703
|
+
showToast: true,
|
|
1704
|
+
toastMessage: 'Navigation disabled in edit mode',
|
|
1705
|
+
});
|
|
1706
|
+
navigationGuard.enable();
|
|
1707
|
+
|
|
1708
|
+
const editor = initEditor({
|
|
1709
|
+
apiEndpoint: '/api/cms/save',
|
|
1710
|
+
authToken: token,
|
|
1711
|
+
richText: true,
|
|
1712
|
+
manifestLoader,
|
|
1713
|
+
draftStorage,
|
|
1714
|
+
currentPage,
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
await editor.enable();
|
|
1718
|
+
|
|
1719
|
+
const toolbar = await createToolbar(editor, {
|
|
1720
|
+
draftStorage,
|
|
1721
|
+
urlState,
|
|
1722
|
+
navigationGuard,
|
|
1723
|
+
manifestLoader,
|
|
1724
|
+
currentPage,
|
|
1725
|
+
});
|
|
1726
|
+
document.body.appendChild(toolbar);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (document.readyState === 'loading') {
|
|
1730
|
+
document.addEventListener('DOMContentLoaded', initSeeMSEditor, { once: true });
|
|
1731
|
+
} else {
|
|
1732
|
+
initSeeMSEditor();
|
|
1733
|
+
}
|
|
1734
|
+
`;
|
|
1735
|
+
await fs3.writeFile(path5.join(srcDir, "cms-editor.ts"), content, "utf-8");
|
|
1736
|
+
}
|
|
1737
|
+
async function createAstroSaveEndpoint(outputDir) {
|
|
1738
|
+
const endpointDir = path5.join(outputDir, "src", "pages", "api", "cms");
|
|
1739
|
+
await fs3.ensureDir(endpointDir);
|
|
1740
|
+
await fs3.writeFile(path5.join(endpointDir, "save.ts"), astroEndpointContent(false), "utf-8");
|
|
1741
|
+
await fs3.writeFile(path5.join(endpointDir, "publish.ts"), astroEndpointContent(true), "utf-8");
|
|
1742
|
+
}
|
|
1743
|
+
function astroEndpointContent(batchPublish) {
|
|
1744
|
+
return `import type { APIRoute } from 'astro';
|
|
1745
|
+
import fs from 'node:fs';
|
|
1746
|
+
import path from 'node:path';
|
|
1747
|
+
|
|
1748
|
+
export const prerender = false;
|
|
1749
|
+
|
|
1750
|
+
const strapiUrl = import.meta.env.PUBLIC_STRAPI_URL || 'http://localhost:1337';
|
|
1751
|
+
|
|
1752
|
+
function json(status: number, body: unknown) {
|
|
1753
|
+
return new Response(JSON.stringify(body), {
|
|
1754
|
+
status,
|
|
1755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
async function verifyToken(token: string) {
|
|
1760
|
+
const adminResponse = await fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
1761
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
1762
|
+
});
|
|
1763
|
+
if (adminResponse.ok) return { user: await adminResponse.json(), isAdminToken: true };
|
|
1764
|
+
|
|
1765
|
+
const userResponse = await fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
1766
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
1767
|
+
});
|
|
1768
|
+
if (userResponse.ok) return { user: await userResponse.json(), isAdminToken: false };
|
|
1769
|
+
|
|
1770
|
+
throw new Error('Invalid or expired token');
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function loadManifest() {
|
|
1774
|
+
const manifestPath = path.join(process.cwd(), 'public', 'cms-manifest.json');
|
|
1775
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function filterFields(pageConfig: any, fields: Record<string, any>) {
|
|
1779
|
+
const data: Record<string, any> = {};
|
|
1780
|
+
for (const [fieldName, value] of Object.entries(fields || {})) {
|
|
1781
|
+
if (pageConfig?.fields?.[fieldName]) data[fieldName] = value;
|
|
1782
|
+
}
|
|
1783
|
+
return data;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
async function writePage(page: string, fields: Record<string, any>, token: string, isAdminToken: boolean, publish: boolean) {
|
|
1787
|
+
const manifest = loadManifest();
|
|
1788
|
+
const pageConfig = manifest.pages[page];
|
|
1789
|
+
if (!pageConfig) throw new Error(\`Page "\${page}" not found in manifest\`);
|
|
1790
|
+
const data = filterFields(pageConfig, fields);
|
|
1791
|
+
|
|
1792
|
+
if (isAdminToken) {
|
|
1793
|
+
const endpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
1794
|
+
const update = await fetch(endpoint, {
|
|
1795
|
+
method: 'PUT',
|
|
1796
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1797
|
+
body: JSON.stringify(data),
|
|
1798
|
+
});
|
|
1799
|
+
if (!update.ok) throw new Error(await update.text());
|
|
1800
|
+
|
|
1801
|
+
if (publish) {
|
|
1802
|
+
const publishResponse = await fetch(\`\${endpoint}/actions/publish\`, {
|
|
1803
|
+
method: 'POST',
|
|
1804
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1805
|
+
body: JSON.stringify({}),
|
|
1806
|
+
});
|
|
1807
|
+
if (!publishResponse.ok) throw new Error(await publishResponse.text());
|
|
1808
|
+
}
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const update = await fetch(\`\${strapiUrl}/api/\${page}\`, {
|
|
1813
|
+
method: 'PUT',
|
|
1814
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1815
|
+
body: JSON.stringify({ data }),
|
|
1816
|
+
});
|
|
1817
|
+
if (!update.ok) throw new Error(await update.text());
|
|
1818
|
+
|
|
1819
|
+
if (publish) {
|
|
1820
|
+
const publishResponse = await fetch(\`\${strapiUrl}/api/\${page}/publish\`, {
|
|
1821
|
+
method: 'POST',
|
|
1822
|
+
headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' },
|
|
1823
|
+
body: JSON.stringify({}),
|
|
1824
|
+
});
|
|
1825
|
+
if (!publishResponse.ok) throw new Error(await publishResponse.text());
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
1830
|
+
const authHeader = request.headers.get('authorization');
|
|
1831
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
1832
|
+
return json(401, { success: false, message: 'Missing authorization header' });
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const token = authHeader.slice(7);
|
|
1836
|
+
try {
|
|
1837
|
+
const { user, isAdminToken } = await verifyToken(token);
|
|
1838
|
+
const body = await request.json();
|
|
1839
|
+
${batchPublish ? ` const pages = Array.isArray(body.pages) ? body.pages : [];
|
|
1840
|
+
const results = await Promise.allSettled(
|
|
1841
|
+
pages.map(({ page, fields }: any) => writePage(page, fields, token, isAdminToken, true))
|
|
1842
|
+
);
|
|
1843
|
+
const failed = results
|
|
1844
|
+
.map((result, index) => ({ result, page: pages[index]?.page }))
|
|
1845
|
+
.filter(({ result }) => result.status === 'rejected')
|
|
1846
|
+
.map(({ result, page }) => ({ page, error: result.status === 'rejected' ? result.reason?.message : 'Unknown error' }));
|
|
1847
|
+
return json(200, {
|
|
1848
|
+
success: failed.length === 0,
|
|
1849
|
+
message: \`Published \${pages.length - failed.length} of \${pages.length} pages\`,
|
|
1850
|
+
failed,
|
|
1851
|
+
user,
|
|
1852
|
+
});` : ` if (!body.page || !body.fields) {
|
|
1853
|
+
return json(400, { success: false, message: 'Missing page or fields' });
|
|
1854
|
+
}
|
|
1855
|
+
await writePage(body.page, body.fields, token, isAdminToken, body.isDraft === false);
|
|
1856
|
+
return json(200, { success: true, message: 'Changes saved successfully', page: body.page, isDraft: body.isDraft !== false, user });`}
|
|
1857
|
+
} catch (error: any) {
|
|
1858
|
+
return json(error.message === 'Invalid or expired token' ? 401 : 500, {
|
|
1859
|
+
success: false,
|
|
1860
|
+
message: error.message || 'Failed to save CMS changes',
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
`;
|
|
1865
|
+
}
|
|
1349
1866
|
|
|
1350
1867
|
// src/boilerplate.ts
|
|
1351
1868
|
import fs4 from "fs-extra";
|
|
1352
|
-
import
|
|
1869
|
+
import path6 from "path";
|
|
1353
1870
|
import { execSync as execSync2 } from "child_process";
|
|
1354
1871
|
import pc2 from "picocolors";
|
|
1355
1872
|
function isGitHubURL(source) {
|
|
@@ -1359,7 +1876,7 @@ async function cloneFromGitHub(repoUrl, outputDir) {
|
|
|
1359
1876
|
console.log(pc2.blue(" Cloning from GitHub..."));
|
|
1360
1877
|
try {
|
|
1361
1878
|
execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
|
|
1362
|
-
const gitDir =
|
|
1879
|
+
const gitDir = path6.join(outputDir, ".git");
|
|
1363
1880
|
await fs4.remove(gitDir);
|
|
1364
1881
|
console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
|
|
1365
1882
|
} catch (error) {
|
|
@@ -1374,30 +1891,88 @@ async function copyFromLocal(sourcePath, outputDir) {
|
|
|
1374
1891
|
}
|
|
1375
1892
|
await fs4.copy(sourcePath, outputDir, {
|
|
1376
1893
|
filter: (src) => {
|
|
1377
|
-
const name =
|
|
1894
|
+
const name = path6.basename(src);
|
|
1378
1895
|
return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
|
|
1379
1896
|
}
|
|
1380
1897
|
});
|
|
1381
1898
|
console.log(pc2.green(" \u2713 Boilerplate copied successfully"));
|
|
1382
1899
|
}
|
|
1383
|
-
async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
1900
|
+
async function setupBoilerplate(boilerplateSource, outputDir, target = "nuxt") {
|
|
1384
1901
|
if (!boilerplateSource) {
|
|
1385
|
-
console.log(pc2.blue(
|
|
1902
|
+
console.log(pc2.blue(`
|
|
1903
|
+
\u{1F4E6} Creating minimal ${target === "astro-vue" ? "Astro + Vue" : "Nuxt"} structure...`));
|
|
1386
1904
|
await fs4.ensureDir(outputDir);
|
|
1387
|
-
await fs4.ensureDir(
|
|
1388
|
-
await fs4.ensureDir(
|
|
1389
|
-
await fs4.ensureDir(
|
|
1390
|
-
await fs4.ensureDir(
|
|
1391
|
-
const configPath =
|
|
1905
|
+
await fs4.ensureDir(target === "astro-vue" ? path6.join(outputDir, "src", "pages") : path6.join(outputDir, "pages"));
|
|
1906
|
+
await fs4.ensureDir(path6.join(outputDir, "assets"));
|
|
1907
|
+
await fs4.ensureDir(path6.join(outputDir, "public"));
|
|
1908
|
+
await fs4.ensureDir(path6.join(outputDir, "utils"));
|
|
1909
|
+
const configPath = path6.join(outputDir, target === "astro-vue" ? "astro.config.mjs" : "nuxt.config.ts");
|
|
1392
1910
|
const configExists = await fs4.pathExists(configPath);
|
|
1393
1911
|
if (!configExists) {
|
|
1394
|
-
const basicConfig = `
|
|
1912
|
+
const basicConfig = target === "astro-vue" ? `import { defineConfig } from 'astro/config';
|
|
1913
|
+
import vue from '@astrojs/vue';
|
|
1914
|
+
import path from 'node:path';
|
|
1915
|
+
import { fileURLToPath } from 'node:url';
|
|
1916
|
+
|
|
1917
|
+
export default defineConfig({
|
|
1918
|
+
integrations: [vue()],
|
|
1919
|
+
vite: {
|
|
1920
|
+
resolve: {
|
|
1921
|
+
alias: {
|
|
1922
|
+
'~': path.dirname(fileURLToPath(import.meta.url)),
|
|
1923
|
+
},
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1926
|
+
});
|
|
1927
|
+
` : `export default defineNuxtConfig({
|
|
1395
1928
|
devtools: { enabled: true },
|
|
1396
1929
|
css: [],
|
|
1397
1930
|
})
|
|
1398
1931
|
`;
|
|
1399
1932
|
await fs4.writeFile(configPath, basicConfig, "utf-8");
|
|
1400
1933
|
}
|
|
1934
|
+
const packageJsonPath = path6.join(outputDir, "package.json");
|
|
1935
|
+
const packageJsonExists = await fs4.pathExists(packageJsonPath);
|
|
1936
|
+
if (!packageJsonExists) {
|
|
1937
|
+
const packageName = path6.basename(outputDir) || (target === "astro-vue" ? "see-ms-astro-site" : "see-ms-nuxt-site");
|
|
1938
|
+
await fs4.writeJson(packageJsonPath, target === "astro-vue" ? {
|
|
1939
|
+
name: packageName,
|
|
1940
|
+
private: true,
|
|
1941
|
+
type: "module",
|
|
1942
|
+
scripts: {
|
|
1943
|
+
dev: "astro dev",
|
|
1944
|
+
build: "astro build",
|
|
1945
|
+
preview: "astro preview"
|
|
1946
|
+
},
|
|
1947
|
+
dependencies: {
|
|
1948
|
+
"@astrojs/vue": "^5.0.0",
|
|
1949
|
+
astro: "^5.0.0",
|
|
1950
|
+
vue: "^3.5.14"
|
|
1951
|
+
},
|
|
1952
|
+
devDependencies: {
|
|
1953
|
+
typescript: "^5.8.3"
|
|
1954
|
+
}
|
|
1955
|
+
} : {
|
|
1956
|
+
name: packageName,
|
|
1957
|
+
private: true,
|
|
1958
|
+
type: "module",
|
|
1959
|
+
scripts: {
|
|
1960
|
+
dev: "nuxt dev",
|
|
1961
|
+
build: "nuxt build",
|
|
1962
|
+
generate: "nuxt generate",
|
|
1963
|
+
preview: "nuxt preview",
|
|
1964
|
+
postinstall: "nuxt prepare"
|
|
1965
|
+
},
|
|
1966
|
+
dependencies: {
|
|
1967
|
+
nuxt: "^3.17.4",
|
|
1968
|
+
vue: "^3.5.14",
|
|
1969
|
+
"vue-router": "^4.5.1"
|
|
1970
|
+
},
|
|
1971
|
+
devDependencies: {
|
|
1972
|
+
typescript: "^5.8.3"
|
|
1973
|
+
}
|
|
1974
|
+
}, { spaces: 2 });
|
|
1975
|
+
}
|
|
1401
1976
|
console.log(pc2.green(" \u2713 Structure created"));
|
|
1402
1977
|
return;
|
|
1403
1978
|
}
|
|
@@ -1415,18 +1990,98 @@ async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
|
1415
1990
|
|
|
1416
1991
|
// src/manifest.ts
|
|
1417
1992
|
import fs6 from "fs-extra";
|
|
1418
|
-
import
|
|
1993
|
+
import path9 from "path";
|
|
1419
1994
|
|
|
1420
1995
|
// src/detector.ts
|
|
1421
1996
|
import * as cheerio2 from "cheerio";
|
|
1422
1997
|
import fs5 from "fs-extra";
|
|
1423
|
-
import
|
|
1424
|
-
|
|
1425
|
-
|
|
1998
|
+
import path8 from "path";
|
|
1999
|
+
import { glob as glob2 } from "glob";
|
|
2000
|
+
|
|
2001
|
+
// src/routes.ts
|
|
2002
|
+
import path7 from "path";
|
|
2003
|
+
function htmlPathToPageId(htmlPath) {
|
|
2004
|
+
const withoutExt = htmlPath.replace(/\.html$/i, "");
|
|
2005
|
+
return withoutExt.replace(/[\\/]/g, "-");
|
|
1426
2006
|
}
|
|
1427
|
-
function
|
|
1428
|
-
|
|
1429
|
-
|
|
2007
|
+
function htmlPathToRoute(htmlPath) {
|
|
2008
|
+
const normalized = htmlPath.replace(/\\/g, "/").replace(/\.html$/i, "");
|
|
2009
|
+
if (normalized === "index" || normalized.endsWith("/index")) {
|
|
2010
|
+
const parent = normalized.replace(/(^|\/)index$/, "");
|
|
2011
|
+
return parent ? `/${parent}` : "/";
|
|
2012
|
+
}
|
|
2013
|
+
return `/${normalized}`;
|
|
2014
|
+
}
|
|
2015
|
+
function htmlPathToVuePath(htmlPath) {
|
|
2016
|
+
return htmlPath.replace(/\.html$/i, ".vue");
|
|
2017
|
+
}
|
|
2018
|
+
function getPageRouteInfo(htmlPath) {
|
|
2019
|
+
return {
|
|
2020
|
+
sourcePath: htmlPath,
|
|
2021
|
+
pageId: htmlPathToPageId(htmlPath),
|
|
2022
|
+
route: htmlPathToRoute(htmlPath),
|
|
2023
|
+
outputPath: path7.posix.join("pages", htmlPathToVuePath(htmlPath).replace(/\\/g, "/"))
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/detector.ts
|
|
2028
|
+
var TEXT_SELECTORS = [
|
|
2029
|
+
"h1",
|
|
2030
|
+
"h2",
|
|
2031
|
+
"h3",
|
|
2032
|
+
"h4",
|
|
2033
|
+
"h5",
|
|
2034
|
+
"h6",
|
|
2035
|
+
"p",
|
|
2036
|
+
"span",
|
|
2037
|
+
"li",
|
|
2038
|
+
"blockquote",
|
|
2039
|
+
"figcaption",
|
|
2040
|
+
"label",
|
|
2041
|
+
"td",
|
|
2042
|
+
"th",
|
|
2043
|
+
"dt",
|
|
2044
|
+
"dd",
|
|
2045
|
+
"cite",
|
|
2046
|
+
"q"
|
|
2047
|
+
// div is NOT included here - handled separately to only detect text-only divs
|
|
2048
|
+
];
|
|
2049
|
+
var IGNORE_PATTERNS = [
|
|
2050
|
+
".sr-only",
|
|
2051
|
+
".visually-hidden",
|
|
2052
|
+
'[aria-hidden="true"]',
|
|
2053
|
+
"script",
|
|
2054
|
+
"style",
|
|
2055
|
+
"noscript",
|
|
2056
|
+
"template"
|
|
2057
|
+
];
|
|
2058
|
+
var DECORATIVE_CLASS_PATTERNS = [
|
|
2059
|
+
"icon",
|
|
2060
|
+
"arrow",
|
|
2061
|
+
"pagination",
|
|
2062
|
+
"breadcrumb",
|
|
2063
|
+
"loader",
|
|
2064
|
+
"spinner",
|
|
2065
|
+
"skeleton",
|
|
2066
|
+
"placeholder"
|
|
2067
|
+
];
|
|
2068
|
+
function isCollectionClass(className, customClasses) {
|
|
2069
|
+
if (!customClasses || customClasses.length === 0) return false;
|
|
2070
|
+
const normalizedName = className.toLowerCase().replace(/-/g, "_");
|
|
2071
|
+
for (const customClass of customClasses) {
|
|
2072
|
+
const normalizedCustom = customClass.toLowerCase().replace(/-/g, "_");
|
|
2073
|
+
if (normalizedName === normalizedCustom || normalizedName.includes(normalizedCustom)) {
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
function cleanClassName(className) {
|
|
2080
|
+
return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
|
|
2081
|
+
}
|
|
2082
|
+
function getPrimaryClass(classAttr) {
|
|
2083
|
+
if (!classAttr) return null;
|
|
2084
|
+
const cleaned = cleanClassName(classAttr);
|
|
1430
2085
|
const classes = cleaned.split(" ").filter((c) => c.length > 0);
|
|
1431
2086
|
if (classes.length === 0) return null;
|
|
1432
2087
|
const original = classes[0];
|
|
@@ -1437,22 +2092,6 @@ function getPrimaryClass(classAttr) {
|
|
|
1437
2092
|
// Normalize for field name
|
|
1438
2093
|
};
|
|
1439
2094
|
}
|
|
1440
|
-
function getContextModifier(_$, $el) {
|
|
1441
|
-
let $current = $el.parent();
|
|
1442
|
-
let depth = 0;
|
|
1443
|
-
while ($current.length > 0 && depth < 5) {
|
|
1444
|
-
const classes = $current.attr("class");
|
|
1445
|
-
if (classes) {
|
|
1446
|
-
const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
|
|
1447
|
-
if (ccClass) {
|
|
1448
|
-
return ccClass.replace("cc-", "").replace(/-/g, "_");
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
$current = $current.parent();
|
|
1452
|
-
depth++;
|
|
1453
|
-
}
|
|
1454
|
-
return null;
|
|
1455
|
-
}
|
|
1456
2095
|
function isDecorativeImage(_$, $img) {
|
|
1457
2096
|
const $parent = $img.parent();
|
|
1458
2097
|
const parentClass = $parent.attr("class") || "";
|
|
@@ -1473,9 +2112,136 @@ function isDecorativeImage(_$, $img) {
|
|
|
1473
2112
|
}
|
|
1474
2113
|
function isInsideButton($, el) {
|
|
1475
2114
|
const $el = $(el);
|
|
1476
|
-
const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
|
|
2115
|
+
const $button = $el.closest("button, a, NuxtLink, nuxt-link, .c_button, .c_icon_button");
|
|
1477
2116
|
return $button.length > 0;
|
|
1478
2117
|
}
|
|
2118
|
+
function shouldIgnoreElement(_$, $el, options = {}) {
|
|
2119
|
+
if ($el.attr("data-cms-ignore") !== void 0) return true;
|
|
2120
|
+
for (const pattern of IGNORE_PATTERNS) {
|
|
2121
|
+
if ($el.is(pattern)) return true;
|
|
2122
|
+
if ($el.closest(pattern).length > 0) return true;
|
|
2123
|
+
}
|
|
2124
|
+
for (const selector of options.ignoreSelectors || []) {
|
|
2125
|
+
if ($el.is(selector)) return true;
|
|
2126
|
+
if ($el.closest(selector).length > 0) return true;
|
|
2127
|
+
}
|
|
2128
|
+
const className = $el.attr("class") || "";
|
|
2129
|
+
for (const ignoredClass of options.ignoreClasses || []) {
|
|
2130
|
+
if (className.split(/\s+/).includes(ignoredClass)) return true;
|
|
2131
|
+
}
|
|
2132
|
+
for (const pattern of DECORATIVE_CLASS_PATTERNS) {
|
|
2133
|
+
if (className.toLowerCase().includes(pattern)) return true;
|
|
2134
|
+
}
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
function isEditableLeaf($el) {
|
|
2138
|
+
if ($el.children().length > 0) {
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
2141
|
+
const text = $el.text().trim();
|
|
2142
|
+
if (text.length === 0) {
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
var globalFieldIndex = 0;
|
|
2148
|
+
function resetGlobalFieldIndex() {
|
|
2149
|
+
globalFieldIndex = 0;
|
|
2150
|
+
}
|
|
2151
|
+
function generateFieldName(_$, $el, elementType, _index) {
|
|
2152
|
+
const dataCms = $el.attr("data-cms");
|
|
2153
|
+
if (dataCms) return dataCms.replace(/-/g, "_");
|
|
2154
|
+
const id = $el.attr("id");
|
|
2155
|
+
if (id) return id.replace(/-/g, "_");
|
|
2156
|
+
const ariaLabel = $el.attr("aria-label");
|
|
2157
|
+
if (ariaLabel) return ariaLabel.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase();
|
|
2158
|
+
const classInfo = getPrimaryClass($el.attr("class"));
|
|
2159
|
+
if (classInfo && !classInfo.fieldName.startsWith("w_") && !classInfo.fieldName.startsWith("c_")) {
|
|
2160
|
+
return classInfo.fieldName;
|
|
2161
|
+
}
|
|
2162
|
+
const $parent = $el.parent();
|
|
2163
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
2164
|
+
if (parentClassInfo && !parentClassInfo.fieldName.startsWith("w_") && !parentClassInfo.fieldName.startsWith("c_")) {
|
|
2165
|
+
return `${parentClassInfo.fieldName}_${elementType}`;
|
|
2166
|
+
}
|
|
2167
|
+
const $section = $el.closest('section, [class*="section"], [class*="hero"], [class*="cta"], [class*="about"]').first();
|
|
2168
|
+
const sectionClassInfo = getPrimaryClass($section.attr("class"));
|
|
2169
|
+
if (sectionClassInfo && $section.length > 0) {
|
|
2170
|
+
return `${sectionClassInfo.fieldName}_${elementType}`;
|
|
2171
|
+
}
|
|
2172
|
+
const text = $el.text().trim();
|
|
2173
|
+
if (text.length > 0 && text.length < 50) {
|
|
2174
|
+
const words = text.split(/\s+/).slice(0, 3);
|
|
2175
|
+
const contentName = words.join("_").toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
2176
|
+
if (contentName.length > 2 && contentName.length < 30) {
|
|
2177
|
+
return `${elementType}_${contentName}`;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return `${elementType}_${globalFieldIndex++}`;
|
|
2181
|
+
}
|
|
2182
|
+
function buildUniqueSelector($, $el) {
|
|
2183
|
+
const tag = ($el.prop("tagName") || "div").toLowerCase();
|
|
2184
|
+
const id = $el.attr("id");
|
|
2185
|
+
if (id) {
|
|
2186
|
+
const selector = `#${id}`;
|
|
2187
|
+
if ($(selector).length === 1) return selector;
|
|
2188
|
+
}
|
|
2189
|
+
const dataCms = $el.attr("data-cms");
|
|
2190
|
+
if (dataCms) {
|
|
2191
|
+
const selector = `[data-cms="${dataCms}"]`;
|
|
2192
|
+
if ($(selector).length === 1) return selector;
|
|
2193
|
+
}
|
|
2194
|
+
const className = $el.attr("class");
|
|
2195
|
+
if (className) {
|
|
2196
|
+
const classes = className.split(" ").filter((c) => c.length > 2 && !c.startsWith("w-"));
|
|
2197
|
+
for (const cls of classes) {
|
|
2198
|
+
const selector = `.${cls}`;
|
|
2199
|
+
if ($(selector).length === 1) return selector;
|
|
2200
|
+
}
|
|
2201
|
+
for (const cls of classes) {
|
|
2202
|
+
const selector = `${tag}.${cls}`;
|
|
2203
|
+
if ($(selector).length === 1) return selector;
|
|
2204
|
+
}
|
|
2205
|
+
for (let i = 2; i <= Math.min(classes.length, 3); i++) {
|
|
2206
|
+
const combo = classes.slice(0, i).map((c) => `.${c}`).join("");
|
|
2207
|
+
if ($(combo).length === 1) return combo;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return buildFullPath($, $el);
|
|
2211
|
+
}
|
|
2212
|
+
function buildFullPath(_$, $el) {
|
|
2213
|
+
const parts = [];
|
|
2214
|
+
let current = $el;
|
|
2215
|
+
while (current.length && current.prop("tagName")) {
|
|
2216
|
+
const tag = (current.prop("tagName") || "").toLowerCase();
|
|
2217
|
+
if (!tag || tag === "html" || tag === "body") break;
|
|
2218
|
+
const $parent = current.parent();
|
|
2219
|
+
const $siblings = $parent.children(tag);
|
|
2220
|
+
let part = tag;
|
|
2221
|
+
if ($siblings.length > 1) {
|
|
2222
|
+
const index = $siblings.index(current) + 1;
|
|
2223
|
+
part = `${tag}:nth-of-type(${index})`;
|
|
2224
|
+
}
|
|
2225
|
+
parts.unshift(part);
|
|
2226
|
+
current = $parent;
|
|
2227
|
+
if (parts.length >= 4) break;
|
|
2228
|
+
}
|
|
2229
|
+
return parts.join(" > ");
|
|
2230
|
+
}
|
|
2231
|
+
function determineFieldType($el, tagName) {
|
|
2232
|
+
const dataCmsType = $el.attr("data-cms-type");
|
|
2233
|
+
if (dataCmsType) return dataCmsType;
|
|
2234
|
+
const hasFormatting = $el.find("strong, em, b, i, br, a").length > 0;
|
|
2235
|
+
const innerHTML = $el.html() || "";
|
|
2236
|
+
const hasHtmlTags = /<[^>]+>/.test(innerHTML);
|
|
2237
|
+
if (hasFormatting || hasHtmlTags) {
|
|
2238
|
+
return "rich";
|
|
2239
|
+
}
|
|
2240
|
+
if (tagName === "a" || tagName === "nuxt-link" || $el.is("NuxtLink")) {
|
|
2241
|
+
return "link";
|
|
2242
|
+
}
|
|
2243
|
+
return "plain";
|
|
2244
|
+
}
|
|
1479
2245
|
function extractTemplateFromVue(vueContent) {
|
|
1480
2246
|
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1481
2247
|
if (!templateMatch) {
|
|
@@ -1483,27 +2249,68 @@ function extractTemplateFromVue(vueContent) {
|
|
|
1483
2249
|
}
|
|
1484
2250
|
return templateMatch[1];
|
|
1485
2251
|
}
|
|
1486
|
-
function detectEditableFields(templateHtml) {
|
|
2252
|
+
function detectEditableFields(templateHtml, options = {}) {
|
|
1487
2253
|
const $ = cheerio2.load(templateHtml);
|
|
1488
2254
|
const detectedFields = {};
|
|
1489
2255
|
const detectedCollections = {};
|
|
2256
|
+
const { collectionClasses, collectionMin = 2, universalDetection = true } = options;
|
|
2257
|
+
resetGlobalFieldIndex();
|
|
1490
2258
|
const collectionElements = /* @__PURE__ */ new Set();
|
|
1491
|
-
const
|
|
2259
|
+
const processedElements = /* @__PURE__ */ new Set();
|
|
2260
|
+
const usedFieldNames = /* @__PURE__ */ new Set();
|
|
2261
|
+
const getUniqueFieldName = (baseName) => {
|
|
2262
|
+
let name = baseName;
|
|
2263
|
+
let counter = 1;
|
|
2264
|
+
while (usedFieldNames.has(name)) {
|
|
2265
|
+
name = `${baseName}_${counter++}`;
|
|
2266
|
+
}
|
|
2267
|
+
usedFieldNames.add(name);
|
|
2268
|
+
return name;
|
|
2269
|
+
};
|
|
2270
|
+
$("[data-cms]").each((_, el) => {
|
|
2271
|
+
const $el = $(el);
|
|
2272
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2273
|
+
const fieldName = $el.attr("data-cms").replace(/-/g, "_");
|
|
2274
|
+
const tagName = ($el.prop("tagName") || "div").toLowerCase();
|
|
2275
|
+
const fieldType = determineFieldType($el, tagName);
|
|
2276
|
+
const selector = buildUniqueSelector($, $el);
|
|
2277
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2278
|
+
selector,
|
|
2279
|
+
type: fieldType,
|
|
2280
|
+
editable: true,
|
|
2281
|
+
source: "attribute"
|
|
2282
|
+
};
|
|
2283
|
+
processedElements.add(el);
|
|
2284
|
+
});
|
|
1492
2285
|
const potentialCollections = /* @__PURE__ */ new Map();
|
|
1493
|
-
$("[
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
potentialCollections.get(primaryClass.fieldName)?.push(el);
|
|
2286
|
+
$("[data-cms-collection]").each((_, el) => {
|
|
2287
|
+
const $el = $(el);
|
|
2288
|
+
const collectionName = $el.attr("data-cms-collection");
|
|
2289
|
+
const normalizedName = collectionName.replace(/-/g, "_");
|
|
2290
|
+
if (!potentialCollections.has(normalizedName)) {
|
|
2291
|
+
potentialCollections.set(normalizedName, []);
|
|
1500
2292
|
}
|
|
2293
|
+
potentialCollections.get(normalizedName)?.push(el);
|
|
1501
2294
|
});
|
|
2295
|
+
if (collectionClasses && collectionClasses.length > 0) {
|
|
2296
|
+
$("[class]").each((_, el) => {
|
|
2297
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
2298
|
+
if (!primaryClass) return;
|
|
2299
|
+
if (primaryClass.fieldName.includes("image")) return;
|
|
2300
|
+
if (primaryClass.fieldName.includes("inner")) return;
|
|
2301
|
+
if (primaryClass.fieldName.includes("wrapper") && !primaryClass.fieldName.includes("card")) return;
|
|
2302
|
+
if (isCollectionClass(primaryClass.fieldName, collectionClasses)) {
|
|
2303
|
+
if (!potentialCollections.has(primaryClass.fieldName)) {
|
|
2304
|
+
potentialCollections.set(primaryClass.fieldName, []);
|
|
2305
|
+
}
|
|
2306
|
+
potentialCollections.get(primaryClass.fieldName)?.push(el);
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
1502
2310
|
potentialCollections.forEach((elements, className) => {
|
|
1503
|
-
if (elements.length >=
|
|
2311
|
+
if (elements.length >= collectionMin) {
|
|
1504
2312
|
const $first = $(elements[0]);
|
|
1505
2313
|
const collectionFields = {};
|
|
1506
|
-
processedCollectionClasses.add(className);
|
|
1507
2314
|
elements.forEach((el) => {
|
|
1508
2315
|
collectionElements.add(el);
|
|
1509
2316
|
$(el).find("*").each((_, child) => {
|
|
@@ -1518,36 +2325,35 @@ function detectEditableFields(templateHtml) {
|
|
|
1518
2325
|
const $parent = $img.parent();
|
|
1519
2326
|
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
1520
2327
|
if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
|
|
1521
|
-
collectionFields.image = `.${parentClassInfo.selector}
|
|
2328
|
+
collectionFields.image = { selector: `.${parentClassInfo.selector}`, type: "image", attribute: "src" };
|
|
2329
|
+
return false;
|
|
2330
|
+
} else {
|
|
2331
|
+
collectionFields.image = { selector: "img", type: "image", attribute: "src" };
|
|
1522
2332
|
return false;
|
|
1523
2333
|
}
|
|
1524
2334
|
});
|
|
1525
|
-
$first.find("
|
|
2335
|
+
$first.find('[class*="tag"]').not('[class*="container"]').first().each((_, el) => {
|
|
1526
2336
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1527
|
-
if (classInfo
|
|
1528
|
-
collectionFields.tag = `.${classInfo.selector}
|
|
1529
|
-
return false;
|
|
2337
|
+
if (classInfo) {
|
|
2338
|
+
collectionFields.tag = { selector: `.${classInfo.selector}`, type: "plain" };
|
|
1530
2339
|
}
|
|
1531
2340
|
});
|
|
1532
2341
|
$first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
|
|
1533
2342
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
2343
|
+
const selector = classInfo ? `.${classInfo.selector}` : el.tagName?.toLowerCase() || "h2";
|
|
2344
|
+
collectionFields.title = { selector, type: "plain" };
|
|
1537
2345
|
});
|
|
1538
2346
|
$first.find("p").first().each((_, el) => {
|
|
1539
2347
|
const classInfo = getPrimaryClass($(el).attr("class"));
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
}
|
|
2348
|
+
const selector = classInfo ? `.${classInfo.selector}` : "p";
|
|
2349
|
+
collectionFields.description = { selector, type: "plain" };
|
|
1543
2350
|
});
|
|
1544
|
-
$first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
|
|
2351
|
+
$first.find("a, NuxtLink, nuxt-link").not(".c_button, .c_icon_button").first().each((_, el) => {
|
|
1545
2352
|
const $link = $(el);
|
|
1546
2353
|
const linkText = $link.text().trim();
|
|
1547
2354
|
if (linkText) {
|
|
1548
2355
|
const classInfo = getPrimaryClass($link.attr("class"));
|
|
1549
|
-
collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
|
|
1550
|
-
return false;
|
|
2356
|
+
collectionFields.link = { selector: classInfo ? `.${classInfo.selector}` : "a", type: "link", attribute: "href" };
|
|
1551
2357
|
}
|
|
1552
2358
|
});
|
|
1553
2359
|
if (Object.keys(collectionFields).length > 0) {
|
|
@@ -1562,103 +2368,102 @@ function detectEditableFields(templateHtml) {
|
|
|
1562
2368
|
}
|
|
1563
2369
|
}
|
|
1564
2370
|
});
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
} else if (modifier) {
|
|
1585
|
-
fieldName = `${modifier}_heading`;
|
|
1586
|
-
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1587
|
-
} else {
|
|
1588
|
-
fieldName = `heading_${index}`;
|
|
1589
|
-
selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
detectedFields[fieldName] = {
|
|
2371
|
+
if (universalDetection) {
|
|
2372
|
+
const $body = $("body");
|
|
2373
|
+
let textIndex = 0;
|
|
2374
|
+
let imageIndex = 0;
|
|
2375
|
+
let linkIndex = 0;
|
|
2376
|
+
const allTextSelectors = [...TEXT_SELECTORS, "div"].join(", ");
|
|
2377
|
+
$body.find(allTextSelectors).each((_, el) => {
|
|
2378
|
+
if (collectionElements.has(el)) return;
|
|
2379
|
+
if (processedElements.has(el)) return;
|
|
2380
|
+
const $el = $(el);
|
|
2381
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2382
|
+
const tagName = ($el.prop("tagName") || "div").toLowerCase();
|
|
2383
|
+
if (tagName === "a" || tagName === "nuxt-link" || $el.is("NuxtLink")) return;
|
|
2384
|
+
if (isInsideButton($, el)) return;
|
|
2385
|
+
if (!isEditableLeaf($el)) return;
|
|
2386
|
+
const fieldName = generateFieldName($, $el, tagName, textIndex++);
|
|
2387
|
+
const fieldType = determineFieldType($el, tagName);
|
|
2388
|
+
const selector = buildUniqueSelector($, $el);
|
|
2389
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
1593
2390
|
selector,
|
|
1594
|
-
type:
|
|
1595
|
-
editable: true
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
1599
|
-
$body.find("p").each((_index, el) => {
|
|
1600
|
-
if (collectionElements.has(el)) return;
|
|
1601
|
-
const $el = $(el);
|
|
1602
|
-
const text = $el.text().trim();
|
|
1603
|
-
const classInfo = getPrimaryClass($el.attr("class"));
|
|
1604
|
-
if (text && text.length > 20 && classInfo) {
|
|
1605
|
-
const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
|
|
1606
|
-
detectedFields[classInfo.fieldName] = {
|
|
1607
|
-
selector: `.${classInfo.selector}`,
|
|
1608
|
-
type: hasFormatting ? "rich" : "plain",
|
|
1609
|
-
editable: true
|
|
2391
|
+
type: fieldType,
|
|
2392
|
+
editable: true,
|
|
2393
|
+
source: "auto"
|
|
1610
2394
|
};
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
selector: `.${parentClassInfo.selector}`,
|
|
2395
|
+
processedElements.add(el);
|
|
2396
|
+
});
|
|
2397
|
+
$body.find("img").each((_, el) => {
|
|
2398
|
+
if (collectionElements.has(el)) return;
|
|
2399
|
+
if (processedElements.has(el)) return;
|
|
2400
|
+
const $el = $(el);
|
|
2401
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2402
|
+
if (isDecorativeImage($, $el)) return;
|
|
2403
|
+
const fieldName = generateFieldName($, $el, "image", imageIndex++);
|
|
2404
|
+
const selector = buildUniqueSelector($, $el);
|
|
2405
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2406
|
+
selector,
|
|
1624
2407
|
type: "image",
|
|
1625
|
-
editable: true
|
|
2408
|
+
editable: true,
|
|
2409
|
+
source: "auto",
|
|
2410
|
+
attribute: "src"
|
|
1626
2411
|
};
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const
|
|
1638
|
-
const
|
|
1639
|
-
detectedFields[fieldName] = {
|
|
1640
|
-
selector
|
|
2412
|
+
processedElements.add(el);
|
|
2413
|
+
});
|
|
2414
|
+
$body.find("a, NuxtLink, nuxt-link").each((_, el) => {
|
|
2415
|
+
if (collectionElements.has(el)) return;
|
|
2416
|
+
if (processedElements.has(el)) return;
|
|
2417
|
+
const $el = $(el);
|
|
2418
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2419
|
+
const hasOnlyImage = $el.children().length === 1 && $el.find("img").length === 1;
|
|
2420
|
+
const linkText = $el.text().trim();
|
|
2421
|
+
if (!hasOnlyImage && (!linkText || linkText.length < 2)) return;
|
|
2422
|
+
const fieldName = generateFieldName($, $el, "link", linkIndex++);
|
|
2423
|
+
const selector = buildUniqueSelector($, $el);
|
|
2424
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2425
|
+
selector,
|
|
2426
|
+
type: "link",
|
|
2427
|
+
editable: true,
|
|
2428
|
+
source: "auto",
|
|
2429
|
+
attribute: "href"
|
|
2430
|
+
};
|
|
2431
|
+
processedElements.add(el);
|
|
2432
|
+
});
|
|
2433
|
+
$body.find('button, .c_button, [class*="button"]').each((_, el) => {
|
|
2434
|
+
if (collectionElements.has(el)) return;
|
|
2435
|
+
if (processedElements.has(el)) return;
|
|
2436
|
+
const $el = $(el);
|
|
2437
|
+
if (shouldIgnoreElement($, $el, options)) return;
|
|
2438
|
+
const text = $el.clone().children().remove().end().text().trim();
|
|
2439
|
+
if (!text || text.length < 2) return;
|
|
2440
|
+
const fieldName = generateFieldName($, $el, "button", textIndex++);
|
|
2441
|
+
const selector = buildUniqueSelector($, $el);
|
|
2442
|
+
detectedFields[getUniqueFieldName(fieldName)] = {
|
|
2443
|
+
selector,
|
|
1641
2444
|
type: "plain",
|
|
1642
|
-
editable: true
|
|
2445
|
+
editable: true,
|
|
2446
|
+
source: "auto"
|
|
1643
2447
|
};
|
|
1644
|
-
|
|
1645
|
-
|
|
2448
|
+
processedElements.add(el);
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
1646
2451
|
return {
|
|
1647
2452
|
fields: detectedFields,
|
|
1648
2453
|
collections: detectedCollections
|
|
1649
2454
|
};
|
|
1650
2455
|
}
|
|
1651
|
-
async function analyzeVuePages(pagesDir) {
|
|
2456
|
+
async function analyzeVuePages(pagesDir, options = {}) {
|
|
1652
2457
|
const results = {};
|
|
1653
|
-
const vueFiles = await
|
|
2458
|
+
const vueFiles = await glob2("**/*.vue", { cwd: pagesDir, nodir: true });
|
|
1654
2459
|
for (const file of vueFiles) {
|
|
1655
2460
|
if (file.endsWith(".vue")) {
|
|
1656
|
-
const filePath =
|
|
2461
|
+
const filePath = path8.join(pagesDir, file);
|
|
1657
2462
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
1658
2463
|
const template = extractTemplateFromVue(content);
|
|
1659
2464
|
if (template) {
|
|
1660
|
-
const pageName = file.replace(
|
|
1661
|
-
results[pageName] = detectEditableFields(template);
|
|
2465
|
+
const pageName = htmlPathToPageId(file.replace(/\.vue$/i, ".html"));
|
|
2466
|
+
results[pageName] = detectEditableFields(template, options);
|
|
1662
2467
|
}
|
|
1663
2468
|
}
|
|
1664
2469
|
}
|
|
@@ -1666,35 +2471,147 @@ async function analyzeVuePages(pagesDir) {
|
|
|
1666
2471
|
}
|
|
1667
2472
|
|
|
1668
2473
|
// src/manifest.ts
|
|
1669
|
-
async function generateManifest(pagesDir) {
|
|
1670
|
-
const
|
|
2474
|
+
async function generateManifest(pagesDir, options = {}) {
|
|
2475
|
+
const collectionItemSelectors = options.sharedComponents?.filter((component) => component.role === "collection-item").map((component) => component.selector) || [];
|
|
2476
|
+
const detectionOptions = {
|
|
2477
|
+
collectionClasses: options.collectionClasses,
|
|
2478
|
+
ignoreSelectors: [
|
|
2479
|
+
...options.ignoreSelectors || [],
|
|
2480
|
+
...collectionItemSelectors
|
|
2481
|
+
],
|
|
2482
|
+
ignoreClasses: options.ignoreClasses
|
|
2483
|
+
};
|
|
2484
|
+
const componentDetectionOptions = {
|
|
2485
|
+
collectionClasses: options.collectionClasses,
|
|
2486
|
+
ignoreSelectors: options.ignoreSelectors,
|
|
2487
|
+
ignoreClasses: options.ignoreClasses
|
|
2488
|
+
};
|
|
2489
|
+
const analyzed = await analyzeVuePages(pagesDir, detectionOptions);
|
|
1671
2490
|
const pages = {};
|
|
1672
2491
|
for (const [pageName, detection] of Object.entries(analyzed)) {
|
|
2492
|
+
let collections = detection.collections;
|
|
2493
|
+
if (options.collectionNames && Object.keys(options.collectionNames).length > 0) {
|
|
2494
|
+
collections = {};
|
|
2495
|
+
for (const [collectionKey, collection] of Object.entries(detection.collections)) {
|
|
2496
|
+
let newName = collectionKey;
|
|
2497
|
+
for (const [className, displayName] of Object.entries(options.collectionNames)) {
|
|
2498
|
+
const normalizedClassName = className.replace(/-/g, "_");
|
|
2499
|
+
if (collectionKey.includes(normalizedClassName) || collectionKey === normalizedClassName) {
|
|
2500
|
+
newName = displayName;
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
collections[newName] = collection;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
1673
2507
|
pages[pageName] = {
|
|
1674
2508
|
fields: detection.fields,
|
|
1675
|
-
collections
|
|
2509
|
+
collections,
|
|
1676
2510
|
meta: {
|
|
1677
|
-
route: pageName === "index" ? "/" : `/${pageName}`
|
|
2511
|
+
route: options.pageRoutes?.[pageName] || (pageName === "index" ? "/" : `/${pageName}`)
|
|
1678
2512
|
}
|
|
1679
2513
|
};
|
|
1680
2514
|
}
|
|
1681
2515
|
const manifest = {
|
|
1682
2516
|
version: "1.0",
|
|
1683
|
-
pages
|
|
2517
|
+
pages,
|
|
2518
|
+
global: options.sharedComponents && options.sharedComponents.length > 0 ? {
|
|
2519
|
+
components: Object.fromEntries(
|
|
2520
|
+
options.sharedComponents.map((component) => [component.name, component])
|
|
2521
|
+
)
|
|
2522
|
+
} : void 0,
|
|
2523
|
+
providers: {
|
|
2524
|
+
[options.provider || "strapi"]: {
|
|
2525
|
+
version: "1"
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
1684
2528
|
};
|
|
2529
|
+
if (options.sharedComponents?.length && options.componentsDir) {
|
|
2530
|
+
const globalFields = {};
|
|
2531
|
+
const components = manifest.global?.components || {};
|
|
2532
|
+
for (const component of options.sharedComponents) {
|
|
2533
|
+
const componentPath = path9.join(options.componentsDir, `${component.name}.vue`);
|
|
2534
|
+
if (!await fs6.pathExists(componentPath)) continue;
|
|
2535
|
+
const content = await fs6.readFile(componentPath, "utf-8");
|
|
2536
|
+
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
|
2537
|
+
if (!templateMatch) continue;
|
|
2538
|
+
const detection = detectEditableFields(templateMatch[1], componentDetectionOptions);
|
|
2539
|
+
const prefixedFields = Object.fromEntries(
|
|
2540
|
+
Object.entries(detection.fields).map(([fieldName, field]) => [
|
|
2541
|
+
`${component.name}_${fieldName}`,
|
|
2542
|
+
field
|
|
2543
|
+
])
|
|
2544
|
+
);
|
|
2545
|
+
const collectionFields = Object.fromEntries(
|
|
2546
|
+
Object.entries(detection.fields).map(([fieldName, field]) => [
|
|
2547
|
+
fieldName,
|
|
2548
|
+
{
|
|
2549
|
+
selector: field.selector,
|
|
2550
|
+
type: field.type,
|
|
2551
|
+
attribute: field.attribute
|
|
2552
|
+
}
|
|
2553
|
+
])
|
|
2554
|
+
);
|
|
2555
|
+
const contentMode = component.contentMode || "shared-global";
|
|
2556
|
+
const role = component.role || "shared-section";
|
|
2557
|
+
components[component.name] = {
|
|
2558
|
+
...component,
|
|
2559
|
+
role,
|
|
2560
|
+
contentMode,
|
|
2561
|
+
fields: role === "collection-item" ? detection.fields : prefixedFields
|
|
2562
|
+
};
|
|
2563
|
+
if (role === "collection-item") {
|
|
2564
|
+
for (const pageId of component.pages || []) {
|
|
2565
|
+
if (!manifest.pages[pageId]) continue;
|
|
2566
|
+
const collectionName = resolveCollectionName(component.collectionName || toCollectionName(component.name), pageId);
|
|
2567
|
+
manifest.pages[pageId].collections = {
|
|
2568
|
+
...manifest.pages[pageId].collections,
|
|
2569
|
+
[collectionName]: {
|
|
2570
|
+
selector: component.selector,
|
|
2571
|
+
fields: collectionFields,
|
|
2572
|
+
componentName: component.name,
|
|
2573
|
+
storage: component.collectionStorage || "collection-type"
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
} else if (contentMode === "per-page") {
|
|
2578
|
+
for (const pageId of component.pages || []) {
|
|
2579
|
+
if (!manifest.pages[pageId]) continue;
|
|
2580
|
+
manifest.pages[pageId].fields = {
|
|
2581
|
+
...manifest.pages[pageId].fields,
|
|
2582
|
+
...prefixedFields
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
} else if (contentMode === "shared-global" || contentMode === "auto") {
|
|
2586
|
+
Object.assign(globalFields, prefixedFields);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (manifest.global) {
|
|
2590
|
+
manifest.global.components = components;
|
|
2591
|
+
if (Object.keys(globalFields || {}).length > 0) {
|
|
2592
|
+
manifest.global.fields = globalFields;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
1685
2596
|
return manifest;
|
|
1686
2597
|
}
|
|
2598
|
+
function toCollectionName(name) {
|
|
2599
|
+
const base = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
2600
|
+
if (base.endsWith("s")) return base;
|
|
2601
|
+
return `${base}s`;
|
|
2602
|
+
}
|
|
2603
|
+
function resolveCollectionName(collectionName, pageId) {
|
|
2604
|
+
return collectionName === pageId ? `${collectionName}_items` : collectionName;
|
|
2605
|
+
}
|
|
1687
2606
|
async function writeManifest(outputDir, manifest) {
|
|
1688
2607
|
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
1689
|
-
const
|
|
1690
|
-
await fs6.writeFile(manifestPath, manifestContent, "utf-8");
|
|
1691
|
-
const publicDir = path7.join(outputDir, "public");
|
|
2608
|
+
const publicDir = path9.join(outputDir, "public");
|
|
1692
2609
|
await fs6.ensureDir(publicDir);
|
|
1693
|
-
const publicManifestPath =
|
|
2610
|
+
const publicManifestPath = path9.join(publicDir, "cms-manifest.json");
|
|
1694
2611
|
await fs6.writeFile(publicManifestPath, manifestContent, "utf-8");
|
|
1695
2612
|
}
|
|
1696
2613
|
async function readManifest(outputDir) {
|
|
1697
|
-
const manifestPath =
|
|
2614
|
+
const manifestPath = path9.join(outputDir, "public", "cms-manifest.json");
|
|
1698
2615
|
const content = await fs6.readFile(manifestPath, "utf-8");
|
|
1699
2616
|
return JSON.parse(content);
|
|
1700
2617
|
}
|
|
@@ -1702,18 +2619,47 @@ async function readManifest(outputDir) {
|
|
|
1702
2619
|
// src/vue-transformer.ts
|
|
1703
2620
|
import * as cheerio3 from "cheerio";
|
|
1704
2621
|
import fs7 from "fs-extra";
|
|
1705
|
-
import
|
|
2622
|
+
import path10 from "path";
|
|
2623
|
+
import { glob as glob3 } from "glob";
|
|
2624
|
+
function isSafeToEmpty($el) {
|
|
2625
|
+
return $el.children().length === 0;
|
|
2626
|
+
}
|
|
1706
2627
|
function replaceWithBinding(_$, $el, fieldName, type) {
|
|
1707
2628
|
if (type === "image") {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
$
|
|
1711
|
-
|
|
2629
|
+
if ($el.is("img")) {
|
|
2630
|
+
$el.attr(":src", `content.${fieldName}`);
|
|
2631
|
+
$el.removeAttr("src");
|
|
2632
|
+
} else {
|
|
2633
|
+
const $img = $el.find("img").first();
|
|
2634
|
+
if ($img.length) {
|
|
2635
|
+
$img.attr(":src", `content.${fieldName}`);
|
|
2636
|
+
$img.removeAttr("src");
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
} else if (type === "link") {
|
|
2640
|
+
const $link = $el.is("a") || $el.is("NuxtLink") || $el.is("nuxt-link") ? $el : $el.find("a, NuxtLink, nuxt-link").first();
|
|
2641
|
+
if ($link.length) {
|
|
2642
|
+
const isNuxtLink = $link.is("NuxtLink") || $link.is("nuxt-link");
|
|
2643
|
+
$link.attr(isNuxtLink ? ":to" : ":href", `content.${fieldName}?.url`);
|
|
2644
|
+
$link.attr(":target", `content.${fieldName}?.newTab ? '_blank' : undefined`);
|
|
2645
|
+
$link.removeAttr("href");
|
|
2646
|
+
if (isNuxtLink) $link.removeAttr("to");
|
|
2647
|
+
$link.removeAttr("target");
|
|
2648
|
+
if (isSafeToEmpty($link)) {
|
|
2649
|
+
$link.empty();
|
|
2650
|
+
$link.text(`{{ content.${fieldName}?.text }}`);
|
|
2651
|
+
}
|
|
1712
2652
|
}
|
|
1713
2653
|
} else if (type === "rich") {
|
|
2654
|
+
if (!isSafeToEmpty($el)) {
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
1714
2657
|
$el.attr("v-html", `content.${fieldName}`);
|
|
1715
2658
|
$el.empty();
|
|
1716
2659
|
} else {
|
|
2660
|
+
if (!isSafeToEmpty($el)) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
1717
2663
|
$el.empty();
|
|
1718
2664
|
$el.text(`{{ content.${fieldName} }}`);
|
|
1719
2665
|
}
|
|
@@ -1722,21 +2668,43 @@ function transformCollection($, collectionName, collection) {
|
|
|
1722
2668
|
const $items = $(collection.selector);
|
|
1723
2669
|
if ($items.length === 0) return;
|
|
1724
2670
|
const $first = $items.first();
|
|
2671
|
+
if (collection.componentName) {
|
|
2672
|
+
$first.replaceWith(`<!--COLLECTION_COMPONENT:${collection.componentName}:${collectionName}-->`);
|
|
2673
|
+
$items.slice(1).remove();
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
1725
2676
|
$first.attr("v-for", `(item, index) in content.${collectionName}`);
|
|
1726
2677
|
$first.attr(":key", "index");
|
|
1727
|
-
Object.entries(collection.fields).forEach(([fieldName,
|
|
2678
|
+
Object.entries(collection.fields).forEach(([fieldName, fieldConfig]) => {
|
|
2679
|
+
const selector = typeof fieldConfig === "string" ? fieldConfig : fieldConfig.selector || fieldConfig;
|
|
2680
|
+
const fieldType = typeof fieldConfig === "object" ? fieldConfig.type : void 0;
|
|
1728
2681
|
const $fieldEl = $first.find(selector);
|
|
1729
2682
|
if ($fieldEl.length) {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
$
|
|
2683
|
+
const isImage = fieldType === "image" || fieldName === "image" || fieldName.includes("image");
|
|
2684
|
+
const isLink = fieldType === "link" || fieldName === "link" || fieldName === "url";
|
|
2685
|
+
if (isImage) {
|
|
2686
|
+
if ($fieldEl.is("img")) {
|
|
2687
|
+
$fieldEl.attr(":src", `item.${fieldName}`);
|
|
2688
|
+
$fieldEl.removeAttr("src");
|
|
2689
|
+
} else {
|
|
2690
|
+
const $img = $fieldEl.find("img").first();
|
|
2691
|
+
if ($img.length) {
|
|
2692
|
+
$img.attr(":src", `item.${fieldName}`);
|
|
2693
|
+
$img.removeAttr("src");
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
} else if (isLink) {
|
|
2697
|
+
const $link = $fieldEl.is("a") || $fieldEl.is("NuxtLink") || $fieldEl.is("nuxt-link") ? $fieldEl : $fieldEl.find("a, NuxtLink, nuxt-link").first();
|
|
2698
|
+
if ($link.length) {
|
|
2699
|
+
const isNuxtLink = $link.is("NuxtLink") || $link.is("nuxt-link");
|
|
2700
|
+
$link.attr(isNuxtLink ? ":to" : ":href", `item.${fieldName}?.url`);
|
|
2701
|
+
$link.attr(":target", `item.${fieldName}?.newTab ? '_blank' : undefined`);
|
|
2702
|
+
$link.removeAttr("href");
|
|
2703
|
+
$link.removeAttr("target");
|
|
2704
|
+
$link.removeAttr("to");
|
|
2705
|
+
$link.empty();
|
|
2706
|
+
$link.text(`{{ item.${fieldName}?.text }}`);
|
|
1735
2707
|
}
|
|
1736
|
-
} else if (fieldName === "link") {
|
|
1737
|
-
$fieldEl.attr(":to", "item.link");
|
|
1738
|
-
$fieldEl.removeAttr("to");
|
|
1739
|
-
$fieldEl.removeAttr("href");
|
|
1740
2708
|
} else {
|
|
1741
2709
|
$fieldEl.empty();
|
|
1742
2710
|
$fieldEl.text(`{{ item.${fieldName} }}`);
|
|
@@ -1745,7 +2713,7 @@ function transformCollection($, collectionName, collection) {
|
|
|
1745
2713
|
});
|
|
1746
2714
|
$items.slice(1).remove();
|
|
1747
2715
|
}
|
|
1748
|
-
async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
2716
|
+
async function transformVueToReactive(vueFilePath, pageName, manifest, options = {}) {
|
|
1749
2717
|
const pageManifest = manifest.pages[pageName];
|
|
1750
2718
|
if (!pageManifest) return;
|
|
1751
2719
|
const vueContent = await fs7.readFile(vueFilePath, "utf-8");
|
|
@@ -1755,7 +2723,8 @@ async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
|
1755
2723
|
}
|
|
1756
2724
|
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1757
2725
|
if (!templateMatch) return;
|
|
1758
|
-
const
|
|
2726
|
+
const componentNames = Object.keys(manifest.global?.components || {});
|
|
2727
|
+
const templateContent = maskComponentTags(templateMatch[1], componentNames);
|
|
1759
2728
|
const $ = cheerio3.load(templateContent, { xmlMode: false });
|
|
1760
2729
|
if (pageManifest.collections) {
|
|
1761
2730
|
Object.entries(pageManifest.collections).forEach(([collectionName, collection]) => {
|
|
@@ -1781,8 +2750,19 @@ async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
|
1781
2750
|
if (wrapperDivMatch) {
|
|
1782
2751
|
transformedTemplate = wrapperDivMatch[1].trim();
|
|
1783
2752
|
}
|
|
2753
|
+
const perPageComponentNames = componentNames.filter((name) => {
|
|
2754
|
+
const component = manifest.global?.components?.[name];
|
|
2755
|
+
return component?.contentMode === "per-page" && component.pages.includes(pageName);
|
|
2756
|
+
});
|
|
2757
|
+
transformedTemplate = restoreCollectionComponentTags(transformedTemplate);
|
|
2758
|
+
transformedTemplate = restoreComponentTags2(transformedTemplate, componentNames, perPageComponentNames);
|
|
2759
|
+
const explicitImports = options.target === "astro-vue" ? [
|
|
2760
|
+
`import { useStrapiContent } from '~/src/composables/useStrapiContent';`,
|
|
2761
|
+
...componentNames.map((name) => `import ${name} from '~/components/${name}.vue';`)
|
|
2762
|
+
].join("\n") : "";
|
|
1784
2763
|
const scriptSetup = `<script setup lang="ts">
|
|
1785
2764
|
// Auto-generated reactive content from Strapi
|
|
2765
|
+
${explicitImports}
|
|
1786
2766
|
const { content } = useStrapiContent('${pageName}');
|
|
1787
2767
|
</script>`;
|
|
1788
2768
|
const finalTemplate = transformedTemplate.split("\n").map((line) => " " + line).join("\n");
|
|
@@ -1794,29 +2774,129 @@ ${finalTemplate}
|
|
|
1794
2774
|
`;
|
|
1795
2775
|
await fs7.writeFile(vueFilePath, newVueContent, "utf-8");
|
|
1796
2776
|
}
|
|
1797
|
-
async function
|
|
1798
|
-
const
|
|
2777
|
+
async function transformSharedComponentsToReactive(componentsDir, manifest, options = {}) {
|
|
2778
|
+
const components = manifest.global?.components || {};
|
|
2779
|
+
for (const [componentName, component] of Object.entries(components)) {
|
|
2780
|
+
const fields = component.fields || {};
|
|
2781
|
+
if (Object.keys(fields).length === 0) continue;
|
|
2782
|
+
const filePath = path10.join(componentsDir, `${componentName}.vue`);
|
|
2783
|
+
if (!await fs7.pathExists(filePath)) continue;
|
|
2784
|
+
const vueContent = await fs7.readFile(filePath, "utf-8");
|
|
2785
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
2786
|
+
if (!templateMatch) continue;
|
|
2787
|
+
const $ = cheerio3.load(templateMatch[1], { xmlMode: false });
|
|
2788
|
+
const isCollectionItem = component.role === "collection-item";
|
|
2789
|
+
const isPerPage = component.contentMode === "per-page";
|
|
2790
|
+
const contentSource = isCollectionItem ? "item" : isPerPage ? "componentContent" : "content";
|
|
2791
|
+
Object.entries(fields).forEach(([fieldName, field]) => {
|
|
2792
|
+
const originalName = fieldName.startsWith(`${componentName}_`) ? fieldName.slice(componentName.length + 1) : fieldName;
|
|
2793
|
+
const selector = field.selector;
|
|
2794
|
+
$(selector).each((_, el) => {
|
|
2795
|
+
replaceWithBinding($, $(el), fieldName, field.type);
|
|
2796
|
+
});
|
|
2797
|
+
if (originalName !== fieldName) {
|
|
2798
|
+
$(selector).each((_, el) => {
|
|
2799
|
+
replaceWithBinding($, $(el), fieldName, field.type);
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
let transformedTemplate = $.html();
|
|
2804
|
+
const bodyMatch = transformedTemplate.match(/<body>([\s\S]*)<\/body>/);
|
|
2805
|
+
if (bodyMatch) transformedTemplate = bodyMatch[1];
|
|
2806
|
+
transformedTemplate = transformedTemplate.replace(/<\/?html[^>]*>/gi, "").replace(/<head><\/head>/gi, "").trim();
|
|
2807
|
+
for (const fieldName of Object.keys(fields)) {
|
|
2808
|
+
transformedTemplate = transformedTemplate.replaceAll(`content.${fieldName}`, `${contentSource}.${fieldName}`);
|
|
2809
|
+
}
|
|
2810
|
+
const importLine = !isCollectionItem && !isPerPage && options.target === "astro-vue" ? `import { useStrapiContent } from '~/src/composables/useStrapiContent';
|
|
2811
|
+
` : "";
|
|
2812
|
+
const contentSetup = isCollectionItem ? `defineProps<{ item: Record<string, any> }>();` : isPerPage ? `const props = defineProps<{ content: Record<string, any> }>();
|
|
2813
|
+
const componentContent = props.content || {};` : `const { content } = useStrapiContent('global');`;
|
|
2814
|
+
const scriptSetup = `<script setup lang="ts">
|
|
2815
|
+
${importLine}${contentSetup}
|
|
2816
|
+
</script>`;
|
|
2817
|
+
await fs7.writeFile(filePath, `${scriptSetup}
|
|
2818
|
+
|
|
2819
|
+
<template>
|
|
2820
|
+
${transformedTemplate}
|
|
2821
|
+
</template>
|
|
2822
|
+
`, "utf-8");
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function restoreComponentTags2(html, componentNames, perPageComponentNames = []) {
|
|
2826
|
+
let restored = html;
|
|
2827
|
+
for (const name of componentNames) {
|
|
2828
|
+
const lowered = name.toLowerCase();
|
|
2829
|
+
const tag = perPageComponentNames.includes(name) ? `<${name} :content="content" />` : `<${name} />`;
|
|
2830
|
+
restored = restored.replace(new RegExp(`<!--COMPONENT:${name}-->`, "g"), tag).replace(new RegExp(`<${lowered}\\s*><\\/${lowered}>`, "g"), tag).replace(new RegExp(`<${lowered}\\s*\\/>`, "g"), tag);
|
|
2831
|
+
}
|
|
2832
|
+
return restored;
|
|
2833
|
+
}
|
|
2834
|
+
function restoreCollectionComponentTags(html) {
|
|
2835
|
+
return html.replace(
|
|
2836
|
+
/<!--COLLECTION_COMPONENT:(\w+):([\w-]+)-->/g,
|
|
2837
|
+
(_match, componentName, collectionName) => `<${componentName} v-for="(item, index) in content.${collectionName}" :key="index" :item="item" />`
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
function maskComponentTags(html, componentNames) {
|
|
2841
|
+
let masked = html;
|
|
2842
|
+
for (const name of componentNames) {
|
|
2843
|
+
masked = masked.replace(new RegExp(`<${name}\\s*\\/>`, "g"), `<!--COMPONENT:${name}-->`).replace(new RegExp(`<${name}\\s*>\\s*<\\/${name}>`, "g"), `<!--COMPONENT:${name}-->`);
|
|
2844
|
+
}
|
|
2845
|
+
return masked;
|
|
2846
|
+
}
|
|
2847
|
+
async function transformAllVuePages(pagesDir, manifest, options = {}) {
|
|
2848
|
+
const vueFiles = await glob3("**/*.vue", { cwd: pagesDir, nodir: true });
|
|
1799
2849
|
for (const file of vueFiles) {
|
|
1800
2850
|
if (file.endsWith(".vue")) {
|
|
1801
|
-
const pageName = file.replace(
|
|
1802
|
-
const vueFilePath =
|
|
1803
|
-
await transformVueToReactive(vueFilePath, pageName, manifest);
|
|
2851
|
+
const pageName = htmlPathToPageId(file.replace(/\.vue$/i, ".html"));
|
|
2852
|
+
const vueFilePath = path10.join(pagesDir, file);
|
|
2853
|
+
await transformVueToReactive(vueFilePath, pageName, manifest, options);
|
|
1804
2854
|
}
|
|
1805
2855
|
}
|
|
1806
2856
|
}
|
|
1807
2857
|
|
|
1808
2858
|
// src/transformer.ts
|
|
2859
|
+
var LINK_COMPONENT_SCHEMA = {
|
|
2860
|
+
collectionName: "components_shared_links",
|
|
2861
|
+
info: {
|
|
2862
|
+
displayName: "Link",
|
|
2863
|
+
icon: "link",
|
|
2864
|
+
description: "A link with URL and text"
|
|
2865
|
+
},
|
|
2866
|
+
options: {},
|
|
2867
|
+
attributes: {
|
|
2868
|
+
url: {
|
|
2869
|
+
type: "string",
|
|
2870
|
+
required: true
|
|
2871
|
+
},
|
|
2872
|
+
text: {
|
|
2873
|
+
type: "string",
|
|
2874
|
+
required: true
|
|
2875
|
+
},
|
|
2876
|
+
newTab: {
|
|
2877
|
+
type: "boolean",
|
|
2878
|
+
default: false
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
1809
2882
|
function mapFieldTypeToStrapi(fieldType) {
|
|
2883
|
+
if (fieldType === "link") {
|
|
2884
|
+
return {
|
|
2885
|
+
type: "component",
|
|
2886
|
+
isComponent: true,
|
|
2887
|
+
component: "shared.link"
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
1810
2890
|
const typeMap = {
|
|
1811
2891
|
plain: "string",
|
|
1812
2892
|
rich: "richtext",
|
|
1813
2893
|
html: "richtext",
|
|
1814
2894
|
image: "media",
|
|
1815
|
-
|
|
2895
|
+
icon: "media",
|
|
1816
2896
|
email: "email",
|
|
1817
2897
|
phone: "string"
|
|
1818
2898
|
};
|
|
1819
|
-
return typeMap[fieldType] || "string";
|
|
2899
|
+
return { type: typeMap[fieldType] || "string" };
|
|
1820
2900
|
}
|
|
1821
2901
|
function pluralize(word) {
|
|
1822
2902
|
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
|
|
@@ -1833,12 +2913,21 @@ function pluralize(word) {
|
|
|
1833
2913
|
function pageToStrapiSchema(pageName, fields) {
|
|
1834
2914
|
const attributes = {};
|
|
1835
2915
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2916
|
+
const strapiType = mapFieldTypeToStrapi(field.type);
|
|
2917
|
+
if (strapiType.isComponent) {
|
|
2918
|
+
attributes[fieldName] = {
|
|
2919
|
+
type: "component",
|
|
2920
|
+
component: strapiType.component,
|
|
2921
|
+
repeatable: false
|
|
2922
|
+
};
|
|
2923
|
+
} else {
|
|
2924
|
+
attributes[fieldName] = {
|
|
2925
|
+
type: strapiType.type,
|
|
2926
|
+
required: field.required || false
|
|
2927
|
+
};
|
|
2928
|
+
if (field.default && typeof field.default === "string") {
|
|
2929
|
+
attributes[fieldName].default = field.default;
|
|
2930
|
+
}
|
|
1842
2931
|
}
|
|
1843
2932
|
}
|
|
1844
2933
|
const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
@@ -1860,20 +2949,44 @@ function pageToStrapiSchema(pageName, fields) {
|
|
|
1860
2949
|
}
|
|
1861
2950
|
function collectionToStrapiSchema(collectionName, collection) {
|
|
1862
2951
|
const attributes = {};
|
|
1863
|
-
for (const [fieldName,
|
|
1864
|
-
let
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2952
|
+
for (const [fieldName, fieldConfig] of Object.entries(collection.fields)) {
|
|
2953
|
+
let fieldType;
|
|
2954
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null && "type" in fieldConfig) {
|
|
2955
|
+
fieldType = fieldConfig.type;
|
|
2956
|
+
}
|
|
2957
|
+
if (fieldType) {
|
|
2958
|
+
const strapiType = mapFieldTypeToStrapi(fieldType);
|
|
2959
|
+
if (strapiType.isComponent) {
|
|
2960
|
+
attributes[fieldName] = {
|
|
2961
|
+
type: "component",
|
|
2962
|
+
component: strapiType.component,
|
|
2963
|
+
repeatable: false
|
|
2964
|
+
};
|
|
2965
|
+
} else {
|
|
2966
|
+
attributes[fieldName] = {
|
|
2967
|
+
type: strapiType.type
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
} else {
|
|
2971
|
+
let type = "string";
|
|
2972
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
2973
|
+
type = "media";
|
|
2974
|
+
} else if (fieldName === "description" || fieldName === "content") {
|
|
2975
|
+
type = "richtext";
|
|
2976
|
+
} else if (fieldName === "link" || fieldName === "url") {
|
|
2977
|
+
attributes[fieldName] = {
|
|
2978
|
+
type: "component",
|
|
2979
|
+
component: "shared.link",
|
|
2980
|
+
repeatable: false
|
|
2981
|
+
};
|
|
2982
|
+
continue;
|
|
2983
|
+
} else if (fieldName === "title" || fieldName === "tag") {
|
|
2984
|
+
type = "string";
|
|
2985
|
+
}
|
|
2986
|
+
attributes[fieldName] = {
|
|
2987
|
+
type
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
1877
2990
|
}
|
|
1878
2991
|
const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1879
2992
|
const kebabCaseName = collectionName.replace(/_/g, "-");
|
|
@@ -1892,6 +3005,37 @@ function collectionToStrapiSchema(collectionName, collection) {
|
|
|
1892
3005
|
attributes
|
|
1893
3006
|
};
|
|
1894
3007
|
}
|
|
3008
|
+
function hasLinkFields(manifest) {
|
|
3009
|
+
for (const page of Object.values(manifest.pages)) {
|
|
3010
|
+
if (page.fields) {
|
|
3011
|
+
for (const field of Object.values(page.fields)) {
|
|
3012
|
+
if (field.type === "link") return true;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
if (page.collections) {
|
|
3016
|
+
for (const collection of Object.values(page.collections)) {
|
|
3017
|
+
for (const fieldConfig of Object.values(collection.fields)) {
|
|
3018
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
3019
|
+
if (fieldConfig.type === "link") return true;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
if (manifest.global?.fields) {
|
|
3026
|
+
for (const field of Object.values(manifest.global.fields)) {
|
|
3027
|
+
if (field.type === "link") return true;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (manifest.global?.components) {
|
|
3031
|
+
for (const component of Object.values(manifest.global.components)) {
|
|
3032
|
+
for (const field of Object.values(component.fields || {})) {
|
|
3033
|
+
if (field.type === "link") return true;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
return false;
|
|
3038
|
+
}
|
|
1895
3039
|
function manifestToSchemas(manifest) {
|
|
1896
3040
|
const schemas = {};
|
|
1897
3041
|
for (const [pageName, page] of Object.entries(manifest.pages)) {
|
|
@@ -1904,16 +3048,22 @@ function manifestToSchemas(manifest) {
|
|
|
1904
3048
|
}
|
|
1905
3049
|
}
|
|
1906
3050
|
}
|
|
3051
|
+
if (manifest.global?.fields && Object.keys(manifest.global.fields).length > 0) {
|
|
3052
|
+
schemas["global"] = pageToStrapiSchema("global", manifest.global.fields);
|
|
3053
|
+
}
|
|
1907
3054
|
return schemas;
|
|
1908
3055
|
}
|
|
3056
|
+
function getLinkComponentSchema(manifest) {
|
|
3057
|
+
return hasLinkFields(manifest) ? LINK_COMPONENT_SCHEMA : null;
|
|
3058
|
+
}
|
|
1909
3059
|
|
|
1910
3060
|
// src/schema-writer.ts
|
|
1911
3061
|
import fs8 from "fs-extra";
|
|
1912
|
-
import
|
|
3062
|
+
import path11 from "path";
|
|
1913
3063
|
async function writeStrapiSchema(outputDir, name, schema) {
|
|
1914
|
-
const schemasDir =
|
|
3064
|
+
const schemasDir = path11.join(outputDir, "cms-schemas");
|
|
1915
3065
|
await fs8.ensureDir(schemasDir);
|
|
1916
|
-
const schemaPath =
|
|
3066
|
+
const schemaPath = path11.join(schemasDir, `${name}.json`);
|
|
1917
3067
|
await fs8.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
1918
3068
|
}
|
|
1919
3069
|
async function writeAllSchemas(outputDir, schemas) {
|
|
@@ -1921,8 +3071,14 @@ async function writeAllSchemas(outputDir, schemas) {
|
|
|
1921
3071
|
await writeStrapiSchema(outputDir, name, schema);
|
|
1922
3072
|
}
|
|
1923
3073
|
}
|
|
3074
|
+
async function writeLinkComponentSchema(outputDir) {
|
|
3075
|
+
const componentsDir = path11.join(outputDir, "cms-schemas", "components", "shared");
|
|
3076
|
+
await fs8.ensureDir(componentsDir);
|
|
3077
|
+
const schemaPath = path11.join(componentsDir, "link.json");
|
|
3078
|
+
await fs8.writeFile(schemaPath, JSON.stringify(LINK_COMPONENT_SCHEMA, null, 2), "utf-8");
|
|
3079
|
+
}
|
|
1924
3080
|
async function createStrapiReadme(outputDir) {
|
|
1925
|
-
const readmePath =
|
|
3081
|
+
const readmePath = path11.join(outputDir, "cms-schemas", "README.md");
|
|
1926
3082
|
const content = `# CMS Schemas
|
|
1927
3083
|
|
|
1928
3084
|
Auto-generated Strapi content type schemas from your Webflow export.
|
|
@@ -1997,7 +3153,17 @@ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
|
|
|
1997
3153
|
|
|
1998
3154
|
// src/content-extractor.ts
|
|
1999
3155
|
import * as cheerio4 from "cheerio";
|
|
2000
|
-
|
|
3156
|
+
function extractLinkValue($element) {
|
|
3157
|
+
const href = $element.attr("href") || $element.attr("to") || "";
|
|
3158
|
+
const text = $element.text().trim();
|
|
3159
|
+
const target = $element.attr("target");
|
|
3160
|
+
const newTab = target === "_blank";
|
|
3161
|
+
return {
|
|
3162
|
+
url: href,
|
|
3163
|
+
text,
|
|
3164
|
+
newTab: newTab || void 0
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
2001
3167
|
function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
2002
3168
|
const $ = cheerio4.load(html);
|
|
2003
3169
|
const content = {
|
|
@@ -2012,6 +3178,11 @@ function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
|
2012
3178
|
if (field.type === "image") {
|
|
2013
3179
|
const src = element.attr("src") || element.find("img").attr("src") || "";
|
|
2014
3180
|
content.fields[fieldName] = src;
|
|
3181
|
+
} else if (field.type === "link") {
|
|
3182
|
+
const linkElement = element.is("a") || element.is("NuxtLink") || element.is("nuxt-link") ? element : element.find("a, NuxtLink, nuxt-link").first();
|
|
3183
|
+
if (linkElement.length > 0) {
|
|
3184
|
+
content.fields[fieldName] = extractLinkValue(linkElement);
|
|
3185
|
+
}
|
|
2015
3186
|
} else {
|
|
2016
3187
|
const text = element.text().trim();
|
|
2017
3188
|
content.fields[fieldName] = text;
|
|
@@ -2026,15 +3197,19 @@ function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
|
2026
3197
|
collectionElements.each((_, elem) => {
|
|
2027
3198
|
const item = {};
|
|
2028
3199
|
const $elem = $(elem);
|
|
2029
|
-
for (const [fieldName,
|
|
3200
|
+
for (const [fieldName, fieldConfig] of Object.entries(collection.fields)) {
|
|
3201
|
+
const fieldSelector = typeof fieldConfig === "string" ? fieldConfig : fieldConfig.selector || fieldConfig;
|
|
3202
|
+
const fieldType = typeof fieldConfig === "object" ? fieldConfig.type : void 0;
|
|
2030
3203
|
const fieldElement = $elem.find(fieldSelector).first();
|
|
2031
3204
|
if (fieldElement.length > 0) {
|
|
2032
|
-
if (fieldName === "image" || fieldName.includes("image")) {
|
|
3205
|
+
if (fieldType === "image" || fieldName === "image" || fieldName.includes("image")) {
|
|
2033
3206
|
const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
|
|
2034
3207
|
item[fieldName] = src;
|
|
2035
|
-
} else if (fieldName === "link" || fieldName === "url") {
|
|
2036
|
-
const
|
|
2037
|
-
|
|
3208
|
+
} else if (fieldType === "link" || fieldName === "link" || fieldName === "url") {
|
|
3209
|
+
const linkElement = fieldElement.is("a") || fieldElement.is("NuxtLink") || fieldElement.is("nuxt-link") ? fieldElement : fieldElement.find("a, NuxtLink, nuxt-link").first();
|
|
3210
|
+
if (linkElement.length > 0) {
|
|
3211
|
+
item[fieldName] = extractLinkValue(linkElement);
|
|
3212
|
+
}
|
|
2038
3213
|
} else {
|
|
2039
3214
|
const text = fieldElement.text().trim();
|
|
2040
3215
|
item[fieldName] = text;
|
|
@@ -2063,16 +3238,23 @@ function extractAllContent(htmlFiles, manifest) {
|
|
|
2063
3238
|
extractedContent.pages[pageName] = content;
|
|
2064
3239
|
}
|
|
2065
3240
|
}
|
|
2066
|
-
|
|
3241
|
+
if (manifest.global?.fields) {
|
|
3242
|
+
const firstPage = Object.keys(manifest.pages)[0];
|
|
3243
|
+
const firstHtml = firstPage ? htmlFiles.get(firstPage) : void 0;
|
|
3244
|
+
if (firstHtml) {
|
|
3245
|
+
extractedContent.global = extractContentFromHTML(firstHtml, "global", {
|
|
3246
|
+
fields: manifest.global.fields,
|
|
3247
|
+
collections: {}
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
return { ...extractedContent, manifest };
|
|
2067
3252
|
}
|
|
2068
3253
|
function normalizeImagePath(imageSrc) {
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
return `/images/${filename}`;
|
|
2074
|
-
}
|
|
2075
|
-
return `/${filename}`;
|
|
3254
|
+
return normalizeImageSeedPath(imageSrc);
|
|
3255
|
+
}
|
|
3256
|
+
function isLinkValue(value) {
|
|
3257
|
+
return typeof value === "object" && value !== null && "url" in value && "text" in value;
|
|
2076
3258
|
}
|
|
2077
3259
|
function formatForStrapi(extracted) {
|
|
2078
3260
|
const seedData = {};
|
|
@@ -2080,7 +3262,9 @@ function formatForStrapi(extracted) {
|
|
|
2080
3262
|
if (Object.keys(content.fields).length > 0) {
|
|
2081
3263
|
const formattedFields = {};
|
|
2082
3264
|
for (const [fieldName, value] of Object.entries(content.fields)) {
|
|
2083
|
-
if (
|
|
3265
|
+
if (isLinkValue(value)) {
|
|
3266
|
+
formattedFields[fieldName] = value;
|
|
3267
|
+
} else if (fieldName.includes("image") || fieldName.includes("img") || fieldName.includes("bg") || isLikelyImagePath(value)) {
|
|
2084
3268
|
formattedFields[fieldName] = normalizeImagePath(value);
|
|
2085
3269
|
} else {
|
|
2086
3270
|
formattedFields[fieldName] = value;
|
|
@@ -2092,7 +3276,9 @@ function formatForStrapi(extracted) {
|
|
|
2092
3276
|
const formattedItems = items.map((item) => {
|
|
2093
3277
|
const formattedItem = {};
|
|
2094
3278
|
for (const [fieldName, value] of Object.entries(item)) {
|
|
2095
|
-
if (
|
|
3279
|
+
if (isLinkValue(value)) {
|
|
3280
|
+
formattedItem[fieldName] = value;
|
|
3281
|
+
} else if (fieldName === "image" || fieldName.includes("image") || fieldName.includes("img") || isLikelyImagePath(value)) {
|
|
2096
3282
|
formattedItem[fieldName] = normalizeImagePath(value);
|
|
2097
3283
|
} else {
|
|
2098
3284
|
formattedItem[fieldName] = value;
|
|
@@ -2103,20 +3289,33 @@ function formatForStrapi(extracted) {
|
|
|
2103
3289
|
seedData[collectionName] = formattedItems;
|
|
2104
3290
|
}
|
|
2105
3291
|
}
|
|
3292
|
+
if (extracted.global && Object.keys(extracted.global.fields).length > 0) {
|
|
3293
|
+
const formattedFields = {};
|
|
3294
|
+
for (const [fieldName, value] of Object.entries(extracted.global.fields)) {
|
|
3295
|
+
if (isLinkValue(value)) {
|
|
3296
|
+
formattedFields[fieldName] = value;
|
|
3297
|
+
} else if (fieldName.includes("image") || fieldName.includes("img") || fieldName.includes("bg") || isLikelyImagePath(value)) {
|
|
3298
|
+
formattedFields[fieldName] = normalizeImagePath(value);
|
|
3299
|
+
} else {
|
|
3300
|
+
formattedFields[fieldName] = value;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
seedData.global = formattedFields;
|
|
3304
|
+
}
|
|
2106
3305
|
return seedData;
|
|
2107
3306
|
}
|
|
2108
3307
|
|
|
2109
3308
|
// src/seed-writer.ts
|
|
2110
3309
|
import fs9 from "fs-extra";
|
|
2111
|
-
import
|
|
3310
|
+
import path12 from "path";
|
|
2112
3311
|
async function writeSeedData(outputDir, seedData) {
|
|
2113
|
-
const seedDir =
|
|
3312
|
+
const seedDir = path12.join(outputDir, "cms-seed");
|
|
2114
3313
|
await fs9.ensureDir(seedDir);
|
|
2115
|
-
const seedPath =
|
|
3314
|
+
const seedPath = path12.join(seedDir, "seed-data.json");
|
|
2116
3315
|
await fs9.writeJson(seedPath, seedData, { spaces: 2 });
|
|
2117
3316
|
}
|
|
2118
3317
|
async function createSeedReadme(outputDir) {
|
|
2119
|
-
const readmePath =
|
|
3318
|
+
const readmePath = path12.join(outputDir, "cms-seed", "README.md");
|
|
2120
3319
|
const content = `# CMS Seed Data
|
|
2121
3320
|
|
|
2122
3321
|
Auto-extracted content from your Webflow export, ready to seed into Strapi.
|
|
@@ -2175,58 +3374,687 @@ When seeding Strapi, these images will be uploaded to Strapi's media library.
|
|
|
2175
3374
|
await fs9.writeFile(readmePath, content, "utf-8");
|
|
2176
3375
|
}
|
|
2177
3376
|
|
|
2178
|
-
// src/
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
3377
|
+
// src/component-extractor.ts
|
|
3378
|
+
import * as cheerio5 from "cheerio";
|
|
3379
|
+
import * as crypto from "crypto";
|
|
3380
|
+
import fs10 from "fs-extra";
|
|
3381
|
+
import path13 from "path";
|
|
3382
|
+
import { glob as glob4 } from "glob";
|
|
3383
|
+
var COMPONENT_NAME_PATTERNS = {
|
|
3384
|
+
TheNav: [/nav/i, /navbar/i, /navigation/i, /header.*nav/i, /main.*menu/i],
|
|
3385
|
+
TheFooter: [/footer/i, /site.*footer/i],
|
|
3386
|
+
TheHeader: [/header/i, /site.*header/i, /page.*header/i],
|
|
3387
|
+
TheSidebar: [/sidebar/i, /side.*bar/i, /aside/i]
|
|
3388
|
+
};
|
|
3389
|
+
var DEFAULT_MIN_SECTION_SIZE = 200;
|
|
3390
|
+
async function parseAllPages(inputDir, options = {}) {
|
|
3391
|
+
const pages = [];
|
|
3392
|
+
const htmlFiles = await glob4("**/*.html", { cwd: inputDir, nodir: true });
|
|
3393
|
+
for (const file of htmlFiles) {
|
|
3394
|
+
if (!file.endsWith(".html")) continue;
|
|
3395
|
+
const filePath = path13.join(inputDir, file);
|
|
3396
|
+
const html = await fs10.readFile(filePath, "utf-8");
|
|
3397
|
+
const $ = cheerio5.load(html);
|
|
3398
|
+
const pageName = htmlPathToPageId(file);
|
|
3399
|
+
const sections = extractSections($, options.minSectionSize ?? DEFAULT_MIN_SECTION_SIZE);
|
|
3400
|
+
pages.push({
|
|
3401
|
+
name: pageName,
|
|
3402
|
+
filePath,
|
|
3403
|
+
sourcePath: file,
|
|
3404
|
+
$,
|
|
3405
|
+
sections
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
return pages;
|
|
3409
|
+
}
|
|
3410
|
+
function extractSections($, minSectionSize) {
|
|
3411
|
+
const sections = [];
|
|
3412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3413
|
+
let $container = $("body");
|
|
3414
|
+
const $bodyChildren = $container.children();
|
|
3415
|
+
if ($bodyChildren.length === 1 && $bodyChildren.first().is("div")) {
|
|
3416
|
+
const $wrapper = $bodyChildren.first();
|
|
3417
|
+
if ($wrapper.children().length > 1) {
|
|
3418
|
+
$container = $wrapper;
|
|
2189
3419
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
console.log(pc3.blue("\n\u{1F4E6} Copying assets..."));
|
|
2197
|
-
await copyAllAssets(inputDir, outputDir, assets);
|
|
2198
|
-
console.log(pc3.green(" \u2713 Assets copied successfully"));
|
|
2199
|
-
console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
|
|
2200
|
-
const htmlFiles = await findHTMLFiles(inputDir);
|
|
2201
|
-
console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
|
|
2202
|
-
const htmlContentMap = /* @__PURE__ */ new Map();
|
|
2203
|
-
for (const htmlFile of htmlFiles) {
|
|
2204
|
-
const html = await readHTMLFile(inputDir, htmlFile);
|
|
2205
|
-
const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
|
|
2206
|
-
htmlContentMap.set(pageName, html);
|
|
2207
|
-
console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
|
|
3420
|
+
}
|
|
3421
|
+
$container.children().each((_, el) => {
|
|
3422
|
+
const $element = $(el);
|
|
3423
|
+
const tagName = ($element.prop("tagName") || "").toLowerCase();
|
|
3424
|
+
if (["script", "style", "link", "meta", "noscript", "template"].includes(tagName)) {
|
|
3425
|
+
return;
|
|
2208
3426
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
$
|
|
3427
|
+
const className = $element.attr("class") || "";
|
|
3428
|
+
if (className.includes("global-embed") || className.includes("globalembed")) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
const html = $.html($element);
|
|
3432
|
+
const elementId = getElementIdentifier($, $element);
|
|
3433
|
+
if (seen.has(elementId)) return;
|
|
3434
|
+
seen.add(elementId);
|
|
3435
|
+
const fingerprint = createFingerprint($, $element);
|
|
3436
|
+
const exactFingerprint = createExactFingerprint(html);
|
|
3437
|
+
const suggestedName = suggestComponentName($element);
|
|
3438
|
+
const semanticName = ["TheNav", "TheFooter", "TheHeader", "TheSidebar", "Nav", "Footer", "Header", "Sidebar"].includes(suggestedName);
|
|
3439
|
+
if (!semanticName && html.length < minSectionSize) return;
|
|
3440
|
+
const uniqueSelector = buildUniqueSelector2($, $element);
|
|
3441
|
+
sections.push({
|
|
3442
|
+
selector: uniqueSelector,
|
|
3443
|
+
fingerprint,
|
|
3444
|
+
exactFingerprint,
|
|
3445
|
+
$element,
|
|
3446
|
+
html,
|
|
3447
|
+
suggestedName
|
|
3448
|
+
});
|
|
3449
|
+
});
|
|
3450
|
+
return sections;
|
|
3451
|
+
}
|
|
3452
|
+
function createExactFingerprint(html) {
|
|
3453
|
+
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();
|
|
3454
|
+
return crypto.createHash("md5").update(normalized).digest("hex").substring(0, 12);
|
|
3455
|
+
}
|
|
3456
|
+
function getElementIdentifier(_$, $element) {
|
|
3457
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3458
|
+
const className = $element.attr("class") || "";
|
|
3459
|
+
const id = $element.attr("id") || "";
|
|
3460
|
+
return `${tag}#${id}.${className}`;
|
|
3461
|
+
}
|
|
3462
|
+
function createFingerprint($, $element) {
|
|
3463
|
+
const structure = getStructure($, $element);
|
|
3464
|
+
return crypto.createHash("md5").update(structure).digest("hex").substring(0, 12);
|
|
3465
|
+
}
|
|
3466
|
+
function getStructure($, $element, depth = 0) {
|
|
3467
|
+
if (depth > 10) return "";
|
|
3468
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3469
|
+
const classNames = normalizeClasses($element.attr("class") || "");
|
|
3470
|
+
let structure = `${tag}`;
|
|
3471
|
+
if (classNames) {
|
|
3472
|
+
structure += `.${classNames}`;
|
|
3473
|
+
}
|
|
3474
|
+
const children = [];
|
|
3475
|
+
$element.children().each((_, child) => {
|
|
3476
|
+
const $child = $(child);
|
|
3477
|
+
const childStructure = getStructure($, $child, depth + 1);
|
|
3478
|
+
if (childStructure) {
|
|
3479
|
+
children.push(childStructure);
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
if (children.length > 0) {
|
|
3483
|
+
const childCounts = countIdentical(children);
|
|
3484
|
+
const compressedChildren = childCounts.map(({ item, count }) => count > 1 ? `${item}*${count}` : item).join(",");
|
|
3485
|
+
structure += `[${compressedChildren}]`;
|
|
3486
|
+
}
|
|
3487
|
+
return structure;
|
|
3488
|
+
}
|
|
3489
|
+
function normalizeClasses(classes) {
|
|
3490
|
+
return classes.split(/\s+/).filter((c) => {
|
|
3491
|
+
if (c.startsWith("w-")) return false;
|
|
3492
|
+
if (c.match(/^(p|m|pt|pb|pl|pr|px|py|mt|mb|ml|mr|mx|my)-\d/)) return false;
|
|
3493
|
+
return c.length > 0;
|
|
3494
|
+
}).sort().join(".");
|
|
3495
|
+
}
|
|
3496
|
+
function countIdentical(items) {
|
|
3497
|
+
const result = [];
|
|
3498
|
+
for (const item of items) {
|
|
3499
|
+
const last = result[result.length - 1];
|
|
3500
|
+
if (last && last.item === item) {
|
|
3501
|
+
last.count++;
|
|
3502
|
+
} else {
|
|
3503
|
+
result.push({ item, count: 1 });
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
return result;
|
|
3507
|
+
}
|
|
3508
|
+
function suggestComponentName($element) {
|
|
3509
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "";
|
|
3510
|
+
const className = $element.attr("class") || "";
|
|
3511
|
+
const id = $element.attr("id") || "";
|
|
3512
|
+
if (tag === "nav") return "TheNav";
|
|
3513
|
+
if (tag === "footer") return "TheFooter";
|
|
3514
|
+
if (tag === "header") return "TheHeader";
|
|
3515
|
+
if (tag === "aside") return "TheSidebar";
|
|
3516
|
+
const normalizedClassName = className.replace(/\bc-/g, "");
|
|
3517
|
+
const searchText = `${className} ${normalizedClassName} ${id}`.toLowerCase();
|
|
3518
|
+
for (const [name, patterns] of Object.entries(COMPONENT_NAME_PATTERNS)) {
|
|
3519
|
+
for (const pattern of patterns) {
|
|
3520
|
+
if (pattern.test(searchText)) {
|
|
3521
|
+
return name;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
const primaryClass = className.split(" ").find((c) => !c.startsWith("w-") && c.length > 2);
|
|
3526
|
+
if (primaryClass) {
|
|
3527
|
+
const cleanName = primaryClass.replace(/^c-/, "");
|
|
3528
|
+
return pascalCase(cleanName);
|
|
3529
|
+
}
|
|
3530
|
+
return "SharedSection";
|
|
3531
|
+
}
|
|
3532
|
+
function pascalCase(str) {
|
|
3533
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(str)) {
|
|
3534
|
+
return str;
|
|
3535
|
+
}
|
|
3536
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3537
|
+
}
|
|
3538
|
+
function buildUniqueSelector2($, $element) {
|
|
3539
|
+
const tag = $element.prop("tagName")?.toLowerCase() || "div";
|
|
3540
|
+
const id = $element.attr("id");
|
|
3541
|
+
const className = $element.attr("class");
|
|
3542
|
+
if (id) {
|
|
3543
|
+
return `#${id}`;
|
|
3544
|
+
}
|
|
3545
|
+
if (className) {
|
|
3546
|
+
const primaryClasses = className.split(" ").filter((c) => !c.startsWith("w-") && c.length > 2).slice(0, 2);
|
|
3547
|
+
if (primaryClasses.length > 0) {
|
|
3548
|
+
const selector = `${tag}.${primaryClasses.join(".")}`;
|
|
3549
|
+
if ($(selector).length === 1) {
|
|
3550
|
+
return selector;
|
|
3551
|
+
}
|
|
3552
|
+
return `.${primaryClasses.join(".")}`;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
const $parent = $element.parent();
|
|
3556
|
+
const siblings = $parent.children(tag);
|
|
3557
|
+
if (siblings.length > 1) {
|
|
3558
|
+
const index = siblings.index($element) + 1;
|
|
3559
|
+
return `${tag}:nth-child(${index})`;
|
|
3560
|
+
}
|
|
3561
|
+
return tag;
|
|
3562
|
+
}
|
|
3563
|
+
function findSharedSections(pages, options = {}) {
|
|
3564
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
3565
|
+
const match = options.match || "exact";
|
|
3566
|
+
for (const page of pages) {
|
|
3567
|
+
for (const section of page.sections) {
|
|
3568
|
+
const fingerprint = match === "structure" ? section.fingerprint : section.exactFingerprint;
|
|
3569
|
+
const existing = fingerprintMap.get(fingerprint) || [];
|
|
3570
|
+
existing.push({ section, page });
|
|
3571
|
+
fingerprintMap.set(fingerprint, existing);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
const sharedComponents = [];
|
|
3575
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
3576
|
+
for (const occurrences of fingerprintMap.values()) {
|
|
3577
|
+
const uniquePages = new Set(occurrences.map((o) => o.page.name));
|
|
3578
|
+
if (occurrences.length < (options.minOccurrences ?? 2)) continue;
|
|
3579
|
+
if (uniquePages.size < (options.minPages ?? options.minOccurrences ?? 2)) continue;
|
|
3580
|
+
const template = occurrences[0];
|
|
3581
|
+
let name = template.section.suggestedName;
|
|
3582
|
+
if (options.include?.length && !options.include.some((item) => name.toLowerCase().includes(item.toLowerCase()))) {
|
|
3583
|
+
const semanticNames = ["TheNav", "TheFooter", "TheHeader", "TheSidebar"];
|
|
3584
|
+
if (!semanticNames.includes(name)) continue;
|
|
3585
|
+
}
|
|
3586
|
+
if (options.exclude?.some((item) => name.toLowerCase().includes(item.toLowerCase()))) continue;
|
|
3587
|
+
let counter = 1;
|
|
3588
|
+
while (usedNames.has(name)) {
|
|
3589
|
+
name = `${template.section.suggestedName}${counter++}`;
|
|
3590
|
+
}
|
|
3591
|
+
usedNames.add(name);
|
|
3592
|
+
const confidence = getComponentConfidence(name, uniquePages.size, pages.length);
|
|
3593
|
+
sharedComponents.push({
|
|
3594
|
+
name,
|
|
3595
|
+
selector: template.section.selector,
|
|
3596
|
+
pages: [...uniquePages],
|
|
3597
|
+
html: template.section.html,
|
|
3598
|
+
fingerprint: template.section.fingerprint,
|
|
3599
|
+
confidence,
|
|
3600
|
+
reason: confidence === "high" ? "Semantic or repeated site-wide section" : match === "exact" ? "Exact repeated HTML section" : "Repeated section with matching DOM structure"
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
return sharedComponents;
|
|
3604
|
+
}
|
|
3605
|
+
function getComponentConfidence(name, pageCount, totalPages) {
|
|
3606
|
+
const semanticNames = ["thenav", "thefooter", "theheader", "nav", "footer", "header"];
|
|
3607
|
+
if (semanticNames.includes(name.toLowerCase())) return "high";
|
|
3608
|
+
if (pageCount === totalPages || pageCount >= 3) return "medium";
|
|
3609
|
+
return "low";
|
|
3610
|
+
}
|
|
3611
|
+
function createVueComponent(component) {
|
|
3612
|
+
let html = component.html;
|
|
3613
|
+
html = html.replace(/\s*data-w-id="[^"]*"/g, "");
|
|
3614
|
+
html = html.replace(/\s*data-wf-page="[^"]*"/g, "");
|
|
3615
|
+
html = html.replace(/\s*data-wf-site="[^"]*"/g, "");
|
|
3616
|
+
html = sanitizeVueComponentHtml(html);
|
|
3617
|
+
return `<script setup lang="ts">
|
|
3618
|
+
/**
|
|
3619
|
+
* ${component.name} Component
|
|
3620
|
+
* Shared across pages: ${component.pages.join(", ")}
|
|
3621
|
+
*
|
|
3622
|
+
* To make content editable, add fields to the 'global' section in cms-manifest.json
|
|
3623
|
+
*/
|
|
3624
|
+
const { content } = useStrapiContent('global')
|
|
3625
|
+
</script>
|
|
3626
|
+
|
|
3627
|
+
<template>
|
|
3628
|
+
${html}
|
|
3629
|
+
</template>
|
|
3630
|
+
|
|
3631
|
+
<style scoped>
|
|
3632
|
+
/* Component-specific styles if needed */
|
|
3633
|
+
</style>
|
|
2218
3634
|
`;
|
|
3635
|
+
}
|
|
3636
|
+
function sanitizeVueComponentHtml(html) {
|
|
3637
|
+
const $ = cheerio5.load(html);
|
|
3638
|
+
$("script, style").remove();
|
|
3639
|
+
$("img").each((_, el) => {
|
|
3640
|
+
const $el = $(el);
|
|
3641
|
+
const src = $el.attr("src");
|
|
3642
|
+
if (src) {
|
|
3643
|
+
$el.attr("src", normalizePublicAssetPath(src));
|
|
3644
|
+
}
|
|
3645
|
+
$el.removeAttr("srcset");
|
|
3646
|
+
$el.removeAttr("sizes");
|
|
3647
|
+
});
|
|
3648
|
+
return $.html();
|
|
3649
|
+
}
|
|
3650
|
+
function replaceWithComponent($, selector, componentName) {
|
|
3651
|
+
const $element = $(selector);
|
|
3652
|
+
if ($element.length > 0) {
|
|
3653
|
+
$element.replaceWith(`<!--COMPONENT:${componentName}-->`);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
async function writeComponents(outputDir, components) {
|
|
3657
|
+
const componentsDir = path13.join(outputDir, "components");
|
|
3658
|
+
await fs10.ensureDir(componentsDir);
|
|
3659
|
+
const sharedComponents = [];
|
|
3660
|
+
for (const component of components) {
|
|
3661
|
+
const vueContent = createVueComponent(component);
|
|
3662
|
+
const filePath = path13.join(componentsDir, `${component.name}.vue`);
|
|
3663
|
+
await fs10.writeFile(filePath, vueContent, "utf-8");
|
|
3664
|
+
sharedComponents.push({
|
|
3665
|
+
name: component.name,
|
|
3666
|
+
selector: component.selector,
|
|
3667
|
+
pages: component.pages,
|
|
3668
|
+
confidence: component.confidence,
|
|
3669
|
+
reason: component.reason,
|
|
3670
|
+
role: component.role || "shared-section",
|
|
3671
|
+
collectionName: component.collectionName,
|
|
3672
|
+
collectionStorage: component.collectionStorage,
|
|
3673
|
+
contentMode: component.contentMode || "shared-global"
|
|
3674
|
+
// Fields will be detected separately
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
return sharedComponents;
|
|
3678
|
+
}
|
|
3679
|
+
async function extractSharedComponents(inputDir, outputDir, options = {}) {
|
|
3680
|
+
const pages = await parseAllPages(inputDir, options);
|
|
3681
|
+
if (pages.length < 2) {
|
|
3682
|
+
return [];
|
|
3683
|
+
}
|
|
3684
|
+
const rules = (options.rules || []).map((rule) => ({
|
|
3685
|
+
...rule,
|
|
3686
|
+
minOccurrences: rule.minOccurrences ?? options.minOccurrences,
|
|
3687
|
+
minPages: rule.minPages ?? options.minPages
|
|
3688
|
+
}));
|
|
3689
|
+
const ruleSections = findRuleSections(pages, rules);
|
|
3690
|
+
const sharedSections = [...ruleSections, ...findSharedSections(pages, options)];
|
|
3691
|
+
if (sharedSections.length === 0) {
|
|
3692
|
+
return [];
|
|
3693
|
+
}
|
|
3694
|
+
const componentsToWrite = sharedSections.filter((component) => meetsConfidence(component.confidence, options.writeConfidence || "medium"));
|
|
3695
|
+
const components = await writeComponents(outputDir, componentsToWrite);
|
|
3696
|
+
return components;
|
|
3697
|
+
}
|
|
3698
|
+
function findRuleSections(pages, rules) {
|
|
3699
|
+
const components = [];
|
|
3700
|
+
for (const rule of rules) {
|
|
3701
|
+
const occurrences = [];
|
|
3702
|
+
for (const page of pages) {
|
|
3703
|
+
const $elements = page.$(rule.selector);
|
|
3704
|
+
if ($elements.length === 0) continue;
|
|
3705
|
+
$elements.each((index, element) => {
|
|
3706
|
+
if (rule.role !== "collection-item" && index > 0) return false;
|
|
3707
|
+
occurrences.push({ page, html: page.$.html(element) });
|
|
3708
|
+
return void 0;
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
const uniquePages = new Set(occurrences.map(({ page }) => page.name));
|
|
3712
|
+
if (occurrences.length < (rule.minOccurrences ?? 2)) continue;
|
|
3713
|
+
if (uniquePages.size < (rule.minPages ?? rule.minOccurrences ?? 2)) continue;
|
|
3714
|
+
components.push({
|
|
3715
|
+
name: pascalCase(rule.name),
|
|
3716
|
+
selector: rule.selector,
|
|
3717
|
+
pages: [...uniquePages],
|
|
3718
|
+
html: occurrences[0].html,
|
|
3719
|
+
fingerprint: crypto.createHash("md5").update(occurrences[0].html).digest("hex").substring(0, 12),
|
|
3720
|
+
confidence: "high",
|
|
3721
|
+
reason: "User-defined component rule",
|
|
3722
|
+
role: rule.role || "shared-section",
|
|
3723
|
+
collectionName: rule.collectionName || toCollectionName2(rule.name),
|
|
3724
|
+
collectionStorage: rule.collectionStorage || "collection-type",
|
|
3725
|
+
contentMode: rule.contentMode || "auto"
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
return components;
|
|
3729
|
+
}
|
|
3730
|
+
function toCollectionName2(name) {
|
|
3731
|
+
const base = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
3732
|
+
if (base.endsWith("s")) return base;
|
|
3733
|
+
return `${base}s`;
|
|
3734
|
+
}
|
|
3735
|
+
function meetsConfidence(confidence, minimum) {
|
|
3736
|
+
const rank = { low: 1, medium: 2, high: 3 };
|
|
3737
|
+
return rank[confidence] >= rank[minimum];
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// src/converter.ts
|
|
3741
|
+
import * as cheerio6 from "cheerio";
|
|
3742
|
+
|
|
3743
|
+
// src/analyzer.ts
|
|
3744
|
+
import path14 from "path";
|
|
3745
|
+
import fs11 from "fs-extra";
|
|
3746
|
+
async function analyzeWebflowExport(inputDir, config = {}) {
|
|
3747
|
+
const inputExists = await fs11.pathExists(inputDir);
|
|
3748
|
+
if (!inputExists) {
|
|
3749
|
+
throw new Error(`Input directory not found: ${inputDir}`);
|
|
3750
|
+
}
|
|
3751
|
+
const [assets, htmlFiles] = await Promise.all([
|
|
3752
|
+
scanAssets(inputDir),
|
|
3753
|
+
findHTMLFiles(inputDir)
|
|
3754
|
+
]);
|
|
3755
|
+
const pages = htmlFiles.sort().map(getPageRouteInfo);
|
|
3756
|
+
const warnings = [];
|
|
3757
|
+
if (pages.length === 0) {
|
|
3758
|
+
warnings.push("No HTML pages were found in the input directory.");
|
|
3759
|
+
}
|
|
3760
|
+
const componentConfig = config.components || {};
|
|
3761
|
+
const parsedPages = await parseAllPages(inputDir, {
|
|
3762
|
+
minSectionSize: componentConfig.minSectionSize
|
|
3763
|
+
});
|
|
3764
|
+
const componentCandidates = findSharedSections(parsedPages, {
|
|
3765
|
+
match: componentConfig.match,
|
|
3766
|
+
minOccurrences: componentConfig.minOccurrences,
|
|
3767
|
+
minPages: componentConfig.minPages,
|
|
3768
|
+
include: componentConfig.include,
|
|
3769
|
+
exclude: componentConfig.exclude,
|
|
3770
|
+
rules: componentConfig.rules
|
|
3771
|
+
}).map((component) => ({
|
|
3772
|
+
name: component.name,
|
|
3773
|
+
selector: component.selector,
|
|
3774
|
+
pages: component.pages,
|
|
3775
|
+
confidence: component.confidence,
|
|
3776
|
+
reason: component.reason
|
|
3777
|
+
}));
|
|
3778
|
+
return {
|
|
3779
|
+
inputDir,
|
|
3780
|
+
pages,
|
|
3781
|
+
assets,
|
|
3782
|
+
componentCandidates,
|
|
3783
|
+
warnings
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
function createConversionReport(input) {
|
|
3787
|
+
return {
|
|
3788
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3789
|
+
stages: input.stages,
|
|
3790
|
+
pages: input.analysis.pages.map((page) => ({
|
|
3791
|
+
source: page.sourcePath,
|
|
3792
|
+
pageId: page.pageId,
|
|
3793
|
+
route: page.route,
|
|
3794
|
+
output: page.outputPath
|
|
3795
|
+
})),
|
|
3796
|
+
assets: {
|
|
3797
|
+
css: input.analysis.assets.css.length,
|
|
3798
|
+
images: input.analysis.assets.images.length,
|
|
3799
|
+
fonts: input.analysis.assets.fonts.length,
|
|
3800
|
+
js: input.analysis.assets.js.length,
|
|
3801
|
+
preservedStructure: true
|
|
3802
|
+
},
|
|
3803
|
+
components: input.components.map((component) => ({
|
|
3804
|
+
name: component.name,
|
|
3805
|
+
selector: component.selector,
|
|
3806
|
+
pages: component.pages,
|
|
3807
|
+
confidence: component.confidence,
|
|
3808
|
+
reason: component.reason
|
|
3809
|
+
})),
|
|
3810
|
+
cms: {
|
|
3811
|
+
provider: input.provider,
|
|
3812
|
+
fields: input.fields,
|
|
3813
|
+
collections: input.collections,
|
|
3814
|
+
schemas: input.schemas,
|
|
3815
|
+
seedPages: input.seedPages
|
|
3816
|
+
},
|
|
3817
|
+
warnings: [...input.analysis.warnings, ...input.warnings || []]
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
async function writeConversionReport(outputDir, report) {
|
|
3821
|
+
const jsonPath = path14.join(outputDir, "see-ms-report.json");
|
|
3822
|
+
const mdPath = path14.join(outputDir, "see-ms-report.md");
|
|
3823
|
+
await fs11.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
3824
|
+
await fs11.writeFile(mdPath, renderReportMarkdown(report), "utf-8");
|
|
3825
|
+
}
|
|
3826
|
+
function renderReportMarkdown(report) {
|
|
3827
|
+
const lines = [
|
|
3828
|
+
"# SeeMS Conversion Report",
|
|
3829
|
+
"",
|
|
3830
|
+
`Generated: ${report.generatedAt}`,
|
|
3831
|
+
"",
|
|
3832
|
+
"## Stages",
|
|
3833
|
+
report.stages.map((stage) => `- ${stage}`).join("\n") || "- none",
|
|
3834
|
+
"",
|
|
3835
|
+
"## Pages",
|
|
3836
|
+
...report.pages.map((page) => `- ${page.source} -> ${page.output} (${page.route}, id: ${page.pageId})`),
|
|
3837
|
+
"",
|
|
3838
|
+
"## Assets",
|
|
3839
|
+
`- CSS: ${report.assets.css}`,
|
|
3840
|
+
`- Images: ${report.assets.images}`,
|
|
3841
|
+
`- Fonts: ${report.assets.fonts}`,
|
|
3842
|
+
`- JS: ${report.assets.js}`,
|
|
3843
|
+
`- Preserved folder structure: ${report.assets.preservedStructure ? "yes" : "no"}`,
|
|
3844
|
+
"",
|
|
3845
|
+
"## Components",
|
|
3846
|
+
...report.components.length ? report.components.map((component) => `- ${component.name} (${component.confidence || "unknown"}): ${component.pages.join(", ")}`) : ["- none"],
|
|
3847
|
+
"",
|
|
3848
|
+
"## CMS",
|
|
3849
|
+
`- Provider: ${report.cms.provider}`,
|
|
3850
|
+
`- Editable fields: ${report.cms.fields}`,
|
|
3851
|
+
`- Collections: ${report.cms.collections}`,
|
|
3852
|
+
`- Schemas: ${report.cms.schemas}`,
|
|
3853
|
+
`- Seeded pages: ${report.cms.seedPages}`,
|
|
3854
|
+
"",
|
|
3855
|
+
"## Warnings",
|
|
3856
|
+
...report.warnings.length ? report.warnings.map((warning) => `- ${warning}`) : ["- none"],
|
|
3857
|
+
""
|
|
3858
|
+
];
|
|
3859
|
+
return lines.join("\n");
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// src/config.ts
|
|
3863
|
+
import fs12 from "fs-extra";
|
|
3864
|
+
import path15 from "path";
|
|
3865
|
+
var DEFAULT_SEEMS_CONFIG = {
|
|
3866
|
+
target: "nuxt",
|
|
3867
|
+
cms: { provider: "strapi", strapi: { scaffold: false, packageManager: "npm", install: true } },
|
|
3868
|
+
components: {
|
|
3869
|
+
enabled: true,
|
|
3870
|
+
match: "structure",
|
|
3871
|
+
minOccurrences: 2,
|
|
3872
|
+
minPages: 2,
|
|
3873
|
+
minSectionSize: 200,
|
|
3874
|
+
writeConfidence: "medium",
|
|
3875
|
+
include: ["nav", "header", "footer"],
|
|
3876
|
+
exclude: [],
|
|
3877
|
+
rules: []
|
|
3878
|
+
},
|
|
3879
|
+
ignore: {
|
|
3880
|
+
selectors: [],
|
|
3881
|
+
classes: []
|
|
3882
|
+
},
|
|
3883
|
+
editor: {
|
|
3884
|
+
enabled: true,
|
|
3885
|
+
previewParam: "preview"
|
|
3886
|
+
},
|
|
3887
|
+
assets: {
|
|
3888
|
+
excludeResponsiveVariants: true
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
function mergeConfig(base = {}, override = {}) {
|
|
3892
|
+
return {
|
|
3893
|
+
...base,
|
|
3894
|
+
...override,
|
|
3895
|
+
cms: {
|
|
3896
|
+
...base.cms,
|
|
3897
|
+
...override.cms,
|
|
3898
|
+
strapi: { ...base.cms?.strapi, ...override.cms?.strapi }
|
|
3899
|
+
},
|
|
3900
|
+
components: { ...base.components, ...override.components },
|
|
3901
|
+
ignore: { ...base.ignore, ...override.ignore },
|
|
3902
|
+
editor: { ...base.editor, ...override.editor },
|
|
3903
|
+
assets: { ...base.assets, ...override.assets },
|
|
3904
|
+
collections: override.collections ?? base.collections,
|
|
3905
|
+
fields: { ...base.fields, ...override.fields }
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
async function loadSeeMSConfig(configPath) {
|
|
3909
|
+
if (!configPath) return {};
|
|
3910
|
+
const absolutePath = path15.resolve(configPath);
|
|
3911
|
+
if (!await fs12.pathExists(absolutePath)) {
|
|
3912
|
+
throw new Error(`Config file not found: ${absolutePath}`);
|
|
3913
|
+
}
|
|
3914
|
+
if (absolutePath.endsWith(".json")) {
|
|
3915
|
+
return JSON.parse(await fs12.readFile(absolutePath, "utf-8"));
|
|
3916
|
+
}
|
|
3917
|
+
const content = await fs12.readFile(absolutePath, "utf-8");
|
|
3918
|
+
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*$/);
|
|
3919
|
+
if (!objectMatch) {
|
|
3920
|
+
throw new Error("see-ms config must export an object literal or be JSON");
|
|
3921
|
+
}
|
|
3922
|
+
return Function(`"use strict"; return (${objectMatch[1]});`)();
|
|
3923
|
+
}
|
|
3924
|
+
async function writeSeeMSConfig(outputDir, config) {
|
|
3925
|
+
const target = path15.join(outputDir, "see-ms.config.ts");
|
|
3926
|
+
const content = `import type { SeeMSConfig } from "@see-ms/types";
|
|
3927
|
+
|
|
3928
|
+
const config: SeeMSConfig = ${JSON.stringify(config, null, 2)};
|
|
3929
|
+
|
|
3930
|
+
export default config;
|
|
3931
|
+
`;
|
|
3932
|
+
await fs12.writeFile(target, content, "utf-8");
|
|
3933
|
+
}
|
|
3934
|
+
function normalizeConfig(config = {}) {
|
|
3935
|
+
return mergeConfig(DEFAULT_SEEMS_CONFIG, config);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
// src/converter.ts
|
|
3939
|
+
async function convertWebflowExport(options) {
|
|
3940
|
+
const { inputDir, outputDir, boilerplate } = options;
|
|
3941
|
+
const loadedConfig = options.configPath ? await loadSeeMSConfig(options.configPath) : {};
|
|
3942
|
+
const config = normalizeConfig(mergeConfig(loadedConfig, options.config || {}));
|
|
3943
|
+
const target = options.target || config.target || "nuxt";
|
|
3944
|
+
const provider = options.cmsBackend || config.cms?.provider || "strapi";
|
|
3945
|
+
const editorEnabled = options.editor ?? config.editor?.enabled ?? true;
|
|
3946
|
+
const shouldGenerateContent = options.generateContent !== false;
|
|
3947
|
+
const collectionClasses = options.collectionClasses || config.collections?.map((collection) => collection.className);
|
|
3948
|
+
const collectionNames = options.collectionNames || Object.fromEntries(
|
|
3949
|
+
(config.collections || []).map((collection) => [collection.className, collection.name || collection.className])
|
|
3950
|
+
);
|
|
3951
|
+
console.log(pc3.cyan(`\u{1F680} Starting Webflow to ${target === "astro-vue" ? "Astro + Vue" : "Nuxt"} conversion...`));
|
|
3952
|
+
console.log(pc3.dim(`Input: ${inputDir}`));
|
|
3953
|
+
console.log(pc3.dim(`Output: ${outputDir}`));
|
|
3954
|
+
try {
|
|
3955
|
+
const analysis = await analyzeWebflowExport(inputDir, config);
|
|
3956
|
+
await setupBoilerplate(boilerplate, outputDir, target);
|
|
3957
|
+
await writeSeeMSConfig(outputDir, config);
|
|
3958
|
+
console.log(pc3.blue("\n\u{1F4C2} Scanning assets..."));
|
|
3959
|
+
const assets = analysis.assets;
|
|
3960
|
+
console.log(pc3.green(` \u2713 Found ${assets.css.length} CSS files`));
|
|
3961
|
+
console.log(pc3.green(` \u2713 Found ${assets.images.length} images`));
|
|
3962
|
+
console.log(pc3.green(` \u2713 Found ${assets.fonts.length} fonts`));
|
|
3963
|
+
console.log(pc3.green(` \u2713 Found ${assets.js.length} JS files`));
|
|
3964
|
+
console.log(pc3.blue("\n\u{1F4E6} Copying assets..."));
|
|
3965
|
+
await copyAllAssets(inputDir, outputDir, assets);
|
|
3966
|
+
console.log(pc3.green(" \u2713 Assets copied successfully"));
|
|
3967
|
+
console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
|
|
3968
|
+
const htmlFiles = analysis.pages.map((page) => page.sourcePath);
|
|
3969
|
+
console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
|
|
3970
|
+
const htmlContentMap = /* @__PURE__ */ new Map();
|
|
3971
|
+
const originalHtmlContentMap = /* @__PURE__ */ new Map();
|
|
3972
|
+
for (const htmlFile of htmlFiles) {
|
|
3973
|
+
const html = await readHTMLFile(inputDir, htmlFile);
|
|
3974
|
+
const pageName = htmlPathToPageId(htmlFile);
|
|
3975
|
+
htmlContentMap.set(pageName, html);
|
|
3976
|
+
originalHtmlContentMap.set(pageName, html);
|
|
3977
|
+
console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
|
|
3978
|
+
}
|
|
3979
|
+
console.log(pc3.blue("\n\u{1F9E9} Extracting shared components..."));
|
|
3980
|
+
const sharedComponents = config.components?.enabled === false ? [] : await extractSharedComponents(inputDir, outputDir, {
|
|
3981
|
+
minOccurrences: config.components?.minOccurrences,
|
|
3982
|
+
minPages: config.components?.minPages,
|
|
3983
|
+
minSectionSize: config.components?.minSectionSize,
|
|
3984
|
+
match: config.components?.match,
|
|
3985
|
+
writeConfidence: config.components?.writeConfidence,
|
|
3986
|
+
include: config.components?.include,
|
|
3987
|
+
exclude: config.components?.exclude,
|
|
3988
|
+
rules: config.components?.rules
|
|
3989
|
+
});
|
|
3990
|
+
const pageComponentMap = /* @__PURE__ */ new Map();
|
|
3991
|
+
if (sharedComponents.length > 0) {
|
|
3992
|
+
console.log(pc3.green(` \u2713 Extracted ${sharedComponents.length} shared components:`));
|
|
3993
|
+
for (const component of sharedComponents) {
|
|
3994
|
+
console.log(pc3.dim(` - ${component.name} (found in ${component.pages.length} pages)`));
|
|
3995
|
+
}
|
|
3996
|
+
for (const [pageName, html] of htmlContentMap.entries()) {
|
|
3997
|
+
const $ = cheerio6.load(html);
|
|
3998
|
+
let modified = false;
|
|
3999
|
+
const usedComponents = [];
|
|
4000
|
+
for (const component of sharedComponents) {
|
|
4001
|
+
if (component.role === "collection-item") {
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
if (component.pages.includes(pageName)) {
|
|
4005
|
+
replaceWithComponent($, component.selector, component.name);
|
|
4006
|
+
usedComponents.push(component.name);
|
|
4007
|
+
modified = true;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
if (modified) {
|
|
4011
|
+
const serializedHtml = $.html();
|
|
4012
|
+
htmlContentMap.set(pageName, serializedHtml);
|
|
4013
|
+
pageComponentMap.set(pageName, usedComponents);
|
|
4014
|
+
}
|
|
2219
4015
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
const vueComponent = htmlToVueComponent(transformed, pageName);
|
|
2223
|
-
await writeVueComponent(outputDir, htmlFile, vueComponent);
|
|
2224
|
-
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
|
|
4016
|
+
} else {
|
|
4017
|
+
console.log(pc3.dim(" No shared components detected across pages"));
|
|
2225
4018
|
}
|
|
2226
|
-
|
|
4019
|
+
console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
|
|
4020
|
+
let allEmbeddedStyles = "";
|
|
4021
|
+
for (const htmlFile of htmlFiles) {
|
|
4022
|
+
const pageName = htmlPathToPageId(htmlFile);
|
|
4023
|
+
const html = htmlContentMap.get(pageName);
|
|
4024
|
+
const parsed = parseHTML(html, htmlFile);
|
|
4025
|
+
if (parsed.embeddedStyles) {
|
|
4026
|
+
allEmbeddedStyles += `
|
|
4027
|
+
/* From ${htmlFile} */
|
|
4028
|
+
${parsed.embeddedStyles}
|
|
4029
|
+
`;
|
|
4030
|
+
}
|
|
4031
|
+
const transformed = transformForNuxt(parsed.htmlContent, htmlFile, {
|
|
4032
|
+
linkMode: target === "astro-vue" ? "anchor" : "nuxt"
|
|
4033
|
+
});
|
|
4034
|
+
const componentImports = pageComponentMap.get(pageName);
|
|
4035
|
+
const vueComponent = htmlToVueComponent(transformed, pageName, componentImports);
|
|
4036
|
+
await writeVueComponent(outputDir, htmlFile, vueComponent, target, assets.css, editorEnabled);
|
|
4037
|
+
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", target === "astro-vue" ? ".astro + .vue" : ".vue")}`));
|
|
4038
|
+
}
|
|
4039
|
+
await formatVueFiles(outputDir, target);
|
|
2227
4040
|
console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
|
|
2228
|
-
const pagesDir =
|
|
2229
|
-
const
|
|
4041
|
+
const pagesDir = target === "astro-vue" ? path16.join(outputDir, "src", "components", "pages") : path16.join(outputDir, "pages");
|
|
4042
|
+
const pageRoutes = Object.fromEntries(
|
|
4043
|
+
htmlFiles.map((htmlFile) => {
|
|
4044
|
+
const info = getPageRouteInfo(htmlFile);
|
|
4045
|
+
return [info.pageId, info.route];
|
|
4046
|
+
})
|
|
4047
|
+
);
|
|
4048
|
+
const manifest = await generateManifest(pagesDir, {
|
|
4049
|
+
collectionClasses,
|
|
4050
|
+
collectionNames,
|
|
4051
|
+
sharedComponents,
|
|
4052
|
+
componentsDir: path16.join(outputDir, "components"),
|
|
4053
|
+
ignoreSelectors: config.ignore?.selectors,
|
|
4054
|
+
ignoreClasses: config.ignore?.classes,
|
|
4055
|
+
provider,
|
|
4056
|
+
pageRoutes
|
|
4057
|
+
});
|
|
2230
4058
|
await writeManifest(outputDir, manifest);
|
|
2231
4059
|
const totalFields = Object.values(manifest.pages).reduce(
|
|
2232
4060
|
(sum, page) => sum + Object.keys(page.fields || {}).length,
|
|
@@ -2239,27 +4067,50 @@ ${parsed.embeddedStyles}
|
|
|
2239
4067
|
console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
|
|
2240
4068
|
console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
|
|
2241
4069
|
console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
|
|
4070
|
+
console.log(pc3.blue("\n\u{1F50C} Generating content runtime..."));
|
|
4071
|
+
if (target === "nuxt") {
|
|
4072
|
+
await createEditorContentComposable(outputDir);
|
|
4073
|
+
await createStrapiContentComposable(outputDir, manifest);
|
|
4074
|
+
await addStrapiUrlToConfig(outputDir);
|
|
4075
|
+
} else {
|
|
4076
|
+
await createAstroStrapiContentComposable(outputDir, manifest);
|
|
4077
|
+
}
|
|
4078
|
+
console.log(pc3.green(" \u2713 Content runtime generated"));
|
|
2242
4079
|
console.log(pc3.blue("\n\u26A1 Transforming Vue files to reactive templates..."));
|
|
2243
|
-
await transformAllVuePages(pagesDir, manifest);
|
|
4080
|
+
await transformAllVuePages(pagesDir, manifest, { target });
|
|
4081
|
+
await transformSharedComponentsToReactive(path16.join(outputDir, "components"), manifest, { target });
|
|
2244
4082
|
console.log(pc3.green(` \u2713 Transformed ${Object.keys(manifest.pages).length} pages to use Vue template syntax`));
|
|
2245
4083
|
console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
|
|
2246
4084
|
console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
|
|
2247
4085
|
console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
4086
|
+
let seedData = {};
|
|
4087
|
+
if (shouldGenerateContent) {
|
|
4088
|
+
const extractedContent = extractAllContent(originalHtmlContentMap, manifest);
|
|
4089
|
+
seedData = formatForStrapi(extractedContent);
|
|
4090
|
+
await writeSeedData(outputDir, seedData);
|
|
4091
|
+
await createSeedReadme(outputDir);
|
|
4092
|
+
}
|
|
4093
|
+
const pagesWithContent = Object.keys(manifest.pages).filter((key) => {
|
|
2253
4094
|
const data = seedData[key];
|
|
4095
|
+
if (!data) return false;
|
|
2254
4096
|
if (Array.isArray(data)) return data.length > 0;
|
|
2255
4097
|
return Object.keys(data).length > 0;
|
|
2256
4098
|
}).length;
|
|
2257
|
-
|
|
2258
|
-
|
|
4099
|
+
if (shouldGenerateContent) {
|
|
4100
|
+
console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
|
|
4101
|
+
console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
|
|
4102
|
+
} else {
|
|
4103
|
+
console.log(pc3.dim(" Skipped initial CMS content generation"));
|
|
4104
|
+
}
|
|
2259
4105
|
console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
2260
4106
|
const schemas = manifestToSchemas(manifest);
|
|
2261
4107
|
await writeAllSchemas(outputDir, schemas);
|
|
2262
4108
|
await createStrapiReadme(outputDir);
|
|
4109
|
+
const linkSchema = getLinkComponentSchema(manifest);
|
|
4110
|
+
if (linkSchema) {
|
|
4111
|
+
await writeLinkComponentSchema(outputDir);
|
|
4112
|
+
console.log(pc3.dim(" \u2713 Generated shared.link component schema"));
|
|
4113
|
+
}
|
|
2263
4114
|
console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
|
|
2264
4115
|
console.log(pc3.dim(" View schemas in: cms-schemas/"));
|
|
2265
4116
|
if (allEmbeddedStyles.trim()) {
|
|
@@ -2268,34 +4119,55 @@ ${parsed.embeddedStyles}
|
|
|
2268
4119
|
await writeEmbeddedStyles(outputDir, dedupedStyles);
|
|
2269
4120
|
console.log(pc3.green(" \u2713 Embedded styles added to main.css"));
|
|
2270
4121
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
4122
|
+
if (target === "nuxt") {
|
|
4123
|
+
console.log(pc3.blue("\n\u{1F527} Generating webflow-assets.ts plugin..."));
|
|
4124
|
+
await writeWebflowAssetPlugin(outputDir, assets.css);
|
|
4125
|
+
console.log(pc3.green(" \u2713 Plugin generated (existing file overwritten)"));
|
|
4126
|
+
console.log(pc3.blue("\n\u2699\uFE0F Updating nuxt.config.ts..."));
|
|
4127
|
+
try {
|
|
4128
|
+
await updateNuxtConfig(outputDir, assets.css);
|
|
4129
|
+
console.log(pc3.green(" \u2713 Config updated"));
|
|
4130
|
+
} catch (error) {
|
|
4131
|
+
console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
|
|
4132
|
+
console.log(pc3.dim(" Please add CSS files manually"));
|
|
4133
|
+
}
|
|
4134
|
+
} else {
|
|
4135
|
+
console.log(pc3.dim("\n\u{1F527} Skipped Nuxt asset plugin; Astro pages import CSS directly"));
|
|
4136
|
+
}
|
|
4137
|
+
if (editorEnabled) {
|
|
4138
|
+
console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
|
|
4139
|
+
if (target === "nuxt") {
|
|
4140
|
+
await createEditorPlugin(outputDir);
|
|
4141
|
+
await createSaveEndpoint(outputDir);
|
|
4142
|
+
await createPublishEndpoint(outputDir);
|
|
4143
|
+
console.log(pc3.green(" \u2713 Nuxt editor plugin created"));
|
|
4144
|
+
console.log(pc3.green(" \u2713 Nuxt save/publish endpoints created"));
|
|
4145
|
+
} else {
|
|
4146
|
+
await createAstroEditorClient(outputDir);
|
|
4147
|
+
await createAstroSaveEndpoint(outputDir);
|
|
4148
|
+
console.log(pc3.green(" \u2713 Astro editor client created"));
|
|
4149
|
+
console.log(pc3.green(" \u2713 Astro save/publish endpoints created"));
|
|
4150
|
+
}
|
|
4151
|
+
await addEditorDependency(outputDir);
|
|
4152
|
+
await createStrapiBootstrap(outputDir);
|
|
4153
|
+
console.log(pc3.green(" \u2713 Editor dependency added"));
|
|
4154
|
+
console.log(pc3.green(" \u2713 Strapi bootstrap file generated"));
|
|
4155
|
+
} else {
|
|
4156
|
+
console.log(pc3.dim("\n\u{1F3A8} Editor overlay disabled by config"));
|
|
4157
|
+
}
|
|
4158
|
+
const report = createConversionReport({
|
|
4159
|
+
analysis,
|
|
4160
|
+
provider,
|
|
4161
|
+
stages: ["scan", "analyze", "plan", "convert", "cms", ...editorEnabled ? ["editor"] : []],
|
|
4162
|
+
components: sharedComponents,
|
|
4163
|
+
fields: totalFields,
|
|
4164
|
+
collections: totalCollections,
|
|
4165
|
+
schemas: Object.keys(schemas).length,
|
|
4166
|
+
seedPages: pagesWithContent,
|
|
4167
|
+
warnings: []
|
|
4168
|
+
});
|
|
4169
|
+
await writeConversionReport(outputDir, report);
|
|
4170
|
+
console.log(pc3.green(" \u2713 Generated see-ms-report.md and see-ms-report.json"));
|
|
2299
4171
|
console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
|
|
2300
4172
|
console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
|
|
2301
4173
|
console.log(pc3.dim(` 1. cd ${outputDir}`));
|
|
@@ -2313,18 +4185,595 @@ ${parsed.embeddedStyles}
|
|
|
2313
4185
|
}
|
|
2314
4186
|
}
|
|
2315
4187
|
|
|
4188
|
+
// src/strapi-setup.ts
|
|
4189
|
+
import fs13 from "fs-extra";
|
|
4190
|
+
import path17 from "path";
|
|
4191
|
+
import { glob as glob5 } from "glob";
|
|
4192
|
+
import * as readline from "readline";
|
|
4193
|
+
import { spawn } from "child_process";
|
|
4194
|
+
var ENV_FILE = ".env";
|
|
4195
|
+
async function loadConfig(projectDir) {
|
|
4196
|
+
const envPath = path17.join(projectDir, ENV_FILE);
|
|
4197
|
+
if (await fs13.pathExists(envPath)) {
|
|
4198
|
+
try {
|
|
4199
|
+
const content = await fs13.readFile(envPath, "utf-8");
|
|
4200
|
+
const config = {};
|
|
4201
|
+
for (const line of content.split("\n")) {
|
|
4202
|
+
const trimmed = line.trim();
|
|
4203
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4204
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
4205
|
+
const value = valueParts.join("=").trim();
|
|
4206
|
+
if (key === "STRAPI_API_TOKEN") {
|
|
4207
|
+
config.apiToken = value;
|
|
4208
|
+
} else if (key === "STRAPI_URL") {
|
|
4209
|
+
config.strapiUrl = value;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
return config;
|
|
4213
|
+
} catch {
|
|
4214
|
+
return {};
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
return {};
|
|
4218
|
+
}
|
|
4219
|
+
async function saveConfig(projectDir, config) {
|
|
4220
|
+
const envPath = path17.join(projectDir, ENV_FILE);
|
|
4221
|
+
let content = "";
|
|
4222
|
+
if (await fs13.pathExists(envPath)) {
|
|
4223
|
+
content = await fs13.readFile(envPath, "utf-8");
|
|
4224
|
+
content = content.split("\n").filter((line) => !line.startsWith("STRAPI_API_TOKEN=") && !line.startsWith("STRAPI_URL=")).join("\n");
|
|
4225
|
+
if (content && !content.endsWith("\n")) {
|
|
4226
|
+
content += "\n";
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
if (config.strapiUrl) {
|
|
4230
|
+
content += `STRAPI_URL=${config.strapiUrl}
|
|
4231
|
+
`;
|
|
4232
|
+
}
|
|
4233
|
+
if (config.apiToken) {
|
|
4234
|
+
content += `STRAPI_API_TOKEN=${config.apiToken}
|
|
4235
|
+
`;
|
|
4236
|
+
}
|
|
4237
|
+
await fs13.writeFile(envPath, content);
|
|
4238
|
+
}
|
|
4239
|
+
async function completeSetup(options) {
|
|
4240
|
+
const { projectDir, strapiDir, strapiUrl: optionUrl, apiToken: optionToken, ignoreSavedToken } = options;
|
|
4241
|
+
if (!await fs13.pathExists(strapiDir)) {
|
|
4242
|
+
if (!options.scaffold) {
|
|
4243
|
+
throw new Error(`Strapi directory not found: ${strapiDir}`);
|
|
4244
|
+
}
|
|
4245
|
+
await scaffoldStrapiProject({
|
|
4246
|
+
strapiDir,
|
|
4247
|
+
...options.scaffoldOptions
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
const savedConfig = await loadConfig(projectDir);
|
|
4251
|
+
const strapiUrl = optionUrl || savedConfig.strapiUrl || "http://localhost:1337";
|
|
4252
|
+
console.log("\u{1F680} Starting complete Strapi setup...\n");
|
|
4253
|
+
console.log("\u{1F4E6} Step 1: Installing schemas...");
|
|
4254
|
+
await installSchemas(projectDir, strapiDir);
|
|
4255
|
+
console.log("\u2713 Schemas installed\n");
|
|
4256
|
+
console.log("\u23F8\uFE0F Step 2: Restart Strapi to load schemas");
|
|
4257
|
+
console.log(" Run: npm run develop (in Strapi directory)");
|
|
4258
|
+
console.log(" Press Enter when Strapi is running...");
|
|
4259
|
+
await waitForEnter();
|
|
4260
|
+
console.log("\n\u{1F50D} Step 3: Checking Strapi connection...");
|
|
4261
|
+
const isRunning = await checkStrapiRunning(strapiUrl);
|
|
4262
|
+
if (!isRunning) {
|
|
4263
|
+
console.error("\u274C Cannot connect to Strapi at", strapiUrl);
|
|
4264
|
+
console.log(" Make sure Strapi is running: npm run develop");
|
|
4265
|
+
process.exit(1);
|
|
4266
|
+
}
|
|
4267
|
+
console.log("\u2713 Connected to Strapi\n");
|
|
4268
|
+
let token = optionToken || (!ignoreSavedToken ? savedConfig.apiToken : void 0);
|
|
4269
|
+
if (token && !ignoreSavedToken) {
|
|
4270
|
+
console.log("\u{1F511} Step 4: Using saved API token");
|
|
4271
|
+
} else if (token && optionToken) {
|
|
4272
|
+
console.log("\u{1F511} Step 4: Using provided API token");
|
|
4273
|
+
} else {
|
|
4274
|
+
console.log("\u{1F511} Step 4: API Token needed");
|
|
4275
|
+
console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
|
|
4276
|
+
console.log(" 2. Go to Settings > API Tokens > Create new API Token");
|
|
4277
|
+
console.log(' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"');
|
|
4278
|
+
console.log(" 4. Copy the token and paste it here:\n");
|
|
4279
|
+
token = await promptForToken();
|
|
4280
|
+
const saveToken = await promptYesNo(" Save token for future use?");
|
|
4281
|
+
if (saveToken) {
|
|
4282
|
+
await saveConfig(projectDir, { ...savedConfig, apiToken: token, strapiUrl });
|
|
4283
|
+
console.log(" \u2713 Token saved to .env");
|
|
4284
|
+
}
|
|
4285
|
+
console.log("");
|
|
4286
|
+
}
|
|
4287
|
+
console.log("\u{1F4F8} Step 5: Uploading images...");
|
|
4288
|
+
const mediaMap = await uploadAllImages(projectDir, strapiUrl, token);
|
|
4289
|
+
console.log(`\u2713 Mapped ${mediaMap.size} media lookup keys
|
|
4290
|
+
`);
|
|
4291
|
+
console.log("\u{1F4DD} Step 6: Seeding content...");
|
|
4292
|
+
await seedContent(projectDir, strapiUrl, token, mediaMap);
|
|
4293
|
+
console.log("\u2713 Content seeded\n");
|
|
4294
|
+
console.log("\u2705 Complete setup finished!");
|
|
4295
|
+
console.log("\n\u{1F4CB} Next steps:");
|
|
4296
|
+
console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
|
|
4297
|
+
console.log(" 2. Check Content Manager - your content should be there!");
|
|
4298
|
+
console.log(" 3. Connect your Nuxt app to Strapi API");
|
|
4299
|
+
}
|
|
4300
|
+
async function scaffoldStrapiProject(options) {
|
|
4301
|
+
const {
|
|
4302
|
+
strapiDir,
|
|
4303
|
+
packageManager = "npm",
|
|
4304
|
+
install = true,
|
|
4305
|
+
run = false,
|
|
4306
|
+
gitInit = false,
|
|
4307
|
+
typescript = true
|
|
4308
|
+
} = options;
|
|
4309
|
+
const resolvedDir = path17.resolve(strapiDir);
|
|
4310
|
+
if (await fs13.pathExists(resolvedDir)) {
|
|
4311
|
+
const entries = await fs13.readdir(resolvedDir);
|
|
4312
|
+
if (entries.length > 0) {
|
|
4313
|
+
throw new Error(`Cannot scaffold Strapi into a non-empty directory: ${resolvedDir}`);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
await fs13.ensureDir(path17.dirname(resolvedDir));
|
|
4317
|
+
const args = [
|
|
4318
|
+
"create-strapi@latest",
|
|
4319
|
+
resolvedDir,
|
|
4320
|
+
typescript ? "--typescript" : "--javascript",
|
|
4321
|
+
"--skip-cloud",
|
|
4322
|
+
"--skip-db",
|
|
4323
|
+
"--no-example",
|
|
4324
|
+
install ? "--install" : "--no-install",
|
|
4325
|
+
gitInit ? "--git-init" : "--no-git-init",
|
|
4326
|
+
`--use-${packageManager}`
|
|
4327
|
+
];
|
|
4328
|
+
if (!run) {
|
|
4329
|
+
args.push("--no-run");
|
|
4330
|
+
}
|
|
4331
|
+
console.log("\u{1F3D7}\uFE0F Scaffolding Strapi project...");
|
|
4332
|
+
console.log(` npx ${args.join(" ")}`);
|
|
4333
|
+
await runCommand("npx", args, process.cwd());
|
|
4334
|
+
if (!await fs13.pathExists(path17.join(resolvedDir, "package.json"))) {
|
|
4335
|
+
throw new Error(`Strapi scaffold did not create a project at ${resolvedDir}`);
|
|
4336
|
+
}
|
|
4337
|
+
console.log(`\u2713 Strapi project scaffolded at ${resolvedDir}`);
|
|
4338
|
+
}
|
|
4339
|
+
function runCommand(command, args, cwd) {
|
|
4340
|
+
return new Promise((resolve, reject) => {
|
|
4341
|
+
const child = spawn(command, args, {
|
|
4342
|
+
cwd,
|
|
4343
|
+
stdio: "inherit",
|
|
4344
|
+
shell: process.platform === "win32"
|
|
4345
|
+
});
|
|
4346
|
+
child.on("error", reject);
|
|
4347
|
+
child.on("close", (code) => {
|
|
4348
|
+
if (code === 0) {
|
|
4349
|
+
resolve();
|
|
4350
|
+
} else {
|
|
4351
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
4352
|
+
}
|
|
4353
|
+
});
|
|
4354
|
+
});
|
|
4355
|
+
}
|
|
4356
|
+
async function installSchemas(projectDir, strapiDir) {
|
|
4357
|
+
if (!await fs13.pathExists(strapiDir)) {
|
|
4358
|
+
console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
|
|
4359
|
+
console.error(` Resolved to: ${path17.resolve(strapiDir)}`);
|
|
4360
|
+
throw new Error(`Strapi directory not found: ${strapiDir}`);
|
|
4361
|
+
}
|
|
4362
|
+
const packageJsonPath = path17.join(strapiDir, "package.json");
|
|
4363
|
+
if (await fs13.pathExists(packageJsonPath)) {
|
|
4364
|
+
const pkg = await fs13.readJson(packageJsonPath);
|
|
4365
|
+
if (!pkg.dependencies?.["@strapi/strapi"]) {
|
|
4366
|
+
console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
const schemaDir = path17.join(projectDir, "cms-schemas");
|
|
4370
|
+
const componentsDir = path17.join(schemaDir, "components");
|
|
4371
|
+
if (await fs13.pathExists(componentsDir)) {
|
|
4372
|
+
const componentFiles = await glob5("**/*.json", {
|
|
4373
|
+
cwd: componentsDir,
|
|
4374
|
+
absolute: false
|
|
4375
|
+
});
|
|
4376
|
+
if (componentFiles.length > 0) {
|
|
4377
|
+
console.log(` Found ${componentFiles.length} component(s)`);
|
|
4378
|
+
for (const file of componentFiles) {
|
|
4379
|
+
const sourcePath = path17.join(componentsDir, file);
|
|
4380
|
+
const targetPath = path17.join(strapiDir, "src", "components", file);
|
|
4381
|
+
await fs13.ensureDir(path17.dirname(targetPath));
|
|
4382
|
+
await fs13.copy(sourcePath, targetPath);
|
|
4383
|
+
console.log(` \u2713 Component: ${file}`);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
const schemaFiles = await glob5("*.json", {
|
|
4388
|
+
cwd: schemaDir,
|
|
4389
|
+
absolute: false
|
|
4390
|
+
});
|
|
4391
|
+
if (schemaFiles.length === 0) {
|
|
4392
|
+
console.log("\u26A0\uFE0F No schema files found");
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
console.log(` Found ${schemaFiles.length} schema file(s)`);
|
|
4396
|
+
for (const file of schemaFiles) {
|
|
4397
|
+
const schemaPath = path17.join(schemaDir, file);
|
|
4398
|
+
const schema = await fs13.readJson(schemaPath);
|
|
4399
|
+
const singularName = schema.info?.singularName || path17.basename(file, ".json");
|
|
4400
|
+
console.log(` Installing ${singularName}...`);
|
|
4401
|
+
try {
|
|
4402
|
+
const apiPath = path17.join(strapiDir, "src", "api", singularName);
|
|
4403
|
+
const contentTypesPath = path17.join(
|
|
4404
|
+
apiPath,
|
|
4405
|
+
"content-types",
|
|
4406
|
+
singularName
|
|
4407
|
+
);
|
|
4408
|
+
const targetPath = path17.join(contentTypesPath, "schema.json");
|
|
4409
|
+
await fs13.ensureDir(contentTypesPath);
|
|
4410
|
+
await fs13.ensureDir(path17.join(apiPath, "routes"));
|
|
4411
|
+
await fs13.ensureDir(path17.join(apiPath, "controllers"));
|
|
4412
|
+
await fs13.ensureDir(path17.join(apiPath, "services"));
|
|
4413
|
+
await fs13.writeJson(targetPath, schema, { spaces: 2 });
|
|
4414
|
+
const routeContent = `import { factories } from '@strapi/strapi';
|
|
4415
|
+
export default factories.createCoreRouter('api::${singularName}.${singularName}');
|
|
4416
|
+
`;
|
|
4417
|
+
await fs13.writeFile(
|
|
4418
|
+
path17.join(apiPath, "routes", `${singularName}.ts`),
|
|
4419
|
+
routeContent
|
|
4420
|
+
);
|
|
4421
|
+
const controllerContent = `import { factories } from '@strapi/strapi';
|
|
4422
|
+
export default factories.createCoreController('api::${singularName}.${singularName}');
|
|
4423
|
+
`;
|
|
4424
|
+
await fs13.writeFile(
|
|
4425
|
+
path17.join(apiPath, "controllers", `${singularName}.ts`),
|
|
4426
|
+
controllerContent
|
|
4427
|
+
);
|
|
4428
|
+
const serviceContent = `import { factories } from '@strapi/strapi';
|
|
4429
|
+
export default factories.createCoreService('api::${singularName}.${singularName}');
|
|
4430
|
+
`;
|
|
4431
|
+
await fs13.writeFile(
|
|
4432
|
+
path17.join(apiPath, "services", `${singularName}.ts`),
|
|
4433
|
+
serviceContent
|
|
4434
|
+
);
|
|
4435
|
+
} catch (error) {
|
|
4436
|
+
console.error(` \u2717 Failed to install ${singularName}: ${error.message}`);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
async function checkStrapiRunning(strapiUrl) {
|
|
4441
|
+
try {
|
|
4442
|
+
const response = await fetch(`${strapiUrl}/_health`);
|
|
4443
|
+
return response.ok;
|
|
4444
|
+
} catch {
|
|
4445
|
+
return false;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function createReadline() {
|
|
4449
|
+
return readline.createInterface({
|
|
4450
|
+
input: process.stdin,
|
|
4451
|
+
output: process.stdout
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
async function waitForEnter() {
|
|
4455
|
+
const rl = createReadline();
|
|
4456
|
+
return new Promise((resolve) => {
|
|
4457
|
+
rl.question("", () => {
|
|
4458
|
+
rl.close();
|
|
4459
|
+
resolve();
|
|
4460
|
+
});
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
async function promptForToken() {
|
|
4464
|
+
const rl = createReadline();
|
|
4465
|
+
return new Promise((resolve) => {
|
|
4466
|
+
rl.question(" Token: ", (answer) => {
|
|
4467
|
+
rl.close();
|
|
4468
|
+
resolve(answer.trim());
|
|
4469
|
+
});
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
async function promptYesNo(question) {
|
|
4473
|
+
const rl = createReadline();
|
|
4474
|
+
return new Promise((resolve) => {
|
|
4475
|
+
rl.question(`${question} (y/n): `, (answer) => {
|
|
4476
|
+
rl.close();
|
|
4477
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
4478
|
+
});
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
async function getExistingMedia(strapiUrl, apiToken) {
|
|
4482
|
+
const existingMedia = /* @__PURE__ */ new Map();
|
|
4483
|
+
try {
|
|
4484
|
+
let page = 1;
|
|
4485
|
+
const pageSize = 100;
|
|
4486
|
+
let hasMore = true;
|
|
4487
|
+
while (hasMore) {
|
|
4488
|
+
const response = await fetch(
|
|
4489
|
+
`${strapiUrl}/api/upload/files?pagination[page]=${page}&pagination[pageSize]=${pageSize}`,
|
|
4490
|
+
{
|
|
4491
|
+
headers: {
|
|
4492
|
+
Authorization: `Bearer ${apiToken}`
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
);
|
|
4496
|
+
if (!response.ok) {
|
|
4497
|
+
break;
|
|
4498
|
+
}
|
|
4499
|
+
const data = await response.json();
|
|
4500
|
+
const files = Array.isArray(data) ? data : data.results || [];
|
|
4501
|
+
for (const file of files) {
|
|
4502
|
+
if (file.name) {
|
|
4503
|
+
existingMedia.set(file.name, file.id);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
hasMore = files.length === pageSize;
|
|
4507
|
+
page++;
|
|
4508
|
+
}
|
|
4509
|
+
} catch (error) {
|
|
4510
|
+
}
|
|
4511
|
+
return existingMedia;
|
|
4512
|
+
}
|
|
4513
|
+
async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
4514
|
+
const mediaMap = /* @__PURE__ */ new Map();
|
|
4515
|
+
const imagesDir = path17.join(projectDir, "public", "assets", "images");
|
|
4516
|
+
if (!await fs13.pathExists(imagesDir)) {
|
|
4517
|
+
console.log(" No images directory found");
|
|
4518
|
+
return mediaMap;
|
|
4519
|
+
}
|
|
4520
|
+
const imageFiles = await glob5("**/*.{jpg,jpeg,png,gif,webp,avif,svg}", {
|
|
4521
|
+
cwd: imagesDir,
|
|
4522
|
+
absolute: false
|
|
4523
|
+
});
|
|
4524
|
+
console.log(` Checking for existing media...`);
|
|
4525
|
+
const existingMedia = await getExistingMedia(strapiUrl, apiToken);
|
|
4526
|
+
let uploadedCount = 0;
|
|
4527
|
+
let skippedCount = 0;
|
|
4528
|
+
console.log(` Processing ${imageFiles.length} images...`);
|
|
4529
|
+
for (const imageFile of imageFiles) {
|
|
4530
|
+
const fileName = path17.basename(imageFile);
|
|
4531
|
+
const existingId = existingMedia.get(fileName);
|
|
4532
|
+
if (existingId) {
|
|
4533
|
+
addMediaMapEntries(mediaMap, imageFile, existingId);
|
|
4534
|
+
skippedCount++;
|
|
4535
|
+
continue;
|
|
4536
|
+
}
|
|
4537
|
+
const imagePath = path17.join(imagesDir, imageFile);
|
|
4538
|
+
const mediaId = await uploadImage(imagePath, imageFile, strapiUrl, apiToken);
|
|
4539
|
+
if (mediaId) {
|
|
4540
|
+
addMediaMapEntries(mediaMap, imageFile, mediaId);
|
|
4541
|
+
uploadedCount++;
|
|
4542
|
+
console.log(` \u2713 ${imageFile}`);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
console.log(` Uploaded: ${uploadedCount}, Skipped (existing): ${skippedCount}`);
|
|
4546
|
+
return mediaMap;
|
|
4547
|
+
}
|
|
4548
|
+
async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
|
|
4549
|
+
try {
|
|
4550
|
+
const fileBuffer = await fs13.readFile(filePath);
|
|
4551
|
+
const mimeType = getMimeType(fileName);
|
|
4552
|
+
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
4553
|
+
const formData = new globalThis.FormData();
|
|
4554
|
+
formData.append("files", blob, fileName);
|
|
4555
|
+
const response = await fetch(`${strapiUrl}/api/upload`, {
|
|
4556
|
+
method: "POST",
|
|
4557
|
+
headers: {
|
|
4558
|
+
Authorization: `Bearer ${apiToken}`
|
|
4559
|
+
},
|
|
4560
|
+
body: formData
|
|
4561
|
+
});
|
|
4562
|
+
if (!response.ok) {
|
|
4563
|
+
const errorText = await response.text();
|
|
4564
|
+
console.error(
|
|
4565
|
+
` \u2717 Failed to upload ${fileName}: ${response.status} - ${errorText}`
|
|
4566
|
+
);
|
|
4567
|
+
return null;
|
|
4568
|
+
}
|
|
4569
|
+
const data = await response.json();
|
|
4570
|
+
return data[0]?.id || null;
|
|
4571
|
+
} catch (error) {
|
|
4572
|
+
console.error(` \u2717 Error uploading ${fileName}:`, error);
|
|
4573
|
+
return null;
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
function getMimeType(fileName) {
|
|
4577
|
+
const ext = path17.extname(fileName).toLowerCase();
|
|
4578
|
+
const mimeTypes = {
|
|
4579
|
+
".jpg": "image/jpeg",
|
|
4580
|
+
".jpeg": "image/jpeg",
|
|
4581
|
+
".png": "image/png",
|
|
4582
|
+
".gif": "image/gif",
|
|
4583
|
+
".webp": "image/webp",
|
|
4584
|
+
".avif": "image/avif",
|
|
4585
|
+
".svg": "image/svg+xml"
|
|
4586
|
+
};
|
|
4587
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
4588
|
+
}
|
|
4589
|
+
async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
|
|
4590
|
+
const seedPath = path17.join(projectDir, "cms-seed", "seed-data.json");
|
|
4591
|
+
if (!await fs13.pathExists(seedPath)) {
|
|
4592
|
+
console.log(" No seed data found");
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
const seedData = await fs13.readJson(seedPath);
|
|
4596
|
+
const schemasDir = path17.join(projectDir, "cms-schemas");
|
|
4597
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
4598
|
+
const schemaFiles = await glob5("*.json", { cwd: schemasDir });
|
|
4599
|
+
for (const file of schemaFiles) {
|
|
4600
|
+
const schema = await fs13.readJson(path17.join(schemasDir, file));
|
|
4601
|
+
const name = path17.basename(file, ".json");
|
|
4602
|
+
schemas.set(name, schema);
|
|
4603
|
+
}
|
|
4604
|
+
let successCount = 0;
|
|
4605
|
+
let totalCount = 0;
|
|
4606
|
+
for (const [contentType, data] of Object.entries(seedData)) {
|
|
4607
|
+
const schema = schemas.get(contentType);
|
|
4608
|
+
if (!schema) {
|
|
4609
|
+
console.log(` \u26A0\uFE0F No schema found for ${contentType}, skipping...`);
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
const singularName = schema.info.singularName;
|
|
4613
|
+
const pluralName = schema.info.pluralName;
|
|
4614
|
+
if (Array.isArray(data)) {
|
|
4615
|
+
console.log(` Seeding ${contentType} (${data.length} items)...`);
|
|
4616
|
+
for (const item of data) {
|
|
4617
|
+
totalCount++;
|
|
4618
|
+
const processedItem = processMediaFields(item, mediaMap);
|
|
4619
|
+
const success = await createEntry(
|
|
4620
|
+
pluralName,
|
|
4621
|
+
processedItem,
|
|
4622
|
+
strapiUrl,
|
|
4623
|
+
apiToken
|
|
4624
|
+
);
|
|
4625
|
+
if (success) successCount++;
|
|
4626
|
+
}
|
|
4627
|
+
} else {
|
|
4628
|
+
console.log(` Seeding ${contentType}...`);
|
|
4629
|
+
totalCount++;
|
|
4630
|
+
const processedData = processMediaFields(data, mediaMap);
|
|
4631
|
+
const success = await createOrUpdateSingleType(
|
|
4632
|
+
singularName,
|
|
4633
|
+
processedData,
|
|
4634
|
+
strapiUrl,
|
|
4635
|
+
apiToken
|
|
4636
|
+
);
|
|
4637
|
+
if (success) successCount++;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
console.log(` \u2713 Successfully seeded ${successCount}/${totalCount} entries`);
|
|
4641
|
+
}
|
|
4642
|
+
function processMediaFields(data, mediaMap) {
|
|
4643
|
+
const processed = {};
|
|
4644
|
+
for (const [key, value] of Object.entries(data)) {
|
|
4645
|
+
if (typeof value === "string") {
|
|
4646
|
+
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)) {
|
|
4647
|
+
const mediaId = findMediaId(mediaMap, value);
|
|
4648
|
+
if (mediaId) {
|
|
4649
|
+
processed[key] = mediaId;
|
|
4650
|
+
} else {
|
|
4651
|
+
processed[key] = null;
|
|
4652
|
+
}
|
|
4653
|
+
} else {
|
|
4654
|
+
processed[key] = value;
|
|
4655
|
+
}
|
|
4656
|
+
} else {
|
|
4657
|
+
processed[key] = value;
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
return processed;
|
|
4661
|
+
}
|
|
4662
|
+
function addMediaMapEntries(mediaMap, imageFile, mediaId) {
|
|
4663
|
+
for (const key of mediaLookupKeys(imageFile)) {
|
|
4664
|
+
mediaMap.set(key, mediaId);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
function findMediaId(mediaMap, value) {
|
|
4668
|
+
for (const key of mediaLookupKeys(value)) {
|
|
4669
|
+
const mediaId = mediaMap.get(key);
|
|
4670
|
+
if (mediaId) return mediaId;
|
|
4671
|
+
}
|
|
4672
|
+
return void 0;
|
|
4673
|
+
}
|
|
4674
|
+
async function createEntry(contentType, data, strapiUrl, apiToken) {
|
|
4675
|
+
try {
|
|
4676
|
+
const response = await fetch(`${strapiUrl}/api/${contentType}`, {
|
|
4677
|
+
method: "POST",
|
|
4678
|
+
headers: {
|
|
4679
|
+
"Content-Type": "application/json",
|
|
4680
|
+
Authorization: `Bearer ${apiToken}`
|
|
4681
|
+
},
|
|
4682
|
+
body: JSON.stringify({ data })
|
|
4683
|
+
});
|
|
4684
|
+
if (!response.ok) {
|
|
4685
|
+
const errorText = await response.text();
|
|
4686
|
+
console.error(
|
|
4687
|
+
` \u2717 Failed to create ${contentType}: ${response.status} - ${errorText}`
|
|
4688
|
+
);
|
|
4689
|
+
return false;
|
|
4690
|
+
}
|
|
4691
|
+
return true;
|
|
4692
|
+
} catch (error) {
|
|
4693
|
+
console.error(` \u2717 Error creating ${contentType}:`, error);
|
|
4694
|
+
return false;
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
async function createOrUpdateSingleType(contentType, data, strapiUrl, apiToken) {
|
|
4698
|
+
try {
|
|
4699
|
+
const response = await fetch(`${strapiUrl}/api/${contentType}`, {
|
|
4700
|
+
method: "PUT",
|
|
4701
|
+
headers: {
|
|
4702
|
+
"Content-Type": "application/json",
|
|
4703
|
+
Authorization: `Bearer ${apiToken}`
|
|
4704
|
+
},
|
|
4705
|
+
body: JSON.stringify({ data })
|
|
4706
|
+
});
|
|
4707
|
+
if (!response.ok) {
|
|
4708
|
+
const errorText = await response.text();
|
|
4709
|
+
console.error(
|
|
4710
|
+
` \u2717 Failed to update ${contentType}: ${response.status} - ${errorText}`
|
|
4711
|
+
);
|
|
4712
|
+
return false;
|
|
4713
|
+
}
|
|
4714
|
+
return true;
|
|
4715
|
+
} catch (error) {
|
|
4716
|
+
console.error(` \u2717 Error updating ${contentType}:`, error);
|
|
4717
|
+
return false;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
async function main() {
|
|
4721
|
+
const args = process.argv.slice(2);
|
|
4722
|
+
if (args.length < 2) {
|
|
4723
|
+
console.log(
|
|
4724
|
+
"Usage: tsx strapi-setup.ts <project-dir> <strapi-dir> [strapi-url] [api-token]"
|
|
4725
|
+
);
|
|
4726
|
+
console.log("");
|
|
4727
|
+
console.log("Example:");
|
|
4728
|
+
console.log(" tsx strapi-setup.ts ./nuxt-project ./strapi-dev");
|
|
4729
|
+
console.log(
|
|
4730
|
+
" tsx strapi-setup.ts ./nuxt-project ./strapi-dev http://localhost:1337 abc123"
|
|
4731
|
+
);
|
|
4732
|
+
process.exit(1);
|
|
4733
|
+
}
|
|
4734
|
+
const [projectDir, strapiDir, strapiUrl, apiToken] = args;
|
|
4735
|
+
await completeSetup({
|
|
4736
|
+
projectDir,
|
|
4737
|
+
strapiDir,
|
|
4738
|
+
strapiUrl,
|
|
4739
|
+
apiToken
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
var isMainModule = process.argv[1] && process.argv[1].endsWith("strapi-setup.ts");
|
|
4743
|
+
if (isMainModule) {
|
|
4744
|
+
main().catch((error) => {
|
|
4745
|
+
console.error("\u274C Setup failed:", error.message);
|
|
4746
|
+
process.exit(1);
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
|
|
2316
4750
|
// src/generator.ts
|
|
2317
4751
|
async function generateSchemas(_manifestPath, _cmsType) {
|
|
2318
4752
|
throw new Error("Not yet implemented");
|
|
2319
4753
|
}
|
|
2320
4754
|
export {
|
|
4755
|
+
LINK_COMPONENT_SCHEMA,
|
|
4756
|
+
analyzeWebflowExport,
|
|
4757
|
+
completeSetup,
|
|
2321
4758
|
convertWebflowExport,
|
|
4759
|
+
createConversionReport,
|
|
2322
4760
|
detectEditableFields,
|
|
4761
|
+
extractSharedComponents,
|
|
4762
|
+
findSharedSections,
|
|
2323
4763
|
generateManifest,
|
|
2324
4764
|
generateSchemas,
|
|
4765
|
+
getLinkComponentSchema,
|
|
4766
|
+
loadSeeMSConfig,
|
|
2325
4767
|
manifestToSchemas,
|
|
4768
|
+
mergeConfig,
|
|
4769
|
+
normalizeConfig,
|
|
4770
|
+
parseAllPages,
|
|
2326
4771
|
readManifest,
|
|
4772
|
+
renderReportMarkdown,
|
|
4773
|
+
scaffoldStrapiProject,
|
|
2327
4774
|
setupBoilerplate,
|
|
2328
|
-
transformAllVuePages
|
|
4775
|
+
transformAllVuePages,
|
|
4776
|
+
writeConversionReport,
|
|
4777
|
+
writeSeeMSConfig
|
|
2329
4778
|
};
|
|
2330
4779
|
//# sourceMappingURL=index.mjs.map
|