@wp-typia/project-tools 0.17.0 → 0.18.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.
Files changed (60) hide show
  1. package/dist/runtime/block-generator-service-core.d.ts +1 -1
  2. package/dist/runtime/block-generator-service-core.js +2 -1
  3. package/dist/runtime/block-generator-service-spec.d.ts +2 -1
  4. package/dist/runtime/built-in-block-artifacts.js +1 -0
  5. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  6. package/dist/runtime/built-in-block-code-templates/compound-child.js +30 -2
  7. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +1 -1
  8. package/dist/runtime/built-in-block-code-templates/compound-parent.js +139 -19
  9. package/dist/runtime/cli-add-block.d.ts +2 -1
  10. package/dist/runtime/cli-add-block.js +19 -1
  11. package/dist/runtime/cli-add-shared.d.ts +55 -1
  12. package/dist/runtime/cli-add-shared.js +101 -2
  13. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  14. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  15. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  16. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  17. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  18. package/dist/runtime/cli-add-workspace.js +10 -1
  19. package/dist/runtime/cli-add.d.ts +3 -3
  20. package/dist/runtime/cli-add.js +2 -2
  21. package/dist/runtime/cli-core.d.ts +3 -1
  22. package/dist/runtime/cli-core.js +2 -1
  23. package/dist/runtime/cli-doctor-workspace.js +135 -1
  24. package/dist/runtime/cli-help.js +10 -6
  25. package/dist/runtime/cli-scaffold.d.ts +10 -2
  26. package/dist/runtime/cli-scaffold.js +136 -36
  27. package/dist/runtime/cli-templates.d.ts +4 -4
  28. package/dist/runtime/cli-templates.js +79 -39
  29. package/dist/runtime/index.d.ts +4 -3
  30. package/dist/runtime/index.js +3 -2
  31. package/dist/runtime/local-dev-presets.js +7 -2
  32. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  33. package/dist/runtime/rest-resource-artifacts.js +158 -0
  34. package/dist/runtime/scaffold-answer-resolution.js +68 -2
  35. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  36. package/dist/runtime/scaffold-apply-utils.js +34 -17
  37. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  38. package/dist/runtime/scaffold-bootstrap.js +29 -7
  39. package/dist/runtime/scaffold-documents.js +2 -1
  40. package/dist/runtime/scaffold-onboarding.js +7 -3
  41. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  42. package/dist/runtime/scaffold.d.ts +7 -1
  43. package/dist/runtime/scaffold.js +50 -8
  44. package/dist/runtime/template-render.d.ts +5 -2
  45. package/dist/runtime/template-render.js +9 -3
  46. package/dist/runtime/template-source-contracts.d.ts +11 -0
  47. package/dist/runtime/template-source-external.d.ts +1 -1
  48. package/dist/runtime/template-source-external.js +45 -13
  49. package/dist/runtime/template-source-normalization.d.ts +1 -1
  50. package/dist/runtime/template-source-normalization.js +5 -1
  51. package/dist/runtime/template-source-remote.d.ts +5 -0
  52. package/dist/runtime/template-source-remote.js +33 -0
  53. package/dist/runtime/template-source.js +30 -1
  54. package/dist/runtime/workspace-inventory.d.ts +43 -1
  55. package/dist/runtime/workspace-inventory.js +132 -1
  56. package/dist/runtime/workspace-project.d.ts +1 -1
  57. package/dist/runtime/workspace-project.js +2 -2
  58. package/package.json +3 -3
  59. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +428 -49
  60. package/templates/query-loop/src/validator-toolkit.ts.mustache +0 -1
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  const PROJECT_ROOT = process.cwd();
6
6
 
7
- const ALLOWED_CHILD_MARKER = '// add-child: insert new allowed child block names here';
7
+ const CHILD_SPEC_MARKER = '// add-child: insert new child specs here';
8
8
  const BLOCK_CONFIG_MARKER = '// add-child: insert new block config entries here';
9
9
  const CHILD_PLACEHOLDER = 'Add supporting details for this internal item.';
10
10
  const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
@@ -16,8 +16,10 @@ type StarterManifestDocument = {
16
16
  };
17
17
 
18
18
  type CompoundParentConfig = {
19
+ blockJsonPath: string;
19
20
  blockName: string;
20
21
  namespace: string;
22
+ blocksRoot: string;
21
23
  slug: string;
22
24
  styleImport: string;
23
25
  textDomain: string;
@@ -25,12 +27,27 @@ type CompoundParentConfig = {
25
27
  typeName: string;
26
28
  };
27
29
 
30
+ type ExistingCompoundChild = {
31
+ blockJsonPath: string;
32
+ blockName: string;
33
+ folderSlug: string;
34
+ key: string;
35
+ };
36
+
37
+ type ParsedArgs = {
38
+ ancestorValues: string[];
39
+ container: boolean;
40
+ inserter?: 'hidden' | 'visible';
41
+ slug?: string;
42
+ title?: string;
43
+ };
44
+
28
45
  function parseArgs() {
29
46
  const args = process.argv.slice( 2 );
30
- const parsed: {
31
- slug?: string;
32
- title?: string;
33
- } = {};
47
+ const parsed: ParsedArgs = {
48
+ ancestorValues: [],
49
+ container: false,
50
+ };
34
51
 
35
52
  for ( let index = 0; index < args.length; index += 1 ) {
36
53
  const arg = args[ index ];
@@ -53,6 +70,31 @@ function parseArgs() {
53
70
  index += 1;
54
71
  continue;
55
72
  }
73
+
74
+ if ( arg === '--ancestor' ) {
75
+ const value = args[ index + 1 ];
76
+ if ( ! value || value.startsWith( '--' ) ) {
77
+ throw new Error( '--ancestor requires a value.' );
78
+ }
79
+ parsed.ancestorValues.push( value );
80
+ index += 1;
81
+ continue;
82
+ }
83
+
84
+ if ( arg === '--container' ) {
85
+ parsed.container = true;
86
+ continue;
87
+ }
88
+
89
+ if ( arg === '--inserter' ) {
90
+ const value = args[ index + 1 ];
91
+ if ( value !== 'hidden' && value !== 'visible' ) {
92
+ throw new Error( "--inserter must be either 'hidden' or 'visible'." );
93
+ }
94
+ parsed.inserter = value;
95
+ index += 1;
96
+ continue;
97
+ }
56
98
  }
57
99
 
58
100
  return parsed;
@@ -177,8 +219,10 @@ function resolveCompoundParentConfig(): CompoundParentConfig {
177
219
  : parentSlug;
178
220
 
179
221
  return {
222
+ blockJsonPath,
180
223
  blockName,
181
224
  namespace,
225
+ blocksRoot,
182
226
  slug: parentSlug,
183
227
  styleImport: `../${ parentSlug }/style.scss`,
184
228
  textDomain,
@@ -188,8 +232,10 @@ function resolveCompoundParentConfig(): CompoundParentConfig {
188
232
  }
189
233
 
190
234
  const {
235
+ blockJsonPath: PARENT_BLOCK_JSON_PATH,
191
236
  blockName: PARENT_BLOCK_NAME,
192
237
  namespace: PARENT_BLOCK_NAMESPACE,
238
+ blocksRoot: BLOCKS_ROOT,
193
239
  slug: PARENT_BLOCK_SLUG,
194
240
  styleImport: PARENT_STYLE_IMPORT,
195
241
  textDomain: TEXT_DOMAIN,
@@ -300,50 +346,263 @@ function insertBeforeMarker( filePath: string, marker: string, insertionLines: s
300
346
  );
301
347
  }
302
348
 
349
+ function readBlockJsonDocument(
350
+ filePath: string
351
+ ): Record< string, unknown > {
352
+ return readJsonFile( filePath );
353
+ }
354
+
355
+ function writeBlockJsonDocument(
356
+ filePath: string,
357
+ document: Record< string, unknown >
358
+ ) {
359
+ fs.writeFileSync(
360
+ filePath,
361
+ `${ JSON.stringify( document, null, '\t' ) }\n`,
362
+ 'utf8'
363
+ );
364
+ }
365
+
366
+ function deriveChildKey( folderSlug: string ): string {
367
+ if ( folderSlug.startsWith( `${ PARENT_BLOCK_SLUG }-` ) ) {
368
+ return folderSlug.slice( PARENT_BLOCK_SLUG.length + 1 );
369
+ }
370
+
371
+ return resolveValidatedBlockSlug( folderSlug );
372
+ }
373
+
374
+ function listExistingCompoundChildren(): ExistingCompoundChild[] {
375
+ if ( ! fs.existsSync( BLOCKS_ROOT ) ) {
376
+ return [];
377
+ }
378
+
379
+ return fs
380
+ .readdirSync( BLOCKS_ROOT, { withFileTypes: true } )
381
+ .filter( ( entry ) => entry.isDirectory() )
382
+ .map( ( entry ) => entry.name )
383
+ .filter( ( folderSlug ) => folderSlug !== PARENT_BLOCK_SLUG )
384
+ .filter( ( folderSlug ) => folderSlug.startsWith( `${ PARENT_BLOCK_SLUG }-` ) )
385
+ .map( ( folderSlug ) => {
386
+ const blockJsonPath = path.join( BLOCKS_ROOT, folderSlug, 'block.json' );
387
+ if ( ! fs.existsSync( blockJsonPath ) ) {
388
+ return null;
389
+ }
390
+
391
+ const blockJson = readBlockJsonDocument( blockJsonPath );
392
+ const blockName =
393
+ typeof blockJson.name === 'string' ? blockJson.name.trim() : '';
394
+ if ( blockName.length === 0 ) {
395
+ return null;
396
+ }
397
+
398
+ return {
399
+ blockJsonPath,
400
+ blockName,
401
+ folderSlug,
402
+ key: deriveChildKey( folderSlug ),
403
+ } satisfies ExistingCompoundChild;
404
+ } )
405
+ .filter( ( child ): child is ExistingCompoundChild => child !== null );
406
+ }
407
+
408
+ function resolveExistingCompoundChild(
409
+ value: string,
410
+ existingChildren: ExistingCompoundChild[]
411
+ ): ExistingCompoundChild {
412
+ const trimmedValue = value.trim();
413
+ if ( trimmedValue.length === 0 ) {
414
+ throw new Error( 'Ancestor references must not be empty.' );
415
+ }
416
+
417
+ const normalizedCandidate = resolveValidatedBlockSlug(
418
+ trimmedValue.includes( '/' )
419
+ ? trimmedValue.slice( trimmedValue.lastIndexOf( '/' ) + 1 )
420
+ : trimmedValue
421
+ );
422
+ const resolved = existingChildren.find(
423
+ ( child ) =>
424
+ child.blockName === trimmedValue ||
425
+ child.folderSlug === trimmedValue ||
426
+ child.key === normalizedCandidate ||
427
+ child.folderSlug === `${ PARENT_BLOCK_SLUG }-${ normalizedCandidate }`
428
+ );
429
+
430
+ if ( ! resolved ) {
431
+ throw new Error(
432
+ `Unable to resolve compound child ancestor "${ value }". Use an existing child key, folder slug, or block name.`
433
+ );
434
+ }
435
+
436
+ return resolved;
437
+ }
438
+
439
+ function ensureUniqueAncestorChain(
440
+ ancestors: ExistingCompoundChild[]
441
+ ): ExistingCompoundChild[] {
442
+ const seenKeys = new Set< string >();
443
+
444
+ return ancestors.filter( ( ancestor ) => {
445
+ if ( seenKeys.has( ancestor.key ) ) {
446
+ return false;
447
+ }
448
+
449
+ seenKeys.add( ancestor.key );
450
+ return true;
451
+ } );
452
+ }
453
+
454
+ function validateAncestorChain(
455
+ ancestorChain: ExistingCompoundChild[]
456
+ ) {
457
+ for ( let index = 0; index < ancestorChain.length - 1; index += 1 ) {
458
+ const currentAncestor = ancestorChain[ index ];
459
+ const nextAncestor = ancestorChain[ index + 1 ];
460
+ const blockJson = readBlockJsonDocument( currentAncestor.blockJsonPath );
461
+ const allowedBlocks = Array.isArray( blockJson.allowedBlocks )
462
+ ? blockJson.allowedBlocks.filter(
463
+ ( value ): value is string => typeof value === 'string'
464
+ )
465
+ : [];
466
+
467
+ if ( allowedBlocks.includes( nextAncestor.blockName ) ) {
468
+ continue;
469
+ }
470
+
471
+ throw new Error(
472
+ `Invalid ancestor chain: ${ currentAncestor.blockName } does not currently allow ${ nextAncestor.blockName } as a direct child.`
473
+ );
474
+ }
475
+ }
476
+
477
+ function findCompoundChildSpecSource(
478
+ childrenRegistrySource: string,
479
+ childKey: string
480
+ ): string | null {
481
+ const childSpecPattern = /\{[\s\S]*?key:\s*["'][^"']+["'][\s\S]*?\n\s*\},/g;
482
+
483
+ for ( const match of childrenRegistrySource.matchAll( childSpecPattern ) ) {
484
+ const candidate = match[ 0 ];
485
+ if (
486
+ candidate.includes( `key: "${ childKey }"` ) ||
487
+ candidate.includes( `key: '${ childKey }'` )
488
+ ) {
489
+ return candidate;
490
+ }
491
+ }
492
+
493
+ return null;
494
+ }
495
+
496
+ function validateAncestorInstantiability(
497
+ childrenFile: string,
498
+ ancestorChain: ExistingCompoundChild[]
499
+ ) {
500
+ if ( ancestorChain.length === 0 ) {
501
+ return;
502
+ }
503
+
504
+ const childrenRegistrySource = fs.readFileSync( childrenFile, 'utf8' );
505
+
506
+ for ( const ancestor of ancestorChain ) {
507
+ const childSpecSource = findCompoundChildSpecSource(
508
+ childrenRegistrySource,
509
+ ancestor.key
510
+ );
511
+ if ( ! childSpecSource ) {
512
+ continue;
513
+ }
514
+
515
+ const supportsInserterMatch = childSpecSource.match(
516
+ /supportsInserter:\s*(true|false)/
517
+ );
518
+ const supportsInserter = supportsInserterMatch?.[ 1 ] === 'true';
519
+ const hasTemplateInstances = ! /templateInstances:\s*\[\s*\]/.test( childSpecSource );
520
+
521
+ if ( supportsInserter || hasTemplateInstances ) {
522
+ continue;
523
+ }
524
+
525
+ throw new Error(
526
+ `Cannot nest descendants under ${ ancestor.blockName } because it is hidden and has no seeded template instances. Re-add or promote that child with a visible inserter or a seeded template first.`
527
+ );
528
+ }
529
+ }
530
+
531
+ function updateAllowedBlocks(
532
+ filePath: string,
533
+ blockName: string
534
+ ) {
535
+ const blockJson = readBlockJsonDocument( filePath );
536
+ const existingAllowedBlocks = Array.isArray( blockJson.allowedBlocks )
537
+ ? blockJson.allowedBlocks.filter(
538
+ ( value ): value is string => typeof value === 'string'
539
+ )
540
+ : [];
541
+
542
+ if ( existingAllowedBlocks.includes( blockName ) ) {
543
+ return;
544
+ }
545
+
546
+ blockJson.allowedBlocks = [ ...existingAllowedBlocks, blockName ];
547
+ writeBlockJsonDocument( filePath, blockJson );
548
+ }
549
+
303
550
  function renderBlockJson(
304
551
  childBlockName: string,
305
552
  childFolderSlug: string,
306
- childTitle: string
553
+ childTitle: string,
554
+ options: {
555
+ allowedBlocks?: string[];
556
+ ancestorBlockNames: string[];
557
+ container: boolean;
558
+ supportsInserter: boolean;
559
+ }
307
560
  ): string {
308
561
  const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
309
-
310
- return `${ JSON.stringify(
311
- {
312
- $schema: 'https://schemas.wp.org/trunk/block.json',
313
- apiVersion: 3,
314
- name: childBlockName,
315
- version: '{{blockMetadataVersion}}',
316
- title: childTitle,
317
- category: '{{compoundChildCategory}}',
318
- icon: '{{compoundChildIcon}}',
319
- description: `Internal item block used by ${ PARENT_BLOCK_TITLE }.`,
320
- parent: [ PARENT_BLOCK_NAME ],
321
- example: {},
322
- supports: {
323
- html: false,
324
- inserter: false,
325
- reusable: false,
562
+ const document: Record< string, unknown > = {
563
+ $schema: 'https://schemas.wp.org/trunk/block.json',
564
+ apiVersion: 3,
565
+ name: childBlockName,
566
+ version: '{{blockMetadataVersion}}',
567
+ title: childTitle,
568
+ category: '{{compoundChildCategory}}',
569
+ icon: '{{compoundChildIcon}}',
570
+ description: `Internal item block used by ${ PARENT_BLOCK_TITLE }.`,
571
+ example: {},
572
+ supports: {
573
+ html: false,
574
+ inserter: options.supportsInserter,
575
+ reusable: false,
576
+ },
577
+ attributes: {
578
+ title: {
579
+ type: 'string',
580
+ source: 'html',
581
+ selector: `.${ childCssClassName }__title`,
582
+ default: childTitle,
326
583
  },
327
- attributes: {
328
- title: {
329
- type: 'string',
330
- source: 'html',
331
- selector: `.${ childCssClassName }__title`,
332
- default: childTitle,
333
- },
334
- body: {
335
- type: 'string',
336
- source: 'html',
337
- selector: `.${ childCssClassName }__body`,
338
- default: CHILD_PLACEHOLDER,
339
- },
584
+ body: {
585
+ type: 'string',
586
+ source: 'html',
587
+ selector: `.${ childCssClassName }__body`,
588
+ default: CHILD_PLACEHOLDER,
340
589
  },
341
- textdomain: TEXT_DOMAIN,
342
- editorScript: 'file:./index.js',
343
590
  },
344
- null,
345
- '\t'
346
- ) }\n`;
591
+ textdomain: TEXT_DOMAIN,
592
+ editorScript: 'file:./index.js',
593
+ };
594
+
595
+ if ( options.ancestorBlockNames.length > 0 ) {
596
+ document.ancestor = options.ancestorBlockNames;
597
+ } else {
598
+ document.parent = [ PARENT_BLOCK_NAME ];
599
+ }
600
+
601
+ if ( options.container || ( options.allowedBlocks && options.allowedBlocks.length > 0 ) ) {
602
+ document.allowedBlocks = options.allowedBlocks ?? [];
603
+ }
604
+
605
+ return `${ JSON.stringify( document, null, '\t' ) }\n`;
347
606
  }
348
607
 
349
608
  function renderTypesFile(
@@ -491,10 +750,16 @@ function renderEditFile(
491
750
  const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
492
751
 
493
752
  return `import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';
494
- import { RichText, useBlockProps } from '@wordpress/block-editor';
753
+ import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';
495
754
  import { Notice } from '@wordpress/components';
496
755
  import { __ } from '@wordpress/i18n';
497
756
 
757
+ import metadata from './block-metadata';
758
+ import {
759
+ \tgetChildAllowedBlocks,
760
+ \tgetChildTemplate,
761
+ \thasNestedChildBlocks,
762
+ } from '../${ PARENT_BLOCK_SLUG }/children';
498
763
  import { useTypiaValidation } from './hooks';
499
764
  import type { ${ childTypeName } } from './types';
500
765
  import {
@@ -513,6 +778,9 @@ export default function Edit( {
513
778
  \t\tattributes,
514
779
  \t\tvalidate${ childInterfaceName }
515
780
  \t);
781
+ \tconst nestedAllowedBlocks = getChildAllowedBlocks( metadata.name );
782
+ \tconst nestedTemplate = getChildTemplate( metadata.name );
783
+ \tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );
516
784
 
517
785
  \treturn (
518
786
  \t\t<div { ...useBlockProps( { className: '${ childCssClassName }' } ) }>
@@ -539,6 +807,16 @@ export default function Edit( {
539
807
  \t\t\t\t\t</ul>
540
808
  \t\t\t\t</Notice>
541
809
  \t\t\t) }
810
+ \t\t\t{ showsNestedChildren && (
811
+ \t\t\t\t<div className="${ childCssClassName }__children">
812
+ \t\t\t\t\t<InnerBlocks
813
+ \t\t\t\t\t\tallowedBlocks={ nestedAllowedBlocks }
814
+ \t\t\t\t\t\trenderAppender={ InnerBlocks.ButtonBlockAppender }
815
+ \t\t\t\t\t\ttemplate={ nestedTemplate }
816
+ \t\t\t\t\t\ttemplateLock={ false }
817
+ \t\t\t\t\t/>
818
+ \t\t\t\t</div>
819
+ \t\t\t) }
542
820
  \t\t</div>
543
821
  \t);
544
822
  }
@@ -548,8 +826,10 @@ export default function Edit( {
548
826
  function renderSaveFile( childFolderSlug: string, childTypeName: string ): string {
549
827
  const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
550
828
 
551
- return `import { RichText, useBlockProps } from '@wordpress/block-editor';
829
+ return `import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';
552
830
 
831
+ import metadata from './block-metadata';
832
+ import { hasNestedChildBlocks } from '../${ PARENT_BLOCK_SLUG }/children';
553
833
  import type { ${ childTypeName } } from './types';
554
834
 
555
835
  export default function Save( {
@@ -557,6 +837,8 @@ export default function Save( {
557
837
  }: {
558
838
  \tattributes: ${ childTypeName };
559
839
  } ) {
840
+ \tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );
841
+
560
842
  \treturn (
561
843
  \t\t<div { ...useBlockProps.save( { className: '${ childCssClassName }' } ) }>
562
844
  \t\t\t<RichText.Content
@@ -569,13 +851,18 @@ export default function Save( {
569
851
  \t\t\t\tclassName="${ childCssClassName }__body"
570
852
  \t\t\t\tvalue={ attributes.body }
571
853
  \t\t\t/>
854
+ \t\t\t{ showsNestedChildren && (
855
+ \t\t\t\t<div className="${ childCssClassName }__children">
856
+ \t\t\t\t\t<InnerBlocks.Content />
857
+ \t\t\t\t</div>
858
+ \t\t\t) }
572
859
  \t\t</div>
573
860
  \t);
574
861
  }
575
862
  `;
576
863
  }
577
864
 
578
- function renderIndexFile( childTypeName: string, childFolderSlug: string ): string {
865
+ function renderIndexFile( childTypeName: string ): string {
579
866
  return `import {
580
867
  \tregisterScaffoldBlockType,
581
868
  \ttype BlockConfiguration,
@@ -604,14 +891,68 @@ registerScaffoldBlockType( registration.name, registration.settings );
604
891
  `;
605
892
  }
606
893
 
894
+ function renderChildSpecLines(
895
+ options: {
896
+ ancestorKeys: string[];
897
+ blockName: string;
898
+ container: boolean;
899
+ folderSlug: string;
900
+ key: string;
901
+ placement: 'nested' | 'root';
902
+ seedTemplate: boolean;
903
+ supportsInserter: boolean;
904
+ title: string;
905
+ }
906
+ ): string[] {
907
+ const templateLines =
908
+ options.seedTemplate
909
+ ? [
910
+ '\ttemplateInstances: [',
911
+ '\t\t{',
912
+ `\t\t\tbody: ${ JSON.stringify( CHILD_PLACEHOLDER ) },`,
913
+ `\t\t\ttitle: ${ JSON.stringify( options.title ) },`,
914
+ '\t\t},',
915
+ '\t],',
916
+ ]
917
+ : [ '\ttemplateInstances: [],' ];
918
+
919
+ return [
920
+ '{',
921
+ `\tancestorKeys: [ ${ options.ancestorKeys.map( ( value ) => JSON.stringify( value ) ).join( ', ' ) } ],`,
922
+ `\tblockName: ${ JSON.stringify( options.blockName ) },`,
923
+ `\tbodyPlaceholder: ${ JSON.stringify( CHILD_PLACEHOLDER ) },`,
924
+ `\tcontainer: ${ options.container ? 'true' : 'false' },`,
925
+ `\tfolderSlug: ${ JSON.stringify( options.folderSlug ) },`,
926
+ `\tkey: ${ JSON.stringify( options.key ) },`,
927
+ `\tplacement: ${ JSON.stringify( options.placement ) },`,
928
+ `\tsupportsInserter: ${ options.supportsInserter ? 'true' : 'false' },`,
929
+ ...templateLines,
930
+ `\ttitle: ${ JSON.stringify( options.title ) },`,
931
+ '},',
932
+ ];
933
+ }
934
+
607
935
  function main() {
608
- const { slug, title } = parseArgs();
936
+ const {
937
+ ancestorValues,
938
+ container,
939
+ inserter,
940
+ slug,
941
+ title,
942
+ } = parseArgs();
609
943
  const normalizedSlug = slug ? resolveValidatedBlockSlug( slug ) : '';
610
944
 
611
945
  if ( normalizedSlug.length === 0 ) {
612
946
  throw new Error( 'Use a child slug with lowercase letters, numbers, and hyphens only.' );
613
947
  }
614
948
 
949
+ const existingChildren = listExistingCompoundChildren();
950
+ const ancestorChain = ensureUniqueAncestorChain(
951
+ ancestorValues.map( ( value ) =>
952
+ resolveExistingCompoundChild( value, existingChildren )
953
+ )
954
+ );
955
+ validateAncestorChain( ancestorChain );
615
956
  const childTitle = title?.trim().length ? title.trim() : toTitleCase( normalizedSlug );
616
957
  const childFolderSlug = `${ PARENT_BLOCK_SLUG }-${ normalizedSlug }`;
617
958
  const childBlockName = `${ PARENT_BLOCK_NAME }-${ normalizedSlug }`;
@@ -628,6 +969,10 @@ function main() {
628
969
  );
629
970
  const blockConfigFile = path.join( PROJECT_ROOT, 'scripts', 'block-config.ts' );
630
971
 
972
+ if ( ancestorChain.some( ( ancestor ) => ancestor.key === normalizedSlug ) ) {
973
+ throw new Error( 'A child block cannot list itself as an ancestor.' );
974
+ }
975
+
631
976
  if ( fs.existsSync( childDir ) ) {
632
977
  throw new Error( `Child block already exists: ${ childFolderSlug }` );
633
978
  }
@@ -638,11 +983,28 @@ function main() {
638
983
  );
639
984
  }
640
985
 
986
+ validateAncestorInstantiability( childrenFile, ancestorChain );
987
+
988
+ const supportsInserter =
989
+ inserter === 'visible'
990
+ ? true
991
+ : inserter === 'hidden'
992
+ ? false
993
+ : container || ancestorChain.length > 0;
994
+ const placement = ancestorChain.length > 0 ? 'nested' : 'root';
995
+ const seedTemplate = supportsInserter || container || ancestorChain.length > 0;
996
+ const directAllowedBlocks: string[] = [];
997
+
641
998
  fs.mkdirSync( childDir, { recursive: true } );
642
999
 
643
1000
  fs.writeFileSync(
644
1001
  path.join( childDir, 'block.json' ),
645
- renderBlockJson( childBlockName, childFolderSlug, childTitle ),
1002
+ renderBlockJson( childBlockName, childFolderSlug, childTitle, {
1003
+ allowedBlocks: directAllowedBlocks,
1004
+ ancestorBlockNames: ancestorChain.map( ( ancestor ) => ancestor.blockName ),
1005
+ container,
1006
+ supportsInserter,
1007
+ } ),
646
1008
  'utf8'
647
1009
  );
648
1010
  fs.writeFileSync(
@@ -688,14 +1050,24 @@ function main() {
688
1050
  );
689
1051
  fs.writeFileSync(
690
1052
  path.join( childDir, 'index.tsx' ),
691
- renderIndexFile( childTypeName, childFolderSlug ),
1053
+ renderIndexFile( childTypeName ),
692
1054
  'utf8'
693
1055
  );
694
1056
 
695
1057
  insertBeforeMarker(
696
1058
  childrenFile,
697
- ALLOWED_CHILD_MARKER,
698
- [ `'${ childBlockName }',` ]
1059
+ CHILD_SPEC_MARKER,
1060
+ renderChildSpecLines( {
1061
+ ancestorKeys: ancestorChain.map( ( ancestor ) => ancestor.key ),
1062
+ blockName: childBlockName,
1063
+ container,
1064
+ folderSlug: childFolderSlug,
1065
+ key: normalizedSlug,
1066
+ placement,
1067
+ seedTemplate,
1068
+ supportsInserter,
1069
+ title: childTitle,
1070
+ } )
699
1071
  );
700
1072
  insertBeforeMarker(
701
1073
  blockConfigFile,
@@ -709,6 +1081,13 @@ function main() {
709
1081
  ]
710
1082
  );
711
1083
 
1084
+ if ( placement === 'root' ) {
1085
+ updateAllowedBlocks( PARENT_BLOCK_JSON_PATH, childBlockName );
1086
+ } else {
1087
+ const directAncestor = ancestorChain[ ancestorChain.length - 1 ];
1088
+ updateAllowedBlocks( directAncestor.blockJsonPath, childBlockName );
1089
+ }
1090
+
712
1091
  console.log( `✅ Added compound child block ${ childBlockName }` );
713
1092
  console.log(
714
1093
  'Run `sync-types` next to generate block.json metadata, manifests, schemas, and PHP validators for the new child block.'