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