@see-ms/converter 1.0.0

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 ADDED
@@ -0,0 +1,2919 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import pc4 from "picocolors";
6
+ import * as readline2 from "readline";
7
+ import fs12 from "fs-extra";
8
+ import path14 from "path";
9
+
10
+ // src/converter.ts
11
+ import pc3 from "picocolors";
12
+ import path12 from "path";
13
+ import fs10 from "fs-extra";
14
+
15
+ // src/filesystem.ts
16
+ import fs from "fs-extra";
17
+ import path from "path";
18
+ import { glob } from "glob";
19
+ import { execSync } from "child_process";
20
+ import pc from "picocolors";
21
+ async function scanAssets(webflowDir) {
22
+ const assets = {
23
+ css: [],
24
+ images: [],
25
+ fonts: [],
26
+ js: []
27
+ };
28
+ const cssFiles = await glob("css/**/*.css", { cwd: webflowDir });
29
+ assets.css = cssFiles;
30
+ const imageFiles = await glob("images/**/*", { cwd: webflowDir });
31
+ assets.images = imageFiles;
32
+ const fontFiles = await glob("fonts/**/*", { cwd: webflowDir });
33
+ assets.fonts = fontFiles;
34
+ const jsFiles = await glob("js/**/*.js", { cwd: webflowDir });
35
+ assets.js = jsFiles;
36
+ return assets;
37
+ }
38
+ async function copyCSSFiles(webflowDir, outputDir, cssFiles) {
39
+ const targetDir = path.join(outputDir, "assets", "css");
40
+ await fs.ensureDir(targetDir);
41
+ for (const file of cssFiles) {
42
+ const source = path.join(webflowDir, file);
43
+ const target = path.join(targetDir, path.basename(file));
44
+ await fs.copy(source, target);
45
+ }
46
+ }
47
+ async function copyImages(webflowDir, outputDir, imageFiles) {
48
+ const targetDir = path.join(outputDir, "public", "assets", "images");
49
+ await fs.ensureDir(targetDir);
50
+ for (const file of imageFiles) {
51
+ const source = path.join(webflowDir, file);
52
+ const target = path.join(targetDir, path.basename(file));
53
+ await fs.copy(source, target);
54
+ }
55
+ }
56
+ async function copyFonts(webflowDir, outputDir, fontFiles) {
57
+ const targetDir = path.join(outputDir, "public", "assets", "fonts");
58
+ await fs.ensureDir(targetDir);
59
+ for (const file of fontFiles) {
60
+ const source = path.join(webflowDir, file);
61
+ const target = path.join(targetDir, path.basename(file));
62
+ await fs.copy(source, target);
63
+ }
64
+ }
65
+ async function copyJSFiles(webflowDir, outputDir, jsFiles) {
66
+ const targetDir = path.join(outputDir, "public", "assets", "js");
67
+ await fs.ensureDir(targetDir);
68
+ for (const file of jsFiles) {
69
+ const source = path.join(webflowDir, file);
70
+ const target = path.join(targetDir, path.basename(file));
71
+ await fs.copy(source, target);
72
+ }
73
+ }
74
+ async function copyAllAssets(webflowDir, outputDir, assets) {
75
+ await copyCSSFiles(webflowDir, outputDir, assets.css);
76
+ await copyImages(webflowDir, outputDir, assets.images);
77
+ await copyFonts(webflowDir, outputDir, assets.fonts);
78
+ await copyJSFiles(webflowDir, outputDir, assets.js);
79
+ }
80
+ async function findHTMLFiles(webflowDir) {
81
+ const htmlFiles = await glob("**/*.html", { cwd: webflowDir });
82
+ return htmlFiles;
83
+ }
84
+ async function readHTMLFile(webflowDir, fileName) {
85
+ const filePath = path.join(webflowDir, fileName);
86
+ return await fs.readFile(filePath, "utf-8");
87
+ }
88
+ async function writeVueComponent(outputDir, fileName, content) {
89
+ const pagesDir = path.join(outputDir, "pages");
90
+ const vueName = fileName.replace(".html", ".vue");
91
+ const targetPath = path.join(pagesDir, vueName);
92
+ await fs.ensureDir(path.dirname(targetPath));
93
+ await fs.writeFile(targetPath, content, "utf-8");
94
+ }
95
+ async function formatVueFiles(outputDir) {
96
+ const pagesDir = path.join(outputDir, "pages");
97
+ try {
98
+ console.log(pc.blue("\n\u2728 Formatting Vue files with Prettier..."));
99
+ execSync("npx prettier --version", { stdio: "ignore" });
100
+ execSync(`npx prettier --write "${pagesDir}/**/*.vue"`, {
101
+ cwd: outputDir,
102
+ stdio: "inherit"
103
+ });
104
+ console.log(pc.green(" \u2713 Vue files formatted"));
105
+ } catch (error) {
106
+ console.log(pc.yellow(" \u26A0 Prettier not available, skipping formatting"));
107
+ }
108
+ }
109
+
110
+ // src/parser.ts
111
+ import * as cheerio from "cheerio";
112
+ import path2 from "path";
113
+ function normalizeRoute(href) {
114
+ let route = href.replace(".html", "");
115
+ if (route === "index" || route === "/index" || route.endsWith("/index")) {
116
+ return "/";
117
+ }
118
+ if (route === ".." || route === "../" || route === "/.." || route === "../index") {
119
+ return "/";
120
+ }
121
+ route = route.replace(/\.\.\//g, "").replace(/\.\//g, "");
122
+ const normalized = path2.posix.normalize(route);
123
+ if (!normalized.startsWith("/")) {
124
+ return "/" + normalized;
125
+ }
126
+ if (normalized === "." || normalized === "") {
127
+ return "/";
128
+ }
129
+ return normalized;
130
+ }
131
+ function normalizeAssetPath(src) {
132
+ if (!src || src.startsWith("http") || src.startsWith("https")) {
133
+ return src;
134
+ }
135
+ let normalized = src.replace(/^(\.\.\/)+/, "").replace(/^\.\//, "");
136
+ if (normalized.startsWith("/assets/")) {
137
+ normalized = normalized.replace(/\/\.\.\//g, "/");
138
+ return normalized;
139
+ }
140
+ return `/assets/${normalized}`;
141
+ }
142
+ function parseHTML(html, fileName) {
143
+ const $ = cheerio.load(html);
144
+ const title = $("title").text() || fileName.replace(".html", "");
145
+ const cssFiles = [];
146
+ $('link[rel="stylesheet"]').each((_, el) => {
147
+ const href = $(el).attr("href");
148
+ if (href) {
149
+ cssFiles.push(href);
150
+ }
151
+ });
152
+ let embeddedStyles = "";
153
+ $(".global-embed style").each((_, el) => {
154
+ embeddedStyles += $(el).html() + "\n";
155
+ });
156
+ $("body > style").each((_, el) => {
157
+ embeddedStyles += $(el).html() + "\n";
158
+ });
159
+ $(".global-embed").remove();
160
+ $("body > style").remove();
161
+ $("body script").remove();
162
+ const images = [];
163
+ $("img").each((_, el) => {
164
+ const src = $(el).attr("src");
165
+ if (src) {
166
+ images.push(src);
167
+ }
168
+ });
169
+ const links = [];
170
+ $("a").each((_, el) => {
171
+ const href = $(el).attr("href");
172
+ if (href) {
173
+ links.push(href);
174
+ }
175
+ });
176
+ const htmlContent = $("body").html() || "";
177
+ return {
178
+ fileName,
179
+ title,
180
+ htmlContent,
181
+ cssFiles,
182
+ embeddedStyles,
183
+ images,
184
+ links
185
+ };
186
+ }
187
+ function transformForNuxt(html) {
188
+ const $ = cheerio.load(html);
189
+ $("html, head, body").each((_, el) => {
190
+ const $el = $(el);
191
+ $el.replaceWith($el.html() || "");
192
+ });
193
+ $("script").remove();
194
+ $("a").each((_, el) => {
195
+ const $el = $(el);
196
+ const href = $el.attr("href");
197
+ if (!href) return;
198
+ const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#");
199
+ if (!isExternal) {
200
+ const route = normalizeRoute(href);
201
+ $el.attr("to", route);
202
+ $el.removeAttr("href");
203
+ const content = $el.html();
204
+ const classes = $el.attr("class") || "";
205
+ $el.replaceWith(`<nuxt-link to="${route}" class="${classes}">${content}</nuxt-link>`);
206
+ }
207
+ });
208
+ $("img").each((_, el) => {
209
+ const $el = $(el);
210
+ const src = $el.attr("src");
211
+ if (src) {
212
+ const normalizedSrc = normalizeAssetPath(src);
213
+ $el.attr("src", normalizedSrc);
214
+ }
215
+ $el.removeAttr("srcset");
216
+ $el.removeAttr("sizes");
217
+ });
218
+ return $.html();
219
+ }
220
+ function htmlToVueComponent(html, pageName) {
221
+ return `
222
+ <script setup lang="ts">
223
+ // Page: ${pageName}
224
+ </script>
225
+
226
+ <template>
227
+ <div>
228
+ ${html}
229
+ </div>
230
+ </template>
231
+ `;
232
+ }
233
+ function deduplicateStyles(styles) {
234
+ if (!styles.trim()) return "";
235
+ const sections = styles.split(/\/\* From .+ \*\//);
236
+ const uniqueStyles = /* @__PURE__ */ new Set();
237
+ for (const section of sections) {
238
+ const trimmed = section.trim();
239
+ if (trimmed) {
240
+ uniqueStyles.add(trimmed);
241
+ }
242
+ }
243
+ return Array.from(uniqueStyles).join("\n\n");
244
+ }
245
+
246
+ // src/config-updater.ts
247
+ import fs2 from "fs-extra";
248
+ import path3 from "path";
249
+ function generateWebflowAssetPlugin(cssFiles) {
250
+ const webflowFiles = cssFiles.map((file) => `/assets/css/${path3.basename(file)}`);
251
+ return `import type { Plugin } from 'vite'
252
+
253
+ const webflowFiles = [${webflowFiles.map((f) => `'${f}'`).join(", ")}]
254
+ const replacements = [
255
+ ['../images/', '/assets/images/'],
256
+ ['../fonts/', '/assets/fonts/']
257
+ ]
258
+
259
+ const webflowURLReset = (): Plugin => ({
260
+ name: 'webflowURLReset',
261
+ config: () => ({
262
+ build: {
263
+ rollupOptions: {
264
+ external: [/\\.\\.\\/fonts\\//, /\\.\\.\\/images\\//]
265
+ }
266
+ }
267
+ }),
268
+ transform: (code, id) => {
269
+ if (webflowFiles.some((path) => id.includes(path))) {
270
+ replacements.forEach(([search, replace]) => {
271
+ code = code.replaceAll(search, replace)
272
+ })
273
+ }
274
+
275
+ return { code, id, map: null }
276
+ }
277
+ })
278
+
279
+ export default webflowURLReset
280
+ `;
281
+ }
282
+ async function writeWebflowAssetPlugin(outputDir, cssFiles) {
283
+ const utilsDir = path3.join(outputDir, "utils");
284
+ await fs2.ensureDir(utilsDir);
285
+ const content = generateWebflowAssetPlugin(cssFiles);
286
+ const targetPath = path3.join(utilsDir, "webflow-assets.ts");
287
+ await fs2.writeFile(targetPath, content, "utf-8");
288
+ }
289
+ async function updateNuxtConfig(outputDir, cssFiles) {
290
+ const configPath = path3.join(outputDir, "nuxt.config.ts");
291
+ const configExists = await fs2.pathExists(configPath);
292
+ if (!configExists) {
293
+ throw new Error("nuxt.config.ts not found in output directory");
294
+ }
295
+ let config = await fs2.readFile(configPath, "utf-8");
296
+ const cssEntries = cssFiles.map((file) => ` '~/assets/css/${path3.basename(file)}'`);
297
+ if (config.includes("css:")) {
298
+ config = config.replace(
299
+ /css:\s*\[/,
300
+ `css: [
301
+ ${cssEntries.join(",\n")},`
302
+ );
303
+ } else {
304
+ config = config.replace(
305
+ /export default defineNuxtConfig\(\{/,
306
+ `export default defineNuxtConfig({
307
+ css: [
308
+ ${cssEntries.join(",\n")}
309
+ ],`
310
+ );
311
+ }
312
+ await fs2.writeFile(configPath, config, "utf-8");
313
+ }
314
+ async function writeEmbeddedStyles(outputDir, styles) {
315
+ if (!styles.trim()) return;
316
+ const cssDir = path3.join(outputDir, "assets", "css");
317
+ await fs2.ensureDir(cssDir);
318
+ const mainCssPath = path3.join(cssDir, "main.css");
319
+ const exists = await fs2.pathExists(mainCssPath);
320
+ if (exists) {
321
+ const existing = await fs2.readFile(mainCssPath, "utf-8");
322
+ await fs2.writeFile(mainCssPath, `${existing}
323
+
324
+ /* Webflow Embedded Styles */
325
+ ${styles}`, "utf-8");
326
+ } else {
327
+ await fs2.writeFile(mainCssPath, `/* Webflow Embedded Styles */
328
+ ${styles}`, "utf-8");
329
+ }
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
+ }
367
+
368
+ // src/editor-integration.ts
369
+ import fs3 from "fs-extra";
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
+ }
578
+ async function createEditorPlugin(outputDir) {
579
+ const pluginsDir = path4.join(outputDir, "plugins");
580
+ await fs3.ensureDir(pluginsDir);
581
+ const pluginContent = `/**
582
+ * CMS Editor Overlay Plugin
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
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
+ }
604
+
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) => {
621
+ // Only run on client side
622
+ if (process.server) return;
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;
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
+ });
748
+ });
749
+ `;
750
+ const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
751
+ await fs3.writeFile(pluginPath, pluginContent, "utf-8");
752
+ }
753
+ async function addEditorDependency(outputDir) {
754
+ const packageJsonPath = path4.join(outputDir, "package.json");
755
+ if (await fs3.pathExists(packageJsonPath)) {
756
+ const packageJson = await fs3.readJson(packageJsonPath);
757
+ if (!packageJson.dependencies) {
758
+ packageJson.dependencies = {};
759
+ }
760
+ packageJson.dependencies["@see-ms/editor-overlay"] = "^1.0.0";
761
+ await fs3.writeJson(packageJsonPath, packageJson, { spaces: 2 });
762
+ }
763
+ }
764
+ async function createSaveEndpoint(outputDir) {
765
+ const serverDir = path4.join(outputDir, "server", "api", "cms");
766
+ await fs3.ensureDir(serverDir);
767
+ const endpointContent = `/**
768
+ * API endpoint for saving CMS changes
769
+ * Handles draft and final saving to Strapi
770
+ */
771
+
772
+ import fs from 'fs';
773
+ import path from 'path';
774
+
775
+ export default defineEventHandler(async (event) => {
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
+ }
960
+ });
961
+ `;
962
+ const endpointPath = path4.join(serverDir, "save.post.ts");
963
+ await fs3.writeFile(endpointPath, endpointContent, "utf-8");
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
+ }
1358
+
1359
+ // src/boilerplate.ts
1360
+ import fs4 from "fs-extra";
1361
+ import path5 from "path";
1362
+ import { execSync as execSync2 } from "child_process";
1363
+ import pc2 from "picocolors";
1364
+ function isGitHubURL(source) {
1365
+ return source.startsWith("https://github.com/") || source.startsWith("git@github.com:") || source.includes("github.com");
1366
+ }
1367
+ async function cloneFromGitHub(repoUrl, outputDir) {
1368
+ console.log(pc2.blue(" Cloning from GitHub..."));
1369
+ try {
1370
+ execSync2(`git clone ${repoUrl} ${outputDir}`, { stdio: "inherit" });
1371
+ const gitDir = path5.join(outputDir, ".git");
1372
+ await fs4.remove(gitDir);
1373
+ console.log(pc2.green(" \u2713 Boilerplate cloned successfully"));
1374
+ } catch (error) {
1375
+ throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
1376
+ }
1377
+ }
1378
+ async function copyFromLocal(sourcePath, outputDir) {
1379
+ console.log(pc2.blue(" Copying from local path..."));
1380
+ const sourceExists = await fs4.pathExists(sourcePath);
1381
+ if (!sourceExists) {
1382
+ throw new Error(`Local boilerplate not found: ${sourcePath}`);
1383
+ }
1384
+ await fs4.copy(sourcePath, outputDir, {
1385
+ filter: (src) => {
1386
+ const name = path5.basename(src);
1387
+ return !["node_modules", ".nuxt", ".output", ".git", "dist"].includes(name);
1388
+ }
1389
+ });
1390
+ console.log(pc2.green(" \u2713 Boilerplate copied successfully"));
1391
+ }
1392
+ async function setupBoilerplate(boilerplateSource, outputDir) {
1393
+ if (!boilerplateSource) {
1394
+ console.log(pc2.blue("\n\u{1F4E6} Creating minimal Nuxt structure..."));
1395
+ await fs4.ensureDir(outputDir);
1396
+ await fs4.ensureDir(path5.join(outputDir, "pages"));
1397
+ await fs4.ensureDir(path5.join(outputDir, "assets"));
1398
+ await fs4.ensureDir(path5.join(outputDir, "public"));
1399
+ await fs4.ensureDir(path5.join(outputDir, "utils"));
1400
+ const configPath = path5.join(outputDir, "nuxt.config.ts");
1401
+ const configExists = await fs4.pathExists(configPath);
1402
+ if (!configExists) {
1403
+ const basicConfig = `export default defineNuxtConfig({
1404
+ devtools: { enabled: true },
1405
+ css: [],
1406
+ })
1407
+ `;
1408
+ await fs4.writeFile(configPath, basicConfig, "utf-8");
1409
+ }
1410
+ console.log(pc2.green(" \u2713 Structure created"));
1411
+ return;
1412
+ }
1413
+ const outputExists = await fs4.pathExists(outputDir);
1414
+ if (outputExists) {
1415
+ throw new Error(`Output directory already exists: ${outputDir}. Please choose a different path or remove it first.`);
1416
+ }
1417
+ console.log(pc2.blue("\n\u{1F4E6} Setting up boilerplate..."));
1418
+ if (isGitHubURL(boilerplateSource)) {
1419
+ await cloneFromGitHub(boilerplateSource, outputDir);
1420
+ } else {
1421
+ await copyFromLocal(boilerplateSource, outputDir);
1422
+ }
1423
+ }
1424
+
1425
+ // src/manifest.ts
1426
+ import fs6 from "fs-extra";
1427
+ import path7 from "path";
1428
+
1429
+ // src/detector.ts
1430
+ import * as cheerio2 from "cheerio";
1431
+ import fs5 from "fs-extra";
1432
+ import path6 from "path";
1433
+ function cleanClassName(className) {
1434
+ return className.split(" ").filter((cls) => !cls.startsWith("c-") && !cls.startsWith("w-")).filter((cls) => cls.length > 0).join(" ");
1435
+ }
1436
+ function getPrimaryClass(classAttr) {
1437
+ if (!classAttr) return null;
1438
+ const cleaned = cleanClassName(classAttr);
1439
+ const classes = cleaned.split(" ").filter((c) => c.length > 0);
1440
+ if (classes.length === 0) return null;
1441
+ const original = classes[0];
1442
+ return {
1443
+ selector: original,
1444
+ // Keep original with dashes for CSS selector
1445
+ fieldName: original.replace(/-/g, "_")
1446
+ // Normalize for field name
1447
+ };
1448
+ }
1449
+ function getContextModifier(_$, $el) {
1450
+ let $current = $el.parent();
1451
+ let depth = 0;
1452
+ while ($current.length > 0 && depth < 5) {
1453
+ const classes = $current.attr("class");
1454
+ if (classes) {
1455
+ const ccClass = classes.split(" ").find((c) => c.startsWith("cc-"));
1456
+ if (ccClass) {
1457
+ return ccClass.replace("cc-", "").replace(/-/g, "_");
1458
+ }
1459
+ }
1460
+ $current = $current.parent();
1461
+ depth++;
1462
+ }
1463
+ return null;
1464
+ }
1465
+ function isDecorativeImage(_$, $img) {
1466
+ const $parent = $img.parent();
1467
+ const parentClass = $parent.attr("class") || "";
1468
+ const decorativePatterns = [
1469
+ "nav",
1470
+ "logo",
1471
+ "icon",
1472
+ "arrow",
1473
+ "button",
1474
+ "quote",
1475
+ "pagination",
1476
+ "footer",
1477
+ "link"
1478
+ ];
1479
+ return decorativePatterns.some(
1480
+ (pattern) => parentClass.includes(pattern) || parentClass.includes(`${pattern}_`)
1481
+ );
1482
+ }
1483
+ function isInsideButton($, el) {
1484
+ const $el = $(el);
1485
+ const $button = $el.closest("button, a, NuxtLink, .c_button, .c_icon_button");
1486
+ return $button.length > 0;
1487
+ }
1488
+ function extractTemplateFromVue(vueContent) {
1489
+ const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
1490
+ if (!templateMatch) {
1491
+ return "";
1492
+ }
1493
+ return templateMatch[1];
1494
+ }
1495
+ function detectEditableFields(templateHtml) {
1496
+ const $ = cheerio2.load(templateHtml);
1497
+ const detectedFields = {};
1498
+ const detectedCollections = {};
1499
+ const collectionElements = /* @__PURE__ */ new Set();
1500
+ const processedCollectionClasses = /* @__PURE__ */ new Set();
1501
+ const potentialCollections = /* @__PURE__ */ new Map();
1502
+ $("[class]").each((_, el) => {
1503
+ const primaryClass = getPrimaryClass($(el).attr("class"));
1504
+ 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")) {
1505
+ if (!potentialCollections.has(primaryClass.fieldName)) {
1506
+ potentialCollections.set(primaryClass.fieldName, []);
1507
+ }
1508
+ potentialCollections.get(primaryClass.fieldName)?.push(el);
1509
+ }
1510
+ });
1511
+ potentialCollections.forEach((elements, className) => {
1512
+ if (elements.length >= 2) {
1513
+ const $first = $(elements[0]);
1514
+ const collectionFields = {};
1515
+ processedCollectionClasses.add(className);
1516
+ elements.forEach((el) => {
1517
+ collectionElements.add(el);
1518
+ $(el).find("*").each((_, child) => {
1519
+ collectionElements.add(child);
1520
+ });
1521
+ });
1522
+ const collectionClassInfo = getPrimaryClass($(elements[0]).attr("class"));
1523
+ const collectionSelector = collectionClassInfo ? `.${collectionClassInfo.selector}` : `.${className}`;
1524
+ $first.find("img").each((_, img) => {
1525
+ if (isInsideButton($, img)) return;
1526
+ const $img = $(img);
1527
+ const $parent = $img.parent();
1528
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
1529
+ if (parentClassInfo && parentClassInfo.fieldName.includes("image")) {
1530
+ collectionFields.image = `.${parentClassInfo.selector}`;
1531
+ return false;
1532
+ }
1533
+ });
1534
+ $first.find("div").each((_, el) => {
1535
+ const classInfo = getPrimaryClass($(el).attr("class"));
1536
+ if (classInfo && classInfo.fieldName.includes("tag") && !classInfo.fieldName.includes("container")) {
1537
+ collectionFields.tag = `.${classInfo.selector}`;
1538
+ return false;
1539
+ }
1540
+ });
1541
+ $first.find("h1, h2, h3, h4, h5, h6").first().each((_, el) => {
1542
+ const classInfo = getPrimaryClass($(el).attr("class"));
1543
+ if (classInfo) {
1544
+ collectionFields.title = `.${classInfo.selector}`;
1545
+ }
1546
+ });
1547
+ $first.find("p").first().each((_, el) => {
1548
+ const classInfo = getPrimaryClass($(el).attr("class"));
1549
+ if (classInfo) {
1550
+ collectionFields.description = `.${classInfo.selector}`;
1551
+ }
1552
+ });
1553
+ $first.find("a, NuxtLink").not(".c_button, .c_icon_button").each((_, el) => {
1554
+ const $link = $(el);
1555
+ const linkText = $link.text().trim();
1556
+ if (linkText) {
1557
+ const classInfo = getPrimaryClass($link.attr("class"));
1558
+ collectionFields.link = classInfo ? `.${classInfo.selector}` : "a";
1559
+ return false;
1560
+ }
1561
+ });
1562
+ if (Object.keys(collectionFields).length > 0) {
1563
+ let collectionName = className;
1564
+ if (!collectionName.endsWith("s")) {
1565
+ collectionName += "s";
1566
+ }
1567
+ detectedCollections[collectionName] = {
1568
+ selector: collectionSelector,
1569
+ fields: collectionFields
1570
+ };
1571
+ }
1572
+ }
1573
+ });
1574
+ const $body = $("body");
1575
+ $body.find("h1, h2, h3, h4, h5, h6").each((index, el) => {
1576
+ if (collectionElements.has(el)) return;
1577
+ const $el = $(el);
1578
+ const text = $el.text().trim();
1579
+ const classInfo = getPrimaryClass($el.attr("class"));
1580
+ if (text) {
1581
+ let fieldName;
1582
+ let selector;
1583
+ if (classInfo && !classInfo.fieldName.startsWith("heading_")) {
1584
+ fieldName = classInfo.fieldName;
1585
+ selector = `.${classInfo.selector}`;
1586
+ } else {
1587
+ const $parent = $el.closest('[class*="header"], [class*="hero"], [class*="cta"]').first();
1588
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
1589
+ const modifier = getContextModifier($, $el);
1590
+ if (parentClassInfo) {
1591
+ fieldName = modifier ? `${modifier}_${parentClassInfo.fieldName}` : parentClassInfo.fieldName;
1592
+ selector = classInfo ? `.${classInfo.selector}` : `.${parentClassInfo.selector}`;
1593
+ } else if (modifier) {
1594
+ fieldName = `${modifier}_heading`;
1595
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
1596
+ } else {
1597
+ fieldName = `heading_${index}`;
1598
+ selector = classInfo ? `.${classInfo.selector}` : el.tagName.toLowerCase();
1599
+ }
1600
+ }
1601
+ detectedFields[fieldName] = {
1602
+ selector,
1603
+ type: "plain",
1604
+ editable: true
1605
+ };
1606
+ }
1607
+ });
1608
+ $body.find("p").each((_index, el) => {
1609
+ if (collectionElements.has(el)) return;
1610
+ const $el = $(el);
1611
+ const text = $el.text().trim();
1612
+ const classInfo = getPrimaryClass($el.attr("class"));
1613
+ if (text && text.length > 20 && classInfo) {
1614
+ const hasFormatting = $el.find("strong, em, b, i, a, NuxtLink").length > 0;
1615
+ detectedFields[classInfo.fieldName] = {
1616
+ selector: `.${classInfo.selector}`,
1617
+ type: hasFormatting ? "rich" : "plain",
1618
+ editable: true
1619
+ };
1620
+ }
1621
+ });
1622
+ $body.find("img").each((_index, el) => {
1623
+ if (collectionElements.has(el)) return;
1624
+ if (isInsideButton($, el)) return;
1625
+ const $el = $(el);
1626
+ if (isDecorativeImage($, $el)) return;
1627
+ const $parent = $el.parent();
1628
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
1629
+ if (parentClassInfo) {
1630
+ const fieldName = parentClassInfo.fieldName.includes("image") ? parentClassInfo.fieldName : `${parentClassInfo.fieldName}_image`;
1631
+ detectedFields[fieldName] = {
1632
+ selector: `.${parentClassInfo.selector}`,
1633
+ type: "image",
1634
+ editable: true
1635
+ };
1636
+ }
1637
+ });
1638
+ $body.find("NuxtLink.c_button, a.c_button, .c_button").each((_index, el) => {
1639
+ if (collectionElements.has(el)) return;
1640
+ const $el = $(el);
1641
+ const text = $el.contents().filter(function() {
1642
+ return this.type === "text" || this.type === "tag" && this.name === "div";
1643
+ }).first().text().trim();
1644
+ if (text && text.length > 2) {
1645
+ const $parent = $el.closest('[class*="cta"]').first();
1646
+ const parentClassInfo = getPrimaryClass($parent.attr("class"));
1647
+ const fieldName = parentClassInfo ? `${parentClassInfo.fieldName}_button_text` : "button_text";
1648
+ detectedFields[fieldName] = {
1649
+ selector: `.c_button`,
1650
+ type: "plain",
1651
+ editable: true
1652
+ };
1653
+ }
1654
+ });
1655
+ return {
1656
+ fields: detectedFields,
1657
+ collections: detectedCollections
1658
+ };
1659
+ }
1660
+ async function analyzeVuePages(pagesDir) {
1661
+ const results = {};
1662
+ const vueFiles = await fs5.readdir(pagesDir);
1663
+ for (const file of vueFiles) {
1664
+ if (file.endsWith(".vue")) {
1665
+ const filePath = path6.join(pagesDir, file);
1666
+ const content = await fs5.readFile(filePath, "utf-8");
1667
+ const template = extractTemplateFromVue(content);
1668
+ if (template) {
1669
+ const pageName = file.replace(".vue", "");
1670
+ results[pageName] = detectEditableFields(template);
1671
+ }
1672
+ }
1673
+ }
1674
+ return results;
1675
+ }
1676
+
1677
+ // src/manifest.ts
1678
+ async function generateManifest(pagesDir) {
1679
+ const analyzed = await analyzeVuePages(pagesDir);
1680
+ const pages = {};
1681
+ for (const [pageName, detection] of Object.entries(analyzed)) {
1682
+ pages[pageName] = {
1683
+ fields: detection.fields,
1684
+ collections: detection.collections,
1685
+ meta: {
1686
+ route: pageName === "index" ? "/" : `/${pageName}`
1687
+ }
1688
+ };
1689
+ }
1690
+ const manifest = {
1691
+ version: "1.0",
1692
+ pages
1693
+ };
1694
+ return manifest;
1695
+ }
1696
+ async function writeManifest(outputDir, manifest) {
1697
+ const manifestContent = JSON.stringify(manifest, null, 2);
1698
+ const manifestPath = path7.join(outputDir, "cms-manifest.json");
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
+ }
1810
+ }
1811
+
1812
+ // src/transformer.ts
1813
+ function mapFieldTypeToStrapi(fieldType) {
1814
+ const typeMap = {
1815
+ plain: "string",
1816
+ rich: "richtext",
1817
+ html: "richtext",
1818
+ image: "media",
1819
+ link: "string",
1820
+ email: "email",
1821
+ phone: "string"
1822
+ };
1823
+ return typeMap[fieldType] || "string";
1824
+ }
1825
+ function pluralize(word) {
1826
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
1827
+ return word + "es";
1828
+ }
1829
+ if (word.endsWith("y") && word.length > 1) {
1830
+ const secondLast = word[word.length - 2];
1831
+ if (!"aeiou".includes(secondLast.toLowerCase())) {
1832
+ return word.slice(0, -1) + "ies";
1833
+ }
1834
+ }
1835
+ return word + "s";
1836
+ }
1837
+ function pageToStrapiSchema(pageName, fields) {
1838
+ const attributes = {};
1839
+ for (const [fieldName, field] of Object.entries(fields)) {
1840
+ attributes[fieldName] = {
1841
+ type: mapFieldTypeToStrapi(field.type),
1842
+ required: field.required || false
1843
+ };
1844
+ if (field.default) {
1845
+ attributes[fieldName].default = field.default;
1846
+ }
1847
+ }
1848
+ const displayName = pageName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1849
+ const kebabCaseName = pageName;
1850
+ const pluralName = pluralize(kebabCaseName);
1851
+ return {
1852
+ kind: "singleType",
1853
+ collectionName: kebabCaseName,
1854
+ info: {
1855
+ singularName: kebabCaseName,
1856
+ pluralName,
1857
+ displayName
1858
+ },
1859
+ options: {
1860
+ draftAndPublish: true
1861
+ },
1862
+ attributes
1863
+ };
1864
+ }
1865
+ function collectionToStrapiSchema(collectionName, collection) {
1866
+ const attributes = {};
1867
+ for (const [fieldName, _selector] of Object.entries(collection.fields)) {
1868
+ let type = "string";
1869
+ if (fieldName === "image" || fieldName.includes("image")) {
1870
+ type = "media";
1871
+ } else if (fieldName === "description" || fieldName === "content") {
1872
+ type = "richtext";
1873
+ } else if (fieldName === "link" || fieldName === "url") {
1874
+ type = "string";
1875
+ } else if (fieldName === "title" || fieldName === "tag") {
1876
+ type = "string";
1877
+ }
1878
+ attributes[fieldName] = {
1879
+ type
1880
+ };
1881
+ }
1882
+ const displayName = collectionName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1883
+ const kebabCaseName = collectionName.replace(/_/g, "-");
1884
+ const singularName = kebabCaseName.endsWith("s") ? kebabCaseName.slice(0, -1) : kebabCaseName;
1885
+ return {
1886
+ kind: "collectionType",
1887
+ collectionName: kebabCaseName,
1888
+ info: {
1889
+ singularName,
1890
+ pluralName: kebabCaseName,
1891
+ displayName
1892
+ },
1893
+ options: {
1894
+ draftAndPublish: true
1895
+ },
1896
+ attributes
1897
+ };
1898
+ }
1899
+ function manifestToSchemas(manifest) {
1900
+ const schemas = {};
1901
+ for (const [pageName, page] of Object.entries(manifest.pages)) {
1902
+ if (page.fields && Object.keys(page.fields).length > 0) {
1903
+ schemas[pageName] = pageToStrapiSchema(pageName, page.fields);
1904
+ }
1905
+ if (page.collections) {
1906
+ for (const [collectionName, collection] of Object.entries(page.collections)) {
1907
+ schemas[collectionName] = collectionToStrapiSchema(collectionName, collection);
1908
+ }
1909
+ }
1910
+ }
1911
+ return schemas;
1912
+ }
1913
+
1914
+ // src/schema-writer.ts
1915
+ import fs8 from "fs-extra";
1916
+ import path9 from "path";
1917
+ async function writeStrapiSchema(outputDir, name, schema) {
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");
1922
+ }
1923
+ async function writeAllSchemas(outputDir, schemas) {
1924
+ for (const [name, schema] of Object.entries(schemas)) {
1925
+ await writeStrapiSchema(outputDir, name, schema);
1926
+ }
1927
+ }
1928
+ async function createStrapiReadme(outputDir) {
1929
+ const readmePath = path9.join(outputDir, "cms-schemas", "README.md");
1930
+ const content = `# CMS Schemas
1931
+
1932
+ Auto-generated Strapi content type schemas from your Webflow export.
1933
+
1934
+ ## What's in this folder?
1935
+
1936
+ Each \`.json\` file is a Strapi content type schema:
1937
+
1938
+ - **Pages** (single types) - Unique pages like \`index.json\`, \`about.json\`
1939
+ - **Collections** (collection types) - Repeating content like \`portfolio_cards.json\`
1940
+
1941
+ ## How to use with Strapi
1942
+
1943
+ ### Option 1: Manual Setup (Recommended for learning)
1944
+
1945
+ 1. Start your Strapi project
1946
+ 2. In Strapi admin, go to **Content-Type Builder**
1947
+ 3. Create each content type manually using these schemas as reference
1948
+ 4. Match the field names and types
1949
+
1950
+ ### Option 2: Automated Setup (Advanced)
1951
+
1952
+ Copy schemas to your Strapi project structure:
1953
+
1954
+ \`\`\`bash
1955
+ # For each schema file, create the Strapi directory structure
1956
+ # Example for index.json (single type):
1957
+ mkdir -p strapi/src/api/index/content-types/index
1958
+ cp cms-schemas/index.json strapi/src/api/index/content-types/index/schema.json
1959
+
1960
+ # Example for portfolio_cards.json (collection type):
1961
+ mkdir -p strapi/src/api/portfolio-cards/content-types/portfolio-card
1962
+ cp cms-schemas/portfolio_cards.json strapi/src/api/portfolio-cards/content-types/portfolio-card/schema.json
1963
+ \`\`\`
1964
+
1965
+ Then restart Strapi - it will auto-create the content types.
1966
+
1967
+ ## Schema Structure
1968
+
1969
+ Each schema defines:
1970
+ - \`kind\`: "singleType" (unique page) or "collectionType" (repeating)
1971
+ - \`attributes\`: Fields and their types (string, richtext, media, etc.)
1972
+ - \`displayName\`: How it appears in Strapi admin
1973
+
1974
+ ## Field Types
1975
+
1976
+ - \`string\` - Plain text
1977
+ - \`richtext\` - Formatted text with HTML
1978
+ - \`media\` - Image uploads
1979
+
1980
+ ## Next Steps
1981
+
1982
+ 1. Set up a Strapi project: \`npx create-strapi-app@latest my-strapi\`
1983
+ 2. Use these schemas to create content types
1984
+ 3. Populate content in Strapi admin
1985
+ 4. Connect your Nuxt app to Strapi API
1986
+
1987
+ ## API Usage in Nuxt
1988
+
1989
+ Once Strapi is running with these content types:
1990
+
1991
+ \`\`\`typescript
1992
+ // Fetch single type (e.g., home page)
1993
+ const { data } = await $fetch('http://localhost:1337/api/index')
1994
+
1995
+ // Fetch collection type (e.g., portfolio cards)
1996
+ const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
1997
+ \`\`\`
1998
+ `;
1999
+ await fs8.writeFile(readmePath, content, "utf-8");
2000
+ }
2001
+
2002
+ // src/content-extractor.ts
2003
+ import * as cheerio4 from "cheerio";
2004
+ import path10 from "path";
2005
+ function extractContentFromHTML(html, _pageName, pageManifest) {
2006
+ const $ = cheerio4.load(html);
2007
+ const content = {
2008
+ fields: {},
2009
+ collections: {}
2010
+ };
2011
+ if (pageManifest.fields) {
2012
+ for (const [fieldName, field] of Object.entries(pageManifest.fields)) {
2013
+ const selector = field.selector;
2014
+ const element = $(selector).first();
2015
+ if (element.length > 0) {
2016
+ if (field.type === "image") {
2017
+ const src = element.attr("src") || element.find("img").attr("src") || "";
2018
+ content.fields[fieldName] = src;
2019
+ } else {
2020
+ const text = element.text().trim();
2021
+ content.fields[fieldName] = text;
2022
+ }
2023
+ }
2024
+ }
2025
+ }
2026
+ if (pageManifest.collections) {
2027
+ for (const [collectionName, collection] of Object.entries(pageManifest.collections)) {
2028
+ const items = [];
2029
+ const collectionElements = $(collection.selector);
2030
+ collectionElements.each((_, elem) => {
2031
+ const item = {};
2032
+ const $elem = $(elem);
2033
+ for (const [fieldName, fieldSelector] of Object.entries(collection.fields)) {
2034
+ const fieldElement = $elem.find(fieldSelector).first();
2035
+ if (fieldElement.length > 0) {
2036
+ if (fieldName === "image" || fieldName.includes("image")) {
2037
+ const src = fieldElement.attr("src") || fieldElement.find("img").attr("src") || "";
2038
+ item[fieldName] = src;
2039
+ } else if (fieldName === "link" || fieldName === "url") {
2040
+ const href = fieldElement.attr("href") || "";
2041
+ item[fieldName] = href;
2042
+ } else {
2043
+ const text = fieldElement.text().trim();
2044
+ item[fieldName] = text;
2045
+ }
2046
+ }
2047
+ }
2048
+ if (Object.keys(item).length > 0) {
2049
+ items.push(item);
2050
+ }
2051
+ });
2052
+ if (items.length > 0) {
2053
+ content.collections[collectionName] = items;
2054
+ }
2055
+ }
2056
+ }
2057
+ return content;
2058
+ }
2059
+ function extractAllContent(htmlFiles, manifest) {
2060
+ const extractedContent = {
2061
+ pages: {}
2062
+ };
2063
+ for (const [pageName, pageManifest] of Object.entries(manifest.pages)) {
2064
+ const html = htmlFiles.get(pageName);
2065
+ if (html) {
2066
+ const content = extractContentFromHTML(html, pageName, pageManifest);
2067
+ extractedContent.pages[pageName] = content;
2068
+ }
2069
+ }
2070
+ return extractedContent;
2071
+ }
2072
+ function normalizeImagePath(imageSrc) {
2073
+ if (!imageSrc) return "";
2074
+ if (imageSrc.startsWith("/")) return imageSrc;
2075
+ const filename = path10.basename(imageSrc);
2076
+ if (imageSrc.includes("images/")) {
2077
+ return `/images/${filename}`;
2078
+ }
2079
+ return `/${filename}`;
2080
+ }
2081
+ function formatForStrapi(extracted) {
2082
+ const seedData = {};
2083
+ for (const [pageName, content] of Object.entries(extracted.pages)) {
2084
+ if (Object.keys(content.fields).length > 0) {
2085
+ const formattedFields = {};
2086
+ for (const [fieldName, value] of Object.entries(content.fields)) {
2087
+ if (fieldName.includes("image") || fieldName.includes("bg")) {
2088
+ formattedFields[fieldName] = normalizeImagePath(value);
2089
+ } else {
2090
+ formattedFields[fieldName] = value;
2091
+ }
2092
+ }
2093
+ seedData[pageName] = formattedFields;
2094
+ }
2095
+ for (const [collectionName, items] of Object.entries(content.collections)) {
2096
+ const formattedItems = items.map((item) => {
2097
+ const formattedItem = {};
2098
+ for (const [fieldName, value] of Object.entries(item)) {
2099
+ if (fieldName === "image" || fieldName.includes("image")) {
2100
+ formattedItem[fieldName] = normalizeImagePath(value);
2101
+ } else {
2102
+ formattedItem[fieldName] = value;
2103
+ }
2104
+ }
2105
+ return formattedItem;
2106
+ });
2107
+ seedData[collectionName] = formattedItems;
2108
+ }
2109
+ }
2110
+ return seedData;
2111
+ }
2112
+
2113
+ // src/seed-writer.ts
2114
+ import fs9 from "fs-extra";
2115
+ import path11 from "path";
2116
+ async function writeSeedData(outputDir, seedData) {
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 });
2121
+ }
2122
+ async function createSeedReadme(outputDir) {
2123
+ const readmePath = path11.join(outputDir, "cms-seed", "README.md");
2124
+ const content = `# CMS Seed Data
2125
+
2126
+ Auto-extracted content from your Webflow export, ready to seed into Strapi.
2127
+
2128
+ ## What's in this folder?
2129
+
2130
+ \`seed-data.json\` contains the actual content extracted from your HTML:
2131
+ - **Single types** - Page-specific content (homepage, about page, etc.)
2132
+ - **Collection types** - Repeating items (portfolio cards, team members, etc.)
2133
+
2134
+ ## Structure
2135
+
2136
+ \`\`\`json
2137
+ {
2138
+ "index": {
2139
+ "hero_heading_container": "Actual heading from HTML",
2140
+ "hero_bg_image": "/images/hero.jpg",
2141
+ ...
2142
+ },
2143
+ "portfolio_cards": [
2144
+ {
2145
+ "image": "/images/card1.jpg",
2146
+ "tag": "Technology",
2147
+ "description": "Card description"
2148
+ }
2149
+ ]
2150
+ }
2151
+ \`\`\`
2152
+
2153
+ ## How to Seed Strapi
2154
+
2155
+ ### Option 1: Manual Entry
2156
+ 1. Open Strapi admin panel
2157
+ 2. Go to Content Manager
2158
+ 3. Create entries using the data from \`seed-data.json\`
2159
+
2160
+ ### Option 2: Automated Seeding (Coming Soon)
2161
+ We'll provide a seeding script that:
2162
+ 1. Uploads images to Strapi media library
2163
+ 2. Creates content entries via Strapi API
2164
+ 3. Handles relationships between content types
2165
+
2166
+ ## Image Paths
2167
+
2168
+ Image paths in the seed data reference files in your Nuxt \`public/\` directory:
2169
+ - \`/images/hero.jpg\` \u2192 \`public/images/hero.jpg\`
2170
+
2171
+ When seeding Strapi, these images will be uploaded to Strapi's media library.
2172
+
2173
+ ## Next Steps
2174
+
2175
+ 1. Review the extracted data for accuracy
2176
+ 2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
2177
+ 3. Use this seed data to populate your CMS
2178
+ `;
2179
+ await fs9.writeFile(readmePath, content, "utf-8");
2180
+ }
2181
+
2182
+ // src/converter.ts
2183
+ async function convertWebflowExport(options) {
2184
+ const { inputDir, outputDir, boilerplate } = options;
2185
+ console.log(pc3.cyan("\u{1F680} Starting Webflow to Nuxt conversion..."));
2186
+ console.log(pc3.dim(`Input: ${inputDir}`));
2187
+ console.log(pc3.dim(`Output: ${outputDir}`));
2188
+ try {
2189
+ await setupBoilerplate(boilerplate, outputDir);
2190
+ const inputExists = await fs10.pathExists(inputDir);
2191
+ if (!inputExists) {
2192
+ throw new Error(`Input directory not found: ${inputDir}`);
2193
+ }
2194
+ console.log(pc3.blue("\n\u{1F4C2} Scanning assets..."));
2195
+ const assets = await scanAssets(inputDir);
2196
+ console.log(pc3.green(` \u2713 Found ${assets.css.length} CSS files`));
2197
+ console.log(pc3.green(` \u2713 Found ${assets.images.length} images`));
2198
+ console.log(pc3.green(` \u2713 Found ${assets.fonts.length} fonts`));
2199
+ console.log(pc3.green(` \u2713 Found ${assets.js.length} JS files`));
2200
+ console.log(pc3.blue("\n\u{1F4E6} Copying assets..."));
2201
+ await copyAllAssets(inputDir, outputDir, assets);
2202
+ console.log(pc3.green(" \u2713 Assets copied successfully"));
2203
+ console.log(pc3.blue("\n\u{1F50D} Finding HTML files..."));
2204
+ const htmlFiles = await findHTMLFiles(inputDir);
2205
+ console.log(pc3.green(` \u2713 Found ${htmlFiles.length} HTML files`));
2206
+ const htmlContentMap = /* @__PURE__ */ new Map();
2207
+ for (const htmlFile of htmlFiles) {
2208
+ const html = await readHTMLFile(inputDir, htmlFile);
2209
+ const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
2210
+ htmlContentMap.set(pageName, html);
2211
+ console.log(pc3.dim(` Stored: ${pageName} from ${htmlFile}`));
2212
+ }
2213
+ console.log(pc3.blue("\n\u2699\uFE0F Converting HTML to Vue components..."));
2214
+ let allEmbeddedStyles = "";
2215
+ for (const htmlFile of htmlFiles) {
2216
+ const html = htmlContentMap.get(htmlFile.replace(".html", "").replace(/\//g, "-"));
2217
+ const parsed = parseHTML(html, htmlFile);
2218
+ if (parsed.embeddedStyles) {
2219
+ allEmbeddedStyles += `
2220
+ /* From ${htmlFile} */
2221
+ ${parsed.embeddedStyles}
2222
+ `;
2223
+ }
2224
+ const transformed = transformForNuxt(parsed.htmlContent);
2225
+ const pageName = htmlFile.replace(".html", "").replace(/\//g, "-");
2226
+ const vueComponent = htmlToVueComponent(transformed, pageName);
2227
+ await writeVueComponent(outputDir, htmlFile, vueComponent);
2228
+ console.log(pc3.green(` \u2713 Created ${htmlFile.replace(".html", ".vue")}`));
2229
+ }
2230
+ await formatVueFiles(outputDir);
2231
+ console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
2232
+ const pagesDir = path12.join(outputDir, "pages");
2233
+ const manifest = await generateManifest(pagesDir);
2234
+ await writeManifest(outputDir, manifest);
2235
+ const totalFields = Object.values(manifest.pages).reduce(
2236
+ (sum, page) => sum + Object.keys(page.fields || {}).length,
2237
+ 0
2238
+ );
2239
+ const totalCollections = Object.values(manifest.pages).reduce(
2240
+ (sum, page) => sum + Object.keys(page.collections || {}).length,
2241
+ 0
2242
+ );
2243
+ console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
2244
+ console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
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`));
2249
+ console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
2250
+ console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
2251
+ console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
2252
+ const extractedContent = extractAllContent(htmlContentMap, manifest);
2253
+ const seedData = formatForStrapi(extractedContent);
2254
+ await writeSeedData(outputDir, seedData);
2255
+ await createSeedReadme(outputDir);
2256
+ const pagesWithContent = Object.keys(seedData).filter((key) => {
2257
+ const data = seedData[key];
2258
+ if (Array.isArray(data)) return data.length > 0;
2259
+ return Object.keys(data).length > 0;
2260
+ }).length;
2261
+ console.log(pc3.green(` \u2713 Extracted content from ${pagesWithContent} pages`));
2262
+ console.log(pc3.green(` \u2713 Generated cms-seed/seed-data.json`));
2263
+ console.log(pc3.blue("\n\u{1F4CB} Generating Strapi schemas..."));
2264
+ const schemas = manifestToSchemas(manifest);
2265
+ await writeAllSchemas(outputDir, schemas);
2266
+ await createStrapiReadme(outputDir);
2267
+ console.log(pc3.green(` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`));
2268
+ console.log(pc3.dim(" View schemas in: cms-schemas/"));
2269
+ if (allEmbeddedStyles.trim()) {
2270
+ console.log(pc3.blue("\n\u2728 Writing embedded styles..."));
2271
+ const dedupedStyles = deduplicateStyles(allEmbeddedStyles);
2272
+ await writeEmbeddedStyles(outputDir, dedupedStyles);
2273
+ console.log(pc3.green(" \u2713 Embedded styles added to main.css"));
2274
+ }
2275
+ console.log(pc3.blue("\n\u{1F527} Generating webflow-assets.ts plugin..."));
2276
+ await writeWebflowAssetPlugin(outputDir, assets.css);
2277
+ console.log(pc3.green(" \u2713 Plugin generated (existing file overwritten)"));
2278
+ console.log(pc3.blue("\n\u2699\uFE0F Updating nuxt.config.ts..."));
2279
+ try {
2280
+ await updateNuxtConfig(outputDir, assets.css);
2281
+ console.log(pc3.green(" \u2713 Config updated"));
2282
+ } catch (error) {
2283
+ console.log(pc3.yellow(" \u26A0 Could not update nuxt.config.ts automatically"));
2284
+ console.log(pc3.dim(" Please add CSS files manually"));
2285
+ }
2286
+ console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
2287
+ await createEditorPlugin(outputDir);
2288
+ await createEditorContentComposable(outputDir);
2289
+ await createStrapiContentComposable(outputDir);
2290
+ await addEditorDependency(outputDir);
2291
+ await createSaveEndpoint(outputDir);
2292
+ await createPublishEndpoint(outputDir);
2293
+ await createStrapiBootstrap(outputDir);
2294
+ await addStrapiUrlToConfig(outputDir);
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"));
2298
+ console.log(pc3.green(" \u2713 Editor dependency added"));
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"));
2303
+ console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
2304
+ console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
2305
+ console.log(pc3.dim(` 1. cd ${outputDir}`));
2306
+ console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
2307
+ console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
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!"));
2313
+ } catch (error) {
2314
+ console.error(pc3.red("\n\u274C Conversion failed:"));
2315
+ console.error(pc3.red(error instanceof Error ? error.message : String(error)));
2316
+ throw error;
2317
+ }
2318
+ }
2319
+
2320
+ // src/strapi-setup.ts
2321
+ import fs11 from "fs-extra";
2322
+ import path13 from "path";
2323
+ import { glob as glob2 } from "glob";
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
+ }
2370
+ async function completeSetup(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";
2374
+ console.log("\u{1F680} Starting complete Strapi setup...\n");
2375
+ console.log("\u{1F4E6} Step 1: Installing schemas...");
2376
+ await installSchemas(projectDir, strapiDir);
2377
+ console.log("\u2713 Schemas installed\n");
2378
+ console.log("\u23F8\uFE0F Step 2: Restart Strapi to load schemas");
2379
+ console.log(" Run: npm run develop (in Strapi directory)");
2380
+ console.log(" Press Enter when Strapi is running...");
2381
+ await waitForEnter();
2382
+ console.log("\n\u{1F50D} Step 3: Checking Strapi connection...");
2383
+ const isRunning = await checkStrapiRunning(strapiUrl);
2384
+ if (!isRunning) {
2385
+ console.error("\u274C Cannot connect to Strapi at", strapiUrl);
2386
+ console.log(" Make sure Strapi is running: npm run develop");
2387
+ process.exit(1);
2388
+ }
2389
+ console.log("\u2713 Connected to Strapi\n");
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 {
2396
+ console.log("\u{1F511} Step 4: API Token needed");
2397
+ console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
2398
+ console.log(" 2. Go to Settings > API Tokens > Create new API Token");
2399
+ console.log(' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"');
2400
+ console.log(" 4. Copy the token and paste it here:\n");
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
+ }
2407
+ console.log("");
2408
+ }
2409
+ console.log("\u{1F4F8} Step 5: Uploading images...");
2410
+ const mediaMap = await uploadAllImages(projectDir, strapiUrl, token);
2411
+ console.log(`\u2713 Uploaded ${Object.keys(mediaMap).length} images
2412
+ `);
2413
+ console.log("\u{1F4DD} Step 6: Seeding content...");
2414
+ await seedContent(projectDir, strapiUrl, token, mediaMap);
2415
+ console.log("\u2713 Content seeded\n");
2416
+ console.log("\u2705 Complete setup finished!");
2417
+ console.log("\n\u{1F4CB} Next steps:");
2418
+ console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
2419
+ console.log(" 2. Check Content Manager - your content should be there!");
2420
+ console.log(" 3. Connect your Nuxt app to Strapi API");
2421
+ }
2422
+ async function installSchemas(projectDir, strapiDir) {
2423
+ if (!await fs11.pathExists(strapiDir)) {
2424
+ console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
2425
+ console.error(` Resolved to: ${path13.resolve(strapiDir)}`);
2426
+ throw new Error(`Strapi directory not found: ${strapiDir}`);
2427
+ }
2428
+ const packageJsonPath = path13.join(strapiDir, "package.json");
2429
+ if (await fs11.pathExists(packageJsonPath)) {
2430
+ const pkg = await fs11.readJson(packageJsonPath);
2431
+ if (!pkg.dependencies?.["@strapi/strapi"]) {
2432
+ console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
2433
+ }
2434
+ }
2435
+ const schemaDir = path13.join(projectDir, "cms-schemas");
2436
+ const schemaFiles = await glob2("*.json", {
2437
+ cwd: schemaDir,
2438
+ absolute: false
2439
+ });
2440
+ if (schemaFiles.length === 0) {
2441
+ console.log("\u26A0\uFE0F No schema files found");
2442
+ return;
2443
+ }
2444
+ console.log(` Found ${schemaFiles.length} schema file(s)`);
2445
+ for (const file of schemaFiles) {
2446
+ const schemaPath = path13.join(schemaDir, file);
2447
+ const schema = await fs11.readJson(schemaPath);
2448
+ const singularName = schema.info?.singularName || path13.basename(file, ".json");
2449
+ console.log(` Installing ${singularName}...`);
2450
+ try {
2451
+ const apiPath = path13.join(strapiDir, "src", "api", singularName);
2452
+ const contentTypesPath = path13.join(
2453
+ apiPath,
2454
+ "content-types",
2455
+ singularName
2456
+ );
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 });
2463
+ const routeContent = `import { factories } from '@strapi/strapi';
2464
+ export default factories.createCoreRouter('api::${singularName}.${singularName}');
2465
+ `;
2466
+ await fs11.writeFile(
2467
+ path13.join(apiPath, "routes", `${singularName}.ts`),
2468
+ routeContent
2469
+ );
2470
+ const controllerContent = `import { factories } from '@strapi/strapi';
2471
+ export default factories.createCoreController('api::${singularName}.${singularName}');
2472
+ `;
2473
+ await fs11.writeFile(
2474
+ path13.join(apiPath, "controllers", `${singularName}.ts`),
2475
+ controllerContent
2476
+ );
2477
+ const serviceContent = `import { factories } from '@strapi/strapi';
2478
+ export default factories.createCoreService('api::${singularName}.${singularName}');
2479
+ `;
2480
+ await fs11.writeFile(
2481
+ path13.join(apiPath, "services", `${singularName}.ts`),
2482
+ serviceContent
2483
+ );
2484
+ } catch (error) {
2485
+ console.error(` \u2717 Failed to install ${singularName}: ${error.message}`);
2486
+ }
2487
+ }
2488
+ }
2489
+ async function checkStrapiRunning(strapiUrl) {
2490
+ try {
2491
+ const response = await fetch(`${strapiUrl}/_health`);
2492
+ return response.ok;
2493
+ } catch {
2494
+ return false;
2495
+ }
2496
+ }
2497
+ function createReadline() {
2498
+ return readline.createInterface({
2499
+ input: process.stdin,
2500
+ output: process.stdout
2501
+ });
2502
+ }
2503
+ async function waitForEnter() {
2504
+ const rl = createReadline();
2505
+ return new Promise((resolve) => {
2506
+ rl.question("", () => {
2507
+ rl.close();
2508
+ resolve();
2509
+ });
2510
+ });
2511
+ }
2512
+ async function promptForToken() {
2513
+ const rl = createReadline();
2514
+ return new Promise((resolve) => {
2515
+ rl.question(" Token: ", (answer) => {
2516
+ rl.close();
2517
+ resolve(answer.trim());
2518
+ });
2519
+ });
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
+ }
2562
+ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
2563
+ const mediaMap = /* @__PURE__ */ new Map();
2564
+ const imagesDir = path13.join(projectDir, "public", "assets", "images");
2565
+ if (!await fs11.pathExists(imagesDir)) {
2566
+ console.log(" No images directory found");
2567
+ return mediaMap;
2568
+ }
2569
+ const imageFiles = await glob2("**/*.{jpg,jpeg,png,gif,webp,svg}", {
2570
+ cwd: imagesDir,
2571
+ absolute: false
2572
+ });
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...`);
2578
+ for (const imageFile of imageFiles) {
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);
2589
+ if (mediaId) {
2590
+ mediaMap.set(`/images/${imageFile}`, mediaId);
2591
+ mediaMap.set(imageFile, mediaId);
2592
+ uploadedCount++;
2593
+ console.log(` \u2713 ${imageFile}`);
2594
+ }
2595
+ }
2596
+ console.log(` Uploaded: ${uploadedCount}, Skipped (existing): ${skippedCount}`);
2597
+ return mediaMap;
2598
+ }
2599
+ async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
2600
+ try {
2601
+ const fileBuffer = await fs11.readFile(filePath);
2602
+ const mimeType = getMimeType(fileName);
2603
+ const blob = new Blob([fileBuffer], { type: mimeType });
2604
+ const formData = new globalThis.FormData();
2605
+ formData.append("files", blob, fileName);
2606
+ const response = await fetch(`${strapiUrl}/api/upload`, {
2607
+ method: "POST",
2608
+ headers: {
2609
+ Authorization: `Bearer ${apiToken}`
2610
+ },
2611
+ body: formData
2612
+ });
2613
+ if (!response.ok) {
2614
+ const errorText = await response.text();
2615
+ console.error(
2616
+ ` \u2717 Failed to upload ${fileName}: ${response.status} - ${errorText}`
2617
+ );
2618
+ return null;
2619
+ }
2620
+ const data = await response.json();
2621
+ return data[0]?.id || null;
2622
+ } catch (error) {
2623
+ console.error(` \u2717 Error uploading ${fileName}:`, error);
2624
+ return null;
2625
+ }
2626
+ }
2627
+ function getMimeType(fileName) {
2628
+ const ext = path13.extname(fileName).toLowerCase();
2629
+ const mimeTypes = {
2630
+ ".jpg": "image/jpeg",
2631
+ ".jpeg": "image/jpeg",
2632
+ ".png": "image/png",
2633
+ ".gif": "image/gif",
2634
+ ".webp": "image/webp",
2635
+ ".svg": "image/svg+xml"
2636
+ };
2637
+ return mimeTypes[ext] || "application/octet-stream";
2638
+ }
2639
+ async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
2640
+ const seedPath = path13.join(projectDir, "cms-seed", "seed-data.json");
2641
+ if (!await fs11.pathExists(seedPath)) {
2642
+ console.log(" No seed data found");
2643
+ return;
2644
+ }
2645
+ const seedData = await fs11.readJson(seedPath);
2646
+ const schemasDir = path13.join(projectDir, "cms-schemas");
2647
+ const schemas = /* @__PURE__ */ new Map();
2648
+ const schemaFiles = await glob2("*.json", { cwd: schemasDir });
2649
+ for (const file of schemaFiles) {
2650
+ const schema = await fs11.readJson(path13.join(schemasDir, file));
2651
+ const name = path13.basename(file, ".json");
2652
+ schemas.set(name, schema);
2653
+ }
2654
+ let successCount = 0;
2655
+ let totalCount = 0;
2656
+ for (const [contentType, data] of Object.entries(seedData)) {
2657
+ const schema = schemas.get(contentType);
2658
+ if (!schema) {
2659
+ console.log(` \u26A0\uFE0F No schema found for ${contentType}, skipping...`);
2660
+ continue;
2661
+ }
2662
+ const singularName = schema.info.singularName;
2663
+ const pluralName = schema.info.pluralName;
2664
+ if (Array.isArray(data)) {
2665
+ console.log(` Seeding ${contentType} (${data.length} items)...`);
2666
+ for (const item of data) {
2667
+ totalCount++;
2668
+ const processedItem = processMediaFields(item, mediaMap);
2669
+ const success = await createEntry(
2670
+ pluralName,
2671
+ processedItem,
2672
+ strapiUrl,
2673
+ apiToken
2674
+ );
2675
+ if (success) successCount++;
2676
+ }
2677
+ } else {
2678
+ console.log(` Seeding ${contentType}...`);
2679
+ totalCount++;
2680
+ const processedData = processMediaFields(data, mediaMap);
2681
+ const success = await createOrUpdateSingleType(
2682
+ singularName,
2683
+ processedData,
2684
+ strapiUrl,
2685
+ apiToken
2686
+ );
2687
+ if (success) successCount++;
2688
+ }
2689
+ }
2690
+ console.log(` \u2713 Successfully seeded ${successCount}/${totalCount} entries`);
2691
+ }
2692
+ function processMediaFields(data, mediaMap) {
2693
+ const processed = {};
2694
+ for (const [key, value] of Object.entries(data)) {
2695
+ if (typeof value === "string") {
2696
+ if (key.includes("image") || key.includes("bg") || value.startsWith("/images/")) {
2697
+ const mediaId = mediaMap.get(value);
2698
+ if (mediaId) {
2699
+ processed[key] = mediaId;
2700
+ } else {
2701
+ processed[key] = value;
2702
+ }
2703
+ } else {
2704
+ processed[key] = value;
2705
+ }
2706
+ } else {
2707
+ processed[key] = value;
2708
+ }
2709
+ }
2710
+ return processed;
2711
+ }
2712
+ async function createEntry(contentType, data, strapiUrl, apiToken) {
2713
+ try {
2714
+ const response = await fetch(`${strapiUrl}/api/${contentType}`, {
2715
+ method: "POST",
2716
+ headers: {
2717
+ "Content-Type": "application/json",
2718
+ Authorization: `Bearer ${apiToken}`
2719
+ },
2720
+ body: JSON.stringify({ data })
2721
+ });
2722
+ if (!response.ok) {
2723
+ const errorText = await response.text();
2724
+ console.error(
2725
+ ` \u2717 Failed to create ${contentType}: ${response.status} - ${errorText}`
2726
+ );
2727
+ return false;
2728
+ }
2729
+ return true;
2730
+ } catch (error) {
2731
+ console.error(` \u2717 Error creating ${contentType}:`, error);
2732
+ return false;
2733
+ }
2734
+ }
2735
+ async function createOrUpdateSingleType(contentType, data, strapiUrl, apiToken) {
2736
+ try {
2737
+ const response = await fetch(`${strapiUrl}/api/${contentType}`, {
2738
+ method: "PUT",
2739
+ headers: {
2740
+ "Content-Type": "application/json",
2741
+ Authorization: `Bearer ${apiToken}`
2742
+ },
2743
+ body: JSON.stringify({ data })
2744
+ });
2745
+ if (!response.ok) {
2746
+ const errorText = await response.text();
2747
+ console.error(
2748
+ ` \u2717 Failed to update ${contentType}: ${response.status} - ${errorText}`
2749
+ );
2750
+ return false;
2751
+ }
2752
+ return true;
2753
+ } catch (error) {
2754
+ console.error(` \u2717 Error updating ${contentType}:`, error);
2755
+ return false;
2756
+ }
2757
+ }
2758
+ async function main() {
2759
+ const args = process.argv.slice(2);
2760
+ if (args.length < 2) {
2761
+ console.log(
2762
+ "Usage: tsx strapi-setup.ts <project-dir> <strapi-dir> [strapi-url] [api-token]"
2763
+ );
2764
+ console.log("");
2765
+ console.log("Example:");
2766
+ console.log(" tsx strapi-setup.ts ./nuxt-project ./strapi-dev");
2767
+ console.log(
2768
+ " tsx strapi-setup.ts ./nuxt-project ./strapi-dev http://localhost:1337 abc123"
2769
+ );
2770
+ process.exit(1);
2771
+ }
2772
+ const [projectDir, strapiDir, strapiUrl, apiToken] = args;
2773
+ await completeSetup({
2774
+ projectDir,
2775
+ strapiDir,
2776
+ strapiUrl,
2777
+ apiToken
2778
+ });
2779
+ }
2780
+ var isMainModule = process.argv[1] && process.argv[1].endsWith("strapi-setup.ts");
2781
+ if (isMainModule) {
2782
+ main().catch((error) => {
2783
+ console.error("\u274C Setup failed:", error.message);
2784
+ process.exit(1);
2785
+ });
2786
+ }
2787
+
2788
+ // src/cli.ts
2789
+ var program = new Command();
2790
+ async function prompt(question) {
2791
+ const rl = readline2.createInterface({
2792
+ input: process.stdin,
2793
+ output: process.stdout
2794
+ });
2795
+ return new Promise((resolve) => {
2796
+ rl.question(question, (answer) => {
2797
+ rl.close();
2798
+ resolve(answer.trim());
2799
+ });
2800
+ });
2801
+ }
2802
+ async function confirm(question) {
2803
+ const answer = await prompt(`${question} (y/n): `);
2804
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
2805
+ }
2806
+ program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.2");
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) => {
2815
+ try {
2816
+ await convertWebflowExport({
2817
+ inputDir: input,
2818
+ outputDir: output,
2819
+ boilerplate: options.boilerplate,
2820
+ overridesPath: options.overrides,
2821
+ generateStrapi: options.generateSchemas,
2822
+ cmsBackend: options.cms
2823
+ });
2824
+ if (options.interactive && options.cms === "strapi") {
2825
+ console.log("");
2826
+ const shouldSetup = await confirm(
2827
+ pc4.cyan("\u{1F3AF} Would you like to setup Strapi now?")
2828
+ );
2829
+ if (shouldSetup) {
2830
+ const strapiDir = await prompt(
2831
+ pc4.cyan(
2832
+ "\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): "
2833
+ )
2834
+ );
2835
+ if (strapiDir) {
2836
+ console.log("");
2837
+ console.log(pc4.cyan("\u{1F680} Starting Strapi setup..."));
2838
+ console.log("");
2839
+ try {
2840
+ await completeSetup({
2841
+ projectDir: output,
2842
+ strapiDir
2843
+ });
2844
+ } catch (error) {
2845
+ console.error(pc4.red("\n\u274C Strapi setup failed"));
2846
+ console.error(pc4.dim("You can run setup manually later with:"));
2847
+ console.error(
2848
+ pc4.dim(` cms setup-strapi ${output} ${strapiDir}`)
2849
+ );
2850
+ }
2851
+ }
2852
+ } else {
2853
+ console.log("");
2854
+ console.log(pc4.dim("\u{1F4A1} You can setup Strapi later with:"));
2855
+ console.log(
2856
+ pc4.dim(` cms setup-strapi ${output} <strapi-directory>`)
2857
+ );
2858
+ }
2859
+ }
2860
+ } catch (error) {
2861
+ console.error(pc4.red("Conversion failed"));
2862
+ process.exit(1);
2863
+ }
2864
+ });
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) => {
2866
+ try {
2867
+ await completeSetup({
2868
+ projectDir,
2869
+ strapiDir,
2870
+ strapiUrl: options.url,
2871
+ apiToken: options.token,
2872
+ ignoreSavedToken: options.newToken
2873
+ });
2874
+ } catch (error) {
2875
+ console.error(pc4.red("Strapi setup failed"));
2876
+ console.error(error);
2877
+ process.exit(1);
2878
+ }
2879
+ });
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
+ }
2917
+ });
2918
+ program.parse();
2919
+ //# sourceMappingURL=cli.mjs.map