@see-ms/converter 1.0.0 → 1.1.1

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