cmx-sdk 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/{add-studio-C2L2NYQC.js → add-studio-TLDFTZTX.js} +3 -19
- package/dist/chunk-XPP5MZKG.js +109 -0
- package/dist/cli.js +1515 -117
- package/dist/index.d.ts +138 -31
- package/dist/index.js +33 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-OD2XGZI2.js → init-XUTF5IBZ.js} +1 -1
- package/dist/interactive-menu-BDZOOGQH.js +66 -0
- package/dist/studio-3YGVKWS4.js +7 -0
- package/package.json +1 -1
- package/dist/interactive-menu-BAMWXEKP.js +0 -57
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
studio
|
|
4
|
+
} from "./chunk-XPP5MZKG.js";
|
|
2
5
|
|
|
3
6
|
// src/cli.ts
|
|
4
7
|
import { Command } from "commander";
|
|
@@ -381,10 +384,10 @@ async function confirm(message) {
|
|
|
381
384
|
input: process.stdin,
|
|
382
385
|
output: process.stdout
|
|
383
386
|
});
|
|
384
|
-
return new Promise((
|
|
387
|
+
return new Promise((resolve5) => {
|
|
385
388
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
386
389
|
rl.close();
|
|
387
|
-
|
|
390
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
388
391
|
});
|
|
389
392
|
});
|
|
390
393
|
}
|
|
@@ -426,10 +429,10 @@ async function confirm2(message) {
|
|
|
426
429
|
input: process.stdin,
|
|
427
430
|
output: process.stdout
|
|
428
431
|
});
|
|
429
|
-
return new Promise((
|
|
432
|
+
return new Promise((resolve5) => {
|
|
430
433
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
431
434
|
rl.close();
|
|
432
|
-
|
|
435
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
433
436
|
});
|
|
434
437
|
});
|
|
435
438
|
}
|
|
@@ -471,10 +474,10 @@ async function confirm3(message) {
|
|
|
471
474
|
input: process.stdin,
|
|
472
475
|
output: process.stdout
|
|
473
476
|
});
|
|
474
|
-
return new Promise((
|
|
477
|
+
return new Promise((resolve5) => {
|
|
475
478
|
rl.question(`${message} (y/N): `, (answer) => {
|
|
476
479
|
rl.close();
|
|
477
|
-
|
|
480
|
+
resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
478
481
|
});
|
|
479
482
|
});
|
|
480
483
|
}
|
|
@@ -513,78 +516,199 @@ async function deleteForm(options) {
|
|
|
513
516
|
// src/commands/sync-components.ts
|
|
514
517
|
import { readdirSync, readFileSync as readFileSync2, existsSync } from "fs";
|
|
515
518
|
import { join } from "path";
|
|
516
|
-
import {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
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);
|
|
537
642
|
}
|
|
538
|
-
|
|
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) {
|
|
539
660
|
if (!existsSync(componentsDir)) {
|
|
540
661
|
console.log("No components directory found at cmx/components/");
|
|
541
|
-
return [];
|
|
662
|
+
return { exists: false, components: [] };
|
|
542
663
|
}
|
|
543
664
|
const files = readdirSync(componentsDir).filter((f) => f.endsWith(".json"));
|
|
544
|
-
if (files.length === 0) {
|
|
545
|
-
console.log("No component definition files found");
|
|
546
|
-
return [];
|
|
547
|
-
}
|
|
548
665
|
const components = [];
|
|
549
666
|
for (const file of files) {
|
|
550
667
|
try {
|
|
551
668
|
const filePath = join(componentsDir, file);
|
|
552
669
|
const content = readFileSync2(filePath, "utf-8");
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
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);
|
|
556
679
|
}
|
|
557
|
-
if (
|
|
558
|
-
console.
|
|
559
|
-
|
|
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);
|
|
560
683
|
}
|
|
561
|
-
|
|
562
|
-
component.examples = component.examples || [];
|
|
563
|
-
components.push(component);
|
|
684
|
+
components.push(result.data);
|
|
564
685
|
} catch (error) {
|
|
565
686
|
console.error(` Error reading ${file}:`, error instanceof Error ? error.message : error);
|
|
687
|
+
process.exit(1);
|
|
566
688
|
}
|
|
567
689
|
}
|
|
568
|
-
return components;
|
|
690
|
+
return { exists: true, components };
|
|
569
691
|
}
|
|
570
|
-
async function syncComponents(
|
|
692
|
+
async function syncComponents() {
|
|
571
693
|
const apiUrl = process.env.CMX_API_URL;
|
|
572
694
|
const apiKey = process.env.CMX_API_KEY;
|
|
573
695
|
if (!apiUrl || !apiKey) {
|
|
574
696
|
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
575
697
|
process.exit(1);
|
|
576
698
|
}
|
|
577
|
-
const environment = options.environment || detectEnvironment();
|
|
578
699
|
const componentsDir = join(process.cwd(), "cmx/components");
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (components.length === 0) {
|
|
700
|
+
const { exists, components } = readComponentDefinitions(componentsDir);
|
|
701
|
+
if (!exists) {
|
|
582
702
|
console.log("No components to sync");
|
|
583
703
|
return;
|
|
584
704
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
}
|
|
588
712
|
}
|
|
589
713
|
console.log(`Syncing to ${apiUrl}...`);
|
|
590
714
|
const result = await manageApiFetch(
|
|
@@ -597,6 +721,7 @@ async function syncComponents(options) {
|
|
|
597
721
|
console.log(`Success: ${result.message || "Components synced"}`);
|
|
598
722
|
if (result.created) console.log(` Created: ${result.created}`);
|
|
599
723
|
if (result.updated) console.log(` Updated: ${result.updated}`);
|
|
724
|
+
if (result.deleted) console.log(` Deleted: ${result.deleted}`);
|
|
600
725
|
}
|
|
601
726
|
|
|
602
727
|
// src/codegen/generator.ts
|
|
@@ -719,7 +844,7 @@ function generateDataTypeFile(dataType) {
|
|
|
719
844
|
const interfaceName = singularize(pascalPlural);
|
|
720
845
|
const slug = dataType.slug;
|
|
721
846
|
const lines = [];
|
|
722
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
847
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
723
848
|
lines.push(`// Data Type: ${dataType.name} (${slug})`);
|
|
724
849
|
lines.push(`// Do not edit manually.`);
|
|
725
850
|
lines.push(``);
|
|
@@ -774,7 +899,7 @@ function generateCollectionFile(collection) {
|
|
|
774
899
|
const slug = collection.slug;
|
|
775
900
|
const lines = [];
|
|
776
901
|
const escapedName = escapeTs(collection.name);
|
|
777
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
902
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
778
903
|
lines.push(`// Collection: ${escapedName} (${slug})`);
|
|
779
904
|
lines.push(`// Do not edit manually.`);
|
|
780
905
|
lines.push(``);
|
|
@@ -860,7 +985,7 @@ function generateFormFile(form) {
|
|
|
860
985
|
const slug = form.slug;
|
|
861
986
|
const lines = [];
|
|
862
987
|
const escapedName = escapeTs(form.name);
|
|
863
|
-
lines.push(`// Auto-generated by cmx-sdk
|
|
988
|
+
lines.push(`// Auto-generated by cmx-sdk codegen types`);
|
|
864
989
|
lines.push(`// Form: ${escapedName} (${slug})`);
|
|
865
990
|
lines.push(`// Do not edit manually.`);
|
|
866
991
|
lines.push(``);
|
|
@@ -892,7 +1017,7 @@ function generateFormFile(form) {
|
|
|
892
1017
|
// src/codegen/generate-index.ts
|
|
893
1018
|
function generateDataTypesIndex(slugs) {
|
|
894
1019
|
const lines = [
|
|
895
|
-
`// Auto-generated by cmx-sdk
|
|
1020
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
896
1021
|
`// Do not edit manually.`,
|
|
897
1022
|
``
|
|
898
1023
|
];
|
|
@@ -904,7 +1029,7 @@ function generateDataTypesIndex(slugs) {
|
|
|
904
1029
|
}
|
|
905
1030
|
function generateCollectionsIndex(slugs) {
|
|
906
1031
|
const lines = [
|
|
907
|
-
`// Auto-generated by cmx-sdk
|
|
1032
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
908
1033
|
`// Do not edit manually.`,
|
|
909
1034
|
``
|
|
910
1035
|
];
|
|
@@ -916,7 +1041,7 @@ function generateCollectionsIndex(slugs) {
|
|
|
916
1041
|
}
|
|
917
1042
|
function generateFormsIndex(slugs) {
|
|
918
1043
|
const lines = [
|
|
919
|
-
`// Auto-generated by cmx-sdk
|
|
1044
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
920
1045
|
`// Do not edit manually.`,
|
|
921
1046
|
``
|
|
922
1047
|
];
|
|
@@ -928,7 +1053,7 @@ function generateFormsIndex(slugs) {
|
|
|
928
1053
|
}
|
|
929
1054
|
function generateRootIndex(hasDataTypes, hasCollections, hasForms) {
|
|
930
1055
|
const lines = [
|
|
931
|
-
`// Auto-generated by cmx-sdk
|
|
1056
|
+
`// Auto-generated by cmx-sdk codegen types`,
|
|
932
1057
|
`// Do not edit manually.`,
|
|
933
1058
|
``
|
|
934
1059
|
];
|
|
@@ -1253,7 +1378,7 @@ async function linkCollectionDataType(opts) {
|
|
|
1253
1378
|
// src/codegen/scaffolder.ts
|
|
1254
1379
|
import { existsSync as existsSync2 } from "fs";
|
|
1255
1380
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1256
|
-
import { join as join3,
|
|
1381
|
+
import { dirname, join as join3, resolve } from "path";
|
|
1257
1382
|
|
|
1258
1383
|
// src/codegen/scaffold-collection.ts
|
|
1259
1384
|
function scaffoldCollectionListPage(collection) {
|
|
@@ -1261,7 +1386,7 @@ function scaffoldCollectionListPage(collection) {
|
|
|
1261
1386
|
const pascal = slugToPascalCase(safeSlug);
|
|
1262
1387
|
const escapedName = escapeTs(collection.name);
|
|
1263
1388
|
const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
|
|
1264
|
-
return `import { get${pascal}Contents } from "
|
|
1389
|
+
return `import { get${pascal}Contents } from "@cmx/generated"
|
|
1265
1390
|
import Link from "next/link"
|
|
1266
1391
|
import type { Metadata } from "next"
|
|
1267
1392
|
|
|
@@ -1294,7 +1419,7 @@ export default async function ${pascal}Page() {
|
|
|
1294
1419
|
function scaffoldCollectionDetailPage(collection) {
|
|
1295
1420
|
const safeSlug = sanitizeIdentifier(collection.slug);
|
|
1296
1421
|
const pascal = slugToPascalCase(safeSlug);
|
|
1297
|
-
return `import { get${pascal}Contents, get${pascal}ContentDetail } from "
|
|
1422
|
+
return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
|
|
1298
1423
|
import { renderMdx } from "cmx-sdk"
|
|
1299
1424
|
import type { Metadata } from "next"
|
|
1300
1425
|
|
|
@@ -1340,8 +1465,8 @@ function scaffoldDataTypePage(dataType) {
|
|
|
1340
1465
|
const interfaceName = singularize(pascalPlural);
|
|
1341
1466
|
const slug = dataType.slug;
|
|
1342
1467
|
const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
|
|
1343
|
-
return `import { get${pascalPlural} } from "
|
|
1344
|
-
import type { ${interfaceName} } from "
|
|
1468
|
+
return `import { get${pascalPlural} } from "@cmx/generated"
|
|
1469
|
+
import type { ${interfaceName} } from "@cmx/generated"
|
|
1345
1470
|
import type { Metadata } from "next"
|
|
1346
1471
|
|
|
1347
1472
|
export const metadata: Metadata = {
|
|
@@ -1377,7 +1502,7 @@ function scaffoldFormPage(form) {
|
|
|
1377
1502
|
const pascal = slugToPascalCase(safeSlug);
|
|
1378
1503
|
const camel = slugToCamelCase(safeSlug);
|
|
1379
1504
|
return `import { ${pascal}Form } from "./_components/${safeSlug}-form"
|
|
1380
|
-
import { ${camel}Schema, submit${pascal} } from "
|
|
1505
|
+
import { ${camel}Schema, submit${pascal} } from "@cmx/generated"
|
|
1381
1506
|
import type { Metadata } from "next"
|
|
1382
1507
|
|
|
1383
1508
|
export const metadata: Metadata = {
|
|
@@ -1424,7 +1549,7 @@ function scaffoldFormComponent(form) {
|
|
|
1424
1549
|
|
|
1425
1550
|
import { useForm } from "react-hook-form"
|
|
1426
1551
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
1427
|
-
import { ${camel}Schema, type ${pascal}FormData } from "
|
|
1552
|
+
import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
|
|
1428
1553
|
import { useState } from "react"
|
|
1429
1554
|
|
|
1430
1555
|
type Props = {
|
|
@@ -1497,12 +1622,393 @@ function getInputType(fieldType) {
|
|
|
1497
1622
|
}
|
|
1498
1623
|
}
|
|
1499
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
|
+
|
|
1500
2006
|
// src/codegen/scaffold-seo.ts
|
|
1501
2007
|
function scaffoldSitemap(collections, siteUrl) {
|
|
1502
2008
|
const imports = collections.map((col) => {
|
|
1503
2009
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
1504
2010
|
const pascal = slugToPascalCase(safeSlug);
|
|
1505
|
-
return `import { get${pascal}Contents } from "
|
|
2011
|
+
return `import { get${pascal}Contents } from "@cmx/generated"`;
|
|
1506
2012
|
}).join("\n");
|
|
1507
2013
|
const fetchBlocks = collections.map((col) => {
|
|
1508
2014
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
@@ -1535,9 +2041,11 @@ ${fetchBlocks}
|
|
|
1535
2041
|
// src/codegen/scaffolder.ts
|
|
1536
2042
|
async function writeScaffoldFile(filePath, content, result, options) {
|
|
1537
2043
|
const resolvedPath = resolve(filePath);
|
|
1538
|
-
const
|
|
1539
|
-
if (!resolvedPath.startsWith(
|
|
1540
|
-
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
|
+
);
|
|
1541
2049
|
}
|
|
1542
2050
|
if (!options.force && existsSync2(filePath)) {
|
|
1543
2051
|
result.skipped.push(filePath);
|
|
@@ -1552,12 +2060,49 @@ async function writeScaffoldFile(filePath, content, result, options) {
|
|
|
1552
2060
|
result.created.push(filePath);
|
|
1553
2061
|
}
|
|
1554
2062
|
function parseOnly(only) {
|
|
1555
|
-
const [
|
|
1556
|
-
if (!
|
|
1557
|
-
|
|
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
|
+
);
|
|
1558
2100
|
}
|
|
1559
2101
|
async function scaffold(options) {
|
|
1560
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;
|
|
1561
2106
|
console.log(`Fetching schema from ${apiUrl} ...`);
|
|
1562
2107
|
const schema = await fetchSchema(apiUrl, apiKey);
|
|
1563
2108
|
const forms = schema.forms ?? [];
|
|
@@ -1573,10 +2118,77 @@ async function scaffold(options) {
|
|
|
1573
2118
|
for (const col of collections) {
|
|
1574
2119
|
const safeSlug = sanitizeIdentifier(col.slug);
|
|
1575
2120
|
if (!safeSlug) continue;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
+
}
|
|
1580
2192
|
}
|
|
1581
2193
|
}
|
|
1582
2194
|
if (!filter || filter.category === "data-types") {
|
|
@@ -1584,8 +2196,38 @@ async function scaffold(options) {
|
|
|
1584
2196
|
for (const dt of dataTypes) {
|
|
1585
2197
|
const safeSlug = sanitizeIdentifier(dt.slug);
|
|
1586
2198
|
if (!safeSlug) continue;
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
+
}
|
|
1589
2231
|
}
|
|
1590
2232
|
}
|
|
1591
2233
|
if (!filter || filter.category === "forms") {
|
|
@@ -1593,17 +2235,56 @@ async function scaffold(options) {
|
|
|
1593
2235
|
for (const form of filteredForms) {
|
|
1594
2236
|
const safeSlug = sanitizeIdentifier(form.slug);
|
|
1595
2237
|
if (!safeSlug) continue;
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
+
}
|
|
1600
2277
|
}
|
|
1601
2278
|
}
|
|
1602
2279
|
if (!filter) {
|
|
1603
2280
|
const sitemapPath = join3(appDir, "sitemap.ts");
|
|
1604
2281
|
if (schema.collections.length > 0) {
|
|
1605
2282
|
const siteUrl = apiUrl.replace(/\/api\/.*$/, "");
|
|
1606
|
-
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
|
+
});
|
|
1607
2288
|
}
|
|
1608
2289
|
}
|
|
1609
2290
|
console.log("");
|
|
@@ -1625,8 +2306,13 @@ Skipped ${result.skipped.length} file(s) (already exist):`);
|
|
|
1625
2306
|
}
|
|
1626
2307
|
console.log("");
|
|
1627
2308
|
console.log("Next steps:");
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
+
}
|
|
1630
2316
|
console.log(" 3. Run `npm run dev` to preview your site");
|
|
1631
2317
|
}
|
|
1632
2318
|
|
|
@@ -1907,10 +2593,10 @@ async function deleteDataEntry(options) {
|
|
|
1907
2593
|
input: process.stdin,
|
|
1908
2594
|
output: process.stdout
|
|
1909
2595
|
});
|
|
1910
|
-
const answer = await new Promise((
|
|
2596
|
+
const answer = await new Promise((resolve5) => {
|
|
1911
2597
|
rl.question(
|
|
1912
2598
|
`Are you sure you want to delete data entry ${options.id}? (yes/no): `,
|
|
1913
|
-
|
|
2599
|
+
resolve5
|
|
1914
2600
|
);
|
|
1915
2601
|
});
|
|
1916
2602
|
rl.close();
|
|
@@ -1930,22 +2616,695 @@ async function deleteDataEntry(options) {
|
|
|
1930
2616
|
console.log(JSON.stringify(result, null, 2));
|
|
1931
2617
|
}
|
|
1932
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
|
+
function stripJsonComments(input) {
|
|
2737
|
+
return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
2738
|
+
}
|
|
2739
|
+
function stripTrailingCommas(input) {
|
|
2740
|
+
return input.replace(/,\s*([}\]])/g, "$1");
|
|
2741
|
+
}
|
|
2742
|
+
function normalizePathValue(input) {
|
|
2743
|
+
return input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2744
|
+
}
|
|
2745
|
+
function collectFiles(rootDir, extensions) {
|
|
2746
|
+
const files = [];
|
|
2747
|
+
function walk(dir) {
|
|
2748
|
+
const entries = readdirSync2(dir);
|
|
2749
|
+
for (const entry of entries) {
|
|
2750
|
+
const fullPath = join5(dir, entry);
|
|
2751
|
+
const stat = statSync(fullPath);
|
|
2752
|
+
if (stat.isDirectory()) {
|
|
2753
|
+
if (entry === "node_modules" || entry === ".next" || entry === "dist") {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
walk(fullPath);
|
|
2757
|
+
} else if (extensions.has(extname(entry))) {
|
|
2758
|
+
files.push(fullPath);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (existsSync4(rootDir)) {
|
|
2763
|
+
walk(rootDir);
|
|
2764
|
+
}
|
|
2765
|
+
return files;
|
|
2766
|
+
}
|
|
2767
|
+
function extractImports(code) {
|
|
2768
|
+
const imports = /* @__PURE__ */ new Set();
|
|
2769
|
+
const fromPattern = /from\s+["']([^"']+)["']/g;
|
|
2770
|
+
let match;
|
|
2771
|
+
while ((match = fromPattern.exec(code)) !== null) {
|
|
2772
|
+
imports.add(match[1]);
|
|
2773
|
+
}
|
|
2774
|
+
const dynamicImportPattern = /import\(\s*["']([^"']+)["']\s*\)/g;
|
|
2775
|
+
while ((match = dynamicImportPattern.exec(code)) !== null) {
|
|
2776
|
+
imports.add(match[1]);
|
|
2777
|
+
}
|
|
2778
|
+
return Array.from(imports);
|
|
2779
|
+
}
|
|
2780
|
+
function resolveGeneratedImport(outputDirAbs, importPath) {
|
|
2781
|
+
if (importPath === "@cmx/generated") {
|
|
2782
|
+
return [join5(outputDirAbs, "index.ts")];
|
|
2783
|
+
}
|
|
2784
|
+
const subPath = importPath.replace(/^@cmx\/generated\//, "");
|
|
2785
|
+
return [
|
|
2786
|
+
join5(outputDirAbs, `${subPath}.ts`),
|
|
2787
|
+
join5(outputDirAbs, subPath, "index.ts")
|
|
2788
|
+
];
|
|
2789
|
+
}
|
|
2790
|
+
async function runCodegenCheck(options = {}) {
|
|
2791
|
+
const projectRoot = resolve2(options.projectRoot ?? process.cwd());
|
|
2792
|
+
const outputDirAbs = resolve2(projectRoot, options.outputDir ?? "cmx/generated");
|
|
2793
|
+
const srcDirAbs = resolve2(projectRoot, options.srcDir ?? "src");
|
|
2794
|
+
const tsconfigAbs = resolve2(projectRoot, options.tsconfigPath ?? "tsconfig.json");
|
|
2795
|
+
const errors = [];
|
|
2796
|
+
const warnings = [];
|
|
2797
|
+
const checks = [];
|
|
2798
|
+
if (!existsSync4(outputDirAbs)) {
|
|
2799
|
+
errors.push(`Generated output directory not found: ${relative(projectRoot, outputDirAbs)}`);
|
|
2800
|
+
} else {
|
|
2801
|
+
checks.push(`Generated directory exists: ${relative(projectRoot, outputDirAbs)}`);
|
|
2802
|
+
const indexPath = join5(outputDirAbs, "index.ts");
|
|
2803
|
+
if (!existsSync4(indexPath)) {
|
|
2804
|
+
errors.push(`Generated index not found: ${relative(projectRoot, indexPath)}`);
|
|
2805
|
+
} else {
|
|
2806
|
+
checks.push(`Generated index exists: ${relative(projectRoot, indexPath)}`);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (!existsSync4(tsconfigAbs)) {
|
|
2810
|
+
errors.push(`tsconfig not found: ${relative(projectRoot, tsconfigAbs)}`);
|
|
2811
|
+
} else {
|
|
2812
|
+
try {
|
|
2813
|
+
const raw = readFileSync4(tsconfigAbs, "utf-8");
|
|
2814
|
+
const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(raw)));
|
|
2815
|
+
const aliasValues = parsed.compilerOptions?.paths?.["@cmx/*"] ?? [];
|
|
2816
|
+
if (aliasValues.length === 0) {
|
|
2817
|
+
errors.push("tsconfig compilerOptions.paths['@cmx/*'] is missing");
|
|
2818
|
+
} else {
|
|
2819
|
+
const normalizedAliasValues = aliasValues.map(normalizePathValue);
|
|
2820
|
+
if (!normalizedAliasValues.includes("cmx/*")) {
|
|
2821
|
+
errors.push(
|
|
2822
|
+
"tsconfig compilerOptions.paths['@cmx/*'] must include './cmx/*' (or equivalent)"
|
|
2823
|
+
);
|
|
2824
|
+
} else {
|
|
2825
|
+
checks.push("tsconfig '@cmx/*' alias is configured");
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
const includeValues = parsed.include ?? [];
|
|
2829
|
+
const normalizedIncludeValues = includeValues.map(normalizePathValue);
|
|
2830
|
+
if (!normalizedIncludeValues.includes("cmx/**/*.ts")) {
|
|
2831
|
+
warnings.push("tsconfig include does not contain 'cmx/**/*.ts'");
|
|
2832
|
+
} else {
|
|
2833
|
+
checks.push("tsconfig include contains 'cmx/**/*.ts'");
|
|
2834
|
+
}
|
|
2835
|
+
} catch (error) {
|
|
2836
|
+
errors.push(`Failed to parse tsconfig: ${error instanceof Error ? error.message : String(error)}`);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (!existsSync4(srcDirAbs)) {
|
|
2840
|
+
warnings.push(`Source directory not found: ${relative(projectRoot, srcDirAbs)}`);
|
|
2841
|
+
} else {
|
|
2842
|
+
const sourceFiles = collectFiles(srcDirAbs, /* @__PURE__ */ new Set([".ts", ".tsx"]));
|
|
2843
|
+
const legacyImportHits = [];
|
|
2844
|
+
const generatedImportHits = [];
|
|
2845
|
+
for (const sourceFile of sourceFiles) {
|
|
2846
|
+
const code = readFileSync4(sourceFile, "utf-8");
|
|
2847
|
+
const imports = extractImports(code);
|
|
2848
|
+
for (const importPath of imports) {
|
|
2849
|
+
if (importPath.startsWith("@/cmx/generated")) {
|
|
2850
|
+
legacyImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
|
|
2851
|
+
}
|
|
2852
|
+
if (importPath === "@cmx/generated" || importPath.startsWith("@cmx/generated/")) {
|
|
2853
|
+
generatedImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
|
|
2854
|
+
const candidates = resolveGeneratedImport(outputDirAbs, importPath);
|
|
2855
|
+
for (const candidate of candidates) {
|
|
2856
|
+
if (existsSync4(candidate)) {
|
|
2857
|
+
break;
|
|
2858
|
+
}
|
|
2859
|
+
const isLast = candidate === candidates.at(-1);
|
|
2860
|
+
if (isLast) {
|
|
2861
|
+
errors.push(
|
|
2862
|
+
`Missing generated target for import '${importPath}' referenced in ${relative(projectRoot, sourceFile)}`
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (legacyImportHits.length > 0) {
|
|
2870
|
+
errors.push("Legacy '@/cmx/generated' imports were found:");
|
|
2871
|
+
for (const hit of legacyImportHits) {
|
|
2872
|
+
errors.push(` - ${hit}`);
|
|
2873
|
+
}
|
|
2874
|
+
} else {
|
|
2875
|
+
checks.push("No legacy '@/cmx/generated' imports found");
|
|
2876
|
+
}
|
|
2877
|
+
if (generatedImportHits.length === 0) {
|
|
2878
|
+
warnings.push("No '@cmx/generated' imports found under src/ (this may be expected for empty projects)");
|
|
2879
|
+
} else {
|
|
2880
|
+
checks.push(`Found ${generatedImportHits.length} '@cmx/generated' import(s) under src/`);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
success: errors.length === 0,
|
|
2885
|
+
errors,
|
|
2886
|
+
warnings,
|
|
2887
|
+
checks
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
async function codegenCheck(options = {}) {
|
|
2891
|
+
const projectRoot = resolve2(options.projectRoot ?? process.cwd());
|
|
2892
|
+
const result = await runCodegenCheck(options);
|
|
2893
|
+
console.log("Codegen check:");
|
|
2894
|
+
for (const check of result.checks) {
|
|
2895
|
+
console.log(` \u2713 ${check}`);
|
|
2896
|
+
}
|
|
2897
|
+
for (const warning of result.warnings) {
|
|
2898
|
+
console.warn(` \u26A0 ${warning}`);
|
|
2899
|
+
}
|
|
2900
|
+
for (const error of result.errors) {
|
|
2901
|
+
console.error(` \u2717 ${error}`);
|
|
2902
|
+
}
|
|
2903
|
+
if (!result.success) {
|
|
2904
|
+
console.error(`
|
|
2905
|
+
Codegen check failed in ${projectRoot}`);
|
|
2906
|
+
process.exit(1);
|
|
2907
|
+
}
|
|
2908
|
+
console.log("\nCodegen check passed");
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// src/commands/mdx-validate.ts
|
|
2912
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2913
|
+
import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
|
|
2914
|
+
|
|
2915
|
+
// src/mdx/validator.ts
|
|
2916
|
+
import { compile } from "@mdx-js/mdx";
|
|
2917
|
+
var FORBIDDEN_PATTERNS = [
|
|
2918
|
+
// import/export statements
|
|
2919
|
+
{ pattern: /^\s*import\s+/m, message: "import\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2920
|
+
{ pattern: /^\s*export\s+/m, message: "export\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2921
|
+
// JS expressions in curly braces (except component props)
|
|
2922
|
+
{ pattern: /\{[^}]*(?:=>|function|new\s+|typeof|instanceof)[^}]*\}/m, message: "JavaScript\u5F0F\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2923
|
+
// eval, Function constructor
|
|
2924
|
+
{ pattern: /\beval\s*\(/m, message: "eval\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2925
|
+
{ pattern: /\bnew\s+Function\s*\(/m, message: "Function constructor\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2926
|
+
// script tags
|
|
2927
|
+
{ pattern: /<script[\s>]/i, message: "script\u30BF\u30B0\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2928
|
+
// on* event handlers
|
|
2929
|
+
{ pattern: /\bon\w+\s*=/i, message: "\u30A4\u30D9\u30F3\u30C8\u30CF\u30F3\u30C9\u30E9\u5C5E\u6027\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2930
|
+
// javascript: URLs
|
|
2931
|
+
{ pattern: /javascript\s*:/i, message: "javascript: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
|
|
2932
|
+
// data: URLs (for images - can be XSS vector)
|
|
2933
|
+
{ pattern: /src\s*=\s*["']?\s*data:/i, message: "data: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" }
|
|
2934
|
+
];
|
|
2935
|
+
function extractComponents(mdx) {
|
|
2936
|
+
const components = [];
|
|
2937
|
+
const lines = mdx.split("\n");
|
|
2938
|
+
const componentPattern = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*\/?>/g;
|
|
2939
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2940
|
+
const line = lines[i];
|
|
2941
|
+
let match;
|
|
2942
|
+
while ((match = componentPattern.exec(line)) !== null) {
|
|
2943
|
+
const name = match[1];
|
|
2944
|
+
const propsString = match[2];
|
|
2945
|
+
const props = parseProps(propsString);
|
|
2946
|
+
components.push({
|
|
2947
|
+
name,
|
|
2948
|
+
props,
|
|
2949
|
+
line: i + 1
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
return components;
|
|
2954
|
+
}
|
|
2955
|
+
function parseProps(propsString) {
|
|
2956
|
+
const props = {};
|
|
2957
|
+
const propPattern = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/g;
|
|
2958
|
+
let match;
|
|
2959
|
+
while ((match = propPattern.exec(propsString)) !== null) {
|
|
2960
|
+
const key = match[1];
|
|
2961
|
+
const stringValue = match[2] ?? match[3];
|
|
2962
|
+
const expressionValue = match[4];
|
|
2963
|
+
if (stringValue !== void 0) {
|
|
2964
|
+
props[key] = stringValue;
|
|
2965
|
+
} else if (expressionValue !== void 0) {
|
|
2966
|
+
const trimmed = expressionValue.trim();
|
|
2967
|
+
if (trimmed === "true") {
|
|
2968
|
+
props[key] = true;
|
|
2969
|
+
} else if (trimmed === "false") {
|
|
2970
|
+
props[key] = false;
|
|
2971
|
+
} else if (/^\d+$/.test(trimmed)) {
|
|
2972
|
+
props[key] = parseInt(trimmed, 10);
|
|
2973
|
+
} else if (/^\d+\.\d+$/.test(trimmed)) {
|
|
2974
|
+
props[key] = parseFloat(trimmed);
|
|
2975
|
+
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2976
|
+
try {
|
|
2977
|
+
props[key] = JSON.parse(trimmed.replace(/'/g, '"'));
|
|
2978
|
+
} catch {
|
|
2979
|
+
props[key] = trimmed;
|
|
2980
|
+
}
|
|
2981
|
+
} else {
|
|
2982
|
+
props[key] = trimmed;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
const booleanPattern = /(?:^|\s)(\w+)(?=\s|\/|>|$)/g;
|
|
2987
|
+
while ((match = booleanPattern.exec(propsString)) !== null) {
|
|
2988
|
+
const key = match[1];
|
|
2989
|
+
if (!(key in props)) {
|
|
2990
|
+
props[key] = true;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
return props;
|
|
2994
|
+
}
|
|
2995
|
+
function extractReferences(components) {
|
|
2996
|
+
const contentIds = [];
|
|
2997
|
+
const assetIds = [];
|
|
2998
|
+
for (const component of components) {
|
|
2999
|
+
if (component.name === "BlogCard" && typeof component.props.contentId === "string") {
|
|
3000
|
+
contentIds.push(component.props.contentId);
|
|
3001
|
+
}
|
|
3002
|
+
if (component.name === "Image" && typeof component.props.assetId === "string") {
|
|
3003
|
+
assetIds.push(component.props.assetId);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return {
|
|
3007
|
+
contentIds: [...new Set(contentIds)],
|
|
3008
|
+
assetIds: [...new Set(assetIds)]
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
async function validateMdx(mdx) {
|
|
3012
|
+
const errors = [];
|
|
3013
|
+
const warnings = [];
|
|
3014
|
+
for (const { pattern, message } of FORBIDDEN_PATTERNS) {
|
|
3015
|
+
if (pattern.test(mdx)) {
|
|
3016
|
+
const lines = mdx.split("\n");
|
|
3017
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3018
|
+
if (pattern.test(lines[i])) {
|
|
3019
|
+
errors.push({
|
|
3020
|
+
type: "forbidden",
|
|
3021
|
+
message,
|
|
3022
|
+
line: i + 1
|
|
3023
|
+
});
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
try {
|
|
3030
|
+
await compile(mdx, {
|
|
3031
|
+
development: false
|
|
3032
|
+
// Minimal compilation to check syntax
|
|
3033
|
+
});
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
const err = error;
|
|
3036
|
+
errors.push({
|
|
3037
|
+
type: "syntax",
|
|
3038
|
+
message: err.message,
|
|
3039
|
+
line: err.line,
|
|
3040
|
+
column: err.column
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
const components = extractComponents(mdx);
|
|
3044
|
+
for (const component of components) {
|
|
3045
|
+
if (!isValidComponent(component.name)) {
|
|
3046
|
+
errors.push({
|
|
3047
|
+
type: "component",
|
|
3048
|
+
message: `\u672A\u77E5\u306E\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8: ${component.name}`,
|
|
3049
|
+
line: component.line,
|
|
3050
|
+
component: component.name
|
|
3051
|
+
});
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
const result = validateComponentProps(component.name, component.props);
|
|
3055
|
+
if (!result.success) {
|
|
3056
|
+
for (const issue of result.error.issues) {
|
|
3057
|
+
errors.push({
|
|
3058
|
+
type: "props",
|
|
3059
|
+
message: `${component.name}: ${issue.path.join(".")} - ${issue.message}`,
|
|
3060
|
+
line: component.line,
|
|
3061
|
+
component: component.name
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const references = extractReferences(components);
|
|
3067
|
+
return {
|
|
3068
|
+
valid: errors.length === 0,
|
|
3069
|
+
errors,
|
|
3070
|
+
warnings,
|
|
3071
|
+
references
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// src/commands/mdx-validate.ts
|
|
3076
|
+
function collectMdxFiles(rootDir) {
|
|
3077
|
+
const files = [];
|
|
3078
|
+
function walk(dir) {
|
|
3079
|
+
const entries = readdirSync3(dir);
|
|
3080
|
+
for (const entry of entries) {
|
|
3081
|
+
const fullPath = join6(dir, entry);
|
|
3082
|
+
const stat = statSync2(fullPath);
|
|
3083
|
+
if (stat.isDirectory()) {
|
|
3084
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
walk(fullPath);
|
|
3088
|
+
} else if (extname2(entry) === ".mdx") {
|
|
3089
|
+
files.push(fullPath);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
if (existsSync5(rootDir)) {
|
|
3094
|
+
walk(rootDir);
|
|
3095
|
+
}
|
|
3096
|
+
return files;
|
|
3097
|
+
}
|
|
3098
|
+
async function mdxValidate(options) {
|
|
3099
|
+
const projectRoot = process.cwd();
|
|
3100
|
+
const inputFile = options.file ? resolve3(projectRoot, options.file) : null;
|
|
3101
|
+
const inputDir = resolve3(projectRoot, options.dir ?? "src");
|
|
3102
|
+
const targets = [];
|
|
3103
|
+
if (inputFile) {
|
|
3104
|
+
if (!existsSync5(inputFile)) {
|
|
3105
|
+
console.error(`Error: file not found: ${options.file}`);
|
|
3106
|
+
process.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
targets.push(inputFile);
|
|
3109
|
+
} else {
|
|
3110
|
+
const dirFiles = collectMdxFiles(inputDir);
|
|
3111
|
+
targets.push(...dirFiles);
|
|
3112
|
+
}
|
|
3113
|
+
if (targets.length === 0) {
|
|
3114
|
+
console.error("No MDX files found to validate");
|
|
3115
|
+
process.exit(1);
|
|
3116
|
+
}
|
|
3117
|
+
let invalidCount = 0;
|
|
3118
|
+
let warningCount = 0;
|
|
3119
|
+
for (const filePath of targets) {
|
|
3120
|
+
const mdx = readFileSync5(filePath, "utf-8");
|
|
3121
|
+
const result = await validateMdx(mdx);
|
|
3122
|
+
const relPath = relative2(projectRoot, filePath);
|
|
3123
|
+
if (result.valid) {
|
|
3124
|
+
console.log(`\u2713 ${relPath}`);
|
|
3125
|
+
} else {
|
|
3126
|
+
invalidCount++;
|
|
3127
|
+
console.log(`\u2717 ${relPath}`);
|
|
3128
|
+
for (const err of result.errors) {
|
|
3129
|
+
const location = err.line ? `:${err.line}${err.column ? `:${err.column}` : ""}` : "";
|
|
3130
|
+
console.log(` - [${err.type}]${location} ${err.message}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
if (result.warnings.length > 0) {
|
|
3134
|
+
warningCount += result.warnings.length;
|
|
3135
|
+
for (const warning of result.warnings) {
|
|
3136
|
+
console.log(` ! [warning] ${warning}`);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
console.log("");
|
|
3141
|
+
console.log(`Validated ${targets.length} file(s)`);
|
|
3142
|
+
console.log(`Invalid: ${invalidCount}`);
|
|
3143
|
+
console.log(`Warnings: ${warningCount}`);
|
|
3144
|
+
if (invalidCount > 0) {
|
|
3145
|
+
process.exit(1);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/commands/mdx-doctor.ts
|
|
3150
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
3151
|
+
import { basename, extname as extname3, join as join7, relative as relative3, resolve as resolve4 } from "path";
|
|
3152
|
+
function collectFiles2(rootDir, extension) {
|
|
3153
|
+
const files = [];
|
|
3154
|
+
function walk(dir) {
|
|
3155
|
+
const entries = readdirSync4(dir);
|
|
3156
|
+
for (const entry of entries) {
|
|
3157
|
+
const fullPath = join7(dir, entry);
|
|
3158
|
+
const stat = statSync3(fullPath);
|
|
3159
|
+
if (stat.isDirectory()) {
|
|
3160
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
walk(fullPath);
|
|
3164
|
+
} else if (extname3(entry) === extension) {
|
|
3165
|
+
files.push(fullPath);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (existsSync6(rootDir)) {
|
|
3170
|
+
walk(rootDir);
|
|
3171
|
+
}
|
|
3172
|
+
return files;
|
|
3173
|
+
}
|
|
3174
|
+
function parseDefinitionFiles(projectRoot, files) {
|
|
3175
|
+
const definitions = [];
|
|
3176
|
+
const errors = [];
|
|
3177
|
+
for (const file of files) {
|
|
3178
|
+
try {
|
|
3179
|
+
const raw = readFileSync6(file, "utf-8");
|
|
3180
|
+
const parsed = JSON.parse(raw);
|
|
3181
|
+
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
3182
|
+
errors.push(`${relative3(projectRoot, file)}: missing string 'name'`);
|
|
3183
|
+
continue;
|
|
3184
|
+
}
|
|
3185
|
+
definitions.push({ name: parsed.name, file });
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
errors.push(
|
|
3188
|
+
`${relative3(projectRoot, file)}: invalid JSON (${error instanceof Error ? error.message : String(error)})`
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
return { definitions, errors };
|
|
3193
|
+
}
|
|
3194
|
+
function collectExportedComponentNames(indexContent) {
|
|
3195
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3196
|
+
const namedExportPattern = /export\s+\{\s*([A-Za-z0-9_]+)(?:\s+as\s+[A-Za-z0-9_]+)?\s*\}\s+from\s+["']\.\/?([^"']+)["']/g;
|
|
3197
|
+
let match;
|
|
3198
|
+
while ((match = namedExportPattern.exec(indexContent)) !== null) {
|
|
3199
|
+
exported.add(match[1]);
|
|
3200
|
+
const pathBase = basename(match[2]);
|
|
3201
|
+
exported.add(pathBase);
|
|
3202
|
+
}
|
|
3203
|
+
const exportAllPattern = /export\s+\*\s+from\s+["']\.\/?([^"']+)["']/g;
|
|
3204
|
+
while ((match = exportAllPattern.exec(indexContent)) !== null) {
|
|
3205
|
+
exported.add(basename(match[1]));
|
|
3206
|
+
}
|
|
3207
|
+
return exported;
|
|
3208
|
+
}
|
|
3209
|
+
async function mdxDoctor(options = {}) {
|
|
3210
|
+
const projectRoot = process.cwd();
|
|
3211
|
+
const definitionsDir = resolve4(projectRoot, options.definitionsDir ?? "cmx/components");
|
|
3212
|
+
const componentsDir = resolve4(projectRoot, options.componentsDir ?? "src/components/custom");
|
|
3213
|
+
const indexFile = resolve4(projectRoot, options.indexFile ?? join7(options.componentsDir ?? "src/components/custom", "index.ts"));
|
|
3214
|
+
const issues = [];
|
|
3215
|
+
const checks = [];
|
|
3216
|
+
if (!existsSync6(definitionsDir)) {
|
|
3217
|
+
issues.push(`Definitions directory not found: ${relative3(projectRoot, definitionsDir)}`);
|
|
3218
|
+
}
|
|
3219
|
+
if (!existsSync6(componentsDir)) {
|
|
3220
|
+
issues.push(`Components directory not found: ${relative3(projectRoot, componentsDir)}`);
|
|
3221
|
+
}
|
|
3222
|
+
if (issues.length > 0) {
|
|
3223
|
+
for (const issue of issues) {
|
|
3224
|
+
console.error(`\u2717 ${issue}`);
|
|
3225
|
+
}
|
|
3226
|
+
process.exit(1);
|
|
3227
|
+
}
|
|
3228
|
+
const definitionFiles = collectFiles2(definitionsDir, ".json");
|
|
3229
|
+
const implFiles = collectFiles2(componentsDir, ".tsx").filter((f) => basename(f) !== "index.tsx");
|
|
3230
|
+
checks.push(`Found ${definitionFiles.length} definition file(s)`);
|
|
3231
|
+
checks.push(`Found ${implFiles.length} implementation file(s)`);
|
|
3232
|
+
const { definitions, errors } = parseDefinitionFiles(projectRoot, definitionFiles);
|
|
3233
|
+
for (const error of errors) {
|
|
3234
|
+
issues.push(error);
|
|
3235
|
+
}
|
|
3236
|
+
const definitionNames = new Set(definitions.map((d) => d.name));
|
|
3237
|
+
const implementationNames = new Set(
|
|
3238
|
+
implFiles.map((f) => basename(f, ".tsx")).filter((name) => name !== "index")
|
|
3239
|
+
);
|
|
3240
|
+
for (const def of definitions) {
|
|
3241
|
+
if (isStandardComponentName(def.name)) {
|
|
3242
|
+
issues.push(`${relative3(projectRoot, def.file)}: '${def.name}' is reserved as a standard component`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
for (const defName of definitionNames) {
|
|
3246
|
+
if (!implementationNames.has(defName)) {
|
|
3247
|
+
issues.push(`Definition exists but implementation is missing: ${defName}.tsx`);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
for (const implName of implementationNames) {
|
|
3251
|
+
if (!definitionNames.has(implName)) {
|
|
3252
|
+
issues.push(`Implementation exists but definition is missing: ${implName}.json`);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (!existsSync6(indexFile)) {
|
|
3256
|
+
issues.push(`Export index not found: ${relative3(projectRoot, indexFile)}`);
|
|
3257
|
+
} else {
|
|
3258
|
+
const indexContent = readFileSync6(indexFile, "utf-8");
|
|
3259
|
+
const exportedNames = collectExportedComponentNames(indexContent);
|
|
3260
|
+
for (const implName of implementationNames) {
|
|
3261
|
+
if (!exportedNames.has(implName)) {
|
|
3262
|
+
issues.push(`Implementation is not exported in ${relative3(projectRoot, indexFile)}: ${implName}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
checks.push(`Checked exports in ${relative3(projectRoot, indexFile)}`);
|
|
3266
|
+
}
|
|
3267
|
+
console.log("MDX doctor:");
|
|
3268
|
+
for (const check of checks) {
|
|
3269
|
+
console.log(` \u2713 ${check}`);
|
|
3270
|
+
}
|
|
3271
|
+
if (issues.length > 0) {
|
|
3272
|
+
for (const issue of issues) {
|
|
3273
|
+
console.error(` \u2717 ${issue}`);
|
|
3274
|
+
}
|
|
3275
|
+
console.error(`
|
|
3276
|
+
Doctor found ${issues.length} issue(s)`);
|
|
3277
|
+
process.exit(1);
|
|
3278
|
+
}
|
|
3279
|
+
console.log("\nDoctor checks passed");
|
|
3280
|
+
}
|
|
3281
|
+
|
|
1933
3282
|
// src/cli.ts
|
|
1934
3283
|
config();
|
|
1935
3284
|
var program = new Command();
|
|
1936
|
-
program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.
|
|
3285
|
+
program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.2.3").allowExcessArguments(false);
|
|
3286
|
+
function requireApiCredentials() {
|
|
3287
|
+
const apiUrl = process.env.CMX_API_URL;
|
|
3288
|
+
const apiKey = process.env.CMX_API_KEY;
|
|
3289
|
+
if (!apiUrl || !apiKey) {
|
|
3290
|
+
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
3291
|
+
process.exit(1);
|
|
3292
|
+
}
|
|
3293
|
+
return { apiUrl, apiKey };
|
|
3294
|
+
}
|
|
1937
3295
|
program.action(async () => {
|
|
1938
|
-
const { interactiveMenu } = await import("./interactive-menu-
|
|
3296
|
+
const { interactiveMenu } = await import("./interactive-menu-BDZOOGQH.js");
|
|
1939
3297
|
await interactiveMenu();
|
|
1940
3298
|
});
|
|
1941
3299
|
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) => {
|
|
1942
|
-
const { init } = await import("./init-
|
|
3300
|
+
const { init } = await import("./init-XUTF5IBZ.js");
|
|
1943
3301
|
await init(projectName, options);
|
|
1944
3302
|
});
|
|
1945
3303
|
program.command("add-studio").description("\u65E2\u5B58\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306B CMX Studio \u3092\u8FFD\u52A0").action(async () => {
|
|
1946
|
-
const { addStudio } = await import("./add-studio-
|
|
3304
|
+
const { addStudio } = await import("./add-studio-TLDFTZTX.js");
|
|
1947
3305
|
await addStudio();
|
|
1948
3306
|
});
|
|
3307
|
+
program.command("studio").description("\u30B5\u30A4\u30C8 (4000) \u3068 Studio (4001) \u3092\u540C\u6642\u8D77\u52D5").action(studio);
|
|
1949
3308
|
program.command("list-collections").description("List all collections").action(listCollections);
|
|
1950
3309
|
program.command("list-data-types").description("List all data types").action(listDataTypes);
|
|
1951
3310
|
program.command("list-forms").description("List all form definitions").action(listForms);
|
|
@@ -1953,20 +3312,75 @@ program.command("list-components").description("List all custom components").act
|
|
|
1953
3312
|
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);
|
|
1954
3313
|
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);
|
|
1955
3314
|
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);
|
|
1956
|
-
program.command("sync-components").description("Sync custom components").
|
|
1957
|
-
program.command("
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
process.exit(1);
|
|
1963
|
-
}
|
|
1964
|
-
return generate({
|
|
3315
|
+
program.command("sync-components").description("Sync custom components").action(syncComponents);
|
|
3316
|
+
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));
|
|
3317
|
+
var codegenCommand = program.command("codegen").description("Generate typed code, page scaffolds, and run compatibility checks");
|
|
3318
|
+
codegenCommand.command("types").description("Generate TypeScript types and fetch helpers from CMX schema").option("--output <dir>", "Output directory", "cmx/generated").action(async (options) => {
|
|
3319
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3320
|
+
await generate({
|
|
1965
3321
|
apiUrl,
|
|
1966
3322
|
apiKey,
|
|
1967
|
-
outputDir: options.output
|
|
3323
|
+
outputDir: options.output
|
|
1968
3324
|
});
|
|
1969
3325
|
});
|
|
3326
|
+
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(
|
|
3327
|
+
async (options) => {
|
|
3328
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3329
|
+
await scaffold({
|
|
3330
|
+
apiUrl,
|
|
3331
|
+
apiKey,
|
|
3332
|
+
appDir: options.appDir,
|
|
3333
|
+
featuresDir: options.featuresDir,
|
|
3334
|
+
template: options.template,
|
|
3335
|
+
dryRun: options.dryRun,
|
|
3336
|
+
force: options.force,
|
|
3337
|
+
only: options.only
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
);
|
|
3341
|
+
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(
|
|
3342
|
+
async (options) => {
|
|
3343
|
+
await codegenCheck({
|
|
3344
|
+
projectRoot: options.projectRoot,
|
|
3345
|
+
outputDir: options.outputDir,
|
|
3346
|
+
srcDir: options.srcDir,
|
|
3347
|
+
tsconfigPath: options.tsconfig
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
);
|
|
3351
|
+
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(
|
|
3352
|
+
async (options) => {
|
|
3353
|
+
const { apiUrl, apiKey } = requireApiCredentials();
|
|
3354
|
+
console.log("\n[1/3] codegen types");
|
|
3355
|
+
await generate({
|
|
3356
|
+
apiUrl,
|
|
3357
|
+
apiKey,
|
|
3358
|
+
outputDir: options.output
|
|
3359
|
+
});
|
|
3360
|
+
console.log("\n[2/3] codegen pages");
|
|
3361
|
+
await scaffold({
|
|
3362
|
+
apiUrl,
|
|
3363
|
+
apiKey,
|
|
3364
|
+
appDir: options.appDir,
|
|
3365
|
+
featuresDir: options.featuresDir,
|
|
3366
|
+
template: options.template,
|
|
3367
|
+
dryRun: options.dryRun,
|
|
3368
|
+
force: options.force,
|
|
3369
|
+
only: options.only
|
|
3370
|
+
});
|
|
3371
|
+
console.log("\n[3/3] codegen check");
|
|
3372
|
+
await codegenCheck({
|
|
3373
|
+
outputDir: options.output,
|
|
3374
|
+
srcDir: options.srcDir,
|
|
3375
|
+
tsconfigPath: options.tsconfig
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
);
|
|
3379
|
+
var mdxCommand = program.command("mdx").description("MDX developer tooling");
|
|
3380
|
+
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));
|
|
3381
|
+
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(
|
|
3382
|
+
(options) => mdxDoctor(options)
|
|
3383
|
+
);
|
|
1970
3384
|
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);
|
|
1971
3385
|
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);
|
|
1972
3386
|
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);
|
|
@@ -1978,22 +3392,6 @@ program.command("add-collection-data-type").description("Add a data type to a co
|
|
|
1978
3392
|
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);
|
|
1979
3393
|
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);
|
|
1980
3394
|
program.command("list-collection-presets").description("List available presets for collection data types").option("--type <type>", "Collection type (post, news, doc, page)").action(listCollectionPresets);
|
|
1981
|
-
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) => {
|
|
1982
|
-
const apiUrl = process.env.CMX_API_URL;
|
|
1983
|
-
const apiKey = process.env.CMX_API_KEY;
|
|
1984
|
-
if (!apiUrl || !apiKey) {
|
|
1985
|
-
console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
|
|
1986
|
-
process.exit(1);
|
|
1987
|
-
}
|
|
1988
|
-
return scaffold({
|
|
1989
|
-
apiUrl,
|
|
1990
|
-
apiKey,
|
|
1991
|
-
appDir: options.appDir || "app/(public)",
|
|
1992
|
-
dryRun: options.dryRun,
|
|
1993
|
-
force: options.force,
|
|
1994
|
-
only: options.only
|
|
1995
|
-
});
|
|
1996
|
-
});
|
|
1997
3395
|
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);
|
|
1998
3396
|
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);
|
|
1999
3397
|
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);
|