@wp-typia/project-tools 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,14 +8,22 @@ import { PROJECT_TOOLS_PACKAGE_ROOT } from "./template-registry.js";
8
8
  import { toPascalCase, toTitleCase } from "./string-case.js";
9
9
  import { findPhpFunctionRange, hasPhpFunctionDefinition, quotePhpString, replacePhpFunctionDefinition, } from "./php-utils.js";
10
10
  import { assertAdminViewDoesNotExist, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
11
- const ADMIN_VIEW_SOURCE_KIND = "rest-resource";
11
+ import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from "./cli-diagnostics.js";
12
+ import { DEFAULT_WORDPRESS_DATAVIEWS_VERSION, DEFAULT_WORDPRESS_CORE_DATA_VERSION, DEFAULT_WORDPRESS_DATA_VERSION, DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, } from "./package-versions.js";
13
+ const ADMIN_VIEW_REST_SOURCE_KIND = "rest-resource";
14
+ const ADMIN_VIEW_CORE_DATA_SOURCE_KIND = "core-data";
15
+ const ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS = ["postType", "taxonomy"];
16
+ const ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
17
+ const ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
18
+ const ADMIN_VIEW_SOURCE_USAGE = "wp-typia add admin-view <name> --source <rest-resource:slug|core-data:kind/name>";
12
19
  const ADMIN_VIEWS_SCRIPT = "build/admin-views/index.js";
13
20
  const ADMIN_VIEWS_ASSET = "build/admin-views/index.asset.php";
14
21
  const ADMIN_VIEWS_STYLE = "build/admin-views/style-index.css";
15
22
  const ADMIN_VIEWS_STYLE_RTL = "build/admin-views/style-index-rtl.css";
16
23
  const ADMIN_VIEWS_PHP_GLOB = "/inc/admin-views/*.php";
17
- const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = "^0.1.0";
18
- const DEFAULT_WORDPRESS_DATAVIEWS_VERSION = "^14.1.0";
24
+ const ADMIN_VIEW_ALLOW_UNPUBLISHED_DATAVIEWS_ENV = "WP_TYPIA_ALLOW_UNPUBLISHED_DATAVIEWS";
25
+ // Lift this gate in the same release that publishes @wp-typia/dataviews.
26
+ const ADMIN_VIEW_PUBLIC_INSTALLS_ENABLED = false;
19
27
  const require = createRequire(import.meta.url);
20
28
  function toCamelCase(input) {
21
29
  const pascalCase = toPascalCase(input);
@@ -28,15 +36,26 @@ function normalizeVersionRange(value, fallback) {
28
36
  }
29
37
  return /^[~^<>=]/u.test(trimmed) ? trimmed : `^${trimmed}`;
30
38
  }
31
- function readPackageManifestVersion(packageJsonPath) {
39
+ function readPackageManifest(packageJsonPath) {
32
40
  try {
33
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
34
- return packageJson.version;
41
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
35
42
  }
36
43
  catch {
37
44
  return undefined;
38
45
  }
39
46
  }
47
+ function readPackageManifestVersion(packageJsonPath) {
48
+ return readPackageManifest(packageJsonPath)?.version;
49
+ }
50
+ function isAdminViewUnpublishedDataViewsOverrideEnabled() {
51
+ return process.env[ADMIN_VIEW_ALLOW_UNPUBLISHED_DATAVIEWS_ENV]?.trim() === "1";
52
+ }
53
+ function assertAdminViewPackageAvailability() {
54
+ if (isAdminViewUnpublishedDataViewsOverrideEnabled() || ADMIN_VIEW_PUBLIC_INSTALLS_ENABLED) {
55
+ return;
56
+ }
57
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, "`wp-typia add admin-view` is temporarily unavailable because `@wp-typia/dataviews` is not published to npm for public installs yet.");
58
+ }
40
59
  function detectJsonIndent(source) {
41
60
  const indentMatch = /\n([ \t]+)"/u.exec(source);
42
61
  return indentMatch?.[1] ?? 2;
@@ -62,22 +81,77 @@ function getAdminViewRelativeModuleSpecifier(adminViewSlug, workspaceFile) {
62
81
  const relativeModulePath = path.posix.relative(adminViewDir, modulePath);
63
82
  return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
64
83
  }
84
+ function isAdminViewCoreDataSource(source) {
85
+ return source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND;
86
+ }
87
+ function isAdminViewRestResourceSource(source) {
88
+ return source?.kind === ADMIN_VIEW_REST_SOURCE_KIND;
89
+ }
90
+ function assertValidCoreDataEntitySegment(label, value) {
91
+ const trimmed = value.trim();
92
+ if (!trimmed) {
93
+ throw new Error(`${label} is required. Use \`${ADMIN_VIEW_SOURCE_USAGE}\`.`);
94
+ }
95
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN.test(trimmed)) {
96
+ throw new Error(`${label} must start with a letter and contain only letters, numbers, underscores, or hyphens.`);
97
+ }
98
+ return trimmed;
99
+ }
100
+ function assertValidCoreDataEntityName(value) {
101
+ const normalized = value.trim();
102
+ if (!normalized) {
103
+ throw new Error(`Admin view source entity name is required. Use \`${ADMIN_VIEW_SOURCE_USAGE}\`.`);
104
+ }
105
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN.test(normalized)) {
106
+ throw new Error("Admin view source entity name must start with a lowercase letter or number and contain only lowercase letters, numbers, underscores, or hyphens.");
107
+ }
108
+ return normalized;
109
+ }
110
+ function assertValidCoreDataEntityKind(value) {
111
+ const normalized = assertValidCoreDataEntitySegment("Admin view source entity kind", value);
112
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS.includes(normalized)) {
113
+ throw new Error(`Admin view core-data sources currently support only: ${ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS.join(", ")}.`);
114
+ }
115
+ return normalized;
116
+ }
117
+ function formatAdminViewSourceLocator(source) {
118
+ if (isAdminViewCoreDataSource(source)) {
119
+ return `${source.kind}:${source.entityKind}/${source.entityName}`;
120
+ }
121
+ return `${source.kind}:${source.slug}`;
122
+ }
65
123
  function parseAdminViewSource(source) {
66
124
  const trimmed = source?.trim();
67
125
  if (!trimmed) {
68
126
  return undefined;
69
127
  }
70
- const [kind, slug, extra] = trimmed.split(":");
71
- if (kind !== ADMIN_VIEW_SOURCE_KIND || !slug || extra !== undefined) {
72
- throw new Error("Admin view source must use `rest-resource:<slug>` for now.");
128
+ const separatorIndex = trimmed.indexOf(":");
129
+ const kind = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
130
+ const locator = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 1);
131
+ if (!locator) {
132
+ throw new Error("Admin view source must use `rest-resource:<slug>` or `core-data:<kind>/<name>`.");
73
133
  }
74
- return {
75
- kind,
76
- slug: assertValidGeneratedSlug("Admin view source slug", normalizeBlockSlug(slug), "wp-typia add admin-view <name> --source rest-resource:<slug>"),
77
- };
134
+ if (kind === ADMIN_VIEW_REST_SOURCE_KIND) {
135
+ return {
136
+ kind,
137
+ slug: assertValidGeneratedSlug("Admin view source slug", locator, ADMIN_VIEW_SOURCE_USAGE),
138
+ };
139
+ }
140
+ if (kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND) {
141
+ const [entityKind, entityName, extra] = locator.split("/");
142
+ if (!entityKind || !entityName || extra !== undefined) {
143
+ throw new Error("Admin view core-data sources must use `core-data:<kind>/<name>`, for example `core-data:postType/post`.");
144
+ }
145
+ return {
146
+ entityKind: assertValidCoreDataEntityKind(entityKind),
147
+ entityName: assertValidCoreDataEntityName(entityName),
148
+ kind,
149
+ };
150
+ }
151
+ throw new Error("Admin view source must use `rest-resource:<slug>` or `core-data:<kind>/<name>`.");
78
152
  }
79
153
  function resolveRestResourceSource(restResources, source) {
80
- if (!source) {
154
+ if (!isAdminViewRestResourceSource(source)) {
81
155
  return undefined;
82
156
  }
83
157
  const restResource = restResources.find((entry) => entry.slug === source.slug);
@@ -97,7 +171,9 @@ function buildAdminViewConfigEntry(adminViewSlug, source) {
97
171
  `\t\tfile: ${quoteTsString(`src/admin-views/${adminViewSlug}/index.tsx`)},`,
98
172
  `\t\tphpFile: ${quoteTsString(`inc/admin-views/${adminViewSlug}.php`)},`,
99
173
  `\t\tslug: ${quoteTsString(adminViewSlug)},`,
100
- source ? `\t\tsource: ${quoteTsString(`${source.kind}:${source.slug}`)},` : null,
174
+ source
175
+ ? `\t\tsource: ${quoteTsString(formatAdminViewSourceLocator(source))},`
176
+ : null,
101
177
  "\t},",
102
178
  ]
103
179
  .filter((line) => typeof line === "string")
@@ -109,8 +185,12 @@ function buildAdminViewRegistrySource(adminViewSlugs) {
109
185
  .join("\n");
110
186
  return `${importLines}${importLines ? "\n\n" : ""}// wp-typia add admin-view entries\n`;
111
187
  }
112
- function buildAdminViewTypesSource(adminViewSlug, restResource) {
188
+ /**
189
+ * Build the generated admin-view item and dataset types for the selected source.
190
+ */
191
+ function buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource) {
113
192
  const pascalName = toPascalCase(adminViewSlug);
193
+ const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
114
194
  const itemTypeName = `${pascalName}AdminViewItem`;
115
195
  const dataSetTypeName = `${pascalName}AdminViewDataSet`;
116
196
  if (restResource) {
@@ -120,6 +200,74 @@ function buildAdminViewTypesSource(adminViewSlug, restResource) {
120
200
 
121
201
  export type ${itemTypeName} = ${restPascalName}Record;
122
202
 
203
+ export interface ${dataSetTypeName} {
204
+ \titems: ${itemTypeName}[];
205
+ \tpaginationInfo: {
206
+ \t\ttotalItems: number;
207
+ \t\ttotalPages: number;
208
+ \t};
209
+ }
210
+ `;
211
+ }
212
+ if (coreDataSource) {
213
+ if (coreDataSource.entityKind === "taxonomy") {
214
+ return `export interface ${coreDataRecordTypeName} {
215
+ \tcount?: number;
216
+ \tdescription?: string;
217
+ \tid: number;
218
+ \tlink?: string;
219
+ \tmeta?: Record<string, unknown>;
220
+ \tname?: string;
221
+ \tparent?: number;
222
+ \tslug?: string;
223
+ \ttaxonomy?: string;
224
+ \t[key: string]: unknown;
225
+ }
226
+
227
+ export interface ${itemTypeName} {
228
+ \tcount: number;
229
+ \tdescription: string;
230
+ \tid: number;
231
+ \tlink: string;
232
+ \tname: string;
233
+ \tparent: number;
234
+ \traw: ${coreDataRecordTypeName};
235
+ \tslug: string;
236
+ \ttaxonomy: string;
237
+ }
238
+
239
+ export interface ${dataSetTypeName} {
240
+ \titems: ${itemTypeName}[];
241
+ \tpaginationInfo: {
242
+ \t\ttotalItems: number;
243
+ \t\ttotalPages: number;
244
+ \t};
245
+ }
246
+ `;
247
+ }
248
+ return `export interface ${coreDataRecordTypeName} {
249
+ \tid: number;
250
+ \tdate?: string;
251
+ \tmodified?: string;
252
+ \tname?: string;
253
+ \tslug?: string;
254
+ \tstatus?: string;
255
+ \ttitle?: string | {
256
+ \t\traw?: string;
257
+ \t\trendered?: string;
258
+ \t};
259
+ \t[key: string]: unknown;
260
+ }
261
+
262
+ export interface ${itemTypeName} {
263
+ \tid: number;
264
+ \traw: ${coreDataRecordTypeName};
265
+ \tslug: string;
266
+ \tstatus: string;
267
+ \ttitle: string;
268
+ \tupdatedAt: string;
269
+ }
270
+
123
271
  export interface ${dataSetTypeName} {
124
272
  \titems: ${itemTypeName}[];
125
273
  \tpaginationInfo: {
@@ -148,19 +296,37 @@ export interface ${dataSetTypeName} {
148
296
  }
149
297
  `;
150
298
  }
151
- function buildAdminViewConfigSource(adminViewSlug, textDomain, restResource) {
299
+ /**
300
+ * Build the generated DataViews config source for an admin-view scaffold.
301
+ */
302
+ function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResource) {
152
303
  const pascalName = toPascalCase(adminViewSlug);
153
304
  const camelName = toCamelCase(adminViewSlug);
154
305
  const itemTypeName = `${pascalName}AdminViewItem`;
155
306
  const dataViewsName = `${camelName}AdminDataViews`;
307
+ const isCoreDataSource = source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND;
308
+ const isTaxonomyCoreDataSource = source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND &&
309
+ source.entityKind === "taxonomy";
156
310
  const defaultViewFields = restResource
157
311
  ? "['id']"
158
- : "['title', 'status', 'updatedAt']";
312
+ : isTaxonomyCoreDataSource
313
+ ? "['name', 'slug', 'count']"
314
+ : isCoreDataSource
315
+ ? "['title', 'slug', 'status', 'updatedAt']"
316
+ : "['title', 'status', 'updatedAt']";
159
317
  const searchEnabled = restResource ? "false" : "true";
160
- const titleFieldSource = restResource ? "" : "\ttitleField: 'title',\n";
318
+ const titleFieldSource = restResource
319
+ ? ""
320
+ : isTaxonomyCoreDataSource
321
+ ? "\ttitleField: 'name',\n"
322
+ : "\ttitleField: 'title',\n";
161
323
  const defaultViewEnhancementsSource = restResource
162
324
  ? ""
163
- : `\t\tsort: {
325
+ : isTaxonomyCoreDataSource
326
+ ? "\t\ttitleField: 'name',\n"
327
+ : isCoreDataSource
328
+ ? "\t\ttitleField: 'title',\n"
329
+ : `\t\tsort: {
164
330
  \t\t\tdirection: 'desc',
165
331
  \t\t\tfield: 'updatedAt',
166
332
  \t\t},
@@ -168,7 +334,58 @@ function buildAdminViewConfigSource(adminViewSlug, textDomain, restResource) {
168
334
  `;
169
335
  const additionalFieldsSource = restResource
170
336
  ? "\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."
171
- : `\t\towner: {
337
+ : isTaxonomyCoreDataSource
338
+ ? `\t\tcount: {
339
+ \t\t\tlabel: __( 'Count', ${quoteTsString(textDomain)} ),
340
+ \t\t\tschema: { type: 'integer' },
341
+ \t\t},
342
+ \t\tdescription: {
343
+ \t\t\tlabel: __( 'Description', ${quoteTsString(textDomain)} ),
344
+ \t\t\tschema: { type: 'string' },
345
+ \t\t},
346
+ \t\tlink: {
347
+ \t\t\tlabel: __( 'Link', ${quoteTsString(textDomain)} ),
348
+ \t\t\tschema: { format: 'uri', type: 'string' },
349
+ \t\t},
350
+ \t\tname: {
351
+ \t\t\tenableGlobalSearch: true,
352
+ \t\t\tlabel: __( 'Name', ${quoteTsString(textDomain)} ),
353
+ \t\t\tschema: { type: 'string' },
354
+ \t\t},
355
+ \t\tparent: {
356
+ \t\t\tlabel: __( 'Parent', ${quoteTsString(textDomain)} ),
357
+ \t\t\tschema: { type: 'integer' },
358
+ \t\t},
359
+ \t\tslug: {
360
+ \t\t\tenableGlobalSearch: true,
361
+ \t\t\tlabel: __( 'Slug', ${quoteTsString(textDomain)} ),
362
+ \t\t\tschema: { type: 'string' },
363
+ \t\t},
364
+ \t\ttaxonomy: {
365
+ \t\t\tlabel: __( 'Taxonomy', ${quoteTsString(textDomain)} ),
366
+ \t\t\tschema: { type: 'string' },
367
+ \t\t},`
368
+ : isCoreDataSource
369
+ ? `\t\tslug: {
370
+ \t\t\tenableGlobalSearch: true,
371
+ \t\t\tlabel: __( 'Slug', ${quoteTsString(textDomain)} ),
372
+ \t\t\tschema: { type: 'string' },
373
+ \t\t},
374
+ \t\tstatus: {
375
+ \t\t\tlabel: __( 'Status', ${quoteTsString(textDomain)} ),
376
+ \t\t\tschema: { type: 'string' },
377
+ \t\t},
378
+ \t\ttitle: {
379
+ \t\t\tenableGlobalSearch: true,
380
+ \t\t\tlabel: __( 'Name', ${quoteTsString(textDomain)} ),
381
+ \t\t\tschema: { type: 'string' },
382
+ \t\t},
383
+ \t\tupdatedAt: {
384
+ \t\t\tlabel: __( 'Updated', ${quoteTsString(textDomain)} ),
385
+ \t\t\tschema: { format: 'date-time', type: 'string' },
386
+ \t\t\ttype: 'datetime',
387
+ \t\t},`
388
+ : `\t\towner: {
172
389
  \t\t\tlabel: __( 'Owner', ${quoteTsString(textDomain)} ),
173
390
  \t\t\tschema: { type: 'string' },
174
391
  \t\t},
@@ -355,6 +572,220 @@ export async function ${fetchName}(
355
572
  }
356
573
  `;
357
574
  }
575
+ /**
576
+ * Build a core-data-backed admin-view data module for a supported entity family.
577
+ */
578
+ function buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) {
579
+ const pascalName = toPascalCase(adminViewSlug);
580
+ const camelName = toCamelCase(adminViewSlug);
581
+ const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
582
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
583
+ const itemTypeName = `${pascalName}AdminViewItem`;
584
+ const queryTypeName = `${pascalName}AdminViewQuery`;
585
+ const dataViewsName = `${camelName}AdminDataViews`;
586
+ const useEntityRecordName = `use${pascalName}EntityRecord`;
587
+ const useEntityRecordsName = `use${pascalName}EntityRecords`;
588
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
589
+ if (coreDataSource.entityKind === "taxonomy") {
590
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
591
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
592
+ import { useMemo } from '@wordpress/element';
593
+
594
+ import { ${dataViewsName} } from './config';
595
+ import type {
596
+ \t${coreDataRecordTypeName},
597
+ \t${dataSetTypeName},
598
+ \t${itemTypeName},
599
+ } from './types';
600
+
601
+ export interface ${queryTypeName} {
602
+ \tpage?: number;
603
+ \tper_page?: number;
604
+ \tsearch?: string;
605
+ }
606
+
607
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
608
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
609
+
610
+ function normalizeCoreDataNumber(value: unknown): number {
611
+ \treturn typeof value === 'number' && Number.isFinite(value) ? value : 0;
612
+ }
613
+
614
+ function normalizeCoreDataString(value: unknown): string {
615
+ \treturn typeof value === 'string' ? value : '';
616
+ }
617
+
618
+ function normalizeTaxonomyRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
619
+ \treturn {
620
+ \t\tcount: normalizeCoreDataNumber(record.count),
621
+ \t\tdescription: normalizeCoreDataString(record.description),
622
+ \t\tid: record.id,
623
+ \t\tlink: normalizeCoreDataString(record.link),
624
+ \t\tname: normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug),
625
+ \t\tparent: normalizeCoreDataNumber(record.parent),
626
+ \t\traw: record,
627
+ \t\tslug: normalizeCoreDataString(record.slug),
628
+ \t\ttaxonomy: normalizeCoreDataString(record.taxonomy),
629
+ \t};
630
+ \t}
631
+
632
+ export function ${useEntityRecordName}(recordId: number | undefined) {
633
+ \treturn useEntityRecord<${coreDataRecordTypeName}>(
634
+ \t\tCORE_DATA_ENTITY_KIND,
635
+ \t\tCORE_DATA_ENTITY_NAME,
636
+ \t\trecordId ?? 0,
637
+ \t\t{ enabled: typeof recordId === 'number' },
638
+ \t);
639
+ \t}
640
+
641
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
642
+ \tconst query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
643
+ \t\tperPageParam: 'per_page',
644
+ \t});
645
+
646
+ \treturn useEntityRecords<${coreDataRecordTypeName}>(
647
+ \t\tCORE_DATA_ENTITY_KIND,
648
+ \t\tCORE_DATA_ENTITY_NAME,
649
+ \t\tquery,
650
+ \t);
651
+ \t}
652
+
653
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
654
+ \tconst { hasResolved, isResolving, records, totalItems, totalPages } =
655
+ \t\t${useEntityRecordsName}(view);
656
+ \tconst items = useMemo(
657
+ \t\t() => (records ?? []).map((record) => normalizeTaxonomyRecord(record)),
658
+ \t\t[records],
659
+ \t);
660
+ \tconst dataSet = useMemo<${dataSetTypeName}>(
661
+ \t\t() => ({
662
+ \t\t\titems,
663
+ \t\t\tpaginationInfo: {
664
+ \t\t\t\ttotalItems: totalItems ?? items.length,
665
+ \t\t\t\ttotalPages: Math.max(1, totalPages ?? 1),
666
+ \t\t\t},
667
+ \t\t}),
668
+ \t\t[items, totalItems, totalPages],
669
+ \t);
670
+ \tconst error =
671
+ \t\t!isResolving && hasResolved && records === null
672
+ \t\t\t? 'Unable to load core-data entity records.'
673
+ \t\t\t: null;
674
+
675
+ \treturn {
676
+ \t\tdataSet,
677
+ \t\terror,
678
+ \t\tisLoading: isResolving,
679
+ \t};
680
+ \t}
681
+ `;
682
+ }
683
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
684
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
685
+ import { useMemo } from '@wordpress/element';
686
+
687
+ import { ${dataViewsName} } from './config';
688
+ import type {
689
+ \t${coreDataRecordTypeName},
690
+ \t${dataSetTypeName},
691
+ \t${itemTypeName},
692
+ } from './types';
693
+
694
+ export interface ${queryTypeName} {
695
+ \tpage?: number;
696
+ \tper_page?: number;
697
+ \tsearch?: string;
698
+ }
699
+
700
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
701
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
702
+
703
+ function normalizeCoreDataString(value: unknown): string {
704
+ \treturn typeof value === 'string' ? value : '';
705
+ }
706
+
707
+ function normalizeCoreDataTitle(record: ${coreDataRecordTypeName}): string {
708
+ \tif (typeof record.title === 'string') {
709
+ \t\treturn record.title;
710
+ \t}
711
+ \tif (record.title && typeof record.title === 'object') {
712
+ \t\tif (typeof record.title.rendered === 'string') {
713
+ \t\t\treturn record.title.rendered;
714
+ \t\t}
715
+ \t\tif (typeof record.title.raw === 'string') {
716
+ \t\t\treturn record.title.raw;
717
+ \t\t}
718
+ \t}
719
+
720
+ \treturn normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug);
721
+ }
722
+
723
+ function normalizeCoreDataUpdatedAt(record: ${coreDataRecordTypeName}): string {
724
+ \treturn normalizeCoreDataString(record.modified) || normalizeCoreDataString(record.date);
725
+ }
726
+
727
+ function normalizeCoreDataRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
728
+ \treturn {
729
+ \t\tid: record.id,
730
+ \t\traw: record,
731
+ \t\tslug: normalizeCoreDataString(record.slug),
732
+ \t\tstatus: normalizeCoreDataString(record.status),
733
+ \t\ttitle: normalizeCoreDataTitle(record),
734
+ \t\tupdatedAt: normalizeCoreDataUpdatedAt(record),
735
+ \t};
736
+ }
737
+
738
+ export function ${useEntityRecordName}(recordId: number | undefined) {
739
+ \treturn useEntityRecord<${coreDataRecordTypeName}>(
740
+ \t\tCORE_DATA_ENTITY_KIND,
741
+ \t\tCORE_DATA_ENTITY_NAME,
742
+ \t\trecordId ?? 0,
743
+ \t\t{ enabled: typeof recordId === 'number' },
744
+ \t);
745
+ }
746
+
747
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
748
+ \tconst query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
749
+ \t\tperPageParam: 'per_page',
750
+ \t});
751
+
752
+ \treturn useEntityRecords<${coreDataRecordTypeName}>(
753
+ \t\tCORE_DATA_ENTITY_KIND,
754
+ \t\tCORE_DATA_ENTITY_NAME,
755
+ \t\tquery,
756
+ \t);
757
+ }
758
+
759
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
760
+ \tconst { hasResolved, isResolving, records, totalItems, totalPages } =
761
+ \t\t${useEntityRecordsName}(view);
762
+ \tconst items = useMemo(
763
+ \t\t() => (records ?? []).map((record) => normalizeCoreDataRecord(record)),
764
+ \t\t[records],
765
+ \t);
766
+ \tconst dataSet = useMemo<${dataSetTypeName}>(
767
+ \t\t() => ({
768
+ \t\t\titems,
769
+ \t\t\tpaginationInfo: {
770
+ \t\t\t\ttotalItems: totalItems ?? items.length,
771
+ \t\t\t\ttotalPages: Math.max(1, totalPages ?? 1),
772
+ \t\t\t},
773
+ \t\t}),
774
+ \t\t[items, totalItems, totalPages],
775
+ \t);
776
+ \tconst error =
777
+ \t\t!isResolving && hasResolved && records === null
778
+ \t\t\t? 'Unable to load core-data entity records.'
779
+ \t\t\t: null;
780
+
781
+ \treturn {
782
+ \t\tdataSet,
783
+ \t\terror,
784
+ \t\tisLoading: isResolving,
785
+ \t};
786
+ }
787
+ `;
788
+ }
358
789
  function buildAdminViewScreenSource(adminViewSlug, textDomain) {
359
790
  const pascalName = toPascalCase(adminViewSlug);
360
791
  const camelName = toCamelCase(adminViewSlug);
@@ -468,6 +899,81 @@ export function ${componentName}() {
468
899
  }
469
900
  `;
470
901
  }
902
+ function buildCoreDataAdminViewScreenSource(adminViewSlug, textDomain) {
903
+ const pascalName = toPascalCase(adminViewSlug);
904
+ const camelName = toCamelCase(adminViewSlug);
905
+ const itemTypeName = `${pascalName}AdminViewItem`;
906
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
907
+ const componentName = `${pascalName}AdminViewScreen`;
908
+ const dataViewsName = `${camelName}AdminDataViews`;
909
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
910
+ const title = toTitleCase(adminViewSlug);
911
+ return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
912
+ import { Notice, Spinner } from '@wordpress/components';
913
+ import { useState } from '@wordpress/element';
914
+ import { __ } from '@wordpress/i18n';
915
+ import { DataViews } from '@wordpress/dataviews/wp';
916
+
917
+ import { ${dataViewsName} } from './config';
918
+ import { ${useAdminViewDataName} } from './data';
919
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
920
+
921
+ const TypedDataViews = DataViews as unknown as <TItem extends object>(
922
+ \tprops: DataViewsConfig<TItem>,
923
+ ) => ReturnType<typeof DataViews>;
924
+
925
+ const EMPTY_DATA_SET: ${dataSetTypeName} = {
926
+ \titems: [],
927
+ \tpaginationInfo: {
928
+ \t\ttotalItems: 0,
929
+ \t\ttotalPages: 1,
930
+ \t},
931
+ };
932
+
933
+ export function ${componentName}() {
934
+ \tconst [view, setView] = useState<DataViewsView<${itemTypeName}>>(
935
+ \t\t${dataViewsName}.defaultView,
936
+ \t);
937
+ \tconst {
938
+ \t\tdataSet = EMPTY_DATA_SET,
939
+ \t\terror,
940
+ \t\tisLoading,
941
+ \t} = ${useAdminViewDataName}(view);
942
+ \tconst config = ${dataViewsName}.createConfig({
943
+ \t\tdata: dataSet.items,
944
+ \t\tisLoading,
945
+ \t\tonChangeView: setView,
946
+ \t\tpaginationInfo: dataSet.paginationInfo,
947
+ \t\tview,
948
+ \t});
949
+
950
+ \treturn (
951
+ \t\t<div className="wp-typia-admin-view-screen">
952
+ \t\t\t<header className="wp-typia-admin-view-screen__header">
953
+ \t\t\t\t<div>
954
+ \t\t\t\t\t<p className="wp-typia-admin-view-screen__eyebrow">
955
+ \t\t\t\t\t\t{ __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
956
+ \t\t\t\t\t</p>
957
+ \t\t\t\t\t<h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
958
+ \t\t\t\t\t<p>
959
+ \t\t\t\t\t\t{ __( '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)} ) }
960
+ \t\t\t\t\t</p>
961
+ \t\t\t\t</div>
962
+ \t\t\t\t<div className="wp-typia-admin-view-screen__actions">
963
+ \t\t\t\t\t{ isLoading ? <Spinner /> : null }
964
+ \t\t\t\t</div>
965
+ \t\t\t</header>
966
+ \t\t\t{ error ? (
967
+ \t\t\t\t<Notice isDismissible={ false } status="error">
968
+ \t\t\t\t\t{ error }
969
+ \t\t\t\t</Notice>
970
+ \t\t\t) : null }
971
+ \t\t\t<TypedDataViews<${itemTypeName}> { ...config } />
972
+ \t\t</div>
973
+ \t);
974
+ }
975
+ `;
976
+ }
471
977
  function buildAdminViewEntrySource(adminViewSlug) {
472
978
  const pascalName = toPascalCase(adminViewSlug);
473
979
  const componentName = `${pascalName}AdminViewScreen`;
@@ -637,15 +1143,26 @@ add_action( 'admin_menu', '${registerFunctionName}' );
637
1143
  add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );
638
1144
  `;
639
1145
  }
640
- async function ensureAdminViewPackageDependencies(workspace) {
1146
+ async function ensureAdminViewPackageDependencies(workspace, adminViewSource) {
641
1147
  const packageJsonPath = path.join(workspace.projectDir, "package.json");
642
1148
  const wpTypiaDataViewsVersion = resolvePackageVersionRange("@wp-typia/dataviews", DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, "wp-typia-dataviews");
643
1149
  const wordpressDataViewsVersion = resolvePackageVersionRange("@wordpress/dataviews", DEFAULT_WORDPRESS_DATAVIEWS_VERSION);
1150
+ const wordpressCoreDataVersion = resolvePackageVersionRange("@wordpress/core-data", DEFAULT_WORDPRESS_CORE_DATA_VERSION);
1151
+ const wordpressDataVersion = resolvePackageVersionRange("@wordpress/data", DEFAULT_WORDPRESS_DATA_VERSION);
644
1152
  await patchFile(packageJsonPath, (source) => {
645
1153
  const packageJson = JSON.parse(source);
1154
+ const coreDataDependencies = isAdminViewCoreDataSource(adminViewSource)
1155
+ ? {
1156
+ "@wordpress/core-data": packageJson.dependencies?.["@wordpress/core-data"] ??
1157
+ wordpressCoreDataVersion,
1158
+ "@wordpress/data": packageJson.dependencies?.["@wordpress/data"] ??
1159
+ wordpressDataVersion,
1160
+ }
1161
+ : {};
646
1162
  const nextDependencies = {
647
1163
  ...(packageJson.dependencies ?? {}),
648
1164
  "@wordpress/dataviews": packageJson.dependencies?.["@wordpress/dataviews"] ?? wordpressDataViewsVersion,
1165
+ ...coreDataDependencies,
649
1166
  };
650
1167
  const nextDevDependencies = {
651
1168
  ...(packageJson.devDependencies ?? {}),
@@ -806,6 +1323,8 @@ async function writeAdminViewRegistry(projectDir, adminViewSlug) {
806
1323
  * Defaults to `process.cwd()`.
807
1324
  * @param options.source Optional data source locator. `rest-resource:<slug>`
808
1325
  * wires the screen to an existing list-capable REST resource.
1326
+ * `core-data:<kind>/<name>` binds the screen to a supported WordPress-owned
1327
+ * core-data entity collection.
809
1328
  * @returns A promise that resolves with the normalized `adminViewSlug`, optional
810
1329
  * `source`, and owning `projectDir` after scaffold files and inventory entries
811
1330
  * are written successfully.
@@ -814,10 +1333,14 @@ async function writeAdminViewRegistry(projectDir, adminViewSlug) {
814
1333
  */
815
1334
  export async function runAddAdminViewCommand({ adminViewName, cwd = process.cwd(), source, }) {
816
1335
  const workspace = resolveWorkspaceProject(cwd);
817
- const adminViewSlug = assertValidGeneratedSlug("Admin view name", normalizeBlockSlug(adminViewName), "wp-typia add admin-view <name> [--source rest-resource:<slug>]");
1336
+ assertAdminViewPackageAvailability();
1337
+ const adminViewSlug = assertValidGeneratedSlug("Admin view name", normalizeBlockSlug(adminViewName), "wp-typia add admin-view <name> [--source <rest-resource:slug|core-data:kind/name>]");
818
1338
  const parsedSource = parseAdminViewSource(source);
819
1339
  const inventory = readWorkspaceInventory(workspace.projectDir);
820
1340
  const restResource = resolveRestResourceSource(inventory.restResources, parsedSource);
1341
+ const coreDataSource = isAdminViewCoreDataSource(parsedSource)
1342
+ ? parsedSource
1343
+ : undefined;
821
1344
  assertAdminViewDoesNotExist(workspace.projectDir, adminViewSlug, inventory);
822
1345
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
823
1346
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
@@ -842,16 +1365,20 @@ export async function runAddAdminViewCommand({ adminViewName, cwd = process.cwd(
842
1365
  try {
843
1366
  await fsp.mkdir(adminViewDir, { recursive: true });
844
1367
  await fsp.mkdir(path.dirname(adminViewPhpPath), { recursive: true });
845
- await ensureAdminViewPackageDependencies(workspace);
1368
+ await ensureAdminViewPackageDependencies(workspace, parsedSource);
846
1369
  await ensureAdminViewBootstrapAnchors(workspace);
847
1370
  await ensureAdminViewBuildScriptAnchors(workspace);
848
1371
  await ensureAdminViewWebpackAnchors(workspace);
849
- await fsp.writeFile(path.join(adminViewDir, "types.ts"), buildAdminViewTypesSource(adminViewSlug, restResource), "utf8");
850
- await fsp.writeFile(path.join(adminViewDir, "config.ts"), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, restResource), "utf8");
851
- await fsp.writeFile(path.join(adminViewDir, "data.ts"), restResource
852
- ? buildRestAdminViewDataSource(adminViewSlug, restResource)
853
- : buildDefaultAdminViewDataSource(adminViewSlug), "utf8");
854
- await fsp.writeFile(path.join(adminViewDir, "Screen.tsx"), buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), "utf8");
1372
+ await fsp.writeFile(path.join(adminViewDir, "types.ts"), buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), "utf8");
1373
+ await fsp.writeFile(path.join(adminViewDir, "config.ts"), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), "utf8");
1374
+ await fsp.writeFile(path.join(adminViewDir, "data.ts"), coreDataSource
1375
+ ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource)
1376
+ : restResource
1377
+ ? buildRestAdminViewDataSource(adminViewSlug, restResource)
1378
+ : buildDefaultAdminViewDataSource(adminViewSlug), "utf8");
1379
+ await fsp.writeFile(path.join(adminViewDir, "Screen.tsx"), coreDataSource
1380
+ ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
1381
+ : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), "utf8");
855
1382
  await fsp.writeFile(path.join(adminViewDir, "index.tsx"), buildAdminViewEntrySource(adminViewSlug), "utf8");
856
1383
  await fsp.writeFile(path.join(adminViewDir, "style.scss"), buildAdminViewStyleSource(), "utf8");
857
1384
  await fsp.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), "utf8");
@@ -862,7 +1389,9 @@ export async function runAddAdminViewCommand({ adminViewName, cwd = process.cwd(
862
1389
  return {
863
1390
  adminViewSlug,
864
1391
  projectDir: workspace.projectDir,
865
- source: parsedSource ? `${parsedSource.kind}:${parsedSource.slug}` : undefined,
1392
+ source: parsedSource
1393
+ ? formatAdminViewSourceLocator(parsedSource)
1394
+ : undefined,
866
1395
  };
867
1396
  }
868
1397
  catch (error) {