@see-ms/converter 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3,10 +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 fs4 from "fs-extra";
10
+ import path11 from "path";
11
+ import fs9 from "fs-extra";
10
12
 
11
13
  // src/filesystem.ts
12
14
  import fs from "fs-extra";
@@ -325,9 +327,86 @@ ${styles}`, "utf-8");
325
327
  }
326
328
  }
327
329
 
328
- // src/boilerplate.ts
330
+ // src/editor-integration.ts
329
331
  import fs3 from "fs-extra";
330
332
  import path4 from "path";
333
+ async function createEditorPlugin(outputDir) {
334
+ const pluginsDir = path4.join(outputDir, "plugins");
335
+ await fs3.ensureDir(pluginsDir);
336
+ const pluginContent = `/**
337
+ * CMS Editor Overlay Plugin
338
+ * Loads the inline editor when ?preview=true
339
+ */
340
+
341
+ export default defineNuxtPlugin(() => {
342
+ // Only run on client side
343
+ if (process.server) return;
344
+
345
+ // Check for preview mode
346
+ const params = new URLSearchParams(window.location.search);
347
+
348
+ if (params.get('preview') === 'true') {
349
+ // Dynamically import the editor
350
+ import('@see-ms/editor-overlay').then(({ initEditor, createToolbar }) => {
351
+ const editor = initEditor({
352
+ apiEndpoint: '/api/cms/save',
353
+ richText: true,
354
+ });
355
+
356
+ editor.enable();
357
+
358
+ const toolbar = createToolbar(editor);
359
+ document.body.appendChild(toolbar);
360
+ });
361
+ }
362
+ });
363
+ `;
364
+ const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
365
+ await fs3.writeFile(pluginPath, pluginContent, "utf-8");
366
+ }
367
+ async function addEditorDependency(outputDir) {
368
+ const packageJsonPath = path4.join(outputDir, "package.json");
369
+ if (await fs3.pathExists(packageJsonPath)) {
370
+ const packageJson = await fs3.readJson(packageJsonPath);
371
+ if (!packageJson.dependencies) {
372
+ packageJson.dependencies = {};
373
+ }
374
+ packageJson.dependencies["@see-ms/editor-overlay"] = "^0.1.1";
375
+ await fs3.writeJson(packageJsonPath, packageJson, { spaces: 2 });
376
+ }
377
+ }
378
+ async function createSaveEndpoint(outputDir) {
379
+ const serverDir = path4.join(outputDir, "server", "api", "cms");
380
+ await fs3.ensureDir(serverDir);
381
+ const endpointContent = `/**
382
+ * API endpoint for saving CMS changes
383
+ */
384
+
385
+ export default defineEventHandler(async (event) => {
386
+ const body = await readBody(event);
387
+
388
+ // TODO: Implement actual saving to Strapi
389
+ // For now, just log the changes
390
+ console.log('CMS changes:', body);
391
+
392
+ // In production, this would:
393
+ // 1. Validate the changes
394
+ // 2. Send to Strapi API
395
+ // 3. Return success/error
396
+
397
+ return {
398
+ success: true,
399
+ message: 'Changes saved (demo mode)',
400
+ };
401
+ });
402
+ `;
403
+ const endpointPath = path4.join(serverDir, "save.post.ts");
404
+ await fs3.writeFile(endpointPath, endpointContent, "utf-8");
405
+ }
406
+
407
+ // src/boilerplate.ts
408
+ import fs4 from "fs-extra";
409
+ import path5 from "path";
331
410
  import { execSync as execSync2 } from "child_process";
332
411
  import pc2 from "picocolors";
333
412
  function isGitHubURL(source) {
@@ -337,8 +416,8 @@ async function cloneFromGitHub(repoUrl, outputDir) {
337
416
  console.log(pc2.blue(" Cloning from GitHub..."));
338
417
  try {
339
418
  execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
340
- const gitDir = path4.join(outputDir, ".git");
341
- await fs3.remove(gitDir);
419
+ const gitDir = path5.join(outputDir, ".git");
420
+ await fs4.remove(gitDir);
342
421
  console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
343
422
  } catch (error) {
344
423
  throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
@@ -346,13 +425,13 @@ async function cloneFromGitHub(repoUrl, outputDir) {
346
425
  }
347
426
  async function copyFromLocal(sourcePath, outputDir) {
348
427
  console.log(pc2.blue(" Copying from local path..."));
349
- const sourceExists = await fs3.pathExists(sourcePath);
428
+ const sourceExists = await fs4.pathExists(sourcePath);
350
429
  if (!sourceExists) {
351
430
  throw new Error(`Local boilerplate not found: ${sourcePath}`);
352
431
  }
353
- await fs3.copy(sourcePath, outputDir, {
432
+ await fs4.copy(sourcePath, outputDir, {
354
433
  filter: (src) => {
355
- const name = path4.basename(src);
434
+ const name = path5.basename(src);
356
435
  return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
357
436
  }
358
437
  });
@@ -361,25 +440,25 @@ async function copyFromLocal(sourcePath, outputDir) {
361
440
  async function setupBoilerplate(boilerplateSource, outputDir) {
362
441
  if (!boilerplateSource) {
363
442
  console.log(pc2.blue("\n\u{1F4E6} Creating minimal Nuxt structure..."));
364
- await fs3.ensureDir(outputDir);
365
- await fs3.ensureDir(path4.join(outputDir, "pages"));
366
- await fs3.ensureDir(path4.join(outputDir, "assets"));
367
- await fs3.ensureDir(path4.join(outputDir, "public"));
368
- await fs3.ensureDir(path4.join(outputDir, "utils"));
369
- const configPath = path4.join(outputDir, "nuxt.config.ts");
370
- const configExists = await fs3.pathExists(configPath);
443
+ await fs4.ensureDir(outputDir);
444
+ await fs4.ensureDir(path5.join(outputDir, "pages"));
445
+ await fs4.ensureDir(path5.join(outputDir, "assets"));
446
+ await fs4.ensureDir(path5.join(outputDir, "public"));
447
+ await fs4.ensureDir(path5.join(outputDir, "utils"));
448
+ const configPath = path5.join(outputDir, "nuxt.config.ts");
449
+ const configExists = await fs4.pathExists(configPath);
371
450
  if (!configExists) {
372
451
  const basicConfig = `export default defineNuxtConfig({
373
452
  devtools: { enabled: true },
374
453
  css: [],
375
454
  })
376
455
  `;
377
- await fs3.writeFile(configPath, basicConfig, "utf-8");
456
+ await fs4.writeFile(configPath, basicConfig, "utf-8");
378
457
  }
379
458
  console.log(pc2.green(" \u2713 Structure created"));
380
459
  return;
381
460
  }
382
- const outputExists = await fs3.pathExists(outputDir);
461
+ const outputExists = await fs4.pathExists(outputDir);
383
462
  if (outputExists) {
384
463
  throw new Error(`Output directory already exists: ${outputDir}. Please choose a different path or remove it first.`);
385
464
  }
@@ -391,6 +470,652 @@ async function setupBoilerplate(boilerplateSource, outputDir) {
391
470
  }
392
471
  }
393
472
 
473
+ // src/manifest.ts
474
+ import fs6 from "fs-extra";
475
+ import path7 from "path";
476
+
477
+ // src/detector.ts
478
+ import * as cheerio2 from "cheerio";
479
+ import fs5 from "fs-extra";
480
+ import path6 from "path";
481
+ function cleanClassName(className) {
482
+ return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
483
+ }
484
+ function getPrimaryClass(classAttr) {
485
+ if (!classAttr) return null;
486
+ const cleaned = cleanClassName(classAttr);
487
+ const classes = cleaned.split(" ").filter((c) => c.length > 0);
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
+ };
496
+ }
497
+ function getContextModifier(_$, $el) {
498
+ let $current = $el.parent();
499
+ let depth = 0;
500
+ while ($current.length > 0 && depth < 5) {
501
+ const classes = $current.attr("class");
502
+ if (classes) {
503
+ const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
504
+ if (ccClass) {
505
+ return ccClass.replace("cc-", "").replace(/-/g, "_");
506
+ }
507
+ }
508
+ $current = $current.parent();
509
+ depth++;
510
+ }
511
+ return null;
512
+ }
513
+ function isDecorativeImage(_$, $img) {
514
+ const $parent = $img.parent();
515
+ const parentClass = $parent.attr("class") || "";
516
+ const decorativePatterns = [
517
+ "nav",
518
+ "logo",
519
+ "icon",
520
+ "arrow",
521
+ "button",
522
+ "quote",
523
+ "pagination",
524
+ "footer",
525
+ "link"
526
+ ];
527
+ return decorativePatterns.some(
528
+ (pattern) => parentClass.includes(pattern) || parentClass.includes(`${pattern}_`)
529
+ );
530
+ }
531
+ function isInsideButton($, el) {
532
+ const $el = $(el);
533
+ const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
534
+ return $button.length > 0;
535
+ }
536
+ function extractTemplateFromVue(vueContent) {
537
+ const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
538
+ if (!templateMatch) {
539
+ return "";
540
+ }
541
+ return templateMatch[1];
542
+ }
543
+ function detectEditableFields(templateHtml) {
544
+ const $ = cheerio2.load(templateHtml);
545
+ const detectedFields = {};
546
+ const detectedCollections = {};
547
+ const collectionElements = /* @__PURE__ */ new Set();
548
+ const processedCollectionClasses = /* @__PURE__ */ new Set();
549
+ const potentialCollections = /* @__PURE__ */ new Map();
550
+ $("[class]").each((_, el) => {
551
+ const primaryClass = getPrimaryClass($(el).attr("class"));
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, []);
555
+ }
556
+ potentialCollections.get(primaryClass.fieldName)?.push(el);
557
+ }
558
+ });
559
+ potentialCollections.forEach((elements, className) => {
560
+ if (elements.length >= 2) {
561
+ const $first = $(elements[0]);
562
+ const collectionFields = {};
563
+ processedCollectionClasses.add(className);
564
+ elements.forEach((el) => {
565
+ collectionElements.add(el);
566
+ $(el).find("*").each((_, child) => {
567
+ collectionElements.add(child);
568
+ });
569
+ });
570
+ const collectionClassInfo = getPrimaryClass($(elements[0]).attr("class"));
571
+ const collectionSelector = collectionClassInfo ? `.${collectionClassInfo.selector}` : `.${className}`;
572
+ $first.find("img").each((_, img) => {
573
+ if (isInsideButton($, img)) return;
574
+ const $img = $(img);
575
+ const $parent = $img.parent();
576
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
577
+ if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
578
+ collectionFields.image = `.${parentClassInfo.selector}`;
579
+ return false;
580
+ }
581
+ });
582
+ $first.find("div").each((_, el) => {
583
+ const classInfo = getPrimaryClass($(el).attr("class"));
584
+ if (classInfo && classInfo.fieldName.includes("tag") && !classInfo.fieldName.includes("container")) {
585
+ collectionFields.tag = `.${classInfo.selector}`;
586
+ return false;
587
+ }
588
+ });
589
+ $first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
590
+ const classInfo = getPrimaryClass($(el).attr("class"));
591
+ if (classInfo) {
592
+ collectionFields.title = `.${classInfo.selector}`;
593
+ }
594
+ });
595
+ $first.find("p").first().each((_, el) => {
596
+ const classInfo = getPrimaryClass($(el).attr("class"));
597
+ if (classInfo) {
598
+ collectionFields.description = `.${classInfo.selector}`;
599
+ }
600
+ });
601
+ $first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
602
+ const $link = $(el);
603
+ const linkText = $link.text().trim();
604
+ if (linkText) {
605
+ const classInfo = getPrimaryClass($link.attr("class"));
606
+ collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
607
+ return false;
608
+ }
609
+ });
610
+ if (Object.keys(collectionFields).length > 0) {
611
+ let collectionName = className;
612
+ if (!collectionName.endsWith("s")) {
613
+ collectionName += "s";
614
+ }
615
+ detectedCollections[collectionName] = {
616
+ selector: collectionSelector,
617
+ fields: collectionFields
618
+ };
619
+ }
620
+ }
621
+ });
622
+ const $body = $("body");
623
+ $body.find("h1, h2, h3, h4, h5, h6").each((index, el) => {
624
+ if (collectionElements.has(el)) return;
625
+ const $el = $(el);
626
+ const text = $el.text().trim();
627
+ const classInfo = getPrimaryClass($el.attr("class"));
628
+ if (text) {
629
+ let fieldName;
630
+ let selector;
631
+ if (classInfo && !classInfo.fieldName.startsWith("heading_")) {
632
+ fieldName = classInfo.fieldName;
633
+ selector = `.${classInfo.selector}`;
634
+ } else {
635
+ const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
636
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
637
+ const modifier = getContextModifier($, $el);
638
+ if (parentClassInfo) {
639
+ fieldName = modifier ? `${modifier}_${parentClassInfo.fieldName}` : parentClassInfo.fieldName;
640
+ selector = classInfo ? `.${classInfo.selector}` : `.${parentClassInfo.selector}`;
641
+ } else if (modifier) {
642
+ fieldName = `${modifier}_heading`;
643
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
644
+ } else {
645
+ fieldName = `heading_${index}`;
646
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
647
+ }
648
+ }
649
+ detectedFields[fieldName] = {
650
+ selector,
651
+ type: "plain",
652
+ editable: true
653
+ };
654
+ }
655
+ });
656
+ $body.find("p").each((_index, el) => {
657
+ if (collectionElements.has(el)) return;
658
+ const $el = $(el);
659
+ const text = $el.text().trim();
660
+ const classInfo = getPrimaryClass($el.attr("class"));
661
+ if (text && text.length > 20 && classInfo) {
662
+ const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
663
+ detectedFields[classInfo.fieldName] = {
664
+ selector: `.${classInfo.selector}`,
665
+ type: hasFormatting ? "rich" : "plain",
666
+ editable: true
667
+ };
668
+ }
669
+ });
670
+ $body.find("img").each((_index, el) => {
671
+ if (collectionElements.has(el)) return;
672
+ if (isInsideButton($, el)) return;
673
+ const $el = $(el);
674
+ if (isDecorativeImage($, $el)) return;
675
+ const $parent = $el.parent();
676
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
677
+ if (parentClassInfo) {
678
+ const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
679
+ detectedFields[fieldName] = {
680
+ selector: `.${parentClassInfo.selector}`,
681
+ type: "image",
682
+ editable: true
683
+ };
684
+ }
685
+ });
686
+ $body.find("NuxtLink.c_button, a.c_button, .c_button").each((_index, el) => {
687
+ if (collectionElements.has(el)) return;
688
+ const $el = $(el);
689
+ const text = $el.contents().filter(function() {
690
+ return this.type === "text" || this.type === "tag" && this.name === "div";
691
+ }).first().text().trim();
692
+ if (text && text.length > 2) {
693
+ const $parent = $el.closest('[class*="cta"]').first();
694
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
695
+ const fieldName = parentClassInfo ? `${parentClassInfo.fieldName}_button_text` : "button_text";
696
+ detectedFields[fieldName] = {
697
+ selector: `.c_button`,
698
+ type: "plain",
699
+ editable: true
700
+ };
701
+ }
702
+ });
703
+ return {
704
+ fields: detectedFields,
705
+ collections: detectedCollections
706
+ };
707
+ }
708
+ async function analyzeVuePages(pagesDir) {
709
+ const results = {};
710
+ const vueFiles = await fs5.readdir(pagesDir);
711
+ for (const file of vueFiles) {
712
+ if (file.endsWith(".vue")) {
713
+ const filePath = path6.join(pagesDir, file);
714
+ const content = await fs5.readFile(filePath, "utf-8");
715
+ const template = extractTemplateFromVue(content);
716
+ if (template) {
717
+ const pageName = file.replace(".vue", "");
718
+ results[pageName] = detectEditableFields(template);
719
+ }
720
+ }
721
+ }
722
+ return results;
723
+ }
724
+
725
+ // src/manifest.ts
726
+ async function generateManifest(pagesDir) {
727
+ const analyzed = await analyzeVuePages(pagesDir);
728
+ const pages = {};
729
+ for (const [pageName, detection] of Object.entries(analyzed)) {
730
+ pages[pageName] = {
731
+ fields: detection.fields,
732
+ collections: detection.collections,
733
+ meta: {
734
+ route: pageName === "index" ? "/" : `/${pageName}`
735
+ }
736
+ };
737
+ }
738
+ const manifest = {
739
+ version: "1.0",
740
+ pages
741
+ };
742
+ return manifest;
743
+ }
744
+ async function writeManifest(outputDir, manifest) {
745
+ const manifestPath = path7.join(outputDir, "cms-manifest.json");
746
+ await fs6.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
747
+ }
748
+
749
+ // src/transformer.ts
750
+ function mapFieldTypeToStrapi(fieldType) {
751
+ const typeMap = {
752
+ plain: "string",
753
+ rich: "richtext",
754
+ html: "richtext",
755
+ image: "media",
756
+ link: "string",
757
+ email: "email",
758
+ phone: "string"
759
+ };
760
+ return typeMap[fieldType] || "string";
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
+ }
774
+ function pageToStrapiSchema(pageName, fields) {
775
+ const attributes = {};
776
+ for (const [fieldName, field] of Object.entries(fields)) {
777
+ attributes[fieldName] = {
778
+ type: mapFieldTypeToStrapi(field.type),
779
+ required: field.required || false
780
+ };
781
+ if (field.default) {
782
+ attributes[fieldName].default = field.default;
783
+ }
784
+ }
785
+ const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
786
+ const kebabCaseName = pageName;
787
+ const pluralName = pluralize(kebabCaseName);
788
+ return {
789
+ kind: "singleType",
790
+ collectionName: kebabCaseName,
791
+ info: {
792
+ singularName: kebabCaseName,
793
+ pluralName,
794
+ displayName
795
+ },
796
+ options: {
797
+ draftAndPublish: true
798
+ },
799
+ attributes
800
+ };
801
+ }
802
+ function collectionToStrapiSchema(collectionName, collection) {
803
+ const attributes = {};
804
+ for (const [fieldName, _selector] of Object.entries(collection.fields)) {
805
+ let type = "string";
806
+ if (fieldName === "image" || fieldName.includes("image")) {
807
+ type = "media";
808
+ } else if (fieldName === "description" || fieldName === "content") {
809
+ type = "richtext";
810
+ } else if (fieldName === "link" || fieldName === "url") {
811
+ type = "string";
812
+ } else if (fieldName === "title" || fieldName === "tag") {
813
+ type = "string";
814
+ }
815
+ attributes[fieldName] = {
816
+ type
817
+ };
818
+ }
819
+ const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
820
+ const kebabCaseName = collectionName.replace(/_/g, "-");
821
+ const singularName = kebabCaseName.endsWith("s") ? kebabCaseName.slice(0, -1) : kebabCaseName;
822
+ return {
823
+ kind: "collectionType",
824
+ collectionName: kebabCaseName,
825
+ info: {
826
+ singularName,
827
+ pluralName: kebabCaseName,
828
+ displayName
829
+ },
830
+ options: {
831
+ draftAndPublish: true
832
+ },
833
+ attributes
834
+ };
835
+ }
836
+ function manifestToSchemas(manifest) {
837
+ const schemas = {};
838
+ for (const [pageName, page] of Object.entries(manifest.pages)) {
839
+ if (page.fields && Object.keys(page.fields).length > 0) {
840
+ schemas[pageName] = pageToStrapiSchema(pageName, page.fields);
841
+ }
842
+ if (page.collections) {
843
+ for (const [collectionName, collection] of Object.entries(page.collections)) {
844
+ schemas[collectionName] = collectionToStrapiSchema(collectionName, collection);
845
+ }
846
+ }
847
+ }
848
+ return schemas;
849
+ }
850
+
851
+ // src/schema-writer.ts
852
+ import fs7 from "fs-extra";
853
+ import path8 from "path";
854
+ async function writeStrapiSchema(outputDir, name, schema) {
855
+ const schemasDir = path8.join(outputDir, "cms-schemas");
856
+ await fs7.ensureDir(schemasDir);
857
+ const schemaPath = path8.join(schemasDir, `${name}.json`);
858
+ await fs7.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
859
+ }
860
+ async function writeAllSchemas(outputDir, schemas) {
861
+ for (const [name, schema] of Object.entries(schemas)) {
862
+ await writeStrapiSchema(outputDir, name, schema);
863
+ }
864
+ }
865
+ async function createStrapiReadme(outputDir) {
866
+ const readmePath = path8.join(outputDir, "cms-schemas", "README.md");
867
+ const content = `# CMS Schemas
868
+
869
+ Auto-generated Strapi content type schemas from your Webflow export.
870
+
871
+ ## What's in this folder?
872
+
873
+ Each \`.json\` file is a Strapi content type schema:
874
+
875
+ - **Pages** (single types) - Unique pages like \`index.json\`, \`about.json\`
876
+ - **Collections** (collection types) - Repeating content like \`portfolio_cards.json\`
877
+
878
+ ## How to use with Strapi
879
+
880
+ ### Option 1: Manual Setup (Recommended for learning)
881
+
882
+ 1. Start your Strapi project
883
+ 2. In Strapi admin, go to **Content-Type Builder**
884
+ 3. Create each content type manually using these schemas as reference
885
+ 4. Match the field names and types
886
+
887
+ ### Option 2: Automated Setup (Advanced)
888
+
889
+ Copy schemas to your Strapi project structure:
890
+
891
+ \`\`\`bash
892
+ # For each schema file, create the Strapi directory structure
893
+ # Example for index.json (single type):
894
+ mkdir -p strapi/src/api/index/content-types/index
895
+ cp cms-schemas/index.json strapi/src/api/index/content-types/index/schema.json
896
+
897
+ # Example for portfolio_cards.json (collection type):
898
+ mkdir -p strapi/src/api/portfolio-cards/content-types/portfolio-card
899
+ cp cms-schemas/portfolio_cards.json strapi/src/api/portfolio-cards/content-types/portfolio-card/schema.json
900
+ \`\`\`
901
+
902
+ Then restart Strapi - it will auto-create the content types.
903
+
904
+ ## Schema Structure
905
+
906
+ Each schema defines:
907
+ - \`kind\`: "singleType" (unique page) or "collectionType" (repeating)
908
+ - \`attributes\`: Fields and their types (string, richtext, media, etc.)
909
+ - \`displayName\`: How it appears in Strapi admin
910
+
911
+ ## Field Types
912
+
913
+ - \`string\` - Plain text
914
+ - \`richtext\` - Formatted text with HTML
915
+ - \`media\` - Image uploads
916
+
917
+ ## Next Steps
918
+
919
+ 1. Set up a Strapi project: \`npx create-strapi-app@latest my-strapi\`
920
+ 2. Use these schemas to create content types
921
+ 3. Populate content in Strapi admin
922
+ 4. Connect your Nuxt app to Strapi API
923
+
924
+ ## API Usage in Nuxt
925
+
926
+ Once Strapi is running with these content types:
927
+
928
+ \`\`\`typescript
929
+ // Fetch single type (e.g., home page)
930
+ const { data } = await $fetch('http://localhost:1337/api/index')
931
+
932
+ // Fetch collection type (e.g., portfolio cards)
933
+ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
934
+ \`\`\`
935
+ `;
936
+ await fs7.writeFile(readmePath, content, "utf-8");
937
+ }
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
+
394
1119
  // src/converter.ts
395
1120
  async function convertWebflowExport(options) {
396
1121
  const { inputDir, outputDir, boilerplate } = options;
@@ -399,7 +1124,7 @@ async function convertWebflowExport(options) {
399
1124
  console.log(pc3.dim(`Output: ${outputDir}`));
400
1125
  try {
401
1126
  await setupBoilerplate(boilerplate, outputDir);
402
- const inputExists = await fs4.pathExists(inputDir);
1127
+ const inputExists = await fs9.pathExists(inputDir);
403
1128
  if (!inputExists) {
404
1129
  throw new Error(`Input directory not found: ${inputDir}`);
405
1130
  }
@@ -415,10 +1140,17 @@ async function convertWebflowExport(options) {
415
1140
  console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
416
1141
  const htmlFiles = await findHTMLFiles(inputDir);
417
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
+ }
418
1150
  console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
419
1151
  let allEmbeddedStyles = "";
420
1152
  for (const htmlFile of htmlFiles) {
421
- const html = await readHTMLFile(inputDir, htmlFile);
1153
+ const html = htmlContentMap.get(htmlFile.replace(".html", "").replace(/\//g, "-"));
422
1154
  const parsed = parseHTML(html, htmlFile);
423
1155
  if (parsed.embeddedStyles) {
424
1156
  allEmbeddedStyles += `
@@ -433,6 +1165,41 @@ ${parsed.embeddedStyles}
433
1165
  console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
434
1166
  }
435
1167
  await formatVueFiles(outputDir);
1168
+ console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
1169
+ const pagesDir = path11.join(outputDir, "pages");
1170
+ const manifest = await generateManifest(pagesDir);
1171
+ await writeManifest(outputDir, manifest);
1172
+ const totalFields = Object.values(manifest.pages).reduce(
1173
+ (sum, page) => sum + Object.keys(page.fields || {}).length,
1174
+ 0
1175
+ );
1176
+ const totalCollections = Object.values(manifest.pages).reduce(
1177
+ (sum, page) => sum + Object.keys(page.collections || {}).length,
1178
+ 0
1179
+ );
1180
+ console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
1181
+ console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
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`));
1197
+ console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
1198
+ const schemas = manifestToSchemas(manifest);
1199
+ await writeAllSchemas(outputDir, schemas);
1200
+ await createStrapiReadme(outputDir);
1201
+ console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
1202
+ console.log(pc3.dim(" View schemas in: cms-schemas/"));
436
1203
  if (allEmbeddedStyles.trim()) {
437
1204
  console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
438
1205
  const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
@@ -447,14 +1214,24 @@ ${parsed.embeddedStyles}
447
1214
  await updateNuxtConfig(outputDir, assets.css);
448
1215
  console.log(pc3.green(" \u2713 Config updated"));
449
1216
  } catch (error) {
450
- console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
1217
+ console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
451
1218
  console.log(pc3.dim(" Please add CSS files manually"));
452
1219
  }
1220
+ console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
1221
+ await createEditorPlugin(outputDir);
1222
+ await addEditorDependency(outputDir);
1223
+ await createSaveEndpoint(outputDir);
1224
+ console.log(pc3.green(" \u2713 Editor plugin created"));
1225
+ console.log(pc3.green(" \u2713 Editor dependency added"));
1226
+ console.log(pc3.green(" \u2713 Save endpoint created"));
453
1227
  console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
454
1228
  console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
455
1229
  console.log(pc3.dim(` 1. cd ${outputDir}`));
456
- console.log(pc3.dim(" 2. pnpm install"));
457
- console.log(pc3.dim(" 3. pnpm dev"));
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!"));
458
1235
  } catch (error) {
459
1236
  console.error(pc3.red("\n\u274C Conversion failed:"));
460
1237
  console.error(pc3.red(error instanceof Error ? error.message : String(error)));
@@ -462,10 +1239,395 @@ ${parsed.embeddedStyles}
462
1239
  }
463
1240
  }
464
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
+
465
1611
  // src/cli.ts
466
1612
  var program = new Command();
467
- program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.0");
468
- program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option("-b, --boilerplate <source>", "Boilerplate source (GitHub URL or local path)").option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option("--cms <type>", "CMS backend type (strapi|contentful|sanity)", "strapi").action(async (input, output, options) => {
1613
+ async function prompt(question) {
1614
+ const rl = readline2.createInterface({
1615
+ input: process.stdin,
1616
+ output: process.stdout
1617
+ });
1618
+ return new Promise((resolve) => {
1619
+ rl.question(question, (answer) => {
1620
+ rl.close();
1621
+ resolve(answer.trim());
1622
+ });
1623
+ });
1624
+ }
1625
+ async function confirm(question) {
1626
+ const answer = await prompt(`${question} (y/n): `);
1627
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
1628
+ }
1629
+ program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.2");
1630
+ program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option("-b, --boilerplate <source>", "Boilerplate source (GitHub URL or local path)").option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option("--cms <type>", "CMS backend type (strapi|contentful|sanity)", "strapi").option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
469
1631
  try {
470
1632
  await convertWebflowExport({
471
1633
  inputDir: input,
@@ -475,13 +1637,57 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
475
1637
  generateStrapi: options.generateSchemas,
476
1638
  cmsBackend: options.cms
477
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
+ }
478
1670
  } catch (error) {
479
1671
  console.error(pc4.red("Conversion failed"));
480
1672
  process.exit(1);
481
1673
  }
482
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
+ });
483
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) => {
484
- console.log(pc4.cyan("\u{1F3D7}\uFE0F SeeMS Schema Generator"));
1690
+ console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
485
1691
  console.log(pc4.dim(`Generating schemas from: ${manifest}`));
486
1692
  console.log(pc4.yellow("\u26A0\uFE0F Schema generation logic to be implemented"));
487
1693
  });