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