@createcms/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -231,12 +231,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
231
231
  allowChildren?: false;
232
232
  } | {
233
233
  allowChildren: true;
234
- allowedChildBlocks?: string[];
235
234
  });
236
235
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
237
236
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
238
237
  properties: TProps;
239
238
  };
239
+ /**
240
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
241
+ * — declares which child block types that parent (or the literal `'root'`) may
242
+ * contain. There are three mutually-exclusive modes, enforced by the type:
243
+ *
244
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
245
+ * entry at all; `'*'` is just an explicit, readable form.)
246
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
247
+ * a block added to the collection later is rejected until listed. `excludes`
248
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
249
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
250
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
251
+ *
252
+ * Whether a parent accepts children AT ALL is the separate, coarser
253
+ * `allowChildren` gate on the block (the root always accepts children); these
254
+ * rules only refine WHICH children an accepting parent may hold.
255
+ */
256
+ type BlockStructureEntry<TBlockName extends string> = {
257
+ /** `'*'` = open base (optional, for readability). */
258
+ accepts?: '*';
259
+ /** Holds anything except these. */
260
+ excludes?: readonly TBlockName[];
261
+ } | {
262
+ /** Holds ONLY these block types. */
263
+ accepts: readonly TBlockName[];
264
+ /**
265
+ * Forbidden alongside a concrete `accepts` list — the list already names
266
+ * exactly what is allowed, so `excludes` would be ignored.
267
+ */
268
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
269
+ };
270
+ /**
271
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
272
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
273
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
274
+ * the collection's block names and are checked at compile time by
275
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
276
+ *
277
+ * This is the single source of truth that the visual editor (drop-zone gating)
278
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
279
+ * alongside each block's `allowChildren` flag, so they can never diverge.
280
+ */
281
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
282
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
283
+ };
240
284
  type SlugConfig = {
241
285
  enabled: false;
242
286
  } | {
@@ -260,6 +304,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
260
304
  * regardless of this flag). Any collection can still be a reference target.
261
305
  */
262
306
  reusableBlock?: boolean;
307
+ /**
308
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
309
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
310
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
311
+ * together with each block's `allowChildren` flag. Open by default; block
312
+ * names are checked at compile time by the field type itself, so a typo is a
313
+ * compile error at the `defineCollection` call site.
314
+ */
315
+ structure?: CollectionStructure<TBlocks>;
263
316
  };
264
317
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
265
318
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -139,12 +139,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
139
139
  allowChildren?: false;
140
140
  } | {
141
141
  allowChildren: true;
142
- allowedChildBlocks?: string[];
143
142
  });
144
143
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
145
144
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
146
145
  properties: TProps;
147
146
  };
147
+ /**
148
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
149
+ * — declares which child block types that parent (or the literal `'root'`) may
150
+ * contain. There are three mutually-exclusive modes, enforced by the type:
151
+ *
152
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
153
+ * entry at all; `'*'` is just an explicit, readable form.)
154
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
155
+ * a block added to the collection later is rejected until listed. `excludes`
156
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
157
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
158
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
159
+ *
160
+ * Whether a parent accepts children AT ALL is the separate, coarser
161
+ * `allowChildren` gate on the block (the root always accepts children); these
162
+ * rules only refine WHICH children an accepting parent may hold.
163
+ */
164
+ type BlockStructureEntry<TBlockName extends string> = {
165
+ /** `'*'` = open base (optional, for readability). */
166
+ accepts?: '*';
167
+ /** Holds anything except these. */
168
+ excludes?: readonly TBlockName[];
169
+ } | {
170
+ /** Holds ONLY these block types. */
171
+ accepts: readonly TBlockName[];
172
+ /**
173
+ * Forbidden alongside a concrete `accepts` list — the list already names
174
+ * exactly what is allowed, so `excludes` would be ignored.
175
+ */
176
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
177
+ };
178
+ /**
179
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
180
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
181
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
182
+ * the collection's block names and are checked at compile time by
183
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
184
+ *
185
+ * This is the single source of truth that the visual editor (drop-zone gating)
186
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
187
+ * alongside each block's `allowChildren` flag, so they can never diverge.
188
+ */
189
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
190
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
191
+ };
148
192
  type SlugConfig = {
149
193
  enabled: false;
150
194
  } | {
@@ -168,6 +212,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
168
212
  * regardless of this flag). Any collection can still be a reference target.
169
213
  */
170
214
  reusableBlock?: boolean;
215
+ /**
216
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
217
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
218
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
219
+ * together with each block's `allowChildren` flag. Open by default; block
220
+ * names are checked at compile time by the field type itself, so a typo is a
221
+ * compile error at the `defineCollection` call site.
222
+ */
223
+ structure?: CollectionStructure<TBlocks>;
171
224
  };
172
225
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
173
226
 
@@ -139,12 +139,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
139
139
  allowChildren?: false;
140
140
  } | {
141
141
  allowChildren: true;
142
- allowedChildBlocks?: string[];
143
142
  });
144
143
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
145
144
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
146
145
  properties: TProps;
147
146
  };
147
+ /**
148
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
149
+ * — declares which child block types that parent (or the literal `'root'`) may
150
+ * contain. There are three mutually-exclusive modes, enforced by the type:
151
+ *
152
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
153
+ * entry at all; `'*'` is just an explicit, readable form.)
154
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
155
+ * a block added to the collection later is rejected until listed. `excludes`
156
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
157
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
158
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
159
+ *
160
+ * Whether a parent accepts children AT ALL is the separate, coarser
161
+ * `allowChildren` gate on the block (the root always accepts children); these
162
+ * rules only refine WHICH children an accepting parent may hold.
163
+ */
164
+ type BlockStructureEntry<TBlockName extends string> = {
165
+ /** `'*'` = open base (optional, for readability). */
166
+ accepts?: '*';
167
+ /** Holds anything except these. */
168
+ excludes?: readonly TBlockName[];
169
+ } | {
170
+ /** Holds ONLY these block types. */
171
+ accepts: readonly TBlockName[];
172
+ /**
173
+ * Forbidden alongside a concrete `accepts` list — the list already names
174
+ * exactly what is allowed, so `excludes` would be ignored.
175
+ */
176
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
177
+ };
178
+ /**
179
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
180
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
181
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
182
+ * the collection's block names and are checked at compile time by
183
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
184
+ *
185
+ * This is the single source of truth that the visual editor (drop-zone gating)
186
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
187
+ * alongside each block's `allowChildren` flag, so they can never diverge.
188
+ */
189
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
190
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
191
+ };
148
192
  type SlugConfig = {
149
193
  enabled: false;
150
194
  } | {
@@ -168,6 +212,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
168
212
  * regardless of this flag). Any collection can still be a reference target.
169
213
  */
170
214
  reusableBlock?: boolean;
215
+ /**
216
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
217
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
218
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
219
+ * together with each block's `allowChildren` flag. Open by default; block
220
+ * names are checked at compile time by the field type itself, so a typo is a
221
+ * compile error at the `defineCollection` call site.
222
+ */
223
+ structure?: CollectionStructure<TBlocks>;
171
224
  };
172
225
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
173
226
 
@@ -150,6 +150,10 @@ const CMS_ERRORS = {
150
150
  status: 404,
151
151
  message: 'Parent block not found'
152
152
  },
153
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
154
+ status: 400,
155
+ message: 'This block type is not allowed inside the target parent'
156
+ },
153
157
  ROOT_NOT_FOUND: {
154
158
  status: 404,
155
159
  message: 'Root block not found in snapshot'
@@ -235,12 +235,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
235
235
  allowChildren?: false;
236
236
  } | {
237
237
  allowChildren: true;
238
- allowedChildBlocks?: string[];
239
238
  });
240
239
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
241
240
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
242
241
  properties: TProps;
243
242
  };
243
+ /**
244
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
245
+ * — declares which child block types that parent (or the literal `'root'`) may
246
+ * contain. There are three mutually-exclusive modes, enforced by the type:
247
+ *
248
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
249
+ * entry at all; `'*'` is just an explicit, readable form.)
250
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
251
+ * a block added to the collection later is rejected until listed. `excludes`
252
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
253
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
254
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
255
+ *
256
+ * Whether a parent accepts children AT ALL is the separate, coarser
257
+ * `allowChildren` gate on the block (the root always accepts children); these
258
+ * rules only refine WHICH children an accepting parent may hold.
259
+ */
260
+ type BlockStructureEntry<TBlockName extends string> = {
261
+ /** `'*'` = open base (optional, for readability). */
262
+ accepts?: '*';
263
+ /** Holds anything except these. */
264
+ excludes?: readonly TBlockName[];
265
+ } | {
266
+ /** Holds ONLY these block types. */
267
+ accepts: readonly TBlockName[];
268
+ /**
269
+ * Forbidden alongside a concrete `accepts` list — the list already names
270
+ * exactly what is allowed, so `excludes` would be ignored.
271
+ */
272
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
273
+ };
274
+ /**
275
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
276
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
277
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
278
+ * the collection's block names and are checked at compile time by
279
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
280
+ *
281
+ * This is the single source of truth that the visual editor (drop-zone gating)
282
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
283
+ * alongside each block's `allowChildren` flag, so they can never diverge.
284
+ */
285
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
286
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
287
+ };
244
288
  type SlugConfig = {
245
289
  enabled: false;
246
290
  } | {
@@ -264,6 +308,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
264
308
  * regardless of this flag). Any collection can still be a reference target.
265
309
  */
266
310
  reusableBlock?: boolean;
311
+ /**
312
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
313
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
314
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
315
+ * together with each block's `allowChildren` flag. Open by default; block
316
+ * names are checked at compile time by the field type itself, so a typo is a
317
+ * compile error at the `defineCollection` call site.
318
+ */
319
+ structure?: CollectionStructure<TBlocks>;
267
320
  };
268
321
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
269
322
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -667,6 +720,10 @@ declare const CMS_ERRORS: {
667
720
  readonly status: 404;
668
721
  readonly message: "Parent block not found";
669
722
  };
723
+ readonly BLOCK_NOT_ALLOWED_IN_PARENT: {
724
+ readonly status: 400;
725
+ readonly message: "This block type is not allowed inside the target parent";
726
+ };
670
727
  readonly ROOT_NOT_FOUND: {
671
728
  readonly status: 404;
672
729
  readonly message: "Root block not found in snapshot";
@@ -235,12 +235,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
235
235
  allowChildren?: false;
236
236
  } | {
237
237
  allowChildren: true;
238
- allowedChildBlocks?: string[];
239
238
  });
240
239
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
241
240
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
242
241
  properties: TProps;
243
242
  };
243
+ /**
244
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
245
+ * — declares which child block types that parent (or the literal `'root'`) may
246
+ * contain. There are three mutually-exclusive modes, enforced by the type:
247
+ *
248
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
249
+ * entry at all; `'*'` is just an explicit, readable form.)
250
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
251
+ * a block added to the collection later is rejected until listed. `excludes`
252
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
253
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
254
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
255
+ *
256
+ * Whether a parent accepts children AT ALL is the separate, coarser
257
+ * `allowChildren` gate on the block (the root always accepts children); these
258
+ * rules only refine WHICH children an accepting parent may hold.
259
+ */
260
+ type BlockStructureEntry<TBlockName extends string> = {
261
+ /** `'*'` = open base (optional, for readability). */
262
+ accepts?: '*';
263
+ /** Holds anything except these. */
264
+ excludes?: readonly TBlockName[];
265
+ } | {
266
+ /** Holds ONLY these block types. */
267
+ accepts: readonly TBlockName[];
268
+ /**
269
+ * Forbidden alongside a concrete `accepts` list — the list already names
270
+ * exactly what is allowed, so `excludes` would be ignored.
271
+ */
272
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
273
+ };
274
+ /**
275
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
276
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
277
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
278
+ * the collection's block names and are checked at compile time by
279
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
280
+ *
281
+ * This is the single source of truth that the visual editor (drop-zone gating)
282
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
283
+ * alongside each block's `allowChildren` flag, so they can never diverge.
284
+ */
285
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
286
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
287
+ };
244
288
  type SlugConfig = {
245
289
  enabled: false;
246
290
  } | {
@@ -264,6 +308,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
264
308
  * regardless of this flag). Any collection can still be a reference target.
265
309
  */
266
310
  reusableBlock?: boolean;
311
+ /**
312
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
313
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
314
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
315
+ * together with each block's `allowChildren` flag. Open by default; block
316
+ * names are checked at compile time by the field type itself, so a typo is a
317
+ * compile error at the `defineCollection` call site.
318
+ */
319
+ structure?: CollectionStructure<TBlocks>;
267
320
  };
268
321
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
269
322
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -667,6 +720,10 @@ declare const CMS_ERRORS: {
667
720
  readonly status: 404;
668
721
  readonly message: "Parent block not found";
669
722
  };
723
+ readonly BLOCK_NOT_ALLOWED_IN_PARENT: {
724
+ readonly status: 400;
725
+ readonly message: "This block type is not allowed inside the target parent";
726
+ };
670
727
  readonly ROOT_NOT_FOUND: {
671
728
  readonly status: 404;
672
729
  readonly message: "Root block not found in snapshot";
@@ -148,6 +148,10 @@ const CMS_ERRORS = {
148
148
  status: 404,
149
149
  message: 'Parent block not found'
150
150
  },
151
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
152
+ status: 400,
153
+ message: 'This block type is not allowed inside the target parent'
154
+ },
151
155
  ROOT_NOT_FOUND: {
152
156
  status: 404,
153
157
  message: 'Root block not found in snapshot'
@@ -153,12 +153,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
153
153
  allowChildren?: false;
154
154
  } | {
155
155
  allowChildren: true;
156
- allowedChildBlocks?: string[];
157
156
  });
158
157
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
159
158
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
160
159
  properties: TProps;
161
160
  };
161
+ /**
162
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
163
+ * — declares which child block types that parent (or the literal `'root'`) may
164
+ * contain. There are three mutually-exclusive modes, enforced by the type:
165
+ *
166
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
167
+ * entry at all; `'*'` is just an explicit, readable form.)
168
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
169
+ * a block added to the collection later is rejected until listed. `excludes`
170
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
171
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
172
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
173
+ *
174
+ * Whether a parent accepts children AT ALL is the separate, coarser
175
+ * `allowChildren` gate on the block (the root always accepts children); these
176
+ * rules only refine WHICH children an accepting parent may hold.
177
+ */
178
+ type BlockStructureEntry<TBlockName extends string> = {
179
+ /** `'*'` = open base (optional, for readability). */
180
+ accepts?: '*';
181
+ /** Holds anything except these. */
182
+ excludes?: readonly TBlockName[];
183
+ } | {
184
+ /** Holds ONLY these block types. */
185
+ accepts: readonly TBlockName[];
186
+ /**
187
+ * Forbidden alongside a concrete `accepts` list — the list already names
188
+ * exactly what is allowed, so `excludes` would be ignored.
189
+ */
190
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
191
+ };
192
+ /**
193
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
194
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
195
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
196
+ * the collection's block names and are checked at compile time by
197
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
198
+ *
199
+ * This is the single source of truth that the visual editor (drop-zone gating)
200
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
201
+ * alongside each block's `allowChildren` flag, so they can never diverge.
202
+ */
203
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
204
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
205
+ };
162
206
  type SlugConfig = {
163
207
  enabled: false;
164
208
  } | {
@@ -182,6 +226,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
182
226
  * regardless of this flag). Any collection can still be a reference target.
183
227
  */
184
228
  reusableBlock?: boolean;
229
+ /**
230
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
231
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
232
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
233
+ * together with each block's `allowChildren` flag. Open by default; block
234
+ * names are checked at compile time by the field type itself, so a typo is a
235
+ * compile error at the `defineCollection` call site.
236
+ */
237
+ structure?: CollectionStructure<TBlocks>;
185
238
  };
186
239
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
187
240
 
@@ -153,12 +153,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
153
153
  allowChildren?: false;
154
154
  } | {
155
155
  allowChildren: true;
156
- allowedChildBlocks?: string[];
157
156
  });
158
157
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
159
158
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
160
159
  properties: TProps;
161
160
  };
161
+ /**
162
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
163
+ * — declares which child block types that parent (or the literal `'root'`) may
164
+ * contain. There are three mutually-exclusive modes, enforced by the type:
165
+ *
166
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
167
+ * entry at all; `'*'` is just an explicit, readable form.)
168
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
169
+ * a block added to the collection later is rejected until listed. `excludes`
170
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
171
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
172
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
173
+ *
174
+ * Whether a parent accepts children AT ALL is the separate, coarser
175
+ * `allowChildren` gate on the block (the root always accepts children); these
176
+ * rules only refine WHICH children an accepting parent may hold.
177
+ */
178
+ type BlockStructureEntry<TBlockName extends string> = {
179
+ /** `'*'` = open base (optional, for readability). */
180
+ accepts?: '*';
181
+ /** Holds anything except these. */
182
+ excludes?: readonly TBlockName[];
183
+ } | {
184
+ /** Holds ONLY these block types. */
185
+ accepts: readonly TBlockName[];
186
+ /**
187
+ * Forbidden alongside a concrete `accepts` list — the list already names
188
+ * exactly what is allowed, so `excludes` would be ignored.
189
+ */
190
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
191
+ };
192
+ /**
193
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
194
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
195
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
196
+ * the collection's block names and are checked at compile time by
197
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
198
+ *
199
+ * This is the single source of truth that the visual editor (drop-zone gating)
200
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
201
+ * alongside each block's `allowChildren` flag, so they can never diverge.
202
+ */
203
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
204
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
205
+ };
162
206
  type SlugConfig = {
163
207
  enabled: false;
164
208
  } | {
@@ -182,6 +226,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
182
226
  * regardless of this flag). Any collection can still be a reference target.
183
227
  */
184
228
  reusableBlock?: boolean;
229
+ /**
230
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
231
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
232
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
233
+ * together with each block's `allowChildren` flag. Open by default; block
234
+ * names are checked at compile time by the field type itself, so a typo is a
235
+ * compile error at the `defineCollection` call site.
236
+ */
237
+ structure?: CollectionStructure<TBlocks>;
185
238
  };
186
239
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
187
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createcms/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "[Work in progress · pre-1.0 · not production-ready] Composable, block-based headless CMS powered by better-call and Drizzle ORM (Postgres). Database-native versioning with Git-like branches, copy-on-write drafts, visual diffs, merges, reusable blocks, nested pages, and a fully type-safe API.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/weepaho3/createCMS#readme",