@see-ms/converter 0.1.3 → 0.1.5
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 +1350 -126
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +14 -1
- package/dist/index.mjs +1146 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import pc4 from "picocolors";
|
|
6
6
|
import * as readline2 from "readline";
|
|
7
|
+
import fs12 from "fs-extra";
|
|
8
|
+
import path14 from "path";
|
|
7
9
|
|
|
8
10
|
// src/converter.ts
|
|
9
11
|
import pc3 from "picocolors";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
+
import path12 from "path";
|
|
13
|
+
import fs10 from "fs-extra";
|
|
12
14
|
|
|
13
15
|
// src/filesystem.ts
|
|
14
16
|
import fs from "fs-extra";
|
|
@@ -326,39 +328,423 @@ ${styles}`, "utf-8");
|
|
|
326
328
|
${styles}`, "utf-8");
|
|
327
329
|
}
|
|
328
330
|
}
|
|
331
|
+
async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:1337") {
|
|
332
|
+
const configPath = path3.join(outputDir, "nuxt.config.ts");
|
|
333
|
+
const configExists = await fs2.pathExists(configPath);
|
|
334
|
+
if (!configExists) {
|
|
335
|
+
throw new Error("nuxt.config.ts not found in output directory");
|
|
336
|
+
}
|
|
337
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
338
|
+
if (config.includes("runtimeConfig:")) {
|
|
339
|
+
if (config.includes("public:")) {
|
|
340
|
+
config = config.replace(
|
|
341
|
+
/public:\s*\{/,
|
|
342
|
+
`public: {
|
|
343
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}',`
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
config = config.replace(
|
|
347
|
+
/runtimeConfig:\s*\{/,
|
|
348
|
+
`runtimeConfig: {
|
|
349
|
+
public: {
|
|
350
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
|
|
351
|
+
},`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
config = config.replace(
|
|
356
|
+
/export default defineNuxtConfig\(\{/,
|
|
357
|
+
`export default defineNuxtConfig({
|
|
358
|
+
runtimeConfig: {
|
|
359
|
+
public: {
|
|
360
|
+
strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
|
|
361
|
+
}
|
|
362
|
+
},`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
await fs2.writeFile(configPath, config, "utf-8");
|
|
366
|
+
}
|
|
329
367
|
|
|
330
368
|
// src/editor-integration.ts
|
|
331
369
|
import fs3 from "fs-extra";
|
|
332
370
|
import path4 from "path";
|
|
371
|
+
async function createEditorContentComposable(outputDir) {
|
|
372
|
+
const composablesDir = path4.join(outputDir, "composables");
|
|
373
|
+
await fs3.ensureDir(composablesDir);
|
|
374
|
+
const composableContent = `/**
|
|
375
|
+
* Global state for editor content in preview mode
|
|
376
|
+
* This allows the editor overlay to update content reactively
|
|
377
|
+
*/
|
|
378
|
+
|
|
379
|
+
// Global reactive state
|
|
380
|
+
const editorState = reactive<{
|
|
381
|
+
isPreviewMode: boolean;
|
|
382
|
+
currentPage: string | null;
|
|
383
|
+
content: Record<string, Record<string, any>>; // page -> field -> value
|
|
384
|
+
hasChanges: Record<string, boolean>; // page -> hasChanges
|
|
385
|
+
}>({
|
|
386
|
+
isPreviewMode: false,
|
|
387
|
+
currentPage: null,
|
|
388
|
+
content: {},
|
|
389
|
+
hasChanges: {},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
export function useEditorContent(pageName?: string) {
|
|
393
|
+
const route = useRoute();
|
|
394
|
+
|
|
395
|
+
// Check if we're in preview mode
|
|
396
|
+
const isPreviewMode = computed(() => route.query.preview === 'true');
|
|
397
|
+
|
|
398
|
+
// Update global state
|
|
399
|
+
if (import.meta.client) {
|
|
400
|
+
editorState.isPreviewMode = isPreviewMode.value;
|
|
401
|
+
if (pageName) {
|
|
402
|
+
editorState.currentPage = pageName;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Get content for specific page
|
|
407
|
+
const getPageContent = (page: string) => {
|
|
408
|
+
return editorState.content[page] || {};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Update a field's value
|
|
412
|
+
const updateField = (page: string, fieldName: string, value: any) => {
|
|
413
|
+
if (!editorState.content[page]) {
|
|
414
|
+
editorState.content[page] = {};
|
|
415
|
+
}
|
|
416
|
+
editorState.content[page][fieldName] = value;
|
|
417
|
+
editorState.hasChanges[page] = true;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Clear all changes for a page
|
|
421
|
+
const clearPageChanges = (page: string) => {
|
|
422
|
+
delete editorState.content[page];
|
|
423
|
+
editorState.hasChanges[page] = false;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Initialize page content from Strapi data
|
|
427
|
+
const initializePageContent = (page: string, content: Record<string, any>) => {
|
|
428
|
+
if (!editorState.content[page]) {
|
|
429
|
+
editorState.content[page] = { ...content };
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Get content for current page (reactive)
|
|
434
|
+
const content = computed(() => {
|
|
435
|
+
const page = pageName || editorState.currentPage;
|
|
436
|
+
if (!page) return {};
|
|
437
|
+
return editorState.content[page] || {};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Check if page has unsaved changes
|
|
441
|
+
const hasChanges = computed(() => {
|
|
442
|
+
const page = pageName || editorState.currentPage;
|
|
443
|
+
if (!page) return false;
|
|
444
|
+
return editorState.hasChanges[page] || false;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Get all pages with changes
|
|
448
|
+
const pagesWithChanges = computed(() => {
|
|
449
|
+
return Object.keys(editorState.hasChanges).filter(
|
|
450
|
+
(page) => editorState.hasChanges[page]
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Expose state for window object (for editor overlay to access)
|
|
455
|
+
if (import.meta.client) {
|
|
456
|
+
(window as any).__editorState = editorState;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
isPreviewMode,
|
|
461
|
+
content,
|
|
462
|
+
hasChanges,
|
|
463
|
+
pagesWithChanges,
|
|
464
|
+
getPageContent,
|
|
465
|
+
updateField,
|
|
466
|
+
clearPageChanges,
|
|
467
|
+
initializePageContent,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
`;
|
|
471
|
+
const composablePath = path4.join(composablesDir, "useEditorContent.ts");
|
|
472
|
+
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
473
|
+
}
|
|
474
|
+
async function createStrapiContentComposable(outputDir) {
|
|
475
|
+
const composablesDir = path4.join(outputDir, "composables");
|
|
476
|
+
await fs3.ensureDir(composablesDir);
|
|
477
|
+
const composableContent = `/**
|
|
478
|
+
* Composable to fetch content from Strapi based on CMS manifest
|
|
479
|
+
* Integrates with editor state for preview mode
|
|
480
|
+
*/
|
|
481
|
+
|
|
482
|
+
export function useStrapiContent(pageName: string) {
|
|
483
|
+
const config = useRuntimeConfig();
|
|
484
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
485
|
+
const editorContent = useEditorContent(pageName);
|
|
486
|
+
|
|
487
|
+
// Helper to transform Strapi image objects to URL strings
|
|
488
|
+
const transformStrapiImages = (data: any, baseUrl: string): any => {
|
|
489
|
+
if (!data || typeof data !== 'object') return data;
|
|
490
|
+
|
|
491
|
+
const transformed: any = {};
|
|
492
|
+
|
|
493
|
+
for (const [key, value] of Object.entries(data)) {
|
|
494
|
+
if (value && typeof value === 'object') {
|
|
495
|
+
// Check if it's a Strapi media object
|
|
496
|
+
if ('url' in value && ('mime' in value || 'formats' in value)) {
|
|
497
|
+
// It's an image - extract the URL
|
|
498
|
+
transformed[key] = value.url.startsWith('http')
|
|
499
|
+
? value.url
|
|
500
|
+
: \`\${baseUrl}\${value.url}\`;
|
|
501
|
+
} else if (Array.isArray(value)) {
|
|
502
|
+
// Handle arrays (collections of images)
|
|
503
|
+
transformed[key] = value.map((item) =>
|
|
504
|
+
item && typeof item === 'object' && 'url' in item
|
|
505
|
+
? item.url.startsWith('http')
|
|
506
|
+
? item.url
|
|
507
|
+
: \`\${baseUrl}\${item.url}\`
|
|
508
|
+
: item
|
|
509
|
+
);
|
|
510
|
+
} else {
|
|
511
|
+
// Recursively transform nested objects
|
|
512
|
+
transformed[key] = transformStrapiImages(value, baseUrl);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
transformed[key] = value;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return transformed;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Fetch content from Strapi with populated media fields
|
|
523
|
+
const { data: strapiData } = useFetch<any>(
|
|
524
|
+
\`\${strapiUrl}/api/\${pageName}\`,
|
|
525
|
+
{
|
|
526
|
+
key: \`strapi-\${pageName}\`,
|
|
527
|
+
query: {
|
|
528
|
+
populate: '*', // Strapi v5: Populate all fields including images
|
|
529
|
+
},
|
|
530
|
+
transform: (response) => {
|
|
531
|
+
// Strapi v5 returns data in response.data
|
|
532
|
+
const data = response?.data || response;
|
|
533
|
+
|
|
534
|
+
// Transform image fields from Strapi objects to URL strings
|
|
535
|
+
if (data && typeof data === 'object') {
|
|
536
|
+
return transformStrapiImages(data, strapiUrl);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return data;
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// Initialize editor state with Strapi data when fetched
|
|
545
|
+
// This runs in both normal AND preview mode to ensure initial content is available
|
|
546
|
+
watch(
|
|
547
|
+
strapiData,
|
|
548
|
+
(newData) => {
|
|
549
|
+
if (newData) {
|
|
550
|
+
// Always initialize from Strapi on first load
|
|
551
|
+
// Drafts will override this when they load in the editor
|
|
552
|
+
editorContent.initializePageContent(pageName, newData);
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
{ immediate: true }
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// In preview mode: use editor state
|
|
559
|
+
// In normal mode: use Strapi data (and sync to editor state)
|
|
560
|
+
const content = computed(() => {
|
|
561
|
+
if (editorContent.isPreviewMode.value) {
|
|
562
|
+
// Use editor state in preview mode
|
|
563
|
+
return editorContent.getPageContent(pageName);
|
|
564
|
+
} else {
|
|
565
|
+
// Use Strapi data in normal mode
|
|
566
|
+
return strapiData.value || editorContent.getPageContent(pageName);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
content,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
const composablePath = path4.join(composablesDir, "useStrapiContent.ts");
|
|
576
|
+
await fs3.writeFile(composablePath, composableContent, "utf-8");
|
|
577
|
+
}
|
|
333
578
|
async function createEditorPlugin(outputDir) {
|
|
334
579
|
const pluginsDir = path4.join(outputDir, "plugins");
|
|
335
580
|
await fs3.ensureDir(pluginsDir);
|
|
336
581
|
const pluginContent = `/**
|
|
337
582
|
* CMS Editor Overlay Plugin
|
|
338
|
-
* Loads the inline editor when ?preview=true
|
|
583
|
+
* Loads the inline editor when ?preview=true with full state management
|
|
584
|
+
*/
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Disable Lenis smooth scroll to allow native scrolling in edit mode
|
|
339
588
|
*/
|
|
589
|
+
function disableLenisInEditMode() {
|
|
590
|
+
try {
|
|
591
|
+
// Check for Lenis in common locations
|
|
592
|
+
const lenisInstances = [
|
|
593
|
+
(window as any).lenis,
|
|
594
|
+
(window as any).__lenis,
|
|
595
|
+
document.querySelector('.lenis'),
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
for (const lenis of lenisInstances) {
|
|
599
|
+
if (lenis && typeof lenis.destroy === 'function') {
|
|
600
|
+
lenis.destroy();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
340
604
|
|
|
341
|
-
|
|
605
|
+
// Check for Vue Lenis component instances
|
|
606
|
+
const lenisElements = document.querySelectorAll('[data-lenis], .lenis');
|
|
607
|
+
if (lenisElements.length > 0) {
|
|
608
|
+
// Try to find and destroy via data attributes or component instances
|
|
609
|
+
lenisElements.forEach((el: any) => {
|
|
610
|
+
if (el.__lenis && typeof el.__lenis.destroy === 'function') {
|
|
611
|
+
el.__lenis.destroy();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
// Silently fail - Lenis may not be present
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
342
621
|
// Only run on client side
|
|
343
622
|
if (process.server) return;
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
623
|
+
|
|
624
|
+
// Import editor overlay modules
|
|
625
|
+
const {
|
|
626
|
+
initEditor,
|
|
627
|
+
createAuthManager,
|
|
628
|
+
showLoginModal,
|
|
629
|
+
createDraftStorage,
|
|
630
|
+
createURLStateManager,
|
|
631
|
+
createManifestLoader,
|
|
632
|
+
createNavigationGuard,
|
|
633
|
+
getCurrentPageFromRoute,
|
|
634
|
+
} = await import('@see-ms/editor-overlay');
|
|
635
|
+
|
|
636
|
+
// Initialize URL state manager
|
|
637
|
+
const urlState = createURLStateManager();
|
|
638
|
+
const state = urlState.getState();
|
|
639
|
+
|
|
640
|
+
// Only proceed if in preview mode
|
|
641
|
+
if (!state.preview) return;
|
|
642
|
+
|
|
643
|
+
// Get Strapi URL from runtime config
|
|
644
|
+
const config = useRuntimeConfig();
|
|
645
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
646
|
+
|
|
647
|
+
// Initialize components
|
|
648
|
+
const authManager = createAuthManager({
|
|
649
|
+
strapiUrl,
|
|
650
|
+
storageKey: 'cms_editor_token',
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const draftStorage = createDraftStorage();
|
|
654
|
+
const manifestLoader = createManifestLoader();
|
|
655
|
+
|
|
656
|
+
// Load manifest
|
|
657
|
+
try {
|
|
658
|
+
await manifestLoader.load();
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.error('[CMS Editor] Failed to load manifest:', error);
|
|
661
|
+
return;
|
|
361
662
|
}
|
|
663
|
+
|
|
664
|
+
// Get current page from route
|
|
665
|
+
let currentPage = getCurrentPageFromRoute();
|
|
666
|
+
if (!currentPage) {
|
|
667
|
+
currentPage = manifestLoader.getPageFromRoute(window.location.pathname);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!currentPage) {
|
|
671
|
+
console.error('[CMS Editor] Could not determine current page');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// URL state only manages preview mode (page is derived from route)
|
|
676
|
+
urlState.setState({ preview: true });
|
|
677
|
+
|
|
678
|
+
// Auth flow
|
|
679
|
+
let token = authManager.getToken();
|
|
680
|
+
if (!token || !await authManager.verifyToken(token)) {
|
|
681
|
+
try {
|
|
682
|
+
token = await showLoginModal(authManager);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
// Login cancelled - exit preview mode
|
|
685
|
+
console.log('[CMS Editor] Login cancelled');
|
|
686
|
+
urlState.clearPreviewMode();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Disable Lenis smooth scroll in edit mode (allows native scrolling)
|
|
692
|
+
disableLenisInEditMode();
|
|
693
|
+
|
|
694
|
+
// Initialize navigation guard
|
|
695
|
+
const navigationGuard = createNavigationGuard({
|
|
696
|
+
showToast: true,
|
|
697
|
+
toastMessage: 'Navigation disabled in edit mode',
|
|
698
|
+
});
|
|
699
|
+
navigationGuard.enable();
|
|
700
|
+
|
|
701
|
+
// Initialize editor with full context
|
|
702
|
+
const editor = initEditor({
|
|
703
|
+
apiEndpoint: '/api/cms/save',
|
|
704
|
+
authToken: token,
|
|
705
|
+
richText: true,
|
|
706
|
+
manifestLoader,
|
|
707
|
+
draftStorage,
|
|
708
|
+
currentPage,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Enable editor (will auto-load drafts)
|
|
712
|
+
await editor.enable();
|
|
713
|
+
|
|
714
|
+
// Create toolbar with navigation
|
|
715
|
+
const { createToolbar } = await import('@see-ms/editor-overlay');
|
|
716
|
+
const toolbar = await createToolbar(editor, {
|
|
717
|
+
draftStorage,
|
|
718
|
+
urlState,
|
|
719
|
+
navigationGuard,
|
|
720
|
+
manifestLoader,
|
|
721
|
+
currentPage,
|
|
722
|
+
});
|
|
723
|
+
document.body.appendChild(toolbar);
|
|
724
|
+
|
|
725
|
+
// Watch for route changes
|
|
726
|
+
const router = useRouter();
|
|
727
|
+
router.afterEach(async (to) => {
|
|
728
|
+
const newPage = manifestLoader.getPageFromRoute(to.path);
|
|
729
|
+
if (newPage && newPage !== currentPage) {
|
|
730
|
+
currentPage = newPage;
|
|
731
|
+
await editor.setPage(newPage);
|
|
732
|
+
|
|
733
|
+
// Update toolbar if it has an update method
|
|
734
|
+
if (typeof (toolbar as any).updateCurrentPage === 'function') {
|
|
735
|
+
await (toolbar as any).updateCurrentPage(newPage);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Cleanup on navigation away from preview mode
|
|
741
|
+
nuxtApp.hook('page:finish', () => {
|
|
742
|
+
const currentState = urlState.getState();
|
|
743
|
+
if (!currentState.preview) {
|
|
744
|
+
navigationGuard.disable();
|
|
745
|
+
editor.destroy();
|
|
746
|
+
}
|
|
747
|
+
});
|
|
362
748
|
});
|
|
363
749
|
`;
|
|
364
750
|
const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
|
|
@@ -380,29 +766,595 @@ async function createSaveEndpoint(outputDir) {
|
|
|
380
766
|
await fs3.ensureDir(serverDir);
|
|
381
767
|
const endpointContent = `/**
|
|
382
768
|
* API endpoint for saving CMS changes
|
|
769
|
+
* Handles draft and final saving to Strapi
|
|
383
770
|
*/
|
|
384
771
|
|
|
772
|
+
import fs from 'fs';
|
|
773
|
+
import path from 'path';
|
|
774
|
+
|
|
385
775
|
export default defineEventHandler(async (event) => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
776
|
+
// Get Strapi URL from runtime config
|
|
777
|
+
const config = useRuntimeConfig();
|
|
778
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
779
|
+
|
|
780
|
+
// Extract Authorization header
|
|
781
|
+
const authHeader = getHeader(event, 'authorization');
|
|
782
|
+
|
|
783
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
784
|
+
throw createError({
|
|
785
|
+
statusCode: 401,
|
|
786
|
+
statusMessage: 'Unauthorized: Missing or invalid authorization header',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
791
|
+
|
|
792
|
+
// Verify token with Strapi and determine if it's an admin or user token
|
|
793
|
+
let userResponse: any;
|
|
794
|
+
let isAdminToken = false;
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
// Try admin token verification first
|
|
798
|
+
try {
|
|
799
|
+
userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
800
|
+
headers: {
|
|
801
|
+
Authorization: \`Bearer \${token}\`,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
isAdminToken = true;
|
|
805
|
+
} catch (adminError) {
|
|
806
|
+
// Fallback to regular user token verification
|
|
807
|
+
userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
808
|
+
headers: {
|
|
809
|
+
Authorization: \`Bearer \${token}\`,
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
isAdminToken = false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Get the request body
|
|
816
|
+
const body = await readBody(event);
|
|
817
|
+
const { page, fields, isDraft = true } = body;
|
|
818
|
+
|
|
819
|
+
if (!page || !fields) {
|
|
820
|
+
throw createError({
|
|
821
|
+
statusCode: 400,
|
|
822
|
+
statusMessage: 'Bad Request: Missing page or fields',
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Load manifest to understand field mappings
|
|
827
|
+
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
828
|
+
let manifest;
|
|
829
|
+
try {
|
|
830
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
831
|
+
manifest = JSON.parse(manifestContent);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
console.error('Failed to load manifest:', error);
|
|
834
|
+
throw createError({
|
|
835
|
+
statusCode: 500,
|
|
836
|
+
statusMessage: 'Failed to load CMS manifest',
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Get page configuration from manifest
|
|
841
|
+
const pageConfig = manifest.pages[page];
|
|
842
|
+
if (!pageConfig) {
|
|
843
|
+
throw createError({
|
|
844
|
+
statusCode: 404,
|
|
845
|
+
statusMessage: \`Page "\${page}" not found in manifest\`,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Transform fields to Strapi format
|
|
850
|
+
const strapiData: Record<string, any> = {};
|
|
851
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
852
|
+
const fieldConfig = pageConfig.fields[fieldName];
|
|
853
|
+
if (!fieldConfig) {
|
|
854
|
+
console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Handle different field types
|
|
859
|
+
if (fieldConfig.type === 'image') {
|
|
860
|
+
// TODO: Handle image uploads - for now just store the value
|
|
861
|
+
strapiData[fieldName] = value;
|
|
862
|
+
} else {
|
|
863
|
+
strapiData[fieldName] = value;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Update Strapi v5 content - use different endpoints for admin vs user tokens
|
|
868
|
+
if (isAdminToken) {
|
|
869
|
+
// Admin tokens use the content-manager API (Strapi v5)
|
|
870
|
+
const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
871
|
+
|
|
872
|
+
// Step 1: Update the content
|
|
873
|
+
await $fetch(contentEndpoint, {
|
|
874
|
+
method: 'PUT',
|
|
875
|
+
headers: {
|
|
876
|
+
'Authorization': \`Bearer \${token}\`,
|
|
877
|
+
'Content-Type': 'application/json',
|
|
878
|
+
},
|
|
879
|
+
body: strapiData,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Step 2: Publish if not a draft (Strapi v5)
|
|
883
|
+
if (!isDraft) {
|
|
884
|
+
const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
|
|
885
|
+
await $fetch(publishEndpoint, {
|
|
886
|
+
method: 'POST',
|
|
887
|
+
headers: {
|
|
888
|
+
'Authorization': \`Bearer \${token}\`,
|
|
889
|
+
'Content-Type': 'application/json',
|
|
890
|
+
},
|
|
891
|
+
body: {},
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
} else {
|
|
895
|
+
// User tokens use the regular REST API
|
|
896
|
+
const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
|
|
897
|
+
|
|
898
|
+
await $fetch(strapiEndpoint, {
|
|
899
|
+
method: 'PUT',
|
|
900
|
+
headers: {
|
|
901
|
+
'Authorization': \`Bearer \${token}\`,
|
|
902
|
+
'Content-Type': 'application/json',
|
|
903
|
+
},
|
|
904
|
+
body: {
|
|
905
|
+
data: strapiData,
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Publish if not a draft (Strapi v5)
|
|
910
|
+
if (!isDraft) {
|
|
911
|
+
const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
|
|
912
|
+
await $fetch(publishEndpoint, {
|
|
913
|
+
method: 'POST',
|
|
914
|
+
headers: {
|
|
915
|
+
'Authorization': \`Bearer \${token}\`,
|
|
916
|
+
'Content-Type': 'application/json',
|
|
917
|
+
},
|
|
918
|
+
body: {},
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
console.log(\`[CMS Save] Updated "\${page}" in Strapi (draft: \${isDraft})\`);
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
success: true,
|
|
927
|
+
message: 'Changes saved successfully',
|
|
928
|
+
page,
|
|
929
|
+
isDraft,
|
|
930
|
+
user: {
|
|
931
|
+
id: userResponse.id,
|
|
932
|
+
username: userResponse.username || userResponse.firstname || 'Unknown',
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
} catch (error: any) {
|
|
936
|
+
console.error('[CMS Save] Error:', error);
|
|
937
|
+
|
|
938
|
+
// Token verification failed
|
|
939
|
+
if (error.statusCode === 401 || error.status === 401) {
|
|
940
|
+
throw createError({
|
|
941
|
+
statusCode: 401,
|
|
942
|
+
statusMessage: 'Unauthorized: Invalid or expired token',
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Strapi error
|
|
947
|
+
if (error.statusCode || error.status) {
|
|
948
|
+
throw createError({
|
|
949
|
+
statusCode: error.statusCode || error.status,
|
|
950
|
+
statusMessage: error.statusMessage || error.message || 'Failed to save to Strapi',
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Generic error
|
|
955
|
+
throw createError({
|
|
956
|
+
statusCode: 500,
|
|
957
|
+
statusMessage: 'Internal server error while saving changes',
|
|
958
|
+
});
|
|
959
|
+
}
|
|
401
960
|
});
|
|
402
961
|
`;
|
|
403
962
|
const endpointPath = path4.join(serverDir, "save.post.ts");
|
|
404
963
|
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
405
964
|
}
|
|
965
|
+
async function createStrapiBootstrap(outputDir) {
|
|
966
|
+
const strapiBootstrapDir = path4.join(outputDir, "strapi-bootstrap");
|
|
967
|
+
await fs3.ensureDir(strapiBootstrapDir);
|
|
968
|
+
const bootstrapContent = `/**
|
|
969
|
+
* Strapi Bootstrap File
|
|
970
|
+
* Auto-enables public read permissions for all single types
|
|
971
|
+
*
|
|
972
|
+
* Place this file in your Strapi project at: src/index.ts
|
|
973
|
+
*/
|
|
974
|
+
|
|
975
|
+
export default {
|
|
976
|
+
/**
|
|
977
|
+
* Bootstrap function runs when Strapi starts
|
|
978
|
+
*/
|
|
979
|
+
async bootstrap({ strapi }: { strapi: any }) {
|
|
980
|
+
try {
|
|
981
|
+
console.log('[Bootstrap] Configuring public permissions for CMS...');
|
|
982
|
+
|
|
983
|
+
// Get the public role
|
|
984
|
+
const publicRole = await strapi
|
|
985
|
+
.query('plugin::users-permissions.role')
|
|
986
|
+
.findOne({ where: { type: 'public' } });
|
|
987
|
+
|
|
988
|
+
if (!publicRole) {
|
|
989
|
+
console.error('[Bootstrap] Public role not found');
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Get all content types
|
|
994
|
+
const contentTypes = Object.keys(strapi.contentTypes).filter(
|
|
995
|
+
(uid) => uid.startsWith('api::')
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
// Enable find and findOne for each content type
|
|
999
|
+
const permissions = await strapi
|
|
1000
|
+
.query('plugin::users-permissions.permission')
|
|
1001
|
+
.findMany({
|
|
1002
|
+
where: {
|
|
1003
|
+
role: publicRole.id,
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
let updatedCount = 0;
|
|
1008
|
+
|
|
1009
|
+
for (const contentType of contentTypes) {
|
|
1010
|
+
const [, apiName] = contentType.split('::');
|
|
1011
|
+
const [controllerName] = apiName.split('.');
|
|
1012
|
+
|
|
1013
|
+
// Find or create find permission
|
|
1014
|
+
const findPermission = permissions.find(
|
|
1015
|
+
(p: any) =>
|
|
1016
|
+
p.action === \`api::\${apiName}.find\` ||
|
|
1017
|
+
p.action === 'find' && p.controller === controllerName
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
const findOnePermission = permissions.find(
|
|
1021
|
+
(p: any) =>
|
|
1022
|
+
p.action === \`api::\${apiName}.findOne\` ||
|
|
1023
|
+
p.action === 'findOne' && p.controller === controllerName
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// Enable find
|
|
1027
|
+
if (findPermission && !findPermission.enabled) {
|
|
1028
|
+
await strapi
|
|
1029
|
+
.query('plugin::users-permissions.permission')
|
|
1030
|
+
.update({
|
|
1031
|
+
where: { id: findPermission.id },
|
|
1032
|
+
data: { enabled: true },
|
|
1033
|
+
});
|
|
1034
|
+
updatedCount++;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Enable findOne
|
|
1038
|
+
if (findOnePermission && !findOnePermission.enabled) {
|
|
1039
|
+
await strapi
|
|
1040
|
+
.query('plugin::users-permissions.permission')
|
|
1041
|
+
.update({
|
|
1042
|
+
where: { id: findOnePermission.id },
|
|
1043
|
+
data: { enabled: true },
|
|
1044
|
+
});
|
|
1045
|
+
updatedCount++;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// If permissions don't exist, create them
|
|
1049
|
+
if (!findPermission) {
|
|
1050
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
1051
|
+
data: {
|
|
1052
|
+
action: \`api::\${apiName}.find\`,
|
|
1053
|
+
role: publicRole.id,
|
|
1054
|
+
enabled: true,
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
updatedCount++;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (!findOnePermission) {
|
|
1061
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
1062
|
+
data: {
|
|
1063
|
+
action: \`api::\${apiName}.findOne\`,
|
|
1064
|
+
role: publicRole.id,
|
|
1065
|
+
enabled: true,
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
updatedCount++;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
console.log(
|
|
1073
|
+
\`[Bootstrap] \u2705 Enabled \${updatedCount} public permissions for \${contentTypes.length} content types\`
|
|
1074
|
+
);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
console.error('[Bootstrap] Error enabling public permissions:', error);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
`;
|
|
1081
|
+
const bootstrapPath = path4.join(strapiBootstrapDir, "index.ts");
|
|
1082
|
+
await fs3.writeFile(bootstrapPath, bootstrapContent, "utf-8");
|
|
1083
|
+
const readmeContent = `# Strapi Bootstrap File
|
|
1084
|
+
|
|
1085
|
+
This file automatically enables public read permissions for all CMS content types when Strapi starts.
|
|
1086
|
+
|
|
1087
|
+
## Installation
|
|
1088
|
+
|
|
1089
|
+
1. Copy the \`index.ts\` file to your Strapi project:
|
|
1090
|
+
\`\`\`bash
|
|
1091
|
+
cp strapi-bootstrap/index.ts <your-strapi-project>/src/index.ts
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
|
|
1094
|
+
2. Restart Strapi:
|
|
1095
|
+
\`\`\`bash
|
|
1096
|
+
cd <your-strapi-project>
|
|
1097
|
+
npm run develop
|
|
1098
|
+
\`\`\`
|
|
1099
|
+
|
|
1100
|
+
3. Check the console logs - you should see:
|
|
1101
|
+
\`\`\`
|
|
1102
|
+
[Bootstrap] \u2705 Enabled X public permissions for Y content types
|
|
1103
|
+
\`\`\`
|
|
1104
|
+
|
|
1105
|
+
## What It Does
|
|
1106
|
+
|
|
1107
|
+
- Runs automatically when Strapi starts
|
|
1108
|
+
- Finds the "Public" role
|
|
1109
|
+
- Enables \`find\` and \`findOne\` permissions for all API content types
|
|
1110
|
+
- Allows unauthenticated users to read published content
|
|
1111
|
+
- Fixes 403 Forbidden errors from \`useStrapiContent\`
|
|
1112
|
+
|
|
1113
|
+
## Manual Alternative
|
|
1114
|
+
|
|
1115
|
+
If you prefer to set permissions manually:
|
|
1116
|
+
|
|
1117
|
+
1. Open Strapi admin: http://localhost:1337/admin
|
|
1118
|
+
2. Go to: Settings \u2192 Users & Permissions Plugin \u2192 Roles \u2192 Public
|
|
1119
|
+
3. For each content type, check:
|
|
1120
|
+
- \u2705 find
|
|
1121
|
+
- \u2705 findOne
|
|
1122
|
+
4. Click Save
|
|
1123
|
+
|
|
1124
|
+
## Notes
|
|
1125
|
+
|
|
1126
|
+
- Only enables READ permissions (find, findOne)
|
|
1127
|
+
- Does NOT enable write permissions (create, update, delete)
|
|
1128
|
+
- Only affects the "Public" role (unauthenticated users)
|
|
1129
|
+
- Safe to run multiple times (idempotent)
|
|
1130
|
+
`;
|
|
1131
|
+
const readmePath = path4.join(strapiBootstrapDir, "README.md");
|
|
1132
|
+
await fs3.writeFile(readmePath, readmeContent, "utf-8");
|
|
1133
|
+
console.log(" \u2713 Generated Strapi bootstrap file");
|
|
1134
|
+
}
|
|
1135
|
+
async function createPublishEndpoint(outputDir) {
|
|
1136
|
+
const serverDir = path4.join(outputDir, "server", "api", "cms");
|
|
1137
|
+
await fs3.ensureDir(serverDir);
|
|
1138
|
+
const endpointContent = `/**
|
|
1139
|
+
* API endpoint for batch publishing CMS changes
|
|
1140
|
+
* Publishes all drafts at once
|
|
1141
|
+
*/
|
|
1142
|
+
|
|
1143
|
+
import fs from 'fs';
|
|
1144
|
+
import path from 'path';
|
|
1145
|
+
|
|
1146
|
+
export default defineEventHandler(async (event) => {
|
|
1147
|
+
// Get Strapi URL from runtime config
|
|
1148
|
+
const config = useRuntimeConfig();
|
|
1149
|
+
const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
|
|
1150
|
+
|
|
1151
|
+
// Extract Authorization header
|
|
1152
|
+
const authHeader = getHeader(event, 'authorization');
|
|
1153
|
+
|
|
1154
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
1155
|
+
throw createError({
|
|
1156
|
+
statusCode: 401,
|
|
1157
|
+
statusMessage: 'Unauthorized: Missing or invalid authorization header',
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
1162
|
+
|
|
1163
|
+
// Verify token with Strapi and determine if it's an admin or user token
|
|
1164
|
+
let userResponse: any;
|
|
1165
|
+
let isAdminToken = false;
|
|
1166
|
+
|
|
1167
|
+
try {
|
|
1168
|
+
// Try admin token verification first
|
|
1169
|
+
try {
|
|
1170
|
+
userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
|
|
1171
|
+
headers: {
|
|
1172
|
+
Authorization: \`Bearer \${token}\`,
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
isAdminToken = true;
|
|
1176
|
+
} catch (adminError) {
|
|
1177
|
+
// Fallback to regular user token verification
|
|
1178
|
+
userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
|
|
1179
|
+
headers: {
|
|
1180
|
+
Authorization: \`Bearer \${token}\`,
|
|
1181
|
+
},
|
|
1182
|
+
});
|
|
1183
|
+
isAdminToken = false;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Get the request body
|
|
1187
|
+
const body = await readBody(event);
|
|
1188
|
+
const { pages } = body;
|
|
1189
|
+
|
|
1190
|
+
if (!pages || !Array.isArray(pages)) {
|
|
1191
|
+
throw createError({
|
|
1192
|
+
statusCode: 400,
|
|
1193
|
+
statusMessage: 'Bad Request: Missing or invalid pages array',
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Load manifest to understand field mappings
|
|
1198
|
+
const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
|
|
1199
|
+
let manifest;
|
|
1200
|
+
try {
|
|
1201
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
1202
|
+
manifest = JSON.parse(manifestContent);
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
console.error('Failed to load manifest:', error);
|
|
1205
|
+
throw createError({
|
|
1206
|
+
statusCode: 500,
|
|
1207
|
+
statusMessage: 'Failed to load CMS manifest',
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Process all pages - call Strapi directly
|
|
1212
|
+
const results = await Promise.allSettled(
|
|
1213
|
+
pages.map(async ({ page, fields }) => {
|
|
1214
|
+
try {
|
|
1215
|
+
// Get page configuration from manifest
|
|
1216
|
+
const pageConfig = manifest.pages[page];
|
|
1217
|
+
if (!pageConfig) {
|
|
1218
|
+
throw new Error(\`Page "\${page}" not found in manifest\`);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Transform fields to Strapi format
|
|
1222
|
+
const strapiData: Record<string, any> = {};
|
|
1223
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
1224
|
+
const fieldConfig = pageConfig.fields[fieldName];
|
|
1225
|
+
if (!fieldConfig) {
|
|
1226
|
+
console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Handle different field types
|
|
1231
|
+
if (fieldConfig.type === 'image') {
|
|
1232
|
+
// TODO: Handle image uploads - for now just store the value
|
|
1233
|
+
strapiData[fieldName] = value;
|
|
1234
|
+
} else {
|
|
1235
|
+
strapiData[fieldName] = value;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Update Strapi v5 content - use different endpoints for admin vs user tokens
|
|
1240
|
+
if (isAdminToken) {
|
|
1241
|
+
// Admin tokens use the content-manager API (Strapi v5)
|
|
1242
|
+
const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
|
|
1243
|
+
|
|
1244
|
+
// Step 1: Update the content
|
|
1245
|
+
await $fetch(contentEndpoint, {
|
|
1246
|
+
method: 'PUT',
|
|
1247
|
+
headers: {
|
|
1248
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1249
|
+
'Content-Type': 'application/json',
|
|
1250
|
+
},
|
|
1251
|
+
body: strapiData,
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
// Step 2: Publish the content (Strapi v5)
|
|
1255
|
+
const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
|
|
1256
|
+
await $fetch(publishEndpoint, {
|
|
1257
|
+
method: 'POST',
|
|
1258
|
+
headers: {
|
|
1259
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1260
|
+
'Content-Type': 'application/json',
|
|
1261
|
+
},
|
|
1262
|
+
body: {},
|
|
1263
|
+
});
|
|
1264
|
+
} else {
|
|
1265
|
+
// User tokens use the regular REST API
|
|
1266
|
+
const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
|
|
1267
|
+
|
|
1268
|
+
await $fetch(strapiEndpoint, {
|
|
1269
|
+
method: 'PUT',
|
|
1270
|
+
headers: {
|
|
1271
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1272
|
+
'Content-Type': 'application/json',
|
|
1273
|
+
},
|
|
1274
|
+
body: {
|
|
1275
|
+
data: strapiData,
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// Publish using the publish endpoint (Strapi v5)
|
|
1280
|
+
const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
|
|
1281
|
+
await $fetch(publishEndpoint, {
|
|
1282
|
+
method: 'POST',
|
|
1283
|
+
headers: {
|
|
1284
|
+
'Authorization': \`Bearer \${token}\`,
|
|
1285
|
+
'Content-Type': 'application/json',
|
|
1286
|
+
},
|
|
1287
|
+
body: {},
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
console.log(\`[CMS Publish] Published "\${page}" to Strapi\`);
|
|
1292
|
+
return { page, success: true };
|
|
1293
|
+
} catch (error: any) {
|
|
1294
|
+
console.error(\`[CMS Publish] Failed to publish "\${page}":\`, error);
|
|
1295
|
+
return {
|
|
1296
|
+
page,
|
|
1297
|
+
success: false,
|
|
1298
|
+
error: error.message || 'Unknown error',
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
})
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
// Separate successful and failed publications
|
|
1305
|
+
const successful: string[] = [];
|
|
1306
|
+
const failed: Array<{ page: string; error: string }> = [];
|
|
1307
|
+
|
|
1308
|
+
results.forEach((result, index) => {
|
|
1309
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
1310
|
+
successful.push(result.value.page);
|
|
1311
|
+
} else if (result.status === 'fulfilled' && !result.value.success) {
|
|
1312
|
+
failed.push({
|
|
1313
|
+
page: result.value.page,
|
|
1314
|
+
error: result.value.error || 'Unknown error',
|
|
1315
|
+
});
|
|
1316
|
+
} else if (result.status === 'rejected') {
|
|
1317
|
+
failed.push({
|
|
1318
|
+
page: pages[index].page,
|
|
1319
|
+
error: result.reason?.message || 'Unknown error',
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
console.log(\`[CMS Publish] Published \${successful.length} pages, \${failed.length} failed\`);
|
|
1325
|
+
|
|
1326
|
+
return {
|
|
1327
|
+
success: failed.length === 0,
|
|
1328
|
+
message: \`Published \${successful.length} of \${pages.length} pages\`,
|
|
1329
|
+
successful,
|
|
1330
|
+
failed,
|
|
1331
|
+
user: {
|
|
1332
|
+
id: userResponse.id,
|
|
1333
|
+
username: userResponse.username || userResponse.firstname || 'Unknown',
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
} catch (error: any) {
|
|
1337
|
+
console.error('[CMS Publish] Error:', error);
|
|
1338
|
+
|
|
1339
|
+
// Token verification failed
|
|
1340
|
+
if (error.statusCode === 401 || error.status === 401) {
|
|
1341
|
+
throw createError({
|
|
1342
|
+
statusCode: 401,
|
|
1343
|
+
statusMessage: 'Unauthorized: Invalid or expired token',
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Generic error
|
|
1348
|
+
throw createError({
|
|
1349
|
+
statusCode: 500,
|
|
1350
|
+
statusMessage: 'Internal server error while publishing changes',
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
`;
|
|
1355
|
+
const endpointPath = path4.join(serverDir, "publish.post.ts");
|
|
1356
|
+
await fs3.writeFile(endpointPath, endpointContent, "utf-8");
|
|
1357
|
+
}
|
|
406
1358
|
|
|
407
1359
|
// src/boilerplate.ts
|
|
408
1360
|
import fs4 from "fs-extra";
|
|
@@ -742,8 +1694,119 @@ async function generateManifest(pagesDir) {
|
|
|
742
1694
|
return manifest;
|
|
743
1695
|
}
|
|
744
1696
|
async function writeManifest(outputDir, manifest) {
|
|
1697
|
+
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
745
1698
|
const manifestPath = path7.join(outputDir, "cms-manifest.json");
|
|
746
|
-
await fs6.writeFile(manifestPath,
|
|
1699
|
+
await fs6.writeFile(manifestPath, manifestContent, "utf-8");
|
|
1700
|
+
const publicDir = path7.join(outputDir, "public");
|
|
1701
|
+
await fs6.ensureDir(publicDir);
|
|
1702
|
+
const publicManifestPath = path7.join(publicDir, "cms-manifest.json");
|
|
1703
|
+
await fs6.writeFile(publicManifestPath, manifestContent, "utf-8");
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/vue-transformer.ts
|
|
1707
|
+
import * as cheerio3 from "cheerio";
|
|
1708
|
+
import fs7 from "fs-extra";
|
|
1709
|
+
import path8 from "path";
|
|
1710
|
+
function replaceWithBinding(_$, $el, fieldName, type) {
|
|
1711
|
+
if (type === "image") {
|
|
1712
|
+
const $img = $el.find("img").first();
|
|
1713
|
+
if ($img.length) {
|
|
1714
|
+
$img.attr(":src", `content.${fieldName}`);
|
|
1715
|
+
$img.removeAttr("src");
|
|
1716
|
+
}
|
|
1717
|
+
} else if (type === "rich") {
|
|
1718
|
+
$el.attr("v-html", `content.${fieldName}`);
|
|
1719
|
+
$el.empty();
|
|
1720
|
+
} else {
|
|
1721
|
+
$el.empty();
|
|
1722
|
+
$el.text(`{{ content.${fieldName} }}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function transformCollection($, collectionName, collection) {
|
|
1726
|
+
const $items = $(collection.selector);
|
|
1727
|
+
if ($items.length === 0) return;
|
|
1728
|
+
const $first = $items.first();
|
|
1729
|
+
$first.attr("v-for", `(item, index) in content.${collectionName}`);
|
|
1730
|
+
$first.attr(":key", "index");
|
|
1731
|
+
Object.entries(collection.fields).forEach(([fieldName, selector]) => {
|
|
1732
|
+
const $fieldEl = $first.find(selector);
|
|
1733
|
+
if ($fieldEl.length) {
|
|
1734
|
+
if (fieldName === "image") {
|
|
1735
|
+
const $img = $fieldEl.find("img").first();
|
|
1736
|
+
if ($img.length) {
|
|
1737
|
+
$img.attr(":src", "item.image");
|
|
1738
|
+
$img.removeAttr("src");
|
|
1739
|
+
}
|
|
1740
|
+
} else if (fieldName === "link") {
|
|
1741
|
+
$fieldEl.attr(":to", "item.link");
|
|
1742
|
+
$fieldEl.removeAttr("to");
|
|
1743
|
+
$fieldEl.removeAttr("href");
|
|
1744
|
+
} else {
|
|
1745
|
+
$fieldEl.empty();
|
|
1746
|
+
$fieldEl.text(`{{ item.${fieldName} }}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
$items.slice(1).remove();
|
|
1751
|
+
}
|
|
1752
|
+
async function transformVueToReactive(vueFilePath, pageName, manifest) {
|
|
1753
|
+
const pageManifest = manifest.pages[pageName];
|
|
1754
|
+
if (!pageManifest) return;
|
|
1755
|
+
const vueContent = await fs7.readFile(vueFilePath, "utf-8");
|
|
1756
|
+
if (vueContent.includes("useStrapiContent")) {
|
|
1757
|
+
console.log(` Skipping ${pageName} - already transformed`);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
|
|
1761
|
+
if (!templateMatch) return;
|
|
1762
|
+
const templateContent = templateMatch[1];
|
|
1763
|
+
const $ = cheerio3.load(templateContent, { xmlMode: false });
|
|
1764
|
+
if (pageManifest.collections) {
|
|
1765
|
+
Object.entries(pageManifest.collections).forEach(([collectionName, collection]) => {
|
|
1766
|
+
transformCollection($, collectionName, collection);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
if (pageManifest.fields) {
|
|
1770
|
+
Object.entries(pageManifest.fields).forEach(([fieldName, field]) => {
|
|
1771
|
+
const $elements = $(field.selector);
|
|
1772
|
+
$elements.each((_, el) => {
|
|
1773
|
+
const $el = $(el);
|
|
1774
|
+
replaceWithBinding($, $el, fieldName, field.type);
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
let transformedTemplate = $.html();
|
|
1779
|
+
const bodyMatch = transformedTemplate.match(/<body>([\s\S]*)<\/body>/);
|
|
1780
|
+
if (bodyMatch) {
|
|
1781
|
+
transformedTemplate = bodyMatch[1];
|
|
1782
|
+
}
|
|
1783
|
+
transformedTemplate = transformedTemplate.replace(/<\/?html[^>]*>/gi, "").replace(/<head><\/head>/gi, "").trim();
|
|
1784
|
+
const wrapperDivMatch = transformedTemplate.match(/^<div>\s*([\s\S]*?)\s*<\/div>$/);
|
|
1785
|
+
if (wrapperDivMatch) {
|
|
1786
|
+
transformedTemplate = wrapperDivMatch[1].trim();
|
|
1787
|
+
}
|
|
1788
|
+
const scriptSetup = `<script setup lang="ts">
|
|
1789
|
+
// Auto-generated reactive content from Strapi
|
|
1790
|
+
const { content } = useStrapiContent('${pageName}');
|
|
1791
|
+
</script>`;
|
|
1792
|
+
const finalTemplate = transformedTemplate.split("\n").map((line) => " " + line).join("\n");
|
|
1793
|
+
const newVueContent = `${scriptSetup}
|
|
1794
|
+
|
|
1795
|
+
<template>
|
|
1796
|
+
${finalTemplate}
|
|
1797
|
+
</template>
|
|
1798
|
+
`;
|
|
1799
|
+
await fs7.writeFile(vueFilePath, newVueContent, "utf-8");
|
|
1800
|
+
}
|
|
1801
|
+
async function transformAllVuePages(pagesDir, manifest) {
|
|
1802
|
+
const vueFiles = await fs7.readdir(pagesDir);
|
|
1803
|
+
for (const file of vueFiles) {
|
|
1804
|
+
if (file.endsWith(".vue")) {
|
|
1805
|
+
const pageName = file.replace(".vue", "");
|
|
1806
|
+
const vueFilePath = path8.join(pagesDir, file);
|
|
1807
|
+
await transformVueToReactive(vueFilePath, pageName, manifest);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
747
1810
|
}
|
|
748
1811
|
|
|
749
1812
|
// src/transformer.ts
|
|
@@ -849,13 +1912,13 @@ function manifestToSchemas(manifest) {
|
|
|
849
1912
|
}
|
|
850
1913
|
|
|
851
1914
|
// src/schema-writer.ts
|
|
852
|
-
import
|
|
853
|
-
import
|
|
1915
|
+
import fs8 from "fs-extra";
|
|
1916
|
+
import path9 from "path";
|
|
854
1917
|
async function writeStrapiSchema(outputDir, name, schema) {
|
|
855
|
-
const schemasDir =
|
|
856
|
-
await
|
|
857
|
-
const schemaPath =
|
|
858
|
-
await
|
|
1918
|
+
const schemasDir = path9.join(outputDir, "cms-schemas");
|
|
1919
|
+
await fs8.ensureDir(schemasDir);
|
|
1920
|
+
const schemaPath = path9.join(schemasDir, `${name}.json`);
|
|
1921
|
+
await fs8.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
859
1922
|
}
|
|
860
1923
|
async function writeAllSchemas(outputDir, schemas) {
|
|
861
1924
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
@@ -863,7 +1926,7 @@ async function writeAllSchemas(outputDir, schemas) {
|
|
|
863
1926
|
}
|
|
864
1927
|
}
|
|
865
1928
|
async function createStrapiReadme(outputDir) {
|
|
866
|
-
const readmePath =
|
|
1929
|
+
const readmePath = path9.join(outputDir, "cms-schemas", "README.md");
|
|
867
1930
|
const content = `# CMS Schemas
|
|
868
1931
|
|
|
869
1932
|
Auto-generated Strapi content type schemas from your Webflow export.
|
|
@@ -933,14 +1996,14 @@ const { data } = await $fetch('http://localhost:1337/api/index')
|
|
|
933
1996
|
const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
|
|
934
1997
|
\`\`\`
|
|
935
1998
|
`;
|
|
936
|
-
await
|
|
1999
|
+
await fs8.writeFile(readmePath, content, "utf-8");
|
|
937
2000
|
}
|
|
938
2001
|
|
|
939
2002
|
// src/content-extractor.ts
|
|
940
|
-
import * as
|
|
941
|
-
import
|
|
2003
|
+
import * as cheerio4 from "cheerio";
|
|
2004
|
+
import path10 from "path";
|
|
942
2005
|
function extractContentFromHTML(html, _pageName, pageManifest) {
|
|
943
|
-
const $ =
|
|
2006
|
+
const $ = cheerio4.load(html);
|
|
944
2007
|
const content = {
|
|
945
2008
|
fields: {},
|
|
946
2009
|
collections: {}
|
|
@@ -1009,7 +2072,7 @@ function extractAllContent(htmlFiles, manifest) {
|
|
|
1009
2072
|
function normalizeImagePath(imageSrc) {
|
|
1010
2073
|
if (!imageSrc) return "";
|
|
1011
2074
|
if (imageSrc.startsWith("/")) return imageSrc;
|
|
1012
|
-
const filename =
|
|
2075
|
+
const filename = path10.basename(imageSrc);
|
|
1013
2076
|
if (imageSrc.includes("images/")) {
|
|
1014
2077
|
return `/images/${filename}`;
|
|
1015
2078
|
}
|
|
@@ -1048,16 +2111,16 @@ function formatForStrapi(extracted) {
|
|
|
1048
2111
|
}
|
|
1049
2112
|
|
|
1050
2113
|
// src/seed-writer.ts
|
|
1051
|
-
import
|
|
1052
|
-
import
|
|
2114
|
+
import fs9 from "fs-extra";
|
|
2115
|
+
import path11 from "path";
|
|
1053
2116
|
async function writeSeedData(outputDir, seedData) {
|
|
1054
|
-
const seedDir =
|
|
1055
|
-
await
|
|
1056
|
-
const seedPath =
|
|
1057
|
-
await
|
|
2117
|
+
const seedDir = path11.join(outputDir, "cms-seed");
|
|
2118
|
+
await fs9.ensureDir(seedDir);
|
|
2119
|
+
const seedPath = path11.join(seedDir, "seed-data.json");
|
|
2120
|
+
await fs9.writeJson(seedPath, seedData, { spaces: 2 });
|
|
1058
2121
|
}
|
|
1059
2122
|
async function createSeedReadme(outputDir) {
|
|
1060
|
-
const readmePath =
|
|
2123
|
+
const readmePath = path11.join(outputDir, "cms-seed", "README.md");
|
|
1061
2124
|
const content = `# CMS Seed Data
|
|
1062
2125
|
|
|
1063
2126
|
Auto-extracted content from your Webflow export, ready to seed into Strapi.
|
|
@@ -1113,7 +2176,7 @@ When seeding Strapi, these images will be uploaded to Strapi's media library.
|
|
|
1113
2176
|
2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
|
|
1114
2177
|
3. Use this seed data to populate your CMS
|
|
1115
2178
|
`;
|
|
1116
|
-
await
|
|
2179
|
+
await fs9.writeFile(readmePath, content, "utf-8");
|
|
1117
2180
|
}
|
|
1118
2181
|
|
|
1119
2182
|
// src/converter.ts
|
|
@@ -1124,7 +2187,7 @@ async function convertWebflowExport(options) {
|
|
|
1124
2187
|
console.log(pc3.dim(`Output: ${outputDir}`));
|
|
1125
2188
|
try {
|
|
1126
2189
|
await setupBoilerplate(boilerplate, outputDir);
|
|
1127
|
-
const inputExists = await
|
|
2190
|
+
const inputExists = await fs10.pathExists(inputDir);
|
|
1128
2191
|
if (!inputExists) {
|
|
1129
2192
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
1130
2193
|
}
|
|
@@ -1166,7 +2229,7 @@ ${parsed.embeddedStyles}
|
|
|
1166
2229
|
}
|
|
1167
2230
|
await formatVueFiles(outputDir);
|
|
1168
2231
|
console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
|
|
1169
|
-
const pagesDir =
|
|
2232
|
+
const pagesDir = path12.join(outputDir, "pages");
|
|
1170
2233
|
const manifest = await generateManifest(pagesDir);
|
|
1171
2234
|
await writeManifest(outputDir, manifest);
|
|
1172
2235
|
const totalFields = Object.values(manifest.pages).reduce(
|
|
@@ -1180,6 +2243,9 @@ ${parsed.embeddedStyles}
|
|
|
1180
2243
|
console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
|
|
1181
2244
|
console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
|
|
1182
2245
|
console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
|
|
2246
|
+
console.log(pc3.blue("\n\u26A1 Transforming Vue files to reactive templates..."));
|
|
2247
|
+
await transformAllVuePages(pagesDir, manifest);
|
|
2248
|
+
console.log(pc3.green(` \u2713 Transformed ${Object.keys(manifest.pages).length} pages to use Vue template syntax`));
|
|
1183
2249
|
console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
|
|
1184
2250
|
console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
|
|
1185
2251
|
console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
|
|
@@ -1219,19 +2285,31 @@ ${parsed.embeddedStyles}
|
|
|
1219
2285
|
}
|
|
1220
2286
|
console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
|
|
1221
2287
|
await createEditorPlugin(outputDir);
|
|
2288
|
+
await createEditorContentComposable(outputDir);
|
|
2289
|
+
await createStrapiContentComposable(outputDir);
|
|
1222
2290
|
await addEditorDependency(outputDir);
|
|
1223
2291
|
await createSaveEndpoint(outputDir);
|
|
2292
|
+
await createPublishEndpoint(outputDir);
|
|
2293
|
+
await createStrapiBootstrap(outputDir);
|
|
2294
|
+
await addStrapiUrlToConfig(outputDir);
|
|
1224
2295
|
console.log(pc3.green(" \u2713 Editor plugin created"));
|
|
2296
|
+
console.log(pc3.green(" \u2713 Editor content composable created"));
|
|
2297
|
+
console.log(pc3.green(" \u2713 Strapi content composable created"));
|
|
1225
2298
|
console.log(pc3.green(" \u2713 Editor dependency added"));
|
|
1226
2299
|
console.log(pc3.green(" \u2713 Save endpoint created"));
|
|
2300
|
+
console.log(pc3.green(" \u2713 Publish endpoint created"));
|
|
2301
|
+
console.log(pc3.green(" \u2713 Strapi bootstrap file generated"));
|
|
2302
|
+
console.log(pc3.green(" \u2713 Strapi config added"));
|
|
1227
2303
|
console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
|
|
1228
2304
|
console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
|
|
1229
2305
|
console.log(pc3.dim(` 1. cd ${outputDir}`));
|
|
1230
2306
|
console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
|
|
1231
2307
|
console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
|
|
1232
|
-
console.log(pc3.dim(" 4.
|
|
1233
|
-
console.log(pc3.dim("
|
|
1234
|
-
console.log(pc3.dim("
|
|
2308
|
+
console.log(pc3.dim(" 4. Copy strapi-bootstrap/index.ts to your Strapi project at src/index.ts"));
|
|
2309
|
+
console.log(pc3.dim(" (This auto-enables public read permissions on Strapi startup)"));
|
|
2310
|
+
console.log(pc3.dim(" 5. Seed Strapi with data from cms-seed/"));
|
|
2311
|
+
console.log(pc3.dim(" 6. pnpm install && pnpm dev"));
|
|
2312
|
+
console.log(pc3.dim(" 7. Visit http://localhost:3000?preview=true to edit inline!"));
|
|
1235
2313
|
} catch (error) {
|
|
1236
2314
|
console.error(pc3.red("\n\u274C Conversion failed:"));
|
|
1237
2315
|
console.error(pc3.red(error instanceof Error ? error.message : String(error)));
|
|
@@ -1240,17 +2318,59 @@ ${parsed.embeddedStyles}
|
|
|
1240
2318
|
}
|
|
1241
2319
|
|
|
1242
2320
|
// src/strapi-setup.ts
|
|
1243
|
-
import
|
|
1244
|
-
import
|
|
2321
|
+
import fs11 from "fs-extra";
|
|
2322
|
+
import path13 from "path";
|
|
1245
2323
|
import { glob as glob2 } from "glob";
|
|
1246
2324
|
import * as readline from "readline";
|
|
2325
|
+
var ENV_FILE = ".env";
|
|
2326
|
+
async function loadConfig(projectDir) {
|
|
2327
|
+
const envPath = path13.join(projectDir, ENV_FILE);
|
|
2328
|
+
if (await fs11.pathExists(envPath)) {
|
|
2329
|
+
try {
|
|
2330
|
+
const content = await fs11.readFile(envPath, "utf-8");
|
|
2331
|
+
const config = {};
|
|
2332
|
+
for (const line of content.split("\n")) {
|
|
2333
|
+
const trimmed = line.trim();
|
|
2334
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2335
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
2336
|
+
const value = valueParts.join("=").trim();
|
|
2337
|
+
if (key === "STRAPI_API_TOKEN") {
|
|
2338
|
+
config.apiToken = value;
|
|
2339
|
+
} else if (key === "STRAPI_URL") {
|
|
2340
|
+
config.strapiUrl = value;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return config;
|
|
2344
|
+
} catch {
|
|
2345
|
+
return {};
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return {};
|
|
2349
|
+
}
|
|
2350
|
+
async function saveConfig(projectDir, config) {
|
|
2351
|
+
const envPath = path13.join(projectDir, ENV_FILE);
|
|
2352
|
+
let content = "";
|
|
2353
|
+
if (await fs11.pathExists(envPath)) {
|
|
2354
|
+
content = await fs11.readFile(envPath, "utf-8");
|
|
2355
|
+
content = content.split("\n").filter((line) => !line.startsWith("STRAPI_API_TOKEN=") && !line.startsWith("STRAPI_URL=")).join("\n");
|
|
2356
|
+
if (content && !content.endsWith("\n")) {
|
|
2357
|
+
content += "\n";
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (config.strapiUrl) {
|
|
2361
|
+
content += `STRAPI_URL=${config.strapiUrl}
|
|
2362
|
+
`;
|
|
2363
|
+
}
|
|
2364
|
+
if (config.apiToken) {
|
|
2365
|
+
content += `STRAPI_API_TOKEN=${config.apiToken}
|
|
2366
|
+
`;
|
|
2367
|
+
}
|
|
2368
|
+
await fs11.writeFile(envPath, content);
|
|
2369
|
+
}
|
|
1247
2370
|
async function completeSetup(options) {
|
|
1248
|
-
const {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
strapiUrl = "http://localhost:1337",
|
|
1252
|
-
apiToken
|
|
1253
|
-
} = options;
|
|
2371
|
+
const { projectDir, strapiDir, strapiUrl: optionUrl, apiToken: optionToken, ignoreSavedToken } = options;
|
|
2372
|
+
const savedConfig = await loadConfig(projectDir);
|
|
2373
|
+
const strapiUrl = optionUrl || savedConfig.strapiUrl || "http://localhost:1337";
|
|
1254
2374
|
console.log("\u{1F680} Starting complete Strapi setup...\n");
|
|
1255
2375
|
console.log("\u{1F4E6} Step 1: Installing schemas...");
|
|
1256
2376
|
await installSchemas(projectDir, strapiDir);
|
|
@@ -1267,16 +2387,23 @@ async function completeSetup(options) {
|
|
|
1267
2387
|
process.exit(1);
|
|
1268
2388
|
}
|
|
1269
2389
|
console.log("\u2713 Connected to Strapi\n");
|
|
1270
|
-
let token = apiToken;
|
|
1271
|
-
if (!
|
|
2390
|
+
let token = optionToken || (!ignoreSavedToken ? savedConfig.apiToken : void 0);
|
|
2391
|
+
if (token && !ignoreSavedToken) {
|
|
2392
|
+
console.log("\u{1F511} Step 4: Using saved API token");
|
|
2393
|
+
} else if (token && optionToken) {
|
|
2394
|
+
console.log("\u{1F511} Step 4: Using provided API token");
|
|
2395
|
+
} else {
|
|
1272
2396
|
console.log("\u{1F511} Step 4: API Token needed");
|
|
1273
2397
|
console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
|
|
1274
2398
|
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
|
-
);
|
|
2399
|
+
console.log(' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"');
|
|
1278
2400
|
console.log(" 4. Copy the token and paste it here:\n");
|
|
1279
2401
|
token = await promptForToken();
|
|
2402
|
+
const saveToken = await promptYesNo(" Save token for future use?");
|
|
2403
|
+
if (saveToken) {
|
|
2404
|
+
await saveConfig(projectDir, { ...savedConfig, apiToken: token, strapiUrl });
|
|
2405
|
+
console.log(" \u2713 Token saved to .env");
|
|
2406
|
+
}
|
|
1280
2407
|
console.log("");
|
|
1281
2408
|
}
|
|
1282
2409
|
console.log("\u{1F4F8} Step 5: Uploading images...");
|
|
@@ -1293,19 +2420,19 @@ async function completeSetup(options) {
|
|
|
1293
2420
|
console.log(" 3. Connect your Nuxt app to Strapi API");
|
|
1294
2421
|
}
|
|
1295
2422
|
async function installSchemas(projectDir, strapiDir) {
|
|
1296
|
-
if (!await
|
|
2423
|
+
if (!await fs11.pathExists(strapiDir)) {
|
|
1297
2424
|
console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
|
|
1298
|
-
console.error(` Resolved to: ${
|
|
2425
|
+
console.error(` Resolved to: ${path13.resolve(strapiDir)}`);
|
|
1299
2426
|
throw new Error(`Strapi directory not found: ${strapiDir}`);
|
|
1300
2427
|
}
|
|
1301
|
-
const packageJsonPath =
|
|
1302
|
-
if (await
|
|
1303
|
-
const pkg = await
|
|
2428
|
+
const packageJsonPath = path13.join(strapiDir, "package.json");
|
|
2429
|
+
if (await fs11.pathExists(packageJsonPath)) {
|
|
2430
|
+
const pkg = await fs11.readJson(packageJsonPath);
|
|
1304
2431
|
if (!pkg.dependencies?.["@strapi/strapi"]) {
|
|
1305
2432
|
console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
|
|
1306
2433
|
}
|
|
1307
2434
|
}
|
|
1308
|
-
const schemaDir =
|
|
2435
|
+
const schemaDir = path13.join(projectDir, "cms-schemas");
|
|
1309
2436
|
const schemaFiles = await glob2("*.json", {
|
|
1310
2437
|
cwd: schemaDir,
|
|
1311
2438
|
absolute: false
|
|
@@ -1316,42 +2443,42 @@ async function installSchemas(projectDir, strapiDir) {
|
|
|
1316
2443
|
}
|
|
1317
2444
|
console.log(` Found ${schemaFiles.length} schema file(s)`);
|
|
1318
2445
|
for (const file of schemaFiles) {
|
|
1319
|
-
const schemaPath =
|
|
1320
|
-
const schema = await
|
|
1321
|
-
const singularName = schema.info?.singularName ||
|
|
2446
|
+
const schemaPath = path13.join(schemaDir, file);
|
|
2447
|
+
const schema = await fs11.readJson(schemaPath);
|
|
2448
|
+
const singularName = schema.info?.singularName || path13.basename(file, ".json");
|
|
1322
2449
|
console.log(` Installing ${singularName}...`);
|
|
1323
2450
|
try {
|
|
1324
|
-
const apiPath =
|
|
1325
|
-
const contentTypesPath =
|
|
2451
|
+
const apiPath = path13.join(strapiDir, "src", "api", singularName);
|
|
2452
|
+
const contentTypesPath = path13.join(
|
|
1326
2453
|
apiPath,
|
|
1327
2454
|
"content-types",
|
|
1328
2455
|
singularName
|
|
1329
2456
|
);
|
|
1330
|
-
const targetPath =
|
|
1331
|
-
await
|
|
1332
|
-
await
|
|
1333
|
-
await
|
|
1334
|
-
await
|
|
1335
|
-
await
|
|
2457
|
+
const targetPath = path13.join(contentTypesPath, "schema.json");
|
|
2458
|
+
await fs11.ensureDir(contentTypesPath);
|
|
2459
|
+
await fs11.ensureDir(path13.join(apiPath, "routes"));
|
|
2460
|
+
await fs11.ensureDir(path13.join(apiPath, "controllers"));
|
|
2461
|
+
await fs11.ensureDir(path13.join(apiPath, "services"));
|
|
2462
|
+
await fs11.writeJson(targetPath, schema, { spaces: 2 });
|
|
1336
2463
|
const routeContent = `import { factories } from '@strapi/strapi';
|
|
1337
2464
|
export default factories.createCoreRouter('api::${singularName}.${singularName}');
|
|
1338
2465
|
`;
|
|
1339
|
-
await
|
|
1340
|
-
|
|
2466
|
+
await fs11.writeFile(
|
|
2467
|
+
path13.join(apiPath, "routes", `${singularName}.ts`),
|
|
1341
2468
|
routeContent
|
|
1342
2469
|
);
|
|
1343
2470
|
const controllerContent = `import { factories } from '@strapi/strapi';
|
|
1344
2471
|
export default factories.createCoreController('api::${singularName}.${singularName}');
|
|
1345
2472
|
`;
|
|
1346
|
-
await
|
|
1347
|
-
|
|
2473
|
+
await fs11.writeFile(
|
|
2474
|
+
path13.join(apiPath, "controllers", `${singularName}.ts`),
|
|
1348
2475
|
controllerContent
|
|
1349
2476
|
);
|
|
1350
2477
|
const serviceContent = `import { factories } from '@strapi/strapi';
|
|
1351
2478
|
export default factories.createCoreService('api::${singularName}.${singularName}');
|
|
1352
2479
|
`;
|
|
1353
|
-
await
|
|
1354
|
-
|
|
2480
|
+
await fs11.writeFile(
|
|
2481
|
+
path13.join(apiPath, "services", `${singularName}.ts`),
|
|
1355
2482
|
serviceContent
|
|
1356
2483
|
);
|
|
1357
2484
|
} catch (error) {
|
|
@@ -1391,10 +2518,51 @@ async function promptForToken() {
|
|
|
1391
2518
|
});
|
|
1392
2519
|
});
|
|
1393
2520
|
}
|
|
2521
|
+
async function promptYesNo(question) {
|
|
2522
|
+
const rl = createReadline();
|
|
2523
|
+
return new Promise((resolve) => {
|
|
2524
|
+
rl.question(`${question} (y/n): `, (answer) => {
|
|
2525
|
+
rl.close();
|
|
2526
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
2527
|
+
});
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
async function getExistingMedia(strapiUrl, apiToken) {
|
|
2531
|
+
const existingMedia = /* @__PURE__ */ new Map();
|
|
2532
|
+
try {
|
|
2533
|
+
let page = 1;
|
|
2534
|
+
const pageSize = 100;
|
|
2535
|
+
let hasMore = true;
|
|
2536
|
+
while (hasMore) {
|
|
2537
|
+
const response = await fetch(
|
|
2538
|
+
`${strapiUrl}/api/upload/files?pagination[page]=${page}&pagination[pageSize]=${pageSize}`,
|
|
2539
|
+
{
|
|
2540
|
+
headers: {
|
|
2541
|
+
Authorization: `Bearer ${apiToken}`
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
);
|
|
2545
|
+
if (!response.ok) {
|
|
2546
|
+
break;
|
|
2547
|
+
}
|
|
2548
|
+
const data = await response.json();
|
|
2549
|
+
const files = Array.isArray(data) ? data : data.results || [];
|
|
2550
|
+
for (const file of files) {
|
|
2551
|
+
if (file.name) {
|
|
2552
|
+
existingMedia.set(file.name, file.id);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
hasMore = files.length === pageSize;
|
|
2556
|
+
page++;
|
|
2557
|
+
}
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
}
|
|
2560
|
+
return existingMedia;
|
|
2561
|
+
}
|
|
1394
2562
|
async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
1395
2563
|
const mediaMap = /* @__PURE__ */ new Map();
|
|
1396
|
-
const imagesDir =
|
|
1397
|
-
if (!await
|
|
2564
|
+
const imagesDir = path13.join(projectDir, "public", "assets", "images");
|
|
2565
|
+
if (!await fs11.pathExists(imagesDir)) {
|
|
1398
2566
|
console.log(" No images directory found");
|
|
1399
2567
|
return mediaMap;
|
|
1400
2568
|
}
|
|
@@ -1402,26 +2570,35 @@ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
|
|
|
1402
2570
|
cwd: imagesDir,
|
|
1403
2571
|
absolute: false
|
|
1404
2572
|
});
|
|
1405
|
-
console.log(`
|
|
2573
|
+
console.log(` Checking for existing media...`);
|
|
2574
|
+
const existingMedia = await getExistingMedia(strapiUrl, apiToken);
|
|
2575
|
+
let uploadedCount = 0;
|
|
2576
|
+
let skippedCount = 0;
|
|
2577
|
+
console.log(` Processing ${imageFiles.length} images...`);
|
|
1406
2578
|
for (const imageFile of imageFiles) {
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
imageFile
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
2579
|
+
const fileName = path13.basename(imageFile);
|
|
2580
|
+
const existingId = existingMedia.get(fileName);
|
|
2581
|
+
if (existingId) {
|
|
2582
|
+
mediaMap.set(`/images/${imageFile}`, existingId);
|
|
2583
|
+
mediaMap.set(imageFile, existingId);
|
|
2584
|
+
skippedCount++;
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
const imagePath = path13.join(imagesDir, imageFile);
|
|
2588
|
+
const mediaId = await uploadImage(imagePath, imageFile, strapiUrl, apiToken);
|
|
1414
2589
|
if (mediaId) {
|
|
1415
2590
|
mediaMap.set(`/images/${imageFile}`, mediaId);
|
|
1416
2591
|
mediaMap.set(imageFile, mediaId);
|
|
2592
|
+
uploadedCount++;
|
|
1417
2593
|
console.log(` \u2713 ${imageFile}`);
|
|
1418
2594
|
}
|
|
1419
2595
|
}
|
|
2596
|
+
console.log(` Uploaded: ${uploadedCount}, Skipped (existing): ${skippedCount}`);
|
|
1420
2597
|
return mediaMap;
|
|
1421
2598
|
}
|
|
1422
2599
|
async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
|
|
1423
2600
|
try {
|
|
1424
|
-
const fileBuffer = await
|
|
2601
|
+
const fileBuffer = await fs11.readFile(filePath);
|
|
1425
2602
|
const mimeType = getMimeType(fileName);
|
|
1426
2603
|
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
1427
2604
|
const formData = new globalThis.FormData();
|
|
@@ -1448,7 +2625,7 @@ async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
|
|
|
1448
2625
|
}
|
|
1449
2626
|
}
|
|
1450
2627
|
function getMimeType(fileName) {
|
|
1451
|
-
const ext =
|
|
2628
|
+
const ext = path13.extname(fileName).toLowerCase();
|
|
1452
2629
|
const mimeTypes = {
|
|
1453
2630
|
".jpg": "image/jpeg",
|
|
1454
2631
|
".jpeg": "image/jpeg",
|
|
@@ -1460,18 +2637,18 @@ function getMimeType(fileName) {
|
|
|
1460
2637
|
return mimeTypes[ext] || "application/octet-stream";
|
|
1461
2638
|
}
|
|
1462
2639
|
async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
|
|
1463
|
-
const seedPath =
|
|
1464
|
-
if (!await
|
|
2640
|
+
const seedPath = path13.join(projectDir, "cms-seed", "seed-data.json");
|
|
2641
|
+
if (!await fs11.pathExists(seedPath)) {
|
|
1465
2642
|
console.log(" No seed data found");
|
|
1466
2643
|
return;
|
|
1467
2644
|
}
|
|
1468
|
-
const seedData = await
|
|
1469
|
-
const schemasDir =
|
|
2645
|
+
const seedData = await fs11.readJson(seedPath);
|
|
2646
|
+
const schemasDir = path13.join(projectDir, "cms-schemas");
|
|
1470
2647
|
const schemas = /* @__PURE__ */ new Map();
|
|
1471
2648
|
const schemaFiles = await glob2("*.json", { cwd: schemasDir });
|
|
1472
2649
|
for (const file of schemaFiles) {
|
|
1473
|
-
const schema = await
|
|
1474
|
-
const name =
|
|
2650
|
+
const schema = await fs11.readJson(path13.join(schemasDir, file));
|
|
2651
|
+
const name = path13.basename(file, ".json");
|
|
1475
2652
|
schemas.set(name, schema);
|
|
1476
2653
|
}
|
|
1477
2654
|
let successCount = 0;
|
|
@@ -1627,7 +2804,14 @@ async function confirm(question) {
|
|
|
1627
2804
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
1628
2805
|
}
|
|
1629
2806
|
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(
|
|
2807
|
+
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(
|
|
2808
|
+
"-b, --boilerplate <source>",
|
|
2809
|
+
"Boilerplate source (GitHub URL or local path)"
|
|
2810
|
+
).option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option(
|
|
2811
|
+
"--cms <type>",
|
|
2812
|
+
"CMS backend type (strapi|contentful|sanity)",
|
|
2813
|
+
"strapi"
|
|
2814
|
+
).option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
|
|
1631
2815
|
try {
|
|
1632
2816
|
await convertWebflowExport({
|
|
1633
2817
|
inputDir: input,
|
|
@@ -1644,7 +2828,9 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
|
|
|
1644
2828
|
);
|
|
1645
2829
|
if (shouldSetup) {
|
|
1646
2830
|
const strapiDir = await prompt(
|
|
1647
|
-
pc4.cyan(
|
|
2831
|
+
pc4.cyan(
|
|
2832
|
+
"\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): "
|
|
2833
|
+
)
|
|
1648
2834
|
);
|
|
1649
2835
|
if (strapiDir) {
|
|
1650
2836
|
console.log("");
|
|
@@ -1658,13 +2844,17 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
|
|
|
1658
2844
|
} catch (error) {
|
|
1659
2845
|
console.error(pc4.red("\n\u274C Strapi setup failed"));
|
|
1660
2846
|
console.error(pc4.dim("You can run setup manually later with:"));
|
|
1661
|
-
console.error(
|
|
2847
|
+
console.error(
|
|
2848
|
+
pc4.dim(` cms setup-strapi ${output} ${strapiDir}`)
|
|
2849
|
+
);
|
|
1662
2850
|
}
|
|
1663
2851
|
}
|
|
1664
2852
|
} else {
|
|
1665
2853
|
console.log("");
|
|
1666
2854
|
console.log(pc4.dim("\u{1F4A1} You can setup Strapi later with:"));
|
|
1667
|
-
console.log(
|
|
2855
|
+
console.log(
|
|
2856
|
+
pc4.dim(` cms setup-strapi ${output} <strapi-directory>`)
|
|
2857
|
+
);
|
|
1668
2858
|
}
|
|
1669
2859
|
}
|
|
1670
2860
|
} catch (error) {
|
|
@@ -1672,13 +2862,14 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
|
|
|
1672
2862
|
process.exit(1);
|
|
1673
2863
|
}
|
|
1674
2864
|
});
|
|
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) => {
|
|
2865
|
+
program.command("setup-strapi").description("Setup Strapi with schemas and seed data").argument("<project-dir>", "Path to converted project directory").argument("<strapi-dir>", "Path to Strapi directory").option("--url <url>", "Strapi URL", "http://localhost:1337").option("--token <token>", "Strapi API token (optional)").option("--new-token", "Ignore saved token and prompt for a new one").action(async (projectDir, strapiDir, options) => {
|
|
1676
2866
|
try {
|
|
1677
2867
|
await completeSetup({
|
|
1678
2868
|
projectDir,
|
|
1679
2869
|
strapiDir,
|
|
1680
2870
|
strapiUrl: options.url,
|
|
1681
|
-
apiToken: options.token
|
|
2871
|
+
apiToken: options.token,
|
|
2872
|
+
ignoreSavedToken: options.newToken
|
|
1682
2873
|
});
|
|
1683
2874
|
} catch (error) {
|
|
1684
2875
|
console.error(pc4.red("Strapi setup failed"));
|
|
@@ -1686,10 +2877,43 @@ program.command("setup-strapi").description("Setup Strapi with schemas and seed
|
|
|
1686
2877
|
process.exit(1);
|
|
1687
2878
|
}
|
|
1688
2879
|
});
|
|
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 (
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2880
|
+
program.command("generate").description("Generate CMS schemas from manifest").argument("<manifest>", "Path to cms-manifest.json").option("-t, --type <cms>", "CMS type (strapi|contentful|sanity)", "strapi").option("-o, --output <dir>", "Output directory for schemas").action(async (manifestPath, options) => {
|
|
2881
|
+
try {
|
|
2882
|
+
console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
|
|
2883
|
+
console.log(pc4.dim(`Reading manifest from: ${manifestPath}`));
|
|
2884
|
+
const manifestExists = await fs12.pathExists(manifestPath);
|
|
2885
|
+
if (!manifestExists) {
|
|
2886
|
+
throw new Error(`Manifest file not found: ${manifestPath}`);
|
|
2887
|
+
}
|
|
2888
|
+
const manifestContent = await fs12.readFile(manifestPath, "utf-8");
|
|
2889
|
+
const manifest = JSON.parse(manifestContent);
|
|
2890
|
+
console.log(pc4.green(` \u2713 Manifest loaded successfully`));
|
|
2891
|
+
const outputDir = options.output || path14.dirname(manifestPath);
|
|
2892
|
+
if (options.type !== "strapi") {
|
|
2893
|
+
console.log(
|
|
2894
|
+
pc4.yellow(
|
|
2895
|
+
`\u26A0\uFE0F Only Strapi is currently supported. Using Strapi schema format.`
|
|
2896
|
+
)
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
console.log(pc4.blue("\n\u{1F4CB} Generating Strapi schemas..."));
|
|
2900
|
+
const schemas = manifestToSchemas(manifest);
|
|
2901
|
+
await writeAllSchemas(outputDir, schemas);
|
|
2902
|
+
await createStrapiReadme(outputDir);
|
|
2903
|
+
console.log(
|
|
2904
|
+
pc4.green(
|
|
2905
|
+
` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`
|
|
2906
|
+
)
|
|
2907
|
+
);
|
|
2908
|
+
console.log(pc4.dim(` \u2713 Schemas written to: ${path14.join(outputDir, "cms-schemas")}/`));
|
|
2909
|
+
console.log(pc4.green("\n\u2705 Schema generation completed successfully!"));
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
console.error(pc4.red("\n\u274C Schema generation failed:"));
|
|
2912
|
+
console.error(
|
|
2913
|
+
pc4.red(error instanceof Error ? error.message : String(error))
|
|
2914
|
+
);
|
|
2915
|
+
process.exit(1);
|
|
2916
|
+
}
|
|
1693
2917
|
});
|
|
1694
2918
|
program.parse();
|
|
1695
2919
|
//# sourceMappingURL=cli.mjs.map
|