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