@wp-typia/project-tools 0.23.0 → 0.23.1

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 (109) hide show
  1. package/dist/runtime/ai-feature-artifacts.js +4 -1
  2. package/dist/runtime/block-generator-service-spec.js +2 -1
  3. package/dist/runtime/cli-add-block-json.js +5 -1
  4. package/dist/runtime/cli-add-help.js +4 -3
  5. package/dist/runtime/cli-add-types.d.ts +18 -6
  6. package/dist/runtime/cli-add-validation.d.ts +7 -0
  7. package/dist/runtime/cli-add-validation.js +9 -0
  8. package/dist/runtime/cli-add-workspace-ability-scaffold.js +4 -1
  9. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +5 -1
  10. package/dist/runtime/cli-add-workspace-admin-view-templates-core-data.d.ts +34 -0
  11. package/dist/runtime/cli-add-workspace-admin-view-templates-core-data.js +483 -0
  12. package/dist/runtime/cli-add-workspace-admin-view-templates-default.d.ts +30 -0
  13. package/dist/runtime/cli-add-workspace-admin-view-templates-default.js +310 -0
  14. package/dist/runtime/cli-add-workspace-admin-view-templates-rest.d.ts +25 -0
  15. package/dist/runtime/cli-add-workspace-admin-view-templates-rest.js +124 -0
  16. package/dist/runtime/cli-add-workspace-admin-view-templates-settings.d.ts +34 -0
  17. package/dist/runtime/cli-add-workspace-admin-view-templates-settings.js +370 -0
  18. package/dist/runtime/cli-add-workspace-admin-view-templates-shared.d.ts +49 -0
  19. package/dist/runtime/cli-add-workspace-admin-view-templates-shared.js +259 -0
  20. package/dist/runtime/cli-add-workspace-admin-view-templates.d.ts +18 -27
  21. package/dist/runtime/cli-add-workspace-admin-view-templates.js +30 -1326
  22. package/dist/runtime/cli-add-workspace-ai-anchors.js +4 -1
  23. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +17 -1
  24. package/dist/runtime/cli-add-workspace-integration-env.d.ts +3 -1
  25. package/dist/runtime/cli-add-workspace-integration-env.js +42 -5
  26. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +8 -0
  27. package/dist/runtime/cli-add-workspace-rest-anchors.js +41 -0
  28. package/dist/runtime/cli-add-workspace-rest-generated.d.ts +9 -0
  29. package/dist/runtime/cli-add-workspace-rest-generated.js +158 -0
  30. package/dist/runtime/cli-add-workspace-rest-manual.d.ts +8 -0
  31. package/dist/runtime/cli-add-workspace-rest-manual.js +279 -0
  32. package/dist/runtime/cli-add-workspace-rest-php-templates.d.ts +24 -0
  33. package/dist/runtime/cli-add-workspace-rest-php-templates.js +678 -0
  34. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +8 -0
  35. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +25 -4
  36. package/dist/runtime/cli-add-workspace-rest-types.d.ts +108 -0
  37. package/dist/runtime/cli-add-workspace-rest-types.js +1 -0
  38. package/dist/runtime/cli-add-workspace-rest.d.ts +3 -20
  39. package/dist/runtime/cli-add-workspace-rest.js +33 -788
  40. package/dist/runtime/cli-core.d.ts +1 -1
  41. package/dist/runtime/cli-core.js +1 -1
  42. package/dist/runtime/cli-diagnostics.d.ts +3 -1
  43. package/dist/runtime/cli-diagnostics.js +17 -5
  44. package/dist/runtime/cli-doctor-workspace-bindings.js +4 -1
  45. package/dist/runtime/cli-doctor-workspace-block-addons.d.ts +12 -0
  46. package/dist/runtime/cli-doctor-workspace-block-addons.js +134 -0
  47. package/dist/runtime/cli-doctor-workspace-block-iframe.d.ts +9 -0
  48. package/dist/runtime/cli-doctor-workspace-block-iframe.js +228 -0
  49. package/dist/runtime/cli-doctor-workspace-block-metadata.d.ts +11 -0
  50. package/dist/runtime/cli-doctor-workspace-block-metadata.js +111 -0
  51. package/dist/runtime/cli-doctor-workspace-blocks.js +6 -424
  52. package/dist/runtime/cli-doctor-workspace-features-abilities.d.ts +11 -0
  53. package/dist/runtime/cli-doctor-workspace-features-abilities.js +112 -0
  54. package/dist/runtime/cli-doctor-workspace-features-admin-views.d.ts +11 -0
  55. package/dist/runtime/cli-doctor-workspace-features-admin-views.js +128 -0
  56. package/dist/runtime/cli-doctor-workspace-features-ai.d.ts +11 -0
  57. package/dist/runtime/cli-doctor-workspace-features-ai.js +57 -0
  58. package/dist/runtime/cli-doctor-workspace-features-editor-plugins.d.ts +11 -0
  59. package/dist/runtime/cli-doctor-workspace-features-editor-plugins.js +80 -0
  60. package/dist/runtime/cli-doctor-workspace-features-post-meta.d.ts +11 -0
  61. package/dist/runtime/cli-doctor-workspace-features-post-meta.js +77 -0
  62. package/dist/runtime/cli-doctor-workspace-features-rest.d.ts +11 -0
  63. package/dist/runtime/cli-doctor-workspace-features-rest.js +120 -0
  64. package/dist/runtime/cli-doctor-workspace-features.js +14 -487
  65. package/dist/runtime/cli-doctor.d.ts +52 -3
  66. package/dist/runtime/cli-doctor.js +79 -8
  67. package/dist/runtime/cli-help.js +6 -3
  68. package/dist/runtime/cli-init-package-json.js +4 -2
  69. package/dist/runtime/cli-prompt.d.ts +16 -2
  70. package/dist/runtime/cli-prompt.js +29 -12
  71. package/dist/runtime/cli-scaffold.d.ts +2 -1
  72. package/dist/runtime/cli-scaffold.js +19 -10
  73. package/dist/runtime/external-template-guards.js +4 -6
  74. package/dist/runtime/index.d.ts +2 -2
  75. package/dist/runtime/index.js +1 -1
  76. package/dist/runtime/json-utils.d.ts +62 -4
  77. package/dist/runtime/json-utils.js +78 -4
  78. package/dist/runtime/local-dev-presets.js +4 -1
  79. package/dist/runtime/migration-ui-capability.js +4 -1
  80. package/dist/runtime/migration-utils.js +4 -1
  81. package/dist/runtime/package-managers.js +6 -1
  82. package/dist/runtime/package-versions.js +6 -1
  83. package/dist/runtime/scaffold-bootstrap.js +7 -2
  84. package/dist/runtime/scaffold-package-manager-files.js +5 -1
  85. package/dist/runtime/scaffold-repository-reference.js +4 -2
  86. package/dist/runtime/scaffold-template-variables.js +2 -1
  87. package/dist/runtime/scaffold.d.ts +18 -1
  88. package/dist/runtime/scaffold.js +55 -2
  89. package/dist/runtime/temp-roots.js +4 -1
  90. package/dist/runtime/template-layers.js +4 -1
  91. package/dist/runtime/template-registry.js +9 -3
  92. package/dist/runtime/template-source-contracts.d.ts +2 -0
  93. package/dist/runtime/template-source-normalization.js +2 -1
  94. package/dist/runtime/template-source-remote.js +18 -5
  95. package/dist/runtime/template-source-seeds.js +10 -3
  96. package/dist/runtime/workspace-inventory-mutations.js +2 -1
  97. package/dist/runtime/workspace-inventory-parser-entries.d.ts +17 -0
  98. package/dist/runtime/workspace-inventory-parser-entries.js +157 -0
  99. package/dist/runtime/workspace-inventory-parser-validation.d.ts +104 -0
  100. package/dist/runtime/workspace-inventory-parser-validation.js +34 -0
  101. package/dist/runtime/workspace-inventory-parser.d.ts +3 -45
  102. package/dist/runtime/workspace-inventory-parser.js +3 -581
  103. package/dist/runtime/workspace-inventory-section-descriptors.d.ts +19 -0
  104. package/dist/runtime/workspace-inventory-section-descriptors.js +435 -0
  105. package/dist/runtime/workspace-inventory-templates.d.ts +1 -1
  106. package/dist/runtime/workspace-inventory-templates.js +1 -0
  107. package/dist/runtime/workspace-inventory-types.d.ts +1 -0
  108. package/dist/runtime/workspace-project.js +4 -6
  109. package/package.json +2 -2
@@ -0,0 +1,678 @@
1
+ import { quotePhpString } from "./php-utils.js";
2
+ import { toTitleCase } from "./string-case.js";
3
+ function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions, options) {
4
+ const collectionRoutes = [];
5
+ const itemRoutes = [];
6
+ const readPermissionCallback = options.permissionCallback
7
+ ? quotePhpString(options.permissionCallback)
8
+ : quotePhpString("__return_true");
9
+ const writePermissionCallback = options.permissionCallback
10
+ ? quotePhpString(options.permissionCallback)
11
+ : options.controllerVariableName
12
+ ? `array( ${options.controllerVariableName}, 'can_manage_rest_resource' )`
13
+ : quotePhpString(functions.canWriteFunctionName);
14
+ const buildRouteCallback = (functionName, methodName) => options.controllerVariableName
15
+ ? `array( ${options.controllerVariableName}, '${methodName}' )`
16
+ : quotePhpString(functionName);
17
+ if (methods.includes("list")) {
18
+ collectionRoutes.push(`\t\tarray(
19
+ \t\t\t'methods' => WP_REST_Server::READABLE,
20
+ \t\t\t'callback' => ${buildRouteCallback(functions.listHandlerName, "list_items")},
21
+ \t\t\t'permission_callback' => ${readPermissionCallback},
22
+ \t\t)`);
23
+ }
24
+ if (methods.includes("create")) {
25
+ collectionRoutes.push(`\t\tarray(
26
+ \t\t\t'methods' => WP_REST_Server::CREATABLE,
27
+ \t\t\t'callback' => ${buildRouteCallback(functions.createHandlerName, "create_item")},
28
+ \t\t\t'permission_callback' => ${writePermissionCallback},
29
+ \t\t)`);
30
+ }
31
+ if (methods.includes("read")) {
32
+ itemRoutes.push(`\t\tarray(
33
+ \t\t\t'methods' => WP_REST_Server::READABLE,
34
+ \t\t\t'callback' => ${buildRouteCallback(functions.readHandlerName, "read_item")},
35
+ \t\t\t'permission_callback' => ${readPermissionCallback},
36
+ \t\t)`);
37
+ }
38
+ if (methods.includes("update")) {
39
+ itemRoutes.push(`\t\tarray(
40
+ \t\t\t'methods' => WP_REST_Server::EDITABLE,
41
+ \t\t\t'callback' => ${buildRouteCallback(functions.updateHandlerName, "update_item")},
42
+ \t\t\t'permission_callback' => ${writePermissionCallback},
43
+ \t\t)`);
44
+ }
45
+ if (methods.includes("delete")) {
46
+ itemRoutes.push(`\t\tarray(
47
+ \t\t\t'methods' => WP_REST_Server::DELETABLE,
48
+ \t\t\t'callback' => ${buildRouteCallback(functions.deleteHandlerName, "delete_item")},
49
+ \t\t\t'permission_callback' => ${writePermissionCallback},
50
+ \t\t)`);
51
+ }
52
+ const registrations = [];
53
+ if (collectionRoutes.length > 0) {
54
+ registrations.push(`\tregister_rest_route(
55
+ \t\t$namespace,
56
+ \t\t'/${restResourceSlug}',
57
+ \t\tarray(
58
+ ${collectionRoutes.join(",\n")}
59
+ \t\t)
60
+ \t);`);
61
+ }
62
+ if (itemRoutes.length > 0) {
63
+ registrations.push(`\tregister_rest_route(
64
+ \t\t$namespace,
65
+ \t\t${quotePhpString(options.routePattern)},
66
+ \t\tarray(
67
+ ${itemRoutes.join(",\n")}
68
+ \t\t)
69
+ \t);`);
70
+ }
71
+ return registrations.join("\n\n");
72
+ }
73
+ function normalizeGlobalPhpClassName(classReference) {
74
+ const normalized = classReference.startsWith("\\")
75
+ ? classReference.slice(1)
76
+ : classReference;
77
+ return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(normalized) ? normalized : undefined;
78
+ }
79
+ function toPhpClassConstantReference(classReference) {
80
+ const normalized = classReference.startsWith("\\")
81
+ ? classReference
82
+ : `\\${classReference}`;
83
+ return `${normalized}::class`;
84
+ }
85
+ function buildRestResourceControllerClassSource(options) {
86
+ const controllerClassName = normalizeGlobalPhpClassName(options.controllerClass);
87
+ if (!controllerClassName) {
88
+ return "";
89
+ }
90
+ const extendsClause = options.controllerExtends
91
+ ? ` extends ${options.controllerExtends.startsWith("\\") ? options.controllerExtends : `\\${options.controllerExtends}`}`
92
+ : "";
93
+ return `
94
+ if ( ! class_exists( ${quotePhpString(controllerClassName)} ) ) {
95
+ \tclass ${controllerClassName}${extendsClause} {
96
+ \t\tpublic function can_manage_rest_resource() {
97
+ \t\t\treturn ${options.functions.canWriteFunctionName}();
98
+ \t\t}
99
+
100
+ \t\tpublic function list_items( WP_REST_Request $request ) {
101
+ \t\t\treturn ${options.functions.listHandlerName}( $request );
102
+ \t\t}
103
+
104
+ \t\tpublic function read_item( WP_REST_Request $request ) {
105
+ \t\t\treturn ${options.functions.readHandlerName}( $request );
106
+ \t\t}
107
+
108
+ \t\tpublic function create_item( WP_REST_Request $request ) {
109
+ \t\t\treturn ${options.functions.createHandlerName}( $request );
110
+ \t\t}
111
+
112
+ \t\tpublic function update_item( WP_REST_Request $request ) {
113
+ \t\t\treturn ${options.functions.updateHandlerName}( $request );
114
+ \t\t}
115
+
116
+ \t\tpublic function delete_item( WP_REST_Request $request ) {
117
+ \t\t\treturn ${options.functions.deleteHandlerName}( $request );
118
+ \t\t}
119
+ \t}
120
+ }
121
+ `;
122
+ }
123
+ /**
124
+ * Build the PHP route/controller glue for generated workspace REST resources.
125
+ *
126
+ * @param restResourceSlug Normalized REST resource slug.
127
+ * @param namespace WordPress REST namespace, such as `vendor/v1`.
128
+ * @param phpPrefix Plugin PHP function prefix.
129
+ * @param methods REST operations to expose.
130
+ * @param options Optional generated route and controller customizations.
131
+ * @returns A complete PHP source file for the generated REST resource.
132
+ */
133
+ export function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, methods, options) {
134
+ const restResourceTitle = toTitleCase(restResourceSlug);
135
+ const restResourcePhpId = restResourceSlug.replace(/-/g, "_");
136
+ const canWriteFunctionName = `${phpPrefix}_${restResourcePhpId}_can_manage_rest_resource`;
137
+ const getItemsFunctionName = `${phpPrefix}_${restResourcePhpId}_get_rest_resource_items`;
138
+ const validatePayloadFunctionName = `${phpPrefix}_${restResourcePhpId}_validate_rest_resource_payload`;
139
+ const normalizeItemFunctionName = `${phpPrefix}_${restResourcePhpId}_normalize_rest_resource_item`;
140
+ const saveItemsFunctionName = `${phpPrefix}_${restResourcePhpId}_save_rest_resource_items`;
141
+ const getOptionNameFunctionName = `${phpPrefix}_${restResourcePhpId}_get_rest_resource_option_name`;
142
+ const listHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_list_rest_resource`;
143
+ const readHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_read_rest_resource`;
144
+ const createHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_create_rest_resource`;
145
+ const updateHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_update_rest_resource`;
146
+ const deleteHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_delete_rest_resource`;
147
+ const registerRoutesFunctionName = `${phpPrefix}_${restResourcePhpId}_register_rest_routes`;
148
+ const controllerVariableName = options.controllerClass ? "$controller" : undefined;
149
+ const routeRegistrations = buildRestResourceRouteRegistrations(restResourceSlug, methods, {
150
+ canWriteFunctionName,
151
+ createHandlerName,
152
+ deleteHandlerName,
153
+ listHandlerName,
154
+ readHandlerName,
155
+ updateHandlerName,
156
+ }, {
157
+ ...(controllerVariableName ? { controllerVariableName } : {}),
158
+ ...(options.permissionCallback
159
+ ? { permissionCallback: options.permissionCallback }
160
+ : {}),
161
+ routePattern: options.routePattern,
162
+ });
163
+ const controllerClassSource = options.controllerClass
164
+ ? buildRestResourceControllerClassSource({
165
+ controllerClass: options.controllerClass,
166
+ ...(options.controllerExtends
167
+ ? { controllerExtends: options.controllerExtends }
168
+ : {}),
169
+ functions: {
170
+ canWriteFunctionName,
171
+ createHandlerName,
172
+ deleteHandlerName,
173
+ listHandlerName,
174
+ readHandlerName,
175
+ updateHandlerName,
176
+ },
177
+ })
178
+ : "";
179
+ const controllerBootstrapSource = options.controllerClass
180
+ ? `\t\t$controller_class = ${toPhpClassConstantReference(options.controllerClass)};
181
+ \t\tif ( ! class_exists( $controller_class ) ) {
182
+ \t\t\treturn;
183
+ \t\t}
184
+ \t\t$controller = new $controller_class();
185
+
186
+ `
187
+ : "";
188
+ return `<?php
189
+ if ( ! defined( 'ABSPATH' ) ) {
190
+ \treturn;
191
+ }
192
+
193
+ if ( ! function_exists( '${getOptionNameFunctionName}' ) ) {
194
+ \tfunction ${getOptionNameFunctionName}() {
195
+ \t\treturn ${quotePhpString(`${phpPrefix}_${restResourcePhpId}_rest_resource_items`)};
196
+ \t}
197
+ }
198
+
199
+ if ( ! function_exists( '${normalizeItemFunctionName}' ) ) {
200
+ \tfunction ${normalizeItemFunctionName}( array $item ) {
201
+ \t\treturn array(
202
+ \t\t\t'id' => isset( $item['id'] ) ? (int) $item['id'] : 0,
203
+ \t\t\t'title' => isset( $item['title'] ) ? (string) $item['title'] : '',
204
+ \t\t\t'content' => isset( $item['content'] ) ? (string) $item['content'] : '',
205
+ \t\t\t'status' => isset( $item['status'] ) && 'published' === $item['status'] ? 'published' : 'draft',
206
+ \t\t\t'updatedAt' => isset( $item['updatedAt'] ) ? (string) $item['updatedAt'] : gmdate( 'c' ),
207
+ \t\t);
208
+ \t}
209
+ }
210
+
211
+ if ( ! function_exists( '${getItemsFunctionName}' ) ) {
212
+ \tfunction ${getItemsFunctionName}() {
213
+ \t\t$seed_items = array(
214
+ \t\t\tarray(
215
+ \t\t\t\t'id' => 1,
216
+ \t\t\t\t'title' => ${quotePhpString(`${restResourceTitle} Starter`)},
217
+ \t\t\t\t'content' => ${quotePhpString(`Replace this seeded ${restResourceTitle.toLowerCase()} content with your plugin data source.`)},
218
+ \t\t\t\t'status' => 'draft',
219
+ \t\t\t\t'updatedAt' => '2026-01-01T00:00:00Z',
220
+ \t\t\t),
221
+ \t\t);
222
+ \t\t$items = get_option( ${getOptionNameFunctionName}(), $seed_items );
223
+
224
+ \t\tif ( ! is_array( $items ) ) {
225
+ \t\t\t$items = $seed_items;
226
+ \t\t}
227
+
228
+ \t\treturn array_values(
229
+ \t\t\tarray_map(
230
+ \t\t\t\t'${normalizeItemFunctionName}',
231
+ \t\t\t\tarray_filter(
232
+ \t\t\t\t\t$items,
233
+ \t\t\t\t\t'is_array'
234
+ \t\t\t\t)
235
+ \t\t\t)
236
+ \t\t);
237
+ \t}
238
+ }
239
+
240
+ if ( ! function_exists( '${saveItemsFunctionName}' ) ) {
241
+ \tfunction ${saveItemsFunctionName}( array $items ) {
242
+ \t\tupdate_option(
243
+ \t\t\t${getOptionNameFunctionName}(),
244
+ \t\t\tarray_values(
245
+ \t\t\t\tarray_map(
246
+ \t\t\t\t\t'${normalizeItemFunctionName}',
247
+ \t\t\t\t\t$items
248
+ \t\t\t\t)
249
+ \t\t\t),
250
+ \t\t\tfalse
251
+ \t\t);
252
+ \t}
253
+ }
254
+
255
+ if ( ! function_exists( '${validatePayloadFunctionName}' ) ) {
256
+ \tfunction ${validatePayloadFunctionName}( $value, $schema_name, $param_name ) {
257
+ \t\tif ( ! function_exists( '${phpPrefix}_validate_and_sanitize_rest_payload' ) ) {
258
+ \t\t\treturn new WP_Error(
259
+ \t\t\t\t'missing_rest_schema_helper',
260
+ \t\t\t\t'Missing REST schema helper. Ensure inc/rest-schema.php is loaded before REST resources.',
261
+ \t\t\t\tarray( 'status' => 500 )
262
+ \t\t\t);
263
+ \t\t}
264
+
265
+ \t\treturn ${phpPrefix}_validate_and_sanitize_rest_payload(
266
+ \t\t\t$value,
267
+ \t\t\t$schema_name,
268
+ \t\t\t$param_name,
269
+ \t\t\tarray( 'resource' => ${quotePhpString(restResourceSlug)} )
270
+ \t\t);
271
+ \t}
272
+ }
273
+
274
+ if ( ! function_exists( '${canWriteFunctionName}' ) ) {
275
+ \tfunction ${canWriteFunctionName}() {
276
+ \t\treturn current_user_can( 'edit_posts' );
277
+ \t}
278
+ }
279
+
280
+ if ( ! function_exists( '${listHandlerName}' ) ) {
281
+ \tfunction ${listHandlerName}( WP_REST_Request $request ) {
282
+ \t\t$payload_input = array();
283
+ \t\t$page = $request->get_param( 'page' );
284
+ \t\t$per_page = $request->get_param( 'perPage' );
285
+ \t\t$search = $request->get_param( 'search' );
286
+
287
+ \t\tif ( null !== $page ) {
288
+ \t\t\t$payload_input['page'] = $page;
289
+ \t\t}
290
+ \t\tif ( null !== $per_page ) {
291
+ \t\t\t$payload_input['perPage'] = $per_page;
292
+ \t\t}
293
+ \t\tif ( null !== $search ) {
294
+ \t\t\t$payload_input['search'] = $search;
295
+ \t\t}
296
+
297
+ \t\t$payload = ${validatePayloadFunctionName}(
298
+ \t\t\t$payload_input,
299
+ \t\t\t'list-query',
300
+ \t\t\t'query'
301
+ \t\t);
302
+
303
+ \t\tif ( is_wp_error( $payload ) ) {
304
+ \t\t\treturn $payload;
305
+ \t\t}
306
+
307
+ \t\t$page = isset( $payload['page'] ) ? max( 1, (int) $payload['page'] ) : 1;
308
+ \t\t$per_page = isset( $payload['perPage'] ) ? min( 50, max( 1, (int) $payload['perPage'] ) ) : 10;
309
+ \t\t$search = isset( $payload['search'] ) ? strtolower( (string) $payload['search'] ) : '';
310
+ \t\t$items = ${getItemsFunctionName}();
311
+
312
+ \t\tif ( '' !== $search ) {
313
+ \t\t\t$items = array_values(
314
+ \t\t\t\tarray_filter(
315
+ \t\t\t\t\t$items,
316
+ \t\t\t\t\tstatic function ( $item ) use ( $search ) {
317
+ \t\t\t\t\t\treturn false !== strpos( strtolower( (string) ( $item['title'] ?? '' ) ), $search ) ||
318
+ \t\t\t\t\t\t\tfalse !== strpos( strtolower( (string) ( $item['content'] ?? '' ) ), $search );
319
+ \t\t\t\t\t}
320
+ \t\t\t\t)
321
+ \t\t\t);
322
+ \t\t}
323
+
324
+ \t\t$total = count( $items );
325
+ \t\t$items = array_slice( $items, ( $page - 1 ) * $per_page, $per_page );
326
+
327
+ \t\treturn rest_ensure_response(
328
+ \t\t\tarray(
329
+ \t\t\t\t'items' => $items,
330
+ \t\t\t\t'page' => $page,
331
+ \t\t\t\t'perPage' => $per_page,
332
+ \t\t\t\t'total' => $total,
333
+ \t\t\t)
334
+ \t\t);
335
+ \t}
336
+ }
337
+
338
+ if ( ! function_exists( '${readHandlerName}' ) ) {
339
+ \tfunction ${readHandlerName}( WP_REST_Request $request ) {
340
+ \t\t$payload = ${validatePayloadFunctionName}(
341
+ \t\t\tarray(
342
+ \t\t\t\t'id' => $request->get_param( 'id' ),
343
+ \t\t\t),
344
+ \t\t\t'read-query',
345
+ \t\t\t'query'
346
+ \t\t);
347
+
348
+ \t\tif ( is_wp_error( $payload ) ) {
349
+ \t\t\treturn $payload;
350
+ \t\t}
351
+
352
+ \t\tforeach ( ${getItemsFunctionName}() as $item ) {
353
+ \t\t\tif ( (int) $item['id'] === (int) $payload['id'] ) {
354
+ \t\t\t\treturn rest_ensure_response( $item );
355
+ \t\t\t}
356
+ \t\t}
357
+
358
+ \t\treturn new WP_Error( 'rest_not_found', 'Resource not found.', array( 'status' => 404 ) );
359
+ \t}
360
+ }
361
+
362
+ if ( ! function_exists( '${createHandlerName}' ) ) {
363
+ \tfunction ${createHandlerName}( WP_REST_Request $request ) {
364
+ \t\t$payload = ${validatePayloadFunctionName}( $request->get_json_params(), 'create-request', 'body' );
365
+ \t\tif ( is_wp_error( $payload ) ) {
366
+ \t\t\treturn $payload;
367
+ \t\t}
368
+
369
+ \t\t$items = ${getItemsFunctionName}();
370
+ \t\t$next_id = 1;
371
+ \t\tforeach ( $items as $item ) {
372
+ \t\t\t$next_id = max( $next_id, (int) $item['id'] + 1 );
373
+ \t\t}
374
+
375
+ \t\t$record = ${normalizeItemFunctionName}(
376
+ \t\t\tarray(
377
+ \t\t\t\t'id' => $next_id,
378
+ \t\t\t\t'title' => (string) $payload['title'],
379
+ \t\t\t\t'content' => isset( $payload['content'] ) ? (string) $payload['content'] : '',
380
+ \t\t\t\t'status' => isset( $payload['status'] ) ? (string) $payload['status'] : 'draft',
381
+ \t\t\t\t'updatedAt' => gmdate( 'c' ),
382
+ \t\t\t)
383
+ \t\t);
384
+
385
+ \t\t$items[] = $record;
386
+ \t\t${saveItemsFunctionName}( $items );
387
+
388
+ \t\treturn rest_ensure_response( $record );
389
+ \t}
390
+ }
391
+
392
+ if ( ! function_exists( '${updateHandlerName}' ) ) {
393
+ \tfunction ${updateHandlerName}( WP_REST_Request $request ) {
394
+ \t\t$query = ${validatePayloadFunctionName}(
395
+ \t\t\tarray(
396
+ \t\t\t\t'id' => $request->get_param( 'id' ),
397
+ \t\t\t),
398
+ \t\t\t'update-query',
399
+ \t\t\t'query'
400
+ \t\t);
401
+ \t\tif ( is_wp_error( $query ) ) {
402
+ \t\t\treturn $query;
403
+ \t\t}
404
+
405
+ \t\t$payload = ${validatePayloadFunctionName}( $request->get_json_params(), 'update-request', 'body' );
406
+ \t\tif ( is_wp_error( $payload ) ) {
407
+ \t\t\treturn $payload;
408
+ \t\t}
409
+
410
+ \t\t$items = ${getItemsFunctionName}();
411
+ \t\tforeach ( $items as $index => $item ) {
412
+ \t\t\tif ( (int) $item['id'] !== (int) $query['id'] ) {
413
+ \t\t\t\tcontinue;
414
+ \t\t\t}
415
+
416
+ \t\t\t$items[ $index ] = ${normalizeItemFunctionName}(
417
+ \t\t\t\tarray(
418
+ \t\t\t\t\t'id' => $item['id'],
419
+ \t\t\t\t\t'title' => isset( $payload['title'] ) ? (string) $payload['title'] : (string) $item['title'],
420
+ \t\t\t\t\t'content' => array_key_exists( 'content', $payload ) ? (string) $payload['content'] : (string) $item['content'],
421
+ \t\t\t\t\t'status' => isset( $payload['status'] ) ? (string) $payload['status'] : (string) $item['status'],
422
+ \t\t\t\t\t'updatedAt' => gmdate( 'c' ),
423
+ \t\t\t\t)
424
+ \t\t\t);
425
+
426
+ \t\t\t${saveItemsFunctionName}( $items );
427
+ \t\t\treturn rest_ensure_response( $items[ $index ] );
428
+ \t\t}
429
+
430
+ \t\treturn new WP_Error( 'rest_not_found', 'Resource not found.', array( 'status' => 404 ) );
431
+ \t}
432
+ }
433
+
434
+ if ( ! function_exists( '${deleteHandlerName}' ) ) {
435
+ \tfunction ${deleteHandlerName}( WP_REST_Request $request ) {
436
+ \t\t$query = ${validatePayloadFunctionName}(
437
+ \t\t\tarray(
438
+ \t\t\t\t'id' => $request->get_param( 'id' ),
439
+ \t\t\t),
440
+ \t\t\t'delete-query',
441
+ \t\t\t'query'
442
+ \t\t);
443
+ \t\tif ( is_wp_error( $query ) ) {
444
+ \t\t\treturn $query;
445
+ \t\t}
446
+
447
+ \t\t$items = ${getItemsFunctionName}();
448
+ \t\t$filtered = array_values(
449
+ \t\t\tarray_filter(
450
+ \t\t\t\t$items,
451
+ \t\t\t\tstatic function ( $item ) use ( $query ) {
452
+ \t\t\t\t\treturn (int) $item['id'] !== (int) $query['id'];
453
+ \t\t\t\t}
454
+ \t\t\t)
455
+ \t\t);
456
+ \t\t$was_deleted = count( $filtered ) !== count( $items );
457
+
458
+ \t\tif ( ! $was_deleted ) {
459
+ \t\t\treturn new WP_Error( 'rest_not_found', 'Resource not found.', array( 'status' => 404 ) );
460
+ \t\t}
461
+
462
+ \t\t${saveItemsFunctionName}( $filtered );
463
+
464
+ \t\treturn rest_ensure_response(
465
+ \t\t\tarray(
466
+ \t\t\t\t'deleted' => true,
467
+ \t\t\t\t'id' => (int) $query['id'],
468
+ \t\t\t)
469
+ \t\t);
470
+ \t}
471
+ }
472
+
473
+ ${controllerClassSource}
474
+ if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
475
+ \tfunction ${registerRoutesFunctionName}() {
476
+ \t\t$namespace = ${quotePhpString(namespace)};
477
+
478
+ ${controllerBootstrapSource}
479
+ ${routeRegistrations}
480
+ \t}
481
+ }
482
+
483
+ add_action( 'rest_api_init', '${registerRoutesFunctionName}' );
484
+ `;
485
+ }
486
+ /**
487
+ * Build the shared PHP helper loaded by workspace bootstraps for generated REST schemas.
488
+ *
489
+ * @param phpPrefix Plugin-scoped PHP function prefix.
490
+ * @returns PHP source for `inc/rest-schema.php`.
491
+ */
492
+ export function buildWorkspaceRestSchemaHelperPhpSource(phpPrefix) {
493
+ return `<?php
494
+ if ( ! defined( 'ABSPATH' ) ) {
495
+ \treturn;
496
+ }
497
+
498
+ if ( ! function_exists( '${phpPrefix}_is_valid_rest_schema_key' ) ) {
499
+ \tfunction ${phpPrefix}_is_valid_rest_schema_key( $value ) {
500
+ \t\treturn is_string( $value ) && 1 === preg_match( '/\\A[A-Za-z0-9_-]+\\z/', $value );
501
+ \t}
502
+ }
503
+
504
+ if ( ! function_exists( '${phpPrefix}_is_valid_rest_resource_slug' ) ) {
505
+ \tfunction ${phpPrefix}_is_valid_rest_resource_slug( $value ) {
506
+ \t\treturn is_string( $value ) && 1 === preg_match( '/\\A[a-z0-9]+(?:-[a-z0-9]+)*\\z/', $value );
507
+ \t}
508
+ }
509
+
510
+ if ( ! function_exists( '${phpPrefix}_resolve_rest_schema_paths' ) ) {
511
+ \tfunction ${phpPrefix}_resolve_rest_schema_paths( $schema_name, $options = array() ) {
512
+ \t\tif ( ! ${phpPrefix}_is_valid_rest_schema_key( $schema_name ) ) {
513
+ \t\t\treturn new WP_Error(
514
+ \t\t\t\t'invalid_rest_schema_name',
515
+ \t\t\t\t'Invalid REST schema name.',
516
+ \t\t\t\tarray( 'status' => 500 )
517
+ \t\t\t);
518
+ \t\t}
519
+
520
+ \t\t$options = is_array( $options ) ? $options : array();
521
+ \t\t$project_root = dirname( __DIR__ );
522
+ \t\t$paths = array();
523
+
524
+ \t\tif ( isset( $options['resource'] ) && '' !== $options['resource'] ) {
525
+ \t\t\tif ( ! ${phpPrefix}_is_valid_rest_resource_slug( $options['resource'] ) ) {
526
+ \t\t\t\treturn new WP_Error(
527
+ \t\t\t\t\t'invalid_rest_schema_resource',
528
+ \t\t\t\t\t'Invalid REST schema resource slug.',
529
+ \t\t\t\t\tarray( 'status' => 500 )
530
+ \t\t\t\t);
531
+ \t\t\t}
532
+
533
+ \t\t\t$resource_slug = $options['resource'];
534
+ \t\t\t$paths[] = __DIR__ . '/rest-schemas/rest/' . $resource_slug . '/' . $schema_name . '.schema.json';
535
+ \t\t\t$paths[] = $project_root . '/src/rest/' . $resource_slug . '/api-schemas/' . $schema_name . '.schema.json';
536
+ \t\t}
537
+
538
+ \t\tif ( isset( $options['paths'] ) && is_array( $options['paths'] ) ) {
539
+ \t\t\tforeach ( $options['paths'] as $schema_path ) {
540
+ \t\t\t\tif ( is_string( $schema_path ) && '' !== $schema_path && ! in_array( $schema_path, $paths, true ) ) {
541
+ \t\t\t\t\t$paths[] = $schema_path;
542
+ \t\t\t\t}
543
+ \t\t\t}
544
+ \t\t}
545
+
546
+ \t\treturn $paths;
547
+ \t}
548
+ }
549
+
550
+ if ( ! function_exists( '${phpPrefix}_load_rest_schema' ) ) {
551
+ \tfunction ${phpPrefix}_load_rest_schema( $schema_name, $options = array() ) {
552
+ \t\t$schema_paths = ${phpPrefix}_resolve_rest_schema_paths( $schema_name, $options );
553
+ \t\tif ( is_wp_error( $schema_paths ) ) {
554
+ \t\t\treturn $schema_paths;
555
+ \t\t}
556
+
557
+ \t\tforeach ( $schema_paths as $schema_path ) {
558
+ \t\t\tif ( ! is_file( $schema_path ) ) {
559
+ \t\t\t\tcontinue;
560
+ \t\t\t}
561
+
562
+ \t\t\tif ( ! is_readable( $schema_path ) ) {
563
+ \t\t\t\treturn new WP_Error(
564
+ \t\t\t\t\t'unreadable_rest_schema',
565
+ \t\t\t\t\t'Generated REST schema is not readable.',
566
+ \t\t\t\t\tarray(
567
+ \t\t\t\t\t\t'path' => $schema_path,
568
+ \t\t\t\t\t\t'status' => 500,
569
+ \t\t\t\t\t)
570
+ \t\t\t\t);
571
+ \t\t\t}
572
+
573
+ \t\t\t$schema_json = file_get_contents( $schema_path );
574
+ \t\t\tif ( false === $schema_json ) {
575
+ \t\t\t\treturn new WP_Error(
576
+ \t\t\t\t\t'rest_schema_read_failed',
577
+ \t\t\t\t\t'Generated REST schema could not be read.',
578
+ \t\t\t\t\tarray(
579
+ \t\t\t\t\t\t'path' => $schema_path,
580
+ \t\t\t\t\t\t'status' => 500,
581
+ \t\t\t\t\t)
582
+ \t\t\t\t);
583
+ \t\t\t}
584
+
585
+ \t\t\t$decoded = json_decode( $schema_json, true );
586
+ \t\t\tif ( ! is_array( $decoded ) ) {
587
+ \t\t\t\treturn new WP_Error(
588
+ \t\t\t\t\t'malformed_rest_schema',
589
+ \t\t\t\t\t'Generated REST schema contains malformed JSON.',
590
+ \t\t\t\t\tarray(
591
+ \t\t\t\t\t\t'json_error' => json_last_error_msg(),
592
+ \t\t\t\t\t\t'path' => $schema_path,
593
+ \t\t\t\t\t\t'status' => 500,
594
+ \t\t\t\t\t)
595
+ \t\t\t\t);
596
+ \t\t\t}
597
+
598
+ \t\t\treturn $decoded;
599
+ \t\t}
600
+
601
+ \t\treturn new WP_Error(
602
+ \t\t\t'missing_rest_schema',
603
+ \t\t\t'Generated REST schema could not be found.',
604
+ \t\t\tarray(
605
+ \t\t\t\t'paths' => $schema_paths,
606
+ \t\t\t\t'schema' => $schema_name,
607
+ \t\t\t\t'status' => 500,
608
+ \t\t\t)
609
+ \t\t);
610
+ \t}
611
+ }
612
+
613
+ if ( ! function_exists( '${phpPrefix}_prepare_rest_schema_for_wordpress' ) ) {
614
+ \tfunction ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema ) {
615
+ \t\tif ( ! is_array( $schema ) ) {
616
+ \t\t\treturn $schema;
617
+ \t\t}
618
+
619
+ \t\tunset( $schema['$schema'], $schema['title'] );
620
+
621
+ \t\tforeach ( array( 'properties', 'patternProperties', 'definitions', '$defs' ) as $schema_map_key ) {
622
+ \t\t\tif ( ! isset( $schema[ $schema_map_key ] ) || ! is_array( $schema[ $schema_map_key ] ) ) {
623
+ \t\t\t\tcontinue;
624
+ \t\t\t}
625
+
626
+ \t\t\tforeach ( $schema[ $schema_map_key ] as $key => $property_schema ) {
627
+ \t\t\t\t$schema[ $schema_map_key ][ $key ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $property_schema );
628
+ \t\t\t}
629
+ \t\t}
630
+
631
+ \t\tforeach ( array( 'items', 'additionalProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else' ) as $nested_schema_key ) {
632
+ \t\t\tif ( isset( $schema[ $nested_schema_key ] ) && is_array( $schema[ $nested_schema_key ] ) ) {
633
+ \t\t\t\t$schema[ $nested_schema_key ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema[ $nested_schema_key ] );
634
+ \t\t\t}
635
+ \t\t}
636
+
637
+ \t\tforeach ( array( 'allOf', 'anyOf', 'oneOf' ) as $schema_list_key ) {
638
+ \t\t\tif ( ! isset( $schema[ $schema_list_key ] ) || ! is_array( $schema[ $schema_list_key ] ) ) {
639
+ \t\t\t\tcontinue;
640
+ \t\t\t}
641
+
642
+ \t\t\tforeach ( $schema[ $schema_list_key ] as $index => $variant_schema ) {
643
+ \t\t\t\t$schema[ $schema_list_key ][ $index ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $variant_schema );
644
+ \t\t\t}
645
+ \t\t}
646
+
647
+ \t\treturn $schema;
648
+ \t}
649
+ }
650
+
651
+ if ( ! function_exists( '${phpPrefix}_get_wordpress_rest_schema' ) ) {
652
+ \tfunction ${phpPrefix}_get_wordpress_rest_schema( $schema_name, $options = array() ) {
653
+ \t\t$schema = ${phpPrefix}_load_rest_schema( $schema_name, $options );
654
+ \t\tif ( is_wp_error( $schema ) ) {
655
+ \t\t\treturn $schema;
656
+ \t\t}
657
+
658
+ \t\treturn ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema );
659
+ \t}
660
+ }
661
+
662
+ if ( ! function_exists( '${phpPrefix}_validate_and_sanitize_rest_payload' ) ) {
663
+ \tfunction ${phpPrefix}_validate_and_sanitize_rest_payload( $value, $schema_name, $param_name, $options = array() ) {
664
+ \t\t$rest_schema = ${phpPrefix}_get_wordpress_rest_schema( $schema_name, $options );
665
+ \t\tif ( is_wp_error( $rest_schema ) ) {
666
+ \t\t\treturn $rest_schema;
667
+ \t\t}
668
+
669
+ \t\t$validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
670
+ \t\tif ( is_wp_error( $validation ) ) {
671
+ \t\t\treturn $validation;
672
+ \t\t}
673
+
674
+ \t\treturn rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
675
+ \t}
676
+ }
677
+ `;
678
+ }