@wp-typia/create 0.1.0
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 +43 -0
- package/dist/cli.js +2492 -0
- package/dist/runtime/cli-core.js +222 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/migration-constants.js +14 -0
- package/dist/runtime/migration-diff.js +521 -0
- package/dist/runtime/migration-fixtures.js +89 -0
- package/dist/runtime/migration-manifest.js +129 -0
- package/dist/runtime/migration-project.js +167 -0
- package/dist/runtime/migration-render.js +267 -0
- package/dist/runtime/migration-types.js +1 -0
- package/dist/runtime/migration-utils.js +184 -0
- package/dist/runtime/migrations.js +232 -0
- package/dist/runtime/package-managers.js +135 -0
- package/dist/runtime/scaffold.js +334 -0
- package/dist/runtime/template-registry.js +75 -0
- package/package.json +65 -0
- package/templates/advanced/README.md.mustache +150 -0
- package/templates/advanced/block.json.mustache +43 -0
- package/templates/advanced/index.js +21 -0
- package/templates/advanced/package.json.mustache +47 -0
- package/templates/advanced/render.php.mustache +83 -0
- package/templates/advanced/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/advanced/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/advanced/src/admin/migration-dashboard.tsx.mustache +315 -0
- package/templates/advanced/src/components/ErrorBoundary.tsx.mustache +47 -0
- package/templates/advanced/src/deprecated.ts.mustache +2 -0
- package/templates/advanced/src/edit.tsx.mustache +97 -0
- package/templates/advanced/src/hooks/useDebounce.ts.mustache +20 -0
- package/templates/advanced/src/hooks/useLocalStorage.ts.mustache +31 -0
- package/templates/advanced/src/hooks.ts.mustache +56 -0
- package/templates/advanced/src/index.tsx.mustache +18 -0
- package/templates/advanced/src/migration-detector.ts.mustache +9 -0
- package/templates/advanced/src/migrations/config.ts.mustache +8 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/README.md.mustache +23 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/fixture.example.json.mustache +36 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/rule.example.ts.mustache +47 -0
- package/templates/advanced/src/migrations/fixtures/README.md.mustache +3 -0
- package/templates/advanced/src/migrations/generated/deprecated.ts.mustache +3 -0
- package/templates/advanced/src/migrations/generated/registry.ts.mustache +9 -0
- package/templates/advanced/src/migrations/generated/verify.ts.mustache +1 -0
- package/templates/advanced/src/migrations/helpers.ts.mustache +354 -0
- package/templates/advanced/src/migrations/index.ts.mustache +616 -0
- package/templates/advanced/src/migrations/rules/README.md.mustache +3 -0
- package/templates/advanced/src/migrations/versions/README.md.mustache +3 -0
- package/templates/advanced/src/save.tsx.mustache +12 -0
- package/templates/advanced/src/style.scss.mustache +84 -0
- package/templates/advanced/src/types.ts.mustache +46 -0
- package/templates/advanced/src/utils/classnames.ts.mustache +51 -0
- package/templates/advanced/src/utils/debounce.ts.mustache +37 -0
- package/templates/advanced/src/utils/index.ts.mustache +7 -0
- package/templates/advanced/src/utils/uuid.ts.mustache +17 -0
- package/templates/advanced/src/validators.ts.mustache +39 -0
- package/templates/advanced/src/view.ts.mustache +59 -0
- package/templates/advanced/tsconfig.json.mustache +20 -0
- package/templates/advanced/webpack.config.js.mustache +95 -0
- package/templates/basic/package.json.mustache +39 -0
- package/templates/basic/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/basic/scripts/sync-types-to-block-json.ts +25 -0
- package/templates/basic/src/block.json +51 -0
- package/templates/basic/src/edit.tsx +85 -0
- package/templates/basic/src/hooks.ts +75 -0
- package/templates/basic/src/index.tsx +37 -0
- package/templates/basic/src/save.tsx +27 -0
- package/templates/basic/src/style.scss +42 -0
- package/templates/basic/src/types.ts +48 -0
- package/templates/basic/src/validators.ts +39 -0
- package/templates/basic/tsconfig.json +20 -0
- package/templates/basic/webpack.config.js +89 -0
- package/templates/full/package.json.mustache +40 -0
- package/templates/full/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/full/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/full/src/block.json.mustache +120 -0
- package/templates/full/src/edit.tsx.mustache +300 -0
- package/templates/full/src/editor.scss.mustache +251 -0
- package/templates/full/src/hooks.ts.mustache +141 -0
- package/templates/full/src/index.tsx.mustache +27 -0
- package/templates/full/src/save.tsx.mustache +39 -0
- package/templates/full/src/style.scss.mustache +224 -0
- package/templates/full/src/types.ts.mustache +35 -0
- package/templates/full/src/validators.ts.mustache +84 -0
- package/templates/full/tsconfig.json.mustache +20 -0
- package/templates/full/webpack.config.js.mustache +89 -0
- package/templates/interactivity/package.json.mustache +41 -0
- package/templates/interactivity/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/interactivity/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/interactivity/src/block.json.mustache +74 -0
- package/templates/interactivity/src/edit.tsx.mustache +206 -0
- package/templates/interactivity/src/index.tsx.mustache +20 -0
- package/templates/interactivity/src/interactivity.ts.mustache +183 -0
- package/templates/interactivity/src/save.tsx.mustache +87 -0
- package/templates/interactivity/src/style.scss.mustache +60 -0
- package/templates/interactivity/src/types.ts.mustache +30 -0
- package/templates/interactivity/tsconfig.json.mustache +20 -0
- package/templates/interactivity/webpack.config.js.mustache +89 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ROOT_MANIFEST, SNAPSHOT_DIR } from "./migration-constants.js";
|
|
4
|
+
import { flattenManifestLeafAttributes, getAttributeByCurrentPath, getManifestDefaultValue, hasManifestDefault, } from "./migration-manifest.js";
|
|
5
|
+
import { isNumber, readJson } from "./migration-utils.js";
|
|
6
|
+
export function createMigrationDiff(state, fromVersion, toVersion) {
|
|
7
|
+
const snapshotManifestPath = path.join(state.projectDir, SNAPSHOT_DIR, fromVersion, ROOT_MANIFEST);
|
|
8
|
+
if (!fs.existsSync(snapshotManifestPath)) {
|
|
9
|
+
throw new Error(`Snapshot manifest for ${fromVersion} does not exist. Run \`migrations snapshot --version ${fromVersion}\` first.`);
|
|
10
|
+
}
|
|
11
|
+
const targetManifest = toVersion === state.config.currentVersion
|
|
12
|
+
? state.currentManifest
|
|
13
|
+
: readJson(path.join(state.projectDir, SNAPSHOT_DIR, toVersion, ROOT_MANIFEST));
|
|
14
|
+
const oldManifest = readJson(snapshotManifestPath);
|
|
15
|
+
const oldAttributes = oldManifest.attributes ?? {};
|
|
16
|
+
const newAttributes = targetManifest.attributes ?? {};
|
|
17
|
+
const oldLeafAttributes = flattenManifestLeafAttributes(oldAttributes);
|
|
18
|
+
const newLeafAttributes = flattenManifestLeafAttributes(newAttributes);
|
|
19
|
+
const autoItems = [];
|
|
20
|
+
const manualItems = [];
|
|
21
|
+
const addedKeys = [];
|
|
22
|
+
const removedKeys = [];
|
|
23
|
+
for (const [key, newAttribute] of Object.entries(newAttributes)) {
|
|
24
|
+
const oldAttribute = oldAttributes[key];
|
|
25
|
+
if (!oldAttribute) {
|
|
26
|
+
addedKeys.push(key);
|
|
27
|
+
if (newAttribute.ts.required && !hasManifestDefault(newAttribute)) {
|
|
28
|
+
manualItems.push({
|
|
29
|
+
detail: "required field has no default in current schema",
|
|
30
|
+
kind: "required-addition",
|
|
31
|
+
path: key,
|
|
32
|
+
status: "manual",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
autoItems.push({
|
|
37
|
+
detail: hasManifestDefault(newAttribute)
|
|
38
|
+
? `default ${JSON.stringify(getManifestDefaultValue(newAttribute))}`
|
|
39
|
+
: "optional addition",
|
|
40
|
+
kind: hasManifestDefault(newAttribute) ? "add-default" : "add-optional",
|
|
41
|
+
path: key,
|
|
42
|
+
status: "auto",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const outcome = compareManifestAttribute(oldAttribute, newAttribute, key);
|
|
48
|
+
if (outcome.status === "manual") {
|
|
49
|
+
manualItems.push(outcome);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
autoItems.push(outcome);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const key of Object.keys(oldAttributes)) {
|
|
56
|
+
if (!(key in newAttributes)) {
|
|
57
|
+
removedKeys.push(key);
|
|
58
|
+
autoItems.push({
|
|
59
|
+
detail: "field removed from current schema",
|
|
60
|
+
kind: "drop",
|
|
61
|
+
path: key,
|
|
62
|
+
status: "auto",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const renameCandidates = createRenameCandidates(oldAttributes, newAttributes, removedKeys, addedKeys, oldLeafAttributes, newLeafAttributes);
|
|
67
|
+
const activeRenameCandidates = renameCandidates.filter((candidate) => candidate.autoApply);
|
|
68
|
+
for (const candidate of activeRenameCandidates) {
|
|
69
|
+
removeOutcomeByPath(autoItems, candidate.legacyPath, "drop");
|
|
70
|
+
removeOutcomeByPath(autoItems, candidate.currentPath, "add-default");
|
|
71
|
+
removeOutcomeByPath(autoItems, candidate.currentPath, "add-optional");
|
|
72
|
+
removeOutcomeByPath(manualItems, candidate.currentPath, "required-addition");
|
|
73
|
+
removeOutcomesByPath(manualItems, candidate.currentPath);
|
|
74
|
+
autoItems.push({
|
|
75
|
+
detail: `legacy field ${candidate.legacyPath}`,
|
|
76
|
+
kind: "rename",
|
|
77
|
+
path: candidate.currentPath,
|
|
78
|
+
status: "auto",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const transformSuggestions = createTransformSuggestions({
|
|
82
|
+
addedKeys,
|
|
83
|
+
manualItems,
|
|
84
|
+
newAttributes,
|
|
85
|
+
newLeafAttributes,
|
|
86
|
+
oldAttributes,
|
|
87
|
+
oldLeafAttributes,
|
|
88
|
+
renameCandidates,
|
|
89
|
+
removedKeys,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
currentTypeName: targetManifest.sourceType ?? state.currentManifest.sourceType,
|
|
93
|
+
fromVersion,
|
|
94
|
+
summary: {
|
|
95
|
+
auto: autoItems.length,
|
|
96
|
+
autoItems,
|
|
97
|
+
manual: manualItems.length,
|
|
98
|
+
manualItems,
|
|
99
|
+
renameCandidates,
|
|
100
|
+
transformSuggestions,
|
|
101
|
+
},
|
|
102
|
+
toVersion,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function removeOutcomeByPath(items, pathLabel, kind) {
|
|
106
|
+
const index = items.findIndex((item) => item.path === pathLabel && item.kind === kind);
|
|
107
|
+
if (index >= 0) {
|
|
108
|
+
items.splice(index, 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function removeOutcomesByPath(items, pathLabel) {
|
|
112
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
113
|
+
if (items[index]?.path === pathLabel) {
|
|
114
|
+
items.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function compareManifestAttribute(oldAttribute, newAttribute, attributePath) {
|
|
119
|
+
if (oldAttribute.ts.kind !== newAttribute.ts.kind) {
|
|
120
|
+
return manualOutcome(attributePath, "type-change", `${oldAttribute.ts.kind} -> ${newAttribute.ts.kind}`);
|
|
121
|
+
}
|
|
122
|
+
if (oldAttribute.ts.kind === "union") {
|
|
123
|
+
return compareUnionAttribute(oldAttribute, newAttribute, attributePath);
|
|
124
|
+
}
|
|
125
|
+
if (oldAttribute.ts.kind === "object") {
|
|
126
|
+
return compareObjectAttribute(oldAttribute, newAttribute, attributePath);
|
|
127
|
+
}
|
|
128
|
+
if (oldAttribute.ts.kind === "array") {
|
|
129
|
+
if (!oldAttribute.ts.items || !newAttribute.ts.items) {
|
|
130
|
+
return autoOutcome(attributePath, "copy", "array shape unchanged");
|
|
131
|
+
}
|
|
132
|
+
const nested = compareManifestAttribute(oldAttribute.ts.items, newAttribute.ts.items, `${attributePath}[]`);
|
|
133
|
+
return nested.status === "manual"
|
|
134
|
+
? nested
|
|
135
|
+
: autoOutcome(attributePath, "hydrate", "array items can be normalized");
|
|
136
|
+
}
|
|
137
|
+
if (hasStricterConstraints(oldAttribute, newAttribute)) {
|
|
138
|
+
return manualOutcome(attributePath, "stricter-constraints", describeConstraintChange(oldAttribute, newAttribute));
|
|
139
|
+
}
|
|
140
|
+
return autoOutcome(attributePath, "copy", "compatible primitive field");
|
|
141
|
+
}
|
|
142
|
+
function compareObjectAttribute(oldAttribute, newAttribute, attributePath) {
|
|
143
|
+
const oldProperties = oldAttribute.ts.properties ?? {};
|
|
144
|
+
const newProperties = newAttribute.ts.properties ?? {};
|
|
145
|
+
for (const [key, nextProperty] of Object.entries(newProperties)) {
|
|
146
|
+
const previousProperty = oldProperties[key];
|
|
147
|
+
if (!previousProperty) {
|
|
148
|
+
if (nextProperty.ts.required && !hasManifestDefault(nextProperty)) {
|
|
149
|
+
return manualOutcome(`${attributePath}.${key}`, "object-change", "required field has no default in current schema");
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const nested = compareManifestAttribute(previousProperty, nextProperty, `${attributePath}.${key}`);
|
|
154
|
+
if (nested.status === "manual") {
|
|
155
|
+
return nested;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return autoOutcome(attributePath, "hydrate", "object can be normalized with current manifest defaults");
|
|
159
|
+
}
|
|
160
|
+
function compareUnionAttribute(oldAttribute, newAttribute, attributePath) {
|
|
161
|
+
const oldUnion = oldAttribute.ts.union;
|
|
162
|
+
const newUnion = newAttribute.ts.union;
|
|
163
|
+
if (!oldUnion || !newUnion) {
|
|
164
|
+
return manualOutcome(attributePath, "union-change", "missing union metadata");
|
|
165
|
+
}
|
|
166
|
+
if (oldUnion.discriminator !== newUnion.discriminator) {
|
|
167
|
+
return manualOutcome(attributePath, "union-discriminator-change", `${oldUnion.discriminator} -> ${newUnion.discriminator}`);
|
|
168
|
+
}
|
|
169
|
+
const oldBranchKeys = Object.keys(oldUnion.branches);
|
|
170
|
+
const newBranchKeys = Object.keys(newUnion.branches);
|
|
171
|
+
for (const branchKey of oldBranchKeys) {
|
|
172
|
+
if (!(branchKey in newUnion.branches)) {
|
|
173
|
+
return manualOutcome(attributePath, "union-branch-removal", `branch ${branchKey} was removed`);
|
|
174
|
+
}
|
|
175
|
+
const nested = compareManifestAttribute(oldUnion.branches[branchKey], newUnion.branches[branchKey], `${attributePath}.${branchKey}`);
|
|
176
|
+
if (nested.status === "manual") {
|
|
177
|
+
return nested;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const addedBranches = newBranchKeys.filter((branchKey) => !(branchKey in oldUnion.branches));
|
|
181
|
+
if (addedBranches.length > 0) {
|
|
182
|
+
return autoOutcome(attributePath, "union-branch-addition", `branches added: ${addedBranches.join(", ")}`);
|
|
183
|
+
}
|
|
184
|
+
return autoOutcome(attributePath, "copy", "compatible discriminated union");
|
|
185
|
+
}
|
|
186
|
+
function hasStricterConstraints(oldAttribute, newAttribute) {
|
|
187
|
+
const oldConstraints = oldAttribute.typia.constraints;
|
|
188
|
+
const nextConstraints = newAttribute.typia.constraints;
|
|
189
|
+
const oldEnum = oldAttribute.wp.enum ?? null;
|
|
190
|
+
const nextEnum = newAttribute.wp.enum ?? null;
|
|
191
|
+
if (nextEnum && (!oldEnum || !oldEnum.every((value) => nextEnum.includes(value)))) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (isNumber(nextConstraints.minLength) &&
|
|
195
|
+
(!isNumber(oldConstraints.minLength) || nextConstraints.minLength > oldConstraints.minLength)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (isNumber(nextConstraints.maxLength) &&
|
|
199
|
+
(!isNumber(oldConstraints.maxLength) || nextConstraints.maxLength < oldConstraints.maxLength)) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (isNumber(nextConstraints.minimum) &&
|
|
203
|
+
(!isNumber(oldConstraints.minimum) || nextConstraints.minimum > oldConstraints.minimum)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (isNumber(nextConstraints.maximum) &&
|
|
207
|
+
(!isNumber(oldConstraints.maximum) || nextConstraints.maximum < oldConstraints.maximum)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (nextConstraints.pattern && nextConstraints.pattern !== oldConstraints.pattern) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (nextConstraints.format && nextConstraints.format !== oldConstraints.format) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (nextConstraints.typeTag && nextConstraints.typeTag !== oldConstraints.typeTag) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
function createRenameCandidates(oldAttributes, newAttributes, removedKeys, addedKeys, oldLeafAttributes, newLeafAttributes) {
|
|
222
|
+
const assessments = [];
|
|
223
|
+
for (const currentPath of addedKeys) {
|
|
224
|
+
const nextAttribute = newAttributes[currentPath];
|
|
225
|
+
if (!nextAttribute)
|
|
226
|
+
continue;
|
|
227
|
+
for (const legacyPath of removedKeys) {
|
|
228
|
+
const previous = oldAttributes[legacyPath];
|
|
229
|
+
if (!previous)
|
|
230
|
+
continue;
|
|
231
|
+
const candidate = assessRenameCandidate(previous, nextAttribute, legacyPath, currentPath);
|
|
232
|
+
if (candidate) {
|
|
233
|
+
assessments.push(candidate);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
|
|
238
|
+
const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
|
|
239
|
+
const removedLeafDescriptors = oldLeafAttributes.filter((descriptor) => !newLeafMap.has(descriptor.currentPath));
|
|
240
|
+
const addedLeafDescriptors = newLeafAttributes.filter((descriptor) => !oldLeafMap.has(descriptor.currentPath));
|
|
241
|
+
for (const nextDescriptor of addedLeafDescriptors) {
|
|
242
|
+
if (!nextDescriptor.currentPath.includes(".")) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const previousDescriptor of removedLeafDescriptors) {
|
|
246
|
+
if (!previousDescriptor.currentPath.includes(".")) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const candidate = assessRenameCandidate(previousDescriptor.attribute, nextDescriptor.attribute, previousDescriptor.currentPath, nextDescriptor.currentPath);
|
|
250
|
+
if (candidate) {
|
|
251
|
+
assessments.push(candidate);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return assessments
|
|
256
|
+
.map((candidate) => {
|
|
257
|
+
const currentMatches = assessments
|
|
258
|
+
.filter((item) => item.currentPath === candidate.currentPath)
|
|
259
|
+
.sort((left, right) => right.score - left.score);
|
|
260
|
+
const legacyMatches = assessments
|
|
261
|
+
.filter((item) => item.legacyPath === candidate.legacyPath)
|
|
262
|
+
.sort((left, right) => right.score - left.score);
|
|
263
|
+
const currentLeader = currentMatches[0];
|
|
264
|
+
const legacyLeader = legacyMatches[0];
|
|
265
|
+
const currentHasTie = currentMatches.length > 1 && Math.abs((currentMatches[1]?.score ?? 0) - currentLeader.score) < 0.05;
|
|
266
|
+
const legacyHasTie = legacyMatches.length > 1 && Math.abs((legacyMatches[1]?.score ?? 0) - legacyLeader.score) < 0.05;
|
|
267
|
+
return {
|
|
268
|
+
...candidate,
|
|
269
|
+
autoApply: currentLeader.legacyPath === candidate.legacyPath &&
|
|
270
|
+
legacyLeader.currentPath === candidate.currentPath &&
|
|
271
|
+
!currentHasTie &&
|
|
272
|
+
!legacyHasTie &&
|
|
273
|
+
candidate.score >= 0.6,
|
|
274
|
+
};
|
|
275
|
+
})
|
|
276
|
+
.filter((candidate, index, list) => {
|
|
277
|
+
const firstMatch = list.findIndex((item) => item.currentPath === candidate.currentPath && item.legacyPath === candidate.legacyPath);
|
|
278
|
+
return firstMatch === index;
|
|
279
|
+
})
|
|
280
|
+
.sort((left, right) => right.score - left.score);
|
|
281
|
+
}
|
|
282
|
+
function createTransformSuggestions({ oldAttributes, newAttributes, addedKeys, removedKeys, manualItems, renameCandidates, oldLeafAttributes, newLeafAttributes, }) {
|
|
283
|
+
const suggestions = [];
|
|
284
|
+
const activeRenameTargets = new Set(renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => candidate.currentPath));
|
|
285
|
+
const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
|
|
286
|
+
const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
|
|
287
|
+
for (const currentPath of [
|
|
288
|
+
...new Set([
|
|
289
|
+
...Object.keys(newAttributes),
|
|
290
|
+
...manualItems.map((item) => item.path),
|
|
291
|
+
...newLeafAttributes.map((item) => item.currentPath),
|
|
292
|
+
]),
|
|
293
|
+
]) {
|
|
294
|
+
if (activeRenameTargets.has(currentPath)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const manualItem = manualItems.find((item) => item.path === currentPath || item.path.startsWith(`${currentPath}.`));
|
|
298
|
+
const currentAttribute = newLeafMap.get(currentPath)?.attribute ??
|
|
299
|
+
getAttributeByCurrentPath(newAttributes, currentPath) ??
|
|
300
|
+
null;
|
|
301
|
+
if (!manualItem || !currentAttribute) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const exactLegacy = oldLeafMap.get(currentPath)?.attribute ??
|
|
305
|
+
getAttributeByCurrentPath(oldAttributes, currentPath) ??
|
|
306
|
+
null;
|
|
307
|
+
if (exactLegacy && exactLegacy.ts.kind !== currentAttribute.ts.kind) {
|
|
308
|
+
suggestions.push({
|
|
309
|
+
bodyLines: buildTransformBodyLines(currentAttribute, currentPath),
|
|
310
|
+
attribute: currentAttribute,
|
|
311
|
+
currentPath,
|
|
312
|
+
legacyPath: currentPath,
|
|
313
|
+
reason: `semantic coercion suggested for ${manualItem.kind}`,
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const bestRenameCandidate = renameCandidates.find((candidate) => candidate.currentPath === currentPath);
|
|
318
|
+
if (bestRenameCandidate && !bestRenameCandidate.autoApply) {
|
|
319
|
+
suggestions.push({
|
|
320
|
+
bodyLines: buildTransformBodyLines(currentAttribute, bestRenameCandidate.legacyPath),
|
|
321
|
+
attribute: currentAttribute,
|
|
322
|
+
currentPath,
|
|
323
|
+
legacyPath: bestRenameCandidate.legacyPath,
|
|
324
|
+
reason: `review coercion from ${bestRenameCandidate.legacyPath}`,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const addedCurrent = addedKeys.includes(currentPath) ||
|
|
329
|
+
(newLeafMap.has(currentPath) && !oldLeafMap.has(currentPath));
|
|
330
|
+
if (!addedCurrent) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const compatibleLegacyPath = [
|
|
334
|
+
...removedKeys,
|
|
335
|
+
...oldLeafAttributes
|
|
336
|
+
.filter((descriptor) => !newLeafMap.has(descriptor.currentPath))
|
|
337
|
+
.map((descriptor) => descriptor.currentPath),
|
|
338
|
+
].find((legacyPath) => passesNameSimilarityRule(legacyPath, currentPath));
|
|
339
|
+
if (compatibleLegacyPath) {
|
|
340
|
+
suggestions.push({
|
|
341
|
+
bodyLines: buildTransformBodyLines(currentAttribute, compatibleLegacyPath),
|
|
342
|
+
attribute: currentAttribute,
|
|
343
|
+
currentPath,
|
|
344
|
+
legacyPath: compatibleLegacyPath,
|
|
345
|
+
reason: `review coercion from ${compatibleLegacyPath}`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return suggestions;
|
|
350
|
+
}
|
|
351
|
+
function isRenameCandidateShapeCompatible(oldAttribute, newAttribute) {
|
|
352
|
+
if (!oldAttribute || !newAttribute || oldAttribute.ts.kind !== newAttribute.ts.kind) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (["string", "number", "boolean"].includes(oldAttribute.ts.kind)) {
|
|
356
|
+
return hasRenameCompatibleConstraints(oldAttribute, newAttribute);
|
|
357
|
+
}
|
|
358
|
+
if (oldAttribute.ts.kind === "union") {
|
|
359
|
+
return compareUnionAttribute(oldAttribute, newAttribute, "$rename").status === "auto";
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
function assessRenameCandidate(oldAttribute, newAttribute, legacyPath, currentPath) {
|
|
364
|
+
if (!isRenameCandidateShapeCompatible(oldAttribute, newAttribute)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const baseScore = scoreRenameSimilarity(legacyPath, currentPath);
|
|
368
|
+
const score = getParentPath(legacyPath) === getParentPath(currentPath) ? Math.max(baseScore, 0.75) : baseScore;
|
|
369
|
+
return {
|
|
370
|
+
autoApply: false,
|
|
371
|
+
currentPath,
|
|
372
|
+
legacyPath,
|
|
373
|
+
reason: describeRenameReason(oldAttribute, legacyPath, currentPath, score),
|
|
374
|
+
score,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function hasRenameCompatibleConstraints(oldAttribute, newAttribute) {
|
|
378
|
+
const oldEnum = oldAttribute.wp.enum ?? null;
|
|
379
|
+
const nextEnum = newAttribute.wp.enum ?? null;
|
|
380
|
+
if (oldEnum && nextEnum) {
|
|
381
|
+
const oldIsSubset = oldEnum.every((value) => nextEnum.includes(value));
|
|
382
|
+
if (!oldIsSubset) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else if (oldEnum && !nextEnum) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
const oldConstraints = oldAttribute.typia.constraints ?? {};
|
|
390
|
+
const nextConstraints = newAttribute.typia.constraints ?? {};
|
|
391
|
+
return [
|
|
392
|
+
compareMinimumBound(oldConstraints.minLength, nextConstraints.minLength),
|
|
393
|
+
compareMaximumBound(oldConstraints.maxLength, nextConstraints.maxLength),
|
|
394
|
+
compareMinimumBound(oldConstraints.minimum, nextConstraints.minimum),
|
|
395
|
+
compareMaximumBound(oldConstraints.maximum, nextConstraints.maximum),
|
|
396
|
+
comparePatternBound(oldConstraints.pattern, nextConstraints.pattern),
|
|
397
|
+
comparePatternBound(oldConstraints.format, nextConstraints.format),
|
|
398
|
+
comparePatternBound(oldConstraints.typeTag, nextConstraints.typeTag),
|
|
399
|
+
].every(Boolean);
|
|
400
|
+
}
|
|
401
|
+
function compareMinimumBound(oldValue, nextValue) {
|
|
402
|
+
if (nextValue === null || nextValue === undefined)
|
|
403
|
+
return true;
|
|
404
|
+
if (oldValue === null || oldValue === undefined)
|
|
405
|
+
return true;
|
|
406
|
+
return Number(oldValue) <= Number(nextValue);
|
|
407
|
+
}
|
|
408
|
+
function compareMaximumBound(oldValue, nextValue) {
|
|
409
|
+
if (nextValue === null || nextValue === undefined)
|
|
410
|
+
return true;
|
|
411
|
+
if (oldValue === null || oldValue === undefined)
|
|
412
|
+
return true;
|
|
413
|
+
return Number(oldValue) >= Number(nextValue);
|
|
414
|
+
}
|
|
415
|
+
function comparePatternBound(oldValue, nextValue) {
|
|
416
|
+
return oldValue === nextValue || oldValue === null || oldValue === undefined;
|
|
417
|
+
}
|
|
418
|
+
function scoreRenameSimilarity(legacyPath, currentPath) {
|
|
419
|
+
const legacy = normalizeFieldName(legacyPath);
|
|
420
|
+
const current = normalizeFieldName(currentPath);
|
|
421
|
+
if (legacy === current)
|
|
422
|
+
return 1;
|
|
423
|
+
if (shareAliasGroup(legacy, current))
|
|
424
|
+
return 0.9;
|
|
425
|
+
const legacyTokens = tokenizeFieldName(legacy);
|
|
426
|
+
const currentTokens = tokenizeFieldName(current);
|
|
427
|
+
const overlap = legacyTokens.filter((token) => currentTokens.includes(token));
|
|
428
|
+
const jaccard = overlap.length / new Set([...legacyTokens, ...currentTokens]).size;
|
|
429
|
+
if (legacy.includes(current) || current.includes(legacy)) {
|
|
430
|
+
return Math.max(jaccard, 0.7);
|
|
431
|
+
}
|
|
432
|
+
if (legacyTokens.length > 0 &&
|
|
433
|
+
currentTokens.length > 0 &&
|
|
434
|
+
legacyTokens[legacyTokens.length - 1] === currentTokens[currentTokens.length - 1]) {
|
|
435
|
+
return Math.max(jaccard, 0.6);
|
|
436
|
+
}
|
|
437
|
+
return jaccard;
|
|
438
|
+
}
|
|
439
|
+
function passesNameSimilarityRule(legacyPath, currentPath) {
|
|
440
|
+
return scoreRenameSimilarity(legacyPath, currentPath) >= 0.6;
|
|
441
|
+
}
|
|
442
|
+
function normalizeFieldName(name) {
|
|
443
|
+
return String(name)
|
|
444
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
445
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
446
|
+
.trim()
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.replace(/\s+/g, "");
|
|
449
|
+
}
|
|
450
|
+
function tokenizeFieldName(name) {
|
|
451
|
+
return String(name)
|
|
452
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
453
|
+
.toLowerCase()
|
|
454
|
+
.split(/[^a-z0-9]+/)
|
|
455
|
+
.filter(Boolean);
|
|
456
|
+
}
|
|
457
|
+
function getParentPath(pathLabel) {
|
|
458
|
+
const segments = String(pathLabel).split(".");
|
|
459
|
+
return segments.length <= 1 ? "" : segments.slice(0, -1).join(".");
|
|
460
|
+
}
|
|
461
|
+
function shareAliasGroup(left, right) {
|
|
462
|
+
const aliasGroups = [
|
|
463
|
+
["content", "headline", "body", "text", "copy", "message"],
|
|
464
|
+
["id", "uniqueid", "uuid"],
|
|
465
|
+
["visible", "isvisible", "show", "shown", "enabled"],
|
|
466
|
+
["align", "alignment", "textalign"],
|
|
467
|
+
["count", "clickcount", "counter"],
|
|
468
|
+
["url", "href", "link"],
|
|
469
|
+
];
|
|
470
|
+
return aliasGroups.some((group) => group.includes(left) && group.includes(right));
|
|
471
|
+
}
|
|
472
|
+
function describeRenameReason(attribute, legacyPath, currentPath, score) {
|
|
473
|
+
if (attribute.ts.kind === "union") {
|
|
474
|
+
return `compatible discriminated union (${legacyPath} → ${currentPath})`;
|
|
475
|
+
}
|
|
476
|
+
if (score >= 0.9)
|
|
477
|
+
return "high-confidence compatible field";
|
|
478
|
+
if (score >= 0.6)
|
|
479
|
+
return "name-similar compatible field";
|
|
480
|
+
return "compatible field requiring review";
|
|
481
|
+
}
|
|
482
|
+
function buildTransformBodyLines(attribute, legacyPath) {
|
|
483
|
+
switch (attribute.ts.kind) {
|
|
484
|
+
case "string":
|
|
485
|
+
return [`// return typeof legacyValue === "string" ? legacyValue : String(legacyValue ?? "");`];
|
|
486
|
+
case "number":
|
|
487
|
+
return [
|
|
488
|
+
`// const numericValue = typeof legacyValue === "number" ? legacyValue : Number(legacyValue ?? 0);`,
|
|
489
|
+
`// return Number.isNaN(numericValue) ? undefined : numericValue;`,
|
|
490
|
+
];
|
|
491
|
+
case "boolean":
|
|
492
|
+
return [`// return typeof legacyValue === "boolean" ? legacyValue : Boolean(legacyValue);`];
|
|
493
|
+
case "union":
|
|
494
|
+
return [
|
|
495
|
+
`// const legacyObject = typeof legacyValue === "object" && legacyValue !== null ? legacyValue : {};`,
|
|
496
|
+
`// return legacyObject; // adjust discriminator / branch fields before verify`,
|
|
497
|
+
];
|
|
498
|
+
default:
|
|
499
|
+
return [`// return legacyValue; // customize migration from ${legacyPath}`];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function describeConstraintChange(oldAttribute, newAttribute) {
|
|
503
|
+
const details = [];
|
|
504
|
+
const oldConstraints = oldAttribute.typia.constraints;
|
|
505
|
+
const nextConstraints = newAttribute.typia.constraints;
|
|
506
|
+
if (newAttribute.wp.enum && JSON.stringify(newAttribute.wp.enum) !== JSON.stringify(oldAttribute.wp.enum)) {
|
|
507
|
+
details.push("enum changed");
|
|
508
|
+
}
|
|
509
|
+
for (const key of ["minLength", "maxLength", "minimum", "maximum", "pattern", "format", "typeTag"]) {
|
|
510
|
+
if (oldConstraints[key] !== nextConstraints[key]) {
|
|
511
|
+
details.push(`${key}: ${oldConstraints[key]} -> ${nextConstraints[key]}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return details.join(", ");
|
|
515
|
+
}
|
|
516
|
+
function autoOutcome(pathLabel, kind, detail) {
|
|
517
|
+
return { detail, kind, path: pathLabel, status: "auto" };
|
|
518
|
+
}
|
|
519
|
+
function manualOutcome(pathLabel, kind, detail) {
|
|
520
|
+
return { detail, kind, path: pathLabel, status: "manual" };
|
|
521
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FIXTURES_DIR, ROOT_MANIFEST, SNAPSHOT_DIR } from "./migration-constants.js";
|
|
4
|
+
import { defaultValueForManifestAttribute } from "./migration-manifest.js";
|
|
5
|
+
import { cloneJsonValue, createFixtureScalarValue, createTransformFixtureValue, deleteValueAtPath, readJson, setValueAtPath, getValueAtPath, } from "./migration-utils.js";
|
|
6
|
+
export function ensureEdgeFixtureFile(projectDir, fromVersion, toVersion, diff) {
|
|
7
|
+
const fixturePath = path.join(projectDir, FIXTURES_DIR, `${fromVersion}-to-${toVersion}.json`);
|
|
8
|
+
if (fs.existsSync(fixturePath)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const manifest = readJson(path.join(projectDir, SNAPSHOT_DIR, fromVersion, ROOT_MANIFEST));
|
|
12
|
+
const attributes = {};
|
|
13
|
+
for (const [key, attribute] of Object.entries(manifest.attributes ?? {})) {
|
|
14
|
+
attributes[key] = defaultValueForManifestAttribute(attribute) ?? null;
|
|
15
|
+
}
|
|
16
|
+
const cases = [
|
|
17
|
+
{
|
|
18
|
+
input: attributes,
|
|
19
|
+
name: "default",
|
|
20
|
+
},
|
|
21
|
+
...createRenameFixtureCases(attributes, diff.summary.renameCandidates),
|
|
22
|
+
...createTransformFixtureCases(attributes, diff.summary.transformSuggestions),
|
|
23
|
+
...createUnionFixtureCases(attributes, manifest.attributes ?? {}, diff.summary.renameCandidates),
|
|
24
|
+
];
|
|
25
|
+
const fixtureDocument = {
|
|
26
|
+
cases,
|
|
27
|
+
fromVersion,
|
|
28
|
+
toVersion,
|
|
29
|
+
};
|
|
30
|
+
fs.writeFileSync(fixturePath, `${JSON.stringify(fixtureDocument, null, "\t")}\n`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
function createRenameFixtureCases(baseAttributes, renameCandidates) {
|
|
33
|
+
return renameCandidates
|
|
34
|
+
.filter((candidate) => candidate.autoApply)
|
|
35
|
+
.map((candidate) => {
|
|
36
|
+
const nextInput = cloneJsonValue(baseAttributes);
|
|
37
|
+
const legacyValue = getValueAtPath(nextInput, candidate.legacyPath);
|
|
38
|
+
deleteValueAtPath(nextInput, candidate.currentPath);
|
|
39
|
+
if (legacyValue === undefined) {
|
|
40
|
+
setValueAtPath(nextInput, candidate.legacyPath, createFixtureScalarValue(candidate.currentPath));
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
input: nextInput,
|
|
44
|
+
name: `rename:${candidate.legacyPath}->${candidate.currentPath}`,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function createTransformFixtureCases(baseAttributes, transformSuggestions) {
|
|
49
|
+
return transformSuggestions.map((suggestion) => {
|
|
50
|
+
const nextInput = cloneJsonValue(baseAttributes);
|
|
51
|
+
const legacyPath = suggestion.legacyPath ?? suggestion.currentPath;
|
|
52
|
+
setValueAtPath(nextInput, legacyPath, createTransformFixtureValue(suggestion.attribute, suggestion.currentPath));
|
|
53
|
+
return {
|
|
54
|
+
input: nextInput,
|
|
55
|
+
name: `transform:${legacyPath}->${suggestion.currentPath}`,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function createUnionFixtureCases(baseAttributes, manifestAttributes, renameCandidates) {
|
|
60
|
+
const cases = [];
|
|
61
|
+
for (const [key, attribute] of Object.entries(manifestAttributes)) {
|
|
62
|
+
if (attribute.ts.kind !== "union" || !attribute.ts.union) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (const [branchKey, branch] of Object.entries(attribute.ts.union.branches ?? {})) {
|
|
66
|
+
const nextInput = cloneJsonValue(baseAttributes);
|
|
67
|
+
const legacyPath = renameCandidates.find((candidate) => candidate.autoApply && candidate.currentPath === key)?.legacyPath ??
|
|
68
|
+
key;
|
|
69
|
+
setValueAtPath(nextInput, legacyPath, createUnionBranchFixtureValue(attribute.ts.union.discriminator, branchKey, branch));
|
|
70
|
+
cases.push({
|
|
71
|
+
input: nextInput,
|
|
72
|
+
name: `union:${key}:${branchKey}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return cases;
|
|
77
|
+
}
|
|
78
|
+
function createUnionBranchFixtureValue(discriminator, branchKey, branchAttribute) {
|
|
79
|
+
const branchValue = defaultValueForManifestAttribute(branchAttribute);
|
|
80
|
+
if (typeof branchValue === "object" && branchValue !== null && !Array.isArray(branchValue)) {
|
|
81
|
+
return {
|
|
82
|
+
...branchValue,
|
|
83
|
+
[discriminator]: branchKey,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
[discriminator]: branchKey,
|
|
88
|
+
};
|
|
89
|
+
}
|