cmx-sdk 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/cli.js +1507 -115
- package/dist/index.d.ts +138 -31
- package/dist/index.js +33 -11
- package/dist/index.js.map +1 -1
- package/dist/{interactive-menu-UQHS5FLW.js → interactive-menu-BDZOOGQH.js} +11 -8
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -384,10 +384,10 @@ async function confirm(message) {
|
|
|
384
384
|
input: process.stdin,
|
|
385
385
|
output: process.stdout
|
|
386
386
|
});
|
|
387
|
-
return new Promise((
|
|
387
|
+
return new Promise((resolve5) => {
|
|
388
388
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
389
389
|
rl.close();
|
|
390
|
-
|
|
390
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
391
391
|
});
|
|
392
392
|
});
|
|
393
393
|
}
|
|
@@ -429,10 +429,10 @@ async function confirm2(message) {
|
|
|
429
429
|
input: process.stdin,
|
|
430
430
|
output: process.stdout
|
|
431
431
|
});
|
|
432
|
-
return new Promise((
|
|
432
|
+
return new Promise((resolve5) => {
|
|
433
433
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
434
434
|
rl.close();
|
|
435
|
-
|
|
435
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
436
436
|
});
|
|
437
437
|
});
|
|
438
438
|
}
|
|
@@ -474,10 +474,10 @@ async function confirm3(message) {
|
|
|
474
474
|
input: process.stdin,
|
|
475
475
|
output: process.stdout
|
|
476
476
|
});
|
|
477
|
-
return new Promise((
|
|
477
|
+
return new Promise((resolve5) => {
|
|
478
478
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
479
479
|
rl.close();
|
|
480
|
-
|
|
480
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
483
|
}
|
|
@@ -516,78 +516,199 @@ async function deleteForm(options) {
|
|
|
516
516
|
// src/commands/sync-components.ts
|
|
517
517
|
import { readdirSync, readFileSync as readFileSync2, existsSync } from "fs";
|
|
518
518
|
import { join } from "path";
|
|
519
|
-
import {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
519
|
+
import { z as z2 } from "zod";
|
|
520
|
+
|
|
521
|
+
// src/mdx/component-catalog.ts
|
|
522
|
+
import { z } from "zod";
|
|
523
|
+
var componentSchemas = {
|
|
524
|
+
BlogCard: z.object({
|
|
525
|
+
contentId: z.string().uuid().describe("\u53C2\u7167\u5148\u30B3\u30F3\u30C6\u30F3\u30C4\u306EUUID")
|
|
526
|
+
}),
|
|
527
|
+
Image: z.object({
|
|
528
|
+
assetId: z.string().uuid().describe("\u30A2\u30BB\u30C3\u30C8\u306EUUID"),
|
|
529
|
+
alt: z.string().optional().describe("\u4EE3\u66FF\u30C6\u30AD\u30B9\u30C8"),
|
|
530
|
+
size: z.enum(["thumbnail", "medium", "large", "original"]).default("large").describe("\u8868\u793A\u30B5\u30A4\u30BA"),
|
|
531
|
+
caption: z.string().optional().describe("\u30AD\u30E3\u30D7\u30B7\u30E7\u30F3")
|
|
532
|
+
}),
|
|
533
|
+
Callout: z.object({
|
|
534
|
+
type: z.enum(["info", "warning", "error", "success", "tip"]).default("info").describe("\u30BF\u30A4\u30D7"),
|
|
535
|
+
title: z.string().optional().describe("\u30BF\u30A4\u30C8\u30EB"),
|
|
536
|
+
children: z.string().describe("\u672C\u6587\uFF08Markdown\u53EF\uFF09")
|
|
537
|
+
}),
|
|
538
|
+
Embed: z.object({
|
|
539
|
+
url: z.string().url().describe("\u57CB\u3081\u8FBC\u307FURL"),
|
|
540
|
+
type: z.enum(["youtube", "twitter", "generic"]).default("generic").describe("\u30BF\u30A4\u30D7")
|
|
541
|
+
}),
|
|
542
|
+
Button: z.object({
|
|
543
|
+
href: z.string().describe("\u30EA\u30F3\u30AF\u5148URL"),
|
|
544
|
+
children: z.string().describe("\u30DC\u30BF\u30F3\u30C6\u30AD\u30B9\u30C8"),
|
|
545
|
+
variant: z.enum(["primary", "secondary", "outline"]).default("primary").describe("\u30B9\u30BF\u30A4\u30EB")
|
|
546
|
+
})
|
|
547
|
+
};
|
|
548
|
+
var componentCatalog = {
|
|
549
|
+
BlogCard: {
|
|
550
|
+
name: "BlogCard",
|
|
551
|
+
displayName: "\u8A18\u4E8B\u30AB\u30FC\u30C9",
|
|
552
|
+
description: "\u4ED6\u306E\u8A18\u4E8B\u3078\u306E\u30EA\u30F3\u30AF\u30AB\u30FC\u30C9\u3092\u8868\u793A\u3057\u307E\u3059",
|
|
553
|
+
category: "reference",
|
|
554
|
+
schema: componentSchemas.BlogCard,
|
|
555
|
+
examples: ['<BlogCard contentId="123e4567-e89b-12d3-a456-426614174000" />'],
|
|
556
|
+
hasReferences: true,
|
|
557
|
+
source: "standard",
|
|
558
|
+
kind: "data-bound",
|
|
559
|
+
locked: true,
|
|
560
|
+
editable: false,
|
|
561
|
+
bindings: [{ prop: "contentId", target: "content", strategy: "by-id" }]
|
|
562
|
+
},
|
|
563
|
+
Image: {
|
|
564
|
+
name: "Image",
|
|
565
|
+
displayName: "\u753B\u50CF",
|
|
566
|
+
description: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u6E08\u307F\u753B\u50CF\u3092\u8868\u793A\u3057\u307E\u3059",
|
|
567
|
+
category: "media",
|
|
568
|
+
schema: componentSchemas.Image,
|
|
569
|
+
examples: [
|
|
570
|
+
'<Image assetId="123e4567-e89b-12d3-a456-426614174000" alt="\u8AAC\u660E" />',
|
|
571
|
+
'<Image assetId="123e4567-e89b-12d3-a456-426614174000" size="medium" caption="\u30AD\u30E3\u30D7\u30B7\u30E7\u30F3" />'
|
|
572
|
+
],
|
|
573
|
+
hasReferences: true,
|
|
574
|
+
source: "standard",
|
|
575
|
+
kind: "data-bound",
|
|
576
|
+
locked: true,
|
|
577
|
+
editable: false,
|
|
578
|
+
bindings: [{ prop: "assetId", target: "asset", strategy: "by-id" }]
|
|
579
|
+
},
|
|
580
|
+
Callout: {
|
|
581
|
+
name: "Callout",
|
|
582
|
+
displayName: "\u30B3\u30FC\u30EB\u30A2\u30A6\u30C8",
|
|
583
|
+
description: "\u6CE8\u610F\u66F8\u304D\u3084\u88DC\u8DB3\u60C5\u5831\u3092\u76EE\u7ACB\u305F\u305B\u308B\u30DC\u30C3\u30AF\u30B9",
|
|
584
|
+
category: "content",
|
|
585
|
+
schema: componentSchemas.Callout,
|
|
586
|
+
examples: [
|
|
587
|
+
'<Callout type="info">\u3053\u308C\u306F\u60C5\u5831\u3067\u3059</Callout>',
|
|
588
|
+
'<Callout type="warning" title="\u6CE8\u610F">\u91CD\u8981\u306A\u6CE8\u610F\u4E8B\u9805\u3067\u3059</Callout>'
|
|
589
|
+
],
|
|
590
|
+
hasReferences: false,
|
|
591
|
+
source: "standard",
|
|
592
|
+
kind: "presentational",
|
|
593
|
+
locked: true,
|
|
594
|
+
editable: false
|
|
595
|
+
},
|
|
596
|
+
Embed: {
|
|
597
|
+
name: "Embed",
|
|
598
|
+
displayName: "\u57CB\u3081\u8FBC\u307F",
|
|
599
|
+
description: "YouTube\u52D5\u753B\u3084\u30C4\u30A4\u30FC\u30C8\u3092\u57CB\u3081\u8FBC\u307F\u307E\u3059",
|
|
600
|
+
category: "media",
|
|
601
|
+
schema: componentSchemas.Embed,
|
|
602
|
+
examples: [
|
|
603
|
+
'<Embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ" type="youtube" />',
|
|
604
|
+
'<Embed url="https://twitter.com/example/status/123456789" type="twitter" />'
|
|
605
|
+
],
|
|
606
|
+
hasReferences: false,
|
|
607
|
+
source: "standard",
|
|
608
|
+
kind: "presentational",
|
|
609
|
+
locked: true,
|
|
610
|
+
editable: false
|
|
611
|
+
},
|
|
612
|
+
Button: {
|
|
613
|
+
name: "Button",
|
|
614
|
+
displayName: "\u30DC\u30BF\u30F3",
|
|
615
|
+
description: "\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u4FC3\u3059\u30DC\u30BF\u30F3\u30EA\u30F3\u30AF",
|
|
616
|
+
category: "content",
|
|
617
|
+
schema: componentSchemas.Button,
|
|
618
|
+
examples: [
|
|
619
|
+
'<Button href="/contact">\u304A\u554F\u3044\u5408\u308F\u305B</Button>',
|
|
620
|
+
'<Button href="https://example.com" variant="secondary">\u8A73\u7D30\u3092\u898B\u308B</Button>'
|
|
621
|
+
],
|
|
622
|
+
hasReferences: false,
|
|
623
|
+
source: "standard",
|
|
624
|
+
kind: "presentational",
|
|
625
|
+
locked: true,
|
|
626
|
+
editable: false
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
function isValidComponent(name) {
|
|
630
|
+
return name in componentCatalog;
|
|
631
|
+
}
|
|
632
|
+
function validateComponentProps(name, props) {
|
|
633
|
+
const schema = componentSchemas[name];
|
|
634
|
+
const result = schema.safeParse(props);
|
|
635
|
+
if (result.success) {
|
|
636
|
+
return { success: true, data: result.data };
|
|
637
|
+
}
|
|
638
|
+
return { success: false, error: result.error };
|
|
639
|
+
}
|
|
640
|
+
function isStandardComponentName(name) {
|
|
641
|
+
return isValidComponent(name);
|
|
540
642
|
}
|
|
541
|
-
|
|
643
|
+
|
|
644
|
+
// src/commands/sync-components.ts
|
|
645
|
+
var ComponentPropSchema = z2.object({
|
|
646
|
+
type: z2.enum(["string", "number", "boolean", "enum", "asset", "content"]),
|
|
647
|
+
description: z2.string().optional(),
|
|
648
|
+
required: z2.boolean().optional().default(false),
|
|
649
|
+
values: z2.array(z2.string()).optional(),
|
|
650
|
+
default: z2.union([z2.string(), z2.number(), z2.boolean()]).optional()
|
|
651
|
+
});
|
|
652
|
+
var ComponentDefinitionSchema = z2.object({
|
|
653
|
+
name: z2.string().min(1).max(100).regex(/^[A-Z][a-zA-Z0-9]*$/),
|
|
654
|
+
displayName: z2.string().min(1).max(255),
|
|
655
|
+
description: z2.string().optional(),
|
|
656
|
+
propsSchema: z2.record(ComponentPropSchema).default({}),
|
|
657
|
+
examples: z2.array(z2.string()).default([])
|
|
658
|
+
});
|
|
659
|
+
function readComponentDefinitions(componentsDir) {
|
|
542
660
|
if (!existsSync(componentsDir)) {
|
|
543
661
|
console.log("No components directory found at cmx/components/");
|
|
544
|
-
return [];
|
|
662
|
+
return { exists: false, components: [] };
|
|
545
663
|
}
|
|
546
664
|
const files = readdirSync(componentsDir).filter((f) => f.endsWith(".json"));
|
|
547
|
-
if (files.length === 0) {
|
|
548
|
-
console.log("No component definition files found");
|
|
549
|
-
return [];
|
|
550
|
-
}
|
|
551
665
|
const components = [];
|
|
552
666
|
for (const file of files) {
|
|
553
667
|
try {
|
|
554
668
|
const filePath = join(componentsDir, file);
|
|
555
669
|
const content = readFileSync2(filePath, "utf-8");
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
670
|
+
const parsed = JSON.parse(content);
|
|
671
|
+
const result = ComponentDefinitionSchema.safeParse(parsed);
|
|
672
|
+
if (!result.success) {
|
|
673
|
+
console.error(` Invalid format in ${file}`);
|
|
674
|
+
for (const issue of result.error.issues) {
|
|
675
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
676
|
+
console.error(` - ${path}: ${issue.message}`);
|
|
677
|
+
}
|
|
678
|
+
process.exit(1);
|
|
559
679
|
}
|
|
560
|
-
if (
|
|
561
|
-
console.
|
|
562
|
-
|
|
680
|
+
if (isValidComponent(result.data.name)) {
|
|
681
|
+
console.error(` ${file}: '${result.data.name}' \u306F\u6A19\u6E96\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u540D\u306E\u305F\u3081\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`);
|
|
682
|
+
process.exit(1);
|
|
563
683
|
}
|
|
564
|
-
|
|
565
|
-
component.examples = component.examples || [];
|
|
566
|
-
components.push(component);
|
|
684
|
+
components.push(result.data);
|
|
567
685
|
} catch (error) {
|
|
568
686
|
console.error(` Error reading ${file}:`, error instanceof Error ? error.message : error);
|
|
687
|
+
process.exit(1);
|
|
569
688
|
}
|
|
570
689
|
}
|
|
571
|
-
return components;
|
|
690
|
+
return { exists: true, components };
|
|
572
691
|
}
|
|
573
|
-
async function syncComponents(
|
|
692
|
+
async function syncComponents() {
|
|
574
693
|
const apiUrl = process.env.CMX_API_URL;
|
|
575
694
|
const apiKey = process.env.CMX_API_KEY;
|
|
576
695
|
if (!apiUrl || !apiKey) {
|
|
577
696
|
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
578
697
|
process.exit(1);
|
|
579
698
|
}
|
|
580
|
-
const environment = options.environment || detectEnvironment();
|
|
581
699
|
const componentsDir = join(process.cwd(), "cmx/components");
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (components.length === 0) {
|
|
700
|
+
const { exists, components } = readComponentDefinitions(componentsDir);
|
|
701
|
+
if (!exists) {
|
|
585
702
|
console.log("No components to sync");
|
|
586
703
|
return;
|
|
587
704
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
705
|
+
if (components.length === 0) {
|
|
706
|
+
console.log("No component definition files found, syncing empty list (will delete existing custom components)");
|
|
707
|
+
} else {
|
|
708
|
+
console.log(`Found ${components.length} components:`);
|
|
709
|
+
for (const c of components) {
|
|
710
|
+
console.log(` - ${c.name} (${c.displayName})`);
|
|
711
|
+
}
|
|
591
712
|
}
|
|
592
713
|
console.log(`Syncing to ${apiUrl}...`);
|
|
593
714
|
const result = await manageApiFetch(
|
|
@@ -600,6 +721,7 @@ async function syncComponents(options) {
|
|
|
600
721
|
console.log(`Success: ${result.message || "Components synced"}`);
|
|
601
722
|
if (result.created) console.log(` Created: ${result.created}`);
|
|
602
723
|
if (result.updated) console.log(` Updated: ${result.updated}`);
|
|
724
|
+
if (result.deleted) console.log(` Deleted: ${result.deleted}`);
|
|
603
725
|
}
|
|
604
726
|
|
|
605
727
|
// src/codegen/generator.ts
|
|
@@ -722,7 +844,7 @@ function generateDataTypeFile(dataType) {
|
|
|
722
844
|
const interfaceName = singularize(pascalPlural);
|
|
723
845
|
const slug = dataType.slug;
|
|
724
846
|
const lines = [];
|
|
725
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
847
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
726
848
|
lines.push(`// Data Type: ${dataType.name} (${slug})`);
|
|
727
849
|
lines.push(`// Do not edit manually.`);
|
|
728
850
|
lines.push(``);
|
|
@@ -777,7 +899,7 @@ function generateCollectionFile(collection) {
|
|
|
777
899
|
const slug = collection.slug;
|
|
778
900
|
const lines = [];
|
|
779
901
|
const escapedName = escapeTs(collection.name);
|
|
780
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
902
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
781
903
|
lines.push(`// Collection: ${escapedName} (${slug})`);
|
|
782
904
|
lines.push(`// Do not edit manually.`);
|
|
783
905
|
lines.push(``);
|
|
@@ -863,7 +985,7 @@ function generateFormFile(form) {
|
|
|
863
985
|
const slug = form.slug;
|
|
864
986
|
const lines = [];
|
|
865
987
|
const escapedName = escapeTs(form.name);
|
|
866
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
988
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
867
989
|
lines.push(`// Form: ${escapedName} (${slug})`);
|
|
868
990
|
lines.push(`// Do not edit manually.`);
|
|
869
991
|
lines.push(``);
|
|
@@ -895,7 +1017,7 @@ function generateFormFile(form) {
|
|
|
895
1017
|
// src/codegen/generate-index.ts
|
|
896
1018
|
function generateDataTypesIndex(slugs) {
|
|
897
1019
|
const lines = [
|
|
898
|
-
`// Auto-generated by cmx-sdk
|
|
1020
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
899
1021
|
`// Do not edit manually.`,
|
|
900
1022
|
``
|
|
901
1023
|
];
|
|
@@ -907,7 +1029,7 @@ function generateDataTypesIndex(slugs) {
|
|
|
907
1029
|
}
|
|
908
1030
|
function generateCollectionsIndex(slugs) {
|
|
909
1031
|
const lines = [
|
|
910
|
-
`// Auto-generated by cmx-sdk
|
|
1032
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
911
1033
|
`// Do not edit manually.`,
|
|
912
1034
|
``
|
|
913
1035
|
];
|
|
@@ -919,7 +1041,7 @@ function generateCollectionsIndex(slugs) {
|
|
|
919
1041
|
}
|
|
920
1042
|
function generateFormsIndex(slugs) {
|
|
921
1043
|
const lines = [
|
|
922
|
-
`// Auto-generated by cmx-sdk
|
|
1044
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
923
1045
|
`// Do not edit manually.`,
|
|
924
1046
|
``
|
|
925
1047
|
];
|
|
@@ -931,7 +1053,7 @@ function generateFormsIndex(slugs) {
|
|
|
931
1053
|
}
|
|
932
1054
|
function generateRootIndex(hasDataTypes, hasCollections, hasForms) {
|
|
933
1055
|
const lines = [
|
|
934
|
-
`// Auto-generated by cmx-sdk
|
|
1056
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
935
1057
|
`// Do not edit manually.`,
|
|
936
1058
|
``
|
|
937
1059
|
];
|
|
@@ -1256,7 +1378,7 @@ async function linkCollectionDataType(opts) {
|
|
|
1256
1378
|
// src/codegen/scaffolder.ts
|
|
1257
1379
|
import { existsSync as existsSync2 } from "fs";
|
|
1258
1380
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1259
|
-
import { join as join3,
|
|
1381
|
+
import { dirname, join as join3, resolve } from "path";
|
|
1260
1382
|
|
|
1261
1383
|
// src/codegen/scaffold-collection.ts
|
|
1262
1384
|
function scaffoldCollectionListPage(collection) {
|
|
@@ -1264,7 +1386,7 @@ function scaffoldCollectionListPage(collection) {
|
|
|
1264
1386
|
const pascal = slugToPascalCase(safeSlug);
|
|
1265
1387
|
const escapedName = escapeTs(collection.name);
|
|
1266
1388
|
const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
|
|
1267
|
-
return `import { get${pascal}Contents } from "
|
|
1389
|
+
return `import { get${pascal}Contents } from "@cmx/generated"
|
|
1268
1390
|
import Link from "next/link"
|
|
1269
1391
|
import type { Metadata } from "next"
|
|
1270
1392
|
|
|
@@ -1297,7 +1419,7 @@ export default async function ${pascal}Page() {
|
|
|
1297
1419
|
function scaffoldCollectionDetailPage(collection) {
|
|
1298
1420
|
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1299
1421
|
const pascal = slugToPascalCase(safeSlug);
|
|
1300
|
-
return `import { get${pascal}Contents, get${pascal}ContentDetail } from "
|
|
1422
|
+
return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
|
|
1301
1423
|
import { renderMdx } from "cmx-sdk"
|
|
1302
1424
|
import type { Metadata } from "next"
|
|
1303
1425
|
|
|
@@ -1343,8 +1465,8 @@ function scaffoldDataTypePage(dataType) {
|
|
|
1343
1465
|
const interfaceName = singularize(pascalPlural);
|
|
1344
1466
|
const slug = dataType.slug;
|
|
1345
1467
|
const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
|
|
1346
|
-
return `import { get${pascalPlural} } from "
|
|
1347
|
-
import type { ${interfaceName} } from "
|
|
1468
|
+
return `import { get${pascalPlural} } from "@cmx/generated"
|
|
1469
|
+
import type { ${interfaceName} } from "@cmx/generated"
|
|
1348
1470
|
import type { Metadata } from "next"
|
|
1349
1471
|
|
|
1350
1472
|
export const metadata: Metadata = {
|
|
@@ -1380,7 +1502,7 @@ function scaffoldFormPage(form) {
|
|
|
1380
1502
|
const pascal = slugToPascalCase(safeSlug);
|
|
1381
1503
|
const camel = slugToCamelCase(safeSlug);
|
|
1382
1504
|
return `import { ${pascal}Form } from "./_components/${safeSlug}-form"
|
|
1383
|
-
import { ${camel}Schema, submit${pascal} } from "
|
|
1505
|
+
import { ${camel}Schema, submit${pascal} } from "@cmx/generated"
|
|
1384
1506
|
import type { Metadata } from "next"
|
|
1385
1507
|
|
|
1386
1508
|
export const metadata: Metadata = {
|
|
@@ -1427,7 +1549,7 @@ function scaffoldFormComponent(form) {
|
|
|
1427
1549
|
|
|
1428
1550
|
import { useForm } from "react-hook-form"
|
|
1429
1551
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
1430
|
-
import { ${camel}Schema, type ${pascal}FormData } from "
|
|
1552
|
+
import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
|
|
1431
1553
|
import { useState } from "react"
|
|
1432
1554
|
|
|
1433
1555
|
type Props = {
|
|
@@ -1500,12 +1622,393 @@ function getInputType(fieldType) {
|
|
|
1500
1622
|
}
|
|
1501
1623
|
}
|
|
1502
1624
|
|
|
1625
|
+
// src/codegen/scaffold-layered.ts
|
|
1626
|
+
function scaffoldLayeredCollectionListRoute(collection, featureImportBase = "@/features") {
|
|
1627
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1628
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1629
|
+
return `import { ${pascal}ListView } from "${featureImportBase}/collections/${safeSlug}/list/view"
|
|
1630
|
+
import { get${pascal}ListData } from "${featureImportBase}/collections/${safeSlug}/list/resolver"
|
|
1631
|
+
export { metadata } from "${featureImportBase}/collections/${safeSlug}/list/meta"
|
|
1632
|
+
|
|
1633
|
+
export default async function ${pascal}Page() {
|
|
1634
|
+
const data = await get${pascal}ListData()
|
|
1635
|
+
return <${pascal}ListView data={data} />
|
|
1636
|
+
}
|
|
1637
|
+
`;
|
|
1638
|
+
}
|
|
1639
|
+
function scaffoldLayeredCollectionListResolver(collection) {
|
|
1640
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1641
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1642
|
+
return `import { get${pascal}Contents } from "@cmx/generated"
|
|
1643
|
+
|
|
1644
|
+
export async function get${pascal}ListData() {
|
|
1645
|
+
return get${pascal}Contents()
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export type ${pascal}ListData = Awaited<ReturnType<typeof get${pascal}ListData>>
|
|
1649
|
+
`;
|
|
1650
|
+
}
|
|
1651
|
+
function scaffoldLayeredCollectionListMeta(collection) {
|
|
1652
|
+
const escapedName = escapeTs(collection.name);
|
|
1653
|
+
const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
|
|
1654
|
+
return `import type { Metadata } from "next"
|
|
1655
|
+
|
|
1656
|
+
export const metadata: Metadata = {
|
|
1657
|
+
title: "${escapedName}",
|
|
1658
|
+
description: "${escapedDescription}",
|
|
1659
|
+
}
|
|
1660
|
+
`;
|
|
1661
|
+
}
|
|
1662
|
+
function scaffoldLayeredCollectionListView(collection) {
|
|
1663
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1664
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1665
|
+
const escapedName = escapeTs(collection.name);
|
|
1666
|
+
return `import Link from "next/link"
|
|
1667
|
+
import type { ${pascal}ListData } from "./resolver"
|
|
1668
|
+
|
|
1669
|
+
type Props = {
|
|
1670
|
+
data: ${pascal}ListData
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
export function ${pascal}ListView({ data }: Props) {
|
|
1674
|
+
const { contents } = data
|
|
1675
|
+
|
|
1676
|
+
return (
|
|
1677
|
+
<main>
|
|
1678
|
+
<h1>${escapedName}</h1>
|
|
1679
|
+
{contents.length === 0 ? (
|
|
1680
|
+
<p>No content yet.</p>
|
|
1681
|
+
) : (
|
|
1682
|
+
<ul>
|
|
1683
|
+
{contents.map((item) => (
|
|
1684
|
+
<li key={item.id}>
|
|
1685
|
+
<Link href="/${safeSlug}/\${item.slug}">
|
|
1686
|
+
<h2>{item.title}</h2>
|
|
1687
|
+
{item.description && <p>{item.description}</p>}
|
|
1688
|
+
</Link>
|
|
1689
|
+
</li>
|
|
1690
|
+
))}
|
|
1691
|
+
</ul>
|
|
1692
|
+
)}
|
|
1693
|
+
</main>
|
|
1694
|
+
)
|
|
1695
|
+
}
|
|
1696
|
+
`;
|
|
1697
|
+
}
|
|
1698
|
+
function scaffoldLayeredCollectionDetailRoute(collection, featureImportBase = "@/features") {
|
|
1699
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1700
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1701
|
+
return `import type { Metadata } from "next"
|
|
1702
|
+
import {
|
|
1703
|
+
get${pascal}DetailData,
|
|
1704
|
+
get${pascal}DetailParams,
|
|
1705
|
+
} from "${featureImportBase}/collections/${safeSlug}/detail/resolver"
|
|
1706
|
+
import { get${pascal}DetailMetadata } from "${featureImportBase}/collections/${safeSlug}/detail/meta"
|
|
1707
|
+
import { ${pascal}DetailView } from "${featureImportBase}/collections/${safeSlug}/detail/view"
|
|
1708
|
+
|
|
1709
|
+
type Props = {
|
|
1710
|
+
params: Promise<{ slug: string }>
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
export async function generateStaticParams() {
|
|
1714
|
+
return get${pascal}DetailParams()
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
1718
|
+
const { slug } = await params
|
|
1719
|
+
return get${pascal}DetailMetadata(slug)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
export default async function ${pascal}DetailPage({ params }: Props) {
|
|
1723
|
+
const { slug } = await params
|
|
1724
|
+
const data = await get${pascal}DetailData(slug)
|
|
1725
|
+
|
|
1726
|
+
return <${pascal}DetailView data={data} />
|
|
1727
|
+
}
|
|
1728
|
+
`;
|
|
1729
|
+
}
|
|
1730
|
+
function scaffoldLayeredCollectionDetailResolver(collection) {
|
|
1731
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1732
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1733
|
+
return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
|
|
1734
|
+
import { renderMdx } from "cmx-sdk"
|
|
1735
|
+
|
|
1736
|
+
export async function get${pascal}DetailParams() {
|
|
1737
|
+
const { contents } = await get${pascal}Contents()
|
|
1738
|
+
return contents.map((item) => ({ slug: item.slug }))
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
export async function get${pascal}DetailData(slug: string) {
|
|
1742
|
+
const { content, references } = await get${pascal}ContentDetail(slug)
|
|
1743
|
+
const { content: rendered } = await renderMdx(content.mdx, references)
|
|
1744
|
+
|
|
1745
|
+
return {
|
|
1746
|
+
content,
|
|
1747
|
+
rendered,
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export type ${pascal}DetailData = Awaited<ReturnType<typeof get${pascal}DetailData>>
|
|
1752
|
+
`;
|
|
1753
|
+
}
|
|
1754
|
+
function scaffoldLayeredCollectionDetailMeta(collection) {
|
|
1755
|
+
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1756
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1757
|
+
return `import type { Metadata } from "next"
|
|
1758
|
+
import { get${pascal}ContentDetail } from "@cmx/generated"
|
|
1759
|
+
|
|
1760
|
+
export async function get${pascal}DetailMetadata(slug: string): Promise<Metadata> {
|
|
1761
|
+
const { content } = await get${pascal}ContentDetail(slug)
|
|
1762
|
+
|
|
1763
|
+
return {
|
|
1764
|
+
title: content.title,
|
|
1765
|
+
description: content.description ?? undefined,
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
`;
|
|
1769
|
+
}
|
|
1770
|
+
function scaffoldLayeredCollectionDetailView(collection) {
|
|
1771
|
+
const pascal = slugToPascalCase(sanitizeIdentifier(collection.slug));
|
|
1772
|
+
return `import type { ${pascal}DetailData } from "./resolver"
|
|
1773
|
+
|
|
1774
|
+
type Props = {
|
|
1775
|
+
data: ${pascal}DetailData
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
export function ${pascal}DetailView({ data }: Props) {
|
|
1779
|
+
const { content, rendered } = data
|
|
1780
|
+
|
|
1781
|
+
return (
|
|
1782
|
+
<main>
|
|
1783
|
+
<article>
|
|
1784
|
+
<h1>{content.title}</h1>
|
|
1785
|
+
{rendered}
|
|
1786
|
+
</article>
|
|
1787
|
+
</main>
|
|
1788
|
+
)
|
|
1789
|
+
}
|
|
1790
|
+
`;
|
|
1791
|
+
}
|
|
1792
|
+
function scaffoldLayeredDataTypeRoute(dataType, featureImportBase = "@/features") {
|
|
1793
|
+
const safeSlug = sanitizeIdentifier(dataType.slug);
|
|
1794
|
+
const pascalPlural = slugToPascalCase(safeSlug);
|
|
1795
|
+
return `import { ${pascalPlural}ListView } from "${featureImportBase}/data-types/${safeSlug}/list/view"
|
|
1796
|
+
import { get${pascalPlural}ListData } from "${featureImportBase}/data-types/${safeSlug}/list/resolver"
|
|
1797
|
+
export { metadata } from "${featureImportBase}/data-types/${safeSlug}/list/meta"
|
|
1798
|
+
|
|
1799
|
+
export default async function ${pascalPlural}Page() {
|
|
1800
|
+
const data = await get${pascalPlural}ListData()
|
|
1801
|
+
return <${pascalPlural}ListView data={data} />
|
|
1802
|
+
}
|
|
1803
|
+
`;
|
|
1804
|
+
}
|
|
1805
|
+
function scaffoldLayeredDataTypeResolver(dataType) {
|
|
1806
|
+
const safeSlug = sanitizeIdentifier(dataType.slug);
|
|
1807
|
+
const pascalPlural = slugToPascalCase(safeSlug);
|
|
1808
|
+
return `import { get${pascalPlural} } from "@cmx/generated"
|
|
1809
|
+
|
|
1810
|
+
export async function get${pascalPlural}ListData() {
|
|
1811
|
+
return get${pascalPlural}()
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
export type ${pascalPlural}ListData = Awaited<ReturnType<typeof get${pascalPlural}ListData>>
|
|
1815
|
+
`;
|
|
1816
|
+
}
|
|
1817
|
+
function scaffoldLayeredDataTypeMeta(dataType) {
|
|
1818
|
+
return `import type { Metadata } from "next"
|
|
1819
|
+
|
|
1820
|
+
export const metadata: Metadata = {
|
|
1821
|
+
title: "${escapeTs(dataType.name)}",
|
|
1822
|
+
}
|
|
1823
|
+
`;
|
|
1824
|
+
}
|
|
1825
|
+
function scaffoldLayeredDataTypeView(dataType) {
|
|
1826
|
+
const safeSlug = sanitizeIdentifier(dataType.slug);
|
|
1827
|
+
const pascalPlural = slugToPascalCase(safeSlug);
|
|
1828
|
+
const interfaceName = singularize(pascalPlural);
|
|
1829
|
+
const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
|
|
1830
|
+
return `import type { ${pascalPlural}ListData } from "./resolver"
|
|
1831
|
+
import type { ${interfaceName} } from "@cmx/generated"
|
|
1832
|
+
|
|
1833
|
+
type Props = {
|
|
1834
|
+
data: ${pascalPlural}ListData
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Available fields:
|
|
1838
|
+
${fieldComments}
|
|
1839
|
+
|
|
1840
|
+
export function ${pascalPlural}ListView({ data }: Props) {
|
|
1841
|
+
const { items } = data
|
|
1842
|
+
|
|
1843
|
+
return (
|
|
1844
|
+
<main>
|
|
1845
|
+
<h1>${escapeTs(dataType.name)}</h1>
|
|
1846
|
+
<ul>
|
|
1847
|
+
{items.map((item: ${interfaceName}, index: number) => (
|
|
1848
|
+
<li key={index}>
|
|
1849
|
+
<pre>{JSON.stringify(item, null, 2)}</pre>
|
|
1850
|
+
</li>
|
|
1851
|
+
))}
|
|
1852
|
+
</ul>
|
|
1853
|
+
</main>
|
|
1854
|
+
)
|
|
1855
|
+
}
|
|
1856
|
+
`;
|
|
1857
|
+
}
|
|
1858
|
+
function scaffoldLayeredFormRoute(form, featureImportBase = "@/features") {
|
|
1859
|
+
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1860
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1861
|
+
return `import { ${pascal}PageView } from "${featureImportBase}/forms/${safeSlug}/view"
|
|
1862
|
+
import { submit${pascal}FormAction } from "${featureImportBase}/forms/${safeSlug}/resolver"
|
|
1863
|
+
export { metadata } from "${featureImportBase}/forms/${safeSlug}/meta"
|
|
1864
|
+
|
|
1865
|
+
export default function ${pascal}Page() {
|
|
1866
|
+
return <${pascal}PageView action={submit${pascal}FormAction} />
|
|
1867
|
+
}
|
|
1868
|
+
`;
|
|
1869
|
+
}
|
|
1870
|
+
function scaffoldLayeredFormResolver(form) {
|
|
1871
|
+
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1872
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1873
|
+
const camel = slugToCamelCase(safeSlug);
|
|
1874
|
+
return `import { ${camel}Schema, submit${pascal}, type ${pascal}FormData } from "@cmx/generated"
|
|
1875
|
+
|
|
1876
|
+
export async function submit${pascal}FormAction(data: Record<string, unknown>) {
|
|
1877
|
+
"use server"
|
|
1878
|
+
const validated = ${camel}Schema.parse(data)
|
|
1879
|
+
return submit${pascal}(validated as ${pascal}FormData)
|
|
1880
|
+
}
|
|
1881
|
+
`;
|
|
1882
|
+
}
|
|
1883
|
+
function scaffoldLayeredFormMeta(form) {
|
|
1884
|
+
return `import type { Metadata } from "next"
|
|
1885
|
+
|
|
1886
|
+
export const metadata: Metadata = {
|
|
1887
|
+
title: "${escapeTs(form.name)}",
|
|
1888
|
+
}
|
|
1889
|
+
`;
|
|
1890
|
+
}
|
|
1891
|
+
function scaffoldLayeredFormView(form) {
|
|
1892
|
+
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1893
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1894
|
+
return `import { ${pascal}FormView } from "./form-view"
|
|
1895
|
+
|
|
1896
|
+
type Props = {
|
|
1897
|
+
action: (data: Record<string, unknown>) => Promise<{ success: true; id: string }>
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
export function ${pascal}PageView({ action }: Props) {
|
|
1901
|
+
return (
|
|
1902
|
+
<main>
|
|
1903
|
+
<h1>${escapeTs(form.name)}</h1>
|
|
1904
|
+
<${pascal}FormView action={action} />
|
|
1905
|
+
</main>
|
|
1906
|
+
)
|
|
1907
|
+
}
|
|
1908
|
+
`;
|
|
1909
|
+
}
|
|
1910
|
+
function scaffoldLayeredFormClientView(form) {
|
|
1911
|
+
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1912
|
+
const pascal = slugToPascalCase(safeSlug);
|
|
1913
|
+
const camel = slugToCamelCase(safeSlug);
|
|
1914
|
+
const inputFields = form.fields.map((field) => {
|
|
1915
|
+
const inputType = getInputType2(field.type);
|
|
1916
|
+
const required = field.required ? " required" : "";
|
|
1917
|
+
const escapedLabel = escapeTs(field.label);
|
|
1918
|
+
return ` <div>
|
|
1919
|
+
<label htmlFor="${field.key}">${escapedLabel}</label>
|
|
1920
|
+
<input
|
|
1921
|
+
id="${field.key}"
|
|
1922
|
+
type="${inputType}"
|
|
1923
|
+
{...register("${field.key}")}${required}
|
|
1924
|
+
/>
|
|
1925
|
+
{errors.${field.key} && (
|
|
1926
|
+
<p role="alert">{errors.${field.key}?.message}</p>
|
|
1927
|
+
)}
|
|
1928
|
+
</div>`;
|
|
1929
|
+
}).join("\n");
|
|
1930
|
+
return `"use client"
|
|
1931
|
+
|
|
1932
|
+
import { useState } from "react"
|
|
1933
|
+
import { useForm } from "react-hook-form"
|
|
1934
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
1935
|
+
import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
|
|
1936
|
+
|
|
1937
|
+
type Props = {
|
|
1938
|
+
action: (data: Record<string, unknown>) => Promise<{ success: true; id: string }>
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
export function ${pascal}FormView({ action }: Props) {
|
|
1942
|
+
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle")
|
|
1943
|
+
|
|
1944
|
+
const {
|
|
1945
|
+
register,
|
|
1946
|
+
handleSubmit,
|
|
1947
|
+
reset,
|
|
1948
|
+
formState: { errors },
|
|
1949
|
+
} = useForm<${pascal}FormData>({
|
|
1950
|
+
resolver: zodResolver(${camel}Schema),
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
const onSubmit = async (data: ${pascal}FormData) => {
|
|
1954
|
+
setStatus("submitting")
|
|
1955
|
+
try {
|
|
1956
|
+
await action(data)
|
|
1957
|
+
setStatus("success")
|
|
1958
|
+
reset()
|
|
1959
|
+
} catch {
|
|
1960
|
+
setStatus("error")
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (status === "success") {
|
|
1965
|
+
return <p>\u9001\u4FE1\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002\u3042\u308A\u304C\u3068\u3046\u3054\u3056\u3044\u307E\u3059\u3002</p>
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
return (
|
|
1969
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
1970
|
+
<div style={{ position: "absolute", left: "-9999px" }} aria-hidden="true">
|
|
1971
|
+
<input type="text" name="_hp" tabIndex={-1} autoComplete="off" />
|
|
1972
|
+
</div>
|
|
1973
|
+
|
|
1974
|
+
${inputFields}
|
|
1975
|
+
|
|
1976
|
+
{status === "error" && (
|
|
1977
|
+
<p role="alert">\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002</p>
|
|
1978
|
+
)}
|
|
1979
|
+
|
|
1980
|
+
<button type="submit" disabled={status === "submitting"}>
|
|
1981
|
+
{status === "submitting" ? "\u9001\u4FE1\u4E2D..." : "\u9001\u4FE1"}
|
|
1982
|
+
</button>
|
|
1983
|
+
</form>
|
|
1984
|
+
)
|
|
1985
|
+
}
|
|
1986
|
+
`;
|
|
1987
|
+
}
|
|
1988
|
+
function getInputType2(fieldType) {
|
|
1989
|
+
switch (fieldType) {
|
|
1990
|
+
case "email":
|
|
1991
|
+
return "email";
|
|
1992
|
+
case "url":
|
|
1993
|
+
return "url";
|
|
1994
|
+
case "number":
|
|
1995
|
+
return "number";
|
|
1996
|
+
case "boolean":
|
|
1997
|
+
return "checkbox";
|
|
1998
|
+
case "textarea":
|
|
1999
|
+
case "richtext":
|
|
2000
|
+
return "text";
|
|
2001
|
+
default:
|
|
2002
|
+
return "text";
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1503
2006
|
// src/codegen/scaffold-seo.ts
|
|
1504
2007
|
function scaffoldSitemap(collections, siteUrl) {
|
|
1505
2008
|
const imports = collections.map((col) => {
|
|
1506
2009
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
1507
2010
|
const pascal = slugToPascalCase(safeSlug);
|
|
1508
|
-
return `import { get${pascal}Contents } from "
|
|
2011
|
+
return `import { get${pascal}Contents } from "@cmx/generated"`;
|
|
1509
2012
|
}).join("\n");
|
|
1510
2013
|
const fetchBlocks = collections.map((col) => {
|
|
1511
2014
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
@@ -1538,9 +2041,11 @@ ${fetchBlocks}
|
|
|
1538
2041
|
// src/codegen/scaffolder.ts
|
|
1539
2042
|
async function writeScaffoldFile(filePath, content, result, options) {
|
|
1540
2043
|
const resolvedPath = resolve(filePath);
|
|
1541
|
-
const
|
|
1542
|
-
if (!resolvedPath.startsWith(
|
|
1543
|
-
throw new Error(
|
|
2044
|
+
const resolvedBaseDir = resolve(options.baseDir);
|
|
2045
|
+
if (!resolvedPath.startsWith(resolvedBaseDir + "/") && resolvedPath !== resolvedBaseDir) {
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
`Path traversal detected: "${filePath}" resolves outside of base dir "${options.baseDir}"`
|
|
2048
|
+
);
|
|
1544
2049
|
}
|
|
1545
2050
|
if (!options.force && existsSync2(filePath)) {
|
|
1546
2051
|
result.skipped.push(filePath);
|
|
@@ -1555,12 +2060,49 @@ async function writeScaffoldFile(filePath, content, result, options) {
|
|
|
1555
2060
|
result.created.push(filePath);
|
|
1556
2061
|
}
|
|
1557
2062
|
function parseOnly(only) {
|
|
1558
|
-
const [
|
|
1559
|
-
if (!
|
|
1560
|
-
|
|
2063
|
+
const [categoryRaw, slug] = only.split(":");
|
|
2064
|
+
if (!categoryRaw || !slug) return null;
|
|
2065
|
+
if (categoryRaw !== "collections" && categoryRaw !== "data-types" && categoryRaw !== "forms") {
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
return { category: categoryRaw, slug };
|
|
2069
|
+
}
|
|
2070
|
+
function parseTemplate(template) {
|
|
2071
|
+
if (!template || template === "layered") {
|
|
2072
|
+
return "layered";
|
|
2073
|
+
}
|
|
2074
|
+
if (template === "legacy") {
|
|
2075
|
+
return "legacy";
|
|
2076
|
+
}
|
|
2077
|
+
throw new Error(`Invalid template: "${template}". Use "legacy" or "layered".`);
|
|
2078
|
+
}
|
|
2079
|
+
function inferFeaturesDir(appDir) {
|
|
2080
|
+
const normalized = appDir.replace(/\\/g, "/");
|
|
2081
|
+
if (normalized.startsWith("src/")) {
|
|
2082
|
+
return "src/features";
|
|
2083
|
+
}
|
|
2084
|
+
if (normalized.includes("/src/")) {
|
|
2085
|
+
return "src/features";
|
|
2086
|
+
}
|
|
2087
|
+
return "features";
|
|
2088
|
+
}
|
|
2089
|
+
function inferFeatureImportBase(featuresDir) {
|
|
2090
|
+
const normalized = featuresDir.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2091
|
+
if (normalized === "src/features") {
|
|
2092
|
+
return "@/features";
|
|
2093
|
+
}
|
|
2094
|
+
if (normalized.startsWith("src/")) {
|
|
2095
|
+
return `@/${normalized.slice("src/".length)}`;
|
|
2096
|
+
}
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`Layered template requires --features-dir to be under src/ (received: "${featuresDir}")`
|
|
2099
|
+
);
|
|
1561
2100
|
}
|
|
1562
2101
|
async function scaffold(options) {
|
|
1563
2102
|
const { apiUrl, apiKey, appDir, dryRun, force, only } = options;
|
|
2103
|
+
const template = parseTemplate(options.template);
|
|
2104
|
+
const featuresDir = template === "layered" ? options.featuresDir ?? inferFeaturesDir(appDir) : null;
|
|
2105
|
+
const featureImportBase = template === "layered" ? inferFeatureImportBase(featuresDir ?? inferFeaturesDir(appDir)) : null;
|
|
1564
2106
|
console.log(`Fetching schema from ${apiUrl} ...`);
|
|
1565
2107
|
const schema = await fetchSchema(apiUrl, apiKey);
|
|
1566
2108
|
const forms = schema.forms ?? [];
|
|
@@ -1576,10 +2118,77 @@ async function scaffold(options) {
|
|
|
1576
2118
|
for (const col of collections) {
|
|
1577
2119
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
1578
2120
|
if (!safeSlug) continue;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2121
|
+
if (template === "legacy") {
|
|
2122
|
+
const listPath = join3(appDir, safeSlug, "page.tsx");
|
|
2123
|
+
await writeScaffoldFile(listPath, scaffoldCollectionListPage(col), result, { dryRun, force, baseDir: appDir });
|
|
2124
|
+
const detailPath = join3(appDir, safeSlug, "[slug]", "page.tsx");
|
|
2125
|
+
await writeScaffoldFile(detailPath, scaffoldCollectionDetailPage(col), result, { dryRun, force, baseDir: appDir });
|
|
2126
|
+
} else {
|
|
2127
|
+
const listRoutePath = join3(appDir, safeSlug, "page.tsx");
|
|
2128
|
+
await writeScaffoldFile(
|
|
2129
|
+
listRoutePath,
|
|
2130
|
+
scaffoldLayeredCollectionListRoute(col, featureImportBase ?? "@/features"),
|
|
2131
|
+
result,
|
|
2132
|
+
{
|
|
2133
|
+
dryRun,
|
|
2134
|
+
force,
|
|
2135
|
+
baseDir: appDir
|
|
2136
|
+
}
|
|
2137
|
+
);
|
|
2138
|
+
const detailRoutePath = join3(appDir, safeSlug, "[slug]", "page.tsx");
|
|
2139
|
+
await writeScaffoldFile(
|
|
2140
|
+
detailRoutePath,
|
|
2141
|
+
scaffoldLayeredCollectionDetailRoute(col, featureImportBase ?? "@/features"),
|
|
2142
|
+
result,
|
|
2143
|
+
{
|
|
2144
|
+
dryRun,
|
|
2145
|
+
force,
|
|
2146
|
+
baseDir: appDir
|
|
2147
|
+
}
|
|
2148
|
+
);
|
|
2149
|
+
const listFeatureBase = join3(featuresDir, "collections", safeSlug, "list");
|
|
2150
|
+
await writeScaffoldFile(
|
|
2151
|
+
join3(listFeatureBase, "resolver.ts"),
|
|
2152
|
+
scaffoldLayeredCollectionListResolver(col),
|
|
2153
|
+
result,
|
|
2154
|
+
{
|
|
2155
|
+
dryRun,
|
|
2156
|
+
force,
|
|
2157
|
+
baseDir: featuresDir
|
|
2158
|
+
}
|
|
2159
|
+
);
|
|
2160
|
+
await writeScaffoldFile(join3(listFeatureBase, "meta.ts"), scaffoldLayeredCollectionListMeta(col), result, {
|
|
2161
|
+
dryRun,
|
|
2162
|
+
force,
|
|
2163
|
+
baseDir: featuresDir
|
|
2164
|
+
});
|
|
2165
|
+
await writeScaffoldFile(join3(listFeatureBase, "view.tsx"), scaffoldLayeredCollectionListView(col), result, {
|
|
2166
|
+
dryRun,
|
|
2167
|
+
force,
|
|
2168
|
+
baseDir: featuresDir
|
|
2169
|
+
});
|
|
2170
|
+
const detailFeatureBase = join3(featuresDir, "collections", safeSlug, "detail");
|
|
2171
|
+
await writeScaffoldFile(
|
|
2172
|
+
join3(detailFeatureBase, "resolver.ts"),
|
|
2173
|
+
scaffoldLayeredCollectionDetailResolver(col),
|
|
2174
|
+
result,
|
|
2175
|
+
{
|
|
2176
|
+
dryRun,
|
|
2177
|
+
force,
|
|
2178
|
+
baseDir: featuresDir
|
|
2179
|
+
}
|
|
2180
|
+
);
|
|
2181
|
+
await writeScaffoldFile(join3(detailFeatureBase, "meta.ts"), scaffoldLayeredCollectionDetailMeta(col), result, {
|
|
2182
|
+
dryRun,
|
|
2183
|
+
force,
|
|
2184
|
+
baseDir: featuresDir
|
|
2185
|
+
});
|
|
2186
|
+
await writeScaffoldFile(join3(detailFeatureBase, "view.tsx"), scaffoldLayeredCollectionDetailView(col), result, {
|
|
2187
|
+
dryRun,
|
|
2188
|
+
force,
|
|
2189
|
+
baseDir: featuresDir
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
1583
2192
|
}
|
|
1584
2193
|
}
|
|
1585
2194
|
if (!filter || filter.category === "data-types") {
|
|
@@ -1587,8 +2196,38 @@ async function scaffold(options) {
|
|
|
1587
2196
|
for (const dt of dataTypes) {
|
|
1588
2197
|
const safeSlug = sanitizeIdentifier(dt.slug);
|
|
1589
2198
|
if (!safeSlug) continue;
|
|
1590
|
-
|
|
1591
|
-
|
|
2199
|
+
if (template === "legacy") {
|
|
2200
|
+
const pagePath = join3(appDir, safeSlug, "page.tsx");
|
|
2201
|
+
await writeScaffoldFile(pagePath, scaffoldDataTypePage(dt), result, { dryRun, force, baseDir: appDir });
|
|
2202
|
+
} else {
|
|
2203
|
+
const pagePath = join3(appDir, safeSlug, "page.tsx");
|
|
2204
|
+
await writeScaffoldFile(
|
|
2205
|
+
pagePath,
|
|
2206
|
+
scaffoldLayeredDataTypeRoute(dt, featureImportBase ?? "@/features"),
|
|
2207
|
+
result,
|
|
2208
|
+
{
|
|
2209
|
+
dryRun,
|
|
2210
|
+
force,
|
|
2211
|
+
baseDir: appDir
|
|
2212
|
+
}
|
|
2213
|
+
);
|
|
2214
|
+
const featureBase = join3(featuresDir, "data-types", safeSlug, "list");
|
|
2215
|
+
await writeScaffoldFile(join3(featureBase, "resolver.ts"), scaffoldLayeredDataTypeResolver(dt), result, {
|
|
2216
|
+
dryRun,
|
|
2217
|
+
force,
|
|
2218
|
+
baseDir: featuresDir
|
|
2219
|
+
});
|
|
2220
|
+
await writeScaffoldFile(join3(featureBase, "meta.ts"), scaffoldLayeredDataTypeMeta(dt), result, {
|
|
2221
|
+
dryRun,
|
|
2222
|
+
force,
|
|
2223
|
+
baseDir: featuresDir
|
|
2224
|
+
});
|
|
2225
|
+
await writeScaffoldFile(join3(featureBase, "view.tsx"), scaffoldLayeredDataTypeView(dt), result, {
|
|
2226
|
+
dryRun,
|
|
2227
|
+
force,
|
|
2228
|
+
baseDir: featuresDir
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
1592
2231
|
}
|
|
1593
2232
|
}
|
|
1594
2233
|
if (!filter || filter.category === "forms") {
|
|
@@ -1596,17 +2235,56 @@ async function scaffold(options) {
|
|
|
1596
2235
|
for (const form of filteredForms) {
|
|
1597
2236
|
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1598
2237
|
if (!safeSlug) continue;
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2238
|
+
if (template === "legacy") {
|
|
2239
|
+
const pagePath = join3(appDir, safeSlug, "page.tsx");
|
|
2240
|
+
await writeScaffoldFile(pagePath, scaffoldFormPage(form), result, { dryRun, force, baseDir: appDir });
|
|
2241
|
+
const componentPath = join3(appDir, safeSlug, "_components", `${safeSlug}-form.tsx`);
|
|
2242
|
+
await writeScaffoldFile(componentPath, scaffoldFormComponent(form), result, {
|
|
2243
|
+
dryRun,
|
|
2244
|
+
force,
|
|
2245
|
+
baseDir: appDir
|
|
2246
|
+
});
|
|
2247
|
+
} else {
|
|
2248
|
+
const pagePath = join3(appDir, safeSlug, "page.tsx");
|
|
2249
|
+
await writeScaffoldFile(
|
|
2250
|
+
pagePath,
|
|
2251
|
+
scaffoldLayeredFormRoute(form, featureImportBase ?? "@/features"),
|
|
2252
|
+
result,
|
|
2253
|
+
{ dryRun, force, baseDir: appDir }
|
|
2254
|
+
);
|
|
2255
|
+
const featureBase = join3(featuresDir, "forms", safeSlug);
|
|
2256
|
+
await writeScaffoldFile(join3(featureBase, "resolver.ts"), scaffoldLayeredFormResolver(form), result, {
|
|
2257
|
+
dryRun,
|
|
2258
|
+
force,
|
|
2259
|
+
baseDir: featuresDir
|
|
2260
|
+
});
|
|
2261
|
+
await writeScaffoldFile(join3(featureBase, "meta.ts"), scaffoldLayeredFormMeta(form), result, {
|
|
2262
|
+
dryRun,
|
|
2263
|
+
force,
|
|
2264
|
+
baseDir: featuresDir
|
|
2265
|
+
});
|
|
2266
|
+
await writeScaffoldFile(join3(featureBase, "view.tsx"), scaffoldLayeredFormView(form), result, {
|
|
2267
|
+
dryRun,
|
|
2268
|
+
force,
|
|
2269
|
+
baseDir: featuresDir
|
|
2270
|
+
});
|
|
2271
|
+
await writeScaffoldFile(join3(featureBase, "form-view.tsx"), scaffoldLayeredFormClientView(form), result, {
|
|
2272
|
+
dryRun,
|
|
2273
|
+
force,
|
|
2274
|
+
baseDir: featuresDir
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
1603
2277
|
}
|
|
1604
2278
|
}
|
|
1605
2279
|
if (!filter) {
|
|
1606
2280
|
const sitemapPath = join3(appDir, "sitemap.ts");
|
|
1607
2281
|
if (schema.collections.length > 0) {
|
|
1608
2282
|
const siteUrl = apiUrl.replace(/\/api\/.*$/, "");
|
|
1609
|
-
await writeScaffoldFile(sitemapPath, scaffoldSitemap(schema.collections, siteUrl), result, {
|
|
2283
|
+
await writeScaffoldFile(sitemapPath, scaffoldSitemap(schema.collections, siteUrl), result, {
|
|
2284
|
+
dryRun,
|
|
2285
|
+
force,
|
|
2286
|
+
baseDir: appDir
|
|
2287
|
+
});
|
|
1610
2288
|
}
|
|
1611
2289
|
}
|
|
1612
2290
|
console.log("");
|
|
@@ -1628,8 +2306,13 @@ Skipped ${result.skipped.length} file(s) (already exist):`);
|
|
|
1628
2306
|
}
|
|
1629
2307
|
console.log("");
|
|
1630
2308
|
console.log("Next steps:");
|
|
1631
|
-
|
|
1632
|
-
|
|
2309
|
+
if (template === "layered") {
|
|
2310
|
+
console.log(" 1. Customize view files under the generated features directory");
|
|
2311
|
+
console.log(` 2. Keep data fetching in resolver files (${featuresDir})`);
|
|
2312
|
+
} else {
|
|
2313
|
+
console.log(" 1. Customize the generated pages' design (HTML/CSS)");
|
|
2314
|
+
console.log(" 2. Add navigation links to your layout");
|
|
2315
|
+
}
|
|
1633
2316
|
console.log(" 3. Run `npm run dev` to preview your site");
|
|
1634
2317
|
}
|
|
1635
2318
|
|
|
@@ -1910,10 +2593,10 @@ async function deleteDataEntry(options) {
|
|
|
1910
2593
|
input: process.stdin,
|
|
1911
2594
|
output: process.stdout
|
|
1912
2595
|
});
|
|
1913
|
-
const answer = await new Promise((
|
|
2596
|
+
const answer = await new Promise((resolve5) => {
|
|
1914
2597
|
rl.question(
|
|
1915
2598
|
`Are you sure you want to delete data entry ${options.id}? (yes/no): `,
|
|
1916
|
-
|
|
2599
|
+
resolve5
|
|
1917
2600
|
);
|
|
1918
2601
|
});
|
|
1919
2602
|
rl.close();
|
|
@@ -1933,12 +2616,682 @@ async function deleteDataEntry(options) {
|
|
|
1933
2616
|
console.log(JSON.stringify(result, null, 2));
|
|
1934
2617
|
}
|
|
1935
2618
|
|
|
2619
|
+
// src/commands/scaffold-component.ts
|
|
2620
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
2621
|
+
import { join as join4 } from "path";
|
|
2622
|
+
function toPascalCase(input) {
|
|
2623
|
+
const cleaned = input.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
2624
|
+
return cleaned.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
2625
|
+
}
|
|
2626
|
+
function toKebabCase(input) {
|
|
2627
|
+
return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
|
|
2628
|
+
}
|
|
2629
|
+
function ensureDir(path) {
|
|
2630
|
+
if (!existsSync3(path)) {
|
|
2631
|
+
mkdirSync(path, { recursive: true });
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function ensureExport(indexPath, exportName) {
|
|
2635
|
+
const exportLine = `export { ${exportName} } from "./${exportName}"
|
|
2636
|
+
`;
|
|
2637
|
+
if (!existsSync3(indexPath)) {
|
|
2638
|
+
writeFileSync(indexPath, exportLine, "utf-8");
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const current = readFileSync3(indexPath, "utf-8");
|
|
2642
|
+
if (current.includes(exportLine.trim())) {
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const needsNewline = current.length > 0 && !current.endsWith("\n");
|
|
2646
|
+
writeFileSync(indexPath, `${current}${needsNewline ? "\n" : ""}${exportLine}`, "utf-8");
|
|
2647
|
+
}
|
|
2648
|
+
async function scaffoldComponent(options) {
|
|
2649
|
+
const pascalName = toPascalCase(options.name);
|
|
2650
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(pascalName)) {
|
|
2651
|
+
console.error("Error: name must resolve to PascalCase (e.g. FeatureCard)");
|
|
2652
|
+
process.exit(1);
|
|
2653
|
+
}
|
|
2654
|
+
const kebabName = toKebabCase(pascalName);
|
|
2655
|
+
const componentsDir = options.componentsDir || join4(process.cwd(), "src/components/custom");
|
|
2656
|
+
const definitionsDir = options.definitionsDir || join4(process.cwd(), "cmx/components");
|
|
2657
|
+
ensureDir(componentsDir);
|
|
2658
|
+
ensureDir(definitionsDir);
|
|
2659
|
+
const componentPath = join4(componentsDir, `${pascalName}.tsx`);
|
|
2660
|
+
const definitionPath = join4(definitionsDir, `${kebabName}.json`);
|
|
2661
|
+
const indexPath = join4(componentsDir, "index.ts");
|
|
2662
|
+
if (!options.force && (existsSync3(componentPath) || existsSync3(definitionPath))) {
|
|
2663
|
+
console.error("Error: target files already exist. Use --force to overwrite");
|
|
2664
|
+
process.exit(1);
|
|
2665
|
+
}
|
|
2666
|
+
const definition = {
|
|
2667
|
+
name: pascalName,
|
|
2668
|
+
displayName: pascalName,
|
|
2669
|
+
description: `Custom component: ${pascalName}`,
|
|
2670
|
+
propsSchema: options.dataBound ? {
|
|
2671
|
+
contentId: {
|
|
2672
|
+
type: "content",
|
|
2673
|
+
description: "Target content ID",
|
|
2674
|
+
required: true
|
|
2675
|
+
},
|
|
2676
|
+
title: {
|
|
2677
|
+
type: "string",
|
|
2678
|
+
description: "Title",
|
|
2679
|
+
required: false
|
|
2680
|
+
}
|
|
2681
|
+
} : {
|
|
2682
|
+
title: {
|
|
2683
|
+
type: "string",
|
|
2684
|
+
description: "Title",
|
|
2685
|
+
required: false
|
|
2686
|
+
},
|
|
2687
|
+
children: {
|
|
2688
|
+
type: "string",
|
|
2689
|
+
description: "Body text",
|
|
2690
|
+
required: false
|
|
2691
|
+
}
|
|
2692
|
+
},
|
|
2693
|
+
examples: options.dataBound ? [`<${pascalName} contentId="123e4567-e89b-12d3-a456-426614174000" />`] : [`<${pascalName} title="Hello">Body</${pascalName}>`]
|
|
2694
|
+
};
|
|
2695
|
+
const componentCode = options.dataBound ? `interface ${pascalName}Props {
|
|
2696
|
+
contentId: string
|
|
2697
|
+
title?: string
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
export function ${pascalName}({ contentId, title }: ${pascalName}Props) {
|
|
2701
|
+
// TODO: Resolve data by contentId and map it to presentational props.
|
|
2702
|
+
return (
|
|
2703
|
+
<section className="rounded-lg border p-4">
|
|
2704
|
+
{title ? <h3 className="mb-2 text-lg font-semibold">{title}</h3> : null}
|
|
2705
|
+
<p className="text-sm text-muted-foreground">Resolve content by ID: {contentId}</p>
|
|
2706
|
+
</section>
|
|
2707
|
+
)
|
|
2708
|
+
}
|
|
2709
|
+
` : `interface ${pascalName}Props {
|
|
2710
|
+
title?: string
|
|
2711
|
+
children?: string
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
export function ${pascalName}({ title, children }: ${pascalName}Props) {
|
|
2715
|
+
return (
|
|
2716
|
+
<section className="rounded-lg border p-4">
|
|
2717
|
+
{title ? <h3 className="mb-2 text-lg font-semibold">{title}</h3> : null}
|
|
2718
|
+
{children ? <p className="text-sm text-muted-foreground">{children}</p> : null}
|
|
2719
|
+
</section>
|
|
2720
|
+
)
|
|
2721
|
+
}
|
|
2722
|
+
`;
|
|
2723
|
+
writeFileSync(definitionPath, `${JSON.stringify(definition, null, 2)}
|
|
2724
|
+
`, "utf-8");
|
|
2725
|
+
writeFileSync(componentPath, componentCode, "utf-8");
|
|
2726
|
+
ensureExport(indexPath, pascalName);
|
|
2727
|
+
console.log(`Created: ${definitionPath}`);
|
|
2728
|
+
console.log(`Created: ${componentPath}`);
|
|
2729
|
+
console.log(`Updated: ${indexPath}`);
|
|
2730
|
+
console.log("Next: run `cmx-sdk sync-components` to register the new definition");
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/commands/codegen-check.ts
|
|
2734
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
|
|
2735
|
+
import { extname, join as join5, relative, resolve as resolve2 } from "path";
|
|
2736
|
+
import stripJsonComments from "strip-json-comments";
|
|
2737
|
+
function stripTrailingCommas(input) {
|
|
2738
|
+
return input.replace(/,\s*([}\]])/g, "$1");
|
|
2739
|
+
}
|
|
2740
|
+
function normalizePathValue(input) {
|
|
2741
|
+
return input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2742
|
+
}
|
|
2743
|
+
function collectFiles(rootDir, extensions) {
|
|
2744
|
+
const files = [];
|
|
2745
|
+
function walk(dir) {
|
|
2746
|
+
const entries = readdirSync2(dir);
|
|
2747
|
+
for (const entry of entries) {
|
|
2748
|
+
const fullPath = join5(dir, entry);
|
|
2749
|
+
const stat = statSync(fullPath);
|
|
2750
|
+
if (stat.isDirectory()) {
|
|
2751
|
+
if (entry === "node_modules" || entry === ".next" || entry === "dist") {
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
walk(fullPath);
|
|
2755
|
+
} else if (extensions.has(extname(entry))) {
|
|
2756
|
+
files.push(fullPath);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
if (existsSync4(rootDir)) {
|
|
2761
|
+
walk(rootDir);
|
|
2762
|
+
}
|
|
2763
|
+
return files;
|
|
2764
|
+
}
|
|
2765
|
+
function extractImports(code) {
|
|
2766
|
+
const imports = /* @__PURE__ */ new Set();
|
|
2767
|
+
const fromPattern = /from\s+["']([^"']+)["']/g;
|
|
2768
|
+
let match;
|
|
2769
|
+
while ((match = fromPattern.exec(code)) !== null) {
|
|
2770
|
+
imports.add(match[1]);
|
|
2771
|
+
}
|
|
2772
|
+
const dynamicImportPattern = /import\(\s*["']([^"']+)["']\s*\)/g;
|
|
2773
|
+
while ((match = dynamicImportPattern.exec(code)) !== null) {
|
|
2774
|
+
imports.add(match[1]);
|
|
2775
|
+
}
|
|
2776
|
+
return Array.from(imports);
|
|
2777
|
+
}
|
|
2778
|
+
function resolveGeneratedImport(outputDirAbs, importPath) {
|
|
2779
|
+
if (importPath === "@cmx/generated") {
|
|
2780
|
+
return [join5(outputDirAbs, "index.ts")];
|
|
2781
|
+
}
|
|
2782
|
+
const subPath = importPath.replace(/^@cmx\/generated\//, "");
|
|
2783
|
+
return [
|
|
2784
|
+
join5(outputDirAbs, `${subPath}.ts`),
|
|
2785
|
+
join5(outputDirAbs, subPath, "index.ts")
|
|
2786
|
+
];
|
|
2787
|
+
}
|
|
2788
|
+
async function runCodegenCheck(options = {}) {
|
|
2789
|
+
const projectRoot = resolve2(options.projectRoot ?? process.cwd());
|
|
2790
|
+
const outputDirAbs = resolve2(projectRoot, options.outputDir ?? "cmx/generated");
|
|
2791
|
+
const srcDirAbs = resolve2(projectRoot, options.srcDir ?? "src");
|
|
2792
|
+
const tsconfigAbs = resolve2(projectRoot, options.tsconfigPath ?? "tsconfig.json");
|
|
2793
|
+
const errors = [];
|
|
2794
|
+
const warnings = [];
|
|
2795
|
+
const checks = [];
|
|
2796
|
+
if (!existsSync4(outputDirAbs)) {
|
|
2797
|
+
errors.push(`Generated output directory not found: ${relative(projectRoot, outputDirAbs)}`);
|
|
2798
|
+
} else {
|
|
2799
|
+
checks.push(`Generated directory exists: ${relative(projectRoot, outputDirAbs)}`);
|
|
2800
|
+
const indexPath = join5(outputDirAbs, "index.ts");
|
|
2801
|
+
if (!existsSync4(indexPath)) {
|
|
2802
|
+
errors.push(`Generated index not found: ${relative(projectRoot, indexPath)}`);
|
|
2803
|
+
} else {
|
|
2804
|
+
checks.push(`Generated index exists: ${relative(projectRoot, indexPath)}`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
if (!existsSync4(tsconfigAbs)) {
|
|
2808
|
+
errors.push(`tsconfig not found: ${relative(projectRoot, tsconfigAbs)}`);
|
|
2809
|
+
} else {
|
|
2810
|
+
try {
|
|
2811
|
+
const raw = readFileSync4(tsconfigAbs, "utf-8");
|
|
2812
|
+
const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(raw)));
|
|
2813
|
+
const aliasValues = parsed.compilerOptions?.paths?.["@cmx/*"] ?? [];
|
|
2814
|
+
if (aliasValues.length === 0) {
|
|
2815
|
+
errors.push("tsconfig compilerOptions.paths['@cmx/*'] is missing");
|
|
2816
|
+
} else {
|
|
2817
|
+
const normalizedAliasValues = aliasValues.map(normalizePathValue);
|
|
2818
|
+
if (!normalizedAliasValues.includes("cmx/*")) {
|
|
2819
|
+
errors.push(
|
|
2820
|
+
"tsconfig compilerOptions.paths['@cmx/*'] must include './cmx/*' (or equivalent)"
|
|
2821
|
+
);
|
|
2822
|
+
} else {
|
|
2823
|
+
checks.push("tsconfig '@cmx/*' alias is configured");
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const includeValues = parsed.include ?? [];
|
|
2827
|
+
const normalizedIncludeValues = includeValues.map(normalizePathValue);
|
|
2828
|
+
if (!normalizedIncludeValues.includes("cmx/**/*.ts")) {
|
|
2829
|
+
warnings.push("tsconfig include does not contain 'cmx/**/*.ts'");
|
|
2830
|
+
} else {
|
|
2831
|
+
checks.push("tsconfig include contains 'cmx/**/*.ts'");
|
|
2832
|
+
}
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
errors.push(`Failed to parse tsconfig: ${error instanceof Error ? error.message : String(error)}`);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
if (!existsSync4(srcDirAbs)) {
|
|
2838
|
+
warnings.push(`Source directory not found: ${relative(projectRoot, srcDirAbs)}`);
|
|
2839
|
+
} else {
|
|
2840
|
+
const sourceFiles = collectFiles(srcDirAbs, /* @__PURE__ */ new Set([".ts", ".tsx"]));
|
|
2841
|
+
const legacyImportHits = [];
|
|
2842
|
+
const generatedImportHits = [];
|
|
2843
|
+
for (const sourceFile of sourceFiles) {
|
|
2844
|
+
const code = readFileSync4(sourceFile, "utf-8");
|
|
2845
|
+
const imports = extractImports(code);
|
|
2846
|
+
for (const importPath of imports) {
|
|
2847
|
+
if (importPath.startsWith("@/cmx/generated")) {
|
|
2848
|
+
legacyImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
|
|
2849
|
+
}
|
|
2850
|
+
if (importPath === "@cmx/generated" || importPath.startsWith("@cmx/generated/")) {
|
|
2851
|
+
generatedImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
|
|
2852
|
+
const candidates = resolveGeneratedImport(outputDirAbs, importPath);
|
|
2853
|
+
for (const candidate of candidates) {
|
|
2854
|
+
if (existsSync4(candidate)) {
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
const isLast = candidate === candidates.at(-1);
|
|
2858
|
+
if (isLast) {
|
|
2859
|
+
errors.push(
|
|
2860
|
+
`Missing generated target for import '${importPath}' referenced in ${relative(projectRoot, sourceFile)}`
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (legacyImportHits.length > 0) {
|
|
2868
|
+
errors.push("Legacy '@/cmx/generated' imports were found:");
|
|
2869
|
+
for (const hit of legacyImportHits) {
|
|
2870
|
+
errors.push(` - ${hit}`);
|
|
2871
|
+
}
|
|
2872
|
+
} else {
|
|
2873
|
+
checks.push("No legacy '@/cmx/generated' imports found");
|
|
2874
|
+
}
|
|
2875
|
+
if (generatedImportHits.length === 0) {
|
|
2876
|
+
warnings.push("No '@cmx/generated' imports found under src/ (this may be expected for empty projects)");
|
|
2877
|
+
} else {
|
|
2878
|
+
checks.push(`Found ${generatedImportHits.length} '@cmx/generated' import(s) under src/`);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
return {
|
|
2882
|
+
success: errors.length === 0,
|
|
2883
|
+
errors,
|
|
2884
|
+
warnings,
|
|
2885
|
+
checks
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
async function codegenCheck(options = {}) {
|
|
2889
|
+
const projectRoot = resolve2(options.projectRoot ?? process.cwd());
|
|
2890
|
+
const result = await runCodegenCheck(options);
|
|
2891
|
+
console.log("Codegen check:");
|
|
2892
|
+
for (const check of result.checks) {
|
|
2893
|
+
console.log(` \u2713 ${check}`);
|
|
2894
|
+
}
|
|
2895
|
+
for (const warning of result.warnings) {
|
|
2896
|
+
console.warn(` \u26A0 ${warning}`);
|
|
2897
|
+
}
|
|
2898
|
+
for (const error of result.errors) {
|
|
2899
|
+
console.error(` \u2717 ${error}`);
|
|
2900
|
+
}
|
|
2901
|
+
if (!result.success) {
|
|
2902
|
+
console.error(`
|
|
2903
|
+
Codegen check failed in ${projectRoot}`);
|
|
2904
|
+
process.exit(1);
|
|
2905
|
+
}
|
|
2906
|
+
console.log("\nCodegen check passed");
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// src/commands/mdx-validate.ts
|
|
2910
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2911
|
+
import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
|
|
2912
|
+
|
|
2913
|
+
// src/mdx/validator.ts
|
|
2914
|
+
import { compile } from "@mdx-js/mdx";
|
|
2915
|
+
var FORBIDDEN_PATTERNS = [
|
|
2916
|
+
// import/export statements
|
|
2917
|
+
{ pattern: /^\s*import\s+/m, message: "import\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2918
|
+
{ pattern: /^\s*export\s+/m, message: "export\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2919
|
+
// JS expressions in curly braces (except component props)
|
|
2920
|
+
{ pattern: /\{[^}]*(?:=>|function|new\s+|typeof|instanceof)[^}]*\}/m, message: "JavaScript\u5F0F\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2921
|
+
// eval, Function constructor
|
|
2922
|
+
{ pattern: /\beval\s*\(/m, message: "eval\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2923
|
+
{ pattern: /\bnew\s+Function\s*\(/m, message: "Function constructor\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2924
|
+
// script tags
|
|
2925
|
+
{ pattern: /<script[\s>]/i, message: "script\u30BF\u30B0\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2926
|
+
// on* event handlers
|
|
2927
|
+
{ pattern: /\bon\w+\s*=/i, message: "\u30A4\u30D9\u30F3\u30C8\u30CF\u30F3\u30C9\u30E9\u5C5E\u6027\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2928
|
+
// javascript: URLs
|
|
2929
|
+
{ pattern: /javascript\s*:/i, message: "javascript: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2930
|
+
// data: URLs (for images - can be XSS vector)
|
|
2931
|
+
{ pattern: /src\s*=\s*["']?\s*data:/i, message: "data: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" }
|
|
2932
|
+
];
|
|
2933
|
+
function extractComponents(mdx) {
|
|
2934
|
+
const components = [];
|
|
2935
|
+
const lines = mdx.split("\n");
|
|
2936
|
+
const componentPattern = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*\/?>/g;
|
|
2937
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2938
|
+
const line = lines[i];
|
|
2939
|
+
let match;
|
|
2940
|
+
while ((match = componentPattern.exec(line)) !== null) {
|
|
2941
|
+
const name = match[1];
|
|
2942
|
+
const propsString = match[2];
|
|
2943
|
+
const props = parseProps(propsString);
|
|
2944
|
+
components.push({
|
|
2945
|
+
name,
|
|
2946
|
+
props,
|
|
2947
|
+
line: i + 1
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
return components;
|
|
2952
|
+
}
|
|
2953
|
+
function parseProps(propsString) {
|
|
2954
|
+
const props = {};
|
|
2955
|
+
const propPattern = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/g;
|
|
2956
|
+
let match;
|
|
2957
|
+
while ((match = propPattern.exec(propsString)) !== null) {
|
|
2958
|
+
const key = match[1];
|
|
2959
|
+
const stringValue = match[2] ?? match[3];
|
|
2960
|
+
const expressionValue = match[4];
|
|
2961
|
+
if (stringValue !== void 0) {
|
|
2962
|
+
props[key] = stringValue;
|
|
2963
|
+
} else if (expressionValue !== void 0) {
|
|
2964
|
+
const trimmed = expressionValue.trim();
|
|
2965
|
+
if (trimmed === "true") {
|
|
2966
|
+
props[key] = true;
|
|
2967
|
+
} else if (trimmed === "false") {
|
|
2968
|
+
props[key] = false;
|
|
2969
|
+
} else if (/^\d+$/.test(trimmed)) {
|
|
2970
|
+
props[key] = parseInt(trimmed, 10);
|
|
2971
|
+
} else if (/^\d+\.\d+$/.test(trimmed)) {
|
|
2972
|
+
props[key] = parseFloat(trimmed);
|
|
2973
|
+
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2974
|
+
try {
|
|
2975
|
+
props[key] = JSON.parse(trimmed.replace(/'/g, '"'));
|
|
2976
|
+
} catch {
|
|
2977
|
+
props[key] = trimmed;
|
|
2978
|
+
}
|
|
2979
|
+
} else {
|
|
2980
|
+
props[key] = trimmed;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const booleanPattern = /(?:^|\s)(\w+)(?=\s|\/|>|$)/g;
|
|
2985
|
+
while ((match = booleanPattern.exec(propsString)) !== null) {
|
|
2986
|
+
const key = match[1];
|
|
2987
|
+
if (!(key in props)) {
|
|
2988
|
+
props[key] = true;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return props;
|
|
2992
|
+
}
|
|
2993
|
+
function extractReferences(components) {
|
|
2994
|
+
const contentIds = [];
|
|
2995
|
+
const assetIds = [];
|
|
2996
|
+
for (const component of components) {
|
|
2997
|
+
if (component.name === "BlogCard" && typeof component.props.contentId === "string") {
|
|
2998
|
+
contentIds.push(component.props.contentId);
|
|
2999
|
+
}
|
|
3000
|
+
if (component.name === "Image" && typeof component.props.assetId === "string") {
|
|
3001
|
+
assetIds.push(component.props.assetId);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return {
|
|
3005
|
+
contentIds: [...new Set(contentIds)],
|
|
3006
|
+
assetIds: [...new Set(assetIds)]
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
async function validateMdx(mdx) {
|
|
3010
|
+
const errors = [];
|
|
3011
|
+
const warnings = [];
|
|
3012
|
+
for (const { pattern, message } of FORBIDDEN_PATTERNS) {
|
|
3013
|
+
if (pattern.test(mdx)) {
|
|
3014
|
+
const lines = mdx.split("\n");
|
|
3015
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3016
|
+
if (pattern.test(lines[i])) {
|
|
3017
|
+
errors.push({
|
|
3018
|
+
type: "forbidden",
|
|
3019
|
+
message,
|
|
3020
|
+
line: i + 1
|
|
3021
|
+
});
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
try {
|
|
3028
|
+
await compile(mdx, {
|
|
3029
|
+
development: false
|
|
3030
|
+
// Minimal compilation to check syntax
|
|
3031
|
+
});
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
const err = error;
|
|
3034
|
+
errors.push({
|
|
3035
|
+
type: "syntax",
|
|
3036
|
+
message: err.message,
|
|
3037
|
+
line: err.line,
|
|
3038
|
+
column: err.column
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
const components = extractComponents(mdx);
|
|
3042
|
+
for (const component of components) {
|
|
3043
|
+
if (!isValidComponent(component.name)) {
|
|
3044
|
+
errors.push({
|
|
3045
|
+
type: "component",
|
|
3046
|
+
message: `\u672A\u77E5\u306E\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8: ${component.name}`,
|
|
3047
|
+
line: component.line,
|
|
3048
|
+
component: component.name
|
|
3049
|
+
});
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
const result = validateComponentProps(component.name, component.props);
|
|
3053
|
+
if (!result.success) {
|
|
3054
|
+
for (const issue of result.error.issues) {
|
|
3055
|
+
errors.push({
|
|
3056
|
+
type: "props",
|
|
3057
|
+
message: `${component.name}: ${issue.path.join(".")} - ${issue.message}`,
|
|
3058
|
+
line: component.line,
|
|
3059
|
+
component: component.name
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
const references = extractReferences(components);
|
|
3065
|
+
return {
|
|
3066
|
+
valid: errors.length === 0,
|
|
3067
|
+
errors,
|
|
3068
|
+
warnings,
|
|
3069
|
+
references
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/commands/mdx-validate.ts
|
|
3074
|
+
function collectMdxFiles(rootDir) {
|
|
3075
|
+
const files = [];
|
|
3076
|
+
function walk(dir) {
|
|
3077
|
+
const entries = readdirSync3(dir);
|
|
3078
|
+
for (const entry of entries) {
|
|
3079
|
+
const fullPath = join6(dir, entry);
|
|
3080
|
+
const stat = statSync2(fullPath);
|
|
3081
|
+
if (stat.isDirectory()) {
|
|
3082
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
walk(fullPath);
|
|
3086
|
+
} else if (extname2(entry) === ".mdx") {
|
|
3087
|
+
files.push(fullPath);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
if (existsSync5(rootDir)) {
|
|
3092
|
+
walk(rootDir);
|
|
3093
|
+
}
|
|
3094
|
+
return files;
|
|
3095
|
+
}
|
|
3096
|
+
async function mdxValidate(options) {
|
|
3097
|
+
const projectRoot = process.cwd();
|
|
3098
|
+
const inputFile = options.file ? resolve3(projectRoot, options.file) : null;
|
|
3099
|
+
const inputDir = resolve3(projectRoot, options.dir ?? "src");
|
|
3100
|
+
const targets = [];
|
|
3101
|
+
if (inputFile) {
|
|
3102
|
+
if (!existsSync5(inputFile)) {
|
|
3103
|
+
console.error(`Error: file not found: ${options.file}`);
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
targets.push(inputFile);
|
|
3107
|
+
} else {
|
|
3108
|
+
const dirFiles = collectMdxFiles(inputDir);
|
|
3109
|
+
targets.push(...dirFiles);
|
|
3110
|
+
}
|
|
3111
|
+
if (targets.length === 0) {
|
|
3112
|
+
console.error("No MDX files found to validate");
|
|
3113
|
+
process.exit(1);
|
|
3114
|
+
}
|
|
3115
|
+
let invalidCount = 0;
|
|
3116
|
+
let warningCount = 0;
|
|
3117
|
+
for (const filePath of targets) {
|
|
3118
|
+
const mdx = readFileSync5(filePath, "utf-8");
|
|
3119
|
+
const result = await validateMdx(mdx);
|
|
3120
|
+
const relPath = relative2(projectRoot, filePath);
|
|
3121
|
+
if (result.valid) {
|
|
3122
|
+
console.log(`\u2713 ${relPath}`);
|
|
3123
|
+
} else {
|
|
3124
|
+
invalidCount++;
|
|
3125
|
+
console.log(`\u2717 ${relPath}`);
|
|
3126
|
+
for (const err of result.errors) {
|
|
3127
|
+
const location = err.line ? `:${err.line}${err.column ? `:${err.column}` : ""}` : "";
|
|
3128
|
+
console.log(` - [${err.type}]${location} ${err.message}`);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (result.warnings.length > 0) {
|
|
3132
|
+
warningCount += result.warnings.length;
|
|
3133
|
+
for (const warning of result.warnings) {
|
|
3134
|
+
console.log(` ! [warning] ${warning}`);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
console.log("");
|
|
3139
|
+
console.log(`Validated ${targets.length} file(s)`);
|
|
3140
|
+
console.log(`Invalid: ${invalidCount}`);
|
|
3141
|
+
console.log(`Warnings: ${warningCount}`);
|
|
3142
|
+
if (invalidCount > 0) {
|
|
3143
|
+
process.exit(1);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// src/commands/mdx-doctor.ts
|
|
3148
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
3149
|
+
import { basename, extname as extname3, join as join7, relative as relative3, resolve as resolve4 } from "path";
|
|
3150
|
+
function collectFiles2(rootDir, extension) {
|
|
3151
|
+
const files = [];
|
|
3152
|
+
function walk(dir) {
|
|
3153
|
+
const entries = readdirSync4(dir);
|
|
3154
|
+
for (const entry of entries) {
|
|
3155
|
+
const fullPath = join7(dir, entry);
|
|
3156
|
+
const stat = statSync3(fullPath);
|
|
3157
|
+
if (stat.isDirectory()) {
|
|
3158
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
walk(fullPath);
|
|
3162
|
+
} else if (extname3(entry) === extension) {
|
|
3163
|
+
files.push(fullPath);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
if (existsSync6(rootDir)) {
|
|
3168
|
+
walk(rootDir);
|
|
3169
|
+
}
|
|
3170
|
+
return files;
|
|
3171
|
+
}
|
|
3172
|
+
function parseDefinitionFiles(projectRoot, files) {
|
|
3173
|
+
const definitions = [];
|
|
3174
|
+
const errors = [];
|
|
3175
|
+
for (const file of files) {
|
|
3176
|
+
try {
|
|
3177
|
+
const raw = readFileSync6(file, "utf-8");
|
|
3178
|
+
const parsed = JSON.parse(raw);
|
|
3179
|
+
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
3180
|
+
errors.push(`${relative3(projectRoot, file)}: missing string 'name'`);
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
definitions.push({ name: parsed.name, file });
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
errors.push(
|
|
3186
|
+
`${relative3(projectRoot, file)}: invalid JSON (${error instanceof Error ? error.message : String(error)})`
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return { definitions, errors };
|
|
3191
|
+
}
|
|
3192
|
+
function collectExportedComponentNames(indexContent) {
|
|
3193
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3194
|
+
const namedExportPattern = /export\s+\{\s*([A-Za-z0-9_]+)(?:\s+as\s+[A-Za-z0-9_]+)?\s*\}\s+from\s+["']\.\/?([^"']+)["']/g;
|
|
3195
|
+
let match;
|
|
3196
|
+
while ((match = namedExportPattern.exec(indexContent)) !== null) {
|
|
3197
|
+
exported.add(match[1]);
|
|
3198
|
+
const pathBase = basename(match[2]);
|
|
3199
|
+
exported.add(pathBase);
|
|
3200
|
+
}
|
|
3201
|
+
const exportAllPattern = /export\s+\*\s+from\s+["']\.\/?([^"']+)["']/g;
|
|
3202
|
+
while ((match = exportAllPattern.exec(indexContent)) !== null) {
|
|
3203
|
+
exported.add(basename(match[1]));
|
|
3204
|
+
}
|
|
3205
|
+
return exported;
|
|
3206
|
+
}
|
|
3207
|
+
async function mdxDoctor(options = {}) {
|
|
3208
|
+
const projectRoot = process.cwd();
|
|
3209
|
+
const definitionsDir = resolve4(projectRoot, options.definitionsDir ?? "cmx/components");
|
|
3210
|
+
const componentsDir = resolve4(projectRoot, options.componentsDir ?? "src/components/custom");
|
|
3211
|
+
const indexFile = resolve4(projectRoot, options.indexFile ?? join7(options.componentsDir ?? "src/components/custom", "index.ts"));
|
|
3212
|
+
const issues = [];
|
|
3213
|
+
const checks = [];
|
|
3214
|
+
if (!existsSync6(definitionsDir)) {
|
|
3215
|
+
issues.push(`Definitions directory not found: ${relative3(projectRoot, definitionsDir)}`);
|
|
3216
|
+
}
|
|
3217
|
+
if (!existsSync6(componentsDir)) {
|
|
3218
|
+
issues.push(`Components directory not found: ${relative3(projectRoot, componentsDir)}`);
|
|
3219
|
+
}
|
|
3220
|
+
if (issues.length > 0) {
|
|
3221
|
+
for (const issue of issues) {
|
|
3222
|
+
console.error(`\u2717 ${issue}`);
|
|
3223
|
+
}
|
|
3224
|
+
process.exit(1);
|
|
3225
|
+
}
|
|
3226
|
+
const definitionFiles = collectFiles2(definitionsDir, ".json");
|
|
3227
|
+
const implFiles = collectFiles2(componentsDir, ".tsx").filter((f) => basename(f) !== "index.tsx");
|
|
3228
|
+
checks.push(`Found ${definitionFiles.length} definition file(s)`);
|
|
3229
|
+
checks.push(`Found ${implFiles.length} implementation file(s)`);
|
|
3230
|
+
const { definitions, errors } = parseDefinitionFiles(projectRoot, definitionFiles);
|
|
3231
|
+
for (const error of errors) {
|
|
3232
|
+
issues.push(error);
|
|
3233
|
+
}
|
|
3234
|
+
const definitionNames = new Set(definitions.map((d) => d.name));
|
|
3235
|
+
const implementationNames = new Set(
|
|
3236
|
+
implFiles.map((f) => basename(f, ".tsx")).filter((name) => name !== "index")
|
|
3237
|
+
);
|
|
3238
|
+
for (const def of definitions) {
|
|
3239
|
+
if (isStandardComponentName(def.name)) {
|
|
3240
|
+
issues.push(`${relative3(projectRoot, def.file)}: '${def.name}' is reserved as a standard component`);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
for (const defName of definitionNames) {
|
|
3244
|
+
if (!implementationNames.has(defName)) {
|
|
3245
|
+
issues.push(`Definition exists but implementation is missing: ${defName}.tsx`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
for (const implName of implementationNames) {
|
|
3249
|
+
if (!definitionNames.has(implName)) {
|
|
3250
|
+
issues.push(`Implementation exists but definition is missing: ${implName}.json`);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (!existsSync6(indexFile)) {
|
|
3254
|
+
issues.push(`Export index not found: ${relative3(projectRoot, indexFile)}`);
|
|
3255
|
+
} else {
|
|
3256
|
+
const indexContent = readFileSync6(indexFile, "utf-8");
|
|
3257
|
+
const exportedNames = collectExportedComponentNames(indexContent);
|
|
3258
|
+
for (const implName of implementationNames) {
|
|
3259
|
+
if (!exportedNames.has(implName)) {
|
|
3260
|
+
issues.push(`Implementation is not exported in ${relative3(projectRoot, indexFile)}: ${implName}`);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
checks.push(`Checked exports in ${relative3(projectRoot, indexFile)}`);
|
|
3264
|
+
}
|
|
3265
|
+
console.log("MDX doctor:");
|
|
3266
|
+
for (const check of checks) {
|
|
3267
|
+
console.log(` \u2713 ${check}`);
|
|
3268
|
+
}
|
|
3269
|
+
if (issues.length > 0) {
|
|
3270
|
+
for (const issue of issues) {
|
|
3271
|
+
console.error(` \u2717 ${issue}`);
|
|
3272
|
+
}
|
|
3273
|
+
console.error(`
|
|
3274
|
+
Doctor found ${issues.length} issue(s)`);
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
console.log("\nDoctor checks passed");
|
|
3278
|
+
}
|
|
3279
|
+
|
|
1936
3280
|
// src/cli.ts
|
|
1937
3281
|
config();
|
|
1938
3282
|
var program = new Command();
|
|
1939
|
-
program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.
|
|
3283
|
+
program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.2.3").allowExcessArguments(false);
|
|
3284
|
+
function requireApiCredentials() {
|
|
3285
|
+
const apiUrl = process.env.CMX_API_URL;
|
|
3286
|
+
const apiKey = process.env.CMX_API_KEY;
|
|
3287
|
+
if (!apiUrl || !apiKey) {
|
|
3288
|
+
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
3289
|
+
process.exit(1);
|
|
3290
|
+
}
|
|
3291
|
+
return { apiUrl, apiKey };
|
|
3292
|
+
}
|
|
1940
3293
|
program.action(async () => {
|
|
1941
|
-
const { interactiveMenu } = await import("./interactive-menu-
|
|
3294
|
+
const { interactiveMenu } = await import("./interactive-menu-BDZOOGQH.js");
|
|
1942
3295
|
await interactiveMenu();
|
|
1943
3296
|
});
|
|
1944
3297
|
program.command("init [project-name]").description("\u65B0\u898F CMX \u30B5\u30A4\u30C8\u3092\u4F5C\u6210").option("--no-studio", "CMX Studio \u3092\u30B9\u30AD\u30C3\u30D7").option("--pm <manager>", "\u30D1\u30C3\u30B1\u30FC\u30B8\u30DE\u30CD\u30FC\u30B8\u30E3\u30FC (npm, pnpm, yarn)").action(async (projectName, options) => {
|
|
@@ -1957,20 +3310,75 @@ program.command("list-components").description("List all custom components").act
|
|
|
1957
3310
|
program.command("create-collection").description("Create a new collection").option("--json <json>", "JSON string with collection data").option("--type <type>", "Collection type (post, page, doc, news)").option("--slug <slug>", "Collection slug").option("--name <name>", "Collection name").option("--description <description>", "Collection description").action(createCollection);
|
|
1958
3311
|
program.command("create-data-type").description("Create a new data type").option("--json <json>", "JSON string with data type data (recommended)").option("--slug <slug>", "Data type slug").option("--name <name>", "Data type name").option("--description <description>", "Data type description").action(createDataType);
|
|
1959
3312
|
program.command("create-form").description("Create a new form definition").option("--json <json>", "JSON string with form data (recommended)").option("--slug <slug>", "Form slug").option("--name <name>", "Form name").option("--description <description>", "Form description").action(createForm);
|
|
1960
|
-
program.command("sync-components").description("Sync custom components").
|
|
1961
|
-
program.command("
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
process.exit(1);
|
|
1967
|
-
}
|
|
1968
|
-
return generate({
|
|
3313
|
+
program.command("sync-components").description("Sync custom components").action(syncComponents);
|
|
3314
|
+
program.command("components").description("Manage custom component definitions and templates").command("scaffold").description("Generate component definition + implementation scaffold").requiredOption("--name <name>", "Component name (PascalCase)").option("--data-bound", "Generate data-bound template with ID resolver props").option("--force", "Overwrite existing files").option("--components-dir <dir>", "Custom components directory (default: src/components/custom)").option("--definitions-dir <dir>", "Component definition directory (default: cmx/components)").action((options) => scaffoldComponent(options));
|
|
3315
|
+
var codegenCommand = program.command("codegen").description("Generate typed code, page scaffolds, and run compatibility checks");
|
|
3316
|
+
codegenCommand.command("types").description("Generate TypeScript types and fetch helpers from CMX schema").option("--output <dir>", "Output directory", "cmx/generated").action(async (options) => {
|
|
3317
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3318
|
+
await generate({
|
|
1969
3319
|
apiUrl,
|
|
1970
3320
|
apiKey,
|
|
1971
|
-
outputDir: options.output
|
|
3321
|
+
outputDir: options.output
|
|
1972
3322
|
});
|
|
1973
3323
|
});
|
|
3324
|
+
codegenCommand.command("pages").description("Generate Next.js pages from CMX schema").option("--app-dir <dir>", "App directory path", "src/app").option("--features-dir <dir>", "Features directory path (layered template only)", "src/features").option("--template <template>", "Template style: layered or legacy", "layered").option("--dry-run", "Preview without writing files").option("--force", "Overwrite existing files").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").action(
|
|
3325
|
+
async (options) => {
|
|
3326
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3327
|
+
await scaffold({
|
|
3328
|
+
apiUrl,
|
|
3329
|
+
apiKey,
|
|
3330
|
+
appDir: options.appDir,
|
|
3331
|
+
featuresDir: options.featuresDir,
|
|
3332
|
+
template: options.template,
|
|
3333
|
+
dryRun: options.dryRun,
|
|
3334
|
+
force: options.force,
|
|
3335
|
+
only: options.only
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
3339
|
+
codegenCommand.command("check").description("Check generated code and project wiring consistency").option("--project-root <dir>", "Project root directory").option("--output-dir <dir>", "Generated output directory", "cmx/generated").option("--src-dir <dir>", "Source directory to scan imports", "src").option("--tsconfig <path>", "Path to tsconfig", "tsconfig.json").action(
|
|
3340
|
+
async (options) => {
|
|
3341
|
+
await codegenCheck({
|
|
3342
|
+
projectRoot: options.projectRoot,
|
|
3343
|
+
outputDir: options.outputDir,
|
|
3344
|
+
srcDir: options.srcDir,
|
|
3345
|
+
tsconfigPath: options.tsconfig
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
);
|
|
3349
|
+
codegenCommand.command("all").description("Run `codegen types`, `codegen pages`, and `codegen check` in sequence").option("--output <dir>", "Output directory for generated types", "cmx/generated").option("--app-dir <dir>", "App directory path", "src/app").option("--features-dir <dir>", "Features directory path (layered template only)", "src/features").option("--template <template>", "Template style: layered or legacy", "layered").option("--dry-run", "Preview page generation without writing files").option("--force", "Overwrite existing files when generating pages").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").option("--src-dir <dir>", "Source directory to scan imports", "src").option("--tsconfig <path>", "Path to tsconfig", "tsconfig.json").action(
|
|
3350
|
+
async (options) => {
|
|
3351
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3352
|
+
console.log("\n[1/3] codegen types");
|
|
3353
|
+
await generate({
|
|
3354
|
+
apiUrl,
|
|
3355
|
+
apiKey,
|
|
3356
|
+
outputDir: options.output
|
|
3357
|
+
});
|
|
3358
|
+
console.log("\n[2/3] codegen pages");
|
|
3359
|
+
await scaffold({
|
|
3360
|
+
apiUrl,
|
|
3361
|
+
apiKey,
|
|
3362
|
+
appDir: options.appDir,
|
|
3363
|
+
featuresDir: options.featuresDir,
|
|
3364
|
+
template: options.template,
|
|
3365
|
+
dryRun: options.dryRun,
|
|
3366
|
+
force: options.force,
|
|
3367
|
+
only: options.only
|
|
3368
|
+
});
|
|
3369
|
+
console.log("\n[3/3] codegen check");
|
|
3370
|
+
await codegenCheck({
|
|
3371
|
+
outputDir: options.output,
|
|
3372
|
+
srcDir: options.srcDir,
|
|
3373
|
+
tsconfigPath: options.tsconfig
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
);
|
|
3377
|
+
var mdxCommand = program.command("mdx").description("MDX developer tooling");
|
|
3378
|
+
mdxCommand.command("validate").description("Validate MDX files for syntax, forbidden patterns, and component usage").option("--file <path>", "Validate a single file").option("--dir <dir>", "Validate all .mdx files under a directory", "src").action((options) => mdxValidate(options));
|
|
3379
|
+
mdxCommand.command("doctor").description("Check consistency between MDX component definitions and implementations").option("--definitions-dir <dir>", "Definitions directory", "cmx/components").option("--components-dir <dir>", "Component implementation directory", "src/components/custom").option("--index-file <path>", "Component export index file", "src/components/custom/index.ts").action(
|
|
3380
|
+
(options) => mdxDoctor(options)
|
|
3381
|
+
);
|
|
1974
3382
|
program.command("update-collection").description("Update an existing collection").requiredOption("--slug <slug>", "Current slug of the collection").option("--json <json>", "JSON string with update data").option("--new-slug <slug>", "New slug").option("--name <name>", "New name").option("--description <description>", "New description").action(updateCollection);
|
|
1975
3383
|
program.command("delete-collection").description("Delete a collection").requiredOption("--slug <slug>", "Collection slug").option("--force", "Skip confirmation prompt").option("--cascade", "Also delete associated content").action(deleteCollection);
|
|
1976
3384
|
program.command("update-data-type").description("Update an existing data type").requiredOption("--slug <slug>", "Current slug of the data type").option("--json <json>", "JSON string with update data").option("--new-slug <slug>", "New slug").option("--name <name>", "New name").option("--description <description>", "New description").action(updateDataType);
|
|
@@ -1982,22 +3390,6 @@ program.command("add-collection-data-type").description("Add a data type to a co
|
|
|
1982
3390
|
program.command("remove-collection-data-type").description("Remove a data type from a collection").requiredOption("--collection <slug>", "Collection slug").requiredOption("--data-type <slug>", "Data type slug to remove").action(removeCollectionDataType);
|
|
1983
3391
|
program.command("link-collection-data-type").description("Link an existing global data type to a collection").requiredOption("--collection <slug>", "Collection slug").requiredOption("--data-type <slug>", "Global data type slug to link").requiredOption("--field-slug <slug>", "Reference field slug").requiredOption("--label <label>", "Reference field display label").option("--reference-type <type>", "Reference type: single or multiple (default: single)", "single").action(linkCollectionDataType);
|
|
1984
3392
|
program.command("list-collection-presets").description("List available presets for collection data types").option("--type <type>", "Collection type (post, news, doc, page)").action(listCollectionPresets);
|
|
1985
|
-
program.command("scaffold").description("Generate Next.js pages from CMX schema").option("--app-dir <dir>", "App directory path (default: app/(public))").option("--dry-run", "Preview without writing files").option("--force", "Overwrite existing files").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").action((options) => {
|
|
1986
|
-
const apiUrl = process.env.CMX_API_URL;
|
|
1987
|
-
const apiKey = process.env.CMX_API_KEY;
|
|
1988
|
-
if (!apiUrl || !apiKey) {
|
|
1989
|
-
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
1990
|
-
process.exit(1);
|
|
1991
|
-
}
|
|
1992
|
-
return scaffold({
|
|
1993
|
-
apiUrl,
|
|
1994
|
-
apiKey,
|
|
1995
|
-
appDir: options.appDir || "app/(public)",
|
|
1996
|
-
dryRun: options.dryRun,
|
|
1997
|
-
force: options.force,
|
|
1998
|
-
only: options.only
|
|
1999
|
-
});
|
|
2000
|
-
});
|
|
2001
3393
|
program.command("create-data-entry").description("Create a new data entry").requiredOption("--type-slug <slug>", "Data type slug").option("--json <json>", "JSON string with entry data").option("--file <file>", "Path to JSON file with entry data").action(createDataEntry);
|
|
2002
3394
|
program.command("list-data-entries").description("List data entries for a data type").requiredOption("--type-slug <slug>", "Data type slug").option("--sort-by <field>", "Sort by field").option("--sort-order <order>", "Sort order (asc or desc)").option("--limit <number>", "Maximum number of entries").option("--offset <number>", "Number of entries to skip").option("--status <status>", "Filter by status (draft or published)").action(listDataEntries);
|
|
2003
3395
|
program.command("get-data-entry").description("Get a single data entry").requiredOption("--type-slug <slug>", "Data type slug").requiredOption("--id <id>", "Data entry ID").action(getDataEntry);
|