@see-ms/converter 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3,11 +3,12 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import pc4 from "picocolors";
6
+ import * as readline2 from "readline";
6
7
 
7
8
  // src/converter.ts
8
9
  import pc3 from "picocolors";
9
- import path9 from "path";
10
- import fs8 from "fs-extra";
10
+ import path11 from "path";
11
+ import fs9 from "fs-extra";
11
12
 
12
13
  // src/filesystem.ts
13
14
  import fs from "fs-extra";
@@ -478,13 +479,20 @@ import * as cheerio2 from "cheerio";
478
479
  import fs5 from "fs-extra";
479
480
  import path6 from "path";
480
481
  function cleanClassName(className) {
481
- return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).map((cls) => cls.replace(/-/g, "_")).join(" ");
482
+ return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
482
483
  }
483
484
  function getPrimaryClass(classAttr) {
484
485
  if (!classAttr) return null;
485
486
  const cleaned = cleanClassName(classAttr);
486
487
  const classes = cleaned.split(" ").filter((c) => c.length > 0);
487
- return classes[0] || null;
488
+ if (classes.length === 0) return null;
489
+ const original = classes[0];
490
+ return {
491
+ selector: original,
492
+ // Keep original with dashes for CSS selector
493
+ fieldName: original.replace(/-/g, "_")
494
+ // Normalize for field name
495
+ };
488
496
  }
489
497
  function getContextModifier(_$, $el) {
490
498
  let $current = $el.parent();
@@ -541,11 +549,11 @@ function detectEditableFields(templateHtml) {
541
549
  const potentialCollections = /* @__PURE__ */ new Map();
542
550
  $("[class]").each((_, el) => {
543
551
  const primaryClass = getPrimaryClass($(el).attr("class"));
544
- if (primaryClass && (primaryClass.includes("card") || primaryClass.includes("item") || primaryClass.includes("post") || primaryClass.includes("feature")) && !primaryClass.includes("image") && !primaryClass.includes("inner")) {
545
- if (!potentialCollections.has(primaryClass)) {
546
- potentialCollections.set(primaryClass, []);
552
+ 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")) {
553
+ if (!potentialCollections.has(primaryClass.fieldName)) {
554
+ potentialCollections.set(primaryClass.fieldName, []);
547
555
  }
548
- potentialCollections.get(primaryClass)?.push(el);
556
+ potentialCollections.get(primaryClass.fieldName)?.push(el);
549
557
  }
550
558
  });
551
559
  potentialCollections.forEach((elements, className) => {
@@ -559,40 +567,43 @@ function detectEditableFields(templateHtml) {
559
567
  collectionElements.add(child);
560
568
  });
561
569
  });
570
+ const collectionClassInfo = getPrimaryClass($(elements[0]).attr("class"));
571
+ const collectionSelector = collectionClassInfo ? `.${collectionClassInfo.selector}` : `.${className}`;
562
572
  $first.find("img").each((_, img) => {
563
573
  if (isInsideButton($, img)) return;
564
574
  const $img = $(img);
565
575
  const $parent = $img.parent();
566
- const parentClass = getPrimaryClass($parent.attr("class"));
567
- if (parentClass && parentClass.includes("image")) {
568
- collectionFields.image = `.${parentClass}`;
576
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
577
+ if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
578
+ collectionFields.image = `.${parentClassInfo.selector}`;
569
579
  return false;
570
580
  }
571
581
  });
572
582
  $first.find("div").each((_, el) => {
573
- const primaryClass = getPrimaryClass($(el).attr("class"));
574
- if (primaryClass && primaryClass.includes("tag") && !primaryClass.includes("container")) {
575
- collectionFields.tag = `.${primaryClass}`;
583
+ const classInfo = getPrimaryClass($(el).attr("class"));
584
+ if (classInfo && classInfo.fieldName.includes("tag") && !classInfo.fieldName.includes("container")) {
585
+ collectionFields.tag = `.${classInfo.selector}`;
576
586
  return false;
577
587
  }
578
588
  });
579
589
  $first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
580
- const primaryClass = getPrimaryClass($(el).attr("class"));
581
- if (primaryClass) {
582
- collectionFields.title = `.${primaryClass}`;
590
+ const classInfo = getPrimaryClass($(el).attr("class"));
591
+ if (classInfo) {
592
+ collectionFields.title = `.${classInfo.selector}`;
583
593
  }
584
594
  });
585
595
  $first.find("p").first().each((_, el) => {
586
- const primaryClass = getPrimaryClass($(el).attr("class"));
587
- if (primaryClass) {
588
- collectionFields.description = `.${primaryClass}`;
596
+ const classInfo = getPrimaryClass($(el).attr("class"));
597
+ if (classInfo) {
598
+ collectionFields.description = `.${classInfo.selector}`;
589
599
  }
590
600
  });
591
601
  $first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
592
602
  const $link = $(el);
593
603
  const linkText = $link.text().trim();
594
604
  if (linkText) {
595
- collectionFields.link = `.${getPrimaryClass($link.attr("class")) || "a"}`;
605
+ const classInfo = getPrimaryClass($link.attr("class"));
606
+ collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
596
607
  return false;
597
608
  }
598
609
  });
@@ -602,7 +613,7 @@ function detectEditableFields(templateHtml) {
602
613
  collectionName += "s";
603
614
  }
604
615
  detectedCollections[collectionName] = {
605
- selector: `.${className}`,
616
+ selector: collectionSelector,
606
617
  fields: collectionFields
607
618
  };
608
619
  }
@@ -613,25 +624,30 @@ function detectEditableFields(templateHtml) {
613
624
  if (collectionElements.has(el)) return;
614
625
  const $el = $(el);
615
626
  const text = $el.text().trim();
616
- const primaryClass = getPrimaryClass($el.attr("class"));
627
+ const classInfo = getPrimaryClass($el.attr("class"));
617
628
  if (text) {
618
629
  let fieldName;
619
- if (primaryClass && !primaryClass.startsWith("heading_")) {
620
- fieldName = primaryClass;
630
+ let selector;
631
+ if (classInfo && !classInfo.fieldName.startsWith("heading_")) {
632
+ fieldName = classInfo.fieldName;
633
+ selector = `.${classInfo.selector}`;
621
634
  } else {
622
635
  const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
623
- const parentClass = getPrimaryClass($parent.attr("class"));
636
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
624
637
  const modifier = getContextModifier($, $el);
625
- if (parentClass) {
626
- fieldName = modifier ? `${modifier}_${parentClass}` : parentClass;
638
+ if (parentClassInfo) {
639
+ fieldName = modifier ? `${modifier}_${parentClassInfo.fieldName}` : parentClassInfo.fieldName;
640
+ selector = classInfo ? `.${classInfo.selector}` : `.${parentClassInfo.selector}`;
627
641
  } else if (modifier) {
628
642
  fieldName = `${modifier}_heading`;
643
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
629
644
  } else {
630
645
  fieldName = `heading_${index}`;
646
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
631
647
  }
632
648
  }
633
649
  detectedFields[fieldName] = {
634
- selector: primaryClass ? `.${primaryClass}` : el.tagName.toLowerCase(),
650
+ selector,
635
651
  type: "plain",
636
652
  editable: true
637
653
  };
@@ -641,11 +657,11 @@ function detectEditableFields(templateHtml) {
641
657
  if (collectionElements.has(el)) return;
642
658
  const $el = $(el);
643
659
  const text = $el.text().trim();
644
- const primaryClass = getPrimaryClass($el.attr("class"));
645
- if (text && text.length > 20 && primaryClass) {
660
+ const classInfo = getPrimaryClass($el.attr("class"));
661
+ if (text && text.length > 20 && classInfo) {
646
662
  const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
647
- detectedFields[primaryClass] = {
648
- selector: `.${primaryClass}`,
663
+ detectedFields[classInfo.fieldName] = {
664
+ selector: `.${classInfo.selector}`,
649
665
  type: hasFormatting ? "rich" : "plain",
650
666
  editable: true
651
667
  };
@@ -657,11 +673,11 @@ function detectEditableFields(templateHtml) {
657
673
  const $el = $(el);
658
674
  if (isDecorativeImage($, $el)) return;
659
675
  const $parent = $el.parent();
660
- const parentClass = getPrimaryClass($parent.attr("class"));
661
- if (parentClass) {
662
- const fieldName = parentClass.includes("image") ? parentClass : `${parentClass}_image`;
676
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
677
+ if (parentClassInfo) {
678
+ const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
663
679
  detectedFields[fieldName] = {
664
- selector: `.${parentClass}`,
680
+ selector: `.${parentClassInfo.selector}`,
665
681
  type: "image",
666
682
  editable: true
667
683
  };
@@ -675,8 +691,9 @@ function detectEditableFields(templateHtml) {
675
691
  }).first().text().trim();
676
692
  if (text && text.length > 2) {
677
693
  const $parent = $el.closest('[class*="cta"]').first();
678
- const parentClass = getPrimaryClass($parent.attr("class")) || "button";
679
- detectedFields[`${parentClass}_button_text`] = {
694
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
695
+ const fieldName = parentClassInfo ? `${parentClassInfo.fieldName}_button_text` : "button_text";
696
+ detectedFields[fieldName] = {
680
697
  selector: `.c_button`,
681
698
  type: "plain",
682
699
  editable: true
@@ -742,6 +759,18 @@ function mapFieldTypeToStrapi(fieldType) {
742
759
  };
743
760
  return typeMap[fieldType] || "string";
744
761
  }
762
+ function pluralize(word) {
763
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
764
+ return word + "es";
765
+ }
766
+ if (word.endsWith("y") && word.length > 1) {
767
+ const secondLast = word[word.length - 2];
768
+ if (!"aeiou".includes(secondLast.toLowerCase())) {
769
+ return word.slice(0, -1) + "ies";
770
+ }
771
+ }
772
+ return word + "s";
773
+ }
745
774
  function pageToStrapiSchema(pageName, fields) {
746
775
  const attributes = {};
747
776
  for (const [fieldName, field] of Object.entries(fields)) {
@@ -754,12 +783,14 @@ function pageToStrapiSchema(pageName, fields) {
754
783
  }
755
784
  }
756
785
  const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
786
+ const kebabCaseName = pageName;
787
+ const pluralName = pluralize(kebabCaseName);
757
788
  return {
758
789
  kind: "singleType",
759
- collectionName: pageName.replace(/-/g, "_"),
790
+ collectionName: kebabCaseName,
760
791
  info: {
761
- singularName: pageName.replace(/-/g, "_"),
762
- pluralName: pageName.replace(/-/g, "_"),
792
+ singularName: kebabCaseName,
793
+ pluralName,
763
794
  displayName
764
795
  },
765
796
  options: {
@@ -786,13 +817,14 @@ function collectionToStrapiSchema(collectionName, collection) {
786
817
  };
787
818
  }
788
819
  const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
789
- const singularName = collectionName.endsWith("s") ? collectionName.slice(0, -1) : collectionName;
820
+ const kebabCaseName = collectionName.replace(/_/g, "-");
821
+ const singularName = kebabCaseName.endsWith("s") ? kebabCaseName.slice(0, -1) : kebabCaseName;
790
822
  return {
791
823
  kind: "collectionType",
792
- collectionName: collectionName.replace(/[-_]/g, "_"),
824
+ collectionName: kebabCaseName,
793
825
  info: {
794
- singularName: singularName.replace(/[-_]/g, "_"),
795
- pluralName: collectionName.replace(/[-_]/g, "_"),
826
+ singularName,
827
+ pluralName: kebabCaseName,
796
828
  displayName
797
829
  },
798
830
  options: {
@@ -904,6 +936,186 @@ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
904
936
  await fs7.writeFile(readmePath, content, "utf-8");
905
937
  }
906
938
 
939
+ // src/content-extractor.ts
940
+ import * as cheerio3 from "cheerio";
941
+ import path9 from "path";
942
+ function extractContentFromHTML(html, _pageName, pageManifest) {
943
+ const $ = cheerio3.load(html);
944
+ const content = {
945
+ fields: {},
946
+ collections: {}
947
+ };
948
+ if (pageManifest.fields) {
949
+ for (const [fieldName, field] of Object.entries(pageManifest.fields)) {
950
+ const selector = field.selector;
951
+ const element = $(selector).first();
952
+ if (element.length > 0) {
953
+ if (field.type === "image") {
954
+ const src = element.attr("src") || element.find("img").attr("src") || "";
955
+ content.fields[fieldName] = src;
956
+ } else {
957
+ const text = element.text().trim();
958
+ content.fields[fieldName] = text;
959
+ }
960
+ }
961
+ }
962
+ }
963
+ if (pageManifest.collections) {
964
+ for (const [collectionName, collection] of Object.entries(pageManifest.collections)) {
965
+ const items = [];
966
+ const collectionElements = $(collection.selector);
967
+ collectionElements.each((_, elem) => {
968
+ const item = {};
969
+ const $elem = $(elem);
970
+ for (const [fieldName, fieldSelector] of Object.entries(collection.fields)) {
971
+ const fieldElement = $elem.find(fieldSelector).first();
972
+ if (fieldElement.length > 0) {
973
+ if (fieldName === "image" || fieldName.includes("image")) {
974
+ const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
975
+ item[fieldName] = src;
976
+ } else if (fieldName === "link" || fieldName === "url") {
977
+ const href = fieldElement.attr("href") || "";
978
+ item[fieldName] = href;
979
+ } else {
980
+ const text = fieldElement.text().trim();
981
+ item[fieldName] = text;
982
+ }
983
+ }
984
+ }
985
+ if (Object.keys(item).length > 0) {
986
+ items.push(item);
987
+ }
988
+ });
989
+ if (items.length > 0) {
990
+ content.collections[collectionName] = items;
991
+ }
992
+ }
993
+ }
994
+ return content;
995
+ }
996
+ function extractAllContent(htmlFiles, manifest) {
997
+ const extractedContent = {
998
+ pages: {}
999
+ };
1000
+ for (const [pageName, pageManifest] of Object.entries(manifest.pages)) {
1001
+ const html = htmlFiles.get(pageName);
1002
+ if (html) {
1003
+ const content = extractContentFromHTML(html, pageName, pageManifest);
1004
+ extractedContent.pages[pageName] = content;
1005
+ }
1006
+ }
1007
+ return extractedContent;
1008
+ }
1009
+ function normalizeImagePath(imageSrc) {
1010
+ if (!imageSrc) return "";
1011
+ if (imageSrc.startsWith("/")) return imageSrc;
1012
+ const filename = path9.basename(imageSrc);
1013
+ if (imageSrc.includes("images/")) {
1014
+ return `/images/${filename}`;
1015
+ }
1016
+ return `/${filename}`;
1017
+ }
1018
+ function formatForStrapi(extracted) {
1019
+ const seedData = {};
1020
+ for (const [pageName, content] of Object.entries(extracted.pages)) {
1021
+ if (Object.keys(content.fields).length > 0) {
1022
+ const formattedFields = {};
1023
+ for (const [fieldName, value] of Object.entries(content.fields)) {
1024
+ if (fieldName.includes("image") || fieldName.includes("bg")) {
1025
+ formattedFields[fieldName] = normalizeImagePath(value);
1026
+ } else {
1027
+ formattedFields[fieldName] = value;
1028
+ }
1029
+ }
1030
+ seedData[pageName] = formattedFields;
1031
+ }
1032
+ for (const [collectionName, items] of Object.entries(content.collections)) {
1033
+ const formattedItems = items.map((item) => {
1034
+ const formattedItem = {};
1035
+ for (const [fieldName, value] of Object.entries(item)) {
1036
+ if (fieldName === "image" || fieldName.includes("image")) {
1037
+ formattedItem[fieldName] = normalizeImagePath(value);
1038
+ } else {
1039
+ formattedItem[fieldName] = value;
1040
+ }
1041
+ }
1042
+ return formattedItem;
1043
+ });
1044
+ seedData[collectionName] = formattedItems;
1045
+ }
1046
+ }
1047
+ return seedData;
1048
+ }
1049
+
1050
+ // src/seed-writer.ts
1051
+ import fs8 from "fs-extra";
1052
+ import path10 from "path";
1053
+ async function writeSeedData(outputDir, seedData) {
1054
+ const seedDir = path10.join(outputDir, "cms-seed");
1055
+ await fs8.ensureDir(seedDir);
1056
+ const seedPath = path10.join(seedDir, "seed-data.json");
1057
+ await fs8.writeJson(seedPath, seedData, { spaces: 2 });
1058
+ }
1059
+ async function createSeedReadme(outputDir) {
1060
+ const readmePath = path10.join(outputDir, "cms-seed", "README.md");
1061
+ const content = `# CMS Seed Data
1062
+
1063
+ Auto-extracted content from your Webflow export, ready to seed into Strapi.
1064
+
1065
+ ## What's in this folder?
1066
+
1067
+ \`seed-data.json\` contains the actual content extracted from your HTML:
1068
+ - **Single types** - Page-specific content (homepage, about page, etc.)
1069
+ - **Collection types** - Repeating items (portfolio cards, team members, etc.)
1070
+
1071
+ ## Structure
1072
+
1073
+ \`\`\`json
1074
+ {
1075
+ "index": {
1076
+ "hero_heading_container": "Actual heading from HTML",
1077
+ "hero_bg_image": "/images/hero.jpg",
1078
+ ...
1079
+ },
1080
+ "portfolio_cards": [
1081
+ {
1082
+ "image": "/images/card1.jpg",
1083
+ "tag": "Technology",
1084
+ "description": "Card description"
1085
+ }
1086
+ ]
1087
+ }
1088
+ \`\`\`
1089
+
1090
+ ## How to Seed Strapi
1091
+
1092
+ ### Option 1: Manual Entry
1093
+ 1. Open Strapi admin panel
1094
+ 2. Go to Content Manager
1095
+ 3. Create entries using the data from \`seed-data.json\`
1096
+
1097
+ ### Option 2: Automated Seeding (Coming Soon)
1098
+ We'll provide a seeding script that:
1099
+ 1. Uploads images to Strapi media library
1100
+ 2. Creates content entries via Strapi API
1101
+ 3. Handles relationships between content types
1102
+
1103
+ ## Image Paths
1104
+
1105
+ Image paths in the seed data reference files in your Nuxt \`public/\` directory:
1106
+ - \`/images/hero.jpg\` \u2192 \`public/images/hero.jpg\`
1107
+
1108
+ When seeding Strapi, these images will be uploaded to Strapi's media library.
1109
+
1110
+ ## Next Steps
1111
+
1112
+ 1. Review the extracted data for accuracy
1113
+ 2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
1114
+ 3. Use this seed data to populate your CMS
1115
+ `;
1116
+ await fs8.writeFile(readmePath, content, "utf-8");
1117
+ }
1118
+
907
1119
  // src/converter.ts
908
1120
  async function convertWebflowExport(options) {
909
1121
  const { inputDir, outputDir, boilerplate } = options;
@@ -912,7 +1124,7 @@ async function convertWebflowExport(options) {
912
1124
  console.log(pc3.dim(`Output: ${outputDir}`));
913
1125
  try {
914
1126
  await setupBoilerplate(boilerplate, outputDir);
915
- const inputExists = await fs8.pathExists(inputDir);
1127
+ const inputExists = await fs9.pathExists(inputDir);
916
1128
  if (!inputExists) {
917
1129
  throw new Error(`Input directory not found: ${inputDir}`);
918
1130
  }
@@ -928,10 +1140,17 @@ async function convertWebflowExport(options) {
928
1140
  console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
929
1141
  const htmlFiles = await findHTMLFiles(inputDir);
930
1142
  console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
1143
+ const htmlContentMap = /* @__PURE__ */ new Map();
1144
+ for (const htmlFile of htmlFiles) {
1145
+ const html = await readHTMLFile(inputDir, htmlFile);
1146
+ const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
1147
+ htmlContentMap.set(pageName, html);
1148
+ console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
1149
+ }
931
1150
  console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
932
1151
  let allEmbeddedStyles = "";
933
1152
  for (const htmlFile of htmlFiles) {
934
- const html = await readHTMLFile(inputDir, htmlFile);
1153
+ const html = htmlContentMap.get(htmlFile.replace(".html", "").replace(/\//g, "-"));
935
1154
  const parsed = parseHTML(html, htmlFile);
936
1155
  if (parsed.embeddedStyles) {
937
1156
  allEmbeddedStyles += `
@@ -947,7 +1166,7 @@ ${parsed.embeddedStyles}
947
1166
  }
948
1167
  await formatVueFiles(outputDir);
949
1168
  console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
950
- const pagesDir = path9.join(outputDir, "pages");
1169
+ const pagesDir = path11.join(outputDir, "pages");
951
1170
  const manifest = await generateManifest(pagesDir);
952
1171
  await writeManifest(outputDir, manifest);
953
1172
  const totalFields = Object.values(manifest.pages).reduce(
@@ -961,12 +1180,26 @@ ${parsed.embeddedStyles}
961
1180
  console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
962
1181
  console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
963
1182
  console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
1183
+ console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
1184
+ console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
1185
+ console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
1186
+ const extractedContent = extractAllContent(htmlContentMap, manifest);
1187
+ const seedData = formatForStrapi(extractedContent);
1188
+ await writeSeedData(outputDir, seedData);
1189
+ await createSeedReadme(outputDir);
1190
+ const pagesWithContent = Object.keys(seedData).filter((key) => {
1191
+ const data = seedData[key];
1192
+ if (Array.isArray(data)) return data.length > 0;
1193
+ return Object.keys(data).length > 0;
1194
+ }).length;
1195
+ console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
1196
+ console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
964
1197
  console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
965
1198
  const schemas = manifestToSchemas(manifest);
966
1199
  await writeAllSchemas(outputDir, schemas);
967
1200
  await createStrapiReadme(outputDir);
968
1201
  console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
969
- console.log(pc3.dim(" View schemas in: strapi/src/api/"));
1202
+ console.log(pc3.dim(" View schemas in: cms-schemas/"));
970
1203
  if (allEmbeddedStyles.trim()) {
971
1204
  console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
972
1205
  const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
@@ -981,7 +1214,7 @@ ${parsed.embeddedStyles}
981
1214
  await updateNuxtConfig(outputDir, assets.css);
982
1215
  console.log(pc3.green(" \u2713 Config updated"));
983
1216
  } catch (error) {
984
- console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
1217
+ console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
985
1218
  console.log(pc3.dim(" Please add CSS files manually"));
986
1219
  }
987
1220
  console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
@@ -994,10 +1227,11 @@ ${parsed.embeddedStyles}
994
1227
  console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
995
1228
  console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
996
1229
  console.log(pc3.dim(` 1. cd ${outputDir}`));
997
- console.log(pc3.dim(" 2. Review cms-manifest.json"));
998
- console.log(pc3.dim(" 3. Copy strapi/ schemas to your Strapi project"));
999
- console.log(pc3.dim(" 4. pnpm install && pnpm dev"));
1000
- console.log(pc3.dim(" 5. Visit http://localhost:3000?preview=true to edit inline!"));
1230
+ console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
1231
+ console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
1232
+ console.log(pc3.dim(" 4. Seed Strapi with data from cms-seed/"));
1233
+ console.log(pc3.dim(" 5. pnpm install && pnpm dev"));
1234
+ console.log(pc3.dim(" 6. Visit http://localhost:3000?preview=true to edit inline!"));
1001
1235
  } catch (error) {
1002
1236
  console.error(pc3.red("\n\u274C Conversion failed:"));
1003
1237
  console.error(pc3.red(error instanceof Error ? error.message : String(error)));
@@ -1005,10 +1239,395 @@ ${parsed.embeddedStyles}
1005
1239
  }
1006
1240
  }
1007
1241
 
1242
+ // src/strapi-setup.ts
1243
+ import fs10 from "fs-extra";
1244
+ import path12 from "path";
1245
+ import { glob as glob2 } from "glob";
1246
+ import * as readline from "readline";
1247
+ async function completeSetup(options) {
1248
+ const {
1249
+ projectDir,
1250
+ strapiDir,
1251
+ strapiUrl = "http://localhost:1337",
1252
+ apiToken
1253
+ } = options;
1254
+ console.log("\u{1F680} Starting complete Strapi setup...\n");
1255
+ console.log("\u{1F4E6} Step 1: Installing schemas...");
1256
+ await installSchemas(projectDir, strapiDir);
1257
+ console.log("\u2713 Schemas installed\n");
1258
+ console.log("\u23F8\uFE0F Step 2: Restart Strapi to load schemas");
1259
+ console.log(" Run: npm run develop (in Strapi directory)");
1260
+ console.log(" Press Enter when Strapi is running...");
1261
+ await waitForEnter();
1262
+ console.log("\n\u{1F50D} Step 3: Checking Strapi connection...");
1263
+ const isRunning = await checkStrapiRunning(strapiUrl);
1264
+ if (!isRunning) {
1265
+ console.error("\u274C Cannot connect to Strapi at", strapiUrl);
1266
+ console.log(" Make sure Strapi is running: npm run develop");
1267
+ process.exit(1);
1268
+ }
1269
+ console.log("\u2713 Connected to Strapi\n");
1270
+ let token = apiToken;
1271
+ if (!token) {
1272
+ console.log("\u{1F511} Step 4: API Token needed");
1273
+ console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
1274
+ console.log(" 2. Go to Settings > API Tokens > Create new API Token");
1275
+ console.log(
1276
+ ' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"'
1277
+ );
1278
+ console.log(" 4. Copy the token and paste it here:\n");
1279
+ token = await promptForToken();
1280
+ console.log("");
1281
+ }
1282
+ console.log("\u{1F4F8} Step 5: Uploading images...");
1283
+ const mediaMap = await uploadAllImages(projectDir, strapiUrl, token);
1284
+ console.log(`\u2713 Uploaded ${Object.keys(mediaMap).length} images
1285
+ `);
1286
+ console.log("\u{1F4DD} Step 6: Seeding content...");
1287
+ await seedContent(projectDir, strapiUrl, token, mediaMap);
1288
+ console.log("\u2713 Content seeded\n");
1289
+ console.log("\u2705 Complete setup finished!");
1290
+ console.log("\n\u{1F4CB} Next steps:");
1291
+ console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
1292
+ console.log(" 2. Check Content Manager - your content should be there!");
1293
+ console.log(" 3. Connect your Nuxt app to Strapi API");
1294
+ }
1295
+ async function installSchemas(projectDir, strapiDir) {
1296
+ if (!await fs10.pathExists(strapiDir)) {
1297
+ console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
1298
+ console.error(` Resolved to: ${path12.resolve(strapiDir)}`);
1299
+ throw new Error(`Strapi directory not found: ${strapiDir}`);
1300
+ }
1301
+ const packageJsonPath = path12.join(strapiDir, "package.json");
1302
+ if (await fs10.pathExists(packageJsonPath)) {
1303
+ const pkg = await fs10.readJson(packageJsonPath);
1304
+ if (!pkg.dependencies?.["@strapi/strapi"]) {
1305
+ console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
1306
+ }
1307
+ }
1308
+ const schemaDir = path12.join(projectDir, "cms-schemas");
1309
+ const schemaFiles = await glob2("*.json", {
1310
+ cwd: schemaDir,
1311
+ absolute: false
1312
+ });
1313
+ if (schemaFiles.length === 0) {
1314
+ console.log("\u26A0\uFE0F No schema files found");
1315
+ return;
1316
+ }
1317
+ console.log(` Found ${schemaFiles.length} schema file(s)`);
1318
+ for (const file of schemaFiles) {
1319
+ const schemaPath = path12.join(schemaDir, file);
1320
+ const schema = await fs10.readJson(schemaPath);
1321
+ const singularName = schema.info?.singularName || path12.basename(file, ".json");
1322
+ console.log(` Installing ${singularName}...`);
1323
+ try {
1324
+ const apiPath = path12.join(strapiDir, "src", "api", singularName);
1325
+ const contentTypesPath = path12.join(
1326
+ apiPath,
1327
+ "content-types",
1328
+ singularName
1329
+ );
1330
+ const targetPath = path12.join(contentTypesPath, "schema.json");
1331
+ await fs10.ensureDir(contentTypesPath);
1332
+ await fs10.ensureDir(path12.join(apiPath, "routes"));
1333
+ await fs10.ensureDir(path12.join(apiPath, "controllers"));
1334
+ await fs10.ensureDir(path12.join(apiPath, "services"));
1335
+ await fs10.writeJson(targetPath, schema, { spaces: 2 });
1336
+ const routeContent = `import { factories } from '@strapi/strapi';
1337
+ export default factories.createCoreRouter('api::${singularName}.${singularName}');
1338
+ `;
1339
+ await fs10.writeFile(
1340
+ path12.join(apiPath, "routes", `${singularName}.ts`),
1341
+ routeContent
1342
+ );
1343
+ const controllerContent = `import { factories } from '@strapi/strapi';
1344
+ export default factories.createCoreController('api::${singularName}.${singularName}');
1345
+ `;
1346
+ await fs10.writeFile(
1347
+ path12.join(apiPath, "controllers", `${singularName}.ts`),
1348
+ controllerContent
1349
+ );
1350
+ const serviceContent = `import { factories } from '@strapi/strapi';
1351
+ export default factories.createCoreService('api::${singularName}.${singularName}');
1352
+ `;
1353
+ await fs10.writeFile(
1354
+ path12.join(apiPath, "services", `${singularName}.ts`),
1355
+ serviceContent
1356
+ );
1357
+ } catch (error) {
1358
+ console.error(` \u2717 Failed to install ${singularName}: ${error.message}`);
1359
+ }
1360
+ }
1361
+ }
1362
+ async function checkStrapiRunning(strapiUrl) {
1363
+ try {
1364
+ const response = await fetch(`${strapiUrl}/_health`);
1365
+ return response.ok;
1366
+ } catch {
1367
+ return false;
1368
+ }
1369
+ }
1370
+ function createReadline() {
1371
+ return readline.createInterface({
1372
+ input: process.stdin,
1373
+ output: process.stdout
1374
+ });
1375
+ }
1376
+ async function waitForEnter() {
1377
+ const rl = createReadline();
1378
+ return new Promise((resolve) => {
1379
+ rl.question("", () => {
1380
+ rl.close();
1381
+ resolve();
1382
+ });
1383
+ });
1384
+ }
1385
+ async function promptForToken() {
1386
+ const rl = createReadline();
1387
+ return new Promise((resolve) => {
1388
+ rl.question(" Token: ", (answer) => {
1389
+ rl.close();
1390
+ resolve(answer.trim());
1391
+ });
1392
+ });
1393
+ }
1394
+ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
1395
+ const mediaMap = /* @__PURE__ */ new Map();
1396
+ const imagesDir = path12.join(projectDir, "public", "assets", "images");
1397
+ if (!await fs10.pathExists(imagesDir)) {
1398
+ console.log(" No images directory found");
1399
+ return mediaMap;
1400
+ }
1401
+ const imageFiles = await glob2("**/*.{jpg,jpeg,png,gif,webp,svg}", {
1402
+ cwd: imagesDir,
1403
+ absolute: false
1404
+ });
1405
+ console.log(` Uploading ${imageFiles.length} images...`);
1406
+ for (const imageFile of imageFiles) {
1407
+ const imagePath = path12.join(imagesDir, imageFile);
1408
+ const mediaId = await uploadImage(
1409
+ imagePath,
1410
+ imageFile,
1411
+ strapiUrl,
1412
+ apiToken
1413
+ );
1414
+ if (mediaId) {
1415
+ mediaMap.set(`/images/${imageFile}`, mediaId);
1416
+ mediaMap.set(imageFile, mediaId);
1417
+ console.log(` \u2713 ${imageFile}`);
1418
+ }
1419
+ }
1420
+ return mediaMap;
1421
+ }
1422
+ async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
1423
+ try {
1424
+ const fileBuffer = await fs10.readFile(filePath);
1425
+ const mimeType = getMimeType(fileName);
1426
+ const blob = new Blob([fileBuffer], { type: mimeType });
1427
+ const formData = new globalThis.FormData();
1428
+ formData.append("files", blob, fileName);
1429
+ const response = await fetch(`${strapiUrl}/api/upload`, {
1430
+ method: "POST",
1431
+ headers: {
1432
+ Authorization: `Bearer ${apiToken}`
1433
+ },
1434
+ body: formData
1435
+ });
1436
+ if (!response.ok) {
1437
+ const errorText = await response.text();
1438
+ console.error(
1439
+ ` \u2717 Failed to upload ${fileName}: ${response.status} - ${errorText}`
1440
+ );
1441
+ return null;
1442
+ }
1443
+ const data = await response.json();
1444
+ return data[0]?.id || null;
1445
+ } catch (error) {
1446
+ console.error(` \u2717 Error uploading ${fileName}:`, error);
1447
+ return null;
1448
+ }
1449
+ }
1450
+ function getMimeType(fileName) {
1451
+ const ext = path12.extname(fileName).toLowerCase();
1452
+ const mimeTypes = {
1453
+ ".jpg": "image/jpeg",
1454
+ ".jpeg": "image/jpeg",
1455
+ ".png": "image/png",
1456
+ ".gif": "image/gif",
1457
+ ".webp": "image/webp",
1458
+ ".svg": "image/svg+xml"
1459
+ };
1460
+ return mimeTypes[ext] || "application/octet-stream";
1461
+ }
1462
+ async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
1463
+ const seedPath = path12.join(projectDir, "cms-seed", "seed-data.json");
1464
+ if (!await fs10.pathExists(seedPath)) {
1465
+ console.log(" No seed data found");
1466
+ return;
1467
+ }
1468
+ const seedData = await fs10.readJson(seedPath);
1469
+ const schemasDir = path12.join(projectDir, "cms-schemas");
1470
+ const schemas = /* @__PURE__ */ new Map();
1471
+ const schemaFiles = await glob2("*.json", { cwd: schemasDir });
1472
+ for (const file of schemaFiles) {
1473
+ const schema = await fs10.readJson(path12.join(schemasDir, file));
1474
+ const name = path12.basename(file, ".json");
1475
+ schemas.set(name, schema);
1476
+ }
1477
+ let successCount = 0;
1478
+ let totalCount = 0;
1479
+ for (const [contentType, data] of Object.entries(seedData)) {
1480
+ const schema = schemas.get(contentType);
1481
+ if (!schema) {
1482
+ console.log(` \u26A0\uFE0F No schema found for ${contentType}, skipping...`);
1483
+ continue;
1484
+ }
1485
+ const singularName = schema.info.singularName;
1486
+ const pluralName = schema.info.pluralName;
1487
+ if (Array.isArray(data)) {
1488
+ console.log(` Seeding ${contentType} (${data.length} items)...`);
1489
+ for (const item of data) {
1490
+ totalCount++;
1491
+ const processedItem = processMediaFields(item, mediaMap);
1492
+ const success = await createEntry(
1493
+ pluralName,
1494
+ processedItem,
1495
+ strapiUrl,
1496
+ apiToken
1497
+ );
1498
+ if (success) successCount++;
1499
+ }
1500
+ } else {
1501
+ console.log(` Seeding ${contentType}...`);
1502
+ totalCount++;
1503
+ const processedData = processMediaFields(data, mediaMap);
1504
+ const success = await createOrUpdateSingleType(
1505
+ singularName,
1506
+ processedData,
1507
+ strapiUrl,
1508
+ apiToken
1509
+ );
1510
+ if (success) successCount++;
1511
+ }
1512
+ }
1513
+ console.log(` \u2713 Successfully seeded ${successCount}/${totalCount} entries`);
1514
+ }
1515
+ function processMediaFields(data, mediaMap) {
1516
+ const processed = {};
1517
+ for (const [key, value] of Object.entries(data)) {
1518
+ if (typeof value === "string") {
1519
+ if (key.includes("image") || key.includes("bg") || value.startsWith("/images/")) {
1520
+ const mediaId = mediaMap.get(value);
1521
+ if (mediaId) {
1522
+ processed[key] = mediaId;
1523
+ } else {
1524
+ processed[key] = value;
1525
+ }
1526
+ } else {
1527
+ processed[key] = value;
1528
+ }
1529
+ } else {
1530
+ processed[key] = value;
1531
+ }
1532
+ }
1533
+ return processed;
1534
+ }
1535
+ async function createEntry(contentType, data, strapiUrl, apiToken) {
1536
+ try {
1537
+ const response = await fetch(`${strapiUrl}/api/${contentType}`, {
1538
+ method: "POST",
1539
+ headers: {
1540
+ "Content-Type": "application/json",
1541
+ Authorization: `Bearer ${apiToken}`
1542
+ },
1543
+ body: JSON.stringify({ data })
1544
+ });
1545
+ if (!response.ok) {
1546
+ const errorText = await response.text();
1547
+ console.error(
1548
+ ` \u2717 Failed to create ${contentType}: ${response.status} - ${errorText}`
1549
+ );
1550
+ return false;
1551
+ }
1552
+ return true;
1553
+ } catch (error) {
1554
+ console.error(` \u2717 Error creating ${contentType}:`, error);
1555
+ return false;
1556
+ }
1557
+ }
1558
+ async function createOrUpdateSingleType(contentType, data, strapiUrl, apiToken) {
1559
+ try {
1560
+ const response = await fetch(`${strapiUrl}/api/${contentType}`, {
1561
+ method: "PUT",
1562
+ headers: {
1563
+ "Content-Type": "application/json",
1564
+ Authorization: `Bearer ${apiToken}`
1565
+ },
1566
+ body: JSON.stringify({ data })
1567
+ });
1568
+ if (!response.ok) {
1569
+ const errorText = await response.text();
1570
+ console.error(
1571
+ ` \u2717 Failed to update ${contentType}: ${response.status} - ${errorText}`
1572
+ );
1573
+ return false;
1574
+ }
1575
+ return true;
1576
+ } catch (error) {
1577
+ console.error(` \u2717 Error updating ${contentType}:`, error);
1578
+ return false;
1579
+ }
1580
+ }
1581
+ async function main() {
1582
+ const args = process.argv.slice(2);
1583
+ if (args.length < 2) {
1584
+ console.log(
1585
+ "Usage: tsx strapi-setup.ts <project-dir> <strapi-dir> [strapi-url] [api-token]"
1586
+ );
1587
+ console.log("");
1588
+ console.log("Example:");
1589
+ console.log(" tsx strapi-setup.ts ./nuxt-project ./strapi-dev");
1590
+ console.log(
1591
+ " tsx strapi-setup.ts ./nuxt-project ./strapi-dev http://localhost:1337 abc123"
1592
+ );
1593
+ process.exit(1);
1594
+ }
1595
+ const [projectDir, strapiDir, strapiUrl, apiToken] = args;
1596
+ await completeSetup({
1597
+ projectDir,
1598
+ strapiDir,
1599
+ strapiUrl,
1600
+ apiToken
1601
+ });
1602
+ }
1603
+ var isMainModule = process.argv[1] && process.argv[1].endsWith("strapi-setup.ts");
1604
+ if (isMainModule) {
1605
+ main().catch((error) => {
1606
+ console.error("\u274C Setup failed:", error.message);
1607
+ process.exit(1);
1608
+ });
1609
+ }
1610
+
1008
1611
  // src/cli.ts
1009
1612
  var program = new Command();
1010
- program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.0");
1011
- program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option("-b, --boilerplate <source>", "Boilerplate source (GitHub URL or local path)").option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option("--cms <type>", "CMS backend type (strapi|contentful|sanity)", "strapi").action(async (input, output, options) => {
1613
+ async function prompt(question) {
1614
+ const rl = readline2.createInterface({
1615
+ input: process.stdin,
1616
+ output: process.stdout
1617
+ });
1618
+ return new Promise((resolve) => {
1619
+ rl.question(question, (answer) => {
1620
+ rl.close();
1621
+ resolve(answer.trim());
1622
+ });
1623
+ });
1624
+ }
1625
+ async function confirm(question) {
1626
+ const answer = await prompt(`${question} (y/n): `);
1627
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
1628
+ }
1629
+ program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.2");
1630
+ program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option("-b, --boilerplate <source>", "Boilerplate source (GitHub URL or local path)").option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option("--cms <type>", "CMS backend type (strapi|contentful|sanity)", "strapi").option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
1012
1631
  try {
1013
1632
  await convertWebflowExport({
1014
1633
  inputDir: input,
@@ -1018,13 +1637,57 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
1018
1637
  generateStrapi: options.generateSchemas,
1019
1638
  cmsBackend: options.cms
1020
1639
  });
1640
+ if (options.interactive && options.cms === "strapi") {
1641
+ console.log("");
1642
+ const shouldSetup = await confirm(
1643
+ pc4.cyan("\u{1F3AF} Would you like to setup Strapi now?")
1644
+ );
1645
+ if (shouldSetup) {
1646
+ const strapiDir = await prompt(
1647
+ pc4.cyan("\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): ")
1648
+ );
1649
+ if (strapiDir) {
1650
+ console.log("");
1651
+ console.log(pc4.cyan("\u{1F680} Starting Strapi setup..."));
1652
+ console.log("");
1653
+ try {
1654
+ await completeSetup({
1655
+ projectDir: output,
1656
+ strapiDir
1657
+ });
1658
+ } catch (error) {
1659
+ console.error(pc4.red("\n\u274C Strapi setup failed"));
1660
+ console.error(pc4.dim("You can run setup manually later with:"));
1661
+ console.error(pc4.dim(` cms setup-strapi ${output} ${strapiDir}`));
1662
+ }
1663
+ }
1664
+ } else {
1665
+ console.log("");
1666
+ console.log(pc4.dim("\u{1F4A1} You can setup Strapi later with:"));
1667
+ console.log(pc4.dim(` cms setup-strapi ${output} <strapi-directory>`));
1668
+ }
1669
+ }
1021
1670
  } catch (error) {
1022
1671
  console.error(pc4.red("Conversion failed"));
1023
1672
  process.exit(1);
1024
1673
  }
1025
1674
  });
1675
+ program.command("setup-strapi").description("Setup Strapi with schemas and seed data").argument("<project-dir>", "Path to converted project directory").argument("<strapi-dir>", "Path to Strapi directory").option("--url <url>", "Strapi URL", "http://localhost:1337").option("--token <token>", "Strapi API token (optional)").action(async (projectDir, strapiDir, options) => {
1676
+ try {
1677
+ await completeSetup({
1678
+ projectDir,
1679
+ strapiDir,
1680
+ strapiUrl: options.url,
1681
+ apiToken: options.token
1682
+ });
1683
+ } catch (error) {
1684
+ console.error(pc4.red("Strapi setup failed"));
1685
+ console.error(error);
1686
+ process.exit(1);
1687
+ }
1688
+ });
1026
1689
  program.command("generate").description("Generate CMS schemas from manifest").argument("<manifest>", "Path to cms-manifest.json").option("-t, --type <cms>", "CMS type (strapi|contentful|sanity)", "strapi").option("-o, --output <dir>", "Output directory for schemas").action(async (manifest, _options) => {
1027
- console.log(pc4.cyan("\u{1F3D7}\uFE0F SeeMS Schema Generator"));
1690
+ console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
1028
1691
  console.log(pc4.dim(`Generating schemas from: ${manifest}`));
1029
1692
  console.log(pc4.yellow("\u26A0\uFE0F Schema generation logic to be implemented"));
1030
1693
  });