@see-ms/converter 0.1.0 → 0.1.2
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 +562 -19
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +20 -10
- package/dist/index.mjs +563 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/cli.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import pc4 from "picocolors";
|
|
|
6
6
|
|
|
7
7
|
// src/converter.ts
|
|
8
8
|
import pc3 from "picocolors";
|
|
9
|
-
import
|
|
9
|
+
import path9 from "path";
|
|
10
|
+
import fs8 from "fs-extra";
|
|
10
11
|
|
|
11
12
|
// src/filesystem.ts
|
|
12
13
|
import fs from "fs-extra";
|
|
@@ -325,9 +326,86 @@ ${styles}`, "utf-8");
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
// src/
|
|
329
|
+
// src/editor-integration.ts
|
|
329
330
|
import fs3 from "fs-extra";
|
|
330
331
|
import path4 from "path";
|
|
332
|
+
async function createEditorPlugin(outputDir) {
|
|
333
|
+
const pluginsDir = path4.join(outputDir, "plugins");
|
|
334
|
+
await fs3.ensureDir(pluginsDir);
|
|
335
|
+
const pluginContent = `/**
|
|
336
|
+
* CMS Editor Overlay Plugin
|
|
337
|
+
* Loads the inline editor when ?preview=true
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
export default defineNuxtPlugin(() => {
|
|
341
|
+
// Only run on client side
|
|
342
|
+
if (process.server) return;
|
|
343
|
+
|
|
344
|
+
// Check for preview mode
|
|
345
|
+
const params = new URLSearchParams(window.location.search);
|
|
346
|
+
|
|
347
|
+
if (params.get('preview') === 'true') {
|
|
348
|
+
// Dynamically import the editor
|
|
349
|
+
import('@see-ms/editor-overlay').then(({ initEditor, createToolbar }) => {
|
|
350
|
+
const editor = initEditor({
|
|
351
|
+
apiEndpoint: '/api/cms/save',
|
|
352
|
+
richText: true,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
editor.enable();
|
|
356
|
+
|
|
357
|
+
const toolbar = createToolbar(editor);
|
|
358
|
+
document.body.appendChild(toolbar);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
`;
|
|
363
|
+
const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
|
|
364
|
+
await fs3.writeFile(pluginPath, pluginContent, "utf-8");
|
|
365
|
+
}
|
|
366
|
+
async function addEditorDependency(outputDir) {
|
|
367
|
+
const packageJsonPath = path4.join(outputDir, "package.json");
|
|
368
|
+
if (await fs3.pathExists(packageJsonPath)) {
|
|
369
|
+
const packageJson = await fs3.readJson(packageJsonPath);
|
|
370
|
+
if (!packageJson.dependencies) {
|
|
371
|
+
packageJson.dependencies = {};
|
|
372
|
+
}
|
|
373
|
+
packageJson.dependencies["@see-ms/editor-overlay"] = "^0.1.1";
|
|
374
|
+
await fs3.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function createSaveEndpoint(outputDir) {
|
|
378
|
+
const serverDir = path4.join(outputDir, "server", "api", "cms");
|
|
379
|
+
await fs3.ensureDir(serverDir);
|
|
380
|
+
const endpointContent = `/**
|
|
381
|
+
* API endpoint for saving CMS changes
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
export default defineEventHandler(async (event) => {
|
|
385
|
+
const body = await readBody(event);
|
|
386
|
+
|
|
387
|
+
// TODO: Implement actual saving to Strapi
|
|
388
|
+
// For now, just log the changes
|
|
389
|
+
console.log('CMS changes:', body);
|
|
390
|
+
|
|
391
|
+
// In production, this would:
|
|
392
|
+
// 1. Validate the changes
|
|
393
|
+
// 2. Send to Strapi API
|
|
394
|
+
// 3. Return success/error
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
message: 'Changes saved (demo mode)',
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
`;
|
|
402
|
+
const endpointPath = path4.join(serverDir, "save.post.ts");
|
|
403
|
+
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/boilerplate.ts
|
|
407
|
+
import fs4 from "fs-extra";
|
|
408
|
+
import path5 from "path";
|
|
331
409
|
import { execSync as execSync2 } from "child_process";
|
|
332
410
|
import pc2 from "picocolors";
|
|
333
411
|
function isGitHubURL(source) {
|
|
@@ -337,8 +415,8 @@ async function cloneFromGitHub(repoUrl, outputDir) {
|
|
|
337
415
|
console.log(pc2.blue(" Cloning from GitHub..."));
|
|
338
416
|
try {
|
|
339
417
|
execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
|
|
340
|
-
const gitDir =
|
|
341
|
-
await
|
|
418
|
+
const gitDir = path5.join(outputDir, ".git");
|
|
419
|
+
await fs4.remove(gitDir);
|
|
342
420
|
console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
|
|
343
421
|
} catch (error) {
|
|
344
422
|
throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -346,13 +424,13 @@ async function cloneFromGitHub(repoUrl, outputDir) {
|
|
|
346
424
|
}
|
|
347
425
|
async function copyFromLocal(sourcePath, outputDir) {
|
|
348
426
|
console.log(pc2.blue(" Copying from local path..."));
|
|
349
|
-
const sourceExists = await
|
|
427
|
+
const sourceExists = await fs4.pathExists(sourcePath);
|
|
350
428
|
if (!sourceExists) {
|
|
351
429
|
throw new Error(`Local boilerplate not found: ${sourcePath}`);
|
|
352
430
|
}
|
|
353
|
-
await
|
|
431
|
+
await fs4.copy(sourcePath, outputDir, {
|
|
354
432
|
filter: (src) => {
|
|
355
|
-
const name =
|
|
433
|
+
const name = path5.basename(src);
|
|
356
434
|
return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
|
|
357
435
|
}
|
|
358
436
|
});
|
|
@@ -361,25 +439,25 @@ async function copyFromLocal(sourcePath, outputDir) {
|
|
|
361
439
|
async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
362
440
|
if (!boilerplateSource) {
|
|
363
441
|
console.log(pc2.blue("\n\u{1F4E6} Creating minimal Nuxt structure..."));
|
|
364
|
-
await
|
|
365
|
-
await
|
|
366
|
-
await
|
|
367
|
-
await
|
|
368
|
-
await
|
|
369
|
-
const configPath =
|
|
370
|
-
const configExists = await
|
|
442
|
+
await fs4.ensureDir(outputDir);
|
|
443
|
+
await fs4.ensureDir(path5.join(outputDir, "pages"));
|
|
444
|
+
await fs4.ensureDir(path5.join(outputDir, "assets"));
|
|
445
|
+
await fs4.ensureDir(path5.join(outputDir, "public"));
|
|
446
|
+
await fs4.ensureDir(path5.join(outputDir, "utils"));
|
|
447
|
+
const configPath = path5.join(outputDir, "nuxt.config.ts");
|
|
448
|
+
const configExists = await fs4.pathExists(configPath);
|
|
371
449
|
if (!configExists) {
|
|
372
450
|
const basicConfig = `export default defineNuxtConfig({
|
|
373
451
|
devtools: { enabled: true },
|
|
374
452
|
css: [],
|
|
375
453
|
})
|
|
376
454
|
`;
|
|
377
|
-
await
|
|
455
|
+
await fs4.writeFile(configPath, basicConfig, "utf-8");
|
|
378
456
|
}
|
|
379
457
|
console.log(pc2.green(" \u2713 Structure created"));
|
|
380
458
|
return;
|
|
381
459
|
}
|
|
382
|
-
const outputExists = await
|
|
460
|
+
const outputExists = await fs4.pathExists(outputDir);
|
|
383
461
|
if (outputExists) {
|
|
384
462
|
throw new Error(`Output directory already exists: ${outputDir}. Please choose a different path or remove it first.`);
|
|
385
463
|
}
|
|
@@ -391,6 +469,441 @@ async function setupBoilerplate(boilerplateSource, outputDir) {
|
|
|
391
469
|
}
|
|
392
470
|
}
|
|
393
471
|
|
|
472
|
+
// src/manifest.ts
|
|
473
|
+
import fs6 from "fs-extra";
|
|
474
|
+
import path7 from "path";
|
|
475
|
+
|
|
476
|
+
// src/detector.ts
|
|
477
|
+
import * as cheerio2 from "cheerio";
|
|
478
|
+
import fs5 from "fs-extra";
|
|
479
|
+
import path6 from "path";
|
|
480
|
+
function cleanClassName(className) {
|
|
481
|
+
return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).map((cls) => cls.replace(/-/g, "_")).join(" ");
|
|
482
|
+
}
|
|
483
|
+
function getPrimaryClass(classAttr) {
|
|
484
|
+
if (!classAttr) return null;
|
|
485
|
+
const cleaned = cleanClassName(classAttr);
|
|
486
|
+
const classes = cleaned.split(" ").filter((c) => c.length > 0);
|
|
487
|
+
return classes[0] || null;
|
|
488
|
+
}
|
|
489
|
+
function getContextModifier(_$, $el) {
|
|
490
|
+
let $current = $el.parent();
|
|
491
|
+
let depth = 0;
|
|
492
|
+
while ($current.length > 0 && depth < 5) {
|
|
493
|
+
const classes = $current.attr("class");
|
|
494
|
+
if (classes) {
|
|
495
|
+
const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
|
|
496
|
+
if (ccClass) {
|
|
497
|
+
return ccClass.replace("cc-", "").replace(/-/g, "_");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
$current = $current.parent();
|
|
501
|
+
depth++;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function isDecorativeImage(_$, $img) {
|
|
506
|
+
const $parent = $img.parent();
|
|
507
|
+
const parentClass = $parent.attr("class") || "";
|
|
508
|
+
const decorativePatterns = [
|
|
509
|
+
"nav",
|
|
510
|
+
"logo",
|
|
511
|
+
"icon",
|
|
512
|
+
"arrow",
|
|
513
|
+
"button",
|
|
514
|
+
"quote",
|
|
515
|
+
"pagination",
|
|
516
|
+
"footer",
|
|
517
|
+
"link"
|
|
518
|
+
];
|
|
519
|
+
return decorativePatterns.some(
|
|
520
|
+
(pattern) => parentClass.includes(pattern) || parentClass.includes(`${pattern}_`)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
function isInsideButton($, el) {
|
|
524
|
+
const $el = $(el);
|
|
525
|
+
const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
|
|
526
|
+
return $button.length > 0;
|
|
527
|
+
}
|
|
528
|
+
function extractTemplateFromVue(vueContent) {
|
|
529
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
530
|
+
if (!templateMatch) {
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
return templateMatch[1];
|
|
534
|
+
}
|
|
535
|
+
function detectEditableFields(templateHtml) {
|
|
536
|
+
const $ = cheerio2.load(templateHtml);
|
|
537
|
+
const detectedFields = {};
|
|
538
|
+
const detectedCollections = {};
|
|
539
|
+
const collectionElements = /* @__PURE__ */ new Set();
|
|
540
|
+
const processedCollectionClasses = /* @__PURE__ */ new Set();
|
|
541
|
+
const potentialCollections = /* @__PURE__ */ new Map();
|
|
542
|
+
$("[class]").each((_, el) => {
|
|
543
|
+
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, []);
|
|
547
|
+
}
|
|
548
|
+
potentialCollections.get(primaryClass)?.push(el);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
potentialCollections.forEach((elements, className) => {
|
|
552
|
+
if (elements.length >= 2) {
|
|
553
|
+
const $first = $(elements[0]);
|
|
554
|
+
const collectionFields = {};
|
|
555
|
+
processedCollectionClasses.add(className);
|
|
556
|
+
elements.forEach((el) => {
|
|
557
|
+
collectionElements.add(el);
|
|
558
|
+
$(el).find("*").each((_, child) => {
|
|
559
|
+
collectionElements.add(child);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
$first.find("img").each((_, img) => {
|
|
563
|
+
if (isInsideButton($, img)) return;
|
|
564
|
+
const $img = $(img);
|
|
565
|
+
const $parent = $img.parent();
|
|
566
|
+
const parentClass = getPrimaryClass($parent.attr("class"));
|
|
567
|
+
if (parentClass && parentClass.includes("image")) {
|
|
568
|
+
collectionFields.image = `.${parentClass}`;
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
$first.find("div").each((_, el) => {
|
|
573
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
574
|
+
if (primaryClass && primaryClass.includes("tag") && !primaryClass.includes("container")) {
|
|
575
|
+
collectionFields.tag = `.${primaryClass}`;
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
$first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
|
|
580
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
581
|
+
if (primaryClass) {
|
|
582
|
+
collectionFields.title = `.${primaryClass}`;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
$first.find("p").first().each((_, el) => {
|
|
586
|
+
const primaryClass = getPrimaryClass($(el).attr("class"));
|
|
587
|
+
if (primaryClass) {
|
|
588
|
+
collectionFields.description = `.${primaryClass}`;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
$first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
|
|
592
|
+
const $link = $(el);
|
|
593
|
+
const linkText = $link.text().trim();
|
|
594
|
+
if (linkText) {
|
|
595
|
+
collectionFields.link = `.${getPrimaryClass($link.attr("class")) || "a"}`;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
if (Object.keys(collectionFields).length > 0) {
|
|
600
|
+
let collectionName = className;
|
|
601
|
+
if (!collectionName.endsWith("s")) {
|
|
602
|
+
collectionName += "s";
|
|
603
|
+
}
|
|
604
|
+
detectedCollections[collectionName] = {
|
|
605
|
+
selector: `.${className}`,
|
|
606
|
+
fields: collectionFields
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
const $body = $("body");
|
|
612
|
+
$body.find("h1, h2, h3, h4, h5, h6").each((index, el) => {
|
|
613
|
+
if (collectionElements.has(el)) return;
|
|
614
|
+
const $el = $(el);
|
|
615
|
+
const text = $el.text().trim();
|
|
616
|
+
const primaryClass = getPrimaryClass($el.attr("class"));
|
|
617
|
+
if (text) {
|
|
618
|
+
let fieldName;
|
|
619
|
+
if (primaryClass && !primaryClass.startsWith("heading_")) {
|
|
620
|
+
fieldName = primaryClass;
|
|
621
|
+
} else {
|
|
622
|
+
const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
|
|
623
|
+
const parentClass = getPrimaryClass($parent.attr("class"));
|
|
624
|
+
const modifier = getContextModifier($, $el);
|
|
625
|
+
if (parentClass) {
|
|
626
|
+
fieldName = modifier ? `${modifier}_${parentClass}` : parentClass;
|
|
627
|
+
} else if (modifier) {
|
|
628
|
+
fieldName = `${modifier}_heading`;
|
|
629
|
+
} else {
|
|
630
|
+
fieldName = `heading_${index}`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
detectedFields[fieldName] = {
|
|
634
|
+
selector: primaryClass ? `.${primaryClass}` : el.tagName.toLowerCase(),
|
|
635
|
+
type: "plain",
|
|
636
|
+
editable: true
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
$body.find("p").each((_index, el) => {
|
|
641
|
+
if (collectionElements.has(el)) return;
|
|
642
|
+
const $el = $(el);
|
|
643
|
+
const text = $el.text().trim();
|
|
644
|
+
const primaryClass = getPrimaryClass($el.attr("class"));
|
|
645
|
+
if (text && text.length > 20 && primaryClass) {
|
|
646
|
+
const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
|
|
647
|
+
detectedFields[primaryClass] = {
|
|
648
|
+
selector: `.${primaryClass}`,
|
|
649
|
+
type: hasFormatting ? "rich" : "plain",
|
|
650
|
+
editable: true
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
$body.find("img").each((_index, el) => {
|
|
655
|
+
if (collectionElements.has(el)) return;
|
|
656
|
+
if (isInsideButton($, el)) return;
|
|
657
|
+
const $el = $(el);
|
|
658
|
+
if (isDecorativeImage($, $el)) return;
|
|
659
|
+
const $parent = $el.parent();
|
|
660
|
+
const parentClass = getPrimaryClass($parent.attr("class"));
|
|
661
|
+
if (parentClass) {
|
|
662
|
+
const fieldName = parentClass.includes("image") ? parentClass : `${parentClass}_image`;
|
|
663
|
+
detectedFields[fieldName] = {
|
|
664
|
+
selector: `.${parentClass}`,
|
|
665
|
+
type: "image",
|
|
666
|
+
editable: true
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
$body.find("NuxtLink.c_button, a.c_button, .c_button").each((_index, el) => {
|
|
671
|
+
if (collectionElements.has(el)) return;
|
|
672
|
+
const $el = $(el);
|
|
673
|
+
const text = $el.contents().filter(function() {
|
|
674
|
+
return this.type === "text" || this.type === "tag" && this.name === "div";
|
|
675
|
+
}).first().text().trim();
|
|
676
|
+
if (text && text.length > 2) {
|
|
677
|
+
const $parent = $el.closest('[class*="cta"]').first();
|
|
678
|
+
const parentClass = getPrimaryClass($parent.attr("class")) || "button";
|
|
679
|
+
detectedFields[`${parentClass}_button_text`] = {
|
|
680
|
+
selector: `.c_button`,
|
|
681
|
+
type: "plain",
|
|
682
|
+
editable: true
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
fields: detectedFields,
|
|
688
|
+
collections: detectedCollections
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async function analyzeVuePages(pagesDir) {
|
|
692
|
+
const results = {};
|
|
693
|
+
const vueFiles = await fs5.readdir(pagesDir);
|
|
694
|
+
for (const file of vueFiles) {
|
|
695
|
+
if (file.endsWith(".vue")) {
|
|
696
|
+
const filePath = path6.join(pagesDir, file);
|
|
697
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
698
|
+
const template = extractTemplateFromVue(content);
|
|
699
|
+
if (template) {
|
|
700
|
+
const pageName = file.replace(".vue", "");
|
|
701
|
+
results[pageName] = detectEditableFields(template);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return results;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/manifest.ts
|
|
709
|
+
async function generateManifest(pagesDir) {
|
|
710
|
+
const analyzed = await analyzeVuePages(pagesDir);
|
|
711
|
+
const pages = {};
|
|
712
|
+
for (const [pageName, detection] of Object.entries(analyzed)) {
|
|
713
|
+
pages[pageName] = {
|
|
714
|
+
fields: detection.fields,
|
|
715
|
+
collections: detection.collections,
|
|
716
|
+
meta: {
|
|
717
|
+
route: pageName === "index" ? "/" : `/${pageName}`
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const manifest = {
|
|
722
|
+
version: "1.0",
|
|
723
|
+
pages
|
|
724
|
+
};
|
|
725
|
+
return manifest;
|
|
726
|
+
}
|
|
727
|
+
async function writeManifest(outputDir, manifest) {
|
|
728
|
+
const manifestPath = path7.join(outputDir, "cms-manifest.json");
|
|
729
|
+
await fs6.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/transformer.ts
|
|
733
|
+
function mapFieldTypeToStrapi(fieldType) {
|
|
734
|
+
const typeMap = {
|
|
735
|
+
plain: "string",
|
|
736
|
+
rich: "richtext",
|
|
737
|
+
html: "richtext",
|
|
738
|
+
image: "media",
|
|
739
|
+
link: "string",
|
|
740
|
+
email: "email",
|
|
741
|
+
phone: "string"
|
|
742
|
+
};
|
|
743
|
+
return typeMap[fieldType] || "string";
|
|
744
|
+
}
|
|
745
|
+
function pageToStrapiSchema(pageName, fields) {
|
|
746
|
+
const attributes = {};
|
|
747
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
748
|
+
attributes[fieldName] = {
|
|
749
|
+
type: mapFieldTypeToStrapi(field.type),
|
|
750
|
+
required: field.required || false
|
|
751
|
+
};
|
|
752
|
+
if (field.default) {
|
|
753
|
+
attributes[fieldName].default = field.default;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
757
|
+
return {
|
|
758
|
+
kind: "singleType",
|
|
759
|
+
collectionName: pageName.replace(/-/g, "_"),
|
|
760
|
+
info: {
|
|
761
|
+
singularName: pageName.replace(/-/g, "_"),
|
|
762
|
+
pluralName: pageName.replace(/-/g, "_"),
|
|
763
|
+
displayName
|
|
764
|
+
},
|
|
765
|
+
options: {
|
|
766
|
+
draftAndPublish: true
|
|
767
|
+
},
|
|
768
|
+
attributes
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function collectionToStrapiSchema(collectionName, collection) {
|
|
772
|
+
const attributes = {};
|
|
773
|
+
for (const [fieldName, _selector] of Object.entries(collection.fields)) {
|
|
774
|
+
let type = "string";
|
|
775
|
+
if (fieldName === "image" || fieldName.includes("image")) {
|
|
776
|
+
type = "media";
|
|
777
|
+
} else if (fieldName === "description" || fieldName === "content") {
|
|
778
|
+
type = "richtext";
|
|
779
|
+
} else if (fieldName === "link" || fieldName === "url") {
|
|
780
|
+
type = "string";
|
|
781
|
+
} else if (fieldName === "title" || fieldName === "tag") {
|
|
782
|
+
type = "string";
|
|
783
|
+
}
|
|
784
|
+
attributes[fieldName] = {
|
|
785
|
+
type
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
789
|
+
const singularName = collectionName.endsWith("s") ? collectionName.slice(0, -1) : collectionName;
|
|
790
|
+
return {
|
|
791
|
+
kind: "collectionType",
|
|
792
|
+
collectionName: collectionName.replace(/[-_]/g, "_"),
|
|
793
|
+
info: {
|
|
794
|
+
singularName: singularName.replace(/[-_]/g, "_"),
|
|
795
|
+
pluralName: collectionName.replace(/[-_]/g, "_"),
|
|
796
|
+
displayName
|
|
797
|
+
},
|
|
798
|
+
options: {
|
|
799
|
+
draftAndPublish: true
|
|
800
|
+
},
|
|
801
|
+
attributes
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function manifestToSchemas(manifest) {
|
|
805
|
+
const schemas = {};
|
|
806
|
+
for (const [pageName, page] of Object.entries(manifest.pages)) {
|
|
807
|
+
if (page.fields && Object.keys(page.fields).length > 0) {
|
|
808
|
+
schemas[pageName] = pageToStrapiSchema(pageName, page.fields);
|
|
809
|
+
}
|
|
810
|
+
if (page.collections) {
|
|
811
|
+
for (const [collectionName, collection] of Object.entries(page.collections)) {
|
|
812
|
+
schemas[collectionName] = collectionToStrapiSchema(collectionName, collection);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return schemas;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/schema-writer.ts
|
|
820
|
+
import fs7 from "fs-extra";
|
|
821
|
+
import path8 from "path";
|
|
822
|
+
async function writeStrapiSchema(outputDir, name, schema) {
|
|
823
|
+
const schemasDir = path8.join(outputDir, "cms-schemas");
|
|
824
|
+
await fs7.ensureDir(schemasDir);
|
|
825
|
+
const schemaPath = path8.join(schemasDir, `${name}.json`);
|
|
826
|
+
await fs7.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
827
|
+
}
|
|
828
|
+
async function writeAllSchemas(outputDir, schemas) {
|
|
829
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
830
|
+
await writeStrapiSchema(outputDir, name, schema);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function createStrapiReadme(outputDir) {
|
|
834
|
+
const readmePath = path8.join(outputDir, "cms-schemas", "README.md");
|
|
835
|
+
const content = `# CMS Schemas
|
|
836
|
+
|
|
837
|
+
Auto-generated Strapi content type schemas from your Webflow export.
|
|
838
|
+
|
|
839
|
+
## What's in this folder?
|
|
840
|
+
|
|
841
|
+
Each \`.json\` file is a Strapi content type schema:
|
|
842
|
+
|
|
843
|
+
- **Pages** (single types) - Unique pages like \`index.json\`, \`about.json\`
|
|
844
|
+
- **Collections** (collection types) - Repeating content like \`portfolio_cards.json\`
|
|
845
|
+
|
|
846
|
+
## How to use with Strapi
|
|
847
|
+
|
|
848
|
+
### Option 1: Manual Setup (Recommended for learning)
|
|
849
|
+
|
|
850
|
+
1. Start your Strapi project
|
|
851
|
+
2. In Strapi admin, go to **Content-Type Builder**
|
|
852
|
+
3. Create each content type manually using these schemas as reference
|
|
853
|
+
4. Match the field names and types
|
|
854
|
+
|
|
855
|
+
### Option 2: Automated Setup (Advanced)
|
|
856
|
+
|
|
857
|
+
Copy schemas to your Strapi project structure:
|
|
858
|
+
|
|
859
|
+
\`\`\`bash
|
|
860
|
+
# For each schema file, create the Strapi directory structure
|
|
861
|
+
# Example for index.json (single type):
|
|
862
|
+
mkdir -p strapi/src/api/index/content-types/index
|
|
863
|
+
cp cms-schemas/index.json strapi/src/api/index/content-types/index/schema.json
|
|
864
|
+
|
|
865
|
+
# Example for portfolio_cards.json (collection type):
|
|
866
|
+
mkdir -p strapi/src/api/portfolio-cards/content-types/portfolio-card
|
|
867
|
+
cp cms-schemas/portfolio_cards.json strapi/src/api/portfolio-cards/content-types/portfolio-card/schema.json
|
|
868
|
+
\`\`\`
|
|
869
|
+
|
|
870
|
+
Then restart Strapi - it will auto-create the content types.
|
|
871
|
+
|
|
872
|
+
## Schema Structure
|
|
873
|
+
|
|
874
|
+
Each schema defines:
|
|
875
|
+
- \`kind\`: "singleType" (unique page) or "collectionType" (repeating)
|
|
876
|
+
- \`attributes\`: Fields and their types (string, richtext, media, etc.)
|
|
877
|
+
- \`displayName\`: How it appears in Strapi admin
|
|
878
|
+
|
|
879
|
+
## Field Types
|
|
880
|
+
|
|
881
|
+
- \`string\` - Plain text
|
|
882
|
+
- \`richtext\` - Formatted text with HTML
|
|
883
|
+
- \`media\` - Image uploads
|
|
884
|
+
|
|
885
|
+
## Next Steps
|
|
886
|
+
|
|
887
|
+
1. Set up a Strapi project: \`npx create-strapi-app@latest my-strapi\`
|
|
888
|
+
2. Use these schemas to create content types
|
|
889
|
+
3. Populate content in Strapi admin
|
|
890
|
+
4. Connect your Nuxt app to Strapi API
|
|
891
|
+
|
|
892
|
+
## API Usage in Nuxt
|
|
893
|
+
|
|
894
|
+
Once Strapi is running with these content types:
|
|
895
|
+
|
|
896
|
+
\`\`\`typescript
|
|
897
|
+
// Fetch single type (e.g., home page)
|
|
898
|
+
const { data } = await $fetch('http://localhost:1337/api/index')
|
|
899
|
+
|
|
900
|
+
// Fetch collection type (e.g., portfolio cards)
|
|
901
|
+
const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
|
|
902
|
+
\`\`\`
|
|
903
|
+
`;
|
|
904
|
+
await fs7.writeFile(readmePath, content, "utf-8");
|
|
905
|
+
}
|
|
906
|
+
|
|
394
907
|
// src/converter.ts
|
|
395
908
|
async function convertWebflowExport(options) {
|
|
396
909
|
const { inputDir, outputDir, boilerplate } = options;
|
|
@@ -399,7 +912,7 @@ async function convertWebflowExport(options) {
|
|
|
399
912
|
console.log(pc3.dim(`Output: ${outputDir}`));
|
|
400
913
|
try {
|
|
401
914
|
await setupBoilerplate(boilerplate, outputDir);
|
|
402
|
-
const inputExists = await
|
|
915
|
+
const inputExists = await fs8.pathExists(inputDir);
|
|
403
916
|
if (!inputExists) {
|
|
404
917
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
405
918
|
}
|
|
@@ -433,6 +946,27 @@ ${parsed.embeddedStyles}
|
|
|
433
946
|
console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
|
|
434
947
|
}
|
|
435
948
|
await formatVueFiles(outputDir);
|
|
949
|
+
console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
|
|
950
|
+
const pagesDir = path9.join(outputDir, "pages");
|
|
951
|
+
const manifest = await generateManifest(pagesDir);
|
|
952
|
+
await writeManifest(outputDir, manifest);
|
|
953
|
+
const totalFields = Object.values(manifest.pages).reduce(
|
|
954
|
+
(sum, page) => sum + Object.keys(page.fields || {}).length,
|
|
955
|
+
0
|
|
956
|
+
);
|
|
957
|
+
const totalCollections = Object.values(manifest.pages).reduce(
|
|
958
|
+
(sum, page) => sum + Object.keys(page.collections || {}).length,
|
|
959
|
+
0
|
|
960
|
+
);
|
|
961
|
+
console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
|
|
962
|
+
console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
|
|
963
|
+
console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
|
|
964
|
+
console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
965
|
+
const schemas = manifestToSchemas(manifest);
|
|
966
|
+
await writeAllSchemas(outputDir, schemas);
|
|
967
|
+
await createStrapiReadme(outputDir);
|
|
968
|
+
console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
|
|
969
|
+
console.log(pc3.dim(" View schemas in: strapi/src/api/"));
|
|
436
970
|
if (allEmbeddedStyles.trim()) {
|
|
437
971
|
console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
|
|
438
972
|
const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
|
|
@@ -450,11 +984,20 @@ ${parsed.embeddedStyles}
|
|
|
450
984
|
console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
|
|
451
985
|
console.log(pc3.dim(" Please add CSS files manually"));
|
|
452
986
|
}
|
|
987
|
+
console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
|
|
988
|
+
await createEditorPlugin(outputDir);
|
|
989
|
+
await addEditorDependency(outputDir);
|
|
990
|
+
await createSaveEndpoint(outputDir);
|
|
991
|
+
console.log(pc3.green(" \u2713 Editor plugin created"));
|
|
992
|
+
console.log(pc3.green(" \u2713 Editor dependency added"));
|
|
993
|
+
console.log(pc3.green(" \u2713 Save endpoint created"));
|
|
453
994
|
console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
|
|
454
995
|
console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
|
|
455
996
|
console.log(pc3.dim(` 1. cd ${outputDir}`));
|
|
456
|
-
console.log(pc3.dim(" 2.
|
|
457
|
-
console.log(pc3.dim(" 3.
|
|
997
|
+
console.log(pc3.dim(" 2. Review cms-manifest.json"));
|
|
998
|
+
console.log(pc3.dim(" 3. Copy strapi/ schemas to your Strapi project"));
|
|
999
|
+
console.log(pc3.dim(" 4. pnpm install && pnpm dev"));
|
|
1000
|
+
console.log(pc3.dim(" 5. Visit http://localhost:3000?preview=true to edit inline!"));
|
|
458
1001
|
} catch (error) {
|
|
459
1002
|
console.error(pc3.red("\n\u274C Conversion failed:"));
|
|
460
1003
|
console.error(pc3.red(error instanceof Error ? error.message : String(error)));
|