@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/README.md +38 -13
- package/dist/cli.mjs +1230 -24
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +20 -10
- package/dist/index.mjs +798 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
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
|
|
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/
|
|
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 =
|
|
341
|
-
await
|
|
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
|
|
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
|
|
432
|
+
await fs4.copy(sourcePath, outputDir, {
|
|
354
433
|
filter: (src) => {
|
|
355
|
-
const name =
|
|
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
|
|
365
|
-
await
|
|
366
|
-
await
|
|
367
|
-
await
|
|
368
|
-
await
|
|
369
|
-
const configPath =
|
|
370
|
-
const configExists = await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
457
|
-
console.log(pc3.dim(" 3.
|
|
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
|
-
|
|
468
|
-
|
|
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{
|
|
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
|
});
|