@wp-typia/project-tools 0.22.1 → 0.22.3

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/dist/runtime/built-in-block-code-templates/interactivity.d.ts +1 -1
  2. package/dist/runtime/built-in-block-code-templates/interactivity.js +4 -2
  3. package/dist/runtime/cli-add-shared.d.ts +49 -0
  4. package/dist/runtime/cli-add-shared.js +204 -71
  5. package/dist/runtime/cli-add-workspace-ability-scaffold.d.ts +5 -0
  6. package/dist/runtime/cli-add-workspace-ability-scaffold.js +392 -0
  7. package/dist/runtime/cli-add-workspace-ability-templates.d.ts +34 -0
  8. package/dist/runtime/cli-add-workspace-ability-templates.js +500 -0
  9. package/dist/runtime/cli-add-workspace-ability-types.d.ts +27 -0
  10. package/dist/runtime/cli-add-workspace-ability-types.js +14 -0
  11. package/dist/runtime/cli-add-workspace-ability.js +12 -852
  12. package/dist/runtime/cli-add-workspace-ai-scaffold.d.ts +21 -0
  13. package/dist/runtime/cli-add-workspace-ai-scaffold.js +91 -0
  14. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +119 -1
  15. package/dist/runtime/cli-add-workspace-ai-templates.d.ts +4 -0
  16. package/dist/runtime/cli-add-workspace-ai-templates.js +605 -0
  17. package/dist/runtime/cli-add-workspace-ai.js +15 -465
  18. package/dist/runtime/cli-add-workspace-assets.js +7 -4
  19. package/dist/runtime/cli-add-workspace.js +1 -19
  20. package/dist/runtime/cli-doctor-workspace-bindings.d.ts +11 -0
  21. package/dist/runtime/cli-doctor-workspace-bindings.js +134 -0
  22. package/dist/runtime/cli-doctor-workspace-blocks.d.ts +11 -0
  23. package/dist/runtime/cli-doctor-workspace-blocks.js +504 -0
  24. package/dist/runtime/cli-doctor-workspace-features.d.ts +11 -0
  25. package/dist/runtime/cli-doctor-workspace-features.js +383 -0
  26. package/dist/runtime/cli-doctor-workspace-package.d.ts +18 -0
  27. package/dist/runtime/cli-doctor-workspace-package.js +59 -0
  28. package/dist/runtime/cli-doctor-workspace-shared.d.ts +69 -0
  29. package/dist/runtime/cli-doctor-workspace-shared.js +87 -0
  30. package/dist/runtime/cli-doctor-workspace.js +25 -1062
  31. package/dist/runtime/scaffold-compatibility.d.ts +2 -0
  32. package/dist/runtime/scaffold-compatibility.js +2 -0
  33. package/dist/runtime/typia-llm.d.ts +37 -2
  34. package/dist/runtime/typia-llm.js +240 -3
  35. package/dist/runtime/workspace-inventory.js +24 -0
  36. package/package.json +3 -3
@@ -1,788 +1,8 @@
1
- import fs from "node:fs";
2
- import { promises as fsp } from "node:fs";
3
- import path from "node:path";
4
- import { syncTypeSchemas } from "@wp-typia/block-runtime/metadata-core";
5
- import semver from "semver";
6
- import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from "./workspace-inventory.js";
1
+ import { assertAbilityDoesNotExist, assertValidGeneratedSlug, normalizeBlockSlug, } from "./cli-add-shared.js";
2
+ import { scaffoldAbilityWorkspace } from "./cli-add-workspace-ability-scaffold.js";
3
+ import { readWorkspaceInventory } from "./workspace-inventory.js";
7
4
  import { resolveWorkspaceProject } from "./workspace-project.js";
8
- import { toPascalCase, toTitleCase } from "./string-case.js";
9
- import { escapeRegex, findPhpFunctionRange, hasPhpFunctionDefinition, quotePhpString, replacePhpFunctionDefinition, } from "./php-utils.js";
10
- import { assertAbilityDoesNotExist, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
11
- import { REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, updatePluginHeaderCompatibility, } from "./scaffold-compatibility.js";
12
- import { DEFAULT_WORDPRESS_ABILITIES_VERSION, DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION, } from "./package-versions.js";
13
- const ABILITY_SERVER_GLOB = "/inc/abilities/*.php";
14
- const ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
15
- const ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
16
- const ABILITY_REGISTRY_END_MARKER = "// wp-typia add ability entries end";
17
- const ABILITY_REGISTRY_START_MARKER = "// wp-typia add ability entries start";
18
- const WP_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/abilities";
19
- const WP_CORE_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/core-abilities";
20
- function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
21
- if (!existingVersion) {
22
- return requiredVersion;
23
- }
24
- const existingMinimum = semver.minVersion(existingVersion);
25
- const requiredMinimum = semver.minVersion(requiredVersion);
26
- if (!existingMinimum || !requiredMinimum) {
27
- return requiredVersion;
28
- }
29
- return semver.gte(existingMinimum, requiredMinimum)
30
- ? existingVersion
31
- : requiredVersion;
32
- }
33
- function toAbilityCategorySlug(workspaceNamespace) {
34
- const normalizedNamespace = workspaceNamespace
35
- .replace(/[^a-z0-9-]+/gu, "-")
36
- .replace(/-{2,}/gu, "-")
37
- .replace(/^-|-$/gu, "");
38
- return `${normalizedNamespace || "workspace"}-workflows`;
39
- }
40
- function buildAbilityConfigEntry(abilitySlug, compatibilityPolicy) {
41
- const pascalCase = toPascalCase(abilitySlug);
42
- return [
43
- "\t{",
44
- `\t\tclientFile: ${quoteTsString(`src/abilities/${abilitySlug}/client.ts`)},`,
45
- `\t\tcompatibility: ${renderScaffoldCompatibilityConfig(compatibilityPolicy)},`,
46
- `\t\tconfigFile: ${quoteTsString(`src/abilities/${abilitySlug}/ability.config.json`)},`,
47
- `\t\tdataFile: ${quoteTsString(`src/abilities/${abilitySlug}/data.ts`)},`,
48
- `\t\tinputSchemaFile: ${quoteTsString(`src/abilities/${abilitySlug}/input.schema.json`)},`,
49
- `\t\tinputTypeName: ${quoteTsString(`${pascalCase}AbilityInput`)},`,
50
- `\t\toutputSchemaFile: ${quoteTsString(`src/abilities/${abilitySlug}/output.schema.json`)},`,
51
- `\t\toutputTypeName: ${quoteTsString(`${pascalCase}AbilityOutput`)},`,
52
- `\t\tphpFile: ${quoteTsString(`inc/abilities/${abilitySlug}.php`)},`,
53
- `\t\tslug: ${quoteTsString(abilitySlug)},`,
54
- `\t\ttypesFile: ${quoteTsString(`src/abilities/${abilitySlug}/types.ts`)},`,
55
- "\t},",
56
- ].join("\n");
57
- }
58
- function buildAbilityConfigSource(abilitySlug, workspaceNamespace) {
59
- const abilityTitle = toTitleCase(abilitySlug);
60
- return `${JSON.stringify({
61
- abilityId: `${workspaceNamespace}/${abilitySlug}`,
62
- category: {
63
- description: `Typed editor and admin workflows exposed by the ${workspaceNamespace} workspace.`,
64
- label: `${toTitleCase(workspaceNamespace)} Workflows`,
65
- slug: toAbilityCategorySlug(workspaceNamespace),
66
- },
67
- description: `Runs the ${abilityTitle} workflow using a typed server callback.`,
68
- label: abilityTitle,
69
- meta: {
70
- annotations: {
71
- destructive: false,
72
- idempotent: true,
73
- readonly: false,
74
- },
75
- mcp: {
76
- public: false,
77
- },
78
- showInRest: true,
79
- },
80
- }, null, 2)}\n`;
81
- }
82
- function buildAbilityTypesSource(abilitySlug) {
83
- const pascalCase = toPascalCase(abilitySlug);
84
- return `export interface ${pascalCase}AbilityInput {
85
- \tcontextId: number;
86
- \tnote?: string;
87
- }
88
-
89
- export interface ${pascalCase}AbilityOutput {
90
- \tprocessedContextId: number;
91
- \treceivedNote?: string;
92
- \tstatus: 'ready';
93
- \tsummary: string;
94
- }
95
- `;
96
- }
97
- function buildAbilityDataSource(abilitySlug) {
98
- const pascalCase = toPascalCase(abilitySlug);
99
- const abilityConstBase = abilitySlug
100
- .toUpperCase()
101
- .replace(/[^A-Z0-9]+/gu, "_")
102
- .replace(/_{2,}/gu, "_")
103
- .replace(/^_|_$/gu, "");
104
- return `import {
105
- \texecuteAbility,
106
- \tgetAbilities,
107
- \tgetAbility as getRegisteredAbility,
108
- } from '@wordpress/abilities';
109
- import '@wordpress/core-abilities';
110
-
111
- import abilityConfig from './ability.config.json';
112
-
113
- import type { ${pascalCase}AbilityInput, ${pascalCase}AbilityOutput } from './types';
114
-
115
- interface WordPressAbilityDefinition {
116
- \tcategory?: string;
117
- \tdescription?: string;
118
- \tlabel?: string;
119
- \tmeta?: Record<string, unknown>;
120
- \tname?: string;
121
- }
122
-
123
- export const ${abilityConstBase}_ABILITY = abilityConfig;
124
- export const ${abilityConstBase}_ABILITY_CATEGORY = abilityConfig.category;
125
- export const ${abilityConstBase}_ABILITY_ID = abilityConfig.abilityId;
126
- export const ${abilityConstBase}_ABILITY_META = abilityConfig.meta;
127
- const ABILITY_DISCOVERY_POLL_INTERVAL_MS = 50;
128
- const ABILITY_DISCOVERY_TIMEOUT_MS = 5000;
129
-
130
- export type {
131
- \t${pascalCase}AbilityInput,
132
- \t${pascalCase}AbilityOutput,
133
- };
134
-
135
- function sleep( milliseconds: number ): Promise< void > {
136
- \treturn new Promise( ( resolve ) => {
137
- \t\tsetTimeout( resolve, milliseconds );
138
- \t} );
139
- }
140
-
141
- async function waitFor${pascalCase}AbilityRegistration(): Promise< void > {
142
- \tconst deadline = Date.now() + ABILITY_DISCOVERY_TIMEOUT_MS;
143
- \twhile ( ! getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) ) {
144
- \t\tif ( Date.now() >= deadline ) {
145
- \t\t\treturn;
146
- \t\t}
147
-
148
- \t\tawait sleep( ABILITY_DISCOVERY_POLL_INTERVAL_MS );
149
- \t}
150
- }
151
-
152
- export async function list${pascalCase}CategoryAbilities(): Promise< WordPressAbilityDefinition[] > {
153
- \tawait waitFor${pascalCase}AbilityRegistration();
154
-
155
- \treturn getAbilities( {
156
- \t\tcategory: ${abilityConstBase}_ABILITY_CATEGORY.slug,
157
- \t} ) as WordPressAbilityDefinition[];
158
- }
159
-
160
- export async function get${pascalCase}Ability(): Promise<
161
- \t| WordPressAbilityDefinition
162
- \t| undefined
163
- > {
164
- \tawait waitFor${pascalCase}AbilityRegistration();
165
-
166
- \treturn getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) as
167
- \t\t| WordPressAbilityDefinition
168
- \t\t| undefined;
169
- }
170
-
171
- export async function require${pascalCase}Ability(): Promise< WordPressAbilityDefinition > {
172
- \tconst ability = await get${pascalCase}Ability();
173
- \tif ( ability ) {
174
- \t\treturn ability;
175
- \t}
176
-
177
- \tthrow new Error(
178
- \t\t[
179
- \t\t\t\`Ability "\${ ${abilityConstBase}_ABILITY_ID }" is not available yet.\`,
180
- \t\t\t'Load the WordPress core abilities integration on this screen and confirm the server-side registration succeeded.',
181
- \t\t].join( ' ' )
182
- \t);
183
- }
184
-
185
- export async function run${pascalCase}Ability(
186
- \tinput: ${pascalCase}AbilityInput
187
- ): Promise< ${pascalCase}AbilityOutput > {
188
- \tawait waitFor${pascalCase}AbilityRegistration();
189
-
190
- \treturn ( await executeAbility(
191
- \t\t${abilityConstBase}_ABILITY_ID,
192
- \t\tinput
193
- \t) ) as ${pascalCase}AbilityOutput;
194
- }
195
- `;
196
- }
197
- function buildAbilityClientSource(abilitySlug) {
198
- const pascalCase = toPascalCase(abilitySlug);
199
- return `/**
200
- * Re-export the typed ${pascalCase} ability client helpers.
201
- *
202
- * The helper methods load the WordPress core abilities integration and wait for
203
- * this server-registered ability before reading or executing it.
204
- */
205
- export * from './data';
206
- `;
207
- }
208
- function buildAbilitySyncScriptSource() {
209
- return `/* eslint-disable no-console */
210
- import { syncTypeSchemas } from '@wp-typia/block-runtime/metadata-core';
211
-
212
- import {
213
- \tABILITIES,
214
- \ttype WorkspaceAbilityConfig,
215
- } from './block-config';
216
-
217
- function parseCliOptions( argv: string[] ) {
218
- \tconst options = {
219
- \t\tcheck: false,
220
- \t};
221
-
222
- \tfor ( const argument of argv ) {
223
- \t\tif ( argument === '--check' ) {
224
- \t\t\toptions.check = true;
225
- \t\t\tcontinue;
226
- \t\t}
227
-
228
- \t\tthrow new Error( \`Unknown sync-abilities flag: \${ argument }\` );
229
- \t}
230
-
231
- \treturn options;
232
- }
233
-
234
- function isWorkspaceAbility(
235
- \tability: WorkspaceAbilityConfig
236
- ): ability is WorkspaceAbilityConfig & {
237
- \tclientFile: string;
238
- \tconfigFile: string;
239
- \tdataFile: string;
240
- \tinputSchemaFile: string;
241
- \tinputTypeName: string;
242
- \toutputSchemaFile: string;
243
- \toutputTypeName: string;
244
- \tphpFile: string;
245
- \ttypesFile: string;
246
- } {
247
- \treturn (
248
- \t\ttypeof ability.clientFile === 'string' &&
249
- \t\ttypeof ability.configFile === 'string' &&
250
- \t\ttypeof ability.dataFile === 'string' &&
251
- \t\ttypeof ability.inputSchemaFile === 'string' &&
252
- \t\ttypeof ability.inputTypeName === 'string' &&
253
- \t\ttypeof ability.outputSchemaFile === 'string' &&
254
- \t\ttypeof ability.outputTypeName === 'string' &&
255
- \t\ttypeof ability.phpFile === 'string' &&
256
- \t\ttypeof ability.typesFile === 'string'
257
- \t);
258
- }
259
-
260
- async function main() {
261
- \tconst options = parseCliOptions( process.argv.slice( 2 ) );
262
- \tconst abilities = ABILITIES.filter( isWorkspaceAbility );
263
-
264
- \tif ( ABILITIES.length > 0 && abilities.length === 0 ) {
265
- \t\tconsole.warn(
266
- \t\t\t'⚠️ Ability inventory entries exist, but none include the required typed schema files. Check scripts/block-config.ts before relying on sync-abilities.'
267
- \t\t);
268
- \t}
269
-
270
- \tif ( abilities.length === 0 ) {
271
- \t\tconsole.log(
272
- \t\t\toptions.check
273
- \t\t\t\t? 'ℹ️ No typed workflow abilities are registered yet. "sync-abilities --check" is already clean.'
274
- \t\t\t\t: 'ℹ️ No typed workflow abilities are registered yet.'
275
- \t\t);
276
- \t\treturn;
277
- \t}
278
-
279
- \tfor ( const ability of abilities ) {
280
- \t\tawait syncTypeSchemas(
281
- \t\t\t{
282
- \t\t\t\tjsonSchemaFile: ability.inputSchemaFile,
283
- \t\t\t\tprojectRoot: process.cwd(),
284
- \t\t\t\tsourceTypeName: ability.inputTypeName,
285
- \t\t\t\ttypesFile: ability.typesFile,
286
- \t\t\t},
287
- \t\t\t{
288
- \t\t\t\tcheck: options.check,
289
- \t\t\t}
290
- \t\t);
291
-
292
- \t\tawait syncTypeSchemas(
293
- \t\t\t{
294
- \t\t\t\tjsonSchemaFile: ability.outputSchemaFile,
295
- \t\t\t\tprojectRoot: process.cwd(),
296
- \t\t\t\tsourceTypeName: ability.outputTypeName,
297
- \t\t\t\ttypesFile: ability.typesFile,
298
- \t\t\t},
299
- \t\t\t{
300
- \t\t\t\tcheck: options.check,
301
- \t\t\t}
302
- \t\t);
303
- \t}
304
-
305
- \tconsole.log(
306
- \t\toptions.check
307
- \t\t\t? '✅ Ability input and output schemas are already up to date for all registered workflow abilities!'
308
- \t\t\t: '✅ Ability input and output schemas generated for all registered workflow abilities!'
309
- \t);
310
- }
311
-
312
- main().catch( ( error ) => {
313
- \tconsole.error( '❌ Ability schema sync failed:', error );
314
- \tprocess.exit( 1 );
315
- } );
316
- `;
317
- }
318
- function buildAbilityPhpSource(abilitySlug, workspace) {
319
- const abilityTitle = toTitleCase(abilitySlug);
320
- const abilityPhpId = abilitySlug.replace(/-/g, "_");
321
- const categoryRegisterFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_register_ability_category`;
322
- const abilityRegisterFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_register_ability`;
323
- const configLoaderFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_load_ability_config`;
324
- const schemaLoaderFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_load_ability_schema`;
325
- const permissionFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_can_execute_ability`;
326
- const executeFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_execute_ability`;
327
- const metaFactoryFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_build_ability_meta`;
328
- return `<?php
329
- if ( ! defined( 'ABSPATH' ) ) {
330
- \treturn;
331
- }
332
-
333
- if ( ! function_exists( '${configLoaderFunctionName}' ) ) {
334
- \tfunction ${configLoaderFunctionName}() {
335
- \t\t$project_root = dirname( __DIR__, 2 );
336
- \t\t$config_path = $project_root . '/src/abilities/${abilitySlug}/ability.config.json';
337
- \t\tif ( ! file_exists( $config_path ) ) {
338
- \t\t\treturn null;
339
- \t\t}
340
-
341
- \t\t$decoded = json_decode( file_get_contents( $config_path ), true );
342
- \t\treturn is_array( $decoded ) ? $decoded : null;
343
- \t}
344
- }
345
-
346
- if ( ! function_exists( '${schemaLoaderFunctionName}' ) ) {
347
- \tfunction ${schemaLoaderFunctionName}( $schema_name ) {
348
- \t\t$project_root = dirname( __DIR__, 2 );
349
- \t\t$schema_path = $project_root . '/src/abilities/${abilitySlug}/' . $schema_name;
350
- \t\tif ( ! file_exists( $schema_path ) ) {
351
- \t\t\treturn null;
352
- \t\t}
353
-
354
- \t\t$decoded = json_decode( file_get_contents( $schema_path ), true );
355
- \t\treturn is_array( $decoded ) ? $decoded : null;
356
- \t}
357
- }
358
-
359
- if ( ! function_exists( '${metaFactoryFunctionName}' ) ) {
360
- \tfunction ${metaFactoryFunctionName}( array $config ) {
361
- \t\t$meta = array(
362
- \t\t\t'annotations' => isset( $config['meta']['annotations'] ) && is_array( $config['meta']['annotations'] )
363
- \t\t\t\t? $config['meta']['annotations']
364
- \t\t\t\t: array(
365
- \t\t\t\t\t'destructive' => false,
366
- \t\t\t\t\t'idempotent' => true,
367
- \t\t\t\t\t'readonly' => false,
368
- \t\t\t\t),
369
- \t\t\t'show_in_rest' => ! empty( $config['meta']['showInRest'] ),
370
- \t\t);
371
-
372
- \t\tif ( ! empty( $config['meta']['mcp']['public'] ) ) {
373
- \t\t\t$meta['mcp'] = array(
374
- \t\t\t\t'public' => true,
375
- \t\t\t);
376
- \t\t}
377
-
378
- \t\treturn $meta;
379
- \t}
380
- }
381
-
382
- if ( ! function_exists( '${permissionFunctionName}' ) ) {
383
- \tfunction ${permissionFunctionName}( $input = array() ) {
384
- \t\tunset( $input );
385
-
386
- \t\treturn current_user_can( 'edit_posts' );
387
- \t}
388
- }
389
-
390
- if ( ! function_exists( '${executeFunctionName}' ) ) {
391
- \tfunction ${executeFunctionName}( $input = array() ) {
392
- \t\t$payload = is_array( $input ) ? $input : array();
393
- \t\t$context_id = isset( $payload['contextId'] ) ? (int) $payload['contextId'] : 0;
394
- \t\t$note = isset( $payload['note'] ) && is_string( $payload['note'] )
395
- \t\t\t? trim( $payload['note'] )
396
- \t\t\t: '';
397
- \t\t$result = array(
398
- \t\t\t'processedContextId' => $context_id,
399
- \t\t\t'status' => 'ready',
400
- \t\t\t'summary' => sprintf(
401
- \t\t\t\t/* translators: 1: workflow title, 2: context id */
402
- \t\t\t\t__( '%1$s processed context %2$d.', ${quotePhpString(workspace.workspace.textDomain)} ),
403
- \t\t\t\t${quotePhpString(abilityTitle)},
404
- \t\t\t\t$context_id
405
- \t\t\t),
406
- \t\t);
407
-
408
- \t\tif ( '' !== $note ) {
409
- \t\t\t$result['receivedNote'] = $note;
410
- \t\t}
411
-
412
- \t\treturn $result;
413
- \t}
414
- }
415
-
416
- if ( ! function_exists( '${categoryRegisterFunctionName}' ) ) {
417
- \tfunction ${categoryRegisterFunctionName}() {
418
- \t\tif ( ! function_exists( 'wp_register_ability_category' ) ) {
419
- \t\t\treturn;
420
- \t\t}
421
-
422
- \t\t$config = ${configLoaderFunctionName}();
423
- \t\tif (
424
- \t\t\t! is_array( $config ) ||
425
- \t\t\tempty( $config['category']['slug'] ) ||
426
- \t\t\tempty( $config['category']['label'] )
427
- \t\t) {
428
- \t\t\treturn;
429
- \t\t}
430
-
431
- \t\twp_register_ability_category(
432
- \t\t\t(string) $config['category']['slug'],
433
- \t\t\tarray(
434
- \t\t\t\t'description' => isset( $config['category']['description'] ) && is_string( $config['category']['description'] )
435
- \t\t\t\t\t? $config['category']['description']
436
- \t\t\t\t\t: '',
437
- \t\t\t\t'label' => (string) $config['category']['label'],
438
- \t\t\t)
439
- \t\t);
440
- \t}
441
- }
442
-
443
- if ( ! function_exists( '${abilityRegisterFunctionName}' ) ) {
444
- \tfunction ${abilityRegisterFunctionName}() {
445
- \t\tif ( ! function_exists( 'wp_register_ability' ) ) {
446
- \t\t\treturn;
447
- \t\t}
448
-
449
- \t\t$config = ${configLoaderFunctionName}();
450
- \t\tif (
451
- \t\t\t! is_array( $config ) ||
452
- \t\t\tempty( $config['abilityId'] ) ||
453
- \t\t\tempty( $config['category']['slug'] ) ||
454
- \t\t\tempty( $config['label'] ) ||
455
- \t\t\tempty( $config['description'] )
456
- \t\t) {
457
- \t\t\treturn;
458
- \t\t}
459
-
460
- \t\t$input_schema = ${schemaLoaderFunctionName}( 'input.schema.json' );
461
- \t\t$output_schema = ${schemaLoaderFunctionName}( 'output.schema.json' );
462
- \t\tif ( ! is_array( $output_schema ) ) {
463
- \t\t\treturn;
464
- \t\t}
465
-
466
- \t\t$args = array(
467
- \t\t\t'category' => (string) $config['category']['slug'],
468
- \t\t\t'description' => (string) $config['description'],
469
- \t\t\t'execute_callback' => ${quotePhpString(executeFunctionName)},
470
- \t\t\t'label' => (string) $config['label'],
471
- \t\t\t'meta' => ${metaFactoryFunctionName}( $config ),
472
- \t\t\t'output_schema' => $output_schema,
473
- \t\t\t'permission_callback' => ${quotePhpString(permissionFunctionName)},
474
- \t\t);
475
-
476
- \t\tif ( is_array( $input_schema ) ) {
477
- \t\t\t$args['input_schema'] = $input_schema;
478
- \t\t}
479
-
480
- \t\twp_register_ability(
481
- \t\t\t(string) $config['abilityId'],
482
- \t\t\t$args
483
- \t\t);
484
- \t}
485
- }
486
-
487
- add_action( 'wp_abilities_api_categories_init', '${categoryRegisterFunctionName}' );
488
- add_action( 'wp_abilities_api_init', '${abilityRegisterFunctionName}' );
489
- `;
490
- }
491
- function buildAbilityRegistrySource(abilitySlugs) {
492
- const exportLines = abilitySlugs
493
- .map((abilitySlug) => `export * from './${abilitySlug}/client';`)
494
- .join("\n");
495
- return [
496
- ABILITY_REGISTRY_START_MARKER,
497
- exportLines,
498
- ABILITY_REGISTRY_END_MARKER,
499
- ]
500
- .filter((line) => line.length > 0)
501
- .join("\n")
502
- .concat("\n");
503
- }
504
- function resolveAbilityRegistryPath(projectDir) {
505
- const abilitiesDir = path.join(projectDir, "src", "abilities");
506
- return [path.join(abilitiesDir, "index.ts"), path.join(abilitiesDir, "index.js")].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(abilitiesDir, "index.ts");
507
- }
508
- function readAbilityRegistrySlugs(registryPath) {
509
- if (!fs.existsSync(registryPath)) {
510
- return [];
511
- }
512
- const source = fs.readFileSync(registryPath, "utf8");
513
- return Array.from(source.matchAll(/^\s*export\s+\*\s+from\s+['"]\.\/([^/'"]+)\/client['"];?\s*$/gmu)).map((match) => match[1]);
514
- }
515
- async function writeAbilityRegistry(projectDir, abilitySlug) {
516
- const abilitiesDir = path.join(projectDir, "src", "abilities");
517
- const registryPath = resolveAbilityRegistryPath(projectDir);
518
- await fsp.mkdir(abilitiesDir, { recursive: true });
519
- const existingAbilitySlugs = readWorkspaceInventory(projectDir).abilities.map((entry) => entry.slug);
520
- const existingRegistrySlugs = readAbilityRegistrySlugs(registryPath);
521
- const nextAbilitySlugs = Array.from(new Set([...existingAbilitySlugs, ...existingRegistrySlugs, abilitySlug])).sort();
522
- const generatedSection = buildAbilityRegistrySource(nextAbilitySlugs);
523
- const existingSource = fs.existsSync(registryPath)
524
- ? fs.readFileSync(registryPath, "utf8")
525
- : "";
526
- const generatedSectionPattern = new RegExp(`${escapeRegex(ABILITY_REGISTRY_START_MARKER)}[\\s\\S]*?${escapeRegex(ABILITY_REGISTRY_END_MARKER)}\\n?`, "u");
527
- const nextSource = existingSource
528
- ? generatedSectionPattern.test(existingSource)
529
- ? existingSource.replace(generatedSectionPattern, generatedSection)
530
- : `${existingSource.trimEnd()}\n\n${generatedSection}`
531
- : generatedSection;
532
- await fsp.writeFile(registryPath, nextSource, "utf8");
533
- }
534
- async function ensureAbilityBootstrapAnchors(workspace) {
535
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
536
- await patchFile(bootstrapPath, (source) => {
537
- let nextSource = source;
538
- const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
539
- const loadFunctionName = `${workspace.workspace.phpPrefix}_load_workflow_abilities`;
540
- const enqueueFunctionName = `${workspace.workspace.phpPrefix}_enqueue_workflow_abilities`;
541
- const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
542
- const adminEnqueueHook = `add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );`;
543
- const editorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
544
- const loadFunction = `
545
-
546
- function ${loadFunctionName}() {
547
- \tforeach ( glob( __DIR__ . '${ABILITY_SERVER_GLOB}' ) ?: array() as $ability_module ) {
548
- \t\trequire_once $ability_module;
549
- \t}
550
- }
551
- `;
552
- const enqueueFunction = `
553
-
554
- function ${enqueueFunctionName}() {
555
- \tif ( ! class_exists( 'WP_Ability' ) ) {
556
- \t\treturn;
557
- \t}
558
-
559
- \t$script_path = __DIR__ . '/${ABILITY_EDITOR_SCRIPT}';
560
- \t$asset_path = __DIR__ . '/${ABILITY_EDITOR_ASSET}';
561
-
562
- \tif ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
563
- \t\treturn;
564
- \t}
565
-
566
- \t$asset = require $asset_path;
567
- \tif ( ! is_array( $asset ) ) {
568
- \t\t$asset = array();
569
- \t}
570
-
571
- \t$dependencies = isset( $asset['dependencies'] ) && is_array( $asset['dependencies'] )
572
- \t\t? $asset['dependencies']
573
- \t\t: array();
574
-
575
- \tforeach ( array( '${WP_CORE_ABILITIES_SCRIPT_MODULE_ID}', '${WP_ABILITIES_SCRIPT_MODULE_ID}' ) as $ability_dependency ) {
576
- \t\t$has_dependency = false;
577
- \t\tforeach ( $dependencies as $dependency ) {
578
- \t\t\t$dependency_id = is_array( $dependency ) && isset( $dependency['id'] )
579
- \t\t\t\t? $dependency['id']
580
- \t\t\t\t: $dependency;
581
- \t\t\tif ( $dependency_id === $ability_dependency ) {
582
- \t\t\t\t$has_dependency = true;
583
- \t\t\t\tbreak;
584
- \t\t\t}
585
- \t\t}
586
- \t\tif ( ! $has_dependency ) {
587
- \t\t\t$dependencies[] = $ability_dependency;
588
- \t\t}
589
- \t}
590
-
591
- \tif ( ! function_exists( 'wp_enqueue_script_module' ) ) {
592
- \t\treturn;
593
- \t}
594
-
595
- \twp_enqueue_script_module(
596
- \t\t'${workspaceBaseName}-abilities',
597
- \t\tplugins_url( '${ABILITY_EDITOR_SCRIPT}', __FILE__ ),
598
- \t\t$dependencies,
599
- \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path )
600
- \t);
601
- }
602
- `;
603
- const insertionAnchors = [
604
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
605
- /\?>\s*$/u,
606
- ];
607
- const insertPhpSnippet = (snippet) => {
608
- for (const anchor of insertionAnchors) {
609
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
610
- if (candidate !== nextSource) {
611
- nextSource = candidate;
612
- return;
613
- }
614
- }
615
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
616
- };
617
- const appendPhpSnippet = (snippet) => {
618
- const closingTagPattern = /\?>\s*$/u;
619
- if (closingTagPattern.test(nextSource)) {
620
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
621
- return;
622
- }
623
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
624
- };
625
- if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
626
- insertPhpSnippet(loadFunction);
627
- }
628
- if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
629
- insertPhpSnippet(enqueueFunction);
630
- }
631
- else if (!findPhpFunctionRange(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
632
- nextSource =
633
- replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction, { trimReplacementStart: true }) ?? nextSource;
634
- }
635
- if (!nextSource.includes(loadHook)) {
636
- appendPhpSnippet(loadHook);
637
- }
638
- if (!nextSource.includes(adminEnqueueHook)) {
639
- appendPhpSnippet(adminEnqueueHook);
640
- }
641
- if (!nextSource.includes(editorEnqueueHook)) {
642
- appendPhpSnippet(editorEnqueueHook);
643
- }
644
- return nextSource;
645
- });
646
- }
647
- async function ensureAbilityPackageScripts(workspace) {
648
- const packageJsonPath = path.join(workspace.projectDir, "package.json");
649
- const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
650
- const nextScripts = {
651
- ...(packageJson.scripts ?? {}),
652
- "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts",
653
- };
654
- const nextDependencies = {
655
- ...(packageJson.dependencies ?? {}),
656
- [WP_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_ABILITIES_SCRIPT_MODULE_ID], DEFAULT_WORDPRESS_ABILITIES_VERSION),
657
- [WP_CORE_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_CORE_ABILITIES_SCRIPT_MODULE_ID], DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION),
658
- };
659
- if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) &&
660
- JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {})) {
661
- return;
662
- }
663
- packageJson.scripts = nextScripts;
664
- packageJson.dependencies = nextDependencies;
665
- await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
666
- }
667
- async function ensureAbilitySyncProjectAnchors(workspace) {
668
- const syncProjectScriptPath = path.join(workspace.projectDir, "scripts", "sync-project.ts");
669
- await patchFile(syncProjectScriptPath, (source) => {
670
- let nextSource = source;
671
- const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
672
- const syncAbilitiesConst = "const syncAbilitiesScriptPath = path.join( 'scripts', 'sync-abilities.ts' );";
673
- const syncRestBlockPattern = /if \( fs\.existsSync\( path\.resolve\( process\.cwd\(\), syncRestScriptPath \) \) \) \{\n\s*runSyncScript\( syncRestScriptPath, options \);\n\s*\}/u;
674
- const syncAbilitiesBlock = [
675
- "if ( fs.existsSync( path.resolve( process.cwd(), syncAbilitiesScriptPath ) ) ) {",
676
- "\trunSyncScript( syncAbilitiesScriptPath, options );",
677
- "}",
678
- ].join("\n");
679
- if (!nextSource.includes(syncAbilitiesConst)) {
680
- if (!nextSource.includes(syncRestConst)) {
681
- throw new Error([
682
- `ensureAbilitySyncProjectAnchors could not patch ${path.basename(syncProjectScriptPath)}.`,
683
- "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
684
- "Restore the generated template or wire sync-abilities manually before retrying.",
685
- ].join(" "));
686
- }
687
- nextSource = nextSource.replace(syncRestConst, `${syncRestConst}\n${syncAbilitiesConst}`);
688
- }
689
- if (!nextSource.includes("runSyncScript( syncAbilitiesScriptPath, options );")) {
690
- if (!syncRestBlockPattern.test(nextSource)) {
691
- throw new Error([
692
- `ensureAbilitySyncProjectAnchors could not patch ${path.basename(syncProjectScriptPath)}.`,
693
- "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
694
- "Restore the generated template or wire sync-abilities manually before retrying.",
695
- ].join(" "));
696
- }
697
- nextSource = nextSource.replace(syncRestBlockPattern, (match) => `${match}\n\n${syncAbilitiesBlock}`);
698
- }
699
- return nextSource;
700
- });
701
- }
702
- async function ensureAbilityBuildScriptAnchors(workspace) {
703
- const buildScriptPath = path.join(workspace.projectDir, "scripts", "build-workspace.mjs");
704
- await patchFile(buildScriptPath, (source) => {
705
- let nextSource = source;
706
- if (/['"]src\/abilities\/index\.(?:ts|js)['"]/u.test(nextSource)) {
707
- return nextSource;
708
- }
709
- const sharedEntriesPattern = /(for\s*\(\s*const\s+relativePath\s+of\s+\[)([\s\S]*?)(\]\s*\)\s*\{)/u;
710
- const match = nextSource.match(sharedEntriesPattern);
711
- if (!match ||
712
- !match[2].includes("src/bindings/index.ts") ||
713
- !match[2].includes("src/editor-plugins/index.ts")) {
714
- throw new Error([
715
- `ensureAbilityBuildScriptAnchors could not patch ${path.basename(buildScriptPath)}.`,
716
- "Missing the expected shared editor entries array in scripts/build-workspace.mjs.",
717
- "Restore the generated template or wire abilities/index manually before retrying.",
718
- ].join(" "));
719
- }
720
- nextSource = nextSource.replace(sharedEntriesPattern, `$1
721
- \t\t'src/bindings/index.ts',
722
- \t\t'src/bindings/index.js',
723
- \t\t'src/editor-plugins/index.ts',
724
- \t\t'src/editor-plugins/index.js',
725
- \t\t'src/abilities/index.ts',
726
- \t\t'src/abilities/index.js',
727
- \t$3`);
728
- return nextSource;
729
- });
730
- }
731
- async function ensureAbilityWebpackAnchors(workspace) {
732
- const webpackConfigPath = path.join(workspace.projectDir, "webpack.config.js");
733
- await patchFile(webpackConfigPath, (source) => {
734
- if (/['"]abilities\/index['"]/u.test(source)) {
735
- return source;
736
- }
737
- const optionalModuleReturnPattern = /(function\s+getOptionalModuleEntries\s*\(\)\s*\{[\s\S]*?)(\n\treturn Object\.fromEntries\(\s*entries\s*\);\n\})/u;
738
- if (optionalModuleReturnPattern.test(source)) {
739
- return source.replace(optionalModuleReturnPattern, `$1
740
-
741
- \tfor ( const [ entryName, candidates ] of [
742
- \t\t[
743
- \t\t\t'abilities/index',
744
- \t\t\t[ 'src/abilities/index.ts', 'src/abilities/index.js' ],
745
- \t\t],
746
- \t] ) {
747
- \t\tfor ( const relativePath of candidates ) {
748
- \t\t\tconst entryPath = path.resolve( process.cwd(), relativePath );
749
- \t\t\tif ( ! fs.existsSync( entryPath ) ) {
750
- \t\t\t\tcontinue;
751
- \t\t\t}
752
-
753
- \t\t\tentries.push( [ entryName, entryPath ] );
754
- \t\t\tbreak;
755
- \t\t}
756
- \t}
757
- $2`);
758
- }
759
- const sharedEntriesPattern = /for\s*\(\s*const\s+\[\s*entryName\s*,\s*candidates\s*\]\s+of\s+\[([\s\S]*?)\]\s*\)\s*\{/u;
760
- const match = source.match(sharedEntriesPattern);
761
- if (!match ||
762
- !match[1].includes("bindings/index") ||
763
- !match[1].includes("editor-plugins/index")) {
764
- throw new Error([
765
- `ensureAbilityWebpackAnchors could not patch ${path.basename(webpackConfigPath)}.`,
766
- "Missing the expected shared editor entries block in webpack.config.js.",
767
- "Restore the generated template or wire abilities/index manually before retrying.",
768
- ].join(" "));
769
- }
770
- return source.replace(sharedEntriesPattern, `for ( const [ entryName, candidates ] of [
771
- \t\t[
772
- \t\t\t'bindings/index',
773
- \t\t\t[ 'src/bindings/index.ts', 'src/bindings/index.js' ],
774
- \t\t],
775
- \t\t[
776
- \t\t\t'editor-plugins/index',
777
- \t\t\t[ 'src/editor-plugins/index.ts', 'src/editor-plugins/index.js' ],
778
- \t\t],
779
- \t\t[
780
- \t\t\t'abilities/index',
781
- \t\t\t[ 'src/abilities/index.ts', 'src/abilities/index.js' ],
782
- \t\t],
783
- \t] ) {`);
784
- });
785
- }
5
+ import { REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
786
6
  /**
787
7
  * Add one typed workflow ability scaffold to an official workspace project.
788
8
  */
@@ -792,73 +12,13 @@ export async function runAddAbilityCommand({ abilityName, cwd = process.cwd(), }
792
12
  const inventory = readWorkspaceInventory(workspace.projectDir);
793
13
  assertAbilityDoesNotExist(workspace.projectDir, abilitySlug, inventory);
794
14
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY);
795
- const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
796
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
797
- const buildScriptPath = path.join(workspace.projectDir, "scripts", "build-workspace.mjs");
798
- const packageJsonPath = path.join(workspace.projectDir, "package.json");
799
- const syncAbilitiesScriptPath = path.join(workspace.projectDir, "scripts", "sync-abilities.ts");
800
- const syncProjectScriptPath = path.join(workspace.projectDir, "scripts", "sync-project.ts");
801
- const webpackConfigPath = path.join(workspace.projectDir, "webpack.config.js");
802
- const abilitiesIndexPath = resolveAbilityRegistryPath(workspace.projectDir);
803
- const abilityDir = path.join(workspace.projectDir, "src", "abilities", abilitySlug);
804
- const configFilePath = path.join(abilityDir, "ability.config.json");
805
- const typesFilePath = path.join(abilityDir, "types.ts");
806
- const dataFilePath = path.join(abilityDir, "data.ts");
807
- const clientFilePath = path.join(abilityDir, "client.ts");
808
- const phpFilePath = path.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
809
- const mutationSnapshot = {
810
- fileSources: await snapshotWorkspaceFiles([
811
- blockConfigPath,
812
- bootstrapPath,
813
- buildScriptPath,
814
- packageJsonPath,
815
- syncAbilitiesScriptPath,
816
- syncProjectScriptPath,
817
- webpackConfigPath,
818
- abilitiesIndexPath,
819
- ]),
820
- snapshotDirs: [],
821
- targetPaths: [abilityDir, phpFilePath, syncAbilitiesScriptPath],
15
+ await scaffoldAbilityWorkspace({
16
+ abilitySlug,
17
+ compatibilityPolicy,
18
+ workspace,
19
+ });
20
+ return {
21
+ abilitySlug,
22
+ projectDir: workspace.projectDir,
822
23
  };
823
- try {
824
- await fsp.mkdir(abilityDir, { recursive: true });
825
- await fsp.mkdir(path.dirname(phpFilePath), { recursive: true });
826
- await ensureAbilityBootstrapAnchors(workspace);
827
- await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
828
- await ensureAbilityPackageScripts(workspace);
829
- await ensureAbilitySyncProjectAnchors(workspace);
830
- await ensureAbilityBuildScriptAnchors(workspace);
831
- await ensureAbilityWebpackAnchors(workspace);
832
- await fsp.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
833
- await fsp.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
834
- await fsp.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
835
- await fsp.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
836
- await fsp.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
837
- await fsp.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
838
- const pascalCase = toPascalCase(abilitySlug);
839
- await syncTypeSchemas({
840
- jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
841
- projectRoot: workspace.projectDir,
842
- sourceTypeName: `${pascalCase}AbilityInput`,
843
- typesFile: `src/abilities/${abilitySlug}/types.ts`,
844
- });
845
- await syncTypeSchemas({
846
- jsonSchemaFile: `src/abilities/${abilitySlug}/output.schema.json`,
847
- projectRoot: workspace.projectDir,
848
- sourceTypeName: `${pascalCase}AbilityOutput`,
849
- typesFile: `src/abilities/${abilitySlug}/types.ts`,
850
- });
851
- await writeAbilityRegistry(workspace.projectDir, abilitySlug);
852
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
853
- abilityEntries: [buildAbilityConfigEntry(abilitySlug, compatibilityPolicy)],
854
- });
855
- return {
856
- abilitySlug,
857
- projectDir: workspace.projectDir,
858
- };
859
- }
860
- catch (error) {
861
- await rollbackWorkspaceMutation(mutationSnapshot);
862
- throw error;
863
- }
864
24
  }