@wp-typia/project-tools 0.16.11 → 0.16.12

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 (101) hide show
  1. package/README.md +9 -3
  2. package/dist/runtime/built-in-block-artifact-documents.d.ts +3 -0
  3. package/dist/runtime/built-in-block-artifact-documents.js +2 -0
  4. package/dist/runtime/built-in-block-artifact-types.d.ts +51 -0
  5. package/dist/runtime/built-in-block-artifact-types.js +304 -0
  6. package/dist/runtime/built-in-block-artifacts.js +4 -803
  7. package/dist/runtime/built-in-block-attribute-emitters.d.ts +71 -0
  8. package/dist/runtime/built-in-block-attribute-emitters.js +176 -0
  9. package/dist/runtime/built-in-block-attribute-specs.d.ts +38 -0
  10. package/dist/runtime/built-in-block-attribute-specs.js +358 -0
  11. package/dist/runtime/built-in-block-code-templates/basic.d.ts +4 -0
  12. package/dist/runtime/built-in-block-code-templates/basic.js +249 -0
  13. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +4 -0
  14. package/dist/runtime/built-in-block-code-templates/compound-child.js +138 -0
  15. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +6 -0
  16. package/dist/runtime/built-in-block-code-templates/compound-parent.js +227 -0
  17. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +4 -0
  18. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +478 -0
  19. package/dist/runtime/built-in-block-code-templates/compound.d.ts +3 -0
  20. package/dist/runtime/built-in-block-code-templates/compound.js +3 -0
  21. package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +5 -0
  22. package/dist/runtime/built-in-block-code-templates/interactivity.js +547 -0
  23. package/dist/runtime/built-in-block-code-templates/persistence.d.ts +5 -0
  24. package/dist/runtime/built-in-block-code-templates/persistence.js +550 -0
  25. package/dist/runtime/built-in-block-code-templates/shared.d.ts +16 -0
  26. package/dist/runtime/built-in-block-code-templates/shared.js +53 -0
  27. package/dist/runtime/built-in-block-code-templates.d.ts +5 -32
  28. package/dist/runtime/built-in-block-code-templates.js +5 -2230
  29. package/dist/runtime/cli-add-block-config.d.ts +6 -0
  30. package/dist/runtime/cli-add-block-config.js +143 -0
  31. package/dist/runtime/cli-add-block-legacy-validator.d.ts +4 -0
  32. package/dist/runtime/cli-add-block-legacy-validator.js +168 -0
  33. package/dist/runtime/cli-add-block.js +3 -301
  34. package/dist/runtime/cli-add-workspace-assets.d.ts +38 -0
  35. package/dist/runtime/cli-add-workspace-assets.js +399 -0
  36. package/dist/runtime/cli-add-workspace.d.ts +2 -38
  37. package/dist/runtime/cli-add-workspace.js +5 -396
  38. package/dist/runtime/cli-doctor-environment.d.ts +12 -0
  39. package/dist/runtime/cli-doctor-environment.js +123 -0
  40. package/dist/runtime/cli-doctor-workspace.d.ts +14 -0
  41. package/dist/runtime/cli-doctor-workspace.js +296 -0
  42. package/dist/runtime/cli-doctor.d.ts +4 -2
  43. package/dist/runtime/cli-doctor.js +10 -405
  44. package/dist/runtime/migration-command-surface.d.ts +67 -0
  45. package/dist/runtime/migration-command-surface.js +189 -0
  46. package/dist/runtime/migration-diff-rename.d.ts +13 -0
  47. package/dist/runtime/migration-diff-rename.js +192 -0
  48. package/dist/runtime/migration-diff-transform.d.ts +14 -0
  49. package/dist/runtime/migration-diff-transform.js +105 -0
  50. package/dist/runtime/migration-diff.js +12 -297
  51. package/dist/runtime/migration-generated-artifacts.d.ts +3 -0
  52. package/dist/runtime/migration-generated-artifacts.js +41 -0
  53. package/dist/runtime/migration-maintenance.d.ts +51 -0
  54. package/dist/runtime/migration-maintenance.js +380 -0
  55. package/dist/runtime/migration-planning.d.ts +23 -0
  56. package/dist/runtime/migration-planning.js +131 -0
  57. package/dist/runtime/migration-project-config-source.d.ts +6 -0
  58. package/dist/runtime/migration-project-config-source.js +424 -0
  59. package/dist/runtime/migration-project-layout-discovery.d.ts +61 -0
  60. package/dist/runtime/migration-project-layout-discovery.js +337 -0
  61. package/dist/runtime/migration-project-layout-paths.d.ts +135 -0
  62. package/dist/runtime/migration-project-layout-paths.js +288 -0
  63. package/dist/runtime/migration-project-layout.d.ts +3 -0
  64. package/dist/runtime/migration-project-layout.js +2 -0
  65. package/dist/runtime/migration-project-workspace.d.ts +47 -0
  66. package/dist/runtime/migration-project-workspace.js +212 -0
  67. package/dist/runtime/migration-project.d.ts +4 -94
  68. package/dist/runtime/migration-project.js +3 -1101
  69. package/dist/runtime/migration-render-diff-rule.d.ts +5 -0
  70. package/dist/runtime/migration-render-diff-rule.js +120 -0
  71. package/dist/runtime/migration-render-execution.d.ts +3 -0
  72. package/dist/runtime/migration-render-execution.js +428 -0
  73. package/dist/runtime/migration-render-generated.d.ts +27 -0
  74. package/dist/runtime/migration-render-generated.js +230 -0
  75. package/dist/runtime/migration-render-support.d.ts +3 -0
  76. package/dist/runtime/migration-render-support.js +16 -0
  77. package/dist/runtime/migration-render.d.ts +3 -33
  78. package/dist/runtime/migration-render.js +3 -789
  79. package/dist/runtime/migrations.d.ts +24 -118
  80. package/dist/runtime/migrations.js +12 -700
  81. package/dist/runtime/scaffold-bootstrap.d.ts +45 -0
  82. package/dist/runtime/scaffold-bootstrap.js +185 -0
  83. package/dist/runtime/scaffold-package-manager-files.d.ts +35 -0
  84. package/dist/runtime/scaffold-package-manager-files.js +79 -0
  85. package/dist/runtime/scaffold.d.ts +1 -12
  86. package/dist/runtime/scaffold.js +10 -393
  87. package/dist/runtime/template-source-contracts.d.ts +81 -0
  88. package/dist/runtime/template-source-contracts.js +1 -0
  89. package/dist/runtime/template-source-external.d.ts +21 -0
  90. package/dist/runtime/template-source-external.js +184 -0
  91. package/dist/runtime/template-source-locators.d.ts +4 -0
  92. package/dist/runtime/template-source-locators.js +72 -0
  93. package/dist/runtime/template-source-normalization.d.ts +7 -0
  94. package/dist/runtime/template-source-normalization.js +53 -0
  95. package/dist/runtime/template-source-remote.d.ts +23 -0
  96. package/dist/runtime/template-source-remote.js +336 -0
  97. package/dist/runtime/template-source-seeds.d.ts +12 -0
  98. package/dist/runtime/template-source-seeds.js +243 -0
  99. package/dist/runtime/template-source.d.ts +4 -86
  100. package/dist/runtime/template-source.js +9 -828
  101. package/package.json +4 -4
@@ -0,0 +1,380 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { ROOT_PHP_MIGRATION_REGISTRY, } from './migration-constants.js';
5
+ import { createMigrationDiff } from './migration-diff.js';
6
+ import { createEdgeFixtureDocument, ensureEdgeFixtureFile, } from './migration-fixtures.js';
7
+ import { collectGeneratedMigrationEntries, } from './migration-generated-artifacts.js';
8
+ import { collectFixtureTargets, formatScaffoldCommand, getSelectedEntriesByBlock, hasSnapshotForVersion, isLegacySingleBlockProject, isSnapshotOptionalForBlockVersion, resolveLegacyVersions, } from './migration-planning.js';
9
+ import { assertRuleHasNoTodos, getFixtureFilePath, getGeneratedDirForBlock, getRuleFilePath, getSnapshotBlockJsonPath, getSnapshotManifestPath, getSnapshotRoot, getSnapshotSavePath, loadMigrationProject, readRuleMetadata, } from './migration-project.js';
10
+ import { renderFuzzFile, renderGeneratedDeprecatedFile, renderGeneratedMigrationIndexFile, renderMigrationRegistryFile, renderPhpMigrationRegistryFile, renderVerifyFile, } from './migration-render.js';
11
+ import { createMigrationRiskSummary, formatMigrationRiskSummary, } from './migration-risk.js';
12
+ import { getLocalTsxBinary, isInteractiveTerminal, readJson, resolveTargetMigrationVersion, } from './migration-utils.js';
13
+ import { readWorkspaceInventory } from './workspace-inventory.js';
14
+ import { getInvalidWorkspaceProjectReason, tryResolveWorkspaceProject, } from './workspace-project.js';
15
+ /**
16
+ * Run deterministic migration verification against generated fixtures.
17
+ *
18
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
19
+ * @param options Verification scope and console rendering options.
20
+ * @returns Verified legacy versions.
21
+ */
22
+ export function verifyProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log, } = {}) {
23
+ const state = loadMigrationProject(projectDir);
24
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
25
+ const blockEntries = getSelectedEntriesByBlock(state, targetVersions, 'verify');
26
+ const legacySingleBlock = isLegacySingleBlockProject(state);
27
+ if (targetVersions.length === 0) {
28
+ renderLine('No legacy migration versions configured for verification.');
29
+ return { verifiedVersions: [] };
30
+ }
31
+ const tsxBinary = getLocalTsxBinary(projectDir);
32
+ for (const [blockKey, entries] of Object.entries(blockEntries)) {
33
+ const block = state.blocks.find((entry) => entry.key === blockKey);
34
+ if (!block || entries.length === 0) {
35
+ continue;
36
+ }
37
+ for (const entry of entries) {
38
+ assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
39
+ }
40
+ const verifyScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), 'verify.ts');
41
+ if (!fs.existsSync(verifyScriptPath)) {
42
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
43
+ throw new Error(`Generated verify script is missing for ${block.blockName} (${selectedVersionsForBlock.join(', ')}). ` +
44
+ `Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
45
+ }
46
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
47
+ const filteredArgs = all
48
+ ? ['--all']
49
+ : ['--from-migration-version', selectedVersionsForBlock[0]];
50
+ execFileSync(tsxBinary, [verifyScriptPath, ...filteredArgs], {
51
+ cwd: projectDir,
52
+ shell: process.platform === 'win32',
53
+ stdio: 'inherit',
54
+ });
55
+ renderLine(legacySingleBlock
56
+ ? `Verified migrations for ${selectedVersionsForBlock.join(', ')}`
57
+ : `Verified ${block.blockName} migrations for ${selectedVersionsForBlock.join(', ')}`);
58
+ }
59
+ return { verifiedVersions: targetVersions };
60
+ }
61
+ function recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck) {
62
+ let invalidWorkspaceReason = null;
63
+ let workspace;
64
+ try {
65
+ invalidWorkspaceReason = getInvalidWorkspaceProjectReason(projectDir);
66
+ workspace = tryResolveWorkspaceProject(projectDir);
67
+ }
68
+ catch (error) {
69
+ recordCheck('fail', 'Workspace migration targets', error instanceof Error ? error.message : String(error));
70
+ return;
71
+ }
72
+ if (!workspace) {
73
+ if (invalidWorkspaceReason) {
74
+ recordCheck('fail', 'Workspace migration targets', invalidWorkspaceReason);
75
+ }
76
+ return;
77
+ }
78
+ try {
79
+ const inventory = readWorkspaceInventory(workspace.projectDir);
80
+ const expectedTargets = inventory.blocks.map((block) => `${workspace.workspace.namespace}/${block.slug}`);
81
+ const configuredTargets = state.blocks.map((block) => block.blockName);
82
+ const expectedTargetSet = new Set(expectedTargets);
83
+ const configuredTargetSet = new Set(configuredTargets);
84
+ const missingTargets = expectedTargets.filter((target) => !configuredTargetSet.has(target));
85
+ const staleTargets = configuredTargets.filter((target) => !expectedTargetSet.has(target));
86
+ recordCheck(missingTargets.length === 0 && staleTargets.length === 0 ? 'pass' : 'fail', 'Workspace migration targets', missingTargets.length === 0 && staleTargets.length === 0
87
+ ? `${expectedTargets.length} workspace block target(s) align with migration config`
88
+ : [
89
+ missingTargets.length > 0
90
+ ? `Missing from migration config: ${missingTargets.join(', ')}`
91
+ : null,
92
+ staleTargets.length > 0
93
+ ? `Not present in scripts/block-config.ts: ${staleTargets.join(', ')}`
94
+ : null,
95
+ ]
96
+ .filter((detail) => typeof detail === 'string')
97
+ .join('; '));
98
+ }
99
+ catch (error) {
100
+ recordCheck('fail', 'Workspace migration targets', error instanceof Error ? error.message : String(error));
101
+ }
102
+ }
103
+ /**
104
+ * Validate the migration workspace without mutating files.
105
+ *
106
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
107
+ * @param options Doctor scope and console rendering options.
108
+ * @returns Structured doctor check results for the selected legacy versions.
109
+ */
110
+ export function doctorProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log, } = {}) {
111
+ const checks = [];
112
+ const recordCheck = (status, label, detail) => {
113
+ checks.push({ detail, label, status });
114
+ renderLine(`${status === 'pass' ? 'PASS' : 'FAIL'} ${label}: ${detail}`);
115
+ };
116
+ let state;
117
+ try {
118
+ state = loadMigrationProject(projectDir);
119
+ const legacySingleBlock = isLegacySingleBlockProject(state);
120
+ recordCheck('pass', 'Migration config', legacySingleBlock
121
+ ? `Loaded ${state.blocks[0]?.blockName} @ ${state.config.currentMigrationVersion}`
122
+ : `Loaded ${state.blocks.length} block target(s) @ ${state.config.currentMigrationVersion}`);
123
+ }
124
+ catch (error) {
125
+ recordCheck('fail', 'Migration config', error instanceof Error ? error.message : String(error));
126
+ throw new Error('Migration doctor failed.');
127
+ }
128
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
129
+ const legacySingleBlock = isLegacySingleBlockProject(state);
130
+ const snapshotVersions = new Set(targetVersions.length > 0
131
+ ? [state.config.currentMigrationVersion, ...targetVersions]
132
+ : state.config.supportedMigrationVersions);
133
+ recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck);
134
+ for (const version of snapshotVersions) {
135
+ for (const block of state.blocks) {
136
+ const snapshotRoot = getSnapshotRoot(projectDir, block, version);
137
+ const blockJsonPath = getSnapshotBlockJsonPath(projectDir, block, version);
138
+ const manifestPath = getSnapshotManifestPath(projectDir, block, version);
139
+ const savePath = getSnapshotSavePath(projectDir, block, version);
140
+ const hasSnapshot = fs.existsSync(snapshotRoot);
141
+ const snapshotIsOptional = !hasSnapshot && isSnapshotOptionalForBlockVersion(state, block, version);
142
+ recordCheck(hasSnapshot || snapshotIsOptional ? 'pass' : 'fail', legacySingleBlock ? `Snapshot ${version}` : `Snapshot ${block.blockName} @ ${version}`, hasSnapshot
143
+ ? path.relative(projectDir, snapshotRoot)
144
+ : 'Not present for this version');
145
+ if (!hasSnapshot) {
146
+ continue;
147
+ }
148
+ for (const targetPath of [blockJsonPath, manifestPath, savePath]) {
149
+ recordCheck(fs.existsSync(targetPath) ? 'pass' : 'fail', legacySingleBlock
150
+ ? `Snapshot file ${version}`
151
+ : `Snapshot file ${block.blockName} @ ${version}`, fs.existsSync(targetPath)
152
+ ? path.relative(projectDir, targetPath)
153
+ : `Missing ${path.relative(projectDir, targetPath)}`);
154
+ }
155
+ }
156
+ }
157
+ try {
158
+ const generatedEntries = collectGeneratedMigrationEntries(state);
159
+ const expectedGeneratedFiles = new Map();
160
+ for (const block of state.blocks) {
161
+ const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
162
+ const entries = blockGeneratedEntries.map(({ entry }) => entry);
163
+ const generatedDir = getGeneratedDirForBlock(state.paths, block);
164
+ expectedGeneratedFiles.set(path.join(generatedDir, 'registry.ts'), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries));
165
+ expectedGeneratedFiles.set(path.join(generatedDir, 'deprecated.ts'), renderGeneratedDeprecatedFile(state, block.key, entries));
166
+ expectedGeneratedFiles.set(path.join(generatedDir, 'verify.ts'), renderVerifyFile(state, block.key, entries));
167
+ expectedGeneratedFiles.set(path.join(generatedDir, 'fuzz.ts'), renderFuzzFile(state, block.key, blockGeneratedEntries));
168
+ }
169
+ expectedGeneratedFiles.set(path.join(state.paths.generatedDir, 'index.ts'), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)));
170
+ expectedGeneratedFiles.set(path.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)));
171
+ for (const [filePath, expectedSource] of expectedGeneratedFiles) {
172
+ const inSync = fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === expectedSource;
173
+ recordCheck(inSync ? 'pass' : 'fail', `Generated ${path.relative(projectDir, filePath)}`, inSync
174
+ ? 'In sync'
175
+ : 'Run `wp-typia migrate scaffold --from-migration-version <label>` or regenerate artifacts');
176
+ }
177
+ }
178
+ catch (error) {
179
+ recordCheck('fail', 'Generated artifacts', error instanceof Error ? error.message : String(error));
180
+ }
181
+ for (const version of targetVersions) {
182
+ for (const block of state.blocks) {
183
+ if (!hasSnapshotForVersion(state, block, version)) {
184
+ recordCheck('pass', `Snapshot coverage ${block.blockName} @ ${version}`, 'Target not present for this version');
185
+ continue;
186
+ }
187
+ const rulePath = getRuleFilePath(state.paths, block, version, state.config.currentMigrationVersion);
188
+ const fixturePath = getFixtureFilePath(state.paths, block, version, state.config.currentMigrationVersion);
189
+ recordCheck(fs.existsSync(rulePath) ? 'pass' : 'fail', legacySingleBlock ? `Rule ${version}` : `Rule ${block.blockName} @ ${version}`, fs.existsSync(rulePath)
190
+ ? path.relative(projectDir, rulePath)
191
+ : `Missing ${path.relative(projectDir, rulePath)}`);
192
+ recordCheck(fs.existsSync(fixturePath) ? 'pass' : 'fail', legacySingleBlock ? `Fixture ${version}` : `Fixture ${block.blockName} @ ${version}`, fs.existsSync(fixturePath)
193
+ ? path.relative(projectDir, fixturePath)
194
+ : `Missing ${path.relative(projectDir, fixturePath)}`);
195
+ if (!fs.existsSync(rulePath) || !fs.existsSync(fixturePath)) {
196
+ continue;
197
+ }
198
+ try {
199
+ assertRuleHasNoTodos(projectDir, block, version, state.config.currentMigrationVersion);
200
+ recordCheck('pass', legacySingleBlock
201
+ ? `Rule TODOs ${version}`
202
+ : `Rule TODOs ${block.blockName} @ ${version}`, 'No TODO MIGRATION markers remain');
203
+ }
204
+ catch (error) {
205
+ recordCheck('fail', legacySingleBlock
206
+ ? `Rule TODOs ${version}`
207
+ : `Rule TODOs ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
208
+ }
209
+ try {
210
+ const ruleMetadata = readRuleMetadata(rulePath);
211
+ recordCheck(ruleMetadata.unresolved.length === 0 ? 'pass' : 'fail', legacySingleBlock
212
+ ? `Rule unresolved ${version}`
213
+ : `Rule unresolved ${block.blockName} @ ${version}`, ruleMetadata.unresolved.length === 0
214
+ ? 'No unresolved entries remain'
215
+ : ruleMetadata.unresolved.join(', '));
216
+ }
217
+ catch (error) {
218
+ recordCheck('fail', legacySingleBlock
219
+ ? `Rule unresolved ${version}`
220
+ : `Rule unresolved ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
221
+ }
222
+ try {
223
+ const fixtureDocument = readJson(fixturePath);
224
+ recordCheck(Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0
225
+ ? 'pass'
226
+ : 'fail', legacySingleBlock
227
+ ? `Fixture parse ${version}`
228
+ : `Fixture parse ${block.blockName} @ ${version}`, Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0
229
+ ? `${fixtureDocument.cases.length} case(s)`
230
+ : 'Fixture document has no cases');
231
+ const diff = createMigrationDiff(state, block, version, state.config.currentMigrationVersion);
232
+ const expectedFixture = createEdgeFixtureDocument(projectDir, block, version, state.config.currentMigrationVersion, diff);
233
+ const actualCaseNames = new Set((fixtureDocument.cases ?? []).map((fixtureCase) => fixtureCase.name));
234
+ const missingCases = expectedFixture.cases
235
+ .map((fixtureCase) => fixtureCase.name)
236
+ .filter((name) => !actualCaseNames.has(name));
237
+ recordCheck(missingCases.length === 0 ? 'pass' : 'fail', legacySingleBlock
238
+ ? `Fixture coverage ${version}`
239
+ : `Fixture coverage ${block.blockName} @ ${version}`, missingCases.length === 0
240
+ ? 'All expected fixture cases are present'
241
+ : `Missing ${missingCases.join(', ')}`);
242
+ recordCheck('pass', legacySingleBlock
243
+ ? `Risk summary ${version}`
244
+ : `Risk summary ${block.blockName} @ ${version}`, formatMigrationRiskSummary(createMigrationRiskSummary(diff)));
245
+ }
246
+ catch (error) {
247
+ recordCheck('fail', legacySingleBlock
248
+ ? `Fixture parse ${version}`
249
+ : `Fixture parse ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
250
+ }
251
+ }
252
+ }
253
+ const failedChecks = checks.filter((check) => check.status === 'fail');
254
+ renderLine(`${failedChecks.length === 0 ? 'PASS' : 'FAIL'} Migration doctor summary: ${checks.length - failedChecks.length}/${checks.length} checks passed`);
255
+ if (failedChecks.length > 0) {
256
+ throw new Error('Migration doctor failed.');
257
+ }
258
+ return {
259
+ checkedVersions: targetVersions,
260
+ checks,
261
+ };
262
+ }
263
+ /**
264
+ * Generate or refresh migration fixtures for one or more legacy edges.
265
+ *
266
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
267
+ * @param options Fixture generation scope and refresh options.
268
+ * @returns Generated and skipped legacy versions.
269
+ */
270
+ export function fixturesProjectMigrations(projectDir, { all = false, confirmOverwrite, force = false, fromMigrationVersion, isInteractive = isInteractiveTerminal(), renderLine = console.log, toMigrationVersion = 'current', } = {}) {
271
+ const state = loadMigrationProject(projectDir);
272
+ const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
273
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
274
+ if (targetVersions.length === 0) {
275
+ renderLine('No legacy migration versions configured for fixture generation.');
276
+ return { generatedVersions: [], skippedVersions: [] };
277
+ }
278
+ const generatedVersions = [];
279
+ const skippedVersions = [];
280
+ const fixtureTargets = collectFixtureTargets(state, targetVersions, targetMigrationVersion);
281
+ if (force) {
282
+ const overwriteTargets = fixtureTargets.filter(({ fixturePath }) => fs.existsSync(fixturePath));
283
+ if (isInteractive && overwriteTargets.length > 0) {
284
+ const confirmed = confirmOverwrite?.(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`) ??
285
+ promptForConfirmation(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`);
286
+ if (!confirmed) {
287
+ renderLine(`Cancelled fixture refresh. Kept ${overwriteTargets.length} existing fixture file(s).`);
288
+ return {
289
+ generatedVersions,
290
+ skippedVersions: overwriteTargets.map(({ scopedLabel }) => scopedLabel),
291
+ };
292
+ }
293
+ }
294
+ }
295
+ for (const { block, fixturePath, scopedLabel, version } of fixtureTargets) {
296
+ const existed = fs.existsSync(fixturePath);
297
+ const diff = createMigrationDiff(state, block, version, targetMigrationVersion);
298
+ const result = ensureEdgeFixtureFile(projectDir, block, version, targetMigrationVersion, diff, { force });
299
+ if (result.written) {
300
+ generatedVersions.push(scopedLabel);
301
+ renderLine(`${existed ? 'Refreshed' : 'Generated'} fixture ${path.relative(projectDir, fixturePath)}`);
302
+ }
303
+ else {
304
+ skippedVersions.push(scopedLabel);
305
+ renderLine(`Preserved existing fixture ${path.relative(projectDir, fixturePath)} (use --force to refresh)`);
306
+ }
307
+ }
308
+ return {
309
+ generatedVersions,
310
+ skippedVersions,
311
+ };
312
+ }
313
+ /**
314
+ * Run seeded migration fuzz verification against generated fuzz artifacts.
315
+ *
316
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
317
+ * @param options Fuzz scope, iteration count, seed, and console rendering options.
318
+ * @returns Fuzzed legacy versions and the effective seed.
319
+ */
320
+ export function fuzzProjectMigrations(projectDir, { all = false, fromMigrationVersion, iterations = 25, renderLine = console.log, seed, } = {}) {
321
+ const state = loadMigrationProject(projectDir);
322
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
323
+ const blockEntries = getSelectedEntriesByBlock(state, targetVersions, 'fuzz');
324
+ const legacySingleBlock = isLegacySingleBlockProject(state);
325
+ if (targetVersions.length === 0) {
326
+ renderLine('No legacy migration versions configured for fuzzing.');
327
+ return { fuzzedVersions: [] };
328
+ }
329
+ const tsxBinary = getLocalTsxBinary(projectDir);
330
+ for (const [blockKey, entries] of Object.entries(blockEntries)) {
331
+ const block = state.blocks.find((entry) => entry.key === blockKey);
332
+ if (!block || entries.length === 0) {
333
+ continue;
334
+ }
335
+ for (const entry of entries) {
336
+ assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
337
+ }
338
+ const fuzzScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), 'fuzz.ts');
339
+ if (!fs.existsSync(fuzzScriptPath)) {
340
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
341
+ throw new Error(`Generated fuzz script is missing for ${block.blockName} (${selectedVersionsForBlock.join(', ')}). ` +
342
+ `Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
343
+ }
344
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
345
+ const args = [
346
+ fuzzScriptPath,
347
+ ...(all ? ['--all'] : ['--from-migration-version', selectedVersionsForBlock[0]]),
348
+ '--iterations',
349
+ String(iterations),
350
+ ...(seed === undefined ? [] : ['--seed', String(seed)]),
351
+ ];
352
+ execFileSync(tsxBinary, args, {
353
+ cwd: projectDir,
354
+ shell: process.platform === 'win32',
355
+ stdio: 'inherit',
356
+ });
357
+ renderLine(legacySingleBlock
358
+ ? `Fuzzed migrations for ${selectedVersionsForBlock.join(', ')}`
359
+ : `Fuzzed ${block.blockName} migrations for ${selectedVersionsForBlock.join(', ')}`);
360
+ }
361
+ return { fuzzedVersions: targetVersions, seed };
362
+ }
363
+ function promptForConfirmation(message) {
364
+ process.stdout.write(`${message} [y/N]: `);
365
+ const buffer = Buffer.alloc(1);
366
+ let answer = '';
367
+ while (true) {
368
+ const bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1, null);
369
+ if (bytesRead === 0) {
370
+ break;
371
+ }
372
+ const char = buffer.toString('utf8', 0, bytesRead);
373
+ if (char === '\n' || char === '\r') {
374
+ break;
375
+ }
376
+ answer += char;
377
+ }
378
+ const normalized = answer.trim().toLowerCase();
379
+ return normalized === 'y' || normalized === 'yes';
380
+ }
@@ -0,0 +1,23 @@
1
+ import type { MigrationProjectState } from "./migration-types.js";
2
+ export declare function hasSnapshotForVersion(state: MigrationProjectState, block: MigrationProjectState["blocks"][number], version: string): boolean;
3
+ export declare function listConfiguredLegacyVersions(state: MigrationProjectState): string[];
4
+ export declare function listPreviewableLegacyVersions(state: MigrationProjectState): string[];
5
+ export declare function resolveLegacyVersions(state: MigrationProjectState, { all, availableVersions, fromMigrationVersion, }: {
6
+ all?: boolean;
7
+ availableVersions?: string[];
8
+ fromMigrationVersion?: string;
9
+ }): string[];
10
+ export declare function isSnapshotOptionalForBlockVersion(state: MigrationProjectState, block: MigrationProjectState["blocks"][number], version: string): boolean;
11
+ export declare function isLegacySingleBlockProject(state: MigrationProjectState): boolean;
12
+ export declare function getSelectedEntriesByBlock(state: MigrationProjectState, targetVersions: string[], command: "fuzz" | "verify"): Record<string, import("./migration-types.js").MigrationEntry[]>;
13
+ export declare function assertDistinctMigrationEdge(command: "diff" | "plan" | "scaffold", fromVersion: string, toVersion: string): void;
14
+ export declare function createMigrationPlanNextSteps(fromVersion: string, targetVersion: string, currentVersion: string): string[];
15
+ export declare function formatEdgeCommand(command: "fixtures" | "scaffold", fromVersion: string, targetVersion: string, currentVersion: string): string;
16
+ export declare function createMissingProjectSnapshotMessage(state: MigrationProjectState, fromVersion: string): string;
17
+ export declare function formatScaffoldCommand(versions: string[]): string;
18
+ export declare function collectFixtureTargets(state: MigrationProjectState, targetVersions: string[], targetVersion: string): {
19
+ block: import("./migration-types.js").ResolvedMigrationBlockTarget;
20
+ fixturePath: string;
21
+ scopedLabel: string;
22
+ version: string;
23
+ }[];
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ import { discoverMigrationEntries, getAvailableSnapshotVersionsForBlock, getFixtureFilePath, getSnapshotManifestPath, } from "./migration-project.js";
3
+ import { compareMigrationVersionLabels } from "./migration-utils.js";
4
+ export function hasSnapshotForVersion(state, block, version) {
5
+ return fs.existsSync(getSnapshotManifestPath(state.projectDir, block, version));
6
+ }
7
+ export function listConfiguredLegacyVersions(state) {
8
+ return state.config.supportedMigrationVersions
9
+ .filter((version) => version !== state.config.currentMigrationVersion)
10
+ .sort(compareMigrationVersionLabels);
11
+ }
12
+ export function listPreviewableLegacyVersions(state) {
13
+ return [
14
+ ...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block))),
15
+ ]
16
+ .filter((version) => version !== state.config.currentMigrationVersion)
17
+ .sort(compareMigrationVersionLabels);
18
+ }
19
+ export function resolveLegacyVersions(state, { all = false, availableVersions, fromMigrationVersion, }) {
20
+ const configuredLegacyVersions = listConfiguredLegacyVersions(state);
21
+ const legacyVersions = availableVersions ?? configuredLegacyVersions;
22
+ if (fromMigrationVersion) {
23
+ if (!legacyVersions.includes(fromMigrationVersion)) {
24
+ throw new Error(legacyVersions.length === 0
25
+ ? availableVersions && configuredLegacyVersions.length > 0
26
+ ? `Unsupported migration version: ${fromMigrationVersion}. No previewable legacy migration versions are available yet because none currently have snapshot coverage. ` +
27
+ `Restore or recapture the missing snapshots first.`
28
+ : `Unsupported migration version: ${fromMigrationVersion}. No legacy migration versions are configured yet. ` +
29
+ `Capture an older schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.`
30
+ : `Unsupported migration version: ${fromMigrationVersion}. Available legacy migration versions: ${legacyVersions.join(", ")}.`);
31
+ }
32
+ return [fromMigrationVersion];
33
+ }
34
+ if (all) {
35
+ return legacyVersions;
36
+ }
37
+ return legacyVersions.slice(0, 1);
38
+ }
39
+ export function isSnapshotOptionalForBlockVersion(state, block, version) {
40
+ if (block.layout !== "multi") {
41
+ return false;
42
+ }
43
+ const introducedVersions = [...new Set(state.config.supportedMigrationVersions)]
44
+ .filter((candidateVersion) => hasSnapshotForVersion(state, block, candidateVersion))
45
+ .sort(compareMigrationVersionLabels);
46
+ const firstIntroducedVersion = introducedVersions[0];
47
+ if (!firstIntroducedVersion) {
48
+ return false;
49
+ }
50
+ return compareMigrationVersionLabels(version, firstIntroducedVersion) < 0;
51
+ }
52
+ export function isLegacySingleBlockProject(state) {
53
+ return state.blocks.length === 1 && state.blocks[0]?.layout === "legacy";
54
+ }
55
+ export function getSelectedEntriesByBlock(state, targetVersions, command) {
56
+ const discoveredEntries = discoverMigrationEntries(state);
57
+ const discoveredEntryKeys = new Set(discoveredEntries.map((entry) => `${entry.block.key}:${entry.fromVersion}`));
58
+ const missingEntries = targetVersions.flatMap((version) => state.blocks
59
+ .filter((block) => hasSnapshotForVersion(state, block, version))
60
+ .filter((block) => !discoveredEntryKeys.has(`${block.key}:${version}`))
61
+ .map((block) => ({ block, version })));
62
+ if (missingEntries.length > 0) {
63
+ const missingLabels = missingEntries
64
+ .map(({ block, version }) => `${block.blockName} @ ${version}`)
65
+ .join(", ");
66
+ const missingVersions = [...new Set(missingEntries.map(({ version }) => version))].sort(compareMigrationVersionLabels);
67
+ throw new Error(`Missing migration ${command} inputs for ${missingLabels}. ` +
68
+ `Run \`${formatScaffoldCommand(missingVersions)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
69
+ }
70
+ return groupEntriesByBlock(discoveredEntries.filter((entry) => targetVersions.includes(entry.fromVersion)));
71
+ }
72
+ export function assertDistinctMigrationEdge(command, fromVersion, toVersion) {
73
+ if (fromVersion === toVersion) {
74
+ throw new Error(`\`migrate ${command}\` requires different source and target migration versions, but both resolved to ${fromVersion}. ` +
75
+ `Choose an older snapshot with \`--from-migration-version <label>\` or capture a newer schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.`);
76
+ }
77
+ }
78
+ export function createMigrationPlanNextSteps(fromVersion, targetVersion, currentVersion) {
79
+ if (targetVersion !== currentVersion) {
80
+ return [
81
+ formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion),
82
+ ];
83
+ }
84
+ return [
85
+ formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion),
86
+ `wp-typia migrate doctor --from-migration-version ${fromVersion}`,
87
+ `wp-typia migrate verify --from-migration-version ${fromVersion}`,
88
+ `wp-typia migrate fuzz --from-migration-version ${fromVersion}`,
89
+ ];
90
+ }
91
+ export function formatEdgeCommand(command, fromVersion, targetVersion, currentVersion) {
92
+ return targetVersion === currentVersion
93
+ ? `wp-typia migrate ${command} --from-migration-version ${fromVersion}`
94
+ : `wp-typia migrate ${command} --from-migration-version ${fromVersion} --to-migration-version ${targetVersion}`;
95
+ }
96
+ export function createMissingProjectSnapshotMessage(state, fromVersion) {
97
+ const snapshotVersions = [
98
+ ...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block))),
99
+ ].sort(compareMigrationVersionLabels);
100
+ return snapshotVersions.length === 0
101
+ ? `No migration block targets have a snapshot for ${fromVersion}. No snapshots exist yet in this project. ` +
102
+ `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.`
103
+ : `No migration block targets have a snapshot for ${fromVersion}. ` +
104
+ `Available snapshot versions in this project: ${snapshotVersions.join(", ")}. ` +
105
+ `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.`;
106
+ }
107
+ export function formatScaffoldCommand(versions) {
108
+ const uniqueVersions = [...new Set(versions)].sort(compareMigrationVersionLabels);
109
+ return uniqueVersions.length === 1
110
+ ? `wp-typia migrate scaffold --from-migration-version ${uniqueVersions[0]}`
111
+ : "wp-typia migrate scaffold --from-migration-version <label>";
112
+ }
113
+ export function collectFixtureTargets(state, targetVersions, targetVersion) {
114
+ return targetVersions.flatMap((version) => state.blocks
115
+ .filter((block) => hasSnapshotForVersion(state, block, version))
116
+ .map((block) => ({
117
+ block,
118
+ fixturePath: getFixtureFilePath(state.paths, block, version, targetVersion),
119
+ scopedLabel: `${block.key}@${version}`,
120
+ version,
121
+ })));
122
+ }
123
+ function groupEntriesByBlock(entries) {
124
+ return entries.reduce((accumulator, entry) => {
125
+ if (!accumulator[entry.block.key]) {
126
+ accumulator[entry.block.key] = [];
127
+ }
128
+ accumulator[entry.block.key].push(entry);
129
+ return accumulator;
130
+ }, {});
131
+ }
@@ -0,0 +1,6 @@
1
+ import type { MigrationConfig } from "./migration-types.js";
2
+ export declare function normalizeRelativePath(value: string): string;
3
+ export declare function ensureRelativePath(projectDir: string, filePath: string): string;
4
+ export declare function hasLegacyConfigKeys(source: string): boolean;
5
+ export declare function createLegacyMigrationWorkspaceResetError(reason: string): Error;
6
+ export declare function parseMigrationConfig(source: string): MigrationConfig;