@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,741 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { MIGRATION_TODO_PREFIX, } from "./migration-constants.js";
4
+ import { summarizeManifest, summarizeUnionBranches } from "./migration-manifest.js";
5
+ import { getSnapshotBlockJsonPath, getSnapshotManifestPath, getSnapshotSavePath, readRuleMetadata, } from "./migration-project.js";
6
+ import { createMigrationRiskSummary, formatMigrationRiskSummary, } from "./migration-risk.js";
7
+ import { escapeForCode, readJson, renderObjectKey, renderPhpValue } from "./migration-utils.js";
8
+ export function formatDiffReport(diff, { includeRiskSummary = true } = {}) {
9
+ const lines = [
10
+ `Migration diff: ${diff.fromVersion} -> ${diff.toVersion}`,
11
+ `Current type: ${diff.currentTypeName}`,
12
+ `Safe changes: ${diff.summary.auto}`,
13
+ `Manual changes: ${diff.summary.manual}`,
14
+ ];
15
+ if (diff.summary.autoItems.length > 0) {
16
+ lines.push("", "Safe changes:");
17
+ for (const item of diff.summary.autoItems) {
18
+ lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
19
+ }
20
+ }
21
+ if (diff.summary.manualItems.length > 0) {
22
+ lines.push("", "Manual review required:");
23
+ for (const item of diff.summary.manualItems) {
24
+ lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
25
+ }
26
+ }
27
+ if (diff.summary.renameCandidates.length > 0) {
28
+ const autoApplied = diff.summary.renameCandidates.filter((item) => item.autoApply);
29
+ const suggested = diff.summary.renameCandidates.filter((item) => !item.autoApply);
30
+ if (autoApplied.length > 0) {
31
+ lines.push("", "Auto-applied renames:");
32
+ for (const item of autoApplied) {
33
+ lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
34
+ }
35
+ }
36
+ if (suggested.length > 0) {
37
+ lines.push("", "Suggested renames:");
38
+ for (const item of suggested) {
39
+ lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
40
+ }
41
+ }
42
+ }
43
+ if (diff.summary.transformSuggestions.length > 0) {
44
+ lines.push("", "Suggested transforms:");
45
+ for (const item of diff.summary.transformSuggestions) {
46
+ lines.push(` - ${item.currentPath}${item.legacyPath ? ` <- ${item.legacyPath}` : ""} (${item.reason})`);
47
+ }
48
+ }
49
+ if (includeRiskSummary) {
50
+ lines.push("", `Risk summary: ${formatMigrationRiskSummary(createMigrationRiskSummary(diff))}`);
51
+ }
52
+ return lines.join("\n");
53
+ }
54
+ export function renderMigrationRuleFile({ block, currentAttributes, currentTypeName, diff, fromVersion, projectDir, rulePath, targetVersion, }) {
55
+ const activeRenameCandidates = diff.summary.renameCandidates.filter((candidate) => candidate.autoApply);
56
+ const suggestedRenameCandidates = diff.summary.renameCandidates.filter((candidate) => !candidate.autoApply);
57
+ const lines = [];
58
+ const ruleDir = path.dirname(rulePath);
59
+ const typesImport = normalizeImportPath(path.relative(ruleDir, path.join(projectDir, block.typesFile)));
60
+ const currentManifestImport = normalizeImportPath(path.relative(ruleDir, path.join(projectDir, block.manifestFile)));
61
+ const helpersImport = normalizeImportPath(path.relative(ruleDir, path.join(projectDir, "src", "migrations", "helpers.ts")), true);
62
+ lines.push(`import type { ${currentTypeName} } from "${typesImport}";`);
63
+ lines.push(`import currentManifest from "${currentManifestImport}";`);
64
+ lines.push(`import {`);
65
+ lines.push(`\ttype RenameMap,`);
66
+ lines.push(`\ttype TransformMap,`);
67
+ lines.push(`\tresolveMigrationAttribute,`);
68
+ lines.push(`} from "${helpersImport}";`);
69
+ lines.push("");
70
+ lines.push(`export const fromVersion = "${fromVersion}" as const;`);
71
+ lines.push(`export const toVersion = "${targetVersion}" as const;`);
72
+ lines.push("");
73
+ lines.push("export const renameMap: RenameMap = {");
74
+ for (const candidate of activeRenameCandidates) {
75
+ lines.push(`\t${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
76
+ }
77
+ for (const candidate of suggestedRenameCandidates) {
78
+ lines.push(`\t// ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
79
+ }
80
+ lines.push("};");
81
+ lines.push("");
82
+ lines.push("export const transforms: TransformMap = {");
83
+ for (const suggestion of diff.summary.transformSuggestions) {
84
+ lines.push(`\t// ${renderObjectKey(suggestion.currentPath)}: (legacyValue, legacyInput) => {`);
85
+ for (const bodyLine of suggestion.bodyLines) {
86
+ lines.push(`\t${bodyLine}`);
87
+ }
88
+ lines.push(`\t// },`);
89
+ }
90
+ lines.push("};");
91
+ lines.push("");
92
+ lines.push("export const unresolved = [");
93
+ for (const item of diff.summary.manualItems) {
94
+ lines.push(`\t"${item.path}: ${item.kind}${item.detail ? ` (${escapeForCode(item.detail)})` : ""}",`);
95
+ }
96
+ for (const candidate of suggestedRenameCandidates) {
97
+ lines.push(`\t"${candidate.currentPath}: rename candidate from ${candidate.legacyPath}",`);
98
+ }
99
+ for (const suggestion of diff.summary.transformSuggestions) {
100
+ lines.push(`\t"${suggestion.currentPath}: transform suggested from ${suggestion.legacyPath ?? suggestion.currentPath}",`);
101
+ }
102
+ lines.push("] as const;");
103
+ lines.push("");
104
+ lines.push(`export function migrate(input: Record<string, unknown>): ${currentTypeName} {`);
105
+ lines.push(`\treturn {`);
106
+ for (const key of Object.keys(currentAttributes)) {
107
+ for (const manualItem of diff.summary.manualItems.filter((item) => item.path === key || item.path.startsWith(`${key}.`))) {
108
+ lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} ${manualItem.path}: ${manualItem.kind}${manualItem.detail ? ` (${manualItem.detail})` : ""}`);
109
+ }
110
+ for (const renameCandidate of suggestedRenameCandidates.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
111
+ lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} consider renameMap[${JSON.stringify(renameCandidate.currentPath)}] = "${renameCandidate.legacyPath}"`);
112
+ }
113
+ for (const suggestion of diff.summary.transformSuggestions.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
114
+ lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} review transforms[${JSON.stringify(suggestion.currentPath)}]`);
115
+ }
116
+ lines.push(`\t\t${key}: resolveMigrationAttribute(currentManifest.attributes.${key}, "${key}", "${key}", input, renameMap, transforms),`);
117
+ }
118
+ lines.push(`\t} as ${currentTypeName};`);
119
+ lines.push("}");
120
+ lines.push("");
121
+ return `${lines.join("\n")}\n`;
122
+ }
123
+ export function renderMigrationRegistryFile(state, blockKey, entries) {
124
+ const block = state.blocks.find((entry) => entry.key === blockKey);
125
+ if (!block) {
126
+ throw new Error(`Unknown migration block target: ${blockKey}`);
127
+ }
128
+ const imports = [
129
+ `import currentManifest from "${normalizeImportPath(path.relative(getGeneratedDir(block, state), path.join(state.projectDir, block.manifestFile)))}";`,
130
+ `import type { ManifestDocument, MigrationRiskSummary } from "${normalizeImportPath(path.relative(getGeneratedDir(block, state), path.join(state.projectDir, "src", "migrations", "helpers.ts")), true)}";`,
131
+ ];
132
+ const body = [];
133
+ entries.forEach(({ entry, riskSummary }, index) => {
134
+ imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
135
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
136
+ body.push(`\t{`);
137
+ body.push(`\t\tfromMigrationVersion: "${entry.fromVersion}",`);
138
+ body.push(`\t\tmanifest: manifest_${index},`);
139
+ body.push(`\t\triskSummary: ${JSON.stringify(riskSummary, null, "\t").replace(/\n/g, "\n\t\t")},`);
140
+ body.push(`\t\trule: rule_${index},`);
141
+ body.push(`\t},`);
142
+ });
143
+ return `/* eslint-disable prettier/prettier, @typescript-eslint/method-signature-style */
144
+ ${imports.join("\n")}
145
+
146
+ interface MigrationRegistryEntry {
147
+ fromMigrationVersion: string;
148
+ manifest: ManifestDocument;
149
+ riskSummary: MigrationRiskSummary;
150
+ rule: {
151
+ migrate(input: Record<string, unknown>): Record<string, unknown>;
152
+ unresolved?: readonly string[];
153
+ };
154
+ }
155
+
156
+ export const migrationRegistry: {
157
+ currentMigrationVersion: string;
158
+ currentManifest: ManifestDocument;
159
+ entries: MigrationRegistryEntry[];
160
+ } = {
161
+ currentMigrationVersion: "${state.config.currentMigrationVersion}",
162
+ currentManifest: currentManifest as ManifestDocument,
163
+ entries: [
164
+ ${body.join("\n")}
165
+ ],
166
+ };
167
+
168
+ export default migrationRegistry;
169
+ `;
170
+ }
171
+ export function renderGeneratedDeprecatedFile(entries) {
172
+ if (entries.length === 0) {
173
+ return `/* eslint-disable prettier/prettier */
174
+ import type { BlockConfiguration } from "@wordpress/blocks";
175
+
176
+ export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [];
177
+ `;
178
+ }
179
+ const imports = [`import type { BlockConfiguration } from "@wordpress/blocks";`];
180
+ const definitions = [];
181
+ const arrayEntries = [];
182
+ entries.forEach((entry, index) => {
183
+ imports.push(`import block_${index} from "${entry.blockJsonImport}";`);
184
+ imports.push(`import save_${index} from "${entry.saveImport}";`);
185
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
186
+ definitions.push(`const deprecated_${index}: NonNullable<BlockConfiguration["deprecated"]>[number] = {`);
187
+ definitions.push(`\tattributes: (block_${index}.attributes ?? {}) as Record<string, unknown>,`);
188
+ definitions.push(`\tsave: save_${index} as BlockConfiguration["save"],`);
189
+ definitions.push(`\tmigrate(attributes: Record<string, unknown>) {`);
190
+ definitions.push(`\t\treturn rule_${index}.migrate(attributes);`);
191
+ definitions.push(`\t},`);
192
+ definitions.push(`};`);
193
+ arrayEntries.push(`deprecated_${index}`);
194
+ });
195
+ return `/* eslint-disable prettier/prettier */
196
+ ${imports.join("\n")}
197
+
198
+ ${definitions.join("\n\n")}
199
+
200
+ export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [${arrayEntries.join(", ")}];
201
+ `;
202
+ }
203
+ export function renderGeneratedMigrationIndexFile(state, entries) {
204
+ if (state.blocks.length === 0) {
205
+ return `export const migrationBlocks = [] as const;\nexport default migrationBlocks;\n`;
206
+ }
207
+ const generatedDir = state.paths.generatedDir;
208
+ const imports = [];
209
+ const definitions = [];
210
+ state.blocks.forEach((block, index) => {
211
+ const scopedEntries = entries.filter((entry) => entry.block.key === block.key);
212
+ const registryImport = block.layout === "legacy" ? "./registry" : `./${block.key}/registry`;
213
+ const deprecatedImport = block.layout === "legacy" ? "./deprecated" : `./${block.key}/deprecated`;
214
+ const validatorsImport = normalizeImportPath(path.relative(generatedDir, path.join(state.projectDir, block.typesFile.replace(/types\.ts$/u, "validators.ts"))), true);
215
+ imports.push(`import registry_${index} from "${registryImport}";`);
216
+ imports.push(`import { deprecated as deprecated_${index} } from "${deprecatedImport}";`);
217
+ imports.push(`import { validators as validators_${index} } from "${validatorsImport}";`);
218
+ definitions.push(`\t{`);
219
+ definitions.push(`\t\tkey: "${block.key}",`);
220
+ definitions.push(`\t\tblockName: "${block.blockName}",`);
221
+ definitions.push(`\t\tregistry: registry_${index},`);
222
+ definitions.push(`\t\tdeprecated: deprecated_${index},`);
223
+ definitions.push(`\t\tvalidators: validators_${index},`);
224
+ definitions.push(`\t\tlegacyMigrationVersions: ${JSON.stringify(scopedEntries.map((entry) => entry.fromVersion))},`);
225
+ definitions.push(`\t},`);
226
+ });
227
+ return `/* eslint-disable prettier/prettier */
228
+ ${imports.join("\n")}
229
+
230
+ export const migrationBlocks = [
231
+ ${definitions.join("\n")}
232
+ ] as const;
233
+
234
+ export default migrationBlocks;
235
+ `;
236
+ }
237
+ export function renderPhpMigrationRegistryFile(state, entries) {
238
+ const blocks = state.blocks.map((block) => {
239
+ const snapshots = Object.fromEntries(state.config.supportedMigrationVersions.map((version) => {
240
+ const manifestPath = getSnapshotManifestPath(state.projectDir, block, version);
241
+ const blockJsonPath = getSnapshotBlockJsonPath(state.projectDir, block, version);
242
+ const savePath = getSnapshotSavePath(state.projectDir, block, version);
243
+ return [
244
+ version,
245
+ {
246
+ blockJson: fs.existsSync(blockJsonPath)
247
+ ? {
248
+ attributeNames: Object.keys((readJson(blockJsonPath).attributes ?? {})),
249
+ name: readJson(blockJsonPath).name ?? null,
250
+ }
251
+ : null,
252
+ hasSaveSnapshot: fs.existsSync(savePath),
253
+ manifest: fs.existsSync(manifestPath)
254
+ ? summarizeManifest(readJson(manifestPath))
255
+ : null,
256
+ },
257
+ ];
258
+ }));
259
+ const edgeSummaries = entries
260
+ .filter((entry) => entry.block.key === block.key)
261
+ .map((entry) => {
262
+ const ruleMetadata = readRuleMetadata(entry.rulePath);
263
+ const snapshotManifest = snapshots[entry.fromVersion]?.manifest ?? null;
264
+ return {
265
+ autoAppliedRenameCount: ruleMetadata.renameMap.length,
266
+ autoAppliedRenames: ruleMetadata.renameMap,
267
+ fromMigrationVersion: entry.fromVersion,
268
+ nestedPathRenames: ruleMetadata.renameMap.filter((item) => item.currentPath.includes(".")),
269
+ ruleFile: path.relative(state.projectDir, entry.rulePath).replace(/\\/g, "/"),
270
+ toMigrationVersion: entry.toVersion,
271
+ transformKeys: ruleMetadata.transforms,
272
+ unionBranches: snapshotManifest ? summarizeUnionBranches(snapshotManifest) : [],
273
+ unresolved: ruleMetadata.unresolved,
274
+ };
275
+ });
276
+ return {
277
+ blockName: block.blockName,
278
+ currentManifest: summarizeManifest(block.currentManifest),
279
+ edges: edgeSummaries,
280
+ key: block.key,
281
+ legacyMigrationVersions: state.config.supportedMigrationVersions.filter((version) => version !== state.config.currentMigrationVersion),
282
+ snapshots,
283
+ };
284
+ });
285
+ return `<?php
286
+ declare(strict_types=1);
287
+
288
+ /**
289
+ * Generated from advanced migration snapshots. Do not edit manually.
290
+ */
291
+ return ${renderPhpValue({
292
+ currentMigrationVersion: state.config.currentMigrationVersion,
293
+ blocks,
294
+ snapshotDir: state.config.snapshotDir,
295
+ supportedMigrationVersions: state.config.supportedMigrationVersions,
296
+ }, 0)};
297
+ `;
298
+ }
299
+ export function renderVerifyFile(state, blockKey, entries) {
300
+ const block = state.blocks.find((entry) => entry.key === blockKey);
301
+ if (!block) {
302
+ throw new Error(`Unknown migration block target: ${blockKey}`);
303
+ }
304
+ if (entries.length === 0) {
305
+ return `/* eslint-disable no-console */
306
+ console.log(
307
+ \t'Run \`wp-typia migrate scaffold --from-migration-version <label>\` before verify.'
308
+ );
309
+ `;
310
+ }
311
+ const imports = [
312
+ `import { validators } from "${entries[0]?.validatorImport ?? "./validators"}";`,
313
+ `import { deprecated } from "./deprecated";`,
314
+ ];
315
+ const checks = [];
316
+ entries.forEach((entry, index) => {
317
+ imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
318
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
319
+ checks.push(`\tif (selectedMigrationVersions.length === 0 || selectedMigrationVersions.includes("${entry.fromVersion}")) {`);
320
+ checks.push(`\t\tif (rule_${index}.unresolved.length > 0) {`);
321
+ checks.push(`\t\t\tthrow new Error("Unresolved migration TODOs remain for ${entry.fromVersion} -> ${entry.toVersion}: " + rule_${index}.unresolved.join(", "));`);
322
+ checks.push(`\t\t}`);
323
+ checks.push(`\t\tconst cases_${index} = Array.isArray(fixture_${index}.cases) ? fixture_${index}.cases : [];`);
324
+ checks.push(`\t\tfor (const fixtureCase of cases_${index}) {`);
325
+ checks.push(`\t\t\tconst migrated_${index} = rule_${index}.migrate(fixtureCase.input ?? {});`);
326
+ checks.push(`\t\t\tconst validation_${index} = validators.validate(migrated_${index});`);
327
+ checks.push(`\t\t\tif (!isValidationSuccess(validation_${index})) {`);
328
+ checks.push(`\t\t\t\tthrow new Error("Current validator rejected migrated fixture for ${entry.fromVersion} case " + String(fixtureCase.name ?? "unknown") + ": " + JSON.stringify(getValidationErrors(validation_${index})));`);
329
+ checks.push(`\t\t\t}`);
330
+ checks.push(`\t\t}`);
331
+ checks.push(`\t\tconsole.log("Verified ${entry.fromVersion} -> ${entry.toVersion} (" + cases_${index}.length + " case(s))");`);
332
+ checks.push(`\t}`);
333
+ });
334
+ return `/* eslint-disable prettier/prettier, no-console, @typescript-eslint/no-unused-vars, no-nested-ternary */
335
+ ${imports.join("\n")}
336
+
337
+ function isValidationSuccess(result: unknown): boolean {
338
+ return (
339
+ result !== null &&
340
+ typeof result === "object" &&
341
+ (
342
+ (result as { isValid?: unknown }).isValid === true ||
343
+ (result as { success?: unknown }).success === true
344
+ )
345
+ );
346
+ }
347
+
348
+ function getValidationErrors(result: unknown): unknown[] {
349
+ if (result !== null && typeof result === "object" && Array.isArray((result as { errors?: unknown[] }).errors)) {
350
+ return (result as { errors: unknown[] }).errors;
351
+ }
352
+
353
+ return [];
354
+ }
355
+
356
+ const args = process.argv.slice(2);
357
+ const selectedMigrationVersions =
358
+ args[0] === "--all"
359
+ ? []
360
+ : args[0] === "--from-migration-version" && args[1]
361
+ ? [args[1]]
362
+ : [];
363
+
364
+ if (deprecated.length !== ${entries.length}) {
365
+ throw new Error("Generated deprecated entries are out of sync with migration registry.");
366
+ }
367
+
368
+ ${checks.join("\n")}
369
+
370
+ console.log("Migration verification passed for ${block.blockName}");
371
+ `;
372
+ }
373
+ export function renderFuzzFile(state, blockKey, entries) {
374
+ const block = state.blocks.find((entry) => entry.key === blockKey);
375
+ if (!block) {
376
+ throw new Error(`Unknown migration block target: ${blockKey}`);
377
+ }
378
+ if (entries.length === 0) {
379
+ return `/* eslint-disable no-console */
380
+ console.log(
381
+ \t'Run \`wp-typia migrate scaffold --from-migration-version <label>\` before fuzz.'
382
+ );
383
+ `;
384
+ }
385
+ const imports = [
386
+ `import { validators } from "${entries[0]?.entry.validatorImport ?? "./validators"}";`,
387
+ ];
388
+ const edgeDefinitions = [];
389
+ entries.forEach(({ entry, fuzzPlan }, index) => {
390
+ imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
391
+ imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
392
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
393
+ edgeDefinitions.push(`{
394
+ \tfromMigrationVersion: "${entry.fromVersion}",
395
+ \ttoMigrationVersion: "${entry.toVersion}",
396
+ \tfixture: fixture_${index},
397
+ \tlegacyManifest: manifest_${index},
398
+ \trule: rule_${index},
399
+ \tplan: ${JSON.stringify(fuzzPlan, null, "\t").replace(/\n/g, "\n\t")},
400
+ }`);
401
+ });
402
+ return `/* eslint-disable prettier/prettier, no-console, no-bitwise, @typescript-eslint/no-unused-vars, no-nested-ternary, @typescript-eslint/method-signature-style */
403
+ ${imports.join("\n")}
404
+
405
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
406
+
407
+ type ManifestAttribute = {
408
+ \ttypia?: {
409
+ \t\tdefaultValue?: JsonValue | null;
410
+ \t\thasDefault?: boolean;
411
+ \t};
412
+ \tts?: {
413
+ \t\titems?: ManifestAttribute | null;
414
+ \t\tkind?: string | null;
415
+ \t\tproperties?: Record<string, ManifestAttribute> | null;
416
+ \t\tunion?: {
417
+ \t\t\tbranches?: Record<string, ManifestAttribute> | null;
418
+ \t\t\tdiscriminator?: string | null;
419
+ \t\t} | null;
420
+ \t} | null;
421
+ \twp?: {
422
+ \t\tdefaultValue?: JsonValue | null;
423
+ \t\tenum?: JsonValue[] | null;
424
+ \t\thasDefault?: boolean;
425
+ \t} | null;
426
+ };
427
+
428
+ type ManifestDocument = {
429
+ \tattributes?: Record<string, ManifestAttribute>;
430
+ };
431
+
432
+ type FixtureDocument = {
433
+ \tcases?: Array<{ input?: Record<string, unknown>; name?: string }>;
434
+ };
435
+
436
+ type FuzzEdge = {
437
+ \tfixture: FixtureDocument;
438
+ \tfromMigrationVersion: string;
439
+ \tlegacyManifest: ManifestDocument;
440
+ \tplan: {
441
+ \t\tblockedPaths: string[];
442
+ \t\tcompatibleMappings: Array<{ currentPath: string; legacyPath: string }>;
443
+ \t};
444
+ \trule: {
445
+ \t\tmigrate(input: Record<string, unknown>): Record<string, unknown>;
446
+ \t};
447
+ \ttoMigrationVersion: string;
448
+ };
449
+
450
+ const edges: FuzzEdge[] = [
451
+ ${edgeDefinitions.join(",\n")}
452
+ ];
453
+
454
+ function cloneJsonValue<T>(value: T): T {
455
+ \treturn JSON.parse(JSON.stringify(value));
456
+ }
457
+
458
+ function getValueAtPath(input: Record<string, unknown>, pathLabel: string): unknown {
459
+ \treturn String(pathLabel)
460
+ \t\t.split(".")
461
+ \t\t.reduce<unknown>(
462
+ \t\t\t(value, segment) => (value && typeof value === "object" ? (value as Record<string, unknown>)[segment] : undefined),
463
+ \t\t\tinput,
464
+ \t\t);
465
+ }
466
+
467
+ function setValueAtPath(input: Record<string, unknown>, pathLabel: string, value: unknown): void {
468
+ \tconst segments = String(pathLabel).split(".").filter(Boolean);
469
+ \tif (segments.length === 0) {
470
+ \t\treturn;
471
+ \t}
472
+
473
+ \tlet target: Record<string, unknown> = input;
474
+ \twhile (segments.length > 1) {
475
+ \t\tconst segment = segments.shift();
476
+ \t\tif (!segment) {
477
+ \t\t\tcontinue;
478
+ \t\t}
479
+ \t\tif (!target[segment] || typeof target[segment] !== "object" || Array.isArray(target[segment])) {
480
+ \t\t\ttarget[segment] = {};
481
+ \t\t}
482
+ \t\ttarget = target[segment];
483
+ \t}
484
+
485
+ \ttarget[segments[0]!] = value;
486
+ }
487
+
488
+ function createDefaultValue(attribute: ManifestAttribute): JsonValue | Record<string, JsonValue> | null {
489
+ \tif (attribute?.typia?.hasDefault) {
490
+ \t\treturn cloneJsonValue(attribute.typia.defaultValue ?? null);
491
+ \t}
492
+ \tif (attribute?.wp?.hasDefault) {
493
+ \t\treturn cloneJsonValue(attribute.wp.defaultValue ?? null);
494
+ \t}
495
+ \tif (Array.isArray(attribute?.wp?.enum) && attribute.wp.enum.length > 0) {
496
+ \t\treturn cloneJsonValue(attribute.wp.enum[0] ?? null);
497
+ \t}
498
+
499
+ \tswitch (attribute?.ts?.kind) {
500
+ \t\tcase "string":
501
+ \t\t\treturn "";
502
+ \t\tcase "number":
503
+ \t\t\treturn 0;
504
+ \t\tcase "boolean":
505
+ \t\t\treturn false;
506
+ \t\tcase "array":
507
+ \t\t\treturn [];
508
+ \t\tcase "object":
509
+ \t\t\treturn Object.fromEntries(
510
+ \t\t\t\tObject.entries(attribute?.ts?.properties ?? {}).map(([key, property]) => [
511
+ \t\t\t\t\tkey,
512
+ \t\t\t\t\tcreateDefaultValue(property),
513
+ \t\t\t\t]),
514
+ \t\t\t);
515
+ \t\tcase "union": {
516
+ \t\t\tconst firstBranch = Object.values(attribute?.ts?.union?.branches ?? {})[0];
517
+ \t\t\treturn firstBranch ? createDefaultValue(firstBranch) : null;
518
+ \t\t}
519
+ \t\tdefault:
520
+ \t\t\treturn null;
521
+ \t}
522
+ }
523
+
524
+ function createDefaultInput(manifest: ManifestDocument): Record<string, unknown> {
525
+ \treturn Object.fromEntries(
526
+ \t\tObject.entries(manifest?.attributes ?? {}).map(([key, attribute]) => [key, createDefaultValue(attribute)]),
527
+ \t);
528
+ }
529
+
530
+ function isValidationSuccess(result: unknown): boolean {
531
+ \tconst typedResult =
532
+ \t\tresult !== null && typeof result === "object"
533
+ \t\t\t? (result as { isValid?: unknown; success?: unknown })
534
+ \t\t\t: null;
535
+
536
+ \treturn (
537
+ \t\ttypedResult !== null &&
538
+ \t\t(
539
+ \t\t\ttypedResult.isValid === true ||
540
+ \t\t\ttypedResult.success === true
541
+ \t\t)
542
+ \t);
543
+ }
544
+
545
+ function getValidationErrors(result: unknown): unknown[] {
546
+ \tif (
547
+ \t\tresult !== null &&
548
+ \t\ttypeof result === "object" &&
549
+ \t\tArray.isArray((result as { errors?: unknown[] }).errors)
550
+ \t) {
551
+ \t\treturn (result as { errors: unknown[] }).errors;
552
+ \t}
553
+
554
+ \treturn [];
555
+ }
556
+
557
+ function createSeededRandom(seed: number): () => number {
558
+ \tlet state = seed >>> 0;
559
+ \treturn () => {
560
+ \t\tstate = (state * 1664525 + 1013904223) >>> 0;
561
+ \t\treturn state / 4294967296;
562
+ \t};
563
+ }
564
+
565
+ function withSeededRandom<T>(seed: number, callback: () => T): T {
566
+ \tconst originalRandom = Math.random;
567
+ \tMath.random = createSeededRandom(seed);
568
+ \ttry {
569
+ \t\treturn callback();
570
+ \t} finally {
571
+ \t\tMath.random = originalRandom;
572
+ \t}
573
+ }
574
+
575
+ function parseArgs(argv: string[]): {
576
+ \tall: boolean;
577
+ \tfromMigrationVersion?: string;
578
+ \titerations: number;
579
+ \tseed?: number;
580
+ } {
581
+ \tlet all = false;
582
+ \tlet fromMigrationVersion: string | undefined;
583
+ \tlet iterations = 25;
584
+ \tlet seed: number | undefined;
585
+
586
+ \tfor (let index = 0; index < argv.length; index += 1) {
587
+ \t\tconst arg = argv[index];
588
+ \t\tconst next = argv[index + 1];
589
+
590
+ \t\tif (arg === "--all") {
591
+ \t\t\tall = true;
592
+ \t\t\tcontinue;
593
+ \t\t}
594
+ \t\tif (arg === "--from-migration-version") {
595
+ \t\t\tfromMigrationVersion = next;
596
+ \t\t\tindex += 1;
597
+ \t\t\tcontinue;
598
+ \t\t}
599
+ \t\tif (arg === "--iterations") {
600
+ \t\t\titerations = Number.parseInt(next ?? "", 10) || 25;
601
+ \t\t\tindex += 1;
602
+ \t\t\tcontinue;
603
+ \t\t}
604
+ \t\tif (arg === "--seed") {
605
+ \t\t\tseed = Number.parseInt(next ?? "", 10);
606
+ \t\t\tindex += 1;
607
+ \t\t}
608
+ \t}
609
+
610
+ \treturn { all, fromMigrationVersion, iterations, seed };
611
+ }
612
+
613
+ function applyCompatibleMappings(
614
+ \tbaseInput: Record<string, unknown>,
615
+ \tcurrentSample: Record<string, unknown>,
616
+ \tmappings: Array<{ currentPath: string; legacyPath: string }>,
617
+ ): Record<string, unknown> {
618
+ \tfor (const mapping of mappings) {
619
+ \t\tconst value = getValueAtPath(currentSample, mapping.currentPath);
620
+ \t\tif (value !== undefined) {
621
+ \t\t\tsetValueAtPath(baseInput, mapping.legacyPath, cloneJsonValue(value));
622
+ \t\t}
623
+ \t}
624
+ \treturn baseInput;
625
+ }
626
+
627
+ function assertValidMigration(
628
+ \tedge: FuzzEdge,
629
+ \tcandidateInput: Record<string, unknown>,
630
+ \tmigratedOutput: Record<string, unknown>,
631
+ \tvalidation: unknown,
632
+ \titerationSeed: number,
633
+ \titeration: number | string,
634
+ ): void {
635
+ \tif (isValidationSuccess(validation)) {
636
+ \t\treturn;
637
+ \t}
638
+
639
+ \tthrow new Error(
640
+ \t\t"Migration fuzz failed for " +
641
+ \t\t\tedge.fromMigrationVersion +
642
+ \t\t\t" -> " +
643
+ \t\t\tedge.toMigrationVersion +
644
+ \t\t\t" (seed " +
645
+ \t\t\tString(iterationSeed) +
646
+ \t\t\t", iteration " +
647
+ \t\t\tString(iteration) +
648
+ \t\t\t"): " +
649
+ \t\t\tJSON.stringify({
650
+ \t\t\t\tinput: candidateInput,
651
+ \t\t\t\tmigrated: migratedOutput,
652
+ \t\t\t\terrors: getValidationErrors(validation),
653
+ \t\t\t}),
654
+ \t\t);
655
+ }
656
+
657
+ const parsed = parseArgs(process.argv.slice(2));
658
+ const selectedMigrationVersions = parsed.all
659
+ \t? []
660
+ \t: parsed.fromMigrationVersion
661
+ \t\t? [parsed.fromMigrationVersion]
662
+ \t\t: edges.map((edge) => edge.fromMigrationVersion);
663
+ const baseSeed = typeof parsed.seed === "number" ? parsed.seed : Date.now();
664
+
665
+ if (!Number.isInteger(parsed.seed)) {
666
+ \tconsole.log("Using migration fuzz seed " + String(baseSeed));
667
+ }
668
+
669
+ if (edges.length === 0) {
670
+ \tconsole.log("No legacy migration versions configured for migration fuzzing.");
671
+ \tprocess.exit(0);
672
+ }
673
+
674
+ let executedEdges = 0;
675
+
676
+ for (const [edgeIndex, edge] of edges.entries()) {
677
+ \tif (selectedMigrationVersions.length > 0 && !selectedMigrationVersions.includes(edge.fromMigrationVersion)) {
678
+ \t\tcontinue;
679
+ \t}
680
+
681
+ \texecutedEdges += 1;
682
+
683
+ \tconst fixtureCases = Array.isArray(edge.fixture?.cases) ? edge.fixture.cases : [];
684
+ \tfor (const fixtureCase of fixtureCases) {
685
+ \t\tconst migrated = edge.rule.migrate(fixtureCase.input ?? {});
686
+ \t\tconst validation = validators.validate(migrated);
687
+ \t\tassertValidMigration(edge, fixtureCase.input ?? {}, migrated, validation, baseSeed, fixtureCase.name ?? "fixture");
688
+ \t}
689
+
690
+ \tfor (let iteration = 0; iteration < parsed.iterations; iteration += 1) {
691
+ \t\tconst iterationSeed = (baseSeed + edgeIndex * 100003 + iteration) >>> 0;
692
+ \t\tconst currentSample = withSeededRandom(iterationSeed, () => validators.random());
693
+ \t\tconst baseFixture = fixtureCases.find((fixtureCase) => fixtureCase.name === "default")?.input;
694
+ \t\tconst legacyInput = applyCompatibleMappings(
695
+ \t\t\tcloneJsonValue(baseFixture ?? createDefaultInput(edge.legacyManifest)),
696
+ \t\t\tcurrentSample,
697
+ \t\t\tedge.plan.compatibleMappings,
698
+ \t\t);
699
+ \t\tconst migrated = edge.rule.migrate(legacyInput);
700
+ \t\tconst validation = validators.validate(migrated);
701
+ \t\tassertValidMigration(edge, legacyInput, migrated, validation, iterationSeed, iteration);
702
+ \t}
703
+
704
+ \tconsole.log(
705
+ \t\t"Fuzzed " +
706
+ \t\t\tedge.fromMigrationVersion +
707
+ \t\t\t" -> " +
708
+ \t\t\tedge.toMigrationVersion +
709
+ \t\t\t" (" +
710
+ \t\t\tString(fixtureCases.length) +
711
+ \t\t\t" fixture case(s), " +
712
+ \t\t\tString(parsed.iterations) +
713
+ \t\t\t" fuzz iteration(s))",
714
+ \t);
715
+ }
716
+
717
+ if (selectedMigrationVersions.length > 0 && executedEdges === 0) {
718
+ \tthrow new Error(
719
+ \t\t"Requested migration version was not exercised by fuzz: " +
720
+ \t\t\tselectedMigrationVersions.join(", "),
721
+ \t);
722
+ }
723
+
724
+ console.log("Migration fuzzing passed for ${block.blockName}");
725
+ `;
726
+ }
727
+ function normalizeImportPath(relativePath, stripExtension = false) {
728
+ let nextPath = relativePath.replace(/\\/g, "/");
729
+ if (!nextPath.startsWith(".")) {
730
+ nextPath = `./${nextPath}`;
731
+ }
732
+ if (stripExtension) {
733
+ nextPath = nextPath.replace(/\.[^.]+$/u, "");
734
+ }
735
+ return nextPath;
736
+ }
737
+ function getGeneratedDir(block, state) {
738
+ return block.layout === "legacy"
739
+ ? state.paths.generatedDir
740
+ : path.join(state.paths.generatedDir, block.key);
741
+ }