@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/README.md +38 -13
- package/dist/cli.mjs +721 -58
- 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,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
|
|
10
|
-
import
|
|
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).
|
|
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
|
-
|
|
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
|
|
567
|
-
if (
|
|
568
|
-
collectionFields.image = `.${
|
|
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
|
|
574
|
-
if (
|
|
575
|
-
collectionFields.tag = `.${
|
|
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
|
|
581
|
-
if (
|
|
582
|
-
collectionFields.title = `.${
|
|
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
|
|
587
|
-
if (
|
|
588
|
-
collectionFields.description = `.${
|
|
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
|
-
|
|
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:
|
|
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
|
|
627
|
+
const classInfo = getPrimaryClass($el.attr("class"));
|
|
617
628
|
if (text) {
|
|
618
629
|
let fieldName;
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
636
|
+
const parentClassInfo = getPrimaryClass($parent.attr("class"));
|
|
624
637
|
const modifier = getContextModifier($, $el);
|
|
625
|
-
if (
|
|
626
|
-
fieldName = modifier ? `${modifier}_${
|
|
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
|
|
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
|
|
645
|
-
if (text && text.length > 20 &&
|
|
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[
|
|
648
|
-
selector: `.${
|
|
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
|
|
661
|
-
if (
|
|
662
|
-
const fieldName =
|
|
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: `.${
|
|
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
|
|
679
|
-
|
|
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:
|
|
790
|
+
collectionName: kebabCaseName,
|
|
760
791
|
info: {
|
|
761
|
-
singularName:
|
|
762
|
-
pluralName
|
|
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
|
|
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:
|
|
824
|
+
collectionName: kebabCaseName,
|
|
793
825
|
info: {
|
|
794
|
-
singularName
|
|
795
|
-
pluralName:
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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.
|
|
999
|
-
console.log(pc3.dim(" 4.
|
|
1000
|
-
console.log(pc3.dim(" 5.
|
|
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
|
-
|
|
1011
|
-
|
|
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{
|
|
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
|
});
|