@wp-typia/project-tools 0.16.6 → 0.16.8

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 (36) hide show
  1. package/README.md +25 -8
  2. package/dist/runtime/block-generator-service.d.ts +5 -1
  3. package/dist/runtime/block-generator-service.js +132 -15
  4. package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
  5. package/dist/runtime/block-generator-tool-contract.js +157 -0
  6. package/dist/runtime/built-in-block-code-artifacts.js +5 -5
  7. package/dist/runtime/cli-add-block.d.ts +36 -0
  8. package/dist/runtime/cli-add-block.js +518 -0
  9. package/dist/runtime/cli-add-shared.d.ts +93 -0
  10. package/dist/runtime/cli-add-shared.js +201 -0
  11. package/dist/runtime/cli-add-workspace.d.ts +81 -0
  12. package/dist/runtime/cli-add-workspace.js +582 -0
  13. package/dist/runtime/cli-add.d.ts +11 -131
  14. package/dist/runtime/cli-add.js +10 -1250
  15. package/dist/runtime/cli-prompt.d.ts +25 -0
  16. package/dist/runtime/cli-prompt.js +32 -20
  17. package/dist/runtime/cli-scaffold.js +1 -2
  18. package/dist/runtime/index.d.ts +2 -0
  19. package/dist/runtime/index.js +1 -0
  20. package/dist/runtime/migration-types.d.ts +9 -53
  21. package/dist/runtime/scaffold.js +1 -2
  22. package/dist/runtime/template-builtins.d.ts +11 -1
  23. package/dist/runtime/template-builtins.js +118 -25
  24. package/dist/runtime/template-layers.d.ts +31 -0
  25. package/dist/runtime/template-layers.js +171 -0
  26. package/dist/runtime/template-render.d.ts +15 -0
  27. package/dist/runtime/template-render.js +36 -0
  28. package/dist/runtime/template-source.d.ts +17 -0
  29. package/dist/runtime/template-source.js +14 -3
  30. package/package.json +6 -8
  31. package/templates/_shared/base/package.json.mustache +1 -0
  32. package/templates/_shared/compound/core/package.json.mustache +1 -1
  33. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +5 -5
  34. package/templates/_shared/compound/persistence/package.json.mustache +1 -1
  35. package/templates/_shared/persistence/core/package.json.mustache +1 -0
  36. package/templates/interactivity/package.json.mustache +2 -1
@@ -1,1253 +1,13 @@
1
- import fs from "node:fs";
2
- import { promises as fsp } from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { syncBlockMetadata, } from "@wp-typia/block-runtime/metadata-core";
6
- import { ensureMigrationDirectories, parseMigrationConfig, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
7
- import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
8
- import { snapshotProjectVersion } from "./migrations.js";
9
- import { getDefaultAnswers, scaffoldProject } from "./scaffold.js";
10
- import { SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
11
- import { copyInterpolatedDirectory } from "./template-render.js";
12
- import { toKebabCase, toTitleCase, toSnakeCase, } from "./string-case.js";
13
- import { appendWorkspaceInventoryEntries, getWorkspaceBlockSelectOptions, readWorkspaceInventory, } from "./workspace-inventory.js";
14
- import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
15
- import { resolveWorkspaceProject, WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
16
1
  /**
17
- * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
18
- */
19
- export const ADD_KIND_IDS = ["block", "variation", "pattern", "binding-source", "hooked-block"];
20
- /**
21
- * Supported built-in block families accepted by `wp-typia add block --template`.
22
- */
23
- export const ADD_BLOCK_TEMPLATE_IDS = [
24
- "basic",
25
- "interactivity",
26
- "persistence",
27
- "compound",
28
- ];
29
- const COLLECTION_IMPORT_LINE = "import '../../collection';";
30
- const REST_MANIFEST_IMPORT_PATTERN = /import\s*\{[^}]*\bdefineEndpointManifest\b[^}]*\}\s*from\s*["']@wp-typia\/block-runtime\/metadata-core["'];?/m;
31
- const VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
32
- const VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
33
- const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
34
- const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
35
- const BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
36
- const BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
37
- const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
38
- function normalizeBlockSlug(input) {
39
- return toKebabCase(input);
40
- }
41
- function assertValidGeneratedSlug(label, slug, usage) {
42
- if (!slug) {
43
- throw new Error(`${label} is required. Use \`${usage}\`.`);
44
- }
45
- if (!WORKSPACE_GENERATED_SLUG_PATTERN.test(slug)) {
46
- throw new Error(`${label} must start with a letter and contain only lowercase letters, numbers, and hyphens.`);
47
- }
48
- return slug;
49
- }
50
- function assertValidHookedBlockPosition(position) {
51
- if (HOOKED_BLOCK_POSITION_IDS.includes(position)) {
52
- return position;
53
- }
54
- throw new Error(`Hook position must be one of: ${HOOKED_BLOCK_POSITION_IDS.join(", ")}.`);
55
- }
56
- function getWorkspaceBootstrapPath(workspace) {
57
- const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
58
- return path.join(workspace.projectDir, `${workspaceBaseName}.php`);
59
- }
60
- function buildWorkspacePhpPrefix(workspacePhpPrefix, slug) {
61
- return toSnakeCase(`${workspacePhpPrefix}_${slug}`);
62
- }
63
- function isAddBlockTemplateId(value) {
64
- return ADD_BLOCK_TEMPLATE_IDS.includes(value);
65
- }
66
- function quoteTsString(value) {
67
- return JSON.stringify(value);
68
- }
69
- function buildServerTemplateRoot(persistencePolicy) {
70
- return path.join(SHARED_WORKSPACE_TEMPLATE_ROOT, persistencePolicy === "public" ? "persistence-public" : "persistence-auth");
71
- }
72
- function buildSingleBlockConfigEntry(variables) {
73
- return [
74
- "\t{",
75
- `\t\tslug: ${quoteTsString(variables.slugKebabCase)},`,
76
- `\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}Attributes`)},`,
77
- `\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/types.ts`)},`,
78
- "\t},",
79
- ].join("\n");
80
- }
81
- function buildPersistenceBlockConfigEntry(variables) {
82
- return [
83
- "\t{",
84
- `\t\tapiTypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/api-types.ts`)},`,
85
- `\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}Attributes`)},`,
86
- "\t\trestManifest: defineEndpointManifest( {",
87
- "\t\t\tcontracts: {",
88
- "\t\t\t\t'bootstrap-query': {",
89
- `\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapQuery`)},`,
90
- "\t\t\t\t},",
91
- "\t\t\t\t'bootstrap-response': {",
92
- `\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapResponse`)},`,
93
- "\t\t\t\t},",
94
- "\t\t\t\t'state-query': {",
95
- `\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateQuery`)},`,
96
- "\t\t\t\t},",
97
- "\t\t\t\t'state-response': {",
98
- `\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateResponse`)},`,
99
- "\t\t\t\t},",
100
- "\t\t\t\t'write-state-request': {",
101
- `\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}WriteStateRequest`)},`,
102
- "\t\t\t\t},",
103
- "\t\t\t},",
104
- "\t\t\tendpoints: [",
105
- "\t\t\t\t{",
106
- "\t\t\t\t\tauth: 'public',",
107
- "\t\t\t\t\tmethod: 'GET',",
108
- `\t\t\t\t\toperationId: ${quoteTsString(`get${variables.pascalCase}State`)},`,
109
- `\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/state`)},`,
110
- "\t\t\t\t\tqueryContract: 'state-query',",
111
- "\t\t\t\t\tresponseContract: 'state-response',",
112
- `\t\t\t\t\tsummary: 'Read the current persisted state.',`,
113
- `\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
114
- "\t\t\t\t},",
115
- "\t\t\t\t{",
116
- `\t\t\t\t\tauth: ${quoteTsString(variables.restWriteAuthIntent)},`,
117
- "\t\t\t\t\tbodyContract: 'write-state-request',",
118
- "\t\t\t\t\tmethod: 'POST',",
119
- `\t\t\t\t\toperationId: ${quoteTsString(`write${variables.pascalCase}State`)},`,
120
- `\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/state`)},`,
121
- "\t\t\t\t\tresponseContract: 'state-response',",
122
- `\t\t\t\t\tsummary: 'Write the current persisted state.',`,
123
- `\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
124
- "\t\t\t\t\twordpressAuth: {",
125
- `\t\t\t\t\t\tmechanism: ${quoteTsString(variables.restWriteAuthMechanism)},`,
126
- "\t\t\t\t\t},",
127
- "\t\t\t\t},",
128
- "\t\t\t\t{",
129
- "\t\t\t\t\tauth: 'public',",
130
- "\t\t\t\t\tmethod: 'GET',",
131
- `\t\t\t\t\toperationId: ${quoteTsString(`get${variables.pascalCase}Bootstrap`)},`,
132
- `\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/bootstrap`)},`,
133
- "\t\t\t\t\tqueryContract: 'bootstrap-query',",
134
- "\t\t\t\t\tresponseContract: 'bootstrap-response',",
135
- `\t\t\t\t\tsummary: 'Read fresh session bootstrap state for the current viewer.',`,
136
- `\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
137
- "\t\t\t\t},",
138
- "\t\t\t],",
139
- "\t\t\tinfo: {",
140
- `\t\t\t\ttitle: ${quoteTsString(`${variables.title} REST API`)},`,
141
- "\t\t\t\tversion: '1.0.0',",
142
- "\t\t\t},",
143
- "\t\t} ),",
144
- `\t\topenApiFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/api.openapi.json`)},`,
145
- `\t\tslug: ${quoteTsString(variables.slugKebabCase)},`,
146
- `\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/types.ts`)},`,
147
- "\t},",
148
- ].join("\n");
149
- }
150
- function buildCompoundChildConfigEntry(variables) {
151
- return [
152
- "\t{",
153
- `\t\tslug: ${quoteTsString(`${variables.slugKebabCase}-item`)},`,
154
- `\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}ItemAttributes`)},`,
155
- `\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}-item/types.ts`)},`,
156
- "\t},",
157
- ].join("\n");
158
- }
159
- function buildConfigEntries(templateId, variables) {
160
- if (templateId === "basic" || templateId === "interactivity") {
161
- return [buildSingleBlockConfigEntry(variables)];
162
- }
163
- if (templateId === "persistence") {
164
- return [buildPersistenceBlockConfigEntry(variables)];
165
- }
166
- if (variables.compoundPersistenceEnabled === "true") {
167
- return [
168
- buildPersistenceBlockConfigEntry(variables),
169
- buildCompoundChildConfigEntry(variables),
170
- ];
171
- }
172
- return [
173
- buildSingleBlockConfigEntry(variables),
174
- buildCompoundChildConfigEntry(variables),
175
- ];
176
- }
177
- function buildMigrationBlocks(templateId, variables) {
178
- if (templateId === "compound") {
179
- return [
180
- {
181
- blockJsonFile: `src/blocks/${variables.slugKebabCase}/block.json`,
182
- blockName: `${variables.namespace}/${variables.slugKebabCase}`,
183
- key: variables.slugKebabCase,
184
- manifestFile: `src/blocks/${variables.slugKebabCase}/typia.manifest.json`,
185
- saveFile: `src/blocks/${variables.slugKebabCase}/save.tsx`,
186
- typesFile: `src/blocks/${variables.slugKebabCase}/types.ts`,
187
- },
188
- {
189
- blockJsonFile: `src/blocks/${variables.slugKebabCase}-item/block.json`,
190
- blockName: `${variables.namespace}/${variables.slugKebabCase}-item`,
191
- key: `${variables.slugKebabCase}-item`,
192
- manifestFile: `src/blocks/${variables.slugKebabCase}-item/typia.manifest.json`,
193
- saveFile: `src/blocks/${variables.slugKebabCase}-item/save.tsx`,
194
- typesFile: `src/blocks/${variables.slugKebabCase}-item/types.ts`,
195
- },
196
- ];
197
- }
198
- return [
199
- {
200
- blockJsonFile: `src/blocks/${variables.slugKebabCase}/block.json`,
201
- blockName: `${variables.namespace}/${variables.slugKebabCase}`,
202
- key: variables.slugKebabCase,
203
- manifestFile: `src/blocks/${variables.slugKebabCase}/typia.manifest.json`,
204
- saveFile: `src/blocks/${variables.slugKebabCase}/save.tsx`,
205
- typesFile: `src/blocks/${variables.slugKebabCase}/types.ts`,
206
- },
207
- ];
208
- }
209
- async function patchFile(filePath, transform) {
210
- const currentSource = await fsp.readFile(filePath, "utf8");
211
- const nextSource = transform(currentSource);
212
- if (nextSource !== currentSource) {
213
- await fsp.writeFile(filePath, nextSource, "utf8");
214
- }
215
- }
216
- async function readOptionalFile(filePath) {
217
- if (!fs.existsSync(filePath)) {
218
- return null;
219
- }
220
- return fsp.readFile(filePath, "utf8");
221
- }
222
- async function restoreOptionalFile(filePath, source) {
223
- if (source === null) {
224
- await fsp.rm(filePath, { force: true });
225
- return;
226
- }
227
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
228
- await fsp.writeFile(filePath, source, "utf8");
229
- }
230
- async function ensureCollectionImport(filePath) {
231
- await patchFile(filePath, (source) => {
232
- if (source.includes(COLLECTION_IMPORT_LINE)) {
233
- return source;
234
- }
235
- if (source.includes("import metadata from './block.json';")) {
236
- return source.replace("import metadata from './block.json';", `${COLLECTION_IMPORT_LINE}\nimport metadata from './block.json';`);
237
- }
238
- return `${COLLECTION_IMPORT_LINE}\n${source}`;
239
- });
240
- }
241
- async function copyTempDirectory(sourceDir, targetDir) {
242
- await fsp.mkdir(path.dirname(targetDir), { recursive: true });
243
- await fsp.cp(sourceDir, targetDir, { recursive: true });
244
- }
245
- async function addCollectionImportsForTemplate(projectDir, templateId, variables) {
246
- if (templateId === "compound") {
247
- await ensureCollectionImport(path.join(projectDir, "src", "blocks", variables.slugKebabCase, "index.tsx"));
248
- await ensureCollectionImport(path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`, "index.tsx"));
249
- return;
250
- }
251
- await ensureCollectionImport(path.join(projectDir, "src", "blocks", variables.slugKebabCase, "index.tsx"));
252
- }
253
- function buildVariationConfigEntry(blockSlug, variationSlug) {
254
- return [
255
- "\t{",
256
- `\t\tblock: ${quoteTsString(blockSlug)},`,
257
- `\t\tfile: ${quoteTsString(`src/blocks/${blockSlug}/variations/${variationSlug}.ts`)},`,
258
- `\t\tslug: ${quoteTsString(variationSlug)},`,
259
- "\t},",
260
- ].join("\n");
261
- }
262
- function buildPatternConfigEntry(patternSlug) {
263
- return [
264
- "\t{",
265
- `\t\tfile: ${quoteTsString(`src/patterns/${patternSlug}.php`)},`,
266
- `\t\tslug: ${quoteTsString(patternSlug)},`,
267
- "\t},",
268
- ].join("\n");
269
- }
270
- function buildBindingSourceConfigEntry(bindingSourceSlug) {
271
- return [
272
- "\t{",
273
- `\t\teditorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
274
- `\t\tserverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
275
- `\t\tslug: ${quoteTsString(bindingSourceSlug)},`,
276
- "\t},",
277
- ].join("\n");
278
- }
279
- function buildVariationConstName(variationSlug) {
280
- const identifierSegments = toKebabCase(variationSlug)
281
- .split("-")
282
- .filter(Boolean);
283
- return `workspaceVariation_${identifierSegments.join("_")}`;
284
- }
285
- function getVariationConstBindings(variationSlugs) {
286
- const seenConstNames = new Map();
287
- return variationSlugs.map((variationSlug) => {
288
- const constName = buildVariationConstName(variationSlug);
289
- const previousSlug = seenConstNames.get(constName);
290
- if (previousSlug && previousSlug !== variationSlug) {
291
- throw new Error(`Variation slugs "${previousSlug}" and "${variationSlug}" generate the same registry identifier "${constName}". Rename one of the variations.`);
292
- }
293
- seenConstNames.set(constName, variationSlug);
294
- return { constName, variationSlug };
295
- });
296
- }
297
- function buildVariationSource(variationSlug, textDomain) {
298
- const variationTitle = toTitleCase(variationSlug);
299
- const variationConstName = buildVariationConstName(variationSlug);
300
- return `import type { BlockVariation } from '@wordpress/blocks';
301
- import { __ } from '@wordpress/i18n';
302
-
303
- export const ${variationConstName} = {
304
- \tname: ${quoteTsString(variationSlug)},
305
- \ttitle: __( ${quoteTsString(variationTitle)}, ${quoteTsString(textDomain)} ),
306
- \tdescription: __(
307
- \t\t${quoteTsString(`A starter variation for ${variationTitle}.`)},
308
- \t\t${quoteTsString(textDomain)},
309
- \t),
310
- \tattributes: {},
311
- \tscope: ['inserter'],
312
- } satisfies BlockVariation;
313
- `;
314
- }
315
- function buildVariationIndexSource(variationSlugs) {
316
- const variationBindings = getVariationConstBindings(variationSlugs);
317
- const importLines = variationBindings
318
- .map(({ constName, variationSlug }) => {
319
- return `import { ${constName} } from './${variationSlug}';`;
320
- })
321
- .join("\n");
322
- const variationConstNames = variationBindings
323
- .map(({ constName }) => constName)
324
- .join(",\n\t\t");
325
- return `import { registerBlockVariation } from '@wordpress/blocks';
326
- import metadata from '../block.json';
327
- ${importLines ? `\n${importLines}` : ""}
328
-
329
- const WORKSPACE_VARIATIONS = [
330
- \t${variationConstNames}
331
- \t// wp-typia add variation entries
332
- ];
333
-
334
- export function registerWorkspaceVariations() {
335
- \tfor (const variation of WORKSPACE_VARIATIONS) {
336
- \t\tregisterBlockVariation(metadata.name, variation);
337
- \t}
338
- }
339
- `;
340
- }
341
- function buildPatternSource(patternSlug, namespace, textDomain) {
342
- const patternTitle = toTitleCase(patternSlug);
343
- return `<?php
344
- if ( ! defined( 'ABSPATH' ) ) {
345
- \treturn;
346
- }
347
-
348
- register_block_pattern(
349
- \t'${namespace}/${patternSlug}',
350
- \tarray(
351
- \t\t'title' => __( ${JSON.stringify(patternTitle)}, '${textDomain}' ),
352
- \t\t'description' => __( ${JSON.stringify(`A starter pattern for ${patternTitle}.`)}, '${textDomain}' ),
353
- \t\t'categories' => array( '${namespace}' ),
354
- \t\t'content' => '<!-- wp:paragraph --><p>' . esc_html__( 'Describe this pattern here.', '${textDomain}' ) . '</p><!-- /wp:paragraph -->',
355
- \t)
356
- );
357
- `;
358
- }
359
- function buildBindingSourceServerSource(bindingSourceSlug, namespace, textDomain) {
360
- const bindingSourceTitle = toTitleCase(bindingSourceSlug);
361
- return `<?php
362
- if ( ! defined( 'ABSPATH' ) ) {
363
- \treturn;
364
- }
365
-
366
- if ( ! function_exists( 'register_block_bindings_source' ) ) {
367
- \treturn;
368
- }
369
-
370
- register_block_bindings_source(
371
- \t'${namespace}/${bindingSourceSlug}',
372
- \tarray(
373
- \t\t'label' => __( ${JSON.stringify(bindingSourceTitle)}, '${textDomain}' ),
374
- \t\t'get_value_callback' => static function( array $source_args ) : string {
375
- \t\t\t$field = isset( $source_args['field'] ) && is_string( $source_args['field'] )
376
- \t\t\t\t? $source_args['field']
377
- \t\t\t\t: '${bindingSourceSlug}';
378
-
379
- \t\t\treturn sprintf(
380
- \t\t\t\t__( 'Replace %s with real binding source data.', '${textDomain}' ),
381
- \t\t\t\t$field
382
- \t\t\t);
383
- \t\t},
384
- \t)
385
- );
386
- `;
387
- }
388
- function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
389
- const bindingSourceTitle = toTitleCase(bindingSourceSlug);
390
- return `import { registerBlockBindingsSource } from '@wordpress/blocks';
391
- import { __ } from '@wordpress/i18n';
392
-
393
- registerBlockBindingsSource( {
394
- \tname: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
395
- \tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
396
- \tgetFieldsList() {
397
- \t\treturn [
398
- \t\t\t{
399
- \t\t\t\tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
400
- \t\t\t\ttype: 'string',
401
- \t\t\t\targs: {
402
- \t\t\t\t\tfield: ${quoteTsString(bindingSourceSlug)},
403
- \t\t\t\t},
404
- \t\t\t},
405
- \t\t];
406
- \t},
407
- \tgetValues( { bindings } ) {
408
- \t\tconst values: Record<string, string> = {};
409
- \t\tfor ( const attributeName of Object.keys( bindings ) ) {
410
- \t\t\tvalues[ attributeName ] = ${quoteTsString(`TODO: replace ${bindingSourceSlug} with real editor-side values.`)};
411
- \t\t}
412
- \t\treturn values;
413
- \t},
414
- } );
415
- `;
416
- }
417
- function buildBindingSourceIndexSource(bindingSourceSlugs) {
418
- const importLines = bindingSourceSlugs
419
- .map((bindingSourceSlug) => `import './${bindingSourceSlug}/editor';`)
420
- .join("\n");
421
- return `${importLines}${importLines ? "\n\n" : ""}// wp-typia add binding-source entries\n`;
422
- }
423
- async function ensureVariationRegistrationHook(blockIndexPath) {
424
- await patchFile(blockIndexPath, (source) => {
425
- let nextSource = source;
426
- if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
427
- nextSource = `${VARIATIONS_IMPORT_LINE}\n${nextSource}`;
428
- }
429
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
430
- const callInsertionPatterns = [
431
- /(registerBlockType<[\s\S]*?\);\s*)/u,
432
- /(registerBlockType\([\s\S]*?\);\s*)/u,
433
- ];
434
- let inserted = false;
435
- for (const pattern of callInsertionPatterns) {
436
- const candidate = nextSource.replace(pattern, (match) => `${match}\n${VARIATIONS_CALL_LINE}\n`);
437
- if (candidate !== nextSource) {
438
- nextSource = candidate;
439
- inserted = true;
440
- break;
441
- }
442
- }
443
- if (!inserted) {
444
- nextSource = `${nextSource.trimEnd()}\n\n${VARIATIONS_CALL_LINE}\n`;
445
- }
446
- }
447
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
448
- throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path.basename(blockIndexPath)}.`);
449
- }
450
- return nextSource;
451
- });
452
- }
453
- async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
454
- const variationsDir = path.join(projectDir, "src", "blocks", blockSlug, "variations");
455
- const variationsIndexPath = path.join(variationsDir, "index.ts");
456
- await fsp.mkdir(variationsDir, { recursive: true });
457
- const existingVariationSlugs = fs.existsSync(variationsDir)
458
- ? fs
459
- .readdirSync(variationsDir)
460
- .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
461
- .map((entry) => entry.replace(/\.ts$/u, ""))
462
- : [];
463
- const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
464
- await fsp.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
465
- }
466
- async function ensurePatternBootstrapAnchors(workspace) {
467
- const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
468
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
469
- await patchFile(bootstrapPath, (source) => {
470
- let nextSource = source;
471
- const patternCategoryFunctionName = `${workspace.workspace.phpPrefix}_register_pattern_category`;
472
- const patternRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_patterns`;
473
- const patternCategoryHook = `add_action( 'init', '${patternCategoryFunctionName}' );`;
474
- const patternRegistrationHook = `add_action( 'init', '${patternRegistrationFunctionName}', 20 );`;
475
- const patternFunctions = `
476
-
477
- function ${patternCategoryFunctionName}() {
478
- \tif ( function_exists( 'register_block_pattern_category' ) ) {
479
- \t\tregister_block_pattern_category(
480
- \t\t\t'${workspace.workspace.namespace}',
481
- \t\t\tarray(
482
- \t\t\t\t'label' => __( ${JSON.stringify(`${toTitleCase(workspaceBaseName)} Patterns`)}, '${workspace.workspace.textDomain}' ),
483
- \t\t\t)
484
- \t\t);
485
- \t}
486
- }
487
-
488
- function ${patternRegistrationFunctionName}() {
489
- \tforeach ( glob( __DIR__ . '/src/patterns/*.php' ) ?: array() as $pattern_module ) {
490
- \t\trequire $pattern_module;
491
- \t}
492
- }
493
- `;
494
- if (!nextSource.includes(PATTERN_BOOTSTRAP_CATEGORY)) {
495
- const insertionAnchors = [
496
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
497
- /\?>\s*$/u,
498
- ];
499
- let inserted = false;
500
- for (const anchor of insertionAnchors) {
501
- const candidate = nextSource.replace(anchor, (match) => `${patternFunctions}\n${match}`);
502
- if (candidate !== nextSource) {
503
- nextSource = candidate;
504
- inserted = true;
505
- break;
506
- }
507
- }
508
- if (!inserted) {
509
- nextSource = `${nextSource.trimEnd()}\n${patternFunctions}\n`;
510
- }
511
- }
512
- if (!nextSource.includes(patternCategoryFunctionName) ||
513
- !nextSource.includes(patternRegistrationFunctionName)) {
514
- throw new Error(`Unable to inject pattern bootstrap functions into ${path.basename(bootstrapPath)}.`);
515
- }
516
- if (!nextSource.includes(patternCategoryHook)) {
517
- nextSource = `${nextSource.trimEnd()}\n${patternCategoryHook}\n`;
518
- }
519
- if (!nextSource.includes(patternRegistrationHook)) {
520
- nextSource = `${nextSource.trimEnd()}\n${patternRegistrationHook}\n`;
521
- }
522
- return nextSource;
523
- });
524
- }
525
- async function ensureBindingSourceBootstrapAnchors(workspace) {
526
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
527
- await patchFile(bootstrapPath, (source) => {
528
- let nextSource = source;
529
- const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
530
- const bindingRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_binding_sources`;
531
- const bindingEditorEnqueueFunctionName = `${workspace.workspace.phpPrefix}_enqueue_binding_sources_editor`;
532
- const bindingRegistrationHook = `add_action( 'init', '${bindingRegistrationFunctionName}', 20 );`;
533
- const bindingEditorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${bindingEditorEnqueueFunctionName}' );`;
534
- const bindingRegistrationFunction = `
535
-
536
- function ${bindingRegistrationFunctionName}() {
537
- \tforeach ( glob( __DIR__ . '${BINDING_SOURCE_SERVER_GLOB}' ) ?: array() as $binding_source_module ) {
538
- \t\trequire_once $binding_source_module;
539
- \t}
540
- }
541
- `;
542
- const bindingEditorEnqueueFunction = `
543
-
544
- function ${bindingEditorEnqueueFunctionName}() {
545
- \t$script_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_SCRIPT}';
546
- \t$asset_path = __DIR__ . '/${BINDING_SOURCE_EDITOR_ASSET}';
547
-
548
- \tif ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
549
- \t\treturn;
550
- \t}
551
-
552
- \t$asset = require $asset_path;
553
- \tif ( ! is_array( $asset ) ) {
554
- \t\t$asset = array();
555
- \t}
556
-
557
- \twp_enqueue_script(
558
- \t\t'${workspaceBaseName}-binding-sources',
559
- \t\tplugins_url( '${BINDING_SOURCE_EDITOR_SCRIPT}', __FILE__ ),
560
- \t\tisset( $asset['dependencies'] ) && is_array( $asset['dependencies'] ) ? $asset['dependencies'] : array(),
561
- \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
562
- \t\ttrue
563
- \t);
564
- }
565
- `;
566
- const insertionAnchors = [
567
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
568
- /\?>\s*$/u,
569
- ];
570
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${functionName}\\s*\\(`, "u").test(nextSource);
571
- const insertPhpSnippet = (snippet) => {
572
- for (const anchor of insertionAnchors) {
573
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
574
- if (candidate !== nextSource) {
575
- nextSource = candidate;
576
- return;
577
- }
578
- }
579
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
580
- };
581
- const appendPhpSnippet = (snippet) => {
582
- const closingTagPattern = /\?>\s*$/u;
583
- if (closingTagPattern.test(nextSource)) {
584
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
585
- return;
586
- }
587
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
588
- };
589
- if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
590
- insertPhpSnippet(bindingRegistrationFunction);
591
- }
592
- if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
593
- insertPhpSnippet(bindingEditorEnqueueFunction);
594
- }
595
- if (!nextSource.includes(bindingRegistrationHook)) {
596
- appendPhpSnippet(bindingRegistrationHook);
597
- }
598
- if (!nextSource.includes(bindingEditorEnqueueHook)) {
599
- appendPhpSnippet(bindingEditorEnqueueHook);
600
- }
601
- return nextSource;
602
- });
603
- }
604
- function ensureBlockConfigCanAddRestManifests(source) {
605
- const importLine = "import { defineEndpointManifest } from '@wp-typia/block-runtime/metadata-core';";
606
- if (REST_MANIFEST_IMPORT_PATTERN.test(source)) {
607
- return source;
608
- }
609
- return `${importLine}\n\n${source}`;
610
- }
611
- async function appendBlockConfigEntries(projectDir, entries, needsRestManifestImport) {
612
- await appendWorkspaceInventoryEntries(projectDir, {
613
- blockEntries: entries,
614
- transformSource: needsRestManifestImport ? ensureBlockConfigCanAddRestManifests : undefined,
615
- });
616
- }
617
- async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
618
- const bindingsDir = path.join(projectDir, "src", "bindings");
619
- const bindingsIndexPath = resolveBindingSourceRegistryPath(projectDir);
620
- await fsp.mkdir(bindingsDir, { recursive: true });
621
- const existingBindingSourceSlugs = fs.existsSync(bindingsDir)
622
- ? fs
623
- .readdirSync(bindingsDir, { withFileTypes: true })
624
- .filter((entry) => entry.isDirectory())
625
- .map((entry) => entry.name)
626
- : [];
627
- const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
628
- await fsp.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
629
- }
630
- function resolveBindingSourceRegistryPath(projectDir) {
631
- const bindingsDir = path.join(projectDir, "src", "bindings");
632
- return [path.join(bindingsDir, "index.ts"), path.join(bindingsDir, "index.js")].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(bindingsDir, "index.ts");
633
- }
634
- async function snapshotWorkspaceFiles(filePaths) {
635
- const uniquePaths = Array.from(new Set(filePaths));
636
- return Promise.all(uniquePaths.map(async (filePath) => ({
637
- filePath,
638
- source: await readOptionalFile(filePath),
639
- })));
640
- }
641
- async function renderWorkspacePersistenceServerModule(projectDir, variables) {
642
- const targetDir = path.join(projectDir, "src", "blocks", variables.slugKebabCase);
643
- const templateDir = buildServerTemplateRoot(variables.persistencePolicy);
644
- await copyInterpolatedDirectory(templateDir, targetDir, variables);
645
- }
646
- const COMPOUND_SHARED_SUPPORT_FILES = ["hooks.ts", "validator-toolkit.ts"];
647
- const LEGACY_ASSERT_PATTERN = /assert:\s*typia\.createAssert</u;
648
- const LEGACY_MANIFEST_PATTERN = /\r?\n[ \t]*manifest:\s*currentManifest,/u;
649
- const LEGACY_TOOLKIT_CALL_PATTERN = /createTemplateValidatorToolkit<\s*(?<typeName>[A-Za-z0-9_]+)\s*>\s*\(\s*\{/u;
650
- const LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN = /from\s*["']\.\.\/\.\.\/validator-toolkit["']/u;
651
- const TYPIA_IMPORT_PATTERN = /^[\uFEFF \t]*import\s+typia\s+from\s*["']typia["'];?/mu;
652
- const COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS = [
653
- /interface\s+TemplateValidatorFunctions\s*<\s*T\s+extends\s+object\s*>\s*\{/u,
654
- /\bassert\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']assert["']\s*\]/u,
655
- /\bclone\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']clone["']\s*\]/u,
656
- /\bis\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']is["']\s*\]/u,
657
- /\bprune\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']prune["']\s*\]/u,
658
- /\brandom\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']random["']\s*\]/u,
659
- /\bvalidate\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']validate["']\s*\]/u,
660
- /createTemplateValidatorToolkit\s*<\s*T\s+extends\s+object\s*>\s*\(\s*\{/u,
661
- ];
662
- function shouldRefreshCompoundValidatorToolkit(source) {
663
- return (source === null ||
664
- !COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS.every((pattern) => pattern.test(source)));
665
- }
666
- function isLegacyCompoundValidatorSource(source) {
667
- return (typeof source === "string" &&
668
- LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN.test(source) &&
669
- !LEGACY_ASSERT_PATTERN.test(source));
670
- }
671
- function hasTypiaImport(source) {
672
- return TYPIA_IMPORT_PATTERN.test(source.replace(/\/\*[\s\S]*?\*\//gu, ""));
673
- }
674
- function upgradeLegacyCompoundValidatorSource(source) {
675
- const typeNameMatch = source.match(LEGACY_TOOLKIT_CALL_PATTERN);
676
- const typeName = typeNameMatch?.groups?.typeName;
677
- if (!typeName) {
678
- throw new Error("Unable to upgrade a legacy compound validator without a generated type import.");
679
- }
680
- let nextSource = source;
681
- if (!hasTypiaImport(nextSource)) {
682
- nextSource = `import typia from 'typia';\n${nextSource}`;
683
- }
684
- nextSource = nextSource.replace(LEGACY_TOOLKIT_CALL_PATTERN, [
685
- `createTemplateValidatorToolkit< ${typeName} >( {`,
686
- `\tassert: typia.createAssert< ${typeName} >(),`,
687
- `\tclone: typia.misc.createClone< ${typeName} >() as (`,
688
- `\t\tvalue: ${typeName},`,
689
- `\t) => ${typeName},`,
690
- `\tis: typia.createIs< ${typeName} >(),`,
691
- ].join("\n") + "\n");
692
- const replacedManifest = nextSource.replace(LEGACY_MANIFEST_PATTERN, [
693
- "",
694
- "\tmanifest: currentManifest,",
695
- `\tprune: typia.misc.createPrune< ${typeName} >(),`,
696
- `\trandom: typia.createRandom< ${typeName} >() as (`,
697
- "\t\t...args: unknown[]",
698
- `\t) => ${typeName},`,
699
- `\tvalidate: typia.createValidate< ${typeName} >(),`,
700
- ].join("\n"));
701
- if (replacedManifest === nextSource) {
702
- throw new Error("Unable to upgrade legacy compound validator: manifest anchor not found.");
703
- }
704
- return replacedManifest;
705
- }
706
- async function collectLegacyCompoundValidatorPaths(projectDir) {
707
- const blocksDir = path.join(projectDir, "src", "blocks");
708
- if (!fs.existsSync(blocksDir)) {
709
- return [];
710
- }
711
- const blockEntries = await fsp.readdir(blocksDir, { withFileTypes: true });
712
- const validatorPaths = await Promise.all(blockEntries
713
- .filter((entry) => entry.isDirectory())
714
- .map(async (entry) => {
715
- const validatorPath = path.join(blocksDir, entry.name, "validators.ts");
716
- const validatorSource = await readOptionalFile(validatorPath);
717
- return isLegacyCompoundValidatorSource(validatorSource) ? validatorPath : null;
718
- }));
719
- return validatorPaths.filter((validatorPath) => validatorPath !== null);
720
- }
721
- async function ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, legacyValidatorPaths) {
722
- for (const fileName of COMPOUND_SHARED_SUPPORT_FILES) {
723
- const sourcePath = path.join(tempProjectDir, "src", fileName);
724
- if (!fs.existsSync(sourcePath)) {
725
- continue;
726
- }
727
- const targetPath = path.join(projectDir, "src", fileName);
728
- const currentSource = await readOptionalFile(targetPath);
729
- if (fileName === "validator-toolkit.ts"
730
- ? shouldRefreshCompoundValidatorToolkit(currentSource)
731
- : currentSource === null) {
732
- await fsp.mkdir(path.dirname(targetPath), { recursive: true });
733
- await fsp.copyFile(sourcePath, targetPath);
734
- }
735
- }
736
- for (const validatorPath of legacyValidatorPaths) {
737
- const currentSource = await readOptionalFile(validatorPath);
738
- if (!isLegacyCompoundValidatorSource(currentSource)) {
739
- continue;
740
- }
741
- await fsp.writeFile(validatorPath, upgradeLegacyCompoundValidatorSource(currentSource), "utf8");
742
- }
743
- }
744
- async function copyScaffoldedBlockSlice(projectDir, templateId, tempProjectDir, variables, legacyValidatorPaths = []) {
745
- if (templateId === "compound") {
746
- await ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, legacyValidatorPaths);
747
- await copyTempDirectory(path.join(tempProjectDir, "src", "blocks", variables.slugKebabCase), path.join(projectDir, "src", "blocks", variables.slugKebabCase));
748
- await copyTempDirectory(path.join(tempProjectDir, "src", "blocks", `${variables.slugKebabCase}-item`), path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`));
749
- if (variables.compoundPersistenceEnabled === "true") {
750
- await renderWorkspacePersistenceServerModule(projectDir, variables);
751
- }
752
- return;
753
- }
754
- await copyTempDirectory(path.join(tempProjectDir, "src"), path.join(projectDir, "src", "blocks", variables.slugKebabCase));
755
- if (templateId === "persistence") {
756
- await renderWorkspacePersistenceServerModule(projectDir, variables);
757
- }
758
- }
759
- function collectWorkspaceBlockPaths(projectDir, templateId, variables) {
760
- if (templateId === "compound") {
761
- return [
762
- path.join(projectDir, "src", "blocks", variables.slugKebabCase),
763
- path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`),
764
- ];
765
- }
766
- return [path.join(projectDir, "src", "blocks", variables.slugKebabCase)];
767
- }
768
- function assertBlockTargetsDoNotExist(projectDir, templateId, variables) {
769
- for (const targetPath of collectWorkspaceBlockPaths(projectDir, templateId, variables)) {
770
- if (fs.existsSync(targetPath)) {
771
- throw new Error(`A block already exists at ${path.relative(projectDir, targetPath)}. Choose a different name.`);
772
- }
773
- }
774
- }
775
- async function updateWorkspaceMigrationConfigIfPresent(projectDir, newBlocks) {
776
- const configPath = path.join(projectDir, "src", "migrations", "config.ts");
777
- if (!fs.existsSync(configPath)) {
778
- return;
779
- }
780
- const configSource = await fsp.readFile(configPath, "utf8");
781
- const config = parseMigrationConfig(configSource);
782
- const existingBlocks = Array.isArray(config.blocks) ? config.blocks : [];
783
- const nextBlocks = [
784
- ...existingBlocks,
785
- ...newBlocks.filter((block) => !existingBlocks.some((existing) => existing.key === block.key)),
786
- ];
787
- writeMigrationConfig(projectDir, {
788
- ...config,
789
- blocks: nextBlocks,
790
- });
791
- snapshotProjectVersion(projectDir, config.currentMigrationVersion, {
792
- skipConfigUpdate: true,
793
- });
794
- }
795
- async function syncWorkspaceBlockMetadata(projectDir, slug, sourceTypeName, typesFile) {
796
- await syncBlockMetadata({
797
- blockJsonFile: path.join("src", "blocks", slug, "block.json"),
798
- jsonSchemaFile: path.join("src", "blocks", slug, "typia.schema.json"),
799
- manifestFile: path.join("src", "blocks", slug, "typia.manifest.json"),
800
- openApiFile: path.join("src", "blocks", slug, "typia.openapi.json"),
801
- projectRoot: projectDir,
802
- sourceTypeName,
803
- typesFile,
804
- });
805
- }
806
- async function syncWorkspacePersistenceArtifacts(projectDir, variables) {
807
- await syncPersistenceRestArtifacts({
808
- apiTypesFile: path.join("src", "blocks", variables.slugKebabCase, "api-types.ts"),
809
- outputDir: path.join("src", "blocks", variables.slugKebabCase),
810
- projectDir,
811
- variables,
812
- });
813
- }
814
- async function syncWorkspaceAddedBlockArtifacts(projectDir, templateId, variables) {
815
- await syncWorkspaceBlockMetadata(projectDir, variables.slugKebabCase, `${variables.pascalCase}Attributes`, path.join("src", "blocks", variables.slugKebabCase, "types.ts"));
816
- if (templateId === "compound") {
817
- await syncWorkspaceBlockMetadata(projectDir, `${variables.slugKebabCase}-item`, `${variables.pascalCase}ItemAttributes`, path.join("src", "blocks", `${variables.slugKebabCase}-item`, "types.ts"));
818
- }
819
- if (templateId === "persistence" ||
820
- (templateId === "compound" && variables.compoundPersistenceEnabled === "true")) {
821
- await syncWorkspacePersistenceArtifacts(projectDir, variables);
822
- }
823
- }
824
- function assertPersistenceFlagsAllowed(templateId, options) {
825
- const hasPersistenceFlags = typeof options.dataStorageMode === "string" ||
826
- typeof options.persistencePolicy === "string";
827
- if (!hasPersistenceFlags) {
828
- return;
829
- }
830
- if (templateId === "persistence" || templateId === "compound") {
831
- if (typeof options.dataStorageMode === "string" &&
832
- options.dataStorageMode !== "custom-table" &&
833
- options.dataStorageMode !== "post-meta") {
834
- throw new Error(`Unsupported data storage mode "${options.dataStorageMode}". Expected one of: post-meta, custom-table.`);
835
- }
836
- if (typeof options.persistencePolicy === "string" &&
837
- options.persistencePolicy !== "authenticated" &&
838
- options.persistencePolicy !== "public") {
839
- throw new Error(`Unsupported persistence policy "${options.persistencePolicy}". Expected one of: authenticated, public.`);
840
- }
841
- return;
842
- }
843
- throw new Error(`--data-storage and --persistence-policy are supported only for \`wp-typia add block --template persistence\` or \`--template compound\`.`);
844
- }
845
- /**
846
- * Returns help text for the canonical `wp-typia add` subcommands.
847
- */
848
- export function formatAddHelpText() {
849
- return `Usage:
850
- wp-typia add block <name> --template <${ADD_BLOCK_TEMPLATE_IDS.join("|")}> [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
851
- wp-typia add variation <name> --block <block-slug>
852
- wp-typia add pattern <name>
853
- wp-typia add binding-source <name>
854
- wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}>
855
-
856
- Notes:
857
- \`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces.
858
- \`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
859
- \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
860
- \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
861
- \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.`;
862
- }
863
- /**
864
- * Seeds an empty official workspace migration project before any blocks are added.
865
- */
866
- export async function seedWorkspaceMigrationProject(projectDir, currentMigrationVersion) {
867
- writeMigrationConfig(projectDir, {
868
- blocks: [],
869
- currentMigrationVersion,
870
- snapshotDir: "src/migrations/versions",
871
- supportedMigrationVersions: [currentMigrationVersion],
872
- });
873
- ensureMigrationDirectories(projectDir, []);
874
- writeInitialMigrationScaffold(projectDir, currentMigrationVersion, []);
875
- }
876
- async function rollbackWorkspaceMutation(snapshot) {
877
- for (const targetPath of snapshot.targetPaths) {
878
- await fsp.rm(targetPath, { force: true, recursive: true });
879
- }
880
- for (const snapshotDir of snapshot.snapshotDirs) {
881
- await fsp.rm(snapshotDir, { force: true, recursive: true });
882
- }
883
- for (const { filePath, source } of snapshot.fileSources) {
884
- await restoreOptionalFile(filePath, source);
885
- }
886
- }
887
- /**
888
- * Adds one built-in block slice to an official workspace project.
889
- */
890
- export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, persistencePolicy, templateId = "basic", }) {
891
- if (!isAddBlockTemplateId(templateId)) {
892
- throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
893
- }
894
- const resolvedTemplateId = templateId;
895
- assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
896
- const workspace = resolveWorkspaceProject(cwd);
897
- const normalizedSlug = normalizeBlockSlug(blockName);
898
- if (!normalizedSlug) {
899
- throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
900
- }
901
- const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
902
- let tempRoot = "";
903
- try {
904
- tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-add-block-"));
905
- const tempProjectDir = path.join(tempRoot, normalizedSlug);
906
- const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
907
- const migrationConfigPath = path.join(workspace.projectDir, "src", "migrations", "config.ts");
908
- const blockPhpPrefix = buildWorkspacePhpPrefix(workspace.workspace.phpPrefix, normalizedSlug);
909
- const migrationConfigSource = await readOptionalFile(migrationConfigPath);
910
- const migrationConfig = migrationConfigSource === null ? null : parseMigrationConfig(migrationConfigSource);
911
- const compoundSupportPaths = resolvedTemplateId === "compound"
912
- ? COMPOUND_SHARED_SUPPORT_FILES.map((fileName) => path.join(workspace.projectDir, "src", fileName))
913
- : [];
914
- const legacyCompoundValidatorPaths = resolvedTemplateId === "compound"
915
- ? await collectLegacyCompoundValidatorPaths(workspace.projectDir)
916
- : [];
917
- const result = await scaffoldProject({
918
- answers: {
919
- ...defaults,
920
- author: workspace.author,
921
- namespace: workspace.workspace.namespace,
922
- phpPrefix: blockPhpPrefix,
923
- slug: normalizedSlug,
924
- textDomain: workspace.workspace.textDomain,
925
- title: defaults.title,
926
- },
927
- cwd: workspace.projectDir,
928
- dataStorageMode: dataStorageMode,
929
- noInstall: true,
930
- packageManager: workspace.packageManager,
931
- persistencePolicy: persistencePolicy,
932
- projectDir: tempProjectDir,
933
- templateId: resolvedTemplateId,
934
- });
935
- assertBlockTargetsDoNotExist(workspace.projectDir, resolvedTemplateId, result.variables);
936
- const mutationSnapshot = {
937
- fileSources: await snapshotWorkspaceFiles([
938
- blockConfigPath,
939
- migrationConfigPath,
940
- ...compoundSupportPaths,
941
- ...legacyCompoundValidatorPaths,
942
- ]),
943
- snapshotDirs: migrationConfig === null
944
- ? []
945
- : buildMigrationBlocks(resolvedTemplateId, result.variables).map((block) => path.join(workspace.projectDir, ...migrationConfig.snapshotDir.split("/"), migrationConfig.currentMigrationVersion, block.key)),
946
- targetPaths: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables),
947
- };
948
- try {
949
- await copyScaffoldedBlockSlice(workspace.projectDir, resolvedTemplateId, tempProjectDir, result.variables, legacyCompoundValidatorPaths);
950
- await addCollectionImportsForTemplate(workspace.projectDir, resolvedTemplateId, result.variables);
951
- await appendBlockConfigEntries(workspace.projectDir, buildConfigEntries(resolvedTemplateId, result.variables), resolvedTemplateId === "persistence" ||
952
- (resolvedTemplateId === "compound" &&
953
- result.variables.compoundPersistenceEnabled === "true"));
954
- await syncWorkspaceAddedBlockArtifacts(workspace.projectDir, resolvedTemplateId, result.variables);
955
- await updateWorkspaceMigrationConfigIfPresent(workspace.projectDir, buildMigrationBlocks(resolvedTemplateId, result.variables));
956
- return {
957
- blockSlugs: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables).map((targetPath) => path.basename(targetPath)),
958
- projectDir: workspace.projectDir,
959
- templateId: resolvedTemplateId,
960
- };
961
- }
962
- catch (error) {
963
- await rollbackWorkspaceMutation(mutationSnapshot);
964
- throw error;
965
- }
966
- }
967
- finally {
968
- if (tempRoot) {
969
- await fsp.rm(tempRoot, { force: true, recursive: true });
970
- }
971
- }
972
- }
973
- function resolveWorkspaceBlock(inventory, blockSlug) {
974
- const block = inventory.blocks.find((entry) => entry.slug === blockSlug);
975
- if (!block) {
976
- throw new Error(`Unknown workspace block "${blockSlug}". Choose one of: ${inventory.blocks.map((entry) => entry.slug).join(", ")}`);
977
- }
978
- return block;
979
- }
980
- function assertValidHookAnchor(anchorBlockName) {
981
- const trimmed = anchorBlockName.trim();
982
- if (!trimmed) {
983
- throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name>.");
984
- }
985
- if (!HOOKED_BLOCK_ANCHOR_PATTERN.test(trimmed)) {
986
- throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name> to use the full `namespace/slug` block name format.");
987
- }
988
- return trimmed;
989
- }
990
- function readWorkspaceBlockJson(projectDir, blockSlug) {
991
- const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
992
- if (!fs.existsSync(blockJsonPath)) {
993
- throw new Error(`Missing ${path.relative(projectDir, blockJsonPath)} for workspace block "${blockSlug}".`);
994
- }
995
- let blockJson;
996
- try {
997
- blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
998
- }
999
- catch (error) {
1000
- throw new Error(error instanceof Error
1001
- ? `Failed to parse ${path.relative(projectDir, blockJsonPath)}: ${error.message}`
1002
- : `Failed to parse ${path.relative(projectDir, blockJsonPath)}.`);
1003
- }
1004
- if (!blockJson || typeof blockJson !== "object" || Array.isArray(blockJson)) {
1005
- throw new Error(`${path.relative(projectDir, blockJsonPath)} must contain a JSON object.`);
1006
- }
1007
- return {
1008
- blockJson: blockJson,
1009
- blockJsonPath,
1010
- };
1011
- }
1012
- function getMutableBlockHooks(blockJson, blockJsonRelativePath) {
1013
- const blockHooks = blockJson.blockHooks;
1014
- if (blockHooks === undefined) {
1015
- const nextHooks = {};
1016
- blockJson.blockHooks = nextHooks;
1017
- return nextHooks;
1018
- }
1019
- if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
1020
- throw new Error(`${blockJsonRelativePath} must define blockHooks as an object when present.`);
1021
- }
1022
- return blockHooks;
1023
- }
1024
- function assertVariationDoesNotExist(projectDir, blockSlug, variationSlug, inventory) {
1025
- const variationPath = path.join(projectDir, "src", "blocks", blockSlug, "variations", `${variationSlug}.ts`);
1026
- if (fs.existsSync(variationPath)) {
1027
- throw new Error(`A variation already exists at ${path.relative(projectDir, variationPath)}. Choose a different name.`);
1028
- }
1029
- if (inventory.variations.some((entry) => entry.block === blockSlug && entry.slug === variationSlug)) {
1030
- throw new Error(`A variation inventory entry already exists for ${blockSlug}/${variationSlug}. Choose a different name.`);
1031
- }
1032
- }
1033
- function assertPatternDoesNotExist(projectDir, patternSlug, inventory) {
1034
- const patternPath = path.join(projectDir, "src", "patterns", `${patternSlug}.php`);
1035
- if (fs.existsSync(patternPath)) {
1036
- throw new Error(`A pattern already exists at ${path.relative(projectDir, patternPath)}. Choose a different name.`);
1037
- }
1038
- if (inventory.patterns.some((entry) => entry.slug === patternSlug)) {
1039
- throw new Error(`A pattern inventory entry already exists for ${patternSlug}. Choose a different name.`);
1040
- }
1041
- }
1042
- function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, inventory) {
1043
- const bindingSourceDir = path.join(projectDir, "src", "bindings", bindingSourceSlug);
1044
- if (fs.existsSync(bindingSourceDir)) {
1045
- throw new Error(`A binding source already exists at ${path.relative(projectDir, bindingSourceDir)}. Choose a different name.`);
1046
- }
1047
- if (inventory.bindingSources.some((entry) => entry.slug === bindingSourceSlug)) {
1048
- throw new Error(`A binding source inventory entry already exists for ${bindingSourceSlug}. Choose a different name.`);
1049
- }
1050
- }
1051
- /**
1052
- * Add one variation entry to an existing workspace block.
1053
- *
1054
- * @param options Command options for the variation scaffold workflow.
1055
- * @param options.blockName Target workspace block slug that will own the variation.
1056
- * @param options.cwd Working directory used to resolve the nearest official workspace.
1057
- * Defaults to `process.cwd()`.
1058
- * @param options.variationName Human-entered variation name that will be normalized
1059
- * and validated before files are written.
1060
- * @returns A promise that resolves with the normalized `blockSlug`,
1061
- * `variationSlug`, and owning `projectDir` after the variation files and
1062
- * inventory entry have been written successfully.
1063
- * @throws {Error} When the command is run outside an official workspace, when
1064
- * the target block is unknown, when the variation slug is invalid, or when a
1065
- * conflicting file or inventory entry already exists.
1066
- */
1067
- export async function runAddVariationCommand({ blockName, cwd = process.cwd(), variationName, }) {
1068
- const workspace = resolveWorkspaceProject(cwd);
1069
- const blockSlug = normalizeBlockSlug(blockName);
1070
- const variationSlug = assertValidGeneratedSlug("Variation name", normalizeBlockSlug(variationName), "wp-typia add variation <name> --block <block-slug>");
1071
- const inventory = readWorkspaceInventory(workspace.projectDir);
1072
- resolveWorkspaceBlock(inventory, blockSlug);
1073
- assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
1074
- const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
1075
- const blockIndexPath = path.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
1076
- const variationsDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
1077
- const variationFilePath = path.join(variationsDir, `${variationSlug}.ts`);
1078
- const variationsIndexPath = path.join(variationsDir, "index.ts");
1079
- const mutationSnapshot = {
1080
- fileSources: await snapshotWorkspaceFiles([
1081
- blockConfigPath,
1082
- blockIndexPath,
1083
- variationsIndexPath,
1084
- ]),
1085
- snapshotDirs: [],
1086
- targetPaths: [variationFilePath],
1087
- };
1088
- try {
1089
- await fsp.mkdir(variationsDir, { recursive: true });
1090
- await fsp.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
1091
- await writeVariationRegistry(workspace.projectDir, blockSlug, variationSlug);
1092
- await ensureVariationRegistrationHook(blockIndexPath);
1093
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
1094
- variationEntries: [buildVariationConfigEntry(blockSlug, variationSlug)],
1095
- });
1096
- return {
1097
- blockSlug,
1098
- projectDir: workspace.projectDir,
1099
- variationSlug,
1100
- };
1101
- }
1102
- catch (error) {
1103
- await rollbackWorkspaceMutation(mutationSnapshot);
1104
- throw error;
1105
- }
1106
- }
1107
- /**
1108
- * Add one PHP block pattern shell to an official workspace project.
1109
- *
1110
- * @param options Command options for the pattern scaffold workflow.
1111
- * @param options.cwd Working directory used to resolve the nearest official workspace.
1112
- * Defaults to `process.cwd()`.
1113
- * @param options.patternName Human-entered pattern name that will be normalized
1114
- * and validated before files are written.
1115
- * @returns A promise that resolves with the normalized `patternSlug` and
1116
- * owning `projectDir` after the pattern file and inventory entry have been
1117
- * written successfully.
1118
- * @throws {Error} When the command is run outside an official workspace, when
1119
- * the pattern slug is invalid, or when a conflicting file or inventory entry
1120
- * already exists.
1121
- */
1122
- export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }) {
1123
- const workspace = resolveWorkspaceProject(cwd);
1124
- const patternSlug = assertValidGeneratedSlug("Pattern name", normalizeBlockSlug(patternName), "wp-typia add pattern <name>");
1125
- const inventory = readWorkspaceInventory(workspace.projectDir);
1126
- assertPatternDoesNotExist(workspace.projectDir, patternSlug, inventory);
1127
- const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
1128
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1129
- const patternFilePath = path.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
1130
- const mutationSnapshot = {
1131
- fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath]),
1132
- snapshotDirs: [],
1133
- targetPaths: [patternFilePath],
1134
- };
1135
- try {
1136
- await fsp.mkdir(path.dirname(patternFilePath), { recursive: true });
1137
- await ensurePatternBootstrapAnchors(workspace);
1138
- await fsp.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1139
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
1140
- patternEntries: [buildPatternConfigEntry(patternSlug)],
1141
- });
1142
- return {
1143
- patternSlug,
1144
- projectDir: workspace.projectDir,
1145
- };
1146
- }
1147
- catch (error) {
1148
- await rollbackWorkspaceMutation(mutationSnapshot);
1149
- throw error;
1150
- }
1151
- }
1152
- /**
1153
- * Add one block binding source scaffold to an official workspace project.
1154
- *
1155
- * @param options Command options for the binding-source scaffold workflow.
1156
- * @param options.bindingSourceName Human-entered binding source name that will
1157
- * be normalized and validated before files are written.
1158
- * @param options.cwd Working directory used to resolve the nearest official
1159
- * workspace. Defaults to `process.cwd()`.
1160
- * @returns A promise that resolves with the normalized `bindingSourceSlug` and
1161
- * owning `projectDir` after the server/editor files and inventory entry have
1162
- * been written successfully.
1163
- * @throws {Error} When the command is run outside an official workspace, when
1164
- * the slug is invalid, or when a conflicting file or inventory entry exists.
1165
- */
1166
- export async function runAddBindingSourceCommand({ bindingSourceName, cwd = process.cwd(), }) {
1167
- const workspace = resolveWorkspaceProject(cwd);
1168
- const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
1169
- const inventory = readWorkspaceInventory(workspace.projectDir);
1170
- assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
1171
- const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
1172
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1173
- const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
1174
- const bindingSourceDir = path.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
1175
- const serverFilePath = path.join(bindingSourceDir, "server.php");
1176
- const editorFilePath = path.join(bindingSourceDir, "editor.ts");
1177
- const mutationSnapshot = {
1178
- fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
1179
- snapshotDirs: [],
1180
- targetPaths: [bindingSourceDir],
1181
- };
1182
- try {
1183
- await fsp.mkdir(bindingSourceDir, { recursive: true });
1184
- await ensureBindingSourceBootstrapAnchors(workspace);
1185
- await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1186
- await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1187
- await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
1188
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
1189
- bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)],
1190
- });
1191
- return {
1192
- bindingSourceSlug,
1193
- projectDir: workspace.projectDir,
1194
- };
1195
- }
1196
- catch (error) {
1197
- await rollbackWorkspaceMutation(mutationSnapshot);
1198
- throw error;
1199
- }
1200
- }
1201
- /**
1202
- * Add one `blockHooks` entry to an existing official workspace block.
2
+ * Public `wp-typia add` facade.
1203
3
  *
1204
- * @param options Command options for the hooked-block workflow.
1205
- * @param options.anchorBlockName Full block name that will anchor the insertion.
1206
- * @param options.blockName Existing workspace block slug to patch.
1207
- * @param options.cwd Working directory used to resolve the nearest official workspace.
1208
- * Defaults to `process.cwd()`.
1209
- * @param options.position Hook position to store in `block.json`.
1210
- * @returns A promise that resolves with the normalized target block slug, anchor
1211
- * block name, position, and owning project directory after `block.json` is written.
1212
- * @throws {Error} When the command is run outside an official workspace, when
1213
- * the target block is unknown, when required flags are missing, or when the
1214
- * block already defines a hook for the requested anchor.
4
+ * The canonical CLI surface stays stable here while the implementation lives
5
+ * in focused internal modules:
6
+ * - `cli-add-shared` for shared validation/help/rollback helpers
7
+ * - `cli-add-block` for built-in block scaffolding
8
+ * - `cli-add-workspace` for workspace mutation commands
1215
9
  */
1216
- export async function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd = process.cwd(), position, }) {
1217
- const workspace = resolveWorkspaceProject(cwd);
1218
- const blockSlug = normalizeBlockSlug(blockName);
1219
- const inventory = readWorkspaceInventory(workspace.projectDir);
1220
- resolveWorkspaceBlock(inventory, blockSlug);
1221
- const resolvedAnchorBlockName = assertValidHookAnchor(anchorBlockName);
1222
- const resolvedPosition = assertValidHookedBlockPosition(position);
1223
- const selfHookAnchor = `${workspace.workspace.namespace}/${blockSlug}`;
1224
- if (resolvedAnchorBlockName === selfHookAnchor) {
1225
- throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
1226
- }
1227
- const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
1228
- const blockJsonRelativePath = path.relative(workspace.projectDir, blockJsonPath);
1229
- const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
1230
- if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
1231
- throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
1232
- }
1233
- const mutationSnapshot = {
1234
- fileSources: await snapshotWorkspaceFiles([blockJsonPath]),
1235
- snapshotDirs: [],
1236
- targetPaths: [],
1237
- };
1238
- try {
1239
- blockHooks[resolvedAnchorBlockName] = resolvedPosition;
1240
- await fsp.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
1241
- return {
1242
- anchorBlockName: resolvedAnchorBlockName,
1243
- blockSlug,
1244
- position: resolvedPosition,
1245
- projectDir: workspace.projectDir,
1246
- };
1247
- }
1248
- catch (error) {
1249
- await rollbackWorkspaceMutation(mutationSnapshot);
1250
- throw error;
1251
- }
1252
- }
1253
- export { getWorkspaceBlockSelectOptions };
10
+ export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
+ export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
12
+ export { runAddBindingSourceCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
+ export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";