@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,1101 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_FILE, FIXTURES_DIR, GENERATED_DIR, MIGRATION_TODO_PREFIX, ROOT_BLOCK_JSON, ROOT_MANIFEST, ROOT_SAVE_FILE, ROOT_TYPES_FILE, RULES_DIR, SNAPSHOT_DIR, SRC_BLOCK_JSON, SRC_MANIFEST, SUPPORTED_PROJECT_FILES, } from "./migration-constants.js";
|
|
4
|
+
import { compareMigrationVersionLabels, formatLegacyMigrationWorkspaceResetGuidance, isLegacySemverMigrationVersion, isMigrationVersionLabel, readJson, runProjectScriptIfPresent, } from "./migration-utils.js";
|
|
5
|
+
const DEFAULT_BLOCK_KEY = "default";
|
|
6
|
+
const SINGLE_BLOCK_LAYOUT_NOT_FOUND = "No supported single-block migration layout was found.";
|
|
7
|
+
const LEGACY_VERSIONED_EDGE_FILE_PATTERN = /^(\d+\.\d+\.\d+)-to-(\d+\.\d+\.\d+)\.(?:ts|json)$/;
|
|
8
|
+
const SINGLE_BLOCK_LAYOUT_CANDIDATES = [
|
|
9
|
+
{
|
|
10
|
+
blockJsonFile: SRC_BLOCK_JSON,
|
|
11
|
+
manifestFile: SRC_MANIFEST,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
blockJsonFile: ROOT_BLOCK_JSON,
|
|
15
|
+
manifestFile: ROOT_MANIFEST,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
const LEGACY_ROOT_SINGLE_BLOCK_LAYOUT = SINGLE_BLOCK_LAYOUT_CANDIDATES[1];
|
|
19
|
+
function createEmptyMigrationProjectBlockJson() {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
function createEmptyMigrationProjectManifest() {
|
|
23
|
+
return {
|
|
24
|
+
attributes: {},
|
|
25
|
+
manifestVersion: 2,
|
|
26
|
+
sourceType: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function normalizeRelativePath(value) {
|
|
30
|
+
return value.replace(/\\/g, "/");
|
|
31
|
+
}
|
|
32
|
+
function stripCommentsAndStrings(source) {
|
|
33
|
+
let result = "";
|
|
34
|
+
let index = 0;
|
|
35
|
+
let mode = "code";
|
|
36
|
+
while (index < source.length) {
|
|
37
|
+
const current = source[index];
|
|
38
|
+
const next = source[index + 1];
|
|
39
|
+
if (mode === "code") {
|
|
40
|
+
if (current === "/" && next === "/") {
|
|
41
|
+
result += " ";
|
|
42
|
+
index += 2;
|
|
43
|
+
mode = "line-comment";
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (current === "/" && next === "*") {
|
|
47
|
+
result += " ";
|
|
48
|
+
index += 2;
|
|
49
|
+
mode = "block-comment";
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (current === "'") {
|
|
53
|
+
result += current;
|
|
54
|
+
index += 1;
|
|
55
|
+
mode = "single-quote";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (current === "\"") {
|
|
59
|
+
result += current;
|
|
60
|
+
index += 1;
|
|
61
|
+
mode = "double-quote";
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (current === "`") {
|
|
65
|
+
result += current;
|
|
66
|
+
index += 1;
|
|
67
|
+
mode = "template";
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
result += current;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (mode === "line-comment") {
|
|
75
|
+
result += current === "\n" ? "\n" : " ";
|
|
76
|
+
index += 1;
|
|
77
|
+
if (current === "\n") {
|
|
78
|
+
mode = "code";
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (mode === "block-comment") {
|
|
83
|
+
if (current === "*" && next === "/") {
|
|
84
|
+
result += " ";
|
|
85
|
+
index += 2;
|
|
86
|
+
mode = "code";
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
result += current === "\n" ? "\n" : " ";
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (current === "\\") {
|
|
94
|
+
result += index + 1 < source.length ? " " : " ";
|
|
95
|
+
index += Math.min(2, source.length - index);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const closingQuote = mode === "single-quote"
|
|
99
|
+
? "'"
|
|
100
|
+
: mode === "double-quote"
|
|
101
|
+
? "\""
|
|
102
|
+
: "`";
|
|
103
|
+
if (current === closingQuote) {
|
|
104
|
+
result += current;
|
|
105
|
+
index += 1;
|
|
106
|
+
mode = "code";
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
result += current === "\n" ? "\n" : " ";
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function findMigrationConfigBodyRange(source) {
|
|
115
|
+
const sanitizedSource = stripCommentsAndStrings(source);
|
|
116
|
+
const configAssignment = /\bmigrationConfig\s*=\s*\{/u.exec(sanitizedSource);
|
|
117
|
+
if (!configAssignment) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const braceStart = configAssignment.index + configAssignment[0].length - 1;
|
|
121
|
+
let braceDepth = 0;
|
|
122
|
+
for (let index = braceStart; index < sanitizedSource.length; index += 1) {
|
|
123
|
+
const current = sanitizedSource[index];
|
|
124
|
+
if (current === "{") {
|
|
125
|
+
braceDepth += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (current === "}") {
|
|
129
|
+
braceDepth -= 1;
|
|
130
|
+
if (braceDepth === 0) {
|
|
131
|
+
return {
|
|
132
|
+
end: index,
|
|
133
|
+
start: braceStart + 1,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function createTopLevelConfigView(source) {
|
|
141
|
+
const range = findMigrationConfigBodyRange(source);
|
|
142
|
+
if (!range) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const bodySource = source.slice(range.start, range.end);
|
|
146
|
+
const sanitizedBody = stripCommentsAndStrings(bodySource);
|
|
147
|
+
let view = "";
|
|
148
|
+
let braceDepth = 0;
|
|
149
|
+
let bracketDepth = 0;
|
|
150
|
+
let parenDepth = 0;
|
|
151
|
+
for (let index = 0; index < sanitizedBody.length; index += 1) {
|
|
152
|
+
const current = sanitizedBody[index];
|
|
153
|
+
if (current === "{") {
|
|
154
|
+
braceDepth += 1;
|
|
155
|
+
}
|
|
156
|
+
else if (current === "}") {
|
|
157
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
158
|
+
}
|
|
159
|
+
else if (current === "[") {
|
|
160
|
+
bracketDepth += 1;
|
|
161
|
+
}
|
|
162
|
+
else if (current === "]") {
|
|
163
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
164
|
+
}
|
|
165
|
+
else if (current === "(") {
|
|
166
|
+
parenDepth += 1;
|
|
167
|
+
}
|
|
168
|
+
else if (current === ")") {
|
|
169
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
170
|
+
}
|
|
171
|
+
const nested = braceDepth > 0 || bracketDepth > 0 || parenDepth > 0;
|
|
172
|
+
view += nested && current !== "{" && current !== "}" && current !== "[" && current !== "]" && current !== "(" && current !== ")"
|
|
173
|
+
? current === "\n"
|
|
174
|
+
? "\n"
|
|
175
|
+
: " "
|
|
176
|
+
: current;
|
|
177
|
+
}
|
|
178
|
+
return view;
|
|
179
|
+
}
|
|
180
|
+
function findTopLevelConfigPropertyValueStart(source, key) {
|
|
181
|
+
const range = findMigrationConfigBodyRange(source);
|
|
182
|
+
if (!range) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const bodySource = source.slice(range.start, range.end);
|
|
186
|
+
const topLevelView = createTopLevelConfigView(source);
|
|
187
|
+
if (!topLevelView) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const pattern = new RegExp(`\\b${key}\\s*:\\s*`, "u");
|
|
191
|
+
const match = pattern.exec(topLevelView);
|
|
192
|
+
if (!match) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
let start = match.index + match[0].length;
|
|
196
|
+
while (start < bodySource.length && /\s/u.test(bodySource[start])) {
|
|
197
|
+
start += 1;
|
|
198
|
+
}
|
|
199
|
+
return { bodySource, start };
|
|
200
|
+
}
|
|
201
|
+
function hasLegacyConfigKeys(source) {
|
|
202
|
+
const topLevelView = createTopLevelConfigView(source);
|
|
203
|
+
if (!topLevelView) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return /\bcurrentVersion\s*:/u.test(topLevelView) || /\bsupportedVersions\s*:/u.test(topLevelView);
|
|
207
|
+
}
|
|
208
|
+
function readQuotedString(source, startIndex) {
|
|
209
|
+
const quote = source[startIndex];
|
|
210
|
+
if (quote !== "\"" && quote !== "'") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
let value = "";
|
|
214
|
+
let index = startIndex + 1;
|
|
215
|
+
while (index < source.length) {
|
|
216
|
+
const current = source[index];
|
|
217
|
+
if (current === "\\") {
|
|
218
|
+
if (index + 1 < source.length) {
|
|
219
|
+
value += source.slice(index, index + 2);
|
|
220
|
+
index += 2;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
value += current;
|
|
224
|
+
index += 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (current === quote) {
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
value += current;
|
|
231
|
+
index += 1;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function readStringArrayLiteral(source, startIndex) {
|
|
236
|
+
if (source[startIndex] !== "[") {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
let bracketDepth = 0;
|
|
240
|
+
let index = startIndex;
|
|
241
|
+
let quote = null;
|
|
242
|
+
while (index < source.length) {
|
|
243
|
+
const current = source[index];
|
|
244
|
+
if (quote) {
|
|
245
|
+
if (current === "\\") {
|
|
246
|
+
index += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (current === quote) {
|
|
250
|
+
quote = null;
|
|
251
|
+
}
|
|
252
|
+
index += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (current === "\"" || current === "'") {
|
|
256
|
+
quote = current;
|
|
257
|
+
index += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (current === "[") {
|
|
261
|
+
bracketDepth += 1;
|
|
262
|
+
}
|
|
263
|
+
else if (current === "]") {
|
|
264
|
+
bracketDepth -= 1;
|
|
265
|
+
if (bracketDepth === 0) {
|
|
266
|
+
const body = source.slice(startIndex + 1, index);
|
|
267
|
+
return [...body.matchAll(/["']([^"']+)["']/gu)].map((match) => match[1]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
index += 1;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
function readArrayLiteralBody(source, startIndex) {
|
|
275
|
+
if (source[startIndex] !== "[") {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
let bracketDepth = 0;
|
|
279
|
+
let index = startIndex;
|
|
280
|
+
let quote = null;
|
|
281
|
+
while (index < source.length) {
|
|
282
|
+
const current = source[index];
|
|
283
|
+
if (quote) {
|
|
284
|
+
if (current === "\\") {
|
|
285
|
+
index += 2;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (current === quote) {
|
|
289
|
+
quote = null;
|
|
290
|
+
}
|
|
291
|
+
index += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (current === "\"" || current === "'") {
|
|
295
|
+
quote = current;
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (current === "[") {
|
|
300
|
+
bracketDepth += 1;
|
|
301
|
+
}
|
|
302
|
+
else if (current === "]") {
|
|
303
|
+
bracketDepth -= 1;
|
|
304
|
+
if (bracketDepth === 0) {
|
|
305
|
+
return source.slice(startIndex + 1, index);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
index += 1;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function extractObjectLiteralBodies(source) {
|
|
313
|
+
const results = [];
|
|
314
|
+
let braceDepth = 0;
|
|
315
|
+
let objectStart = -1;
|
|
316
|
+
let quote = null;
|
|
317
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
318
|
+
const current = source[index];
|
|
319
|
+
if (quote) {
|
|
320
|
+
if (current === "\\") {
|
|
321
|
+
index += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (current === quote) {
|
|
325
|
+
quote = null;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (current === "\"" || current === "'") {
|
|
330
|
+
quote = current;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (current === "{") {
|
|
334
|
+
if (braceDepth === 0) {
|
|
335
|
+
objectStart = index + 1;
|
|
336
|
+
}
|
|
337
|
+
braceDepth += 1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (current === "}") {
|
|
341
|
+
braceDepth -= 1;
|
|
342
|
+
if (braceDepth === 0 && objectStart >= 0) {
|
|
343
|
+
results.push(source.slice(objectStart, index));
|
|
344
|
+
objectStart = -1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
function createLegacyMigrationWorkspaceResetError(reason) {
|
|
351
|
+
return new Error(`Detected a legacy semver-based migration workspace. ${formatLegacyMigrationWorkspaceResetGuidance(reason)}`);
|
|
352
|
+
}
|
|
353
|
+
function ensureRelativePath(projectDir, filePath) {
|
|
354
|
+
return normalizeRelativePath(path.relative(projectDir, filePath));
|
|
355
|
+
}
|
|
356
|
+
function toImportPath(fromDir, targetPath, stripExtension = false) {
|
|
357
|
+
let relativePath = normalizeRelativePath(path.relative(fromDir, targetPath));
|
|
358
|
+
if (!relativePath.startsWith(".")) {
|
|
359
|
+
relativePath = `./${relativePath}`;
|
|
360
|
+
}
|
|
361
|
+
if (stripExtension) {
|
|
362
|
+
relativePath = relativePath.replace(/\.[^.]+$/u, "");
|
|
363
|
+
}
|
|
364
|
+
return relativePath;
|
|
365
|
+
}
|
|
366
|
+
function readSingleBlockTarget(projectDir, { blockJsonFile, manifestFile, }) {
|
|
367
|
+
const requiredFiles = [blockJsonFile, ROOT_SAVE_FILE, ROOT_TYPES_FILE];
|
|
368
|
+
if (requiredFiles.some((relativePath) => !fs.existsSync(path.join(projectDir, relativePath)))) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const blockName = readJson(path.join(projectDir, blockJsonFile))?.name;
|
|
372
|
+
if (typeof blockName !== "string" || blockName.length === 0) {
|
|
373
|
+
throw new Error(`Unable to resolve block name from ${normalizeRelativePath(blockJsonFile)}`);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
blockJsonFile: normalizeRelativePath(blockJsonFile),
|
|
377
|
+
blockName,
|
|
378
|
+
key: DEFAULT_BLOCK_KEY,
|
|
379
|
+
manifestFile: normalizeRelativePath(manifestFile),
|
|
380
|
+
saveFile: ROOT_SAVE_FILE,
|
|
381
|
+
typesFile: ROOT_TYPES_FILE,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function collectSingleBlockCandidates(projectDir) {
|
|
385
|
+
return SINGLE_BLOCK_LAYOUT_CANDIDATES.filter(({ blockJsonFile }) => hasSingleBlockLayoutFiles(projectDir, blockJsonFile));
|
|
386
|
+
}
|
|
387
|
+
function hasSingleBlockLayoutFiles(projectDir, blockJsonFile) {
|
|
388
|
+
return [blockJsonFile, ROOT_SAVE_FILE, ROOT_TYPES_FILE].every((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
|
|
389
|
+
}
|
|
390
|
+
function orderSingleBlockCandidates(projectDir, candidates) {
|
|
391
|
+
const candidatesWithManifest = candidates.filter(({ manifestFile }) => fs.existsSync(path.join(projectDir, manifestFile)));
|
|
392
|
+
return [
|
|
393
|
+
...candidatesWithManifest,
|
|
394
|
+
...candidates.filter((candidate) => !candidatesWithManifest.includes(candidate)),
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
function createImplicitLegacyBlock(projectDir, blockName) {
|
|
398
|
+
if (blockName) {
|
|
399
|
+
try {
|
|
400
|
+
const rootTarget = readSingleBlockTarget(projectDir, LEGACY_ROOT_SINGLE_BLOCK_LAYOUT);
|
|
401
|
+
const srcTarget = readSingleBlockTarget(projectDir, SINGLE_BLOCK_LAYOUT_CANDIDATES[0]);
|
|
402
|
+
const hasSrcManifest = fs.existsSync(path.join(projectDir, SRC_MANIFEST));
|
|
403
|
+
if (rootTarget?.blockName === blockName &&
|
|
404
|
+
(!srcTarget || srcTarget.blockName !== blockName || !hasSrcManifest)) {
|
|
405
|
+
return {
|
|
406
|
+
...rootTarget,
|
|
407
|
+
key: DEFAULT_BLOCK_KEY,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Fall back to the shared discovery flow so malformed legacy roots do not block valid layouts.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const discovered = discoverSingleBlockTarget(projectDir, blockName);
|
|
416
|
+
return {
|
|
417
|
+
...discovered,
|
|
418
|
+
key: DEFAULT_BLOCK_KEY,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function createMalformedMultiBlockTargetError(directory, reason) {
|
|
422
|
+
return new Error("Unable to auto-detect a supported migration retrofit layout. " +
|
|
423
|
+
`Detected ${path.join("src", "blocks", directory, "block.json")} but ${reason}. ` +
|
|
424
|
+
"Create `src/migrations/config.ts` manually if your project uses a custom layout.");
|
|
425
|
+
}
|
|
426
|
+
function getRequiredProjectFiles(projectDir, blocks) {
|
|
427
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
428
|
+
return [
|
|
429
|
+
"package.json",
|
|
430
|
+
...blocks.flatMap((block) => [block.blockJsonFile, block.saveFile, block.typesFile]),
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
const configPath = path.join(projectDir, CONFIG_FILE);
|
|
434
|
+
if (fs.existsSync(configPath)) {
|
|
435
|
+
const config = parseMigrationConfig(fs.readFileSync(configPath, "utf8"));
|
|
436
|
+
const configuredBlocks = config.blocks ?? [createImplicitLegacyBlock(projectDir, config.blockName)];
|
|
437
|
+
return [
|
|
438
|
+
"package.json",
|
|
439
|
+
...configuredBlocks.flatMap((block) => [block.blockJsonFile, block.saveFile, block.typesFile]),
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
const discoveredLayout = discoverMigrationLayout(projectDir);
|
|
443
|
+
if (discoveredLayout?.mode === "multi") {
|
|
444
|
+
return [
|
|
445
|
+
"package.json",
|
|
446
|
+
...discoveredLayout.blocks.flatMap((block) => [block.blockJsonFile, block.saveFile, block.typesFile]),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
if (discoveredLayout?.mode === "single") {
|
|
450
|
+
return [
|
|
451
|
+
"package.json",
|
|
452
|
+
discoveredLayout.block.blockJsonFile,
|
|
453
|
+
discoveredLayout.block.saveFile,
|
|
454
|
+
discoveredLayout.block.typesFile,
|
|
455
|
+
];
|
|
456
|
+
}
|
|
457
|
+
return SUPPORTED_PROJECT_FILES;
|
|
458
|
+
}
|
|
459
|
+
export function ensureAdvancedMigrationProject(projectDir, blocks) {
|
|
460
|
+
const missing = getRequiredProjectFiles(projectDir, blocks).filter((relativePath) => !fs.existsSync(path.join(projectDir, relativePath)));
|
|
461
|
+
if (missing.length > 0) {
|
|
462
|
+
throw new Error(`This directory is not a supported migration-capable project. Missing: ${missing.join(", ")}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
export function getProjectPaths(projectDir) {
|
|
466
|
+
return {
|
|
467
|
+
configFile: path.join(projectDir, CONFIG_FILE),
|
|
468
|
+
fixturesDir: path.join(projectDir, FIXTURES_DIR),
|
|
469
|
+
generatedDir: path.join(projectDir, GENERATED_DIR),
|
|
470
|
+
rulesDir: path.join(projectDir, RULES_DIR),
|
|
471
|
+
snapshotDir: path.join(projectDir, SNAPSHOT_DIR),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function createBlockTarget(projectDir, { blockJsonFile, key, manifestFile, saveFile, typesFile, }) {
|
|
475
|
+
const requiredFiles = [blockJsonFile, saveFile, typesFile];
|
|
476
|
+
if (requiredFiles.some((relativePath) => !fs.existsSync(path.join(projectDir, relativePath)))) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const blockName = readJson(path.join(projectDir, blockJsonFile))?.name;
|
|
480
|
+
if (typeof blockName !== "string" || blockName.length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
blockJsonFile: normalizeRelativePath(blockJsonFile),
|
|
485
|
+
blockName,
|
|
486
|
+
key,
|
|
487
|
+
manifestFile: normalizeRelativePath(manifestFile),
|
|
488
|
+
saveFile: normalizeRelativePath(saveFile),
|
|
489
|
+
typesFile: normalizeRelativePath(typesFile),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function discoverSingleBlockTarget(projectDir, preferredBlockName) {
|
|
493
|
+
const candidates = collectSingleBlockCandidates(projectDir);
|
|
494
|
+
if (candidates.length === 0) {
|
|
495
|
+
throw new Error(SINGLE_BLOCK_LAYOUT_NOT_FOUND);
|
|
496
|
+
}
|
|
497
|
+
const readCandidate = (candidate) => readSingleBlockTarget(projectDir, candidate);
|
|
498
|
+
const orderedCandidates = orderSingleBlockCandidates(projectDir, candidates);
|
|
499
|
+
if (preferredBlockName) {
|
|
500
|
+
const validTargets = [];
|
|
501
|
+
let firstReadError = null;
|
|
502
|
+
for (const candidate of orderedCandidates) {
|
|
503
|
+
try {
|
|
504
|
+
const target = readCandidate(candidate);
|
|
505
|
+
if (!target) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (target.blockName === preferredBlockName) {
|
|
509
|
+
return target;
|
|
510
|
+
}
|
|
511
|
+
validTargets.push(target);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (!firstReadError && error instanceof Error) {
|
|
515
|
+
firstReadError = error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (validTargets.length > 0) {
|
|
520
|
+
throw new Error(`Configured migration blockName ${preferredBlockName} does not match the detected single-block layout(s): ${validTargets
|
|
521
|
+
.map((target) => target.blockName)
|
|
522
|
+
.join(", ")}.`);
|
|
523
|
+
}
|
|
524
|
+
if (firstReadError) {
|
|
525
|
+
throw firstReadError;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
let firstReadError;
|
|
529
|
+
let sawReadError = false;
|
|
530
|
+
for (const candidate of orderedCandidates) {
|
|
531
|
+
try {
|
|
532
|
+
const target = readCandidate(candidate);
|
|
533
|
+
if (target) {
|
|
534
|
+
return target;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (!sawReadError) {
|
|
539
|
+
firstReadError = error;
|
|
540
|
+
sawReadError = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (sawReadError) {
|
|
545
|
+
throw firstReadError;
|
|
546
|
+
}
|
|
547
|
+
throw new Error(SINGLE_BLOCK_LAYOUT_NOT_FOUND);
|
|
548
|
+
}
|
|
549
|
+
function discoverMigrationLayout(projectDir) {
|
|
550
|
+
const blocksRoot = path.join(projectDir, "src", "blocks");
|
|
551
|
+
let firstMultiBlockError = null;
|
|
552
|
+
if (fs.existsSync(blocksRoot) && fs.statSync(blocksRoot).isDirectory()) {
|
|
553
|
+
const blockDirectories = fs
|
|
554
|
+
.readdirSync(blocksRoot, { withFileTypes: true })
|
|
555
|
+
.filter((entry) => entry.isDirectory())
|
|
556
|
+
.map((entry) => entry.name);
|
|
557
|
+
const candidateDirectories = blockDirectories.filter((directory) => fs.existsSync(path.join(blocksRoot, directory, "block.json")));
|
|
558
|
+
if (candidateDirectories.length > 0) {
|
|
559
|
+
const blocks = candidateDirectories.flatMap((directory) => {
|
|
560
|
+
const saveFile = path.join("src", "blocks", directory, "save.tsx");
|
|
561
|
+
const typesFile = path.join("src", "blocks", directory, "types.ts");
|
|
562
|
+
const missingFiles = [saveFile, typesFile].filter((relativePath) => !fs.existsSync(path.join(projectDir, relativePath)));
|
|
563
|
+
if (missingFiles.length > 0) {
|
|
564
|
+
firstMultiBlockError ?? (firstMultiBlockError = createMalformedMultiBlockTargetError(directory, `the block target is missing ${missingFiles.join(", ")}`));
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
let block = null;
|
|
568
|
+
try {
|
|
569
|
+
block = createBlockTarget(projectDir, {
|
|
570
|
+
blockJsonFile: path.join("src", "blocks", directory, "block.json"),
|
|
571
|
+
key: directory,
|
|
572
|
+
manifestFile: path.join("src", "blocks", directory, "typia.manifest.json"),
|
|
573
|
+
saveFile,
|
|
574
|
+
typesFile,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
firstMultiBlockError ?? (firstMultiBlockError = error instanceof Error
|
|
579
|
+
? createMalformedMultiBlockTargetError(directory, `could not be parsed (${error.message})`)
|
|
580
|
+
: createMalformedMultiBlockTargetError(directory, "could not be parsed"));
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
if (!block) {
|
|
584
|
+
firstMultiBlockError ?? (firstMultiBlockError = createMalformedMultiBlockTargetError(directory, "it does not expose a valid block name"));
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
return [block];
|
|
588
|
+
});
|
|
589
|
+
if (blocks.length > 0) {
|
|
590
|
+
return {
|
|
591
|
+
blocks: blocks.sort((left, right) => left.key.localeCompare(right.key)),
|
|
592
|
+
mode: "multi",
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
return {
|
|
599
|
+
block: discoverSingleBlockTarget(projectDir),
|
|
600
|
+
mode: "single",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (error instanceof Error && error.message === SINGLE_BLOCK_LAYOUT_NOT_FOUND) {
|
|
605
|
+
if (firstMultiBlockError) {
|
|
606
|
+
throw firstMultiBlockError;
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Detects the supported migration retrofit layout for `migrate init`.
|
|
615
|
+
*
|
|
616
|
+
* Multi-block targets under `src/blocks/<slug>` take precedence over
|
|
617
|
+
* single-block layouts.
|
|
618
|
+
* Returns the detected layout on success and throws an actionable error when no
|
|
619
|
+
* supported first-party layout can be inferred.
|
|
620
|
+
*/
|
|
621
|
+
export function discoverMigrationInitLayout(projectDir) {
|
|
622
|
+
const discoveredLayout = discoverMigrationLayout(projectDir);
|
|
623
|
+
if (discoveredLayout) {
|
|
624
|
+
return discoveredLayout;
|
|
625
|
+
}
|
|
626
|
+
throw new Error("Unable to auto-detect a supported migration retrofit layout. " +
|
|
627
|
+
"Expected either `src/blocks/*/block.json` with matching `types.ts` and `save.tsx`, " +
|
|
628
|
+
"or a single-block layout using `src/block.json` (or legacy root `block.json`) with `src/types.ts` and `src/save.tsx`. " +
|
|
629
|
+
"Create `src/migrations/config.ts` manually if your project uses a custom layout.");
|
|
630
|
+
}
|
|
631
|
+
export function resolveMigrationBlocks(projectDir, config) {
|
|
632
|
+
if (Array.isArray(config.blocks)) {
|
|
633
|
+
if (config.blocks.length === 0) {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
return config.blocks.map((block) => {
|
|
637
|
+
const blockJsonPath = path.join(projectDir, block.blockJsonFile);
|
|
638
|
+
const manifestPath = path.join(projectDir, block.manifestFile);
|
|
639
|
+
return {
|
|
640
|
+
...block,
|
|
641
|
+
currentBlockJson: readJson(blockJsonPath),
|
|
642
|
+
currentManifest: readJson(manifestPath),
|
|
643
|
+
layout: "multi",
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return [createImplicitLegacyBlock(projectDir, config.blockName)].map((block) => {
|
|
648
|
+
const blockJsonPath = path.join(projectDir, block.blockJsonFile);
|
|
649
|
+
const manifestPath = path.join(projectDir, block.manifestFile);
|
|
650
|
+
return {
|
|
651
|
+
...block,
|
|
652
|
+
currentBlockJson: readJson(blockJsonPath),
|
|
653
|
+
currentManifest: readJson(manifestPath),
|
|
654
|
+
layout: "legacy",
|
|
655
|
+
};
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
export function getSnapshotRoot(projectDir, block, version) {
|
|
659
|
+
if ("layout" in block && block.layout === "legacy") {
|
|
660
|
+
return path.join(projectDir, SNAPSHOT_DIR, version);
|
|
661
|
+
}
|
|
662
|
+
return path.join(projectDir, SNAPSHOT_DIR, version, block.key);
|
|
663
|
+
}
|
|
664
|
+
export function getSnapshotBlockJsonPath(projectDir, block, version) {
|
|
665
|
+
return path.join(getSnapshotRoot(projectDir, block, version), ROOT_BLOCK_JSON);
|
|
666
|
+
}
|
|
667
|
+
export function getSnapshotManifestPath(projectDir, block, version) {
|
|
668
|
+
return path.join(getSnapshotRoot(projectDir, block, version), ROOT_MANIFEST);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Lists the snapshot versions currently present for a specific block target.
|
|
672
|
+
*
|
|
673
|
+
* Returns the sorted subset of supported migration versions that have a manifest on disk
|
|
674
|
+
* for the provided block, or an empty array when none exist.
|
|
675
|
+
*/
|
|
676
|
+
export function getAvailableSnapshotVersionsForBlock(projectDir, supportedMigrationVersions, block) {
|
|
677
|
+
return supportedMigrationVersions
|
|
678
|
+
.filter((version) => fs.existsSync(getSnapshotManifestPath(projectDir, block, version)))
|
|
679
|
+
.sort(compareMigrationVersionLabels);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Formats the standard missing-snapshot guidance for a block target.
|
|
683
|
+
*
|
|
684
|
+
* Returns a user-facing message that either lists the available snapshot
|
|
685
|
+
* versions or explains that no snapshots exist yet for the block.
|
|
686
|
+
*/
|
|
687
|
+
export function createMissingBlockSnapshotMessage(blockName, fromVersion, availableSnapshotVersions) {
|
|
688
|
+
return availableSnapshotVersions.length === 0
|
|
689
|
+
? `Snapshot manifest for ${blockName} @ ${fromVersion} does not exist. ` +
|
|
690
|
+
`No snapshots exist yet for ${blockName}. Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first.`
|
|
691
|
+
: `Snapshot manifest for ${blockName} @ ${fromVersion} does not exist. ` +
|
|
692
|
+
`Available snapshot versions for ${blockName}: ${availableSnapshotVersions.join(", ")}. ` +
|
|
693
|
+
`Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that release.`;
|
|
694
|
+
}
|
|
695
|
+
export function getSnapshotSavePath(projectDir, block, version) {
|
|
696
|
+
return path.join(getSnapshotRoot(projectDir, block, version), "save.tsx");
|
|
697
|
+
}
|
|
698
|
+
export function getGeneratedDirForBlock(paths, block) {
|
|
699
|
+
if ("layout" in block && block.layout === "legacy") {
|
|
700
|
+
return paths.generatedDir;
|
|
701
|
+
}
|
|
702
|
+
return path.join(paths.generatedDir, block.key);
|
|
703
|
+
}
|
|
704
|
+
export function getRuleFilePath(paths, block, fromVersion, toVersion) {
|
|
705
|
+
if ("layout" in block && block.layout === "legacy") {
|
|
706
|
+
return path.join(paths.rulesDir, `${fromVersion}-to-${toVersion}.ts`);
|
|
707
|
+
}
|
|
708
|
+
return path.join(paths.rulesDir, block.key, `${fromVersion}-to-${toVersion}.ts`);
|
|
709
|
+
}
|
|
710
|
+
export function getFixtureFilePath(paths, block, fromVersion, toVersion) {
|
|
711
|
+
if ("layout" in block && block.layout === "legacy") {
|
|
712
|
+
return path.join(paths.fixturesDir, `${fromVersion}-to-${toVersion}.json`);
|
|
713
|
+
}
|
|
714
|
+
return path.join(paths.fixturesDir, block.key, `${fromVersion}-to-${toVersion}.json`);
|
|
715
|
+
}
|
|
716
|
+
export function getValidatorsImportPath(projectDir, block, fromDir) {
|
|
717
|
+
const validatorPath = path.join(projectDir, block.typesFile.replace(/types\.ts$/u, "validators.ts"));
|
|
718
|
+
return toImportPath(fromDir, validatorPath, true);
|
|
719
|
+
}
|
|
720
|
+
export function ensureMigrationDirectories(projectDir, blocks) {
|
|
721
|
+
const paths = getProjectPaths(projectDir);
|
|
722
|
+
fs.mkdirSync(paths.fixturesDir, { recursive: true });
|
|
723
|
+
fs.mkdirSync(paths.generatedDir, { recursive: true });
|
|
724
|
+
fs.mkdirSync(paths.rulesDir, { recursive: true });
|
|
725
|
+
fs.mkdirSync(paths.snapshotDir, { recursive: true });
|
|
726
|
+
if (!Array.isArray(blocks) || blocks.length === 0) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
for (const block of blocks) {
|
|
730
|
+
fs.mkdirSync(path.join(paths.fixturesDir, block.key), { recursive: true });
|
|
731
|
+
fs.mkdirSync(path.join(paths.generatedDir, block.key), { recursive: true });
|
|
732
|
+
fs.mkdirSync(path.join(paths.rulesDir, block.key), { recursive: true });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
export function writeInitialMigrationScaffold(projectDir, currentMigrationVersion, blocks) {
|
|
736
|
+
const paths = getProjectPaths(projectDir);
|
|
737
|
+
const readmeFiles = [
|
|
738
|
+
[path.join(paths.snapshotDir, "README.md"), `# Migration Version Snapshots\n\nSnapshots for ${currentMigrationVersion} and future migration versions live here.\n`],
|
|
739
|
+
[path.join(paths.rulesDir, "README.md"), "# Migration Rules\n\nScaffold direct legacy-to-current migration rules in this directory.\n"],
|
|
740
|
+
[path.join(paths.fixturesDir, "README.md"), "# Migration Fixtures\n\nGenerated fixtures are used by verify to assert migrations.\n"],
|
|
741
|
+
];
|
|
742
|
+
for (const [targetPath, content] of readmeFiles) {
|
|
743
|
+
if (!fs.existsSync(targetPath)) {
|
|
744
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (!Array.isArray(blocks) || blocks.length === 0) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
for (const block of blocks) {
|
|
751
|
+
const scopedReadmes = [
|
|
752
|
+
[path.join(paths.rulesDir, block.key, "README.md"), `# ${block.blockName} Migration Rules\n\nScaffold direct legacy-to-current migration rules for ${block.blockName} in this directory.\n`],
|
|
753
|
+
[path.join(paths.fixturesDir, block.key, "README.md"), `# ${block.blockName} Migration Fixtures\n\nGenerated fixtures for ${block.blockName} are stored in this directory.\n`],
|
|
754
|
+
];
|
|
755
|
+
for (const [targetPath, content] of scopedReadmes) {
|
|
756
|
+
if (!fs.existsSync(targetPath)) {
|
|
757
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
758
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function findLegacySemverMigrationArtifacts(projectDir) {
|
|
764
|
+
const paths = getProjectPaths(projectDir);
|
|
765
|
+
const matches = [];
|
|
766
|
+
if (fs.existsSync(paths.snapshotDir)) {
|
|
767
|
+
for (const entry of fs.readdirSync(paths.snapshotDir, { withFileTypes: true })) {
|
|
768
|
+
if (entry.isDirectory() && isLegacySemverMigrationVersion(entry.name)) {
|
|
769
|
+
matches.push(normalizeRelativePath(path.join(SNAPSHOT_DIR, entry.name)));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const scanEdgeDir = (directory, relativeRoot) => {
|
|
774
|
+
if (!fs.existsSync(directory)) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const walk = (currentDir, currentRelativeDir) => {
|
|
778
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
779
|
+
if (entry.isDirectory()) {
|
|
780
|
+
walk(path.join(currentDir, entry.name), path.join(currentRelativeDir, entry.name));
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (LEGACY_VERSIONED_EDGE_FILE_PATTERN.test(entry.name)) {
|
|
784
|
+
matches.push(normalizeRelativePath(path.join(currentRelativeDir, entry.name)));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
walk(directory, relativeRoot);
|
|
789
|
+
};
|
|
790
|
+
scanEdgeDir(paths.rulesDir, RULES_DIR);
|
|
791
|
+
scanEdgeDir(paths.fixturesDir, FIXTURES_DIR);
|
|
792
|
+
return matches;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Guards a project directory against legacy semver-based migration workspaces.
|
|
796
|
+
*
|
|
797
|
+
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
798
|
+
* @returns Nothing.
|
|
799
|
+
* @throws Error When legacy config keys or semver-named migration artifacts are detected.
|
|
800
|
+
*/
|
|
801
|
+
export function assertNoLegacySemverMigrationWorkspace(projectDir) {
|
|
802
|
+
const paths = getProjectPaths(projectDir);
|
|
803
|
+
if (fs.existsSync(paths.configFile)) {
|
|
804
|
+
const source = fs.readFileSync(paths.configFile, "utf8");
|
|
805
|
+
if (hasLegacyConfigKeys(source)) {
|
|
806
|
+
throw createLegacyMigrationWorkspaceResetError("Detected legacy config keys `currentVersion` / `supportedVersions` in `src/migrations/config.ts`.");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const artifactMatches = findLegacySemverMigrationArtifacts(projectDir);
|
|
810
|
+
if (artifactMatches.length > 0) {
|
|
811
|
+
throw createLegacyMigrationWorkspaceResetError(`Detected legacy semver-named migration artifacts: ${artifactMatches.join(", ")}.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Loads the migration workspace state for a project directory.
|
|
816
|
+
*
|
|
817
|
+
* By default this loader may run the project's `sync-types` script when the
|
|
818
|
+
* current manifest files are missing, because later migration commands depend
|
|
819
|
+
* on those generated artifacts. Pass `allowSyncTypes: false` to keep the call
|
|
820
|
+
* read-only and fail instead of mutating the workspace.
|
|
821
|
+
*
|
|
822
|
+
* When `allowMissingConfig` is enabled and the migration config file does not
|
|
823
|
+
* exist yet, the loader synthesizes a minimal legacy-root config so bootstrap
|
|
824
|
+
* flows can continue before the first config write.
|
|
825
|
+
*
|
|
826
|
+
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
827
|
+
* @param options Loader flags controlling config fallback and `sync-types` side effects.
|
|
828
|
+
* @returns The resolved migration project state, including config, block targets, and helper paths.
|
|
829
|
+
* @throws Error When the project is not migration-capable, required manifests remain missing, or generated files cannot be read.
|
|
830
|
+
*/
|
|
831
|
+
export function loadMigrationProject(projectDir, { allowMissingConfig = false, allowSyncTypes = true, } = {}) {
|
|
832
|
+
assertNoLegacySemverMigrationWorkspace(projectDir);
|
|
833
|
+
ensureAdvancedMigrationProject(projectDir);
|
|
834
|
+
const paths = getProjectPaths(projectDir);
|
|
835
|
+
const config = allowMissingConfig && !fs.existsSync(paths.configFile)
|
|
836
|
+
? {
|
|
837
|
+
blocks: [createImplicitLegacyBlock(projectDir)],
|
|
838
|
+
currentMigrationVersion: "v1",
|
|
839
|
+
snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
|
|
840
|
+
supportedMigrationVersions: [],
|
|
841
|
+
}
|
|
842
|
+
: parseMigrationConfig(fs.readFileSync(paths.configFile, "utf8"));
|
|
843
|
+
const configuredBlocks = config.blocks === undefined ? [createImplicitLegacyBlock(projectDir, config.blockName)] : config.blocks;
|
|
844
|
+
const missingManifestFiles = configuredBlocks
|
|
845
|
+
.filter((block) => !fs.existsSync(path.join(projectDir, block.manifestFile)))
|
|
846
|
+
.map((block) => block.manifestFile);
|
|
847
|
+
if (missingManifestFiles.length > 0) {
|
|
848
|
+
if (!allowSyncTypes) {
|
|
849
|
+
throw new Error("Migration planning is read-only and cannot run `sync-types` automatically. " +
|
|
850
|
+
`Missing current manifest file(s): ${missingManifestFiles.join(", ")}. ` +
|
|
851
|
+
"Run your project's `sync-types` script in the project root first, then rerun the planning command.");
|
|
852
|
+
}
|
|
853
|
+
runProjectScriptIfPresent(projectDir, "sync-types");
|
|
854
|
+
const remainingManifestFiles = configuredBlocks
|
|
855
|
+
.filter((block) => !fs.existsSync(path.join(projectDir, block.manifestFile)))
|
|
856
|
+
.map((block) => block.manifestFile);
|
|
857
|
+
if (remainingManifestFiles.length > 0) {
|
|
858
|
+
throw new Error(`Missing current manifest file(s): ${remainingManifestFiles.join(", ")}. ` +
|
|
859
|
+
"Run your project's `sync-types` script in the project root first, then retry.");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const blocks = resolveMigrationBlocks(projectDir, config);
|
|
863
|
+
return {
|
|
864
|
+
blocks,
|
|
865
|
+
config,
|
|
866
|
+
currentBlockJson: blocks[0]?.currentBlockJson ??
|
|
867
|
+
(config.blocks !== undefined
|
|
868
|
+
? createEmptyMigrationProjectBlockJson()
|
|
869
|
+
: readJson(path.join(projectDir, ROOT_BLOCK_JSON))),
|
|
870
|
+
currentManifest: blocks[0]?.currentManifest ??
|
|
871
|
+
(config.blocks !== undefined
|
|
872
|
+
? createEmptyMigrationProjectManifest()
|
|
873
|
+
: readJson(path.join(projectDir, ROOT_MANIFEST))),
|
|
874
|
+
paths,
|
|
875
|
+
projectDir,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
export function discoverMigrationEntries(state) {
|
|
879
|
+
const entries = [];
|
|
880
|
+
const currentVersion = state.config.currentMigrationVersion;
|
|
881
|
+
for (const block of state.blocks) {
|
|
882
|
+
const generatedDir = getGeneratedDirForBlock(state.paths, block);
|
|
883
|
+
for (const version of state.config.supportedMigrationVersions) {
|
|
884
|
+
if (version === currentVersion) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const manifestPath = getSnapshotManifestPath(state.projectDir, block, version);
|
|
888
|
+
const blockJsonPath = getSnapshotBlockJsonPath(state.projectDir, block, version);
|
|
889
|
+
const savePath = getSnapshotSavePath(state.projectDir, block, version);
|
|
890
|
+
const rulePath = getRuleFilePath(state.paths, block, version, currentVersion);
|
|
891
|
+
if (!fs.existsSync(manifestPath) ||
|
|
892
|
+
!fs.existsSync(blockJsonPath) ||
|
|
893
|
+
!fs.existsSync(savePath) ||
|
|
894
|
+
!fs.existsSync(rulePath)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
entries.push({
|
|
898
|
+
block,
|
|
899
|
+
blockJsonImport: toImportPath(generatedDir, blockJsonPath),
|
|
900
|
+
fixtureImport: toImportPath(generatedDir, getFixtureFilePath(state.paths, block, version, currentVersion)),
|
|
901
|
+
fromVersion: version,
|
|
902
|
+
generatedDir,
|
|
903
|
+
manifestImport: toImportPath(generatedDir, manifestPath),
|
|
904
|
+
ruleImport: toImportPath(generatedDir, rulePath, true),
|
|
905
|
+
rulePath,
|
|
906
|
+
saveImport: toImportPath(generatedDir, savePath, true),
|
|
907
|
+
toVersion: currentVersion,
|
|
908
|
+
validatorImport: getValidatorsImportPath(state.projectDir, block, generatedDir),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return entries.sort((left, right) => {
|
|
913
|
+
const versionDelta = compareMigrationVersionLabels(right.fromVersion, left.fromVersion);
|
|
914
|
+
if (versionDelta !== 0) {
|
|
915
|
+
return versionDelta;
|
|
916
|
+
}
|
|
917
|
+
return left.block.key.localeCompare(right.block.key);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
export function parseMigrationConfig(source) {
|
|
921
|
+
if (hasLegacyConfigKeys(source)) {
|
|
922
|
+
throw createLegacyMigrationWorkspaceResetError("Detected legacy config keys `currentVersion` / `supportedVersions` in `src/migrations/config.ts`.");
|
|
923
|
+
}
|
|
924
|
+
const blockName = matchRootConfigValue(source, "blockName");
|
|
925
|
+
const currentMigrationVersion = matchRootConfigValue(source, "currentMigrationVersion");
|
|
926
|
+
const snapshotDir = matchRootConfigValue(source, "snapshotDir");
|
|
927
|
+
const supportedMigrationVersions = matchRootConfigStringArrayValue(source, "supportedMigrationVersions");
|
|
928
|
+
const blocksArrayBody = matchRootConfigArrayBody(source, "blocks");
|
|
929
|
+
const blocks = blocksArrayBody === null ? [] : parseMigrationBlocks(source);
|
|
930
|
+
if (!currentMigrationVersion || !snapshotDir || !supportedMigrationVersions) {
|
|
931
|
+
throw new Error("Unable to parse migration config. Regenerate with `wp-typia migrate init --current-migration-version v1`.");
|
|
932
|
+
}
|
|
933
|
+
if (blocksArrayBody !== null && blocks.length === 0 && blocksArrayBody.trim().length > 0) {
|
|
934
|
+
throw new Error("Migration config defines `blocks`, but the array entries could not be parsed. Regenerate the config or fix the malformed block targets in `src/migrations/config.ts`.");
|
|
935
|
+
}
|
|
936
|
+
if (!blockName && blocks.length === 0 && blocksArrayBody === null) {
|
|
937
|
+
throw new Error("Migration config must define `blockName` or `blocks`.");
|
|
938
|
+
}
|
|
939
|
+
if (!isMigrationVersionLabel(currentMigrationVersion)) {
|
|
940
|
+
if (isLegacySemverMigrationVersion(currentMigrationVersion)) {
|
|
941
|
+
throw createLegacyMigrationWorkspaceResetError(`Detected legacy semver migration version label \`${currentMigrationVersion}\` in \`src/migrations/config.ts\`.`);
|
|
942
|
+
}
|
|
943
|
+
throw new Error(`Invalid current migration version: ${currentMigrationVersion}. Expected vN with N >= 1.`);
|
|
944
|
+
}
|
|
945
|
+
for (const version of supportedMigrationVersions) {
|
|
946
|
+
if (!isMigrationVersionLabel(version)) {
|
|
947
|
+
if (isLegacySemverMigrationVersion(version)) {
|
|
948
|
+
throw createLegacyMigrationWorkspaceResetError(`Detected legacy semver migration version label \`${version}\` in \`src/migrations/config.ts\`.`);
|
|
949
|
+
}
|
|
950
|
+
throw new Error(`Invalid supported migration version: ${version}. Expected vN with N >= 1.`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
supportedMigrationVersions.sort(compareMigrationVersionLabels);
|
|
954
|
+
return {
|
|
955
|
+
blockName: blockName ?? undefined,
|
|
956
|
+
blocks: blocksArrayBody === null ? undefined : blocks,
|
|
957
|
+
currentMigrationVersion,
|
|
958
|
+
snapshotDir,
|
|
959
|
+
supportedMigrationVersions,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function parseMigrationBlocks(source) {
|
|
963
|
+
const blocksArrayBody = matchRootConfigArrayBody(source, "blocks");
|
|
964
|
+
if (!blocksArrayBody) {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
const blockLiterals = extractObjectLiteralBodies(blocksArrayBody);
|
|
968
|
+
return blockLiterals
|
|
969
|
+
.map((body) => {
|
|
970
|
+
const key = matchConfigValue(body, "key");
|
|
971
|
+
const blockName = matchConfigValue(body, "blockName");
|
|
972
|
+
const blockJsonFile = matchConfigValue(body, "blockJsonFile");
|
|
973
|
+
const manifestFile = matchConfigValue(body, "manifestFile");
|
|
974
|
+
const saveFile = matchConfigValue(body, "saveFile");
|
|
975
|
+
const typesFile = matchConfigValue(body, "typesFile");
|
|
976
|
+
if (!key || !blockName || !blockJsonFile || !manifestFile || !saveFile || !typesFile) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
blockJsonFile,
|
|
981
|
+
blockName,
|
|
982
|
+
key,
|
|
983
|
+
manifestFile,
|
|
984
|
+
saveFile,
|
|
985
|
+
typesFile,
|
|
986
|
+
};
|
|
987
|
+
})
|
|
988
|
+
.filter((block) => block !== null);
|
|
989
|
+
}
|
|
990
|
+
function matchConfigValue(source, key) {
|
|
991
|
+
const pattern = new RegExp(`${key}:\\s*["']([^"']+)["']`, "u");
|
|
992
|
+
return source.match(pattern)?.[1] ?? null;
|
|
993
|
+
}
|
|
994
|
+
function matchRootConfigValue(source, key) {
|
|
995
|
+
const match = findTopLevelConfigPropertyValueStart(source, key);
|
|
996
|
+
if (!match) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
return readQuotedString(match.bodySource, match.start);
|
|
1000
|
+
}
|
|
1001
|
+
function matchRootConfigStringArrayValue(source, key) {
|
|
1002
|
+
const match = findTopLevelConfigPropertyValueStart(source, key);
|
|
1003
|
+
if (!match) {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
return readStringArrayLiteral(match.bodySource, match.start);
|
|
1007
|
+
}
|
|
1008
|
+
function matchRootConfigArrayBody(source, key) {
|
|
1009
|
+
const match = findTopLevelConfigPropertyValueStart(source, key);
|
|
1010
|
+
if (!match) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
return readArrayLiteralBody(match.bodySource, match.start);
|
|
1014
|
+
}
|
|
1015
|
+
export function writeMigrationConfig(projectDir, config) {
|
|
1016
|
+
const paths = getProjectPaths(projectDir);
|
|
1017
|
+
fs.mkdirSync(path.dirname(paths.configFile), { recursive: true });
|
|
1018
|
+
if (!Array.isArray(config.blocks)) {
|
|
1019
|
+
fs.writeFileSync(paths.configFile, `export const migrationConfig = {
|
|
1020
|
+
\tblockName: '${config.blockName ?? readProjectBlockName(projectDir)}',
|
|
1021
|
+
\tcurrentMigrationVersion: '${config.currentMigrationVersion}',
|
|
1022
|
+
\tsupportedMigrationVersions: [ ${config.supportedMigrationVersions.map((version) => `'${version}'`).join(", ")} ],
|
|
1023
|
+
\tsnapshotDir: '${config.snapshotDir}',
|
|
1024
|
+
} as const;
|
|
1025
|
+
|
|
1026
|
+
export default migrationConfig;
|
|
1027
|
+
`, "utf8");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const blocks = config.blocks;
|
|
1031
|
+
const blocksSource = blocks
|
|
1032
|
+
.map((block) => `\t\t{
|
|
1033
|
+
\t\t\tkey: '${block.key}',
|
|
1034
|
+
\t\t\tblockName: '${block.blockName}',
|
|
1035
|
+
\t\t\tblockJsonFile: '${normalizeRelativePath(block.blockJsonFile)}',
|
|
1036
|
+
\t\t\tmanifestFile: '${normalizeRelativePath(block.manifestFile)}',
|
|
1037
|
+
\t\t\tsaveFile: '${normalizeRelativePath(block.saveFile)}',
|
|
1038
|
+
\t\t\ttypesFile: '${normalizeRelativePath(block.typesFile)}',
|
|
1039
|
+
\t\t},`)
|
|
1040
|
+
.join("\n");
|
|
1041
|
+
fs.writeFileSync(paths.configFile, `export const migrationConfig = {
|
|
1042
|
+
\tcurrentMigrationVersion: '${config.currentMigrationVersion}',
|
|
1043
|
+
\tsupportedMigrationVersions: [ ${config.supportedMigrationVersions.map((version) => `'${version}'`).join(", ")} ],
|
|
1044
|
+
\tsnapshotDir: '${config.snapshotDir}',
|
|
1045
|
+
\tblocks: [
|
|
1046
|
+
${blocksSource}
|
|
1047
|
+
\t],
|
|
1048
|
+
} as const;
|
|
1049
|
+
|
|
1050
|
+
export default migrationConfig;
|
|
1051
|
+
`, "utf8");
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Returns the discovered block name for a supported single-block project.
|
|
1055
|
+
*
|
|
1056
|
+
* Uses `discoverSingleBlockTarget(projectDir)` internally and throws when the
|
|
1057
|
+
* project directory does not resolve to a supported single-block migration
|
|
1058
|
+
* layout.
|
|
1059
|
+
*/
|
|
1060
|
+
export function readProjectBlockName(projectDir) {
|
|
1061
|
+
return discoverSingleBlockTarget(projectDir).blockName;
|
|
1062
|
+
}
|
|
1063
|
+
export function assertRuleHasNoTodos(projectDir, block, fromMigrationVersion, toMigrationVersion) {
|
|
1064
|
+
const rulePath = getRuleFilePath(getProjectPaths(projectDir), block, fromMigrationVersion, toMigrationVersion);
|
|
1065
|
+
if (!fs.existsSync(rulePath)) {
|
|
1066
|
+
throw new Error(`Missing migration rule: ${path.relative(projectDir, rulePath)}`);
|
|
1067
|
+
}
|
|
1068
|
+
const source = fs.readFileSync(rulePath, "utf8");
|
|
1069
|
+
if (source.includes(MIGRATION_TODO_PREFIX)) {
|
|
1070
|
+
throw new Error(`Migration rule still contains ${MIGRATION_TODO_PREFIX} markers: ${path.relative(projectDir, rulePath)}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
export function readRuleMetadata(rulePath) {
|
|
1074
|
+
const source = fs.readFileSync(rulePath, "utf8");
|
|
1075
|
+
const unresolvedBlock = source.match(/export const unresolved = \[([\s\S]*?)\] as const;/);
|
|
1076
|
+
const renameMapBlock = source.match(/export const renameMap: RenameMap = \{([\s\S]*?)\};/);
|
|
1077
|
+
const transformsBlock = source.match(/export const transforms: TransformMap = \{([\s\S]*?)\};/);
|
|
1078
|
+
const unresolved = unresolvedBlock
|
|
1079
|
+
? [...unresolvedBlock[1].matchAll(/"([^"]+)"/g)].map((match) => match[1])
|
|
1080
|
+
: [];
|
|
1081
|
+
const renameMap = renameMapBlock
|
|
1082
|
+
? [...renameMapBlock[1].matchAll(/^\s*"([^"]+)":\s*"([^"]+)"/gm)].map((match) => ({
|
|
1083
|
+
currentPath: match[1],
|
|
1084
|
+
legacyPath: match[2],
|
|
1085
|
+
}))
|
|
1086
|
+
: [];
|
|
1087
|
+
const transforms = transformsBlock
|
|
1088
|
+
? [...transformsBlock[1].matchAll(/^\s*"([^"]+)":\s*\(/gm)].map((match) => match[1])
|
|
1089
|
+
: [];
|
|
1090
|
+
return { renameMap, transforms, unresolved };
|
|
1091
|
+
}
|
|
1092
|
+
export function createMigrationBlockConfig(block) {
|
|
1093
|
+
return {
|
|
1094
|
+
blockJsonFile: ensureRelativePath(process.cwd(), block.blockJsonFile),
|
|
1095
|
+
blockName: block.blockName,
|
|
1096
|
+
key: block.key,
|
|
1097
|
+
manifestFile: ensureRelativePath(process.cwd(), block.manifestFile),
|
|
1098
|
+
saveFile: ensureRelativePath(process.cwd(), block.saveFile),
|
|
1099
|
+
typesFile: ensureRelativePath(process.cwd(), block.typesFile),
|
|
1100
|
+
};
|
|
1101
|
+
}
|