@wp-typia/project-tools 0.12.0 → 0.13.1

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.
@@ -1,8 +1,9 @@
1
1
  import { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
2
+ import { type HookedBlockPositionId } from "./hooked-blocks.js";
2
3
  /**
3
4
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
4
5
  */
5
- export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern"];
6
+ export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "hooked-block"];
6
7
  export type AddKindId = (typeof ADD_KIND_IDS)[number];
7
8
  /**
8
9
  * Supported built-in block families accepted by `wp-typia add block --template`.
@@ -18,6 +19,16 @@ interface RunAddPatternCommandOptions {
18
19
  cwd?: string;
19
20
  patternName: string;
20
21
  }
22
+ interface RunAddBindingSourceCommandOptions {
23
+ bindingSourceName: string;
24
+ cwd?: string;
25
+ }
26
+ interface RunAddHookedBlockCommandOptions {
27
+ anchorBlockName: string;
28
+ blockName: string;
29
+ cwd?: string;
30
+ position: string;
31
+ }
21
32
  interface RunAddBlockCommandOptions {
22
33
  blockName: string;
23
34
  cwd?: string;
@@ -81,4 +92,43 @@ export declare function runAddPatternCommand({ cwd, patternName, }: RunAddPatter
81
92
  patternSlug: string;
82
93
  projectDir: string;
83
94
  }>;
95
+ /**
96
+ * Add one block binding source scaffold to an official workspace project.
97
+ *
98
+ * @param options Command options for the binding-source scaffold workflow.
99
+ * @param options.bindingSourceName Human-entered binding source name that will
100
+ * be normalized and validated before files are written.
101
+ * @param options.cwd Working directory used to resolve the nearest official
102
+ * workspace. Defaults to `process.cwd()`.
103
+ * @returns A promise that resolves with the normalized `bindingSourceSlug` and
104
+ * owning `projectDir` after the server/editor files and inventory entry have
105
+ * been written successfully.
106
+ * @throws {Error} When the command is run outside an official workspace, when
107
+ * the slug is invalid, or when a conflicting file or inventory entry exists.
108
+ */
109
+ export declare function runAddBindingSourceCommand({ bindingSourceName, cwd, }: RunAddBindingSourceCommandOptions): Promise<{
110
+ bindingSourceSlug: string;
111
+ projectDir: string;
112
+ }>;
113
+ /**
114
+ * Add one `blockHooks` entry to an existing official workspace block.
115
+ *
116
+ * @param options Command options for the hooked-block workflow.
117
+ * @param options.anchorBlockName Full block name that will anchor the insertion.
118
+ * @param options.blockName Existing workspace block slug to patch.
119
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
120
+ * Defaults to `process.cwd()`.
121
+ * @param options.position Hook position to store in `block.json`.
122
+ * @returns A promise that resolves with the normalized target block slug, anchor
123
+ * block name, position, and owning project directory after `block.json` is written.
124
+ * @throws {Error} When the command is run outside an official workspace, when
125
+ * the target block is unknown, when required flags are missing, or when the
126
+ * block already defines a hook for the requested anchor.
127
+ */
128
+ export declare function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd, position, }: RunAddHookedBlockCommandOptions): Promise<{
129
+ anchorBlockName: string;
130
+ blockSlug: string;
131
+ position: HookedBlockPositionId;
132
+ projectDir: string;
133
+ }>;
84
134
  export { getWorkspaceBlockSelectOptions };
@@ -10,11 +10,12 @@ import { SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
10
10
  import { copyInterpolatedDirectory } from "./template-render.js";
11
11
  import { toKebabCase, toTitleCase, toSnakeCase, } from "./string-case.js";
12
12
  import { appendWorkspaceInventoryEntries, getWorkspaceBlockSelectOptions, readWorkspaceInventory, } from "./workspace-inventory.js";
13
+ import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
13
14
  import { resolveWorkspaceProject, WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
14
15
  /**
15
16
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
16
17
  */
17
- export const ADD_KIND_IDS = ["block", "variation", "pattern"];
18
+ export const ADD_KIND_IDS = ["block", "variation", "pattern", "binding-source", "hooked-block"];
18
19
  /**
19
20
  * Supported built-in block families accepted by `wp-typia add block --template`.
20
21
  */
@@ -29,6 +30,9 @@ const REST_MANIFEST_IMPORT_PATTERN = /import\s*\{[^}]*\bdefineEndpointManifest\b
29
30
  const VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
30
31
  const VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
31
32
  const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
33
+ const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
34
+ const BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
35
+ const BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
32
36
  const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
33
37
  function normalizeBlockSlug(input) {
34
38
  return toKebabCase(input);
@@ -42,6 +46,12 @@ function assertValidGeneratedSlug(label, slug, usage) {
42
46
  }
43
47
  return slug;
44
48
  }
49
+ function assertValidHookedBlockPosition(position) {
50
+ if (HOOKED_BLOCK_POSITION_IDS.includes(position)) {
51
+ return position;
52
+ }
53
+ throw new Error(`Hook position must be one of: ${HOOKED_BLOCK_POSITION_IDS.join(", ")}.`);
54
+ }
45
55
  function getWorkspaceBootstrapPath(workspace) {
46
56
  const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
47
57
  return path.join(workspace.projectDir, `${workspaceBaseName}.php`);
@@ -284,6 +294,15 @@ function buildPatternConfigEntry(patternSlug) {
284
294
  "\t},",
285
295
  ].join("\n");
286
296
  }
297
+ function buildBindingSourceConfigEntry(bindingSourceSlug) {
298
+ return [
299
+ "\t{",
300
+ `\t\teditorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
301
+ `\t\tserverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
302
+ `\t\tslug: ${quoteTsString(bindingSourceSlug)},`,
303
+ "\t},",
304
+ ].join("\n");
305
+ }
287
306
  function buildVariationConstName(variationSlug) {
288
307
  const identifierSegments = toKebabCase(variationSlug)
289
308
  .split("-")
@@ -364,6 +383,70 @@ register_block_pattern(
364
383
  );
365
384
  `;
366
385
  }
386
+ function buildBindingSourceServerSource(bindingSourceSlug, namespace, textDomain) {
387
+ const bindingSourceTitle = toTitleCase(bindingSourceSlug);
388
+ return `<?php
389
+ if ( ! defined( 'ABSPATH' ) ) {
390
+ \treturn;
391
+ }
392
+
393
+ if ( ! function_exists( 'register_block_bindings_source' ) ) {
394
+ \treturn;
395
+ }
396
+
397
+ register_block_bindings_source(
398
+ \t'${namespace}/${bindingSourceSlug}',
399
+ \tarray(
400
+ \t\t'label' => __( ${JSON.stringify(bindingSourceTitle)}, '${textDomain}' ),
401
+ \t\t'get_value_callback' => static function( array $source_args ) : string {
402
+ \t\t\t$field = isset( $source_args['field'] ) && is_string( $source_args['field'] )
403
+ \t\t\t\t? $source_args['field']
404
+ \t\t\t\t: '${bindingSourceSlug}';
405
+
406
+ \t\t\treturn sprintf(
407
+ \t\t\t\t__( 'Replace %s with real binding source data.', '${textDomain}' ),
408
+ \t\t\t\t$field
409
+ \t\t\t);
410
+ \t\t},
411
+ \t)
412
+ );
413
+ `;
414
+ }
415
+ function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
416
+ const bindingSourceTitle = toTitleCase(bindingSourceSlug);
417
+ return `import { registerBlockBindingsSource } from '@wordpress/blocks';
418
+ import { __ } from '@wordpress/i18n';
419
+
420
+ registerBlockBindingsSource( {
421
+ \tname: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
422
+ \tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
423
+ \tgetFieldsList() {
424
+ \t\treturn [
425
+ \t\t\t{
426
+ \t\t\t\tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
427
+ \t\t\t\ttype: 'string',
428
+ \t\t\t\targs: {
429
+ \t\t\t\t\tfield: ${quoteTsString(bindingSourceSlug)},
430
+ \t\t\t\t},
431
+ \t\t\t},
432
+ \t\t];
433
+ \t},
434
+ \tgetValues( { bindings } ) {
435
+ \t\tconst values: Record<string, string> = {};
436
+ \t\tfor ( const attributeName of Object.keys( bindings ) ) {
437
+ \t\t\tvalues[ attributeName ] = ${quoteTsString(`TODO: replace ${bindingSourceSlug} with real editor-side values.`)};
438
+ \t\t}
439
+ \t\treturn values;
440
+ \t},
441
+ } );
442
+ `;
443
+ }
444
+ function buildBindingSourceIndexSource(bindingSourceSlugs) {
445
+ const importLines = bindingSourceSlugs
446
+ .map((bindingSourceSlug) => `import './${bindingSourceSlug}/editor';`)
447
+ .join("\n");
448
+ return `${importLines}${importLines ? "\n\n" : ""}// wp-typia add binding-source entries\n`;
449
+ }
367
450
  async function ensureVariationRegistrationHook(blockIndexPath) {
368
451
  await patchFile(blockIndexPath, (source) => {
369
452
  let nextSource = source;
@@ -466,6 +549,85 @@ function ${patternRegistrationFunctionName}() {
466
549
  return nextSource;
467
550
  });
468
551
  }
552
+ async function ensureBindingSourceBootstrapAnchors(workspace) {
553
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
554
+ await patchFile(bootstrapPath, (source) => {
555
+ let nextSource = source;
556
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
557
+ const bindingRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_binding_sources`;
558
+ const bindingEditorEnqueueFunctionName = `${workspace.workspace.phpPrefix}_enqueue_binding_sources_editor`;
559
+ const bindingRegistrationHook = `add_action( 'init', '${bindingRegistrationFunctionName}', 20 );`;
560
+ const bindingEditorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${bindingEditorEnqueueFunctionName}' );`;
561
+ const bindingRegistrationFunction = `
562
+
563
+ function ${bindingRegistrationFunctionName}() {
564
+ \tforeach ( glob( __DIR__ . '${BINDING_SOURCE_SERVER_GLOB}' ) ?: array() as $binding_source_module ) {
565
+ \t\trequire_once $binding_source_module;
566
+ \t}
567
+ }
568
+ `;
569
+ const bindingEditorEnqueueFunction = `
570
+
571
+ function ${bindingEditorEnqueueFunctionName}() {
572
+ \t$script_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_SCRIPT}';
573
+ \t$asset_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_ASSET}';
574
+
575
+ \tif ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
576
+ \t\treturn;
577
+ \t}
578
+
579
+ \t$asset = require $asset_path;
580
+ \tif ( ! is_array( $asset ) ) {
581
+ \t\t$asset = array();
582
+ \t}
583
+
584
+ \twp_enqueue_script(
585
+ \t\t'${workspaceBaseName}-binding-sources',
586
+ \t\tplugins_url( '${BINDING_SOURCE_EDITOR_SCRIPT}', __FILE__ ),
587
+ \t\tisset( $asset['dependencies'] ) && is_array( $asset['dependencies'] ) ? $asset['dependencies'] : array(),
588
+ \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
589
+ \t\ttrue
590
+ \t);
591
+ }
592
+ `;
593
+ const insertionAnchors = [
594
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
595
+ /\?>\s*$/u,
596
+ ];
597
+ const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${functionName}\\s*\\(`, "u").test(nextSource);
598
+ const insertPhpSnippet = (snippet) => {
599
+ for (const anchor of insertionAnchors) {
600
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
601
+ if (candidate !== nextSource) {
602
+ nextSource = candidate;
603
+ return;
604
+ }
605
+ }
606
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
607
+ };
608
+ const appendPhpSnippet = (snippet) => {
609
+ const closingTagPattern = /\?>\s*$/u;
610
+ if (closingTagPattern.test(nextSource)) {
611
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
612
+ return;
613
+ }
614
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
615
+ };
616
+ if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
617
+ insertPhpSnippet(bindingRegistrationFunction);
618
+ }
619
+ if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
620
+ insertPhpSnippet(bindingEditorEnqueueFunction);
621
+ }
622
+ if (!nextSource.includes(bindingRegistrationHook)) {
623
+ appendPhpSnippet(bindingRegistrationHook);
624
+ }
625
+ if (!nextSource.includes(bindingEditorEnqueueHook)) {
626
+ appendPhpSnippet(bindingEditorEnqueueHook);
627
+ }
628
+ return nextSource;
629
+ });
630
+ }
469
631
  function ensureBlockConfigCanAddRestManifests(source) {
470
632
  const importLine = "import { defineEndpointManifest } from '@wp-typia/block-runtime/metadata-core';";
471
633
  if (REST_MANIFEST_IMPORT_PATTERN.test(source)) {
@@ -479,6 +641,23 @@ async function appendBlockConfigEntries(projectDir, entries, needsRestManifestIm
479
641
  transformSource: needsRestManifestImport ? ensureBlockConfigCanAddRestManifests : undefined,
480
642
  });
481
643
  }
644
+ async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
645
+ const bindingsDir = path.join(projectDir, "src", "bindings");
646
+ const bindingsIndexPath = resolveBindingSourceRegistryPath(projectDir);
647
+ await fsp.mkdir(bindingsDir, { recursive: true });
648
+ const existingBindingSourceSlugs = fs.existsSync(bindingsDir)
649
+ ? fs
650
+ .readdirSync(bindingsDir, { withFileTypes: true })
651
+ .filter((entry) => entry.isDirectory())
652
+ .map((entry) => entry.name)
653
+ : [];
654
+ const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
655
+ await fsp.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
656
+ }
657
+ function resolveBindingSourceRegistryPath(projectDir) {
658
+ const bindingsDir = path.join(projectDir, "src", "bindings");
659
+ return [path.join(bindingsDir, "index.ts"), path.join(bindingsDir, "index.js")].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(bindingsDir, "index.ts");
660
+ }
482
661
  async function snapshotWorkspaceFiles(filePaths) {
483
662
  const uniquePaths = Array.from(new Set(filePaths));
484
663
  return Promise.all(uniquePaths.map(async (filePath) => ({
@@ -616,11 +795,15 @@ export function formatAddHelpText() {
616
795
  wp-typia add block <name> --template <${ADD_BLOCK_TEMPLATE_IDS.join("|")}> [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
617
796
  wp-typia add variation <name> --block <block-slug>
618
797
  wp-typia add pattern <name>
798
+ wp-typia add binding-source <name>
799
+ wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}>
619
800
 
620
801
  Notes:
621
802
  \`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces.
622
803
  \`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
623
- \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.`;
804
+ \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
805
+ \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
806
+ \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.`;
624
807
  }
625
808
  /**
626
809
  * Seeds an empty official workspace migration project before any blocks are added.
@@ -728,6 +911,50 @@ function resolveWorkspaceBlock(inventory, blockSlug) {
728
911
  }
729
912
  return block;
730
913
  }
914
+ function assertValidHookAnchor(anchorBlockName) {
915
+ const trimmed = anchorBlockName.trim();
916
+ if (!trimmed) {
917
+ throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name>.");
918
+ }
919
+ if (!HOOKED_BLOCK_ANCHOR_PATTERN.test(trimmed)) {
920
+ throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name> to use the full `namespace/slug` block name format.");
921
+ }
922
+ return trimmed;
923
+ }
924
+ function readWorkspaceBlockJson(projectDir, blockSlug) {
925
+ const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
926
+ if (!fs.existsSync(blockJsonPath)) {
927
+ throw new Error(`Missing ${path.relative(projectDir, blockJsonPath)} for workspace block "${blockSlug}".`);
928
+ }
929
+ let blockJson;
930
+ try {
931
+ blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
932
+ }
933
+ catch (error) {
934
+ throw new Error(error instanceof Error
935
+ ? `Failed to parse ${path.relative(projectDir, blockJsonPath)}: ${error.message}`
936
+ : `Failed to parse ${path.relative(projectDir, blockJsonPath)}.`);
937
+ }
938
+ if (!blockJson || typeof blockJson !== "object" || Array.isArray(blockJson)) {
939
+ throw new Error(`${path.relative(projectDir, blockJsonPath)} must contain a JSON object.`);
940
+ }
941
+ return {
942
+ blockJson: blockJson,
943
+ blockJsonPath,
944
+ };
945
+ }
946
+ function getMutableBlockHooks(blockJson, blockJsonRelativePath) {
947
+ const blockHooks = blockJson.blockHooks;
948
+ if (blockHooks === undefined) {
949
+ const nextHooks = {};
950
+ blockJson.blockHooks = nextHooks;
951
+ return nextHooks;
952
+ }
953
+ if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
954
+ throw new Error(`${blockJsonRelativePath} must define blockHooks as an object when present.`);
955
+ }
956
+ return blockHooks;
957
+ }
731
958
  function assertVariationDoesNotExist(projectDir, blockSlug, variationSlug, inventory) {
732
959
  const variationPath = path.join(projectDir, "src", "blocks", blockSlug, "variations", `${variationSlug}.ts`);
733
960
  if (fs.existsSync(variationPath)) {
@@ -746,6 +973,15 @@ function assertPatternDoesNotExist(projectDir, patternSlug, inventory) {
746
973
  throw new Error(`A pattern inventory entry already exists for ${patternSlug}. Choose a different name.`);
747
974
  }
748
975
  }
976
+ function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, inventory) {
977
+ const bindingSourceDir = path.join(projectDir, "src", "bindings", bindingSourceSlug);
978
+ if (fs.existsSync(bindingSourceDir)) {
979
+ throw new Error(`A binding source already exists at ${path.relative(projectDir, bindingSourceDir)}. Choose a different name.`);
980
+ }
981
+ if (inventory.bindingSources.some((entry) => entry.slug === bindingSourceSlug)) {
982
+ throw new Error(`A binding source inventory entry already exists for ${bindingSourceSlug}. Choose a different name.`);
983
+ }
984
+ }
749
985
  /**
750
986
  * Add one variation entry to an existing workspace block.
751
987
  *
@@ -847,4 +1083,105 @@ export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }
847
1083
  throw error;
848
1084
  }
849
1085
  }
1086
+ /**
1087
+ * Add one block binding source scaffold to an official workspace project.
1088
+ *
1089
+ * @param options Command options for the binding-source scaffold workflow.
1090
+ * @param options.bindingSourceName Human-entered binding source name that will
1091
+ * be normalized and validated before files are written.
1092
+ * @param options.cwd Working directory used to resolve the nearest official
1093
+ * workspace. Defaults to `process.cwd()`.
1094
+ * @returns A promise that resolves with the normalized `bindingSourceSlug` and
1095
+ * owning `projectDir` after the server/editor files and inventory entry have
1096
+ * been written successfully.
1097
+ * @throws {Error} When the command is run outside an official workspace, when
1098
+ * the slug is invalid, or when a conflicting file or inventory entry exists.
1099
+ */
1100
+ export async function runAddBindingSourceCommand({ bindingSourceName, cwd = process.cwd(), }) {
1101
+ const workspace = resolveWorkspaceProject(cwd);
1102
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
1103
+ const inventory = readWorkspaceInventory(workspace.projectDir);
1104
+ assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
1105
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
1106
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1107
+ const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
1108
+ const bindingSourceDir = path.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
1109
+ const serverFilePath = path.join(bindingSourceDir, "server.php");
1110
+ const editorFilePath = path.join(bindingSourceDir, "editor.ts");
1111
+ const mutationSnapshot = {
1112
+ fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
1113
+ snapshotDirs: [],
1114
+ targetPaths: [bindingSourceDir],
1115
+ };
1116
+ try {
1117
+ await fsp.mkdir(bindingSourceDir, { recursive: true });
1118
+ await ensureBindingSourceBootstrapAnchors(workspace);
1119
+ await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1120
+ await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1121
+ await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
1122
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
1123
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)],
1124
+ });
1125
+ return {
1126
+ bindingSourceSlug,
1127
+ projectDir: workspace.projectDir,
1128
+ };
1129
+ }
1130
+ catch (error) {
1131
+ await rollbackWorkspaceMutation(mutationSnapshot);
1132
+ throw error;
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Add one `blockHooks` entry to an existing official workspace block.
1137
+ *
1138
+ * @param options Command options for the hooked-block workflow.
1139
+ * @param options.anchorBlockName Full block name that will anchor the insertion.
1140
+ * @param options.blockName Existing workspace block slug to patch.
1141
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
1142
+ * Defaults to `process.cwd()`.
1143
+ * @param options.position Hook position to store in `block.json`.
1144
+ * @returns A promise that resolves with the normalized target block slug, anchor
1145
+ * block name, position, and owning project directory after `block.json` is written.
1146
+ * @throws {Error} When the command is run outside an official workspace, when
1147
+ * the target block is unknown, when required flags are missing, or when the
1148
+ * block already defines a hook for the requested anchor.
1149
+ */
1150
+ export async function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd = process.cwd(), position, }) {
1151
+ const workspace = resolveWorkspaceProject(cwd);
1152
+ const blockSlug = normalizeBlockSlug(blockName);
1153
+ const inventory = readWorkspaceInventory(workspace.projectDir);
1154
+ resolveWorkspaceBlock(inventory, blockSlug);
1155
+ const resolvedAnchorBlockName = assertValidHookAnchor(anchorBlockName);
1156
+ const resolvedPosition = assertValidHookedBlockPosition(position);
1157
+ const selfHookAnchor = `${workspace.workspace.namespace}/${blockSlug}`;
1158
+ if (resolvedAnchorBlockName === selfHookAnchor) {
1159
+ throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
1160
+ }
1161
+ const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
1162
+ const blockJsonRelativePath = path.relative(workspace.projectDir, blockJsonPath);
1163
+ const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
1164
+ if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
1165
+ throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
1166
+ }
1167
+ const mutationSnapshot = {
1168
+ fileSources: await snapshotWorkspaceFiles([blockJsonPath]),
1169
+ snapshotDirs: [],
1170
+ targetPaths: [],
1171
+ };
1172
+ try {
1173
+ blockHooks[resolvedAnchorBlockName] = resolvedPosition;
1174
+ await fsp.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
1175
+ return {
1176
+ anchorBlockName: resolvedAnchorBlockName,
1177
+ blockSlug,
1178
+ position: resolvedPosition,
1179
+ projectDir: workspace.projectDir,
1180
+ };
1181
+ }
1182
+ catch (error) {
1183
+ await rollbackWorkspaceMutation(mutationSnapshot);
1184
+ throw error;
1185
+ }
1186
+ }
850
1187
  export { getWorkspaceBlockSelectOptions };
@@ -7,6 +7,8 @@
7
7
  *
8
8
  * Import `formatAddHelpText`, `runAddBlockCommand`,
9
9
  * `runAddVariationCommand`, `runAddPatternCommand`,
10
+ * `runAddBindingSourceCommand`, `runAddHookedBlockCommand`,
11
+ * and `HOOKED_BLOCK_POSITION_IDS`,
10
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
11
13
  * explicit `wp-typia add` flows,
12
14
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
@@ -19,7 +21,9 @@
19
21
  * template inspection flows.
20
22
  */
21
23
  export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
22
- export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
24
+ export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
25
+ export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
26
+ export type { HookedBlockPositionId } from "./hooked-blocks.js";
23
27
  export { formatHelpText } from "./cli-help.js";
24
28
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
25
29
  export { createReadlinePrompt, type ReadlinePrompt } from "./cli-prompt.js";
@@ -7,6 +7,8 @@
7
7
  *
8
8
  * Import `formatAddHelpText`, `runAddBlockCommand`,
9
9
  * `runAddVariationCommand`, `runAddPatternCommand`,
10
+ * `runAddBindingSourceCommand`, `runAddHookedBlockCommand`,
11
+ * and `HOOKED_BLOCK_POSITION_IDS`,
10
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
11
13
  * explicit `wp-typia add` flows,
12
14
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
@@ -19,7 +21,8 @@
19
21
  * template inspection flows.
20
22
  */
21
23
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
22
- export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
24
+ export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
25
+ export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
23
26
  export { formatHelpText } from "./cli-help.js";
24
27
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
25
28
  export { createReadlinePrompt } from "./cli-prompt.js";
@@ -4,11 +4,15 @@ import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { access, constants as fsConstants, rm, writeFile } from "node:fs/promises";
6
6
  import { getBuiltInTemplateLayerDirs } from "./template-builtins.js";
7
+ import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
7
8
  import { listTemplates } from "./template-registry.js";
8
9
  import { readWorkspaceInventory } from "./workspace-inventory.js";
9
10
  import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
10
11
  const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
11
12
  const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collection["']\s*;?\s*$/m;
13
+ const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
14
+ const WORKSPACE_BINDING_EDITOR_SCRIPT = "build/bindings/index.js";
15
+ const WORKSPACE_BINDING_EDITOR_ASSET = "build/bindings/index.asset.php";
12
16
  const WORKSPACE_GENERATED_BLOCK_ARTIFACTS = [
13
17
  "block.json",
14
18
  "typia.manifest.json",
@@ -129,6 +133,41 @@ function checkWorkspaceBlockMetadata(projectDir, workspace, block) {
129
133
  ? `block.json matches ${expectedName} and ${workspace.workspace.textDomain}`
130
134
  : issues.join("; "));
131
135
  }
136
+ function checkWorkspaceBlockHooks(projectDir, blockSlug) {
137
+ const blockJsonRelativePath = path.join("src", "blocks", blockSlug, "block.json");
138
+ const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
139
+ if (!fs.existsSync(blockJsonPath)) {
140
+ return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `Missing ${blockJsonRelativePath}`);
141
+ }
142
+ let blockJson;
143
+ try {
144
+ blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
145
+ }
146
+ catch (error) {
147
+ return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", error instanceof Error ? error.message : String(error));
148
+ }
149
+ const blockHooks = blockJson.blockHooks;
150
+ if (blockHooks === undefined) {
151
+ return createDoctorCheck(`Block hooks ${blockSlug}`, "pass", "No blockHooks metadata configured");
152
+ }
153
+ if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
154
+ return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `${blockJsonRelativePath} must define blockHooks as an object when present.`);
155
+ }
156
+ const blockName = typeof blockJson.name === "string" && blockJson.name.trim().length > 0
157
+ ? blockJson.name.trim()
158
+ : null;
159
+ const invalidEntries = Object.entries(blockHooks).filter(([anchor, position]) => (blockName !== null && anchor.trim() === blockName) ||
160
+ anchor.trim().length === 0 ||
161
+ anchor !== anchor.trim() ||
162
+ !HOOKED_BLOCK_ANCHOR_PATTERN.test(anchor) ||
163
+ typeof position !== "string" ||
164
+ !HOOKED_BLOCK_POSITION_SET.has(position));
165
+ return createDoctorCheck(`Block hooks ${blockSlug}`, invalidEntries.length === 0 ? "pass" : "fail", invalidEntries.length === 0
166
+ ? `blockHooks metadata is valid${Object.keys(blockHooks).length > 0 ? ` (${Object.keys(blockHooks).join(", ")})` : ""}`
167
+ : `Invalid blockHooks entries: ${invalidEntries
168
+ .map(([anchor, position]) => `${anchor || "<empty>"} => ${String(position)}`)
169
+ .join(", ")}`);
170
+ }
132
171
  function checkWorkspaceBlockCollectionImport(projectDir, blockSlug) {
133
172
  const entryRelativePath = path.join("src", "blocks", blockSlug, "index.tsx");
134
173
  const entryPath = path.join(projectDir, entryRelativePath);
@@ -154,6 +193,33 @@ function checkWorkspacePatternBootstrap(projectDir, packageName) {
154
193
  ? "Pattern category and loader hooks are present"
155
194
  : "Missing pattern category registration or src/patterns loader hook");
156
195
  }
196
+ function checkWorkspaceBindingBootstrap(projectDir, packageName) {
197
+ const packageBaseName = packageName.split("/").pop() ?? packageName;
198
+ const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
199
+ if (!fs.existsSync(bootstrapPath)) {
200
+ return createDoctorCheck("Binding bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
201
+ }
202
+ const source = fs.readFileSync(bootstrapPath, "utf8");
203
+ const hasServerGlob = source.includes(WORKSPACE_BINDING_SERVER_GLOB);
204
+ const hasEditorEnqueueHook = source.includes("enqueue_block_editor_assets");
205
+ const hasEditorScript = source.includes(WORKSPACE_BINDING_EDITOR_SCRIPT);
206
+ const hasEditorAsset = source.includes(WORKSPACE_BINDING_EDITOR_ASSET);
207
+ return createDoctorCheck("Binding bootstrap", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset ? "pass" : "fail", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset
208
+ ? "Binding source PHP and editor bootstrap hooks are present"
209
+ : "Missing binding source PHP require glob or editor enqueue hook");
210
+ }
211
+ function checkWorkspaceBindingSourcesIndex(projectDir, bindingSources) {
212
+ const indexRelativePath = [path.join("src", "bindings", "index.ts"), path.join("src", "bindings", "index.js")].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
213
+ if (!indexRelativePath) {
214
+ return createDoctorCheck("Binding sources index", "fail", "Missing src/bindings/index.ts or src/bindings/index.js");
215
+ }
216
+ const indexPath = path.join(projectDir, indexRelativePath);
217
+ const source = fs.readFileSync(indexPath, "utf8");
218
+ const missingImports = bindingSources.filter((bindingSource) => !source.includes(`./${bindingSource.slug}/editor`));
219
+ return createDoctorCheck("Binding sources index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
220
+ ? "Binding source editor registrations are aggregated"
221
+ : `Missing editor imports for: ${missingImports.map((entry) => entry.slug).join(", ")}`);
222
+ }
157
223
  function checkVariationEntrypoint(projectDir, blockSlug) {
158
224
  const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
159
225
  if (!fs.existsSync(entryPath)) {
@@ -274,12 +340,13 @@ export async function getDoctorChecks(cwd) {
274
340
  checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
275
341
  try {
276
342
  const inventory = readWorkspaceInventory(workspace.projectDir);
277
- checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(s), ${inventory.patterns.length} pattern(s)`));
343
+ checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(s), ${inventory.patterns.length} pattern(s), ${inventory.bindingSources.length} binding source(s)`));
278
344
  for (const block of inventory.blocks) {
279
345
  checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, [
280
346
  ...getWorkspaceBlockRequiredFiles(block),
281
347
  ]));
282
348
  checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
349
+ checks.push(checkWorkspaceBlockHooks(workspace.projectDir, block.slug));
283
350
  checks.push(checkWorkspaceBlockCollectionImport(workspace.projectDir, block.slug));
284
351
  }
285
352
  const registeredBlockSlugs = new Set(inventory.blocks.map((block) => block.slug));
@@ -303,6 +370,16 @@ export async function getDoctorChecks(cwd) {
303
370
  for (const pattern of inventory.patterns) {
304
371
  checks.push(checkExistingFiles(workspace.projectDir, `Pattern ${pattern.slug}`, [pattern.file]));
305
372
  }
373
+ if (inventory.bindingSources.length > 0) {
374
+ checks.push(checkWorkspaceBindingBootstrap(workspace.projectDir, workspace.packageName));
375
+ checks.push(checkWorkspaceBindingSourcesIndex(workspace.projectDir, inventory.bindingSources));
376
+ }
377
+ for (const bindingSource of inventory.bindingSources) {
378
+ checks.push(checkExistingFiles(workspace.projectDir, `Binding source ${bindingSource.slug}`, [
379
+ bindingSource.serverFile,
380
+ bindingSource.editorFile,
381
+ ]));
382
+ }
306
383
  const migrationWorkspaceCheck = checkMigrationWorkspaceHint(workspace, workspacePackageJson);
307
384
  if (migrationWorkspaceCheck) {
308
385
  checks.push(migrationWorkspaceCheck);
@@ -19,6 +19,8 @@ export function formatHelpText() {
19
19
  wp-typia add block <name> --template <basic|interactivity|persistence|compound> [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
20
20
  wp-typia add variation <name> --block <block-slug>
21
21
  wp-typia add pattern <name>
22
+ wp-typia add binding-source <name>
23
+ wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <before|after|firstChild|lastChild>
22
24
  wp-typia migrate <init|snapshot|diff|scaffold|verify|doctor|fixtures|fuzz> [...]
23
25
  wp-typia templates list
24
26
  wp-typia templates inspect <id>
@@ -34,6 +36,8 @@ Notes:
34
36
  \`wp-typia <project-dir>\` remains a backward-compatible alias to \`create\`.
35
37
  \`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
36
38
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
39
+ \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
40
+ \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.
37
41
  \`wp-typia doctor\` checks environment readiness plus workspace inventory and source-tree drift.
38
42
  \`wp-typia migrate doctor --all\` checks migration target alignment, snapshots, fixtures, and generated migration artifacts.
39
43
  \`migrate\` is the canonical migration command; \`migrations\` is no longer supported.`;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared hooked-block metadata primitives used by add flows, doctor checks,
3
+ * and interactive prompts.
4
+ */
5
+ export declare const HOOKED_BLOCK_POSITION_IDS: readonly ["before", "after", "firstChild", "lastChild"];
6
+ /**
7
+ * Union of valid `blockHooks` positions accepted by wp-typia workspace flows.
8
+ */
9
+ export type HookedBlockPositionId = (typeof HOOKED_BLOCK_POSITION_IDS)[number];
10
+ /**
11
+ * Fast lookup set for validating hooked-block positions across runtime surfaces.
12
+ */
13
+ export declare const HOOKED_BLOCK_POSITION_SET: Set<string>;
14
+ /**
15
+ * Canonical `namespace/slug` block name format required for hooked-block anchors.
16
+ */
17
+ export declare const HOOKED_BLOCK_ANCHOR_PATTERN: RegExp;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared hooked-block metadata primitives used by add flows, doctor checks,
3
+ * and interactive prompts.
4
+ */
5
+ export const HOOKED_BLOCK_POSITION_IDS = ["before", "after", "firstChild", "lastChild"];
6
+ /**
7
+ * Fast lookup set for validating hooked-block positions across runtime surfaces.
8
+ */
9
+ export const HOOKED_BLOCK_POSITION_SET = new Set(HOOKED_BLOCK_POSITION_IDS);
10
+ /**
11
+ * Canonical `namespace/slug` block name format required for hooked-block anchors.
12
+ */
13
+ export const HOOKED_BLOCK_ANCHOR_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/;
@@ -5,8 +5,9 @@
5
5
  * CLI while keeping reusable project logic out of the CLI package itself.
6
6
  * Consumers should prefer these exports for scaffold, add, migrate, doctor,
7
7
  * and workspace-aware helpers such as `getWorkspaceBlockSelectOptions`,
8
- * `runAddBlockCommand`, `runAddVariationCommand`, `runAddPatternCommand`, and
9
- * `runDoctor`.
8
+ * `runAddBlockCommand`, `runAddVariationCommand`, `runAddPatternCommand`,
9
+ * `runAddBindingSourceCommand`, `runAddHookedBlockCommand`,
10
+ * `HOOKED_BLOCK_POSITION_IDS`, and `runDoctor`.
10
11
  */
11
12
  export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
12
13
  export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
@@ -16,5 +17,5 @@ export { buildCompoundChildStarterManifestDocument, getStarterManifestFiles, str
16
17
  export type { EndpointAuthIntent, EndpointOpenApiAuthMode, EndpointOpenApiContractDocument, EndpointOpenApiDocumentOptions, EndpointOpenApiEndpointDefinition, EndpointOpenApiMethod, EndpointWordPressAuthDefinition, EndpointWordPressAuthMechanism, JsonSchemaDocument, JsonSchemaProjectionProfile, JsonSchemaObject, NormalizedEndpointAuthDefinition, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiSecurityScheme, } from "./schema-core.js";
17
18
  export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, getPackageManagerSelectOptions, transformPackageManagerText, } from "./package-managers.js";
18
19
  export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
19
- export { createReadlinePrompt, formatAddHelpText, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, runAddBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
20
- export type { DoctorCheck, ReadlinePrompt } from "./cli-core.js";
20
+ export { createReadlinePrompt, formatAddHelpText, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, HOOKED_BLOCK_POSITION_IDS, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
21
+ export type { DoctorCheck, HookedBlockPositionId, ReadlinePrompt } from "./cli-core.js";
@@ -5,8 +5,9 @@
5
5
  * CLI while keeping reusable project logic out of the CLI package itself.
6
6
  * Consumers should prefer these exports for scaffold, add, migrate, doctor,
7
7
  * and workspace-aware helpers such as `getWorkspaceBlockSelectOptions`,
8
- * `runAddBlockCommand`, `runAddVariationCommand`, `runAddPatternCommand`, and
9
- * `runDoctor`.
8
+ * `runAddBlockCommand`, `runAddVariationCommand`, `runAddPatternCommand`,
9
+ * `runAddBindingSourceCommand`, `runAddHookedBlockCommand`,
10
+ * `HOOKED_BLOCK_POSITION_IDS`, and `runDoctor`.
10
11
  */
11
12
  export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
12
13
  export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
@@ -15,4 +16,4 @@ export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJso
15
16
  export { buildCompoundChildStarterManifestDocument, getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
16
17
  export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, getPackageManagerSelectOptions, transformPackageManagerText, } from "./package-managers.js";
17
18
  export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
18
- export { createReadlinePrompt, formatAddHelpText, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, runAddBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
19
+ export { createReadlinePrompt, formatAddHelpText, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, HOOKED_BLOCK_POSITION_IDS, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
@@ -14,9 +14,16 @@ export interface WorkspacePatternInventoryEntry {
14
14
  file: string;
15
15
  slug: string;
16
16
  }
17
+ export interface WorkspaceBindingSourceInventoryEntry {
18
+ editorFile: string;
19
+ serverFile: string;
20
+ slug: string;
21
+ }
17
22
  export interface WorkspaceInventory {
23
+ bindingSources: WorkspaceBindingSourceInventoryEntry[];
18
24
  blockConfigPath: string;
19
25
  blocks: WorkspaceBlockInventoryEntry[];
26
+ hasBindingSourcesSection: boolean;
20
27
  hasPatternsSection: boolean;
21
28
  hasVariationsSection: boolean;
22
29
  patterns: WorkspacePatternInventoryEntry[];
@@ -26,6 +33,7 @@ export interface WorkspaceInventory {
26
33
  export declare const BLOCK_CONFIG_ENTRY_MARKER = "\t// wp-typia add block entries";
27
34
  export declare const VARIATION_CONFIG_ENTRY_MARKER = "\t// wp-typia add variation entries";
28
35
  export declare const PATTERN_CONFIG_ENTRY_MARKER = "\t// wp-typia add pattern entries";
36
+ export declare const BINDING_SOURCE_CONFIG_ENTRY_MARKER = "\t// wp-typia add binding-source entries";
29
37
  /**
30
38
  * Parse workspace inventory entries from the source of `scripts/block-config.ts`.
31
39
  *
@@ -67,8 +75,9 @@ export declare function getWorkspaceBlockSelectOptions(projectDir: string): Arra
67
75
  * @param options Entry lists plus an optional source transformer.
68
76
  * @returns Updated source text with all requested inventory entries appended.
69
77
  */
70
- export declare function updateWorkspaceInventorySource(source: string, { blockEntries, patternEntries, variationEntries, transformSource, }?: {
78
+ export declare function updateWorkspaceInventorySource(source: string, { blockEntries, bindingSourceEntries, patternEntries, variationEntries, transformSource, }?: {
71
79
  blockEntries?: string[];
80
+ bindingSourceEntries?: string[];
72
81
  patternEntries?: string[];
73
82
  transformSource?: (source: string) => string;
74
83
  variationEntries?: string[];
@@ -5,29 +5,48 @@ import ts from "typescript";
5
5
  export const BLOCK_CONFIG_ENTRY_MARKER = "\t// wp-typia add block entries";
6
6
  export const VARIATION_CONFIG_ENTRY_MARKER = "\t// wp-typia add variation entries";
7
7
  export const PATTERN_CONFIG_ENTRY_MARKER = "\t// wp-typia add pattern entries";
8
- const VARIATIONS_SECTION = `
8
+ export const BINDING_SOURCE_CONFIG_ENTRY_MARKER = "\t// wp-typia add binding-source entries";
9
+ const VARIATIONS_INTERFACE_SECTION = `
9
10
 
10
11
  export interface WorkspaceVariationConfig {
11
12
  \tblock: string;
12
13
  \tfile: string;
13
14
  \tslug: string;
14
15
  }
16
+ `;
17
+ const VARIATIONS_CONST_SECTION = `
15
18
 
16
19
  export const VARIATIONS: WorkspaceVariationConfig[] = [
17
20
  \t// wp-typia add variation entries
18
21
  ];
19
22
  `;
20
- const PATTERNS_SECTION = `
23
+ const PATTERNS_INTERFACE_SECTION = `
21
24
 
22
25
  export interface WorkspacePatternConfig {
23
26
  \tfile: string;
24
27
  \tslug: string;
25
28
  }
29
+ `;
30
+ const PATTERNS_CONST_SECTION = `
26
31
 
27
32
  export const PATTERNS: WorkspacePatternConfig[] = [
28
33
  \t// wp-typia add pattern entries
29
34
  ];
30
35
  `;
36
+ const BINDING_SOURCES_INTERFACE_SECTION = `
37
+
38
+ export interface WorkspaceBindingSourceConfig {
39
+ \teditorFile: string;
40
+ \tserverFile: string;
41
+ \tslug: string;
42
+ }
43
+ `;
44
+ const BINDING_SOURCES_CONST_SECTION = `
45
+
46
+ export const BINDING_SOURCES: WorkspaceBindingSourceConfig[] = [
47
+ \t// wp-typia add binding-source entries
48
+ ];
49
+ `;
31
50
  function getPropertyNameText(name) {
32
51
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
33
52
  return name.text;
@@ -123,6 +142,18 @@ function parsePatternEntries(arrayLiteral) {
123
142
  };
124
143
  });
125
144
  }
145
+ function parseBindingSourceEntries(arrayLiteral) {
146
+ return arrayLiteral.elements.map((element, elementIndex) => {
147
+ if (!ts.isObjectLiteralExpression(element)) {
148
+ throw new Error(`BINDING_SOURCES[${elementIndex}] must be an object literal in scripts/block-config.ts.`);
149
+ }
150
+ return {
151
+ editorFile: getRequiredStringProperty("BINDING_SOURCES", elementIndex, element, "editorFile"),
152
+ serverFile: getRequiredStringProperty("BINDING_SOURCES", elementIndex, element, "serverFile"),
153
+ slug: getRequiredStringProperty("BINDING_SOURCES", elementIndex, element, "slug"),
154
+ };
155
+ });
156
+ }
126
157
  /**
127
158
  * Parse workspace inventory entries from the source of `scripts/block-config.ts`.
128
159
  *
@@ -138,14 +169,22 @@ export function parseWorkspaceInventorySource(source) {
138
169
  }
139
170
  const variationArray = findExportedArrayLiteral(sourceFile, "VARIATIONS");
140
171
  const patternArray = findExportedArrayLiteral(sourceFile, "PATTERNS");
172
+ const bindingSourceArray = findExportedArrayLiteral(sourceFile, "BINDING_SOURCES");
141
173
  if (variationArray.found && !variationArray.array) {
142
174
  throw new Error("scripts/block-config.ts must export VARIATIONS as an array literal.");
143
175
  }
144
176
  if (patternArray.found && !patternArray.array) {
145
177
  throw new Error("scripts/block-config.ts must export PATTERNS as an array literal.");
146
178
  }
179
+ if (bindingSourceArray.found && !bindingSourceArray.array) {
180
+ throw new Error("scripts/block-config.ts must export BINDING_SOURCES as an array literal.");
181
+ }
147
182
  return {
183
+ bindingSources: bindingSourceArray.array
184
+ ? parseBindingSourceEntries(bindingSourceArray.array)
185
+ : [],
148
186
  blocks: parseBlockEntries(blockArray.array),
187
+ hasBindingSourcesSection: bindingSourceArray.found,
149
188
  hasPatternsSection: patternArray.found,
150
189
  hasVariationsSection: variationArray.found,
151
190
  patterns: patternArray.array ? parsePatternEntries(patternArray.array) : [],
@@ -199,26 +238,22 @@ export function getWorkspaceBlockSelectOptions(projectDir) {
199
238
  function ensureWorkspaceInventorySections(source) {
200
239
  let nextSource = source.trimEnd();
201
240
  if (!/export\s+interface\s+WorkspaceVariationConfig\b/u.test(nextSource)) {
202
- nextSource += VARIATIONS_SECTION;
241
+ nextSource += VARIATIONS_INTERFACE_SECTION;
203
242
  }
204
- else if (!/export\s+const\s+VARIATIONS\b/u.test(nextSource)) {
205
- nextSource += `
206
-
207
- export const VARIATIONS: WorkspaceVariationConfig[] = [
208
- \t// wp-typia add variation entries
209
- ];
210
- `;
243
+ if (!/export\s+const\s+VARIATIONS\b/u.test(nextSource)) {
244
+ nextSource += VARIATIONS_CONST_SECTION;
211
245
  }
212
246
  if (!/export\s+interface\s+WorkspacePatternConfig\b/u.test(nextSource)) {
213
- nextSource += PATTERNS_SECTION;
247
+ nextSource += PATTERNS_INTERFACE_SECTION;
214
248
  }
215
- else if (!/export\s+const\s+PATTERNS\b/u.test(nextSource)) {
216
- nextSource += `
217
-
218
- export const PATTERNS: WorkspacePatternConfig[] = [
219
- \t// wp-typia add pattern entries
220
- ];
221
- `;
249
+ if (!/export\s+const\s+PATTERNS\b/u.test(nextSource)) {
250
+ nextSource += PATTERNS_CONST_SECTION;
251
+ }
252
+ if (!/export\s+interface\s+WorkspaceBindingSourceConfig\b/u.test(nextSource)) {
253
+ nextSource += BINDING_SOURCES_INTERFACE_SECTION;
254
+ }
255
+ if (!/export\s+const\s+BINDING_SOURCES\b/u.test(nextSource)) {
256
+ nextSource += BINDING_SOURCES_CONST_SECTION;
222
257
  }
223
258
  return `${nextSource}\n`;
224
259
  }
@@ -242,7 +277,7 @@ function appendEntriesAtMarker(source, marker, entries) {
242
277
  * @param options Entry lists plus an optional source transformer.
243
278
  * @returns Updated source text with all requested inventory entries appended.
244
279
  */
245
- export function updateWorkspaceInventorySource(source, { blockEntries = [], patternEntries = [], variationEntries = [], transformSource, } = {}) {
280
+ export function updateWorkspaceInventorySource(source, { blockEntries = [], bindingSourceEntries = [], patternEntries = [], variationEntries = [], transformSource, } = {}) {
246
281
  let nextSource = ensureWorkspaceInventorySections(source);
247
282
  if (transformSource) {
248
283
  nextSource = transformSource(nextSource);
@@ -250,6 +285,7 @@ export function updateWorkspaceInventorySource(source, { blockEntries = [], patt
250
285
  nextSource = appendEntriesAtMarker(nextSource, BLOCK_CONFIG_ENTRY_MARKER, blockEntries);
251
286
  nextSource = appendEntriesAtMarker(nextSource, VARIATION_CONFIG_ENTRY_MARKER, variationEntries);
252
287
  nextSource = appendEntriesAtMarker(nextSource, PATTERN_CONFIG_ENTRY_MARKER, patternEntries);
288
+ nextSource = appendEntriesAtMarker(nextSource, BINDING_SOURCE_CONFIG_ENTRY_MARKER, bindingSourceEntries);
253
289
  return nextSource;
254
290
  }
255
291
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@wp-typia/api-client": "^0.4.0",
65
65
  "@wp-typia/block-runtime": "^0.4.0",
66
- "@wp-typia/rest": "^0.3.1",
66
+ "@wp-typia/rest": "^0.3.2",
67
67
  "@wp-typia/block-types": "^0.2.0",
68
68
  "mustache": "^4.2.0",
69
69
  "npm-package-arg": "^13.0.0",