@wp-typia/project-tools 0.22.9 → 0.23.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.
Files changed (90) hide show
  1. package/dist/runtime/ai-artifacts.js +3 -4
  2. package/dist/runtime/ai-feature-artifacts.js +2 -4
  3. package/dist/runtime/cli-add-collision.d.ts +25 -0
  4. package/dist/runtime/cli-add-collision.js +76 -0
  5. package/dist/runtime/cli-add-filesystem.js +2 -15
  6. package/dist/runtime/cli-add-help.js +11 -2
  7. package/dist/runtime/cli-add-kind-ids.d.ts +1 -1
  8. package/dist/runtime/cli-add-kind-ids.js +3 -0
  9. package/dist/runtime/cli-add-types.d.ts +117 -0
  10. package/dist/runtime/cli-add-types.js +29 -1
  11. package/dist/runtime/cli-add-validation.d.ts +90 -1
  12. package/dist/runtime/cli-add-validation.js +304 -1
  13. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +74 -19
  14. package/dist/runtime/cli-add-workspace-admin-view-source.js +11 -2
  15. package/dist/runtime/cli-add-workspace-admin-view-templates.d.ts +20 -2
  16. package/dist/runtime/cli-add-workspace-admin-view-templates.js +359 -3
  17. package/dist/runtime/cli-add-workspace-admin-view-types.d.ts +21 -0
  18. package/dist/runtime/cli-add-workspace-admin-view-types.js +22 -0
  19. package/dist/runtime/cli-add-workspace-ai-anchors.js +121 -31
  20. package/dist/runtime/cli-add-workspace-contract-source-emitters.d.ts +15 -0
  21. package/dist/runtime/cli-add-workspace-contract-source-emitters.js +42 -0
  22. package/dist/runtime/cli-add-workspace-contract.d.ts +15 -0
  23. package/dist/runtime/cli-add-workspace-contract.js +65 -0
  24. package/dist/runtime/cli-add-workspace-integration-env.d.ts +24 -0
  25. package/dist/runtime/cli-add-workspace-integration-env.js +391 -0
  26. package/dist/runtime/cli-add-workspace-post-meta-anchors.d.ts +23 -0
  27. package/dist/runtime/cli-add-workspace-post-meta-anchors.js +244 -0
  28. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.d.ts +63 -0
  29. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.js +179 -0
  30. package/dist/runtime/cli-add-workspace-post-meta.d.ts +15 -0
  31. package/dist/runtime/cli-add-workspace-post-meta.js +107 -0
  32. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +1 -0
  33. package/dist/runtime/cli-add-workspace-rest-anchors.js +285 -21
  34. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +90 -2
  35. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +302 -29
  36. package/dist/runtime/cli-add-workspace-rest.d.ts +15 -2
  37. package/dist/runtime/cli-add-workspace-rest.js +329 -21
  38. package/dist/runtime/cli-add-workspace.d.ts +15 -0
  39. package/dist/runtime/cli-add-workspace.js +15 -0
  40. package/dist/runtime/cli-add.d.ts +1 -1
  41. package/dist/runtime/cli-add.js +1 -1
  42. package/dist/runtime/cli-core.d.ts +2 -1
  43. package/dist/runtime/cli-core.js +2 -1
  44. package/dist/runtime/cli-doctor-environment.js +1 -3
  45. package/dist/runtime/cli-doctor-workspace-features.js +128 -10
  46. package/dist/runtime/cli-doctor-workspace-package.d.ts +25 -3
  47. package/dist/runtime/cli-doctor-workspace-package.js +35 -13
  48. package/dist/runtime/cli-doctor-workspace-shared.d.ts +2 -0
  49. package/dist/runtime/cli-doctor-workspace-shared.js +5 -0
  50. package/dist/runtime/cli-doctor-workspace.d.ts +1 -1
  51. package/dist/runtime/cli-doctor-workspace.js +16 -8
  52. package/dist/runtime/cli-doctor.js +1 -1
  53. package/dist/runtime/cli-help.js +7 -0
  54. package/dist/runtime/cli-init-templates.js +11 -1
  55. package/dist/runtime/contract-artifacts.d.ts +14 -0
  56. package/dist/runtime/contract-artifacts.js +15 -0
  57. package/dist/runtime/fs-async.d.ts +7 -0
  58. package/dist/runtime/fs-async.js +11 -2
  59. package/dist/runtime/index.d.ts +1 -1
  60. package/dist/runtime/index.js +1 -1
  61. package/dist/runtime/migration-maintenance-verify.js +3 -0
  62. package/dist/runtime/migration-render-generated.js +4 -0
  63. package/dist/runtime/package-versions.js +3 -7
  64. package/dist/runtime/rest-resource-artifacts.d.ts +57 -1
  65. package/dist/runtime/rest-resource-artifacts.js +97 -1
  66. package/dist/runtime/scaffold-repository-reference.js +3 -7
  67. package/dist/runtime/template-render.d.ts +1 -1
  68. package/dist/runtime/template-render.js +1 -1
  69. package/dist/runtime/template-source-cache-markers.d.ts +37 -0
  70. package/dist/runtime/template-source-cache-markers.js +125 -0
  71. package/dist/runtime/template-source-cache.d.ts +1 -4
  72. package/dist/runtime/template-source-cache.js +16 -122
  73. package/dist/runtime/template-source-external.d.ts +4 -2
  74. package/dist/runtime/template-source-external.js +4 -2
  75. package/dist/runtime/template-source-remote.d.ts +8 -4
  76. package/dist/runtime/template-source-remote.js +8 -4
  77. package/dist/runtime/typia-llm.js +3 -4
  78. package/dist/runtime/workspace-inventory-mutations.d.ts +24 -0
  79. package/dist/runtime/workspace-inventory-mutations.js +181 -0
  80. package/dist/runtime/workspace-inventory-parser.d.ts +53 -0
  81. package/dist/runtime/workspace-inventory-parser.js +632 -0
  82. package/dist/runtime/workspace-inventory-read.d.ts +51 -0
  83. package/dist/runtime/workspace-inventory-read.js +98 -0
  84. package/dist/runtime/workspace-inventory-templates.d.ts +50 -0
  85. package/dist/runtime/workspace-inventory-templates.js +268 -0
  86. package/dist/runtime/workspace-inventory-types.d.ts +220 -0
  87. package/dist/runtime/workspace-inventory-types.js +1 -0
  88. package/dist/runtime/workspace-inventory.d.ts +5 -252
  89. package/dist/runtime/workspace-inventory.js +4 -928
  90. package/package.json +2 -2
@@ -1,11 +1,101 @@
1
1
  import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
2
2
  import { toSnakeCase, } from "./string-case.js";
3
- import { ADD_BLOCK_TEMPLATE_IDS, EDITOR_PLUGIN_SLOT_IDS, REST_RESOURCE_METHOD_IDS, resolveEditorPluginSlotAlias, } from "./cli-add-types.js";
3
+ import { ADD_BLOCK_TEMPLATE_IDS, EDITOR_PLUGIN_SLOT_IDS, INTEGRATION_ENV_SERVICE_IDS, MANUAL_REST_CONTRACT_AUTH_IDS, MANUAL_REST_CONTRACT_HTTP_METHOD_IDS, REST_RESOURCE_METHOD_IDS, resolveEditorPluginSlotAlias, } from "./cli-add-types.js";
4
4
  const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
5
+ const WORDPRESS_POST_TYPE_PATTERN = /^[a-z0-9_][a-z0-9_-]*$/u;
6
+ const TYPESCRIPT_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
7
+ const TYPESCRIPT_RESERVED_IDENTIFIERS = new Set([
8
+ "abstract",
9
+ "any",
10
+ "as",
11
+ "asserts",
12
+ "async",
13
+ "await",
14
+ "bigint",
15
+ "boolean",
16
+ "break",
17
+ "case",
18
+ "catch",
19
+ "class",
20
+ "const",
21
+ "constructor",
22
+ "continue",
23
+ "debugger",
24
+ "declare",
25
+ "default",
26
+ "delete",
27
+ "do",
28
+ "else",
29
+ "enum",
30
+ "export",
31
+ "extends",
32
+ "false",
33
+ "finally",
34
+ "for",
35
+ "from",
36
+ "function",
37
+ "get",
38
+ "global",
39
+ "if",
40
+ "implements",
41
+ "import",
42
+ "in",
43
+ "infer",
44
+ "instanceof",
45
+ "interface",
46
+ "intrinsic",
47
+ "is",
48
+ "keyof",
49
+ "let",
50
+ "module",
51
+ "namespace",
52
+ "never",
53
+ "new",
54
+ "null",
55
+ "number",
56
+ "object",
57
+ "of",
58
+ "out",
59
+ "override",
60
+ "package",
61
+ "private",
62
+ "protected",
63
+ "public",
64
+ "readonly",
65
+ "require",
66
+ "return",
67
+ "satisfies",
68
+ "set",
69
+ "static",
70
+ "string",
71
+ "super",
72
+ "switch",
73
+ "symbol",
74
+ "this",
75
+ "throw",
76
+ "true",
77
+ "try",
78
+ "type",
79
+ "typeof",
80
+ "undefined",
81
+ "unique",
82
+ "unknown",
83
+ "using",
84
+ "var",
85
+ "void",
86
+ "while",
87
+ "with",
88
+ "yield",
89
+ ]);
5
90
  /**
6
91
  * Namespace format accepted by plugin-level REST resources.
7
92
  */
8
93
  export const REST_RESOURCE_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*(?:\/[a-z0-9-]+)+$/u;
94
+ const PHP_IDENTIFIER_PATTERN = "[A-Za-z_][A-Za-z0-9_]*";
95
+ const PHP_QUALIFIED_NAME_PATTERN = new RegExp(`^\\\\?${PHP_IDENTIFIER_PATTERN}(?:\\\\${PHP_IDENTIFIER_PATTERN})*$`, "u");
96
+ const PHP_CALLBACK_REFERENCE_PATTERN = new RegExp(`^\\\\?${PHP_IDENTIFIER_PATTERN}(?:\\\\${PHP_IDENTIFIER_PATTERN})*(?:::${PHP_IDENTIFIER_PATTERN})?$`, "u");
97
+ const REST_ROUTE_NAMED_CAPTURE_PATTERN = /\(\?P<([A-Za-z_][A-Za-z0-9_]*)>/gu;
98
+ const REST_ROUTE_UNSUPPORTED_CAPTURE_PATTERN = /\((?!\?(?:P<[A-Za-z_][A-Za-z0-9_]*>|:))/u;
9
99
  /**
10
100
  * Validate a normalized workspace-generated slug.
11
101
  *
@@ -24,6 +114,28 @@ export function assertValidGeneratedSlug(label, slug, usage) {
24
114
  }
25
115
  return slug;
26
116
  }
117
+ /**
118
+ * Validate a source type name used by generated schema artifact workflows.
119
+ *
120
+ * @param label Human-readable field label used in error messages.
121
+ * @param value TypeScript identifier candidate from CLI input or defaults.
122
+ * @param usage CLI usage hint shown when the identifier is empty.
123
+ * @returns The trimmed, validated TypeScript identifier.
124
+ * @throws {Error} When the value is empty or not a TypeScript identifier.
125
+ */
126
+ export function assertValidTypeScriptIdentifier(label, value, usage) {
127
+ const trimmed = value.trim();
128
+ if (!trimmed) {
129
+ throw new Error(`${label} is required. Use \`${usage}\`.`);
130
+ }
131
+ if (!TYPESCRIPT_IDENTIFIER_PATTERN.test(trimmed)) {
132
+ throw new Error(`${label} must be a valid TypeScript identifier, such as ExternalRetrieveResponse.`);
133
+ }
134
+ if (TYPESCRIPT_RESERVED_IDENTIFIERS.has(trimmed)) {
135
+ throw new Error(`${label} must not be a reserved TypeScript keyword, such as ${trimmed}.`);
136
+ }
137
+ return trimmed;
138
+ }
27
139
  /**
28
140
  * Validate a REST resource namespace.
29
141
  *
@@ -52,6 +164,44 @@ export function assertValidRestResourceNamespace(namespace) {
52
164
  export function resolveRestResourceNamespace(workspaceNamespace, namespace) {
53
165
  return assertValidRestResourceNamespace(namespace ?? `${workspaceNamespace}/v1`);
54
166
  }
167
+ /**
168
+ * Validate a WordPress post type key used as post-meta scope.
169
+ *
170
+ * @param postType Raw post type key from CLI input.
171
+ * @returns The trimmed post type key.
172
+ * @throws {Error} When the key is empty or contains unsupported characters.
173
+ */
174
+ export function assertValidPostMetaPostType(postType) {
175
+ const trimmed = postType.trim();
176
+ if (!trimmed) {
177
+ throw new Error("`wp-typia add post-meta` requires --post-type <post-type>.");
178
+ }
179
+ if (trimmed.length > 20) {
180
+ throw new Error("Post meta post type must be 20 characters or fewer.");
181
+ }
182
+ if (!WORDPRESS_POST_TYPE_PATTERN.test(trimmed)) {
183
+ throw new Error("Post meta post type must use a WordPress post type key such as `post` or `example_post_type`.");
184
+ }
185
+ return trimmed;
186
+ }
187
+ /**
188
+ * Resolve a post-meta key from explicit input or the workspace slug default.
189
+ *
190
+ * @param options Optional explicit meta key plus workspace prefix and slug.
191
+ * @returns A validated post-meta key.
192
+ * @throws {Error} When the key is empty or contains whitespace/control characters.
193
+ */
194
+ export function resolvePostMetaKey({ metaKey, phpPrefix, slug, }) {
195
+ const resolvedMetaKey = metaKey ?? `_${toSnakeCase(`${phpPrefix}_${slug}`)}`;
196
+ const trimmed = resolvedMetaKey.trim();
197
+ if (!trimmed) {
198
+ throw new Error("Post meta key cannot be empty.");
199
+ }
200
+ if (/[\p{Cc}\s]/u.test(trimmed)) {
201
+ throw new Error("Post meta key must not contain whitespace or control characters.");
202
+ }
203
+ return trimmed;
204
+ }
55
205
  /**
56
206
  * Parse and validate REST resource method ids from a comma-separated list.
57
207
  *
@@ -73,6 +223,145 @@ export function assertValidRestResourceMethods(methods) {
73
223
  }
74
224
  return normalizedMethods;
75
225
  }
226
+ /**
227
+ * Validate a PHP callable reference supplied to generated REST route metadata.
228
+ *
229
+ * @param label Human-readable field label for errors.
230
+ * @param callback Optional PHP function or `ClassName::method` callback string.
231
+ * @returns The trimmed callback when present.
232
+ * @throws {Error} When the callback cannot be emitted as a safe PHP callable string.
233
+ */
234
+ export function resolveOptionalPhpCallbackReference(label, callback) {
235
+ if (callback == null) {
236
+ return undefined;
237
+ }
238
+ const trimmed = callback.trim();
239
+ if (!trimmed) {
240
+ throw new Error(`${label} cannot be empty.`);
241
+ }
242
+ if (!PHP_CALLBACK_REFERENCE_PATTERN.test(trimmed)) {
243
+ throw new Error(`${label} must be a PHP function reference or ClassName::method callback.`);
244
+ }
245
+ return trimmed;
246
+ }
247
+ /**
248
+ * Validate a PHP class reference used by generated REST controller wrappers.
249
+ *
250
+ * @param label Human-readable field label for errors.
251
+ * @param classReference Optional PHP class name.
252
+ * @returns The trimmed class reference when present.
253
+ * @throws {Error} When the class reference cannot be emitted safely.
254
+ */
255
+ export function resolveOptionalPhpClassReference(label, classReference) {
256
+ if (classReference == null) {
257
+ return undefined;
258
+ }
259
+ const trimmed = classReference.trim();
260
+ if (!trimmed) {
261
+ throw new Error(`${label} cannot be empty.`);
262
+ }
263
+ if (!PHP_QUALIFIED_NAME_PATTERN.test(trimmed)) {
264
+ throw new Error(`${label} must be a PHP class reference such as Demo_Rest_Controller or Vendor\\Plugin\\Controller.`);
265
+ }
266
+ return trimmed;
267
+ }
268
+ /**
269
+ * Normalize and validate the HTTP method used by a manual REST contract.
270
+ *
271
+ * @param method Optional method input. Defaults to GET.
272
+ * @returns A canonical uppercase HTTP method.
273
+ * @throws {Error} When the method is unsupported.
274
+ */
275
+ export function assertValidManualRestContractHttpMethod(method = "GET") {
276
+ const normalized = method.trim().toUpperCase();
277
+ if (MANUAL_REST_CONTRACT_HTTP_METHOD_IDS.includes(normalized)) {
278
+ return normalized;
279
+ }
280
+ throw new Error(`Manual REST contract method must be one of: ${MANUAL_REST_CONTRACT_HTTP_METHOD_IDS.join(", ")}.`);
281
+ }
282
+ /**
283
+ * Normalize and validate the auth intent used by a manual REST contract.
284
+ *
285
+ * @param auth Optional auth intent input. Defaults to public.
286
+ * @returns A canonical auth intent.
287
+ * @throws {Error} When the auth intent is unsupported.
288
+ */
289
+ export function assertValidManualRestContractAuth(auth = "public") {
290
+ const normalized = auth.trim().toLowerCase();
291
+ if (MANUAL_REST_CONTRACT_AUTH_IDS.includes(normalized)) {
292
+ return normalized;
293
+ }
294
+ throw new Error(`Manual REST contract auth must be one of: ${MANUAL_REST_CONTRACT_AUTH_IDS.join(", ")}.`);
295
+ }
296
+ /**
297
+ * Normalize and validate a manual REST contract route path pattern.
298
+ *
299
+ * @param slug Generated contract slug used for the default route path.
300
+ * @param pathPattern Optional route path pattern, relative to the namespace.
301
+ * @returns A route pattern with a leading slash.
302
+ * @throws {Error} When the path pattern is empty or clearly not a route path.
303
+ */
304
+ function resolveRestRoutePathPattern(options) {
305
+ const explicitPath = typeof options.pathPattern === "string" ? options.pathPattern.trim() : undefined;
306
+ if (explicitPath === "") {
307
+ throw new Error(options.emptyMessage);
308
+ }
309
+ const trimmed = explicitPath ?? options.defaultPath;
310
+ if (/^https?:\/\//iu.test(trimmed)) {
311
+ throw new Error(`${options.label} must be a route pattern relative to the namespace, not an absolute URL.`);
312
+ }
313
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
314
+ if (!withLeadingSlash || withLeadingSlash === "/") {
315
+ throw new Error(options.emptyMessage);
316
+ }
317
+ if (/\s/u.test(withLeadingSlash)) {
318
+ throw new Error(`${options.label} must not contain whitespace.`);
319
+ }
320
+ return withLeadingSlash;
321
+ }
322
+ /**
323
+ * Check whether a generated REST item route keeps the generated `id` contract aligned.
324
+ *
325
+ * @param routePattern Route pattern relative to the namespace.
326
+ * @returns True when the pattern has no regex groups or uses only `(?P<id>...)`.
327
+ */
328
+ export function isGeneratedRestResourceRoutePatternCompatible(routePattern) {
329
+ const namedCaptures = Array.from(routePattern.matchAll(REST_ROUTE_NAMED_CAPTURE_PATTERN), (match) => match[1]);
330
+ const hasRegexGroup = routePattern.includes("(");
331
+ const hasUnsupportedCapture = REST_ROUTE_UNSUPPORTED_CAPTURE_PATTERN.test(routePattern);
332
+ return (!hasUnsupportedCapture &&
333
+ (!hasRegexGroup || (namedCaptures.length === 1 && namedCaptures[0] === "id")));
334
+ }
335
+ export function resolveManualRestContractPathPattern(slug, pathPattern) {
336
+ return resolveRestRoutePathPattern({
337
+ defaultPath: `/${slug}`,
338
+ emptyMessage: "Manual REST contract path is required. Use `--path <route-pattern>` such as `/external-record/(?P<id>[\\d]+)`.",
339
+ label: "Manual REST contract path",
340
+ pathPattern,
341
+ });
342
+ }
343
+ /**
344
+ * Normalize and validate a generated REST resource item route pattern.
345
+ *
346
+ * @param slug Generated resource slug used for the default item route path.
347
+ * @param routePattern Optional route path pattern, relative to the namespace.
348
+ * @returns A route pattern with a leading slash.
349
+ * @throws {Error} When the route pattern is empty or clearly not a route path.
350
+ */
351
+ export function resolveGeneratedRestResourceRoutePattern(slug, routePattern) {
352
+ const resolvedRoutePattern = resolveRestRoutePathPattern({
353
+ defaultPath: `/${slug}/item`,
354
+ emptyMessage: "Generated REST resource route pattern is required. Use `--route-pattern <route-pattern>` such as `/records/(?P<id>[\\d]+)`.",
355
+ label: "Generated REST resource route pattern",
356
+ pathPattern: routePattern,
357
+ });
358
+ const hasExplicitRoutePattern = typeof routePattern === "string" && routePattern.trim().length > 0;
359
+ if (hasExplicitRoutePattern &&
360
+ !isGeneratedRestResourceRoutePatternCompatible(resolvedRoutePattern)) {
361
+ throw new Error("Generated REST resource route pattern must use only an `(?P<id>...)` named capture when using regex groups.");
362
+ }
363
+ return resolvedRoutePattern;
364
+ }
76
365
  /**
77
366
  * Validate a hooked block insertion position.
78
367
  *
@@ -145,3 +434,17 @@ export function assertValidEditorPluginSlot(slot = "sidebar") {
145
434
  }
146
435
  throw new Error(`Editor plugin slot must be one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")}. Legacy aliases: PluginSidebar, PluginDocumentSettingPanel.`);
147
436
  }
437
+ /**
438
+ * Validate and normalize the optional integration environment service starter.
439
+ *
440
+ * @param service Optional service starter id. Defaults to `none`.
441
+ * @returns The canonical integration environment service id.
442
+ * @throws {Error} When the service starter is unsupported.
443
+ */
444
+ export function assertValidIntegrationEnvService(service = "none") {
445
+ const trimmed = service.trim();
446
+ if (INTEGRATION_ENV_SERVICE_IDS.includes(trimmed)) {
447
+ return trimmed;
448
+ }
449
+ throw new Error(`Integration environment service must be one of: ${INTEGRATION_ENV_SERVICE_IDS.join(", ")}.`);
450
+ }
@@ -2,8 +2,9 @@ import { promises as fsp } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathExists, readOptionalUtf8File } from './fs-async.js';
4
4
  import { appendWorkspaceInventoryEntries, readWorkspaceInventoryAsync, } from './workspace-inventory.js';
5
- import { buildAdminViewConfigEntry, buildAdminViewConfigSource, buildAdminViewEntrySource, buildAdminViewPhpSource, buildAdminViewRegistrySource, buildAdminViewScreenSource, buildAdminViewStyleSource, buildAdminViewTypesSource, buildCoreDataAdminViewDataSource, buildCoreDataAdminViewScreenSource, buildDefaultAdminViewDataSource, buildRestAdminViewDataSource, } from './cli-add-workspace-admin-view-templates.js';
6
- import { ADMIN_VIEWS_PHP_GLOB, isAdminViewCoreDataSource, } from './cli-add-workspace-admin-view-types.js';
5
+ import { buildAdminViewConfigEntry, buildAdminViewConfigSource, buildAdminViewEntrySource, buildAdminViewPhpSource, buildAdminViewRegistrySource, buildAdminViewScreenSource, buildAdminViewStyleSource, buildAdminViewTypesSource, buildCoreDataAdminViewDataSource, buildCoreDataAdminViewScreenSource, buildDefaultAdminViewDataSource, buildRestAdminViewDataSource, buildRestSettingsAdminViewConfigSource, buildRestSettingsAdminViewDataSource, buildRestSettingsAdminViewScreenSource, buildRestSettingsAdminViewTypesSource, } from './cli-add-workspace-admin-view-templates.js';
6
+ import { ADMIN_VIEWS_PHP_GLOB, isAdminViewCoreDataSource, isAdminViewManualSettingsRestResource, } from './cli-add-workspace-admin-view-types.js';
7
+ import { buildManualRestContractApiSource } from './cli-add-workspace-rest-source-emitters.js';
7
8
  import { getWorkspaceBootstrapPath, patchFile, } from './cli-add-shared.js';
8
9
  import { appendPhpSnippetBeforeClosingTag, executeWorkspaceMutationPlan, insertPhpSnippetBeforeWorkspaceAnchors, } from './cli-add-workspace-mutation.js';
9
10
  import { DEFAULT_WORDPRESS_CORE_DATA_VERSION, DEFAULT_WORDPRESS_DATA_VERSION, DEFAULT_WORDPRESS_DATAVIEWS_VERSION, DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, resolveManagedPackageVersionRange, } from './package-versions.js';
@@ -12,7 +13,34 @@ function detectJsonIndent(source) {
12
13
  const indentMatch = /\n([ \t]+)"/u.exec(source);
13
14
  return indentMatch?.[1] ?? 2;
14
15
  }
15
- async function ensureAdminViewPackageDependencies(workspace, adminViewSource) {
16
+ const LEGACY_MANUAL_REST_API_SOURCE_PATTERN = /^\s*export\s+\*\s+from\s+["']\.\/api-client["'];?\s*$/u;
17
+ const MANUAL_REST_API_DIRECT_CALL_EXPORT_PATTERN = /(?:^|\n)\s*export\s+(?:(?:async\s+)?function|const|let|var)\s+callManualRestContract\b/u;
18
+ const MANUAL_REST_API_NAMED_EXPORT_PATTERN = /(?:^|\n)\s*export\s*\{([^}]*)\}/gu;
19
+ function hasManualRestContractCallerExport(apiSource) {
20
+ if (MANUAL_REST_API_DIRECT_CALL_EXPORT_PATTERN.test(apiSource)) {
21
+ return true;
22
+ }
23
+ return Array.from(apiSource.matchAll(MANUAL_REST_API_NAMED_EXPORT_PATTERN)).some((match) => match[1].split(',').some((specifier) => {
24
+ const [localName, exportedName] = specifier.trim().split(/\s+as\s+/u);
25
+ return (exportedName ?? localName)?.trim() === 'callManualRestContract';
26
+ }));
27
+ }
28
+ async function ensureManualSettingsRestApiShim(workspace, restResource) {
29
+ const apiPath = path.join(workspace.projectDir, restResource.apiFile);
30
+ const apiSource = await fsp.readFile(apiPath, 'utf8');
31
+ if (hasManualRestContractCallerExport(apiSource)) {
32
+ return;
33
+ }
34
+ if (!LEGACY_MANUAL_REST_API_SOURCE_PATTERN.test(apiSource)) {
35
+ throw new Error(`Manual REST resource source "${restResource.slug}" must export callManualRestContract from ${restResource.apiFile}. Restore the generated manual REST api.ts or update it before scaffolding a settings admin view.`);
36
+ }
37
+ await fsp.writeFile(apiPath, buildManualRestContractApiSource({
38
+ bodyTypeName: restResource.bodyTypeName,
39
+ queryTypeName: restResource.queryTypeName,
40
+ restResourceSlug: restResource.slug,
41
+ }), 'utf8');
42
+ }
43
+ async function ensureAdminViewPackageDependencies(workspace, adminViewSource, restResource) {
16
44
  const packageJsonPath = path.join(workspace.projectDir, 'package.json');
17
45
  const wpTypiaDataViewsVersion = resolveManagedPackageVersionRange({
18
46
  fallback: DEFAULT_WP_TYPIA_DATAVIEWS_VERSION,
@@ -33,6 +61,7 @@ async function ensureAdminViewPackageDependencies(workspace, adminViewSource) {
33
61
  });
34
62
  await patchFile(packageJsonPath, (source) => {
35
63
  const packageJson = JSON.parse(source);
64
+ const needsDataViews = !isAdminViewManualSettingsRestResource(restResource);
36
65
  const coreDataDependencies = isAdminViewCoreDataSource(adminViewSource)
37
66
  ? {
38
67
  '@wordpress/core-data': packageJson.dependencies?.['@wordpress/core-data'] ??
@@ -43,14 +72,22 @@ async function ensureAdminViewPackageDependencies(workspace, adminViewSource) {
43
72
  : {};
44
73
  const nextDependencies = {
45
74
  ...(packageJson.dependencies ?? {}),
46
- '@wordpress/dataviews': packageJson.dependencies?.['@wordpress/dataviews'] ??
47
- wordpressDataViewsVersion,
75
+ ...(needsDataViews
76
+ ? {
77
+ '@wordpress/dataviews': packageJson.dependencies?.['@wordpress/dataviews'] ??
78
+ wordpressDataViewsVersion,
79
+ }
80
+ : {}),
48
81
  ...coreDataDependencies,
49
82
  };
50
83
  const nextDevDependencies = {
51
84
  ...(packageJson.devDependencies ?? {}),
52
- '@wp-typia/dataviews': packageJson.devDependencies?.['@wp-typia/dataviews'] ??
53
- wpTypiaDataViewsVersion,
85
+ ...(needsDataViews
86
+ ? {
87
+ '@wp-typia/dataviews': packageJson.devDependencies?.['@wp-typia/dataviews'] ??
88
+ wpTypiaDataViewsVersion,
89
+ }
90
+ : {}),
54
91
  };
55
92
  if (JSON.stringify(nextDependencies) ===
56
93
  JSON.stringify(packageJson.dependencies ?? {}) &&
@@ -193,6 +230,10 @@ export async function scaffoldAdminViewWorkspace(options) {
193
230
  const webpackConfigPath = path.join(workspace.projectDir, 'webpack.config.js');
194
231
  const adminViewsIndexPath = await resolveAdminViewRegistryPath(workspace.projectDir);
195
232
  const adminViewDir = path.join(workspace.projectDir, 'src', 'admin-views', adminViewSlug);
233
+ const manualSettingsRestResource = isAdminViewManualSettingsRestResource(restResource) ? restResource : undefined;
234
+ const manualSettingsRestApiPath = manualSettingsRestResource
235
+ ? path.join(workspace.projectDir, manualSettingsRestResource.apiFile)
236
+ : undefined;
196
237
  const adminViewPhpPath = path.join(workspace.projectDir, 'inc', 'admin-views', `${adminViewSlug}.php`);
197
238
  await executeWorkspaceMutationPlan({
198
239
  filePaths: [
@@ -202,26 +243,40 @@ export async function scaffoldAdminViewWorkspace(options) {
202
243
  buildScriptPath,
203
244
  packageJsonPath,
204
245
  webpackConfigPath,
246
+ ...(manualSettingsRestApiPath ? [manualSettingsRestApiPath] : []),
205
247
  ],
206
248
  targetPaths: [adminViewDir, adminViewPhpPath],
207
249
  run: async () => {
208
250
  await fsp.mkdir(adminViewDir, { recursive: true });
209
251
  await fsp.mkdir(path.dirname(adminViewPhpPath), { recursive: true });
210
- await ensureAdminViewPackageDependencies(workspace, parsedSource);
252
+ await ensureAdminViewPackageDependencies(workspace, parsedSource, restResource);
211
253
  await ensureAdminViewBootstrapAnchors(workspace);
212
254
  await ensureAdminViewBuildScriptAnchors(workspace);
213
255
  await ensureAdminViewWebpackAnchors(workspace);
214
- await fsp.writeFile(path.join(adminViewDir, 'types.ts'), buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), 'utf8');
215
- await fsp.writeFile(path.join(adminViewDir, 'config.ts'), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), 'utf8');
216
- await fsp.writeFile(path.join(adminViewDir, 'data.ts'), coreDataSource
217
- ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource)
218
- : restResource
219
- ? buildRestAdminViewDataSource(adminViewSlug, restResource)
220
- : buildDefaultAdminViewDataSource(adminViewSlug), 'utf8');
221
- await fsp.writeFile(path.join(adminViewDir, 'Screen.tsx'), coreDataSource
222
- ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
223
- : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), 'utf8');
224
- await fsp.writeFile(path.join(adminViewDir, 'index.tsx'), buildAdminViewEntrySource(adminViewSlug), 'utf8');
256
+ if (manualSettingsRestResource) {
257
+ await ensureManualSettingsRestApiShim(workspace, manualSettingsRestResource);
258
+ }
259
+ await fsp.writeFile(path.join(adminViewDir, 'types.ts'), manualSettingsRestResource
260
+ ? buildRestSettingsAdminViewTypesSource(adminViewSlug, manualSettingsRestResource)
261
+ : buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), 'utf8');
262
+ await fsp.writeFile(path.join(adminViewDir, 'config.ts'), manualSettingsRestResource
263
+ ? buildRestSettingsAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, manualSettingsRestResource)
264
+ : buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), 'utf8');
265
+ await fsp.writeFile(path.join(adminViewDir, 'data.ts'), manualSettingsRestResource
266
+ ? buildRestSettingsAdminViewDataSource(adminViewSlug, manualSettingsRestResource)
267
+ : coreDataSource
268
+ ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource)
269
+ : restResource
270
+ ? buildRestAdminViewDataSource(adminViewSlug, restResource)
271
+ : buildDefaultAdminViewDataSource(adminViewSlug), 'utf8');
272
+ await fsp.writeFile(path.join(adminViewDir, 'Screen.tsx'), manualSettingsRestResource
273
+ ? buildRestSettingsAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
274
+ : coreDataSource
275
+ ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
276
+ : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), 'utf8');
277
+ await fsp.writeFile(path.join(adminViewDir, 'index.tsx'), buildAdminViewEntrySource(adminViewSlug, {
278
+ includeDataViewsStyle: !manualSettingsRestResource,
279
+ }), 'utf8');
225
280
  await fsp.writeFile(path.join(adminViewDir, 'style.scss'), buildAdminViewStyleSource(), 'utf8');
226
281
  await fsp.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), 'utf8');
227
282
  await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
@@ -1,5 +1,5 @@
1
1
  import { assertValidGeneratedSlug } from './cli-add-shared.js';
2
- import { ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS, ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN, ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN, ADMIN_VIEW_CORE_DATA_SOURCE_KIND, ADMIN_VIEW_REST_SOURCE_KIND, ADMIN_VIEW_SOURCE_USAGE, isAdminViewCoreDataSource, isAdminViewRestResourceSource, } from './cli-add-workspace-admin-view-types.js';
2
+ import { ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS, ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN, ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN, ADMIN_VIEW_CORE_DATA_SOURCE_KIND, ADMIN_VIEW_REST_SOURCE_KIND, ADMIN_VIEW_SOURCE_USAGE, hasAdminViewManualSettingsRouteParameters, isAdminViewCoreDataSource, isAdminViewManualSettingsRestResource, isAdminViewRestResourceSource, } from './cli-add-workspace-admin-view-types.js';
3
3
  /**
4
4
  * Assert that admin-view package dependencies are available before file writes.
5
5
  *
@@ -75,8 +75,17 @@ export function resolveRestResourceSource(restResources, source) {
75
75
  if (!restResource) {
76
76
  throw new Error(`Unknown REST resource source "${source.slug}". Choose one of: ${restResources.map((entry) => entry.slug).join(', ') || '<none>'}.`);
77
77
  }
78
+ if (isAdminViewManualSettingsRestResource(restResource)) {
79
+ if (hasAdminViewManualSettingsRouteParameters(restResource)) {
80
+ throw new Error(`REST resource source "${source.slug}" uses route parameters or regex groups and cannot scaffold a singleton admin settings form. Use a manual REST contract without path parameters or build a custom admin UI.`);
81
+ }
82
+ return restResource;
83
+ }
84
+ if (restResource.mode === 'manual') {
85
+ throw new Error(`REST resource source "${source.slug}" must define a request body type before it can scaffold an admin settings form.`);
86
+ }
78
87
  if (!restResource.methods.includes('list')) {
79
- throw new Error(`REST resource source "${source.slug}" must include the list method for DataViews pagination.`);
88
+ throw new Error(`REST resource source "${source.slug}" must include the list method for DataViews pagination or be a manual settings contract with a body type.`);
80
89
  }
81
90
  return restResource;
82
91
  }
@@ -1,5 +1,5 @@
1
1
  import { type WorkspaceProject } from './workspace-project.js';
2
- import { type AdminViewCoreDataSource, type AdminViewRestResource, type AdminViewSource } from './cli-add-workspace-admin-view-types.js';
2
+ import { type AdminViewCoreDataSource, type AdminViewManualSettingsRestResource, type AdminViewRestResource, type AdminViewSource } from './cli-add-workspace-admin-view-types.js';
3
3
  export declare function buildAdminViewConfigEntry(adminViewSlug: string, source: AdminViewSource | undefined): string;
4
4
  export declare function buildAdminViewRegistrySource(adminViewSlugs: string[]): string;
5
5
  /**
@@ -12,12 +12,30 @@ export declare function buildAdminViewTypesSource(adminViewSlug: string, restRes
12
12
  export declare function buildAdminViewConfigSource(adminViewSlug: string, textDomain: string, source: AdminViewSource | undefined, restResource: AdminViewRestResource | undefined): string;
13
13
  export declare function buildDefaultAdminViewDataSource(adminViewSlug: string): string;
14
14
  export declare function buildRestAdminViewDataSource(adminViewSlug: string, restResource: AdminViewRestResource): string;
15
+ /**
16
+ * Build type aliases for a manual REST settings form admin screen.
17
+ */
18
+ export declare function buildRestSettingsAdminViewTypesSource(adminViewSlug: string, restResource: AdminViewManualSettingsRestResource): string;
19
+ /**
20
+ * Build display metadata for a manual REST settings form admin screen.
21
+ */
22
+ export declare function buildRestSettingsAdminViewConfigSource(adminViewSlug: string, textDomain: string, restResource: AdminViewManualSettingsRestResource): string;
23
+ /**
24
+ * Build data helpers for a manual REST settings form admin screen.
25
+ */
26
+ export declare function buildRestSettingsAdminViewDataSource(adminViewSlug: string, restResource: AdminViewManualSettingsRestResource): string;
27
+ /**
28
+ * Build a React settings form screen backed by a manual REST contract.
29
+ */
30
+ export declare function buildRestSettingsAdminViewScreenSource(adminViewSlug: string, textDomain: string): string;
15
31
  /**
16
32
  * Build a core-data-backed admin-view data module for a supported entity family.
17
33
  */
18
34
  export declare function buildCoreDataAdminViewDataSource(adminViewSlug: string, coreDataSource: AdminViewCoreDataSource): string;
19
35
  export declare function buildAdminViewScreenSource(adminViewSlug: string, textDomain: string): string;
20
36
  export declare function buildCoreDataAdminViewScreenSource(adminViewSlug: string, textDomain: string): string;
21
- export declare function buildAdminViewEntrySource(adminViewSlug: string): string;
37
+ export declare function buildAdminViewEntrySource(adminViewSlug: string, options?: {
38
+ includeDataViewsStyle?: boolean;
39
+ }): string;
22
40
  export declare function buildAdminViewStyleSource(): string;
23
41
  export declare function buildAdminViewPhpSource(adminViewSlug: string, workspace: WorkspaceProject): string;