@wp-typia/project-tools 0.20.2 → 0.21.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 (47) hide show
  1. package/dist/runtime/cli-add-shared.d.ts +73 -5
  2. package/dist/runtime/cli-add-shared.js +58 -11
  3. package/dist/runtime/cli-add-workspace-ability.js +11 -57
  4. package/dist/runtime/cli-add-workspace-admin-view.d.ts +23 -0
  5. package/dist/runtime/cli-add-workspace-admin-view.js +872 -0
  6. package/dist/runtime/cli-add-workspace-ai-anchors.js +2 -5
  7. package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +0 -4
  8. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +7 -17
  9. package/dist/runtime/cli-add-workspace-ai.js +4 -6
  10. package/dist/runtime/cli-add-workspace-assets.d.ts +13 -5
  11. package/dist/runtime/cli-add-workspace-assets.js +290 -106
  12. package/dist/runtime/cli-add-workspace-rest-anchors.js +2 -5
  13. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +0 -1
  14. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +7 -14
  15. package/dist/runtime/cli-add-workspace-rest.js +4 -6
  16. package/dist/runtime/cli-add-workspace.d.ts +58 -1
  17. package/dist/runtime/cli-add-workspace.js +588 -18
  18. package/dist/runtime/cli-add.d.ts +1 -1
  19. package/dist/runtime/cli-add.js +1 -1
  20. package/dist/runtime/cli-core.d.ts +8 -5
  21. package/dist/runtime/cli-core.js +7 -4
  22. package/dist/runtime/cli-diagnostics.d.ts +83 -1
  23. package/dist/runtime/cli-diagnostics.js +85 -2
  24. package/dist/runtime/cli-doctor-workspace.js +552 -13
  25. package/dist/runtime/cli-doctor.d.ts +4 -2
  26. package/dist/runtime/cli-doctor.js +2 -1
  27. package/dist/runtime/cli-help.js +19 -9
  28. package/dist/runtime/cli-init.d.ts +67 -3
  29. package/dist/runtime/cli-init.js +603 -64
  30. package/dist/runtime/cli-validation.js +4 -3
  31. package/dist/runtime/index.d.ts +9 -4
  32. package/dist/runtime/index.js +7 -3
  33. package/dist/runtime/package-json-types.d.ts +12 -0
  34. package/dist/runtime/package-json-types.js +1 -0
  35. package/dist/runtime/package-versions.d.ts +17 -2
  36. package/dist/runtime/package-versions.js +46 -1
  37. package/dist/runtime/php-utils.d.ts +16 -0
  38. package/dist/runtime/php-utils.js +59 -0
  39. package/dist/runtime/scaffold-answer-resolution.js +7 -6
  40. package/dist/runtime/scaffold-apply-utils.d.ts +2 -3
  41. package/dist/runtime/scaffold-apply-utils.js +3 -43
  42. package/dist/runtime/template-source-cache.d.ts +112 -0
  43. package/dist/runtime/template-source-cache.js +434 -0
  44. package/dist/runtime/template-source-seeds.js +319 -53
  45. package/dist/runtime/workspace-inventory.d.ts +43 -2
  46. package/dist/runtime/workspace-inventory.js +138 -5
  47. package/package.json +2 -2
@@ -1,10 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
+ import { syncBlockMetadata, } from "@wp-typia/block-runtime/metadata-core";
5
+ import ts from "typescript";
4
6
  import { resolveWorkspaceProject, } from "./workspace-project.js";
5
- import { readWorkspaceInventory, appendWorkspaceInventoryEntries } from "./workspace-inventory.js";
6
- import { toTitleCase } from "./string-case.js";
7
- import { assertBindingSourceDoesNotExist, assertEditorPluginDoesNotExist, assertPatternDoesNotExist, assertValidEditorPluginSlot, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
7
+ import { readWorkspaceInventory, appendWorkspaceInventoryEntries, } from "./workspace-inventory.js";
8
+ import { toPascalCase, toTitleCase } from "./string-case.js";
9
+ import { findPhpFunctionRange, hasPhpFunctionDefinition, quotePhpString, replacePhpFunctionDefinition, } from "./php-utils.js";
10
+ import { assertBindingSourceDoesNotExist, assertEditorPluginDoesNotExist, assertPatternDoesNotExist, assertValidEditorPluginSlot, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
8
11
  const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
9
12
  const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
10
13
  const BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
@@ -13,58 +16,7 @@ const EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
13
16
  const EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
14
17
  const EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
15
18
  const EDITOR_PLUGIN_EDITOR_STYLE_RTL = "build/editor-plugins/style-index-rtl.css";
16
- function escapeRegex(value) {
17
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
18
- }
19
- function quotePhpString(value) {
20
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
21
- }
22
- function findPhpFunctionRange(source, functionName) {
23
- const signaturePattern = new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u");
24
- const signatureMatch = signaturePattern.exec(source);
25
- if (!signatureMatch || signatureMatch.index === undefined) {
26
- return null;
27
- }
28
- const functionStart = signatureMatch.index;
29
- const openBraceIndex = source.indexOf("{", functionStart);
30
- if (openBraceIndex === -1) {
31
- return null;
32
- }
33
- let depth = 0;
34
- for (let index = openBraceIndex; index < source.length; index += 1) {
35
- const character = source[index];
36
- if (character === "{") {
37
- depth += 1;
38
- continue;
39
- }
40
- if (character !== "}") {
41
- continue;
42
- }
43
- depth -= 1;
44
- if (depth === 0) {
45
- let functionEnd = index + 1;
46
- while (functionEnd < source.length && /[\r\n]/u.test(source[functionEnd] ?? "")) {
47
- functionEnd += 1;
48
- }
49
- return {
50
- end: functionEnd,
51
- start: functionStart,
52
- };
53
- }
54
- }
55
- return null;
56
- }
57
- function replacePhpFunctionDefinition(source, functionName, replacement) {
58
- const functionRange = findPhpFunctionRange(source, functionName);
59
- if (!functionRange) {
60
- return null;
61
- }
62
- return [
63
- source.slice(0, functionRange.start),
64
- replacement,
65
- source.slice(functionRange.end),
66
- ].join("");
67
- }
19
+ const BINDING_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
68
20
  function buildPatternConfigEntry(patternSlug) {
69
21
  return [
70
22
  "\t{",
@@ -73,15 +25,47 @@ function buildPatternConfigEntry(patternSlug) {
73
25
  "\t},",
74
26
  ].join("\n");
75
27
  }
76
- function buildBindingSourceConfigEntry(bindingSourceSlug) {
28
+ function buildBindingSourceConfigEntry(bindingSourceSlug, target) {
77
29
  return [
78
30
  "\t{",
31
+ ...(target ? [`\t\tattribute: ${quoteTsString(target.attributeName)},`] : []),
32
+ ...(target ? [`\t\tblock: ${quoteTsString(target.blockSlug)},`] : []),
79
33
  `\t\teditorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
80
34
  `\t\tserverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
81
35
  `\t\tslug: ${quoteTsString(bindingSourceSlug)},`,
82
36
  "\t},",
83
37
  ].join("\n");
84
38
  }
39
+ function assertValidBindingAttributeName(attributeName) {
40
+ const trimmed = attributeName.trim();
41
+ if (!trimmed) {
42
+ throw new Error("`wp-typia add binding-source` requires --attribute <attribute> to include a value when --block is provided.");
43
+ }
44
+ if (!BINDING_ATTRIBUTE_NAME_PATTERN.test(trimmed)) {
45
+ throw new Error(`Binding attribute "${attributeName}" must start with a letter and use only letters, numbers, underscores, or hyphens.`);
46
+ }
47
+ return trimmed;
48
+ }
49
+ function resolveBindingTargetBlockSlug(blockName, namespace) {
50
+ const trimmed = blockName.trim();
51
+ if (!trimmed) {
52
+ throw new Error("`wp-typia add binding-source` requires --block <block-slug|namespace/block-slug> to include a value when --attribute is provided.");
53
+ }
54
+ const blockNameSegments = trimmed.split("/");
55
+ if (blockNameSegments.length > 2) {
56
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format.`);
57
+ }
58
+ if (blockNameSegments.some((segment) => segment.trim() === "")) {
59
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format without empty path segments.`);
60
+ }
61
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2
62
+ ? blockNameSegments
63
+ : [undefined, blockNameSegments[0]];
64
+ if (maybeNamespace && maybeNamespace !== namespace) {
65
+ throw new Error(`Binding target block "${trimmed}" uses namespace "${maybeNamespace}". Expected "${namespace}".`);
66
+ }
67
+ return normalizeBlockSlug(maybeSlug ?? "");
68
+ }
85
69
  function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
86
70
  return [
87
71
  "\t{",
@@ -91,13 +75,6 @@ function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
91
75
  "\t},",
92
76
  ].join("\n");
93
77
  }
94
- function toPascalCaseFromSlug(slug) {
95
- return normalizeBlockSlug(slug)
96
- .split("-")
97
- .filter(Boolean)
98
- .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
99
- .join("");
100
- }
101
78
  function buildPatternSource(patternSlug, namespace, textDomain) {
102
79
  const patternTitle = toTitleCase(patternSlug);
103
80
  return `<?php
@@ -116,12 +93,36 @@ register_block_pattern(
116
93
  );
117
94
  `;
118
95
  }
119
- function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain) {
96
+ function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain, target) {
120
97
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
121
98
  const bindingSourcePhpId = bindingSourceSlug.replace(/-/g, "_");
122
99
  const bindingSourceValueFunctionName = `${phpPrefix}_${bindingSourcePhpId}_binding_source_values`;
123
100
  const bindingSourceResolveFunctionName = `${phpPrefix}_${bindingSourcePhpId}_resolve_binding_source_value`;
101
+ const bindingSourceSupportedAttributesFunctionName = `${phpPrefix}_${bindingSourcePhpId}_supported_binding_attributes`;
124
102
  const starterValue = `${bindingSourceTitle} starter value`;
103
+ const supportedAttributesSource = target
104
+ ? `
105
+ if ( ! function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
106
+ \tfunction ${bindingSourceSupportedAttributesFunctionName}( array $supported_attributes ) : array {
107
+ \t\tif ( ! in_array( ${quotePhpString(target.attributeName)}, $supported_attributes, true ) ) {
108
+ \t\t\t$supported_attributes[] = ${quotePhpString(target.attributeName)};
109
+ \t\t}
110
+
111
+ \t\treturn $supported_attributes;
112
+ \t}
113
+ }
114
+ `
115
+ : "";
116
+ const supportedAttributesHook = target
117
+ ? `
118
+ if ( function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
119
+ \tadd_filter(
120
+ \t\t${quotePhpString(`block_bindings_supported_attributes_${namespace}/${target.blockSlug}`)},
121
+ \t\t${quotePhpString(bindingSourceSupportedAttributesFunctionName)}
122
+ \t);
123
+ }
124
+ `
125
+ : "";
125
126
  return `<?php
126
127
  if ( ! defined( 'ABSPATH' ) ) {
127
128
  \treturn;
@@ -150,6 +151,7 @@ if ( ! function_exists( '${bindingSourceResolveFunctionName}' ) ) {
150
151
  \t\treturn is_string( $value ) ? $value : '';
151
152
  \t}
152
153
  }
154
+ ${supportedAttributesSource}
153
155
 
154
156
  register_block_bindings_source(
155
157
  \t${quotePhpString(`${namespace}/${bindingSourceSlug}`)},
@@ -158,11 +160,22 @@ register_block_bindings_source(
158
160
  \t\t'get_value_callback' => ${quotePhpString(bindingSourceResolveFunctionName)},
159
161
  \t)
160
162
  );
161
- `;
163
+ ${supportedAttributesHook}`;
162
164
  }
163
- function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
165
+ function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain, target) {
164
166
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
165
167
  const starterValue = `${bindingSourceTitle} starter value`;
168
+ const bindingSourceName = `${namespace}/${bindingSourceSlug}`;
169
+ const targetSource = target
170
+ ? `
171
+ export const BINDING_SOURCE_TARGET = {
172
+ \tattribute: ${quoteTsString(target.attributeName)},
173
+ \tblock: ${quoteTsString(`${namespace}/${target.blockSlug}`)},
174
+ \tfield: ${quoteTsString(bindingSourceSlug)},
175
+ \tsource: ${quoteTsString(bindingSourceName)},
176
+ } as const;
177
+ `
178
+ : "";
166
179
  return `import { registerBlockBindingsSource } from '@wordpress/blocks';
167
180
  import { __ } from '@wordpress/i18n';
168
181
 
@@ -175,13 +188,14 @@ interface BindingSourceRegistration {
175
188
  const BINDING_SOURCE_VALUES: Record<string, string> = {
176
189
  \t${quoteTsString(bindingSourceSlug)}: ${quoteTsString(starterValue)},
177
190
  };
191
+ ${targetSource}
178
192
 
179
193
  function resolveBindingSourceValue( field: string ): string {
180
194
  \treturn BINDING_SOURCE_VALUES[ field ] ?? '';
181
195
  }
182
196
 
183
197
  registerBlockBindingsSource( {
184
- \tname: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
198
+ \tname: ${quoteTsString(bindingSourceName)},
185
199
  \tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
186
200
  \tgetFieldsList() {
187
201
  \t\treturn [
@@ -210,8 +224,99 @@ registerBlockBindingsSource( {
210
224
  } );
211
225
  `;
212
226
  }
227
+ function resolveBindingTarget(options, namespace) {
228
+ const hasBlock = options.blockName !== undefined && options.blockName.trim().length > 0;
229
+ const hasAttribute = options.attributeName !== undefined && options.attributeName.trim().length > 0;
230
+ if (!hasBlock && !hasAttribute) {
231
+ return undefined;
232
+ }
233
+ if (!hasBlock || !hasAttribute) {
234
+ throw new Error("`wp-typia add binding-source` requires --block and --attribute to be provided together.");
235
+ }
236
+ return {
237
+ attributeName: assertValidBindingAttributeName(options.attributeName ?? ""),
238
+ blockSlug: resolveBindingTargetBlockSlug(options.blockName ?? "", namespace),
239
+ };
240
+ }
241
+ function formatBindingAttributeTypeMember(attributeName) {
242
+ const propertyName = /^[A-Za-z_$][\w$]*$/u.test(attributeName)
243
+ ? attributeName
244
+ : JSON.stringify(attributeName);
245
+ return [
246
+ "\t/**",
247
+ "\t * Starter string attribute declared for WordPress Block Bindings.",
248
+ "\t */",
249
+ `\t${propertyName}?: string;`,
250
+ ].join("\n");
251
+ }
252
+ function getInterfaceDeclaration(source, interfaceName) {
253
+ const sourceFile = ts.createSourceFile("types.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
254
+ let declaration;
255
+ const visit = (node) => {
256
+ if (ts.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
257
+ declaration = node;
258
+ return true;
259
+ }
260
+ return ts.forEachChild(node, (child) => (visit(child) ? true : undefined)) ?? false;
261
+ };
262
+ visit(sourceFile);
263
+ return declaration ? { declaration, sourceFile } : undefined;
264
+ }
265
+ function getPropertyNameText(name) {
266
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
267
+ return name.text;
268
+ }
269
+ return undefined;
270
+ }
271
+ function interfaceHasAttributeMember(declaration, attributeName) {
272
+ return declaration.members.some((member) => ts.isPropertySignature(member) &&
273
+ member.name !== undefined &&
274
+ getPropertyNameText(member.name) === attributeName);
275
+ }
276
+ function insertBindingAttributeTypeMember(source, declaration, attributeName) {
277
+ let closeBracePosition = declaration.end - 1;
278
+ while (closeBracePosition > declaration.pos && source[closeBracePosition] !== "}") {
279
+ closeBracePosition -= 1;
280
+ }
281
+ if (source[closeBracePosition] !== "}") {
282
+ throw new Error("Unable to locate the target interface closing brace.");
283
+ }
284
+ const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
285
+ const beforeCloseBrace = source.slice(0, closeBracePosition);
286
+ const afterCloseBrace = source.slice(closeBracePosition);
287
+ const memberSource = formatBindingAttributeTypeMember(attributeName)
288
+ .split("\n")
289
+ .join(lineEnding);
290
+ const prefix = beforeCloseBrace.endsWith(lineEnding) ? "" : lineEnding;
291
+ return `${beforeCloseBrace}${prefix}${memberSource}${lineEnding}${afterCloseBrace}`;
292
+ }
293
+ async function ensureBindingTargetBlockAttributeType(projectDir, block, target) {
294
+ if (!block.attributeTypeName) {
295
+ throw new Error(`Workspace block "${block.slug}" must include attributeTypeName in scripts/block-config.ts before it can receive binding-source targets.`);
296
+ }
297
+ const typesPath = path.join(projectDir, block.typesFile);
298
+ const source = await fsp.readFile(typesPath, "utf8");
299
+ const targetInterface = getInterfaceDeclaration(source, block.attributeTypeName);
300
+ if (!targetInterface) {
301
+ throw new Error(`Unable to locate interface ${block.attributeTypeName} in ${block.typesFile}.`);
302
+ }
303
+ let nextSource = source;
304
+ if (!interfaceHasAttributeMember(targetInterface.declaration, target.attributeName)) {
305
+ nextSource = insertBindingAttributeTypeMember(source, targetInterface.declaration, target.attributeName);
306
+ await fsp.writeFile(typesPath, nextSource, "utf8");
307
+ }
308
+ await syncBlockMetadata({
309
+ blockJsonFile: path.join("src", "blocks", block.slug, "block.json"),
310
+ jsonSchemaFile: path.join("src", "blocks", block.slug, "typia.schema.json"),
311
+ manifestFile: path.join("src", "blocks", block.slug, "typia.manifest.json"),
312
+ openApiFile: path.join("src", "blocks", block.slug, "typia.openapi.json"),
313
+ projectRoot: projectDir,
314
+ sourceTypeName: block.attributeTypeName,
315
+ typesFile: block.typesFile,
316
+ });
317
+ }
213
318
  function buildEditorPluginTypesSource(editorPluginSlug) {
214
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
319
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
215
320
  return `export interface ${typeName} {
216
321
  \tprimaryActionLabel: string;
217
322
  \tsummary: string;
@@ -219,22 +324,22 @@ function buildEditorPluginTypesSource(editorPluginSlug) {
219
324
  `;
220
325
  }
221
326
  function buildEditorPluginDataSource(editorPluginSlug, slot) {
222
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
327
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
223
328
  const pluginTitle = toTitleCase(editorPluginSlug);
224
- const modelFactoryName = `get${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
225
- const enabledFactoryName = `is${toPascalCaseFromSlug(editorPluginSlug)}Enabled`;
329
+ const modelFactoryName = `get${toPascalCase(editorPluginSlug)}EditorPluginModel`;
330
+ const enabledFactoryName = `is${toPascalCase(editorPluginSlug)}Enabled`;
226
331
  return `import type { ${typeName} } from './types';
227
332
 
228
333
  export const EDITOR_PLUGIN_SLOT = ${quoteTsString(slot)} as const;
229
334
  export const REQUIRED_CAPABILITY = 'edit_posts' as const;
230
335
 
231
- const DEFAULT_SIDEBAR_MODEL: ${typeName} = {
336
+ const DEFAULT_EDITOR_PLUGIN_MODEL: ${typeName} = {
232
337
  \tprimaryActionLabel: ${quoteTsString(`Review ${pluginTitle}`)},
233
338
  \tsummary: ${quoteTsString(`Replace this summary with your ${pluginTitle} workflow state.`)},
234
339
  };
235
340
 
236
341
  export function ${modelFactoryName}(): ${typeName} {
237
- \treturn DEFAULT_SIDEBAR_MODEL;
342
+ \treturn DEFAULT_EDITOR_PLUGIN_MODEL;
238
343
  }
239
344
 
240
345
  export function ${enabledFactoryName}(): boolean {
@@ -242,11 +347,52 @@ export function ${enabledFactoryName}(): boolean {
242
347
  }
243
348
  `;
244
349
  }
245
- function buildEditorPluginSidebarSource(editorPluginSlug, textDomain) {
246
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
247
- const modelFactoryName = `get${pascalName}SidebarModel`;
350
+ function buildEditorPluginSurfaceSource(editorPluginSlug, slot, textDomain) {
351
+ const pascalName = toPascalCase(editorPluginSlug);
352
+ const modelFactoryName = `get${pascalName}EditorPluginModel`;
248
353
  const enabledFactoryName = `is${pascalName}Enabled`;
249
- const componentName = `${pascalName}Sidebar`;
354
+ const componentName = `${pascalName}Surface`;
355
+ if (slot === "document-setting-panel") {
356
+ return `import { Button } from '@wordpress/components';
357
+ import { PluginDocumentSettingPanel } from '@wordpress/editor';
358
+ import { __ } from '@wordpress/i18n';
359
+
360
+ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
361
+ import './style.scss';
362
+
363
+ export interface ${componentName}Props {
364
+ \tsurfaceName: string;
365
+ \ttitle: string;
366
+ }
367
+
368
+ export function ${componentName}( {
369
+ \tsurfaceName,
370
+ \ttitle,
371
+ }: ${componentName}Props ) {
372
+ \tif ( ! ${enabledFactoryName}() ) {
373
+ \t\treturn null;
374
+ \t}
375
+
376
+ \tconst editorPluginModel = ${modelFactoryName}();
377
+
378
+ \treturn (
379
+ \t\t<PluginDocumentSettingPanel
380
+ \t\t\tclassName="wp-typia-editor-plugin-shell"
381
+ \t\t\tname={ surfaceName }
382
+ \t\t\ttitle={ title }
383
+ \t\t>
384
+ \t\t\t<p>{ editorPluginModel.summary }</p>
385
+ \t\t\t<Button variant="secondary">
386
+ \t\t\t\t{ editorPluginModel.primaryActionLabel }
387
+ \t\t\t</Button>
388
+ \t\t\t<p className="wp-typia-editor-plugin-shell__hint">
389
+ \t\t\t\t{ __( 'Use data.ts to add post type, capability, or editor context guards before showing this panel.', ${quoteTsString(textDomain)} ) }
390
+ \t\t\t</p>
391
+ \t\t</PluginDocumentSettingPanel>
392
+ \t);
393
+ }
394
+ `;
395
+ }
250
396
  return `import { Button, PanelBody } from '@wordpress/components';
251
397
  import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/editor';
252
398
  import { __ } from '@wordpress/i18n';
@@ -255,34 +401,34 @@ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
255
401
  import './style.scss';
256
402
 
257
403
  export interface ${componentName}Props {
258
- \tpluginName: string;
404
+ \tsurfaceName: string;
259
405
  \ttitle: string;
260
406
  }
261
407
 
262
408
  export function ${componentName}( {
263
- \tpluginName,
409
+ \tsurfaceName,
264
410
  \ttitle,
265
411
  }: ${componentName}Props ) {
266
412
  \tif ( ! ${enabledFactoryName}() ) {
267
413
  \t\treturn null;
268
414
  \t}
269
415
 
270
- \tconst sidebarModel = ${modelFactoryName}();
416
+ \tconst editorPluginModel = ${modelFactoryName}();
271
417
 
272
418
  \treturn (
273
419
  \t\t<>
274
- \t\t\t<PluginSidebarMoreMenuItem target={ pluginName }>
420
+ \t\t\t<PluginSidebarMoreMenuItem target={ surfaceName }>
275
421
  \t\t\t\t{ title }
276
422
  \t\t\t</PluginSidebarMoreMenuItem>
277
- \t\t\t<PluginSidebar name={ pluginName } title={ title }>
423
+ \t\t\t<PluginSidebar name={ surfaceName } title={ title }>
278
424
  \t\t\t\t<div className="wp-typia-editor-plugin-shell">
279
425
  \t\t\t\t\t<PanelBody
280
426
  \t\t\t\t\t\tinitialOpen
281
427
  \t\t\t\t\t\ttitle={ __( 'Document workflow', ${quoteTsString(textDomain)} ) }
282
428
  \t\t\t\t\t>
283
- \t\t\t\t\t\t<p>{ sidebarModel.summary }</p>
429
+ \t\t\t\t\t\t<p>{ editorPluginModel.summary }</p>
284
430
  \t\t\t\t\t\t<Button variant="secondary">
285
- \t\t\t\t\t\t\t{ sidebarModel.primaryActionLabel }
431
+ \t\t\t\t\t\t\t{ editorPluginModel.primaryActionLabel }
286
432
  \t\t\t\t\t\t</Button>
287
433
  \t\t\t\t\t</PanelBody>
288
434
  \t\t\t\t</div>
@@ -293,24 +439,26 @@ export function ${componentName}( {
293
439
  `;
294
440
  }
295
441
  function buildEditorPluginEntrySource(editorPluginSlug, namespace, textDomain) {
296
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
297
- const componentName = `${pascalName}Sidebar`;
442
+ const pascalName = toPascalCase(editorPluginSlug);
443
+ const componentName = `${pascalName}Surface`;
298
444
  const pluginName = `${namespace}-${editorPluginSlug}`;
445
+ const surfaceName = `${pluginName}-surface`;
299
446
  const pluginTitle = toTitleCase(editorPluginSlug);
300
447
  return `import { registerPlugin } from '@wordpress/plugins';
301
448
  import { __ } from '@wordpress/i18n';
302
449
 
303
450
  import { REQUIRED_CAPABILITY } from './data';
304
- import { ${componentName} } from './Sidebar';
451
+ import { ${componentName} } from './Surface';
305
452
 
306
453
  const EDITOR_PLUGIN_NAME = ${quoteTsString(pluginName)};
454
+ const EDITOR_PLUGIN_SURFACE_NAME = ${quoteTsString(surfaceName)};
307
455
  const EDITOR_PLUGIN_TITLE = __( ${quoteTsString(pluginTitle)}, ${quoteTsString(textDomain)} );
308
456
 
309
457
  registerPlugin( EDITOR_PLUGIN_NAME, {
310
458
  \ticon: 'admin-generic',
311
459
  \trender: () => (
312
460
  \t\t<${componentName}
313
- \t\t\tpluginName={ EDITOR_PLUGIN_NAME }
461
+ \t\t\tsurfaceName={ EDITOR_PLUGIN_SURFACE_NAME }
314
462
  \t\t\ttitle={ EDITOR_PLUGIN_TITLE }
315
463
  \t\t/>
316
464
  \t),
@@ -327,6 +475,11 @@ function buildEditorPluginStyleSource() {
327
475
  .wp-typia-editor-plugin-shell p {
328
476
  \tmargin: 0 0 12px;
329
477
  }
478
+
479
+ .wp-typia-editor-plugin-shell__hint {
480
+ \tcolor: #757575;
481
+ \tfont-size: 12px;
482
+ }
330
483
  `;
331
484
  }
332
485
  function buildBindingSourceIndexSource(bindingSourceSlugs) {
@@ -445,7 +598,6 @@ function ${bindingEditorEnqueueFunctionName}() {
445
598
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
446
599
  /\?>\s*$/u,
447
600
  ];
448
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
449
601
  const insertPhpSnippet = (snippet) => {
450
602
  for (const anchor of insertionAnchors) {
451
603
  const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
@@ -464,10 +616,10 @@ function ${bindingEditorEnqueueFunctionName}() {
464
616
  }
465
617
  nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
466
618
  };
467
- if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
619
+ if (!hasPhpFunctionDefinition(nextSource, bindingRegistrationFunctionName)) {
468
620
  insertPhpSnippet(bindingRegistrationFunction);
469
621
  }
470
- if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
622
+ if (!hasPhpFunctionDefinition(nextSource, bindingEditorEnqueueFunctionName)) {
471
623
  insertPhpSnippet(bindingEditorEnqueueFunction);
472
624
  }
473
625
  if (!nextSource.includes(bindingRegistrationHook)) {
@@ -528,7 +680,6 @@ function ${enqueueFunctionName}() {
528
680
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
529
681
  /\?>\s*$/u,
530
682
  ];
531
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
532
683
  const insertPhpSnippet = (snippet) => {
533
684
  for (const anchor of insertionAnchors) {
534
685
  const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
@@ -547,7 +698,7 @@ function ${enqueueFunctionName}() {
547
698
  }
548
699
  nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
549
700
  };
550
- if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
701
+ if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
551
702
  insertPhpSnippet(enqueueFunction);
552
703
  }
553
704
  else {
@@ -657,7 +808,7 @@ async function writeEditorPluginRegistry(projectDir, editorPluginSlug) {
657
808
  * Defaults to `process.cwd()`.
658
809
  * @param options.editorPluginName Human-entered editor-plugin name that will be
659
810
  * normalized and validated before files are written.
660
- * @param options.slot Optional editor plugin shell slot. Defaults to `PluginSidebar`.
811
+ * @param options.slot Optional editor plugin shell slot. Defaults to `sidebar`.
661
812
  * @returns A promise that resolves with the normalized `editorPluginSlug`, chosen
662
813
  * `slot`, and owning `projectDir` after the scaffold files and inventory entry
663
814
  * are written successfully.
@@ -666,7 +817,7 @@ async function writeEditorPluginRegistry(projectDir, editorPluginSlug) {
666
817
  */
667
818
  export async function runAddEditorPluginCommand({ cwd = process.cwd(), editorPluginName, slot, }) {
668
819
  const workspace = resolveWorkspaceProject(cwd);
669
- const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <PluginSidebar>]");
820
+ const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <sidebar|document-setting-panel>]");
670
821
  const resolvedSlot = assertValidEditorPluginSlot(slot);
671
822
  const inventory = readWorkspaceInventory(workspace.projectDir);
672
823
  assertEditorPluginDoesNotExist(workspace.projectDir, editorPluginSlug, inventory);
@@ -677,7 +828,7 @@ export async function runAddEditorPluginCommand({ cwd = process.cwd(), editorPlu
677
828
  const webpackConfigPath = path.join(workspace.projectDir, "webpack.config.js");
678
829
  const editorPluginDir = path.join(workspace.projectDir, "src", "editor-plugins", editorPluginSlug);
679
830
  const entryFilePath = path.join(editorPluginDir, "index.tsx");
680
- const sidebarFilePath = path.join(editorPluginDir, "Sidebar.tsx");
831
+ const surfaceFilePath = path.join(editorPluginDir, "Surface.tsx");
681
832
  const dataFilePath = path.join(editorPluginDir, "data.ts");
682
833
  const typesFilePath = path.join(editorPluginDir, "types.ts");
683
834
  const styleFilePath = path.join(editorPluginDir, "style.scss");
@@ -698,7 +849,7 @@ export async function runAddEditorPluginCommand({ cwd = process.cwd(), editorPlu
698
849
  await ensureEditorPluginBuildScriptAnchors(workspace);
699
850
  await ensureEditorPluginWebpackAnchors(workspace);
700
851
  await fsp.writeFile(entryFilePath, buildEditorPluginEntrySource(editorPluginSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
701
- await fsp.writeFile(sidebarFilePath, buildEditorPluginSidebarSource(editorPluginSlug, workspace.workspace.textDomain), "utf8");
852
+ await fsp.writeFile(surfaceFilePath, buildEditorPluginSurfaceSource(editorPluginSlug, resolvedSlot, workspace.workspace.textDomain), "utf8");
702
853
  await fsp.writeFile(dataFilePath, buildEditorPluginDataSource(editorPluginSlug, resolvedSlot), "utf8");
703
854
  await fsp.writeFile(typesFilePath, buildEditorPluginTypesSource(editorPluginSlug), "utf8");
704
855
  await fsp.writeFile(styleFilePath, buildEditorPluginStyleSource(), "utf8");
@@ -768,42 +919,75 @@ export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }
768
919
  * Add one block binding source scaffold to an official workspace project.
769
920
  *
770
921
  * @param options Command options for the binding-source scaffold workflow.
922
+ * @param options.attributeName Optional generated block attribute to declare as
923
+ * bindable. Must be provided together with `blockName`.
924
+ * @param options.blockName Optional generated block slug or full block name to
925
+ * receive the bindable attribute wiring. Must be provided together with
926
+ * `attributeName`.
771
927
  * @param options.bindingSourceName Human-entered binding source name that will
772
928
  * be normalized and validated before files are written.
773
929
  * @param options.cwd Working directory used to resolve the nearest official
774
930
  * workspace. Defaults to `process.cwd()`.
775
931
  * @returns A promise that resolves with the normalized `bindingSourceSlug` and
776
- * owning `projectDir` after the server/editor files and inventory entry have
777
- * been written successfully.
932
+ * owning `projectDir` after the server/editor files, optional target block
933
+ * metadata, and inventory entry have been written successfully.
778
934
  * @throws {Error} When the command is run outside an official workspace, when
779
- * the slug is invalid, or when a conflicting file or inventory entry exists.
935
+ * the slug is invalid, when a binding target is incomplete or unknown, or when
936
+ * a conflicting file or inventory entry exists.
780
937
  */
781
- export async function runAddBindingSourceCommand({ bindingSourceName, cwd = process.cwd(), }) {
938
+ export async function runAddBindingSourceCommand({ attributeName, bindingSourceName, blockName, cwd = process.cwd(), }) {
782
939
  const workspace = resolveWorkspaceProject(cwd);
783
- const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
940
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
784
941
  const inventory = readWorkspaceInventory(workspace.projectDir);
785
942
  assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
943
+ const target = resolveBindingTarget({
944
+ attributeName,
945
+ blockName,
946
+ }, workspace.workspace.namespace);
947
+ const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
786
948
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
787
949
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
788
950
  const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
789
951
  const bindingSourceDir = path.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
790
952
  const serverFilePath = path.join(bindingSourceDir, "server.php");
791
953
  const editorFilePath = path.join(bindingSourceDir, "editor.ts");
954
+ const blockJsonPath = target
955
+ ? path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "block.json")
956
+ : undefined;
957
+ const targetGeneratedMetadataPaths = target
958
+ ? [
959
+ path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.manifest.json"),
960
+ path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.openapi.json"),
961
+ path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.schema.json"),
962
+ path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia-validator.php"),
963
+ ]
964
+ : [];
792
965
  const mutationSnapshot = {
793
- fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
966
+ fileSources: await snapshotWorkspaceFiles([
967
+ blockConfigPath,
968
+ bootstrapPath,
969
+ bindingsIndexPath,
970
+ ...(blockJsonPath ? [blockJsonPath] : []),
971
+ ...(targetBlock ? [path.join(workspace.projectDir, targetBlock.typesFile)] : []),
972
+ ...targetGeneratedMetadataPaths,
973
+ ]),
794
974
  snapshotDirs: [],
795
975
  targetPaths: [bindingSourceDir],
796
976
  };
797
977
  try {
798
978
  await fsp.mkdir(bindingSourceDir, { recursive: true });
799
979
  await ensureBindingSourceBootstrapAnchors(workspace);
800
- await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
801
- await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
980
+ await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
981
+ await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
982
+ if (target && targetBlock) {
983
+ await ensureBindingTargetBlockAttributeType(workspace.projectDir, targetBlock, target);
984
+ }
802
985
  await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
803
986
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
804
- bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)],
987
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug, target)],
805
988
  });
806
989
  return {
990
+ ...(target ? { attributeName: target.attributeName, blockSlug: target.blockSlug } : {}),
807
991
  bindingSourceSlug,
808
992
  projectDir: workspace.projectDir,
809
993
  };