@wp-typia/project-tools 0.16.6 → 0.16.7

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.
@@ -0,0 +1,582 @@
1
+ import fs from "node:fs";
2
+ import { promises as fsp } from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveWorkspaceProject, } from "./workspace-project.js";
5
+ import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from "./workspace-inventory.js";
6
+ import { toKebabCase, toTitleCase, } from "./string-case.js";
7
+ import { assertBindingSourceDoesNotExist, assertPatternDoesNotExist, assertValidGeneratedSlug, assertValidHookAnchor, assertValidHookedBlockPosition, assertVariationDoesNotExist, getMutableBlockHooks, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, readWorkspaceBlockJson, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
8
+ const VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
9
+ const VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
10
+ const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
11
+ const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
12
+ const BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
13
+ const BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
14
+ function escapeRegex(value) {
15
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
16
+ }
17
+ function buildVariationConfigEntry(blockSlug, variationSlug) {
18
+ return [
19
+ "\t{",
20
+ `\t\tblock: ${quoteTsString(blockSlug)},`,
21
+ `\t\tfile: ${quoteTsString(`src/blocks/${blockSlug}/variations/${variationSlug}.ts`)},`,
22
+ `\t\tslug: ${quoteTsString(variationSlug)},`,
23
+ "\t},",
24
+ ].join("\n");
25
+ }
26
+ function buildPatternConfigEntry(patternSlug) {
27
+ return [
28
+ "\t{",
29
+ `\t\tfile: ${quoteTsString(`src/patterns/${patternSlug}.php`)},`,
30
+ `\t\tslug: ${quoteTsString(patternSlug)},`,
31
+ "\t},",
32
+ ].join("\n");
33
+ }
34
+ function buildBindingSourceConfigEntry(bindingSourceSlug) {
35
+ return [
36
+ "\t{",
37
+ `\t\teditorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
38
+ `\t\tserverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
39
+ `\t\tslug: ${quoteTsString(bindingSourceSlug)},`,
40
+ "\t},",
41
+ ].join("\n");
42
+ }
43
+ function buildVariationConstName(variationSlug) {
44
+ const identifierSegments = toKebabCase(variationSlug)
45
+ .split("-")
46
+ .filter(Boolean);
47
+ return `workspaceVariation_${identifierSegments.join("_")}`;
48
+ }
49
+ function getVariationConstBindings(variationSlugs) {
50
+ const seenConstNames = new Map();
51
+ return variationSlugs.map((variationSlug) => {
52
+ const constName = buildVariationConstName(variationSlug);
53
+ const previousSlug = seenConstNames.get(constName);
54
+ if (previousSlug && previousSlug !== variationSlug) {
55
+ throw new Error(`Variation slugs "${previousSlug}" and "${variationSlug}" generate the same registry identifier "${constName}". Rename one of the variations.`);
56
+ }
57
+ seenConstNames.set(constName, variationSlug);
58
+ return { constName, variationSlug };
59
+ });
60
+ }
61
+ function buildVariationSource(variationSlug, textDomain) {
62
+ const variationTitle = toTitleCase(variationSlug);
63
+ const variationConstName = buildVariationConstName(variationSlug);
64
+ return `import type { BlockVariation } from '@wordpress/blocks';
65
+ import { __ } from '@wordpress/i18n';
66
+
67
+ export const ${variationConstName} = {
68
+ \tname: ${quoteTsString(variationSlug)},
69
+ \ttitle: __( ${quoteTsString(variationTitle)}, ${quoteTsString(textDomain)} ),
70
+ \tdescription: __(
71
+ \t\t${quoteTsString(`A starter variation for ${variationTitle}.`)},
72
+ \t\t${quoteTsString(textDomain)},
73
+ \t),
74
+ \tattributes: {},
75
+ \tscope: ['inserter'],
76
+ } satisfies BlockVariation;
77
+ `;
78
+ }
79
+ function buildVariationIndexSource(variationSlugs) {
80
+ const variationBindings = getVariationConstBindings(variationSlugs);
81
+ const importLines = variationBindings
82
+ .map(({ constName, variationSlug }) => {
83
+ return `import { ${constName} } from './${variationSlug}';`;
84
+ })
85
+ .join("\n");
86
+ const variationConstNames = variationBindings
87
+ .map(({ constName }) => constName)
88
+ .join(",\n\t\t");
89
+ return `import { registerBlockVariation } from '@wordpress/blocks';
90
+ import metadata from '../block.json';
91
+ ${importLines ? `\n${importLines}` : ""}
92
+
93
+ const WORKSPACE_VARIATIONS = [
94
+ \t${variationConstNames}
95
+ \t// wp-typia add variation entries
96
+ ];
97
+
98
+ export function registerWorkspaceVariations() {
99
+ \tfor (const variation of WORKSPACE_VARIATIONS) {
100
+ \t\tregisterBlockVariation(metadata.name, variation);
101
+ \t}
102
+ }
103
+ `;
104
+ }
105
+ function buildPatternSource(patternSlug, namespace, textDomain) {
106
+ const patternTitle = toTitleCase(patternSlug);
107
+ return `<?php
108
+ if ( ! defined( 'ABSPATH' ) ) {
109
+ \treturn;
110
+ }
111
+
112
+ register_block_pattern(
113
+ \t'${namespace}/${patternSlug}',
114
+ \tarray(
115
+ \t\t'title' => __( ${JSON.stringify(patternTitle)}, '${textDomain}' ),
116
+ \t\t'description' => __( ${JSON.stringify(`A starter pattern for ${patternTitle}.`)}, '${textDomain}' ),
117
+ \t\t'categories' => array( '${namespace}' ),
118
+ \t\t'content' => '<!-- wp:paragraph --><p>' . esc_html__( 'Describe this pattern here.', '${textDomain}' ) . '</p><!-- /wp:paragraph -->',
119
+ \t)
120
+ );
121
+ `;
122
+ }
123
+ function buildBindingSourceServerSource(bindingSourceSlug, namespace, textDomain) {
124
+ const bindingSourceTitle = toTitleCase(bindingSourceSlug);
125
+ return `<?php
126
+ if ( ! defined( 'ABSPATH' ) ) {
127
+ \treturn;
128
+ }
129
+
130
+ if ( ! function_exists( 'register_block_bindings_source' ) ) {
131
+ \treturn;
132
+ }
133
+
134
+ register_block_bindings_source(
135
+ \t'${namespace}/${bindingSourceSlug}',
136
+ \tarray(
137
+ \t\t'label' => __( ${JSON.stringify(bindingSourceTitle)}, '${textDomain}' ),
138
+ \t\t'get_value_callback' => static function( array $source_args ) : string {
139
+ \t\t\t$field = isset( $source_args['field'] ) && is_string( $source_args['field'] )
140
+ \t\t\t\t? $source_args['field']
141
+ \t\t\t\t: '${bindingSourceSlug}';
142
+
143
+ \t\t\treturn sprintf(
144
+ \t\t\t\t__( 'Replace %s with real binding source data.', '${textDomain}' ),
145
+ \t\t\t\t$field
146
+ \t\t\t);
147
+ \t\t},
148
+ \t)
149
+ );
150
+ `;
151
+ }
152
+ function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
153
+ const bindingSourceTitle = toTitleCase(bindingSourceSlug);
154
+ return `import { registerBlockBindingsSource } from '@wordpress/blocks';
155
+ import { __ } from '@wordpress/i18n';
156
+
157
+ registerBlockBindingsSource( {
158
+ \tname: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
159
+ \tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
160
+ \tgetFieldsList() {
161
+ \t\treturn [
162
+ \t\t\t{
163
+ \t\t\t\tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
164
+ \t\t\t\ttype: 'string',
165
+ \t\t\t\targs: {
166
+ \t\t\t\t\tfield: ${quoteTsString(bindingSourceSlug)},
167
+ \t\t\t\t},
168
+ \t\t\t},
169
+ \t\t];
170
+ \t},
171
+ \tgetValues( { bindings } ) {
172
+ \t\tconst values: Record<string, string> = {};
173
+ \t\tfor ( const attributeName of Object.keys( bindings ) ) {
174
+ \t\t\tvalues[ attributeName ] = ${quoteTsString(`TODO: replace ${bindingSourceSlug} with real editor-side values.`)};
175
+ \t\t}
176
+ \t\treturn values;
177
+ \t},
178
+ } );
179
+ `;
180
+ }
181
+ function buildBindingSourceIndexSource(bindingSourceSlugs) {
182
+ const importLines = bindingSourceSlugs
183
+ .map((bindingSourceSlug) => `import './${bindingSourceSlug}/editor';`)
184
+ .join("\n");
185
+ return `${importLines}${importLines ? "\n\n" : ""}// wp-typia add binding-source entries\n`;
186
+ }
187
+ async function ensureVariationRegistrationHook(blockIndexPath) {
188
+ await patchFile(blockIndexPath, (source) => {
189
+ let nextSource = source;
190
+ if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
191
+ nextSource = `${VARIATIONS_IMPORT_LINE}\n${nextSource}`;
192
+ }
193
+ if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
194
+ const callInsertionPatterns = [
195
+ /(registerBlockType<[\s\S]*?\);\s*)/u,
196
+ /(registerBlockType\([\s\S]*?\);\s*)/u,
197
+ ];
198
+ let inserted = false;
199
+ for (const pattern of callInsertionPatterns) {
200
+ const candidate = nextSource.replace(pattern, (match) => `${match}\n${VARIATIONS_CALL_LINE}\n`);
201
+ if (candidate !== nextSource) {
202
+ nextSource = candidate;
203
+ inserted = true;
204
+ break;
205
+ }
206
+ }
207
+ if (!inserted) {
208
+ nextSource = `${nextSource.trimEnd()}\n\n${VARIATIONS_CALL_LINE}\n`;
209
+ }
210
+ }
211
+ if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
212
+ throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path.basename(blockIndexPath)}.`);
213
+ }
214
+ return nextSource;
215
+ });
216
+ }
217
+ async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
218
+ const variationsDir = path.join(projectDir, "src", "blocks", blockSlug, "variations");
219
+ const variationsIndexPath = path.join(variationsDir, "index.ts");
220
+ await fsp.mkdir(variationsDir, { recursive: true });
221
+ const existingVariationSlugs = fs
222
+ .readdirSync(variationsDir)
223
+ .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
224
+ .map((entry) => entry.replace(/\.ts$/u, ""));
225
+ const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
226
+ await fsp.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
227
+ }
228
+ async function ensurePatternBootstrapAnchors(workspace) {
229
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
230
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
231
+ await patchFile(bootstrapPath, (source) => {
232
+ let nextSource = source;
233
+ const patternCategoryFunctionName = `${workspace.workspace.phpPrefix}_register_pattern_category`;
234
+ const patternRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_patterns`;
235
+ const patternCategoryHook = `add_action( 'init', '${patternCategoryFunctionName}' );`;
236
+ const patternRegistrationHook = `add_action( 'init', '${patternRegistrationFunctionName}', 20 );`;
237
+ const patternFunctions = `
238
+
239
+ function ${patternCategoryFunctionName}() {
240
+ \tif ( function_exists( 'register_block_pattern_category' ) ) {
241
+ \t\tregister_block_pattern_category(
242
+ \t\t\t'${workspace.workspace.namespace}',
243
+ \t\t\tarray(
244
+ \t\t\t\t'label' => __( ${JSON.stringify(`${toTitleCase(workspaceBaseName)} Patterns`)}, '${workspace.workspace.textDomain}' ),
245
+ \t\t\t)
246
+ \t\t);
247
+ \t}
248
+ }
249
+
250
+ function ${patternRegistrationFunctionName}() {
251
+ \tforeach ( glob( __DIR__ . '/src/patterns/*.php' ) ?: array() as $pattern_module ) {
252
+ \t\trequire $pattern_module;
253
+ \t}
254
+ }
255
+ `;
256
+ if (!nextSource.includes(PATTERN_BOOTSTRAP_CATEGORY)) {
257
+ const insertionAnchors = [
258
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
259
+ /\?>\s*$/u,
260
+ ];
261
+ let inserted = false;
262
+ for (const anchor of insertionAnchors) {
263
+ const candidate = nextSource.replace(anchor, (match) => `${patternFunctions}\n${match}`);
264
+ if (candidate !== nextSource) {
265
+ nextSource = candidate;
266
+ inserted = true;
267
+ break;
268
+ }
269
+ }
270
+ if (!inserted) {
271
+ nextSource = `${nextSource.trimEnd()}\n${patternFunctions}\n`;
272
+ }
273
+ }
274
+ if (!nextSource.includes(patternCategoryFunctionName) ||
275
+ !nextSource.includes(patternRegistrationFunctionName)) {
276
+ throw new Error(`Unable to inject pattern bootstrap functions into ${path.basename(bootstrapPath)}.`);
277
+ }
278
+ if (!nextSource.includes(patternCategoryHook)) {
279
+ nextSource = `${nextSource.trimEnd()}\n${patternCategoryHook}\n`;
280
+ }
281
+ if (!nextSource.includes(patternRegistrationHook)) {
282
+ nextSource = `${nextSource.trimEnd()}\n${patternRegistrationHook}\n`;
283
+ }
284
+ return nextSource;
285
+ });
286
+ }
287
+ async function ensureBindingSourceBootstrapAnchors(workspace) {
288
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
289
+ await patchFile(bootstrapPath, (source) => {
290
+ let nextSource = source;
291
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
292
+ const bindingRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_binding_sources`;
293
+ const bindingEditorEnqueueFunctionName = `${workspace.workspace.phpPrefix}_enqueue_binding_sources_editor`;
294
+ const bindingRegistrationHook = `add_action( 'init', '${bindingRegistrationFunctionName}', 20 );`;
295
+ const bindingEditorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${bindingEditorEnqueueFunctionName}' );`;
296
+ const bindingRegistrationFunction = `
297
+
298
+ function ${bindingRegistrationFunctionName}() {
299
+ \tforeach ( glob( __DIR__ . '${BINDING_SOURCE_SERVER_GLOB}' ) ?: array() as $binding_source_module ) {
300
+ \t\trequire_once $binding_source_module;
301
+ \t}
302
+ }
303
+ `;
304
+ const bindingEditorEnqueueFunction = `
305
+
306
+ function ${bindingEditorEnqueueFunctionName}() {
307
+ \t$script_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_SCRIPT}';
308
+ \t$asset_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_ASSET}';
309
+
310
+ \tif ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
311
+ \t\treturn;
312
+ \t}
313
+
314
+ \t$asset = require $asset_path;
315
+ \tif ( ! is_array( $asset ) ) {
316
+ \t\t$asset = array();
317
+ \t}
318
+
319
+ \twp_enqueue_script(
320
+ \t\t'${workspaceBaseName}-binding-sources',
321
+ \t\tplugins_url( '${BINDING_SOURCE_EDITOR_SCRIPT}', __FILE__ ),
322
+ \t\tisset( $asset['dependencies'] ) && is_array( $asset['dependencies'] ) ? $asset['dependencies'] : array(),
323
+ \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
324
+ \t\ttrue
325
+ \t);
326
+ }
327
+ `;
328
+ const insertionAnchors = [
329
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
330
+ /\?>\s*$/u,
331
+ ];
332
+ const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
333
+ const insertPhpSnippet = (snippet) => {
334
+ for (const anchor of insertionAnchors) {
335
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
336
+ if (candidate !== nextSource) {
337
+ nextSource = candidate;
338
+ return;
339
+ }
340
+ }
341
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
342
+ };
343
+ const appendPhpSnippet = (snippet) => {
344
+ const closingTagPattern = /\?>\s*$/u;
345
+ if (closingTagPattern.test(nextSource)) {
346
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
347
+ return;
348
+ }
349
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
350
+ };
351
+ if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
352
+ insertPhpSnippet(bindingRegistrationFunction);
353
+ }
354
+ if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
355
+ insertPhpSnippet(bindingEditorEnqueueFunction);
356
+ }
357
+ if (!nextSource.includes(bindingRegistrationHook)) {
358
+ appendPhpSnippet(bindingRegistrationHook);
359
+ }
360
+ if (!nextSource.includes(bindingEditorEnqueueHook)) {
361
+ appendPhpSnippet(bindingEditorEnqueueHook);
362
+ }
363
+ return nextSource;
364
+ });
365
+ }
366
+ function resolveBindingSourceRegistryPath(projectDir) {
367
+ const bindingsDir = path.join(projectDir, "src", "bindings");
368
+ return [path.join(bindingsDir, "index.ts"), path.join(bindingsDir, "index.js")].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(bindingsDir, "index.ts");
369
+ }
370
+ async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
371
+ const bindingsDir = path.join(projectDir, "src", "bindings");
372
+ const bindingsIndexPath = resolveBindingSourceRegistryPath(projectDir);
373
+ await fsp.mkdir(bindingsDir, { recursive: true });
374
+ const existingBindingSourceSlugs = fs
375
+ .readdirSync(bindingsDir, { withFileTypes: true })
376
+ .filter((entry) => entry.isDirectory())
377
+ .map((entry) => entry.name);
378
+ const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
379
+ await fsp.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
380
+ }
381
+ /**
382
+ * Add one variation entry to an existing workspace block.
383
+ *
384
+ * @param options Command options for the variation scaffold workflow.
385
+ * @param options.blockName Target workspace block slug that will own the variation.
386
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
387
+ * Defaults to `process.cwd()`.
388
+ * @param options.variationName Human-entered variation name that will be normalized
389
+ * and validated before files are written.
390
+ * @returns A promise that resolves with the normalized `blockSlug`,
391
+ * `variationSlug`, and owning `projectDir` after the variation files and
392
+ * inventory entry have been written successfully.
393
+ * @throws {Error} When the command is run outside an official workspace, when
394
+ * the target block is unknown, when the variation slug is invalid, or when a
395
+ * conflicting file or inventory entry already exists.
396
+ */
397
+ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), variationName, }) {
398
+ const workspace = resolveWorkspaceProject(cwd);
399
+ const blockSlug = normalizeBlockSlug(blockName);
400
+ const variationSlug = assertValidGeneratedSlug("Variation name", normalizeBlockSlug(variationName), "wp-typia add variation <name> --block <block-slug>");
401
+ const inventory = readWorkspaceInventory(workspace.projectDir);
402
+ resolveWorkspaceBlock(inventory, blockSlug);
403
+ assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
404
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
405
+ const blockIndexPath = path.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
406
+ const variationsDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
407
+ const variationFilePath = path.join(variationsDir, `${variationSlug}.ts`);
408
+ const variationsIndexPath = path.join(variationsDir, "index.ts");
409
+ const mutationSnapshot = {
410
+ fileSources: await snapshotWorkspaceFiles([
411
+ blockConfigPath,
412
+ blockIndexPath,
413
+ variationsIndexPath,
414
+ ]),
415
+ snapshotDirs: [],
416
+ targetPaths: [variationFilePath],
417
+ };
418
+ try {
419
+ await fsp.mkdir(variationsDir, { recursive: true });
420
+ await fsp.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
421
+ await writeVariationRegistry(workspace.projectDir, blockSlug, variationSlug);
422
+ await ensureVariationRegistrationHook(blockIndexPath);
423
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
424
+ variationEntries: [buildVariationConfigEntry(blockSlug, variationSlug)],
425
+ });
426
+ return {
427
+ blockSlug,
428
+ projectDir: workspace.projectDir,
429
+ variationSlug,
430
+ };
431
+ }
432
+ catch (error) {
433
+ await rollbackWorkspaceMutation(mutationSnapshot);
434
+ throw error;
435
+ }
436
+ }
437
+ /**
438
+ * Add one PHP block pattern shell to an official workspace project.
439
+ *
440
+ * @param options Command options for the pattern scaffold workflow.
441
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
442
+ * Defaults to `process.cwd()`.
443
+ * @param options.patternName Human-entered pattern name that will be normalized
444
+ * and validated before files are written.
445
+ * @returns A promise that resolves with the normalized `patternSlug` and
446
+ * owning `projectDir` after the pattern file and inventory entry have been
447
+ * written successfully.
448
+ * @throws {Error} When the command is run outside an official workspace, when
449
+ * the pattern slug is invalid, or when a conflicting file or inventory entry
450
+ * already exists.
451
+ */
452
+ export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }) {
453
+ const workspace = resolveWorkspaceProject(cwd);
454
+ const patternSlug = assertValidGeneratedSlug("Pattern name", normalizeBlockSlug(patternName), "wp-typia add pattern <name>");
455
+ const inventory = readWorkspaceInventory(workspace.projectDir);
456
+ assertPatternDoesNotExist(workspace.projectDir, patternSlug, inventory);
457
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
458
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
459
+ const patternFilePath = path.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
460
+ const mutationSnapshot = {
461
+ fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath]),
462
+ snapshotDirs: [],
463
+ targetPaths: [patternFilePath],
464
+ };
465
+ try {
466
+ await fsp.mkdir(path.dirname(patternFilePath), { recursive: true });
467
+ await ensurePatternBootstrapAnchors(workspace);
468
+ await fsp.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
469
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
470
+ patternEntries: [buildPatternConfigEntry(patternSlug)],
471
+ });
472
+ return {
473
+ patternSlug,
474
+ projectDir: workspace.projectDir,
475
+ };
476
+ }
477
+ catch (error) {
478
+ await rollbackWorkspaceMutation(mutationSnapshot);
479
+ throw error;
480
+ }
481
+ }
482
+ /**
483
+ * Add one block binding source scaffold to an official workspace project.
484
+ *
485
+ * @param options Command options for the binding-source scaffold workflow.
486
+ * @param options.bindingSourceName Human-entered binding source name that will
487
+ * be normalized and validated before files are written.
488
+ * @param options.cwd Working directory used to resolve the nearest official
489
+ * workspace. Defaults to `process.cwd()`.
490
+ * @returns A promise that resolves with the normalized `bindingSourceSlug` and
491
+ * owning `projectDir` after the server/editor files and inventory entry have
492
+ * been written successfully.
493
+ * @throws {Error} When the command is run outside an official workspace, when
494
+ * the slug is invalid, or when a conflicting file or inventory entry exists.
495
+ */
496
+ export async function runAddBindingSourceCommand({ bindingSourceName, cwd = process.cwd(), }) {
497
+ const workspace = resolveWorkspaceProject(cwd);
498
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
499
+ const inventory = readWorkspaceInventory(workspace.projectDir);
500
+ assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
501
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
502
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
503
+ const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
504
+ const bindingSourceDir = path.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
505
+ const serverFilePath = path.join(bindingSourceDir, "server.php");
506
+ const editorFilePath = path.join(bindingSourceDir, "editor.ts");
507
+ const mutationSnapshot = {
508
+ fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
509
+ snapshotDirs: [],
510
+ targetPaths: [bindingSourceDir],
511
+ };
512
+ try {
513
+ await fsp.mkdir(bindingSourceDir, { recursive: true });
514
+ await ensureBindingSourceBootstrapAnchors(workspace);
515
+ await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
516
+ await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
517
+ await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
518
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
519
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)],
520
+ });
521
+ return {
522
+ bindingSourceSlug,
523
+ projectDir: workspace.projectDir,
524
+ };
525
+ }
526
+ catch (error) {
527
+ await rollbackWorkspaceMutation(mutationSnapshot);
528
+ throw error;
529
+ }
530
+ }
531
+ /**
532
+ * Add one `blockHooks` entry to an existing official workspace block.
533
+ *
534
+ * @param options Command options for the hooked-block workflow.
535
+ * @param options.anchorBlockName Full block name that will anchor the insertion.
536
+ * @param options.blockName Existing workspace block slug to patch.
537
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
538
+ * Defaults to `process.cwd()`.
539
+ * @param options.position Hook position to store in `block.json`.
540
+ * @returns A promise that resolves with the normalized target block slug, anchor
541
+ * block name, position, and owning project directory after `block.json` is written.
542
+ * @throws {Error} When the command is run outside an official workspace, when
543
+ * the target block is unknown, when required flags are missing, or when the
544
+ * block already defines a hook for the requested anchor.
545
+ */
546
+ export async function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd = process.cwd(), position, }) {
547
+ const workspace = resolveWorkspaceProject(cwd);
548
+ const blockSlug = normalizeBlockSlug(blockName);
549
+ const inventory = readWorkspaceInventory(workspace.projectDir);
550
+ resolveWorkspaceBlock(inventory, blockSlug);
551
+ const resolvedAnchorBlockName = assertValidHookAnchor(anchorBlockName);
552
+ const resolvedPosition = assertValidHookedBlockPosition(position);
553
+ const selfHookAnchor = `${workspace.workspace.namespace}/${blockSlug}`;
554
+ if (resolvedAnchorBlockName === selfHookAnchor) {
555
+ throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
556
+ }
557
+ const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
558
+ const blockJsonRelativePath = path.relative(workspace.projectDir, blockJsonPath);
559
+ const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
560
+ if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
561
+ throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
562
+ }
563
+ const mutationSnapshot = {
564
+ fileSources: await snapshotWorkspaceFiles([blockJsonPath]),
565
+ snapshotDirs: [],
566
+ targetPaths: [],
567
+ };
568
+ try {
569
+ blockHooks[resolvedAnchorBlockName] = resolvedPosition;
570
+ await fsp.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
571
+ return {
572
+ anchorBlockName: resolvedAnchorBlockName,
573
+ blockSlug,
574
+ position: resolvedPosition,
575
+ projectDir: workspace.projectDir,
576
+ };
577
+ }
578
+ catch (error) {
579
+ await rollbackWorkspaceMutation(mutationSnapshot);
580
+ throw error;
581
+ }
582
+ }