wp-typia 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 (31) hide show
  1. package/README.md +2 -1
  2. package/bin/routing-metadata.generated.js +4 -0
  3. package/dist-bunli/.bunli/commands.gen.js +4103 -3086
  4. package/dist-bunli/{cli-hhp1d348.js → cli-1170yyve.js} +8 -7
  5. package/dist-bunli/{cli-qse6myha.js → cli-8hxf9qw6.js} +11 -3
  6. package/dist-bunli/{cli-8reep89s.js → cli-9fx0qgb7.js} +2 -2
  7. package/dist-bunli/{cli-add-21bvpfgw.js → cli-add-xjaaa01x.js} +1560 -1525
  8. package/dist-bunli/{cli-52ke0ptp.js → cli-am5x7tb4.js} +8 -2
  9. package/dist-bunli/cli-ccax7s0s.js +34 -0
  10. package/dist-bunli/{cli-diagnostics-5dvztm7q.js → cli-diagnostics-10drxh34.js} +1 -1
  11. package/dist-bunli/{cli-doctor-wy2yjsge.js → cli-doctor-19e8313m.js} +602 -459
  12. package/dist-bunli/{cli-2rqf6t0b.js → cli-e4bwd81c.js} +8 -11
  13. package/dist-bunli/{cli-9npd9was.js → cli-epsczb1c.js} +12 -10
  14. package/dist-bunli/{cli-agywa5n6.js → cli-fp16mntv.js} +8 -4
  15. package/dist-bunli/{cli-init-xnsbxncv.js → cli-init-2b4yn2cc.js} +14 -10
  16. package/dist-bunli/{cli-ts9thts5.js → cli-k5q5v8g6.js} +184 -162
  17. package/dist-bunli/{cli-c2acv5dv.js → cli-nvs5atj1.js} +2 -2
  18. package/dist-bunli/{cli-prompt-614tq57c.js → cli-prompt-ncyg68rn.js} +1 -1
  19. package/dist-bunli/{cli-bq2v559b.js → cli-rdcga1bd.js} +31 -13
  20. package/dist-bunli/{cli-scaffold-zhp2ym8z.js → cli-scaffold-4tjw4jk5.js} +27 -15
  21. package/dist-bunli/{cli-templates-hc71dfc2.js → cli-templates-g8t4fm11.js} +3 -2
  22. package/dist-bunli/{cli-p95wr1q8.js → cli-tq730sqt.js} +6 -3
  23. package/dist-bunli/{cli-1meywwsy.js → cli-y7w3pybs.js} +848 -246
  24. package/dist-bunli/{cli-z5qkx2pn.js → cli-ymecd15q.js} +37 -10
  25. package/dist-bunli/cli.js +4 -4
  26. package/dist-bunli/{command-list-aqrkx021.js → command-list-vme7dr5v.js} +81 -45
  27. package/dist-bunli/{create-template-validation-rtec5sng.js → create-template-validation-4fr851vg.js} +5 -4
  28. package/dist-bunli/{migrations-bx0yvc2v.js → migrations-pb5vvtdp.js} +9 -8
  29. package/dist-bunli/node-cli.js +399 -317
  30. package/dist-bunli/{workspace-project-csnnggz6.js → workspace-project-gmv2a71z.js} +4 -3
  31. package/package.json +2 -2
@@ -2,10 +2,13 @@
2
2
  import {
3
3
  OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY,
4
4
  REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY,
5
+ appendPhpSnippetBeforeClosingTag,
5
6
  assertExternalLayerCompositionOptions,
6
7
  copyInterpolatedDirectory,
7
8
  createScaffoldCompatibilityConfig,
9
+ executeWorkspaceMutationPlan,
8
10
  getDefaultAnswers,
11
+ insertPhpSnippetBeforeWorkspaceAnchors,
9
12
  isCompoundPersistenceEnabled,
10
13
  listInterpolatedDirectoryOutputs,
11
14
  normalizeOptionalCliString,
@@ -17,14 +20,15 @@ import {
17
20
  resolveOptionalInteractiveExternalLayerId,
18
21
  resolveScaffoldCompatibilityPolicy,
19
22
  resolveTemplateSeed,
23
+ runAddIntegrationEnvCommand,
20
24
  scaffoldProject,
21
25
  syncPersistenceRestArtifacts,
22
26
  updatePluginHeaderCompatibility
23
- } from "./cli-1meywwsy.js";
27
+ } from "./cli-y7w3pybs.js";
24
28
  import {
25
29
  parseTemplateLocator,
26
30
  require_semver
27
- } from "./cli-8reep89s.js";
31
+ } from "./cli-9fx0qgb7.js";
28
32
  import {
29
33
  ADMIN_VIEWS_ASSET,
30
34
  ADMIN_VIEWS_PHP_GLOB,
@@ -47,7 +51,7 @@ import {
47
51
  isAdminViewRestResourceSource,
48
52
  maskTypeScriptCommentsAndLiterals
49
53
  } from "./cli-j8et6jvr.js";
50
- import"./cli-c2acv5dv.js";
54
+ import"./cli-nvs5atj1.js";
51
55
  import {
52
56
  DEFAULT_WORDPRESS_ABILITIES_VERSION,
53
57
  DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION,
@@ -57,19 +61,19 @@ import {
57
61
  DEFAULT_WP_TYPIA_DATAVIEWS_VERSION,
58
62
  getPackageVersions,
59
63
  resolveManagedPackageVersionRange
60
- } from "./cli-agywa5n6.js";
64
+ } from "./cli-fp16mntv.js";
61
65
  import {
62
66
  SHARED_WORKSPACE_TEMPLATE_ROOT
63
- } from "./cli-qse6myha.js";
67
+ } from "./cli-8hxf9qw6.js";
64
68
  import {
65
69
  snapshotProjectVersion
66
- } from "./cli-9npd9was.js";
70
+ } from "./cli-epsczb1c.js";
67
71
  import {
68
72
  ensureMigrationDirectories,
69
73
  parseMigrationConfig,
70
74
  writeInitialMigrationScaffold,
71
75
  writeMigrationConfig
72
- } from "./cli-2rqf6t0b.js";
76
+ } from "./cli-e4bwd81c.js";
73
77
  import {
74
78
  ADD_BLOCK_TEMPLATE_IDS,
75
79
  EDITOR_PLUGIN_SLOT_IDS,
@@ -89,7 +93,6 @@ import {
89
93
  assertValidGeneratedSlug,
90
94
  assertValidHookAnchor,
91
95
  assertValidHookedBlockPosition,
92
- assertValidIntegrationEnvService,
93
96
  assertValidManualRestContractAuth,
94
97
  assertValidManualRestContractHttpMethod,
95
98
  assertValidPostMetaPostType,
@@ -97,6 +100,7 @@ import {
97
100
  assertValidTypeScriptIdentifier,
98
101
  assertVariationDoesNotExist,
99
102
  buildWorkspacePhpPrefix,
103
+ collectRestRouteNamedCaptureNames,
100
104
  escapeRegex,
101
105
  findPhpFunctionRange,
102
106
  formatAddHelpText,
@@ -136,7 +140,7 @@ import {
136
140
  toPascalCase,
137
141
  toSnakeCase,
138
142
  toTitleCase
139
- } from "./cli-ts9thts5.js";
143
+ } from "./cli-k5q5v8g6.js";
140
144
  import"./cli-cvxvcw7c.js";
141
145
  import {
142
146
  createManagedTempRoot
@@ -144,15 +148,18 @@ import {
144
148
  import {
145
149
  ADD_KIND_IDS
146
150
  } from "./cli-43mx1vfb.js";
147
- import"./cli-p95wr1q8.js";
151
+ import"./cli-tq730sqt.js";
148
152
  import {
149
153
  resolveWorkspaceProject
150
- } from "./cli-hhp1d348.js";
154
+ } from "./cli-1170yyve.js";
155
+ import {
156
+ formatInstallCommand
157
+ } from "./cli-am5x7tb4.js";
151
158
  import {
152
- formatInstallCommand,
153
- formatRunScript
154
- } from "./cli-52ke0ptp.js";
155
- import"./cli-bq2v559b.js";
159
+ readJsonFile,
160
+ safeJsonParse
161
+ } from "./cli-ccax7s0s.js";
162
+ import"./cli-rdcga1bd.js";
156
163
  import {
157
164
  __reExport,
158
165
  __toESM
@@ -980,58 +987,14 @@ function resolveAdminViewCoreDataSource(source) {
980
987
  import { promises as fsp3 } from "fs";
981
988
  import path6 from "path";
982
989
 
983
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates.ts
984
- import path4 from "path";
985
- function getAdminViewRelativeModuleSpecifier(adminViewSlug, workspaceFile) {
986
- const adminViewDir = `src/admin-views/${adminViewSlug}`;
987
- const normalizedFile = workspaceFile.replace(/\\/gu, "/");
988
- const modulePath = normalizedFile.replace(/\.[cm]?[jt]sx?$/u, "");
989
- const relativeModulePath = path4.posix.relative(adminViewDir, modulePath);
990
- return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
991
- }
992
- function buildAdminViewConfigEntry(adminViewSlug, source) {
993
- return [
994
- "\t{",
995
- ` file: ${quoteTsString(`src/admin-views/${adminViewSlug}/index.tsx`)},`,
996
- ` phpFile: ${quoteTsString(`inc/admin-views/${adminViewSlug}.php`)},`,
997
- ` slug: ${quoteTsString(adminViewSlug)},`,
998
- source ? ` source: ${quoteTsString(formatAdminViewSourceLocator(source))},` : null,
999
- "\t},"
1000
- ].filter((line) => typeof line === "string").join(`
1001
- `);
1002
- }
1003
- function buildAdminViewRegistrySource(adminViewSlugs) {
1004
- const importLines = adminViewSlugs.map((adminViewSlug) => `import './${adminViewSlug}';`).join(`
1005
- `);
1006
- return `${importLines}${importLines ? `
1007
-
1008
- ` : ""}// wp-typia add admin-view entries
1009
- `;
1010
- }
1011
- function buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource) {
990
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates-core-data.ts
991
+ function buildCoreDataAdminViewTypesSource(adminViewSlug, coreDataSource) {
1012
992
  const pascalName = toPascalCase(adminViewSlug);
1013
993
  const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
1014
994
  const itemTypeName = `${pascalName}AdminViewItem`;
1015
995
  const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1016
- if (restResource) {
1017
- const restPascalName = toPascalCase(restResource.slug);
1018
- const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1019
- return `import type { ${restPascalName}Record } from ${quoteTsString(restTypesModule)};
1020
-
1021
- export type ${itemTypeName} = ${restPascalName}Record;
1022
-
1023
- export interface ${dataSetTypeName} {
1024
- items: ${itemTypeName}[];
1025
- paginationInfo: {
1026
- totalItems: number;
1027
- totalPages: number;
1028
- };
1029
- }
1030
- `;
1031
- }
1032
- if (coreDataSource) {
1033
- if (coreDataSource.entityKind === "taxonomy") {
1034
- return `export interface ${coreDataRecordTypeName} {
996
+ if (coreDataSource.entityKind === "taxonomy") {
997
+ return `export interface ${coreDataRecordTypeName} {
1035
998
  count?: number;
1036
999
  description?: string;
1037
1000
  id: number;
@@ -1064,8 +1027,8 @@ export interface ${dataSetTypeName} {
1064
1027
  };
1065
1028
  }
1066
1029
  `;
1067
- }
1068
- return `export interface ${coreDataRecordTypeName} {
1030
+ }
1031
+ return `export interface ${coreDataRecordTypeName} {
1069
1032
  id: number;
1070
1033
  date?: string;
1071
1034
  modified?: string;
@@ -1088,25 +1051,6 @@ export interface ${itemTypeName} {
1088
1051
  updatedAt: string;
1089
1052
  }
1090
1053
 
1091
- export interface ${dataSetTypeName} {
1092
- items: ${itemTypeName}[];
1093
- paginationInfo: {
1094
- totalItems: number;
1095
- totalPages: number;
1096
- };
1097
- }
1098
- `;
1099
- }
1100
- return `export type ${pascalName}AdminViewStatus = 'draft' | 'published';
1101
-
1102
- export interface ${itemTypeName} {
1103
- id: number;
1104
- owner: string;
1105
- status: ${pascalName}AdminViewStatus;
1106
- title: string;
1107
- updatedAt: string;
1108
- }
1109
-
1110
1054
  export interface ${dataSetTypeName} {
1111
1055
  items: ${itemTypeName}[];
1112
1056
  paginationInfo: {
@@ -1116,27 +1060,20 @@ export interface ${dataSetTypeName} {
1116
1060
  }
1117
1061
  `;
1118
1062
  }
1119
- function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResource) {
1063
+ function buildCoreDataAdminViewConfigSource(adminViewSlug, textDomain, coreDataSource) {
1120
1064
  const pascalName = toPascalCase(adminViewSlug);
1121
1065
  const camelName = toCamelCase(adminViewSlug);
1122
1066
  const itemTypeName = `${pascalName}AdminViewItem`;
1123
1067
  const dataViewsName = `${camelName}AdminDataViews`;
1124
- const isCoreDataSource = isAdminViewCoreDataSource(source);
1125
- const isTaxonomyCoreDataSource = isAdminViewCoreDataSource(source) && source.entityKind === "taxonomy";
1126
- const defaultViewFields = restResource ? "['id']" : isTaxonomyCoreDataSource ? "['name', 'slug', 'count']" : isCoreDataSource ? "['title', 'slug', 'status', 'updatedAt']" : "['title', 'status', 'updatedAt']";
1127
- const searchEnabled = restResource ? "false" : "true";
1128
- const titleFieldSource = restResource ? "" : isTaxonomyCoreDataSource ? ` titleField: 'name',
1068
+ const isTaxonomyCoreDataSource = coreDataSource.entityKind === "taxonomy";
1069
+ const defaultViewFields = isTaxonomyCoreDataSource ? "['name', 'slug', 'count']" : "['title', 'slug', 'status', 'updatedAt']";
1070
+ const titleFieldSource = isTaxonomyCoreDataSource ? ` titleField: 'name',
1129
1071
  ` : ` titleField: 'title',
1130
1072
  `;
1131
- const defaultViewEnhancementsSource = restResource ? "" : isTaxonomyCoreDataSource ? ` titleField: 'name',
1132
- ` : isCoreDataSource ? ` titleField: 'title',
1133
- ` : ` sort: {
1134
- direction: 'desc',
1135
- field: 'updatedAt',
1136
- },
1137
- titleField: 'title',
1073
+ const defaultViewEnhancementsSource = isTaxonomyCoreDataSource ? ` titleField: 'name',
1074
+ ` : ` titleField: 'title',
1138
1075
  `;
1139
- const additionalFieldsSource = restResource ? "\t\t// REST-backed screens start with the guaranteed ID column. Add project-owned fields here once they are declared on the REST record type." : isTaxonomyCoreDataSource ? ` count: {
1076
+ const additionalFieldsSource = isTaxonomyCoreDataSource ? ` count: {
1140
1077
  label: __( 'Count', ${quoteTsString(textDomain)} ),
1141
1078
  schema: { type: 'integer' },
1142
1079
  },
@@ -1165,7 +1102,7 @@ function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResou
1165
1102
  taxonomy: {
1166
1103
  label: __( 'Taxonomy', ${quoteTsString(textDomain)} ),
1167
1104
  schema: { type: 'string' },
1168
- },` : isCoreDataSource ? ` slug: {
1105
+ },` : ` slug: {
1169
1106
  enableGlobalSearch: true,
1170
1107
  label: __( 'Slug', ${quoteTsString(textDomain)} ),
1171
1108
  schema: { type: 'string' },
@@ -1176,37 +1113,10 @@ function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResou
1176
1113
  },
1177
1114
  title: {
1178
1115
  enableGlobalSearch: true,
1179
- label: __( 'Name', ${quoteTsString(textDomain)} ),
1180
- schema: { type: 'string' },
1181
- },
1182
- updatedAt: {
1183
- label: __( 'Updated', ${quoteTsString(textDomain)} ),
1184
- schema: { format: 'date-time', type: 'string' },
1185
- type: 'datetime',
1186
- },` : ` owner: {
1187
- label: __( 'Owner', ${quoteTsString(textDomain)} ),
1188
- schema: { type: 'string' },
1189
- },
1190
- status: {
1191
- filterBy: { operators: ['isAny', 'isNone'] },
1192
- label: __( 'Status', ${quoteTsString(textDomain)} ),
1193
- schema: {
1194
- enum: ['draft', 'published'],
1195
- enumLabels: {
1196
- draft: __( 'Draft', ${quoteTsString(textDomain)} ),
1197
- published: __( 'Published', ${quoteTsString(textDomain)} ),
1198
- },
1199
- type: 'string',
1200
- },
1201
- },
1202
- title: {
1203
- enableGlobalSearch: true,
1204
- enableSorting: true,
1205
1116
  label: __( 'Title', ${quoteTsString(textDomain)} ),
1206
1117
  schema: { type: 'string' },
1207
1118
  },
1208
1119
  updatedAt: {
1209
- enableSorting: true,
1210
1120
  label: __( 'Updated', ${quoteTsString(textDomain)} ),
1211
1121
  schema: { format: 'date-time', type: 'string' },
1212
1122
  type: 'datetime',
@@ -1218,7 +1128,7 @@ import type { ${itemTypeName} } from './types';
1218
1128
 
1219
1129
  export const ${dataViewsName} = defineDataViews<${itemTypeName}>({
1220
1130
  idField: 'id',
1221
- search: ${searchEnabled},
1131
+ search: true,
1222
1132
  searchLabel: __( 'Search records', ${quoteTsString(textDomain)} ),
1223
1133
  ${titleFieldSource}
1224
1134
  defaultView: {
@@ -1240,406 +1150,279 @@ ${additionalFieldsSource}
1240
1150
  });
1241
1151
  `;
1242
1152
  }
1243
- function buildDefaultAdminViewDataSource(adminViewSlug) {
1153
+ function buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) {
1244
1154
  const pascalName = toPascalCase(adminViewSlug);
1245
1155
  const camelName = toCamelCase(adminViewSlug);
1246
- const title = toTitleCase(adminViewSlug);
1247
- const itemTypeName = `${pascalName}AdminViewItem`;
1156
+ const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
1248
1157
  const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1158
+ const itemTypeName = `${pascalName}AdminViewItem`;
1249
1159
  const queryTypeName = `${pascalName}AdminViewQuery`;
1250
1160
  const dataViewsName = `${camelName}AdminDataViews`;
1251
- const fetchName = `fetch${pascalName}AdminViewData`;
1252
- return `import type { DataViewsView } from '@wp-typia/dataviews';
1161
+ const useEntityRecordName = `use${pascalName}EntityRecord`;
1162
+ const useEntityRecordsName = `use${pascalName}EntityRecords`;
1163
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
1164
+ if (coreDataSource.entityKind === "taxonomy") {
1165
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1166
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1167
+ import { useMemo } from '@wordpress/element';
1253
1168
 
1254
1169
  import { ${dataViewsName} } from './config';
1255
- import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1170
+ import type {
1171
+ ${coreDataRecordTypeName},
1172
+ ${dataSetTypeName},
1173
+ ${itemTypeName},
1174
+ } from './types';
1256
1175
 
1257
1176
  export interface ${queryTypeName} {
1258
1177
  page?: number;
1259
- perPage?: number;
1178
+ per_page?: number;
1260
1179
  search?: string;
1261
1180
  }
1262
1181
 
1263
- const STARTER_ITEMS: ${itemTypeName}[] = [
1264
- {
1265
- id: 1,
1266
- owner: 'Editorial',
1267
- status: 'published',
1268
- title: ${quoteTsString(`${title} launch checklist`)},
1269
- updatedAt: '2026-04-01T10:30:00Z',
1270
- },
1271
- {
1272
- id: 2,
1273
- owner: 'Design',
1274
- status: 'draft',
1275
- title: ${quoteTsString(`${title} content refresh`)},
1276
- updatedAt: '2026-04-03T14:15:00Z',
1277
- },
1278
- {
1279
- id: 3,
1280
- owner: 'Operations',
1281
- status: 'published',
1282
- title: ${quoteTsString(`${title} support handoff`)},
1283
- updatedAt: '2026-04-08T08:45:00Z',
1284
- },
1285
- ];
1182
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1183
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1286
1184
 
1287
- function matchesSearch(item: ${itemTypeName}, search: string | undefined): boolean {
1288
- if (!search) {
1289
- return true;
1185
+ function normalizeCoreDataNumber(value: unknown): number {
1186
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
1187
+ }
1188
+
1189
+ function normalizeCoreDataString(value: unknown): string {
1190
+ return typeof value === 'string' ? value : '';
1191
+ }
1192
+
1193
+ function normalizeTaxonomyRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1194
+ return {
1195
+ count: normalizeCoreDataNumber(record.count),
1196
+ description: normalizeCoreDataString(record.description),
1197
+ id: record.id,
1198
+ link: normalizeCoreDataString(record.link),
1199
+ name: normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug),
1200
+ parent: normalizeCoreDataNumber(record.parent),
1201
+ raw: record,
1202
+ slug: normalizeCoreDataString(record.slug),
1203
+ taxonomy: normalizeCoreDataString(record.taxonomy),
1204
+ };
1290
1205
  }
1291
1206
 
1292
- const needle = search.toLowerCase();
1293
- return [item.title, item.owner, item.status].some((value) =>
1294
- value.toLowerCase().includes(needle),
1207
+ export function ${useEntityRecordName}(recordId: number | undefined) {
1208
+ return useEntityRecord<${coreDataRecordTypeName}>(
1209
+ CORE_DATA_ENTITY_KIND,
1210
+ CORE_DATA_ENTITY_NAME,
1211
+ recordId ?? 0,
1212
+ { enabled: typeof recordId === 'number' },
1295
1213
  );
1296
- }
1214
+ }
1297
1215
 
1298
- export async function ${fetchName}(
1299
- view: DataViewsView<${itemTypeName}>,
1300
- ): Promise<${dataSetTypeName}> {
1216
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1301
1217
  const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1302
- perPageParam: 'perPage',
1218
+ perPageParam: 'per_page',
1303
1219
  });
1304
- const requestedPage = query.page ?? 1;
1305
- const page = requestedPage > 0 ? requestedPage : 1;
1306
- const requestedPerPage = query.perPage ?? view.perPage ?? 10;
1307
- const perPage = requestedPerPage > 0 ? requestedPerPage : 10;
1308
- const filteredItems = STARTER_ITEMS.filter((item) =>
1309
- matchesSearch(item, query.search),
1220
+
1221
+ return useEntityRecords<${coreDataRecordTypeName}>(
1222
+ CORE_DATA_ENTITY_KIND,
1223
+ CORE_DATA_ENTITY_NAME,
1224
+ query,
1310
1225
  );
1311
- const offset = (page - 1) * perPage;
1312
- const items = filteredItems.slice(offset, offset + perPage);
1226
+ }
1227
+
1228
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1229
+ const { hasResolved, isResolving, records, totalItems, totalPages } =
1230
+ ${useEntityRecordsName}(view);
1231
+ const items = useMemo(
1232
+ () => (records ?? []).map((record) => normalizeTaxonomyRecord(record)),
1233
+ [records],
1234
+ );
1235
+ const dataSet = useMemo<${dataSetTypeName}>(
1236
+ () => ({
1237
+ items,
1238
+ paginationInfo: {
1239
+ totalItems: totalItems ?? items.length,
1240
+ totalPages: Math.max(1, totalPages ?? 1),
1241
+ },
1242
+ }),
1243
+ [items, totalItems, totalPages],
1244
+ );
1245
+ const error =
1246
+ !isResolving && hasResolved && records === null
1247
+ ? 'Unable to load core-data entity records.'
1248
+ : null;
1313
1249
 
1314
1250
  return {
1315
- items,
1316
- paginationInfo: {
1317
- totalItems: filteredItems.length,
1318
- totalPages: Math.max(1, Math.ceil(filteredItems.length / perPage)),
1319
- },
1251
+ dataSet,
1252
+ error,
1253
+ isLoading: isResolving,
1320
1254
  };
1321
- }
1255
+ }
1322
1256
  `;
1323
- }
1324
- function buildRestAdminViewDataSource(adminViewSlug, restResource) {
1325
- const pascalName = toPascalCase(adminViewSlug);
1326
- const restPascalName = toPascalCase(restResource.slug);
1327
- const camelName = toCamelCase(adminViewSlug);
1328
- const itemTypeName = `${pascalName}AdminViewItem`;
1329
- const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1330
- const dataViewsName = `${camelName}AdminDataViews`;
1331
- const fetchName = `fetch${pascalName}AdminViewData`;
1332
- const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
1333
- const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1257
+ }
1334
1258
  return `import type { DataViewsView } from '@wp-typia/dataviews';
1259
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1260
+ import { useMemo } from '@wordpress/element';
1335
1261
 
1336
- import { listResource } from ${quoteTsString(restApiModule)};
1337
- import type { ${restPascalName}ListQuery } from ${quoteTsString(restTypesModule)};
1338
1262
  import { ${dataViewsName} } from './config';
1339
- import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1263
+ import type {
1264
+ ${coreDataRecordTypeName},
1265
+ ${dataSetTypeName},
1266
+ ${itemTypeName},
1267
+ } from './types';
1340
1268
 
1341
- function resolveTotalPages(total: number, perPage: number | undefined): number {
1342
- const resolvedPerPage = perPage && perPage > 0 ? perPage : 1;
1343
- return Math.max(1, Math.ceil(total / resolvedPerPage));
1269
+ export interface ${queryTypeName} {
1270
+ page?: number;
1271
+ per_page?: number;
1272
+ search?: string;
1344
1273
  }
1345
1274
 
1346
- export async function ${fetchName}(
1347
- view: DataViewsView<${itemTypeName}>,
1348
- ): Promise<${dataSetTypeName}> {
1349
- const query = ${dataViewsName}.toQueryArgs<${restPascalName}ListQuery>(view, {
1350
- perPageParam: 'perPage',
1351
- searchParam: false,
1352
- });
1353
- const result = await listResource({
1354
- page: query.page,
1355
- perPage: query.perPage,
1356
- });
1357
- if (!result.isValid || !result.data) {
1358
- throw new Error('Unable to load REST resource records.');
1359
- }
1360
-
1361
- const response = result.data;
1275
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1276
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1362
1277
 
1363
- return {
1364
- items: response.items,
1365
- paginationInfo: {
1366
- totalItems: response.total,
1367
- totalPages: resolveTotalPages(response.total, response.perPage ?? query.perPage),
1368
- },
1369
- };
1370
- }
1371
- `;
1278
+ function normalizeCoreDataString(value: unknown): string {
1279
+ return typeof value === 'string' ? value : '';
1372
1280
  }
1373
- function buildRestSettingsAdminViewTypesSource(adminViewSlug, restResource) {
1374
- const pascalName = toPascalCase(adminViewSlug);
1375
- const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1376
- const formStateTypeName = `${pascalName}SettingsFormState`;
1377
- const loadResultTypeName = `${pascalName}SettingsLoadResult`;
1378
- return `import type {
1379
- ${restResource.bodyTypeName},
1380
- ${restResource.queryTypeName},
1381
- ${restResource.responseTypeName},
1382
- } from ${quoteTsString(restTypesModule)};
1383
1281
 
1384
- export type ${pascalName}SettingsRequest = ${restResource.bodyTypeName};
1385
- export type ${pascalName}SettingsQuery = ${restResource.queryTypeName};
1386
- export type ${pascalName}SettingsResponse = ${restResource.responseTypeName};
1387
- export type ${formStateTypeName} = Partial<${pascalName}SettingsRequest>;
1282
+ function normalizeCoreDataTitle(record: ${coreDataRecordTypeName}): string {
1283
+ if (typeof record.title === 'string') {
1284
+ return record.title;
1285
+ }
1286
+ if (record.title && typeof record.title === 'object') {
1287
+ if (typeof record.title.rendered === 'string') {
1288
+ return record.title.rendered;
1289
+ }
1290
+ if (typeof record.title.raw === 'string') {
1291
+ return record.title.raw;
1292
+ }
1293
+ }
1388
1294
 
1389
- export interface ${loadResultTypeName} {
1390
- form: ${formStateTypeName};
1391
- response: ${pascalName}SettingsResponse | null;
1392
- }
1393
- `;
1295
+ return normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug);
1394
1296
  }
1395
- function buildRestSettingsAdminViewConfigSource(adminViewSlug, textDomain, restResource) {
1396
- const pascalName = toPascalCase(adminViewSlug);
1397
- const camelName = toCamelCase(adminViewSlug);
1398
- const title = toTitleCase(adminViewSlug);
1399
- const configName = `${camelName}SettingsConfig`;
1400
- const formStateTypeName = `${pascalName}SettingsFormState`;
1401
- const secretFieldSource = restResource.secretFieldName && restResource.secretStateFieldName ? ` {
1402
- description: __( 'Write-only secret value. Leave blank to keep the existing secret unless your route treats blank values as removal.', ${quoteTsString(textDomain)} ),
1403
- id: ${quoteTsString(restResource.secretFieldName)},
1404
- label: __( ${quoteTsString(toTitleCase(restResource.secretFieldName))}, ${quoteTsString(textDomain)} ),
1405
- secretStateField: ${quoteTsString(restResource.secretStateFieldName)},
1406
- type: 'secret',
1407
- },` : "";
1408
- return `import { __ } from '@wordpress/i18n';
1409
-
1410
- import type { ${formStateTypeName} } from './types';
1411
1297
 
1412
- export type ${pascalName}SettingsFieldType = 'secret' | 'text' | 'textarea';
1298
+ function normalizeCoreDataUpdatedAt(record: ${coreDataRecordTypeName}): string {
1299
+ return normalizeCoreDataString(record.modified) || normalizeCoreDataString(record.date);
1300
+ }
1413
1301
 
1414
- export interface ${pascalName}SettingsField {
1415
- description?: string;
1416
- id: Extract<keyof ${formStateTypeName}, string> | string;
1417
- label: string;
1418
- secretStateField?: string;
1419
- type: ${pascalName}SettingsFieldType;
1302
+ function normalizeCoreDataRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1303
+ return {
1304
+ id: record.id,
1305
+ raw: record,
1306
+ slug: normalizeCoreDataString(record.slug),
1307
+ status: normalizeCoreDataString(record.status),
1308
+ title: normalizeCoreDataTitle(record),
1309
+ updatedAt: normalizeCoreDataUpdatedAt(record),
1310
+ };
1420
1311
  }
1421
1312
 
1422
- export const ${configName} = {
1423
- description: __( 'This generated settings form is backed by the ${restResource.slug} REST contract. Adjust config.ts and data.ts as the contract becomes product-specific.', ${quoteTsString(textDomain)} ),
1424
- fields: [
1425
- {
1426
- description: __( 'Primary settings payload for this integration.', ${quoteTsString(textDomain)} ),
1427
- id: 'payload',
1428
- label: __( 'Payload', ${quoteTsString(textDomain)} ),
1429
- type: 'textarea',
1430
- },
1431
- {
1432
- description: __( 'Optional operator note included with the save request.', ${quoteTsString(textDomain)} ),
1433
- id: 'comment',
1434
- label: __( 'Comment', ${quoteTsString(textDomain)} ),
1435
- type: 'text',
1436
- },
1437
- ${secretFieldSource}
1438
- ] satisfies ${pascalName}SettingsField[],
1439
- secretFieldName: ${restResource.secretFieldName ? quoteTsString(restResource.secretFieldName) : "undefined"},
1440
- secretStateFieldName: ${restResource.secretStateFieldName ? quoteTsString(restResource.secretStateFieldName) : "undefined"},
1441
- title: __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ),
1442
- };
1443
- `;
1313
+ export function ${useEntityRecordName}(recordId: number | undefined) {
1314
+ return useEntityRecord<${coreDataRecordTypeName}>(
1315
+ CORE_DATA_ENTITY_KIND,
1316
+ CORE_DATA_ENTITY_NAME,
1317
+ recordId ?? 0,
1318
+ { enabled: typeof recordId === 'number' },
1319
+ );
1444
1320
  }
1445
- function buildRestSettingsAdminViewDataSource(adminViewSlug, restResource) {
1446
- const pascalName = toPascalCase(adminViewSlug);
1447
- const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
1448
- const formStateTypeName = `${pascalName}SettingsFormState`;
1449
- const loadResultTypeName = `${pascalName}SettingsLoadResult`;
1450
- const loadName = `load${pascalName}Settings`;
1451
- const saveName = `save${pascalName}Settings`;
1452
- const initialFields = [
1453
- "\tpayload: '',",
1454
- "\tcomment: '',"
1455
- ].join(`
1456
- `);
1457
- const requestBodySource = restResource.secretFieldName ? ` const requestBody = { ...form } as Record<string, unknown>;
1458
- if (requestBody[${quoteTsString(restResource.secretFieldName)}] === '') {
1459
- delete requestBody[${quoteTsString(restResource.secretFieldName)}];
1460
- }
1461
- ` : ` const requestBody = form as Record<string, unknown>;
1462
- `;
1463
- return `import { callManualRestContract } from ${quoteTsString(restApiModule)};
1464
- import type {
1465
- ${formStateTypeName},
1466
- ${loadResultTypeName},
1467
- ${pascalName}SettingsQuery,
1468
- ${pascalName}SettingsRequest,
1469
- ${pascalName}SettingsResponse,
1470
- } from './types';
1471
1321
 
1472
- function formatValidationError(prefix: string, errors: unknown): string {
1473
- if (!Array.isArray(errors) || errors.length === 0) {
1474
- return prefix;
1475
- }
1322
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1323
+ const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1324
+ perPageParam: 'per_page',
1325
+ });
1476
1326
 
1477
- return \`\${prefix} \${JSON.stringify(errors)}\`;
1327
+ return useEntityRecords<${coreDataRecordTypeName}>(
1328
+ CORE_DATA_ENTITY_KIND,
1329
+ CORE_DATA_ENTITY_NAME,
1330
+ query,
1331
+ );
1478
1332
  }
1479
1333
 
1480
- export function createInitial${pascalName}SettingsFormState(): ${formStateTypeName} {
1481
- return {
1482
- ${initialFields}
1483
- } as ${formStateTypeName};
1484
- }
1334
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1335
+ const { hasResolved, isResolving, records, totalItems, totalPages } =
1336
+ ${useEntityRecordsName}(view);
1337
+ const items = useMemo(
1338
+ () => (records ?? []).map((record) => normalizeCoreDataRecord(record)),
1339
+ [records],
1340
+ );
1341
+ const dataSet = useMemo<${dataSetTypeName}>(
1342
+ () => ({
1343
+ items,
1344
+ paginationInfo: {
1345
+ totalItems: totalItems ?? items.length,
1346
+ totalPages: Math.max(1, totalPages ?? 1),
1347
+ },
1348
+ }),
1349
+ [items, totalItems, totalPages],
1350
+ );
1351
+ const error =
1352
+ !isResolving && hasResolved && records === null
1353
+ ? 'Unable to load core-data entity records.'
1354
+ : null;
1485
1355
 
1486
- export async function ${loadName}(): Promise<${loadResultTypeName}> {
1487
1356
  return {
1488
- form: createInitial${pascalName}SettingsFormState(),
1489
- response: null,
1357
+ dataSet,
1358
+ error,
1359
+ isLoading: isResolving,
1490
1360
  };
1491
1361
  }
1492
-
1493
- export async function ${saveName}(
1494
- form: ${formStateTypeName},
1495
- query: Partial<${pascalName}SettingsQuery> = {},
1496
- ): Promise<${pascalName}SettingsResponse> {
1497
- ${requestBodySource}
1498
- const result = await callManualRestContract({
1499
- body: requestBody as unknown as ${pascalName}SettingsRequest,
1500
- query: query as ${pascalName}SettingsQuery,
1501
- });
1502
- if (!result.isValid) {
1503
- const message =
1504
- result.validationTarget === 'request'
1505
- ? 'Settings request failed validation.'
1506
- : 'Settings response failed validation.';
1507
- throw new Error(formatValidationError(message, result.errors));
1508
- }
1509
-
1510
- return result.data as ${pascalName}SettingsResponse;
1511
- }
1512
1362
  `;
1513
1363
  }
1514
- function buildRestSettingsAdminViewScreenSource(adminViewSlug, textDomain) {
1364
+ function buildCoreDataAdminViewScreenSource(adminViewSlug, textDomain) {
1515
1365
  const pascalName = toPascalCase(adminViewSlug);
1516
- const componentName = `${pascalName}AdminViewScreen`;
1517
- const formStateTypeName = `${pascalName}SettingsFormState`;
1518
- const responseTypeName = `${pascalName}SettingsResponse`;
1519
1366
  const camelName = toCamelCase(adminViewSlug);
1520
- const configName = `${camelName}SettingsConfig`;
1521
- const loadName = `load${pascalName}Settings`;
1522
- const saveName = `save${pascalName}Settings`;
1523
- return `import {
1524
- Button,
1525
- Notice,
1526
- Spinner,
1527
- TextControl,
1528
- TextareaControl,
1529
- } from '@wordpress/components';
1530
- import { useEffect, useState } from '@wordpress/element';
1367
+ const itemTypeName = `${pascalName}AdminViewItem`;
1368
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1369
+ const componentName = `${pascalName}AdminViewScreen`;
1370
+ const dataViewsName = `${camelName}AdminDataViews`;
1371
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
1372
+ const title = toTitleCase(adminViewSlug);
1373
+ return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
1374
+ import { Notice, Spinner } from '@wordpress/components';
1375
+ import { useState } from '@wordpress/element';
1531
1376
  import { __ } from '@wordpress/i18n';
1377
+ import { DataViews } from '@wordpress/dataviews/wp';
1532
1378
 
1533
- import { ${configName} } from './config';
1534
- import { ${loadName}, ${saveName} } from './data';
1535
- import type { ${formStateTypeName}, ${responseTypeName} } from './types';
1379
+ import { ${dataViewsName} } from './config';
1380
+ import { ${useAdminViewDataName} } from './data';
1381
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1536
1382
 
1537
- function getFieldValue(form: ${formStateTypeName}, fieldId: string): string {
1538
- const value = (form as Record<string, unknown>)[fieldId];
1539
- if (typeof value === 'string') {
1540
- return value;
1541
- }
1542
- if (value == null) {
1543
- return '';
1544
- }
1545
-
1546
- return String(value);
1547
- }
1548
-
1549
- function getSecretState(response: ${responseTypeName} | null): boolean | null {
1550
- const stateField = ${configName}.secretStateFieldName;
1551
- if (!stateField || !response) {
1552
- return null;
1553
- }
1383
+ const TypedDataViews = DataViews as unknown as <TItem extends object>(
1384
+ props: DataViewsConfig<TItem>,
1385
+ ) => ReturnType<typeof DataViews>;
1554
1386
 
1555
- const value = (response as unknown as Record<string, unknown>)[stateField];
1556
- return typeof value === 'boolean' ? value : null;
1557
- }
1387
+ const EMPTY_DATA_SET: ${dataSetTypeName} = {
1388
+ items: [],
1389
+ paginationInfo: {
1390
+ totalItems: 0,
1391
+ totalPages: 1,
1392
+ },
1393
+ };
1558
1394
 
1559
1395
  export function ${componentName}() {
1560
- const [form, setForm] = useState<${formStateTypeName}>({});
1561
- const [response, setResponse] = useState<${responseTypeName} | null>(null);
1562
- const [error, setError] = useState<string | null>(null);
1563
- const [isLoading, setIsLoading] = useState(true);
1564
- const [isSaving, setIsSaving] = useState(false);
1565
- const [successMessage, setSuccessMessage] = useState<string | null>(null);
1566
-
1567
- useEffect(() => {
1568
- let isCurrent = true;
1569
- setIsLoading(true);
1570
- setError(null);
1571
-
1572
- void ${loadName}()
1573
- .then((result) => {
1574
- if (isCurrent) {
1575
- setForm(result.form);
1576
- setResponse(result.response);
1577
- }
1578
- })
1579
- .catch((nextError: unknown) => {
1580
- if (isCurrent) {
1581
- setError(
1582
- nextError instanceof Error
1583
- ? nextError.message
1584
- : __( 'Unable to prepare settings form.', ${quoteTsString(textDomain)} ),
1585
- );
1586
- }
1587
- })
1588
- .finally(() => {
1589
- if (isCurrent) {
1590
- setIsLoading(false);
1591
- }
1592
- });
1593
-
1594
- return () => {
1595
- isCurrent = false;
1596
- };
1597
- }, []);
1598
-
1599
- const setFormValue = (fieldId: string, value: string) => {
1600
- setForm(
1601
- (current) =>
1602
- ({
1603
- ...current,
1604
- [fieldId]: value,
1605
- }) as ${formStateTypeName},
1606
- );
1607
- };
1608
- const secretState = getSecretState(response);
1609
-
1610
- const handleSubmit = (event: { preventDefault: () => void }) => {
1611
- event.preventDefault();
1612
- setError(null);
1613
- setSuccessMessage(null);
1614
- setIsSaving(true);
1615
-
1616
- void ${saveName}(form)
1617
- .then((nextResponse) => {
1618
- setResponse(nextResponse);
1619
- setSuccessMessage(__( 'Settings saved.', ${quoteTsString(textDomain)} ));
1620
- })
1621
- .catch((nextError: unknown) => {
1622
- setError(
1623
- nextError instanceof Error
1624
- ? nextError.message
1625
- : __( 'Unable to save settings.', ${quoteTsString(textDomain)} ),
1626
- );
1627
- })
1628
- .finally(() => setIsSaving(false));
1629
- };
1396
+ const [view, setView] = useState<DataViewsView<${itemTypeName}>>(
1397
+ ${dataViewsName}.defaultView,
1398
+ );
1399
+ const {
1400
+ dataSet = EMPTY_DATA_SET,
1401
+ error,
1402
+ isLoading,
1403
+ } = ${useAdminViewDataName}(view);
1404
+ const config = ${dataViewsName}.createConfig({
1405
+ data: dataSet.items,
1406
+ isLoading,
1407
+ onChangeView: setView,
1408
+ paginationInfo: dataSet.paginationInfo,
1409
+ view,
1410
+ });
1630
1411
 
1631
1412
  return (
1632
- <div className="wp-typia-admin-view-screen wp-typia-admin-view-screen--settings">
1413
+ <div className="wp-typia-admin-view-screen">
1633
1414
  <header className="wp-typia-admin-view-screen__header">
1634
1415
  <div>
1635
1416
  <p className="wp-typia-admin-view-screen__eyebrow">
1636
- { __( 'Typed settings screen', ${quoteTsString(textDomain)} ) }
1417
+ { __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
1418
+ </p>
1419
+ <h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
1420
+ <p>
1421
+ { __( 'This screen reads from the WordPress core-data entity store. Extend data.ts when you need entity-specific field mapping or edit flows.', ${quoteTsString(textDomain)} ) }
1637
1422
  </p>
1638
- <h1>{ ${configName}.title }</h1>
1639
- <p>{ ${configName}.description }</p>
1640
1423
  </div>
1641
1424
  <div className="wp-typia-admin-view-screen__actions">
1642
- { isLoading || isSaving ? <Spinner /> : null }
1425
+ { isLoading ? <Spinner /> : null }
1643
1426
  </div>
1644
1427
  </header>
1645
1428
  { error ? (
@@ -1647,260 +1430,179 @@ export function ${componentName}() {
1647
1430
  { error }
1648
1431
  </Notice>
1649
1432
  ) : null }
1650
- { successMessage ? (
1651
- <Notice isDismissible={ false } status="success">
1652
- { successMessage }
1653
- </Notice>
1654
- ) : null }
1655
- { secretState !== null ? (
1656
- <Notice isDismissible={ false } status="info">
1657
- { secretState
1658
- ? __( 'A secret is currently configured for this integration.', ${quoteTsString(textDomain)} )
1659
- : __( 'No secret is currently configured for this integration.', ${quoteTsString(textDomain)} ) }
1660
- </Notice>
1661
- ) : null }
1662
- <form className="wp-typia-admin-view-screen__settings-form" onSubmit={ handleSubmit }>
1663
- { ${configName}.fields.map((field) => (
1664
- <div className="wp-typia-admin-view-screen__field" key={ field.id }>
1665
- { field.type === 'textarea' ? (
1666
- <TextareaControl
1667
- help={ field.description }
1668
- label={ field.label }
1669
- onChange={ (value) => setFormValue(field.id, value) }
1670
- value={ getFieldValue(form, field.id) }
1671
- />
1672
- ) : (
1673
- <TextControl
1674
- help={ field.description }
1675
- label={ field.label }
1676
- onChange={ (value) => setFormValue(field.id, value) }
1677
- type={ field.type === 'secret' ? 'password' : 'text' }
1678
- value={ getFieldValue(form, field.id) }
1679
- />
1680
- ) }
1681
- </div>
1682
- )) }
1683
- <Button
1684
- disabled={ isLoading || isSaving }
1685
- isBusy={ isSaving }
1686
- type="submit"
1687
- variant="primary"
1688
- >
1689
- { __( 'Save settings', ${quoteTsString(textDomain)} ) }
1690
- </Button>
1691
- </form>
1433
+ <TypedDataViews<${itemTypeName}> { ...config } />
1692
1434
  </div>
1693
1435
  );
1694
1436
  }
1695
1437
  `;
1696
1438
  }
1697
- function buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) {
1439
+
1440
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates-default.ts
1441
+ function buildDefaultAdminViewTypesSource(adminViewSlug) {
1698
1442
  const pascalName = toPascalCase(adminViewSlug);
1699
- const camelName = toCamelCase(adminViewSlug);
1700
- const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
1701
- const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1702
1443
  const itemTypeName = `${pascalName}AdminViewItem`;
1703
- const queryTypeName = `${pascalName}AdminViewQuery`;
1704
- const dataViewsName = `${camelName}AdminDataViews`;
1705
- const useEntityRecordName = `use${pascalName}EntityRecord`;
1706
- const useEntityRecordsName = `use${pascalName}EntityRecords`;
1707
- const useAdminViewDataName = `use${pascalName}AdminViewData`;
1708
- if (coreDataSource.entityKind === "taxonomy") {
1709
- return `import type { DataViewsView } from '@wp-typia/dataviews';
1710
- import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1711
- import { useMemo } from '@wordpress/element';
1712
-
1713
- import { ${dataViewsName} } from './config';
1714
- import type {
1715
- ${coreDataRecordTypeName},
1716
- ${dataSetTypeName},
1717
- ${itemTypeName},
1718
- } from './types';
1444
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1445
+ return `export type ${pascalName}AdminViewStatus = 'draft' | 'published';
1719
1446
 
1720
- export interface ${queryTypeName} {
1721
- page?: number;
1722
- per_page?: number;
1723
- search?: string;
1447
+ export interface ${itemTypeName} {
1448
+ id: number;
1449
+ owner: string;
1450
+ status: ${pascalName}AdminViewStatus;
1451
+ title: string;
1452
+ updatedAt: string;
1724
1453
  }
1725
1454
 
1726
- const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1727
- const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1728
-
1729
- function normalizeCoreDataNumber(value: unknown): number {
1730
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
1455
+ export interface ${dataSetTypeName} {
1456
+ items: ${itemTypeName}[];
1457
+ paginationInfo: {
1458
+ totalItems: number;
1459
+ totalPages: number;
1460
+ };
1731
1461
  }
1732
-
1733
- function normalizeCoreDataString(value: unknown): string {
1734
- return typeof value === 'string' ? value : '';
1462
+ `;
1735
1463
  }
1464
+ function buildDefaultAdminViewConfigSource(adminViewSlug, textDomain) {
1465
+ const pascalName = toPascalCase(adminViewSlug);
1466
+ const camelName = toCamelCase(adminViewSlug);
1467
+ const itemTypeName = `${pascalName}AdminViewItem`;
1468
+ const dataViewsName = `${camelName}AdminDataViews`;
1469
+ return `import { defineDataViews } from '@wp-typia/dataviews';
1470
+ import { __ } from '@wordpress/i18n';
1736
1471
 
1737
- function normalizeTaxonomyRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1738
- return {
1739
- count: normalizeCoreDataNumber(record.count),
1740
- description: normalizeCoreDataString(record.description),
1741
- id: record.id,
1742
- link: normalizeCoreDataString(record.link),
1743
- name: normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug),
1744
- parent: normalizeCoreDataNumber(record.parent),
1745
- raw: record,
1746
- slug: normalizeCoreDataString(record.slug),
1747
- taxonomy: normalizeCoreDataString(record.taxonomy),
1748
- };
1749
- }
1750
-
1751
- export function ${useEntityRecordName}(recordId: number | undefined) {
1752
- return useEntityRecord<${coreDataRecordTypeName}>(
1753
- CORE_DATA_ENTITY_KIND,
1754
- CORE_DATA_ENTITY_NAME,
1755
- recordId ?? 0,
1756
- { enabled: typeof recordId === 'number' },
1757
- );
1758
- }
1759
-
1760
- export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1761
- const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1762
- perPageParam: 'per_page',
1763
- });
1764
-
1765
- return useEntityRecords<${coreDataRecordTypeName}>(
1766
- CORE_DATA_ENTITY_KIND,
1767
- CORE_DATA_ENTITY_NAME,
1768
- query,
1769
- );
1770
- }
1472
+ import type { ${itemTypeName} } from './types';
1771
1473
 
1772
- export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1773
- const { hasResolved, isResolving, records, totalItems, totalPages } =
1774
- ${useEntityRecordsName}(view);
1775
- const items = useMemo(
1776
- () => (records ?? []).map((record) => normalizeTaxonomyRecord(record)),
1777
- [records],
1778
- );
1779
- const dataSet = useMemo<${dataSetTypeName}>(
1780
- () => ({
1781
- items,
1782
- paginationInfo: {
1783
- totalItems: totalItems ?? items.length,
1784
- totalPages: Math.max(1, totalPages ?? 1),
1474
+ export const ${dataViewsName} = defineDataViews<${itemTypeName}>({
1475
+ idField: 'id',
1476
+ search: true,
1477
+ searchLabel: __( 'Search records', ${quoteTsString(textDomain)} ),
1478
+ titleField: 'title',
1479
+ defaultView: {
1480
+ fields: ['title', 'status', 'updatedAt'],
1481
+ page: 1,
1482
+ perPage: 10,
1483
+ sort: {
1484
+ direction: 'desc',
1485
+ field: 'updatedAt',
1486
+ },
1487
+ titleField: 'title',
1488
+ type: 'table',
1489
+ },
1490
+ fields: {
1491
+ id: {
1492
+ enableHiding: false,
1493
+ label: __( 'ID', ${quoteTsString(textDomain)} ),
1494
+ readOnly: true,
1495
+ schema: { type: 'integer' },
1496
+ },
1497
+ owner: {
1498
+ label: __( 'Owner', ${quoteTsString(textDomain)} ),
1499
+ schema: { type: 'string' },
1500
+ },
1501
+ status: {
1502
+ filterBy: { operators: ['isAny', 'isNone'] },
1503
+ label: __( 'Status', ${quoteTsString(textDomain)} ),
1504
+ schema: {
1505
+ enum: ['draft', 'published'],
1506
+ enumLabels: {
1507
+ draft: __( 'Draft', ${quoteTsString(textDomain)} ),
1508
+ published: __( 'Published', ${quoteTsString(textDomain)} ),
1509
+ },
1510
+ type: 'string',
1785
1511
  },
1786
- }),
1787
- [items, totalItems, totalPages],
1788
- );
1789
- const error =
1790
- !isResolving && hasResolved && records === null
1791
- ? 'Unable to load core-data entity records.'
1792
- : null;
1793
-
1794
- return {
1795
- dataSet,
1796
- error,
1797
- isLoading: isResolving,
1798
- };
1799
- }
1512
+ },
1513
+ title: {
1514
+ enableGlobalSearch: true,
1515
+ enableSorting: true,
1516
+ label: __( 'Title', ${quoteTsString(textDomain)} ),
1517
+ schema: { type: 'string' },
1518
+ },
1519
+ updatedAt: {
1520
+ enableSorting: true,
1521
+ label: __( 'Updated', ${quoteTsString(textDomain)} ),
1522
+ schema: { format: 'date-time', type: 'string' },
1523
+ type: 'datetime',
1524
+ },
1525
+ },
1526
+ });
1800
1527
  `;
1801
- }
1528
+ }
1529
+ function buildDefaultAdminViewDataSource(adminViewSlug) {
1530
+ const pascalName = toPascalCase(adminViewSlug);
1531
+ const camelName = toCamelCase(adminViewSlug);
1532
+ const title = toTitleCase(adminViewSlug);
1533
+ const itemTypeName = `${pascalName}AdminViewItem`;
1534
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1535
+ const queryTypeName = `${pascalName}AdminViewQuery`;
1536
+ const dataViewsName = `${camelName}AdminDataViews`;
1537
+ const fetchName = `fetch${pascalName}AdminViewData`;
1802
1538
  return `import type { DataViewsView } from '@wp-typia/dataviews';
1803
- import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1804
- import { useMemo } from '@wordpress/element';
1805
1539
 
1806
1540
  import { ${dataViewsName} } from './config';
1807
- import type {
1808
- ${coreDataRecordTypeName},
1809
- ${dataSetTypeName},
1810
- ${itemTypeName},
1811
- } from './types';
1541
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1812
1542
 
1813
1543
  export interface ${queryTypeName} {
1814
1544
  page?: number;
1815
- per_page?: number;
1545
+ perPage?: number;
1816
1546
  search?: string;
1817
1547
  }
1818
1548
 
1819
- const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1820
- const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1821
-
1822
- function normalizeCoreDataString(value: unknown): string {
1823
- return typeof value === 'string' ? value : '';
1824
- }
1549
+ const STARTER_ITEMS: ${itemTypeName}[] = [
1550
+ {
1551
+ id: 1,
1552
+ owner: 'Editorial',
1553
+ status: 'published',
1554
+ title: ${quoteTsString(`${title} launch checklist`)},
1555
+ updatedAt: '2026-04-01T10:30:00Z',
1556
+ },
1557
+ {
1558
+ id: 2,
1559
+ owner: 'Design',
1560
+ status: 'draft',
1561
+ title: ${quoteTsString(`${title} content refresh`)},
1562
+ updatedAt: '2026-04-03T14:15:00Z',
1563
+ },
1564
+ {
1565
+ id: 3,
1566
+ owner: 'Operations',
1567
+ status: 'published',
1568
+ title: ${quoteTsString(`${title} support handoff`)},
1569
+ updatedAt: '2026-04-08T08:45:00Z',
1570
+ },
1571
+ ];
1825
1572
 
1826
- function normalizeCoreDataTitle(record: ${coreDataRecordTypeName}): string {
1827
- if (typeof record.title === 'string') {
1828
- return record.title;
1829
- }
1830
- if (record.title && typeof record.title === 'object') {
1831
- if (typeof record.title.rendered === 'string') {
1832
- return record.title.rendered;
1833
- }
1834
- if (typeof record.title.raw === 'string') {
1835
- return record.title.raw;
1836
- }
1573
+ function matchesSearch(item: ${itemTypeName}, search: string | undefined): boolean {
1574
+ if (!search) {
1575
+ return true;
1837
1576
  }
1838
1577
 
1839
- return normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug);
1840
- }
1841
-
1842
- function normalizeCoreDataUpdatedAt(record: ${coreDataRecordTypeName}): string {
1843
- return normalizeCoreDataString(record.modified) || normalizeCoreDataString(record.date);
1844
- }
1845
-
1846
- function normalizeCoreDataRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1847
- return {
1848
- id: record.id,
1849
- raw: record,
1850
- slug: normalizeCoreDataString(record.slug),
1851
- status: normalizeCoreDataString(record.status),
1852
- title: normalizeCoreDataTitle(record),
1853
- updatedAt: normalizeCoreDataUpdatedAt(record),
1854
- };
1855
- }
1856
-
1857
- export function ${useEntityRecordName}(recordId: number | undefined) {
1858
- return useEntityRecord<${coreDataRecordTypeName}>(
1859
- CORE_DATA_ENTITY_KIND,
1860
- CORE_DATA_ENTITY_NAME,
1861
- recordId ?? 0,
1862
- { enabled: typeof recordId === 'number' },
1578
+ const needle = search.toLowerCase();
1579
+ return [item.title, item.owner, item.status].some((value) =>
1580
+ value.toLowerCase().includes(needle),
1863
1581
  );
1864
1582
  }
1865
1583
 
1866
- export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1584
+ export async function ${fetchName}(
1585
+ view: DataViewsView<${itemTypeName}>,
1586
+ ): Promise<${dataSetTypeName}> {
1867
1587
  const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1868
- perPageParam: 'per_page',
1588
+ perPageParam: 'perPage',
1869
1589
  });
1870
-
1871
- return useEntityRecords<${coreDataRecordTypeName}>(
1872
- CORE_DATA_ENTITY_KIND,
1873
- CORE_DATA_ENTITY_NAME,
1874
- query,
1875
- );
1876
- }
1877
-
1878
- export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1879
- const { hasResolved, isResolving, records, totalItems, totalPages } =
1880
- ${useEntityRecordsName}(view);
1881
- const items = useMemo(
1882
- () => (records ?? []).map((record) => normalizeCoreDataRecord(record)),
1883
- [records],
1884
- );
1885
- const dataSet = useMemo<${dataSetTypeName}>(
1886
- () => ({
1887
- items,
1888
- paginationInfo: {
1889
- totalItems: totalItems ?? items.length,
1890
- totalPages: Math.max(1, totalPages ?? 1),
1891
- },
1892
- }),
1893
- [items, totalItems, totalPages],
1590
+ const requestedPage = query.page ?? 1;
1591
+ const page = requestedPage > 0 ? requestedPage : 1;
1592
+ const requestedPerPage = query.perPage ?? view.perPage ?? 10;
1593
+ const perPage = requestedPerPage > 0 ? requestedPerPage : 10;
1594
+ const filteredItems = STARTER_ITEMS.filter((item) =>
1595
+ matchesSearch(item, query.search),
1894
1596
  );
1895
- const error =
1896
- !isResolving && hasResolved && records === null
1897
- ? 'Unable to load core-data entity records.'
1898
- : null;
1597
+ const offset = (page - 1) * perPage;
1598
+ const items = filteredItems.slice(offset, offset + perPage);
1899
1599
 
1900
1600
  return {
1901
- dataSet,
1902
- error,
1903
- isLoading: isResolving,
1601
+ items,
1602
+ paginationInfo: {
1603
+ totalItems: filteredItems.length,
1604
+ totalPages: Math.max(1, Math.ceil(filteredItems.length / perPage)),
1605
+ },
1904
1606
  };
1905
1607
  }
1906
1608
  `;
@@ -2018,79 +1720,33 @@ export function ${componentName}() {
2018
1720
  }
2019
1721
  `;
2020
1722
  }
2021
- function buildCoreDataAdminViewScreenSource(adminViewSlug, textDomain) {
2022
- const pascalName = toPascalCase(adminViewSlug);
2023
- const camelName = toCamelCase(adminViewSlug);
2024
- const itemTypeName = `${pascalName}AdminViewItem`;
2025
- const dataSetTypeName = `${pascalName}AdminViewDataSet`;
2026
- const componentName = `${pascalName}AdminViewScreen`;
2027
- const dataViewsName = `${camelName}AdminDataViews`;
2028
- const useAdminViewDataName = `use${pascalName}AdminViewData`;
2029
- const title = toTitleCase(adminViewSlug);
2030
- return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
2031
- import { Notice, Spinner } from '@wordpress/components';
2032
- import { useState } from '@wordpress/element';
2033
- import { __ } from '@wordpress/i18n';
2034
- import { DataViews } from '@wordpress/dataviews/wp';
2035
1723
 
2036
- import { ${dataViewsName} } from './config';
2037
- import { ${useAdminViewDataName} } from './data';
2038
- import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1724
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates-shared.ts
1725
+ import path4 from "path";
1726
+ function getAdminViewRelativeModuleSpecifier(adminViewSlug, workspaceFile) {
1727
+ const adminViewDir = `src/admin-views/${adminViewSlug}`;
1728
+ const normalizedFile = workspaceFile.replace(/\\/gu, "/");
1729
+ const modulePath = normalizedFile.replace(/\.[cm]?[jt]sx?$/u, "");
1730
+ const relativeModulePath = path4.posix.relative(adminViewDir, modulePath);
1731
+ return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
1732
+ }
1733
+ function buildAdminViewConfigEntry(adminViewSlug, source) {
1734
+ return [
1735
+ "\t{",
1736
+ ` file: ${quoteTsString(`src/admin-views/${adminViewSlug}/index.tsx`)},`,
1737
+ ` phpFile: ${quoteTsString(`inc/admin-views/${adminViewSlug}.php`)},`,
1738
+ ` slug: ${quoteTsString(adminViewSlug)},`,
1739
+ source ? ` source: ${quoteTsString(formatAdminViewSourceLocator(source))},` : null,
1740
+ "\t},"
1741
+ ].filter((line) => typeof line === "string").join(`
1742
+ `);
1743
+ }
1744
+ function buildAdminViewRegistrySource(adminViewSlugs) {
1745
+ const importLines = adminViewSlugs.map((adminViewSlug) => `import './${adminViewSlug}';`).join(`
1746
+ `);
1747
+ return `${importLines}${importLines ? `
2039
1748
 
2040
- const TypedDataViews = DataViews as unknown as <TItem extends object>(
2041
- props: DataViewsConfig<TItem>,
2042
- ) => ReturnType<typeof DataViews>;
2043
-
2044
- const EMPTY_DATA_SET: ${dataSetTypeName} = {
2045
- items: [],
2046
- paginationInfo: {
2047
- totalItems: 0,
2048
- totalPages: 1,
2049
- },
2050
- };
2051
-
2052
- export function ${componentName}() {
2053
- const [view, setView] = useState<DataViewsView<${itemTypeName}>>(
2054
- ${dataViewsName}.defaultView,
2055
- );
2056
- const {
2057
- dataSet = EMPTY_DATA_SET,
2058
- error,
2059
- isLoading,
2060
- } = ${useAdminViewDataName}(view);
2061
- const config = ${dataViewsName}.createConfig({
2062
- data: dataSet.items,
2063
- isLoading,
2064
- onChangeView: setView,
2065
- paginationInfo: dataSet.paginationInfo,
2066
- view,
2067
- });
2068
-
2069
- return (
2070
- <div className="wp-typia-admin-view-screen">
2071
- <header className="wp-typia-admin-view-screen__header">
2072
- <div>
2073
- <p className="wp-typia-admin-view-screen__eyebrow">
2074
- { __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
2075
- </p>
2076
- <h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
2077
- <p>
2078
- { __( 'This screen reads from the WordPress core-data entity store. Extend data.ts when you need entity-specific field mapping or edit flows.', ${quoteTsString(textDomain)} ) }
2079
- </p>
2080
- </div>
2081
- <div className="wp-typia-admin-view-screen__actions">
2082
- { isLoading ? <Spinner /> : null }
2083
- </div>
2084
- </header>
2085
- { error ? (
2086
- <Notice isDismissible={ false } status="error">
2087
- { error }
2088
- </Notice>
2089
- ) : null }
2090
- <TypedDataViews<${itemTypeName}> { ...config } />
2091
- </div>
2092
- );
2093
- }
1749
+ ` : ""}// wp-typia add admin-view entries
2094
1750
  `;
2095
1751
  }
2096
1752
  function buildAdminViewEntrySource(adminViewSlug, options = {}) {
@@ -2273,12 +1929,473 @@ if ( ! function_exists( '${enqueueFunctionName}' ) ) {
2273
1929
  }
2274
1930
  }
2275
1931
  }
2276
-
2277
- add_action( 'admin_menu', '${registerFunctionName}' );
2278
- add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );
1932
+
1933
+ add_action( 'admin_menu', '${registerFunctionName}' );
1934
+ add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );
1935
+ `;
1936
+ }
1937
+
1938
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates-rest.ts
1939
+ function buildRestAdminViewTypesSource(adminViewSlug, restResource) {
1940
+ const pascalName = toPascalCase(adminViewSlug);
1941
+ const restPascalName = toPascalCase(restResource.slug);
1942
+ const itemTypeName = `${pascalName}AdminViewItem`;
1943
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1944
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1945
+ return `import type { ${restPascalName}Record } from ${quoteTsString(restTypesModule)};
1946
+
1947
+ export type ${itemTypeName} = ${restPascalName}Record;
1948
+
1949
+ export interface ${dataSetTypeName} {
1950
+ items: ${itemTypeName}[];
1951
+ paginationInfo: {
1952
+ totalItems: number;
1953
+ totalPages: number;
1954
+ };
1955
+ }
1956
+ `;
1957
+ }
1958
+ function buildRestAdminViewConfigSource(adminViewSlug, textDomain) {
1959
+ const pascalName = toPascalCase(adminViewSlug);
1960
+ const camelName = toCamelCase(adminViewSlug);
1961
+ const itemTypeName = `${pascalName}AdminViewItem`;
1962
+ const dataViewsName = `${camelName}AdminDataViews`;
1963
+ return `import { defineDataViews } from '@wp-typia/dataviews';
1964
+ import { __ } from '@wordpress/i18n';
1965
+
1966
+ import type { ${itemTypeName} } from './types';
1967
+
1968
+ export const ${dataViewsName} = defineDataViews<${itemTypeName}>({
1969
+ idField: 'id',
1970
+ search: false,
1971
+ searchLabel: __( 'Search records', ${quoteTsString(textDomain)} ),
1972
+ defaultView: {
1973
+ fields: ['id'],
1974
+ page: 1,
1975
+ perPage: 10,
1976
+ type: 'table',
1977
+ },
1978
+ fields: {
1979
+ id: {
1980
+ enableHiding: false,
1981
+ label: __( 'ID', ${quoteTsString(textDomain)} ),
1982
+ readOnly: true,
1983
+ schema: { type: 'integer' },
1984
+ },
1985
+ // REST-backed screens start with the guaranteed ID column. Add project-owned fields here once they are declared on the REST record type.
1986
+ },
1987
+ });
1988
+ `;
1989
+ }
1990
+ function buildRestAdminViewDataSource(adminViewSlug, restResource) {
1991
+ const pascalName = toPascalCase(adminViewSlug);
1992
+ const restPascalName = toPascalCase(restResource.slug);
1993
+ const camelName = toCamelCase(adminViewSlug);
1994
+ const itemTypeName = `${pascalName}AdminViewItem`;
1995
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1996
+ const dataViewsName = `${camelName}AdminDataViews`;
1997
+ const fetchName = `fetch${pascalName}AdminViewData`;
1998
+ const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
1999
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
2000
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
2001
+
2002
+ import { listResource } from ${quoteTsString(restApiModule)};
2003
+ import type { ${restPascalName}ListQuery } from ${quoteTsString(restTypesModule)};
2004
+ import { ${dataViewsName} } from './config';
2005
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
2006
+
2007
+ function resolveTotalPages(total: number, perPage: number | undefined): number {
2008
+ const resolvedPerPage = perPage && perPage > 0 ? perPage : 1;
2009
+ return Math.max(1, Math.ceil(total / resolvedPerPage));
2010
+ }
2011
+
2012
+ export async function ${fetchName}(
2013
+ view: DataViewsView<${itemTypeName}>,
2014
+ ): Promise<${dataSetTypeName}> {
2015
+ const query = ${dataViewsName}.toQueryArgs<${restPascalName}ListQuery>(view, {
2016
+ perPageParam: 'perPage',
2017
+ searchParam: false,
2018
+ });
2019
+ const result = await listResource({
2020
+ page: query.page,
2021
+ perPage: query.perPage,
2022
+ });
2023
+ if (!result.isValid || !result.data) {
2024
+ throw new Error('Unable to load REST resource records.');
2025
+ }
2026
+
2027
+ const response = result.data;
2028
+
2029
+ return {
2030
+ items: response.items,
2031
+ paginationInfo: {
2032
+ totalItems: response.total,
2033
+ totalPages: resolveTotalPages(response.total, response.perPage ?? query.perPage),
2034
+ },
2035
+ };
2036
+ }
2037
+ `;
2038
+ }
2039
+
2040
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates-settings.ts
2041
+ function buildRestSettingsAdminViewTypesSource(adminViewSlug, restResource) {
2042
+ const pascalName = toPascalCase(adminViewSlug);
2043
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
2044
+ const formStateTypeName = `${pascalName}SettingsFormState`;
2045
+ const loadResultTypeName = `${pascalName}SettingsLoadResult`;
2046
+ return `import type {
2047
+ ${restResource.bodyTypeName},
2048
+ ${restResource.queryTypeName},
2049
+ ${restResource.responseTypeName},
2050
+ } from ${quoteTsString(restTypesModule)};
2051
+
2052
+ export type ${pascalName}SettingsRequest = ${restResource.bodyTypeName};
2053
+ export type ${pascalName}SettingsQuery = ${restResource.queryTypeName};
2054
+ export type ${pascalName}SettingsResponse = ${restResource.responseTypeName};
2055
+ export type ${formStateTypeName} = Partial<${pascalName}SettingsRequest>;
2056
+
2057
+ export interface ${loadResultTypeName} {
2058
+ form: ${formStateTypeName};
2059
+ response: ${pascalName}SettingsResponse | null;
2060
+ }
2061
+ `;
2062
+ }
2063
+ function buildRestSettingsAdminViewConfigSource(adminViewSlug, textDomain, restResource) {
2064
+ const pascalName = toPascalCase(adminViewSlug);
2065
+ const camelName = toCamelCase(adminViewSlug);
2066
+ const title = toTitleCase(adminViewSlug);
2067
+ const configName = `${camelName}SettingsConfig`;
2068
+ const formStateTypeName = `${pascalName}SettingsFormState`;
2069
+ const secretPreserveOnEmpty = restResource.secretPreserveOnEmpty !== false;
2070
+ const secretFieldSource = restResource.secretFieldName && restResource.secretStateFieldName ? ` {
2071
+ description: __( ${quoteTsString(secretPreserveOnEmpty ? "Write-only secret value. Leave blank to keep the existing secret." : "Write-only secret value. Blank submissions are sent to the REST route.")}, ${quoteTsString(textDomain)} ),
2072
+ id: ${quoteTsString(restResource.secretFieldName)},
2073
+ label: __( ${quoteTsString(toTitleCase(restResource.secretFieldName))}, ${quoteTsString(textDomain)} ),
2074
+ preserveOnEmpty: ${secretPreserveOnEmpty},
2075
+ secretStateField: ${quoteTsString(restResource.secretStateFieldName)},
2076
+ type: 'secret',
2077
+ },` : "";
2078
+ return `import { __ } from '@wordpress/i18n';
2079
+
2080
+ import type { ${formStateTypeName} } from './types';
2081
+
2082
+ export type ${pascalName}SettingsFieldType = 'secret' | 'text' | 'textarea';
2083
+
2084
+ export interface ${pascalName}SettingsField {
2085
+ description?: string;
2086
+ id: Extract<keyof ${formStateTypeName}, string> | string;
2087
+ label: string;
2088
+ preserveOnEmpty?: boolean;
2089
+ secretStateField?: string;
2090
+ type: ${pascalName}SettingsFieldType;
2091
+ }
2092
+
2093
+ export const ${configName} = {
2094
+ description: __( 'This generated settings form is backed by the ${restResource.slug} REST contract. Adjust config.ts and data.ts as the contract becomes product-specific.', ${quoteTsString(textDomain)} ),
2095
+ fields: [
2096
+ {
2097
+ description: __( 'Primary settings payload for this integration.', ${quoteTsString(textDomain)} ),
2098
+ id: 'payload',
2099
+ label: __( 'Payload', ${quoteTsString(textDomain)} ),
2100
+ type: 'textarea',
2101
+ },
2102
+ {
2103
+ description: __( 'Optional operator note included with the save request.', ${quoteTsString(textDomain)} ),
2104
+ id: 'comment',
2105
+ label: __( 'Comment', ${quoteTsString(textDomain)} ),
2106
+ type: 'text',
2107
+ },
2108
+ ${secretFieldSource}
2109
+ ] satisfies ${pascalName}SettingsField[],
2110
+ secretFieldName: ${restResource.secretFieldName ? quoteTsString(restResource.secretFieldName) : "undefined"},
2111
+ secretPreserveOnEmpty: ${restResource.secretFieldName ? secretPreserveOnEmpty : "undefined"},
2112
+ secretStateFieldName: ${restResource.secretStateFieldName ? quoteTsString(restResource.secretStateFieldName) : "undefined"},
2113
+ title: __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ),
2114
+ };
2115
+ `;
2116
+ }
2117
+ function buildRestSettingsAdminViewDataSource(adminViewSlug, restResource) {
2118
+ const pascalName = toPascalCase(adminViewSlug);
2119
+ const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
2120
+ const formStateTypeName = `${pascalName}SettingsFormState`;
2121
+ const loadResultTypeName = `${pascalName}SettingsLoadResult`;
2122
+ const loadName = `load${pascalName}Settings`;
2123
+ const saveName = `save${pascalName}Settings`;
2124
+ const secretPreserveOnEmpty = restResource.secretPreserveOnEmpty !== false;
2125
+ const initialFieldLines = [
2126
+ "\tpayload: '',",
2127
+ "\tcomment: '',",
2128
+ ...restResource.secretFieldName && !secretPreserveOnEmpty ? [` ${quoteTsString(restResource.secretFieldName)}: '',`] : []
2129
+ ];
2130
+ const initialFields = initialFieldLines.join(`
2131
+ `);
2132
+ const requestBodySource = restResource.secretFieldName && secretPreserveOnEmpty ? ` const requestBody = { ...form } as Record<string, unknown>;
2133
+ if (requestBody[${quoteTsString(restResource.secretFieldName)}] === '') {
2134
+ delete requestBody[${quoteTsString(restResource.secretFieldName)}];
2135
+ }
2136
+ ` : ` const requestBody = form as Record<string, unknown>;
2137
+ `;
2138
+ return `import { callManualRestContract } from ${quoteTsString(restApiModule)};
2139
+ import type {
2140
+ ${formStateTypeName},
2141
+ ${loadResultTypeName},
2142
+ ${pascalName}SettingsQuery,
2143
+ ${pascalName}SettingsRequest,
2144
+ ${pascalName}SettingsResponse,
2145
+ } from './types';
2146
+
2147
+ function formatValidationError(prefix: string, errors: unknown): string {
2148
+ if (!Array.isArray(errors) || errors.length === 0) {
2149
+ return prefix;
2150
+ }
2151
+
2152
+ return \`\${prefix} \${JSON.stringify(errors)}\`;
2153
+ }
2154
+
2155
+ export function createInitial${pascalName}SettingsFormState(): ${formStateTypeName} {
2156
+ return {
2157
+ ${initialFields}
2158
+ } as ${formStateTypeName};
2159
+ }
2160
+
2161
+ export async function ${loadName}(): Promise<${loadResultTypeName}> {
2162
+ return {
2163
+ form: createInitial${pascalName}SettingsFormState(),
2164
+ response: null,
2165
+ };
2166
+ }
2167
+
2168
+ export async function ${saveName}(
2169
+ form: ${formStateTypeName},
2170
+ query: Partial<${pascalName}SettingsQuery> = {},
2171
+ ): Promise<${pascalName}SettingsResponse> {
2172
+ ${requestBodySource}
2173
+ const result = await callManualRestContract({
2174
+ body: requestBody as unknown as ${pascalName}SettingsRequest,
2175
+ query: query as ${pascalName}SettingsQuery,
2176
+ });
2177
+ if (!result.isValid) {
2178
+ const message =
2179
+ result.validationTarget === 'request'
2180
+ ? 'Settings request failed validation.'
2181
+ : 'Settings response failed validation.';
2182
+ throw new Error(formatValidationError(message, result.errors));
2183
+ }
2184
+
2185
+ return result.data as ${pascalName}SettingsResponse;
2186
+ }
2187
+ `;
2188
+ }
2189
+ function buildRestSettingsAdminViewScreenSource(adminViewSlug, textDomain) {
2190
+ const pascalName = toPascalCase(adminViewSlug);
2191
+ const componentName = `${pascalName}AdminViewScreen`;
2192
+ const formStateTypeName = `${pascalName}SettingsFormState`;
2193
+ const responseTypeName = `${pascalName}SettingsResponse`;
2194
+ const camelName = toCamelCase(adminViewSlug);
2195
+ const configName = `${camelName}SettingsConfig`;
2196
+ const loadName = `load${pascalName}Settings`;
2197
+ const saveName = `save${pascalName}Settings`;
2198
+ return `import {
2199
+ Button,
2200
+ Notice,
2201
+ Spinner,
2202
+ TextControl,
2203
+ TextareaControl,
2204
+ } from '@wordpress/components';
2205
+ import { useEffect, useState } from '@wordpress/element';
2206
+ import { __ } from '@wordpress/i18n';
2207
+
2208
+ import { ${configName} } from './config';
2209
+ import { ${loadName}, ${saveName} } from './data';
2210
+ import type { ${formStateTypeName}, ${responseTypeName} } from './types';
2211
+
2212
+ function getFieldValue(form: ${formStateTypeName}, fieldId: string): string {
2213
+ const value = (form as Record<string, unknown>)[fieldId];
2214
+ if (typeof value === 'string') {
2215
+ return value;
2216
+ }
2217
+ if (value == null) {
2218
+ return '';
2219
+ }
2220
+
2221
+ return String(value);
2222
+ }
2223
+
2224
+ function getSecretState(response: ${responseTypeName} | null): boolean | null {
2225
+ const stateField = ${configName}.secretStateFieldName;
2226
+ if (!stateField || !response) {
2227
+ return null;
2228
+ }
2229
+
2230
+ const value = (response as unknown as Record<string, unknown>)[stateField];
2231
+ return typeof value === 'boolean' ? value : null;
2232
+ }
2233
+
2234
+ export function ${componentName}() {
2235
+ const [form, setForm] = useState<${formStateTypeName}>({});
2236
+ const [response, setResponse] = useState<${responseTypeName} | null>(null);
2237
+ const [error, setError] = useState<string | null>(null);
2238
+ const [isLoading, setIsLoading] = useState(true);
2239
+ const [isSaving, setIsSaving] = useState(false);
2240
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
2241
+
2242
+ useEffect(() => {
2243
+ let isCurrent = true;
2244
+ setIsLoading(true);
2245
+ setError(null);
2246
+
2247
+ void ${loadName}()
2248
+ .then((result) => {
2249
+ if (isCurrent) {
2250
+ setForm(result.form);
2251
+ setResponse(result.response);
2252
+ }
2253
+ })
2254
+ .catch((nextError: unknown) => {
2255
+ if (isCurrent) {
2256
+ setError(
2257
+ nextError instanceof Error
2258
+ ? nextError.message
2259
+ : __( 'Unable to prepare settings form.', ${quoteTsString(textDomain)} ),
2260
+ );
2261
+ }
2262
+ })
2263
+ .finally(() => {
2264
+ if (isCurrent) {
2265
+ setIsLoading(false);
2266
+ }
2267
+ });
2268
+
2269
+ return () => {
2270
+ isCurrent = false;
2271
+ };
2272
+ }, []);
2273
+
2274
+ const setFormValue = (fieldId: string, value: string) => {
2275
+ setForm(
2276
+ (current) =>
2277
+ ({
2278
+ ...current,
2279
+ [fieldId]: value,
2280
+ }) as ${formStateTypeName},
2281
+ );
2282
+ };
2283
+ const secretState = getSecretState(response);
2284
+
2285
+ const handleSubmit = (event: { preventDefault: () => void }) => {
2286
+ event.preventDefault();
2287
+ setError(null);
2288
+ setSuccessMessage(null);
2289
+ setIsSaving(true);
2290
+
2291
+ void ${saveName}(form)
2292
+ .then((nextResponse) => {
2293
+ setResponse(nextResponse);
2294
+ setSuccessMessage(__( 'Settings saved.', ${quoteTsString(textDomain)} ));
2295
+ })
2296
+ .catch((nextError: unknown) => {
2297
+ setError(
2298
+ nextError instanceof Error
2299
+ ? nextError.message
2300
+ : __( 'Unable to save settings.', ${quoteTsString(textDomain)} ),
2301
+ );
2302
+ })
2303
+ .finally(() => setIsSaving(false));
2304
+ };
2305
+
2306
+ return (
2307
+ <div className="wp-typia-admin-view-screen wp-typia-admin-view-screen--settings">
2308
+ <header className="wp-typia-admin-view-screen__header">
2309
+ <div>
2310
+ <p className="wp-typia-admin-view-screen__eyebrow">
2311
+ { __( 'Typed settings screen', ${quoteTsString(textDomain)} ) }
2312
+ </p>
2313
+ <h1>{ ${configName}.title }</h1>
2314
+ <p>{ ${configName}.description }</p>
2315
+ </div>
2316
+ <div className="wp-typia-admin-view-screen__actions">
2317
+ { isLoading || isSaving ? <Spinner /> : null }
2318
+ </div>
2319
+ </header>
2320
+ { error ? (
2321
+ <Notice isDismissible={ false } status="error">
2322
+ { error }
2323
+ </Notice>
2324
+ ) : null }
2325
+ { successMessage ? (
2326
+ <Notice isDismissible={ false } status="success">
2327
+ { successMessage }
2328
+ </Notice>
2329
+ ) : null }
2330
+ { secretState !== null ? (
2331
+ <Notice isDismissible={ false } status="info">
2332
+ { secretState
2333
+ ? __( 'A secret is currently configured for this integration.', ${quoteTsString(textDomain)} )
2334
+ : __( 'No secret is currently configured for this integration.', ${quoteTsString(textDomain)} ) }
2335
+ </Notice>
2336
+ ) : null }
2337
+ <form className="wp-typia-admin-view-screen__settings-form" onSubmit={ handleSubmit }>
2338
+ { ${configName}.fields.map((field) => (
2339
+ <div className="wp-typia-admin-view-screen__field" key={ field.id }>
2340
+ { field.type === 'textarea' ? (
2341
+ <TextareaControl
2342
+ help={ field.description }
2343
+ label={ field.label }
2344
+ onChange={ (value) => setFormValue(field.id, value) }
2345
+ value={ getFieldValue(form, field.id) }
2346
+ />
2347
+ ) : (
2348
+ <TextControl
2349
+ help={ field.description }
2350
+ label={ field.label }
2351
+ onChange={ (value) => setFormValue(field.id, value) }
2352
+ type={ field.type === 'secret' ? 'password' : 'text' }
2353
+ value={ getFieldValue(form, field.id) }
2354
+ />
2355
+ ) }
2356
+ </div>
2357
+ )) }
2358
+ <Button
2359
+ disabled={ isLoading || isSaving }
2360
+ isBusy={ isSaving }
2361
+ type="submit"
2362
+ variant="primary"
2363
+ >
2364
+ { __( 'Save settings', ${quoteTsString(textDomain)} ) }
2365
+ </Button>
2366
+ </form>
2367
+ </div>
2368
+ );
2369
+ }
2279
2370
  `;
2280
2371
  }
2281
2372
 
2373
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-templates.ts
2374
+ function buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource) {
2375
+ if (restResource) {
2376
+ return buildRestAdminViewTypesSource(adminViewSlug, restResource);
2377
+ }
2378
+ if (coreDataSource) {
2379
+ return buildCoreDataAdminViewTypesSource(adminViewSlug, coreDataSource);
2380
+ }
2381
+ return buildDefaultAdminViewTypesSource(adminViewSlug);
2382
+ }
2383
+ function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResource) {
2384
+ if (restResource) {
2385
+ return buildRestAdminViewConfigSource(adminViewSlug, textDomain);
2386
+ }
2387
+ if (isAdminViewCoreDataSource(source)) {
2388
+ return buildCoreDataAdminViewConfigSource(adminViewSlug, textDomain, source);
2389
+ }
2390
+ return buildDefaultAdminViewConfigSource(adminViewSlug, textDomain);
2391
+ }
2392
+ function buildAdminViewScreenSource2(adminViewSlug, textDomain) {
2393
+ return buildAdminViewScreenSource(adminViewSlug, textDomain);
2394
+ }
2395
+ function buildAdminViewPhpSource2(adminViewSlug, workspace) {
2396
+ return buildAdminViewPhpSource(adminViewSlug, workspace);
2397
+ }
2398
+
2282
2399
  // ../wp-typia-project-tools/src/runtime/rest-resource-artifacts.ts
2283
2400
  import path5 from "path";
2284
2401
  import {
@@ -2581,18 +2698,22 @@ function buildManualRestContractConfigEntry(options) {
2581
2698
  ` auth: ${quoteTsString(options.auth)},`,
2582
2699
  ...options.bodyTypeName ? [` bodyTypeName: ${quoteTsString(options.bodyTypeName)},`] : [],
2583
2700
  ` clientFile: ${quoteTsString(`src/rest/${options.restResourceSlug}/api-client.ts`)},`,
2701
+ ...options.controllerClass ? [` controllerClass: ${quoteTsString(options.controllerClass)},`] : [],
2702
+ ...options.controllerExtends ? [` controllerExtends: ${quoteTsString(options.controllerExtends)},`] : [],
2584
2703
  ` method: ${quoteTsString(options.method)},`,
2585
2704
  "\t\tmethods: [],",
2586
2705
  "\t\tmode: 'manual',",
2587
2706
  ` namespace: ${quoteTsString(options.namespace)},`,
2588
2707
  ` openApiFile: ${quoteTsString(`src/rest/${options.restResourceSlug}/api.openapi.json`)},`,
2589
2708
  ` pathPattern: ${quoteTsString(options.pathPattern)},`,
2709
+ ...options.permissionCallback ? [` permissionCallback: ${quoteTsString(options.permissionCallback)},`] : [],
2590
2710
  ` queryTypeName: ${quoteTsString(options.queryTypeName)},`,
2591
2711
  "\t\trestManifest: defineEndpointManifest(",
2592
2712
  indentMultiline(JSON.stringify(manifest, null, "\t"), "\t\t\t"),
2593
2713
  "\t\t),",
2594
2714
  ` responseTypeName: ${quoteTsString(options.responseTypeName)},`,
2595
2715
  ...options.secretFieldName ? [` secretFieldName: ${quoteTsString(options.secretFieldName)},`] : [],
2716
+ ...options.secretPreserveOnEmpty !== undefined ? [` secretPreserveOnEmpty: ${options.secretPreserveOnEmpty},`] : [],
2596
2717
  ...options.secretStateFieldName ? [` secretStateFieldName: ${quoteTsString(options.secretStateFieldName)},`] : [],
2597
2718
  ` slug: ${quoteTsString(options.restResourceSlug)},`,
2598
2719
  ` typesFile: ${quoteTsString(`src/rest/${options.restResourceSlug}/api-types.ts`)},`,
@@ -2603,18 +2724,21 @@ function buildManualRestContractConfigEntry(options) {
2603
2724
  }
2604
2725
  function buildManualRestContractTypesSource(options) {
2605
2726
  const title = toTitleCase(options.restResourceSlug);
2727
+ const pathParameterNames = Array.from(new Set(options.pathParameterNames ?? []));
2728
+ const queryFields = pathParameterNames.length > 0 ? pathParameterNames.map((parameterName) => ` ${parameterName}: string & tags.MinLength< 1 >;`) : ["\tid?: string & tags.MinLength< 1 >;"];
2606
2729
  const lines = [
2607
2730
  "import type { tags } from '@wp-typia/block-runtime/typia-tags';",
2608
2731
  "",
2609
2732
  `export interface ${options.queryTypeName} {`,
2610
- "\tid?: string & tags.MinLength< 1 >;",
2611
- "\tpreview?: boolean;",
2733
+ ...queryFields,
2734
+ ...pathParameterNames.includes("preview") ? [] : ["\tpreview?: boolean;"],
2612
2735
  "}"
2613
2736
  ];
2614
2737
  if (options.bodyTypeName) {
2738
+ const secretPreserveOnEmpty = options.secretPreserveOnEmpty ?? true;
2615
2739
  const secretLines = options.secretFieldName && options.secretStateFieldName ? [
2616
- ` ${options.secretFieldName}?: string & tags.MinLength< 1 > & tags.MaxLength< 4096 > & tags.Secret< ${quoteTsString(options.secretStateFieldName)} >;`,
2617
- ` // ${options.secretFieldName} is write-only: persist it server-side and expose ${options.secretStateFieldName} in responses instead of returning the raw value.`
2740
+ ` ${options.secretFieldName}?: string${secretPreserveOnEmpty ? " & tags.MinLength< 1 >" : ""} & tags.MaxLength< 4096 > & tags.Secret< ${quoteTsString(options.secretStateFieldName)} >${secretPreserveOnEmpty ? " & tags.PreserveOnEmpty< true >" : ""};`,
2741
+ secretPreserveOnEmpty ? ` // ${options.secretFieldName} is write-only: omit or submit an empty value to preserve the stored secret, and expose ${options.secretStateFieldName} in responses instead of returning the raw value.` : ` // ${options.secretFieldName} is write-only: persist it server-side and expose ${options.secretStateFieldName} in responses instead of returning the raw value.`
2618
2742
  ] : [];
2619
2743
  lines.push("", `export interface ${options.bodyTypeName} {`, ...secretLines, "\tpayload: string & tags.MinLength< 1 >;", "\tcomment?: string & tags.MaxLength< 500 >;", "}");
2620
2744
  }
@@ -3105,67 +3229,6 @@ ${exportedBindings.join(`
3105
3229
  `;
3106
3230
  }
3107
3231
 
3108
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-mutation.ts
3109
- var DEFAULT_PHP_SNIPPET_INSERTION_ANCHORS = [
3110
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
3111
- /\?>\s*$/u
3112
- ];
3113
-
3114
- class WorkspaceMutationRollbackError extends Error {
3115
- mutationError;
3116
- rollbackError;
3117
- constructor(mutationError, rollbackError) {
3118
- super("Workspace mutation failed and rollback also failed.");
3119
- this.name = "WorkspaceMutationRollbackError";
3120
- this.mutationError = mutationError;
3121
- this.rollbackError = rollbackError;
3122
- }
3123
- }
3124
- async function executeWorkspaceMutationPlan({
3125
- filePaths,
3126
- run,
3127
- snapshotDirs = [],
3128
- targetPaths = []
3129
- }) {
3130
- const mutationSnapshot = {
3131
- fileSources: await snapshotWorkspaceFiles(filePaths),
3132
- snapshotDirs: [...snapshotDirs],
3133
- targetPaths: [...targetPaths]
3134
- };
3135
- try {
3136
- return await run();
3137
- } catch (error) {
3138
- try {
3139
- await rollbackWorkspaceMutation(mutationSnapshot);
3140
- } catch (rollbackError) {
3141
- throw new WorkspaceMutationRollbackError(error, rollbackError);
3142
- }
3143
- throw error;
3144
- }
3145
- }
3146
- function insertPhpSnippetBeforeWorkspaceAnchors(source, snippet) {
3147
- for (const anchor of DEFAULT_PHP_SNIPPET_INSERTION_ANCHORS) {
3148
- const candidate = source.replace(anchor, (match) => `${snippet}
3149
- ${match}`);
3150
- if (candidate !== source) {
3151
- return candidate;
3152
- }
3153
- }
3154
- return `${source.trimEnd()}
3155
- ${snippet}
3156
- `;
3157
- }
3158
- function appendPhpSnippetBeforeClosingTag(source, snippet) {
3159
- const closingTagPattern = /\?>\s*$/u;
3160
- if (closingTagPattern.test(source)) {
3161
- return source.replace(closingTagPattern, `${snippet}
3162
- ?>`);
3163
- }
3164
- return `${source.trimEnd()}
3165
- ${snippet}
3166
- `;
3167
- }
3168
-
3169
3232
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view-scaffold.ts
3170
3233
  function detectJsonIndent(source) {
3171
3234
  const indentMatch = /\n([ \t]+)"/u.exec(source);
@@ -3218,7 +3281,10 @@ async function ensureAdminViewPackageDependencies(workspace, adminViewSource, re
3218
3281
  packageName: "@wordpress/data"
3219
3282
  });
3220
3283
  await patchFile(packageJsonPath, (source) => {
3221
- const packageJson = JSON.parse(source);
3284
+ const packageJson = safeJsonParse(source, {
3285
+ context: "admin view package manifest",
3286
+ filePath: packageJsonPath
3287
+ });
3222
3288
  const needsDataViews = !isAdminViewManualSettingsRestResource(restResource);
3223
3289
  const coreDataDependencies = isAdminViewCoreDataSource(adminViewSource) ? {
3224
3290
  "@wordpress/core-data": packageJson.dependencies?.["@wordpress/core-data"] ?? wordpressCoreDataVersion,
@@ -3429,12 +3495,12 @@ async function scaffoldAdminViewWorkspace(options) {
3429
3495
  await fsp3.writeFile(path6.join(adminViewDir, "types.ts"), manualSettingsRestResource ? buildRestSettingsAdminViewTypesSource(adminViewSlug, manualSettingsRestResource) : buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), "utf8");
3430
3496
  await fsp3.writeFile(path6.join(adminViewDir, "config.ts"), manualSettingsRestResource ? buildRestSettingsAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, manualSettingsRestResource) : buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), "utf8");
3431
3497
  await fsp3.writeFile(path6.join(adminViewDir, "data.ts"), manualSettingsRestResource ? buildRestSettingsAdminViewDataSource(adminViewSlug, manualSettingsRestResource) : coreDataSource ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) : restResource ? buildRestAdminViewDataSource(adminViewSlug, restResource) : buildDefaultAdminViewDataSource(adminViewSlug), "utf8");
3432
- await fsp3.writeFile(path6.join(adminViewDir, "Screen.tsx"), manualSettingsRestResource ? buildRestSettingsAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain) : coreDataSource ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain) : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), "utf8");
3498
+ await fsp3.writeFile(path6.join(adminViewDir, "Screen.tsx"), manualSettingsRestResource ? buildRestSettingsAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain) : coreDataSource ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain) : buildAdminViewScreenSource2(adminViewSlug, workspace.workspace.textDomain), "utf8");
3433
3499
  await fsp3.writeFile(path6.join(adminViewDir, "index.tsx"), buildAdminViewEntrySource(adminViewSlug, {
3434
3500
  includeDataViewsStyle: !manualSettingsRestResource
3435
3501
  }), "utf8");
3436
3502
  await fsp3.writeFile(path6.join(adminViewDir, "style.scss"), buildAdminViewStyleSource(), "utf8");
3437
- await fsp3.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), "utf8");
3503
+ await fsp3.writeFile(adminViewPhpPath, buildAdminViewPhpSource2(adminViewSlug, workspace), "utf8");
3438
3504
  await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
3439
3505
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
3440
3506
  adminViewEntries: [
@@ -4332,486 +4398,107 @@ async function runAddPatternCommand({
4332
4398
  throw error;
4333
4399
  }
4334
4400
  }
4335
- async function runAddBindingSourceCommand({
4336
- attributeName,
4337
- bindingSourceName,
4338
- blockName,
4339
- cwd = process.cwd()
4340
- }) {
4341
- const workspace = resolveWorkspaceProject(cwd);
4342
- const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
4343
- const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
4344
- assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
4345
- const target = resolveBindingTarget({
4346
- attributeName,
4347
- blockName
4348
- }, workspace.workspace.namespace);
4349
- const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
4350
- const blockConfigPath = path7.join(workspace.projectDir, "scripts", "block-config.ts");
4351
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4352
- const bindingsIndexPath = await resolveBindingSourceRegistryPath(workspace.projectDir);
4353
- const bindingSourceDir = path7.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
4354
- const serverFilePath = path7.join(bindingSourceDir, "server.php");
4355
- const editorFilePath = path7.join(bindingSourceDir, "editor.ts");
4356
- const blockJsonPath = target ? path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "block.json") : undefined;
4357
- const targetGeneratedMetadataPaths = target ? [
4358
- path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.manifest.json"),
4359
- path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.openapi.json"),
4360
- path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.schema.json"),
4361
- path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia-validator.php")
4362
- ] : [];
4363
- const mutationSnapshot = {
4364
- fileSources: await snapshotWorkspaceFiles([
4365
- blockConfigPath,
4366
- bootstrapPath,
4367
- bindingsIndexPath,
4368
- ...blockJsonPath ? [blockJsonPath] : [],
4369
- ...targetBlock ? [path7.join(workspace.projectDir, targetBlock.typesFile)] : [],
4370
- ...targetGeneratedMetadataPaths
4371
- ]),
4372
- snapshotDirs: [],
4373
- targetPaths: [bindingSourceDir]
4374
- };
4375
- try {
4376
- await fsp4.mkdir(bindingSourceDir, { recursive: true });
4377
- await ensureBindingSourceBootstrapAnchors(workspace);
4378
- await fsp4.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
4379
- await fsp4.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
4380
- if (target && targetBlock) {
4381
- await ensureBindingTargetBlockAttributeType(workspace.projectDir, targetBlock, target);
4382
- }
4383
- await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
4384
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
4385
- bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug, target)]
4386
- });
4387
- return {
4388
- ...target ? { attributeName: target.attributeName, blockSlug: target.blockSlug } : {},
4389
- bindingSourceSlug,
4390
- projectDir: workspace.projectDir
4391
- };
4392
- } catch (error) {
4393
- await rollbackWorkspaceMutation(mutationSnapshot);
4394
- throw error;
4395
- }
4396
- }
4397
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-integration-env.ts
4398
- import { promises as fsp5 } from "fs";
4399
- import path8 from "path";
4400
- var WP_ENV_PACKAGE_VERSION = "^11.2.0";
4401
- function buildWpEnvConfigSource() {
4402
- return `${JSON.stringify({
4403
- $schema: "https://schemas.wp.org/trunk/wp-env.json",
4404
- core: null,
4405
- port: 8888,
4406
- testsEnvironment: false,
4407
- plugins: ["."],
4408
- config: {
4409
- WP_DEBUG: true,
4410
- WP_DEBUG_LOG: true,
4411
- WP_DEBUG_DISPLAY: false,
4412
- SCRIPT_DEBUG: true,
4413
- WP_ENVIRONMENT_TYPE: "local"
4414
- }
4415
- }, null, 2)}
4416
- `;
4417
- }
4418
- function buildDockerComposeSource() {
4419
- return `services:
4420
- integration-service:
4421
- image: node:22-alpine
4422
- working_dir: /workspace
4423
- volumes:
4424
- - .:/workspace
4425
- command: >
4426
- node -e "require('node:http').createServer((request, response) => {
4427
- response.writeHead(request.url === '/health' ? 200 : 404, {
4428
- 'content-type': 'application/json'
4429
- });
4430
- response.end(JSON.stringify({
4431
- ok: request.url === '/health',
4432
- service: 'wp-typia-integration-starter'
4433
- }));
4434
- }).listen(3000, '0.0.0.0')"
4435
- ports:
4436
- - "3000:3000"
4437
- `;
4438
- }
4439
- function buildIntegrationSmokeScriptSource(integrationEnvSlug) {
4440
- return `import fs from "node:fs";
4441
- import path from "node:path";
4442
- import { fileURLToPath } from "node:url";
4443
-
4444
- const ROOT_DIR = path.resolve(
4445
- fileURLToPath(new URL("../..", import.meta.url)),
4446
- );
4447
- const ENV_FILE = path.join(ROOT_DIR, ".env");
4448
-
4449
- function parseEnvValue(value) {
4450
- const trimmed = value.trim();
4451
- if (
4452
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
4453
- (trimmed.startsWith("'") && trimmed.endsWith("'"))
4454
- ) {
4455
- return trimmed.slice(1, -1);
4456
- }
4457
-
4458
- return trimmed;
4459
- }
4460
-
4461
- function readEnvFile(filePath) {
4462
- if (!fs.existsSync(filePath)) {
4463
- return {};
4464
- }
4465
-
4466
- return Object.fromEntries(
4467
- fs
4468
- .readFileSync(filePath, "utf8")
4469
- .split(/\\r?\\n/u)
4470
- .map((line) => line.trim())
4471
- .filter((line) => line.length > 0 && !line.startsWith("#"))
4472
- .map((line) => {
4473
- const separatorIndex = line.indexOf("=");
4474
- if (separatorIndex === -1) {
4475
- return null;
4476
- }
4477
-
4478
- return [
4479
- line.slice(0, separatorIndex).trim(),
4480
- parseEnvValue(line.slice(separatorIndex + 1)),
4481
- ];
4482
- })
4483
- .filter(Boolean),
4484
- );
4485
- }
4486
-
4487
- const envFile = readEnvFile(ENV_FILE);
4488
-
4489
- function getEnv(name, fallback) {
4490
- return process.env[name] ?? envFile[name] ?? fallback;
4491
- }
4492
-
4493
- function resolveEndpointUrl(baseUrl, endpointPath) {
4494
- const normalizedBaseUrl = new URL(baseUrl);
4495
- if (!normalizedBaseUrl.pathname.endsWith("/")) {
4496
- normalizedBaseUrl.pathname = \`\${normalizedBaseUrl.pathname}/\`;
4497
- }
4498
-
4499
- const relativePath = endpointPath.replace(/^\\/+/u, "");
4500
- return new URL(relativePath, normalizedBaseUrl);
4501
- }
4502
-
4503
- async function assertJsonEndpoint(label, url) {
4504
- const response = await fetch(url, {
4505
- headers: {
4506
- accept: "application/json",
4507
- },
4508
- });
4509
-
4510
- if (!response.ok) {
4511
- throw new Error(
4512
- \`\${label} failed at \${url} with HTTP \${response.status}.\`,
4513
- );
4514
- }
4515
-
4516
- const contentType = response.headers.get("content-type") ?? "";
4517
- if (!contentType.includes("application/json")) {
4518
- throw new Error(
4519
- \`\${label} at \${url} did not return JSON (content-type: \${contentType || "missing"}).\`,
4520
- );
4521
- }
4522
-
4523
- return response.json();
4524
- }
4525
-
4526
- const baseUrl = new URL(
4527
- getEnv("WP_TYPIA_SMOKE_BASE_URL", "http://localhost:8888"),
4528
- );
4529
- const serviceUrl = getEnv("WP_TYPIA_SERVICE_URL", "").trim();
4530
-
4531
- await assertJsonEndpoint(
4532
- "WordPress REST index",
4533
- resolveEndpointUrl(baseUrl, "wp-json/"),
4534
- );
4535
-
4536
- if (serviceUrl.length > 0) {
4537
- await assertJsonEndpoint(
4538
- "Local integration service healthcheck",
4539
- resolveEndpointUrl(serviceUrl, "health"),
4540
- );
4541
- }
4542
-
4543
- console.log("wp-typia integration smoke passed: ${integrationEnvSlug}");
4544
- `;
4545
- }
4546
- function buildEnvExampleSource(service) {
4547
- return [
4548
- "# wp-typia integration smoke settings",
4549
- "WP_TYPIA_SMOKE_BASE_URL=http://localhost:8888",
4550
- "WP_TYPIA_SMOKE_USERNAME=admin",
4551
- "WP_TYPIA_SMOKE_PASSWORD=password",
4552
- ...service === "docker-compose" ? [
4553
- "",
4554
- "# Optional docker-compose integration service starter.",
4555
- "WP_TYPIA_SERVICE_URL=http://localhost:3000"
4556
- ] : [
4557
- "",
4558
- "# Set this when your smoke test needs a project-specific service.",
4559
- "# WP_TYPIA_SERVICE_URL=http://localhost:3000"
4560
- ],
4561
- ""
4562
- ].join(`
4563
- `);
4564
- }
4565
- function buildIntegrationEnvReadmeSource({
4566
- integrationEnvSlug,
4567
- service,
4568
- withWpEnv
4569
- }) {
4570
- const title = toTitleCase(integrationEnvSlug);
4571
- const setupSteps = [
4572
- "Copy `.env.example` to `.env` and adjust the URLs or credentials for your local project.",
4573
- ...withWpEnv ? [
4574
- "Run `npm run wp-env:start` to start the generated WordPress environment."
4575
- ] : [
4576
- "Point `WP_TYPIA_SMOKE_BASE_URL` at the WordPress environment you already run locally."
4577
- ],
4578
- ...service === "docker-compose" ? [
4579
- "Run `npm run service:start` if you want the placeholder docker-compose service available at `WP_TYPIA_SERVICE_URL`."
4580
- ] : [
4581
- "Set `WP_TYPIA_SERVICE_URL` only when your integration smoke needs a local service dependency."
4582
- ],
4583
- `Run \`npm run smoke:${integrationEnvSlug}\` to execute the starter smoke check.`
4584
- ];
4585
- return `# ${title} Integration Environment
4586
-
4587
- This starter keeps local WordPress integration smoke checks opt-in and editable.
4588
- It does not change default block scaffolds or require wp-env unless this add
4589
- workflow was run with \`--wp-env\`.
4590
-
4591
- ## Setup
4592
-
4593
- ${setupSteps.map((step, index) => `${index + 1}. ${step}`).join(`
4594
- `)}
4595
-
4596
- ## Adapting the Starter
4597
-
4598
- - Extend \`scripts/integration-smoke/${integrationEnvSlug}.mjs\` with the REST,
4599
- editor, or service assertions that matter for this project.
4600
- - Keep secrets in \`.env\`; \`.env.example\` should document only safe defaults.
4601
- - If your project uses a real service stack, replace the placeholder
4602
- \`docker-compose.integration.yml\` service with your database, queue, API, or
4603
- emulator containers.
4604
- - Keep the smoke script focused on high-signal integration checks so CI and
4605
- local debugging stay fast.
4606
- `;
4607
- }
4608
- async function appendMissingLines(filePath, lines) {
4609
- const current = await readOptionalUtf8File(filePath) ?? "";
4610
- const missingLines = lines.filter((line) => !current.includes(`${line}
4611
- `) && !current.endsWith(line));
4612
- if (missingLines.length === 0) {
4613
- return;
4614
- }
4615
- const separator = current.length === 0 || current.endsWith(`
4616
- `) ? "" : `
4617
- `;
4618
- await fsp5.mkdir(path8.dirname(filePath), { recursive: true });
4619
- await fsp5.writeFile(filePath, `${current}${separator}${missingLines.join(`
4620
- `)}
4621
- `, "utf8");
4622
- }
4623
- async function writeFileIfAbsent({
4624
- filePath,
4625
- source,
4626
- warnings
4627
- }) {
4628
- if (await pathExists(filePath)) {
4629
- warnings.push(`Preserved existing ${path8.basename(filePath)}; review it manually if you need different local integration settings.`);
4630
- return;
4631
- }
4632
- await fsp5.mkdir(path8.dirname(filePath), { recursive: true });
4633
- await fsp5.writeFile(filePath, source, "utf8");
4634
- }
4635
- async function writeNewScaffoldFile(filePath, source) {
4636
- if (await pathExists(filePath)) {
4637
- throw new Error(`An integration environment scaffold already exists at ${filePath}. Choose a different name.`);
4638
- }
4639
- await fsp5.mkdir(path8.dirname(filePath), { recursive: true });
4640
- await fsp5.writeFile(filePath, source, "utf8");
4641
- }
4642
- function addScriptIfMissing({
4643
- scriptName,
4644
- scripts,
4645
- scriptValue,
4646
- warnings
4647
- }) {
4648
- if (scripts[scriptName] === undefined) {
4649
- scripts[scriptName] = scriptValue;
4650
- return;
4651
- }
4652
- if (scripts[scriptName] !== scriptValue) {
4653
- warnings.push(`Preserved existing package script "${scriptName}"; add "${scriptValue}" manually if you want the generated integration command.`);
4654
- }
4655
- }
4656
- async function mutatePackageJson(projectDir, mutate) {
4657
- const packageJsonPath = path8.join(projectDir, "package.json");
4658
- const packageJson = JSON.parse(await fsp5.readFile(packageJsonPath, "utf8"));
4659
- mutate(packageJson);
4660
- await fsp5.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4661
- `, "utf8");
4662
- }
4663
- function addIntegrationEnvPackageJsonEntries({
4664
- integrationEnvSlug,
4665
- packageManager,
4666
- packageJson,
4667
- service,
4668
- warnings,
4669
- withWpEnv
4670
- }) {
4671
- const devDependencies = {
4672
- ...packageJson.devDependencies ?? {}
4673
- };
4674
- if (withWpEnv && devDependencies["@wordpress/env"] === undefined) {
4675
- devDependencies["@wordpress/env"] = WP_ENV_PACKAGE_VERSION;
4676
- }
4677
- packageJson.devDependencies = devDependencies;
4678
- const scripts = {
4679
- ...packageJson.scripts ?? {}
4680
- };
4681
- addScriptIfMissing({
4682
- scriptName: `smoke:${integrationEnvSlug}`,
4683
- scriptValue: `node scripts/integration-smoke/${integrationEnvSlug}.mjs`,
4684
- scripts,
4685
- warnings
4686
- });
4687
- addScriptIfMissing({
4688
- scriptName: "smoke:integration",
4689
- scriptValue: formatRunScript(packageManager, `smoke:${integrationEnvSlug}`),
4690
- scripts,
4691
- warnings
4692
- });
4693
- if (withWpEnv) {
4694
- addScriptIfMissing({
4695
- scriptName: "wp-env:start",
4696
- scriptValue: "wp-env start",
4697
- scripts,
4698
- warnings
4699
- });
4700
- addScriptIfMissing({
4701
- scriptName: "wp-env:stop",
4702
- scriptValue: "wp-env stop",
4703
- scripts,
4704
- warnings
4705
- });
4706
- addScriptIfMissing({
4707
- scriptName: "wp-env:reset",
4708
- scriptValue: "wp-env destroy all && wp-env start",
4709
- scripts,
4710
- warnings
4711
- });
4712
- }
4713
- if (service === "docker-compose") {
4714
- addScriptIfMissing({
4715
- scriptName: "service:start",
4716
- scriptValue: "docker compose -f docker-compose.integration.yml up -d",
4717
- scripts,
4718
- warnings
4719
- });
4720
- addScriptIfMissing({
4721
- scriptName: "service:stop",
4722
- scriptValue: "docker compose -f docker-compose.integration.yml down",
4723
- scripts,
4724
- warnings
4725
- });
4726
- }
4727
- packageJson.scripts = scripts;
4728
- }
4729
- async function runAddIntegrationEnvCommand({
4730
- cwd = process.cwd(),
4731
- integrationEnvName,
4732
- service,
4733
- withWpEnv = false
4401
+ async function runAddBindingSourceCommand({
4402
+ attributeName,
4403
+ bindingSourceName,
4404
+ blockName,
4405
+ cwd = process.cwd()
4734
4406
  }) {
4735
4407
  const workspace = resolveWorkspaceProject(cwd);
4736
- const integrationEnvSlug = assertValidGeneratedSlug("Integration environment name", normalizeBlockSlug(integrationEnvName), "wp-typia add integration-env <name> [--wp-env]");
4737
- const serviceId = assertValidIntegrationEnvService(service);
4738
- const warnings = [];
4739
- const packageJsonPath = path8.join(workspace.projectDir, "package.json");
4740
- const gitignorePath = path8.join(workspace.projectDir, ".gitignore");
4741
- const envExamplePath = path8.join(workspace.projectDir, ".env.example");
4742
- const wpEnvPath = path8.join(workspace.projectDir, ".wp-env.json");
4743
- const dockerComposePath = path8.join(workspace.projectDir, "docker-compose.integration.yml");
4744
- const smokeDir = path8.join(workspace.projectDir, "scripts", "integration-smoke");
4745
- const docsDir = path8.join(workspace.projectDir, "docs", "integration-env");
4746
- const smokeScriptPath = path8.join(smokeDir, `${integrationEnvSlug}.mjs`);
4747
- const docsPath = path8.join(docsDir, `${integrationEnvSlug}.md`);
4748
- const shouldRemoveSmokeDirOnRollback = !await pathExists(smokeDir);
4749
- const shouldRemoveDocsDirOnRollback = !await pathExists(docsDir);
4750
- await executeWorkspaceMutationPlan({
4751
- filePaths: [
4752
- packageJsonPath,
4753
- gitignorePath,
4754
- envExamplePath,
4755
- ...withWpEnv ? [wpEnvPath] : [],
4756
- ...serviceId === "docker-compose" ? [dockerComposePath] : []
4757
- ],
4758
- targetPaths: [
4759
- smokeScriptPath,
4760
- docsPath,
4761
- ...shouldRemoveSmokeDirOnRollback ? [smokeDir] : [],
4762
- ...shouldRemoveDocsDirOnRollback ? [docsDir] : []
4763
- ],
4764
- run: async () => {
4765
- await writeNewScaffoldFile(smokeScriptPath, buildIntegrationSmokeScriptSource(integrationEnvSlug));
4766
- await writeNewScaffoldFile(docsPath, buildIntegrationEnvReadmeSource({
4767
- integrationEnvSlug,
4768
- service: serviceId,
4769
- withWpEnv
4770
- }));
4771
- await appendMissingLines(envExamplePath, [
4772
- ...buildEnvExampleSource(serviceId).trimEnd().split(`
4773
- `)
4774
- ]);
4775
- await appendMissingLines(gitignorePath, [".env", ".env.local"]);
4776
- if (withWpEnv) {
4777
- await writeFileIfAbsent({
4778
- filePath: wpEnvPath,
4779
- source: buildWpEnvConfigSource(),
4780
- warnings
4781
- });
4782
- }
4783
- if (serviceId === "docker-compose") {
4784
- await writeFileIfAbsent({
4785
- filePath: dockerComposePath,
4786
- source: buildDockerComposeSource(),
4787
- warnings
4788
- });
4789
- }
4790
- await mutatePackageJson(workspace.projectDir, (packageJson) => addIntegrationEnvPackageJsonEntries({
4791
- integrationEnvSlug,
4792
- packageManager: workspace.packageManager,
4793
- packageJson,
4794
- service: serviceId,
4795
- warnings,
4796
- withWpEnv
4797
- }));
4798
- }
4799
- });
4800
- return {
4801
- integrationEnvSlug,
4802
- projectDir: workspace.projectDir,
4803
- service: serviceId,
4804
- warnings: warnings.length > 0 ? warnings : undefined,
4805
- withWpEnv
4408
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
4409
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
4410
+ assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
4411
+ const target = resolveBindingTarget({
4412
+ attributeName,
4413
+ blockName
4414
+ }, workspace.workspace.namespace);
4415
+ const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
4416
+ const blockConfigPath = path7.join(workspace.projectDir, "scripts", "block-config.ts");
4417
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4418
+ const bindingsIndexPath = await resolveBindingSourceRegistryPath(workspace.projectDir);
4419
+ const bindingSourceDir = path7.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
4420
+ const serverFilePath = path7.join(bindingSourceDir, "server.php");
4421
+ const editorFilePath = path7.join(bindingSourceDir, "editor.ts");
4422
+ const blockJsonPath = target ? path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "block.json") : undefined;
4423
+ const targetGeneratedMetadataPaths = target ? [
4424
+ path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.manifest.json"),
4425
+ path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.openapi.json"),
4426
+ path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.schema.json"),
4427
+ path7.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia-validator.php")
4428
+ ] : [];
4429
+ const mutationSnapshot = {
4430
+ fileSources: await snapshotWorkspaceFiles([
4431
+ blockConfigPath,
4432
+ bootstrapPath,
4433
+ bindingsIndexPath,
4434
+ ...blockJsonPath ? [blockJsonPath] : [],
4435
+ ...targetBlock ? [path7.join(workspace.projectDir, targetBlock.typesFile)] : [],
4436
+ ...targetGeneratedMetadataPaths
4437
+ ]),
4438
+ snapshotDirs: [],
4439
+ targetPaths: [bindingSourceDir]
4806
4440
  };
4441
+ try {
4442
+ await fsp4.mkdir(bindingSourceDir, { recursive: true });
4443
+ await ensureBindingSourceBootstrapAnchors(workspace);
4444
+ await fsp4.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
4445
+ await fsp4.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
4446
+ if (target && targetBlock) {
4447
+ await ensureBindingTargetBlockAttributeType(workspace.projectDir, targetBlock, target);
4448
+ }
4449
+ await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
4450
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
4451
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug, target)]
4452
+ });
4453
+ return {
4454
+ ...target ? { attributeName: target.attributeName, blockSlug: target.blockSlug } : {},
4455
+ bindingSourceSlug,
4456
+ projectDir: workspace.projectDir
4457
+ };
4458
+ } catch (error) {
4459
+ await rollbackWorkspaceMutation(mutationSnapshot);
4460
+ throw error;
4461
+ }
4807
4462
  }
4808
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
4809
- import { promises as fsp6 } from "fs";
4810
- import path10 from "path";
4463
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-generated.ts
4464
+ import { promises as fsp5 } from "fs";
4465
+ import path9 from "path";
4811
4466
 
4812
4467
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-anchors.ts
4813
- import path9 from "path";
4468
+ import path8 from "path";
4814
4469
  var REST_RESOURCE_SERVER_GLOB = "/inc/rest/*.php";
4470
+ var REST_SCHEMA_HELPER_PATH = "/inc/rest-schema.php";
4471
+ async function ensureRestSchemaHelperBootstrapAnchors(workspace) {
4472
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4473
+ await patchFile(bootstrapPath, (source) => {
4474
+ let nextSource = source;
4475
+ const loadFunctionName = `${workspace.workspace.phpPrefix}_load_rest_schema_helpers`;
4476
+ const loadCall = `${loadFunctionName}();`;
4477
+ const helperFunction = `
4478
+
4479
+ function ${loadFunctionName}() {
4480
+ $helper_path = __DIR__ . '${REST_SCHEMA_HELPER_PATH}';
4481
+ if ( is_readable( $helper_path ) ) {
4482
+ require_once $helper_path;
4483
+ }
4484
+ }
4485
+
4486
+ ${loadCall}
4487
+ `;
4488
+ if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
4489
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, helperFunction);
4490
+ } else if (!nextSource.includes(REST_SCHEMA_HELPER_PATH)) {
4491
+ throw new Error([
4492
+ `Unable to patch ${path8.basename(bootstrapPath)} in ensureRestSchemaHelperBootstrapAnchors.`,
4493
+ `The existing ${loadFunctionName}() definition does not include ${REST_SCHEMA_HELPER_PATH}.`,
4494
+ "Restore the generated bootstrap shape or load inc/rest-schema.php manually before retrying."
4495
+ ].join(" "));
4496
+ } else if (!nextSource.includes(loadCall)) {
4497
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, loadCall);
4498
+ }
4499
+ return nextSource;
4500
+ });
4501
+ }
4815
4502
  async function ensureRestResourceBootstrapAnchors(workspace) {
4816
4503
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4817
4504
  await patchFile(bootstrapPath, (source) => {
@@ -4830,7 +4517,7 @@ function ${registerFunctionName}() {
4830
4517
  nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, registerFunction);
4831
4518
  } else if (!nextSource.includes(REST_RESOURCE_SERVER_GLOB)) {
4832
4519
  throw new Error([
4833
- `Unable to patch ${path9.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
4520
+ `Unable to patch ${path8.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
4834
4521
  `The existing ${registerFunctionName}() definition does not include ${REST_RESOURCE_SERVER_GLOB}.`,
4835
4522
  "Restore the generated bootstrap shape or wire the REST resource loader manually before retrying."
4836
4523
  ].join(" "));
@@ -4844,7 +4531,7 @@ function ${registerFunctionName}() {
4844
4531
  function assertSyncRestAnchor(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
4845
4532
  if (!nextSource.includes(target) && !hasAnchor) {
4846
4533
  throw new Error([
4847
- `ensureRestResourceSyncScriptAnchors could not patch ${path9.basename(syncRestScriptPath)}.`,
4534
+ `ensureRestResourceSyncScriptAnchors could not patch ${path8.basename(syncRestScriptPath)}.`,
4848
4535
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
4849
4536
  "Restore the generated template or add the REST_RESOURCES wiring manually before retrying."
4850
4537
  ].join(" "));
@@ -4860,7 +4547,7 @@ function replaceRequiredSyncRestSource(nextSource, target, anchor, replacement,
4860
4547
  }
4861
4548
  function getSyncRestPatchErrorMessage(functionName, syncRestScriptPath, anchorDescription, subject) {
4862
4549
  return [
4863
- `${functionName} could not patch ${path9.basename(syncRestScriptPath)}.`,
4550
+ `${functionName} could not patch ${path8.basename(syncRestScriptPath)}.`,
4864
4551
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
4865
4552
  `Restore the generated template or add the ${subject} wiring manually before retrying.`
4866
4553
  ].join(" ");
@@ -5123,7 +4810,7 @@ ${resourceLoopAnchor}`);
5123
4810
  `), "success log insertion point", syncRestScriptPath);
5124
4811
  }
5125
4812
  async function ensureContractSyncScriptAnchors(workspace) {
5126
- const syncRestScriptPath = path9.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4813
+ const syncRestScriptPath = path8.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5127
4814
  await patchFile(syncRestScriptPath, (source) => {
5128
4815
  let nextSource = replaceBlockConfigImportForContracts(source, syncRestScriptPath);
5129
4816
  const helperInsertionAnchor = "async function assertTypeArtifactsCurrent";
@@ -5156,7 +4843,7 @@ async function ensureContractSyncScriptAnchors(workspace) {
5156
4843
  });
5157
4844
  }
5158
4845
  async function ensureRestResourceSyncScriptAnchors(workspace) {
5159
- const syncRestScriptPath = path9.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4846
+ const syncRestScriptPath = path8.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5160
4847
  await patchFile(syncRestScriptPath, (source) => {
5161
4848
  let nextSource = replaceBlockConfigImportForRestResources(source, syncRestScriptPath);
5162
4849
  const helperInsertionAnchor = "async function assertTypeArtifactsCurrent";
@@ -5252,14 +4939,7 @@ async function ensureRestResourceSyncScriptAnchors(workspace) {
5252
4939
  });
5253
4940
  }
5254
4941
 
5255
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
5256
- var MANUAL_REST_REQUEST_BODY_FIELD_NAMES = new Set(["payload", "comment"]);
5257
- var MANUAL_REST_RESPONSE_FIELD_NAMES = new Set([
5258
- "id",
5259
- "status",
5260
- "message",
5261
- "updatedAt"
5262
- ]);
4942
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-php-templates.ts
5263
4943
  function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions, options) {
5264
4944
  const collectionRoutes = [];
5265
4945
  const itemRoutes = [];
@@ -5370,13 +5050,11 @@ if ( ! class_exists( ${quotePhpString(controllerClassName)} ) ) {
5370
5050
  }
5371
5051
  `;
5372
5052
  }
5373
- function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, textDomain, methods, options) {
5053
+ function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, methods, options) {
5374
5054
  const restResourceTitle = toTitleCase(restResourceSlug);
5375
5055
  const restResourcePhpId = restResourceSlug.replace(/-/g, "_");
5376
5056
  const canWriteFunctionName = `${phpPrefix}_${restResourcePhpId}_can_manage_rest_resource`;
5377
5057
  const getItemsFunctionName = `${phpPrefix}_${restResourcePhpId}_get_rest_resource_items`;
5378
- const loadSchemaFunctionName = `${phpPrefix}_${restResourcePhpId}_load_rest_resource_schema`;
5379
- const normalizeSchemaFunctionName = `${phpPrefix}_${restResourcePhpId}_sanitize_rest_resource_schema`;
5380
5058
  const validatePayloadFunctionName = `${phpPrefix}_${restResourcePhpId}_validate_rest_resource_payload`;
5381
5059
  const normalizeItemFunctionName = `${phpPrefix}_${restResourcePhpId}_normalize_rest_resource_item`;
5382
5060
  const saveItemsFunctionName = `${phpPrefix}_${restResourcePhpId}_save_rest_resource_items`;
@@ -5486,55 +5164,22 @@ if ( ! function_exists( '${saveItemsFunctionName}' ) ) {
5486
5164
  }
5487
5165
  }
5488
5166
 
5489
- if ( ! function_exists( '${loadSchemaFunctionName}' ) ) {
5490
- function ${loadSchemaFunctionName}( $schema_name ) {
5491
- $project_root = dirname( __DIR__, 2 );
5492
- $schema_path = $project_root . '/src/rest/${restResourceSlug}/api-schemas/' . $schema_name . '.schema.json';
5493
- if ( ! file_exists( $schema_path ) ) {
5494
- return null;
5495
- }
5496
-
5497
- $decoded = json_decode( file_get_contents( $schema_path ), true );
5498
- return is_array( $decoded ) ? $decoded : null;
5499
- }
5500
- }
5501
-
5502
- if ( ! function_exists( '${normalizeSchemaFunctionName}' ) ) {
5503
- function ${normalizeSchemaFunctionName}( $schema ) {
5504
- if ( ! is_array( $schema ) ) {
5505
- return $schema;
5506
- }
5507
-
5508
- unset( $schema['$schema'], $schema['title'] );
5509
-
5510
- if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
5511
- foreach ( $schema['properties'] as $key => $property_schema ) {
5512
- $schema['properties'][ $key ] = ${normalizeSchemaFunctionName}( $property_schema );
5513
- }
5514
- }
5515
-
5516
- if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
5517
- $schema['items'] = ${normalizeSchemaFunctionName}( $schema['items'] );
5518
- }
5519
-
5520
- return $schema;
5521
- }
5522
- }
5523
-
5524
5167
  if ( ! function_exists( '${validatePayloadFunctionName}' ) ) {
5525
5168
  function ${validatePayloadFunctionName}( $value, $schema_name, $param_name ) {
5526
- $schema = ${loadSchemaFunctionName}( $schema_name );
5527
- if ( ! is_array( $schema ) ) {
5528
- return new WP_Error( 'missing_schema', 'Missing REST schema.', array( 'status' => 500 ) );
5529
- }
5530
-
5531
- $rest_schema = ${normalizeSchemaFunctionName}( $schema );
5532
- $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
5533
- if ( is_wp_error( $validation ) ) {
5534
- return $validation;
5169
+ if ( ! function_exists( '${phpPrefix}_validate_and_sanitize_rest_payload' ) ) {
5170
+ return new WP_Error(
5171
+ 'missing_rest_schema_helper',
5172
+ 'Missing REST schema helper. Ensure inc/rest-schema.php is loaded before REST resources.',
5173
+ array( 'status' => 500 )
5174
+ );
5535
5175
  }
5536
5176
 
5537
- return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
5177
+ return ${phpPrefix}_validate_and_sanitize_rest_payload(
5178
+ $value,
5179
+ $schema_name,
5180
+ $param_name,
5181
+ array( 'resource' => ${quotePhpString(restResourceSlug)} )
5182
+ );
5538
5183
  }
5539
5184
  }
5540
5185
 
@@ -5742,170 +5387,247 @@ if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
5742
5387
  function ${registerRoutesFunctionName}() {
5743
5388
  $namespace = ${quotePhpString(namespace)};
5744
5389
 
5745
- ${controllerBootstrapSource}
5746
- ${routeRegistrations}
5390
+ ${controllerBootstrapSource}
5391
+ ${routeRegistrations}
5392
+ }
5393
+ }
5394
+
5395
+ add_action( 'rest_api_init', '${registerRoutesFunctionName}' );
5396
+ `;
5397
+ }
5398
+ function buildWorkspaceRestSchemaHelperPhpSource(phpPrefix) {
5399
+ return `<?php
5400
+ if ( ! defined( 'ABSPATH' ) ) {
5401
+ return;
5402
+ }
5403
+
5404
+ if ( ! function_exists( '${phpPrefix}_is_valid_rest_schema_key' ) ) {
5405
+ function ${phpPrefix}_is_valid_rest_schema_key( $value ) {
5406
+ return is_string( $value ) && 1 === preg_match( '/\\A[A-Za-z0-9_-]+\\z/', $value );
5407
+ }
5408
+ }
5409
+
5410
+ if ( ! function_exists( '${phpPrefix}_is_valid_rest_resource_slug' ) ) {
5411
+ function ${phpPrefix}_is_valid_rest_resource_slug( $value ) {
5412
+ return is_string( $value ) && 1 === preg_match( '/\\A[a-z0-9]+(?:-[a-z0-9]+)*\\z/', $value );
5413
+ }
5414
+ }
5415
+
5416
+ if ( ! function_exists( '${phpPrefix}_resolve_rest_schema_paths' ) ) {
5417
+ function ${phpPrefix}_resolve_rest_schema_paths( $schema_name, $options = array() ) {
5418
+ if ( ! ${phpPrefix}_is_valid_rest_schema_key( $schema_name ) ) {
5419
+ return new WP_Error(
5420
+ 'invalid_rest_schema_name',
5421
+ 'Invalid REST schema name.',
5422
+ array( 'status' => 500 )
5423
+ );
5424
+ }
5425
+
5426
+ $options = is_array( $options ) ? $options : array();
5427
+ $project_root = dirname( __DIR__ );
5428
+ $paths = array();
5429
+
5430
+ if ( isset( $options['resource'] ) && '' !== $options['resource'] ) {
5431
+ if ( ! ${phpPrefix}_is_valid_rest_resource_slug( $options['resource'] ) ) {
5432
+ return new WP_Error(
5433
+ 'invalid_rest_schema_resource',
5434
+ 'Invalid REST schema resource slug.',
5435
+ array( 'status' => 500 )
5436
+ );
5437
+ }
5438
+
5439
+ $resource_slug = $options['resource'];
5440
+ $paths[] = __DIR__ . '/rest-schemas/rest/' . $resource_slug . '/' . $schema_name . '.schema.json';
5441
+ $paths[] = $project_root . '/src/rest/' . $resource_slug . '/api-schemas/' . $schema_name . '.schema.json';
5442
+ }
5443
+
5444
+ if ( isset( $options['paths'] ) && is_array( $options['paths'] ) ) {
5445
+ foreach ( $options['paths'] as $schema_path ) {
5446
+ if ( is_string( $schema_path ) && '' !== $schema_path && ! in_array( $schema_path, $paths, true ) ) {
5447
+ $paths[] = $schema_path;
5448
+ }
5449
+ }
5450
+ }
5451
+
5452
+ return $paths;
5453
+ }
5454
+ }
5455
+
5456
+ if ( ! function_exists( '${phpPrefix}_load_rest_schema' ) ) {
5457
+ function ${phpPrefix}_load_rest_schema( $schema_name, $options = array() ) {
5458
+ $schema_paths = ${phpPrefix}_resolve_rest_schema_paths( $schema_name, $options );
5459
+ if ( is_wp_error( $schema_paths ) ) {
5460
+ return $schema_paths;
5461
+ }
5462
+
5463
+ foreach ( $schema_paths as $schema_path ) {
5464
+ if ( ! is_file( $schema_path ) ) {
5465
+ continue;
5466
+ }
5467
+
5468
+ if ( ! is_readable( $schema_path ) ) {
5469
+ return new WP_Error(
5470
+ 'unreadable_rest_schema',
5471
+ 'Generated REST schema is not readable.',
5472
+ array(
5473
+ 'path' => $schema_path,
5474
+ 'status' => 500,
5475
+ )
5476
+ );
5477
+ }
5478
+
5479
+ $schema_json = file_get_contents( $schema_path );
5480
+ if ( false === $schema_json ) {
5481
+ return new WP_Error(
5482
+ 'rest_schema_read_failed',
5483
+ 'Generated REST schema could not be read.',
5484
+ array(
5485
+ 'path' => $schema_path,
5486
+ 'status' => 500,
5487
+ )
5488
+ );
5489
+ }
5490
+
5491
+ $decoded = json_decode( $schema_json, true );
5492
+ if ( ! is_array( $decoded ) ) {
5493
+ return new WP_Error(
5494
+ 'malformed_rest_schema',
5495
+ 'Generated REST schema contains malformed JSON.',
5496
+ array(
5497
+ 'json_error' => json_last_error_msg(),
5498
+ 'path' => $schema_path,
5499
+ 'status' => 500,
5500
+ )
5501
+ );
5502
+ }
5503
+
5504
+ return $decoded;
5505
+ }
5506
+
5507
+ return new WP_Error(
5508
+ 'missing_rest_schema',
5509
+ 'Generated REST schema could not be found.',
5510
+ array(
5511
+ 'paths' => $schema_paths,
5512
+ 'schema' => $schema_name,
5513
+ 'status' => 500,
5514
+ )
5515
+ );
5516
+ }
5517
+ }
5518
+
5519
+ if ( ! function_exists( '${phpPrefix}_prepare_rest_schema_for_wordpress' ) ) {
5520
+ function ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema ) {
5521
+ if ( ! is_array( $schema ) ) {
5522
+ return $schema;
5523
+ }
5524
+
5525
+ unset( $schema['$schema'], $schema['title'] );
5526
+
5527
+ foreach ( array( 'properties', 'patternProperties', 'definitions', '$defs' ) as $schema_map_key ) {
5528
+ if ( ! isset( $schema[ $schema_map_key ] ) || ! is_array( $schema[ $schema_map_key ] ) ) {
5529
+ continue;
5530
+ }
5531
+
5532
+ foreach ( $schema[ $schema_map_key ] as $key => $property_schema ) {
5533
+ $schema[ $schema_map_key ][ $key ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $property_schema );
5534
+ }
5535
+ }
5536
+
5537
+ foreach ( array( 'items', 'additionalProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else' ) as $nested_schema_key ) {
5538
+ if ( isset( $schema[ $nested_schema_key ] ) && is_array( $schema[ $nested_schema_key ] ) ) {
5539
+ $schema[ $nested_schema_key ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema[ $nested_schema_key ] );
5540
+ }
5541
+ }
5542
+
5543
+ foreach ( array( 'allOf', 'anyOf', 'oneOf' ) as $schema_list_key ) {
5544
+ if ( ! isset( $schema[ $schema_list_key ] ) || ! is_array( $schema[ $schema_list_key ] ) ) {
5545
+ continue;
5546
+ }
5547
+
5548
+ foreach ( $schema[ $schema_list_key ] as $index => $variant_schema ) {
5549
+ $schema[ $schema_list_key ][ $index ] = ${phpPrefix}_prepare_rest_schema_for_wordpress( $variant_schema );
5550
+ }
5551
+ }
5552
+
5553
+ return $schema;
5554
+ }
5555
+ }
5556
+
5557
+ if ( ! function_exists( '${phpPrefix}_get_wordpress_rest_schema' ) ) {
5558
+ function ${phpPrefix}_get_wordpress_rest_schema( $schema_name, $options = array() ) {
5559
+ $schema = ${phpPrefix}_load_rest_schema( $schema_name, $options );
5560
+ if ( is_wp_error( $schema ) ) {
5561
+ return $schema;
5562
+ }
5563
+
5564
+ return ${phpPrefix}_prepare_rest_schema_for_wordpress( $schema );
5565
+ }
5566
+ }
5567
+
5568
+ if ( ! function_exists( '${phpPrefix}_validate_and_sanitize_rest_payload' ) ) {
5569
+ function ${phpPrefix}_validate_and_sanitize_rest_payload( $value, $schema_name, $param_name, $options = array() ) {
5570
+ $rest_schema = ${phpPrefix}_get_wordpress_rest_schema( $schema_name, $options );
5571
+ if ( is_wp_error( $rest_schema ) ) {
5572
+ return $rest_schema;
5573
+ }
5574
+
5575
+ $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
5576
+ if ( is_wp_error( $validation ) ) {
5577
+ return $validation;
5578
+ }
5579
+
5580
+ return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
5747
5581
  }
5748
5582
  }
5749
-
5750
- add_action( 'rest_api_init', '${registerRoutesFunctionName}' );
5751
5583
  `;
5752
5584
  }
5753
- async function runAddRestResourceCommand({
5754
- auth,
5755
- bodyTypeName,
5585
+
5586
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-generated.ts
5587
+ async function ensureWorkspaceRestSchemaHelperFile(helperFilePath, phpPrefix) {
5588
+ let currentSource = null;
5589
+ try {
5590
+ currentSource = await fsp5.readFile(helperFilePath, "utf8");
5591
+ } catch (error) {
5592
+ if (error.code !== "ENOENT") {
5593
+ throw error;
5594
+ }
5595
+ }
5596
+ if (currentSource === null) {
5597
+ await fsp5.mkdir(path9.dirname(helperFilePath), { recursive: true });
5598
+ await fsp5.writeFile(helperFilePath, buildWorkspaceRestSchemaHelperPhpSource(phpPrefix), "utf8");
5599
+ return;
5600
+ }
5601
+ const requiredFunctions = [
5602
+ `${phpPrefix}_load_rest_schema`,
5603
+ `${phpPrefix}_prepare_rest_schema_for_wordpress`,
5604
+ `${phpPrefix}_get_wordpress_rest_schema`,
5605
+ `${phpPrefix}_validate_and_sanitize_rest_payload`
5606
+ ];
5607
+ const missingFunctions = requiredFunctions.filter((functionName) => !hasPhpFunctionDefinition(currentSource, functionName));
5608
+ if (missingFunctions.length > 0) {
5609
+ throw new Error([
5610
+ `Existing ${path9.relative(process.cwd(), helperFilePath)} is missing generated REST schema helper functions: ${missingFunctions.join(", ")}.`,
5611
+ "Restore the generated inc/rest-schema.php helper or add these functions manually before retrying."
5612
+ ].join(" "));
5613
+ }
5614
+ }
5615
+ async function scaffoldGeneratedRestResource({
5756
5616
  controllerClass,
5757
5617
  controllerExtends,
5758
- cwd = process.cwd(),
5759
- manual,
5760
- method,
5761
5618
  methods,
5762
5619
  namespace,
5763
5620
  permissionCallback,
5764
- pathPattern,
5765
- queryTypeName,
5766
- restResourceName,
5767
- responseTypeName,
5621
+ restResourceSlug,
5768
5622
  routePattern,
5769
- secretFieldName,
5770
- secretStateFieldName
5623
+ workspace
5771
5624
  }) {
5772
- const workspace = resolveWorkspaceProject(cwd);
5773
- const restResourceSlug = assertValidGeneratedSlug("REST resource name", normalizeBlockSlug(restResourceName), "wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create>]");
5774
- const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
5775
- const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
5776
- assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
5777
- const blockConfigPath = path10.join(workspace.projectDir, "scripts", "block-config.ts");
5778
- const syncRestScriptPath = path10.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5779
- const restResourceDir = path10.join(workspace.projectDir, "src", "rest", restResourceSlug);
5780
- const typesFilePath = path10.join(restResourceDir, "api-types.ts");
5781
- const validatorsFilePath = path10.join(restResourceDir, "api-validators.ts");
5782
- const apiFilePath = path10.join(restResourceDir, "api.ts");
5783
- if (manual) {
5784
- if (controllerClass || controllerExtends || permissionCallback || routePattern) {
5785
- throw new Error("Manual REST contracts do not generate PHP route glue. Use generated rest-resource mode for --route-pattern, --permission-callback, --controller-class, or --controller-extends.");
5786
- }
5787
- const pascalCase = toPascalCase(restResourceSlug);
5788
- const resolvedAuth = assertValidManualRestContractAuth(auth);
5789
- const resolvedMethod = assertValidManualRestContractHttpMethod(method);
5790
- const resolvedPathPattern = resolveManualRestContractPathPattern(restResourceSlug, pathPattern);
5791
- const resolvedQueryTypeName = assertValidTypeScriptIdentifier("Manual REST contract query type", queryTypeName ?? `${pascalCase}Query`, "wp-typia add rest-resource <name> --manual [--query-type <ExportedQueryType>]");
5792
- const resolvedResponseTypeName = assertValidTypeScriptIdentifier("Manual REST contract response type", responseTypeName ?? `${pascalCase}Response`, "wp-typia add rest-resource <name> --manual [--response-type <ExportedResponseType>]");
5793
- const defaultsToBody = bodyTypeName == null && ["PATCH", "POST", "PUT"].includes(resolvedMethod);
5794
- const resolvedBodyTypeName = bodyTypeName != null || defaultsToBody ? assertValidTypeScriptIdentifier("Manual REST contract body type", bodyTypeName ?? `${pascalCase}Request`, "wp-typia add rest-resource <name> --manual [--body-type <ExportedBodyType>]") : undefined;
5795
- if (resolvedMethod === "GET" && resolvedBodyTypeName) {
5796
- throw new Error("Manual REST contract GET routes cannot define a body type. Remove --body-type or use POST, PUT, or PATCH.");
5797
- }
5798
- if (secretStateFieldName && !secretFieldName) {
5799
- throw new Error("Manual REST contract --secret-state-field requires --secret-field.");
5800
- }
5801
- if (secretFieldName && !resolvedBodyTypeName) {
5802
- throw new Error("Manual REST contract secret fields require a request body. Use POST, PUT, or PATCH so a request body is generated.");
5803
- }
5804
- const resolvedSecretFieldName = secretFieldName ? assertValidTypeScriptIdentifier("Manual REST contract secret field", secretFieldName, "wp-typia add rest-resource <name> --manual --method POST --secret-field <field>") : undefined;
5805
- const resolvedSecretStateFieldName = resolvedSecretFieldName ? assertValidTypeScriptIdentifier("Manual REST contract secret state field", secretStateFieldName ?? `has${toPascalCase(resolvedSecretFieldName)}`, "wp-typia add rest-resource <name> --manual --method POST --secret-state-field <field>") : undefined;
5806
- if (resolvedSecretFieldName && MANUAL_REST_REQUEST_BODY_FIELD_NAMES.has(resolvedSecretFieldName)) {
5807
- throw new Error(`Manual REST contract secret field must not reuse scaffolded request body fields: ${Array.from(MANUAL_REST_REQUEST_BODY_FIELD_NAMES).join(", ")}.`);
5808
- }
5809
- if (resolvedSecretStateFieldName && MANUAL_REST_RESPONSE_FIELD_NAMES.has(resolvedSecretStateFieldName)) {
5810
- throw new Error(`Manual REST contract secret state field must not reuse scaffolded response fields: ${Array.from(MANUAL_REST_RESPONSE_FIELD_NAMES).join(", ")}.`);
5811
- }
5812
- if (resolvedSecretFieldName && resolvedSecretStateFieldName && resolvedSecretFieldName === resolvedSecretStateFieldName) {
5813
- throw new Error("Manual REST contract secret state field must be different from the raw secret field.");
5814
- }
5815
- const manualTypeNames = [
5816
- resolvedQueryTypeName,
5817
- resolvedResponseTypeName,
5818
- resolvedBodyTypeName
5819
- ].filter((value) => value != null);
5820
- const duplicateManualTypeNames = manualTypeNames.filter((name, index) => manualTypeNames.indexOf(name) !== index);
5821
- if (duplicateManualTypeNames.length > 0) {
5822
- throw new Error(`Manual REST contract type names must be unique: ${Array.from(new Set(duplicateManualTypeNames)).join(", ")}. Use distinct --query-type, --body-type, and --response-type values.`);
5823
- }
5824
- const mutationSnapshot2 = {
5825
- fileSources: await snapshotWorkspaceFiles([
5826
- blockConfigPath,
5827
- syncRestScriptPath
5828
- ]),
5829
- snapshotDirs: [],
5830
- targetPaths: [restResourceDir]
5831
- };
5832
- try {
5833
- await fsp6.mkdir(restResourceDir, { recursive: true });
5834
- await ensureRestResourceSyncScriptAnchors(workspace);
5835
- await fsp6.writeFile(typesFilePath, buildManualRestContractTypesSource({
5836
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5837
- queryTypeName: resolvedQueryTypeName,
5838
- responseTypeName: resolvedResponseTypeName,
5839
- restResourceSlug,
5840
- ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5841
- ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5842
- }), "utf8");
5843
- await fsp6.writeFile(validatorsFilePath, buildManualRestContractValidatorsSource({
5844
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5845
- queryTypeName: resolvedQueryTypeName,
5846
- responseTypeName: resolvedResponseTypeName
5847
- }), "utf8");
5848
- await fsp6.writeFile(apiFilePath, buildManualRestContractApiSource({
5849
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5850
- queryTypeName: resolvedQueryTypeName,
5851
- restResourceSlug
5852
- }), "utf8");
5853
- await syncManualRestContractArtifacts({
5854
- clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
5855
- outputDir: restResourceDir,
5856
- projectDir: workspace.projectDir,
5857
- typesFile: `src/rest/${restResourceSlug}/api-types.ts`,
5858
- validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
5859
- variables: {
5860
- auth: resolvedAuth,
5861
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5862
- method: resolvedMethod,
5863
- namespace: resolvedNamespace,
5864
- pascalCase,
5865
- pathPattern: resolvedPathPattern,
5866
- queryTypeName: resolvedQueryTypeName,
5867
- responseTypeName: resolvedResponseTypeName,
5868
- slugKebabCase: restResourceSlug,
5869
- title: toTitleCase(restResourceSlug)
5870
- }
5871
- });
5872
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
5873
- restResourceEntries: [
5874
- buildManualRestContractConfigEntry({
5875
- auth: resolvedAuth,
5876
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5877
- method: resolvedMethod,
5878
- namespace: resolvedNamespace,
5879
- pathPattern: resolvedPathPattern,
5880
- queryTypeName: resolvedQueryTypeName,
5881
- responseTypeName: resolvedResponseTypeName,
5882
- restResourceSlug,
5883
- ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5884
- ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5885
- })
5886
- ],
5887
- transformSource: ensureBlockConfigCanAddRestManifests
5888
- });
5889
- return {
5890
- auth: resolvedAuth,
5891
- ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5892
- method: resolvedMethod,
5893
- methods: [],
5894
- mode: "manual",
5895
- namespace: resolvedNamespace,
5896
- pathPattern: resolvedPathPattern,
5897
- projectDir: workspace.projectDir,
5898
- queryTypeName: resolvedQueryTypeName,
5899
- restResourceSlug,
5900
- responseTypeName: resolvedResponseTypeName,
5901
- ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5902
- ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5903
- };
5904
- } catch (error) {
5905
- await rollbackWorkspaceMutation(mutationSnapshot2);
5906
- throw error;
5907
- }
5908
- }
5625
+ const blockConfigPath = path9.join(workspace.projectDir, "scripts", "block-config.ts");
5626
+ const syncRestScriptPath = path9.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5627
+ const restResourceDir = path9.join(workspace.projectDir, "src", "rest", restResourceSlug);
5628
+ const typesFilePath = path9.join(restResourceDir, "api-types.ts");
5629
+ const validatorsFilePath = path9.join(restResourceDir, "api-validators.ts");
5630
+ const apiFilePath = path9.join(restResourceDir, "api.ts");
5909
5631
  const resolvedMethods = assertValidRestResourceMethods(methods);
5910
5632
  const resolvedRoutePattern = resolveGeneratedRestResourceRoutePattern(restResourceSlug, routePattern);
5911
5633
  const hasCustomRoutePattern = typeof routePattern === "string" && routePattern.trim().length > 0;
@@ -5916,27 +5638,31 @@ async function runAddRestResourceCommand({
5916
5638
  throw new Error("Generated REST resource controller base class requires --controller-class.");
5917
5639
  }
5918
5640
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
5919
- const dataFilePath = path10.join(restResourceDir, "data.ts");
5920
- const phpFilePath = path10.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
5641
+ const dataFilePath = path9.join(restResourceDir, "data.ts");
5642
+ const restSchemaHelperPath = path9.join(workspace.projectDir, "inc", "rest-schema.php");
5643
+ const phpFilePath = path9.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
5921
5644
  const mutationSnapshot = {
5922
5645
  fileSources: await snapshotWorkspaceFiles([
5923
5646
  blockConfigPath,
5924
5647
  bootstrapPath,
5648
+ restSchemaHelperPath,
5925
5649
  syncRestScriptPath
5926
5650
  ]),
5927
5651
  snapshotDirs: [],
5928
- targetPaths: [restResourceDir, phpFilePath]
5652
+ targetPaths: [restResourceDir, restSchemaHelperPath, phpFilePath]
5929
5653
  };
5930
5654
  try {
5931
- await fsp6.mkdir(restResourceDir, { recursive: true });
5932
- await fsp6.mkdir(path10.dirname(phpFilePath), { recursive: true });
5655
+ await fsp5.mkdir(restResourceDir, { recursive: true });
5656
+ await fsp5.mkdir(path9.dirname(phpFilePath), { recursive: true });
5657
+ await ensureWorkspaceRestSchemaHelperFile(restSchemaHelperPath, workspace.workspace.phpPrefix);
5658
+ await ensureRestSchemaHelperBootstrapAnchors(workspace);
5933
5659
  await ensureRestResourceBootstrapAnchors(workspace);
5934
5660
  await ensureRestResourceSyncScriptAnchors(workspace);
5935
- await fsp6.writeFile(typesFilePath, buildRestResourceTypesSource(restResourceSlug, resolvedMethods), "utf8");
5936
- await fsp6.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
5937
- await fsp6.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
5938
- await fsp6.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
5939
- await fsp6.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods, {
5661
+ await fsp5.writeFile(typesFilePath, buildRestResourceTypesSource(restResourceSlug, resolvedMethods), "utf8");
5662
+ await fsp5.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
5663
+ await fsp5.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
5664
+ await fsp5.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
5665
+ await fsp5.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, namespace, workspace.workspace.phpPrefix, resolvedMethods, {
5940
5666
  ...resolvedControllerClass ? { controllerClass: resolvedControllerClass } : {},
5941
5667
  ...resolvedControllerExtends ? { controllerExtends: resolvedControllerExtends } : {},
5942
5668
  ...resolvedPermissionCallback ? { permissionCallback: resolvedPermissionCallback } : {},
@@ -5950,7 +5676,7 @@ async function runAddRestResourceCommand({
5950
5676
  typesFile: `src/rest/${restResourceSlug}/api-types.ts`,
5951
5677
  validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
5952
5678
  variables: {
5953
- namespace: resolvedNamespace,
5679
+ namespace,
5954
5680
  pascalCase: toPascalCase(restResourceSlug),
5955
5681
  ...hasCustomRoutePattern ? { routePattern: resolvedRoutePattern } : {},
5956
5682
  slugKebabCase: restResourceSlug,
@@ -5963,7 +5689,7 @@ async function runAddRestResourceCommand({
5963
5689
  ...resolvedControllerClass ? { controllerClass: resolvedControllerClass } : {},
5964
5690
  ...resolvedControllerExtends ? { controllerExtends: resolvedControllerExtends } : {},
5965
5691
  methods: resolvedMethods,
5966
- namespace: resolvedNamespace,
5692
+ namespace,
5967
5693
  ...resolvedPermissionCallback ? { permissionCallback: resolvedPermissionCallback } : {},
5968
5694
  restResourceSlug,
5969
5695
  ...hasCustomRoutePattern ? { routePattern: resolvedRoutePattern } : {}
@@ -5976,7 +5702,7 @@ async function runAddRestResourceCommand({
5976
5702
  ...resolvedControllerExtends ? { controllerExtends: resolvedControllerExtends } : {},
5977
5703
  methods: resolvedMethods,
5978
5704
  mode: "generated",
5979
- namespace: resolvedNamespace,
5705
+ namespace,
5980
5706
  ...resolvedPermissionCallback ? { permissionCallback: resolvedPermissionCallback } : {},
5981
5707
  projectDir: workspace.projectDir,
5982
5708
  restResourceSlug,
@@ -5987,6 +5713,293 @@ async function runAddRestResourceCommand({
5987
5713
  throw error;
5988
5714
  }
5989
5715
  }
5716
+
5717
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-manual.ts
5718
+ import { promises as fsp6 } from "fs";
5719
+ import path10 from "path";
5720
+ var MANUAL_REST_REQUEST_BODY_FIELD_NAMES = new Set(["payload", "comment"]);
5721
+ var MANUAL_REST_RESPONSE_FIELD_NAMES = new Set([
5722
+ "id",
5723
+ "status",
5724
+ "message",
5725
+ "updatedAt"
5726
+ ]);
5727
+ function resolveManualRestSecretPreserveOnEmpty(value) {
5728
+ if (value === undefined) {
5729
+ return true;
5730
+ }
5731
+ if (typeof value === "boolean") {
5732
+ return value;
5733
+ }
5734
+ const normalized = value.trim().toLowerCase();
5735
+ if (["1", "true", "yes"].includes(normalized)) {
5736
+ return true;
5737
+ }
5738
+ if (["0", "false", "no"].includes(normalized)) {
5739
+ return false;
5740
+ }
5741
+ throw new Error("Manual REST contract --secret-preserve-on-empty must be true or false.");
5742
+ }
5743
+ function resolveManualRestSecretStateFieldCandidate(options) {
5744
+ const candidates = [
5745
+ options.secretStateFieldName,
5746
+ options.secretHasValueFieldName,
5747
+ options.secretMaskedResponseFieldName
5748
+ ].filter((value) => typeof value === "string");
5749
+ const distinct = Array.from(new Set(candidates));
5750
+ if (distinct.length > 1) {
5751
+ throw new Error("Manual REST contract secret state, has-value, and masked response field flags must match when combined.");
5752
+ }
5753
+ return distinct[0];
5754
+ }
5755
+ function resolveManualRestRoutePathPattern(options) {
5756
+ const trimmedPathPattern = typeof options.pathPattern === "string" ? options.pathPattern.trim() : undefined;
5757
+ const trimmedRoutePattern = typeof options.routePattern === "string" ? options.routePattern.trim() : undefined;
5758
+ if (trimmedPathPattern && trimmedRoutePattern && trimmedPathPattern !== trimmedRoutePattern) {
5759
+ throw new Error("Manual REST contract --path and --route-pattern must match when both are provided. Use one route pattern flag for provider routes.");
5760
+ }
5761
+ return resolveManualRestContractPathPattern(options.restResourceSlug, options.pathPattern ?? options.routePattern);
5762
+ }
5763
+ async function scaffoldManualRestContract({
5764
+ auth,
5765
+ bodyTypeName,
5766
+ controllerClass,
5767
+ controllerExtends,
5768
+ method,
5769
+ namespace,
5770
+ pathPattern,
5771
+ permissionCallback,
5772
+ queryTypeName,
5773
+ responseTypeName,
5774
+ restResourceSlug,
5775
+ routePattern,
5776
+ secretFieldName,
5777
+ secretHasValueFieldName,
5778
+ secretMaskedResponseFieldName,
5779
+ secretPreserveOnEmpty,
5780
+ secretStateFieldName,
5781
+ workspace
5782
+ }) {
5783
+ const blockConfigPath = path10.join(workspace.projectDir, "scripts", "block-config.ts");
5784
+ const syncRestScriptPath = path10.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5785
+ const restResourceDir = path10.join(workspace.projectDir, "src", "rest", restResourceSlug);
5786
+ const typesFilePath = path10.join(restResourceDir, "api-types.ts");
5787
+ const validatorsFilePath = path10.join(restResourceDir, "api-validators.ts");
5788
+ const apiFilePath = path10.join(restResourceDir, "api.ts");
5789
+ const pascalCase = toPascalCase(restResourceSlug);
5790
+ const resolvedAuth = assertValidManualRestContractAuth(auth);
5791
+ const resolvedMethod = assertValidManualRestContractHttpMethod(method);
5792
+ const resolvedPathPattern = resolveManualRestRoutePathPattern({
5793
+ pathPattern,
5794
+ restResourceSlug,
5795
+ routePattern
5796
+ });
5797
+ const pathParameterNames = collectRestRouteNamedCaptureNames(resolvedPathPattern);
5798
+ const resolvedPermissionCallback = resolveOptionalPhpCallbackReference("Manual REST contract permission callback", permissionCallback);
5799
+ const resolvedControllerClass = resolveOptionalPhpClassReference("Manual REST contract controller class", controllerClass);
5800
+ const resolvedControllerExtends = resolveOptionalPhpClassReference("Manual REST contract controller base class", controllerExtends);
5801
+ if (resolvedControllerExtends && !resolvedControllerClass) {
5802
+ throw new Error("Manual REST contract controller base class requires --controller-class.");
5803
+ }
5804
+ const resolvedQueryTypeName = assertValidTypeScriptIdentifier("Manual REST contract query type", queryTypeName ?? `${pascalCase}Query`, "wp-typia add rest-resource <name> --manual [--query-type <ExportedQueryType>]");
5805
+ const resolvedResponseTypeName = assertValidTypeScriptIdentifier("Manual REST contract response type", responseTypeName ?? `${pascalCase}Response`, "wp-typia add rest-resource <name> --manual [--response-type <ExportedResponseType>]");
5806
+ const defaultsToBody = bodyTypeName == null && ["PATCH", "POST", "PUT"].includes(resolvedMethod);
5807
+ const resolvedBodyTypeName = bodyTypeName != null || defaultsToBody ? assertValidTypeScriptIdentifier("Manual REST contract body type", bodyTypeName ?? `${pascalCase}Request`, "wp-typia add rest-resource <name> --manual [--body-type <ExportedBodyType>]") : undefined;
5808
+ if (resolvedMethod === "GET" && resolvedBodyTypeName) {
5809
+ throw new Error("Manual REST contract GET routes cannot define a body type. Remove --body-type or use POST, PUT, or PATCH.");
5810
+ }
5811
+ const secretStateFieldCandidate = resolveManualRestSecretStateFieldCandidate({
5812
+ secretHasValueFieldName,
5813
+ secretMaskedResponseFieldName,
5814
+ secretStateFieldName
5815
+ });
5816
+ if (secretPreserveOnEmpty !== undefined && !secretFieldName) {
5817
+ throw new Error("Manual REST contract --secret-preserve-on-empty requires --secret-field.");
5818
+ }
5819
+ if (secretStateFieldCandidate !== undefined && !secretFieldName) {
5820
+ throw new Error("Manual REST contract secret state, has-value, and masked response field flags require --secret-field.");
5821
+ }
5822
+ if (secretFieldName && !resolvedBodyTypeName) {
5823
+ throw new Error("Manual REST contract secret fields require a request body. Use POST, PUT, or PATCH so a request body is generated.");
5824
+ }
5825
+ const resolvedSecretFieldName = secretFieldName ? assertValidTypeScriptIdentifier("Manual REST contract secret field", secretFieldName, "wp-typia add rest-resource <name> --manual --method POST --secret-field <field>") : undefined;
5826
+ const resolvedSecretPreserveOnEmpty = resolvedSecretFieldName ? resolveManualRestSecretPreserveOnEmpty(secretPreserveOnEmpty) : undefined;
5827
+ const resolvedSecretStateFieldName = resolvedSecretFieldName ? assertValidTypeScriptIdentifier("Manual REST contract secret state field", secretStateFieldCandidate ?? `has${toPascalCase(resolvedSecretFieldName)}`, "wp-typia add rest-resource <name> --manual --method POST --secret-state-field <field>") : undefined;
5828
+ if (resolvedSecretFieldName && MANUAL_REST_REQUEST_BODY_FIELD_NAMES.has(resolvedSecretFieldName)) {
5829
+ throw new Error(`Manual REST contract secret field must not reuse scaffolded request body fields: ${Array.from(MANUAL_REST_REQUEST_BODY_FIELD_NAMES).join(", ")}.`);
5830
+ }
5831
+ if (resolvedSecretStateFieldName && MANUAL_REST_RESPONSE_FIELD_NAMES.has(resolvedSecretStateFieldName)) {
5832
+ throw new Error(`Manual REST contract secret state field must not reuse scaffolded response fields: ${Array.from(MANUAL_REST_RESPONSE_FIELD_NAMES).join(", ")}.`);
5833
+ }
5834
+ if (resolvedSecretFieldName && resolvedSecretStateFieldName && resolvedSecretFieldName === resolvedSecretStateFieldName) {
5835
+ throw new Error("Manual REST contract secret state field must be different from the raw secret field.");
5836
+ }
5837
+ const manualTypeNames = [
5838
+ resolvedQueryTypeName,
5839
+ resolvedResponseTypeName,
5840
+ resolvedBodyTypeName
5841
+ ].filter((value) => value != null);
5842
+ const duplicateManualTypeNames = manualTypeNames.filter((name, index) => manualTypeNames.indexOf(name) !== index);
5843
+ if (duplicateManualTypeNames.length > 0) {
5844
+ throw new Error(`Manual REST contract type names must be unique: ${Array.from(new Set(duplicateManualTypeNames)).join(", ")}. Use distinct --query-type, --body-type, and --response-type values.`);
5845
+ }
5846
+ const mutationSnapshot = {
5847
+ fileSources: await snapshotWorkspaceFiles([
5848
+ blockConfigPath,
5849
+ syncRestScriptPath
5850
+ ]),
5851
+ snapshotDirs: [],
5852
+ targetPaths: [restResourceDir]
5853
+ };
5854
+ try {
5855
+ await fsp6.mkdir(restResourceDir, { recursive: true });
5856
+ await ensureRestResourceSyncScriptAnchors(workspace);
5857
+ await fsp6.writeFile(typesFilePath, buildManualRestContractTypesSource({
5858
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5859
+ pathParameterNames,
5860
+ queryTypeName: resolvedQueryTypeName,
5861
+ responseTypeName: resolvedResponseTypeName,
5862
+ restResourceSlug,
5863
+ ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5864
+ ...resolvedSecretPreserveOnEmpty !== undefined ? { secretPreserveOnEmpty: resolvedSecretPreserveOnEmpty } : {},
5865
+ ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5866
+ }), "utf8");
5867
+ await fsp6.writeFile(validatorsFilePath, buildManualRestContractValidatorsSource({
5868
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5869
+ queryTypeName: resolvedQueryTypeName,
5870
+ responseTypeName: resolvedResponseTypeName
5871
+ }), "utf8");
5872
+ await fsp6.writeFile(apiFilePath, buildManualRestContractApiSource({
5873
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5874
+ queryTypeName: resolvedQueryTypeName,
5875
+ restResourceSlug
5876
+ }), "utf8");
5877
+ await syncManualRestContractArtifacts({
5878
+ clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
5879
+ outputDir: restResourceDir,
5880
+ projectDir: workspace.projectDir,
5881
+ typesFile: `src/rest/${restResourceSlug}/api-types.ts`,
5882
+ validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
5883
+ variables: {
5884
+ auth: resolvedAuth,
5885
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5886
+ method: resolvedMethod,
5887
+ namespace,
5888
+ pascalCase,
5889
+ pathPattern: resolvedPathPattern,
5890
+ queryTypeName: resolvedQueryTypeName,
5891
+ responseTypeName: resolvedResponseTypeName,
5892
+ slugKebabCase: restResourceSlug,
5893
+ title: toTitleCase(restResourceSlug)
5894
+ }
5895
+ });
5896
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
5897
+ restResourceEntries: [
5898
+ buildManualRestContractConfigEntry({
5899
+ auth: resolvedAuth,
5900
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5901
+ ...resolvedControllerClass ? { controllerClass: resolvedControllerClass } : {},
5902
+ ...resolvedControllerExtends ? { controllerExtends: resolvedControllerExtends } : {},
5903
+ method: resolvedMethod,
5904
+ namespace,
5905
+ pathPattern: resolvedPathPattern,
5906
+ ...resolvedPermissionCallback ? { permissionCallback: resolvedPermissionCallback } : {},
5907
+ queryTypeName: resolvedQueryTypeName,
5908
+ responseTypeName: resolvedResponseTypeName,
5909
+ restResourceSlug,
5910
+ ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5911
+ ...resolvedSecretPreserveOnEmpty !== undefined ? { secretPreserveOnEmpty: resolvedSecretPreserveOnEmpty } : {},
5912
+ ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5913
+ })
5914
+ ],
5915
+ transformSource: ensureBlockConfigCanAddRestManifests
5916
+ });
5917
+ return {
5918
+ auth: resolvedAuth,
5919
+ ...resolvedBodyTypeName ? { bodyTypeName: resolvedBodyTypeName } : {},
5920
+ ...resolvedControllerClass ? { controllerClass: resolvedControllerClass } : {},
5921
+ ...resolvedControllerExtends ? { controllerExtends: resolvedControllerExtends } : {},
5922
+ method: resolvedMethod,
5923
+ methods: [],
5924
+ mode: "manual",
5925
+ namespace,
5926
+ pathPattern: resolvedPathPattern,
5927
+ ...resolvedPermissionCallback ? { permissionCallback: resolvedPermissionCallback } : {},
5928
+ projectDir: workspace.projectDir,
5929
+ queryTypeName: resolvedQueryTypeName,
5930
+ restResourceSlug,
5931
+ responseTypeName: resolvedResponseTypeName,
5932
+ ...resolvedSecretFieldName ? { secretFieldName: resolvedSecretFieldName } : {},
5933
+ ...resolvedSecretPreserveOnEmpty !== undefined ? { secretPreserveOnEmpty: resolvedSecretPreserveOnEmpty } : {},
5934
+ ...resolvedSecretStateFieldName ? { secretStateFieldName: resolvedSecretStateFieldName } : {}
5935
+ };
5936
+ } catch (error) {
5937
+ await rollbackWorkspaceMutation(mutationSnapshot);
5938
+ throw error;
5939
+ }
5940
+ }
5941
+
5942
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
5943
+ async function runAddRestResourceCommand({
5944
+ auth,
5945
+ bodyTypeName,
5946
+ controllerClass,
5947
+ controllerExtends,
5948
+ cwd = process.cwd(),
5949
+ manual,
5950
+ method,
5951
+ methods,
5952
+ namespace,
5953
+ permissionCallback,
5954
+ pathPattern,
5955
+ queryTypeName,
5956
+ restResourceName,
5957
+ responseTypeName,
5958
+ routePattern,
5959
+ secretFieldName,
5960
+ secretHasValueFieldName,
5961
+ secretMaskedResponseFieldName,
5962
+ secretPreserveOnEmpty,
5963
+ secretStateFieldName
5964
+ }) {
5965
+ const workspace = resolveWorkspaceProject(cwd);
5966
+ const restResourceSlug = assertValidGeneratedSlug("REST resource name", normalizeBlockSlug(restResourceName), "wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create>]");
5967
+ const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
5968
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
5969
+ assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
5970
+ if (manual) {
5971
+ return scaffoldManualRestContract({
5972
+ auth,
5973
+ bodyTypeName,
5974
+ controllerClass,
5975
+ controllerExtends,
5976
+ method,
5977
+ namespace: resolvedNamespace,
5978
+ pathPattern,
5979
+ permissionCallback,
5980
+ queryTypeName,
5981
+ responseTypeName,
5982
+ restResourceSlug,
5983
+ routePattern,
5984
+ secretFieldName,
5985
+ secretHasValueFieldName,
5986
+ secretMaskedResponseFieldName,
5987
+ secretPreserveOnEmpty,
5988
+ secretStateFieldName,
5989
+ workspace
5990
+ });
5991
+ }
5992
+ return scaffoldGeneratedRestResource({
5993
+ controllerClass,
5994
+ controllerExtends,
5995
+ methods,
5996
+ namespace: resolvedNamespace,
5997
+ permissionCallback,
5998
+ restResourceSlug,
5999
+ routePattern,
6000
+ workspace
6001
+ });
6002
+ }
5990
6003
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-contract.ts
5991
6004
  import { promises as fsp7 } from "fs";
5992
6005
  import path11 from "path";
@@ -7189,7 +7202,9 @@ function ${enqueueFunctionName}() {
7189
7202
  }
7190
7203
  async function ensureAbilityPackageScripts(workspace) {
7191
7204
  const packageJsonPath = path14.join(workspace.projectDir, "package.json");
7192
- const packageJson = JSON.parse(await fsp9.readFile(packageJsonPath, "utf8"));
7205
+ const packageJson = await readJsonFile(packageJsonPath, {
7206
+ context: "workspace package manifest"
7207
+ });
7193
7208
  const nextScripts = {
7194
7209
  ...packageJson.scripts ?? {},
7195
7210
  "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts"
@@ -7568,7 +7583,9 @@ async function syncAiFeatureSchemaArtifact({
7568
7583
  projectDir
7569
7584
  }) {
7570
7585
  const sourceSchemaPath = path15.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
7571
- const responseSchema = assertJsonObject(JSON.parse(await readFile(sourceSchemaPath, "utf8")), sourceSchemaPath);
7586
+ const responseSchema = assertJsonObject(await readJsonFile(sourceSchemaPath, {
7587
+ context: "AI feature response schema"
7588
+ }), sourceSchemaPath);
7572
7589
  const aiSchema = projectWordPressAiSchema(responseSchema);
7573
7590
  await reconcileGeneratedArtifact({
7574
7591
  check,
@@ -7963,7 +7980,23 @@ async function reconcileGeneratedArtifact( options: {
7963
7980
  }
7964
7981
 
7965
7982
  async function loadJsonDocument( filePath: string ) {
7966
- const decoded = JSON.parse( await readFile( filePath, 'utf8' ) ) as unknown;
7983
+ let source: string;
7984
+ try {
7985
+ source = await readFile( filePath, 'utf8' );
7986
+ } catch ( error ) {
7987
+ throw new Error(
7988
+ \`Failed to read AI schema document at \${ filePath }: \${ error instanceof Error ? error.message : String( error ) }\`
7989
+ );
7990
+ }
7991
+
7992
+ let decoded: unknown;
7993
+ try {
7994
+ decoded = JSON.parse( source ) as unknown;
7995
+ } catch ( error ) {
7996
+ throw new Error(
7997
+ \`Failed to parse AI schema document at \${ filePath }: \${ error instanceof Error ? error.message : String( error ) }\`
7998
+ );
7999
+ }
7967
8000
  if ( ! decoded || typeof decoded !== 'object' || Array.isArray( decoded ) ) {
7968
8001
  throw new Error( \`Expected \${ filePath } to decode to a JSON object.\` );
7969
8002
  }
@@ -8054,7 +8087,9 @@ function ${registerFunctionName}() {
8054
8087
  }
8055
8088
  async function ensureAiFeaturePackageScripts(workspace) {
8056
8089
  const packageJsonPath = path16.join(workspace.projectDir, "package.json");
8057
- const packageJson = JSON.parse(await fsp10.readFile(packageJsonPath, "utf8"));
8090
+ const packageJson = await readJsonFile(packageJsonPath, {
8091
+ context: "workspace package manifest"
8092
+ });
8058
8093
  const nextScripts = {
8059
8094
  ...packageJson.scripts ?? {},
8060
8095
  "sync-ai": packageJson.scripts?.["sync-ai"] ?? "tsx scripts/sync-ai-features.ts"
@@ -9695,4 +9730,4 @@ export {
9695
9730
  ADD_BLOCK_TEMPLATE_IDS
9696
9731
  };
9697
9732
 
9698
- //# debugId=DC64DBB36FA16B4964756E2164756E21
9733
+ //# debugId=6B83DC9C0043FA6264756E2164756E21