wp-typia 0.20.4 → 0.21.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,13 @@ import {
20
20
  scaffoldProject,
21
21
  syncPersistenceRestArtifacts,
22
22
  updatePluginHeaderCompatibility
23
- } from "./cli-c5021kqy.js";
23
+ } from "./cli-pav309dt.js";
24
24
  import {
25
25
  getPackageVersions
26
- } from "./cli-tesygdnr.js";
26
+ } from "./cli-syg9qpxw.js";
27
27
  import {
28
28
  snapshotProjectVersion
29
- } from "./cli-2rev5hqm.js";
29
+ } from "./cli-jfj54qej.js";
30
30
  import {
31
31
  ensureMigrationDirectories,
32
32
  parseMigrationConfig,
@@ -39,6 +39,7 @@ import {
39
39
  } from "./cli-sj5mtyzj.js";
40
40
  import"./cli-10pe4mf8.js";
41
41
  import {
42
+ PROJECT_TOOLS_PACKAGE_ROOT,
42
43
  SHARED_WORKSPACE_TEMPLATE_ROOT
43
44
  } from "./cli-tke8twkn.js";
44
45
  import {
@@ -47,6 +48,7 @@ import {
47
48
  EDITOR_PLUGIN_SLOT_IDS,
48
49
  appendWorkspaceInventoryEntries,
49
50
  assertAbilityDoesNotExist,
51
+ assertAdminViewDoesNotExist,
50
52
  assertAiFeatureDoesNotExist,
51
53
  assertBindingSourceDoesNotExist,
52
54
  assertEditorPluginDoesNotExist,
@@ -59,28 +61,37 @@ import {
59
61
  assertValidRestResourceMethods,
60
62
  assertVariationDoesNotExist,
61
63
  buildWorkspacePhpPrefix,
64
+ escapeRegex,
65
+ findPhpFunctionRange,
62
66
  formatAddHelpText,
63
67
  getMutableBlockHooks,
64
68
  getWorkspaceBlockSelectOptions,
65
69
  getWorkspaceBootstrapPath,
70
+ hasPhpFunctionDefinition,
66
71
  isAddBlockTemplateId,
67
72
  normalizeBlockSlug,
68
73
  patchFile,
74
+ quotePhpString,
69
75
  quoteTsString,
70
76
  readOptionalFile,
71
77
  readWorkspaceBlockJson,
72
78
  readWorkspaceInventory,
79
+ replacePhpFunctionDefinition,
80
+ require_typescript,
73
81
  resolveNonEmptyNormalizedBlockSlug,
74
82
  resolveRestResourceNamespace,
75
83
  resolveWorkspaceBlock,
76
84
  rollbackWorkspaceMutation,
77
85
  snapshotWorkspaceFiles,
78
86
  toKebabCase,
87
+ toPascalCase,
88
+ toSnakeCase,
79
89
  toTitleCase
80
- } from "./cli-3w3qxq9w.js";
90
+ } from "./cli-hx88xwr4.js";
81
91
  import {
82
92
  createManagedTempRoot
83
93
  } from "./cli-t73q5aqz.js";
94
+ import"./cli-p95wr1q8.js";
84
95
  import {
85
96
  resolveWorkspaceProject
86
97
  } from "./cli-pd5pqgre.js";
@@ -641,7 +652,7 @@ async function runAddBlockCommand({
641
652
  throw new Error("`wp-typia add block --template query-loop` is not supported. Query Loop is a create-time `core/query` variation scaffold, so use `wp-typia create <project-dir> --template query-loop` instead.");
642
653
  }
643
654
  if (!isAddBlockTemplateId(templateId)) {
644
- throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
655
+ throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}. Run \`wp-typia templates list\` to inspect available templates.`);
645
656
  }
646
657
  const resolvedTemplateId = templateId;
647
658
  assertPersistenceFlagsAllowed(resolvedTemplateId, {
@@ -759,74 +770,894 @@ async function runAddBlockCommand({
759
770
  }
760
771
  }
761
772
  // ../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";
773
+ import fs6 from "fs";
774
+ import { promises as fsp9 } from "fs";
775
+ import path13 from "path";
765
776
 
766
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace-assets.ts
777
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-admin-view.ts
767
778
  import fs3 from "fs";
768
779
  import { promises as fsp3 } from "fs";
780
+ import { createRequire } from "module";
769
781
  import path4 from "path";
770
- var PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
771
- var BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
772
- var BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
773
- var BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
774
- var EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
775
- var EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
776
- var EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
777
- var EDITOR_PLUGIN_EDITOR_STYLE_RTL = "build/editor-plugins/style-index-rtl.css";
778
- function escapeRegex(value) {
779
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
782
+ var ADMIN_VIEW_SOURCE_KIND = "rest-resource";
783
+ var ADMIN_VIEWS_SCRIPT = "build/admin-views/index.js";
784
+ var ADMIN_VIEWS_ASSET = "build/admin-views/index.asset.php";
785
+ var ADMIN_VIEWS_STYLE = "build/admin-views/style-index.css";
786
+ var ADMIN_VIEWS_STYLE_RTL = "build/admin-views/style-index-rtl.css";
787
+ var ADMIN_VIEWS_PHP_GLOB = "/inc/admin-views/*.php";
788
+ var DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = "^0.1.0";
789
+ var DEFAULT_WORDPRESS_DATAVIEWS_VERSION = "^14.1.0";
790
+ var require2 = createRequire(import.meta.url);
791
+ function toCamelCase(input) {
792
+ const pascalCase = toPascalCase(input);
793
+ return `${pascalCase.charAt(0).toLowerCase()}${pascalCase.slice(1)}`;
794
+ }
795
+ function normalizeVersionRange(value, fallback) {
796
+ const trimmed = value?.trim();
797
+ if (!trimmed || trimmed.startsWith("workspace:")) {
798
+ return fallback;
799
+ }
800
+ return /^[~^<>=]/u.test(trimmed) ? trimmed : `^${trimmed}`;
801
+ }
802
+ function readPackageManifestVersion(packageJsonPath) {
803
+ try {
804
+ const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf8"));
805
+ return packageJson.version;
806
+ } catch {
807
+ return;
808
+ }
780
809
  }
781
- function quotePhpString(value) {
782
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
810
+ function detectJsonIndent(source) {
811
+ const indentMatch = /\n([ \t]+)"/u.exec(source);
812
+ return indentMatch?.[1] ?? 2;
783
813
  }
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;
814
+ function resolvePackageVersionRange(packageName, fallback, workspacePackageDirName) {
815
+ if (workspacePackageDirName) {
816
+ const workspaceVersion = readPackageManifestVersion(path4.join(PROJECT_TOOLS_PACKAGE_ROOT, "..", workspacePackageDirName, "package.json"));
817
+ if (workspaceVersion) {
818
+ return normalizeVersionRange(workspaceVersion, fallback);
819
+ }
789
820
  }
790
- const functionStart = signatureMatch.index;
791
- const openBraceIndex = source.indexOf("{", functionStart);
792
- if (openBraceIndex === -1) {
793
- return null;
821
+ try {
822
+ return normalizeVersionRange(readPackageManifestVersion(require2.resolve(`${packageName}/package.json`)), fallback);
823
+ } catch {
824
+ return fallback;
794
825
  }
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;
826
+ }
827
+ function getAdminViewRelativeModuleSpecifier(adminViewSlug, workspaceFile) {
828
+ const adminViewDir = `src/admin-views/${adminViewSlug}`;
829
+ const normalizedFile = workspaceFile.replace(/\\/gu, "/");
830
+ const modulePath = normalizedFile.replace(/\.[cm]?[jt]sx?$/u, "");
831
+ const relativeModulePath = path4.posix.relative(adminViewDir, modulePath);
832
+ return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
833
+ }
834
+ function parseAdminViewSource(source) {
835
+ const trimmed = source?.trim();
836
+ if (!trimmed) {
837
+ return;
838
+ }
839
+ const [kind, slug, extra] = trimmed.split(":");
840
+ if (kind !== ADMIN_VIEW_SOURCE_KIND || !slug || extra !== undefined) {
841
+ throw new Error("Admin view source must use `rest-resource:<slug>` for now.");
842
+ }
843
+ return {
844
+ kind,
845
+ slug: assertValidGeneratedSlug("Admin view source slug", normalizeBlockSlug(slug), "wp-typia add admin-view <name> --source rest-resource:<slug>")
846
+ };
847
+ }
848
+ function resolveRestResourceSource(restResources, source) {
849
+ if (!source) {
850
+ return;
851
+ }
852
+ const restResource = restResources.find((entry) => entry.slug === source.slug);
853
+ if (!restResource) {
854
+ throw new Error(`Unknown REST resource source "${source.slug}". Choose one of: ${restResources.map((entry) => entry.slug).join(", ") || "<none>"}.`);
855
+ }
856
+ if (!restResource.methods.includes("list")) {
857
+ throw new Error(`REST resource source "${source.slug}" must include the list method for DataViews pagination.`);
858
+ }
859
+ return restResource;
860
+ }
861
+ function buildAdminViewConfigEntry(adminViewSlug, source) {
862
+ return [
863
+ "\t{",
864
+ ` file: ${quoteTsString(`src/admin-views/${adminViewSlug}/index.tsx`)},`,
865
+ ` phpFile: ${quoteTsString(`inc/admin-views/${adminViewSlug}.php`)},`,
866
+ ` slug: ${quoteTsString(adminViewSlug)},`,
867
+ source ? ` source: ${quoteTsString(`${source.kind}:${source.slug}`)},` : null,
868
+ "\t},"
869
+ ].filter((line) => typeof line === "string").join(`
870
+ `);
871
+ }
872
+ function buildAdminViewRegistrySource(adminViewSlugs) {
873
+ const importLines = adminViewSlugs.map((adminViewSlug) => `import './${adminViewSlug}';`).join(`
874
+ `);
875
+ return `${importLines}${importLines ? `
876
+
877
+ ` : ""}// wp-typia add admin-view entries
878
+ `;
879
+ }
880
+ function buildAdminViewTypesSource(adminViewSlug, restResource) {
881
+ const pascalName = toPascalCase(adminViewSlug);
882
+ const itemTypeName = `${pascalName}AdminViewItem`;
883
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
884
+ if (restResource) {
885
+ const restPascalName = toPascalCase(restResource.slug);
886
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
887
+ return `import type { ${restPascalName}Record } from ${quoteTsString(restTypesModule)};
888
+
889
+ export type ${itemTypeName} = ${restPascalName}Record;
890
+
891
+ export interface ${dataSetTypeName} {
892
+ items: ${itemTypeName}[];
893
+ paginationInfo: {
894
+ totalItems: number;
895
+ totalPages: number;
896
+ };
897
+ }
898
+ `;
899
+ }
900
+ return `export type ${pascalName}AdminViewStatus = 'draft' | 'published';
901
+
902
+ export interface ${itemTypeName} {
903
+ id: number;
904
+ owner: string;
905
+ status: ${pascalName}AdminViewStatus;
906
+ title: string;
907
+ updatedAt: string;
908
+ }
909
+
910
+ export interface ${dataSetTypeName} {
911
+ items: ${itemTypeName}[];
912
+ paginationInfo: {
913
+ totalItems: number;
914
+ totalPages: number;
915
+ };
916
+ }
917
+ `;
918
+ }
919
+ function buildAdminViewConfigSource(adminViewSlug, textDomain, restResource) {
920
+ const pascalName = toPascalCase(adminViewSlug);
921
+ const camelName = toCamelCase(adminViewSlug);
922
+ const itemTypeName = `${pascalName}AdminViewItem`;
923
+ const dataViewsName = `${camelName}AdminDataViews`;
924
+ const defaultViewFields = restResource ? "['id']" : "['title', 'status', 'updatedAt']";
925
+ const searchEnabled = restResource ? "false" : "true";
926
+ const titleFieldSource = restResource ? "" : ` titleField: 'title',
927
+ `;
928
+ const defaultViewEnhancementsSource = restResource ? "" : ` sort: {
929
+ direction: 'desc',
930
+ field: 'updatedAt',
931
+ },
932
+ titleField: 'title',
933
+ `;
934
+ 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." : ` owner: {
935
+ label: __( 'Owner', ${quoteTsString(textDomain)} ),
936
+ schema: { type: 'string' },
937
+ },
938
+ status: {
939
+ filterBy: { operators: ['isAny', 'isNone'] },
940
+ label: __( 'Status', ${quoteTsString(textDomain)} ),
941
+ schema: {
942
+ enum: ['draft', 'published'],
943
+ enumLabels: {
944
+ draft: __( 'Draft', ${quoteTsString(textDomain)} ),
945
+ published: __( 'Published', ${quoteTsString(textDomain)} ),
946
+ },
947
+ type: 'string',
948
+ },
949
+ },
950
+ title: {
951
+ enableGlobalSearch: true,
952
+ enableSorting: true,
953
+ label: __( 'Title', ${quoteTsString(textDomain)} ),
954
+ schema: { type: 'string' },
955
+ },
956
+ updatedAt: {
957
+ enableSorting: true,
958
+ label: __( 'Updated', ${quoteTsString(textDomain)} ),
959
+ schema: { format: 'date-time', type: 'string' },
960
+ type: 'datetime',
961
+ },`;
962
+ return `import { defineDataViews } from '@wp-typia/dataviews';
963
+ import { __ } from '@wordpress/i18n';
964
+
965
+ import type { ${itemTypeName} } from './types';
966
+
967
+ export const ${dataViewsName} = defineDataViews<${itemTypeName}>({
968
+ idField: 'id',
969
+ search: ${searchEnabled},
970
+ searchLabel: __( 'Search records', ${quoteTsString(textDomain)} ),
971
+ ${titleFieldSource}
972
+ defaultView: {
973
+ fields: ${defaultViewFields},
974
+ page: 1,
975
+ perPage: 10,
976
+ ${defaultViewEnhancementsSource}
977
+ type: 'table',
978
+ },
979
+ fields: {
980
+ id: {
981
+ enableHiding: false,
982
+ label: __( 'ID', ${quoteTsString(textDomain)} ),
983
+ readOnly: true,
984
+ schema: { type: 'integer' },
985
+ },
986
+ ${additionalFieldsSource}
987
+ },
988
+ });
989
+ `;
990
+ }
991
+ function buildDefaultAdminViewDataSource(adminViewSlug) {
992
+ const pascalName = toPascalCase(adminViewSlug);
993
+ const camelName = toCamelCase(adminViewSlug);
994
+ const title = toTitleCase(adminViewSlug);
995
+ const itemTypeName = `${pascalName}AdminViewItem`;
996
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
997
+ const queryTypeName = `${pascalName}AdminViewQuery`;
998
+ const dataViewsName = `${camelName}AdminDataViews`;
999
+ const fetchName = `fetch${pascalName}AdminViewData`;
1000
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1001
+
1002
+ import { ${dataViewsName} } from './config';
1003
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1004
+
1005
+ export interface ${queryTypeName} {
1006
+ page?: number;
1007
+ perPage?: number;
1008
+ search?: string;
1009
+ }
1010
+
1011
+ const STARTER_ITEMS: ${itemTypeName}[] = [
1012
+ {
1013
+ id: 1,
1014
+ owner: 'Editorial',
1015
+ status: 'published',
1016
+ title: ${quoteTsString(`${title} launch checklist`)},
1017
+ updatedAt: '2026-04-01T10:30:00Z',
1018
+ },
1019
+ {
1020
+ id: 2,
1021
+ owner: 'Design',
1022
+ status: 'draft',
1023
+ title: ${quoteTsString(`${title} content refresh`)},
1024
+ updatedAt: '2026-04-03T14:15:00Z',
1025
+ },
1026
+ {
1027
+ id: 3,
1028
+ owner: 'Operations',
1029
+ status: 'published',
1030
+ title: ${quoteTsString(`${title} support handoff`)},
1031
+ updatedAt: '2026-04-08T08:45:00Z',
1032
+ },
1033
+ ];
1034
+
1035
+ function matchesSearch(item: ${itemTypeName}, search: string | undefined): boolean {
1036
+ if (!search) {
1037
+ return true;
1038
+ }
1039
+
1040
+ const needle = search.toLowerCase();
1041
+ return [item.title, item.owner, item.status].some((value) =>
1042
+ value.toLowerCase().includes(needle),
1043
+ );
1044
+ }
1045
+
1046
+ export async function ${fetchName}(
1047
+ view: DataViewsView<${itemTypeName}>,
1048
+ ): Promise<${dataSetTypeName}> {
1049
+ const query = ${dataViewsName}.toQueryArgs<${queryTypeName}>(view, {
1050
+ perPageParam: 'perPage',
1051
+ });
1052
+ const requestedPage = query.page ?? 1;
1053
+ const page = requestedPage > 0 ? requestedPage : 1;
1054
+ const requestedPerPage = query.perPage ?? view.perPage ?? 10;
1055
+ const perPage = requestedPerPage > 0 ? requestedPerPage : 10;
1056
+ const filteredItems = STARTER_ITEMS.filter((item) =>
1057
+ matchesSearch(item, query.search),
1058
+ );
1059
+ const offset = (page - 1) * perPage;
1060
+ const items = filteredItems.slice(offset, offset + perPage);
1061
+
1062
+ return {
1063
+ items,
1064
+ paginationInfo: {
1065
+ totalItems: filteredItems.length,
1066
+ totalPages: Math.max(1, Math.ceil(filteredItems.length / perPage)),
1067
+ },
1068
+ };
1069
+ }
1070
+ `;
1071
+ }
1072
+ function buildRestAdminViewDataSource(adminViewSlug, restResource) {
1073
+ const pascalName = toPascalCase(adminViewSlug);
1074
+ const restPascalName = toPascalCase(restResource.slug);
1075
+ const camelName = toCamelCase(adminViewSlug);
1076
+ const itemTypeName = `${pascalName}AdminViewItem`;
1077
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1078
+ const dataViewsName = `${camelName}AdminDataViews`;
1079
+ const fetchName = `fetch${pascalName}AdminViewData`;
1080
+ const restApiModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.apiFile);
1081
+ const restTypesModule = getAdminViewRelativeModuleSpecifier(adminViewSlug, restResource.typesFile);
1082
+ return `import type { DataViewsView } from '@wp-typia/dataviews';
1083
+
1084
+ import { listResource } from ${quoteTsString(restApiModule)};
1085
+ import type { ${restPascalName}ListQuery } from ${quoteTsString(restTypesModule)};
1086
+ import { ${dataViewsName} } from './config';
1087
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1088
+
1089
+ function resolveTotalPages(total: number, perPage: number | undefined): number {
1090
+ const resolvedPerPage = perPage && perPage > 0 ? perPage : 1;
1091
+ return Math.max(1, Math.ceil(total / resolvedPerPage));
1092
+ }
1093
+
1094
+ export async function ${fetchName}(
1095
+ view: DataViewsView<${itemTypeName}>,
1096
+ ): Promise<${dataSetTypeName}> {
1097
+ const query = ${dataViewsName}.toQueryArgs<${restPascalName}ListQuery>(view, {
1098
+ perPageParam: 'perPage',
1099
+ searchParam: false,
1100
+ });
1101
+ const result = await listResource({
1102
+ page: query.page,
1103
+ perPage: query.perPage,
1104
+ });
1105
+ if (!result.isValid || !result.data) {
1106
+ throw new Error('Unable to load REST resource records.');
1107
+ }
1108
+
1109
+ const response = result.data;
1110
+
1111
+ return {
1112
+ items: response.items,
1113
+ paginationInfo: {
1114
+ totalItems: response.total,
1115
+ totalPages: resolveTotalPages(response.total, response.perPage ?? query.perPage),
1116
+ },
1117
+ };
1118
+ }
1119
+ `;
1120
+ }
1121
+ function buildAdminViewScreenSource(adminViewSlug, textDomain) {
1122
+ const pascalName = toPascalCase(adminViewSlug);
1123
+ const camelName = toCamelCase(adminViewSlug);
1124
+ const itemTypeName = `${pascalName}AdminViewItem`;
1125
+ const dataSetTypeName = `${pascalName}AdminViewDataSet`;
1126
+ const componentName = `${pascalName}AdminViewScreen`;
1127
+ const dataViewsName = `${camelName}AdminDataViews`;
1128
+ const fetchName = `fetch${pascalName}AdminViewData`;
1129
+ const title = toTitleCase(adminViewSlug);
1130
+ return `import type { DataViewsConfig, DataViewsView } from '@wp-typia/dataviews';
1131
+ import { Button, Notice, Spinner } from '@wordpress/components';
1132
+ import { useEffect, useState } from '@wordpress/element';
1133
+ import { __ } from '@wordpress/i18n';
1134
+ import { DataViews } from '@wordpress/dataviews/wp';
1135
+
1136
+ import { ${dataViewsName} } from './config';
1137
+ import { ${fetchName} } from './data';
1138
+ import type { ${dataSetTypeName}, ${itemTypeName} } from './types';
1139
+
1140
+ const TypedDataViews = DataViews as unknown as <TItem extends object>(
1141
+ props: DataViewsConfig<TItem>,
1142
+ ) => ReturnType<typeof DataViews>;
1143
+
1144
+ const EMPTY_DATA_SET: ${dataSetTypeName} = {
1145
+ items: [],
1146
+ paginationInfo: {
1147
+ totalItems: 0,
1148
+ totalPages: 1,
1149
+ },
1150
+ };
1151
+
1152
+ export function ${componentName}() {
1153
+ const [view, setView] = useState<DataViewsView<${itemTypeName}>>(
1154
+ ${dataViewsName}.defaultView,
1155
+ );
1156
+ const [dataSet, setDataSet] = useState<${dataSetTypeName}>(EMPTY_DATA_SET);
1157
+ const [error, setError] = useState<string | null>(null);
1158
+ const [isLoading, setIsLoading] = useState(true);
1159
+ const [reloadToken, setReloadToken] = useState(0);
1160
+
1161
+ useEffect(() => {
1162
+ let isCurrentRequest = true;
1163
+ setIsLoading(true);
1164
+ setError(null);
1165
+
1166
+ void ${fetchName}(view)
1167
+ .then((nextDataSet) => {
1168
+ if (isCurrentRequest) {
1169
+ setDataSet(nextDataSet);
1170
+ }
1171
+ })
1172
+ .catch((nextError: unknown) => {
1173
+ if (isCurrentRequest) {
1174
+ setError(
1175
+ nextError instanceof Error
1176
+ ? nextError.message
1177
+ : __( 'Unable to load records.', ${quoteTsString(textDomain)} ),
1178
+ );
1179
+ }
1180
+ })
1181
+ .finally(() => {
1182
+ if (isCurrentRequest) {
1183
+ setIsLoading(false);
1184
+ }
1185
+ });
1186
+
1187
+ return () => {
1188
+ isCurrentRequest = false;
1189
+ };
1190
+ }, [reloadToken, view]);
1191
+
1192
+ const config = ${dataViewsName}.createConfig({
1193
+ data: dataSet.items,
1194
+ isLoading,
1195
+ onChangeView: setView,
1196
+ paginationInfo: dataSet.paginationInfo,
1197
+ view,
1198
+ });
1199
+
1200
+ return (
1201
+ <div className="wp-typia-admin-view-screen">
1202
+ <header className="wp-typia-admin-view-screen__header">
1203
+ <div>
1204
+ <p className="wp-typia-admin-view-screen__eyebrow">
1205
+ { __( 'DataViews admin screen', ${quoteTsString(textDomain)} ) }
1206
+ </p>
1207
+ <h1>{ __( ${quoteTsString(title)}, ${quoteTsString(textDomain)} ) }</h1>
1208
+ <p>
1209
+ { __( 'Replace the fetcher in data.ts with your project data source when this screen graduates from scaffold to product UI.', ${quoteTsString(textDomain)} ) }
1210
+ </p>
1211
+ </div>
1212
+ <div className="wp-typia-admin-view-screen__actions">
1213
+ { isLoading ? <Spinner /> : null }
1214
+ <Button
1215
+ isBusy={ isLoading }
1216
+ onClick={ () => setReloadToken((token) => token + 1) }
1217
+ variant="secondary"
1218
+ >
1219
+ { __( 'Reload', ${quoteTsString(textDomain)} ) }
1220
+ </Button>
1221
+ </div>
1222
+ </header>
1223
+ { error ? (
1224
+ <Notice isDismissible={ false } status="error">
1225
+ { error }
1226
+ </Notice>
1227
+ ) : null }
1228
+ <TypedDataViews<${itemTypeName}> { ...config } />
1229
+ </div>
1230
+ );
1231
+ }
1232
+ `;
1233
+ }
1234
+ function buildAdminViewEntrySource(adminViewSlug) {
1235
+ const pascalName = toPascalCase(adminViewSlug);
1236
+ const componentName = `${pascalName}AdminViewScreen`;
1237
+ const rootId = `wp-typia-admin-view-${adminViewSlug}`;
1238
+ return `import { createRoot } from '@wordpress/element';
1239
+
1240
+ import '@wordpress/dataviews/build-style/style.css';
1241
+ import { ${componentName} } from './Screen';
1242
+ import './style.scss';
1243
+
1244
+ const ROOT_ELEMENT_ID = ${quoteTsString(rootId)};
1245
+
1246
+ function mountAdminView() {
1247
+ const rootElement = document.getElementById(ROOT_ELEMENT_ID);
1248
+ if (!rootElement) {
1249
+ return;
1250
+ }
1251
+
1252
+ createRoot(rootElement).render(<${componentName} />);
1253
+ }
1254
+
1255
+ if (document.readyState === 'loading') {
1256
+ document.addEventListener('DOMContentLoaded', mountAdminView);
1257
+ } else {
1258
+ mountAdminView();
1259
+ }
1260
+ `;
1261
+ }
1262
+ function buildAdminViewStyleSource() {
1263
+ return `.wp-typia-admin-view-screen {
1264
+ box-sizing: border-box;
1265
+ max-width: 1180px;
1266
+ padding: 24px 24px 48px 0;
1267
+ }
1268
+
1269
+ .wp-typia-admin-view-screen__header {
1270
+ align-items: flex-start;
1271
+ display: flex;
1272
+ gap: 24px;
1273
+ justify-content: space-between;
1274
+ margin-bottom: 24px;
1275
+ }
1276
+
1277
+ .wp-typia-admin-view-screen__header h1 {
1278
+ font-size: 28px;
1279
+ line-height: 1.2;
1280
+ margin: 0 0 8px;
1281
+ }
1282
+
1283
+ .wp-typia-admin-view-screen__header p {
1284
+ max-width: 680px;
1285
+ }
1286
+
1287
+ .wp-typia-admin-view-screen__eyebrow {
1288
+ color: #3858e9;
1289
+ font-size: 11px;
1290
+ font-weight: 600;
1291
+ letter-spacing: 0.08em;
1292
+ margin: 0 0 8px;
1293
+ text-transform: uppercase;
1294
+ }
1295
+
1296
+ .wp-typia-admin-view-screen__actions {
1297
+ align-items: center;
1298
+ display: flex;
1299
+ gap: 12px;
1300
+ }
1301
+ `;
1302
+ }
1303
+ function buildAdminViewPhpSource(adminViewSlug, workspace) {
1304
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
1305
+ const phpSlug = adminViewSlug.replace(/-/g, "_");
1306
+ const functionPrefix = `${workspace.workspace.phpPrefix}_${phpSlug}`;
1307
+ const menuSlugFunctionName = `${functionPrefix}_admin_view_menu_slug`;
1308
+ const renderFunctionName = `${functionPrefix}_render_admin_view`;
1309
+ const registerFunctionName = `${functionPrefix}_register_admin_view`;
1310
+ const enqueueFunctionName = `${functionPrefix}_enqueue_admin_view`;
1311
+ const hookGlobalName = `${functionPrefix}_admin_view_hook`;
1312
+ const rootId = `wp-typia-admin-view-${adminViewSlug}`;
1313
+ const title = toTitleCase(adminViewSlug);
1314
+ return `<?php
1315
+ if ( ! defined( 'ABSPATH' ) ) {
1316
+ return;
1317
+ }
1318
+
1319
+ if ( ! function_exists( '${menuSlugFunctionName}' ) ) {
1320
+ function ${menuSlugFunctionName}() : string {
1321
+ return '${workspaceBaseName}-${adminViewSlug}';
1322
+ }
1323
+ }
1324
+
1325
+ if ( ! function_exists( '${renderFunctionName}' ) ) {
1326
+ function ${renderFunctionName}() : void {
1327
+ ?>
1328
+ <div class="wrap">
1329
+ <div id="${rootId}"></div>
1330
+ </div>
1331
+ <?php
1332
+ }
1333
+ }
1334
+
1335
+ if ( ! function_exists( '${registerFunctionName}' ) ) {
1336
+ function ${registerFunctionName}() : void {
1337
+ $GLOBALS['${hookGlobalName}'] = add_submenu_page(
1338
+ 'tools.php',
1339
+ __( ${quotePhpString(title)}, ${quotePhpString(workspace.workspace.textDomain)} ),
1340
+ __( ${quotePhpString(title)}, ${quotePhpString(workspace.workspace.textDomain)} ),
1341
+ 'edit_posts',
1342
+ ${menuSlugFunctionName}(),
1343
+ '${renderFunctionName}'
1344
+ );
1345
+ }
1346
+ }
1347
+
1348
+ if ( ! function_exists( '${enqueueFunctionName}' ) ) {
1349
+ function ${enqueueFunctionName}( string $hook_suffix ) : void {
1350
+ $page_hook = isset( $GLOBALS['${hookGlobalName}'] ) && is_string( $GLOBALS['${hookGlobalName}'] )
1351
+ ? $GLOBALS['${hookGlobalName}']
1352
+ : '';
1353
+
1354
+ if ( $page_hook !== $hook_suffix ) {
1355
+ return;
1356
+ }
1357
+
1358
+ $plugin_file = dirname( __DIR__, 2 ) . '/${workspaceBaseName}.php';
1359
+ $script_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_SCRIPT}';
1360
+ $asset_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_ASSET}';
1361
+ $style_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_STYLE}';
1362
+ $style_rtl_path = dirname( __DIR__, 2 ) . '/${ADMIN_VIEWS_STYLE_RTL}';
1363
+
1364
+ if ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
1365
+ return;
1366
+ }
1367
+
1368
+ $asset = require $asset_path;
1369
+ if ( ! is_array( $asset ) ) {
1370
+ $asset = array();
1371
+ }
1372
+
1373
+ $dependencies = isset( $asset['dependencies'] ) && is_array( $asset['dependencies'] )
1374
+ ? $asset['dependencies']
1375
+ : array();
1376
+
1377
+ wp_enqueue_script(
1378
+ '${workspaceBaseName}-${adminViewSlug}-admin-view',
1379
+ plugins_url( '${ADMIN_VIEWS_SCRIPT}', $plugin_file ),
1380
+ $dependencies,
1381
+ isset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
1382
+ true
1383
+ );
1384
+
1385
+ if ( file_exists( $style_path ) ) {
1386
+ wp_enqueue_style(
1387
+ '${workspaceBaseName}-${adminViewSlug}-admin-view',
1388
+ plugins_url( '${ADMIN_VIEWS_STYLE}', $plugin_file ),
1389
+ array( 'wp-components' ),
1390
+ isset( $asset['version'] ) ? $asset['version'] : filemtime( $style_path )
1391
+ );
1392
+ if ( file_exists( $style_rtl_path ) ) {
1393
+ wp_style_add_data( '${workspaceBaseName}-${adminViewSlug}-admin-view', 'rtl', 'replace' );
1394
+ }
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ add_action( 'admin_menu', '${registerFunctionName}' );
1400
+ add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );
1401
+ `;
1402
+ }
1403
+ async function ensureAdminViewPackageDependencies(workspace) {
1404
+ const packageJsonPath = path4.join(workspace.projectDir, "package.json");
1405
+ const wpTypiaDataViewsVersion = resolvePackageVersionRange("@wp-typia/dataviews", DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, "wp-typia-dataviews");
1406
+ const wordpressDataViewsVersion = resolvePackageVersionRange("@wordpress/dataviews", DEFAULT_WORDPRESS_DATAVIEWS_VERSION);
1407
+ await patchFile(packageJsonPath, (source) => {
1408
+ const packageJson = JSON.parse(source);
1409
+ const nextDependencies = {
1410
+ ...packageJson.dependencies ?? {},
1411
+ "@wordpress/dataviews": packageJson.dependencies?.["@wordpress/dataviews"] ?? wordpressDataViewsVersion
1412
+ };
1413
+ const nextDevDependencies = {
1414
+ ...packageJson.devDependencies ?? {},
1415
+ "@wp-typia/dataviews": packageJson.devDependencies?.["@wp-typia/dataviews"] ?? wpTypiaDataViewsVersion
1416
+ };
1417
+ if (JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {}) && JSON.stringify(nextDevDependencies) === JSON.stringify(packageJson.devDependencies ?? {})) {
1418
+ return source;
804
1419
  }
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;
1420
+ packageJson.dependencies = nextDependencies;
1421
+ packageJson.devDependencies = nextDevDependencies;
1422
+ return `${JSON.stringify(packageJson, null, detectJsonIndent(source))}
1423
+ `;
1424
+ });
1425
+ }
1426
+ async function ensureAdminViewBootstrapAnchors(workspace) {
1427
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1428
+ await patchFile(bootstrapPath, (source) => {
1429
+ let nextSource = source;
1430
+ const loadFunctionName = `${workspace.workspace.phpPrefix}_load_admin_views`;
1431
+ const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
1432
+ const loadFunction = `
1433
+
1434
+ function ${loadFunctionName}() {
1435
+ foreach ( glob( __DIR__ . '${ADMIN_VIEWS_PHP_GLOB}' ) ?: array() as $admin_view_module ) {
1436
+ require_once $admin_view_module;
1437
+ }
1438
+ }
1439
+ `;
1440
+ const insertionAnchors = [
1441
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1442
+ /\?>\s*$/u
1443
+ ];
1444
+ const insertPhpSnippet = (snippet) => {
1445
+ for (const anchor of insertionAnchors) {
1446
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}
1447
+ ${match}`);
1448
+ if (candidate !== nextSource) {
1449
+ nextSource = candidate;
1450
+ return;
1451
+ }
810
1452
  }
811
- return {
812
- end: functionEnd,
813
- start: functionStart
814
- };
1453
+ nextSource = `${nextSource.trimEnd()}
1454
+ ${snippet}
1455
+ `;
1456
+ };
1457
+ const appendPhpSnippet = (snippet) => {
1458
+ const closingTagPattern = /\?>\s*$/u;
1459
+ if (closingTagPattern.test(nextSource)) {
1460
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}
1461
+ ?>`);
1462
+ return;
1463
+ }
1464
+ nextSource = `${nextSource.trimEnd()}
1465
+ ${snippet}
1466
+ `;
1467
+ };
1468
+ if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
1469
+ insertPhpSnippet(loadFunction);
1470
+ } else {
1471
+ const functionRange = findPhpFunctionRange(nextSource, loadFunctionName);
1472
+ const functionSource = functionRange ? nextSource.slice(functionRange.start, functionRange.end) : "";
1473
+ if (!functionSource.includes(ADMIN_VIEWS_PHP_GLOB)) {
1474
+ const replacedSource = replacePhpFunctionDefinition(nextSource, loadFunctionName, loadFunction);
1475
+ if (!replacedSource) {
1476
+ throw new Error(`Unable to repair ${path4.basename(bootstrapPath)} for ${loadFunctionName}.`);
1477
+ }
1478
+ nextSource = replacedSource;
1479
+ }
1480
+ }
1481
+ if (!nextSource.includes(loadHook)) {
1482
+ appendPhpSnippet(loadHook);
1483
+ }
1484
+ return nextSource;
1485
+ });
1486
+ }
1487
+ async function ensureAdminViewBuildScriptAnchors(workspace) {
1488
+ const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1489
+ await patchFile(buildScriptPath, (source) => {
1490
+ if (/['"]src\/admin-views\/index\.(?:ts|js)['"]/u.test(source)) {
1491
+ return source;
1492
+ }
1493
+ const currentSharedEntriesPattern = /(\r?\n\s*['"]src\/editor-plugins\/index\.js['"])\s*,?/u;
1494
+ let nextSource = source.replace(currentSharedEntriesPattern, `$1,
1495
+ 'src/admin-views/index.ts',
1496
+ 'src/admin-views/index.js',`);
1497
+ if (nextSource !== source) {
1498
+ return nextSource;
1499
+ }
1500
+ const legacySharedEntriesPattern = /\[\s*['"]src\/bindings\/index\.ts['"]\s*,\s*['"]src\/bindings\/index\.js['"]\s*(?:,\s*)?\]/u;
1501
+ nextSource = source.replace(legacySharedEntriesPattern, `[
1502
+ 'src/bindings/index.ts',
1503
+ 'src/bindings/index.js',
1504
+ 'src/editor-plugins/index.ts',
1505
+ 'src/editor-plugins/index.js',
1506
+ 'src/admin-views/index.ts',
1507
+ 'src/admin-views/index.js',
1508
+ ]`);
1509
+ if (nextSource !== source) {
1510
+ return nextSource;
1511
+ }
1512
+ throw new Error(`Unable to update ${path4.relative(workspace.projectDir, buildScriptPath)} for admin view shared entries.`);
1513
+ });
1514
+ }
1515
+ async function ensureAdminViewWebpackAnchors(workspace) {
1516
+ const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
1517
+ await patchFile(webpackConfigPath, (source) => {
1518
+ if (/['"]admin-views\/index['"]/u.test(source)) {
1519
+ return source;
1520
+ }
1521
+ const editorPluginEntryPattern = /(\n\s*\[\s*['"]editor-plugins\/index['"][\s\S]*?['"]src\/editor-plugins\/index\.js['"][\s\S]*?\]\s*\])\s*,?/u;
1522
+ let nextSource = source.replace(editorPluginEntryPattern, `$1,
1523
+ [
1524
+ 'admin-views/index',
1525
+ [ 'src/admin-views/index.ts', 'src/admin-views/index.js' ],
1526
+ ],`);
1527
+ if (nextSource !== source) {
1528
+ return nextSource;
815
1529
  }
1530
+ 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;
1531
+ const nextSharedEntriesBlock = ` for ( const [ entryName, candidates ] of [
1532
+ [
1533
+ 'bindings/index',
1534
+ [ 'src/bindings/index.ts', 'src/bindings/index.js' ],
1535
+ ],
1536
+ [
1537
+ 'editor-plugins/index',
1538
+ [ 'src/editor-plugins/index.ts', 'src/editor-plugins/index.js' ],
1539
+ ],
1540
+ [
1541
+ 'admin-views/index',
1542
+ [ 'src/admin-views/index.ts', 'src/admin-views/index.js' ],
1543
+ ],
1544
+ ] ) {
1545
+ for ( const relativePath of candidates ) {
1546
+ const entryPath = path.resolve( process.cwd(), relativePath );
1547
+ if ( ! fs.existsSync( entryPath ) ) {
1548
+ continue;
1549
+ }
1550
+
1551
+ entries.push( [ entryName, entryPath ] );
1552
+ break;
1553
+ }
1554
+ }`;
1555
+ nextSource = source.replace(legacySharedEntriesBlockPattern, nextSharedEntriesBlock);
1556
+ if (nextSource === source) {
1557
+ throw new Error(`Unable to update ${path4.relative(workspace.projectDir, webpackConfigPath)} for admin view shared entries.`);
1558
+ }
1559
+ return nextSource;
1560
+ });
1561
+ }
1562
+ function resolveAdminViewRegistryPath(projectDir) {
1563
+ const adminViewsDir = path4.join(projectDir, "src", "admin-views");
1564
+ return [
1565
+ path4.join(adminViewsDir, "index.ts"),
1566
+ path4.join(adminViewsDir, "index.js")
1567
+ ].find((candidatePath) => fs3.existsSync(candidatePath)) ?? path4.join(adminViewsDir, "index.ts");
1568
+ }
1569
+ function readAdminViewRegistrySlugs(registryPath) {
1570
+ if (!fs3.existsSync(registryPath)) {
1571
+ return [];
816
1572
  }
817
- return null;
1573
+ const source = fs3.readFileSync(registryPath, "utf8");
1574
+ return Array.from(source.matchAll(/^\s*import\s+['"]\.\/([^/'"]+)(?:\/index(?:\.[cm]?[jt]sx?)?)?['"];?\s*$/gmu)).map((match) => match[1]);
818
1575
  }
819
- function replacePhpFunctionDefinition(source, functionName, replacement) {
820
- const functionRange = findPhpFunctionRange(source, functionName);
821
- if (!functionRange) {
822
- return null;
1576
+ async function writeAdminViewRegistry(projectDir, adminViewSlug) {
1577
+ const adminViewsDir = path4.join(projectDir, "src", "admin-views");
1578
+ const registryPath = resolveAdminViewRegistryPath(projectDir);
1579
+ await fsp3.mkdir(adminViewsDir, { recursive: true });
1580
+ const existingAdminViewSlugs = readWorkspaceInventory(projectDir).adminViews.map((entry) => entry.slug);
1581
+ const existingRegistrySlugs = readAdminViewRegistrySlugs(registryPath);
1582
+ const nextAdminViewSlugs = Array.from(new Set([...existingAdminViewSlugs, ...existingRegistrySlugs, adminViewSlug])).sort();
1583
+ await fsp3.writeFile(registryPath, buildAdminViewRegistrySource(nextAdminViewSlugs), "utf8");
1584
+ }
1585
+ async function runAddAdminViewCommand({
1586
+ adminViewName,
1587
+ cwd = process.cwd(),
1588
+ source
1589
+ }) {
1590
+ const workspace = resolveWorkspaceProject(cwd);
1591
+ const adminViewSlug = assertValidGeneratedSlug("Admin view name", normalizeBlockSlug(adminViewName), "wp-typia add admin-view <name> [--source rest-resource:<slug>]");
1592
+ const parsedSource = parseAdminViewSource(source);
1593
+ const inventory = readWorkspaceInventory(workspace.projectDir);
1594
+ const restResource = resolveRestResourceSource(inventory.restResources, parsedSource);
1595
+ assertAdminViewDoesNotExist(workspace.projectDir, adminViewSlug, inventory);
1596
+ const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
1597
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1598
+ const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1599
+ const packageJsonPath = path4.join(workspace.projectDir, "package.json");
1600
+ const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
1601
+ const adminViewsIndexPath = resolveAdminViewRegistryPath(workspace.projectDir);
1602
+ const adminViewDir = path4.join(workspace.projectDir, "src", "admin-views", adminViewSlug);
1603
+ const adminViewPhpPath = path4.join(workspace.projectDir, "inc", "admin-views", `${adminViewSlug}.php`);
1604
+ const mutationSnapshot = {
1605
+ fileSources: await snapshotWorkspaceFiles([
1606
+ adminViewsIndexPath,
1607
+ blockConfigPath,
1608
+ bootstrapPath,
1609
+ buildScriptPath,
1610
+ packageJsonPath,
1611
+ webpackConfigPath
1612
+ ]),
1613
+ snapshotDirs: [],
1614
+ targetPaths: [adminViewDir, adminViewPhpPath]
1615
+ };
1616
+ try {
1617
+ await fsp3.mkdir(adminViewDir, { recursive: true });
1618
+ await fsp3.mkdir(path4.dirname(adminViewPhpPath), { recursive: true });
1619
+ await ensureAdminViewPackageDependencies(workspace);
1620
+ await ensureAdminViewBootstrapAnchors(workspace);
1621
+ await ensureAdminViewBuildScriptAnchors(workspace);
1622
+ await ensureAdminViewWebpackAnchors(workspace);
1623
+ await fsp3.writeFile(path4.join(adminViewDir, "types.ts"), buildAdminViewTypesSource(adminViewSlug, restResource), "utf8");
1624
+ await fsp3.writeFile(path4.join(adminViewDir, "config.ts"), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, restResource), "utf8");
1625
+ await fsp3.writeFile(path4.join(adminViewDir, "data.ts"), restResource ? buildRestAdminViewDataSource(adminViewSlug, restResource) : buildDefaultAdminViewDataSource(adminViewSlug), "utf8");
1626
+ await fsp3.writeFile(path4.join(adminViewDir, "Screen.tsx"), buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), "utf8");
1627
+ await fsp3.writeFile(path4.join(adminViewDir, "index.tsx"), buildAdminViewEntrySource(adminViewSlug), "utf8");
1628
+ await fsp3.writeFile(path4.join(adminViewDir, "style.scss"), buildAdminViewStyleSource(), "utf8");
1629
+ await fsp3.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), "utf8");
1630
+ await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
1631
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
1632
+ adminViewEntries: [buildAdminViewConfigEntry(adminViewSlug, parsedSource)]
1633
+ });
1634
+ return {
1635
+ adminViewSlug,
1636
+ projectDir: workspace.projectDir,
1637
+ source: parsedSource ? `${parsedSource.kind}:${parsedSource.slug}` : undefined
1638
+ };
1639
+ } catch (error) {
1640
+ await rollbackWorkspaceMutation(mutationSnapshot);
1641
+ throw error;
823
1642
  }
824
- return [
825
- source.slice(0, functionRange.start),
826
- replacement,
827
- source.slice(functionRange.end)
828
- ].join("");
829
1643
  }
1644
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-assets.ts
1645
+ var import_typescript = __toESM(require_typescript(), 1);
1646
+ import fs4 from "fs";
1647
+ import { promises as fsp4 } from "fs";
1648
+ import path5 from "path";
1649
+ import {
1650
+ syncBlockMetadata as syncBlockMetadata2
1651
+ } from "@wp-typia/block-runtime/metadata-core";
1652
+ var PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
1653
+ var BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
1654
+ var BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
1655
+ var BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
1656
+ var EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
1657
+ var EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
1658
+ var EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
1659
+ var EDITOR_PLUGIN_EDITOR_STYLE_RTL = "build/editor-plugins/style-index-rtl.css";
1660
+ var BINDING_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
830
1661
  function buildPatternConfigEntry(patternSlug) {
831
1662
  return [
832
1663
  "\t{",
@@ -836,9 +1667,11 @@ function buildPatternConfigEntry(patternSlug) {
836
1667
  ].join(`
837
1668
  `);
838
1669
  }
839
- function buildBindingSourceConfigEntry(bindingSourceSlug) {
1670
+ function buildBindingSourceConfigEntry(bindingSourceSlug, target) {
840
1671
  return [
841
1672
  "\t{",
1673
+ ...target ? [` attribute: ${quoteTsString(target.attributeName)},`] : [],
1674
+ ...target ? [` block: ${quoteTsString(target.blockSlug)},`] : [],
842
1675
  ` editorFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/editor.ts`)},`,
843
1676
  ` serverFile: ${quoteTsString(`src/bindings/${bindingSourceSlug}/server.php`)},`,
844
1677
  ` slug: ${quoteTsString(bindingSourceSlug)},`,
@@ -846,6 +1679,34 @@ function buildBindingSourceConfigEntry(bindingSourceSlug) {
846
1679
  ].join(`
847
1680
  `);
848
1681
  }
1682
+ function assertValidBindingAttributeName(attributeName) {
1683
+ const trimmed = attributeName.trim();
1684
+ if (!trimmed) {
1685
+ throw new Error("`wp-typia add binding-source` requires --attribute <attribute> to include a value when --block is provided.");
1686
+ }
1687
+ if (!BINDING_ATTRIBUTE_NAME_PATTERN.test(trimmed)) {
1688
+ throw new Error(`Binding attribute "${attributeName}" must start with a letter and use only letters, numbers, underscores, or hyphens.`);
1689
+ }
1690
+ return trimmed;
1691
+ }
1692
+ function resolveBindingTargetBlockSlug(blockName, namespace) {
1693
+ const trimmed = blockName.trim();
1694
+ if (!trimmed) {
1695
+ throw new Error("`wp-typia add binding-source` requires --block <block-slug|namespace/block-slug> to include a value when --attribute is provided.");
1696
+ }
1697
+ const blockNameSegments = trimmed.split("/");
1698
+ if (blockNameSegments.length > 2) {
1699
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format.`);
1700
+ }
1701
+ if (blockNameSegments.some((segment) => segment.trim() === "")) {
1702
+ throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format without empty path segments.`);
1703
+ }
1704
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2 ? blockNameSegments : [undefined, blockNameSegments[0]];
1705
+ if (maybeNamespace && maybeNamespace !== namespace) {
1706
+ throw new Error(`Binding target block "${trimmed}" uses namespace "${maybeNamespace}". Expected "${namespace}".`);
1707
+ }
1708
+ return normalizeBlockSlug(maybeSlug ?? "");
1709
+ }
849
1710
  function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
850
1711
  return [
851
1712
  "\t{",
@@ -856,9 +1717,6 @@ function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
856
1717
  ].join(`
857
1718
  `);
858
1719
  }
859
- function toPascalCaseFromSlug(slug) {
860
- return normalizeBlockSlug(slug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
861
- }
862
1720
  function buildPatternSource(patternSlug, namespace, textDomain) {
863
1721
  const patternTitle = toTitleCase(patternSlug);
864
1722
  return `<?php
@@ -877,12 +1735,32 @@ register_block_pattern(
877
1735
  );
878
1736
  `;
879
1737
  }
880
- function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain) {
1738
+ function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain, target) {
881
1739
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
882
1740
  const bindingSourcePhpId = bindingSourceSlug.replace(/-/g, "_");
883
1741
  const bindingSourceValueFunctionName = `${phpPrefix}_${bindingSourcePhpId}_binding_source_values`;
884
1742
  const bindingSourceResolveFunctionName = `${phpPrefix}_${bindingSourcePhpId}_resolve_binding_source_value`;
1743
+ const bindingSourceSupportedAttributesFunctionName = `${phpPrefix}_${bindingSourcePhpId}_supported_binding_attributes`;
885
1744
  const starterValue = `${bindingSourceTitle} starter value`;
1745
+ const supportedAttributesSource = target ? `
1746
+ if ( ! function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
1747
+ function ${bindingSourceSupportedAttributesFunctionName}( array $supported_attributes ) : array {
1748
+ if ( ! in_array( ${quotePhpString(target.attributeName)}, $supported_attributes, true ) ) {
1749
+ $supported_attributes[] = ${quotePhpString(target.attributeName)};
1750
+ }
1751
+
1752
+ return $supported_attributes;
1753
+ }
1754
+ }
1755
+ ` : "";
1756
+ const supportedAttributesHook = target ? `
1757
+ if ( function_exists( '${bindingSourceSupportedAttributesFunctionName}' ) ) {
1758
+ add_filter(
1759
+ ${quotePhpString(`block_bindings_supported_attributes_${namespace}/${target.blockSlug}`)},
1760
+ ${quotePhpString(bindingSourceSupportedAttributesFunctionName)}
1761
+ );
1762
+ }
1763
+ ` : "";
886
1764
  return `<?php
887
1765
  if ( ! defined( 'ABSPATH' ) ) {
888
1766
  return;
@@ -911,6 +1789,7 @@ if ( ! function_exists( '${bindingSourceResolveFunctionName}' ) ) {
911
1789
  return is_string( $value ) ? $value : '';
912
1790
  }
913
1791
  }
1792
+ ${supportedAttributesSource}
914
1793
 
915
1794
  register_block_bindings_source(
916
1795
  ${quotePhpString(`${namespace}/${bindingSourceSlug}`)},
@@ -919,11 +1798,20 @@ register_block_bindings_source(
919
1798
  'get_value_callback' => ${quotePhpString(bindingSourceResolveFunctionName)},
920
1799
  )
921
1800
  );
922
- `;
1801
+ ${supportedAttributesHook}`;
923
1802
  }
924
- function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
1803
+ function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain, target) {
925
1804
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
926
1805
  const starterValue = `${bindingSourceTitle} starter value`;
1806
+ const bindingSourceName = `${namespace}/${bindingSourceSlug}`;
1807
+ const targetSource = target ? `
1808
+ export const BINDING_SOURCE_TARGET = {
1809
+ attribute: ${quoteTsString(target.attributeName)},
1810
+ block: ${quoteTsString(`${namespace}/${target.blockSlug}`)},
1811
+ field: ${quoteTsString(bindingSourceSlug)},
1812
+ source: ${quoteTsString(bindingSourceName)},
1813
+ } as const;
1814
+ ` : "";
927
1815
  return `import { registerBlockBindingsSource } from '@wordpress/blocks';
928
1816
  import { __ } from '@wordpress/i18n';
929
1817
 
@@ -936,13 +1824,14 @@ interface BindingSourceRegistration {
936
1824
  const BINDING_SOURCE_VALUES: Record<string, string> = {
937
1825
  ${quoteTsString(bindingSourceSlug)}: ${quoteTsString(starterValue)},
938
1826
  };
1827
+ ${targetSource}
939
1828
 
940
1829
  function resolveBindingSourceValue( field: string ): string {
941
1830
  return BINDING_SOURCE_VALUES[ field ] ?? '';
942
1831
  }
943
1832
 
944
1833
  registerBlockBindingsSource( {
945
- name: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
1834
+ name: ${quoteTsString(bindingSourceName)},
946
1835
  label: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
947
1836
  getFieldsList() {
948
1837
  return [
@@ -971,8 +1860,98 @@ registerBlockBindingsSource( {
971
1860
  } );
972
1861
  `;
973
1862
  }
1863
+ function resolveBindingTarget(options, namespace) {
1864
+ const hasBlock = options.blockName !== undefined && options.blockName.trim().length > 0;
1865
+ const hasAttribute = options.attributeName !== undefined && options.attributeName.trim().length > 0;
1866
+ if (!hasBlock && !hasAttribute) {
1867
+ return;
1868
+ }
1869
+ if (!hasBlock || !hasAttribute) {
1870
+ throw new Error("`wp-typia add binding-source` requires --block and --attribute to be provided together.");
1871
+ }
1872
+ return {
1873
+ attributeName: assertValidBindingAttributeName(options.attributeName ?? ""),
1874
+ blockSlug: resolveBindingTargetBlockSlug(options.blockName ?? "", namespace)
1875
+ };
1876
+ }
1877
+ function formatBindingAttributeTypeMember(attributeName) {
1878
+ const propertyName = /^[A-Za-z_$][\w$]*$/u.test(attributeName) ? attributeName : JSON.stringify(attributeName);
1879
+ return [
1880
+ "\t/**",
1881
+ "\t * Starter string attribute declared for WordPress Block Bindings.",
1882
+ "\t */",
1883
+ ` ${propertyName}?: string;`
1884
+ ].join(`
1885
+ `);
1886
+ }
1887
+ function getInterfaceDeclaration(source, interfaceName) {
1888
+ const sourceFile = import_typescript.default.createSourceFile("types.ts", source, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TS);
1889
+ let declaration;
1890
+ const visit = (node) => {
1891
+ if (import_typescript.default.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
1892
+ declaration = node;
1893
+ return true;
1894
+ }
1895
+ return import_typescript.default.forEachChild(node, (child) => visit(child) ? true : undefined) ?? false;
1896
+ };
1897
+ visit(sourceFile);
1898
+ return declaration ? { declaration, sourceFile } : undefined;
1899
+ }
1900
+ function getPropertyNameText(name) {
1901
+ if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name) || import_typescript.default.isNumericLiteral(name)) {
1902
+ return name.text;
1903
+ }
1904
+ return;
1905
+ }
1906
+ function interfaceHasAttributeMember(declaration, attributeName) {
1907
+ return declaration.members.some((member) => import_typescript.default.isPropertySignature(member) && member.name !== undefined && getPropertyNameText(member.name) === attributeName);
1908
+ }
1909
+ function insertBindingAttributeTypeMember(source, declaration, attributeName) {
1910
+ let closeBracePosition = declaration.end - 1;
1911
+ while (closeBracePosition > declaration.pos && source[closeBracePosition] !== "}") {
1912
+ closeBracePosition -= 1;
1913
+ }
1914
+ if (source[closeBracePosition] !== "}") {
1915
+ throw new Error("Unable to locate the target interface closing brace.");
1916
+ }
1917
+ const lineEnding = source.includes(`\r
1918
+ `) ? `\r
1919
+ ` : `
1920
+ `;
1921
+ const beforeCloseBrace = source.slice(0, closeBracePosition);
1922
+ const afterCloseBrace = source.slice(closeBracePosition);
1923
+ const memberSource = formatBindingAttributeTypeMember(attributeName).split(`
1924
+ `).join(lineEnding);
1925
+ const prefix = beforeCloseBrace.endsWith(lineEnding) ? "" : lineEnding;
1926
+ return `${beforeCloseBrace}${prefix}${memberSource}${lineEnding}${afterCloseBrace}`;
1927
+ }
1928
+ async function ensureBindingTargetBlockAttributeType(projectDir, block, target) {
1929
+ if (!block.attributeTypeName) {
1930
+ throw new Error(`Workspace block "${block.slug}" must include attributeTypeName in scripts/block-config.ts before it can receive binding-source targets.`);
1931
+ }
1932
+ const typesPath = path5.join(projectDir, block.typesFile);
1933
+ const source = await fsp4.readFile(typesPath, "utf8");
1934
+ const targetInterface = getInterfaceDeclaration(source, block.attributeTypeName);
1935
+ if (!targetInterface) {
1936
+ throw new Error(`Unable to locate interface ${block.attributeTypeName} in ${block.typesFile}.`);
1937
+ }
1938
+ let nextSource = source;
1939
+ if (!interfaceHasAttributeMember(targetInterface.declaration, target.attributeName)) {
1940
+ nextSource = insertBindingAttributeTypeMember(source, targetInterface.declaration, target.attributeName);
1941
+ await fsp4.writeFile(typesPath, nextSource, "utf8");
1942
+ }
1943
+ await syncBlockMetadata2({
1944
+ blockJsonFile: path5.join("src", "blocks", block.slug, "block.json"),
1945
+ jsonSchemaFile: path5.join("src", "blocks", block.slug, "typia.schema.json"),
1946
+ manifestFile: path5.join("src", "blocks", block.slug, "typia.manifest.json"),
1947
+ openApiFile: path5.join("src", "blocks", block.slug, "typia.openapi.json"),
1948
+ projectRoot: projectDir,
1949
+ sourceTypeName: block.attributeTypeName,
1950
+ typesFile: block.typesFile
1951
+ });
1952
+ }
974
1953
  function buildEditorPluginTypesSource(editorPluginSlug) {
975
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
1954
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
976
1955
  return `export interface ${typeName} {
977
1956
  primaryActionLabel: string;
978
1957
  summary: string;
@@ -980,22 +1959,22 @@ function buildEditorPluginTypesSource(editorPluginSlug) {
980
1959
  `;
981
1960
  }
982
1961
  function buildEditorPluginDataSource(editorPluginSlug, slot) {
983
- const typeName = `${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
1962
+ const typeName = `${toPascalCase(editorPluginSlug)}EditorPluginModel`;
984
1963
  const pluginTitle = toTitleCase(editorPluginSlug);
985
- const modelFactoryName = `get${toPascalCaseFromSlug(editorPluginSlug)}SidebarModel`;
986
- const enabledFactoryName = `is${toPascalCaseFromSlug(editorPluginSlug)}Enabled`;
1964
+ const modelFactoryName = `get${toPascalCase(editorPluginSlug)}EditorPluginModel`;
1965
+ const enabledFactoryName = `is${toPascalCase(editorPluginSlug)}Enabled`;
987
1966
  return `import type { ${typeName} } from './types';
988
1967
 
989
1968
  export const EDITOR_PLUGIN_SLOT = ${quoteTsString(slot)} as const;
990
1969
  export const REQUIRED_CAPABILITY = 'edit_posts' as const;
991
1970
 
992
- const DEFAULT_SIDEBAR_MODEL: ${typeName} = {
1971
+ const DEFAULT_EDITOR_PLUGIN_MODEL: ${typeName} = {
993
1972
  primaryActionLabel: ${quoteTsString(`Review ${pluginTitle}`)},
994
1973
  summary: ${quoteTsString(`Replace this summary with your ${pluginTitle} workflow state.`)},
995
1974
  };
996
1975
 
997
1976
  export function ${modelFactoryName}(): ${typeName} {
998
- return DEFAULT_SIDEBAR_MODEL;
1977
+ return DEFAULT_EDITOR_PLUGIN_MODEL;
999
1978
  }
1000
1979
 
1001
1980
  export function ${enabledFactoryName}(): boolean {
@@ -1003,11 +1982,52 @@ export function ${enabledFactoryName}(): boolean {
1003
1982
  }
1004
1983
  `;
1005
1984
  }
1006
- function buildEditorPluginSidebarSource(editorPluginSlug, textDomain) {
1007
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
1008
- const modelFactoryName = `get${pascalName}SidebarModel`;
1985
+ function buildEditorPluginSurfaceSource(editorPluginSlug, slot, textDomain) {
1986
+ const pascalName = toPascalCase(editorPluginSlug);
1987
+ const modelFactoryName = `get${pascalName}EditorPluginModel`;
1009
1988
  const enabledFactoryName = `is${pascalName}Enabled`;
1010
- const componentName = `${pascalName}Sidebar`;
1989
+ const componentName = `${pascalName}Surface`;
1990
+ if (slot === "document-setting-panel") {
1991
+ return `import { Button } from '@wordpress/components';
1992
+ import { PluginDocumentSettingPanel } from '@wordpress/editor';
1993
+ import { __ } from '@wordpress/i18n';
1994
+
1995
+ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
1996
+ import './style.scss';
1997
+
1998
+ export interface ${componentName}Props {
1999
+ surfaceName: string;
2000
+ title: string;
2001
+ }
2002
+
2003
+ export function ${componentName}( {
2004
+ surfaceName,
2005
+ title,
2006
+ }: ${componentName}Props ) {
2007
+ if ( ! ${enabledFactoryName}() ) {
2008
+ return null;
2009
+ }
2010
+
2011
+ const editorPluginModel = ${modelFactoryName}();
2012
+
2013
+ return (
2014
+ <PluginDocumentSettingPanel
2015
+ className="wp-typia-editor-plugin-shell"
2016
+ name={ surfaceName }
2017
+ title={ title }
2018
+ >
2019
+ <p>{ editorPluginModel.summary }</p>
2020
+ <Button variant="secondary">
2021
+ { editorPluginModel.primaryActionLabel }
2022
+ </Button>
2023
+ <p className="wp-typia-editor-plugin-shell__hint">
2024
+ { __( 'Use data.ts to add post type, capability, or editor context guards before showing this panel.', ${quoteTsString(textDomain)} ) }
2025
+ </p>
2026
+ </PluginDocumentSettingPanel>
2027
+ );
2028
+ }
2029
+ `;
2030
+ }
1011
2031
  return `import { Button, PanelBody } from '@wordpress/components';
1012
2032
  import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/editor';
1013
2033
  import { __ } from '@wordpress/i18n';
@@ -1016,34 +2036,34 @@ import { ${modelFactoryName}, ${enabledFactoryName} } from './data';
1016
2036
  import './style.scss';
1017
2037
 
1018
2038
  export interface ${componentName}Props {
1019
- pluginName: string;
2039
+ surfaceName: string;
1020
2040
  title: string;
1021
2041
  }
1022
2042
 
1023
2043
  export function ${componentName}( {
1024
- pluginName,
2044
+ surfaceName,
1025
2045
  title,
1026
2046
  }: ${componentName}Props ) {
1027
2047
  if ( ! ${enabledFactoryName}() ) {
1028
2048
  return null;
1029
2049
  }
1030
2050
 
1031
- const sidebarModel = ${modelFactoryName}();
2051
+ const editorPluginModel = ${modelFactoryName}();
1032
2052
 
1033
2053
  return (
1034
2054
  <>
1035
- <PluginSidebarMoreMenuItem target={ pluginName }>
2055
+ <PluginSidebarMoreMenuItem target={ surfaceName }>
1036
2056
  { title }
1037
2057
  </PluginSidebarMoreMenuItem>
1038
- <PluginSidebar name={ pluginName } title={ title }>
2058
+ <PluginSidebar name={ surfaceName } title={ title }>
1039
2059
  <div className="wp-typia-editor-plugin-shell">
1040
2060
  <PanelBody
1041
2061
  initialOpen
1042
2062
  title={ __( 'Document workflow', ${quoteTsString(textDomain)} ) }
1043
2063
  >
1044
- <p>{ sidebarModel.summary }</p>
2064
+ <p>{ editorPluginModel.summary }</p>
1045
2065
  <Button variant="secondary">
1046
- { sidebarModel.primaryActionLabel }
2066
+ { editorPluginModel.primaryActionLabel }
1047
2067
  </Button>
1048
2068
  </PanelBody>
1049
2069
  </div>
@@ -1054,24 +2074,26 @@ export function ${componentName}( {
1054
2074
  `;
1055
2075
  }
1056
2076
  function buildEditorPluginEntrySource(editorPluginSlug, namespace, textDomain) {
1057
- const pascalName = toPascalCaseFromSlug(editorPluginSlug);
1058
- const componentName = `${pascalName}Sidebar`;
2077
+ const pascalName = toPascalCase(editorPluginSlug);
2078
+ const componentName = `${pascalName}Surface`;
1059
2079
  const pluginName = `${namespace}-${editorPluginSlug}`;
2080
+ const surfaceName = `${pluginName}-surface`;
1060
2081
  const pluginTitle = toTitleCase(editorPluginSlug);
1061
2082
  return `import { registerPlugin } from '@wordpress/plugins';
1062
2083
  import { __ } from '@wordpress/i18n';
1063
2084
 
1064
2085
  import { REQUIRED_CAPABILITY } from './data';
1065
- import { ${componentName} } from './Sidebar';
2086
+ import { ${componentName} } from './Surface';
1066
2087
 
1067
2088
  const EDITOR_PLUGIN_NAME = ${quoteTsString(pluginName)};
2089
+ const EDITOR_PLUGIN_SURFACE_NAME = ${quoteTsString(surfaceName)};
1068
2090
  const EDITOR_PLUGIN_TITLE = __( ${quoteTsString(pluginTitle)}, ${quoteTsString(textDomain)} );
1069
2091
 
1070
2092
  registerPlugin( EDITOR_PLUGIN_NAME, {
1071
2093
  icon: 'admin-generic',
1072
2094
  render: () => (
1073
2095
  <${componentName}
1074
- pluginName={ EDITOR_PLUGIN_NAME }
2096
+ surfaceName={ EDITOR_PLUGIN_SURFACE_NAME }
1075
2097
  title={ EDITOR_PLUGIN_TITLE }
1076
2098
  />
1077
2099
  ),
@@ -1088,6 +2110,11 @@ function buildEditorPluginStyleSource() {
1088
2110
  .wp-typia-editor-plugin-shell p {
1089
2111
  margin: 0 0 12px;
1090
2112
  }
2113
+
2114
+ .wp-typia-editor-plugin-shell__hint {
2115
+ color: #757575;
2116
+ font-size: 12px;
2117
+ }
1091
2118
  `;
1092
2119
  }
1093
2120
  function buildBindingSourceIndexSource(bindingSourceSlugs) {
@@ -1156,7 +2183,7 @@ ${patternFunctions}
1156
2183
  }
1157
2184
  }
1158
2185
  if (!nextSource.includes(patternCategoryFunctionName) || !nextSource.includes(patternRegistrationFunctionName)) {
1159
- throw new Error(`Unable to inject pattern bootstrap functions into ${path4.basename(bootstrapPath)}.`);
2186
+ throw new Error(`Unable to inject pattern bootstrap functions into ${path5.basename(bootstrapPath)}.`);
1160
2187
  }
1161
2188
  if (!nextSource.includes(patternCategoryHook)) {
1162
2189
  nextSource = `${nextSource.trimEnd()}
@@ -1216,7 +2243,6 @@ function ${bindingEditorEnqueueFunctionName}() {
1216
2243
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1217
2244
  /\?>\s*$/u
1218
2245
  ];
1219
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
1220
2246
  const insertPhpSnippet = (snippet) => {
1221
2247
  for (const anchor of insertionAnchors) {
1222
2248
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1241,10 +2267,10 @@ ${snippet}
1241
2267
  ${snippet}
1242
2268
  `;
1243
2269
  };
1244
- if (!hasPhpFunctionDefinition(bindingRegistrationFunctionName)) {
2270
+ if (!hasPhpFunctionDefinition(nextSource, bindingRegistrationFunctionName)) {
1245
2271
  insertPhpSnippet(bindingRegistrationFunction);
1246
2272
  }
1247
- if (!hasPhpFunctionDefinition(bindingEditorEnqueueFunctionName)) {
2273
+ if (!hasPhpFunctionDefinition(nextSource, bindingEditorEnqueueFunctionName)) {
1248
2274
  insertPhpSnippet(bindingEditorEnqueueFunction);
1249
2275
  }
1250
2276
  if (!nextSource.includes(bindingRegistrationHook)) {
@@ -1305,7 +2331,6 @@ function ${enqueueFunctionName}() {
1305
2331
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1306
2332
  /\?>\s*$/u
1307
2333
  ];
1308
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
1309
2334
  const insertPhpSnippet = (snippet) => {
1310
2335
  for (const anchor of insertionAnchors) {
1311
2336
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1330,7 +2355,7 @@ ${snippet}
1330
2355
  ${snippet}
1331
2356
  `;
1332
2357
  };
1333
- if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
2358
+ if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
1334
2359
  insertPhpSnippet(enqueueFunction);
1335
2360
  } else {
1336
2361
  const requiredReferences = [
@@ -1346,7 +2371,7 @@ ${snippet}
1346
2371
  if (missingReferences.length > 0) {
1347
2372
  const replacedSource = replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction);
1348
2373
  if (!replacedSource) {
1349
- throw new Error(`Unable to repair ${path4.basename(bootstrapPath)} for ${enqueueFunctionName}.`);
2374
+ throw new Error(`Unable to repair ${path5.basename(bootstrapPath)} for ${enqueueFunctionName}.`);
1350
2375
  }
1351
2376
  nextSource = replacedSource;
1352
2377
  }
@@ -1358,7 +2383,7 @@ ${snippet}
1358
2383
  });
1359
2384
  }
1360
2385
  async function ensureEditorPluginBuildScriptAnchors(workspace) {
1361
- const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
2386
+ const buildScriptPath = path5.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1362
2387
  await patchFile(buildScriptPath, (source) => {
1363
2388
  if (/['"]src\/editor-plugins\/index\.(?:ts|js)['"]/u.test(source)) {
1364
2389
  return source;
@@ -1371,13 +2396,13 @@ async function ensureEditorPluginBuildScriptAnchors(workspace) {
1371
2396
  'src/editor-plugins/index.js',
1372
2397
  ]`);
1373
2398
  if (nextSource === source) {
1374
- throw new Error(`Unable to update ${path4.relative(workspace.projectDir, buildScriptPath)} for editor plugin shared entries.`);
2399
+ throw new Error(`Unable to update ${path5.relative(workspace.projectDir, buildScriptPath)} for editor plugin shared entries.`);
1375
2400
  }
1376
2401
  return nextSource;
1377
2402
  });
1378
2403
  }
1379
2404
  async function ensureEditorPluginWebpackAnchors(workspace) {
1380
- const webpackConfigPath = path4.join(workspace.projectDir, "webpack.config.js");
2405
+ const webpackConfigPath = path5.join(workspace.projectDir, "webpack.config.js");
1381
2406
  await patchFile(webpackConfigPath, (source) => {
1382
2407
  if (/['"]editor-plugins\/index['"]/u.test(source)) {
1383
2408
  return source;
@@ -1405,45 +2430,45 @@ async function ensureEditorPluginWebpackAnchors(workspace) {
1405
2430
  }`;
1406
2431
  const nextSource = source.replace(legacySharedEntriesBlockPattern, nextSharedEntriesBlock);
1407
2432
  if (nextSource === source) {
1408
- throw new Error(`Unable to update ${path4.relative(workspace.projectDir, webpackConfigPath)} for editor plugin shared entries.`);
2433
+ throw new Error(`Unable to update ${path5.relative(workspace.projectDir, webpackConfigPath)} for editor plugin shared entries.`);
1409
2434
  }
1410
2435
  return nextSource;
1411
2436
  });
1412
2437
  }
1413
2438
  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");
2439
+ const bindingsDir = path5.join(projectDir, "src", "bindings");
2440
+ return [path5.join(bindingsDir, "index.ts"), path5.join(bindingsDir, "index.js")].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path5.join(bindingsDir, "index.ts");
1416
2441
  }
1417
2442
  async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
1418
- const bindingsDir = path4.join(projectDir, "src", "bindings");
2443
+ const bindingsDir = path5.join(projectDir, "src", "bindings");
1419
2444
  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);
2445
+ await fsp4.mkdir(bindingsDir, { recursive: true });
2446
+ const existingBindingSourceSlugs = fs4.readdirSync(bindingsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1422
2447
  const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
1423
- await fsp3.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
2448
+ await fsp4.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
1424
2449
  }
1425
2450
  function resolveEditorPluginRegistryPath(projectDir) {
1426
- const editorPluginsDir = path4.join(projectDir, "src", "editor-plugins");
2451
+ const editorPluginsDir = path5.join(projectDir, "src", "editor-plugins");
1427
2452
  return [
1428
- path4.join(editorPluginsDir, "index.ts"),
1429
- path4.join(editorPluginsDir, "index.js")
1430
- ].find((candidatePath) => fs3.existsSync(candidatePath)) ?? path4.join(editorPluginsDir, "index.ts");
2453
+ path5.join(editorPluginsDir, "index.ts"),
2454
+ path5.join(editorPluginsDir, "index.js")
2455
+ ].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path5.join(editorPluginsDir, "index.ts");
1431
2456
  }
1432
2457
  function readEditorPluginRegistrySlugs(registryPath) {
1433
- if (!fs3.existsSync(registryPath)) {
2458
+ if (!fs4.existsSync(registryPath)) {
1434
2459
  return [];
1435
2460
  }
1436
- const source = fs3.readFileSync(registryPath, "utf8");
2461
+ const source = fs4.readFileSync(registryPath, "utf8");
1437
2462
  return Array.from(source.matchAll(/^\s*import\s+['"]\.\/([^/'"]+)(?:\/index(?:\.[cm]?[jt]sx?)?)?['"];?\s*$/gmu)).map((match) => match[1]);
1438
2463
  }
1439
2464
  async function writeEditorPluginRegistry(projectDir, editorPluginSlug) {
1440
- const editorPluginsDir = path4.join(projectDir, "src", "editor-plugins");
2465
+ const editorPluginsDir = path5.join(projectDir, "src", "editor-plugins");
1441
2466
  const registryPath = resolveEditorPluginRegistryPath(projectDir);
1442
- await fsp3.mkdir(editorPluginsDir, { recursive: true });
2467
+ await fsp4.mkdir(editorPluginsDir, { recursive: true });
1443
2468
  const existingEditorPluginSlugs = readWorkspaceInventory(projectDir).editorPlugins.map((entry) => entry.slug);
1444
2469
  const existingRegistrySlugs = readEditorPluginRegistrySlugs(registryPath);
1445
2470
  const nextEditorPluginSlugs = Array.from(new Set([...existingEditorPluginSlugs, ...existingRegistrySlugs, editorPluginSlug])).sort();
1446
- await fsp3.writeFile(registryPath, buildEditorPluginRegistrySource(nextEditorPluginSlugs), "utf8");
2471
+ await fsp4.writeFile(registryPath, buildEditorPluginRegistrySource(nextEditorPluginSlugs), "utf8");
1447
2472
  }
1448
2473
  async function runAddEditorPluginCommand({
1449
2474
  cwd = process.cwd(),
@@ -1451,21 +2476,21 @@ async function runAddEditorPluginCommand({
1451
2476
  slot
1452
2477
  }) {
1453
2478
  const workspace = resolveWorkspaceProject(cwd);
1454
- const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <PluginSidebar>]");
2479
+ const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <sidebar|document-setting-panel>]");
1455
2480
  const resolvedSlot = assertValidEditorPluginSlot(slot);
1456
2481
  const inventory = readWorkspaceInventory(workspace.projectDir);
1457
2482
  assertEditorPluginDoesNotExist(workspace.projectDir, editorPluginSlug, inventory);
1458
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
2483
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1459
2484
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1460
- const buildScriptPath = path4.join(workspace.projectDir, "scripts", "build-workspace.mjs");
2485
+ const buildScriptPath = path5.join(workspace.projectDir, "scripts", "build-workspace.mjs");
1461
2486
  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");
2487
+ const webpackConfigPath = path5.join(workspace.projectDir, "webpack.config.js");
2488
+ const editorPluginDir = path5.join(workspace.projectDir, "src", "editor-plugins", editorPluginSlug);
2489
+ const entryFilePath = path5.join(editorPluginDir, "index.tsx");
2490
+ const surfaceFilePath = path5.join(editorPluginDir, "Surface.tsx");
2491
+ const dataFilePath = path5.join(editorPluginDir, "data.ts");
2492
+ const typesFilePath = path5.join(editorPluginDir, "types.ts");
2493
+ const styleFilePath = path5.join(editorPluginDir, "style.scss");
1469
2494
  const mutationSnapshot = {
1470
2495
  fileSources: await snapshotWorkspaceFiles([
1471
2496
  blockConfigPath,
@@ -1478,15 +2503,15 @@ async function runAddEditorPluginCommand({
1478
2503
  targetPaths: [editorPluginDir]
1479
2504
  };
1480
2505
  try {
1481
- await fsp3.mkdir(editorPluginDir, { recursive: true });
2506
+ await fsp4.mkdir(editorPluginDir, { recursive: true });
1482
2507
  await ensureEditorPluginBootstrapAnchors(workspace);
1483
2508
  await ensureEditorPluginBuildScriptAnchors(workspace);
1484
2509
  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");
2510
+ await fsp4.writeFile(entryFilePath, buildEditorPluginEntrySource(editorPluginSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
2511
+ await fsp4.writeFile(surfaceFilePath, buildEditorPluginSurfaceSource(editorPluginSlug, resolvedSlot, workspace.workspace.textDomain), "utf8");
2512
+ await fsp4.writeFile(dataFilePath, buildEditorPluginDataSource(editorPluginSlug, resolvedSlot), "utf8");
2513
+ await fsp4.writeFile(typesFilePath, buildEditorPluginTypesSource(editorPluginSlug), "utf8");
2514
+ await fsp4.writeFile(styleFilePath, buildEditorPluginStyleSource(), "utf8");
1490
2515
  await writeEditorPluginRegistry(workspace.projectDir, editorPluginSlug);
1491
2516
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1492
2517
  editorPluginEntries: [
@@ -1511,18 +2536,18 @@ async function runAddPatternCommand({
1511
2536
  const patternSlug = assertValidGeneratedSlug("Pattern name", normalizeBlockSlug(patternName), "wp-typia add pattern <name>");
1512
2537
  const inventory = readWorkspaceInventory(workspace.projectDir);
1513
2538
  assertPatternDoesNotExist(workspace.projectDir, patternSlug, inventory);
1514
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
2539
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1515
2540
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1516
- const patternFilePath = path4.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
2541
+ const patternFilePath = path5.join(workspace.projectDir, "src", "patterns", `${patternSlug}.php`);
1517
2542
  const mutationSnapshot = {
1518
2543
  fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath]),
1519
2544
  snapshotDirs: [],
1520
2545
  targetPaths: [patternFilePath]
1521
2546
  };
1522
2547
  try {
1523
- await fsp3.mkdir(path4.dirname(patternFilePath), { recursive: true });
2548
+ await fsp4.mkdir(path5.dirname(patternFilePath), { recursive: true });
1524
2549
  await ensurePatternBootstrapAnchors(workspace);
1525
- await fsp3.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
2550
+ await fsp4.writeFile(patternFilePath, buildPatternSource(patternSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
1526
2551
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1527
2552
  patternEntries: [buildPatternConfigEntry(patternSlug)]
1528
2553
  });
@@ -1536,34 +2561,59 @@ async function runAddPatternCommand({
1536
2561
  }
1537
2562
  }
1538
2563
  async function runAddBindingSourceCommand({
2564
+ attributeName,
1539
2565
  bindingSourceName,
2566
+ blockName,
1540
2567
  cwd = process.cwd()
1541
2568
  }) {
1542
2569
  const workspace = resolveWorkspaceProject(cwd);
1543
- const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name>");
2570
+ const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
1544
2571
  const inventory = readWorkspaceInventory(workspace.projectDir);
1545
2572
  assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
1546
- const blockConfigPath = path4.join(workspace.projectDir, "scripts", "block-config.ts");
2573
+ const target = resolveBindingTarget({
2574
+ attributeName,
2575
+ blockName
2576
+ }, workspace.workspace.namespace);
2577
+ const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
2578
+ const blockConfigPath = path5.join(workspace.projectDir, "scripts", "block-config.ts");
1547
2579
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1548
2580
  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");
2581
+ const bindingSourceDir = path5.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
2582
+ const serverFilePath = path5.join(bindingSourceDir, "server.php");
2583
+ const editorFilePath = path5.join(bindingSourceDir, "editor.ts");
2584
+ const blockJsonPath = target ? path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "block.json") : undefined;
2585
+ const targetGeneratedMetadataPaths = target ? [
2586
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.manifest.json"),
2587
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.openapi.json"),
2588
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia.schema.json"),
2589
+ path5.join(workspace.projectDir, "src", "blocks", target.blockSlug, "typia-validator.php")
2590
+ ] : [];
1552
2591
  const mutationSnapshot = {
1553
- fileSources: await snapshotWorkspaceFiles([blockConfigPath, bootstrapPath, bindingsIndexPath]),
2592
+ fileSources: await snapshotWorkspaceFiles([
2593
+ blockConfigPath,
2594
+ bootstrapPath,
2595
+ bindingsIndexPath,
2596
+ ...blockJsonPath ? [blockJsonPath] : [],
2597
+ ...targetBlock ? [path5.join(workspace.projectDir, targetBlock.typesFile)] : [],
2598
+ ...targetGeneratedMetadataPaths
2599
+ ]),
1554
2600
  snapshotDirs: [],
1555
2601
  targetPaths: [bindingSourceDir]
1556
2602
  };
1557
2603
  try {
1558
- await fsp3.mkdir(bindingSourceDir, { recursive: true });
2604
+ await fsp4.mkdir(bindingSourceDir, { recursive: true });
1559
2605
  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");
2606
+ await fsp4.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
2607
+ await fsp4.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain, target), "utf8");
2608
+ if (target && targetBlock) {
2609
+ await ensureBindingTargetBlockAttributeType(workspace.projectDir, targetBlock, target);
2610
+ }
1562
2611
  await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
1563
2612
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
1564
- bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug)]
2613
+ bindingSourceEntries: [buildBindingSourceConfigEntry(bindingSourceSlug, target)]
1565
2614
  });
1566
2615
  return {
2616
+ ...target ? { attributeName: target.attributeName, blockSlug: target.blockSlug } : {},
1567
2617
  bindingSourceSlug,
1568
2618
  projectDir: workspace.projectDir
1569
2619
  };
@@ -1573,15 +2623,12 @@ async function runAddBindingSourceCommand({
1573
2623
  }
1574
2624
  }
1575
2625
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
1576
- import { promises as fsp4 } from "fs";
1577
- import path7 from "path";
2626
+ import { promises as fsp5 } from "fs";
2627
+ import path8 from "path";
1578
2628
 
1579
2629
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest-anchors.ts
1580
- import path5 from "path";
2630
+ import path6 from "path";
1581
2631
  var REST_RESOURCE_SERVER_GLOB = "/inc/rest/*.php";
1582
- function escapeRegex2(value) {
1583
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1584
- }
1585
2632
  async function ensureRestResourceBootstrapAnchors(workspace) {
1586
2633
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
1587
2634
  await patchFile(bootstrapPath, (source) => {
@@ -1600,7 +2647,6 @@ function ${registerFunctionName}() {
1600
2647
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
1601
2648
  /\?>\s*$/u
1602
2649
  ];
1603
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex2(functionName)}\\s*\\(`, "u").test(nextSource);
1604
2650
  const insertPhpSnippet = (snippet) => {
1605
2651
  for (const anchor of insertionAnchors) {
1606
2652
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -1625,11 +2671,11 @@ ${snippet}
1625
2671
  ${snippet}
1626
2672
  `;
1627
2673
  };
1628
- if (!hasPhpFunctionDefinition(registerFunctionName)) {
2674
+ if (!hasPhpFunctionDefinition(nextSource, registerFunctionName)) {
1629
2675
  insertPhpSnippet(registerFunction);
1630
2676
  } else if (!nextSource.includes(REST_RESOURCE_SERVER_GLOB)) {
1631
2677
  throw new Error([
1632
- `Unable to patch ${path5.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
2678
+ `Unable to patch ${path6.basename(bootstrapPath)} in ensureRestResourceBootstrapAnchors.`,
1633
2679
  `The existing ${registerFunctionName}() definition does not include ${REST_RESOURCE_SERVER_GLOB}.`,
1634
2680
  "Restore the generated bootstrap shape or wire the REST resource loader manually before retrying."
1635
2681
  ].join(" "));
@@ -1643,7 +2689,7 @@ ${snippet}
1643
2689
  function assertSyncRestAnchor(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
1644
2690
  if (!nextSource.includes(target) && !hasAnchor) {
1645
2691
  throw new Error([
1646
- `ensureRestResourceSyncScriptAnchors could not patch ${path5.basename(syncRestScriptPath)}.`,
2692
+ `ensureRestResourceSyncScriptAnchors could not patch ${path6.basename(syncRestScriptPath)}.`,
1647
2693
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
1648
2694
  "Restore the generated template or add the REST_RESOURCES wiring manually before retrying."
1649
2695
  ].join(" "));
@@ -1658,7 +2704,7 @@ function replaceRequiredSyncRestSource(nextSource, target, anchor, replacement,
1658
2704
  return nextSource.replace(anchor, replacement);
1659
2705
  }
1660
2706
  async function ensureRestResourceSyncScriptAnchors(workspace) {
1661
- const syncRestScriptPath = path5.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
2707
+ const syncRestScriptPath = path6.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
1662
2708
  await patchFile(syncRestScriptPath, (source) => {
1663
2709
  let nextSource = source;
1664
2710
  const importAnchor = "import { BLOCKS, type WorkspaceBlockConfig } from './block-config';";
@@ -1776,7 +2822,7 @@ async function ensureRestResourceSyncScriptAnchors(workspace) {
1776
2822
  }
1777
2823
 
1778
2824
  // ../wp-typia-project-tools/src/runtime/rest-resource-artifacts.ts
1779
- import path6 from "path";
2825
+ import path7 from "path";
1780
2826
  import {
1781
2827
  defineEndpointManifest,
1782
2828
  syncEndpointClient,
@@ -1912,8 +2958,8 @@ async function syncRestResourceArtifacts({
1912
2958
  const manifest = buildRestResourceEndpointManifest(variables, methods);
1913
2959
  for (const [baseName, contract] of Object.entries(manifest.contracts)) {
1914
2960
  await syncTypeSchemas({
1915
- jsonSchemaFile: path6.join(outputDir, "api-schemas", `${baseName}.schema.json`),
1916
- openApiFile: path6.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
2961
+ jsonSchemaFile: path7.join(outputDir, "api-schemas", `${baseName}.schema.json`),
2962
+ openApiFile: path7.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
1917
2963
  projectRoot: projectDir,
1918
2964
  sourceTypeName: contract.sourceTypeName,
1919
2965
  typesFile
@@ -1921,7 +2967,7 @@ async function syncRestResourceArtifacts({
1921
2967
  }
1922
2968
  await syncRestOpenApi({
1923
2969
  manifest,
1924
- openApiFile: path6.join(outputDir, "api.openapi.json"),
2970
+ openApiFile: path7.join(outputDir, "api.openapi.json"),
1925
2971
  projectRoot: projectDir,
1926
2972
  typesFile
1927
2973
  });
@@ -1935,16 +2981,13 @@ async function syncRestResourceArtifacts({
1935
2981
  }
1936
2982
 
1937
2983
  // ../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
2984
  function indentMultiline(source, prefix) {
1942
2985
  return source.split(`
1943
2986
  `).map((line) => `${prefix}${line}`).join(`
1944
2987
  `);
1945
2988
  }
1946
2989
  function buildRestResourceConfigEntry(restResourceSlug, namespace, methods) {
1947
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
2990
+ const pascalCase = toPascalCase(restResourceSlug);
1948
2991
  const title = toTitleCase(restResourceSlug);
1949
2992
  const manifest = buildRestResourceEndpointManifest({
1950
2993
  namespace,
@@ -1972,7 +3015,7 @@ function buildRestResourceConfigEntry(restResourceSlug, namespace, methods) {
1972
3015
  `);
1973
3016
  }
1974
3017
  function buildRestResourceTypesSource(restResourceSlug, methods) {
1975
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3018
+ const pascalCase = toPascalCase(restResourceSlug);
1976
3019
  const lines = [
1977
3020
  "import { tags } from 'typia';",
1978
3021
  "",
@@ -2006,7 +3049,7 @@ function buildRestResourceTypesSource(restResourceSlug, methods) {
2006
3049
  `;
2007
3050
  }
2008
3051
  function buildRestResourceValidatorsSource(restResourceSlug, methods) {
2009
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3052
+ const pascalCase = toPascalCase(restResourceSlug);
2010
3053
  const importedTypes = new Set;
2011
3054
  const validatorDeclarations = [];
2012
3055
  const validatorEntries = [];
@@ -2054,7 +3097,7 @@ ${validatorEntries.join(`
2054
3097
  `;
2055
3098
  }
2056
3099
  function buildRestResourceApiSource(restResourceSlug, methods) {
2057
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3100
+ const pascalCase = toPascalCase(restResourceSlug);
2058
3101
  const typeImports = new Set;
2059
3102
  const clientEndpointImports = [];
2060
3103
  const exportedBindings = [];
@@ -2199,7 +3242,7 @@ ${exportedBindings.join(`
2199
3242
  `;
2200
3243
  }
2201
3244
  function buildRestResourceDataSource(restResourceSlug, methods) {
2202
- const pascalCase = toPascalCaseFromSlug2(restResourceSlug);
3245
+ const pascalCase = toPascalCase(restResourceSlug);
2203
3246
  const typeImports = new Set;
2204
3247
  const endpointImports = [];
2205
3248
  const exportedBindings = [];
@@ -2320,9 +3363,6 @@ ${exportedBindings.join(`
2320
3363
  }
2321
3364
 
2322
3365
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-rest.ts
2323
- function quotePhpString2(value) {
2324
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
2325
- }
2326
3366
  function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions) {
2327
3367
  const collectionRoutes = [];
2328
3368
  const itemRoutes = [];
@@ -2418,7 +3458,7 @@ if ( ! defined( 'ABSPATH' ) ) {
2418
3458
 
2419
3459
  if ( ! function_exists( '${getOptionNameFunctionName}' ) ) {
2420
3460
  function ${getOptionNameFunctionName}() {
2421
- return ${quotePhpString2(`${phpPrefix}_${restResourcePhpId}_rest_resource_items`)};
3461
+ return ${quotePhpString(`${phpPrefix}_${restResourcePhpId}_rest_resource_items`)};
2422
3462
  }
2423
3463
  }
2424
3464
 
@@ -2439,8 +3479,8 @@ if ( ! function_exists( '${getItemsFunctionName}' ) ) {
2439
3479
  $seed_items = array(
2440
3480
  array(
2441
3481
  'id' => 1,
2442
- 'title' => ${quotePhpString2(`${restResourceTitle} Starter`)},
2443
- 'content' => ${quotePhpString2(`Replace this seeded ${restResourceTitle.toLowerCase()} content with your plugin data source.`)},
3482
+ 'title' => ${quotePhpString(`${restResourceTitle} Starter`)},
3483
+ 'content' => ${quotePhpString(`Replace this seeded ${restResourceTitle.toLowerCase()} content with your plugin data source.`)},
2444
3484
  'status' => 'draft',
2445
3485
  'updatedAt' => '2026-01-01T00:00:00Z',
2446
3486
  ),
@@ -2731,7 +3771,7 @@ if ( ! function_exists( '${deleteHandlerName}' ) ) {
2731
3771
 
2732
3772
  if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
2733
3773
  function ${registerRoutesFunctionName}() {
2734
- $namespace = ${quotePhpString2(namespace)};
3774
+ $namespace = ${quotePhpString(namespace)};
2735
3775
 
2736
3776
  ${routeRegistrations}
2737
3777
  }
@@ -2752,15 +3792,15 @@ async function runAddRestResourceCommand({
2752
3792
  const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
2753
3793
  const inventory = readWorkspaceInventory(workspace.projectDir);
2754
3794
  assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
2755
- const blockConfigPath = path7.join(workspace.projectDir, "scripts", "block-config.ts");
3795
+ const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
2756
3796
  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`);
3797
+ const syncRestScriptPath = path8.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
3798
+ const restResourceDir = path8.join(workspace.projectDir, "src", "rest", restResourceSlug);
3799
+ const typesFilePath = path8.join(restResourceDir, "api-types.ts");
3800
+ const validatorsFilePath = path8.join(restResourceDir, "api-validators.ts");
3801
+ const apiFilePath = path8.join(restResourceDir, "api.ts");
3802
+ const dataFilePath = path8.join(restResourceDir, "data.ts");
3803
+ const phpFilePath = path8.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
2764
3804
  const mutationSnapshot = {
2765
3805
  fileSources: await snapshotWorkspaceFiles([
2766
3806
  blockConfigPath,
@@ -2771,15 +3811,15 @@ async function runAddRestResourceCommand({
2771
3811
  targetPaths: [restResourceDir, phpFilePath]
2772
3812
  };
2773
3813
  try {
2774
- await fsp4.mkdir(restResourceDir, { recursive: true });
2775
- await fsp4.mkdir(path7.dirname(phpFilePath), { recursive: true });
3814
+ await fsp5.mkdir(restResourceDir, { recursive: true });
3815
+ await fsp5.mkdir(path8.dirname(phpFilePath), { recursive: true });
2776
3816
  await ensureRestResourceBootstrapAnchors(workspace);
2777
3817
  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");
3818
+ await fsp5.writeFile(typesFilePath, buildRestResourceTypesSource(restResourceSlug, resolvedMethods), "utf8");
3819
+ await fsp5.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
3820
+ await fsp5.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
3821
+ await fsp5.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
3822
+ await fsp5.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods), "utf8");
2783
3823
  await syncRestResourceArtifacts({
2784
3824
  clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
2785
3825
  methods: resolvedMethods,
@@ -2789,7 +3829,7 @@ async function runAddRestResourceCommand({
2789
3829
  validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
2790
3830
  variables: {
2791
3831
  namespace: resolvedNamespace,
2792
- pascalCase: toPascalCaseFromSlug2(restResourceSlug),
3832
+ pascalCase: toPascalCase(restResourceSlug),
2793
3833
  slugKebabCase: restResourceSlug,
2794
3834
  title: toTitleCase(restResourceSlug)
2795
3835
  }
@@ -2813,9 +3853,9 @@ async function runAddRestResourceCommand({
2813
3853
  }
2814
3854
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ability.ts
2815
3855
  var import_semver = __toESM(require_semver(), 1);
2816
- import fs4 from "fs";
2817
- import { promises as fsp5 } from "fs";
2818
- import path8 from "path";
3856
+ import fs5 from "fs";
3857
+ import { promises as fsp6 } from "fs";
3858
+ import path9 from "path";
2819
3859
  import { syncTypeSchemas as syncTypeSchemas2 } from "@wp-typia/block-runtime/metadata-core";
2820
3860
  var ABILITY_SERVER_GLOB = "/inc/abilities/*.php";
2821
3861
  var ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
@@ -2826,45 +3866,6 @@ var WP_ABILITIES_PACKAGE_VERSION = "^0.10.0";
2826
3866
  var WP_CORE_ABILITIES_PACKAGE_VERSION = "^0.9.0";
2827
3867
  var WP_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/abilities";
2828
3868
  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
3869
  function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
2869
3870
  if (!existingVersion) {
2870
3871
  return requiredVersion;
@@ -2876,15 +3877,12 @@ function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
2876
3877
  }
2877
3878
  return import_semver.default.gte(existingMinimum, requiredMinimum) ? existingVersion : requiredVersion;
2878
3879
  }
2879
- function toPascalCaseFromAbilitySlug(abilitySlug) {
2880
- return normalizeBlockSlug(abilitySlug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
2881
- }
2882
3880
  function toAbilityCategorySlug(workspaceNamespace) {
2883
3881
  const normalizedNamespace = workspaceNamespace.replace(/[^a-z0-9-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^-|-$/gu, "");
2884
3882
  return `${normalizedNamespace || "workspace"}-workflows`;
2885
3883
  }
2886
3884
  function buildAbilityConfigEntry(abilitySlug, compatibilityPolicy) {
2887
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
3885
+ const pascalCase = toPascalCase(abilitySlug);
2888
3886
  return [
2889
3887
  "\t{",
2890
3888
  ` clientFile: ${quoteTsString(`src/abilities/${abilitySlug}/client.ts`)},`,
@@ -2928,7 +3926,7 @@ function buildAbilityConfigSource(abilitySlug, workspaceNamespace) {
2928
3926
  `;
2929
3927
  }
2930
3928
  function buildAbilityTypesSource(abilitySlug) {
2931
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
3929
+ const pascalCase = toPascalCase(abilitySlug);
2932
3930
  return `export interface ${pascalCase}AbilityInput {
2933
3931
  contextId: number;
2934
3932
  note?: string;
@@ -2943,7 +3941,7 @@ export interface ${pascalCase}AbilityOutput {
2943
3941
  `;
2944
3942
  }
2945
3943
  function buildAbilityDataSource(abilitySlug) {
2946
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
3944
+ const pascalCase = toPascalCase(abilitySlug);
2947
3945
  const abilityConstBase = abilitySlug.toUpperCase().replace(/[^A-Z0-9]+/gu, "_").replace(/_{2,}/gu, "_").replace(/^_|_$/gu, "");
2948
3946
  return `import {
2949
3947
  executeAbility,
@@ -3039,7 +4037,7 @@ export async function run${pascalCase}Ability(
3039
4037
  `;
3040
4038
  }
3041
4039
  function buildAbilityClientSource(abilitySlug) {
3042
- const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
4040
+ const pascalCase = toPascalCase(abilitySlug);
3043
4041
  return `/**
3044
4042
  * Re-export the typed ${pascalCase} ability client helpers.
3045
4043
  *
@@ -3243,8 +4241,8 @@ if ( ! function_exists( '${executeFunctionName}' ) ) {
3243
4241
  'status' => 'ready',
3244
4242
  'summary' => sprintf(
3245
4243
  /* translators: 1: workflow title, 2: context id */
3246
- __( '%1$s processed context %2$d.', ${quotePhpString3(workspace.workspace.textDomain)} ),
3247
- ${quotePhpString3(abilityTitle)},
4244
+ __( '%1$s processed context %2$d.', ${quotePhpString(workspace.workspace.textDomain)} ),
4245
+ ${quotePhpString(abilityTitle)},
3248
4246
  $context_id
3249
4247
  ),
3250
4248
  );
@@ -3310,11 +4308,11 @@ if ( ! function_exists( '${abilityRegisterFunctionName}' ) ) {
3310
4308
  $args = array(
3311
4309
  'category' => (string) $config['category']['slug'],
3312
4310
  'description' => (string) $config['description'],
3313
- 'execute_callback' => ${quotePhpString3(executeFunctionName)},
4311
+ 'execute_callback' => ${quotePhpString(executeFunctionName)},
3314
4312
  'label' => (string) $config['label'],
3315
4313
  'meta' => ${metaFactoryFunctionName}( $config ),
3316
4314
  'output_schema' => $output_schema,
3317
- 'permission_callback' => ${quotePhpString3(permissionFunctionName)},
4315
+ 'permission_callback' => ${quotePhpString(permissionFunctionName)},
3318
4316
  );
3319
4317
 
3320
4318
  if ( is_array( $input_schema ) ) {
@@ -3344,30 +4342,30 @@ function buildAbilityRegistrySource(abilitySlugs) {
3344
4342
  `);
3345
4343
  }
3346
4344
  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");
4345
+ const abilitiesDir = path9.join(projectDir, "src", "abilities");
4346
+ return [path9.join(abilitiesDir, "index.ts"), path9.join(abilitiesDir, "index.js")].find((candidatePath) => fs5.existsSync(candidatePath)) ?? path9.join(abilitiesDir, "index.ts");
3349
4347
  }
3350
4348
  function readAbilityRegistrySlugs(registryPath) {
3351
- if (!fs4.existsSync(registryPath)) {
4349
+ if (!fs5.existsSync(registryPath)) {
3352
4350
  return [];
3353
4351
  }
3354
- const source = fs4.readFileSync(registryPath, "utf8");
4352
+ const source = fs5.readFileSync(registryPath, "utf8");
3355
4353
  return Array.from(source.matchAll(/^\s*export\s+\*\s+from\s+['"]\.\/([^/'"]+)\/client['"];?\s*$/gmu)).map((match) => match[1]);
3356
4354
  }
3357
4355
  async function writeAbilityRegistry(projectDir, abilitySlug) {
3358
- const abilitiesDir = path8.join(projectDir, "src", "abilities");
4356
+ const abilitiesDir = path9.join(projectDir, "src", "abilities");
3359
4357
  const registryPath = resolveAbilityRegistryPath(projectDir);
3360
- await fsp5.mkdir(abilitiesDir, { recursive: true });
4358
+ await fsp6.mkdir(abilitiesDir, { recursive: true });
3361
4359
  const existingAbilitySlugs = readWorkspaceInventory(projectDir).abilities.map((entry) => entry.slug);
3362
4360
  const existingRegistrySlugs = readAbilityRegistrySlugs(registryPath);
3363
4361
  const nextAbilitySlugs = Array.from(new Set([...existingAbilitySlugs, ...existingRegistrySlugs, abilitySlug])).sort();
3364
4362
  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");
4363
+ const existingSource = fs5.existsSync(registryPath) ? fs5.readFileSync(registryPath, "utf8") : "";
4364
+ const generatedSectionPattern = new RegExp(`${escapeRegex(ABILITY_REGISTRY_START_MARKER)}[\\s\\S]*?${escapeRegex(ABILITY_REGISTRY_END_MARKER)}\\n?`, "u");
3367
4365
  const nextSource = existingSource ? generatedSectionPattern.test(existingSource) ? existingSource.replace(generatedSectionPattern, generatedSection) : `${existingSource.trimEnd()}
3368
4366
 
3369
4367
  ${generatedSection}` : generatedSection;
3370
- await fsp5.writeFile(registryPath, nextSource, "utf8");
4368
+ await fsp6.writeFile(registryPath, nextSource, "utf8");
3371
4369
  }
3372
4370
  async function ensureAbilityBootstrapAnchors(workspace) {
3373
4371
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
@@ -3442,7 +4440,6 @@ function ${enqueueFunctionName}() {
3442
4440
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
3443
4441
  /\?>\s*$/u
3444
4442
  ];
3445
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex3(functionName)}\\s*\\(`, "u").test(nextSource);
3446
4443
  const insertPhpSnippet = (snippet) => {
3447
4444
  for (const anchor of insertionAnchors) {
3448
4445
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -3467,13 +4464,13 @@ ${snippet}
3467
4464
  ${snippet}
3468
4465
  `;
3469
4466
  };
3470
- if (!hasPhpFunctionDefinition(loadFunctionName)) {
4467
+ if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
3471
4468
  insertPhpSnippet(loadFunction);
3472
4469
  }
3473
- if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
4470
+ if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
3474
4471
  insertPhpSnippet(enqueueFunction);
3475
- } else if (!findPhpFunctionRange2(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
3476
- nextSource = replacePhpFunctionDefinition2(nextSource, enqueueFunctionName, enqueueFunction);
4472
+ } else if (!findPhpFunctionRange(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
4473
+ nextSource = replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction, { trimReplacementStart: true }) ?? nextSource;
3477
4474
  }
3478
4475
  if (!nextSource.includes(loadHook)) {
3479
4476
  appendPhpSnippet(loadHook);
@@ -3488,8 +4485,8 @@ ${snippet}
3488
4485
  });
3489
4486
  }
3490
4487
  async function ensureAbilityPackageScripts(workspace) {
3491
- const packageJsonPath = path8.join(workspace.projectDir, "package.json");
3492
- const packageJson = JSON.parse(await fsp5.readFile(packageJsonPath, "utf8"));
4488
+ const packageJsonPath = path9.join(workspace.projectDir, "package.json");
4489
+ const packageJson = JSON.parse(await fsp6.readFile(packageJsonPath, "utf8"));
3493
4490
  const nextScripts = {
3494
4491
  ...packageJson.scripts ?? {},
3495
4492
  "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts"
@@ -3504,11 +4501,11 @@ async function ensureAbilityPackageScripts(workspace) {
3504
4501
  }
3505
4502
  packageJson.scripts = nextScripts;
3506
4503
  packageJson.dependencies = nextDependencies;
3507
- await fsp5.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4504
+ await fsp6.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
3508
4505
  `, "utf8");
3509
4506
  }
3510
4507
  async function ensureAbilitySyncProjectAnchors(workspace) {
3511
- const syncProjectScriptPath = path8.join(workspace.projectDir, "scripts", "sync-project.ts");
4508
+ const syncProjectScriptPath = path9.join(workspace.projectDir, "scripts", "sync-project.ts");
3512
4509
  await patchFile(syncProjectScriptPath, (source) => {
3513
4510
  let nextSource = source;
3514
4511
  const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
@@ -3523,7 +4520,7 @@ async function ensureAbilitySyncProjectAnchors(workspace) {
3523
4520
  if (!nextSource.includes(syncAbilitiesConst)) {
3524
4521
  if (!nextSource.includes(syncRestConst)) {
3525
4522
  throw new Error([
3526
- `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
4523
+ `ensureAbilitySyncProjectAnchors could not patch ${path9.basename(syncProjectScriptPath)}.`,
3527
4524
  "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
3528
4525
  "Restore the generated template or wire sync-abilities manually before retrying."
3529
4526
  ].join(" "));
@@ -3534,7 +4531,7 @@ ${syncAbilitiesConst}`);
3534
4531
  if (!nextSource.includes("runSyncScript( syncAbilitiesScriptPath, options );")) {
3535
4532
  if (!syncRestBlockPattern.test(nextSource)) {
3536
4533
  throw new Error([
3537
- `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
4534
+ `ensureAbilitySyncProjectAnchors could not patch ${path9.basename(syncProjectScriptPath)}.`,
3538
4535
  "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
3539
4536
  "Restore the generated template or wire sync-abilities manually before retrying."
3540
4537
  ].join(" "));
@@ -3547,7 +4544,7 @@ ${syncAbilitiesBlock}`);
3547
4544
  });
3548
4545
  }
3549
4546
  async function ensureAbilityBuildScriptAnchors(workspace) {
3550
- const buildScriptPath = path8.join(workspace.projectDir, "scripts", "build-workspace.mjs");
4547
+ const buildScriptPath = path9.join(workspace.projectDir, "scripts", "build-workspace.mjs");
3551
4548
  await patchFile(buildScriptPath, (source) => {
3552
4549
  let nextSource = source;
3553
4550
  if (/['"]src\/abilities\/index\.(?:ts|js)['"]/u.test(nextSource)) {
@@ -3557,7 +4554,7 @@ async function ensureAbilityBuildScriptAnchors(workspace) {
3557
4554
  const match = nextSource.match(sharedEntriesPattern);
3558
4555
  if (!match || !match[2].includes("src/bindings/index.ts") || !match[2].includes("src/editor-plugins/index.ts")) {
3559
4556
  throw new Error([
3560
- `ensureAbilityBuildScriptAnchors could not patch ${path8.basename(buildScriptPath)}.`,
4557
+ `ensureAbilityBuildScriptAnchors could not patch ${path9.basename(buildScriptPath)}.`,
3561
4558
  "Missing the expected shared editor entries array in scripts/build-workspace.mjs.",
3562
4559
  "Restore the generated template or wire abilities/index manually before retrying."
3563
4560
  ].join(" "));
@@ -3574,7 +4571,7 @@ async function ensureAbilityBuildScriptAnchors(workspace) {
3574
4571
  });
3575
4572
  }
3576
4573
  async function ensureAbilityWebpackAnchors(workspace) {
3577
- const webpackConfigPath = path8.join(workspace.projectDir, "webpack.config.js");
4574
+ const webpackConfigPath = path9.join(workspace.projectDir, "webpack.config.js");
3578
4575
  await patchFile(webpackConfigPath, (source) => {
3579
4576
  if (/['"]abilities\/index['"]/u.test(source)) {
3580
4577
  return source;
@@ -3605,7 +4602,7 @@ $2`);
3605
4602
  const match = source.match(sharedEntriesPattern);
3606
4603
  if (!match || !match[1].includes("bindings/index") || !match[1].includes("editor-plugins/index")) {
3607
4604
  throw new Error([
3608
- `ensureAbilityWebpackAnchors could not patch ${path8.basename(webpackConfigPath)}.`,
4605
+ `ensureAbilityWebpackAnchors could not patch ${path9.basename(webpackConfigPath)}.`,
3609
4606
  "Missing the expected shared editor entries block in webpack.config.js.",
3610
4607
  "Restore the generated template or wire abilities/index manually before retrying."
3611
4608
  ].join(" "));
@@ -3635,20 +4632,20 @@ async function runAddAbilityCommand({
3635
4632
  const inventory = readWorkspaceInventory(workspace.projectDir);
3636
4633
  assertAbilityDoesNotExist(workspace.projectDir, abilitySlug, inventory);
3637
4634
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY);
3638
- const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
4635
+ const blockConfigPath = path9.join(workspace.projectDir, "scripts", "block-config.ts");
3639
4636
  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");
4637
+ const buildScriptPath = path9.join(workspace.projectDir, "scripts", "build-workspace.mjs");
4638
+ const packageJsonPath = path9.join(workspace.projectDir, "package.json");
4639
+ const syncAbilitiesScriptPath = path9.join(workspace.projectDir, "scripts", "sync-abilities.ts");
4640
+ const syncProjectScriptPath = path9.join(workspace.projectDir, "scripts", "sync-project.ts");
4641
+ const webpackConfigPath = path9.join(workspace.projectDir, "webpack.config.js");
3645
4642
  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`);
4643
+ const abilityDir = path9.join(workspace.projectDir, "src", "abilities", abilitySlug);
4644
+ const configFilePath = path9.join(abilityDir, "ability.config.json");
4645
+ const typesFilePath = path9.join(abilityDir, "types.ts");
4646
+ const dataFilePath = path9.join(abilityDir, "data.ts");
4647
+ const clientFilePath = path9.join(abilityDir, "client.ts");
4648
+ const phpFilePath = path9.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
3652
4649
  const mutationSnapshot = {
3653
4650
  fileSources: await snapshotWorkspaceFiles([
3654
4651
  blockConfigPath,
@@ -3664,21 +4661,21 @@ async function runAddAbilityCommand({
3664
4661
  targetPaths: [abilityDir, phpFilePath, syncAbilitiesScriptPath]
3665
4662
  };
3666
4663
  try {
3667
- await fsp5.mkdir(abilityDir, { recursive: true });
3668
- await fsp5.mkdir(path8.dirname(phpFilePath), { recursive: true });
4664
+ await fsp6.mkdir(abilityDir, { recursive: true });
4665
+ await fsp6.mkdir(path9.dirname(phpFilePath), { recursive: true });
3669
4666
  await ensureAbilityBootstrapAnchors(workspace);
3670
4667
  await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
3671
4668
  await ensureAbilityPackageScripts(workspace);
3672
4669
  await ensureAbilitySyncProjectAnchors(workspace);
3673
4670
  await ensureAbilityBuildScriptAnchors(workspace);
3674
4671
  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);
4672
+ await fsp6.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
4673
+ await fsp6.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
4674
+ await fsp6.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
4675
+ await fsp6.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
4676
+ await fsp6.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
4677
+ await fsp6.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
4678
+ const pascalCase = toPascalCase(abilitySlug);
3682
4679
  await syncTypeSchemas2({
3683
4680
  jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
3684
4681
  projectRoot: workspace.projectDir,
@@ -3705,12 +4702,12 @@ async function runAddAbilityCommand({
3705
4702
  }
3706
4703
  }
3707
4704
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
3708
- import { promises as fsp7 } from "fs";
3709
- import path11 from "path";
4705
+ import { promises as fsp8 } from "fs";
4706
+ import path12 from "path";
3710
4707
 
3711
4708
  // ../wp-typia-project-tools/src/runtime/ai-feature-artifacts.ts
3712
4709
  import { mkdir, readFile, writeFile } from "fs/promises";
3713
- import path9 from "path";
4710
+ import path10 from "path";
3714
4711
  import {
3715
4712
  defineEndpointManifest as defineEndpointManifest2,
3716
4713
  syncEndpointClient as syncEndpointClient2,
@@ -3736,7 +4733,7 @@ function normalizeGeneratedArtifactContent(content) {
3736
4733
  }
3737
4734
  async function reconcileGeneratedArtifact(options) {
3738
4735
  if (!options.check) {
3739
- await mkdir(path9.dirname(options.filePath), {
4736
+ await mkdir(path10.dirname(options.filePath), {
3740
4737
  recursive: true
3741
4738
  });
3742
4739
  await writeFile(options.filePath, options.content, "utf8");
@@ -3808,8 +4805,8 @@ async function syncAiFeatureRestArtifacts({
3808
4805
  const manifest = buildAiFeatureEndpointManifest(variables);
3809
4806
  for (const [baseName, contract] of Object.entries(manifest.contracts)) {
3810
4807
  await syncTypeSchemas3({
3811
- jsonSchemaFile: path9.join(outputDir, "api-schemas", `${baseName}.schema.json`),
3812
- openApiFile: path9.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
4808
+ jsonSchemaFile: path10.join(outputDir, "api-schemas", `${baseName}.schema.json`),
4809
+ openApiFile: path10.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
3813
4810
  projectRoot: projectDir,
3814
4811
  sourceTypeName: contract.sourceTypeName,
3815
4812
  typesFile
@@ -3817,7 +4814,7 @@ async function syncAiFeatureRestArtifacts({
3817
4814
  }
3818
4815
  await syncRestOpenApi2({
3819
4816
  manifest,
3820
- openApiFile: path9.join(outputDir, "api.openapi.json"),
4817
+ openApiFile: path10.join(outputDir, "api.openapi.json"),
3821
4818
  projectRoot: projectDir,
3822
4819
  typesFile
3823
4820
  }, executionOptions);
@@ -3835,35 +4832,32 @@ async function syncAiFeatureSchemaArtifact({
3835
4832
  outputDir,
3836
4833
  projectDir
3837
4834
  }) {
3838
- const sourceSchemaPath = path9.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
4835
+ const sourceSchemaPath = path10.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
3839
4836
  const responseSchema = assertJsonObject(JSON.parse(await readFile(sourceSchemaPath, "utf8")), sourceSchemaPath);
3840
4837
  const aiSchema = projectWordPressAiSchema(responseSchema);
3841
4838
  await reconcileGeneratedArtifact({
3842
4839
  check,
3843
4840
  content: `${JSON.stringify(aiSchema, null, 2)}
3844
4841
  `,
3845
- filePath: path9.join(projectDir, aiSchemaFile),
4842
+ filePath: path10.join(projectDir, aiSchemaFile),
3846
4843
  label: "AI feature schema"
3847
4844
  });
3848
4845
  return {
3849
4846
  aiSchema,
3850
- aiSchemaPath: path9.join(projectDir, aiSchemaFile),
4847
+ aiSchemaPath: path10.join(projectDir, aiSchemaFile),
3851
4848
  check,
3852
4849
  sourceSchemaPath
3853
4850
  };
3854
4851
  }
3855
4852
 
3856
4853
  // ../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
4854
  function indentMultiline2(source, prefix) {
3861
4855
  return source.split(`
3862
4856
  `).map((line) => `${prefix}${line}`).join(`
3863
4857
  `);
3864
4858
  }
3865
4859
  function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
3866
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4860
+ const pascalCase = toPascalCase(aiFeatureSlug);
3867
4861
  const title = toTitleCase(aiFeatureSlug);
3868
4862
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
3869
4863
  const manifest = buildAiFeatureEndpointManifest({
@@ -3893,7 +4887,7 @@ function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
3893
4887
  `);
3894
4888
  }
3895
4889
  function buildAiFeatureTypesSource(aiFeatureSlug) {
3896
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4890
+ const pascalCase = toPascalCase(aiFeatureSlug);
3897
4891
  return `import { tags } from 'typia';
3898
4892
 
3899
4893
  export interface ${pascalCase}AiFeatureRequest {
@@ -3931,7 +4925,7 @@ export interface ${pascalCase}AiFeatureResponse {
3931
4925
  `;
3932
4926
  }
3933
4927
  function buildAiFeatureValidatorsSource(aiFeatureSlug) {
3934
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4928
+ const pascalCase = toPascalCase(aiFeatureSlug);
3935
4929
  return `import typia from 'typia';
3936
4930
 
3937
4931
  import { toValidationResult } from '@wp-typia/rest';
@@ -3962,7 +4956,7 @@ export const apiValidators = {
3962
4956
  `;
3963
4957
  }
3964
4958
  function buildAiFeatureApiSource(aiFeatureSlug) {
3965
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4959
+ const pascalCase = toPascalCase(aiFeatureSlug);
3966
4960
  return `import {
3967
4961
  callEndpoint,
3968
4962
  resolveRestRouteUrl,
@@ -4017,7 +5011,7 @@ export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
4017
5011
  `;
4018
5012
  }
4019
5013
  function buildAiFeatureDataSource(aiFeatureSlug) {
4020
- const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
5014
+ const pascalCase = toPascalCase(aiFeatureSlug);
4021
5015
  return `import {
4022
5016
  useEndpointMutation,
4023
5017
  type UseEndpointMutationOptions,
@@ -4173,12 +5167,9 @@ main().catch( ( error ) => {
4173
5167
  }
4174
5168
 
4175
5169
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai-anchors.ts
4176
- import { promises as fsp6 } from "fs";
4177
- import path10 from "path";
5170
+ import { promises as fsp7 } from "fs";
5171
+ import path11 from "path";
4178
5172
  var AI_FEATURE_SERVER_GLOB = "/inc/ai-features/*.php";
4179
- function escapeRegex4(value) {
4180
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4181
- }
4182
5173
  async function ensureAiFeatureBootstrapAnchors(workspace) {
4183
5174
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4184
5175
  await patchFile(bootstrapPath, (source) => {
@@ -4197,7 +5188,6 @@ function ${registerFunctionName}() {
4197
5188
  /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
4198
5189
  /\?>\s*$/u
4199
5190
  ];
4200
- const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex4(functionName)}\\s*\\(`, "u").test(nextSource);
4201
5191
  const insertPhpSnippet = (snippet) => {
4202
5192
  for (const anchor of insertionAnchors) {
4203
5193
  const candidate = nextSource.replace(anchor, (match) => `${snippet}
@@ -4222,11 +5212,11 @@ ${snippet}
4222
5212
  ${snippet}
4223
5213
  `;
4224
5214
  };
4225
- if (!hasPhpFunctionDefinition(registerFunctionName)) {
5215
+ if (!hasPhpFunctionDefinition(nextSource, registerFunctionName)) {
4226
5216
  insertPhpSnippet(registerFunction);
4227
5217
  } else if (!nextSource.includes(AI_FEATURE_SERVER_GLOB)) {
4228
5218
  throw new Error([
4229
- `Unable to patch ${path10.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
5219
+ `Unable to patch ${path11.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
4230
5220
  `The existing ${registerFunctionName}() definition does not include ${AI_FEATURE_SERVER_GLOB}.`,
4231
5221
  "Restore the generated bootstrap shape or wire the AI feature loader manually before retrying."
4232
5222
  ].join(" "));
@@ -4238,8 +5228,8 @@ ${snippet}
4238
5228
  });
4239
5229
  }
4240
5230
  async function ensureAiFeaturePackageScripts(workspace) {
4241
- const packageJsonPath = path10.join(workspace.projectDir, "package.json");
4242
- const packageJson = JSON.parse(await fsp6.readFile(packageJsonPath, "utf8"));
5231
+ const packageJsonPath = path11.join(workspace.projectDir, "package.json");
5232
+ const packageJson = JSON.parse(await fsp7.readFile(packageJsonPath, "utf8"));
4243
5233
  const nextScripts = {
4244
5234
  ...packageJson.scripts ?? {},
4245
5235
  "sync-ai": packageJson.scripts?.["sync-ai"] ?? "tsx scripts/sync-ai-features.ts"
@@ -4258,7 +5248,7 @@ async function ensureAiFeaturePackageScripts(workspace) {
4258
5248
  }
4259
5249
  packageJson.scripts = nextScripts;
4260
5250
  packageJson.devDependencies = nextDevDependencies;
4261
- await fsp6.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
5251
+ await fsp7.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4262
5252
  `, "utf8");
4263
5253
  return {
4264
5254
  addedProjectToolsDependency,
@@ -4266,7 +5256,7 @@ async function ensureAiFeaturePackageScripts(workspace) {
4266
5256
  };
4267
5257
  }
4268
5258
  async function ensureAiFeatureSyncProjectAnchors(workspace) {
4269
- const syncProjectScriptPath = path10.join(workspace.projectDir, "scripts", "sync-project.ts");
5259
+ const syncProjectScriptPath = path11.join(workspace.projectDir, "scripts", "sync-project.ts");
4270
5260
  await patchFile(syncProjectScriptPath, (source) => {
4271
5261
  let nextSource = source;
4272
5262
  const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
@@ -4281,7 +5271,7 @@ async function ensureAiFeatureSyncProjectAnchors(workspace) {
4281
5271
  if (!nextSource.includes(syncAiConst)) {
4282
5272
  if (!nextSource.includes(syncRestConst)) {
4283
5273
  throw new Error([
4284
- `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
5274
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path11.basename(syncProjectScriptPath)}.`,
4285
5275
  "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
4286
5276
  "Restore the generated template or wire sync-ai manually before retrying."
4287
5277
  ].join(" "));
@@ -4292,7 +5282,7 @@ ${syncAiConst}`);
4292
5282
  if (!nextSource.includes("runSyncScript( syncAiScriptPath, options );")) {
4293
5283
  if (!syncRestBlockPattern.test(nextSource)) {
4294
5284
  throw new Error([
4295
- `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
5285
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path11.basename(syncProjectScriptPath)}.`,
4296
5286
  "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
4297
5287
  "Restore the generated template or wire sync-ai manually before retrying."
4298
5288
  ].join(" "));
@@ -4307,7 +5297,7 @@ ${syncAiBlock}`);
4307
5297
  function assertSyncRestAnchor2(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
4308
5298
  if (!nextSource.includes(target) && !hasAnchor) {
4309
5299
  throw new Error([
4310
- `ensureAiFeatureSyncRestAnchors could not patch ${path10.basename(syncRestScriptPath)}.`,
5300
+ `ensureAiFeatureSyncRestAnchors could not patch ${path11.basename(syncRestScriptPath)}.`,
4311
5301
  `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
4312
5302
  "Restore the generated template or add the AI feature wiring manually before retrying."
4313
5303
  ].join(" "));
@@ -4322,7 +5312,7 @@ function replaceRequiredSyncRestSource2(nextSource, target, anchor, replacement,
4322
5312
  return nextSource.replace(anchor, replacement);
4323
5313
  }
4324
5314
  async function ensureAiFeatureSyncRestAnchors(workspace) {
4325
- const syncRestScriptPath = path10.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5315
+ const syncRestScriptPath = path11.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4326
5316
  await patchFile(syncRestScriptPath, (source) => {
4327
5317
  let nextSource = source;
4328
5318
  const importAnchor = [
@@ -4451,9 +5441,6 @@ async function ensureAiFeatureSyncRestAnchors(workspace) {
4451
5441
  }
4452
5442
 
4453
5443
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
4454
- function quotePhpString4(value) {
4455
- return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
4456
- }
4457
5444
  function buildAiFeaturePhpSource(aiFeatureSlug, namespace, phpPrefix, textDomain) {
4458
5445
  const aiFeatureTitle = toTitleCase(aiFeatureSlug);
4459
5446
  const aiFeaturePhpId = aiFeatureSlug.replace(/-/g, "_");
@@ -4549,7 +5536,7 @@ if ( ! function_exists( '${buildPromptFunctionName}' ) ) {
4549
5536
  function ${buildPromptFunctionName}( array $payload ) {
4550
5537
  return sprintf(
4551
5538
  '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)},
5539
+ ${quotePhpString(aiFeatureTitle)},
4553
5540
  wp_json_encode( $payload )
4554
5541
  );
4555
5542
  }
@@ -4678,8 +5665,8 @@ if ( ! function_exists( '${adminNoticeFunctionName}' ) ) {
4678
5665
 
4679
5666
  $message = sprintf(
4680
5667
  /* 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)}
5668
+ __( '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)} ),
5669
+ ${quotePhpString(aiFeatureTitle)}
4683
5670
  );
4684
5671
  printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $message ) );
4685
5672
  }
@@ -4814,7 +5801,7 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
4814
5801
  if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
4815
5802
  function ${registerRoutesFunctionName}() {
4816
5803
  register_rest_route(
4817
- ${quotePhpString4(namespace)},
5804
+ ${quotePhpString(namespace)},
4818
5805
  '/ai/${aiFeatureSlug}',
4819
5806
  array(
4820
5807
  array(
@@ -4842,18 +5829,18 @@ async function runAddAiFeatureCommand({
4842
5829
  const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
4843
5830
  const inventory = readWorkspaceInventory(workspace.projectDir);
4844
5831
  assertAiFeatureDoesNotExist(workspace.projectDir, aiFeatureSlug, inventory);
4845
- const blockConfigPath = path11.join(workspace.projectDir, "scripts", "block-config.ts");
5832
+ const blockConfigPath = path12.join(workspace.projectDir, "scripts", "block-config.ts");
4846
5833
  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`);
5834
+ const packageJsonPath = path12.join(workspace.projectDir, "package.json");
5835
+ const syncAiScriptPath = path12.join(workspace.projectDir, "scripts", "sync-ai-features.ts");
5836
+ const syncProjectScriptPath = path12.join(workspace.projectDir, "scripts", "sync-project.ts");
5837
+ const syncRestScriptPath = path12.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
5838
+ const aiFeatureDir = path12.join(workspace.projectDir, "src", "ai-features", aiFeatureSlug);
5839
+ const typesFilePath = path12.join(aiFeatureDir, "api-types.ts");
5840
+ const validatorsFilePath = path12.join(aiFeatureDir, "api-validators.ts");
5841
+ const apiFilePath = path12.join(aiFeatureDir, "api.ts");
5842
+ const dataFilePath = path12.join(aiFeatureDir, "data.ts");
5843
+ const phpFilePath = path12.join(workspace.projectDir, "inc", "ai-features", `${aiFeatureSlug}.php`);
4857
5844
  const mutationSnapshot = {
4858
5845
  fileSources: await snapshotWorkspaceFiles([
4859
5846
  blockConfigPath,
@@ -4867,23 +5854,23 @@ async function runAddAiFeatureCommand({
4867
5854
  targetPaths: [aiFeatureDir, phpFilePath, syncAiScriptPath]
4868
5855
  };
4869
5856
  try {
4870
- await fsp7.mkdir(aiFeatureDir, { recursive: true });
4871
- await fsp7.mkdir(path11.dirname(phpFilePath), { recursive: true });
5857
+ await fsp8.mkdir(aiFeatureDir, { recursive: true });
5858
+ await fsp8.mkdir(path12.dirname(phpFilePath), { recursive: true });
4872
5859
  await ensureAiFeatureBootstrapAnchors(workspace);
4873
5860
  await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
4874
5861
  const packageScriptChanges = await ensureAiFeaturePackageScripts(workspace);
4875
5862
  await ensureAiFeatureSyncProjectAnchors(workspace);
4876
5863
  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);
5864
+ await fsp8.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
5865
+ await fsp8.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
5866
+ await fsp8.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
5867
+ await fsp8.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
5868
+ await fsp8.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
5869
+ await fsp8.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
5870
+ const pascalCase = toPascalCase(aiFeatureSlug);
4884
5871
  await syncAiFeatureRestArtifacts({
4885
5872
  clientFile: `src/ai-features/${aiFeatureSlug}/api-client.ts`,
4886
- outputDir: path11.join("src", "ai-features", aiFeatureSlug),
5873
+ outputDir: path12.join("src", "ai-features", aiFeatureSlug),
4887
5874
  projectDir: workspace.projectDir,
4888
5875
  typesFile: `src/ai-features/${aiFeatureSlug}/api-types.ts`,
4889
5876
  validatorsFile: `src/ai-features/${aiFeatureSlug}/api-validators.ts`,
@@ -4896,7 +5883,7 @@ async function runAddAiFeatureCommand({
4896
5883
  });
4897
5884
  await syncAiFeatureSchemaArtifact({
4898
5885
  aiSchemaFile: `src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`,
4899
- outputDir: path11.join("src", "ai-features", aiFeatureSlug),
5886
+ outputDir: path12.join("src", "ai-features", aiFeatureSlug),
4900
5887
  projectDir: workspace.projectDir
4901
5888
  });
4902
5889
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -4921,7 +5908,168 @@ async function runAddAiFeatureCommand({
4921
5908
 
4922
5909
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
4923
5910
  var VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
5911
+ var VARIATIONS_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceVariations\s*\}\s*from\s*["']\.\/variations["']\s*;?\s*$/mu;
4924
5912
  var VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
5913
+ var VARIATIONS_CALL_PATTERN = /registerWorkspaceVariations\s*\(\s*\)\s*;?/u;
5914
+ var BLOCK_STYLES_IMPORT_LINE = "import { registerWorkspaceBlockStyles } from './styles';";
5915
+ var BLOCK_STYLES_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceBlockStyles\s*\}\s*from\s*["']\.\/styles["']\s*;?\s*$/mu;
5916
+ var BLOCK_STYLES_CALL_LINE = "registerWorkspaceBlockStyles();";
5917
+ var BLOCK_STYLES_CALL_PATTERN = /registerWorkspaceBlockStyles\s*\(\s*\)\s*;?/u;
5918
+ var BLOCK_TRANSFORMS_IMPORT_LINE = "import { applyWorkspaceBlockTransforms } from './transforms';";
5919
+ var BLOCK_TRANSFORMS_IMPORT_PATTERN = /^\s*import\s*\{\s*applyWorkspaceBlockTransforms\s*\}\s*from\s*["']\.\/transforms["']\s*;?\s*$/mu;
5920
+ var BLOCK_TRANSFORMS_CALL_LINE = "applyWorkspaceBlockTransforms(registration.settings);";
5921
+ var BLOCK_TRANSFORMS_CALL_PATTERN = /applyWorkspaceBlockTransforms\s*\(\s*registration\s*\.\s*settings\s*\)\s*;?/u;
5922
+ var SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN = /registerScaffoldBlockType\s*\(\s*registration\s*\.\s*name\s*,\s*registration\s*\.\s*settings\s*\)\s*;?/u;
5923
+ var FULL_BLOCK_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/u;
5924
+ function maskSourceSegment(segment) {
5925
+ return segment.replace(/[^\n\r]/gu, " ");
5926
+ }
5927
+ function maskTypeScriptComments(source) {
5928
+ return source.replace(/\/\*[\s\S]*?\*\//gu, maskSourceSegment).replace(/\/\/[^\n\r]*/gu, maskSourceSegment);
5929
+ }
5930
+ function maskTypeScriptCommentsAndLiterals(source) {
5931
+ let maskedSource = "";
5932
+ let index = 0;
5933
+ while (index < source.length) {
5934
+ const current = source[index];
5935
+ const next = source[index + 1];
5936
+ if (current === "/" && next === "/") {
5937
+ const start = index;
5938
+ index += 2;
5939
+ while (index < source.length && source[index] !== `
5940
+ ` && source[index] !== "\r") {
5941
+ index += 1;
5942
+ }
5943
+ maskedSource += maskSourceSegment(source.slice(start, index));
5944
+ continue;
5945
+ }
5946
+ if (current === "/" && next === "*") {
5947
+ const start = index;
5948
+ index += 2;
5949
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
5950
+ index += 1;
5951
+ }
5952
+ index = Math.min(index + 2, source.length);
5953
+ maskedSource += maskSourceSegment(source.slice(start, index));
5954
+ continue;
5955
+ }
5956
+ if (current === "'" || current === '"' || current === "`") {
5957
+ const start = index;
5958
+ const quote = current;
5959
+ index += 1;
5960
+ while (index < source.length) {
5961
+ const char = source[index];
5962
+ if (char === "\\") {
5963
+ index += 2;
5964
+ continue;
5965
+ }
5966
+ index += 1;
5967
+ if (char === quote) {
5968
+ break;
5969
+ }
5970
+ }
5971
+ maskedSource += maskSourceSegment(source.slice(start, index));
5972
+ continue;
5973
+ }
5974
+ maskedSource += current;
5975
+ index += 1;
5976
+ }
5977
+ return maskedSource;
5978
+ }
5979
+ function hasExecutablePattern(source, pattern) {
5980
+ return pattern.test(maskTypeScriptCommentsAndLiterals(source));
5981
+ }
5982
+ function hasUncommentedPattern(source, pattern) {
5983
+ return pattern.test(maskTypeScriptComments(source));
5984
+ }
5985
+ function findExecutablePatternMatch(source, patterns) {
5986
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
5987
+ for (const pattern of patterns) {
5988
+ const match = pattern.exec(maskedSource);
5989
+ if (match && match.index !== undefined) {
5990
+ return {
5991
+ end: match.index + match[0].length,
5992
+ start: match.index
5993
+ };
5994
+ }
5995
+ }
5996
+ return;
5997
+ }
5998
+ function isIdentifierBoundary(source, index) {
5999
+ if (index < 0 || index >= source.length) {
6000
+ return true;
6001
+ }
6002
+ return !/[A-Za-z0-9_$]/u.test(source[index] ?? "");
6003
+ }
6004
+ function skipWhitespace(source, index) {
6005
+ let cursor = index;
6006
+ while (cursor < source.length && /\s/u.test(source[cursor] ?? "")) {
6007
+ cursor += 1;
6008
+ }
6009
+ return cursor;
6010
+ }
6011
+ function findMatchingDelimiterEnd(source, openIndex, openDelimiter, closeDelimiter) {
6012
+ let depth = 0;
6013
+ for (let index = openIndex;index < source.length; index += 1) {
6014
+ const char = source[index];
6015
+ if (char === openDelimiter) {
6016
+ depth += 1;
6017
+ continue;
6018
+ }
6019
+ if (char === closeDelimiter) {
6020
+ depth -= 1;
6021
+ if (depth === 0) {
6022
+ return index + 1;
6023
+ }
6024
+ }
6025
+ }
6026
+ return;
6027
+ }
6028
+ function findExecutableCallRange(source, callName) {
6029
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
6030
+ let searchIndex = 0;
6031
+ while (searchIndex < maskedSource.length) {
6032
+ const callNameIndex = maskedSource.indexOf(callName, searchIndex);
6033
+ if (callNameIndex === -1) {
6034
+ return;
6035
+ }
6036
+ const callNameEnd = callNameIndex + callName.length;
6037
+ if (!isIdentifierBoundary(maskedSource, callNameIndex - 1) || !isIdentifierBoundary(maskedSource, callNameEnd)) {
6038
+ searchIndex = callNameEnd;
6039
+ continue;
6040
+ }
6041
+ let cursor = skipWhitespace(maskedSource, callNameEnd);
6042
+ if (maskedSource[cursor] === "<") {
6043
+ const genericEnd = findMatchingDelimiterEnd(maskedSource, cursor, "<", ">");
6044
+ if (genericEnd === undefined) {
6045
+ searchIndex = callNameEnd;
6046
+ continue;
6047
+ }
6048
+ cursor = skipWhitespace(maskedSource, genericEnd);
6049
+ }
6050
+ if (maskedSource[cursor] !== "(") {
6051
+ searchIndex = callNameEnd;
6052
+ continue;
6053
+ }
6054
+ const callEnd = findMatchingDelimiterEnd(maskedSource, cursor, "(", ")");
6055
+ if (callEnd === undefined) {
6056
+ searchIndex = callNameEnd;
6057
+ continue;
6058
+ }
6059
+ let end = skipWhitespace(maskedSource, callEnd);
6060
+ if (maskedSource[end] === ";") {
6061
+ end += 1;
6062
+ }
6063
+ return {
6064
+ end,
6065
+ start: callNameIndex
6066
+ };
6067
+ }
6068
+ return;
6069
+ }
6070
+ function findBlockRegistrationCallRange(source) {
6071
+ return findExecutableCallRange(source, "registerScaffoldBlockType") ?? findExecutableCallRange(source, "registerBlockType");
6072
+ }
4925
6073
  function buildVariationConfigEntry(blockSlug, variationSlug) {
4926
6074
  return [
4927
6075
  "\t{",
@@ -4991,49 +6139,300 @@ export function registerWorkspaceVariations() {
4991
6139
  }
4992
6140
  `;
4993
6141
  }
6142
+ function buildWorkspaceConstName(prefix, slug) {
6143
+ return `workspace${prefix}_${toSnakeCase(slug)}`;
6144
+ }
6145
+ function buildBlockStyleConfigEntry(blockSlug, styleSlug) {
6146
+ return [
6147
+ "\t{",
6148
+ ` block: ${quoteTsString(blockSlug)},`,
6149
+ ` file: ${quoteTsString(`src/blocks/${blockSlug}/styles/${styleSlug}.ts`)},`,
6150
+ ` slug: ${quoteTsString(styleSlug)},`,
6151
+ "\t},"
6152
+ ].join(`
6153
+ `);
6154
+ }
6155
+ function buildBlockTransformConfigEntry(options) {
6156
+ return [
6157
+ "\t{",
6158
+ ` block: ${quoteTsString(options.blockSlug)},`,
6159
+ ` file: ${quoteTsString(`src/blocks/${options.blockSlug}/transforms/${options.transformSlug}.ts`)},`,
6160
+ ` from: ${quoteTsString(options.fromBlockName)},`,
6161
+ ` slug: ${quoteTsString(options.transformSlug)},`,
6162
+ ` to: ${quoteTsString(options.toBlockName)},`,
6163
+ "\t},"
6164
+ ].join(`
6165
+ `);
6166
+ }
6167
+ function getBlockStyleConstBindings(styleSlugs) {
6168
+ const seenConstNames = new Map;
6169
+ return styleSlugs.map((styleSlug) => {
6170
+ const constName = buildWorkspaceConstName("BlockStyle", styleSlug);
6171
+ const previousSlug = seenConstNames.get(constName);
6172
+ if (previousSlug && previousSlug !== styleSlug) {
6173
+ throw new Error(`Style slugs "${previousSlug}" and "${styleSlug}" generate the same registry identifier "${constName}". Rename one of the styles.`);
6174
+ }
6175
+ seenConstNames.set(constName, styleSlug);
6176
+ return { constName, styleSlug };
6177
+ });
6178
+ }
6179
+ function getBlockTransformConstBindings(transformSlugs) {
6180
+ const seenConstNames = new Map;
6181
+ return transformSlugs.map((transformSlug) => {
6182
+ const constName = buildWorkspaceConstName("BlockTransform", transformSlug);
6183
+ const previousSlug = seenConstNames.get(constName);
6184
+ if (previousSlug && previousSlug !== transformSlug) {
6185
+ throw new Error(`Transform slugs "${previousSlug}" and "${transformSlug}" generate the same registry identifier "${constName}". Rename one of the transforms.`);
6186
+ }
6187
+ seenConstNames.set(constName, transformSlug);
6188
+ return { constName, transformSlug };
6189
+ });
6190
+ }
6191
+ function buildBlockStyleSource(styleSlug, textDomain) {
6192
+ const styleTitle = toTitleCase(styleSlug);
6193
+ const styleConstName = buildWorkspaceConstName("BlockStyle", styleSlug);
6194
+ return `import { __ } from '@wordpress/i18n';
6195
+
6196
+ export const ${styleConstName} = {
6197
+ name: ${quoteTsString(styleSlug)},
6198
+ label: __( ${quoteTsString(styleTitle)}, ${quoteTsString(textDomain)} ),
6199
+ } as const;
6200
+ `;
6201
+ }
6202
+ function buildBlockStyleIndexSource(styleSlugs) {
6203
+ const styleBindings = getBlockStyleConstBindings(styleSlugs);
6204
+ const importLines = styleBindings.map(({ constName, styleSlug }) => `import { ${constName} } from './${styleSlug}';`).join(`
6205
+ `);
6206
+ const styleConstNames = styleBindings.map(({ constName }) => constName).join(`,
6207
+ `);
6208
+ return `import { registerBlockStyle } from '@wordpress/blocks';
6209
+ import metadata from '../block.json';
6210
+ ${importLines ? `
6211
+ ${importLines}` : ""}
6212
+
6213
+ const WORKSPACE_BLOCK_STYLES = [
6214
+ ${styleConstNames}
6215
+ // wp-typia add style entries
6216
+ ] as const;
6217
+
6218
+ export function registerWorkspaceBlockStyles() {
6219
+ for (const style of WORKSPACE_BLOCK_STYLES) {
6220
+ registerBlockStyle(metadata.name, style);
6221
+ }
6222
+ }
6223
+ `;
6224
+ }
6225
+ function buildBlockTransformSource(options) {
6226
+ const transformTitle = toTitleCase(options.transformSlug);
6227
+ const transformConstName = buildWorkspaceConstName("BlockTransform", options.transformSlug);
6228
+ return `import { createBlock } from '@wordpress/blocks';
6229
+ import { __ } from '@wordpress/i18n';
6230
+ import metadata from '../block.json';
6231
+
6232
+ type TransformAttributes = Record<string, unknown>;
6233
+ type TransformInnerBlock = ReturnType<typeof createBlock>;
6234
+
6235
+ function mapTransformAttributes(attributes: TransformAttributes): TransformAttributes {
6236
+ const content = attributes.content;
6237
+
6238
+ return typeof content === 'string' ? { content } : {};
6239
+ }
6240
+
6241
+ export const ${transformConstName} = {
6242
+ type: 'block',
6243
+ blocks: [${quoteTsString(options.fromBlockName)}],
6244
+ title: __( ${quoteTsString(transformTitle)}, ${quoteTsString(options.textDomain)} ),
6245
+ transform: (
6246
+ attributes: TransformAttributes,
6247
+ innerBlocks: TransformInnerBlock[] = [],
6248
+ ) => createBlock(metadata.name, mapTransformAttributes(attributes), innerBlocks),
6249
+ } as const;
6250
+ `;
6251
+ }
6252
+ function buildBlockTransformIndexSource(transformSlugs) {
6253
+ const transformBindings = getBlockTransformConstBindings(transformSlugs);
6254
+ const importLines = transformBindings.map(({ constName, transformSlug }) => `import { ${constName} } from './${transformSlug}';`).join(`
6255
+ `);
6256
+ const transformConstNames = transformBindings.map(({ constName }) => constName).join(`,
6257
+ `);
6258
+ return `${importLines ? `${importLines}
6259
+
6260
+ ` : ""}type BlockSettingsWithTransforms = {
6261
+ transforms?: {
6262
+ from?: unknown[];
6263
+ to?: unknown[];
6264
+ };
6265
+ };
6266
+
6267
+ const WORKSPACE_BLOCK_TRANSFORMS = [
6268
+ ${transformConstNames}
6269
+ // wp-typia add transform entries
6270
+ ] as const;
6271
+
6272
+ export function applyWorkspaceBlockTransforms(settings: BlockSettingsWithTransforms) {
6273
+ const transforms = settings.transforms ?? {};
6274
+
6275
+ settings.transforms = {
6276
+ ...transforms,
6277
+ from: [...(transforms.from ?? []), ...WORKSPACE_BLOCK_TRANSFORMS],
6278
+ };
6279
+ }
6280
+ `;
6281
+ }
4994
6282
  async function ensureVariationRegistrationHook(blockIndexPath) {
4995
6283
  await patchFile(blockIndexPath, (source) => {
4996
6284
  let nextSource = source;
4997
- if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
6285
+ if (!hasUncommentedPattern(nextSource, VARIATIONS_IMPORT_PATTERN)) {
4998
6286
  nextSource = `${VARIATIONS_IMPORT_LINE}
4999
6287
  ${nextSource}`;
5000
6288
  }
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}
6289
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
6290
+ const callRange = findBlockRegistrationCallRange(nextSource);
6291
+ if (callRange) {
6292
+ nextSource = [
6293
+ nextSource.slice(0, callRange.end),
6294
+ `
5009
6295
  ${VARIATIONS_CALL_LINE}
5010
- `);
5011
- if (candidate !== nextSource) {
5012
- nextSource = candidate;
5013
- inserted = true;
5014
- break;
5015
- }
5016
- }
5017
- if (!inserted) {
6296
+ `,
6297
+ nextSource.slice(callRange.end)
6298
+ ].join("");
6299
+ } else {
5018
6300
  nextSource = `${nextSource.trimEnd()}
5019
6301
 
5020
6302
  ${VARIATIONS_CALL_LINE}
5021
6303
  `;
5022
6304
  }
5023
6305
  }
5024
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
5025
- throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path12.basename(blockIndexPath)}.`);
6306
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
6307
+ throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path13.basename(blockIndexPath)}.`);
6308
+ }
6309
+ return nextSource;
6310
+ });
6311
+ }
6312
+ async function ensureBlockStyleRegistrationHook(blockIndexPath) {
6313
+ await patchFile(blockIndexPath, (source) => {
6314
+ let nextSource = source;
6315
+ if (!hasUncommentedPattern(nextSource, BLOCK_STYLES_IMPORT_PATTERN)) {
6316
+ nextSource = `${BLOCK_STYLES_IMPORT_LINE}
6317
+ ${nextSource}`;
6318
+ }
6319
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
6320
+ const callRange = findBlockRegistrationCallRange(nextSource);
6321
+ if (callRange) {
6322
+ nextSource = [
6323
+ nextSource.slice(0, callRange.end),
6324
+ `
6325
+ ${BLOCK_STYLES_CALL_LINE}
6326
+ `,
6327
+ nextSource.slice(callRange.end)
6328
+ ].join("");
6329
+ } else {
6330
+ nextSource = `${nextSource.trimEnd()}
6331
+
6332
+ ${BLOCK_STYLES_CALL_LINE}
6333
+ `;
6334
+ }
6335
+ }
6336
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
6337
+ throw new Error(`Unable to inject ${BLOCK_STYLES_CALL_LINE} into ${path13.basename(blockIndexPath)}.`);
6338
+ }
6339
+ return nextSource;
6340
+ });
6341
+ }
6342
+ async function ensureBlockTransformRegistrationHook(blockIndexPath) {
6343
+ await patchFile(blockIndexPath, (source) => {
6344
+ let nextSource = source;
6345
+ if (!hasUncommentedPattern(nextSource, BLOCK_TRANSFORMS_IMPORT_PATTERN)) {
6346
+ nextSource = `${BLOCK_TRANSFORMS_IMPORT_LINE}
6347
+ ${nextSource}`;
6348
+ }
6349
+ if (!hasExecutablePattern(nextSource, BLOCK_TRANSFORMS_CALL_PATTERN)) {
6350
+ const callRange = findExecutablePatternMatch(nextSource, [
6351
+ SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN
6352
+ ]);
6353
+ if (!callRange) {
6354
+ throw new Error(`Unable to inject ${BLOCK_TRANSFORMS_CALL_LINE} into ${path13.basename(blockIndexPath)} because it does not expose a scaffold registration settings object.`);
6355
+ }
6356
+ nextSource = [
6357
+ nextSource.slice(0, callRange.start),
6358
+ `${BLOCK_TRANSFORMS_CALL_LINE}
6359
+ `,
6360
+ nextSource.slice(callRange.start)
6361
+ ].join("");
5026
6362
  }
5027
6363
  return nextSource;
5028
6364
  });
5029
6365
  }
5030
6366
  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, ""));
6367
+ const variationsDir = path13.join(projectDir, "src", "blocks", blockSlug, "variations");
6368
+ const variationsIndexPath = path13.join(variationsDir, "index.ts");
6369
+ await fsp9.mkdir(variationsDir, { recursive: true });
6370
+ const existingVariationSlugs = fs6.readdirSync(variationsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
5035
6371
  const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
5036
- await fsp8.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
6372
+ await fsp9.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
6373
+ }
6374
+ async function writeBlockStyleRegistry(projectDir, blockSlug, styleSlug) {
6375
+ const stylesDir = path13.join(projectDir, "src", "blocks", blockSlug, "styles");
6376
+ const stylesIndexPath = path13.join(stylesDir, "index.ts");
6377
+ await fsp9.mkdir(stylesDir, { recursive: true });
6378
+ const existingStyleSlugs = fs6.readdirSync(stylesDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
6379
+ const nextStyleSlugs = Array.from(new Set([...existingStyleSlugs, styleSlug])).sort();
6380
+ await fsp9.writeFile(stylesIndexPath, buildBlockStyleIndexSource(nextStyleSlugs), "utf8");
6381
+ }
6382
+ async function writeBlockTransformRegistry(projectDir, blockSlug, transformSlug) {
6383
+ const transformsDir = path13.join(projectDir, "src", "blocks", blockSlug, "transforms");
6384
+ const transformsIndexPath = path13.join(transformsDir, "index.ts");
6385
+ await fsp9.mkdir(transformsDir, { recursive: true });
6386
+ const existingTransformSlugs = fs6.readdirSync(transformsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
6387
+ const nextTransformSlugs = Array.from(new Set([...existingTransformSlugs, transformSlug])).sort();
6388
+ await fsp9.writeFile(transformsIndexPath, buildBlockTransformIndexSource(nextTransformSlugs), "utf8");
6389
+ }
6390
+ function assertBlockStyleDoesNotExist(projectDir, blockSlug, styleSlug, inventory) {
6391
+ const stylePath = path13.join(projectDir, "src", "blocks", blockSlug, "styles", `${styleSlug}.ts`);
6392
+ if (fs6.existsSync(stylePath)) {
6393
+ throw new Error(`A block style already exists at ${path13.relative(projectDir, stylePath)}. Choose a different name.`);
6394
+ }
6395
+ if (inventory.blockStyles.some((entry) => entry.block === blockSlug && entry.slug === styleSlug)) {
6396
+ throw new Error(`A block style inventory entry already exists for ${blockSlug}/${styleSlug}. Choose a different name.`);
6397
+ }
6398
+ }
6399
+ function assertBlockTransformDoesNotExist(projectDir, blockSlug, transformSlug, inventory) {
6400
+ const transformPath = path13.join(projectDir, "src", "blocks", blockSlug, "transforms", `${transformSlug}.ts`);
6401
+ if (fs6.existsSync(transformPath)) {
6402
+ throw new Error(`A block transform already exists at ${path13.relative(projectDir, transformPath)}. Choose a different name.`);
6403
+ }
6404
+ if (inventory.blockTransforms.some((entry) => entry.block === blockSlug && entry.slug === transformSlug)) {
6405
+ throw new Error(`A block transform inventory entry already exists for ${blockSlug}/${transformSlug}. Choose a different name.`);
6406
+ }
6407
+ }
6408
+ function assertFullBlockName(blockName, flagName) {
6409
+ const trimmed = blockName.trim();
6410
+ if (!trimmed) {
6411
+ throw new Error(`\`${flagName}\` requires a block name.`);
6412
+ }
6413
+ if (!FULL_BLOCK_NAME_PATTERN.test(trimmed)) {
6414
+ throw new Error(`\`${flagName}\` must use <namespace/block-slug> format.`);
6415
+ }
6416
+ return trimmed;
6417
+ }
6418
+ function resolveWorkspaceTargetBlockName(blockName, namespace, flagName) {
6419
+ const trimmed = blockName.trim();
6420
+ if (!trimmed) {
6421
+ throw new Error(`\`${flagName}\` requires <block-slug|namespace/block-slug>.`);
6422
+ }
6423
+ const blockNameSegments = trimmed.split("/");
6424
+ if (blockNameSegments.length > 2 || blockNameSegments.some((segment) => segment.trim() === "")) {
6425
+ throw new Error(`\`${flagName}\` must use <block-slug|namespace/block-slug> format.`);
6426
+ }
6427
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2 ? blockNameSegments : [undefined, blockNameSegments[0]];
6428
+ if (maybeNamespace && maybeNamespace !== namespace) {
6429
+ throw new Error(`\`${flagName}\` references namespace "${maybeNamespace}". Expected "${namespace}".`);
6430
+ }
6431
+ const blockSlug = normalizeBlockSlug(maybeSlug ?? "");
6432
+ return {
6433
+ blockName: `${namespace}/${blockSlug}`,
6434
+ blockSlug
6435
+ };
5037
6436
  }
5038
6437
  async function runAddVariationCommand({
5039
6438
  blockName,
@@ -5046,11 +6445,12 @@ async function runAddVariationCommand({
5046
6445
  const inventory = readWorkspaceInventory(workspace.projectDir);
5047
6446
  resolveWorkspaceBlock(inventory, blockSlug);
5048
6447
  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");
6448
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
6449
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
6450
+ const variationsDir = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
6451
+ const variationFilePath = path13.join(variationsDir, `${variationSlug}.ts`);
6452
+ const variationsIndexPath = path13.join(variationsDir, "index.ts");
6453
+ const shouldRemoveVariationsDirOnRollback = !fs6.existsSync(variationsDir);
5054
6454
  const mutationSnapshot = {
5055
6455
  fileSources: await snapshotWorkspaceFiles([
5056
6456
  blockConfigPath,
@@ -5058,11 +6458,14 @@ async function runAddVariationCommand({
5058
6458
  variationsIndexPath
5059
6459
  ]),
5060
6460
  snapshotDirs: [],
5061
- targetPaths: [variationFilePath]
6461
+ targetPaths: [
6462
+ variationFilePath,
6463
+ ...shouldRemoveVariationsDirOnRollback ? [variationsDir] : []
6464
+ ]
5062
6465
  };
5063
6466
  try {
5064
- await fsp8.mkdir(variationsDir, { recursive: true });
5065
- await fsp8.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
6467
+ await fsp9.mkdir(variationsDir, { recursive: true });
6468
+ await fsp9.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
5066
6469
  await writeVariationRegistry(workspace.projectDir, blockSlug, variationSlug);
5067
6470
  await ensureVariationRegistrationHook(blockIndexPath);
5068
6471
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -5078,6 +6481,115 @@ async function runAddVariationCommand({
5078
6481
  throw error;
5079
6482
  }
5080
6483
  }
6484
+ async function runAddBlockStyleCommand({
6485
+ blockName,
6486
+ cwd = process.cwd(),
6487
+ styleName
6488
+ }) {
6489
+ const workspace = resolveWorkspaceProject(cwd);
6490
+ const blockSlug = normalizeBlockSlug(blockName);
6491
+ const styleSlug = assertValidGeneratedSlug("Style name", normalizeBlockSlug(styleName), "wp-typia add style <name> --block <block-slug>");
6492
+ const inventory = readWorkspaceInventory(workspace.projectDir);
6493
+ resolveWorkspaceBlock(inventory, blockSlug);
6494
+ assertBlockStyleDoesNotExist(workspace.projectDir, blockSlug, styleSlug, inventory);
6495
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
6496
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
6497
+ const stylesDir = path13.join(workspace.projectDir, "src", "blocks", blockSlug, "styles");
6498
+ const styleFilePath = path13.join(stylesDir, `${styleSlug}.ts`);
6499
+ const stylesIndexPath = path13.join(stylesDir, "index.ts");
6500
+ const shouldRemoveStylesDirOnRollback = !fs6.existsSync(stylesDir);
6501
+ const mutationSnapshot = {
6502
+ fileSources: await snapshotWorkspaceFiles([
6503
+ blockConfigPath,
6504
+ blockIndexPath,
6505
+ stylesIndexPath
6506
+ ]),
6507
+ snapshotDirs: [],
6508
+ targetPaths: [
6509
+ styleFilePath,
6510
+ ...shouldRemoveStylesDirOnRollback ? [stylesDir] : []
6511
+ ]
6512
+ };
6513
+ try {
6514
+ await fsp9.mkdir(stylesDir, { recursive: true });
6515
+ await fsp9.writeFile(styleFilePath, buildBlockStyleSource(styleSlug, workspace.workspace.textDomain), "utf8");
6516
+ await writeBlockStyleRegistry(workspace.projectDir, blockSlug, styleSlug);
6517
+ await ensureBlockStyleRegistrationHook(blockIndexPath);
6518
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
6519
+ blockStyleEntries: [buildBlockStyleConfigEntry(blockSlug, styleSlug)]
6520
+ });
6521
+ return {
6522
+ blockSlug,
6523
+ projectDir: workspace.projectDir,
6524
+ styleSlug
6525
+ };
6526
+ } catch (error) {
6527
+ await rollbackWorkspaceMutation(mutationSnapshot);
6528
+ throw error;
6529
+ }
6530
+ }
6531
+ async function runAddBlockTransformCommand({
6532
+ cwd = process.cwd(),
6533
+ fromBlockName,
6534
+ toBlockName,
6535
+ transformName
6536
+ }) {
6537
+ const workspace = resolveWorkspaceProject(cwd);
6538
+ const transformSlug = assertValidGeneratedSlug("Transform name", normalizeBlockSlug(transformName), "wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug>");
6539
+ const resolvedFromBlockName = assertFullBlockName(fromBlockName, "--from");
6540
+ const target = resolveWorkspaceTargetBlockName(toBlockName, workspace.workspace.namespace, "--to");
6541
+ const inventory = readWorkspaceInventory(workspace.projectDir);
6542
+ resolveWorkspaceBlock(inventory, target.blockSlug);
6543
+ assertBlockTransformDoesNotExist(workspace.projectDir, target.blockSlug, transformSlug, inventory);
6544
+ const blockConfigPath = path13.join(workspace.projectDir, "scripts", "block-config.ts");
6545
+ const blockIndexPath = path13.join(workspace.projectDir, "src", "blocks", target.blockSlug, "index.tsx");
6546
+ const transformsDir = path13.join(workspace.projectDir, "src", "blocks", target.blockSlug, "transforms");
6547
+ const transformFilePath = path13.join(transformsDir, `${transformSlug}.ts`);
6548
+ const transformsIndexPath = path13.join(transformsDir, "index.ts");
6549
+ const shouldRemoveTransformsDirOnRollback = !fs6.existsSync(transformsDir);
6550
+ const mutationSnapshot = {
6551
+ fileSources: await snapshotWorkspaceFiles([
6552
+ blockConfigPath,
6553
+ blockIndexPath,
6554
+ transformsIndexPath
6555
+ ]),
6556
+ snapshotDirs: [],
6557
+ targetPaths: [
6558
+ transformFilePath,
6559
+ ...shouldRemoveTransformsDirOnRollback ? [transformsDir] : []
6560
+ ]
6561
+ };
6562
+ try {
6563
+ await fsp9.mkdir(transformsDir, { recursive: true });
6564
+ await fsp9.writeFile(transformFilePath, buildBlockTransformSource({
6565
+ fromBlockName: resolvedFromBlockName,
6566
+ textDomain: workspace.workspace.textDomain,
6567
+ transformSlug
6568
+ }), "utf8");
6569
+ await writeBlockTransformRegistry(workspace.projectDir, target.blockSlug, transformSlug);
6570
+ await ensureBlockTransformRegistrationHook(blockIndexPath);
6571
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
6572
+ blockTransformEntries: [
6573
+ buildBlockTransformConfigEntry({
6574
+ blockSlug: target.blockSlug,
6575
+ fromBlockName: resolvedFromBlockName,
6576
+ toBlockName: target.blockName,
6577
+ transformSlug
6578
+ })
6579
+ ]
6580
+ });
6581
+ return {
6582
+ blockSlug: target.blockSlug,
6583
+ fromBlockName: resolvedFromBlockName,
6584
+ projectDir: workspace.projectDir,
6585
+ toBlockName: target.blockName,
6586
+ transformSlug
6587
+ };
6588
+ } catch (error) {
6589
+ await rollbackWorkspaceMutation(mutationSnapshot);
6590
+ throw error;
6591
+ }
6592
+ }
5081
6593
  async function runAddHookedBlockCommand({
5082
6594
  anchorBlockName,
5083
6595
  blockName,
@@ -5095,7 +6607,7 @@ async function runAddHookedBlockCommand({
5095
6607
  throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
5096
6608
  }
5097
6609
  const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
5098
- const blockJsonRelativePath = path12.relative(workspace.projectDir, blockJsonPath);
6610
+ const blockJsonRelativePath = path13.relative(workspace.projectDir, blockJsonPath);
5099
6611
  const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
5100
6612
  if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
5101
6613
  throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
@@ -5107,7 +6619,7 @@ async function runAddHookedBlockCommand({
5107
6619
  };
5108
6620
  try {
5109
6621
  blockHooks[resolvedAnchorBlockName] = resolvedPosition;
5110
- await fsp8.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
6622
+ await fsp9.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
5111
6623
  return {
5112
6624
  anchorBlockName: resolvedAnchorBlockName,
5113
6625
  blockSlug,
@@ -5126,9 +6638,12 @@ export {
5126
6638
  runAddPatternCommand,
5127
6639
  runAddHookedBlockCommand,
5128
6640
  runAddEditorPluginCommand,
6641
+ runAddBlockTransformCommand,
6642
+ runAddBlockStyleCommand,
5129
6643
  runAddBlockCommand,
5130
6644
  runAddBindingSourceCommand,
5131
6645
  runAddAiFeatureCommand,
6646
+ runAddAdminViewCommand,
5132
6647
  runAddAbilityCommand,
5133
6648
  getWorkspaceBlockSelectOptions,
5134
6649
  formatAddHelpText,
@@ -5137,4 +6652,4 @@ export {
5137
6652
  ADD_BLOCK_TEMPLATE_IDS
5138
6653
  };
5139
6654
 
5140
- //# debugId=B375BD42A8A3F6C764756E2164756E21
6655
+ //# debugId=7C11AF8BB98BD78764756E2164756E21