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