wp-typia 0.20.5 → 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.
@@ -20,13 +20,19 @@ import {
20
20
  scaffoldProject,
21
21
  syncPersistenceRestArtifacts,
22
22
  updatePluginHeaderCompatibility
23
- } from "./cli-j1tyw390.js";
23
+ } from "./cli-1w5vkye4.js";
24
24
  import {
25
+ DEFAULT_WORDPRESS_ABILITIES_VERSION,
26
+ DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION,
27
+ DEFAULT_WORDPRESS_CORE_DATA_VERSION,
28
+ DEFAULT_WORDPRESS_DATAVIEWS_VERSION,
29
+ DEFAULT_WORDPRESS_DATA_VERSION,
30
+ DEFAULT_WP_TYPIA_DATAVIEWS_VERSION,
25
31
  getPackageVersions
26
- } from "./cli-tesygdnr.js";
32
+ } from "./cli-39er8888.js";
27
33
  import {
28
34
  snapshotProjectVersion
29
- } from "./cli-2rev5hqm.js";
35
+ } from "./cli-e623rs7g.js";
30
36
  import {
31
37
  ensureMigrationDirectories,
32
38
  parseMigrationConfig,
@@ -39,6 +45,7 @@ import {
39
45
  } from "./cli-sj5mtyzj.js";
40
46
  import"./cli-10pe4mf8.js";
41
47
  import {
48
+ PROJECT_TOOLS_PACKAGE_ROOT,
42
49
  SHARED_WORKSPACE_TEMPLATE_ROOT
43
50
  } from "./cli-tke8twkn.js";
44
51
  import {
@@ -47,6 +54,7 @@ import {
47
54
  EDITOR_PLUGIN_SLOT_IDS,
48
55
  appendWorkspaceInventoryEntries,
49
56
  assertAbilityDoesNotExist,
57
+ assertAdminViewDoesNotExist,
50
58
  assertAiFeatureDoesNotExist,
51
59
  assertBindingSourceDoesNotExist,
52
60
  assertEditorPluginDoesNotExist,
@@ -59,28 +67,40 @@ import {
59
67
  assertValidRestResourceMethods,
60
68
  assertVariationDoesNotExist,
61
69
  buildWorkspacePhpPrefix,
70
+ escapeRegex,
71
+ findPhpFunctionRange,
62
72
  formatAddHelpText,
63
73
  getMutableBlockHooks,
64
74
  getWorkspaceBlockSelectOptions,
65
75
  getWorkspaceBootstrapPath,
76
+ hasPhpFunctionDefinition,
66
77
  isAddBlockTemplateId,
67
78
  normalizeBlockSlug,
68
79
  patchFile,
80
+ quotePhpString,
69
81
  quoteTsString,
70
82
  readOptionalFile,
71
83
  readWorkspaceBlockJson,
72
84
  readWorkspaceInventory,
85
+ replacePhpFunctionDefinition,
86
+ require_typescript,
73
87
  resolveNonEmptyNormalizedBlockSlug,
74
88
  resolveRestResourceNamespace,
75
89
  resolveWorkspaceBlock,
76
90
  rollbackWorkspaceMutation,
77
91
  snapshotWorkspaceFiles,
78
92
  toKebabCase,
93
+ toPascalCase,
94
+ toSnakeCase,
79
95
  toTitleCase
80
- } from "./cli-3w3qxq9w.js";
96
+ } from "./cli-j180bk07.js";
81
97
  import {
82
98
  createManagedTempRoot
83
99
  } from "./cli-t73q5aqz.js";
100
+ import {
101
+ CLI_DIAGNOSTIC_CODES,
102
+ createCliDiagnosticCodeError
103
+ } from "./cli-p95wr1q8.js";
84
104
  import {
85
105
  resolveWorkspaceProject
86
106
  } from "./cli-pd5pqgre.js";
@@ -669,104 +689,1462 @@ async function runAddBlockCommand({
669
689
  externalLayerSource: normalizedExternalLayerSource,
670
690
  selectExternalLayerId
671
691
  });
672
- let tempRoot = "";
673
- let cleanupTempRoot;
692
+ let tempRoot = "";
693
+ let cleanupTempRoot;
694
+ try {
695
+ const normalizedSlug = resolveNonEmptyNormalizedBlockSlug({
696
+ input: blockName,
697
+ label: "Block name",
698
+ usage: "wp-typia add block <name> --template <family>"
699
+ });
700
+ const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
701
+ ({
702
+ path: tempRoot,
703
+ cleanup: cleanupTempRoot
704
+ } = await createManagedTempRoot("wp-typia-add-block-"));
705
+ const tempProjectDir = path3.join(tempRoot, normalizedSlug);
706
+ const blockConfigPath = path3.join(workspace.projectDir, "scripts", "block-config.ts");
707
+ const migrationConfigPath = path3.join(workspace.projectDir, "src", "migrations", "config.ts");
708
+ const blockPhpPrefix = buildWorkspacePhpPrefix(workspace.workspace.phpPrefix, normalizedSlug);
709
+ const migrationConfigSource = await readOptionalFile(migrationConfigPath);
710
+ const migrationConfig = migrationConfigSource === null ? null : parseMigrationConfig(migrationConfigSource);
711
+ const compoundSupportPaths = resolvedTemplateId === "compound" ? COMPOUND_SHARED_SUPPORT_FILES.map((fileName) => path3.join(workspace.projectDir, "src", fileName)) : [];
712
+ const legacyCompoundValidatorPaths = resolvedTemplateId === "compound" ? await collectLegacyCompoundValidatorPaths(workspace.projectDir) : [];
713
+ const result = await (async () => {
714
+ const scaffoldResult = await scaffoldProject({
715
+ alternateRenderTargets,
716
+ answers: {
717
+ ...defaults,
718
+ author: workspace.author,
719
+ compoundInnerBlocksPreset: resolvedInnerBlocksPreset,
720
+ namespace: workspace.workspace.namespace,
721
+ phpPrefix: blockPhpPrefix,
722
+ slug: normalizedSlug,
723
+ textDomain: workspace.workspace.textDomain,
724
+ title: defaults.title
725
+ },
726
+ cwd: workspace.projectDir,
727
+ dataStorageMode,
728
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
729
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
730
+ externalLayerSourceLabel: normalizedExternalLayerSource,
731
+ noInstall: true,
732
+ packageManager: workspace.packageManager,
733
+ persistencePolicy,
734
+ projectDir: tempProjectDir,
735
+ templateId: resolvedTemplateId
736
+ });
737
+ await assertAddBlockSupportsExternalLayerOutputs({
738
+ callerCwd: cwd,
739
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
740
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
741
+ templateId: resolvedTemplateId,
742
+ variables: scaffoldResult.variables
743
+ });
744
+ return scaffoldResult;
745
+ })();
746
+ assertBlockTargetsDoNotExist(workspace.projectDir, resolvedTemplateId, result.variables);
747
+ const mutationSnapshot = {
748
+ fileSources: await snapshotWorkspaceFiles([
749
+ blockConfigPath,
750
+ migrationConfigPath,
751
+ ...compoundSupportPaths,
752
+ ...legacyCompoundValidatorPaths
753
+ ]),
754
+ snapshotDirs: migrationConfig === null ? [] : buildMigrationBlocks(resolvedTemplateId, result.variables).map((block) => path3.join(workspace.projectDir, ...migrationConfig.snapshotDir.split("/"), migrationConfig.currentMigrationVersion, block.key)),
755
+ targetPaths: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables)
756
+ };
757
+ try {
758
+ await copyScaffoldedBlockSlice(workspace.projectDir, resolvedTemplateId, tempProjectDir, result.variables, legacyCompoundValidatorPaths);
759
+ await addCollectionImportsForTemplate(workspace.projectDir, resolvedTemplateId, result.variables);
760
+ await appendBlockConfigEntries(workspace.projectDir, buildConfigEntries(resolvedTemplateId, result.variables), resolvedTemplateId === "persistence" || resolvedTemplateId === "compound" && result.variables.compoundPersistenceEnabled === "true");
761
+ await syncWorkspaceAddedBlockArtifacts(workspace.projectDir, resolvedTemplateId, result.variables);
762
+ await updateWorkspaceMigrationConfigIfPresent(workspace.projectDir, buildMigrationBlocks(resolvedTemplateId, result.variables));
763
+ return {
764
+ blockSlugs: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables).map((targetPath) => path3.basename(targetPath)),
765
+ projectDir: workspace.projectDir,
766
+ templateId: resolvedTemplateId,
767
+ warnings: result.warnings
768
+ };
769
+ } catch (error) {
770
+ await rollbackWorkspaceMutation(mutationSnapshot);
771
+ throw error;
772
+ }
773
+ } finally {
774
+ try {
775
+ await resolvedExternalLayerSelection.cleanup?.();
776
+ } finally {
777
+ await cleanupTempRoot?.();
778
+ }
779
+ }
780
+ }
781
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
782
+ import fs6 from "fs";
783
+ import { promises as fsp9 } from "fs";
784
+ import path13 from "path";
785
+
786
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view.ts
787
+ import fs3 from "fs";
788
+ import { promises as fsp3 } from "fs";
789
+ import { createRequire } from "module";
790
+ import path4 from "path";
791
+ var ADMIN_VIEW_REST_SOURCE_KIND = "rest-resource";
792
+ var ADMIN_VIEW_CORE_DATA_SOURCE_KIND = "core-data";
793
+ var ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS = ["postType", "taxonomy"];
794
+ var ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
795
+ var ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
796
+ var ADMIN_VIEW_SOURCE_USAGE = "wp-typia add admin-view <name> --source <rest-resource:slug|core-data:kind/name>";
797
+ var ADMIN_VIEWS_SCRIPT = "build/admin-views/index.js";
798
+ var ADMIN_VIEWS_ASSET = "build/admin-views/index.asset.php";
799
+ var ADMIN_VIEWS_STYLE = "build/admin-views/style-index.css";
800
+ var ADMIN_VIEWS_STYLE_RTL = "build/admin-views/style-index-rtl.css";
801
+ var ADMIN_VIEWS_PHP_GLOB = "/inc/admin-views/*.php";
802
+ var ADMIN_VIEW_ALLOW_UNPUBLISHED_DATAVIEWS_ENV = "WP_TYPIA_ALLOW_UNPUBLISHED_DATAVIEWS";
803
+ var ADMIN_VIEW_PUBLIC_INSTALLS_ENABLED = false;
804
+ var require2 = createRequire(import.meta.url);
805
+ function toCamelCase(input) {
806
+ const pascalCase = toPascalCase(input);
807
+ return `${pascalCase.charAt(0).toLowerCase()}${pascalCase.slice(1)}`;
808
+ }
809
+ function normalizeVersionRange(value, fallback) {
810
+ const trimmed = value?.trim();
811
+ if (!trimmed || trimmed.startsWith("workspace:")) {
812
+ return fallback;
813
+ }
814
+ return /^[~^<>=]/u.test(trimmed) ? trimmed : `^${trimmed}`;
815
+ }
816
+ function readPackageManifest(packageJsonPath) {
817
+ try {
818
+ return JSON.parse(fs3.readFileSync(packageJsonPath, "utf8"));
819
+ } catch {
820
+ return;
821
+ }
822
+ }
823
+ function readPackageManifestVersion(packageJsonPath) {
824
+ return readPackageManifest(packageJsonPath)?.version;
825
+ }
826
+ function isAdminViewUnpublishedDataViewsOverrideEnabled() {
827
+ return process.env[ADMIN_VIEW_ALLOW_UNPUBLISHED_DATAVIEWS_ENV]?.trim() === "1";
828
+ }
829
+ function assertAdminViewPackageAvailability() {
830
+ if (isAdminViewUnpublishedDataViewsOverrideEnabled() || ADMIN_VIEW_PUBLIC_INSTALLS_ENABLED) {
831
+ return;
832
+ }
833
+ 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.");
834
+ }
835
+ function detectJsonIndent(source) {
836
+ const indentMatch = /\n([ \t]+)"/u.exec(source);
837
+ return indentMatch?.[1] ?? 2;
838
+ }
839
+ function resolvePackageVersionRange(packageName, fallback, workspacePackageDirName) {
840
+ if (workspacePackageDirName) {
841
+ const workspaceVersion = readPackageManifestVersion(path4.join(PROJECT_TOOLS_PACKAGE_ROOT, "..", workspacePackageDirName, "package.json"));
842
+ if (workspaceVersion) {
843
+ return normalizeVersionRange(workspaceVersion, fallback);
844
+ }
845
+ }
846
+ try {
847
+ return normalizeVersionRange(readPackageManifestVersion(require2.resolve(`${packageName}/package.json`)), fallback);
848
+ } catch {
849
+ return fallback;
850
+ }
851
+ }
852
+ function getAdminViewRelativeModuleSpecifier(adminViewSlug, workspaceFile) {
853
+ const adminViewDir = `src/admin-views/${adminViewSlug}`;
854
+ const normalizedFile = workspaceFile.replace(/\\/gu, "/");
855
+ const modulePath = normalizedFile.replace(/\.[cm]?[jt]sx?$/u, "");
856
+ const relativeModulePath = path4.posix.relative(adminViewDir, modulePath);
857
+ return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
858
+ }
859
+ function isAdminViewCoreDataSource(source) {
860
+ return source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND;
861
+ }
862
+ function isAdminViewRestResourceSource(source) {
863
+ return source?.kind === ADMIN_VIEW_REST_SOURCE_KIND;
864
+ }
865
+ function assertValidCoreDataEntitySegment(label, value) {
866
+ const trimmed = value.trim();
867
+ if (!trimmed) {
868
+ throw new Error(`${label} is required. Use \`${ADMIN_VIEW_SOURCE_USAGE}\`.`);
869
+ }
870
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_SEGMENT_PATTERN.test(trimmed)) {
871
+ throw new Error(`${label} must start with a letter and contain only letters, numbers, underscores, or hyphens.`);
872
+ }
873
+ return trimmed;
874
+ }
875
+ function assertValidCoreDataEntityName(value) {
876
+ const normalized = value.trim();
877
+ if (!normalized) {
878
+ throw new Error(`Admin view source entity name is required. Use \`${ADMIN_VIEW_SOURCE_USAGE}\`.`);
879
+ }
880
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_NAME_PATTERN.test(normalized)) {
881
+ 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.");
882
+ }
883
+ return normalized;
884
+ }
885
+ function assertValidCoreDataEntityKind(value) {
886
+ const normalized = assertValidCoreDataEntitySegment("Admin view source entity kind", value);
887
+ if (!ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS.includes(normalized)) {
888
+ throw new Error(`Admin view core-data sources currently support only: ${ADMIN_VIEW_CORE_DATA_ENTITY_KIND_IDS.join(", ")}.`);
889
+ }
890
+ return normalized;
891
+ }
892
+ function formatAdminViewSourceLocator(source) {
893
+ if (isAdminViewCoreDataSource(source)) {
894
+ return `${source.kind}:${source.entityKind}/${source.entityName}`;
895
+ }
896
+ return `${source.kind}:${source.slug}`;
897
+ }
898
+ function parseAdminViewSource(source) {
899
+ const trimmed = source?.trim();
900
+ if (!trimmed) {
901
+ return;
902
+ }
903
+ const separatorIndex = trimmed.indexOf(":");
904
+ const kind = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
905
+ const locator = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 1);
906
+ if (!locator) {
907
+ throw new Error("Admin view source must use `rest-resource:<slug>` or `core-data:<kind>/<name>`.");
908
+ }
909
+ if (kind === ADMIN_VIEW_REST_SOURCE_KIND) {
910
+ return {
911
+ kind,
912
+ slug: assertValidGeneratedSlug("Admin view source slug", locator, ADMIN_VIEW_SOURCE_USAGE)
913
+ };
914
+ }
915
+ if (kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND) {
916
+ const [entityKind, entityName, extra] = locator.split("/");
917
+ if (!entityKind || !entityName || extra !== undefined) {
918
+ throw new Error("Admin view core-data sources must use `core-data:<kind>/<name>`, for example `core-data:postType/post`.");
919
+ }
920
+ return {
921
+ entityKind: assertValidCoreDataEntityKind(entityKind),
922
+ entityName: assertValidCoreDataEntityName(entityName),
923
+ kind
924
+ };
925
+ }
926
+ throw new Error("Admin view source must use `rest-resource:<slug>` or `core-data:<kind>/<name>`.");
927
+ }
928
+ function resolveRestResourceSource(restResources, source) {
929
+ if (!isAdminViewRestResourceSource(source)) {
930
+ return;
931
+ }
932
+ const restResource = restResources.find((entry) => entry.slug === source.slug);
933
+ if (!restResource) {
934
+ throw new Error(`Unknown REST resource source "${source.slug}". Choose one of: ${restResources.map((entry) => entry.slug).join(", ") || "<none>"}.`);
935
+ }
936
+ if (!restResource.methods.includes("list")) {
937
+ throw new Error(`REST resource source "${source.slug}" must include the list method for DataViews pagination.`);
938
+ }
939
+ return restResource;
940
+ }
941
+ function buildAdminViewConfigEntry(adminViewSlug, source) {
942
+ return [
943
+ "\t{",
944
+ ` file: ${quoteTsString(`src/admin-views/${adminViewSlug}/index.tsx`)},`,
945
+ ` phpFile: ${quoteTsString(`inc/admin-views/${adminViewSlug}.php`)},`,
946
+ ` slug: ${quoteTsString(adminViewSlug)},`,
947
+ source ? ` source: ${quoteTsString(formatAdminViewSourceLocator(source))},` : null,
948
+ "\t},"
949
+ ].filter((line) => typeof line === "string").join(`
950
+ `);
951
+ }
952
+ function buildAdminViewRegistrySource(adminViewSlugs) {
953
+ const importLines = adminViewSlugs.map((adminViewSlug) => `import './${adminViewSlug}';`).join(`
954
+ `);
955
+ return `${importLines}${importLines ? `
956
+
957
+ ` : ""}// wp-typia add admin-view entries
958
+ `;
959
+ }
960
+ function buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource) {
961
+ const pascalName = toPascalCase(adminViewSlug);
962
+ const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
963
+ const itemTypeName = `${pascalName}AdminViewItem`;
964
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
965
+ if (restResource) {
966
+ const restPascalName = toPascalCase(restResource.slug);
967
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
968
+ return `import type { ${restPascalName}Record } from ${quoteTsString(restTypesModule)};
969
+
970
+ export type ${itemTypeName} = ${restPascalName}Record;
971
+
972
+ export interface ${dataSetTypeName} {
973
+ items: ${itemTypeName}[];
974
+ paginationInfo: {
975
+ totalItems: number;
976
+ totalPages: number;
977
+ };
978
+ }
979
+ `;
980
+ }
981
+ if (coreDataSource) {
982
+ if (coreDataSource.entityKind === "taxonomy") {
983
+ return `export interface ${coreDataRecordTypeName} {
984
+ count?: number;
985
+ description?: string;
986
+ id: number;
987
+ link?: string;
988
+ meta?: Record<string, unknown>;
989
+ name?: string;
990
+ parent?: number;
991
+ slug?: string;
992
+ taxonomy?: string;
993
+ [key: string]: unknown;
994
+ }
995
+
996
+ export interface ${itemTypeName} {
997
+ count: number;
998
+ description: string;
999
+ id: number;
1000
+ link: string;
1001
+ name: string;
1002
+ parent: number;
1003
+ raw: ${coreDataRecordTypeName};
1004
+ slug: string;
1005
+ taxonomy: string;
1006
+ }
1007
+
1008
+ export interface ${dataSetTypeName} {
1009
+ items: ${itemTypeName}[];
1010
+ paginationInfo: {
1011
+ totalItems: number;
1012
+ totalPages: number;
1013
+ };
1014
+ }
1015
+ `;
1016
+ }
1017
+ return `export interface ${coreDataRecordTypeName} {
1018
+ id: number;
1019
+ date?: string;
1020
+ modified?: string;
1021
+ name?: string;
1022
+ slug?: string;
1023
+ status?: string;
1024
+ title?: string | {
1025
+ raw?: string;
1026
+ rendered?: string;
1027
+ };
1028
+ [key: string]: unknown;
1029
+ }
1030
+
1031
+ export interface ${itemTypeName} {
1032
+ id: number;
1033
+ raw: ${coreDataRecordTypeName};
1034
+ slug: string;
1035
+ status: string;
1036
+ title: string;
1037
+ updatedAt: string;
1038
+ }
1039
+
1040
+ export interface ${dataSetTypeName} {
1041
+ items: ${itemTypeName}[];
1042
+ paginationInfo: {
1043
+ totalItems: number;
1044
+ totalPages: number;
1045
+ };
1046
+ }
1047
+ `;
1048
+ }
1049
+ return `export type ${pascalName}AdminViewStatus = 'draft' | 'published';
1050
+
1051
+ export interface ${itemTypeName} {
1052
+ id: number;
1053
+ owner: string;
1054
+ status: ${pascalName}AdminViewStatus;
1055
+ title: string;
1056
+ updatedAt: string;
1057
+ }
1058
+
1059
+ export interface ${dataSetTypeName} {
1060
+ items: ${itemTypeName}[];
1061
+ paginationInfo: {
1062
+ totalItems: number;
1063
+ totalPages: number;
1064
+ };
1065
+ }
1066
+ `;
1067
+ }
1068
+ function buildAdminViewConfigSource(adminViewSlug, textDomain, source, restResource) {
1069
+ const pascalName = toPascalCase(adminViewSlug);
1070
+ const camelName = toCamelCase(adminViewSlug);
1071
+ const itemTypeName = `${pascalName}AdminViewItem`;
1072
+ const dataViewsName = `${camelName}AdminDataViews`;
1073
+ const isCoreDataSource = source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND;
1074
+ const isTaxonomyCoreDataSource = source?.kind === ADMIN_VIEW_CORE_DATA_SOURCE_KIND && source.entityKind === "taxonomy";
1075
+ const defaultViewFields = restResource ? "['id']" : isTaxonomyCoreDataSource ? "['name', 'slug', 'count']" : isCoreDataSource ? "['title', 'slug', 'status', 'updatedAt']" : "['title', 'status', 'updatedAt']";
1076
+ const searchEnabled = restResource ? "false" : "true";
1077
+ const titleFieldSource = restResource ? "" : isTaxonomyCoreDataSource ? ` titleField: 'name',
1078
+ ` : ` titleField: 'title',
1079
+ `;
1080
+ const defaultViewEnhancementsSource = restResource ? "" : isTaxonomyCoreDataSource ? ` titleField: 'name',
1081
+ ` : isCoreDataSource ? ` titleField: 'title',
1082
+ ` : ` sort: {
1083
+ direction: 'desc',
1084
+ field: 'updatedAt',
1085
+ },
1086
+ titleField: 'title',
1087
+ `;
1088
+ 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: {
1089
+ label: __( 'Count', ${quoteTsString(textDomain)} ),
1090
+ schema: { type: 'integer' },
1091
+ },
1092
+ description: {
1093
+ label: __( 'Description', ${quoteTsString(textDomain)} ),
1094
+ schema: { type: 'string' },
1095
+ },
1096
+ link: {
1097
+ label: __( 'Link', ${quoteTsString(textDomain)} ),
1098
+ schema: { format: 'uri', type: 'string' },
1099
+ },
1100
+ name: {
1101
+ enableGlobalSearch: true,
1102
+ label: __( 'Name', ${quoteTsString(textDomain)} ),
1103
+ schema: { type: 'string' },
1104
+ },
1105
+ parent: {
1106
+ label: __( 'Parent', ${quoteTsString(textDomain)} ),
1107
+ schema: { type: 'integer' },
1108
+ },
1109
+ slug: {
1110
+ enableGlobalSearch: true,
1111
+ label: __( 'Slug', ${quoteTsString(textDomain)} ),
1112
+ schema: { type: 'string' },
1113
+ },
1114
+ taxonomy: {
1115
+ label: __( 'Taxonomy', ${quoteTsString(textDomain)} ),
1116
+ schema: { type: 'string' },
1117
+ },` : isCoreDataSource ? ` slug: {
1118
+ enableGlobalSearch: true,
1119
+ label: __( 'Slug', ${quoteTsString(textDomain)} ),
1120
+ schema: { type: 'string' },
1121
+ },
1122
+ status: {
1123
+ label: __( 'Status', ${quoteTsString(textDomain)} ),
1124
+ schema: { type: 'string' },
1125
+ },
1126
+ title: {
1127
+ enableGlobalSearch: true,
1128
+ label: __( 'Name', ${quoteTsString(textDomain)} ),
1129
+ schema: { type: 'string' },
1130
+ },
1131
+ updatedAt: {
1132
+ label: __( 'Updated', ${quoteTsString(textDomain)} ),
1133
+ schema: { format: 'date-time', type: 'string' },
1134
+ type: 'datetime',
1135
+ },` : ` owner: {
1136
+ label: __( 'Owner', ${quoteTsString(textDomain)} ),
1137
+ schema: { type: 'string' },
1138
+ },
1139
+ status: {
1140
+ filterBy: { operators: ['isAny', 'isNone'] },
1141
+ label: __( 'Status', ${quoteTsString(textDomain)} ),
1142
+ schema: {
1143
+ enum: ['draft', 'published'],
1144
+ enumLabels: {
1145
+ draft: __( 'Draft', ${quoteTsString(textDomain)} ),
1146
+ published: __( 'Published', ${quoteTsString(textDomain)} ),
1147
+ },
1148
+ type: 'string',
1149
+ },
1150
+ },
1151
+ title: {
1152
+ enableGlobalSearch: true,
1153
+ enableSorting: true,
1154
+ label: __( 'Title', ${quoteTsString(textDomain)} ),
1155
+ schema: { type: 'string' },
1156
+ },
1157
+ updatedAt: {
1158
+ enableSorting: true,
1159
+ label: __( 'Updated', ${quoteTsString(textDomain)} ),
1160
+ schema: { format: 'date-time', type: 'string' },
1161
+ type: 'datetime',
1162
+ },`;
1163
+ return `import { defineDataViews } from '@wp-typia/dataviews';
1164
+ import { __ } from '@wordpress/i18n';
1165
+
1166
+ import type { ${itemTypeName} } from './types';
1167
+
1168
+ export const ${dataViewsName} = defineDataViews<${itemTypeName}>({
1169
+ idField: 'id',
1170
+ search: ${searchEnabled},
1171
+ searchLabel: __( 'Search records', ${quoteTsString(textDomain)} ),
1172
+ ${titleFieldSource}
1173
+ defaultView: {
1174
+ fields: ${defaultViewFields},
1175
+ page: 1,
1176
+ perPage: 10,
1177
+ ${defaultViewEnhancementsSource}
1178
+ type: 'table',
1179
+ },
1180
+ fields: {
1181
+ id: {
1182
+ enableHiding: false,
1183
+ label: __( 'ID', ${quoteTsString(textDomain)} ),
1184
+ readOnly: true,
1185
+ schema: { type: 'integer' },
1186
+ },
1187
+ ${additionalFieldsSource}
1188
+ },
1189
+ });
1190
+ `;
1191
+ }
1192
+ function buildDefaultAdminViewDataSource(adminViewSlug) {
1193
+ const pascalName = toPascalCase(adminViewSlug);
1194
+ const camelName = toCamelCase(adminViewSlug);
1195
+ const title = toTitleCase(adminViewSlug);
1196
+ const itemTypeName = `${pascalName}AdminViewItem`;
1197
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1198
+ const queryTypeName = `${pascalName}AdminViewQuery`;
1199
+ const dataViewsName = `${camelName}AdminDataViews`;
1200
+ const fetchName = `fetch${pascalName}AdminViewData`;
1201
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1202
+
1203
+ import { ${dataViewsName} } from './config';
1204
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1205
+
1206
+ export interface ${queryTypeName} {
1207
+ page?: number;
1208
+ perPage?: number;
1209
+ search?: string;
1210
+ }
1211
+
1212
+ const STARTER_ITEMS: ${itemTypeName}[] = [
1213
+ {
1214
+ id: 1,
1215
+ owner: 'Editorial',
1216
+ status: 'published',
1217
+ title: ${quoteTsString(`${title} launch checklist`)},
1218
+ updatedAt: '2026-04-01T10:30:00Z',
1219
+ },
1220
+ {
1221
+ id: 2,
1222
+ owner: 'Design',
1223
+ status: 'draft',
1224
+ title: ${quoteTsString(`${title} content refresh`)},
1225
+ updatedAt: '2026-04-03T14:15:00Z',
1226
+ },
1227
+ {
1228
+ id: 3,
1229
+ owner: 'Operations',
1230
+ status: 'published',
1231
+ title: ${quoteTsString(`${title} support handoff`)},
1232
+ updatedAt: '2026-04-08T08:45:00Z',
1233
+ },
1234
+ ];
1235
+
1236
+ function matchesSearch(item: ${itemTypeName}, search: string | undefined): boolean {
1237
+ if (!search) {
1238
+ return true;
1239
+ }
1240
+
1241
+ const needle = search.toLowerCase();
1242
+ return [item.title, item.owner, item.status].some((value) =>
1243
+ value.toLowerCase().includes(needle),
1244
+ );
1245
+ }
1246
+
1247
+ export async function ${fetchName}(
1248
+ view: DataViewsView<${itemTypeName}>,
1249
+ ): Promise<${dataSetTypeName}> {
1250
+ const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1251
+ perPageParam: 'perPage',
1252
+ });
1253
+ const requestedPage = query.page ?? 1;
1254
+ const page = requestedPage > 0 ? requestedPage : 1;
1255
+ const requestedPerPage = query.perPage ?? view.perPage ?? 10;
1256
+ const perPage = requestedPerPage > 0 ? requestedPerPage : 10;
1257
+ const filteredItems = STARTER_ITEMS.filter((item) =>
1258
+ matchesSearch(item, query.search),
1259
+ );
1260
+ const offset = (page - 1) * perPage;
1261
+ const items = filteredItems.slice(offset, offset + perPage);
1262
+
1263
+ return {
1264
+ items,
1265
+ paginationInfo: {
1266
+ totalItems: filteredItems.length,
1267
+ totalPages: Math.max(1, Math.ceil(filteredItems.length / perPage)),
1268
+ },
1269
+ };
1270
+ }
1271
+ `;
1272
+ }
1273
+ function buildRestAdminViewDataSource(adminViewSlug, restResource) {
1274
+ const pascalName = toPascalCase(adminViewSlug);
1275
+ const restPascalName = toPascalCase(restResource.slug);
1276
+ const camelName = toCamelCase(adminViewSlug);
1277
+ const itemTypeName = `${pascalName}AdminViewItem`;
1278
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1279
+ const dataViewsName = `${camelName}AdminDataViews`;
1280
+ const fetchName = `fetch${pascalName}AdminViewData`;
1281
+ const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
1282
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1283
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1284
+
1285
+ import { listResource } from ${quoteTsString(restApiModule)};
1286
+ import type { ${restPascalName}ListQuery } from ${quoteTsString(restTypesModule)};
1287
+ import { ${dataViewsName} } from './config';
1288
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1289
+
1290
+ function resolveTotalPages(total: number, perPage: number | undefined): number {
1291
+ const resolvedPerPage = perPage && perPage > 0 ? perPage : 1;
1292
+ return Math.max(1, Math.ceil(total / resolvedPerPage));
1293
+ }
1294
+
1295
+ export async function ${fetchName}(
1296
+ view: DataViewsView<${itemTypeName}>,
1297
+ ): Promise<${dataSetTypeName}> {
1298
+ const query = ${dataViewsName}.toQueryArgs<${restPascalName}ListQuery>(view, {
1299
+ perPageParam: 'perPage',
1300
+ searchParam: false,
1301
+ });
1302
+ const result = await listResource({
1303
+ page: query.page,
1304
+ perPage: query.perPage,
1305
+ });
1306
+ if (!result.isValid || !result.data) {
1307
+ throw new Error('Unable to load REST resource records.');
1308
+ }
1309
+
1310
+ const response = result.data;
1311
+
1312
+ return {
1313
+ items: response.items,
1314
+ paginationInfo: {
1315
+ totalItems: response.total,
1316
+ totalPages: resolveTotalPages(response.total, response.perPage ?? query.perPage),
1317
+ },
1318
+ };
1319
+ }
1320
+ `;
1321
+ }
1322
+ function buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) {
1323
+ const pascalName = toPascalCase(adminViewSlug);
1324
+ const camelName = toCamelCase(adminViewSlug);
1325
+ const coreDataRecordTypeName = `${pascalName}CoreDataRecord`;
1326
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1327
+ const itemTypeName = `${pascalName}AdminViewItem`;
1328
+ const queryTypeName = `${pascalName}AdminViewQuery`;
1329
+ const dataViewsName = `${camelName}AdminDataViews`;
1330
+ const useEntityRecordName = `use${pascalName}EntityRecord`;
1331
+ const useEntityRecordsName = `use${pascalName}EntityRecords`;
1332
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
1333
+ if (coreDataSource.entityKind === "taxonomy") {
1334
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1335
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1336
+ import { useMemo } from '@wordpress/element';
1337
+
1338
+ import { ${dataViewsName} } from './config';
1339
+ import type {
1340
+ ${coreDataRecordTypeName},
1341
+ ${dataSetTypeName},
1342
+ ${itemTypeName},
1343
+ } from './types';
1344
+
1345
+ export interface ${queryTypeName} {
1346
+ page?: number;
1347
+ per_page?: number;
1348
+ search?: string;
1349
+ }
1350
+
1351
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1352
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1353
+
1354
+ function normalizeCoreDataNumber(value: unknown): number {
1355
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
1356
+ }
1357
+
1358
+ function normalizeCoreDataString(value: unknown): string {
1359
+ return typeof value === 'string' ? value : '';
1360
+ }
1361
+
1362
+ function normalizeTaxonomyRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1363
+ return {
1364
+ count: normalizeCoreDataNumber(record.count),
1365
+ description: normalizeCoreDataString(record.description),
1366
+ id: record.id,
1367
+ link: normalizeCoreDataString(record.link),
1368
+ name: normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug),
1369
+ parent: normalizeCoreDataNumber(record.parent),
1370
+ raw: record,
1371
+ slug: normalizeCoreDataString(record.slug),
1372
+ taxonomy: normalizeCoreDataString(record.taxonomy),
1373
+ };
1374
+ }
1375
+
1376
+ export function ${useEntityRecordName}(recordId: number | undefined) {
1377
+ return useEntityRecord<${coreDataRecordTypeName}>(
1378
+ CORE_DATA_ENTITY_KIND,
1379
+ CORE_DATA_ENTITY_NAME,
1380
+ recordId ?? 0,
1381
+ { enabled: typeof recordId === 'number' },
1382
+ );
1383
+ }
1384
+
1385
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1386
+ const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1387
+ perPageParam: 'per_page',
1388
+ });
1389
+
1390
+ return useEntityRecords<${coreDataRecordTypeName}>(
1391
+ CORE_DATA_ENTITY_KIND,
1392
+ CORE_DATA_ENTITY_NAME,
1393
+ query,
1394
+ );
1395
+ }
1396
+
1397
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1398
+ const { hasResolved, isResolving, records, totalItems, totalPages } =
1399
+ ${useEntityRecordsName}(view);
1400
+ const items = useMemo(
1401
+ () => (records ?? []).map((record) => normalizeTaxonomyRecord(record)),
1402
+ [records],
1403
+ );
1404
+ const dataSet = useMemo<${dataSetTypeName}>(
1405
+ () => ({
1406
+ items,
1407
+ paginationInfo: {
1408
+ totalItems: totalItems ?? items.length,
1409
+ totalPages: Math.max(1, totalPages ?? 1),
1410
+ },
1411
+ }),
1412
+ [items, totalItems, totalPages],
1413
+ );
1414
+ const error =
1415
+ !isResolving && hasResolved && records === null
1416
+ ? 'Unable to load core-data entity records.'
1417
+ : null;
1418
+
1419
+ return {
1420
+ dataSet,
1421
+ error,
1422
+ isLoading: isResolving,
1423
+ };
1424
+ }
1425
+ `;
1426
+ }
1427
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1428
+ import { useEntityRecord, useEntityRecords } from '@wordpress/core-data';
1429
+ import { useMemo } from '@wordpress/element';
1430
+
1431
+ import { ${dataViewsName} } from './config';
1432
+ import type {
1433
+ ${coreDataRecordTypeName},
1434
+ ${dataSetTypeName},
1435
+ ${itemTypeName},
1436
+ } from './types';
1437
+
1438
+ export interface ${queryTypeName} {
1439
+ page?: number;
1440
+ per_page?: number;
1441
+ search?: string;
1442
+ }
1443
+
1444
+ const CORE_DATA_ENTITY_KIND = ${quoteTsString(coreDataSource.entityKind)};
1445
+ const CORE_DATA_ENTITY_NAME = ${quoteTsString(coreDataSource.entityName)};
1446
+
1447
+ function normalizeCoreDataString(value: unknown): string {
1448
+ return typeof value === 'string' ? value : '';
1449
+ }
1450
+
1451
+ function normalizeCoreDataTitle(record: ${coreDataRecordTypeName}): string {
1452
+ if (typeof record.title === 'string') {
1453
+ return record.title;
1454
+ }
1455
+ if (record.title && typeof record.title === 'object') {
1456
+ if (typeof record.title.rendered === 'string') {
1457
+ return record.title.rendered;
1458
+ }
1459
+ if (typeof record.title.raw === 'string') {
1460
+ return record.title.raw;
1461
+ }
1462
+ }
1463
+
1464
+ return normalizeCoreDataString(record.name) || normalizeCoreDataString(record.slug);
1465
+ }
1466
+
1467
+ function normalizeCoreDataUpdatedAt(record: ${coreDataRecordTypeName}): string {
1468
+ return normalizeCoreDataString(record.modified) || normalizeCoreDataString(record.date);
1469
+ }
1470
+
1471
+ function normalizeCoreDataRecord(record: ${coreDataRecordTypeName}): ${itemTypeName} {
1472
+ return {
1473
+ id: record.id,
1474
+ raw: record,
1475
+ slug: normalizeCoreDataString(record.slug),
1476
+ status: normalizeCoreDataString(record.status),
1477
+ title: normalizeCoreDataTitle(record),
1478
+ updatedAt: normalizeCoreDataUpdatedAt(record),
1479
+ };
1480
+ }
1481
+
1482
+ export function ${useEntityRecordName}(recordId: number | undefined) {
1483
+ return useEntityRecord<${coreDataRecordTypeName}>(
1484
+ CORE_DATA_ENTITY_KIND,
1485
+ CORE_DATA_ENTITY_NAME,
1486
+ recordId ?? 0,
1487
+ { enabled: typeof recordId === 'number' },
1488
+ );
1489
+ }
1490
+
1491
+ export function ${useEntityRecordsName}(view: DataViewsView<${itemTypeName}>) {
1492
+ const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1493
+ perPageParam: 'per_page',
1494
+ });
1495
+
1496
+ return useEntityRecords<${coreDataRecordTypeName}>(
1497
+ CORE_DATA_ENTITY_KIND,
1498
+ CORE_DATA_ENTITY_NAME,
1499
+ query,
1500
+ );
1501
+ }
1502
+
1503
+ export function ${useAdminViewDataName}(view: DataViewsView<${itemTypeName}>) {
1504
+ const { hasResolved, isResolving, records, totalItems, totalPages } =
1505
+ ${useEntityRecordsName}(view);
1506
+ const items = useMemo(
1507
+ () => (records ?? []).map((record) => normalizeCoreDataRecord(record)),
1508
+ [records],
1509
+ );
1510
+ const dataSet = useMemo<${dataSetTypeName}>(
1511
+ () => ({
1512
+ items,
1513
+ paginationInfo: {
1514
+ totalItems: totalItems ?? items.length,
1515
+ totalPages: Math.max(1, totalPages ?? 1),
1516
+ },
1517
+ }),
1518
+ [items, totalItems, totalPages],
1519
+ );
1520
+ const error =
1521
+ !isResolving && hasResolved && records === null
1522
+ ? 'Unable to load core-data entity records.'
1523
+ : null;
1524
+
1525
+ return {
1526
+ dataSet,
1527
+ error,
1528
+ isLoading: isResolving,
1529
+ };
1530
+ }
1531
+ `;
1532
+ }
1533
+ function buildAdminViewScreenSource(adminViewSlug, textDomain) {
1534
+ const pascalName = toPascalCase(adminViewSlug);
1535
+ const camelName = toCamelCase(adminViewSlug);
1536
+ const itemTypeName = `${pascalName}AdminViewItem`;
1537
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1538
+ const componentName = `${pascalName}AdminViewScreen`;
1539
+ const dataViewsName = `${camelName}AdminDataViews`;
1540
+ const fetchName = `fetch${pascalName}AdminViewData`;
1541
+ const title = toTitleCase(adminViewSlug);
1542
+ return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
1543
+ import { Button, Notice, Spinner } from '@wordpress/components';
1544
+ import { useEffect, useState } from '@wordpress/element';
1545
+ import { __ } from '@wordpress/i18n';
1546
+ import { DataViews } from '@wordpress/dataviews/wp';
1547
+
1548
+ import { ${dataViewsName} } from './config';
1549
+ import { ${fetchName} } from './data';
1550
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1551
+
1552
+ const TypedDataViews = DataViews as unknown as <TItem extends object>(
1553
+ props: DataViewsConfig<TItem>,
1554
+ ) => ReturnType<typeof DataViews>;
1555
+
1556
+ const EMPTY_DATA_SET: ${dataSetTypeName} = {
1557
+ items: [],
1558
+ paginationInfo: {
1559
+ totalItems: 0,
1560
+ totalPages: 1,
1561
+ },
1562
+ };
1563
+
1564
+ export function ${componentName}() {
1565
+ const [view, setView] = useState<DataViewsView<${itemTypeName}>>(
1566
+ ${dataViewsName}.defaultView,
1567
+ );
1568
+ const [dataSet, setDataSet] = useState<${dataSetTypeName}>(EMPTY_DATA_SET);
1569
+ const [error, setError] = useState<string | null>(null);
1570
+ const [isLoading, setIsLoading] = useState(true);
1571
+ const [reloadToken, setReloadToken] = useState(0);
1572
+
1573
+ useEffect(() => {
1574
+ let isCurrentRequest = true;
1575
+ setIsLoading(true);
1576
+ setError(null);
1577
+
1578
+ void ${fetchName}(view)
1579
+ .then((nextDataSet) => {
1580
+ if (isCurrentRequest) {
1581
+ setDataSet(nextDataSet);
1582
+ }
1583
+ })
1584
+ .catch((nextError: unknown) => {
1585
+ if (isCurrentRequest) {
1586
+ setError(
1587
+ nextError instanceof Error
1588
+ ? nextError.message
1589
+ : __( 'Unable to load records.', ${quoteTsString(textDomain)} ),
1590
+ );
1591
+ }
1592
+ })
1593
+ .finally(() => {
1594
+ if (isCurrentRequest) {
1595
+ setIsLoading(false);
1596
+ }
1597
+ });
1598
+
1599
+ return () => {
1600
+ isCurrentRequest = false;
1601
+ };
1602
+ }, [reloadToken, view]);
1603
+
1604
+ const config = ${dataViewsName}.createConfig({
1605
+ data: dataSet.items,
1606
+ isLoading,
1607
+ onChangeView: setView,
1608
+ paginationInfo: dataSet.paginationInfo,
1609
+ view,
1610
+ });
1611
+
1612
+ return (
1613
+ <div className="wp-typia-admin-view-screen">
1614
+ <header className="wp-typia-admin-view-screen__header">
1615
+ <div>
1616
+ <p className="wp-typia-admin-view-screen__eyebrow">
1617
+ { __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
1618
+ </p>
1619
+ <h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
1620
+ <p>
1621
+ { __( 'Replace the fetcher in data.ts with your project data source when this screen graduates from scaffold to product UI.', ${quoteTsString(textDomain)} ) }
1622
+ </p>
1623
+ </div>
1624
+ <div className="wp-typia-admin-view-screen__actions">
1625
+ { isLoading ? <Spinner /> : null }
1626
+ <Button
1627
+ isBusy={ isLoading }
1628
+ onClick={ () => setReloadToken((token) => token + 1) }
1629
+ variant="secondary"
1630
+ >
1631
+ { __( 'Reload', ${quoteTsString(textDomain)} ) }
1632
+ </Button>
1633
+ </div>
1634
+ </header>
1635
+ { error ? (
1636
+ <Notice isDismissible={ false } status="error">
1637
+ { error }
1638
+ </Notice>
1639
+ ) : null }
1640
+ <TypedDataViews<${itemTypeName}> { ...config } />
1641
+ </div>
1642
+ );
1643
+ }
1644
+ `;
1645
+ }
1646
+ function buildCoreDataAdminViewScreenSource(adminViewSlug, textDomain) {
1647
+ const pascalName = toPascalCase(adminViewSlug);
1648
+ const camelName = toCamelCase(adminViewSlug);
1649
+ const itemTypeName = `${pascalName}AdminViewItem`;
1650
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1651
+ const componentName = `${pascalName}AdminViewScreen`;
1652
+ const dataViewsName = `${camelName}AdminDataViews`;
1653
+ const useAdminViewDataName = `use${pascalName}AdminViewData`;
1654
+ const title = toTitleCase(adminViewSlug);
1655
+ return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
1656
+ import { Notice, Spinner } from '@wordpress/components';
1657
+ import { useState } from '@wordpress/element';
1658
+ import { __ } from '@wordpress/i18n';
1659
+ import { DataViews } from '@wordpress/dataviews/wp';
1660
+
1661
+ import { ${dataViewsName} } from './config';
1662
+ import { ${useAdminViewDataName} } from './data';
1663
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1664
+
1665
+ const TypedDataViews = DataViews as unknown as <TItem extends object>(
1666
+ props: DataViewsConfig<TItem>,
1667
+ ) => ReturnType<typeof DataViews>;
1668
+
1669
+ const EMPTY_DATA_SET: ${dataSetTypeName} = {
1670
+ items: [],
1671
+ paginationInfo: {
1672
+ totalItems: 0,
1673
+ totalPages: 1,
1674
+ },
1675
+ };
1676
+
1677
+ export function ${componentName}() {
1678
+ const [view, setView] = useState<DataViewsView<${itemTypeName}>>(
1679
+ ${dataViewsName}.defaultView,
1680
+ );
1681
+ const {
1682
+ dataSet = EMPTY_DATA_SET,
1683
+ error,
1684
+ isLoading,
1685
+ } = ${useAdminViewDataName}(view);
1686
+ const config = ${dataViewsName}.createConfig({
1687
+ data: dataSet.items,
1688
+ isLoading,
1689
+ onChangeView: setView,
1690
+ paginationInfo: dataSet.paginationInfo,
1691
+ view,
1692
+ });
1693
+
1694
+ return (
1695
+ <div className="wp-typia-admin-view-screen">
1696
+ <header className="wp-typia-admin-view-screen__header">
1697
+ <div>
1698
+ <p className="wp-typia-admin-view-screen__eyebrow">
1699
+ { __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
1700
+ </p>
1701
+ <h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
1702
+ <p>
1703
+ { __( '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)} ) }
1704
+ </p>
1705
+ </div>
1706
+ <div className="wp-typia-admin-view-screen__actions">
1707
+ { isLoading ? <Spinner /> : null }
1708
+ </div>
1709
+ </header>
1710
+ { error ? (
1711
+ <Notice isDismissible={ false } status="error">
1712
+ { error }
1713
+ </Notice>
1714
+ ) : null }
1715
+ <TypedDataViews<${itemTypeName}> { ...config } />
1716
+ </div>
1717
+ );
1718
+ }
1719
+ `;
1720
+ }
1721
+ function buildAdminViewEntrySource(adminViewSlug) {
1722
+ const pascalName = toPascalCase(adminViewSlug);
1723
+ const componentName = `${pascalName}AdminViewScreen`;
1724
+ const rootId = `wp-typia-admin-view-${adminViewSlug}`;
1725
+ return `import { createRoot } from '@wordpress/element';
1726
+
1727
+ import '@wordpress/dataviews/build-style/style.css';
1728
+ import { ${componentName} } from './Screen';
1729
+ import './style.scss';
1730
+
1731
+ const ROOT_ELEMENT_ID = ${quoteTsString(rootId)};
1732
+
1733
+ function mountAdminView() {
1734
+ const rootElement = document.getElementById(ROOT_ELEMENT_ID);
1735
+ if (!rootElement) {
1736
+ return;
1737
+ }
1738
+
1739
+ createRoot(rootElement).render(<${componentName} />);
1740
+ }
1741
+
1742
+ if (document.readyState === 'loading') {
1743
+ document.addEventListener('DOMContentLoaded', mountAdminView);
1744
+ } else {
1745
+ mountAdminView();
1746
+ }
1747
+ `;
1748
+ }
1749
+ function buildAdminViewStyleSource() {
1750
+ return `.wp-typia-admin-view-screen {
1751
+ box-sizing: border-box;
1752
+ max-width: 1180px;
1753
+ padding: 24px 24px 48px 0;
1754
+ }
1755
+
1756
+ .wp-typia-admin-view-screen__header {
1757
+ align-items: flex-start;
1758
+ display: flex;
1759
+ gap: 24px;
1760
+ justify-content: space-between;
1761
+ margin-bottom: 24px;
1762
+ }
1763
+
1764
+ .wp-typia-admin-view-screen__header h1 {
1765
+ font-size: 28px;
1766
+ line-height: 1.2;
1767
+ margin: 0 0 8px;
1768
+ }
1769
+
1770
+ .wp-typia-admin-view-screen__header p {
1771
+ max-width: 680px;
1772
+ }
1773
+
1774
+ .wp-typia-admin-view-screen__eyebrow {
1775
+ color: #3858e9;
1776
+ font-size: 11px;
1777
+ font-weight: 600;
1778
+ letter-spacing: 0.08em;
1779
+ margin: 0 0 8px;
1780
+ text-transform: uppercase;
1781
+ }
1782
+
1783
+ .wp-typia-admin-view-screen__actions {
1784
+ align-items: center;
1785
+ display: flex;
1786
+ gap: 12px;
1787
+ }
1788
+ `;
1789
+ }
1790
+ function buildAdminViewPhpSource(adminViewSlug, workspace) {
1791
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
1792
+ const phpSlug = adminViewSlug.replace(/-/g, "_");
1793
+ const functionPrefix = `${workspace.workspace.phpPrefix}_${phpSlug}`;
1794
+ const menuSlugFunctionName = `${functionPrefix}_admin_view_menu_slug`;
1795
+ const renderFunctionName = `${functionPrefix}_render_admin_view`;
1796
+ const registerFunctionName = `${functionPrefix}_register_admin_view`;
1797
+ const enqueueFunctionName = `${functionPrefix}_enqueue_admin_view`;
1798
+ const hookGlobalName = `${functionPrefix}_admin_view_hook`;
1799
+ const rootId = `wp-typia-admin-view-${adminViewSlug}`;
1800
+ const title = toTitleCase(adminViewSlug);
1801
+ return `<?php
1802
+ if ( ! defined( 'ABSPATH' ) ) {
1803
+ return;
1804
+ }
1805
+
1806
+ if ( ! function_exists( '${menuSlugFunctionName}' ) ) {
1807
+ function ${menuSlugFunctionName}() : string {
1808
+ return '${workspaceBaseName}-${adminViewSlug}';
1809
+ }
1810
+ }
1811
+
1812
+ if ( ! function_exists( '${renderFunctionName}' ) ) {
1813
+ function ${renderFunctionName}() : void {
1814
+ ?>
1815
+ <div class="wrap">
1816
+ <div id="${rootId}"></div>
1817
+ </div>
1818
+ <?php
1819
+ }
1820
+ }
1821
+
1822
+ if ( ! function_exists( '${registerFunctionName}' ) ) {
1823
+ function ${registerFunctionName}() : void {
1824
+ $GLOBALS['${hookGlobalName}'] = add_submenu_page(
1825
+ 'tools.php',
1826
+ __( ${quotePhpString(title)}, ${quotePhpString(workspace.workspace.textDomain)} ),
1827
+ __( ${quotePhpString(title)}, ${quotePhpString(workspace.workspace.textDomain)} ),
1828
+ 'edit_posts',
1829
+ ${menuSlugFunctionName}(),
1830
+ '${renderFunctionName}'
1831
+ );
1832
+ }
1833
+ }
1834
+
1835
+ if ( ! function_exists( '${enqueueFunctionName}' ) ) {
1836
+ function ${enqueueFunctionName}( string $hook_suffix ) : void {
1837
+ $page_hook = isset( $GLOBALS['${hookGlobalName}'] ) && is_string( $GLOBALS['${hookGlobalName}'] )
1838
+ ? $GLOBALS['${hookGlobalName}']
1839
+ : '';
1840
+
1841
+ if ( $page_hook !== $hook_suffix ) {
1842
+ return;
1843
+ }
1844
+
1845
+ $plugin_file = dirname( __DIR__, 2 ) . '/${workspaceBaseName}.php';
1846
+ $script_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_SCRIPT}';
1847
+ $asset_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_ASSET}';
1848
+ $style_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_STYLE}';
1849
+ $style_rtl_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_STYLE_RTL}';
1850
+
1851
+ if ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
1852
+ return;
1853
+ }
1854
+
1855
+ $asset = require $asset_path;
1856
+ if ( ! is_array( $asset ) ) {
1857
+ $asset = array();
1858
+ }
1859
+
1860
+ $dependencies = isset( $asset['dependencies'] ) && is_array( $asset['dependencies'] )
1861
+ ? $asset['dependencies']
1862
+ : array();
1863
+
1864
+ wp_enqueue_script(
1865
+ '${workspaceBaseName}-${adminViewSlug}-admin-view',
1866
+ plugins_url( '${ADMIN_VIEWS_SCRIPT}', $plugin_file ),
1867
+ $dependencies,
1868
+ isset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
1869
+ true
1870
+ );
1871
+
1872
+ if ( file_exists( $style_path ) ) {
1873
+ wp_enqueue_style(
1874
+ '${workspaceBaseName}-${adminViewSlug}-admin-view',
1875
+ plugins_url( '${ADMIN_VIEWS_STYLE}', $plugin_file ),
1876
+ array( 'wp-components' ),
1877
+ isset( $asset['version'] ) ? $asset['version'] : filemtime( $style_path )
1878
+ );
1879
+ if ( file_exists( $style_rtl_path ) ) {
1880
+ wp_style_add_data( '${workspaceBaseName}-${adminViewSlug}-admin-view', 'rtl', 'replace' );
1881
+ }
1882
+ }
1883
+ }
1884
+ }
1885
+
1886
+ add_action( 'admin_menu', '${registerFunctionName}' );
1887
+ add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );
1888
+ `;
1889
+ }
1890
+ async function ensureAdminViewPackageDependencies(workspace, adminViewSource) {
1891
+ const packageJsonPath = path4.join(workspace.projectDir, "package.json");
1892
+ const wpTypiaDataViewsVersion = resolvePackageVersionRange("@wp-typia/dataviews", DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, "wp-typia-dataviews");
1893
+ const wordpressDataViewsVersion = resolvePackageVersionRange("@wordpress/dataviews", DEFAULT_WORDPRESS_DATAVIEWS_VERSION);
1894
+ const wordpressCoreDataVersion = resolvePackageVersionRange("@wordpress/core-data", DEFAULT_WORDPRESS_CORE_DATA_VERSION);
1895
+ const wordpressDataVersion = resolvePackageVersionRange("@wordpress/data", DEFAULT_WORDPRESS_DATA_VERSION);
1896
+ await patchFile(packageJsonPath, (source) => {
1897
+ const packageJson = JSON.parse(source);
1898
+ const coreDataDependencies = isAdminViewCoreDataSource(adminViewSource) ? {
1899
+ "@wordpress/core-data": packageJson.dependencies?.["@wordpress/core-data"] ?? wordpressCoreDataVersion,
1900
+ "@wordpress/data": packageJson.dependencies?.["@wordpress/data"] ?? wordpressDataVersion
1901
+ } : {};
1902
+ const nextDependencies = {
1903
+ ...packageJson.dependencies ?? {},
1904
+ "@wordpress/dataviews": packageJson.dependencies?.["@wordpress/dataviews"] ?? wordpressDataViewsVersion,
1905
+ ...coreDataDependencies
1906
+ };
1907
+ const nextDevDependencies = {
1908
+ ...packageJson.devDependencies ?? {},
1909
+ "@wp-typia/dataviews": packageJson.devDependencies?.["@wp-typia/dataviews"] ?? wpTypiaDataViewsVersion
1910
+ };
1911
+ if (JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {}) && JSON.stringify(nextDevDependencies) === JSON.stringify(packageJson.devDependencies ?? {})) {
1912
+ return source;
1913
+ }
1914
+ packageJson.dependencies = nextDependencies;
1915
+ packageJson.devDependencies = nextDevDependencies;
1916
+ return `${JSON.stringify(packageJson, null, detectJsonIndent(source))}
1917
+ `;
1918
+ });
1919
+ }
1920
+ async function ensureAdminViewBootstrapAnchors(workspace) {
1921
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1922
+ await patchFile(bootstrapPath, (source) => {
1923
+ let nextSource = source;
1924
+ const loadFunctionName = `${workspace.workspace.phpPrefix}_load_admin_views`;
1925
+ const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
1926
+ const loadFunction = `
1927
+
1928
+ function ${loadFunctionName}() {
1929
+ foreach ( glob( __DIR__ . '${ADMIN_VIEWS_PHP_GLOB}' ) ?: array() as $admin_view_module ) {
1930
+ require_once $admin_view_module;
1931
+ }
1932
+ }
1933
+ `;
1934
+ const insertionAnchors = [
1935
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1936
+ /\?>\s*$/u
1937
+ ];
1938
+ const insertPhpSnippet = (snippet) => {
1939
+ for (const anchor of insertionAnchors) {
1940
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}
1941
+ ${match}`);
1942
+ if (candidate !== nextSource) {
1943
+ nextSource = candidate;
1944
+ return;
1945
+ }
1946
+ }
1947
+ nextSource = `${nextSource.trimEnd()}
1948
+ ${snippet}
1949
+ `;
1950
+ };
1951
+ const appendPhpSnippet = (snippet) => {
1952
+ const closingTagPattern = /\?>\s*$/u;
1953
+ if (closingTagPattern.test(nextSource)) {
1954
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}
1955
+ ?>`);
1956
+ return;
1957
+ }
1958
+ nextSource = `${nextSource.trimEnd()}
1959
+ ${snippet}
1960
+ `;
1961
+ };
1962
+ if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
1963
+ insertPhpSnippet(loadFunction);
1964
+ } else {
1965
+ const functionRange = findPhpFunctionRange(nextSource, loadFunctionName);
1966
+ const functionSource = functionRange ? nextSource.slice(functionRange.start, functionRange.end) : "";
1967
+ if (!functionSource.includes(ADMIN_VIEWS_PHP_GLOB)) {
1968
+ const replacedSource = replacePhpFunctionDefinition(nextSource, loadFunctionName, loadFunction);
1969
+ if (!replacedSource) {
1970
+ throw new Error(`Unable to repair ${path4.basename(bootstrapPath)} for ${loadFunctionName}.`);
1971
+ }
1972
+ nextSource = replacedSource;
1973
+ }
1974
+ }
1975
+ if (!nextSource.includes(loadHook)) {
1976
+ appendPhpSnippet(loadHook);
1977
+ }
1978
+ return nextSource;
1979
+ });
1980
+ }
1981
+ async function ensureAdminViewBuildScriptAnchors(workspace) {
1982
+ const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1983
+ await patchFile(buildScriptPath, (source) => {
1984
+ if (/['"]src\/admin-views\/index\.(?:ts|js)['"]/u.test(source)) {
1985
+ return source;
1986
+ }
1987
+ const currentSharedEntriesPattern = /(\r?\n\s*['"]src\/editor-plugins\/index\.js['"])\s*,?/u;
1988
+ let nextSource = source.replace(currentSharedEntriesPattern, `$1,
1989
+ 'src/admin-views/index.ts',
1990
+ 'src/admin-views/index.js',`);
1991
+ if (nextSource !== source) {
1992
+ return nextSource;
1993
+ }
1994
+ const legacySharedEntriesPattern = /\[\s*['"]src\/bindings\/index\.ts['"]\s*,\s*['"]src\/bindings\/index\.js['"]\s*(?:,\s*)?\]/u;
1995
+ nextSource = source.replace(legacySharedEntriesPattern, `[
1996
+ 'src/bindings/index.ts',
1997
+ 'src/bindings/index.js',
1998
+ 'src/editor-plugins/index.ts',
1999
+ 'src/editor-plugins/index.js',
2000
+ 'src/admin-views/index.ts',
2001
+ 'src/admin-views/index.js',
2002
+ ]`);
2003
+ if (nextSource !== source) {
2004
+ return nextSource;
2005
+ }
2006
+ throw new Error(`Unable to update ${path4.relative(workspace.projectDir, buildScriptPath)} for admin view shared entries.`);
2007
+ });
2008
+ }
2009
+ async function ensureAdminViewWebpackAnchors(workspace) {
2010
+ const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
2011
+ await patchFile(webpackConfigPath, (source) => {
2012
+ if (/['"]admin-views\/index['"]/u.test(source)) {
2013
+ return source;
2014
+ }
2015
+ const editorPluginEntryPattern = /(\n\s*\[\s*['"]editor-plugins\/index['"][\s\S]*?['"]src\/editor-plugins\/index\.js['"][\s\S]*?\]\s*\])\s*,?/u;
2016
+ let nextSource = source.replace(editorPluginEntryPattern, `$1,
2017
+ [
2018
+ 'admin-views/index',
2019
+ [ 'src/admin-views/index.ts', 'src/admin-views/index.js' ],
2020
+ ],`);
2021
+ if (nextSource !== source) {
2022
+ return nextSource;
2023
+ }
2024
+ const legacySharedEntriesBlockPattern = /for\s*\(\s*const\s+relativePath\s+of\s+\[\s*['"]src\/bindings\/index\.ts['"]\s*,\s*['"]src\/bindings\/index\.js['"]\s*(?:,\s*)?\]\s*\)\s*\{[\s\S]*?entries\.push\(\s*\[\s*['"]bindings\/index['"]\s*,\s*entryPath\s*\]\s*\);\s*break;\s*\}/u;
2025
+ const nextSharedEntriesBlock = ` for ( const [ entryName, candidates ] of [
2026
+ [
2027
+ 'bindings/index',
2028
+ [ 'src/bindings/index.ts', 'src/bindings/index.js' ],
2029
+ ],
2030
+ [
2031
+ 'editor-plugins/index',
2032
+ [ 'src/editor-plugins/index.ts', 'src/editor-plugins/index.js' ],
2033
+ ],
2034
+ [
2035
+ 'admin-views/index',
2036
+ [ 'src/admin-views/index.ts', 'src/admin-views/index.js' ],
2037
+ ],
2038
+ ] ) {
2039
+ for ( const relativePath of candidates ) {
2040
+ const entryPath = path.resolve( process.cwd(), relativePath );
2041
+ if ( ! fs.existsSync( entryPath ) ) {
2042
+ continue;
2043
+ }
2044
+
2045
+ entries.push( [ entryName, entryPath ] );
2046
+ break;
2047
+ }
2048
+ }`;
2049
+ nextSource = source.replace(legacySharedEntriesBlockPattern, nextSharedEntriesBlock);
2050
+ if (nextSource === source) {
2051
+ throw new Error(`Unable to update ${path4.relative(workspace.projectDir, webpackConfigPath)} for admin view shared entries.`);
2052
+ }
2053
+ return nextSource;
2054
+ });
2055
+ }
2056
+ function resolveAdminViewRegistryPath(projectDir) {
2057
+ const adminViewsDir = path4.join(projectDir, "src", "admin-views");
2058
+ return [
2059
+ path4.join(adminViewsDir, "index.ts"),
2060
+ path4.join(adminViewsDir, "index.js")
2061
+ ].find((candidatePath) => fs3.existsSync(candidatePath)) ?? path4.join(adminViewsDir, "index.ts");
2062
+ }
2063
+ function readAdminViewRegistrySlugs(registryPath) {
2064
+ if (!fs3.existsSync(registryPath)) {
2065
+ return [];
2066
+ }
2067
+ const source = fs3.readFileSync(registryPath, "utf8");
2068
+ return Array.from(source.matchAll(/^\s*import\s+['"]\.\/([^/'"]+)(?:\/index(?:\.[cm]?[jt]sx?)?)?['"];?\s*$/gmu)).map((match) => match[1]);
2069
+ }
2070
+ async function writeAdminViewRegistry(projectDir, adminViewSlug) {
2071
+ const adminViewsDir = path4.join(projectDir, "src", "admin-views");
2072
+ const registryPath = resolveAdminViewRegistryPath(projectDir);
2073
+ await fsp3.mkdir(adminViewsDir, { recursive: true });
2074
+ const existingAdminViewSlugs = readWorkspaceInventory(projectDir).adminViews.map((entry) => entry.slug);
2075
+ const existingRegistrySlugs = readAdminViewRegistrySlugs(registryPath);
2076
+ const nextAdminViewSlugs = Array.from(new Set([...existingAdminViewSlugs, ...existingRegistrySlugs, adminViewSlug])).sort();
2077
+ await fsp3.writeFile(registryPath, buildAdminViewRegistrySource(nextAdminViewSlugs), "utf8");
2078
+ }
2079
+ async function runAddAdminViewCommand({
2080
+ adminViewName,
2081
+ cwd = process.cwd(),
2082
+ source
2083
+ }) {
2084
+ const workspace = resolveWorkspaceProject(cwd);
2085
+ assertAdminViewPackageAvailability();
2086
+ const adminViewSlug = assertValidGeneratedSlug("Admin view name", normalizeBlockSlug(adminViewName), "wp-typia add admin-view <name> [--source <rest-resource:slug|core-data:kind/name>]");
2087
+ const parsedSource = parseAdminViewSource(source);
2088
+ const inventory = readWorkspaceInventory(workspace.projectDir);
2089
+ const restResource = resolveRestResourceSource(inventory.restResources, parsedSource);
2090
+ const coreDataSource = isAdminViewCoreDataSource(parsedSource) ? parsedSource : undefined;
2091
+ assertAdminViewDoesNotExist(workspace.projectDir, adminViewSlug, inventory);
2092
+ const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
2093
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
2094
+ const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
2095
+ const packageJsonPath = path4.join(workspace.projectDir, "package.json");
2096
+ const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
2097
+ const adminViewsIndexPath = resolveAdminViewRegistryPath(workspace.projectDir);
2098
+ const adminViewDir = path4.join(workspace.projectDir, "src", "admin-views", adminViewSlug);
2099
+ const adminViewPhpPath = path4.join(workspace.projectDir, "inc", "admin-views", `${adminViewSlug}.php`);
2100
+ const mutationSnapshot = {
2101
+ fileSources: await snapshotWorkspaceFiles([
2102
+ adminViewsIndexPath,
2103
+ blockConfigPath,
2104
+ bootstrapPath,
2105
+ buildScriptPath,
2106
+ packageJsonPath,
2107
+ webpackConfigPath
2108
+ ]),
2109
+ snapshotDirs: [],
2110
+ targetPaths: [adminViewDir, adminViewPhpPath]
2111
+ };
674
2112
  try {
675
- const normalizedSlug = resolveNonEmptyNormalizedBlockSlug({
676
- input: blockName,
677
- label: "Block name",
678
- usage: "wp-typia add block <name> --template <family>"
2113
+ await fsp3.mkdir(adminViewDir, { recursive: true });
2114
+ await fsp3.mkdir(path4.dirname(adminViewPhpPath), { recursive: true });
2115
+ await ensureAdminViewPackageDependencies(workspace, parsedSource);
2116
+ await ensureAdminViewBootstrapAnchors(workspace);
2117
+ await ensureAdminViewBuildScriptAnchors(workspace);
2118
+ await ensureAdminViewWebpackAnchors(workspace);
2119
+ await fsp3.writeFile(path4.join(adminViewDir, "types.ts"), buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), "utf8");
2120
+ await fsp3.writeFile(path4.join(adminViewDir, "config.ts"), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), "utf8");
2121
+ await fsp3.writeFile(path4.join(adminViewDir, "data.ts"), coreDataSource ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource) : restResource ? buildRestAdminViewDataSource(adminViewSlug, restResource) : buildDefaultAdminViewDataSource(adminViewSlug), "utf8");
2122
+ await fsp3.writeFile(path4.join(adminViewDir, "Screen.tsx"), coreDataSource ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain) : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), "utf8");
2123
+ await fsp3.writeFile(path4.join(adminViewDir, "index.tsx"), buildAdminViewEntrySource(adminViewSlug), "utf8");
2124
+ await fsp3.writeFile(path4.join(adminViewDir, "style.scss"), buildAdminViewStyleSource(), "utf8");
2125
+ await fsp3.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), "utf8");
2126
+ await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
2127
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
2128
+ adminViewEntries: [buildAdminViewConfigEntry(adminViewSlug, parsedSource)]
679
2129
  });
680
- const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
681
- ({
682
- path: tempRoot,
683
- cleanup: cleanupTempRoot
684
- } = await createManagedTempRoot("wp-typia-add-block-"));
685
- const tempProjectDir = path3.join(tempRoot, normalizedSlug);
686
- const blockConfigPath = path3.join(workspace.projectDir, "scripts", "block-config.ts");
687
- const migrationConfigPath = path3.join(workspace.projectDir, "src", "migrations", "config.ts");
688
- const blockPhpPrefix = buildWorkspacePhpPrefix(workspace.workspace.phpPrefix, normalizedSlug);
689
- const migrationConfigSource = await readOptionalFile(migrationConfigPath);
690
- const migrationConfig = migrationConfigSource === null ? null : parseMigrationConfig(migrationConfigSource);
691
- const compoundSupportPaths = resolvedTemplateId === "compound" ? COMPOUND_SHARED_SUPPORT_FILES.map((fileName) => path3.join(workspace.projectDir, "src", fileName)) : [];
692
- const legacyCompoundValidatorPaths = resolvedTemplateId === "compound" ? await collectLegacyCompoundValidatorPaths(workspace.projectDir) : [];
693
- const result = await (async () => {
694
- const scaffoldResult = await scaffoldProject({
695
- alternateRenderTargets,
696
- answers: {
697
- ...defaults,
698
- author: workspace.author,
699
- compoundInnerBlocksPreset: resolvedInnerBlocksPreset,
700
- namespace: workspace.workspace.namespace,
701
- phpPrefix: blockPhpPrefix,
702
- slug: normalizedSlug,
703
- textDomain: workspace.workspace.textDomain,
704
- title: defaults.title
705
- },
706
- cwd: workspace.projectDir,
707
- dataStorageMode,
708
- externalLayerId: resolvedExternalLayerSelection.externalLayerId,
709
- externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
710
- externalLayerSourceLabel: normalizedExternalLayerSource,
711
- noInstall: true,
712
- packageManager: workspace.packageManager,
713
- persistencePolicy,
714
- projectDir: tempProjectDir,
715
- templateId: resolvedTemplateId
716
- });
717
- await assertAddBlockSupportsExternalLayerOutputs({
718
- callerCwd: cwd,
719
- externalLayerId: resolvedExternalLayerSelection.externalLayerId,
720
- externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
721
- templateId: resolvedTemplateId,
722
- variables: scaffoldResult.variables
723
- });
724
- return scaffoldResult;
725
- })();
726
- assertBlockTargetsDoNotExist(workspace.projectDir, resolvedTemplateId, result.variables);
727
- const mutationSnapshot = {
728
- fileSources: await snapshotWorkspaceFiles([
729
- blockConfigPath,
730
- migrationConfigPath,
731
- ...compoundSupportPaths,
732
- ...legacyCompoundValidatorPaths
733
- ]),
734
- snapshotDirs: migrationConfig === null ? [] : buildMigrationBlocks(resolvedTemplateId, result.variables).map((block) => path3.join(workspace.projectDir, ...migrationConfig.snapshotDir.split("/"), migrationConfig.currentMigrationVersion, block.key)),
735
- targetPaths: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables)
2130
+ return {
2131
+ adminViewSlug,
2132
+ projectDir: workspace.projectDir,
2133
+ source: parsedSource ? formatAdminViewSourceLocator(parsedSource) : undefined
736
2134
  };
737
- try {
738
- await copyScaffoldedBlockSlice(workspace.projectDir, resolvedTemplateId, tempProjectDir, result.variables, legacyCompoundValidatorPaths);
739
- await addCollectionImportsForTemplate(workspace.projectDir, resolvedTemplateId, result.variables);
740
- await appendBlockConfigEntries(workspace.projectDir, buildConfigEntries(resolvedTemplateId, result.variables), resolvedTemplateId === "persistence" || resolvedTemplateId === "compound" && result.variables.compoundPersistenceEnabled === "true");
741
- await syncWorkspaceAddedBlockArtifacts(workspace.projectDir, resolvedTemplateId, result.variables);
742
- await updateWorkspaceMigrationConfigIfPresent(workspace.projectDir, buildMigrationBlocks(resolvedTemplateId, result.variables));
743
- return {
744
- blockSlugs: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables).map((targetPath) => path3.basename(targetPath)),
745
- projectDir: workspace.projectDir,
746
- templateId: resolvedTemplateId,
747
- warnings: result.warnings
748
- };
749
- } catch (error) {
750
- await rollbackWorkspaceMutation(mutationSnapshot);
751
- throw error;
752
- }
753
- } finally {
754
- try {
755
- await resolvedExternalLayerSelection.cleanup?.();
756
- } finally {
757
- await cleanupTempRoot?.();
758
- }
2135
+ } catch (error) {
2136
+ await rollbackWorkspaceMutation(mutationSnapshot);
2137
+ throw error;
759
2138
  }
760
2139
  }
761
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
762
- import fs5 from "fs";
763
- import { promises as fsp8 } from "fs";
764
- import path12 from "path";
765
-
766
2140
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-assets.ts
767
- import fs3 from "fs";
768
- import { promises as fsp3 } from "fs";
769
- import path4 from "path";
2141
+ var import_typescript = __toESM(require_typescript(), 1);
2142
+ import fs4 from "fs";
2143
+ import { promises as fsp4 } from "fs";
2144
+ import path5 from "path";
2145
+ import {
2146
+ syncBlockMetadata as syncBlockMetadata2
2147
+ } from "@wp-typia/block-runtime/metadata-core";
770
2148
  var PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
771
2149
  var BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
772
2150
  var BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
@@ -775,58 +2153,7 @@ var EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
775
2153
  var EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
776
2154
  var EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
777
2155
  var EDITOR_PLUGIN_EDITOR_STYLE_RTL = "build/editor-plugins/style-index-rtl.css";
778
- function escapeRegex(value) {
779
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
780
- }
781
- function quotePhpString(value) {
782
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
783
- }
784
- function findPhpFunctionRange(source, functionName) {
785
- const signaturePattern = new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u");
786
- const signatureMatch = signaturePattern.exec(source);
787
- if (!signatureMatch || signatureMatch.index === undefined) {
788
- return null;
789
- }
790
- const functionStart = signatureMatch.index;
791
- const openBraceIndex = source.indexOf("{", functionStart);
792
- if (openBraceIndex === -1) {
793
- return null;
794
- }
795
- let depth = 0;
796
- for (let index = openBraceIndex;index < source.length; index += 1) {
797
- const character = source[index];
798
- if (character === "{") {
799
- depth += 1;
800
- continue;
801
- }
802
- if (character !== "}") {
803
- continue;
804
- }
805
- depth -= 1;
806
- if (depth === 0) {
807
- let functionEnd = index + 1;
808
- while (functionEnd < source.length && /[\r\n]/u.test(source[functionEnd] ?? "")) {
809
- functionEnd += 1;
810
- }
811
- return {
812
- end: functionEnd,
813
- start: functionStart
814
- };
815
- }
816
- }
817
- return null;
818
- }
819
- function replacePhpFunctionDefinition(source, functionName, replacement) {
820
- const functionRange = findPhpFunctionRange(source, functionName);
821
- if (!functionRange) {
822
- return null;
823
- }
824
- return [
825
- source.slice(0, functionRange.start),
826
- replacement,
827
- source.slice(functionRange.end)
828
- ].join("");
829
- }
2156
+ var BINDING_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
830
2157
  function buildPatternConfigEntry(patternSlug) {
831
2158
  return [
832
2159
  "\t{",
@@ -836,9 +2163,11 @@ function buildPatternConfigEntry(patternSlug) {
836
2163
  ].join(`
837
2164
  `);
838
2165
  }
839
- function buildBindingSourceConfigEntry(bindingSourceSlug) {
2166
+ function buildBindingSourceConfigEntry(bindingSourceSlug, target) {
840
2167
  return [
841
2168
  "\t{",
2169
+ ...target ? [` attribute: ${quoteTsString(target.attributeName)},`] : [],
2170
+ ...target ? [` block: ${quoteTsString(target.blockSlug)},`] : [],
842
2171
  ` editorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
843
2172
  ` serverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
844
2173
  ` slug: ${quoteTsString(bindingSourceSlug)},`,
@@ -846,6 +2175,34 @@ function buildBindingSourceConfigEntry(bindingSourceSlug) {
846
2175
  ].join(`
847
2176
  `);
848
2177
  }
2178
+ function assertValidBindingAttributeName(attributeName) {
2179
+ const trimmed = attributeName.trim();
2180
+ if (!trimmed) {
2181
+ throw new Error("`wp-typia add binding-source` requires --attribute <attribute> to include a value when --block is provided.");
2182
+ }
2183
+ if (!BINDING_ATTRIBUTE_NAME_PATTERN.test(trimmed)) {
2184
+ throw new Error(`Binding attribute "${attributeName}" must start with a letter and use only letters, numbers, underscores, or hyphens.`);
2185
+ }
2186
+ return trimmed;
2187
+ }
2188
+ function resolveBindingTargetBlockSlug(blockName, namespace) {
2189
+ const trimmed = blockName.trim();
2190
+ if (!trimmed) {
2191
+ throw new Error("`wp-typia add binding-source` requires --block <block-slug|namespace/block-slug> to include a value when --attribute is provided.");
2192
+ }
2193
+ const blockNameSegments = trimmed.split("/");
2194
+ if (blockNameSegments.length > 2) {
2195
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format.`);
2196
+ }
2197
+ if (blockNameSegments.some((segment) => segment.trim() === "")) {
2198
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format without empty path segments.`);
2199
+ }
2200
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2 ? blockNameSegments : [undefined, blockNameSegments[0]];
2201
+ if (maybeNamespace && maybeNamespace !== namespace) {
2202
+ throw new Error(`Binding target block "${trimmed}" uses namespace "${maybeNamespace}". Expected "${namespace}".`);
2203
+ }
2204
+ return normalizeBlockSlug(maybeSlug ?? "");
2205
+ }
849
2206
  function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
850
2207
  return [
851
2208
  "\t{",
@@ -856,9 +2213,6 @@ function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
856
2213
  ].join(`
857
2214
  `);
858
2215
  }
859
- function toPascalCaseFromSlug(slug) {
860
- return normalizeBlockSlug(slug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
861
- }
862
2216
  function buildPatternSource(patternSlug, namespace, textDomain) {
863
2217
  const patternTitle = toTitleCase(patternSlug);
864
2218
  return `<?php
@@ -877,12 +2231,32 @@ register_block_pattern(
877
2231
  );
878
2232
  `;
879
2233
  }
880
- function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain) {
2234
+ function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain, target) {
881
2235
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
882
2236
  const bindingSourcePhpId = bindingSourceSlug.replace(/-/g, "_");
883
2237
  const bindingSourceValueFunctionName = `${phpPrefix}_${bindingSourcePhpId}_binding_source_values`;
884
2238
  const bindingSourceResolveFunctionName = `${phpPrefix}_${bindingSourcePhpId}_resolve_binding_source_value`;
2239
+ const bindingSourceSupportedAttributesFunctionName = `${phpPrefix}_${bindingSourcePhpId}_supported_binding_attributes`;
885
2240
  const starterValue = `${bindingSourceTitle} starter value`;
2241
+ const supportedAttributesSource = target ? `
2242
+ if ( ! function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
2243
+ function ${bindingSourceSupportedAttributesFunctionName}( array $supported_attributes ) : array {
2244
+ if ( ! in_array( ${quotePhpString(target.attributeName)}, $supported_attributes, true ) ) {
2245
+ $supported_attributes[] = ${quotePhpString(target.attributeName)};
2246
+ }
2247
+
2248
+ return $supported_attributes;
2249
+ }
2250
+ }
2251
+ ` : "";
2252
+ const supportedAttributesHook = target ? `
2253
+ if ( function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
2254
+ add_filter(
2255
+ ${quotePhpString(`block_bindings_supported_attributes_${namespace}/${target.blockSlug}`)},
2256
+ ${quotePhpString(bindingSourceSupportedAttributesFunctionName)}
2257
+ );
2258
+ }
2259
+ ` : "";
886
2260
  return `<?php
887
2261
  if ( ! defined( 'ABSPATH' ) ) {
888
2262
  return;
@@ -911,6 +2285,7 @@ if ( ! function_exists( '${bindingSourceResolveFunctionName}' ) ) {
911
2285
  return is_string( $value ) ? $value : '';
912
2286
  }
913
2287
  }
2288
+ ${supportedAttributesSource}
914
2289
 
915
2290
  register_block_bindings_source(
916
2291
  ${quotePhpString(`${namespace}/${bindingSourceSlug}`)},
@@ -919,11 +2294,20 @@ register_block_bindings_source(
919
2294
  'get_value_callback' => ${quotePhpString(bindingSourceResolveFunctionName)},
920
2295
  )
921
2296
  );
922
- `;
2297
+ ${supportedAttributesHook}`;
923
2298
  }
924
- function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
2299
+ function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain, target) {
925
2300
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
926
2301
  const starterValue = `${bindingSourceTitle} starter value`;
2302
+ const bindingSourceName = `${namespace}/${bindingSourceSlug}`;
2303
+ const targetSource = target ? `
2304
+ export const BINDING_SOURCE_TARGET = {
2305
+ attribute: ${quoteTsString(target.attributeName)},
2306
+ block: ${quoteTsString(`${namespace}/${target.blockSlug}`)},
2307
+ field: ${quoteTsString(bindingSourceSlug)},
2308
+ source: ${quoteTsString(bindingSourceName)},
2309
+ } as const;
2310
+ ` : "";
927
2311
  return `import { registerBlockBindingsSource } from '@wordpress/blocks';
928
2312
  import { __ } from '@wordpress/i18n';
929
2313
 
@@ -936,13 +2320,14 @@ interface BindingSourceRegistration {
936
2320
  const BINDING_SOURCE_VALUES: Record<string, string> = {
937
2321
  ${quoteTsString(bindingSourceSlug)}: ${quoteTsString(starterValue)},
938
2322
  };
2323
+ ${targetSource}
939
2324
 
940
2325
  function resolveBindingSourceValue( field: string ): string {
941
2326
  return BINDING_SOURCE_VALUES[ field ] ?? '';
942
2327
  }
943
2328
 
944
2329
  registerBlockBindingsSource( {
945
- name: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
2330
+ name: ${quoteTsString(bindingSourceName)},
946
2331
  label: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
947
2332
  getFieldsList() {
948
2333
  return [
@@ -971,8 +2356,98 @@ registerBlockBindingsSource( {
971
2356
  } );
972
2357
  `;
973
2358
  }
2359
+ function resolveBindingTarget(options, namespace) {
2360
+ const hasBlock = options.blockName !== undefined && options.blockName.trim().length > 0;
2361
+ const hasAttribute = options.attributeName !== undefined && options.attributeName.trim().length > 0;
2362
+ if (!hasBlock && !hasAttribute) {
2363
+ return;
2364
+ }
2365
+ if (!hasBlock || !hasAttribute) {
2366
+ throw new Error("`wp-typia add binding-source` requires --block and --attribute to be provided together.");
2367
+ }
2368
+ return {
2369
+ attributeName: assertValidBindingAttributeName(options.attributeName ?? ""),
2370
+ blockSlug: resolveBindingTargetBlockSlug(options.blockName ?? "", namespace)
2371
+ };
2372
+ }
2373
+ function formatBindingAttributeTypeMember(attributeName) {
2374
+ const propertyName = /^[A-Za-z_$][\w$]*$/u.test(attributeName) ? attributeName : JSON.stringify(attributeName);
2375
+ return [
2376
+ "\t/**",
2377
+ "\t * Starter string attribute declared for WordPress Block Bindings.",
2378
+ "\t */",
2379
+ ` ${propertyName}?: string;`
2380
+ ].join(`
2381
+ `);
2382
+ }
2383
+ function getInterfaceDeclaration(source, interfaceName) {
2384
+ const sourceFile = import_typescript.default.createSourceFile("types.ts", source, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TS);
2385
+ let declaration;
2386
+ const visit = (node) => {
2387
+ if (import_typescript.default.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
2388
+ declaration = node;
2389
+ return true;
2390
+ }
2391
+ return import_typescript.default.forEachChild(node, (child) => visit(child) ? true : undefined) ?? false;
2392
+ };
2393
+ visit(sourceFile);
2394
+ return declaration ? { declaration, sourceFile } : undefined;
2395
+ }
2396
+ function getPropertyNameText(name) {
2397
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
2398
+ return name.text;
2399
+ }
2400
+ return;
2401
+ }
2402
+ function interfaceHasAttributeMember(declaration, attributeName) {
2403
+ return declaration.members.some((member) => import_typescript.default.isPropertySignature(member) && member.name !== undefined && getPropertyNameText(member.name) === attributeName);
2404
+ }
2405
+ function insertBindingAttributeTypeMember(source, declaration, attributeName) {
2406
+ let closeBracePosition = declaration.end - 1;
2407
+ while (closeBracePosition > declaration.pos && source[closeBracePosition] !== "}") {
2408
+ closeBracePosition -= 1;
2409
+ }
2410
+ if (source[closeBracePosition] !== "}") {
2411
+ throw new Error("Unable to locate the target interface closing brace.");
2412
+ }
2413
+ const lineEnding = source.includes(`\r
2414
+ `) ? `\r
2415
+ ` : `
2416
+ `;
2417
+ const beforeCloseBrace = source.slice(0, closeBracePosition);
2418
+ const afterCloseBrace = source.slice(closeBracePosition);
2419
+ const memberSource = formatBindingAttributeTypeMember(attributeName).split(`
2420
+ `).join(lineEnding);
2421
+ const prefix = beforeCloseBrace.endsWith(lineEnding) ? "" : lineEnding;
2422
+ return `${beforeCloseBrace}${prefix}${memberSource}${lineEnding}${afterCloseBrace}`;
2423
+ }
2424
+ async function ensureBindingTargetBlockAttributeType(projectDir, block, target) {
2425
+ if (!block.attributeTypeName) {
2426
+ throw new Error(`Workspace block "${block.slug}" must include attributeTypeName in scripts/block-config.ts before it can receive binding-source targets.`);
2427
+ }
2428
+ const typesPath = path5.join(projectDir, block.typesFile);
2429
+ const source = await fsp4.readFile(typesPath, "utf8");
2430
+ const targetInterface = getInterfaceDeclaration(source, block.attributeTypeName);
2431
+ if (!targetInterface) {
2432
+ throw new Error(`Unable to locate interface ${block.attributeTypeName} in ${block.typesFile}.`);
2433
+ }
2434
+ let nextSource = source;
2435
+ if (!interfaceHasAttributeMember(targetInterface.declaration, target.attributeName)) {
2436
+ nextSource = insertBindingAttributeTypeMember(source, targetInterface.declaration, target.attributeName);
2437
+ await fsp4.writeFile(typesPath, nextSource, "utf8");
2438
+ }
2439
+ await syncBlockMetadata2({
2440
+ blockJsonFile: path5.join("src", "blocks", block.slug, "block.json"),
2441
+ jsonSchemaFile: path5.join("src", "blocks", block.slug, "typia.schema.json"),
2442
+ manifestFile: path5.join("src", "blocks", block.slug, "typia.manifest.json"),
2443
+ openApiFile: path5.join("src", "blocks", block.slug, "typia.openapi.json"),
2444
+ projectRoot: projectDir,
2445
+ sourceTypeName: block.attributeTypeName,
2446
+ typesFile: block.typesFile
2447
+ });
2448
+ }
974
2449
  function buildEditorPluginTypesSource(editorPluginSlug) {
975
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
2450
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
976
2451
  return `export interface ${typeName} {
977
2452
  primaryActionLabel: string;
978
2453
  summary: string;
@@ -980,22 +2455,22 @@ function buildEditorPluginTypesSource(editorPluginSlug) {
980
2455
  `;
981
2456
  }
982
2457
  function buildEditorPluginDataSource(editorPluginSlug, slot) {
983
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
2458
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
984
2459
  const pluginTitle = toTitleCase(editorPluginSlug);
985
- const modelFactoryName = `get${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
986
- const enabledFactoryName = `is${toPascalCaseFromSlug(editorPluginSlug)}Enabled`;
2460
+ const modelFactoryName = `get${toPascalCase(editorPluginSlug)}EditorPluginModel`;
2461
+ const enabledFactoryName = `is${toPascalCase(editorPluginSlug)}Enabled`;
987
2462
  return `import type { ${typeName} } from './types';
988
2463
 
989
2464
  export const EDITOR_PLUGIN_SLOT = ${quoteTsString(slot)} as const;
990
2465
  export const REQUIRED_CAPABILITY = 'edit_posts' as const;
991
2466
 
992
- const DEFAULT_SIDEBAR_MODEL: ${typeName} = {
2467
+ const DEFAULT_EDITOR_PLUGIN_MODEL: ${typeName} = {
993
2468
  primaryActionLabel: ${quoteTsString(`Review ${pluginTitle}`)},
994
2469
  summary: ${quoteTsString(`Replace this summary with your ${pluginTitle} workflow state.`)},
995
2470
  };
996
2471
 
997
2472
  export function ${modelFactoryName}(): ${typeName} {
998
- return DEFAULT_SIDEBAR_MODEL;
2473
+ return DEFAULT_EDITOR_PLUGIN_MODEL;
999
2474
  }
1000
2475
 
1001
2476
  export function ${enabledFactoryName}(): boolean {
@@ -1003,11 +2478,52 @@ export function ${enabledFactoryName}(): boolean {
1003
2478
  }
1004
2479
  `;
1005
2480
  }
1006
- function buildEditorPluginSidebarSource(editorPluginSlug, textDomain) {
1007
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
1008
- const modelFactoryName = `get${pascalName}SidebarModel`;
2481
+ function buildEditorPluginSurfaceSource(editorPluginSlug, slot, textDomain) {
2482
+ const pascalName = toPascalCase(editorPluginSlug);
2483
+ const modelFactoryName = `get${pascalName}EditorPluginModel`;
1009
2484
  const enabledFactoryName = `is${pascalName}Enabled`;
1010
- const componentName = `${pascalName}Sidebar`;
2485
+ const componentName = `${pascalName}Surface`;
2486
+ if (slot === "document-setting-panel") {
2487
+ return `import { Button } from '@wordpress/components';
2488
+ import { PluginDocumentSettingPanel } from '@wordpress/editor';
2489
+ import { __ } from '@wordpress/i18n';
2490
+
2491
+ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
2492
+ import './style.scss';
2493
+
2494
+ export interface ${componentName}Props {
2495
+ surfaceName: string;
2496
+ title: string;
2497
+ }
2498
+
2499
+ export function ${componentName}( {
2500
+ surfaceName,
2501
+ title,
2502
+ }: ${componentName}Props ) {
2503
+ if ( ! ${enabledFactoryName}() ) {
2504
+ return null;
2505
+ }
2506
+
2507
+ const editorPluginModel = ${modelFactoryName}();
2508
+
2509
+ return (
2510
+ <PluginDocumentSettingPanel
2511
+ className="wp-typia-editor-plugin-shell"
2512
+ name={ surfaceName }
2513
+ title={ title }
2514
+ >
2515
+ <p>{ editorPluginModel.summary }</p>
2516
+ <Button variant="secondary">
2517
+ { editorPluginModel.primaryActionLabel }
2518
+ </Button>
2519
+ <p className="wp-typia-editor-plugin-shell__hint">
2520
+ { __( 'Use data.ts to add post type, capability, or editor context guards before showing this panel.', ${quoteTsString(textDomain)} ) }
2521
+ </p>
2522
+ </PluginDocumentSettingPanel>
2523
+ );
2524
+ }
2525
+ `;
2526
+ }
1011
2527
  return `import { Button, PanelBody } from '@wordpress/components';
1012
2528
  import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/editor';
1013
2529
  import { __ } from '@wordpress/i18n';
@@ -1016,34 +2532,34 @@ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
1016
2532
  import './style.scss';
1017
2533
 
1018
2534
  export interface ${componentName}Props {
1019
- pluginName: string;
2535
+ surfaceName: string;
1020
2536
  title: string;
1021
2537
  }
1022
2538
 
1023
2539
  export function ${componentName}( {
1024
- pluginName,
2540
+ surfaceName,
1025
2541
  title,
1026
2542
  }: ${componentName}Props ) {
1027
2543
  if ( ! ${enabledFactoryName}() ) {
1028
2544
  return null;
1029
2545
  }
1030
2546
 
1031
- const sidebarModel = ${modelFactoryName}();
2547
+ const editorPluginModel = ${modelFactoryName}();
1032
2548
 
1033
2549
  return (
1034
2550
  <>
1035
- <PluginSidebarMoreMenuItem target={ pluginName }>
2551
+ <PluginSidebarMoreMenuItem target={ surfaceName }>
1036
2552
  { title }
1037
2553
  </PluginSidebarMoreMenuItem>
1038
- <PluginSidebar name={ pluginName } title={ title }>
2554
+ <PluginSidebar name={ surfaceName } title={ title }>
1039
2555
  <div className="wp-typia-editor-plugin-shell">
1040
2556
  <PanelBody
1041
2557
  initialOpen
1042
2558
  title={ __( 'Document workflow', ${quoteTsString(textDomain)} ) }
1043
2559
  >
1044
- <p>{ sidebarModel.summary }</p>
2560
+ <p>{ editorPluginModel.summary }</p>
1045
2561
  <Button variant="secondary">
1046
- { sidebarModel.primaryActionLabel }
2562
+ { editorPluginModel.primaryActionLabel }
1047
2563
  </Button>
1048
2564
  </PanelBody>
1049
2565
  </div>
@@ -1054,24 +2570,26 @@ export function ${componentName}( {
1054
2570
  `;
1055
2571
  }
1056
2572
  function buildEditorPluginEntrySource(editorPluginSlug, namespace, textDomain) {
1057
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
1058
- const componentName = `${pascalName}Sidebar`;
2573
+ const pascalName = toPascalCase(editorPluginSlug);
2574
+ const componentName = `${pascalName}Surface`;
1059
2575
  const pluginName = `${namespace}-${editorPluginSlug}`;
2576
+ const surfaceName = `${pluginName}-surface`;
1060
2577
  const pluginTitle = toTitleCase(editorPluginSlug);
1061
2578
  return `import { registerPlugin } from '@wordpress/plugins';
1062
2579
  import { __ } from '@wordpress/i18n';
1063
2580
 
1064
2581
  import { REQUIRED_CAPABILITY } from './data';
1065
- import { ${componentName} } from './Sidebar';
2582
+ import { ${componentName} } from './Surface';
1066
2583
 
1067
2584
  const EDITOR_PLUGIN_NAME = ${quoteTsString(pluginName)};
2585
+ const EDITOR_PLUGIN_SURFACE_NAME = ${quoteTsString(surfaceName)};
1068
2586
  const EDITOR_PLUGIN_TITLE = __( ${quoteTsString(pluginTitle)}, ${quoteTsString(textDomain)} );
1069
2587
 
1070
2588
  registerPlugin( EDITOR_PLUGIN_NAME, {
1071
2589
  icon: 'admin-generic',
1072
2590
  render: () => (
1073
2591
  <${componentName}
1074
- pluginName={ EDITOR_PLUGIN_NAME }
2592
+ surfaceName={ EDITOR_PLUGIN_SURFACE_NAME }
1075
2593
  title={ EDITOR_PLUGIN_TITLE }
1076
2594
  />
1077
2595
  ),
@@ -1088,6 +2606,11 @@ function buildEditorPluginStyleSource() {
1088
2606
  .wp-typia-editor-plugin-shell p {
1089
2607
  margin: 0 0 12px;
1090
2608
  }
2609
+
2610
+ .wp-typia-editor-plugin-shell__hint {
2611
+ color: #757575;
2612
+ font-size: 12px;
2613
+ }
1091
2614
  `;
1092
2615
  }
1093
2616
  function buildBindingSourceIndexSource(bindingSourceSlugs) {
@@ -1156,7 +2679,7 @@ ${patternFunctions}
1156
2679
  }
1157
2680
  }
1158
2681
  if (!nextSource.includes(patternCategoryFunctionName) || !nextSource.includes(patternRegistrationFunctionName)) {
1159
- throw new Error(`Unable to inject pattern bootstrap functions into ${path4.basename(bootstrapPath)}.`);
2682
+ throw new Error(`Unable to inject pattern bootstrap functions into ${path5.basename(bootstrapPath)}.`);
1160
2683
  }
1161
2684
  if (!nextSource.includes(patternCategoryHook)) {
1162
2685
  nextSource = `${nextSource.trimEnd()}
@@ -1216,7 +2739,6 @@ function ${bindingEditorEnqueueFunctionName}() {
1216
2739
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1217
2740
  /\?>\s*$/u
1218
2741
  ];
1219
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
1220
2742
  const insertPhpSnippet = (snippet) => {
1221
2743
  for (const anchor of insertionAnchors) {
1222
2744
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1241,10 +2763,10 @@ ${snippet}
1241
2763
  ${snippet}
1242
2764
  `;
1243
2765
  };
1244
- if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
2766
+ if (!hasPhpFunctionDefinition(nextSource, bindingRegistrationFunctionName)) {
1245
2767
  insertPhpSnippet(bindingRegistrationFunction);
1246
2768
  }
1247
- if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
2769
+ if (!hasPhpFunctionDefinition(nextSource, bindingEditorEnqueueFunctionName)) {
1248
2770
  insertPhpSnippet(bindingEditorEnqueueFunction);
1249
2771
  }
1250
2772
  if (!nextSource.includes(bindingRegistrationHook)) {
@@ -1305,7 +2827,6 @@ function ${enqueueFunctionName}() {
1305
2827
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1306
2828
  /\?>\s*$/u
1307
2829
  ];
1308
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
1309
2830
  const insertPhpSnippet = (snippet) => {
1310
2831
  for (const anchor of insertionAnchors) {
1311
2832
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1330,7 +2851,7 @@ ${snippet}
1330
2851
  ${snippet}
1331
2852
  `;
1332
2853
  };
1333
- if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
2854
+ if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
1334
2855
  insertPhpSnippet(enqueueFunction);
1335
2856
  } else {
1336
2857
  const requiredReferences = [
@@ -1346,7 +2867,7 @@ ${snippet}
1346
2867
  if (missingReferences.length > 0) {
1347
2868
  const replacedSource = replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction);
1348
2869
  if (!replacedSource) {
1349
- throw new Error(`Unable to repair ${path4.basename(bootstrapPath)} for ${enqueueFunctionName}.`);
2870
+ throw new Error(`Unable to repair ${path5.basename(bootstrapPath)} for ${enqueueFunctionName}.`);
1350
2871
  }
1351
2872
  nextSource = replacedSource;
1352
2873
  }
@@ -1358,7 +2879,7 @@ ${snippet}
1358
2879
  });
1359
2880
  }
1360
2881
  async function ensureEditorPluginBuildScriptAnchors(workspace) {
1361
- const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
2882
+ const buildScriptPath = path5.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1362
2883
  await patchFile(buildScriptPath, (source) => {
1363
2884
  if (/['"]src\/editor-plugins\/index\.(?:ts|js)['"]/u.test(source)) {
1364
2885
  return source;
@@ -1371,13 +2892,13 @@ async function ensureEditorPluginBuildScriptAnchors(workspace) {
1371
2892
  'src/editor-plugins/index.js',
1372
2893
  ]`);
1373
2894
  if (nextSource === source) {
1374
- throw new Error(`Unable to update ${path4.relative(workspace.projectDir, buildScriptPath)} for editor plugin shared entries.`);
2895
+ throw new Error(`Unable to update ${path5.relative(workspace.projectDir, buildScriptPath)} for editor plugin shared entries.`);
1375
2896
  }
1376
2897
  return nextSource;
1377
2898
  });
1378
2899
  }
1379
2900
  async function ensureEditorPluginWebpackAnchors(workspace) {
1380
- const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
2901
+ const webpackConfigPath = path5.join(workspace.projectDir, "webpack.config.js");
1381
2902
  await patchFile(webpackConfigPath, (source) => {
1382
2903
  if (/['"]editor-plugins\/index['"]/u.test(source)) {
1383
2904
  return source;
@@ -1405,45 +2926,45 @@ async function ensureEditorPluginWebpackAnchors(workspace) {
1405
2926
  }`;
1406
2927
  const nextSource = source.replace(legacySharedEntriesBlockPattern, nextSharedEntriesBlock);
1407
2928
  if (nextSource === source) {
1408
- throw new Error(`Unable to update ${path4.relative(workspace.projectDir, webpackConfigPath)} for editor plugin shared entries.`);
2929
+ throw new Error(`Unable to update ${path5.relative(workspace.projectDir, webpackConfigPath)} for editor plugin shared entries.`);
1409
2930
  }
1410
2931
  return nextSource;
1411
2932
  });
1412
2933
  }
1413
2934
  function resolveBindingSourceRegistryPath(projectDir) {
1414
- const bindingsDir = path4.join(projectDir, "src", "bindings");
1415
- return [path4.join(bindingsDir, "index.ts"), path4.join(bindingsDir, "index.js")].find((candidatePath) => fs3.existsSync(candidatePath)) ?? path4.join(bindingsDir, "index.ts");
2935
+ const bindingsDir = path5.join(projectDir, "src", "bindings");
2936
+ return [path5.join(bindingsDir, "index.ts"), path5.join(bindingsDir, "index.js")].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path5.join(bindingsDir, "index.ts");
1416
2937
  }
1417
2938
  async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
1418
- const bindingsDir = path4.join(projectDir, "src", "bindings");
2939
+ const bindingsDir = path5.join(projectDir, "src", "bindings");
1419
2940
  const bindingsIndexPath = resolveBindingSourceRegistryPath(projectDir);
1420
- await fsp3.mkdir(bindingsDir, { recursive: true });
1421
- const existingBindingSourceSlugs = fs3.readdirSync(bindingsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2941
+ await fsp4.mkdir(bindingsDir, { recursive: true });
2942
+ const existingBindingSourceSlugs = fs4.readdirSync(bindingsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1422
2943
  const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
1423
- await fsp3.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
2944
+ await fsp4.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
1424
2945
  }
1425
2946
  function resolveEditorPluginRegistryPath(projectDir) {
1426
- const editorPluginsDir = path4.join(projectDir, "src", "editor-plugins");
2947
+ const editorPluginsDir = path5.join(projectDir, "src", "editor-plugins");
1427
2948
  return [
1428
- path4.join(editorPluginsDir, "index.ts"),
1429
- path4.join(editorPluginsDir, "index.js")
1430
- ].find((candidatePath) => fs3.existsSync(candidatePath)) ?? path4.join(editorPluginsDir, "index.ts");
2949
+ path5.join(editorPluginsDir, "index.ts"),
2950
+ path5.join(editorPluginsDir, "index.js")
2951
+ ].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path5.join(editorPluginsDir, "index.ts");
1431
2952
  }
1432
2953
  function readEditorPluginRegistrySlugs(registryPath) {
1433
- if (!fs3.existsSync(registryPath)) {
2954
+ if (!fs4.existsSync(registryPath)) {
1434
2955
  return [];
1435
2956
  }
1436
- const source = fs3.readFileSync(registryPath, "utf8");
2957
+ const source = fs4.readFileSync(registryPath, "utf8");
1437
2958
  return Array.from(source.matchAll(/^\s*import\s+['"]\.\/([^/'"]+)(?:\/index(?:\.[cm]?[jt]sx?)?)?['"];?\s*$/gmu)).map((match) => match[1]);
1438
2959
  }
1439
2960
  async function writeEditorPluginRegistry(projectDir, editorPluginSlug) {
1440
- const editorPluginsDir = path4.join(projectDir, "src", "editor-plugins");
2961
+ const editorPluginsDir = path5.join(projectDir, "src", "editor-plugins");
1441
2962
  const registryPath = resolveEditorPluginRegistryPath(projectDir);
1442
- await fsp3.mkdir(editorPluginsDir, { recursive: true });
2963
+ await fsp4.mkdir(editorPluginsDir, { recursive: true });
1443
2964
  const existingEditorPluginSlugs = readWorkspaceInventory(projectDir).editorPlugins.map((entry) => entry.slug);
1444
2965
  const existingRegistrySlugs = readEditorPluginRegistrySlugs(registryPath);
1445
2966
  const nextEditorPluginSlugs = Array.from(new Set([...existingEditorPluginSlugs, ...existingRegistrySlugs, editorPluginSlug])).sort();
1446
- await fsp3.writeFile(registryPath, buildEditorPluginRegistrySource(nextEditorPluginSlugs), "utf8");
2967
+ await fsp4.writeFile(registryPath, buildEditorPluginRegistrySource(nextEditorPluginSlugs), "utf8");
1447
2968
  }
1448
2969
  async function runAddEditorPluginCommand({
1449
2970
  cwd = process.cwd(),
@@ -1451,21 +2972,21 @@ async function runAddEditorPluginCommand({
1451
2972
  slot
1452
2973
  }) {
1453
2974
  const workspace = resolveWorkspaceProject(cwd);
1454
- const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <PluginSidebar>]");
2975
+ const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <sidebar|document-setting-panel>]");
1455
2976
  const resolvedSlot = assertValidEditorPluginSlot(slot);
1456
2977
  const inventory = readWorkspaceInventory(workspace.projectDir);
1457
2978
  assertEditorPluginDoesNotExist(workspace.projectDir, editorPluginSlug, inventory);
1458
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
2979
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1459
2980
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1460
- const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
2981
+ const buildScriptPath = path5.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1461
2982
  const editorPluginsIndexPath = resolveEditorPluginRegistryPath(workspace.projectDir);
1462
- const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
1463
- const editorPluginDir = path4.join(workspace.projectDir, "src", "editor-plugins", editorPluginSlug);
1464
- const entryFilePath = path4.join(editorPluginDir, "index.tsx");
1465
- const sidebarFilePath = path4.join(editorPluginDir, "Sidebar.tsx");
1466
- const dataFilePath = path4.join(editorPluginDir, "data.ts");
1467
- const typesFilePath = path4.join(editorPluginDir, "types.ts");
1468
- const styleFilePath = path4.join(editorPluginDir, "style.scss");
2983
+ const webpackConfigPath = path5.join(workspace.projectDir, "webpack.config.js");
2984
+ const editorPluginDir = path5.join(workspace.projectDir, "src", "editor-plugins", editorPluginSlug);
2985
+ const entryFilePath = path5.join(editorPluginDir, "index.tsx");
2986
+ const surfaceFilePath = path5.join(editorPluginDir, "Surface.tsx");
2987
+ const dataFilePath = path5.join(editorPluginDir, "data.ts");
2988
+ const typesFilePath = path5.join(editorPluginDir, "types.ts");
2989
+ const styleFilePath = path5.join(editorPluginDir, "style.scss");
1469
2990
  const mutationSnapshot = {
1470
2991
  fileSources: await snapshotWorkspaceFiles([
1471
2992
  blockConfigPath,
@@ -1478,15 +2999,15 @@ async function runAddEditorPluginCommand({
1478
2999
  targetPaths: [editorPluginDir]
1479
3000
  };
1480
3001
  try {
1481
- await fsp3.mkdir(editorPluginDir, { recursive: true });
3002
+ await fsp4.mkdir(editorPluginDir, { recursive: true });
1482
3003
  await ensureEditorPluginBootstrapAnchors(workspace);
1483
3004
  await ensureEditorPluginBuildScriptAnchors(workspace);
1484
3005
  await ensureEditorPluginWebpackAnchors(workspace);
1485
- await fsp3.writeFile(entryFilePath, buildEditorPluginEntrySource(editorPluginSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1486
- await fsp3.writeFile(sidebarFilePath, buildEditorPluginSidebarSource(editorPluginSlug, workspace.workspace.textDomain), "utf8");
1487
- await fsp3.writeFile(dataFilePath, buildEditorPluginDataSource(editorPluginSlug, resolvedSlot), "utf8");
1488
- await fsp3.writeFile(typesFilePath, buildEditorPluginTypesSource(editorPluginSlug), "utf8");
1489
- await fsp3.writeFile(styleFilePath, buildEditorPluginStyleSource(), "utf8");
3006
+ await fsp4.writeFile(entryFilePath, buildEditorPluginEntrySource(editorPluginSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
3007
+ await fsp4.writeFile(surfaceFilePath, buildEditorPluginSurfaceSource(editorPluginSlug, resolvedSlot, workspace.workspace.textDomain), "utf8");
3008
+ await fsp4.writeFile(dataFilePath, buildEditorPluginDataSource(editorPluginSlug, resolvedSlot), "utf8");
3009
+ await fsp4.writeFile(typesFilePath, buildEditorPluginTypesSource(editorPluginSlug), "utf8");
3010
+ await fsp4.writeFile(styleFilePath, buildEditorPluginStyleSource(), "utf8");
1490
3011
  await writeEditorPluginRegistry(workspace.projectDir, editorPluginSlug);
1491
3012
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1492
3013
  editorPluginEntries: [
@@ -1511,18 +3032,18 @@ async function runAddPatternCommand({
1511
3032
  const patternSlug = assertValidGeneratedSlug("Pattern name", normalizeBlockSlug(patternName), "wp-typia add pattern <name>");
1512
3033
  const inventory = readWorkspaceInventory(workspace.projectDir);
1513
3034
  assertPatternDoesNotExist(workspace.projectDir, patternSlug, inventory);
1514
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
3035
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1515
3036
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1516
- const patternFilePath = path4.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
3037
+ const patternFilePath = path5.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
1517
3038
  const mutationSnapshot = {
1518
3039
  fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath]),
1519
3040
  snapshotDirs: [],
1520
3041
  targetPaths: [patternFilePath]
1521
3042
  };
1522
3043
  try {
1523
- await fsp3.mkdir(path4.dirname(patternFilePath), { recursive: true });
3044
+ await fsp4.mkdir(path5.dirname(patternFilePath), { recursive: true });
1524
3045
  await ensurePatternBootstrapAnchors(workspace);
1525
- await fsp3.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
3046
+ await fsp4.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1526
3047
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1527
3048
  patternEntries: [buildPatternConfigEntry(patternSlug)]
1528
3049
  });
@@ -1536,34 +3057,59 @@ async function runAddPatternCommand({
1536
3057
  }
1537
3058
  }
1538
3059
  async function runAddBindingSourceCommand({
3060
+ attributeName,
1539
3061
  bindingSourceName,
3062
+ blockName,
1540
3063
  cwd = process.cwd()
1541
3064
  }) {
1542
3065
  const workspace = resolveWorkspaceProject(cwd);
1543
- const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
3066
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
1544
3067
  const inventory = readWorkspaceInventory(workspace.projectDir);
1545
3068
  assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
1546
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
3069
+ const target = resolveBindingTarget({
3070
+ attributeName,
3071
+ blockName
3072
+ }, workspace.workspace.namespace);
3073
+ const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
3074
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1547
3075
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1548
3076
  const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
1549
- const bindingSourceDir = path4.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
1550
- const serverFilePath = path4.join(bindingSourceDir, "server.php");
1551
- const editorFilePath = path4.join(bindingSourceDir, "editor.ts");
3077
+ const bindingSourceDir = path5.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
3078
+ const serverFilePath = path5.join(bindingSourceDir, "server.php");
3079
+ const editorFilePath = path5.join(bindingSourceDir, "editor.ts");
3080
+ const blockJsonPath = target ? path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "block.json") : undefined;
3081
+ const targetGeneratedMetadataPaths = target ? [
3082
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.manifest.json"),
3083
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.openapi.json"),
3084
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.schema.json"),
3085
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia-validator.php")
3086
+ ] : [];
1552
3087
  const mutationSnapshot = {
1553
- fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
3088
+ fileSources: await snapshotWorkspaceFiles([
3089
+ blockConfigPath,
3090
+ bootstrapPath,
3091
+ bindingsIndexPath,
3092
+ ...blockJsonPath ? [blockJsonPath] : [],
3093
+ ...targetBlock ? [path5.join(workspace.projectDir, targetBlock.typesFile)] : [],
3094
+ ...targetGeneratedMetadataPaths
3095
+ ]),
1554
3096
  snapshotDirs: [],
1555
3097
  targetPaths: [bindingSourceDir]
1556
3098
  };
1557
3099
  try {
1558
- await fsp3.mkdir(bindingSourceDir, { recursive: true });
3100
+ await fsp4.mkdir(bindingSourceDir, { recursive: true });
1559
3101
  await ensureBindingSourceBootstrapAnchors(workspace);
1560
- await fsp3.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1561
- await fsp3.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
3102
+ await fsp4.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
3103
+ await fsp4.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
3104
+ if (target && targetBlock) {
3105
+ await ensureBindingTargetBlockAttributeType(workspace.projectDir, targetBlock, target);
3106
+ }
1562
3107
  await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
1563
3108
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1564
- bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)]
3109
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug, target)]
1565
3110
  });
1566
3111
  return {
3112
+ ...target ? { attributeName: target.attributeName, blockSlug: target.blockSlug } : {},
1567
3113
  bindingSourceSlug,
1568
3114
  projectDir: workspace.projectDir
1569
3115
  };
@@ -1573,15 +3119,12 @@ async function runAddBindingSourceCommand({
1573
3119
  }
1574
3120
  }
1575
3121
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
1576
- import { promises as fsp4 } from "fs";
1577
- import path7 from "path";
3122
+ import { promises as fsp5 } from "fs";
3123
+ import path8 from "path";
1578
3124
 
1579
3125
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-anchors.ts
1580
- import path5 from "path";
3126
+ import path6 from "path";
1581
3127
  var REST_RESOURCE_SERVER_GLOB = "/inc/rest/*.php";
1582
- function escapeRegex2(value) {
1583
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1584
- }
1585
3128
  async function ensureRestResourceBootstrapAnchors(workspace) {
1586
3129
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1587
3130
  await patchFile(bootstrapPath, (source) => {
@@ -1600,7 +3143,6 @@ function ${registerFunctionName}() {
1600
3143
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1601
3144
  /\?>\s*$/u
1602
3145
  ];
1603
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex2(functionName)}\\s*\\(`, "u").test(nextSource);
1604
3146
  const insertPhpSnippet = (snippet) => {
1605
3147
  for (const anchor of insertionAnchors) {
1606
3148
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1625,11 +3167,11 @@ ${snippet}
1625
3167
  ${snippet}
1626
3168
  `;
1627
3169
  };
1628
- if (!hasPhpFunctionDefinition(registerFunctionName)) {
3170
+ if (!hasPhpFunctionDefinition(nextSource, registerFunctionName)) {
1629
3171
  insertPhpSnippet(registerFunction);
1630
3172
  } else if (!nextSource.includes(REST_RESOURCE_SERVER_GLOB)) {
1631
3173
  throw new Error([
1632
- `Unable to patch ${path5.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
3174
+ `Unable to patch ${path6.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
1633
3175
  `The existing ${registerFunctionName}() definition does not include ${REST_RESOURCE_SERVER_GLOB}.`,
1634
3176
  "Restore the generated bootstrap shape or wire the REST resource loader manually before retrying."
1635
3177
  ].join(" "));
@@ -1643,7 +3185,7 @@ ${snippet}
1643
3185
  function assertSyncRestAnchor(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
1644
3186
  if (!nextSource.includes(target) && !hasAnchor) {
1645
3187
  throw new Error([
1646
- `ensureRestResourceSyncScriptAnchors could not patch ${path5.basename(syncRestScriptPath)}.`,
3188
+ `ensureRestResourceSyncScriptAnchors could not patch ${path6.basename(syncRestScriptPath)}.`,
1647
3189
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
1648
3190
  "Restore the generated template or add the REST_RESOURCES wiring manually before retrying."
1649
3191
  ].join(" "));
@@ -1658,7 +3200,7 @@ function replaceRequiredSyncRestSource(nextSource, target, anchor, replacement,
1658
3200
  return nextSource.replace(anchor, replacement);
1659
3201
  }
1660
3202
  async function ensureRestResourceSyncScriptAnchors(workspace) {
1661
- const syncRestScriptPath = path5.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
3203
+ const syncRestScriptPath = path6.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
1662
3204
  await patchFile(syncRestScriptPath, (source) => {
1663
3205
  let nextSource = source;
1664
3206
  const importAnchor = "import { BLOCKS, type WorkspaceBlockConfig } from './block-config';";
@@ -1776,7 +3318,7 @@ async function ensureRestResourceSyncScriptAnchors(workspace) {
1776
3318
  }
1777
3319
 
1778
3320
  // ../wp-typia-project-tools/src/runtime/rest-resource-artifacts.ts
1779
- import path6 from "path";
3321
+ import path7 from "path";
1780
3322
  import {
1781
3323
  defineEndpointManifest,
1782
3324
  syncEndpointClient,
@@ -1912,8 +3454,8 @@ async function syncRestResourceArtifacts({
1912
3454
  const manifest = buildRestResourceEndpointManifest(variables, methods);
1913
3455
  for (const [baseName, contract] of Object.entries(manifest.contracts)) {
1914
3456
  await syncTypeSchemas({
1915
- jsonSchemaFile: path6.join(outputDir, "api-schemas", `${baseName}.schema.json`),
1916
- openApiFile: path6.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
3457
+ jsonSchemaFile: path7.join(outputDir, "api-schemas", `${baseName}.schema.json`),
3458
+ openApiFile: path7.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
1917
3459
  projectRoot: projectDir,
1918
3460
  sourceTypeName: contract.sourceTypeName,
1919
3461
  typesFile
@@ -1921,7 +3463,7 @@ async function syncRestResourceArtifacts({
1921
3463
  }
1922
3464
  await syncRestOpenApi({
1923
3465
  manifest,
1924
- openApiFile: path6.join(outputDir, "api.openapi.json"),
3466
+ openApiFile: path7.join(outputDir, "api.openapi.json"),
1925
3467
  projectRoot: projectDir,
1926
3468
  typesFile
1927
3469
  });
@@ -1935,16 +3477,13 @@ async function syncRestResourceArtifacts({
1935
3477
  }
1936
3478
 
1937
3479
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-source-emitters.ts
1938
- function toPascalCaseFromSlug2(slug) {
1939
- return normalizeBlockSlug(slug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
1940
- }
1941
3480
  function indentMultiline(source, prefix) {
1942
3481
  return source.split(`
1943
3482
  `).map((line) => `${prefix}${line}`).join(`
1944
3483
  `);
1945
3484
  }
1946
3485
  function buildRestResourceConfigEntry(restResourceSlug, namespace, methods) {
1947
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3486
+ const pascalCase = toPascalCase(restResourceSlug);
1948
3487
  const title = toTitleCase(restResourceSlug);
1949
3488
  const manifest = buildRestResourceEndpointManifest({
1950
3489
  namespace,
@@ -1972,7 +3511,7 @@ function buildRestResourceConfigEntry(restResourceSlug, namespace, methods) {
1972
3511
  `);
1973
3512
  }
1974
3513
  function buildRestResourceTypesSource(restResourceSlug, methods) {
1975
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3514
+ const pascalCase = toPascalCase(restResourceSlug);
1976
3515
  const lines = [
1977
3516
  "import { tags } from 'typia';",
1978
3517
  "",
@@ -2006,7 +3545,7 @@ function buildRestResourceTypesSource(restResourceSlug, methods) {
2006
3545
  `;
2007
3546
  }
2008
3547
  function buildRestResourceValidatorsSource(restResourceSlug, methods) {
2009
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3548
+ const pascalCase = toPascalCase(restResourceSlug);
2010
3549
  const importedTypes = new Set;
2011
3550
  const validatorDeclarations = [];
2012
3551
  const validatorEntries = [];
@@ -2054,7 +3593,7 @@ ${validatorEntries.join(`
2054
3593
  `;
2055
3594
  }
2056
3595
  function buildRestResourceApiSource(restResourceSlug, methods) {
2057
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3596
+ const pascalCase = toPascalCase(restResourceSlug);
2058
3597
  const typeImports = new Set;
2059
3598
  const clientEndpointImports = [];
2060
3599
  const exportedBindings = [];
@@ -2199,7 +3738,7 @@ ${exportedBindings.join(`
2199
3738
  `;
2200
3739
  }
2201
3740
  function buildRestResourceDataSource(restResourceSlug, methods) {
2202
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3741
+ const pascalCase = toPascalCase(restResourceSlug);
2203
3742
  const typeImports = new Set;
2204
3743
  const endpointImports = [];
2205
3744
  const exportedBindings = [];
@@ -2320,9 +3859,6 @@ ${exportedBindings.join(`
2320
3859
  }
2321
3860
 
2322
3861
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
2323
- function quotePhpString2(value) {
2324
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
2325
- }
2326
3862
  function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions) {
2327
3863
  const collectionRoutes = [];
2328
3864
  const itemRoutes = [];
@@ -2418,7 +3954,7 @@ if ( ! defined( 'ABSPATH' ) ) {
2418
3954
 
2419
3955
  if ( ! function_exists( '${getOptionNameFunctionName}' ) ) {
2420
3956
  function ${getOptionNameFunctionName}() {
2421
- return ${quotePhpString2(`${phpPrefix}_${restResourcePhpId}_rest_resource_items`)};
3957
+ return ${quotePhpString(`${phpPrefix}_${restResourcePhpId}_rest_resource_items`)};
2422
3958
  }
2423
3959
  }
2424
3960
 
@@ -2439,8 +3975,8 @@ if ( ! function_exists( '${getItemsFunctionName}' ) ) {
2439
3975
  $seed_items = array(
2440
3976
  array(
2441
3977
  'id' => 1,
2442
- 'title' => ${quotePhpString2(`${restResourceTitle} Starter`)},
2443
- 'content' => ${quotePhpString2(`Replace this seeded ${restResourceTitle.toLowerCase()} content with your plugin data source.`)},
3978
+ 'title' => ${quotePhpString(`${restResourceTitle} Starter`)},
3979
+ 'content' => ${quotePhpString(`Replace this seeded ${restResourceTitle.toLowerCase()} content with your plugin data source.`)},
2444
3980
  'status' => 'draft',
2445
3981
  'updatedAt' => '2026-01-01T00:00:00Z',
2446
3982
  ),
@@ -2731,7 +4267,7 @@ if ( ! function_exists( '${deleteHandlerName}' ) ) {
2731
4267
 
2732
4268
  if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
2733
4269
  function ${registerRoutesFunctionName}() {
2734
- $namespace = ${quotePhpString2(namespace)};
4270
+ $namespace = ${quotePhpString(namespace)};
2735
4271
 
2736
4272
  ${routeRegistrations}
2737
4273
  }
@@ -2752,15 +4288,15 @@ async function runAddRestResourceCommand({
2752
4288
  const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
2753
4289
  const inventory = readWorkspaceInventory(workspace.projectDir);
2754
4290
  assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
2755
- const blockConfigPath = path7.join(workspace.projectDir, "scripts", "block-config.ts");
4291
+ const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
2756
4292
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
2757
- const syncRestScriptPath = path7.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
2758
- const restResourceDir = path7.join(workspace.projectDir, "src", "rest", restResourceSlug);
2759
- const typesFilePath = path7.join(restResourceDir, "api-types.ts");
2760
- const validatorsFilePath = path7.join(restResourceDir, "api-validators.ts");
2761
- const apiFilePath = path7.join(restResourceDir, "api.ts");
2762
- const dataFilePath = path7.join(restResourceDir, "data.ts");
2763
- const phpFilePath = path7.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
4293
+ const syncRestScriptPath = path8.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4294
+ const restResourceDir = path8.join(workspace.projectDir, "src", "rest", restResourceSlug);
4295
+ const typesFilePath = path8.join(restResourceDir, "api-types.ts");
4296
+ const validatorsFilePath = path8.join(restResourceDir, "api-validators.ts");
4297
+ const apiFilePath = path8.join(restResourceDir, "api.ts");
4298
+ const dataFilePath = path8.join(restResourceDir, "data.ts");
4299
+ const phpFilePath = path8.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
2764
4300
  const mutationSnapshot = {
2765
4301
  fileSources: await snapshotWorkspaceFiles([
2766
4302
  blockConfigPath,
@@ -2771,15 +4307,15 @@ async function runAddRestResourceCommand({
2771
4307
  targetPaths: [restResourceDir, phpFilePath]
2772
4308
  };
2773
4309
  try {
2774
- await fsp4.mkdir(restResourceDir, { recursive: true });
2775
- await fsp4.mkdir(path7.dirname(phpFilePath), { recursive: true });
4310
+ await fsp5.mkdir(restResourceDir, { recursive: true });
4311
+ await fsp5.mkdir(path8.dirname(phpFilePath), { recursive: true });
2776
4312
  await ensureRestResourceBootstrapAnchors(workspace);
2777
4313
  await ensureRestResourceSyncScriptAnchors(workspace);
2778
- await fsp4.writeFile(typesFilePath, buildRestResourceTypesSource(restResourceSlug, resolvedMethods), "utf8");
2779
- await fsp4.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
2780
- await fsp4.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
2781
- await fsp4.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
2782
- await fsp4.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods), "utf8");
4314
+ await fsp5.writeFile(typesFilePath, buildRestResourceTypesSource(restResourceSlug, resolvedMethods), "utf8");
4315
+ await fsp5.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
4316
+ await fsp5.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
4317
+ await fsp5.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
4318
+ await fsp5.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods), "utf8");
2783
4319
  await syncRestResourceArtifacts({
2784
4320
  clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
2785
4321
  methods: resolvedMethods,
@@ -2789,7 +4325,7 @@ async function runAddRestResourceCommand({
2789
4325
  validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
2790
4326
  variables: {
2791
4327
  namespace: resolvedNamespace,
2792
- pascalCase: toPascalCaseFromSlug2(restResourceSlug),
4328
+ pascalCase: toPascalCase(restResourceSlug),
2793
4329
  slugKebabCase: restResourceSlug,
2794
4330
  title: toTitleCase(restResourceSlug)
2795
4331
  }
@@ -2813,58 +4349,17 @@ async function runAddRestResourceCommand({
2813
4349
  }
2814
4350
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ability.ts
2815
4351
  var import_semver = __toESM(require_semver(), 1);
2816
- import fs4 from "fs";
2817
- import { promises as fsp5 } from "fs";
2818
- import path8 from "path";
4352
+ import fs5 from "fs";
4353
+ import { promises as fsp6 } from "fs";
4354
+ import path9 from "path";
2819
4355
  import { syncTypeSchemas as syncTypeSchemas2 } from "@wp-typia/block-runtime/metadata-core";
2820
4356
  var ABILITY_SERVER_GLOB = "/inc/abilities/*.php";
2821
4357
  var ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
2822
4358
  var ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
2823
4359
  var ABILITY_REGISTRY_END_MARKER = "// wp-typia add ability entries end";
2824
4360
  var ABILITY_REGISTRY_START_MARKER = "// wp-typia add ability entries start";
2825
- var WP_ABILITIES_PACKAGE_VERSION = "^0.10.0";
2826
- var WP_CORE_ABILITIES_PACKAGE_VERSION = "^0.9.0";
2827
4361
  var WP_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/abilities";
2828
4362
  var WP_CORE_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/core-abilities";
2829
- function escapeRegex3(value) {
2830
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
2831
- }
2832
- function quotePhpString3(value) {
2833
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
2834
- }
2835
- function findPhpFunctionRange2(source, functionName) {
2836
- const functionPattern = new RegExp(`function\\s+${escapeRegex3(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u");
2837
- const match = functionPattern.exec(source);
2838
- if (!match) {
2839
- return null;
2840
- }
2841
- const openingBraceIndex = match.index + match[0].length - 1;
2842
- let depth = 0;
2843
- for (let index = openingBraceIndex;index < source.length; index += 1) {
2844
- const character = source[index];
2845
- if (character === "{") {
2846
- depth += 1;
2847
- } else if (character === "}") {
2848
- depth -= 1;
2849
- if (depth === 0) {
2850
- const end = index + 1;
2851
- return {
2852
- end,
2853
- source: source.slice(match.index, end),
2854
- start: match.index
2855
- };
2856
- }
2857
- }
2858
- }
2859
- return null;
2860
- }
2861
- function replacePhpFunctionDefinition2(source, functionName, replacement) {
2862
- const functionRange = findPhpFunctionRange2(source, functionName);
2863
- if (!functionRange) {
2864
- return source;
2865
- }
2866
- return `${source.slice(0, functionRange.start)}${replacement.trimStart()}${source.slice(functionRange.end)}`;
2867
- }
2868
4363
  function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
2869
4364
  if (!existingVersion) {
2870
4365
  return requiredVersion;
@@ -2876,15 +4371,12 @@ function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
2876
4371
  }
2877
4372
  return import_semver.default.gte(existingMinimum, requiredMinimum) ? existingVersion : requiredVersion;
2878
4373
  }
2879
- function toPascalCaseFromAbilitySlug(abilitySlug) {
2880
- return normalizeBlockSlug(abilitySlug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
2881
- }
2882
4374
  function toAbilityCategorySlug(workspaceNamespace) {
2883
4375
  const normalizedNamespace = workspaceNamespace.replace(/[^a-z0-9-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^-|-$/gu, "");
2884
4376
  return `${normalizedNamespace || "workspace"}-workflows`;
2885
4377
  }
2886
4378
  function buildAbilityConfigEntry(abilitySlug, compatibilityPolicy) {
2887
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
4379
+ const pascalCase = toPascalCase(abilitySlug);
2888
4380
  return [
2889
4381
  "\t{",
2890
4382
  ` clientFile: ${quoteTsString(`src/abilities/${abilitySlug}/client.ts`)},`,
@@ -2928,7 +4420,7 @@ function buildAbilityConfigSource(abilitySlug, workspaceNamespace) {
2928
4420
  `;
2929
4421
  }
2930
4422
  function buildAbilityTypesSource(abilitySlug) {
2931
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
4423
+ const pascalCase = toPascalCase(abilitySlug);
2932
4424
  return `export interface ${pascalCase}AbilityInput {
2933
4425
  contextId: number;
2934
4426
  note?: string;
@@ -2943,7 +4435,7 @@ export interface ${pascalCase}AbilityOutput {
2943
4435
  `;
2944
4436
  }
2945
4437
  function buildAbilityDataSource(abilitySlug) {
2946
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
4438
+ const pascalCase = toPascalCase(abilitySlug);
2947
4439
  const abilityConstBase = abilitySlug.toUpperCase().replace(/[^A-Z0-9]+/gu, "_").replace(/_{2,}/gu, "_").replace(/^_|_$/gu, "");
2948
4440
  return `import {
2949
4441
  executeAbility,
@@ -3039,7 +4531,7 @@ export async function run${pascalCase}Ability(
3039
4531
  `;
3040
4532
  }
3041
4533
  function buildAbilityClientSource(abilitySlug) {
3042
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
4534
+ const pascalCase = toPascalCase(abilitySlug);
3043
4535
  return `/**
3044
4536
  * Re-export the typed ${pascalCase} ability client helpers.
3045
4537
  *
@@ -3243,8 +4735,8 @@ if ( ! function_exists( '${executeFunctionName}' ) ) {
3243
4735
  'status' => 'ready',
3244
4736
  'summary' => sprintf(
3245
4737
  /* translators: 1: workflow title, 2: context id */
3246
- __( '%1$s processed context %2$d.', ${quotePhpString3(workspace.workspace.textDomain)} ),
3247
- ${quotePhpString3(abilityTitle)},
4738
+ __( '%1$s processed context %2$d.', ${quotePhpString(workspace.workspace.textDomain)} ),
4739
+ ${quotePhpString(abilityTitle)},
3248
4740
  $context_id
3249
4741
  ),
3250
4742
  );
@@ -3310,11 +4802,11 @@ if ( ! function_exists( '${abilityRegisterFunctionName}' ) ) {
3310
4802
  $args = array(
3311
4803
  'category' => (string) $config['category']['slug'],
3312
4804
  'description' => (string) $config['description'],
3313
- 'execute_callback' => ${quotePhpString3(executeFunctionName)},
4805
+ 'execute_callback' => ${quotePhpString(executeFunctionName)},
3314
4806
  'label' => (string) $config['label'],
3315
4807
  'meta' => ${metaFactoryFunctionName}( $config ),
3316
4808
  'output_schema' => $output_schema,
3317
- 'permission_callback' => ${quotePhpString3(permissionFunctionName)},
4809
+ 'permission_callback' => ${quotePhpString(permissionFunctionName)},
3318
4810
  );
3319
4811
 
3320
4812
  if ( is_array( $input_schema ) ) {
@@ -3344,30 +4836,30 @@ function buildAbilityRegistrySource(abilitySlugs) {
3344
4836
  `);
3345
4837
  }
3346
4838
  function resolveAbilityRegistryPath(projectDir) {
3347
- const abilitiesDir = path8.join(projectDir, "src", "abilities");
3348
- return [path8.join(abilitiesDir, "index.ts"), path8.join(abilitiesDir, "index.js")].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path8.join(abilitiesDir, "index.ts");
4839
+ const abilitiesDir = path9.join(projectDir, "src", "abilities");
4840
+ return [path9.join(abilitiesDir, "index.ts"), path9.join(abilitiesDir, "index.js")].find((candidatePath) => fs5.existsSync(candidatePath)) ?? path9.join(abilitiesDir, "index.ts");
3349
4841
  }
3350
4842
  function readAbilityRegistrySlugs(registryPath) {
3351
- if (!fs4.existsSync(registryPath)) {
4843
+ if (!fs5.existsSync(registryPath)) {
3352
4844
  return [];
3353
4845
  }
3354
- const source = fs4.readFileSync(registryPath, "utf8");
4846
+ const source = fs5.readFileSync(registryPath, "utf8");
3355
4847
  return Array.from(source.matchAll(/^\s*export\s+\*\s+from\s+['"]\.\/([^/'"]+)\/client['"];?\s*$/gmu)).map((match) => match[1]);
3356
4848
  }
3357
4849
  async function writeAbilityRegistry(projectDir, abilitySlug) {
3358
- const abilitiesDir = path8.join(projectDir, "src", "abilities");
4850
+ const abilitiesDir = path9.join(projectDir, "src", "abilities");
3359
4851
  const registryPath = resolveAbilityRegistryPath(projectDir);
3360
- await fsp5.mkdir(abilitiesDir, { recursive: true });
4852
+ await fsp6.mkdir(abilitiesDir, { recursive: true });
3361
4853
  const existingAbilitySlugs = readWorkspaceInventory(projectDir).abilities.map((entry) => entry.slug);
3362
4854
  const existingRegistrySlugs = readAbilityRegistrySlugs(registryPath);
3363
4855
  const nextAbilitySlugs = Array.from(new Set([...existingAbilitySlugs, ...existingRegistrySlugs, abilitySlug])).sort();
3364
4856
  const generatedSection = buildAbilityRegistrySource(nextAbilitySlugs);
3365
- const existingSource = fs4.existsSync(registryPath) ? fs4.readFileSync(registryPath, "utf8") : "";
3366
- const generatedSectionPattern = new RegExp(`${escapeRegex3(ABILITY_REGISTRY_START_MARKER)}[\\s\\S]*?${escapeRegex3(ABILITY_REGISTRY_END_MARKER)}\\n?`, "u");
4857
+ const existingSource = fs5.existsSync(registryPath) ? fs5.readFileSync(registryPath, "utf8") : "";
4858
+ const generatedSectionPattern = new RegExp(`${escapeRegex(ABILITY_REGISTRY_START_MARKER)}[\\s\\S]*?${escapeRegex(ABILITY_REGISTRY_END_MARKER)}\\n?`, "u");
3367
4859
  const nextSource = existingSource ? generatedSectionPattern.test(existingSource) ? existingSource.replace(generatedSectionPattern, generatedSection) : `${existingSource.trimEnd()}
3368
4860
 
3369
4861
  ${generatedSection}` : generatedSection;
3370
- await fsp5.writeFile(registryPath, nextSource, "utf8");
4862
+ await fsp6.writeFile(registryPath, nextSource, "utf8");
3371
4863
  }
3372
4864
  async function ensureAbilityBootstrapAnchors(workspace) {
3373
4865
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
@@ -3442,7 +4934,6 @@ function ${enqueueFunctionName}() {
3442
4934
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
3443
4935
  /\?>\s*$/u
3444
4936
  ];
3445
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex3(functionName)}\\s*\\(`, "u").test(nextSource);
3446
4937
  const insertPhpSnippet = (snippet) => {
3447
4938
  for (const anchor of insertionAnchors) {
3448
4939
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -3467,13 +4958,13 @@ ${snippet}
3467
4958
  ${snippet}
3468
4959
  `;
3469
4960
  };
3470
- if (!hasPhpFunctionDefinition(loadFunctionName)) {
4961
+ if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
3471
4962
  insertPhpSnippet(loadFunction);
3472
4963
  }
3473
- if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
4964
+ if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
3474
4965
  insertPhpSnippet(enqueueFunction);
3475
- } else if (!findPhpFunctionRange2(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
3476
- nextSource = replacePhpFunctionDefinition2(nextSource, enqueueFunctionName, enqueueFunction);
4966
+ } else if (!findPhpFunctionRange(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
4967
+ nextSource = replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction, { trimReplacementStart: true }) ?? nextSource;
3477
4968
  }
3478
4969
  if (!nextSource.includes(loadHook)) {
3479
4970
  appendPhpSnippet(loadHook);
@@ -3488,27 +4979,27 @@ ${snippet}
3488
4979
  });
3489
4980
  }
3490
4981
  async function ensureAbilityPackageScripts(workspace) {
3491
- const packageJsonPath = path8.join(workspace.projectDir, "package.json");
3492
- const packageJson = JSON.parse(await fsp5.readFile(packageJsonPath, "utf8"));
4982
+ const packageJsonPath = path9.join(workspace.projectDir, "package.json");
4983
+ const packageJson = JSON.parse(await fsp6.readFile(packageJsonPath, "utf8"));
3493
4984
  const nextScripts = {
3494
4985
  ...packageJson.scripts ?? {},
3495
4986
  "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts"
3496
4987
  };
3497
4988
  const nextDependencies = {
3498
4989
  ...packageJson.dependencies ?? {},
3499
- [WP_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_ABILITIES_SCRIPT_MODULE_ID], WP_ABILITIES_PACKAGE_VERSION),
3500
- [WP_CORE_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_CORE_ABILITIES_SCRIPT_MODULE_ID], WP_CORE_ABILITIES_PACKAGE_VERSION)
4990
+ [WP_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_ABILITIES_SCRIPT_MODULE_ID], DEFAULT_WORDPRESS_ABILITIES_VERSION),
4991
+ [WP_CORE_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_CORE_ABILITIES_SCRIPT_MODULE_ID], DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION)
3501
4992
  };
3502
4993
  if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) && JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {})) {
3503
4994
  return;
3504
4995
  }
3505
4996
  packageJson.scripts = nextScripts;
3506
4997
  packageJson.dependencies = nextDependencies;
3507
- await fsp5.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4998
+ await fsp6.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
3508
4999
  `, "utf8");
3509
5000
  }
3510
5001
  async function ensureAbilitySyncProjectAnchors(workspace) {
3511
- const syncProjectScriptPath = path8.join(workspace.projectDir, "scripts", "sync-project.ts");
5002
+ const syncProjectScriptPath = path9.join(workspace.projectDir, "scripts", "sync-project.ts");
3512
5003
  await patchFile(syncProjectScriptPath, (source) => {
3513
5004
  let nextSource = source;
3514
5005
  const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
@@ -3523,7 +5014,7 @@ async function ensureAbilitySyncProjectAnchors(workspace) {
3523
5014
  if (!nextSource.includes(syncAbilitiesConst)) {
3524
5015
  if (!nextSource.includes(syncRestConst)) {
3525
5016
  throw new Error([
3526
- `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
5017
+ `ensureAbilitySyncProjectAnchors could not patch ${path9.basename(syncProjectScriptPath)}.`,
3527
5018
  "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
3528
5019
  "Restore the generated template or wire sync-abilities manually before retrying."
3529
5020
  ].join(" "));
@@ -3534,7 +5025,7 @@ ${syncAbilitiesConst}`);
3534
5025
  if (!nextSource.includes("runSyncScript( syncAbilitiesScriptPath, options );")) {
3535
5026
  if (!syncRestBlockPattern.test(nextSource)) {
3536
5027
  throw new Error([
3537
- `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
5028
+ `ensureAbilitySyncProjectAnchors could not patch ${path9.basename(syncProjectScriptPath)}.`,
3538
5029
  "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
3539
5030
  "Restore the generated template or wire sync-abilities manually before retrying."
3540
5031
  ].join(" "));
@@ -3547,7 +5038,7 @@ ${syncAbilitiesBlock}`);
3547
5038
  });
3548
5039
  }
3549
5040
  async function ensureAbilityBuildScriptAnchors(workspace) {
3550
- const buildScriptPath = path8.join(workspace.projectDir, "scripts", "build-workspace.mjs");
5041
+ const buildScriptPath = path9.join(workspace.projectDir, "scripts", "build-workspace.mjs");
3551
5042
  await patchFile(buildScriptPath, (source) => {
3552
5043
  let nextSource = source;
3553
5044
  if (/['"]src\/abilities\/index\.(?:ts|js)['"]/u.test(nextSource)) {
@@ -3557,7 +5048,7 @@ async function ensureAbilityBuildScriptAnchors(workspace) {
3557
5048
  const match = nextSource.match(sharedEntriesPattern);
3558
5049
  if (!match || !match[2].includes("src/bindings/index.ts") || !match[2].includes("src/editor-plugins/index.ts")) {
3559
5050
  throw new Error([
3560
- `ensureAbilityBuildScriptAnchors could not patch ${path8.basename(buildScriptPath)}.`,
5051
+ `ensureAbilityBuildScriptAnchors could not patch ${path9.basename(buildScriptPath)}.`,
3561
5052
  "Missing the expected shared editor entries array in scripts/build-workspace.mjs.",
3562
5053
  "Restore the generated template or wire abilities/index manually before retrying."
3563
5054
  ].join(" "));
@@ -3574,7 +5065,7 @@ async function ensureAbilityBuildScriptAnchors(workspace) {
3574
5065
  });
3575
5066
  }
3576
5067
  async function ensureAbilityWebpackAnchors(workspace) {
3577
- const webpackConfigPath = path8.join(workspace.projectDir, "webpack.config.js");
5068
+ const webpackConfigPath = path9.join(workspace.projectDir, "webpack.config.js");
3578
5069
  await patchFile(webpackConfigPath, (source) => {
3579
5070
  if (/['"]abilities\/index['"]/u.test(source)) {
3580
5071
  return source;
@@ -3605,7 +5096,7 @@ $2`);
3605
5096
  const match = source.match(sharedEntriesPattern);
3606
5097
  if (!match || !match[1].includes("bindings/index") || !match[1].includes("editor-plugins/index")) {
3607
5098
  throw new Error([
3608
- `ensureAbilityWebpackAnchors could not patch ${path8.basename(webpackConfigPath)}.`,
5099
+ `ensureAbilityWebpackAnchors could not patch ${path9.basename(webpackConfigPath)}.`,
3609
5100
  "Missing the expected shared editor entries block in webpack.config.js.",
3610
5101
  "Restore the generated template or wire abilities/index manually before retrying."
3611
5102
  ].join(" "));
@@ -3635,20 +5126,20 @@ async function runAddAbilityCommand({
3635
5126
  const inventory = readWorkspaceInventory(workspace.projectDir);
3636
5127
  assertAbilityDoesNotExist(workspace.projectDir, abilitySlug, inventory);
3637
5128
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY);
3638
- const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
5129
+ const blockConfigPath = path9.join(workspace.projectDir, "scripts", "block-config.ts");
3639
5130
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
3640
- const buildScriptPath = path8.join(workspace.projectDir, "scripts", "build-workspace.mjs");
3641
- const packageJsonPath = path8.join(workspace.projectDir, "package.json");
3642
- const syncAbilitiesScriptPath = path8.join(workspace.projectDir, "scripts", "sync-abilities.ts");
3643
- const syncProjectScriptPath = path8.join(workspace.projectDir, "scripts", "sync-project.ts");
3644
- const webpackConfigPath = path8.join(workspace.projectDir, "webpack.config.js");
5131
+ const buildScriptPath = path9.join(workspace.projectDir, "scripts", "build-workspace.mjs");
5132
+ const packageJsonPath = path9.join(workspace.projectDir, "package.json");
5133
+ const syncAbilitiesScriptPath = path9.join(workspace.projectDir, "scripts", "sync-abilities.ts");
5134
+ const syncProjectScriptPath = path9.join(workspace.projectDir, "scripts", "sync-project.ts");
5135
+ const webpackConfigPath = path9.join(workspace.projectDir, "webpack.config.js");
3645
5136
  const abilitiesIndexPath = resolveAbilityRegistryPath(workspace.projectDir);
3646
- const abilityDir = path8.join(workspace.projectDir, "src", "abilities", abilitySlug);
3647
- const configFilePath = path8.join(abilityDir, "ability.config.json");
3648
- const typesFilePath = path8.join(abilityDir, "types.ts");
3649
- const dataFilePath = path8.join(abilityDir, "data.ts");
3650
- const clientFilePath = path8.join(abilityDir, "client.ts");
3651
- const phpFilePath = path8.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
5137
+ const abilityDir = path9.join(workspace.projectDir, "src", "abilities", abilitySlug);
5138
+ const configFilePath = path9.join(abilityDir, "ability.config.json");
5139
+ const typesFilePath = path9.join(abilityDir, "types.ts");
5140
+ const dataFilePath = path9.join(abilityDir, "data.ts");
5141
+ const clientFilePath = path9.join(abilityDir, "client.ts");
5142
+ const phpFilePath = path9.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
3652
5143
  const mutationSnapshot = {
3653
5144
  fileSources: await snapshotWorkspaceFiles([
3654
5145
  blockConfigPath,
@@ -3664,21 +5155,21 @@ async function runAddAbilityCommand({
3664
5155
  targetPaths: [abilityDir, phpFilePath, syncAbilitiesScriptPath]
3665
5156
  };
3666
5157
  try {
3667
- await fsp5.mkdir(abilityDir, { recursive: true });
3668
- await fsp5.mkdir(path8.dirname(phpFilePath), { recursive: true });
5158
+ await fsp6.mkdir(abilityDir, { recursive: true });
5159
+ await fsp6.mkdir(path9.dirname(phpFilePath), { recursive: true });
3669
5160
  await ensureAbilityBootstrapAnchors(workspace);
3670
5161
  await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
3671
5162
  await ensureAbilityPackageScripts(workspace);
3672
5163
  await ensureAbilitySyncProjectAnchors(workspace);
3673
5164
  await ensureAbilityBuildScriptAnchors(workspace);
3674
5165
  await ensureAbilityWebpackAnchors(workspace);
3675
- await fsp5.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
3676
- await fsp5.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
3677
- await fsp5.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
3678
- await fsp5.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
3679
- await fsp5.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
3680
- await fsp5.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
3681
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
5166
+ await fsp6.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
5167
+ await fsp6.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
5168
+ await fsp6.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
5169
+ await fsp6.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
5170
+ await fsp6.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
5171
+ await fsp6.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
5172
+ const pascalCase = toPascalCase(abilitySlug);
3682
5173
  await syncTypeSchemas2({
3683
5174
  jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
3684
5175
  projectRoot: workspace.projectDir,
@@ -3705,12 +5196,12 @@ async function runAddAbilityCommand({
3705
5196
  }
3706
5197
  }
3707
5198
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
3708
- import { promises as fsp7 } from "fs";
3709
- import path11 from "path";
5199
+ import { promises as fsp8 } from "fs";
5200
+ import path12 from "path";
3710
5201
 
3711
5202
  // ../wp-typia-project-tools/src/runtime/ai-feature-artifacts.ts
3712
5203
  import { mkdir, readFile, writeFile } from "fs/promises";
3713
- import path9 from "path";
5204
+ import path10 from "path";
3714
5205
  import {
3715
5206
  defineEndpointManifest as defineEndpointManifest2,
3716
5207
  syncEndpointClient as syncEndpointClient2,
@@ -3736,7 +5227,7 @@ function normalizeGeneratedArtifactContent(content) {
3736
5227
  }
3737
5228
  async function reconcileGeneratedArtifact(options) {
3738
5229
  if (!options.check) {
3739
- await mkdir(path9.dirname(options.filePath), {
5230
+ await mkdir(path10.dirname(options.filePath), {
3740
5231
  recursive: true
3741
5232
  });
3742
5233
  await writeFile(options.filePath, options.content, "utf8");
@@ -3808,8 +5299,8 @@ async function syncAiFeatureRestArtifacts({
3808
5299
  const manifest = buildAiFeatureEndpointManifest(variables);
3809
5300
  for (const [baseName, contract] of Object.entries(manifest.contracts)) {
3810
5301
  await syncTypeSchemas3({
3811
- jsonSchemaFile: path9.join(outputDir, "api-schemas", `${baseName}.schema.json`),
3812
- openApiFile: path9.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
5302
+ jsonSchemaFile: path10.join(outputDir, "api-schemas", `${baseName}.schema.json`),
5303
+ openApiFile: path10.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
3813
5304
  projectRoot: projectDir,
3814
5305
  sourceTypeName: contract.sourceTypeName,
3815
5306
  typesFile
@@ -3817,7 +5308,7 @@ async function syncAiFeatureRestArtifacts({
3817
5308
  }
3818
5309
  await syncRestOpenApi2({
3819
5310
  manifest,
3820
- openApiFile: path9.join(outputDir, "api.openapi.json"),
5311
+ openApiFile: path10.join(outputDir, "api.openapi.json"),
3821
5312
  projectRoot: projectDir,
3822
5313
  typesFile
3823
5314
  }, executionOptions);
@@ -3835,35 +5326,32 @@ async function syncAiFeatureSchemaArtifact({
3835
5326
  outputDir,
3836
5327
  projectDir
3837
5328
  }) {
3838
- const sourceSchemaPath = path9.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
5329
+ const sourceSchemaPath = path10.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
3839
5330
  const responseSchema = assertJsonObject(JSON.parse(await readFile(sourceSchemaPath, "utf8")), sourceSchemaPath);
3840
5331
  const aiSchema = projectWordPressAiSchema(responseSchema);
3841
5332
  await reconcileGeneratedArtifact({
3842
5333
  check,
3843
5334
  content: `${JSON.stringify(aiSchema, null, 2)}
3844
5335
  `,
3845
- filePath: path9.join(projectDir, aiSchemaFile),
5336
+ filePath: path10.join(projectDir, aiSchemaFile),
3846
5337
  label: "AI feature schema"
3847
5338
  });
3848
5339
  return {
3849
5340
  aiSchema,
3850
- aiSchemaPath: path9.join(projectDir, aiSchemaFile),
5341
+ aiSchemaPath: path10.join(projectDir, aiSchemaFile),
3851
5342
  check,
3852
5343
  sourceSchemaPath
3853
5344
  };
3854
5345
  }
3855
5346
 
3856
5347
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai-source-emitters.ts
3857
- function toPascalCaseFromAiFeatureSlug(slug) {
3858
- return normalizeBlockSlug(slug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
3859
- }
3860
5348
  function indentMultiline2(source, prefix) {
3861
5349
  return source.split(`
3862
5350
  `).map((line) => `${prefix}${line}`).join(`
3863
5351
  `);
3864
5352
  }
3865
5353
  function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
3866
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5354
+ const pascalCase = toPascalCase(aiFeatureSlug);
3867
5355
  const title = toTitleCase(aiFeatureSlug);
3868
5356
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
3869
5357
  const manifest = buildAiFeatureEndpointManifest({
@@ -3893,7 +5381,7 @@ function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
3893
5381
  `);
3894
5382
  }
3895
5383
  function buildAiFeatureTypesSource(aiFeatureSlug) {
3896
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5384
+ const pascalCase = toPascalCase(aiFeatureSlug);
3897
5385
  return `import { tags } from 'typia';
3898
5386
 
3899
5387
  export interface ${pascalCase}AiFeatureRequest {
@@ -3931,7 +5419,7 @@ export interface ${pascalCase}AiFeatureResponse {
3931
5419
  `;
3932
5420
  }
3933
5421
  function buildAiFeatureValidatorsSource(aiFeatureSlug) {
3934
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5422
+ const pascalCase = toPascalCase(aiFeatureSlug);
3935
5423
  return `import typia from 'typia';
3936
5424
 
3937
5425
  import { toValidationResult } from '@wp-typia/rest';
@@ -3962,7 +5450,7 @@ export const apiValidators = {
3962
5450
  `;
3963
5451
  }
3964
5452
  function buildAiFeatureApiSource(aiFeatureSlug) {
3965
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5453
+ const pascalCase = toPascalCase(aiFeatureSlug);
3966
5454
  return `import {
3967
5455
  callEndpoint,
3968
5456
  resolveRestRouteUrl,
@@ -4017,7 +5505,7 @@ export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
4017
5505
  `;
4018
5506
  }
4019
5507
  function buildAiFeatureDataSource(aiFeatureSlug) {
4020
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5508
+ const pascalCase = toPascalCase(aiFeatureSlug);
4021
5509
  return `import {
4022
5510
  useEndpointMutation,
4023
5511
  type UseEndpointMutationOptions,
@@ -4173,12 +5661,9 @@ main().catch( ( error ) => {
4173
5661
  }
4174
5662
 
4175
5663
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai-anchors.ts
4176
- import { promises as fsp6 } from "fs";
4177
- import path10 from "path";
5664
+ import { promises as fsp7 } from "fs";
5665
+ import path11 from "path";
4178
5666
  var AI_FEATURE_SERVER_GLOB = "/inc/ai-features/*.php";
4179
- function escapeRegex4(value) {
4180
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4181
- }
4182
5667
  async function ensureAiFeatureBootstrapAnchors(workspace) {
4183
5668
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4184
5669
  await patchFile(bootstrapPath, (source) => {
@@ -4197,7 +5682,6 @@ function ${registerFunctionName}() {
4197
5682
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
4198
5683
  /\?>\s*$/u
4199
5684
  ];
4200
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex4(functionName)}\\s*\\(`, "u").test(nextSource);
4201
5685
  const insertPhpSnippet = (snippet) => {
4202
5686
  for (const anchor of insertionAnchors) {
4203
5687
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -4222,11 +5706,11 @@ ${snippet}
4222
5706
  ${snippet}
4223
5707
  `;
4224
5708
  };
4225
- if (!hasPhpFunctionDefinition(registerFunctionName)) {
5709
+ if (!hasPhpFunctionDefinition(nextSource, registerFunctionName)) {
4226
5710
  insertPhpSnippet(registerFunction);
4227
5711
  } else if (!nextSource.includes(AI_FEATURE_SERVER_GLOB)) {
4228
5712
  throw new Error([
4229
- `Unable to patch ${path10.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
5713
+ `Unable to patch ${path11.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
4230
5714
  `The existing ${registerFunctionName}() definition does not include ${AI_FEATURE_SERVER_GLOB}.`,
4231
5715
  "Restore the generated bootstrap shape or wire the AI feature loader manually before retrying."
4232
5716
  ].join(" "));
@@ -4238,8 +5722,8 @@ ${snippet}
4238
5722
  });
4239
5723
  }
4240
5724
  async function ensureAiFeaturePackageScripts(workspace) {
4241
- const packageJsonPath = path10.join(workspace.projectDir, "package.json");
4242
- const packageJson = JSON.parse(await fsp6.readFile(packageJsonPath, "utf8"));
5725
+ const packageJsonPath = path11.join(workspace.projectDir, "package.json");
5726
+ const packageJson = JSON.parse(await fsp7.readFile(packageJsonPath, "utf8"));
4243
5727
  const nextScripts = {
4244
5728
  ...packageJson.scripts ?? {},
4245
5729
  "sync-ai": packageJson.scripts?.["sync-ai"] ?? "tsx scripts/sync-ai-features.ts"
@@ -4258,7 +5742,7 @@ async function ensureAiFeaturePackageScripts(workspace) {
4258
5742
  }
4259
5743
  packageJson.scripts = nextScripts;
4260
5744
  packageJson.devDependencies = nextDevDependencies;
4261
- await fsp6.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
5745
+ await fsp7.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4262
5746
  `, "utf8");
4263
5747
  return {
4264
5748
  addedProjectToolsDependency,
@@ -4266,7 +5750,7 @@ async function ensureAiFeaturePackageScripts(workspace) {
4266
5750
  };
4267
5751
  }
4268
5752
  async function ensureAiFeatureSyncProjectAnchors(workspace) {
4269
- const syncProjectScriptPath = path10.join(workspace.projectDir, "scripts", "sync-project.ts");
5753
+ const syncProjectScriptPath = path11.join(workspace.projectDir, "scripts", "sync-project.ts");
4270
5754
  await patchFile(syncProjectScriptPath, (source) => {
4271
5755
  let nextSource = source;
4272
5756
  const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
@@ -4281,7 +5765,7 @@ async function ensureAiFeatureSyncProjectAnchors(workspace) {
4281
5765
  if (!nextSource.includes(syncAiConst)) {
4282
5766
  if (!nextSource.includes(syncRestConst)) {
4283
5767
  throw new Error([
4284
- `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
5768
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path11.basename(syncProjectScriptPath)}.`,
4285
5769
  "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
4286
5770
  "Restore the generated template or wire sync-ai manually before retrying."
4287
5771
  ].join(" "));
@@ -4292,7 +5776,7 @@ ${syncAiConst}`);
4292
5776
  if (!nextSource.includes("runSyncScript( syncAiScriptPath, options );")) {
4293
5777
  if (!syncRestBlockPattern.test(nextSource)) {
4294
5778
  throw new Error([
4295
- `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
5779
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path11.basename(syncProjectScriptPath)}.`,
4296
5780
  "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
4297
5781
  "Restore the generated template or wire sync-ai manually before retrying."
4298
5782
  ].join(" "));
@@ -4307,7 +5791,7 @@ ${syncAiBlock}`);
4307
5791
  function assertSyncRestAnchor2(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
4308
5792
  if (!nextSource.includes(target) && !hasAnchor) {
4309
5793
  throw new Error([
4310
- `ensureAiFeatureSyncRestAnchors could not patch ${path10.basename(syncRestScriptPath)}.`,
5794
+ `ensureAiFeatureSyncRestAnchors could not patch ${path11.basename(syncRestScriptPath)}.`,
4311
5795
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
4312
5796
  "Restore the generated template or add the AI feature wiring manually before retrying."
4313
5797
  ].join(" "));
@@ -4322,7 +5806,7 @@ function replaceRequiredSyncRestSource2(nextSource, target, anchor, replacement,
4322
5806
  return nextSource.replace(anchor, replacement);
4323
5807
  }
4324
5808
  async function ensureAiFeatureSyncRestAnchors(workspace) {
4325
- const syncRestScriptPath = path10.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5809
+ const syncRestScriptPath = path11.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4326
5810
  await patchFile(syncRestScriptPath, (source) => {
4327
5811
  let nextSource = source;
4328
5812
  const importAnchor = [
@@ -4451,9 +5935,6 @@ async function ensureAiFeatureSyncRestAnchors(workspace) {
4451
5935
  }
4452
5936
 
4453
5937
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
4454
- function quotePhpString4(value) {
4455
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
4456
- }
4457
5938
  function buildAiFeaturePhpSource(aiFeatureSlug, namespace, phpPrefix, textDomain) {
4458
5939
  const aiFeatureTitle = toTitleCase(aiFeatureSlug);
4459
5940
  const aiFeaturePhpId = aiFeatureSlug.replace(/-/g, "_");
@@ -4549,7 +6030,7 @@ if ( ! function_exists( '${buildPromptFunctionName}' ) ) {
4549
6030
  function ${buildPromptFunctionName}( array $payload ) {
4550
6031
  return sprintf(
4551
6032
  'You are helping with the %1$s AI workflow. Read the JSON request payload and return JSON that matches the provided schema. Request payload: %2$s',
4552
- ${quotePhpString4(aiFeatureTitle)},
6033
+ ${quotePhpString(aiFeatureTitle)},
4553
6034
  wp_json_encode( $payload )
4554
6035
  );
4555
6036
  }
@@ -4678,8 +6159,8 @@ if ( ! function_exists( '${adminNoticeFunctionName}' ) ) {
4678
6159
 
4679
6160
  $message = sprintf(
4680
6161
  /* translators: %s: AI feature name. */
4681
- __( 'The %s AI feature is optional and remains disabled until the WordPress AI Client is available with structured text generation support for the generated schema.', ${quotePhpString4(textDomain)} ),
4682
- ${quotePhpString4(aiFeatureTitle)}
6162
+ __( 'The %s AI feature is optional and remains disabled until the WordPress AI Client is available with structured text generation support for the generated schema.', ${quotePhpString(textDomain)} ),
6163
+ ${quotePhpString(aiFeatureTitle)}
4683
6164
  );
4684
6165
  printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $message ) );
4685
6166
  }
@@ -4814,7 +6295,7 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
4814
6295
  if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
4815
6296
  function ${registerRoutesFunctionName}() {
4816
6297
  register_rest_route(
4817
- ${quotePhpString4(namespace)},
6298
+ ${quotePhpString(namespace)},
4818
6299
  '/ai/${aiFeatureSlug}',
4819
6300
  array(
4820
6301
  array(
@@ -4842,18 +6323,18 @@ async function runAddAiFeatureCommand({
4842
6323
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
4843
6324
  const inventory = readWorkspaceInventory(workspace.projectDir);
4844
6325
  assertAiFeatureDoesNotExist(workspace.projectDir, aiFeatureSlug, inventory);
4845
- const blockConfigPath = path11.join(workspace.projectDir, "scripts", "block-config.ts");
6326
+ const blockConfigPath = path12.join(workspace.projectDir, "scripts", "block-config.ts");
4846
6327
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4847
- const packageJsonPath = path11.join(workspace.projectDir, "package.json");
4848
- const syncAiScriptPath = path11.join(workspace.projectDir, "scripts", "sync-ai-features.ts");
4849
- const syncProjectScriptPath = path11.join(workspace.projectDir, "scripts", "sync-project.ts");
4850
- const syncRestScriptPath = path11.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4851
- const aiFeatureDir = path11.join(workspace.projectDir, "src", "ai-features", aiFeatureSlug);
4852
- const typesFilePath = path11.join(aiFeatureDir, "api-types.ts");
4853
- const validatorsFilePath = path11.join(aiFeatureDir, "api-validators.ts");
4854
- const apiFilePath = path11.join(aiFeatureDir, "api.ts");
4855
- const dataFilePath = path11.join(aiFeatureDir, "data.ts");
4856
- const phpFilePath = path11.join(workspace.projectDir, "inc", "ai-features", `${aiFeatureSlug}.php`);
6328
+ const packageJsonPath = path12.join(workspace.projectDir, "package.json");
6329
+ const syncAiScriptPath = path12.join(workspace.projectDir, "scripts", "sync-ai-features.ts");
6330
+ const syncProjectScriptPath = path12.join(workspace.projectDir, "scripts", "sync-project.ts");
6331
+ const syncRestScriptPath = path12.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
6332
+ const aiFeatureDir = path12.join(workspace.projectDir, "src", "ai-features", aiFeatureSlug);
6333
+ const typesFilePath = path12.join(aiFeatureDir, "api-types.ts");
6334
+ const validatorsFilePath = path12.join(aiFeatureDir, "api-validators.ts");
6335
+ const apiFilePath = path12.join(aiFeatureDir, "api.ts");
6336
+ const dataFilePath = path12.join(aiFeatureDir, "data.ts");
6337
+ const phpFilePath = path12.join(workspace.projectDir, "inc", "ai-features", `${aiFeatureSlug}.php`);
4857
6338
  const mutationSnapshot = {
4858
6339
  fileSources: await snapshotWorkspaceFiles([
4859
6340
  blockConfigPath,
@@ -4867,23 +6348,23 @@ async function runAddAiFeatureCommand({
4867
6348
  targetPaths: [aiFeatureDir, phpFilePath, syncAiScriptPath]
4868
6349
  };
4869
6350
  try {
4870
- await fsp7.mkdir(aiFeatureDir, { recursive: true });
4871
- await fsp7.mkdir(path11.dirname(phpFilePath), { recursive: true });
6351
+ await fsp8.mkdir(aiFeatureDir, { recursive: true });
6352
+ await fsp8.mkdir(path12.dirname(phpFilePath), { recursive: true });
4872
6353
  await ensureAiFeatureBootstrapAnchors(workspace);
4873
6354
  await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
4874
6355
  const packageScriptChanges = await ensureAiFeaturePackageScripts(workspace);
4875
6356
  await ensureAiFeatureSyncProjectAnchors(workspace);
4876
6357
  await ensureAiFeatureSyncRestAnchors(workspace);
4877
- await fsp7.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
4878
- await fsp7.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
4879
- await fsp7.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
4880
- await fsp7.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
4881
- await fsp7.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
4882
- await fsp7.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
4883
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
6358
+ await fsp8.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
6359
+ await fsp8.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
6360
+ await fsp8.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
6361
+ await fsp8.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
6362
+ await fsp8.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
6363
+ await fsp8.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
6364
+ const pascalCase = toPascalCase(aiFeatureSlug);
4884
6365
  await syncAiFeatureRestArtifacts({
4885
6366
  clientFile: `src/ai-features/${aiFeatureSlug}/api-client.ts`,
4886
- outputDir: path11.join("src", "ai-features", aiFeatureSlug),
6367
+ outputDir: path12.join("src", "ai-features", aiFeatureSlug),
4887
6368
  projectDir: workspace.projectDir,
4888
6369
  typesFile: `src/ai-features/${aiFeatureSlug}/api-types.ts`,
4889
6370
  validatorsFile: `src/ai-features/${aiFeatureSlug}/api-validators.ts`,
@@ -4896,7 +6377,7 @@ async function runAddAiFeatureCommand({
4896
6377
  });
4897
6378
  await syncAiFeatureSchemaArtifact({
4898
6379
  aiSchemaFile: `src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`,
4899
- outputDir: path11.join("src", "ai-features", aiFeatureSlug),
6380
+ outputDir: path12.join("src", "ai-features", aiFeatureSlug),
4900
6381
  projectDir: workspace.projectDir
4901
6382
  });
4902
6383
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -4921,7 +6402,168 @@ async function runAddAiFeatureCommand({
4921
6402
 
4922
6403
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
4923
6404
  var VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
6405
+ var VARIATIONS_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceVariations\s*\}\s*from\s*["']\.\/variations["']\s*;?\s*$/mu;
4924
6406
  var VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
6407
+ var VARIATIONS_CALL_PATTERN = /registerWorkspaceVariations\s*\(\s*\)\s*;?/u;
6408
+ var BLOCK_STYLES_IMPORT_LINE = "import { registerWorkspaceBlockStyles } from './styles';";
6409
+ var BLOCK_STYLES_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceBlockStyles\s*\}\s*from\s*["']\.\/styles["']\s*;?\s*$/mu;
6410
+ var BLOCK_STYLES_CALL_LINE = "registerWorkspaceBlockStyles();";
6411
+ var BLOCK_STYLES_CALL_PATTERN = /registerWorkspaceBlockStyles\s*\(\s*\)\s*;?/u;
6412
+ var BLOCK_TRANSFORMS_IMPORT_LINE = "import { applyWorkspaceBlockTransforms } from './transforms';";
6413
+ var BLOCK_TRANSFORMS_IMPORT_PATTERN = /^\s*import\s*\{\s*applyWorkspaceBlockTransforms\s*\}\s*from\s*["']\.\/transforms["']\s*;?\s*$/mu;
6414
+ var BLOCK_TRANSFORMS_CALL_LINE = "applyWorkspaceBlockTransforms(registration.settings);";
6415
+ var BLOCK_TRANSFORMS_CALL_PATTERN = /applyWorkspaceBlockTransforms\s*\(\s*registration\s*\.\s*settings\s*\)\s*;?/u;
6416
+ var SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN = /registerScaffoldBlockType\s*\(\s*registration\s*\.\s*name\s*,\s*registration\s*\.\s*settings\s*\)\s*;?/u;
6417
+ var FULL_BLOCK_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/u;
6418
+ function maskSourceSegment(segment) {
6419
+ return segment.replace(/[^\n\r]/gu, " ");
6420
+ }
6421
+ function maskTypeScriptComments(source) {
6422
+ return source.replace(/\/\*[\s\S]*?\*\//gu, maskSourceSegment).replace(/\/\/[^\n\r]*/gu, maskSourceSegment);
6423
+ }
6424
+ function maskTypeScriptCommentsAndLiterals(source) {
6425
+ let maskedSource = "";
6426
+ let index = 0;
6427
+ while (index < source.length) {
6428
+ const current = source[index];
6429
+ const next = source[index + 1];
6430
+ if (current === "/" && next === "/") {
6431
+ const start = index;
6432
+ index += 2;
6433
+ while (index < source.length && source[index] !== `
6434
+ ` && source[index] !== "\r") {
6435
+ index += 1;
6436
+ }
6437
+ maskedSource += maskSourceSegment(source.slice(start, index));
6438
+ continue;
6439
+ }
6440
+ if (current === "/" && next === "*") {
6441
+ const start = index;
6442
+ index += 2;
6443
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
6444
+ index += 1;
6445
+ }
6446
+ index = Math.min(index + 2, source.length);
6447
+ maskedSource += maskSourceSegment(source.slice(start, index));
6448
+ continue;
6449
+ }
6450
+ if (current === "'" || current === '"' || current === "`") {
6451
+ const start = index;
6452
+ const quote = current;
6453
+ index += 1;
6454
+ while (index < source.length) {
6455
+ const char = source[index];
6456
+ if (char === "\\") {
6457
+ index += 2;
6458
+ continue;
6459
+ }
6460
+ index += 1;
6461
+ if (char === quote) {
6462
+ break;
6463
+ }
6464
+ }
6465
+ maskedSource += maskSourceSegment(source.slice(start, index));
6466
+ continue;
6467
+ }
6468
+ maskedSource += current;
6469
+ index += 1;
6470
+ }
6471
+ return maskedSource;
6472
+ }
6473
+ function hasExecutablePattern(source, pattern) {
6474
+ return pattern.test(maskTypeScriptCommentsAndLiterals(source));
6475
+ }
6476
+ function hasUncommentedPattern(source, pattern) {
6477
+ return pattern.test(maskTypeScriptComments(source));
6478
+ }
6479
+ function findExecutablePatternMatch(source, patterns) {
6480
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
6481
+ for (const pattern of patterns) {
6482
+ const match = pattern.exec(maskedSource);
6483
+ if (match && match.index !== undefined) {
6484
+ return {
6485
+ end: match.index + match[0].length,
6486
+ start: match.index
6487
+ };
6488
+ }
6489
+ }
6490
+ return;
6491
+ }
6492
+ function isIdentifierBoundary(source, index) {
6493
+ if (index < 0 || index >= source.length) {
6494
+ return true;
6495
+ }
6496
+ return !/[A-Za-z0-9_$]/u.test(source[index] ?? "");
6497
+ }
6498
+ function skipWhitespace(source, index) {
6499
+ let cursor = index;
6500
+ while (cursor < source.length && /\s/u.test(source[cursor] ?? "")) {
6501
+ cursor += 1;
6502
+ }
6503
+ return cursor;
6504
+ }
6505
+ function findMatchingDelimiterEnd(source, openIndex, openDelimiter, closeDelimiter) {
6506
+ let depth = 0;
6507
+ for (let index = openIndex;index < source.length; index += 1) {
6508
+ const char = source[index];
6509
+ if (char === openDelimiter) {
6510
+ depth += 1;
6511
+ continue;
6512
+ }
6513
+ if (char === closeDelimiter) {
6514
+ depth -= 1;
6515
+ if (depth === 0) {
6516
+ return index + 1;
6517
+ }
6518
+ }
6519
+ }
6520
+ return;
6521
+ }
6522
+ function findExecutableCallRange(source, callName) {
6523
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
6524
+ let searchIndex = 0;
6525
+ while (searchIndex < maskedSource.length) {
6526
+ const callNameIndex = maskedSource.indexOf(callName, searchIndex);
6527
+ if (callNameIndex === -1) {
6528
+ return;
6529
+ }
6530
+ const callNameEnd = callNameIndex + callName.length;
6531
+ if (!isIdentifierBoundary(maskedSource, callNameIndex - 1) || !isIdentifierBoundary(maskedSource, callNameEnd)) {
6532
+ searchIndex = callNameEnd;
6533
+ continue;
6534
+ }
6535
+ let cursor = skipWhitespace(maskedSource, callNameEnd);
6536
+ if (maskedSource[cursor] === "<") {
6537
+ const genericEnd = findMatchingDelimiterEnd(maskedSource, cursor, "<", ">");
6538
+ if (genericEnd === undefined) {
6539
+ searchIndex = callNameEnd;
6540
+ continue;
6541
+ }
6542
+ cursor = skipWhitespace(maskedSource, genericEnd);
6543
+ }
6544
+ if (maskedSource[cursor] !== "(") {
6545
+ searchIndex = callNameEnd;
6546
+ continue;
6547
+ }
6548
+ const callEnd = findMatchingDelimiterEnd(maskedSource, cursor, "(", ")");
6549
+ if (callEnd === undefined) {
6550
+ searchIndex = callNameEnd;
6551
+ continue;
6552
+ }
6553
+ let end = skipWhitespace(maskedSource, callEnd);
6554
+ if (maskedSource[end] === ";") {
6555
+ end += 1;
6556
+ }
6557
+ return {
6558
+ end,
6559
+ start: callNameIndex
6560
+ };
6561
+ }
6562
+ return;
6563
+ }
6564
+ function findBlockRegistrationCallRange(source) {
6565
+ return findExecutableCallRange(source, "registerScaffoldBlockType") ?? findExecutableCallRange(source, "registerBlockType");
6566
+ }
4925
6567
  function buildVariationConfigEntry(blockSlug, variationSlug) {
4926
6568
  return [
4927
6569
  "\t{",
@@ -4991,49 +6633,300 @@ export function registerWorkspaceVariations() {
4991
6633
  }
4992
6634
  `;
4993
6635
  }
6636
+ function buildWorkspaceConstName(prefix, slug) {
6637
+ return `workspace${prefix}_${toSnakeCase(slug)}`;
6638
+ }
6639
+ function buildBlockStyleConfigEntry(blockSlug, styleSlug) {
6640
+ return [
6641
+ "\t{",
6642
+ ` block: ${quoteTsString(blockSlug)},`,
6643
+ ` file: ${quoteTsString(`src/blocks/${blockSlug}/styles/${styleSlug}.ts`)},`,
6644
+ ` slug: ${quoteTsString(styleSlug)},`,
6645
+ "\t},"
6646
+ ].join(`
6647
+ `);
6648
+ }
6649
+ function buildBlockTransformConfigEntry(options) {
6650
+ return [
6651
+ "\t{",
6652
+ ` block: ${quoteTsString(options.blockSlug)},`,
6653
+ ` file: ${quoteTsString(`src/blocks/${options.blockSlug}/transforms/${options.transformSlug}.ts`)},`,
6654
+ ` from: ${quoteTsString(options.fromBlockName)},`,
6655
+ ` slug: ${quoteTsString(options.transformSlug)},`,
6656
+ ` to: ${quoteTsString(options.toBlockName)},`,
6657
+ "\t},"
6658
+ ].join(`
6659
+ `);
6660
+ }
6661
+ function getBlockStyleConstBindings(styleSlugs) {
6662
+ const seenConstNames = new Map;
6663
+ return styleSlugs.map((styleSlug) => {
6664
+ const constName = buildWorkspaceConstName("BlockStyle", styleSlug);
6665
+ const previousSlug = seenConstNames.get(constName);
6666
+ if (previousSlug && previousSlug !== styleSlug) {
6667
+ throw new Error(`Style slugs "${previousSlug}" and "${styleSlug}" generate the same registry identifier "${constName}". Rename one of the styles.`);
6668
+ }
6669
+ seenConstNames.set(constName, styleSlug);
6670
+ return { constName, styleSlug };
6671
+ });
6672
+ }
6673
+ function getBlockTransformConstBindings(transformSlugs) {
6674
+ const seenConstNames = new Map;
6675
+ return transformSlugs.map((transformSlug) => {
6676
+ const constName = buildWorkspaceConstName("BlockTransform", transformSlug);
6677
+ const previousSlug = seenConstNames.get(constName);
6678
+ if (previousSlug && previousSlug !== transformSlug) {
6679
+ throw new Error(`Transform slugs "${previousSlug}" and "${transformSlug}" generate the same registry identifier "${constName}". Rename one of the transforms.`);
6680
+ }
6681
+ seenConstNames.set(constName, transformSlug);
6682
+ return { constName, transformSlug };
6683
+ });
6684
+ }
6685
+ function buildBlockStyleSource(styleSlug, textDomain) {
6686
+ const styleTitle = toTitleCase(styleSlug);
6687
+ const styleConstName = buildWorkspaceConstName("BlockStyle", styleSlug);
6688
+ return `import { __ } from '@wordpress/i18n';
6689
+
6690
+ export const ${styleConstName} = {
6691
+ name: ${quoteTsString(styleSlug)},
6692
+ label: __( ${quoteTsString(styleTitle)}, ${quoteTsString(textDomain)} ),
6693
+ } as const;
6694
+ `;
6695
+ }
6696
+ function buildBlockStyleIndexSource(styleSlugs) {
6697
+ const styleBindings = getBlockStyleConstBindings(styleSlugs);
6698
+ const importLines = styleBindings.map(({ constName, styleSlug }) => `import { ${constName} } from './${styleSlug}';`).join(`
6699
+ `);
6700
+ const styleConstNames = styleBindings.map(({ constName }) => constName).join(`,
6701
+ `);
6702
+ return `import { registerBlockStyle } from '@wordpress/blocks';
6703
+ import metadata from '../block.json';
6704
+ ${importLines ? `
6705
+ ${importLines}` : ""}
6706
+
6707
+ const WORKSPACE_BLOCK_STYLES = [
6708
+ ${styleConstNames}
6709
+ // wp-typia add style entries
6710
+ ] as const;
6711
+
6712
+ export function registerWorkspaceBlockStyles() {
6713
+ for (const style of WORKSPACE_BLOCK_STYLES) {
6714
+ registerBlockStyle(metadata.name, style);
6715
+ }
6716
+ }
6717
+ `;
6718
+ }
6719
+ function buildBlockTransformSource(options) {
6720
+ const transformTitle = toTitleCase(options.transformSlug);
6721
+ const transformConstName = buildWorkspaceConstName("BlockTransform", options.transformSlug);
6722
+ return `import { createBlock } from '@wordpress/blocks';
6723
+ import { __ } from '@wordpress/i18n';
6724
+ import metadata from '../block.json';
6725
+
6726
+ type TransformAttributes = Record<string, unknown>;
6727
+ type TransformInnerBlock = ReturnType<typeof createBlock>;
6728
+
6729
+ function mapTransformAttributes(attributes: TransformAttributes): TransformAttributes {
6730
+ const content = attributes.content;
6731
+
6732
+ return typeof content === 'string' ? { content } : {};
6733
+ }
6734
+
6735
+ export const ${transformConstName} = {
6736
+ type: 'block',
6737
+ blocks: [${quoteTsString(options.fromBlockName)}],
6738
+ title: __( ${quoteTsString(transformTitle)}, ${quoteTsString(options.textDomain)} ),
6739
+ transform: (
6740
+ attributes: TransformAttributes,
6741
+ innerBlocks: TransformInnerBlock[] = [],
6742
+ ) => createBlock(metadata.name, mapTransformAttributes(attributes), innerBlocks),
6743
+ } as const;
6744
+ `;
6745
+ }
6746
+ function buildBlockTransformIndexSource(transformSlugs) {
6747
+ const transformBindings = getBlockTransformConstBindings(transformSlugs);
6748
+ const importLines = transformBindings.map(({ constName, transformSlug }) => `import { ${constName} } from './${transformSlug}';`).join(`
6749
+ `);
6750
+ const transformConstNames = transformBindings.map(({ constName }) => constName).join(`,
6751
+ `);
6752
+ return `${importLines ? `${importLines}
6753
+
6754
+ ` : ""}type BlockSettingsWithTransforms = {
6755
+ transforms?: {
6756
+ from?: unknown[];
6757
+ to?: unknown[];
6758
+ };
6759
+ };
6760
+
6761
+ const WORKSPACE_BLOCK_TRANSFORMS = [
6762
+ ${transformConstNames}
6763
+ // wp-typia add transform entries
6764
+ ] as const;
6765
+
6766
+ export function applyWorkspaceBlockTransforms(settings: BlockSettingsWithTransforms) {
6767
+ const transforms = settings.transforms ?? {};
6768
+
6769
+ settings.transforms = {
6770
+ ...transforms,
6771
+ from: [...(transforms.from ?? []), ...WORKSPACE_BLOCK_TRANSFORMS],
6772
+ };
6773
+ }
6774
+ `;
6775
+ }
4994
6776
  async function ensureVariationRegistrationHook(blockIndexPath) {
4995
6777
  await patchFile(blockIndexPath, (source) => {
4996
6778
  let nextSource = source;
4997
- if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
6779
+ if (!hasUncommentedPattern(nextSource, VARIATIONS_IMPORT_PATTERN)) {
4998
6780
  nextSource = `${VARIATIONS_IMPORT_LINE}
4999
6781
  ${nextSource}`;
5000
6782
  }
5001
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
5002
- const callInsertionPatterns = [
5003
- /(registerBlockType<[\s\S]*?\);\s*)/u,
5004
- /(registerBlockType\([\s\S]*?\);\s*)/u
5005
- ];
5006
- let inserted = false;
5007
- for (const pattern of callInsertionPatterns) {
5008
- const candidate = nextSource.replace(pattern, (match) => `${match}
6783
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
6784
+ const callRange = findBlockRegistrationCallRange(nextSource);
6785
+ if (callRange) {
6786
+ nextSource = [
6787
+ nextSource.slice(0, callRange.end),
6788
+ `
5009
6789
  ${VARIATIONS_CALL_LINE}
5010
- `);
5011
- if (candidate !== nextSource) {
5012
- nextSource = candidate;
5013
- inserted = true;
5014
- break;
5015
- }
5016
- }
5017
- if (!inserted) {
6790
+ `,
6791
+ nextSource.slice(callRange.end)
6792
+ ].join("");
6793
+ } else {
5018
6794
  nextSource = `${nextSource.trimEnd()}
5019
6795
 
5020
6796
  ${VARIATIONS_CALL_LINE}
5021
6797
  `;
5022
6798
  }
5023
6799
  }
5024
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
5025
- throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path12.basename(blockIndexPath)}.`);
6800
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
6801
+ throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path13.basename(blockIndexPath)}.`);
6802
+ }
6803
+ return nextSource;
6804
+ });
6805
+ }
6806
+ async function ensureBlockStyleRegistrationHook(blockIndexPath) {
6807
+ await patchFile(blockIndexPath, (source) => {
6808
+ let nextSource = source;
6809
+ if (!hasUncommentedPattern(nextSource, BLOCK_STYLES_IMPORT_PATTERN)) {
6810
+ nextSource = `${BLOCK_STYLES_IMPORT_LINE}
6811
+ ${nextSource}`;
6812
+ }
6813
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
6814
+ const callRange = findBlockRegistrationCallRange(nextSource);
6815
+ if (callRange) {
6816
+ nextSource = [
6817
+ nextSource.slice(0, callRange.end),
6818
+ `
6819
+ ${BLOCK_STYLES_CALL_LINE}
6820
+ `,
6821
+ nextSource.slice(callRange.end)
6822
+ ].join("");
6823
+ } else {
6824
+ nextSource = `${nextSource.trimEnd()}
6825
+
6826
+ ${BLOCK_STYLES_CALL_LINE}
6827
+ `;
6828
+ }
6829
+ }
6830
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
6831
+ throw new Error(`Unable to inject ${BLOCK_STYLES_CALL_LINE} into ${path13.basename(blockIndexPath)}.`);
6832
+ }
6833
+ return nextSource;
6834
+ });
6835
+ }
6836
+ async function ensureBlockTransformRegistrationHook(blockIndexPath) {
6837
+ await patchFile(blockIndexPath, (source) => {
6838
+ let nextSource = source;
6839
+ if (!hasUncommentedPattern(nextSource, BLOCK_TRANSFORMS_IMPORT_PATTERN)) {
6840
+ nextSource = `${BLOCK_TRANSFORMS_IMPORT_LINE}
6841
+ ${nextSource}`;
6842
+ }
6843
+ if (!hasExecutablePattern(nextSource, BLOCK_TRANSFORMS_CALL_PATTERN)) {
6844
+ const callRange = findExecutablePatternMatch(nextSource, [
6845
+ SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN
6846
+ ]);
6847
+ if (!callRange) {
6848
+ throw new Error(`Unable to inject ${BLOCK_TRANSFORMS_CALL_LINE} into ${path13.basename(blockIndexPath)} because it does not expose a scaffold registration settings object.`);
6849
+ }
6850
+ nextSource = [
6851
+ nextSource.slice(0, callRange.start),
6852
+ `${BLOCK_TRANSFORMS_CALL_LINE}
6853
+ `,
6854
+ nextSource.slice(callRange.start)
6855
+ ].join("");
5026
6856
  }
5027
6857
  return nextSource;
5028
6858
  });
5029
6859
  }
5030
6860
  async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
5031
- const variationsDir = path12.join(projectDir, "src", "blocks", blockSlug, "variations");
5032
- const variationsIndexPath = path12.join(variationsDir, "index.ts");
5033
- await fsp8.mkdir(variationsDir, { recursive: true });
5034
- const existingVariationSlugs = fs5.readdirSync(variationsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
6861
+ const variationsDir = path13.join(projectDir, "src", "blocks", blockSlug, "variations");
6862
+ const variationsIndexPath = path13.join(variationsDir, "index.ts");
6863
+ await fsp9.mkdir(variationsDir, { recursive: true });
6864
+ const existingVariationSlugs = fs6.readdirSync(variationsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
5035
6865
  const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
5036
- await fsp8.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
6866
+ await fsp9.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
6867
+ }
6868
+ async function writeBlockStyleRegistry(projectDir, blockSlug, styleSlug) {
6869
+ const stylesDir = path13.join(projectDir, "src", "blocks", blockSlug, "styles");
6870
+ const stylesIndexPath = path13.join(stylesDir, "index.ts");
6871
+ await fsp9.mkdir(stylesDir, { recursive: true });
6872
+ const existingStyleSlugs = fs6.readdirSync(stylesDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
6873
+ const nextStyleSlugs = Array.from(new Set([...existingStyleSlugs, styleSlug])).sort();
6874
+ await fsp9.writeFile(stylesIndexPath, buildBlockStyleIndexSource(nextStyleSlugs), "utf8");
6875
+ }
6876
+ async function writeBlockTransformRegistry(projectDir, blockSlug, transformSlug) {
6877
+ const transformsDir = path13.join(projectDir, "src", "blocks", blockSlug, "transforms");
6878
+ const transformsIndexPath = path13.join(transformsDir, "index.ts");
6879
+ await fsp9.mkdir(transformsDir, { recursive: true });
6880
+ const existingTransformSlugs = fs6.readdirSync(transformsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
6881
+ const nextTransformSlugs = Array.from(new Set([...existingTransformSlugs, transformSlug])).sort();
6882
+ await fsp9.writeFile(transformsIndexPath, buildBlockTransformIndexSource(nextTransformSlugs), "utf8");
6883
+ }
6884
+ function assertBlockStyleDoesNotExist(projectDir, blockSlug, styleSlug, inventory) {
6885
+ const stylePath = path13.join(projectDir, "src", "blocks", blockSlug, "styles", `${styleSlug}.ts`);
6886
+ if (fs6.existsSync(stylePath)) {
6887
+ throw new Error(`A block style already exists at ${path13.relative(projectDir, stylePath)}. Choose a different name.`);
6888
+ }
6889
+ if (inventory.blockStyles.some((entry) => entry.block === blockSlug && entry.slug === styleSlug)) {
6890
+ throw new Error(`A block style inventory entry already exists for ${blockSlug}/${styleSlug}. Choose a different name.`);
6891
+ }
6892
+ }
6893
+ function assertBlockTransformDoesNotExist(projectDir, blockSlug, transformSlug, inventory) {
6894
+ const transformPath = path13.join(projectDir, "src", "blocks", blockSlug, "transforms", `${transformSlug}.ts`);
6895
+ if (fs6.existsSync(transformPath)) {
6896
+ throw new Error(`A block transform already exists at ${path13.relative(projectDir, transformPath)}. Choose a different name.`);
6897
+ }
6898
+ if (inventory.blockTransforms.some((entry) => entry.block === blockSlug && entry.slug === transformSlug)) {
6899
+ throw new Error(`A block transform inventory entry already exists for ${blockSlug}/${transformSlug}. Choose a different name.`);
6900
+ }
6901
+ }
6902
+ function assertFullBlockName(blockName, flagName) {
6903
+ const trimmed = blockName.trim();
6904
+ if (!trimmed) {
6905
+ throw new Error(`\`${flagName}\` requires a block name.`);
6906
+ }
6907
+ if (!FULL_BLOCK_NAME_PATTERN.test(trimmed)) {
6908
+ throw new Error(`\`${flagName}\` must use <namespace/block-slug> format.`);
6909
+ }
6910
+ return trimmed;
6911
+ }
6912
+ function resolveWorkspaceTargetBlockName(blockName, namespace, flagName) {
6913
+ const trimmed = blockName.trim();
6914
+ if (!trimmed) {
6915
+ throw new Error(`\`${flagName}\` requires <block-slug|namespace/block-slug>.`);
6916
+ }
6917
+ const blockNameSegments = trimmed.split("/");
6918
+ if (blockNameSegments.length > 2 || blockNameSegments.some((segment) => segment.trim() === "")) {
6919
+ throw new Error(`\`${flagName}\` must use <block-slug|namespace/block-slug> format.`);
6920
+ }
6921
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2 ? blockNameSegments : [undefined, blockNameSegments[0]];
6922
+ if (maybeNamespace && maybeNamespace !== namespace) {
6923
+ throw new Error(`\`${flagName}\` references namespace "${maybeNamespace}". Expected "${namespace}".`);
6924
+ }
6925
+ const blockSlug = normalizeBlockSlug(maybeSlug ?? "");
6926
+ return {
6927
+ blockName: `${namespace}/${blockSlug}`,
6928
+ blockSlug
6929
+ };
5037
6930
  }
5038
6931
  async function runAddVariationCommand({
5039
6932
  blockName,
@@ -5046,11 +6939,12 @@ async function runAddVariationCommand({
5046
6939
  const inventory = readWorkspaceInventory(workspace.projectDir);
5047
6940
  resolveWorkspaceBlock(inventory, blockSlug);
5048
6941
  assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
5049
- const blockConfigPath = path12.join(workspace.projectDir, "scripts", "block-config.ts");
5050
- const blockIndexPath = path12.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
5051
- const variationsDir = path12.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
5052
- const variationFilePath = path12.join(variationsDir, `${variationSlug}.ts`);
5053
- const variationsIndexPath = path12.join(variationsDir, "index.ts");
6942
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
6943
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
6944
+ const variationsDir = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
6945
+ const variationFilePath = path13.join(variationsDir, `${variationSlug}.ts`);
6946
+ const variationsIndexPath = path13.join(variationsDir, "index.ts");
6947
+ const shouldRemoveVariationsDirOnRollback = !fs6.existsSync(variationsDir);
5054
6948
  const mutationSnapshot = {
5055
6949
  fileSources: await snapshotWorkspaceFiles([
5056
6950
  blockConfigPath,
@@ -5058,11 +6952,14 @@ async function runAddVariationCommand({
5058
6952
  variationsIndexPath
5059
6953
  ]),
5060
6954
  snapshotDirs: [],
5061
- targetPaths: [variationFilePath]
6955
+ targetPaths: [
6956
+ variationFilePath,
6957
+ ...shouldRemoveVariationsDirOnRollback ? [variationsDir] : []
6958
+ ]
5062
6959
  };
5063
6960
  try {
5064
- await fsp8.mkdir(variationsDir, { recursive: true });
5065
- await fsp8.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
6961
+ await fsp9.mkdir(variationsDir, { recursive: true });
6962
+ await fsp9.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
5066
6963
  await writeVariationRegistry(workspace.projectDir, blockSlug, variationSlug);
5067
6964
  await ensureVariationRegistrationHook(blockIndexPath);
5068
6965
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -5078,6 +6975,115 @@ async function runAddVariationCommand({
5078
6975
  throw error;
5079
6976
  }
5080
6977
  }
6978
+ async function runAddBlockStyleCommand({
6979
+ blockName,
6980
+ cwd = process.cwd(),
6981
+ styleName
6982
+ }) {
6983
+ const workspace = resolveWorkspaceProject(cwd);
6984
+ const blockSlug = normalizeBlockSlug(blockName);
6985
+ const styleSlug = assertValidGeneratedSlug("Style name", normalizeBlockSlug(styleName), "wp-typia add style <name> --block <block-slug>");
6986
+ const inventory = readWorkspaceInventory(workspace.projectDir);
6987
+ resolveWorkspaceBlock(inventory, blockSlug);
6988
+ assertBlockStyleDoesNotExist(workspace.projectDir, blockSlug, styleSlug, inventory);
6989
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
6990
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
6991
+ const stylesDir = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "styles");
6992
+ const styleFilePath = path13.join(stylesDir, `${styleSlug}.ts`);
6993
+ const stylesIndexPath = path13.join(stylesDir, "index.ts");
6994
+ const shouldRemoveStylesDirOnRollback = !fs6.existsSync(stylesDir);
6995
+ const mutationSnapshot = {
6996
+ fileSources: await snapshotWorkspaceFiles([
6997
+ blockConfigPath,
6998
+ blockIndexPath,
6999
+ stylesIndexPath
7000
+ ]),
7001
+ snapshotDirs: [],
7002
+ targetPaths: [
7003
+ styleFilePath,
7004
+ ...shouldRemoveStylesDirOnRollback ? [stylesDir] : []
7005
+ ]
7006
+ };
7007
+ try {
7008
+ await fsp9.mkdir(stylesDir, { recursive: true });
7009
+ await fsp9.writeFile(styleFilePath, buildBlockStyleSource(styleSlug, workspace.workspace.textDomain), "utf8");
7010
+ await writeBlockStyleRegistry(workspace.projectDir, blockSlug, styleSlug);
7011
+ await ensureBlockStyleRegistrationHook(blockIndexPath);
7012
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
7013
+ blockStyleEntries: [buildBlockStyleConfigEntry(blockSlug, styleSlug)]
7014
+ });
7015
+ return {
7016
+ blockSlug,
7017
+ projectDir: workspace.projectDir,
7018
+ styleSlug
7019
+ };
7020
+ } catch (error) {
7021
+ await rollbackWorkspaceMutation(mutationSnapshot);
7022
+ throw error;
7023
+ }
7024
+ }
7025
+ async function runAddBlockTransformCommand({
7026
+ cwd = process.cwd(),
7027
+ fromBlockName,
7028
+ toBlockName,
7029
+ transformName
7030
+ }) {
7031
+ const workspace = resolveWorkspaceProject(cwd);
7032
+ const transformSlug = assertValidGeneratedSlug("Transform name", normalizeBlockSlug(transformName), "wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug>");
7033
+ const resolvedFromBlockName = assertFullBlockName(fromBlockName, "--from");
7034
+ const target = resolveWorkspaceTargetBlockName(toBlockName, workspace.workspace.namespace, "--to");
7035
+ const inventory = readWorkspaceInventory(workspace.projectDir);
7036
+ resolveWorkspaceBlock(inventory, target.blockSlug);
7037
+ assertBlockTransformDoesNotExist(workspace.projectDir, target.blockSlug, transformSlug, inventory);
7038
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
7039
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", target.blockSlug, "index.tsx");
7040
+ const transformsDir = path13.join(workspace.projectDir, "src", "blocks", target.blockSlug, "transforms");
7041
+ const transformFilePath = path13.join(transformsDir, `${transformSlug}.ts`);
7042
+ const transformsIndexPath = path13.join(transformsDir, "index.ts");
7043
+ const shouldRemoveTransformsDirOnRollback = !fs6.existsSync(transformsDir);
7044
+ const mutationSnapshot = {
7045
+ fileSources: await snapshotWorkspaceFiles([
7046
+ blockConfigPath,
7047
+ blockIndexPath,
7048
+ transformsIndexPath
7049
+ ]),
7050
+ snapshotDirs: [],
7051
+ targetPaths: [
7052
+ transformFilePath,
7053
+ ...shouldRemoveTransformsDirOnRollback ? [transformsDir] : []
7054
+ ]
7055
+ };
7056
+ try {
7057
+ await fsp9.mkdir(transformsDir, { recursive: true });
7058
+ await fsp9.writeFile(transformFilePath, buildBlockTransformSource({
7059
+ fromBlockName: resolvedFromBlockName,
7060
+ textDomain: workspace.workspace.textDomain,
7061
+ transformSlug
7062
+ }), "utf8");
7063
+ await writeBlockTransformRegistry(workspace.projectDir, target.blockSlug, transformSlug);
7064
+ await ensureBlockTransformRegistrationHook(blockIndexPath);
7065
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
7066
+ blockTransformEntries: [
7067
+ buildBlockTransformConfigEntry({
7068
+ blockSlug: target.blockSlug,
7069
+ fromBlockName: resolvedFromBlockName,
7070
+ toBlockName: target.blockName,
7071
+ transformSlug
7072
+ })
7073
+ ]
7074
+ });
7075
+ return {
7076
+ blockSlug: target.blockSlug,
7077
+ fromBlockName: resolvedFromBlockName,
7078
+ projectDir: workspace.projectDir,
7079
+ toBlockName: target.blockName,
7080
+ transformSlug
7081
+ };
7082
+ } catch (error) {
7083
+ await rollbackWorkspaceMutation(mutationSnapshot);
7084
+ throw error;
7085
+ }
7086
+ }
5081
7087
  async function runAddHookedBlockCommand({
5082
7088
  anchorBlockName,
5083
7089
  blockName,
@@ -5095,7 +7101,7 @@ async function runAddHookedBlockCommand({
5095
7101
  throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
5096
7102
  }
5097
7103
  const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
5098
- const blockJsonRelativePath = path12.relative(workspace.projectDir, blockJsonPath);
7104
+ const blockJsonRelativePath = path13.relative(workspace.projectDir, blockJsonPath);
5099
7105
  const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
5100
7106
  if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
5101
7107
  throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
@@ -5107,7 +7113,7 @@ async function runAddHookedBlockCommand({
5107
7113
  };
5108
7114
  try {
5109
7115
  blockHooks[resolvedAnchorBlockName] = resolvedPosition;
5110
- await fsp8.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
7116
+ await fsp9.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
5111
7117
  return {
5112
7118
  anchorBlockName: resolvedAnchorBlockName,
5113
7119
  blockSlug,
@@ -5126,9 +7132,12 @@ export {
5126
7132
  runAddPatternCommand,
5127
7133
  runAddHookedBlockCommand,
5128
7134
  runAddEditorPluginCommand,
7135
+ runAddBlockTransformCommand,
7136
+ runAddBlockStyleCommand,
5129
7137
  runAddBlockCommand,
5130
7138
  runAddBindingSourceCommand,
5131
7139
  runAddAiFeatureCommand,
7140
+ runAddAdminViewCommand,
5132
7141
  runAddAbilityCommand,
5133
7142
  getWorkspaceBlockSelectOptions,
5134
7143
  formatAddHelpText,
@@ -5137,4 +7146,4 @@ export {
5137
7146
  ADD_BLOCK_TEMPLATE_IDS
5138
7147
  };
5139
7148
 
5140
- //# debugId=497A4F5F67BAF4EF64756E2164756E21
7149
+ //# debugId=8932DC6C8A5BAD5564756E2164756E21