@momentumcms/core 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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,
@@ -70,6 +71,7 @@ __export(src_exports, {
70
71
  richText: () => richText,
71
72
  row: () => row,
72
73
  select: () => select,
74
+ shouldSyncSchema: () => shouldSyncSchema,
73
75
  slug: () => slug,
74
76
  tabs: () => tabs,
75
77
  text: () => text,
@@ -648,7 +650,8 @@ function defineMomentumConfig(config) {
648
650
  admin: {
649
651
  basePath: config.admin?.basePath ?? "/admin",
650
652
  branding: config.admin?.branding ?? {},
651
- toasts: config.admin?.toasts ?? true
653
+ toasts: config.admin?.toasts ?? true,
654
+ components: config.admin?.components
652
655
  },
653
656
  server: {
654
657
  port: config.server?.port ?? 3e3,
@@ -674,6 +677,15 @@ function defineMomentumConfig(config) {
674
677
  migrations: resolveMigrationConfig(config.migrations)
675
678
  };
676
679
  }
680
+ function shouldSyncSchema(config) {
681
+ const explicit = config.db.syncSchema ?? "auto";
682
+ if (typeof explicit === "boolean")
683
+ return explicit;
684
+ if (!config.migrations)
685
+ return true;
686
+ const mode = resolveMigrationMode(config.migrations.mode);
687
+ return mode !== "migrate";
688
+ }
677
689
  function getDbAdapter(config) {
678
690
  return config.db.adapter;
679
691
  }
@@ -769,6 +781,14 @@ function createSeedHelpers() {
769
781
  }
770
782
  };
771
783
  }
784
+
785
+ // libs/core/src/lib/versions/version.types.ts
786
+ function hasVersionDrafts(collection) {
787
+ const v = collection.versions;
788
+ if (!v || typeof v === "boolean")
789
+ return false;
790
+ return !!v.drafts;
791
+ }
772
792
  // Annotate the CommonJS export names for ESM import in node:
773
793
  0 && (module.exports = {
774
794
  LAYOUT_FIELD_TYPES,
@@ -802,6 +822,7 @@ function createSeedHelpers() {
802
822
  hasAllRoles,
803
823
  hasAnyRole,
804
824
  hasRole,
825
+ hasVersionDrafts,
805
826
  humanizeFieldName,
806
827
  isAuthenticated,
807
828
  isLayoutField,
@@ -821,6 +842,7 @@ function createSeedHelpers() {
821
842
  richText,
822
843
  row,
823
844
  select,
845
+ shouldSyncSchema,
824
846
  slug,
825
847
  tabs,
826
848
  text,
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,
@@ -593,6 +594,15 @@ function defineMomentumConfig(config) {
593
594
  migrations: resolveMigrationConfig(config.migrations)
594
595
  };
595
596
  }
597
+ function shouldSyncSchema(config) {
598
+ const explicit = config.db.syncSchema ?? "auto";
599
+ if (typeof explicit === "boolean")
600
+ return explicit;
601
+ if (!config.migrations)
602
+ return true;
603
+ const mode = resolveMigrationMode(config.migrations.mode);
604
+ return mode !== "migrate";
605
+ }
596
606
  function getDbAdapter(config) {
597
607
  return config.db.adapter;
598
608
  }
@@ -688,6 +698,14 @@ function createSeedHelpers() {
688
698
  }
689
699
  };
690
700
  }
701
+
702
+ // libs/core/src/lib/versions/version.types.ts
703
+ function hasVersionDrafts(collection) {
704
+ const v = collection.versions;
705
+ if (!v || typeof v === "boolean")
706
+ return false;
707
+ return !!v.drafts;
708
+ }
691
709
  export {
692
710
  LAYOUT_FIELD_TYPES,
693
711
  MIN_PASSWORD_LENGTH,
@@ -720,6 +738,7 @@ export {
720
738
  hasAllRoles,
721
739
  hasAnyRole,
722
740
  hasRole,
741
+ hasVersionDrafts,
723
742
  humanizeFieldName,
724
743
  isAuthenticated,
725
744
  isLayoutField,
@@ -739,6 +758,7 @@ export {
739
758
  richText,
740
759
  row,
741
760
  select,
761
+ shouldSyncSchema,
742
762
  slug,
743
763
  tabs,
744
764
  text,
@@ -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 */
@@ -193,6 +193,50 @@ export interface DatabaseConfig {
193
193
  * Use @momentumcms/db-drizzle for Drizzle ORM support.
194
194
  */
195
195
  adapter: DatabaseAdapter;
196
+ /**
197
+ * Whether to auto-sync the database schema on server start.
198
+ *
199
+ * - `true`: Always run `adapter.initialize()` (CREATE TABLE IF NOT EXISTS).
200
+ * - `false`: Never auto-sync — expect migrations to be run separately.
201
+ * - `'auto'`: Sync when no migration config or in `push` mode;
202
+ * skip when migration mode is `migrate`.
203
+ *
204
+ * @default 'auto'
205
+ */
206
+ syncSchema?: boolean | 'auto';
207
+ }
208
+ /**
209
+ * Admin component overrides and layout slot registrations.
210
+ * Used in `AdminPanelConfig.components` for global overrides
211
+ * and in `MomentumPlugin.adminComponents` for plugin-level overrides.
212
+ *
213
+ * Loaders use `() => Promise<unknown>` so this interface stays
214
+ * framework-agnostic (core is `env:universal`). The admin package
215
+ * casts to `Type<unknown>` at registration time.
216
+ */
217
+ export interface AdminComponentsConfig {
218
+ /** Replace the dashboard page */
219
+ dashboard?: () => Promise<unknown>;
220
+ /** Replace the login page */
221
+ login?: () => Promise<unknown>;
222
+ /** Replace the media library page */
223
+ media?: () => Promise<unknown>;
224
+ /** Slot: before navigation links in sidebar */
225
+ beforeNavigation?: () => Promise<unknown>;
226
+ /** Slot: after navigation links in sidebar */
227
+ afterNavigation?: () => Promise<unknown>;
228
+ /** Slot: global header (between mobile header and main content) */
229
+ header?: () => Promise<unknown>;
230
+ /** Slot: global footer (after main content) */
231
+ footer?: () => Promise<unknown>;
232
+ /** Slot: before dashboard content */
233
+ beforeDashboard?: () => Promise<unknown>;
234
+ /** Slot: after dashboard content */
235
+ afterDashboard?: () => Promise<unknown>;
236
+ /** Slot: before login form */
237
+ beforeLogin?: () => Promise<unknown>;
238
+ /** Slot: after login form */
239
+ afterLogin?: () => Promise<unknown>;
196
240
  }
197
241
  /**
198
242
  * Global admin panel configuration.
@@ -219,6 +263,12 @@ export interface AdminPanelConfig {
219
263
  * @default true
220
264
  */
221
265
  toasts?: boolean;
266
+ /**
267
+ * Custom admin component overrides and layout slots.
268
+ * Register page replacements (dashboard, login, media) and
269
+ * slot components (header, footer, beforeDashboard, etc.).
270
+ */
271
+ components?: AdminComponentsConfig;
222
272
  }
223
273
  /**
224
274
  * Server configuration.
@@ -378,11 +428,16 @@ export type ResolvedSeedingOptions = Required<SeedingOptions>;
378
428
  export interface ResolvedSeedingConfig extends SeedingConfig {
379
429
  options: ResolvedSeedingOptions;
380
430
  }
431
+ /**
432
+ * AdminPanelConfig with primitive defaults resolved (basePath, branding, toasts).
433
+ * `components` remains optional since it has no default.
434
+ */
435
+ export type ResolvedAdminPanelConfig = Required<Omit<AdminPanelConfig, 'components'>> & Pick<AdminPanelConfig, 'components'>;
381
436
  /**
382
437
  * Internal config with resolved defaults.
383
438
  */
384
439
  export interface ResolvedMomentumConfig extends MomentumConfig {
385
- admin: Required<AdminPanelConfig>;
440
+ admin: ResolvedAdminPanelConfig;
386
441
  server: Required<ServerConfig>;
387
442
  seeding?: ResolvedSeedingConfig;
388
443
  logging: ResolvedLoggingConfig;
@@ -415,6 +470,14 @@ export interface ResolvedMomentumConfig extends MomentumConfig {
415
470
  * ```
416
471
  */
417
472
  export declare function defineMomentumConfig(config: MomentumConfig): ResolvedMomentumConfig;
473
+ /**
474
+ * Determine whether the server should auto-sync the database schema on boot.
475
+ *
476
+ * Resolution order:
477
+ * 1. Explicit `db.syncSchema` (true / false) — always wins.
478
+ * 2. `'auto'` (or omitted) — sync unless migration mode is `'migrate'`.
479
+ */
480
+ export declare function shouldSyncSchema(config: MomentumConfig | ResolvedMomentumConfig): boolean;
418
481
  /**
419
482
  * Gets the database adapter from the config.
420
483
  */
@@ -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
  */