@momentumcms/core 0.5.3 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Core collection config, fields, hooks, and access control for Momentum CMS",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -465,6 +465,35 @@ ${indent}}`;
465
465
  }
466
466
  return "undefined";
467
467
  }
468
+ function serializeComponentLoaders(components, configPath, outputPath, indent) {
469
+ const entries = [];
470
+ for (const [key, value] of Object.entries(components)) {
471
+ if (typeof value !== "function")
472
+ continue;
473
+ const source = value.toString();
474
+ const importMatch = /(?:import|__vite_ssr_dynamic_import__)\(\s*['"]([^'"]+)['"]\s*\)/.exec(
475
+ source
476
+ );
477
+ if (!importMatch)
478
+ continue;
479
+ const importPath = importMatch[1];
480
+ const abs = (0, import_node_path.resolve)((0, import_node_path.dirname)(configPath), importPath);
481
+ const rel = computeRelativeImport(outputPath, abs + ".ts");
482
+ const memberMatch = /\.then\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*\1(?:\.(\w+)|\[["'](\w+)["']\])\s*\)/.exec(source);
483
+ if (!memberMatch)
484
+ continue;
485
+ const memberName = memberMatch[2] || memberMatch[3];
486
+ const safeKey = needsQuoting2(key) ? safeQuote(key) : key;
487
+ entries.push(
488
+ `${indent} ${safeKey}: () => import(${JSON.stringify(rel)}).then((m) => m.${memberName})`
489
+ );
490
+ }
491
+ if (entries.length === 0)
492
+ return null;
493
+ return `{
494
+ ${entries.join(",\n")},
495
+ ${indent}}`;
496
+ }
468
497
  function serializeField(field, indent = " ") {
469
498
  const props = [];
470
499
  props.push(`${indent}name: ${JSON.stringify(field.name)}`);
@@ -614,7 +643,7 @@ ${indent} }`;
614
643
  ${items},
615
644
  ${indent}]`;
616
645
  }
617
- function serializeCollection(collection, indent = " ") {
646
+ function serializeCollection(collection, indent = " ", configPath, outputPath) {
618
647
  const parts = [];
619
648
  parts.push(`${indent} slug: ${JSON.stringify(collection.slug)}`);
620
649
  if (collection.labels) {
@@ -622,16 +651,37 @@ function serializeCollection(collection, indent = " ") {
622
651
  }
623
652
  parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
624
653
  if (collection.admin) {
625
- const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
654
+ const componentsObj = collection.admin["components"];
655
+ const adminEntries = Object.entries(collection.admin).filter(([k, v]) => v !== void 0 && k !== "components").map(([k, v]) => {
626
656
  if (k === "preview" && typeof v === "function") {
627
657
  const fn = v;
628
658
  return [k, previewFunctionToTemplate(fn, collection.fields)];
629
659
  }
630
660
  return [k, v];
631
661
  }).filter(([, v]) => typeof v !== "function");
632
- if (adminEntries.length > 0) {
633
- const adminObj = Object.fromEntries(adminEntries);
634
- parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
662
+ let componentsStr = null;
663
+ if (componentsObj && typeof componentsObj === "object" && configPath && outputPath) {
664
+ const loaders = {};
665
+ for (const [k, v] of Object.entries(componentsObj)) {
666
+ loaders[k] = v;
667
+ }
668
+ componentsStr = serializeComponentLoaders(loaders, configPath, outputPath, indent + " ");
669
+ }
670
+ if (adminEntries.length > 0 || componentsStr) {
671
+ const adminProps = [];
672
+ if (adminEntries.length > 0) {
673
+ const adminObj = Object.fromEntries(adminEntries);
674
+ for (const [k, v] of Object.entries(adminObj)) {
675
+ const key = needsQuoting2(k) ? safeQuote(k) : k;
676
+ adminProps.push(`${indent} ${key}: ${serializeValue(v, indent + " ")}`);
677
+ }
678
+ }
679
+ if (componentsStr) {
680
+ adminProps.push(`${indent} components: ${componentsStr}`);
681
+ }
682
+ parts.push(`${indent} admin: {
683
+ ${adminProps.join(",\n")},
684
+ ${indent} }`);
635
685
  }
636
686
  }
637
687
  if (collection.auth) {
@@ -691,14 +741,24 @@ function computeRelativeImport(fromFile, toFile) {
691
741
  }
692
742
  return rel;
693
743
  }
694
- function generateAdminConfig(config, typesRelPath) {
744
+ function generateAdminConfig(config, typesRelPath, configPath, outputPath) {
695
745
  const lines = [];
696
746
  const allCollections = resolveAllCollections(config);
697
747
  const globals = config.globals ?? [];
698
748
  const plugins = config.plugins ?? [];
699
- const pluginsWithAdminRoutes = plugins.filter(
700
- (p) => p.browserImports?.adminRoutes && p.adminRoutes && p.adminRoutes.length > 0
701
- );
749
+ const SAFE_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
750
+ const pluginsWithAdminRoutes = plugins.filter((p) => {
751
+ if (!p.browserImports?.adminRoutes || !p.adminRoutes || p.adminRoutes.length === 0)
752
+ return false;
753
+ const exportName = p.browserImports.adminRoutes.exportName;
754
+ if (!SAFE_IDENTIFIER.test(exportName)) {
755
+ console.warn(
756
+ `[generateAdminConfig] Skipping plugin "${p.name}": exportName "${exportName}" is not a valid identifier`
757
+ );
758
+ return false;
759
+ }
760
+ return true;
761
+ });
702
762
  lines.push("/**");
703
763
  lines.push(" * AUTO-GENERATED by @momentumcms/core");
704
764
  lines.push(" * DO NOT EDIT - regenerate with: nx run <app>:generate");
@@ -714,13 +774,13 @@ function generateAdminConfig(config, typesRelPath) {
714
774
  const imp = plugin.browserImports?.adminRoutes;
715
775
  if (!imp)
716
776
  continue;
717
- lines.push(`import { ${imp.exportName} } from '${imp.path}';`);
777
+ lines.push(`import { ${imp.exportName} } from ${JSON.stringify(imp.path)};`);
718
778
  }
719
779
  lines.push("");
720
780
  const genericParams = globals.length > 0 ? "<CollectionSlug, GlobalSlug>" : "<CollectionSlug>";
721
781
  lines.push(`export const adminConfig: MomentumAdminConfig${genericParams} = {`);
722
782
  if (allCollections.length > 0) {
723
- const collectionItems = allCollections.map((c) => ` ${serializeCollection(c, " ")}`).join(",\n");
783
+ const collectionItems = allCollections.map((c) => ` ${serializeCollection(c, " ", configPath, outputPath)}`).join(",\n");
724
784
  lines.push(` collections: [
725
785
  ${collectionItems},
726
786
  ],`);
@@ -734,15 +794,31 @@ ${globalItems},
734
794
  ],`);
735
795
  }
736
796
  if (config.admin) {
737
- const adminObj = {};
738
- if (config.admin.basePath)
739
- adminObj["basePath"] = config.admin.basePath;
740
- if (config.admin.branding)
741
- adminObj["branding"] = config.admin.branding;
742
- if (config.admin.toasts !== void 0)
743
- adminObj["toasts"] = config.admin.toasts;
744
- if (Object.keys(adminObj).length > 0) {
745
- lines.push(` admin: ${serializeValue(adminObj)},`);
797
+ const adminParts = [];
798
+ if (config.admin.basePath) {
799
+ adminParts.push(` basePath: ${JSON.stringify(config.admin.basePath)}`);
800
+ }
801
+ if (config.admin.branding) {
802
+ adminParts.push(` branding: ${serializeValue(config.admin.branding, " ")}`);
803
+ }
804
+ if (config.admin.toasts !== void 0) {
805
+ adminParts.push(` toasts: ${String(config.admin.toasts)}`);
806
+ }
807
+ if (config.admin.components && configPath && outputPath) {
808
+ const componentsStr = serializeComponentLoaders(
809
+ config.admin.components,
810
+ configPath,
811
+ outputPath,
812
+ " "
813
+ );
814
+ if (componentsStr) {
815
+ adminParts.push(` components: ${componentsStr}`);
816
+ }
817
+ }
818
+ if (adminParts.length > 0) {
819
+ lines.push(` admin: {
820
+ ${adminParts.join(",\n")},
821
+ },`);
746
822
  }
747
823
  }
748
824
  if (pluginsWithAdminRoutes.length > 0) {
@@ -812,7 +888,12 @@ async function runGenerator(options) {
812
888
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(typesOutputPath), { recursive: true });
813
889
  (0, import_node_fs.writeFileSync)(typesOutputPath, typesContent, "utf-8");
814
890
  console.info(`Types generated: ${typesOutputPath}`);
815
- const adminConfigContent = generateAdminConfig(config, typesRelPath);
891
+ const adminConfigContent = generateAdminConfig(
892
+ config,
893
+ typesRelPath,
894
+ configPath,
895
+ configOutputPath
896
+ );
816
897
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(configOutputPath), { recursive: true });
817
898
  (0, import_node_fs.writeFileSync)(configOutputPath, adminConfigContent, "utf-8");
818
899
  console.info(`Admin config generated: ${configOutputPath}`);
@@ -149,6 +149,7 @@ interface MomentumConfig {
149
149
  title?: string;
150
150
  };
151
151
  toasts?: boolean;
152
+ components?: Record<string, unknown>;
152
153
  };
153
154
  plugins?: PluginDescriptor[];
154
155
  }
@@ -175,7 +176,7 @@ export declare function serializeField(field: FieldDefinition, indent?: string):
175
176
  /**
176
177
  * Serialize a collection definition, stripping server-only properties.
177
178
  */
178
- export declare function serializeCollection(collection: CollectionDefinition, indent?: string): string;
179
+ export declare function serializeCollection(collection: CollectionDefinition, indent?: string, configPath?: string, outputPath?: string): string;
179
180
  /**
180
181
  * Serialize a global definition, stripping server-only properties.
181
182
  */
@@ -189,7 +190,7 @@ export declare function computeRelativeImport(fromFile: string, toFile: string):
189
190
  * Collections and globals are inlined with server-only properties stripped.
190
191
  * Only plugin admin routes are still imported (they have loadComponent functions).
191
192
  */
192
- export declare function generateAdminConfig(config: MomentumConfig, typesRelPath: string): string;
193
+ export declare function generateAdminConfig(config: MomentumConfig, typesRelPath: string, configPath?: string, outputPath?: string): string;
193
194
  export default function runGenerator(options: GeneratorOptions): Promise<{
194
195
  success: boolean;
195
196
  }>;
@@ -434,6 +434,35 @@ ${indent}}`;
434
434
  }
435
435
  return "undefined";
436
436
  }
437
+ function serializeComponentLoaders(components, configPath, outputPath, indent) {
438
+ const entries = [];
439
+ for (const [key, value] of Object.entries(components)) {
440
+ if (typeof value !== "function")
441
+ continue;
442
+ const source = value.toString();
443
+ const importMatch = /(?:import|__vite_ssr_dynamic_import__)\(\s*['"]([^'"]+)['"]\s*\)/.exec(
444
+ source
445
+ );
446
+ if (!importMatch)
447
+ continue;
448
+ const importPath = importMatch[1];
449
+ const abs = resolve(dirname(configPath), importPath);
450
+ const rel = computeRelativeImport(outputPath, abs + ".ts");
451
+ const memberMatch = /\.then\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*\1(?:\.(\w+)|\[["'](\w+)["']\])\s*\)/.exec(source);
452
+ if (!memberMatch)
453
+ continue;
454
+ const memberName = memberMatch[2] || memberMatch[3];
455
+ const safeKey = needsQuoting2(key) ? safeQuote(key) : key;
456
+ entries.push(
457
+ `${indent} ${safeKey}: () => import(${JSON.stringify(rel)}).then((m) => m.${memberName})`
458
+ );
459
+ }
460
+ if (entries.length === 0)
461
+ return null;
462
+ return `{
463
+ ${entries.join(",\n")},
464
+ ${indent}}`;
465
+ }
437
466
  function serializeField(field, indent = " ") {
438
467
  const props = [];
439
468
  props.push(`${indent}name: ${JSON.stringify(field.name)}`);
@@ -583,7 +612,7 @@ ${indent} }`;
583
612
  ${items},
584
613
  ${indent}]`;
585
614
  }
586
- function serializeCollection(collection, indent = " ") {
615
+ function serializeCollection(collection, indent = " ", configPath, outputPath) {
587
616
  const parts = [];
588
617
  parts.push(`${indent} slug: ${JSON.stringify(collection.slug)}`);
589
618
  if (collection.labels) {
@@ -591,16 +620,37 @@ function serializeCollection(collection, indent = " ") {
591
620
  }
592
621
  parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
593
622
  if (collection.admin) {
594
- const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
623
+ const componentsObj = collection.admin["components"];
624
+ const adminEntries = Object.entries(collection.admin).filter(([k, v]) => v !== void 0 && k !== "components").map(([k, v]) => {
595
625
  if (k === "preview" && typeof v === "function") {
596
626
  const fn = v;
597
627
  return [k, previewFunctionToTemplate(fn, collection.fields)];
598
628
  }
599
629
  return [k, v];
600
630
  }).filter(([, v]) => typeof v !== "function");
601
- if (adminEntries.length > 0) {
602
- const adminObj = Object.fromEntries(adminEntries);
603
- parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
631
+ let componentsStr = null;
632
+ if (componentsObj && typeof componentsObj === "object" && configPath && outputPath) {
633
+ const loaders = {};
634
+ for (const [k, v] of Object.entries(componentsObj)) {
635
+ loaders[k] = v;
636
+ }
637
+ componentsStr = serializeComponentLoaders(loaders, configPath, outputPath, indent + " ");
638
+ }
639
+ if (adminEntries.length > 0 || componentsStr) {
640
+ const adminProps = [];
641
+ if (adminEntries.length > 0) {
642
+ const adminObj = Object.fromEntries(adminEntries);
643
+ for (const [k, v] of Object.entries(adminObj)) {
644
+ const key = needsQuoting2(k) ? safeQuote(k) : k;
645
+ adminProps.push(`${indent} ${key}: ${serializeValue(v, indent + " ")}`);
646
+ }
647
+ }
648
+ if (componentsStr) {
649
+ adminProps.push(`${indent} components: ${componentsStr}`);
650
+ }
651
+ parts.push(`${indent} admin: {
652
+ ${adminProps.join(",\n")},
653
+ ${indent} }`);
604
654
  }
605
655
  }
606
656
  if (collection.auth) {
@@ -660,14 +710,24 @@ function computeRelativeImport(fromFile, toFile) {
660
710
  }
661
711
  return rel;
662
712
  }
663
- function generateAdminConfig(config, typesRelPath) {
713
+ function generateAdminConfig(config, typesRelPath, configPath, outputPath) {
664
714
  const lines = [];
665
715
  const allCollections = resolveAllCollections(config);
666
716
  const globals = config.globals ?? [];
667
717
  const plugins = config.plugins ?? [];
668
- const pluginsWithAdminRoutes = plugins.filter(
669
- (p) => p.browserImports?.adminRoutes && p.adminRoutes && p.adminRoutes.length > 0
670
- );
718
+ const SAFE_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
719
+ const pluginsWithAdminRoutes = plugins.filter((p) => {
720
+ if (!p.browserImports?.adminRoutes || !p.adminRoutes || p.adminRoutes.length === 0)
721
+ return false;
722
+ const exportName = p.browserImports.adminRoutes.exportName;
723
+ if (!SAFE_IDENTIFIER.test(exportName)) {
724
+ console.warn(
725
+ `[generateAdminConfig] Skipping plugin "${p.name}": exportName "${exportName}" is not a valid identifier`
726
+ );
727
+ return false;
728
+ }
729
+ return true;
730
+ });
671
731
  lines.push("/**");
672
732
  lines.push(" * AUTO-GENERATED by @momentumcms/core");
673
733
  lines.push(" * DO NOT EDIT - regenerate with: nx run <app>:generate");
@@ -683,13 +743,13 @@ function generateAdminConfig(config, typesRelPath) {
683
743
  const imp = plugin.browserImports?.adminRoutes;
684
744
  if (!imp)
685
745
  continue;
686
- lines.push(`import { ${imp.exportName} } from '${imp.path}';`);
746
+ lines.push(`import { ${imp.exportName} } from ${JSON.stringify(imp.path)};`);
687
747
  }
688
748
  lines.push("");
689
749
  const genericParams = globals.length > 0 ? "<CollectionSlug, GlobalSlug>" : "<CollectionSlug>";
690
750
  lines.push(`export const adminConfig: MomentumAdminConfig${genericParams} = {`);
691
751
  if (allCollections.length > 0) {
692
- const collectionItems = allCollections.map((c) => ` ${serializeCollection(c, " ")}`).join(",\n");
752
+ const collectionItems = allCollections.map((c) => ` ${serializeCollection(c, " ", configPath, outputPath)}`).join(",\n");
693
753
  lines.push(` collections: [
694
754
  ${collectionItems},
695
755
  ],`);
@@ -703,15 +763,31 @@ ${globalItems},
703
763
  ],`);
704
764
  }
705
765
  if (config.admin) {
706
- const adminObj = {};
707
- if (config.admin.basePath)
708
- adminObj["basePath"] = config.admin.basePath;
709
- if (config.admin.branding)
710
- adminObj["branding"] = config.admin.branding;
711
- if (config.admin.toasts !== void 0)
712
- adminObj["toasts"] = config.admin.toasts;
713
- if (Object.keys(adminObj).length > 0) {
714
- lines.push(` admin: ${serializeValue(adminObj)},`);
766
+ const adminParts = [];
767
+ if (config.admin.basePath) {
768
+ adminParts.push(` basePath: ${JSON.stringify(config.admin.basePath)}`);
769
+ }
770
+ if (config.admin.branding) {
771
+ adminParts.push(` branding: ${serializeValue(config.admin.branding, " ")}`);
772
+ }
773
+ if (config.admin.toasts !== void 0) {
774
+ adminParts.push(` toasts: ${String(config.admin.toasts)}`);
775
+ }
776
+ if (config.admin.components && configPath && outputPath) {
777
+ const componentsStr = serializeComponentLoaders(
778
+ config.admin.components,
779
+ configPath,
780
+ outputPath,
781
+ " "
782
+ );
783
+ if (componentsStr) {
784
+ adminParts.push(` components: ${componentsStr}`);
785
+ }
786
+ }
787
+ if (adminParts.length > 0) {
788
+ lines.push(` admin: {
789
+ ${adminParts.join(",\n")},
790
+ },`);
715
791
  }
716
792
  }
717
793
  if (pluginsWithAdminRoutes.length > 0) {
@@ -781,7 +857,12 @@ async function runGenerator(options) {
781
857
  mkdirSync(dirname(typesOutputPath), { recursive: true });
782
858
  writeFileSync(typesOutputPath, typesContent, "utf-8");
783
859
  console.info(`Types generated: ${typesOutputPath}`);
784
- const adminConfigContent = generateAdminConfig(config, typesRelPath);
860
+ const adminConfigContent = generateAdminConfig(
861
+ config,
862
+ typesRelPath,
863
+ configPath,
864
+ configOutputPath
865
+ );
785
866
  mkdirSync(dirname(configOutputPath), { recursive: true });
786
867
  writeFileSync(configOutputPath, adminConfigContent, "utf-8");
787
868
  console.info(`Admin config generated: ${configOutputPath}`);
package/src/index.cjs CHANGED
@@ -51,6 +51,7 @@ __export(src_exports, {
51
51
  hasAllRoles: () => hasAllRoles,
52
52
  hasAnyRole: () => hasAnyRole,
53
53
  hasRole: () => hasRole,
54
+ hasVersionDrafts: () => hasVersionDrafts,
54
55
  humanizeFieldName: () => humanizeFieldName,
55
56
  isAuthenticated: () => isAuthenticated,
56
57
  isLayoutField: () => isLayoutField,
@@ -648,7 +649,8 @@ function defineMomentumConfig(config) {
648
649
  admin: {
649
650
  basePath: config.admin?.basePath ?? "/admin",
650
651
  branding: config.admin?.branding ?? {},
651
- toasts: config.admin?.toasts ?? true
652
+ toasts: config.admin?.toasts ?? true,
653
+ components: config.admin?.components
652
654
  },
653
655
  server: {
654
656
  port: config.server?.port ?? 3e3,
@@ -769,6 +771,14 @@ function createSeedHelpers() {
769
771
  }
770
772
  };
771
773
  }
774
+
775
+ // libs/core/src/lib/versions/version.types.ts
776
+ function hasVersionDrafts(collection) {
777
+ const v = collection.versions;
778
+ if (!v || typeof v === "boolean")
779
+ return false;
780
+ return !!v.drafts;
781
+ }
772
782
  // Annotate the CommonJS export names for ESM import in node:
773
783
  0 && (module.exports = {
774
784
  LAYOUT_FIELD_TYPES,
@@ -802,6 +812,7 @@ function createSeedHelpers() {
802
812
  hasAllRoles,
803
813
  hasAnyRole,
804
814
  hasRole,
815
+ hasVersionDrafts,
805
816
  humanizeFieldName,
806
817
  isAuthenticated,
807
818
  isLayoutField,
package/src/index.js CHANGED
@@ -567,7 +567,8 @@ function defineMomentumConfig(config) {
567
567
  admin: {
568
568
  basePath: config.admin?.basePath ?? "/admin",
569
569
  branding: config.admin?.branding ?? {},
570
- toasts: config.admin?.toasts ?? true
570
+ toasts: config.admin?.toasts ?? true,
571
+ components: config.admin?.components
571
572
  },
572
573
  server: {
573
574
  port: config.server?.port ?? 3e3,
@@ -688,6 +689,14 @@ function createSeedHelpers() {
688
689
  }
689
690
  };
690
691
  }
692
+
693
+ // libs/core/src/lib/versions/version.types.ts
694
+ function hasVersionDrafts(collection) {
695
+ const v = collection.versions;
696
+ if (!v || typeof v === "boolean")
697
+ return false;
698
+ return !!v.drafts;
699
+ }
691
700
  export {
692
701
  LAYOUT_FIELD_TYPES,
693
702
  MIN_PASSWORD_LENGTH,
@@ -720,6 +729,7 @@ export {
720
729
  hasAllRoles,
721
730
  hasAnyRole,
722
731
  hasRole,
732
+ hasVersionDrafts,
723
733
  humanizeFieldName,
724
734
  isAuthenticated,
725
735
  isLayoutField,
@@ -37,6 +37,8 @@ export interface AccessConfig {
37
37
  publishVersions?: AccessFunction;
38
38
  /** Control who can restore previous versions */
39
39
  restoreVersions?: AccessFunction;
40
+ /** Control who can see draft (unpublished) documents. Falls back to `update` access if not set. */
41
+ readDrafts?: AccessFunction;
40
42
  }
41
43
  export interface HookArgs {
42
44
  req: RequestContext;
@@ -84,6 +86,39 @@ export interface AdminConfig {
84
86
  /** HTTP endpoint path for the action (e.g., '/api/auth/api-keys') */
85
87
  endpoint?: string;
86
88
  }>;
89
+ /**
90
+ * Custom components for this collection's admin pages.
91
+ * Register page replacements and per-collection layout slots.
92
+ */
93
+ components?: CollectionAdminComponentsConfig;
94
+ }
95
+ /**
96
+ * Per-collection admin component overrides and layout slots.
97
+ * Used in `AdminConfig.components` for collection-specific customization.
98
+ *
99
+ * Loaders use `() => Promise<unknown>` to stay framework-agnostic.
100
+ */
101
+ export interface CollectionAdminComponentsConfig {
102
+ /** Replace the list page for this collection */
103
+ list?: () => Promise<unknown>;
104
+ /** Replace the edit page for this collection */
105
+ edit?: () => Promise<unknown>;
106
+ /** Replace the view page for this collection */
107
+ view?: () => Promise<unknown>;
108
+ /** Slot: before the collection list */
109
+ beforeList?: () => Promise<unknown>;
110
+ /** Slot: after the collection list */
111
+ afterList?: () => Promise<unknown>;
112
+ /** Slot: before the edit form */
113
+ beforeEdit?: () => Promise<unknown>;
114
+ /** Slot: after the edit form */
115
+ afterEdit?: () => Promise<unknown>;
116
+ /** Slot: sidebar panel on edit page */
117
+ editSidebar?: () => Promise<unknown>;
118
+ /** Slot: before the view page */
119
+ beforeView?: () => Promise<unknown>;
120
+ /** Slot: after the view page */
121
+ afterView?: () => Promise<unknown>;
87
122
  }
88
123
  export interface VersionsConfig {
89
124
  /** Enable draft versions */
@@ -194,6 +194,39 @@ export interface DatabaseConfig {
194
194
  */
195
195
  adapter: DatabaseAdapter;
196
196
  }
197
+ /**
198
+ * Admin component overrides and layout slot registrations.
199
+ * Used in `AdminPanelConfig.components` for global overrides
200
+ * and in `MomentumPlugin.adminComponents` for plugin-level overrides.
201
+ *
202
+ * Loaders use `() => Promise<unknown>` so this interface stays
203
+ * framework-agnostic (core is `env:universal`). The admin package
204
+ * casts to `Type<unknown>` at registration time.
205
+ */
206
+ export interface AdminComponentsConfig {
207
+ /** Replace the dashboard page */
208
+ dashboard?: () => Promise<unknown>;
209
+ /** Replace the login page */
210
+ login?: () => Promise<unknown>;
211
+ /** Replace the media library page */
212
+ media?: () => Promise<unknown>;
213
+ /** Slot: before navigation links in sidebar */
214
+ beforeNavigation?: () => Promise<unknown>;
215
+ /** Slot: after navigation links in sidebar */
216
+ afterNavigation?: () => Promise<unknown>;
217
+ /** Slot: global header (between mobile header and main content) */
218
+ header?: () => Promise<unknown>;
219
+ /** Slot: global footer (after main content) */
220
+ footer?: () => Promise<unknown>;
221
+ /** Slot: before dashboard content */
222
+ beforeDashboard?: () => Promise<unknown>;
223
+ /** Slot: after dashboard content */
224
+ afterDashboard?: () => Promise<unknown>;
225
+ /** Slot: before login form */
226
+ beforeLogin?: () => Promise<unknown>;
227
+ /** Slot: after login form */
228
+ afterLogin?: () => Promise<unknown>;
229
+ }
197
230
  /**
198
231
  * Global admin panel configuration.
199
232
  * (Distinct from collection-level AdminConfig)
@@ -219,6 +252,12 @@ export interface AdminPanelConfig {
219
252
  * @default true
220
253
  */
221
254
  toasts?: boolean;
255
+ /**
256
+ * Custom admin component overrides and layout slots.
257
+ * Register page replacements (dashboard, login, media) and
258
+ * slot components (header, footer, beforeDashboard, etc.).
259
+ */
260
+ components?: AdminComponentsConfig;
222
261
  }
223
262
  /**
224
263
  * Server configuration.
@@ -378,11 +417,16 @@ export type ResolvedSeedingOptions = Required<SeedingOptions>;
378
417
  export interface ResolvedSeedingConfig extends SeedingConfig {
379
418
  options: ResolvedSeedingOptions;
380
419
  }
420
+ /**
421
+ * AdminPanelConfig with primitive defaults resolved (basePath, branding, toasts).
422
+ * `components` remains optional since it has no default.
423
+ */
424
+ export type ResolvedAdminPanelConfig = Required<Omit<AdminPanelConfig, 'components'>> & Pick<AdminPanelConfig, 'components'>;
381
425
  /**
382
426
  * Internal config with resolved defaults.
383
427
  */
384
428
  export interface ResolvedMomentumConfig extends MomentumConfig {
385
- admin: Required<AdminPanelConfig>;
429
+ admin: ResolvedAdminPanelConfig;
386
430
  server: Required<ServerConfig>;
387
431
  seeding?: ResolvedSeedingConfig;
388
432
  logging: ResolvedLoggingConfig;
@@ -6,7 +6,7 @@
6
6
  * Runtime implementations (PluginRunner, etc.) live in @momentumcms/plugins/core.
7
7
  */
8
8
  import type { CollectionConfig, UserContext } from './collections';
9
- import type { MomentumConfig } from './config';
9
+ import type { AdminComponentsConfig, MomentumConfig } from './config';
10
10
  /**
11
11
  * Descriptor for Express middleware/routes that a plugin wants auto-mounted.
12
12
  * Plugins register these during onInit via context.registerMiddleware().
@@ -185,6 +185,11 @@ export interface MomentumPlugin {
185
185
  onReady?(context: PluginReadyContext): void | Promise<void>;
186
186
  /** Called on graceful shutdown. Clean up resources. */
187
187
  onShutdown?(context: PluginContext): void | Promise<void>;
188
+ /**
189
+ * Admin component overrides and layout slots registered by this plugin.
190
+ * Merged into the global registries at admin init time.
191
+ */
192
+ adminComponents?: AdminComponentsConfig;
188
193
  /** Browser-safe import descriptors for the admin config generator.
189
194
  * Tells the generator where to import collections, admin routes, and
190
195
  * modifyCollections from browser-safe sub-paths instead of the main
@@ -134,6 +134,16 @@ export interface SchedulePublishResult {
134
134
  /** Scheduled publish date (ISO string) */
135
135
  scheduledPublishAt: string;
136
136
  }
137
+ /**
138
+ * Check if a collection has version drafts enabled.
139
+ * `versions: true` enables versioning but NOT drafts.
140
+ * Only `versions: { drafts: true }` (or `{ drafts: { ... } }`) enables drafts.
141
+ */
142
+ export declare function hasVersionDrafts(collection: {
143
+ versions?: boolean | {
144
+ drafts?: boolean | object;
145
+ };
146
+ }): boolean;
137
147
  /**
138
148
  * Event data passed to version-related hooks.
139
149
  */