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