wp-typia 0.24.4 → 0.24.6

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 (58) hide show
  1. package/README.md +8 -6
  2. package/bin/wp-typia.js +24 -103
  3. package/{dist-bunli/node-cli.js → dist/cli.js} +5086 -3693
  4. package/package.json +9 -36
  5. package/bin/routing-metadata.generated.d.ts +0 -8
  6. package/bin/routing-metadata.generated.js +0 -93
  7. package/bin/runtime-routing.d.ts +0 -34
  8. package/bin/runtime-routing.js +0 -124
  9. package/dist-bunli/.bunli/commands.gen.js +0 -304441
  10. package/dist-bunli/.bunli/highlights-eq9cgrbb.scm +0 -604
  11. package/dist-bunli/.bunli/highlights-ghv9g403.scm +0 -205
  12. package/dist-bunli/.bunli/highlights-hk7bwhj4.scm +0 -284
  13. package/dist-bunli/.bunli/highlights-r812a2qc.scm +0 -150
  14. package/dist-bunli/.bunli/highlights-x6tmsnaa.scm +0 -115
  15. package/dist-bunli/.bunli/injections-73j83es3.scm +0 -27
  16. package/dist-bunli/.bunli/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  17. package/dist-bunli/.bunli/tree-sitter-markdown-411r6y9b.wasm +0 -0
  18. package/dist-bunli/.bunli/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  19. package/dist-bunli/.bunli/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  20. package/dist-bunli/.bunli/tree-sitter-zig-e78zbjpm.wasm +0 -0
  21. package/dist-bunli/agents-91fpdyyt.js +0 -12
  22. package/dist-bunli/chunk-bdqvmfwv-f5qmzmxg.js +0 -16825
  23. package/dist-bunli/cli-03j0axbt.js +0 -163
  24. package/dist-bunli/cli-1170yyve.js +0 -106
  25. package/dist-bunli/cli-368d4cgy.js +0 -1235
  26. package/dist-bunli/cli-377p86mf.js +0 -191
  27. package/dist-bunli/cli-6v0pcxw6.js +0 -314
  28. package/dist-bunli/cli-84c7wff4.js +0 -198
  29. package/dist-bunli/cli-8hxf9qw6.js +0 -198
  30. package/dist-bunli/cli-9fx0qgb7.js +0 -3680
  31. package/dist-bunli/cli-ac2ebaf8.js +0 -3
  32. package/dist-bunli/cli-add-qjd3ba8j.js +0 -10671
  33. package/dist-bunli/cli-am5x7tb4.js +0 -192
  34. package/dist-bunli/cli-bajwv85z.js +0 -24
  35. package/dist-bunli/cli-ccax7s0s.js +0 -34
  36. package/dist-bunli/cli-cvxvcw7c.js +0 -46
  37. package/dist-bunli/cli-diagnostics-10drxh34.js +0 -34
  38. package/dist-bunli/cli-doctor-6fyxq940.js +0 -1446
  39. package/dist-bunli/cli-e4bwd81c.js +0 -1260
  40. package/dist-bunli/cli-fv4h3ydt.js +0 -173823
  41. package/dist-bunli/cli-hv2yedw2.js +0 -74591
  42. package/dist-bunli/cli-init-7avk42dh.js +0 -880
  43. package/dist-bunli/cli-kfm9mm68.js +0 -14679
  44. package/dist-bunli/cli-prompt-ncyg68rn.js +0 -12
  45. package/dist-bunli/cli-rdcga1bd.js +0 -135
  46. package/dist-bunli/cli-scaffold-0bb6pr3w.js +0 -538
  47. package/dist-bunli/cli-t73q5aqz.js +0 -103
  48. package/dist-bunli/cli-templates-g8t4fm11.js +0 -167
  49. package/dist-bunli/cli-tj7ajdvf.js +0 -2612
  50. package/dist-bunli/cli-tq730sqt.js +0 -344
  51. package/dist-bunli/cli-xnn9xjcy.js +0 -68
  52. package/dist-bunli/cli-z48frc8t.js +0 -229
  53. package/dist-bunli/cli.js +0 -2523
  54. package/dist-bunli/command-list-y3g7e9rb.js +0 -4013
  55. package/dist-bunli/create-template-validation-4fr851vg.js +0 -16
  56. package/dist-bunli/migrations-3vngdy51.js +0 -47
  57. package/dist-bunli/sync-k2k8svyc.js +0 -13
  58. package/dist-bunli/workspace-project-gmv2a71z.js +0 -22
@@ -1,2612 +0,0 @@
1
- // @bun
2
- import {
3
- MIGRATION_TODO_PREFIX,
4
- ROOT_PHP_MIGRATION_REGISTRY,
5
- SNAPSHOT_DIR,
6
- assertMigrationVersionLabel,
7
- assertNoLegacySemverMigrationWorkspace,
8
- assertRuleHasNoTodos,
9
- compareMigrationVersionLabels,
10
- copyFile,
11
- createFixtureScalarValue,
12
- createMissingBlockSnapshotMessage,
13
- createTransformFixtureValue,
14
- deleteValueAtPath,
15
- detectPackageManagerId,
16
- discoverMigrationEntries,
17
- discoverMigrationInitLayout,
18
- ensureAdvancedMigrationProject,
19
- ensureMigrationDirectories,
20
- escapeForCode,
21
- formatLegacyMigrationWorkspaceResetGuidance,
22
- getAvailableSnapshotVersionsForBlock,
23
- getFixtureFilePath,
24
- getGeneratedDirForBlock,
25
- getLocalTsxBinary,
26
- getProjectPaths,
27
- getRuleFilePath,
28
- getSnapshotBlockJsonPath,
29
- getSnapshotManifestPath,
30
- getSnapshotRoot,
31
- getSnapshotSavePath,
32
- getValueAtPath,
33
- isInteractiveTerminal,
34
- isNumber,
35
- loadMigrationProject,
36
- readJson,
37
- readRuleMetadata,
38
- renderObjectKey,
39
- renderPhpValue,
40
- resolveTargetMigrationVersion,
41
- runProjectScriptIfPresent,
42
- sanitizeSaveSnapshotSource,
43
- sanitizeSnapshotBlockJson,
44
- setValueAtPath,
45
- writeInitialMigrationScaffold,
46
- writeMigrationConfig
47
- } from "./cli-e4bwd81c.js";
48
- import {
49
- readWorkspaceInventory
50
- } from "./cli-fv4h3ydt.js";
51
- import {
52
- getInvalidWorkspaceProjectReason,
53
- tryResolveWorkspaceProject
54
- } from "./cli-1170yyve.js";
55
- import {
56
- formatRunScript
57
- } from "./cli-am5x7tb4.js";
58
- import {
59
- cloneJsonValue
60
- } from "./cli-ccax7s0s.js";
61
- import {
62
- createReadlinePrompt
63
- } from "./cli-rdcga1bd.js";
64
-
65
- // ../wp-typia-project-tools/src/runtime/migrations.ts
66
- import fs8 from "fs";
67
- import path8 from "path";
68
-
69
- // ../wp-typia-project-tools/src/runtime/migration-command-surface.ts
70
- function formatMigrationHelpText() {
71
- return `Usage:
72
- wp-typia migrate init --current-migration-version <label>
73
- wp-typia migrate snapshot --migration-version <label>
74
- wp-typia migrate plan --from-migration-version <label> [--to-migration-version current]
75
- wp-typia migrate wizard
76
- wp-typia migrate diff --from-migration-version <label> [--to-migration-version current]
77
- wp-typia migrate scaffold --from-migration-version <label> [--to-migration-version current]
78
- wp-typia migrate verify [--from-migration-version <label>|--all]
79
- wp-typia migrate doctor [--from-migration-version <label>|--all]
80
- wp-typia migrate fixtures [--from-migration-version <label>|--all] [--to-migration-version current] [--force]
81
- wp-typia migrate fuzz [--from-migration-version <label>|--all] [--iterations <n>] [--seed <n>]
82
-
83
- Notes:
84
- \`migrate init\` auto-detects supported single-block and \`src/blocks/*\` multi-block layouts.
85
- \`migrate init\` only retrofits migration support into projects that already match those layouts.
86
- A broader project-level \`wp-typia init\` path remains future work.
87
- Migration versions use strict schema labels like \`v1\`, \`v2\`, and \`v3\`.
88
- \`migrate wizard\` is TTY-only and helps you choose one legacy migration version to preview.
89
- \`migrate plan\` and \`migrate wizard\` are read-only previews; they do not scaffold rules or fixtures.
90
- --all runs across every configured legacy migration version and every configured block target.
91
- Existing fixture files are preserved and reported as skipped unless you pass \`--force\`.
92
- Use \`migrate fixtures --force\` as the explicit refresh path for generated fixture files.
93
- In TTY usage, \`migrate fixtures --force\` asks before overwriting existing fixture files.
94
- In non-interactive usage, \`migrate fixtures --force\` overwrites immediately for script compatibility.`;
95
- }
96
- function parseMigrationArgs(argv) {
97
- const parsed = {
98
- command: undefined,
99
- flags: {
100
- all: false,
101
- currentMigrationVersion: undefined,
102
- force: false,
103
- fromMigrationVersion: undefined,
104
- iterations: undefined,
105
- migrationVersion: undefined,
106
- seed: undefined,
107
- toMigrationVersion: "current"
108
- }
109
- };
110
- if (argv.length === 0) {
111
- throw new Error(formatMigrationHelpText());
112
- }
113
- parsed.command = argv[0];
114
- for (let index = 1;index < argv.length; index += 1) {
115
- const arg = argv[index];
116
- const next = argv[index + 1];
117
- if (arg === "--")
118
- continue;
119
- if (arg === "--all") {
120
- parsed.flags.all = true;
121
- continue;
122
- }
123
- if (arg === "--force") {
124
- parsed.flags.force = true;
125
- continue;
126
- }
127
- if (arg === "--current-migration-version") {
128
- parsed.flags.currentMigrationVersion = next;
129
- index += 1;
130
- continue;
131
- }
132
- if (arg.startsWith("--current-migration-version=")) {
133
- parsed.flags.currentMigrationVersion = arg.split("=", 2)[1];
134
- continue;
135
- }
136
- if (arg === "--from-migration-version") {
137
- parsed.flags.fromMigrationVersion = next;
138
- index += 1;
139
- continue;
140
- }
141
- if (arg.startsWith("--from-migration-version=")) {
142
- parsed.flags.fromMigrationVersion = arg.split("=", 2)[1];
143
- continue;
144
- }
145
- if (arg === "--iterations") {
146
- parsed.flags.iterations = next;
147
- index += 1;
148
- continue;
149
- }
150
- if (arg.startsWith("--iterations=")) {
151
- parsed.flags.iterations = arg.split("=", 2)[1];
152
- continue;
153
- }
154
- if (arg === "--seed") {
155
- parsed.flags.seed = next;
156
- index += 1;
157
- continue;
158
- }
159
- if (arg.startsWith("--seed=")) {
160
- parsed.flags.seed = arg.split("=", 2)[1];
161
- continue;
162
- }
163
- if (arg === "--to-migration-version") {
164
- parsed.flags.toMigrationVersion = next;
165
- index += 1;
166
- continue;
167
- }
168
- if (arg.startsWith("--to-migration-version=")) {
169
- parsed.flags.toMigrationVersion = arg.split("=", 2)[1];
170
- continue;
171
- }
172
- if (arg === "--migration-version") {
173
- parsed.flags.migrationVersion = next;
174
- index += 1;
175
- continue;
176
- }
177
- if (arg.startsWith("--migration-version=")) {
178
- parsed.flags.migrationVersion = arg.split("=", 2)[1];
179
- continue;
180
- }
181
- if (arg === "--current-version" || arg.startsWith("--current-version=") || arg === "--version" || arg.startsWith("--version=") || arg === "--from" || arg.startsWith("--from=") || arg === "--to" || arg.startsWith("--to=")) {
182
- throwLegacyMigrationFlagError(arg);
183
- }
184
- throw new Error(`Unknown migration flag: ${arg}`);
185
- }
186
- return parsed;
187
- }
188
- function parsePositiveInteger(value, label) {
189
- if (!value) {
190
- return;
191
- }
192
- if (!/^\d+$/.test(value)) {
193
- throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
194
- }
195
- const parsed = Number.parseInt(value, 10);
196
- if (!Number.isInteger(parsed) || parsed <= 0) {
197
- throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
198
- }
199
- return parsed;
200
- }
201
- function parseNonNegativeInteger(value, label) {
202
- if (!value) {
203
- return;
204
- }
205
- if (!/^\d+$/.test(value)) {
206
- throw new Error(`Invalid ${label}: ${value}. Expected a non-negative integer.`);
207
- }
208
- const parsed = Number.parseInt(value, 10);
209
- if (!Number.isInteger(parsed) || parsed < 0) {
210
- throw new Error(`Invalid ${label}: ${value}. Expected a non-negative integer.`);
211
- }
212
- return parsed;
213
- }
214
- function throwLegacyMigrationFlagError(flag) {
215
- const replacement = flag.startsWith("--current-version") ? "--current-migration-version" : flag.startsWith("--version") ? "--migration-version" : flag.startsWith("--from") ? "--from-migration-version" : "--to-migration-version";
216
- throw new Error(`Legacy migration flag \`${flag}\` is no longer supported. Use \`${replacement}\` with schema labels like \`v1\` and \`v2\` instead. ` + formatLegacyMigrationWorkspaceResetGuidance());
217
- }
218
-
219
- // ../wp-typia-project-tools/src/runtime/migration-diff.ts
220
- import fs from "fs";
221
-
222
- // ../wp-typia-project-tools/src/runtime/migration-manifest.ts
223
- function flattenManifestLeafAttributes(attributes) {
224
- return Object.entries(attributes).flatMap(([key, attribute]) => flattenManifestAttribute(attribute, key, key, {
225
- rootPath: key,
226
- unionBranch: null,
227
- unionDiscriminator: null,
228
- unionRoot: null
229
- }));
230
- }
231
- function flattenManifestAttribute(attribute, currentPath, sourcePath, context) {
232
- if (!attribute) {
233
- return [];
234
- }
235
- if (attribute.ts.kind === "object") {
236
- const properties = Object.entries(attribute.ts.properties ?? {});
237
- if (properties.length === 0) {
238
- return [{ ...context, attribute, currentPath, sourcePath }];
239
- }
240
- return properties.flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${key}`, `${sourcePath}.${key}`, {
241
- ...context
242
- }));
243
- }
244
- if (attribute.ts.kind === "union" && attribute.ts.union) {
245
- const unionMetadata = attribute.ts.union;
246
- return Object.entries(attribute.ts.union.branches ?? {}).flatMap(([branchKey, branchAttribute]) => Object.entries(branchAttribute.ts.properties ?? {}).filter(([key]) => key !== unionMetadata.discriminator).flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${branchKey}.${key}`, `${sourcePath}.${key}`, {
247
- rootPath: context.rootPath,
248
- unionBranch: branchKey,
249
- unionDiscriminator: unionMetadata.discriminator,
250
- unionRoot: currentPath
251
- })));
252
- }
253
- return [{ ...context, attribute, currentPath, sourcePath }];
254
- }
255
- function getAttributeByCurrentPath(attributes, currentPath) {
256
- const segments = String(currentPath).split(".");
257
- const rootKey = segments.shift();
258
- if (!rootKey) {
259
- return null;
260
- }
261
- let attribute = attributes[rootKey] ?? null;
262
- while (attribute && segments.length > 0) {
263
- if (attribute.ts.kind === "union" && attribute.ts.union) {
264
- const branchKey = segments.shift();
265
- if (!branchKey || !(branchKey in attribute.ts.union.branches)) {
266
- return null;
267
- }
268
- attribute = attribute.ts.union.branches[branchKey];
269
- continue;
270
- }
271
- if (attribute.ts.kind === "object" && attribute.ts.properties) {
272
- const propertyKey = segments.shift();
273
- if (!propertyKey || !(propertyKey in attribute.ts.properties)) {
274
- return null;
275
- }
276
- attribute = attribute.ts.properties[propertyKey];
277
- continue;
278
- }
279
- return null;
280
- }
281
- return attribute;
282
- }
283
- function hasManifestDefault(attribute) {
284
- return attribute?.typia?.hasDefault === true;
285
- }
286
- function getManifestDefaultValue(attribute) {
287
- return attribute?.typia?.defaultValue ?? null;
288
- }
289
- function summarizeManifest(manifest) {
290
- return {
291
- attributes: Object.fromEntries(Object.entries(manifest.attributes ?? {}).map(([name, attribute]) => [
292
- name,
293
- {
294
- constraints: attribute.typia?.constraints ?? {},
295
- defaultValue: attribute.typia?.defaultValue ?? null,
296
- hasDefault: attribute.typia?.hasDefault ?? false,
297
- enum: attribute.wp?.enum ?? null,
298
- kind: attribute.ts?.kind ?? null,
299
- required: attribute.ts?.required ?? false,
300
- union: attribute.ts?.union ?? null
301
- }
302
- ])),
303
- manifestVersion: manifest.manifestVersion ?? null,
304
- sourceType: manifest.sourceType ?? null
305
- };
306
- }
307
- function summarizeUnionBranches(manifestSummary) {
308
- if (!manifestSummary?.attributes) {
309
- return [];
310
- }
311
- return Object.entries(manifestSummary.attributes).filter(([, attribute]) => attribute.kind === "union" && attribute.union).map(([field, attribute]) => ({
312
- branches: Object.keys(attribute.union?.branches ?? {}),
313
- discriminator: attribute.union?.discriminator ?? null,
314
- field
315
- }));
316
- }
317
- function defaultValueForManifestAttribute(attribute) {
318
- if (attribute.typia?.hasDefault) {
319
- return attribute.typia.defaultValue;
320
- }
321
- if (attribute.wp?.enum && attribute.wp.enum.length > 0) {
322
- return attribute.wp.enum[0] ?? null;
323
- }
324
- switch (attribute.ts.kind) {
325
- case "string":
326
- return "";
327
- case "number":
328
- return 0;
329
- case "boolean":
330
- return false;
331
- case "array":
332
- return [];
333
- case "object": {
334
- const result = {};
335
- for (const [key, property] of Object.entries(attribute.ts.properties ?? {})) {
336
- result[key] = defaultValueForManifestAttribute(property);
337
- }
338
- return result;
339
- }
340
- case "union": {
341
- const firstBranch = Object.values(attribute.ts.union?.branches ?? {})[0];
342
- return firstBranch ? defaultValueForManifestAttribute(firstBranch) : null;
343
- }
344
- default:
345
- return null;
346
- }
347
- }
348
-
349
- // ../wp-typia-project-tools/src/runtime/migration-diff-rename.ts
350
- function createRenameCandidates({
351
- addedKeys,
352
- isUnionRenameCompatible,
353
- newAttributes,
354
- newLeafAttributes,
355
- oldAttributes,
356
- oldLeafAttributes,
357
- removedKeys
358
- }) {
359
- const assessments = [];
360
- for (const currentPath of addedKeys) {
361
- const nextAttribute = newAttributes[currentPath];
362
- if (!nextAttribute)
363
- continue;
364
- for (const legacyPath of removedKeys) {
365
- const previous = oldAttributes[legacyPath];
366
- if (!previous)
367
- continue;
368
- const candidate = assessRenameCandidate(previous, nextAttribute, legacyPath, currentPath, isUnionRenameCompatible);
369
- if (candidate) {
370
- assessments.push(candidate);
371
- }
372
- }
373
- }
374
- const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
375
- const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
376
- const removedLeafDescriptors = oldLeafAttributes.filter((descriptor) => !newLeafMap.has(descriptor.currentPath));
377
- const addedLeafDescriptors = newLeafAttributes.filter((descriptor) => !oldLeafMap.has(descriptor.currentPath));
378
- for (const nextDescriptor of addedLeafDescriptors) {
379
- if (!nextDescriptor.currentPath.includes(".")) {
380
- continue;
381
- }
382
- for (const previousDescriptor of removedLeafDescriptors) {
383
- if (!previousDescriptor.currentPath.includes(".")) {
384
- continue;
385
- }
386
- const candidate = assessRenameCandidate(previousDescriptor.attribute, nextDescriptor.attribute, previousDescriptor.currentPath, nextDescriptor.currentPath, isUnionRenameCompatible);
387
- if (candidate) {
388
- assessments.push(candidate);
389
- }
390
- }
391
- }
392
- return assessments.map((candidate) => {
393
- const currentMatches = assessments.filter((item) => item.currentPath === candidate.currentPath).sort((left, right) => right.score - left.score);
394
- const legacyMatches = assessments.filter((item) => item.legacyPath === candidate.legacyPath).sort((left, right) => right.score - left.score);
395
- const currentLeader = currentMatches[0];
396
- const legacyLeader = legacyMatches[0];
397
- const currentHasTie = currentMatches.length > 1 && Math.abs((currentMatches[1]?.score ?? 0) - currentLeader.score) < 0.05;
398
- const legacyHasTie = legacyMatches.length > 1 && Math.abs((legacyMatches[1]?.score ?? 0) - legacyLeader.score) < 0.05;
399
- return {
400
- ...candidate,
401
- autoApply: currentLeader.legacyPath === candidate.legacyPath && legacyLeader.currentPath === candidate.currentPath && !currentHasTie && !legacyHasTie && candidate.score >= 0.6
402
- };
403
- }).filter((candidate, index, list) => {
404
- const firstMatch = list.findIndex((item) => item.currentPath === candidate.currentPath && item.legacyPath === candidate.legacyPath);
405
- return firstMatch === index;
406
- }).sort((left, right) => right.score - left.score);
407
- }
408
- function passesNameSimilarityRule(legacyPath, currentPath) {
409
- return scoreRenameSimilarity(legacyPath, currentPath) >= 0.6;
410
- }
411
- function isRenameCandidateShapeCompatible(oldAttribute, newAttribute, isUnionRenameCompatible) {
412
- if (!oldAttribute || !newAttribute || oldAttribute.ts.kind !== newAttribute.ts.kind) {
413
- return false;
414
- }
415
- if (["string", "number", "boolean"].includes(oldAttribute.ts.kind)) {
416
- return hasRenameCompatibleConstraints(oldAttribute, newAttribute);
417
- }
418
- if (oldAttribute.ts.kind === "union") {
419
- return isUnionRenameCompatible(oldAttribute, newAttribute);
420
- }
421
- return false;
422
- }
423
- function assessRenameCandidate(oldAttribute, newAttribute, legacyPath, currentPath, isUnionRenameCompatible) {
424
- if (!isRenameCandidateShapeCompatible(oldAttribute, newAttribute, isUnionRenameCompatible)) {
425
- return null;
426
- }
427
- const baseScore = scoreRenameSimilarity(legacyPath, currentPath);
428
- const score = getParentPath(legacyPath) === getParentPath(currentPath) ? Math.max(baseScore, 0.75) : baseScore;
429
- return {
430
- autoApply: false,
431
- currentPath,
432
- legacyPath,
433
- reason: describeRenameReason(oldAttribute, legacyPath, currentPath, score),
434
- score
435
- };
436
- }
437
- function hasRenameCompatibleConstraints(oldAttribute, newAttribute) {
438
- const oldEnum = oldAttribute.wp.enum ?? null;
439
- const nextEnum = newAttribute.wp.enum ?? null;
440
- if (oldEnum && nextEnum) {
441
- const oldIsSubset = oldEnum.every((value) => nextEnum.includes(value));
442
- if (!oldIsSubset) {
443
- return false;
444
- }
445
- } else if (oldEnum && !nextEnum) {
446
- return false;
447
- }
448
- const oldConstraints = oldAttribute.typia.constraints ?? {};
449
- const nextConstraints = newAttribute.typia.constraints ?? {};
450
- return [
451
- compareMinimumBound(oldConstraints.minLength, nextConstraints.minLength),
452
- compareMaximumBound(oldConstraints.maxLength, nextConstraints.maxLength),
453
- compareMinimumBound(oldConstraints.minimum, nextConstraints.minimum),
454
- compareMaximumBound(oldConstraints.maximum, nextConstraints.maximum),
455
- comparePatternBound(oldConstraints.pattern, nextConstraints.pattern),
456
- comparePatternBound(oldConstraints.format, nextConstraints.format),
457
- comparePatternBound(oldConstraints.typeTag, nextConstraints.typeTag)
458
- ].every(Boolean);
459
- }
460
- function compareMinimumBound(oldValue, nextValue) {
461
- if (nextValue === null || nextValue === undefined)
462
- return true;
463
- if (oldValue === null || oldValue === undefined)
464
- return true;
465
- return Number(oldValue) <= Number(nextValue);
466
- }
467
- function compareMaximumBound(oldValue, nextValue) {
468
- if (nextValue === null || nextValue === undefined)
469
- return true;
470
- if (oldValue === null || oldValue === undefined)
471
- return true;
472
- return Number(oldValue) >= Number(nextValue);
473
- }
474
- function comparePatternBound(oldValue, nextValue) {
475
- return oldValue === nextValue || oldValue === null || oldValue === undefined;
476
- }
477
- function scoreRenameSimilarity(legacyPath, currentPath) {
478
- const legacy = normalizeFieldName(legacyPath);
479
- const current = normalizeFieldName(currentPath);
480
- if (legacy === current)
481
- return 1;
482
- if (shareAliasGroup(legacy, current))
483
- return 0.9;
484
- const legacyTokens = tokenizeFieldName(legacy);
485
- const currentTokens = tokenizeFieldName(current);
486
- const overlap = legacyTokens.filter((token) => currentTokens.includes(token));
487
- const jaccard = overlap.length / new Set([...legacyTokens, ...currentTokens]).size;
488
- if (legacy.includes(current) || current.includes(legacy)) {
489
- return Math.max(jaccard, 0.7);
490
- }
491
- if (legacyTokens.length > 0 && currentTokens.length > 0 && legacyTokens[legacyTokens.length - 1] === currentTokens[currentTokens.length - 1]) {
492
- return Math.max(jaccard, 0.6);
493
- }
494
- return jaccard;
495
- }
496
- function normalizeFieldName(name) {
497
- return String(name).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase().replace(/\s+/g, "");
498
- }
499
- function tokenizeFieldName(name) {
500
- return String(name).replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
501
- }
502
- function getParentPath(pathLabel) {
503
- const segments = String(pathLabel).split(".");
504
- return segments.length <= 1 ? "" : segments.slice(0, -1).join(".");
505
- }
506
- function shareAliasGroup(left, right) {
507
- const aliasGroups = [
508
- ["content", "headline", "body", "text", "copy", "message"],
509
- ["id", "uniqueid", "uuid"],
510
- ["visible", "isvisible", "show", "shown", "enabled"],
511
- ["align", "alignment", "textalign"],
512
- ["count", "clickcount", "counter"],
513
- ["url", "href", "link"]
514
- ];
515
- return aliasGroups.some((group) => group.includes(left) && group.includes(right));
516
- }
517
- function describeRenameReason(attribute, legacyPath, currentPath, score) {
518
- if (attribute.ts.kind === "union") {
519
- return `compatible discriminated union (${legacyPath} \u2192 ${currentPath})`;
520
- }
521
- if (score >= 0.9)
522
- return "high-confidence compatible field";
523
- if (score >= 0.6)
524
- return "name-similar compatible field";
525
- return "compatible field requiring review";
526
- }
527
-
528
- // ../wp-typia-project-tools/src/runtime/migration-diff-transform.ts
529
- function createTransformSuggestions({
530
- oldAttributes,
531
- newAttributes,
532
- addedKeys,
533
- removedKeys,
534
- manualItems,
535
- renameCandidates,
536
- oldLeafAttributes,
537
- newLeafAttributes
538
- }) {
539
- const suggestions = [];
540
- const activeRenameTargets = new Set(renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => candidate.currentPath));
541
- const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
542
- const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
543
- for (const currentPath of [
544
- ...new Set([
545
- ...Object.keys(newAttributes),
546
- ...manualItems.map((item) => item.path),
547
- ...newLeafAttributes.map((item) => item.currentPath)
548
- ])
549
- ]) {
550
- if (activeRenameTargets.has(currentPath)) {
551
- continue;
552
- }
553
- const manualItem = manualItems.find((item) => item.path === currentPath || item.path.startsWith(`${currentPath}.`));
554
- const currentAttribute = newLeafMap.get(currentPath)?.attribute ?? getAttributeByCurrentPath(newAttributes, currentPath) ?? null;
555
- if (!manualItem || !currentAttribute) {
556
- continue;
557
- }
558
- const exactLegacy = oldLeafMap.get(currentPath)?.attribute ?? getAttributeByCurrentPath(oldAttributes, currentPath) ?? null;
559
- if (exactLegacy && exactLegacy.ts.kind !== currentAttribute.ts.kind) {
560
- suggestions.push({
561
- bodyLines: buildTransformBodyLines(currentAttribute, currentPath),
562
- attribute: currentAttribute,
563
- currentPath,
564
- legacyPath: currentPath,
565
- reason: `semantic coercion suggested for ${manualItem.kind}`
566
- });
567
- continue;
568
- }
569
- const bestRenameCandidate = renameCandidates.find((candidate) => candidate.currentPath === currentPath);
570
- if (bestRenameCandidate && !bestRenameCandidate.autoApply) {
571
- suggestions.push({
572
- bodyLines: buildTransformBodyLines(currentAttribute, bestRenameCandidate.legacyPath),
573
- attribute: currentAttribute,
574
- currentPath,
575
- legacyPath: bestRenameCandidate.legacyPath,
576
- reason: `review coercion from ${bestRenameCandidate.legacyPath}`
577
- });
578
- continue;
579
- }
580
- const addedCurrent = addedKeys.includes(currentPath) || newLeafMap.has(currentPath) && !oldLeafMap.has(currentPath);
581
- if (!addedCurrent) {
582
- continue;
583
- }
584
- const compatibleLegacyPath = [
585
- ...removedKeys,
586
- ...oldLeafAttributes.filter((descriptor) => !newLeafMap.has(descriptor.currentPath)).map((descriptor) => descriptor.currentPath)
587
- ].find((legacyPath) => passesNameSimilarityRule(legacyPath, currentPath));
588
- if (compatibleLegacyPath) {
589
- suggestions.push({
590
- bodyLines: buildTransformBodyLines(currentAttribute, compatibleLegacyPath),
591
- attribute: currentAttribute,
592
- currentPath,
593
- legacyPath: compatibleLegacyPath,
594
- reason: `review coercion from ${compatibleLegacyPath}`
595
- });
596
- }
597
- }
598
- return suggestions;
599
- }
600
- function describeConstraintChange(oldAttribute, newAttribute) {
601
- const details = [];
602
- const oldConstraints = oldAttribute.typia.constraints;
603
- const nextConstraints = newAttribute.typia.constraints;
604
- if (newAttribute.wp.enum && JSON.stringify(newAttribute.wp.enum) !== JSON.stringify(oldAttribute.wp.enum)) {
605
- details.push("enum changed");
606
- }
607
- for (const key of ["minLength", "maxLength", "minimum", "maximum", "pattern", "format", "typeTag"]) {
608
- if (oldConstraints[key] !== nextConstraints[key]) {
609
- details.push(`${key}: ${oldConstraints[key]} -> ${nextConstraints[key]}`);
610
- }
611
- }
612
- return details.join(", ");
613
- }
614
- function buildTransformBodyLines(attribute, legacyPath) {
615
- switch (attribute.ts.kind) {
616
- case "string":
617
- return [`// return typeof legacyValue === "string" ? legacyValue : String(legacyValue ?? "");`];
618
- case "number":
619
- return [
620
- `// const numericValue = typeof legacyValue === "number" ? legacyValue : Number(legacyValue ?? 0);`,
621
- `// return Number.isNaN(numericValue) ? undefined : numericValue;`
622
- ];
623
- case "boolean":
624
- return [`// return typeof legacyValue === "boolean" ? legacyValue : Boolean(legacyValue);`];
625
- case "union":
626
- return [
627
- `// const legacyObject = typeof legacyValue === "object" && legacyValue !== null ? legacyValue : {};`,
628
- `// return legacyObject; // adjust discriminator / branch fields before verify`
629
- ];
630
- default:
631
- return [`// return legacyValue; // customize migration from ${legacyPath}`];
632
- }
633
- }
634
-
635
- // ../wp-typia-project-tools/src/runtime/migration-diff.ts
636
- function createMigrationDiff(state, blockOrFromVersion, fromVersionOrToVersion, maybeToVersion) {
637
- if (typeof blockOrFromVersion === "string" && state.blocks.length > 1) {
638
- throw new Error("A block key is required when diffing a multi-block migration project.");
639
- }
640
- const block = typeof blockOrFromVersion === "string" ? state.blocks[0] : state.blocks.find((entry) => entry.key === blockOrFromVersion.key) ?? null;
641
- const fromVersion = typeof blockOrFromVersion === "string" ? blockOrFromVersion : fromVersionOrToVersion;
642
- const toVersion = typeof blockOrFromVersion === "string" ? fromVersionOrToVersion : maybeToVersion ?? state.config.currentMigrationVersion;
643
- if (!block) {
644
- throw new Error(typeof blockOrFromVersion === "string" ? "No migration block targets are configured for this project." : `Unknown migration block key: ${blockOrFromVersion.key}`);
645
- }
646
- const snapshotManifestPath = getSnapshotManifestPath(state.projectDir, block, fromVersion);
647
- if (!fs.existsSync(snapshotManifestPath)) {
648
- throw new Error(createMissingBlockSnapshotMessage(block.blockName, fromVersion, getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block)));
649
- }
650
- const targetManifest = toVersion === state.config.currentMigrationVersion ? block.currentManifest : readJson(getSnapshotManifestPath(state.projectDir, block, toVersion));
651
- const oldManifest = readJson(snapshotManifestPath);
652
- const oldAttributes = oldManifest.attributes ?? {};
653
- const newAttributes = targetManifest.attributes ?? {};
654
- const oldLeafAttributes = flattenManifestLeafAttributes(oldAttributes);
655
- const newLeafAttributes = flattenManifestLeafAttributes(newAttributes);
656
- const autoItems = [];
657
- const manualItems = [];
658
- const addedKeys = [];
659
- const removedKeys = [];
660
- for (const [key, newAttribute] of Object.entries(newAttributes)) {
661
- const oldAttribute = oldAttributes[key];
662
- if (!oldAttribute) {
663
- addedKeys.push(key);
664
- if (newAttribute.ts.required && !hasManifestDefault(newAttribute)) {
665
- manualItems.push({
666
- detail: "required field has no default in current schema",
667
- kind: "required-addition",
668
- path: key,
669
- status: "manual"
670
- });
671
- } else {
672
- autoItems.push({
673
- detail: hasManifestDefault(newAttribute) ? `default ${JSON.stringify(getManifestDefaultValue(newAttribute))}` : "optional addition",
674
- kind: hasManifestDefault(newAttribute) ? "add-default" : "add-optional",
675
- path: key,
676
- status: "auto"
677
- });
678
- }
679
- continue;
680
- }
681
- const outcome = compareManifestAttribute(oldAttribute, newAttribute, key);
682
- if (outcome.status === "manual") {
683
- manualItems.push(outcome);
684
- } else {
685
- autoItems.push(outcome);
686
- }
687
- }
688
- for (const key of Object.keys(oldAttributes)) {
689
- if (!(key in newAttributes)) {
690
- removedKeys.push(key);
691
- autoItems.push({
692
- detail: "field removed from current schema",
693
- kind: "drop",
694
- path: key,
695
- status: "auto"
696
- });
697
- }
698
- }
699
- const renameCandidates = createRenameCandidates({
700
- addedKeys,
701
- isUnionRenameCompatible: (oldAttribute, newAttribute) => compareUnionAttribute(oldAttribute, newAttribute, "$rename").status === "auto",
702
- newAttributes,
703
- newLeafAttributes,
704
- oldAttributes,
705
- oldLeafAttributes,
706
- removedKeys
707
- });
708
- const activeRenameCandidates = renameCandidates.filter((candidate) => candidate.autoApply);
709
- for (const candidate of activeRenameCandidates) {
710
- removeOutcomeByPath(autoItems, candidate.legacyPath, "drop");
711
- removeOutcomeByPath(autoItems, candidate.currentPath, "add-default");
712
- removeOutcomeByPath(autoItems, candidate.currentPath, "add-optional");
713
- removeOutcomeByPath(manualItems, candidate.currentPath, "required-addition");
714
- removeOutcomesByPath(manualItems, candidate.currentPath);
715
- autoItems.push({
716
- detail: `legacy field ${candidate.legacyPath}`,
717
- kind: "rename",
718
- path: candidate.currentPath,
719
- status: "auto"
720
- });
721
- }
722
- const transformSuggestions = createTransformSuggestions({
723
- addedKeys,
724
- manualItems,
725
- newAttributes,
726
- newLeafAttributes,
727
- oldAttributes,
728
- oldLeafAttributes,
729
- renameCandidates,
730
- removedKeys
731
- });
732
- return {
733
- currentTypeName: targetManifest.sourceType ?? block.currentManifest.sourceType,
734
- fromVersion,
735
- summary: {
736
- auto: autoItems.length,
737
- autoItems,
738
- manual: manualItems.length,
739
- manualItems,
740
- renameCandidates,
741
- transformSuggestions
742
- },
743
- toVersion
744
- };
745
- }
746
- function removeOutcomeByPath(items, pathLabel, kind) {
747
- const index = items.findIndex((item) => item.path === pathLabel && item.kind === kind);
748
- if (index >= 0) {
749
- items.splice(index, 1);
750
- }
751
- }
752
- function removeOutcomesByPath(items, pathLabel) {
753
- for (let index = items.length - 1;index >= 0; index -= 1) {
754
- if (items[index]?.path === pathLabel) {
755
- items.splice(index, 1);
756
- }
757
- }
758
- }
759
- function compareManifestAttribute(oldAttribute, newAttribute, attributePath) {
760
- if (oldAttribute.ts.kind !== newAttribute.ts.kind) {
761
- return manualOutcome(attributePath, "type-change", `${oldAttribute.ts.kind} -> ${newAttribute.ts.kind}`);
762
- }
763
- if (oldAttribute.ts.kind === "union") {
764
- return compareUnionAttribute(oldAttribute, newAttribute, attributePath);
765
- }
766
- if (oldAttribute.ts.kind === "object") {
767
- return compareObjectAttribute(oldAttribute, newAttribute, attributePath);
768
- }
769
- if (oldAttribute.ts.kind === "array") {
770
- if (!oldAttribute.ts.items || !newAttribute.ts.items) {
771
- return autoOutcome(attributePath, "copy", "array shape unchanged");
772
- }
773
- const nested = compareManifestAttribute(oldAttribute.ts.items, newAttribute.ts.items, `${attributePath}[]`);
774
- return nested.status === "manual" ? nested : autoOutcome(attributePath, "hydrate", "array items can be normalized");
775
- }
776
- if (hasStricterConstraints(oldAttribute, newAttribute)) {
777
- return manualOutcome(attributePath, "stricter-constraints", describeConstraintChange(oldAttribute, newAttribute));
778
- }
779
- return autoOutcome(attributePath, "copy", "compatible primitive field");
780
- }
781
- function compareObjectAttribute(oldAttribute, newAttribute, attributePath) {
782
- const oldProperties = oldAttribute.ts.properties ?? {};
783
- const newProperties = newAttribute.ts.properties ?? {};
784
- for (const [key, nextProperty] of Object.entries(newProperties)) {
785
- const previousProperty = oldProperties[key];
786
- if (!previousProperty) {
787
- if (nextProperty.ts.required && !hasManifestDefault(nextProperty)) {
788
- return manualOutcome(`${attributePath}.${key}`, "object-change", "required field has no default in current schema");
789
- }
790
- continue;
791
- }
792
- const nested = compareManifestAttribute(previousProperty, nextProperty, `${attributePath}.${key}`);
793
- if (nested.status === "manual") {
794
- return nested;
795
- }
796
- }
797
- return autoOutcome(attributePath, "hydrate", "object can be normalized with current manifest defaults");
798
- }
799
- function compareUnionAttribute(oldAttribute, newAttribute, attributePath) {
800
- const oldUnion = oldAttribute.ts.union;
801
- const newUnion = newAttribute.ts.union;
802
- if (!oldUnion || !newUnion) {
803
- return manualOutcome(attributePath, "union-change", "missing union metadata");
804
- }
805
- if (oldUnion.discriminator !== newUnion.discriminator) {
806
- return manualOutcome(attributePath, "union-discriminator-change", `${oldUnion.discriminator} -> ${newUnion.discriminator}`);
807
- }
808
- const oldBranchKeys = Object.keys(oldUnion.branches);
809
- const newBranchKeys = Object.keys(newUnion.branches);
810
- for (const branchKey of oldBranchKeys) {
811
- if (!(branchKey in newUnion.branches)) {
812
- return manualOutcome(attributePath, "union-branch-removal", `branch ${branchKey} was removed`);
813
- }
814
- const nested = compareManifestAttribute(oldUnion.branches[branchKey], newUnion.branches[branchKey], `${attributePath}.${branchKey}`);
815
- if (nested.status === "manual") {
816
- return manualOutcome(nested.path, nested.kind.startsWith("union-") ? nested.kind : "union-manual", nested.detail ?? `branch ${branchKey} requires manual review`);
817
- }
818
- }
819
- const addedBranches = newBranchKeys.filter((branchKey) => !(branchKey in oldUnion.branches));
820
- if (addedBranches.length > 0) {
821
- return autoOutcome(attributePath, "union-branch-addition", `branches added: ${addedBranches.join(", ")}`);
822
- }
823
- return autoOutcome(attributePath, "copy", "compatible discriminated union");
824
- }
825
- function hasStricterConstraints(oldAttribute, newAttribute) {
826
- const oldConstraints = oldAttribute.typia.constraints;
827
- const nextConstraints = newAttribute.typia.constraints;
828
- const oldEnum = oldAttribute.wp.enum ?? null;
829
- const nextEnum = newAttribute.wp.enum ?? null;
830
- if (nextEnum && (!oldEnum || !oldEnum.every((value) => nextEnum.includes(value)))) {
831
- return true;
832
- }
833
- if (isNumber(nextConstraints.minLength) && (!isNumber(oldConstraints.minLength) || nextConstraints.minLength > oldConstraints.minLength)) {
834
- return true;
835
- }
836
- if (isNumber(nextConstraints.maxLength) && (!isNumber(oldConstraints.maxLength) || nextConstraints.maxLength < oldConstraints.maxLength)) {
837
- return true;
838
- }
839
- if (isNumber(nextConstraints.minimum) && (!isNumber(oldConstraints.minimum) || nextConstraints.minimum > oldConstraints.minimum)) {
840
- return true;
841
- }
842
- if (isNumber(nextConstraints.maximum) && (!isNumber(oldConstraints.maximum) || nextConstraints.maximum < oldConstraints.maximum)) {
843
- return true;
844
- }
845
- if (nextConstraints.pattern && nextConstraints.pattern !== oldConstraints.pattern) {
846
- return true;
847
- }
848
- if (nextConstraints.format && nextConstraints.format !== oldConstraints.format) {
849
- return true;
850
- }
851
- if (nextConstraints.typeTag && nextConstraints.typeTag !== oldConstraints.typeTag) {
852
- return true;
853
- }
854
- return false;
855
- }
856
- function autoOutcome(pathLabel, kind, detail) {
857
- return { detail, kind, path: pathLabel, status: "auto" };
858
- }
859
- function manualOutcome(pathLabel, kind, detail) {
860
- return { detail, kind, path: pathLabel, status: "manual" };
861
- }
862
-
863
- // ../wp-typia-project-tools/src/runtime/migration-fixtures.ts
864
- import fs2 from "fs";
865
- import path from "path";
866
- function ensureEdgeFixtureFile(projectDir, block, fromVersion, toVersion, diff, { force = false } = {}) {
867
- const fixturePath = getFixtureFilePath(getProjectPaths(projectDir), block, fromVersion, toVersion);
868
- fs2.mkdirSync(path.dirname(fixturePath), { recursive: true });
869
- if (!force && fs2.existsSync(fixturePath)) {
870
- return { fixturePath, written: false };
871
- }
872
- const fixtureDocument = createEdgeFixtureDocument(projectDir, block, fromVersion, toVersion, diff);
873
- fs2.writeFileSync(fixturePath, `${JSON.stringify(fixtureDocument, null, "\t")}
874
- `, "utf8");
875
- return { fixturePath, written: true };
876
- }
877
- function createEdgeFixtureDocument(projectDir, block, fromVersion, toVersion, diff) {
878
- const manifest = readJson(getSnapshotManifestPath(projectDir, block, fromVersion));
879
- const attributes = {};
880
- for (const [key, attribute] of Object.entries(manifest.attributes ?? {})) {
881
- attributes[key] = defaultValueForManifestAttribute(attribute) ?? null;
882
- }
883
- const cases = [
884
- {
885
- input: attributes,
886
- name: "default"
887
- },
888
- ...createRenameFixtureCases(attributes, diff.summary.renameCandidates),
889
- ...createTransformFixtureCases(attributes, diff.summary.transformSuggestions),
890
- ...createUnionFixtureCases(attributes, manifest.attributes ?? {}, diff.summary.renameCandidates)
891
- ];
892
- return {
893
- cases,
894
- fromVersion,
895
- toVersion
896
- };
897
- }
898
- function createRenameFixtureCases(baseAttributes, renameCandidates) {
899
- return renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => {
900
- const nextInput = cloneJsonValue(baseAttributes);
901
- const legacyValue = getValueAtPath(nextInput, candidate.legacyPath);
902
- deleteValueAtPath(nextInput, candidate.currentPath);
903
- if (legacyValue === undefined) {
904
- setValueAtPath(nextInput, candidate.legacyPath, createFixtureScalarValue(candidate.currentPath));
905
- }
906
- return {
907
- input: nextInput,
908
- name: `rename:${candidate.legacyPath}->${candidate.currentPath}`
909
- };
910
- });
911
- }
912
- function createTransformFixtureCases(baseAttributes, transformSuggestions) {
913
- return transformSuggestions.map((suggestion) => {
914
- const nextInput = cloneJsonValue(baseAttributes);
915
- const legacyPath = suggestion.legacyPath ?? suggestion.currentPath;
916
- setValueAtPath(nextInput, legacyPath, createTransformFixtureValue(suggestion.attribute, suggestion.currentPath));
917
- return {
918
- input: nextInput,
919
- name: `transform:${legacyPath}->${suggestion.currentPath}`
920
- };
921
- });
922
- }
923
- function createUnionFixtureCases(baseAttributes, manifestAttributes, renameCandidates) {
924
- const cases = [];
925
- for (const [key, attribute] of Object.entries(manifestAttributes)) {
926
- if (attribute.ts.kind !== "union" || !attribute.ts.union) {
927
- continue;
928
- }
929
- for (const [branchKey, branch] of Object.entries(attribute.ts.union.branches ?? {})) {
930
- const nextInput = cloneJsonValue(baseAttributes);
931
- const legacyPath = renameCandidates.find((candidate) => candidate.autoApply && candidate.currentPath === key)?.legacyPath ?? key;
932
- setValueAtPath(nextInput, legacyPath, createUnionBranchFixtureValue(attribute.ts.union.discriminator, branchKey, branch));
933
- cases.push({
934
- input: nextInput,
935
- name: `union:${key}:${branchKey}`
936
- });
937
- }
938
- }
939
- return cases;
940
- }
941
- function createUnionBranchFixtureValue(discriminator, branchKey, branchAttribute) {
942
- const branchValue = defaultValueForManifestAttribute(branchAttribute);
943
- if (typeof branchValue === "object" && branchValue !== null && !Array.isArray(branchValue)) {
944
- return {
945
- ...branchValue,
946
- [discriminator]: branchKey
947
- };
948
- }
949
- return {
950
- [discriminator]: branchKey
951
- };
952
- }
953
-
954
- // ../wp-typia-project-tools/src/runtime/migration-generated-artifacts.ts
955
- import fs4 from "fs";
956
- import path5 from "path";
957
-
958
- // ../wp-typia-project-tools/src/runtime/migration-fuzz-plan.ts
959
- function isBlockedPath(pathLabel, blockedPaths) {
960
- return blockedPaths.some((blockedPath) => pathLabel === blockedPath || pathLabel.startsWith(`${blockedPath}.`));
961
- }
962
- function unique(items) {
963
- return [...new Set(items)];
964
- }
965
- function createMigrationFuzzPlan(legacyManifest, currentManifest, diff) {
966
- const legacyLeafMap = new Map(flattenManifestLeafAttributes(legacyManifest.attributes ?? {}).map((descriptor) => [
967
- descriptor.currentPath,
968
- descriptor
969
- ]));
970
- const currentLeafDescriptors = flattenManifestLeafAttributes(currentManifest.attributes ?? {});
971
- const autoRenameMap = new Map(diff.summary.renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => [candidate.currentPath, candidate.legacyPath]));
972
- const blockedPaths = unique([
973
- ...diff.summary.manualItems.map((item) => item.path),
974
- ...diff.summary.transformSuggestions.map((item) => item.currentPath),
975
- ...diff.summary.renameCandidates.filter((candidate) => !candidate.autoApply).map((candidate) => candidate.currentPath)
976
- ]);
977
- const compatibleMappings = [];
978
- for (const descriptor of currentLeafDescriptors) {
979
- const currentPath = descriptor.currentPath;
980
- if (isBlockedPath(currentPath, blockedPaths)) {
981
- continue;
982
- }
983
- const legacyPath = autoRenameMap.get(currentPath) ?? currentPath;
984
- const legacyDescriptor = legacyLeafMap.get(legacyPath);
985
- if (!legacyDescriptor) {
986
- continue;
987
- }
988
- if (legacyDescriptor.attribute.ts.kind !== descriptor.attribute.ts.kind) {
989
- continue;
990
- }
991
- if (!["string", "number", "boolean"].includes(descriptor.attribute.ts.kind)) {
992
- continue;
993
- }
994
- compatibleMappings.push({
995
- currentPath,
996
- legacyPath
997
- });
998
- }
999
- return {
1000
- blockedPaths,
1001
- compatibleMappings
1002
- };
1003
- }
1004
-
1005
- // ../wp-typia-project-tools/src/runtime/migration-render-diff-rule.ts
1006
- import path3 from "path";
1007
-
1008
- // ../wp-typia-project-tools/src/runtime/migration-risk.ts
1009
- function createRiskBucket(items) {
1010
- return {
1011
- count: items.length,
1012
- items
1013
- };
1014
- }
1015
- function formatDiffOutcome(item) {
1016
- return `${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`;
1017
- }
1018
- function formatRenameCandidate(candidate) {
1019
- return `${candidate.currentPath} <- ${candidate.legacyPath} (${candidate.autoApply ? "auto" : "review"}, ${candidate.reason})`;
1020
- }
1021
- function formatTransformSuggestion(suggestion) {
1022
- return `${suggestion.currentPath}${suggestion.legacyPath ? ` <- ${suggestion.legacyPath}` : ""} (${suggestion.reason})`;
1023
- }
1024
- function unique2(items) {
1025
- return [...new Set(items)];
1026
- }
1027
- function formatMigrationRiskSummary(summary) {
1028
- return `additive=${summary.additive.count}, rename=${summary.rename.count}, semanticTransform=${summary.semanticTransform.count}, unionBreaking=${summary.unionBreaking.count}`;
1029
- }
1030
- function createMigrationRiskSummary(diff) {
1031
- const additiveKinds = new Set([
1032
- "add-default",
1033
- "add-optional",
1034
- "drop",
1035
- "hydrate",
1036
- "union-branch-addition"
1037
- ]);
1038
- const additiveItems = unique2(diff.summary.autoItems.filter((item) => additiveKinds.has(item.kind)).map(formatDiffOutcome));
1039
- const renameItems = unique2(diff.summary.renameCandidates.map(formatRenameCandidate));
1040
- const semanticTransformItems = unique2(diff.summary.transformSuggestions.map(formatTransformSuggestion));
1041
- const unionBreakingItems = unique2(diff.summary.manualItems.filter((item) => item.kind.startsWith("union-")).map(formatDiffOutcome));
1042
- return {
1043
- additive: createRiskBucket(additiveItems),
1044
- rename: createRiskBucket(renameItems),
1045
- semanticTransform: createRiskBucket(semanticTransformItems),
1046
- unionBreaking: createRiskBucket(unionBreakingItems)
1047
- };
1048
- }
1049
-
1050
- // ../wp-typia-project-tools/src/runtime/migration-render-support.ts
1051
- import path2 from "path";
1052
- function normalizeImportPath(relativePath, stripExtension = false) {
1053
- let nextPath = relativePath.replace(/\\/g, "/");
1054
- if (!nextPath.startsWith(".")) {
1055
- nextPath = `./${nextPath}`;
1056
- }
1057
- if (stripExtension) {
1058
- nextPath = nextPath.replace(/\.[^.]+$/u, "");
1059
- }
1060
- return nextPath;
1061
- }
1062
- function getGeneratedDir(block, state) {
1063
- return block.layout === "legacy" ? state.paths.generatedDir : path2.join(state.paths.generatedDir, block.key);
1064
- }
1065
-
1066
- // ../wp-typia-project-tools/src/runtime/migration-render-diff-rule.ts
1067
- function formatDiffReport(diff, { includeRiskSummary = true } = {}) {
1068
- const lines = [
1069
- `Migration diff: ${diff.fromVersion} -> ${diff.toVersion}`,
1070
- `Current type: ${diff.currentTypeName}`,
1071
- `Safe changes: ${diff.summary.auto}`,
1072
- `Manual changes: ${diff.summary.manual}`
1073
- ];
1074
- if (diff.summary.autoItems.length > 0) {
1075
- lines.push("", "Safe changes:");
1076
- for (const item of diff.summary.autoItems) {
1077
- lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
1078
- }
1079
- }
1080
- if (diff.summary.manualItems.length > 0) {
1081
- lines.push("", "Manual review required:");
1082
- for (const item of diff.summary.manualItems) {
1083
- lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
1084
- }
1085
- }
1086
- if (diff.summary.renameCandidates.length > 0) {
1087
- const autoApplied = diff.summary.renameCandidates.filter((item) => item.autoApply);
1088
- const suggested = diff.summary.renameCandidates.filter((item) => !item.autoApply);
1089
- if (autoApplied.length > 0) {
1090
- lines.push("", "Auto-applied renames:");
1091
- for (const item of autoApplied) {
1092
- lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
1093
- }
1094
- }
1095
- if (suggested.length > 0) {
1096
- lines.push("", "Suggested renames:");
1097
- for (const item of suggested) {
1098
- lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
1099
- }
1100
- }
1101
- }
1102
- if (diff.summary.transformSuggestions.length > 0) {
1103
- lines.push("", "Suggested transforms:");
1104
- for (const item of diff.summary.transformSuggestions) {
1105
- lines.push(` - ${item.currentPath}${item.legacyPath ? ` <- ${item.legacyPath}` : ""} (${item.reason})`);
1106
- }
1107
- }
1108
- if (includeRiskSummary) {
1109
- lines.push("", `Risk summary: ${formatMigrationRiskSummary(createMigrationRiskSummary(diff))}`);
1110
- }
1111
- return lines.join(`
1112
- `);
1113
- }
1114
- function renderMigrationRuleFile({
1115
- block,
1116
- currentAttributes,
1117
- currentTypeName,
1118
- diff,
1119
- fromVersion,
1120
- projectDir,
1121
- rulePath,
1122
- targetVersion
1123
- }) {
1124
- const activeRenameCandidates = diff.summary.renameCandidates.filter((candidate) => candidate.autoApply);
1125
- const suggestedRenameCandidates = diff.summary.renameCandidates.filter((candidate) => !candidate.autoApply);
1126
- const lines = [];
1127
- const ruleDir = path3.dirname(rulePath);
1128
- const typesImport = normalizeImportPath(path3.relative(ruleDir, path3.join(projectDir, block.typesFile)));
1129
- const currentManifestImport = normalizeImportPath(path3.relative(ruleDir, path3.join(projectDir, block.manifestFile)));
1130
- const helpersImport = normalizeImportPath(path3.relative(ruleDir, path3.join(projectDir, "src", "migrations", "helpers.ts")), true);
1131
- lines.push(`import type { ${currentTypeName} } from "${typesImport}";`);
1132
- lines.push(`import currentManifest from "${currentManifestImport}";`);
1133
- lines.push(`import {`);
1134
- lines.push(` type RenameMap,`);
1135
- lines.push(` type TransformMap,`);
1136
- lines.push(` resolveMigrationAttribute,`);
1137
- lines.push(`} from "${helpersImport}";`);
1138
- lines.push("");
1139
- lines.push(`export const fromVersion = "${fromVersion}" as const;`);
1140
- lines.push(`export const toVersion = "${targetVersion}" as const;`);
1141
- lines.push("");
1142
- lines.push("export const renameMap: RenameMap = {");
1143
- for (const candidate of activeRenameCandidates) {
1144
- lines.push(` ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
1145
- }
1146
- for (const candidate of suggestedRenameCandidates) {
1147
- lines.push(` // ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
1148
- }
1149
- lines.push("};");
1150
- lines.push("");
1151
- lines.push("export const transforms: TransformMap = {");
1152
- for (const suggestion of diff.summary.transformSuggestions) {
1153
- lines.push(` // ${renderObjectKey(suggestion.currentPath)}: (legacyValue, legacyInput) => {`);
1154
- for (const bodyLine of suggestion.bodyLines) {
1155
- lines.push(` ${bodyLine}`);
1156
- }
1157
- lines.push(` // },`);
1158
- }
1159
- lines.push("};");
1160
- lines.push("");
1161
- lines.push("export const unresolved = [");
1162
- for (const item of diff.summary.manualItems) {
1163
- lines.push(` "${item.path}: ${item.kind}${item.detail ? ` (${escapeForCode(item.detail)})` : ""}",`);
1164
- }
1165
- for (const candidate of suggestedRenameCandidates) {
1166
- lines.push(` "${candidate.currentPath}: rename candidate from ${candidate.legacyPath}",`);
1167
- }
1168
- for (const suggestion of diff.summary.transformSuggestions) {
1169
- lines.push(` "${suggestion.currentPath}: transform suggested from ${suggestion.legacyPath ?? suggestion.currentPath}",`);
1170
- }
1171
- lines.push("] as const;");
1172
- lines.push("");
1173
- lines.push(`export function migrate(input: Record<string, unknown>): ${currentTypeName} {`);
1174
- lines.push(` return {`);
1175
- for (const key of Object.keys(currentAttributes)) {
1176
- for (const manualItem of diff.summary.manualItems.filter((item) => item.path === key || item.path.startsWith(`${key}.`))) {
1177
- lines.push(` // ${MIGRATION_TODO_PREFIX} ${manualItem.path}: ${manualItem.kind}${manualItem.detail ? ` (${manualItem.detail})` : ""}`);
1178
- }
1179
- for (const renameCandidate of suggestedRenameCandidates.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
1180
- lines.push(` // ${MIGRATION_TODO_PREFIX} consider renameMap[${JSON.stringify(renameCandidate.currentPath)}] = "${renameCandidate.legacyPath}"`);
1181
- }
1182
- for (const suggestion of diff.summary.transformSuggestions.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
1183
- lines.push(` // ${MIGRATION_TODO_PREFIX} review transforms[${JSON.stringify(suggestion.currentPath)}]`);
1184
- }
1185
- lines.push(` ${key}: resolveMigrationAttribute(currentManifest.attributes.${key}, "${key}", "${key}", input, renameMap, transforms),`);
1186
- }
1187
- lines.push(` } as ${currentTypeName};`);
1188
- lines.push("}");
1189
- lines.push("");
1190
- return `${lines.join(`
1191
- `)}
1192
- `;
1193
- }
1194
- // ../wp-typia-project-tools/src/runtime/migration-render-generated.ts
1195
- import fs3 from "fs";
1196
- import path4 from "path";
1197
- function renderMigrationRegistryFile(state, blockKey, entries) {
1198
- const block = state.blocks.find((entry) => entry.key === blockKey);
1199
- if (!block) {
1200
- throw new Error(`Unknown migration block target: ${blockKey}`);
1201
- }
1202
- const generatedDir = getGeneratedDir(block, state);
1203
- const currentManifestWrapperCandidates = [
1204
- block.manifestFile.replace(/typia\.manifest\.json$/u, "manifest-document.ts"),
1205
- path4.join(path4.dirname(block.typesFile), "manifest-document.ts")
1206
- ].filter((candidate, index, allCandidates) => candidate !== block.manifestFile && allCandidates.indexOf(candidate) === index);
1207
- const currentManifestWrapperFile = currentManifestWrapperCandidates.find((candidate) => fs3.existsSync(path4.join(state.projectDir, candidate))) ?? null;
1208
- const currentManifestSourceFile = currentManifestWrapperFile ?? block.manifestFile;
1209
- const currentManifestImport = normalizeImportPath(path4.relative(generatedDir, path4.join(state.projectDir, currentManifestSourceFile)), currentManifestWrapperFile !== null);
1210
- const imports = [
1211
- `import rawCurrentManifest from "${currentManifestImport}";`,
1212
- `import type { ManifestDocument, MigrationRiskSummary } from "${normalizeImportPath(path4.relative(getGeneratedDir(block, state), path4.join(state.projectDir, "src", "migrations", "helpers.ts")), true)}";`,
1213
- `import { parseManifestDocument } from "@wp-typia/block-runtime/editor";`
1214
- ];
1215
- const body = [];
1216
- entries.forEach(({ entry, riskSummary }, index) => {
1217
- imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
1218
- imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
1219
- body.push(` {`);
1220
- body.push(` fromMigrationVersion: "${entry.fromVersion}",`);
1221
- body.push(` manifest: parseManifestDocument<ManifestDocument>(manifest_${index}),`);
1222
- body.push(` riskSummary: ${JSON.stringify(riskSummary, null, "\t").replace(/\n/g, `
1223
- `)},`);
1224
- body.push(` rule: rule_${index},`);
1225
- body.push(` },`);
1226
- });
1227
- return `/* eslint-disable prettier/prettier, @typescript-eslint/method-signature-style */
1228
- ${imports.join(`
1229
- `)}
1230
-
1231
- interface MigrationRegistryEntry {
1232
- fromMigrationVersion: string;
1233
- manifest: ManifestDocument;
1234
- riskSummary: MigrationRiskSummary;
1235
- rule: {
1236
- migrate(input: Record<string, unknown>): Record<string, unknown>;
1237
- unresolved?: readonly string[];
1238
- };
1239
- }
1240
-
1241
- export const migrationRegistry: {
1242
- currentMigrationVersion: string;
1243
- currentManifest: ManifestDocument;
1244
- entries: MigrationRegistryEntry[];
1245
- } = {
1246
- currentMigrationVersion: "${state.config.currentMigrationVersion}",
1247
- currentManifest: parseManifestDocument<ManifestDocument>(rawCurrentManifest),
1248
- entries: [
1249
- ${body.join(`
1250
- `)}
1251
- ],
1252
- };
1253
-
1254
- export default migrationRegistry;
1255
- `;
1256
- }
1257
- function renderGeneratedDeprecatedFile(state, blockKey, entries) {
1258
- const block = state.blocks.find((entry) => entry.key === blockKey);
1259
- if (!block) {
1260
- throw new Error(`Unknown migration block target: ${blockKey}`);
1261
- }
1262
- const currentTypeName = typeof block.currentManifest.sourceType === "string" && block.currentManifest.sourceType.length > 0 ? block.currentManifest.sourceType : "Record<string, unknown>";
1263
- const hasNamedCurrentType = currentTypeName !== "Record<string, unknown>";
1264
- const generatedDir = getGeneratedDir(block, state);
1265
- const typesImport = normalizeImportPath(path4.relative(generatedDir, path4.join(state.projectDir, block.typesFile)), true);
1266
- if (entries.length === 0) {
1267
- return `/* eslint-disable prettier/prettier */
1268
- import type { BlockDeprecationList } from "@wp-typia/block-types/blocks/registration";
1269
- ${hasNamedCurrentType ? `import type { ${currentTypeName} } from "${typesImport}";
1270
- ` : ""}
1271
-
1272
- export const deprecated: BlockDeprecationList<${currentTypeName}> = [];
1273
- `;
1274
- }
1275
- const imports = [
1276
- `import type { BlockConfiguration, BlockDeprecationList } from "@wp-typia/block-types/blocks/registration";`,
1277
- ...hasNamedCurrentType ? [`import type { ${currentTypeName} } from "${typesImport}";`] : []
1278
- ];
1279
- const definitions = [];
1280
- const arrayEntries = [];
1281
- entries.forEach((entry, index) => {
1282
- imports.push(`import block_${index} from "${entry.blockJsonImport}";`);
1283
- imports.push(`import save_${index} from "${entry.saveImport}";`);
1284
- imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
1285
- definitions.push(`const deprecated_${index}: BlockDeprecationList<${currentTypeName}>[number] = {`);
1286
- definitions.push(` attributes: (block_${index}.attributes ?? {}) as BlockConfiguration["attributes"],`);
1287
- definitions.push(` save: save_${index} as BlockConfiguration["save"],`);
1288
- definitions.push(` migrate(attributes: Record<string, unknown>) {`);
1289
- definitions.push(` return rule_${index}.migrate(attributes);`);
1290
- definitions.push(` },`);
1291
- definitions.push(`};`);
1292
- arrayEntries.push(`deprecated_${index}`);
1293
- });
1294
- return `/* eslint-disable prettier/prettier */
1295
- ${imports.join(`
1296
- `)}
1297
-
1298
- ${definitions.join(`
1299
-
1300
- `)}
1301
-
1302
- export const deprecated: BlockDeprecationList<${currentTypeName}> = [${arrayEntries.join(", ")}];
1303
- `;
1304
- }
1305
- function renderGeneratedMigrationIndexFile(state, entries) {
1306
- if (state.blocks.length === 0) {
1307
- return `export const migrationBlocks = [] as const;
1308
- export default migrationBlocks;
1309
- `;
1310
- }
1311
- const generatedDir = state.paths.generatedDir;
1312
- const imports = [];
1313
- const definitions = [];
1314
- state.blocks.forEach((block, index) => {
1315
- const scopedEntries = entries.filter((entry) => entry.block.key === block.key);
1316
- const registryImport = block.layout === "legacy" ? "./registry" : `./${block.key}/registry`;
1317
- const deprecatedImport = block.layout === "legacy" ? "./deprecated" : `./${block.key}/deprecated`;
1318
- const validatorsImport = normalizeImportPath(path4.relative(generatedDir, path4.join(state.projectDir, block.typesFile.replace(/types\.ts$/u, "validators.ts"))), true);
1319
- imports.push(`import registry_${index} from "${registryImport}";`);
1320
- imports.push(`import { deprecated as deprecated_${index} } from "${deprecatedImport}";`);
1321
- imports.push(`import { validators as validators_${index} } from "${validatorsImport}";`);
1322
- definitions.push(` {`);
1323
- definitions.push(` key: "${block.key}",`);
1324
- definitions.push(` blockName: "${block.blockName}",`);
1325
- definitions.push(` registry: registry_${index},`);
1326
- definitions.push(` deprecated: deprecated_${index},`);
1327
- definitions.push(` validators: validators_${index},`);
1328
- definitions.push(` legacyMigrationVersions: ${JSON.stringify(scopedEntries.map((entry) => entry.fromVersion))},`);
1329
- definitions.push(` },`);
1330
- });
1331
- return `/* eslint-disable prettier/prettier */
1332
- ${imports.join(`
1333
- `)}
1334
-
1335
- export const migrationBlocks = [
1336
- ${definitions.join(`
1337
- `)}
1338
- ] as const;
1339
-
1340
- export default migrationBlocks;
1341
- `;
1342
- }
1343
- function renderPhpMigrationRegistryFile(state, entries) {
1344
- const blocks = state.blocks.map((block) => {
1345
- const snapshots = Object.fromEntries(state.config.supportedMigrationVersions.map((version) => {
1346
- const manifestPath = getSnapshotManifestPath(state.projectDir, block, version);
1347
- const blockJsonPath = getSnapshotBlockJsonPath(state.projectDir, block, version);
1348
- const savePath = getSnapshotSavePath(state.projectDir, block, version);
1349
- return [
1350
- version,
1351
- {
1352
- blockJson: fs3.existsSync(blockJsonPath) ? {
1353
- attributeNames: Object.keys(readJson(blockJsonPath).attributes ?? {}),
1354
- name: readJson(blockJsonPath).name ?? null
1355
- } : null,
1356
- hasSaveSnapshot: fs3.existsSync(savePath),
1357
- manifest: fs3.existsSync(manifestPath) ? summarizeManifest(readJson(manifestPath)) : null
1358
- }
1359
- ];
1360
- }));
1361
- const edgeSummaries = entries.filter((entry) => entry.block.key === block.key).map((entry) => {
1362
- const ruleMetadata = readRuleMetadata(entry.rulePath);
1363
- const snapshotManifest = snapshots[entry.fromVersion]?.manifest ?? null;
1364
- return {
1365
- autoAppliedRenameCount: ruleMetadata.renameMap.length,
1366
- autoAppliedRenames: ruleMetadata.renameMap,
1367
- fromMigrationVersion: entry.fromVersion,
1368
- nestedPathRenames: ruleMetadata.renameMap.filter((item) => item.currentPath.includes(".")),
1369
- ruleFile: path4.relative(state.projectDir, entry.rulePath).replace(/\\/g, "/"),
1370
- toMigrationVersion: entry.toVersion,
1371
- transformKeys: ruleMetadata.transforms,
1372
- unionBranches: snapshotManifest ? summarizeUnionBranches(snapshotManifest) : [],
1373
- unresolved: ruleMetadata.unresolved
1374
- };
1375
- });
1376
- return {
1377
- blockName: block.blockName,
1378
- currentManifest: summarizeManifest(block.currentManifest),
1379
- edges: edgeSummaries,
1380
- key: block.key,
1381
- legacyMigrationVersions: state.config.supportedMigrationVersions.filter((version) => version !== state.config.currentMigrationVersion),
1382
- snapshots
1383
- };
1384
- });
1385
- return `<?php
1386
- declare(strict_types=1);
1387
-
1388
- if ( ! defined( 'ABSPATH' ) ) {
1389
- exit;
1390
- }
1391
-
1392
- /**
1393
- * Generated from advanced migration snapshots. Do not edit manually.
1394
- */
1395
- return ${renderPhpValue({
1396
- currentMigrationVersion: state.config.currentMigrationVersion,
1397
- blocks,
1398
- snapshotDir: state.config.snapshotDir,
1399
- supportedMigrationVersions: state.config.supportedMigrationVersions
1400
- }, 0)};
1401
- `;
1402
- }
1403
- // ../wp-typia-project-tools/src/runtime/migration-render-execution.ts
1404
- function renderVerifyFile(state, blockKey, entries) {
1405
- const block = state.blocks.find((entry) => entry.key === blockKey);
1406
- if (!block) {
1407
- throw new Error(`Unknown migration block target: ${blockKey}`);
1408
- }
1409
- if (entries.length === 0) {
1410
- return `/* eslint-disable no-console */
1411
- console.log(
1412
- 'Run \`wp-typia migrate scaffold --from-migration-version <label>\` before verify.'
1413
- );
1414
- `;
1415
- }
1416
- const imports = [
1417
- `import { validators } from "${entries[0]?.validatorImport ?? "./validators"}";`,
1418
- `import { deprecated } from "./deprecated";`
1419
- ];
1420
- const checks = [];
1421
- entries.forEach((entry, index) => {
1422
- imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
1423
- imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
1424
- checks.push(` if (selectedMigrationVersions.length === 0 || selectedMigrationVersions.includes("${entry.fromVersion}")) {`);
1425
- checks.push(` if (rule_${index}.unresolved.length > 0) {`);
1426
- checks.push(` throw new Error("Unresolved migration TODOs remain for ${entry.fromVersion} -> ${entry.toVersion}: " + rule_${index}.unresolved.join(", "));`);
1427
- checks.push(` }`);
1428
- checks.push(` const cases_${index} = Array.isArray(fixture_${index}.cases) ? fixture_${index}.cases : [];`);
1429
- checks.push(` for (const fixtureCase of cases_${index}) {`);
1430
- checks.push(` const migrated_${index} = rule_${index}.migrate(fixtureCase.input ?? {});`);
1431
- checks.push(` const validation_${index} = validators.validate(migrated_${index});`);
1432
- checks.push(` if (!isValidationSuccess(validation_${index})) {`);
1433
- checks.push(` throw new Error("Current validator rejected migrated fixture for ${entry.fromVersion} case " + String(fixtureCase.name ?? "unknown") + ": " + JSON.stringify(getValidationErrors(validation_${index})));`);
1434
- checks.push(` }`);
1435
- checks.push(` }`);
1436
- checks.push(` console.log("Verified ${entry.fromVersion} -> ${entry.toVersion} (" + cases_${index}.length + " case(s))");`);
1437
- checks.push(` }`);
1438
- });
1439
- return `/* eslint-disable prettier/prettier, no-console, @typescript-eslint/no-unused-vars, no-nested-ternary */
1440
- ${imports.join(`
1441
- `)}
1442
-
1443
- function isValidationSuccess(result: unknown): boolean {
1444
- return (
1445
- result !== null &&
1446
- typeof result === "object" &&
1447
- (
1448
- (result as { isValid?: unknown }).isValid === true ||
1449
- (result as { success?: unknown }).success === true
1450
- )
1451
- );
1452
- }
1453
-
1454
- function getValidationErrors(result: unknown): unknown[] {
1455
- if (result !== null && typeof result === "object" && Array.isArray((result as { errors?: unknown[] }).errors)) {
1456
- return (result as { errors: unknown[] }).errors;
1457
- }
1458
-
1459
- return [];
1460
- }
1461
-
1462
- const args = process.argv.slice(2);
1463
- const selectedMigrationVersions =
1464
- args[0] === "--all"
1465
- ? []
1466
- : args[0] === "--from-migration-version" && args[1]
1467
- ? [args[1]]
1468
- : [];
1469
-
1470
- if (deprecated.length !== ${entries.length}) {
1471
- throw new Error("Generated deprecated entries are out of sync with migration registry.");
1472
- }
1473
-
1474
- ${checks.join(`
1475
- `)}
1476
-
1477
- console.log("Migration verification passed for ${block.blockName}");
1478
- `;
1479
- }
1480
- function renderFuzzFile(state, blockKey, entries) {
1481
- const block = state.blocks.find((entry) => entry.key === blockKey);
1482
- if (!block) {
1483
- throw new Error(`Unknown migration block target: ${blockKey}`);
1484
- }
1485
- if (entries.length === 0) {
1486
- return `/* eslint-disable no-console */
1487
- console.log(
1488
- 'Run \`wp-typia migrate scaffold --from-migration-version <label>\` before fuzz.'
1489
- );
1490
- `;
1491
- }
1492
- const imports = [
1493
- `import { validators } from "${entries[0]?.entry.validatorImport ?? "./validators"}";`
1494
- ];
1495
- const edgeDefinitions = [];
1496
- entries.forEach(({ entry, fuzzPlan }, index) => {
1497
- imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
1498
- imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
1499
- imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
1500
- edgeDefinitions.push(`{
1501
- fromMigrationVersion: "${entry.fromVersion}",
1502
- toMigrationVersion: "${entry.toVersion}",
1503
- fixture: fixture_${index},
1504
- legacyManifest: manifest_${index},
1505
- rule: rule_${index},
1506
- plan: ${JSON.stringify(fuzzPlan, null, "\t").replace(/\n/g, `
1507
- `)},
1508
- }`);
1509
- });
1510
- return `/* eslint-disable prettier/prettier, no-console, no-bitwise, @typescript-eslint/no-unused-vars, no-nested-ternary, @typescript-eslint/method-signature-style */
1511
- ${imports.join(`
1512
- `)}
1513
-
1514
- type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
1515
-
1516
- type ManifestAttribute = {
1517
- typia?: {
1518
- defaultValue?: JsonValue | null;
1519
- hasDefault?: boolean;
1520
- };
1521
- ts?: {
1522
- items?: ManifestAttribute | null;
1523
- kind?: string | null;
1524
- properties?: Record<string, ManifestAttribute> | null;
1525
- union?: {
1526
- branches?: Record<string, ManifestAttribute> | null;
1527
- discriminator?: string | null;
1528
- } | null;
1529
- } | null;
1530
- wp?: {
1531
- defaultValue?: JsonValue | null;
1532
- enum?: JsonValue[] | null;
1533
- hasDefault?: boolean;
1534
- } | null;
1535
- };
1536
-
1537
- type ManifestDocument = {
1538
- attributes?: Record<string, ManifestAttribute>;
1539
- };
1540
-
1541
- type FixtureDocument = {
1542
- cases?: Array<{ input?: Record<string, unknown>; name?: string }>;
1543
- };
1544
-
1545
- type FuzzEdge = {
1546
- fixture: FixtureDocument;
1547
- fromMigrationVersion: string;
1548
- legacyManifest: ManifestDocument;
1549
- plan: {
1550
- blockedPaths: string[];
1551
- compatibleMappings: Array<{ currentPath: string; legacyPath: string }>;
1552
- };
1553
- rule: {
1554
- migrate(input: Record<string, unknown>): Record<string, unknown>;
1555
- };
1556
- toMigrationVersion: string;
1557
- };
1558
-
1559
- const edges: FuzzEdge[] = [
1560
- ${edgeDefinitions.join(`,
1561
- `)}
1562
- ];
1563
-
1564
- function cloneJsonValue<T>(value: T): T {
1565
- return JSON.parse(JSON.stringify(value));
1566
- }
1567
-
1568
- function getValueAtPath(input: Record<string, unknown>, pathLabel: string): unknown {
1569
- return String(pathLabel)
1570
- .split(".")
1571
- .reduce<unknown>(
1572
- (value, segment) => (value && typeof value === "object" ? (value as Record<string, unknown>)[segment] : undefined),
1573
- input,
1574
- );
1575
- }
1576
-
1577
- function setValueAtPath(input: Record<string, unknown>, pathLabel: string, value: unknown): void {
1578
- const segments = String(pathLabel).split(".").filter(Boolean);
1579
- if (segments.length === 0) {
1580
- return;
1581
- }
1582
-
1583
- let target: Record<string, unknown> = input;
1584
- while (segments.length > 1) {
1585
- const segment = segments.shift();
1586
- if (!segment) {
1587
- continue;
1588
- }
1589
- if (!target[segment] || typeof target[segment] !== "object" || Array.isArray(target[segment])) {
1590
- target[segment] = {};
1591
- }
1592
- target = target[segment];
1593
- }
1594
-
1595
- target[segments[0]!] = value;
1596
- }
1597
-
1598
- function createDefaultValue(attribute: ManifestAttribute): JsonValue | Record<string, JsonValue> | null {
1599
- if (attribute?.typia?.hasDefault) {
1600
- return cloneJsonValue(attribute.typia.defaultValue ?? null);
1601
- }
1602
- if (attribute?.wp?.hasDefault) {
1603
- return cloneJsonValue(attribute.wp.defaultValue ?? null);
1604
- }
1605
- if (Array.isArray(attribute?.wp?.enum) && attribute.wp.enum.length > 0) {
1606
- return cloneJsonValue(attribute.wp.enum[0] ?? null);
1607
- }
1608
-
1609
- switch (attribute?.ts?.kind) {
1610
- case "string":
1611
- return "";
1612
- case "number":
1613
- return 0;
1614
- case "boolean":
1615
- return false;
1616
- case "array":
1617
- return [];
1618
- case "object":
1619
- return Object.fromEntries(
1620
- Object.entries(attribute?.ts?.properties ?? {}).map(([key, property]) => [
1621
- key,
1622
- createDefaultValue(property),
1623
- ]),
1624
- );
1625
- case "union": {
1626
- const firstBranch = Object.values(attribute?.ts?.union?.branches ?? {})[0];
1627
- return firstBranch ? createDefaultValue(firstBranch) : null;
1628
- }
1629
- default:
1630
- return null;
1631
- }
1632
- }
1633
-
1634
- function createDefaultInput(manifest: ManifestDocument): Record<string, unknown> {
1635
- return Object.fromEntries(
1636
- Object.entries(manifest?.attributes ?? {}).map(([key, attribute]) => [key, createDefaultValue(attribute)]),
1637
- );
1638
- }
1639
-
1640
- function isValidationSuccess(result: unknown): boolean {
1641
- const typedResult =
1642
- result !== null && typeof result === "object"
1643
- ? (result as { isValid?: unknown; success?: unknown })
1644
- : null;
1645
-
1646
- return (
1647
- typedResult !== null &&
1648
- (
1649
- typedResult.isValid === true ||
1650
- typedResult.success === true
1651
- )
1652
- );
1653
- }
1654
-
1655
- function getValidationErrors(result: unknown): unknown[] {
1656
- if (
1657
- result !== null &&
1658
- typeof result === "object" &&
1659
- Array.isArray((result as { errors?: unknown[] }).errors)
1660
- ) {
1661
- return (result as { errors: unknown[] }).errors;
1662
- }
1663
-
1664
- return [];
1665
- }
1666
-
1667
- function createSeededRandom(seed: number): () => number {
1668
- let state = seed >>> 0;
1669
- return () => {
1670
- state = (state * 1664525 + 1013904223) >>> 0;
1671
- return state / 4294967296;
1672
- };
1673
- }
1674
-
1675
- function withSeededRandom<T>(seed: number, callback: () => T): T {
1676
- const originalRandom = Math.random;
1677
- Math.random = createSeededRandom(seed);
1678
- try {
1679
- return callback();
1680
- } finally {
1681
- Math.random = originalRandom;
1682
- }
1683
- }
1684
-
1685
- function parseArgs(argv: string[]): {
1686
- all: boolean;
1687
- fromMigrationVersion?: string;
1688
- iterations: number;
1689
- seed?: number;
1690
- } {
1691
- let all = false;
1692
- let fromMigrationVersion: string | undefined;
1693
- let iterations = 25;
1694
- let seed: number | undefined;
1695
-
1696
- for (let index = 0; index < argv.length; index += 1) {
1697
- const arg = argv[index];
1698
- const next = argv[index + 1];
1699
-
1700
- if (arg === "--all") {
1701
- all = true;
1702
- continue;
1703
- }
1704
- if (arg === "--from-migration-version") {
1705
- fromMigrationVersion = next;
1706
- index += 1;
1707
- continue;
1708
- }
1709
- if (arg === "--iterations") {
1710
- iterations = Number.parseInt(next ?? "", 10) || 25;
1711
- index += 1;
1712
- continue;
1713
- }
1714
- if (arg === "--seed") {
1715
- seed = Number.parseInt(next ?? "", 10);
1716
- index += 1;
1717
- }
1718
- }
1719
-
1720
- return { all, fromMigrationVersion, iterations, seed };
1721
- }
1722
-
1723
- function applyCompatibleMappings(
1724
- baseInput: Record<string, unknown>,
1725
- currentSample: Record<string, unknown>,
1726
- mappings: Array<{ currentPath: string; legacyPath: string }>,
1727
- ): Record<string, unknown> {
1728
- for (const mapping of mappings) {
1729
- const value = getValueAtPath(currentSample, mapping.currentPath);
1730
- if (value !== undefined) {
1731
- setValueAtPath(baseInput, mapping.legacyPath, cloneJsonValue(value));
1732
- }
1733
- }
1734
- return baseInput;
1735
- }
1736
-
1737
- function assertValidMigration(
1738
- edge: FuzzEdge,
1739
- candidateInput: Record<string, unknown>,
1740
- migratedOutput: Record<string, unknown>,
1741
- validation: unknown,
1742
- iterationSeed: number,
1743
- iteration: number | string,
1744
- ): void {
1745
- if (isValidationSuccess(validation)) {
1746
- return;
1747
- }
1748
-
1749
- throw new Error(
1750
- "Migration fuzz failed for " +
1751
- edge.fromMigrationVersion +
1752
- " -> " +
1753
- edge.toMigrationVersion +
1754
- " (seed " +
1755
- String(iterationSeed) +
1756
- ", iteration " +
1757
- String(iteration) +
1758
- "): " +
1759
- JSON.stringify({
1760
- input: candidateInput,
1761
- migrated: migratedOutput,
1762
- errors: getValidationErrors(validation),
1763
- }),
1764
- );
1765
- }
1766
-
1767
- const parsed = parseArgs(process.argv.slice(2));
1768
- const selectedMigrationVersions = parsed.all
1769
- ? []
1770
- : parsed.fromMigrationVersion
1771
- ? [parsed.fromMigrationVersion]
1772
- : edges.map((edge) => edge.fromMigrationVersion);
1773
- const baseSeed = typeof parsed.seed === "number" ? parsed.seed : Date.now();
1774
-
1775
- if (!Number.isInteger(parsed.seed)) {
1776
- console.log("Using migration fuzz seed " + String(baseSeed));
1777
- }
1778
-
1779
- if (edges.length === 0) {
1780
- console.log("No legacy migration versions configured for migration fuzzing.");
1781
- process.exit(0);
1782
- }
1783
-
1784
- let executedEdges = 0;
1785
-
1786
- for (const [edgeIndex, edge] of edges.entries()) {
1787
- if (selectedMigrationVersions.length > 0 && !selectedMigrationVersions.includes(edge.fromMigrationVersion)) {
1788
- continue;
1789
- }
1790
-
1791
- executedEdges += 1;
1792
-
1793
- const fixtureCases = Array.isArray(edge.fixture?.cases) ? edge.fixture.cases : [];
1794
- for (const fixtureCase of fixtureCases) {
1795
- const migrated = edge.rule.migrate(fixtureCase.input ?? {});
1796
- const validation = validators.validate(migrated);
1797
- assertValidMigration(edge, fixtureCase.input ?? {}, migrated, validation, baseSeed, fixtureCase.name ?? "fixture");
1798
- }
1799
-
1800
- for (let iteration = 0; iteration < parsed.iterations; iteration += 1) {
1801
- const iterationSeed = (baseSeed + edgeIndex * 100003 + iteration) >>> 0;
1802
- const currentSample = withSeededRandom(iterationSeed, () => validators.random());
1803
- const baseFixture = fixtureCases.find((fixtureCase) => fixtureCase.name === "default")?.input;
1804
- const legacyInput = applyCompatibleMappings(
1805
- cloneJsonValue(baseFixture ?? createDefaultInput(edge.legacyManifest)),
1806
- currentSample,
1807
- edge.plan.compatibleMappings,
1808
- );
1809
- const migrated = edge.rule.migrate(legacyInput);
1810
- const validation = validators.validate(migrated);
1811
- assertValidMigration(edge, legacyInput, migrated, validation, iterationSeed, iteration);
1812
- }
1813
-
1814
- console.log(
1815
- "Fuzzed " +
1816
- edge.fromMigrationVersion +
1817
- " -> " +
1818
- edge.toMigrationVersion +
1819
- " (" +
1820
- String(fixtureCases.length) +
1821
- " fixture case(s), " +
1822
- String(parsed.iterations) +
1823
- " fuzz iteration(s))",
1824
- );
1825
- }
1826
-
1827
- if (selectedMigrationVersions.length > 0 && executedEdges === 0) {
1828
- throw new Error(
1829
- "Requested migration version was not exercised by fuzz: " +
1830
- selectedMigrationVersions.join(", "),
1831
- );
1832
- }
1833
-
1834
- console.log("Migration fuzzing passed for ${block.blockName}");
1835
- `;
1836
- }
1837
- // ../wp-typia-project-tools/src/runtime/migration-generated-artifacts.ts
1838
- function collectGeneratedMigrationEntries(state) {
1839
- return discoverMigrationEntries(state).map((entry) => {
1840
- const block = state.blocks.find((target) => target.key === entry.block.key);
1841
- if (!block) {
1842
- throw new Error(`Unknown migration block target: ${entry.block.key}`);
1843
- }
1844
- const diff = createMigrationDiff(state, entry.block, entry.fromVersion, entry.toVersion);
1845
- const legacyManifest = readJson(getSnapshotManifestPath(state.projectDir, entry.block, entry.fromVersion));
1846
- return {
1847
- diff,
1848
- entry,
1849
- fuzzPlan: createMigrationFuzzPlan(legacyManifest, block.currentManifest, diff),
1850
- riskSummary: createMigrationRiskSummary(diff)
1851
- };
1852
- });
1853
- }
1854
- function regenerateGeneratedArtifacts(projectDir) {
1855
- const state = loadMigrationProject(projectDir);
1856
- const generatedEntries = collectGeneratedMigrationEntries(state);
1857
- for (const block of state.blocks) {
1858
- const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
1859
- const entries = blockGeneratedEntries.map(({ entry }) => entry);
1860
- const generatedDir = getGeneratedDirForBlock(state.paths, block);
1861
- fs4.mkdirSync(generatedDir, { recursive: true });
1862
- fs4.writeFileSync(path5.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries), "utf8");
1863
- fs4.writeFileSync(path5.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(state, block.key, entries), "utf8");
1864
- fs4.writeFileSync(path5.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries), "utf8");
1865
- fs4.writeFileSync(path5.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries), "utf8");
1866
- }
1867
- fs4.writeFileSync(path5.join(state.paths.generatedDir, "index.ts"), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)), "utf8");
1868
- fs4.writeFileSync(path5.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)), "utf8");
1869
- }
1870
-
1871
- // ../wp-typia-project-tools/src/runtime/migration-planning.ts
1872
- import fs5 from "fs";
1873
- function hasSnapshotForVersion(state, block, version) {
1874
- return fs5.existsSync(getSnapshotManifestPath(state.projectDir, block, version));
1875
- }
1876
- function listConfiguredLegacyVersions(state) {
1877
- return state.config.supportedMigrationVersions.filter((version) => version !== state.config.currentMigrationVersion).sort(compareMigrationVersionLabels);
1878
- }
1879
- function listPreviewableLegacyVersions(state) {
1880
- return [
1881
- ...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block)))
1882
- ].filter((version) => version !== state.config.currentMigrationVersion).sort(compareMigrationVersionLabels);
1883
- }
1884
- function resolveLegacyVersions(state, {
1885
- all = false,
1886
- availableVersions,
1887
- fromMigrationVersion
1888
- }) {
1889
- const configuredLegacyVersions = listConfiguredLegacyVersions(state);
1890
- const legacyVersions = availableVersions ?? configuredLegacyVersions;
1891
- if (fromMigrationVersion) {
1892
- if (!legacyVersions.includes(fromMigrationVersion)) {
1893
- throw new Error(legacyVersions.length === 0 ? availableVersions && configuredLegacyVersions.length > 0 ? `Unsupported migration version: ${fromMigrationVersion}. No previewable legacy migration versions are available yet because none currently have snapshot coverage. ` + `Restore or recapture the missing snapshots first.` : `Unsupported migration version: ${fromMigrationVersion}. No legacy migration versions are configured yet. ` + `Capture an older schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.` : `Unsupported migration version: ${fromMigrationVersion}. Available legacy migration versions: ${legacyVersions.join(", ")}.`);
1894
- }
1895
- return [fromMigrationVersion];
1896
- }
1897
- if (all) {
1898
- return legacyVersions;
1899
- }
1900
- return legacyVersions.slice(0, 1);
1901
- }
1902
- function isSnapshotOptionalForBlockVersion(state, block, version) {
1903
- if (block.layout !== "multi") {
1904
- return false;
1905
- }
1906
- const introducedVersions = [...new Set(state.config.supportedMigrationVersions)].filter((candidateVersion) => hasSnapshotForVersion(state, block, candidateVersion)).sort(compareMigrationVersionLabels);
1907
- const firstIntroducedVersion = introducedVersions[0];
1908
- if (!firstIntroducedVersion) {
1909
- return false;
1910
- }
1911
- return compareMigrationVersionLabels(version, firstIntroducedVersion) < 0;
1912
- }
1913
- function isLegacySingleBlockProject(state) {
1914
- return state.blocks.length === 1 && state.blocks[0]?.layout === "legacy";
1915
- }
1916
- function getSelectedEntriesByBlock(state, targetVersions, command) {
1917
- const discoveredEntries = discoverMigrationEntries(state);
1918
- const discoveredEntryKeys = new Set(discoveredEntries.map((entry) => `${entry.block.key}:${entry.fromVersion}`));
1919
- const missingEntries = targetVersions.flatMap((version) => state.blocks.filter((block) => hasSnapshotForVersion(state, block, version)).filter((block) => !discoveredEntryKeys.has(`${block.key}:${version}`)).map((block) => ({ block, version })));
1920
- if (missingEntries.length > 0) {
1921
- const missingLabels = missingEntries.map(({ block, version }) => `${block.blockName} @ ${version}`).join(", ");
1922
- const missingVersions = [...new Set(missingEntries.map(({ version }) => version))].sort(compareMigrationVersionLabels);
1923
- throw new Error(`Missing migration ${command} inputs for ${missingLabels}. ` + `Run \`${formatScaffoldCommand(missingVersions)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
1924
- }
1925
- return groupEntriesByBlock(discoveredEntries.filter((entry) => targetVersions.includes(entry.fromVersion)));
1926
- }
1927
- function assertDistinctMigrationEdge(command, fromVersion, toVersion) {
1928
- if (fromVersion === toVersion) {
1929
- throw new Error(`\`migrate ${command}\` requires different source and target migration versions, but both resolved to ${fromVersion}. ` + `Choose an older snapshot with \`--from-migration-version <label>\` or capture a newer schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.`);
1930
- }
1931
- }
1932
- function createMigrationPlanNextSteps(fromVersion, targetVersion, currentVersion) {
1933
- if (targetVersion !== currentVersion) {
1934
- return [
1935
- formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion)
1936
- ];
1937
- }
1938
- return [
1939
- formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion),
1940
- `wp-typia migrate doctor --from-migration-version ${fromVersion}`,
1941
- `wp-typia migrate verify --from-migration-version ${fromVersion}`,
1942
- `wp-typia migrate fuzz --from-migration-version ${fromVersion}`
1943
- ];
1944
- }
1945
- function formatEdgeCommand(command, fromVersion, targetVersion, currentVersion) {
1946
- return targetVersion === currentVersion ? `wp-typia migrate ${command} --from-migration-version ${fromVersion}` : `wp-typia migrate ${command} --from-migration-version ${fromVersion} --to-migration-version ${targetVersion}`;
1947
- }
1948
- function createMissingProjectSnapshotMessage(state, fromVersion) {
1949
- const snapshotVersions = [
1950
- ...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block)))
1951
- ].sort(compareMigrationVersionLabels);
1952
- return snapshotVersions.length === 0 ? `No migration block targets have a snapshot for ${fromVersion}. No snapshots exist yet in this project. ` + `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.` : `No migration block targets have a snapshot for ${fromVersion}. ` + `Available snapshot versions in this project: ${snapshotVersions.join(", ")}. ` + `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.`;
1953
- }
1954
- function formatScaffoldCommand(versions) {
1955
- const uniqueVersions = [...new Set(versions)].sort(compareMigrationVersionLabels);
1956
- return uniqueVersions.length === 1 ? `wp-typia migrate scaffold --from-migration-version ${uniqueVersions[0]}` : "wp-typia migrate scaffold --from-migration-version <label>";
1957
- }
1958
- function collectFixtureTargets(state, targetVersions, targetVersion) {
1959
- return targetVersions.flatMap((version) => state.blocks.filter((block) => hasSnapshotForVersion(state, block, version)).map((block) => ({
1960
- block,
1961
- fixturePath: getFixtureFilePath(state.paths, block, version, targetVersion),
1962
- scopedLabel: `${block.key}@${version}`,
1963
- version
1964
- })));
1965
- }
1966
- function groupEntriesByBlock(entries) {
1967
- return entries.reduce((accumulator, entry) => {
1968
- if (!accumulator[entry.block.key]) {
1969
- accumulator[entry.block.key] = [];
1970
- }
1971
- accumulator[entry.block.key].push(entry);
1972
- return accumulator;
1973
- }, {});
1974
- }
1975
-
1976
- // ../wp-typia-project-tools/src/runtime/migration-maintenance-verify.ts
1977
- import fs6 from "fs";
1978
- import path6 from "path";
1979
- import { execFileSync } from "child_process";
1980
- function verifyProjectMigrations(projectDir, {
1981
- all = false,
1982
- fromMigrationVersion,
1983
- renderLine = console.log
1984
- } = {}) {
1985
- const state = loadMigrationProject(projectDir);
1986
- const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
1987
- const blockEntries = getSelectedEntriesByBlock(state, targetVersions, "verify");
1988
- const legacySingleBlock = isLegacySingleBlockProject(state);
1989
- if (targetVersions.length === 0) {
1990
- renderLine("No legacy migration versions configured for verification.");
1991
- return { verifiedVersions: [] };
1992
- }
1993
- const tsxBinary = getLocalTsxBinary(projectDir);
1994
- for (const [blockKey, entries] of Object.entries(blockEntries)) {
1995
- const block = state.blocks.find((entry) => entry.key === blockKey);
1996
- if (!block || entries.length === 0) {
1997
- continue;
1998
- }
1999
- for (const entry of entries) {
2000
- assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
2001
- }
2002
- const verifyScriptPath = path6.join(getGeneratedDirForBlock(state.paths, block), "verify.ts");
2003
- if (!fs6.existsSync(verifyScriptPath)) {
2004
- const selectedVersionsForBlock2 = entries.map((entry) => entry.fromVersion);
2005
- throw new Error(`Generated verify script is missing for ${block.blockName} (${selectedVersionsForBlock2.join(", ")}). ` + `Run \`${formatScaffoldCommand(selectedVersionsForBlock2)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
2006
- }
2007
- const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
2008
- const filteredArgs = all ? ["--all"] : ["--from-migration-version", selectedVersionsForBlock[0]];
2009
- execFileSync(tsxBinary, [verifyScriptPath, ...filteredArgs], {
2010
- cwd: projectDir,
2011
- shell: process.platform === "win32",
2012
- stdio: "inherit"
2013
- });
2014
- renderLine(legacySingleBlock ? `Verified migrations for ${selectedVersionsForBlock.join(", ")}` : `Verified ${block.blockName} migrations for ${selectedVersionsForBlock.join(", ")}`);
2015
- }
2016
- return { verifiedVersions: targetVersions };
2017
- }
2018
- function recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck) {
2019
- let invalidWorkspaceReason = null;
2020
- let workspace;
2021
- try {
2022
- invalidWorkspaceReason = getInvalidWorkspaceProjectReason(projectDir);
2023
- workspace = tryResolveWorkspaceProject(projectDir);
2024
- } catch (error) {
2025
- recordCheck("fail", "Workspace migration targets", error instanceof Error ? error.message : String(error));
2026
- return;
2027
- }
2028
- if (!workspace) {
2029
- if (invalidWorkspaceReason) {
2030
- recordCheck("fail", "Workspace migration targets", invalidWorkspaceReason);
2031
- }
2032
- return;
2033
- }
2034
- try {
2035
- const inventory = readWorkspaceInventory(workspace.projectDir);
2036
- const expectedTargets = inventory.blocks.map((block) => `${workspace.workspace.namespace}/${block.slug}`);
2037
- const configuredTargets = state.blocks.map((block) => block.blockName);
2038
- const expectedTargetSet = new Set(expectedTargets);
2039
- const configuredTargetSet = new Set(configuredTargets);
2040
- const missingTargets = expectedTargets.filter((target) => !configuredTargetSet.has(target));
2041
- const staleTargets = configuredTargets.filter((target) => !expectedTargetSet.has(target));
2042
- recordCheck(missingTargets.length === 0 && staleTargets.length === 0 ? "pass" : "fail", "Workspace migration targets", missingTargets.length === 0 && staleTargets.length === 0 ? `${expectedTargets.length} workspace block target(s) align with migration config` : [
2043
- missingTargets.length > 0 ? `Missing from migration config: ${missingTargets.join(", ")}` : null,
2044
- staleTargets.length > 0 ? `Not present in scripts/block-config.ts: ${staleTargets.join(", ")}` : null
2045
- ].filter((detail) => typeof detail === "string").join("; "));
2046
- } catch (error) {
2047
- recordCheck("fail", "Workspace migration targets", error instanceof Error ? error.message : String(error));
2048
- }
2049
- }
2050
- function doctorProjectMigrations(projectDir, {
2051
- all = false,
2052
- fromMigrationVersion,
2053
- renderLine = console.log
2054
- } = {}) {
2055
- const checks = [];
2056
- const recordCheck = (status, label, detail) => {
2057
- checks.push({ detail, label, status });
2058
- renderLine(`${status === "pass" ? "PASS" : "FAIL"} ${label}: ${detail}`);
2059
- };
2060
- let state;
2061
- try {
2062
- state = loadMigrationProject(projectDir);
2063
- const legacySingleBlock2 = isLegacySingleBlockProject(state);
2064
- recordCheck("pass", "Migration config", legacySingleBlock2 ? `Loaded ${state.blocks[0]?.blockName} @ ${state.config.currentMigrationVersion}` : `Loaded ${state.blocks.length} block target(s) @ ${state.config.currentMigrationVersion}`);
2065
- } catch (error) {
2066
- recordCheck("fail", "Migration config", error instanceof Error ? error.message : String(error));
2067
- throw new Error("Migration doctor failed.");
2068
- }
2069
- const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
2070
- const legacySingleBlock = isLegacySingleBlockProject(state);
2071
- const snapshotVersions = new Set(targetVersions.length > 0 ? [state.config.currentMigrationVersion, ...targetVersions] : state.config.supportedMigrationVersions);
2072
- recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck);
2073
- for (const version of snapshotVersions) {
2074
- for (const block of state.blocks) {
2075
- const snapshotRoot = getSnapshotRoot(projectDir, block, version);
2076
- const blockJsonPath = getSnapshotBlockJsonPath(projectDir, block, version);
2077
- const manifestPath = getSnapshotManifestPath(projectDir, block, version);
2078
- const savePath = getSnapshotSavePath(projectDir, block, version);
2079
- const hasSnapshot = fs6.existsSync(snapshotRoot);
2080
- const snapshotIsOptional = !hasSnapshot && isSnapshotOptionalForBlockVersion(state, block, version);
2081
- recordCheck(hasSnapshot || snapshotIsOptional ? "pass" : "fail", legacySingleBlock ? `Snapshot ${version}` : `Snapshot ${block.blockName} @ ${version}`, hasSnapshot ? path6.relative(projectDir, snapshotRoot) : "Not present for this version");
2082
- if (!hasSnapshot) {
2083
- continue;
2084
- }
2085
- for (const targetPath of [blockJsonPath, manifestPath, savePath]) {
2086
- recordCheck(fs6.existsSync(targetPath) ? "pass" : "fail", legacySingleBlock ? `Snapshot file ${version}` : `Snapshot file ${block.blockName} @ ${version}`, fs6.existsSync(targetPath) ? path6.relative(projectDir, targetPath) : `Missing ${path6.relative(projectDir, targetPath)}`);
2087
- }
2088
- }
2089
- }
2090
- try {
2091
- const generatedEntries = collectGeneratedMigrationEntries(state);
2092
- const expectedGeneratedFiles = new Map;
2093
- for (const block of state.blocks) {
2094
- const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
2095
- const entries = blockGeneratedEntries.map(({ entry }) => entry);
2096
- const generatedDir = getGeneratedDirForBlock(state.paths, block);
2097
- expectedGeneratedFiles.set(path6.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries));
2098
- expectedGeneratedFiles.set(path6.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(state, block.key, entries));
2099
- expectedGeneratedFiles.set(path6.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries));
2100
- expectedGeneratedFiles.set(path6.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries));
2101
- }
2102
- expectedGeneratedFiles.set(path6.join(state.paths.generatedDir, "index.ts"), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)));
2103
- expectedGeneratedFiles.set(path6.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)));
2104
- for (const [filePath, expectedSource] of expectedGeneratedFiles) {
2105
- const inSync = fs6.existsSync(filePath) && fs6.readFileSync(filePath, "utf8") === expectedSource;
2106
- recordCheck(inSync ? "pass" : "fail", `Generated ${path6.relative(projectDir, filePath)}`, inSync ? "In sync" : "Run `wp-typia migrate scaffold --from-migration-version <label>` or regenerate artifacts");
2107
- }
2108
- } catch (error) {
2109
- recordCheck("fail", "Generated artifacts", error instanceof Error ? error.message : String(error));
2110
- }
2111
- for (const version of targetVersions) {
2112
- for (const block of state.blocks) {
2113
- if (!hasSnapshotForVersion(state, block, version)) {
2114
- recordCheck("pass", `Snapshot coverage ${block.blockName} @ ${version}`, "Target not present for this version");
2115
- continue;
2116
- }
2117
- const rulePath = getRuleFilePath(state.paths, block, version, state.config.currentMigrationVersion);
2118
- const fixturePath = getFixtureFilePath(state.paths, block, version, state.config.currentMigrationVersion);
2119
- recordCheck(fs6.existsSync(rulePath) ? "pass" : "fail", legacySingleBlock ? `Rule ${version}` : `Rule ${block.blockName} @ ${version}`, fs6.existsSync(rulePath) ? path6.relative(projectDir, rulePath) : `Missing ${path6.relative(projectDir, rulePath)}`);
2120
- recordCheck(fs6.existsSync(fixturePath) ? "pass" : "fail", legacySingleBlock ? `Fixture ${version}` : `Fixture ${block.blockName} @ ${version}`, fs6.existsSync(fixturePath) ? path6.relative(projectDir, fixturePath) : `Missing ${path6.relative(projectDir, fixturePath)}`);
2121
- if (!fs6.existsSync(rulePath) || !fs6.existsSync(fixturePath)) {
2122
- continue;
2123
- }
2124
- try {
2125
- assertRuleHasNoTodos(projectDir, block, version, state.config.currentMigrationVersion);
2126
- recordCheck("pass", legacySingleBlock ? `Rule TODOs ${version}` : `Rule TODOs ${block.blockName} @ ${version}`, "No TODO MIGRATION markers remain");
2127
- } catch (error) {
2128
- recordCheck("fail", legacySingleBlock ? `Rule TODOs ${version}` : `Rule TODOs ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
2129
- }
2130
- try {
2131
- const ruleMetadata = readRuleMetadata(rulePath);
2132
- recordCheck(ruleMetadata.unresolved.length === 0 ? "pass" : "fail", legacySingleBlock ? `Rule unresolved ${version}` : `Rule unresolved ${block.blockName} @ ${version}`, ruleMetadata.unresolved.length === 0 ? "No unresolved entries remain" : ruleMetadata.unresolved.join(", "));
2133
- } catch (error) {
2134
- recordCheck("fail", legacySingleBlock ? `Rule unresolved ${version}` : `Rule unresolved ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
2135
- }
2136
- try {
2137
- const fixtureDocument = readJson(fixturePath);
2138
- recordCheck(Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0 ? "pass" : "fail", legacySingleBlock ? `Fixture parse ${version}` : `Fixture parse ${block.blockName} @ ${version}`, Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0 ? `${fixtureDocument.cases.length} case(s)` : "Fixture document has no cases");
2139
- const diff = createMigrationDiff(state, block, version, state.config.currentMigrationVersion);
2140
- const expectedFixture = createEdgeFixtureDocument(projectDir, block, version, state.config.currentMigrationVersion, diff);
2141
- const actualCaseNames = new Set((fixtureDocument.cases ?? []).map((fixtureCase) => fixtureCase.name));
2142
- const missingCases = expectedFixture.cases.map((fixtureCase) => fixtureCase.name).filter((name) => !actualCaseNames.has(name));
2143
- recordCheck(missingCases.length === 0 ? "pass" : "fail", legacySingleBlock ? `Fixture coverage ${version}` : `Fixture coverage ${block.blockName} @ ${version}`, missingCases.length === 0 ? "All expected fixture cases are present" : `Missing ${missingCases.join(", ")}`);
2144
- recordCheck("pass", legacySingleBlock ? `Risk summary ${version}` : `Risk summary ${block.blockName} @ ${version}`, formatMigrationRiskSummary(createMigrationRiskSummary(diff)));
2145
- } catch (error) {
2146
- recordCheck("fail", legacySingleBlock ? `Fixture parse ${version}` : `Fixture parse ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
2147
- }
2148
- }
2149
- }
2150
- const failedChecks = checks.filter((check) => check.status === "fail");
2151
- renderLine(`${failedChecks.length === 0 ? "PASS" : "FAIL"} Migration doctor summary: ${checks.length - failedChecks.length}/${checks.length} checks passed`);
2152
- if (failedChecks.length > 0) {
2153
- throw new Error("Migration doctor failed.");
2154
- }
2155
- return {
2156
- checkedVersions: targetVersions,
2157
- checks
2158
- };
2159
- }
2160
- // ../wp-typia-project-tools/src/runtime/migration-maintenance-fixtures.ts
2161
- import fs7 from "fs";
2162
- import path7 from "path";
2163
- import { execFileSync as execFileSync2 } from "child_process";
2164
- function fixturesProjectMigrations(projectDir, {
2165
- all = false,
2166
- confirmOverwrite,
2167
- force = false,
2168
- fromMigrationVersion,
2169
- isInteractive = isInteractiveTerminal(),
2170
- renderLine = console.log,
2171
- toMigrationVersion = "current"
2172
- } = {}) {
2173
- const state = loadMigrationProject(projectDir);
2174
- const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
2175
- const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
2176
- if (targetVersions.length === 0) {
2177
- renderLine("No legacy migration versions configured for fixture generation.");
2178
- return { generatedVersions: [], skippedVersions: [] };
2179
- }
2180
- const generatedVersions = [];
2181
- const skippedVersions = [];
2182
- const fixtureTargets = collectFixtureTargets(state, targetVersions, targetMigrationVersion);
2183
- if (force) {
2184
- const overwriteTargets = fixtureTargets.filter(({ fixturePath }) => fs7.existsSync(fixturePath));
2185
- if (isInteractive && overwriteTargets.length > 0) {
2186
- const confirmed = confirmOverwrite?.(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`) ?? promptForConfirmation(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`);
2187
- if (!confirmed) {
2188
- renderLine(`Cancelled fixture refresh. Kept ${overwriteTargets.length} existing fixture file(s).`);
2189
- return {
2190
- generatedVersions,
2191
- skippedVersions: overwriteTargets.map(({ scopedLabel }) => scopedLabel)
2192
- };
2193
- }
2194
- }
2195
- }
2196
- for (const { block, fixturePath, scopedLabel, version } of fixtureTargets) {
2197
- const existed = fs7.existsSync(fixturePath);
2198
- const diff = createMigrationDiff(state, block, version, targetMigrationVersion);
2199
- const result = ensureEdgeFixtureFile(projectDir, block, version, targetMigrationVersion, diff, { force });
2200
- if (result.written) {
2201
- generatedVersions.push(scopedLabel);
2202
- renderLine(`${existed ? "Refreshed" : "Generated"} fixture ${path7.relative(projectDir, fixturePath)}`);
2203
- } else {
2204
- skippedVersions.push(scopedLabel);
2205
- renderLine(`Preserved existing fixture ${path7.relative(projectDir, fixturePath)} (use --force to refresh)`);
2206
- }
2207
- }
2208
- return {
2209
- generatedVersions,
2210
- skippedVersions
2211
- };
2212
- }
2213
- function fuzzProjectMigrations(projectDir, {
2214
- all = false,
2215
- fromMigrationVersion,
2216
- iterations = 25,
2217
- renderLine = console.log,
2218
- seed
2219
- } = {}) {
2220
- const state = loadMigrationProject(projectDir);
2221
- const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
2222
- const blockEntries = getSelectedEntriesByBlock(state, targetVersions, "fuzz");
2223
- const legacySingleBlock = isLegacySingleBlockProject(state);
2224
- if (targetVersions.length === 0) {
2225
- renderLine("No legacy migration versions configured for fuzzing.");
2226
- return { fuzzedVersions: [], seed };
2227
- }
2228
- const tsxBinary = getLocalTsxBinary(projectDir);
2229
- for (const [blockKey, entries] of Object.entries(blockEntries)) {
2230
- const block = state.blocks.find((entry) => entry.key === blockKey);
2231
- if (!block || entries.length === 0) {
2232
- continue;
2233
- }
2234
- for (const entry of entries) {
2235
- assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
2236
- }
2237
- const fuzzScriptPath = path7.join(getGeneratedDirForBlock(state.paths, block), "fuzz.ts");
2238
- if (!fs7.existsSync(fuzzScriptPath)) {
2239
- const selectedVersionsForBlock2 = entries.map((entry) => entry.fromVersion);
2240
- throw new Error(`Generated fuzz script is missing for ${block.blockName} (${selectedVersionsForBlock2.join(", ")}). ` + `Run \`${formatScaffoldCommand(selectedVersionsForBlock2)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
2241
- }
2242
- const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
2243
- const args = [
2244
- fuzzScriptPath,
2245
- ...all ? ["--all"] : ["--from-migration-version", selectedVersionsForBlock[0]],
2246
- "--iterations",
2247
- String(iterations),
2248
- ...seed === undefined ? [] : ["--seed", String(seed)]
2249
- ];
2250
- execFileSync2(tsxBinary, args, {
2251
- cwd: projectDir,
2252
- shell: process.platform === "win32",
2253
- stdio: "inherit"
2254
- });
2255
- renderLine(legacySingleBlock ? `Fuzzed migrations for ${selectedVersionsForBlock.join(", ")}` : `Fuzzed ${block.blockName} migrations for ${selectedVersionsForBlock.join(", ")}`);
2256
- }
2257
- return { fuzzedVersions: targetVersions, seed };
2258
- }
2259
- function promptForConfirmation(message) {
2260
- process.stdout.write(`${message} [y/N]: `);
2261
- const buffer = Buffer.alloc(1);
2262
- let answer = "";
2263
- while (true) {
2264
- const bytesRead = fs7.readSync(process.stdin.fd, buffer, 0, 1, null);
2265
- if (bytesRead === 0) {
2266
- break;
2267
- }
2268
- const char = buffer.toString("utf8", 0, bytesRead);
2269
- if (char === `
2270
- ` || char === "\r") {
2271
- break;
2272
- }
2273
- answer += char;
2274
- }
2275
- const normalized = answer.trim().toLowerCase();
2276
- return normalized === "y" || normalized === "yes";
2277
- }
2278
- // ../wp-typia-project-tools/src/runtime/migrations.ts
2279
- function runMigrationCommand(command, cwd, { prompt, renderLine = console.log } = {}) {
2280
- switch (command.command) {
2281
- case "init":
2282
- if (!command.flags.currentMigrationVersion) {
2283
- throw new Error("`migrate init` requires --current-migration-version <label>.");
2284
- }
2285
- return initProjectMigrations(cwd, command.flags.currentMigrationVersion, { renderLine });
2286
- case "snapshot":
2287
- if (!command.flags.migrationVersion) {
2288
- throw new Error("`migrate snapshot` requires --migration-version <label>.");
2289
- }
2290
- return snapshotProjectVersion(cwd, command.flags.migrationVersion, { renderLine });
2291
- case "plan":
2292
- if (!command.flags.fromMigrationVersion) {
2293
- throw new Error("`migrate plan` requires --from-migration-version <label>.");
2294
- }
2295
- return planProjectMigrations(cwd, {
2296
- fromMigrationVersion: command.flags.fromMigrationVersion,
2297
- renderLine,
2298
- toMigrationVersion: command.flags.toMigrationVersion ?? "current"
2299
- });
2300
- case "wizard":
2301
- return wizardProjectMigrations(cwd, {
2302
- prompt,
2303
- renderLine
2304
- });
2305
- case "diff":
2306
- if (!command.flags.fromMigrationVersion) {
2307
- throw new Error("`migrate diff` requires --from-migration-version <label>.");
2308
- }
2309
- return diffProjectMigrations(cwd, {
2310
- fromMigrationVersion: command.flags.fromMigrationVersion,
2311
- renderLine,
2312
- toMigrationVersion: command.flags.toMigrationVersion ?? "current"
2313
- });
2314
- case "scaffold":
2315
- if (!command.flags.fromMigrationVersion) {
2316
- throw new Error("`migrate scaffold` requires --from-migration-version <label>.");
2317
- }
2318
- return scaffoldProjectMigrations(cwd, {
2319
- fromMigrationVersion: command.flags.fromMigrationVersion,
2320
- renderLine,
2321
- toMigrationVersion: command.flags.toMigrationVersion ?? "current"
2322
- });
2323
- case "verify":
2324
- return verifyProjectMigrations(cwd, {
2325
- all: command.flags.all,
2326
- fromMigrationVersion: command.flags.fromMigrationVersion,
2327
- renderLine
2328
- });
2329
- case "doctor":
2330
- return doctorProjectMigrations(cwd, {
2331
- all: command.flags.all,
2332
- fromMigrationVersion: command.flags.fromMigrationVersion,
2333
- renderLine
2334
- });
2335
- case "fixtures":
2336
- return fixturesProjectMigrations(cwd, {
2337
- all: command.flags.all,
2338
- force: command.flags.force,
2339
- fromMigrationVersion: command.flags.fromMigrationVersion,
2340
- renderLine,
2341
- toMigrationVersion: command.flags.toMigrationVersion ?? "current"
2342
- });
2343
- case "fuzz":
2344
- return fuzzProjectMigrations(cwd, {
2345
- all: command.flags.all,
2346
- fromMigrationVersion: command.flags.fromMigrationVersion,
2347
- iterations: parsePositiveInteger(command.flags.iterations, "iterations") ?? 25,
2348
- renderLine,
2349
- seed: parseNonNegativeInteger(command.flags.seed, "seed")
2350
- });
2351
- default:
2352
- throw new Error(formatMigrationHelpText());
2353
- }
2354
- }
2355
- function planProjectMigrations(projectDir, { fromMigrationVersion, renderLine = console.log, toMigrationVersion = "current" } = {}) {
2356
- if (!fromMigrationVersion) {
2357
- throw new Error("`migrate plan` requires --from-migration-version <label>.");
2358
- }
2359
- const state = loadMigrationProject(projectDir, { allowSyncTypes: false });
2360
- const availableLegacyVersions = listPreviewableLegacyVersions(state).sort(compareMigrationVersionLabels).reverse();
2361
- const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
2362
- assertDistinctMigrationEdge("plan", fromMigrationVersion, targetMigrationVersion);
2363
- resolveLegacyVersions(state, {
2364
- fromMigrationVersion,
2365
- availableVersions: availableLegacyVersions
2366
- });
2367
- const includedBlocks = state.blocks.filter((block) => hasSnapshotForVersion(state, block, fromMigrationVersion));
2368
- if (includedBlocks.length === 0) {
2369
- throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
2370
- }
2371
- const skippedBlocks = state.blocks.filter((block) => !hasSnapshotForVersion(state, block, fromMigrationVersion)).map((block) => block.blockName);
2372
- const summaries = includedBlocks.map((block) => {
2373
- const diff = createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion);
2374
- return {
2375
- blockName: block.blockName,
2376
- diff,
2377
- riskSummary: createMigrationRiskSummary(diff)
2378
- };
2379
- });
2380
- const nextSteps = createMigrationPlanNextSteps(fromMigrationVersion, targetMigrationVersion, state.config.currentMigrationVersion);
2381
- renderLine(`Current migration version: ${state.config.currentMigrationVersion}`);
2382
- renderLine(`Available legacy migration versions: ${availableLegacyVersions.length > 0 ? availableLegacyVersions.join(", ") : "None configured"}`);
2383
- renderLine(`Selected migration edge: ${fromMigrationVersion} -> ${targetMigrationVersion}`);
2384
- renderLine(`Included block targets: ${includedBlocks.map((block) => block.blockName).join(", ")}`);
2385
- renderLine(`Skipped block targets: ${skippedBlocks.length > 0 ? skippedBlocks.join(", ") : "None"}`);
2386
- for (const summary of summaries) {
2387
- renderLine(`Block: ${summary.blockName}`);
2388
- renderLine(formatDiffReport(summary.diff, { includeRiskSummary: false }));
2389
- renderLine(`Risk summary: ${formatMigrationRiskSummary(summary.riskSummary)}`);
2390
- }
2391
- renderLine("Next steps:");
2392
- for (const command of nextSteps) {
2393
- renderLine(` ${command}`);
2394
- }
2395
- renderLine(`Optional after editing rules: ${formatEdgeCommand("fixtures", fromMigrationVersion, targetMigrationVersion, state.config.currentMigrationVersion)} --force`);
2396
- return {
2397
- availableLegacyVersions,
2398
- currentMigrationVersion: state.config.currentMigrationVersion,
2399
- fromMigrationVersion,
2400
- includedBlocks: includedBlocks.map((block) => block.blockName),
2401
- nextSteps,
2402
- skippedBlocks,
2403
- summaries,
2404
- targetMigrationVersion
2405
- };
2406
- }
2407
- async function wizardProjectMigrations(projectDir, {
2408
- isInteractive = isInteractiveTerminal(),
2409
- prompt,
2410
- renderLine = console.log
2411
- } = {}) {
2412
- if (!isInteractive) {
2413
- throw new Error("`migrate wizard` requires an interactive terminal. " + "Use `wp-typia migrate plan --from-migration-version <label>` for a read-only preview or run the direct migration commands with explicit flags.");
2414
- }
2415
- const state = loadMigrationProject(projectDir, { allowSyncTypes: false });
2416
- const availableLegacyVersions = listPreviewableLegacyVersions(state).sort(compareMigrationVersionLabels).reverse();
2417
- if (availableLegacyVersions.length === 0) {
2418
- throw new Error("No legacy migration versions are configured yet. " + "Capture an older schema release with `wp-typia migrate snapshot --migration-version <label>` first, then rerun `wp-typia migrate wizard`.");
2419
- }
2420
- const activePrompt = prompt ?? createReadlinePrompt();
2421
- const createdPrompt = !prompt;
2422
- try {
2423
- const selectedVersion = await activePrompt.select("Choose a legacy version to preview", [
2424
- ...availableLegacyVersions.map((version) => ({
2425
- hint: `Preview ${version} -> ${state.config.currentMigrationVersion}`,
2426
- label: version,
2427
- value: version
2428
- })),
2429
- {
2430
- hint: "Exit without previewing a migration edge",
2431
- label: "Cancel",
2432
- value: "cancel"
2433
- }
2434
- ], 1);
2435
- if (selectedVersion === "cancel") {
2436
- renderLine("Cancelled migration planning.");
2437
- return { cancelled: true };
2438
- }
2439
- return planProjectMigrations(projectDir, {
2440
- fromMigrationVersion: selectedVersion,
2441
- renderLine
2442
- });
2443
- } finally {
2444
- if (createdPrompt) {
2445
- activePrompt.close();
2446
- }
2447
- }
2448
- }
2449
- function initProjectMigrations(projectDir, currentMigrationVersion, { renderLine = console.log } = {}) {
2450
- assertMigrationVersionLabel(currentMigrationVersion, "current migration version");
2451
- assertNoLegacySemverMigrationWorkspace(projectDir);
2452
- const discoveredLayout = discoverMigrationInitLayout(projectDir);
2453
- const configuredBlocks = discoveredLayout.mode === "multi" ? discoveredLayout.blocks : undefined;
2454
- ensureAdvancedMigrationProject(projectDir, configuredBlocks);
2455
- ensureMigrationDirectories(projectDir, configuredBlocks);
2456
- writeMigrationConfig(projectDir, {
2457
- blockName: discoveredLayout.mode === "single" ? discoveredLayout.block.blockName : undefined,
2458
- blocks: configuredBlocks,
2459
- currentMigrationVersion,
2460
- snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
2461
- supportedMigrationVersions: [currentMigrationVersion]
2462
- });
2463
- writeInitialMigrationScaffold(projectDir, currentMigrationVersion, configuredBlocks);
2464
- snapshotProjectVersion(projectDir, currentMigrationVersion, { renderLine, skipConfigUpdate: true });
2465
- regenerateGeneratedArtifacts(projectDir);
2466
- if (discoveredLayout.mode === "multi") {
2467
- renderLine(`Detected multi-block migration retrofit (${discoveredLayout.blocks.length} targets): ${discoveredLayout.blocks.map((block) => block.blockName).join(", ")}`);
2468
- } else {
2469
- renderLine(`Detected single-block migration retrofit: ${discoveredLayout.block.blockName}`);
2470
- }
2471
- renderLine("Wrote src/migrations/config.ts");
2472
- renderLine(`Initialized migrations for ${discoveredLayout.mode === "multi" ? discoveredLayout.blocks.map((block) => block.blockName).join(", ") : discoveredLayout.block.blockName} at migration version ${currentMigrationVersion}`);
2473
- return loadMigrationProject(projectDir);
2474
- }
2475
- function snapshotProjectVersion(projectDir, migrationVersion, {
2476
- renderLine = console.log,
2477
- skipConfigUpdate = false,
2478
- skipSyncTypes = false
2479
- } = {}) {
2480
- ensureAdvancedMigrationProject(projectDir);
2481
- assertMigrationVersionLabel(migrationVersion, "migration version");
2482
- if (!skipSyncTypes) {
2483
- try {
2484
- runProjectScriptIfPresent(projectDir, "sync-types");
2485
- } catch (error) {
2486
- const syncTypesCommand = formatRunScript(detectPackageManagerId(projectDir), "sync-types");
2487
- const reason = error instanceof Error ? error.message : String(error);
2488
- throw new Error(`Could not capture migration snapshot ${migrationVersion} because \`${syncTypesCommand}\` failed first. ` + `Install project dependencies if needed, rerun \`${syncTypesCommand}\` in the project root to inspect the underlying error, ` + `then retry \`wp-typia migrate snapshot --migration-version ${migrationVersion}\`.
2489
- ` + `Original error: ${reason}`);
2490
- }
2491
- }
2492
- const state = loadMigrationProject(projectDir, { allowMissingConfig: skipConfigUpdate });
2493
- for (const block of state.blocks) {
2494
- const snapshotRoot = getSnapshotRoot(projectDir, block, migrationVersion);
2495
- fs8.mkdirSync(snapshotRoot, { recursive: true });
2496
- fs8.writeFileSync(getSnapshotBlockJsonPath(projectDir, block, migrationVersion), `${JSON.stringify(sanitizeSnapshotBlockJson(readJson(path8.join(projectDir, block.blockJsonFile))), null, "\t")}
2497
- `, "utf8");
2498
- copyFile(path8.join(projectDir, block.manifestFile), getSnapshotManifestPath(projectDir, block, migrationVersion));
2499
- fs8.writeFileSync(getSnapshotSavePath(projectDir, block, migrationVersion), sanitizeSaveSnapshotSource(fs8.readFileSync(path8.join(projectDir, block.saveFile), "utf8")), "utf8");
2500
- }
2501
- if (!skipConfigUpdate) {
2502
- const nextSupported = [
2503
- ...new Set([...state.config.supportedMigrationVersions, migrationVersion])
2504
- ].sort(compareMigrationVersionLabels);
2505
- writeMigrationConfig(projectDir, {
2506
- ...state.config,
2507
- currentMigrationVersion: migrationVersion,
2508
- supportedMigrationVersions: nextSupported
2509
- });
2510
- }
2511
- regenerateGeneratedArtifacts(projectDir);
2512
- renderLine(`Snapshot stored for migration version ${migrationVersion}`);
2513
- return loadMigrationProject(projectDir);
2514
- }
2515
- function diffProjectMigrations(projectDir, {
2516
- fromMigrationVersion,
2517
- toMigrationVersion = "current",
2518
- renderLine = console.log
2519
- } = {}) {
2520
- if (!fromMigrationVersion) {
2521
- throw new Error("`migrate diff` requires --from-migration-version <label>.");
2522
- }
2523
- const state = loadMigrationProject(projectDir);
2524
- const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
2525
- assertDistinctMigrationEdge("diff", fromMigrationVersion, targetMigrationVersion);
2526
- const diffs = state.blocks.filter((block) => hasSnapshotForVersion(state, block, fromMigrationVersion)).map((block) => ({
2527
- block,
2528
- diff: createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion)
2529
- }));
2530
- if (diffs.length === 0) {
2531
- throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
2532
- }
2533
- for (const { block, diff } of diffs) {
2534
- renderLine(`Block: ${block.blockName}`);
2535
- renderLine(formatDiffReport(diff));
2536
- }
2537
- return diffs.length === 1 ? diffs[0].diff : diffs;
2538
- }
2539
- function scaffoldProjectMigrations(projectDir, {
2540
- fromMigrationVersion,
2541
- toMigrationVersion = "current",
2542
- renderLine = console.log
2543
- } = {}) {
2544
- if (!fromMigrationVersion) {
2545
- throw new Error("`migrate scaffold` requires --from-migration-version <label>.");
2546
- }
2547
- ensureMigrationDirectories(projectDir);
2548
- const state = loadMigrationProject(projectDir);
2549
- const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
2550
- assertDistinctMigrationEdge("scaffold", fromMigrationVersion, targetMigrationVersion);
2551
- const paths = getProjectPaths(projectDir);
2552
- const scaffolded = [];
2553
- let eligibleBlocks = 0;
2554
- for (const block of state.blocks) {
2555
- if (!hasSnapshotForVersion(state, block, fromMigrationVersion)) {
2556
- renderLine(`Skipped ${block.blockName}: no snapshot for ${fromMigrationVersion}`);
2557
- continue;
2558
- }
2559
- eligibleBlocks += 1;
2560
- const diff = createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion);
2561
- const rulePath = getRuleFilePath(paths, block, fromMigrationVersion, targetMigrationVersion);
2562
- if (!fs8.existsSync(rulePath)) {
2563
- fs8.mkdirSync(path8.dirname(rulePath), { recursive: true });
2564
- fs8.writeFileSync(rulePath, renderMigrationRuleFile({
2565
- block,
2566
- currentAttributes: block.currentManifest.attributes ?? {},
2567
- currentTypeName: block.currentManifest.sourceType,
2568
- diff,
2569
- fromVersion: fromMigrationVersion,
2570
- projectDir,
2571
- rulePath,
2572
- targetVersion: targetMigrationVersion
2573
- }), "utf8");
2574
- }
2575
- ensureEdgeFixtureFile(projectDir, block, fromMigrationVersion, targetMigrationVersion, diff);
2576
- scaffolded.push({ blockName: block.blockName, diff, rulePath });
2577
- }
2578
- regenerateGeneratedArtifacts(projectDir);
2579
- for (const entry of scaffolded) {
2580
- renderLine(`Block: ${entry.blockName}`);
2581
- renderLine(formatDiffReport(entry.diff));
2582
- renderLine(`Scaffolded ${path8.relative(projectDir, entry.rulePath)}`);
2583
- }
2584
- if (eligibleBlocks === 0) {
2585
- throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
2586
- }
2587
- return scaffolded.length === 1 ? scaffolded[0] : { scaffolded };
2588
- }
2589
- function seedProjectMigrations(projectDir, currentMigrationVersion, blocks, { renderLine = console.log } = {}) {
2590
- ensureAdvancedMigrationProject(projectDir, blocks);
2591
- assertMigrationVersionLabel(currentMigrationVersion, "current migration version");
2592
- ensureMigrationDirectories(projectDir, blocks);
2593
- writeMigrationConfig(projectDir, {
2594
- blocks,
2595
- currentMigrationVersion,
2596
- snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
2597
- supportedMigrationVersions: [currentMigrationVersion]
2598
- });
2599
- writeInitialMigrationScaffold(projectDir, currentMigrationVersion, blocks);
2600
- snapshotProjectVersion(projectDir, currentMigrationVersion, {
2601
- renderLine,
2602
- skipConfigUpdate: true,
2603
- skipSyncTypes: true
2604
- });
2605
- regenerateGeneratedArtifacts(projectDir);
2606
- renderLine(`Initialized migrations for ${blocks.map((block) => block.blockName).join(", ")} at migration version ${currentMigrationVersion}`);
2607
- return loadMigrationProject(projectDir);
2608
- }
2609
-
2610
- export { formatMigrationHelpText, parseMigrationArgs, formatDiffReport, verifyProjectMigrations, doctorProjectMigrations, fixturesProjectMigrations, fuzzProjectMigrations, runMigrationCommand, planProjectMigrations, wizardProjectMigrations, initProjectMigrations, snapshotProjectVersion, diffProjectMigrations, scaffoldProjectMigrations, seedProjectMigrations };
2611
-
2612
- //# debugId=A37691396E12D94E64756E2164756E21