@wp-typia/project-tools 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,886 @@
1
+ import apiFetch from '@wordpress/api-fetch';
2
+ import { parse, serialize, type BlockInstance } from '@wordpress/blocks';
3
+ import typia from 'typia';
4
+
5
+ import migrationBlocks from './generated';
6
+ import {
7
+ type ManifestAttribute,
8
+ type ManifestDocument,
9
+ type MigrationRiskSummary,
10
+ manifestMatchesDocument,
11
+ summarizeVersionDelta,
12
+ } from './helpers';
13
+
14
+ export interface MigrationAnalysis {
15
+ needsMigration: boolean;
16
+ currentMigrationVersion: string;
17
+ targetMigrationVersion: string;
18
+ confidence: number;
19
+ reasons: string[];
20
+ riskSummary: MigrationRiskSummary;
21
+ warnings: string[];
22
+ affectedFields: {
23
+ added: string[];
24
+ changed: string[];
25
+ removed: string[];
26
+ };
27
+ }
28
+
29
+ export interface UnionBranchPreview {
30
+ field: string;
31
+ legacyBranch: string | null;
32
+ nextBranch: string | null;
33
+ status: 'auto' | 'current' | 'manual' | 'unknown';
34
+ }
35
+
36
+ export interface MigrationPreview {
37
+ after: Record< string, unknown > | null;
38
+ before: Record< string, unknown >;
39
+ changedFields: string[];
40
+ unresolved: string[];
41
+ unionBranches: UnionBranchPreview[];
42
+ validationErrors: string[];
43
+ }
44
+
45
+ export interface BlockScanResult {
46
+ analysis: MigrationAnalysis;
47
+ attributes: Record< string, unknown >;
48
+ blockName: string;
49
+ blockPath: number[];
50
+ postId: number;
51
+ postTitle: string;
52
+ postType: string;
53
+ preview: MigrationPreview;
54
+ rawContent: string;
55
+ restBase: string;
56
+ targetKey: string;
57
+ }
58
+
59
+ export interface BatchMigrationBlockResult {
60
+ blockName: string;
61
+ blockPath: number[];
62
+ currentMigrationVersion: string;
63
+ preview: MigrationPreview;
64
+ reason?: string;
65
+ status: 'failed' | 'success';
66
+ targetMigrationVersion: string;
67
+ }
68
+
69
+ export interface BatchMigrationPostResult {
70
+ postId: number;
71
+ postTitle: string;
72
+ postType: string;
73
+ previews: BatchMigrationBlockResult[];
74
+ reason?: string;
75
+ status: 'failed' | 'success';
76
+ }
77
+
78
+ export interface BatchMigrationResult {
79
+ errors: Array< { postId: number; reason: string } >;
80
+ failed: number;
81
+ posts: BatchMigrationPostResult[];
82
+ successful: number;
83
+ total: number;
84
+ }
85
+
86
+ interface GroupedScanResult {
87
+ postId: number;
88
+ postTitle: string;
89
+ postType: string;
90
+ rawContent: string;
91
+ restBase: string;
92
+ results: BlockScanResult[];
93
+ }
94
+
95
+ interface EditablePostUpdateRequest {
96
+ content: string;
97
+ }
98
+
99
+ type ParsedBlock = BlockInstance< Record< string, unknown > >;
100
+
101
+ interface EditablePostType {
102
+ rest_base: string;
103
+ slug: string;
104
+ }
105
+
106
+ interface EditablePostRecord {
107
+ content?: {
108
+ raw?: string;
109
+ };
110
+ id: number;
111
+ title?: {
112
+ rendered?: string;
113
+ };
114
+ }
115
+
116
+ interface GeneratedMigrationRegistry {
117
+ currentManifest: ManifestDocument;
118
+ currentMigrationVersion: string;
119
+ entries: Array< {
120
+ fromMigrationVersion: string;
121
+ manifest: ManifestDocument;
122
+ riskSummary: MigrationRiskSummary;
123
+ rule: {
124
+ migrate( input: Record< string, unknown > ): Record< string, unknown >;
125
+ unresolved?: readonly string[];
126
+ };
127
+ } >;
128
+ }
129
+
130
+ interface MigrationTargetRuntime {
131
+ blockName: string;
132
+ deprecated: readonly unknown[];
133
+ key: string;
134
+ legacyMigrationVersions: readonly string[];
135
+ registry: GeneratedMigrationRegistry;
136
+ validators: {
137
+ validate(
138
+ attributes: unknown
139
+ ): {
140
+ errors?: Array< { expected?: string; path?: string } >;
141
+ isValid: boolean;
142
+ };
143
+ };
144
+ }
145
+
146
+ interface MigrationResolution {
147
+ analysis: MigrationAnalysis;
148
+ preview: MigrationPreview;
149
+ }
150
+
151
+ const EMPTY_RISK_SUMMARY: MigrationRiskSummary = {
152
+ additive: {
153
+ count: 0,
154
+ items: [],
155
+ },
156
+ rename: {
157
+ count: 0,
158
+ items: [],
159
+ },
160
+ semanticTransform: {
161
+ count: 0,
162
+ items: [],
163
+ },
164
+ unionBreaking: {
165
+ count: 0,
166
+ items: [],
167
+ },
168
+ };
169
+
170
+ const targets = migrationBlocks as readonly MigrationTargetRuntime[];
171
+
172
+ function formatRiskSummary( riskSummary: MigrationRiskSummary ): string {
173
+ return `additive ${ riskSummary.additive.count }, rename ${ riskSummary.rename.count }, transform ${ riskSummary.semanticTransform.count }, union breaking ${ riskSummary.unionBreaking.count }`;
174
+ }
175
+
176
+ function getTargetByBlockName(
177
+ blockName: string
178
+ ): MigrationTargetRuntime | undefined {
179
+ return targets.find( ( target ) => target.blockName === blockName );
180
+ }
181
+
182
+ export function detectBlockMigration(
183
+ blockName: string,
184
+ attributes: Record< string, unknown >
185
+ ): MigrationAnalysis {
186
+ const target = getTargetByBlockName( blockName );
187
+ if ( ! target ) {
188
+ throw new Error( `Unsupported migration block: ${ blockName }` );
189
+ }
190
+
191
+ return resolveMigrationState( target, attributes ).analysis;
192
+ }
193
+
194
+ export function autoMigrateBlock(
195
+ blockName: string,
196
+ attributes: Record< string, unknown >
197
+ ) {
198
+ const target = getTargetByBlockName( blockName );
199
+ if ( ! target ) {
200
+ throw new Error( `Unsupported migration block: ${ blockName }` );
201
+ }
202
+
203
+ const resolution = resolveMigrationState( target, attributes );
204
+ if ( ! resolution.preview.after ) {
205
+ throw new Error(
206
+ resolution.preview.validationErrors[ 0 ] ??
207
+ resolution.preview.unresolved[ 0 ] ??
208
+ `Unable to migrate block attributes for ${ blockName }.`
209
+ );
210
+ }
211
+
212
+ return resolution.preview.after;
213
+ }
214
+
215
+ export async function scanSiteForMigrations(
216
+ blockNames: readonly string[] = targets.map( ( target ) => target.blockName )
217
+ ): Promise< BlockScanResult[] > {
218
+ const allowedBlockNames = new Set( blockNames );
219
+ const postTypes = await fetchEditablePostTypes();
220
+ const results: BlockScanResult[] = [];
221
+
222
+ for ( const postType of postTypes ) {
223
+ const posts = await fetchAllPosts( postType.rest_base );
224
+ for ( const post of posts ) {
225
+ const content = post?.content?.raw;
226
+ if ( typeof content !== 'string' || content.length === 0 ) {
227
+ continue;
228
+ }
229
+
230
+ const blocks = parse( content );
231
+ walkBlocks( blocks, [], ( block, blockPath ) => {
232
+ if ( ! block.name || ! allowedBlockNames.has( block.name ) ) {
233
+ return;
234
+ }
235
+
236
+ const target = getTargetByBlockName( block.name );
237
+ if ( ! target ) {
238
+ return;
239
+ }
240
+
241
+ const attributes = ( block.attributes ?? {} ) as Record<
242
+ string,
243
+ unknown
244
+ >;
245
+ const resolution = resolveMigrationState( target, attributes );
246
+
247
+ if (
248
+ resolution.analysis.needsMigration ||
249
+ resolution.preview.changedFields.length > 0 ||
250
+ resolution.preview.unresolved.length > 0 ||
251
+ resolution.preview.validationErrors.length > 0
252
+ ) {
253
+ results.push( {
254
+ analysis: resolution.analysis,
255
+ attributes,
256
+ blockName: block.name,
257
+ blockPath,
258
+ postId: post.id,
259
+ postTitle: post?.title?.rendered ?? `Post ${ post.id }`,
260
+ postType: postType.slug,
261
+ preview: resolution.preview,
262
+ rawContent: content,
263
+ restBase: postType.rest_base,
264
+ targetKey: target.key,
265
+ } );
266
+ }
267
+ } );
268
+ }
269
+ }
270
+
271
+ return results;
272
+ }
273
+
274
+ export async function batchMigrateScanResults(
275
+ results: BlockScanResult[],
276
+ { dryRun = false }: { dryRun?: boolean } = {}
277
+ ): Promise< BatchMigrationResult > {
278
+ const grouped = groupResultsByPost( results );
279
+ const summary: BatchMigrationResult = {
280
+ errors: [],
281
+ failed: 0,
282
+ posts: [],
283
+ successful: 0,
284
+ total: Object.keys( grouped ).length,
285
+ };
286
+
287
+ for ( const group of Object.values( grouped ) ) {
288
+ const blockPreviews = group.results.map( ( result ) => {
289
+ const target = getTargetByBlockName( result.blockName );
290
+ if ( ! target ) {
291
+ return {
292
+ blockName: result.blockName,
293
+ blockPath: result.blockPath,
294
+ currentMigrationVersion: 'unknown',
295
+ preview: result.preview,
296
+ reason: `Unsupported migration block: ${ result.blockName }`,
297
+ status: 'failed',
298
+ targetMigrationVersion: 'unknown',
299
+ } satisfies BatchMigrationBlockResult;
300
+ }
301
+ const resolution = resolveMigrationState( target, result.attributes );
302
+ const reason =
303
+ resolution.preview.validationErrors[ 0 ] ??
304
+ resolution.preview.unresolved[ 0 ] ??
305
+ undefined;
306
+ const status =
307
+ resolution.preview.after &&
308
+ resolution.preview.unresolved.length === 0 &&
309
+ resolution.preview.validationErrors.length === 0
310
+ ? 'success'
311
+ : 'failed';
312
+
313
+ return {
314
+ blockName: result.blockName,
315
+ blockPath: result.blockPath,
316
+ currentMigrationVersion: resolution.analysis.currentMigrationVersion,
317
+ preview: resolution.preview,
318
+ reason,
319
+ status,
320
+ targetMigrationVersion: resolution.analysis.targetMigrationVersion,
321
+ } satisfies BatchMigrationBlockResult;
322
+ } );
323
+
324
+ const failedPreview = blockPreviews.find(
325
+ ( preview ) => preview.status === 'failed'
326
+ );
327
+ if ( failedPreview ) {
328
+ summary.failed += 1;
329
+ summary.errors.push( {
330
+ postId: group.postId,
331
+ reason:
332
+ failedPreview.reason ??
333
+ 'One or more blocks could not be migrated.',
334
+ } );
335
+ summary.posts.push( {
336
+ postId: group.postId,
337
+ postTitle: group.postTitle,
338
+ postType: group.postType,
339
+ previews: blockPreviews,
340
+ reason: failedPreview.reason,
341
+ status: 'failed',
342
+ } );
343
+ continue;
344
+ }
345
+
346
+ const migratedContent = migratePostContent(
347
+ blockPreviews,
348
+ group.rawContent
349
+ );
350
+ if ( ! dryRun ) {
351
+ const latestPost = await fetchPostById(
352
+ group.restBase,
353
+ group.postId
354
+ );
355
+ const latestContent = latestPost.content?.raw;
356
+ if (
357
+ typeof latestContent !== 'string' ||
358
+ latestContent !== group.rawContent
359
+ ) {
360
+ summary.failed += 1;
361
+ summary.errors.push( {
362
+ postId: group.postId,
363
+ reason: 'Post content changed after the scan. Re-run the migration scan before writing.',
364
+ } );
365
+ summary.posts.push( {
366
+ postId: group.postId,
367
+ postTitle: group.postTitle,
368
+ postType: group.postType,
369
+ previews: blockPreviews,
370
+ reason: 'Post content changed after the scan. Re-run the migration scan before writing.',
371
+ status: 'failed',
372
+ } );
373
+ continue;
374
+ }
375
+
376
+ await apiFetch( {
377
+ body: typia.json.assertStringify< EditablePostUpdateRequest >( {
378
+ content: migratedContent,
379
+ } ),
380
+ headers: {
381
+ 'Content-Type': 'application/json',
382
+ },
383
+ method: 'POST',
384
+ path: `/wp/v2/${ group.restBase }/${ group.postId }`,
385
+ } );
386
+ }
387
+
388
+ summary.successful += 1;
389
+ summary.posts.push( {
390
+ postId: group.postId,
391
+ postTitle: group.postTitle,
392
+ postType: group.postType,
393
+ previews: blockPreviews,
394
+ status: 'success',
395
+ } );
396
+ }
397
+
398
+ return summary;
399
+ }
400
+
401
+ export function generateMigrationReport(
402
+ scanResults: BlockScanResult[]
403
+ ): string {
404
+ let report = '# Migration Report\n\n';
405
+ report += `- Supported block targets: ${ targets.length }\n`;
406
+ report += `- Supported deprecated entries: ${ targets.reduce( ( total, target ) => total + target.deprecated.length, 0 ) }\n`;
407
+ report += `- Scan results needing attention: ${ scanResults.length }\n\n`;
408
+
409
+ for ( const entry of scanResults ) {
410
+ report += `## ${ entry.postTitle } (#${ entry.postId })\n`;
411
+ report += `- Block: ${ entry.blockName }\n`;
412
+ report += `- Migration version: ${ entry.analysis.currentMigrationVersion } -> ${ entry.analysis.targetMigrationVersion }\n`;
413
+ report += `- Confidence: ${ entry.analysis.confidence }\n`;
414
+ report += `- Risk summary: ${ formatRiskSummary(
415
+ entry.analysis.riskSummary
416
+ ) }\n`;
417
+ if ( entry.preview.changedFields.length > 0 ) {
418
+ report += `- Changed fields: ${ entry.preview.changedFields.join(
419
+ ', '
420
+ ) }\n`;
421
+ }
422
+ if ( entry.preview.unionBranches.length > 0 ) {
423
+ report += `- Union branches:\n`;
424
+ for ( const branch of entry.preview.unionBranches ) {
425
+ report += ` - ${ branch.field }: ${
426
+ branch.legacyBranch ?? 'unknown'
427
+ } -> ${ branch.nextBranch ?? 'unknown' } (${
428
+ branch.status
429
+ })\n`;
430
+ }
431
+ }
432
+ if ( entry.preview.unresolved.length > 0 ) {
433
+ report += `- Unresolved: ${ entry.preview.unresolved.join(
434
+ ', '
435
+ ) }\n`;
436
+ }
437
+ if ( entry.preview.validationErrors.length > 0 ) {
438
+ report += `- Validation errors: ${ entry.preview.validationErrors.join(
439
+ ', '
440
+ ) }\n`;
441
+ }
442
+ report += '\n### Before\n\n```json\n';
443
+ report += `${ JSON.stringify( entry.preview.before, null, 2 ) }\n`;
444
+ report += '```\n\n';
445
+ report += '### After\n\n```json\n';
446
+ report += `${ JSON.stringify( entry.preview.after, null, 2 ) }\n`;
447
+ report += '```\n\n';
448
+ }
449
+
450
+ return report;
451
+ }
452
+
453
+ export const migrationUtils = {
454
+ getStats() {
455
+ return {
456
+ targets: targets.map( ( target ) => ( {
457
+ blockName: target.blockName,
458
+ currentMigrationVersion: target.registry.currentMigrationVersion,
459
+ deprecatedEntries: target.deprecated.length,
460
+ key: target.key,
461
+ legacyMigrationVersions: target.legacyMigrationVersions,
462
+ supportedMigrationVersions: [
463
+ ...target.legacyMigrationVersions,
464
+ target.registry.currentMigrationVersion,
465
+ ],
466
+ } ) ),
467
+ };
468
+ },
469
+ testMigration(
470
+ blockName: string,
471
+ attributes: Record< string, unknown >
472
+ ): Record< string, unknown > {
473
+ return autoMigrateBlock( blockName, attributes );
474
+ },
475
+ };
476
+
477
+ async function fetchEditablePostTypes(): Promise< EditablePostType[] > {
478
+ const result = ( await apiFetch( {
479
+ path: '/wp/v2/types?context=edit',
480
+ } ) ) as Record<
481
+ string,
482
+ { rest_base?: string; slug?: string; viewable?: boolean }
483
+ >;
484
+
485
+ return Object.values( result )
486
+ .filter( ( postType ) => postType?.viewable && postType?.rest_base )
487
+ .map( ( postType ) => ( {
488
+ rest_base: postType.rest_base!,
489
+ slug: postType.slug!,
490
+ } ) );
491
+ }
492
+
493
+ async function fetchAllPosts(
494
+ restBase: string
495
+ ): Promise< EditablePostRecord[] > {
496
+ let page = 1;
497
+ const entries: EditablePostRecord[] = [];
498
+
499
+ while ( true ) {
500
+ const result = ( await apiFetch( {
501
+ parse: false,
502
+ path: `/wp/v2/${ restBase }?context=edit&per_page=100&page=${ page }`,
503
+ } ) ) as Response;
504
+ const pageEntries = ( await result.json() ) as EditablePostRecord[];
505
+ entries.push( ...pageEntries );
506
+ const totalPages = Number.parseInt(
507
+ result.headers.get( 'X-WP-TotalPages' ) ?? '1',
508
+ 10
509
+ );
510
+ page += 1;
511
+ if ( page > totalPages ) {
512
+ break;
513
+ }
514
+ }
515
+
516
+ return entries;
517
+ }
518
+
519
+ async function fetchPostById(
520
+ restBase: string,
521
+ postId: number
522
+ ): Promise< EditablePostRecord > {
523
+ return ( await apiFetch( {
524
+ path: `/wp/v2/${ restBase }/${ postId }?context=edit`,
525
+ } ) ) as EditablePostRecord;
526
+ }
527
+
528
+ function walkBlocks(
529
+ blocks: ParsedBlock[],
530
+ pathPrefix: number[],
531
+ visitor: ( block: ParsedBlock, path: number[] ) => void
532
+ ): void {
533
+ blocks.forEach( ( block, index ) => {
534
+ const blockPath = [ ...pathPrefix, index ];
535
+ visitor( block, blockPath );
536
+ if (
537
+ Array.isArray( block.innerBlocks ) &&
538
+ block.innerBlocks.length > 0
539
+ ) {
540
+ walkBlocks( block.innerBlocks, blockPath, visitor );
541
+ }
542
+ } );
543
+ }
544
+
545
+ function groupResultsByPost(
546
+ results: BlockScanResult[]
547
+ ): Record< string, GroupedScanResult > {
548
+ return results.reduce< Record< string, GroupedScanResult > >(
549
+ ( accumulator, result ) => {
550
+ const key = `${ result.restBase }:${ result.postId }`;
551
+ if ( ! accumulator[ key ] ) {
552
+ accumulator[ key ] = {
553
+ postId: result.postId,
554
+ postTitle: result.postTitle,
555
+ postType: result.postType,
556
+ rawContent: result.rawContent,
557
+ restBase: result.restBase,
558
+ results: [],
559
+ };
560
+ }
561
+ accumulator[ key ].results.push( result );
562
+ return accumulator;
563
+ },
564
+ {}
565
+ );
566
+ }
567
+
568
+ function migratePostContent(
569
+ results: BatchMigrationBlockResult[],
570
+ rawContent: string
571
+ ): string {
572
+ const replacements = new Map(
573
+ results
574
+ .filter( ( result ) => result.preview.after )
575
+ .map( ( result ) => [
576
+ result.blockPath.join( '.' ),
577
+ result.preview.after as Record< string, unknown >,
578
+ ] )
579
+ );
580
+ const blocks = parse( rawContent ) as ParsedBlock[];
581
+ const nextBlocks = replaceBlocks( blocks, [], replacements );
582
+ return serialize( nextBlocks );
583
+ }
584
+
585
+ function replaceBlocks(
586
+ blocks: ParsedBlock[],
587
+ pathPrefix: number[],
588
+ replacements: Map< string, Record< string, unknown > >
589
+ ): ParsedBlock[] {
590
+ return blocks.map( ( block, index ) => {
591
+ const blockPath = [ ...pathPrefix, index ];
592
+ const replacement = replacements.get( blockPath.join( '.' ) );
593
+ const innerBlocks = Array.isArray( block.innerBlocks )
594
+ ? replaceBlocks( block.innerBlocks, blockPath, replacements )
595
+ : [];
596
+
597
+ if ( ! replacement ) {
598
+ return {
599
+ ...block,
600
+ innerBlocks,
601
+ };
602
+ }
603
+
604
+ return {
605
+ ...block,
606
+ attributes: replacement,
607
+ innerBlocks,
608
+ };
609
+ } );
610
+ }
611
+
612
+ function resolveMigrationState(
613
+ target: MigrationTargetRuntime,
614
+ attributes: Record< string, unknown >
615
+ ): MigrationResolution {
616
+ const currentValidation = target.validators.validate( attributes as any );
617
+ if ( currentValidation.isValid ) {
618
+ return {
619
+ analysis: {
620
+ affectedFields: {
621
+ added: [],
622
+ changed: [],
623
+ removed: [],
624
+ },
625
+ confidence: 1,
626
+ currentMigrationVersion: target.registry.currentMigrationVersion,
627
+ needsMigration: false,
628
+ reasons: [ 'Current Typia validator accepted the attributes.' ],
629
+ riskSummary: EMPTY_RISK_SUMMARY,
630
+ targetMigrationVersion: target.registry.currentMigrationVersion,
631
+ warnings: [],
632
+ } satisfies MigrationAnalysis,
633
+ preview: createPreview( {
634
+ after: attributes,
635
+ before: attributes,
636
+ currentManifest:
637
+ target.registry.currentManifest as ManifestDocument,
638
+ legacyManifest: null,
639
+ status: 'current',
640
+ unresolved: [],
641
+ validationErrors: [],
642
+ } ),
643
+ };
644
+ }
645
+
646
+ for ( const entry of target.registry.entries ) {
647
+ if (
648
+ manifestMatchesDocument(
649
+ entry.manifest as ManifestDocument,
650
+ attributes
651
+ )
652
+ ) {
653
+ const migrated = entry.rule.migrate( attributes );
654
+ const migratedValidation = target.validators.validate(
655
+ migrated as any
656
+ );
657
+ const unresolved = Array.isArray( entry.rule.unresolved )
658
+ ? [ ...entry.rule.unresolved ]
659
+ : [];
660
+ const validationErrors = migratedValidation.isValid
661
+ ? []
662
+ : formatValidationErrors( migratedValidation.errors );
663
+ let status: 'auto' | 'manual' = 'manual';
664
+ if ( migratedValidation.isValid && unresolved.length === 0 ) {
665
+ status = 'auto';
666
+ }
667
+ const preview = createPreview( {
668
+ after: migratedValidation.isValid
669
+ ? ( migrated as Record< string, unknown > )
670
+ : null,
671
+ before: attributes,
672
+ currentManifest:
673
+ target.registry.currentManifest as ManifestDocument,
674
+ legacyManifest: entry.manifest as ManifestDocument,
675
+ status,
676
+ unresolved,
677
+ validationErrors,
678
+ } );
679
+ const delta = summarizeVersionDelta(
680
+ entry.manifest as ManifestDocument,
681
+ target.registry.currentManifest as ManifestDocument
682
+ );
683
+
684
+ return {
685
+ analysis: {
686
+ affectedFields: delta,
687
+ confidence: unresolved.length > 0 ? 0.8 : 0.95,
688
+ currentMigrationVersion: entry.fromMigrationVersion,
689
+ needsMigration: true,
690
+ reasons: [
691
+ `Snapshot ${ entry.fromMigrationVersion } matched this block.`,
692
+ ...preview.unionBranches.map(
693
+ ( branch ) =>
694
+ `Union ${ branch.field }: ${
695
+ branch.legacyBranch ?? 'unknown'
696
+ } -> ${ branch.nextBranch ?? 'unknown' } (${
697
+ branch.status
698
+ })`
699
+ ),
700
+ ],
701
+ riskSummary: entry.riskSummary ?? EMPTY_RISK_SUMMARY,
702
+ targetMigrationVersion: target.registry.currentMigrationVersion,
703
+ warnings: [ ...unresolved, ...validationErrors ],
704
+ } satisfies MigrationAnalysis,
705
+ preview,
706
+ };
707
+ }
708
+ }
709
+
710
+ return {
711
+ analysis: {
712
+ affectedFields: {
713
+ added: [],
714
+ changed: [],
715
+ removed: [],
716
+ },
717
+ confidence: 0.2,
718
+ currentMigrationVersion: 'unknown',
719
+ needsMigration: true,
720
+ reasons: [
721
+ 'No legacy snapshot matched and current Typia validator rejected the attributes.',
722
+ ],
723
+ riskSummary: EMPTY_RISK_SUMMARY,
724
+ targetMigrationVersion: target.registry.currentMigrationVersion,
725
+ warnings: formatValidationErrors( currentValidation.errors ),
726
+ } satisfies MigrationAnalysis,
727
+ preview: createPreview( {
728
+ after: null,
729
+ before: attributes,
730
+ currentManifest:
731
+ target.registry.currentManifest as ManifestDocument,
732
+ legacyManifest: null,
733
+ status: 'unknown',
734
+ unresolved: [
735
+ 'Manual migration review is required because the block does not match any supported snapshot.',
736
+ ],
737
+ validationErrors: formatValidationErrors(
738
+ currentValidation.errors
739
+ ),
740
+ } ),
741
+ };
742
+ }
743
+
744
+ function createPreview( {
745
+ after,
746
+ before,
747
+ currentManifest,
748
+ legacyManifest,
749
+ status,
750
+ unresolved,
751
+ validationErrors,
752
+ }: {
753
+ after: Record< string, unknown > | null;
754
+ before: Record< string, unknown >;
755
+ currentManifest: ManifestDocument;
756
+ legacyManifest: ManifestDocument | null;
757
+ status: UnionBranchPreview[ 'status' ];
758
+ unresolved: string[];
759
+ validationErrors: string[];
760
+ } ): MigrationPreview {
761
+ return {
762
+ after,
763
+ before,
764
+ changedFields: after ? collectChangedFieldPaths( before, after ) : [],
765
+ unresolved,
766
+ unionBranches: collectUnionBranchPreview(
767
+ legacyManifest,
768
+ currentManifest,
769
+ before,
770
+ after,
771
+ status
772
+ ),
773
+ validationErrors,
774
+ };
775
+ }
776
+
777
+ function collectChangedFieldPaths(
778
+ before: Record< string, unknown >,
779
+ after: Record< string, unknown >,
780
+ prefix = ''
781
+ ): string[] {
782
+ const keys = new Set( [
783
+ ...Object.keys( before ),
784
+ ...Object.keys( after ),
785
+ ] );
786
+ const changes: string[] = [];
787
+
788
+ for ( const key of keys ) {
789
+ const nextPrefix = prefix ? `${ prefix }.${ key }` : key;
790
+ const left = before[ key ];
791
+ const right = after[ key ];
792
+
793
+ if ( isPlainObject( left ) && isPlainObject( right ) ) {
794
+ changes.push(
795
+ ...collectChangedFieldPaths( left, right, nextPrefix )
796
+ );
797
+ continue;
798
+ }
799
+ if ( JSON.stringify( left ) !== JSON.stringify( right ) ) {
800
+ changes.push( nextPrefix );
801
+ }
802
+ }
803
+
804
+ return changes;
805
+ }
806
+
807
+ function collectUnionBranchPreview(
808
+ legacyManifest: ManifestDocument | null,
809
+ currentManifest: ManifestDocument,
810
+ before: Record< string, unknown >,
811
+ after: Record< string, unknown > | null,
812
+ status: UnionBranchPreview[ 'status' ]
813
+ ): UnionBranchPreview[] {
814
+ const fieldNames = new Set< string >();
815
+
816
+ for ( const [ field, attribute ] of Object.entries(
817
+ legacyManifest?.attributes ?? {}
818
+ ) ) {
819
+ if ( attribute.ts.kind === 'union' ) {
820
+ fieldNames.add( field );
821
+ }
822
+ }
823
+ for ( const [ field, attribute ] of Object.entries(
824
+ currentManifest.attributes ?? {}
825
+ ) ) {
826
+ if ( attribute.ts.kind === 'union' ) {
827
+ fieldNames.add( field );
828
+ }
829
+ }
830
+
831
+ return [ ...fieldNames ].map( ( field ) => {
832
+ const legacyAttribute = legacyManifest?.attributes?.[ field ] ?? null;
833
+ const currentAttribute = currentManifest.attributes?.[ field ] ?? null;
834
+ return {
835
+ field,
836
+ legacyBranch: resolveUnionBranchKey(
837
+ legacyAttribute,
838
+ before[ field ]
839
+ ),
840
+ nextBranch: resolveUnionBranchKey(
841
+ currentAttribute,
842
+ ( after ?? before )[ field ]
843
+ ),
844
+ status,
845
+ };
846
+ } );
847
+ }
848
+
849
+ function resolveUnionBranchKey(
850
+ attribute: ManifestAttribute | null,
851
+ value: unknown
852
+ ): string | null {
853
+ if (
854
+ ! attribute ||
855
+ attribute.ts.kind !== 'union' ||
856
+ ! attribute.ts.union ||
857
+ ! isPlainObject( value )
858
+ ) {
859
+ return null;
860
+ }
861
+
862
+ const discriminatorValue = value[ attribute.ts.union.discriminator ];
863
+ if ( typeof discriminatorValue !== 'string' ) {
864
+ return null;
865
+ }
866
+
867
+ return discriminatorValue in attribute.ts.union.branches
868
+ ? discriminatorValue
869
+ : null;
870
+ }
871
+
872
+ function formatValidationErrors(
873
+ errors: Array< { expected?: string; path?: string } > = []
874
+ ): string[] {
875
+ return errors.map( ( error ) => {
876
+ const pathLabel = error.path ?? '$';
877
+ const expectedLabel = error.expected ?? 'unknown';
878
+ return `${ pathLabel }: ${ expectedLabel }`;
879
+ } );
880
+ }
881
+
882
+ function isPlainObject( value: unknown ): value is Record< string, unknown > {
883
+ return (
884
+ typeof value === 'object' && value !== null && ! Array.isArray( value )
885
+ );
886
+ }