@see-ms/converter 1.0.0 → 1.1.0

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