@wp-typia/project-tools 0.22.2 → 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.
- package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +1 -1
- package/dist/runtime/built-in-block-code-templates/interactivity.js +4 -2
- package/dist/runtime/cli-add-shared.d.ts +49 -0
- package/dist/runtime/cli-add-shared.js +204 -71
- package/dist/runtime/cli-add-workspace-ability-scaffold.d.ts +5 -0
- package/dist/runtime/cli-add-workspace-ability-scaffold.js +392 -0
- package/dist/runtime/cli-add-workspace-ability-templates.d.ts +34 -0
- package/dist/runtime/cli-add-workspace-ability-templates.js +500 -0
- package/dist/runtime/cli-add-workspace-ability-types.d.ts +27 -0
- package/dist/runtime/cli-add-workspace-ability-types.js +14 -0
- package/dist/runtime/cli-add-workspace-ability.js +12 -852
- package/dist/runtime/cli-add-workspace-ai-scaffold.d.ts +21 -0
- package/dist/runtime/cli-add-workspace-ai-scaffold.js +91 -0
- package/dist/runtime/cli-add-workspace-ai-templates.d.ts +4 -0
- package/dist/runtime/cli-add-workspace-ai-templates.js +605 -0
- package/dist/runtime/cli-add-workspace-ai.js +15 -688
- package/dist/runtime/cli-add-workspace-assets.js +7 -4
- package/dist/runtime/cli-add-workspace.js +1 -19
- package/dist/runtime/cli-doctor-workspace-bindings.d.ts +11 -0
- package/dist/runtime/cli-doctor-workspace-bindings.js +134 -0
- package/dist/runtime/cli-doctor-workspace-blocks.d.ts +11 -0
- package/dist/runtime/cli-doctor-workspace-blocks.js +504 -0
- package/dist/runtime/cli-doctor-workspace-features.d.ts +11 -0
- package/dist/runtime/cli-doctor-workspace-features.js +383 -0
- package/dist/runtime/cli-doctor-workspace-package.d.ts +18 -0
- package/dist/runtime/cli-doctor-workspace-package.js +59 -0
- package/dist/runtime/cli-doctor-workspace-shared.d.ts +69 -0
- package/dist/runtime/cli-doctor-workspace-shared.js +87 -0
- package/dist/runtime/cli-doctor-workspace.js +25 -1062
- package/package.json +3 -3
|
@@ -1,788 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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 {
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
}
|