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