@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,129 @@
|
|
|
1
|
+
export function flattenManifestLeafAttributes(attributes) {
|
|
2
|
+
return Object.entries(attributes).flatMap(([key, attribute]) => flattenManifestAttribute(attribute, key, key, {
|
|
3
|
+
rootPath: key,
|
|
4
|
+
unionBranch: null,
|
|
5
|
+
unionDiscriminator: null,
|
|
6
|
+
unionRoot: null,
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
export function flattenManifestAttribute(attribute, currentPath, sourcePath, context) {
|
|
10
|
+
if (!attribute) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
if (attribute.ts.kind === "object") {
|
|
14
|
+
const properties = Object.entries(attribute.ts.properties ?? {});
|
|
15
|
+
if (properties.length === 0) {
|
|
16
|
+
return [{ ...context, attribute, currentPath, sourcePath }];
|
|
17
|
+
}
|
|
18
|
+
return properties.flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${key}`, `${sourcePath}.${key}`, {
|
|
19
|
+
...context,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
if (attribute.ts.kind === "union" && attribute.ts.union) {
|
|
23
|
+
const unionMetadata = attribute.ts.union;
|
|
24
|
+
return Object.entries(attribute.ts.union.branches ?? {}).flatMap(([branchKey, branchAttribute]) => Object.entries(branchAttribute.ts.properties ?? {})
|
|
25
|
+
.filter(([key]) => key !== unionMetadata.discriminator)
|
|
26
|
+
.flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${branchKey}.${key}`, `${sourcePath}.${key}`, {
|
|
27
|
+
rootPath: context.rootPath,
|
|
28
|
+
unionBranch: branchKey,
|
|
29
|
+
unionDiscriminator: unionMetadata.discriminator,
|
|
30
|
+
unionRoot: currentPath,
|
|
31
|
+
})));
|
|
32
|
+
}
|
|
33
|
+
return [{ ...context, attribute, currentPath, sourcePath }];
|
|
34
|
+
}
|
|
35
|
+
export function getAttributeByCurrentPath(attributes, currentPath) {
|
|
36
|
+
const segments = String(currentPath).split(".");
|
|
37
|
+
const rootKey = segments.shift();
|
|
38
|
+
if (!rootKey) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let attribute = attributes[rootKey] ?? null;
|
|
42
|
+
while (attribute && segments.length > 0) {
|
|
43
|
+
if (attribute.ts.kind === "union" && attribute.ts.union) {
|
|
44
|
+
const branchKey = segments.shift();
|
|
45
|
+
if (!branchKey || !(branchKey in attribute.ts.union.branches)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
attribute = attribute.ts.union.branches[branchKey];
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (attribute.ts.kind === "object" && attribute.ts.properties) {
|
|
52
|
+
const propertyKey = segments.shift();
|
|
53
|
+
if (!propertyKey || !(propertyKey in attribute.ts.properties)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
attribute = attribute.ts.properties[propertyKey];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return attribute;
|
|
62
|
+
}
|
|
63
|
+
export function hasManifestDefault(attribute) {
|
|
64
|
+
return attribute?.typia?.hasDefault === true;
|
|
65
|
+
}
|
|
66
|
+
export function getManifestDefaultValue(attribute) {
|
|
67
|
+
return attribute?.typia?.defaultValue ?? null;
|
|
68
|
+
}
|
|
69
|
+
export function summarizeManifest(manifest) {
|
|
70
|
+
return {
|
|
71
|
+
attributes: Object.fromEntries(Object.entries(manifest.attributes ?? {}).map(([name, attribute]) => [
|
|
72
|
+
name,
|
|
73
|
+
{
|
|
74
|
+
constraints: attribute.typia?.constraints ?? {},
|
|
75
|
+
defaultValue: attribute.typia?.defaultValue ?? null,
|
|
76
|
+
hasDefault: attribute.typia?.hasDefault ?? false,
|
|
77
|
+
enum: attribute.wp?.enum ?? null,
|
|
78
|
+
kind: attribute.ts?.kind ?? null,
|
|
79
|
+
required: attribute.ts?.required ?? false,
|
|
80
|
+
union: attribute.ts?.union ?? null,
|
|
81
|
+
},
|
|
82
|
+
])),
|
|
83
|
+
manifestVersion: manifest.manifestVersion ?? null,
|
|
84
|
+
sourceType: manifest.sourceType ?? null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function summarizeUnionBranches(manifestSummary) {
|
|
88
|
+
if (!manifestSummary?.attributes) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return Object.entries(manifestSummary.attributes)
|
|
92
|
+
.filter(([, attribute]) => attribute.kind === "union" && attribute.union)
|
|
93
|
+
.map(([field, attribute]) => ({
|
|
94
|
+
branches: Object.keys(attribute.union?.branches ?? {}),
|
|
95
|
+
discriminator: attribute.union?.discriminator ?? null,
|
|
96
|
+
field,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
export function defaultValueForManifestAttribute(attribute) {
|
|
100
|
+
if (attribute.typia?.hasDefault) {
|
|
101
|
+
return attribute.typia.defaultValue;
|
|
102
|
+
}
|
|
103
|
+
if (attribute.wp?.enum && attribute.wp.enum.length > 0) {
|
|
104
|
+
return attribute.wp.enum[0] ?? null;
|
|
105
|
+
}
|
|
106
|
+
switch (attribute.ts.kind) {
|
|
107
|
+
case "string":
|
|
108
|
+
return "";
|
|
109
|
+
case "number":
|
|
110
|
+
return 0;
|
|
111
|
+
case "boolean":
|
|
112
|
+
return false;
|
|
113
|
+
case "array":
|
|
114
|
+
return [];
|
|
115
|
+
case "object": {
|
|
116
|
+
const result = {};
|
|
117
|
+
for (const [key, property] of Object.entries(attribute.ts.properties ?? {})) {
|
|
118
|
+
result[key] = defaultValueForManifestAttribute(property);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
case "union": {
|
|
123
|
+
const firstBranch = Object.values(attribute.ts.union?.branches ?? {})[0];
|
|
124
|
+
return firstBranch ? defaultValueForManifestAttribute(firstBranch) : null;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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, RULES_DIR, SNAPSHOT_DIR, SUPPORTED_PROJECT_FILES, } from "./migration-constants.js";
|
|
4
|
+
import { compareSemver, readJson, runProjectScriptIfPresent, } from "./migration-utils.js";
|
|
5
|
+
export function ensureAdvancedMigrationProject(projectDir) {
|
|
6
|
+
const missing = SUPPORTED_PROJECT_FILES.filter((relativePath) => !fs.existsSync(path.join(projectDir, relativePath)));
|
|
7
|
+
if (missing.length > 0) {
|
|
8
|
+
throw new Error(`This directory is not a supported advanced migration project. Missing: ${missing.join(", ")}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function getProjectPaths(projectDir) {
|
|
12
|
+
return {
|
|
13
|
+
configFile: path.join(projectDir, CONFIG_FILE),
|
|
14
|
+
fixturesDir: path.join(projectDir, FIXTURES_DIR),
|
|
15
|
+
generatedDir: path.join(projectDir, GENERATED_DIR),
|
|
16
|
+
rulesDir: path.join(projectDir, RULES_DIR),
|
|
17
|
+
snapshotDir: path.join(projectDir, SNAPSHOT_DIR),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function ensureMigrationDirectories(projectDir) {
|
|
21
|
+
const paths = getProjectPaths(projectDir);
|
|
22
|
+
fs.mkdirSync(paths.fixturesDir, { recursive: true });
|
|
23
|
+
fs.mkdirSync(paths.generatedDir, { recursive: true });
|
|
24
|
+
fs.mkdirSync(paths.rulesDir, { recursive: true });
|
|
25
|
+
fs.mkdirSync(paths.snapshotDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
export function writeInitialMigrationScaffold(projectDir, currentVersion) {
|
|
28
|
+
const paths = getProjectPaths(projectDir);
|
|
29
|
+
const readmeFiles = [
|
|
30
|
+
[path.join(paths.snapshotDir, "README.md"), `# Version Snapshots\n\nSnapshots for ${currentVersion} and future versions live here.\n`],
|
|
31
|
+
[path.join(paths.rulesDir, "README.md"), `# Migration Rules\n\nScaffold direct legacy-to-current migration rules in this directory.\n`],
|
|
32
|
+
[path.join(paths.fixturesDir, "README.md"), `# Migration Fixtures\n\nGenerated fixtures are used by verify to assert migrations.\n`],
|
|
33
|
+
];
|
|
34
|
+
for (const [targetPath, content] of readmeFiles) {
|
|
35
|
+
if (!fs.existsSync(targetPath)) {
|
|
36
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function loadMigrationProject(projectDir, { allowMissingConfig = false } = {}) {
|
|
41
|
+
ensureAdvancedMigrationProject(projectDir);
|
|
42
|
+
if (!fs.existsSync(path.join(projectDir, ROOT_MANIFEST))) {
|
|
43
|
+
runProjectScriptIfPresent(projectDir, "sync-types");
|
|
44
|
+
}
|
|
45
|
+
const paths = getProjectPaths(projectDir);
|
|
46
|
+
const config = allowMissingConfig && !fs.existsSync(paths.configFile)
|
|
47
|
+
? {
|
|
48
|
+
blockName: readProjectBlockName(projectDir),
|
|
49
|
+
currentVersion: "0.0.0",
|
|
50
|
+
snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
|
|
51
|
+
supportedVersions: [],
|
|
52
|
+
}
|
|
53
|
+
: parseMigrationConfig(fs.readFileSync(paths.configFile, "utf8"));
|
|
54
|
+
return {
|
|
55
|
+
config,
|
|
56
|
+
currentBlockJson: readJson(path.join(projectDir, ROOT_BLOCK_JSON)),
|
|
57
|
+
currentManifest: readJson(path.join(projectDir, ROOT_MANIFEST)),
|
|
58
|
+
paths,
|
|
59
|
+
projectDir,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function discoverMigrationEntries(state) {
|
|
63
|
+
const entries = [];
|
|
64
|
+
const currentVersion = state.config.currentVersion;
|
|
65
|
+
for (const version of state.config.supportedVersions) {
|
|
66
|
+
if (version === currentVersion) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const snapshotRoot = path.join(state.projectDir, SNAPSHOT_DIR, version);
|
|
70
|
+
const manifestPath = path.join(snapshotRoot, ROOT_MANIFEST);
|
|
71
|
+
const blockJsonPath = path.join(snapshotRoot, ROOT_BLOCK_JSON);
|
|
72
|
+
const savePath = path.join(snapshotRoot, "save.tsx");
|
|
73
|
+
const rulePath = getRuleFilePath(state.paths, version, currentVersion);
|
|
74
|
+
if (fs.existsSync(manifestPath) &&
|
|
75
|
+
fs.existsSync(blockJsonPath) &&
|
|
76
|
+
fs.existsSync(savePath) &&
|
|
77
|
+
fs.existsSync(rulePath)) {
|
|
78
|
+
entries.push({
|
|
79
|
+
blockJsonImport: `../versions/${version}/block.json`,
|
|
80
|
+
fixtureImport: `../fixtures/${version}-to-${currentVersion}.json`,
|
|
81
|
+
fromVersion: version,
|
|
82
|
+
manifestImport: `../versions/${version}/typia.manifest.json`,
|
|
83
|
+
ruleImport: `../rules/${version}-to-${currentVersion}`,
|
|
84
|
+
rulePath,
|
|
85
|
+
saveImport: `../versions/${version}/save`,
|
|
86
|
+
toVersion: currentVersion,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return entries.sort((left, right) => compareSemver(right.fromVersion, left.fromVersion));
|
|
91
|
+
}
|
|
92
|
+
export function parseMigrationConfig(source) {
|
|
93
|
+
const blockName = matchConfigValue(source, "blockName");
|
|
94
|
+
const currentVersion = matchConfigValue(source, "currentVersion");
|
|
95
|
+
const snapshotDir = matchConfigValue(source, "snapshotDir");
|
|
96
|
+
const supportedVersionsMatch = source.match(/supportedVersions:\s*\[([\s\S]*?)\]/);
|
|
97
|
+
if (!blockName || !currentVersion || !snapshotDir || !supportedVersionsMatch) {
|
|
98
|
+
throw new Error("Unable to parse migration config. Regenerate with `wp-typia migrations init`.");
|
|
99
|
+
}
|
|
100
|
+
const supportedVersions = supportedVersionsMatch[1]
|
|
101
|
+
.split(",")
|
|
102
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.sort(compareSemver);
|
|
105
|
+
return {
|
|
106
|
+
blockName,
|
|
107
|
+
currentVersion,
|
|
108
|
+
snapshotDir,
|
|
109
|
+
supportedVersions,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function matchConfigValue(source, key) {
|
|
113
|
+
return source.match(new RegExp(`${key}:\\s*"([^"]+)"`))?.[1] ?? null;
|
|
114
|
+
}
|
|
115
|
+
export function writeMigrationConfig(projectDir, config) {
|
|
116
|
+
const paths = getProjectPaths(projectDir);
|
|
117
|
+
fs.mkdirSync(path.dirname(paths.configFile), { recursive: true });
|
|
118
|
+
fs.writeFileSync(paths.configFile, `export const migrationConfig = {
|
|
119
|
+
blockName: "${config.blockName}",
|
|
120
|
+
currentVersion: "${config.currentVersion}",
|
|
121
|
+
supportedVersions: [${config.supportedVersions.map((version) => `"${version}"`).join(", ")}],
|
|
122
|
+
snapshotDir: "${config.snapshotDir}",
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
export default migrationConfig;
|
|
126
|
+
`, "utf8");
|
|
127
|
+
}
|
|
128
|
+
export function readProjectBlockName(projectDir) {
|
|
129
|
+
const blockJson = readJson(path.join(projectDir, ROOT_BLOCK_JSON));
|
|
130
|
+
const blockName = blockJson?.name;
|
|
131
|
+
if (typeof blockName !== "string" || blockName.length === 0) {
|
|
132
|
+
throw new Error("Unable to resolve block name from block.json");
|
|
133
|
+
}
|
|
134
|
+
return blockName;
|
|
135
|
+
}
|
|
136
|
+
export function assertRuleHasNoTodos(projectDir, fromVersion, toVersion) {
|
|
137
|
+
const rulePath = getRuleFilePath(getProjectPaths(projectDir), fromVersion, toVersion);
|
|
138
|
+
if (!fs.existsSync(rulePath)) {
|
|
139
|
+
throw new Error(`Missing migration rule: ${path.relative(projectDir, rulePath)}`);
|
|
140
|
+
}
|
|
141
|
+
const source = fs.readFileSync(rulePath, "utf8");
|
|
142
|
+
if (source.includes(MIGRATION_TODO_PREFIX)) {
|
|
143
|
+
throw new Error(`Migration rule still contains ${MIGRATION_TODO_PREFIX} markers: ${path.relative(projectDir, rulePath)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function getRuleFilePath(paths, fromVersion, toVersion) {
|
|
147
|
+
return path.join(paths.rulesDir, `${fromVersion}-to-${toVersion}.ts`);
|
|
148
|
+
}
|
|
149
|
+
export function readRuleMetadata(rulePath) {
|
|
150
|
+
const source = fs.readFileSync(rulePath, "utf8");
|
|
151
|
+
const unresolvedBlock = source.match(/export const unresolved = \[([\s\S]*?)\] as const;/);
|
|
152
|
+
const renameMapBlock = source.match(/export const renameMap: RenameMap = \{([\s\S]*?)\};/);
|
|
153
|
+
const transformsBlock = source.match(/export const transforms: TransformMap = \{([\s\S]*?)\};/);
|
|
154
|
+
const unresolved = unresolvedBlock
|
|
155
|
+
? [...unresolvedBlock[1].matchAll(/"([^"]+)"/g)].map((match) => match[1])
|
|
156
|
+
: [];
|
|
157
|
+
const renameMap = renameMapBlock
|
|
158
|
+
? [...renameMapBlock[1].matchAll(/^\s*"([^"]+)":\s*"([^"]+)"/gm)].map((match) => ({
|
|
159
|
+
currentPath: match[1],
|
|
160
|
+
legacyPath: match[2],
|
|
161
|
+
}))
|
|
162
|
+
: [];
|
|
163
|
+
const transforms = transformsBlock
|
|
164
|
+
? [...transformsBlock[1].matchAll(/^\s*"([^"]+)":\s*\(/gm)].map((match) => match[1])
|
|
165
|
+
: [];
|
|
166
|
+
return { renameMap, transforms, unresolved };
|
|
167
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MIGRATION_TODO_PREFIX, ROOT_BLOCK_JSON, ROOT_MANIFEST, SNAPSHOT_DIR, } from "./migration-constants.js";
|
|
4
|
+
import { summarizeManifest, summarizeUnionBranches } from "./migration-manifest.js";
|
|
5
|
+
import { readRuleMetadata } from "./migration-project.js";
|
|
6
|
+
import { escapeForCode, readJson, renderObjectKey, renderPhpValue } from "./migration-utils.js";
|
|
7
|
+
export function formatDiffReport(diff) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`Migration diff: ${diff.fromVersion} -> ${diff.toVersion}`,
|
|
10
|
+
`Current type: ${diff.currentTypeName}`,
|
|
11
|
+
`Safe changes: ${diff.summary.auto}`,
|
|
12
|
+
`Manual changes: ${diff.summary.manual}`,
|
|
13
|
+
];
|
|
14
|
+
if (diff.summary.autoItems.length > 0) {
|
|
15
|
+
lines.push("", "Safe changes:");
|
|
16
|
+
for (const item of diff.summary.autoItems) {
|
|
17
|
+
lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (diff.summary.manualItems.length > 0) {
|
|
21
|
+
lines.push("", "Manual review required:");
|
|
22
|
+
for (const item of diff.summary.manualItems) {
|
|
23
|
+
lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (diff.summary.renameCandidates.length > 0) {
|
|
27
|
+
const autoApplied = diff.summary.renameCandidates.filter((item) => item.autoApply);
|
|
28
|
+
const suggested = diff.summary.renameCandidates.filter((item) => !item.autoApply);
|
|
29
|
+
if (autoApplied.length > 0) {
|
|
30
|
+
lines.push("", "Auto-applied renames:");
|
|
31
|
+
for (const item of autoApplied) {
|
|
32
|
+
lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (suggested.length > 0) {
|
|
36
|
+
lines.push("", "Suggested renames:");
|
|
37
|
+
for (const item of suggested) {
|
|
38
|
+
lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (diff.summary.transformSuggestions.length > 0) {
|
|
43
|
+
lines.push("", "Suggested transforms:");
|
|
44
|
+
for (const item of diff.summary.transformSuggestions) {
|
|
45
|
+
lines.push(` - ${item.currentPath}${item.legacyPath ? ` <- ${item.legacyPath}` : ""} (${item.reason})`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
export function renderMigrationRuleFile({ currentAttributes, currentTypeName, diff, fromVersion, targetVersion, }) {
|
|
51
|
+
const activeRenameCandidates = diff.summary.renameCandidates.filter((candidate) => candidate.autoApply);
|
|
52
|
+
const suggestedRenameCandidates = diff.summary.renameCandidates.filter((candidate) => !candidate.autoApply);
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(`import type { ${currentTypeName} } from "../../types";`);
|
|
55
|
+
lines.push(`import currentManifest from "../../../${ROOT_MANIFEST}";`);
|
|
56
|
+
lines.push(`import {`);
|
|
57
|
+
lines.push(`\ttype RenameMap,`);
|
|
58
|
+
lines.push(`\ttype TransformMap,`);
|
|
59
|
+
lines.push(`\tresolveMigrationAttribute,`);
|
|
60
|
+
lines.push(`} from "../helpers";`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(`export const fromVersion = "${fromVersion}" as const;`);
|
|
63
|
+
lines.push(`export const toVersion = "${targetVersion}" as const;`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("export const renameMap: RenameMap = {");
|
|
66
|
+
for (const candidate of activeRenameCandidates) {
|
|
67
|
+
lines.push(`\t${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
|
|
68
|
+
}
|
|
69
|
+
for (const candidate of suggestedRenameCandidates) {
|
|
70
|
+
lines.push(`\t// ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
|
|
71
|
+
}
|
|
72
|
+
lines.push("};");
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("export const transforms: TransformMap = {");
|
|
75
|
+
for (const suggestion of diff.summary.transformSuggestions) {
|
|
76
|
+
lines.push(`\t// ${renderObjectKey(suggestion.currentPath)}: (legacyValue, legacyInput) => {`);
|
|
77
|
+
for (const bodyLine of suggestion.bodyLines) {
|
|
78
|
+
lines.push(`\t${bodyLine}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push(`\t// },`);
|
|
81
|
+
}
|
|
82
|
+
lines.push("};");
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("export const unresolved = [");
|
|
85
|
+
for (const item of diff.summary.manualItems) {
|
|
86
|
+
lines.push(`\t"${item.path}: ${item.kind}${item.detail ? ` (${escapeForCode(item.detail)})` : ""}",`);
|
|
87
|
+
}
|
|
88
|
+
for (const candidate of suggestedRenameCandidates) {
|
|
89
|
+
lines.push(`\t"${candidate.currentPath}: rename candidate from ${candidate.legacyPath}",`);
|
|
90
|
+
}
|
|
91
|
+
for (const suggestion of diff.summary.transformSuggestions) {
|
|
92
|
+
lines.push(`\t"${suggestion.currentPath}: transform suggested from ${suggestion.legacyPath ?? suggestion.currentPath}",`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("] as const;");
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push(`export function migrate(input: Record<string, unknown>): ${currentTypeName} {`);
|
|
97
|
+
lines.push(`\treturn {`);
|
|
98
|
+
for (const key of Object.keys(currentAttributes)) {
|
|
99
|
+
for (const manualItem of diff.summary.manualItems.filter((item) => item.path === key || item.path.startsWith(`${key}.`))) {
|
|
100
|
+
lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} ${manualItem.path}: ${manualItem.kind}${manualItem.detail ? ` (${manualItem.detail})` : ""}`);
|
|
101
|
+
}
|
|
102
|
+
for (const renameCandidate of suggestedRenameCandidates.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
|
|
103
|
+
lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} consider renameMap[${JSON.stringify(renameCandidate.currentPath)}] = "${renameCandidate.legacyPath}"`);
|
|
104
|
+
}
|
|
105
|
+
for (const suggestion of diff.summary.transformSuggestions.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
|
|
106
|
+
lines.push(`\t\t// ${MIGRATION_TODO_PREFIX} review transforms[${JSON.stringify(suggestion.currentPath)}]`);
|
|
107
|
+
}
|
|
108
|
+
lines.push(`\t\t${key}: resolveMigrationAttribute(currentManifest.attributes.${key}, "${key}", "${key}", input, renameMap, transforms),`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(`\t} as ${currentTypeName};`);
|
|
111
|
+
lines.push("}");
|
|
112
|
+
lines.push("");
|
|
113
|
+
return `${lines.join("\n")}\n`;
|
|
114
|
+
}
|
|
115
|
+
export function renderMigrationRegistryFile(state, entries) {
|
|
116
|
+
const imports = [`import currentManifest from "../../../${ROOT_MANIFEST}";`];
|
|
117
|
+
const body = [];
|
|
118
|
+
entries.forEach((entry, index) => {
|
|
119
|
+
imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
|
|
120
|
+
imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
|
|
121
|
+
body.push(`\t{`);
|
|
122
|
+
body.push(`\t\tfromVersion: "${entry.fromVersion}",`);
|
|
123
|
+
body.push(`\t\tmanifest: manifest_${index},`);
|
|
124
|
+
body.push(`\t\trule: rule_${index},`);
|
|
125
|
+
body.push(`\t},`);
|
|
126
|
+
});
|
|
127
|
+
return `${imports.join("\n")}
|
|
128
|
+
|
|
129
|
+
export const migrationRegistry = {
|
|
130
|
+
currentVersion: "${state.config.currentVersion}",
|
|
131
|
+
currentManifest,
|
|
132
|
+
entries: [
|
|
133
|
+
${body.join("\n")}
|
|
134
|
+
],
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
export default migrationRegistry;
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
export function renderGeneratedDeprecatedFile(entries) {
|
|
141
|
+
if (entries.length === 0) {
|
|
142
|
+
return `import type { BlockConfiguration } from "@wordpress/blocks";
|
|
143
|
+
|
|
144
|
+
export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [];
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
const imports = [`import type { BlockConfiguration } from "@wordpress/blocks";`];
|
|
148
|
+
const definitions = [];
|
|
149
|
+
const arrayEntries = [];
|
|
150
|
+
entries.forEach((entry, index) => {
|
|
151
|
+
imports.push(`import block_${index} from "${entry.blockJsonImport}";`);
|
|
152
|
+
imports.push(`import save_${index} from "${entry.saveImport}";`);
|
|
153
|
+
imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
|
|
154
|
+
definitions.push(`const deprecated_${index}: NonNullable<BlockConfiguration["deprecated"]>[number] = {`);
|
|
155
|
+
definitions.push(`\tattributes: (block_${index}.attributes ?? {}) as Record<string, unknown>,`);
|
|
156
|
+
definitions.push(`\tsave: save_${index} as BlockConfiguration["save"],`);
|
|
157
|
+
definitions.push(`\tmigrate(attributes: Record<string, unknown>) {`);
|
|
158
|
+
definitions.push(`\t\treturn rule_${index}.migrate(attributes);`);
|
|
159
|
+
definitions.push(`\t},`);
|
|
160
|
+
definitions.push(`};`);
|
|
161
|
+
arrayEntries.push(`deprecated_${index}`);
|
|
162
|
+
});
|
|
163
|
+
return `${imports.join("\n")}
|
|
164
|
+
|
|
165
|
+
${definitions.join("\n\n")}
|
|
166
|
+
|
|
167
|
+
export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [${arrayEntries.join(", ")}];
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
export function renderPhpMigrationRegistryFile(state, entries) {
|
|
171
|
+
const snapshots = Object.fromEntries(state.config.supportedVersions.map((version) => {
|
|
172
|
+
const snapshotRoot = path.join(state.projectDir, SNAPSHOT_DIR, version);
|
|
173
|
+
const manifestPath = path.join(snapshotRoot, ROOT_MANIFEST);
|
|
174
|
+
const blockJsonPath = path.join(snapshotRoot, ROOT_BLOCK_JSON);
|
|
175
|
+
const savePath = path.join(snapshotRoot, "save.tsx");
|
|
176
|
+
return [
|
|
177
|
+
version,
|
|
178
|
+
{
|
|
179
|
+
blockJson: fs.existsSync(blockJsonPath)
|
|
180
|
+
? {
|
|
181
|
+
attributeNames: Object.keys((readJson(blockJsonPath).attributes ?? {})),
|
|
182
|
+
name: readJson(blockJsonPath).name ?? null,
|
|
183
|
+
}
|
|
184
|
+
: null,
|
|
185
|
+
hasSaveSnapshot: fs.existsSync(savePath),
|
|
186
|
+
manifest: fs.existsSync(manifestPath)
|
|
187
|
+
? summarizeManifest(readJson(manifestPath))
|
|
188
|
+
: null,
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
}));
|
|
192
|
+
const edgeSummaries = entries.map((entry) => {
|
|
193
|
+
const ruleMetadata = readRuleMetadata(entry.rulePath);
|
|
194
|
+
const snapshotManifest = snapshots[entry.fromVersion]?.manifest ?? null;
|
|
195
|
+
return {
|
|
196
|
+
autoAppliedRenameCount: ruleMetadata.renameMap.length,
|
|
197
|
+
autoAppliedRenames: ruleMetadata.renameMap,
|
|
198
|
+
fromVersion: entry.fromVersion,
|
|
199
|
+
nestedPathRenames: ruleMetadata.renameMap.filter((item) => item.currentPath.includes(".")),
|
|
200
|
+
ruleFile: path.relative(state.projectDir, entry.rulePath).replace(/\\/g, "/"),
|
|
201
|
+
toVersion: entry.toVersion,
|
|
202
|
+
transformKeys: ruleMetadata.transforms,
|
|
203
|
+
unionBranches: snapshotManifest ? summarizeUnionBranches(snapshotManifest) : [],
|
|
204
|
+
unresolved: ruleMetadata.unresolved,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
return `<?php
|
|
208
|
+
declare(strict_types=1);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Generated from advanced migration snapshots. Do not edit manually.
|
|
212
|
+
*/
|
|
213
|
+
return ${renderPhpValue({
|
|
214
|
+
blockName: state.config.blockName,
|
|
215
|
+
currentManifest: summarizeManifest(state.currentManifest),
|
|
216
|
+
currentVersion: state.config.currentVersion,
|
|
217
|
+
edges: edgeSummaries,
|
|
218
|
+
legacyVersions: state.config.supportedVersions.filter((version) => version !== state.config.currentVersion),
|
|
219
|
+
snapshotDir: state.config.snapshotDir,
|
|
220
|
+
snapshots,
|
|
221
|
+
supportedVersions: state.config.supportedVersions,
|
|
222
|
+
}, 0)};
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
export function renderVerifyFile(state, entries) {
|
|
226
|
+
const imports = [
|
|
227
|
+
`import { validators } from "../../validators";`,
|
|
228
|
+
`import { deprecated } from "./deprecated";`,
|
|
229
|
+
];
|
|
230
|
+
const checks = [];
|
|
231
|
+
entries.forEach((entry, index) => {
|
|
232
|
+
imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
|
|
233
|
+
imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
|
|
234
|
+
checks.push(`\tif (selectedVersions.length === 0 || selectedVersions.includes("${entry.fromVersion}")) {`);
|
|
235
|
+
checks.push(`\t\tif (rule_${index}.unresolved.length > 0) {`);
|
|
236
|
+
checks.push(`\t\t\tthrow new Error("Unresolved migration TODOs remain for ${entry.fromVersion} -> ${entry.toVersion}: " + rule_${index}.unresolved.join(", "));`);
|
|
237
|
+
checks.push(`\t\t}`);
|
|
238
|
+
checks.push(`\t\tconst cases_${index} = Array.isArray(fixture_${index}.cases) ? fixture_${index}.cases : [];`);
|
|
239
|
+
checks.push(`\t\tfor (const fixtureCase of cases_${index}) {`);
|
|
240
|
+
checks.push(`\t\t\tconst migrated_${index} = rule_${index}.migrate(fixtureCase.input ?? {});`);
|
|
241
|
+
checks.push(`\t\t\tconst validation_${index} = validators.validate(migrated_${index});`);
|
|
242
|
+
checks.push(`\t\t\tif (!validation_${index}.success) {`);
|
|
243
|
+
checks.push(`\t\t\t\tthrow new Error("Current validator rejected migrated fixture for ${entry.fromVersion} case " + String(fixtureCase.name ?? "unknown") + ": " + JSON.stringify(validation_${index}.errors));`);
|
|
244
|
+
checks.push(`\t\t\t}`);
|
|
245
|
+
checks.push(`\t\t}`);
|
|
246
|
+
checks.push(`\t\tconsole.log("Verified ${entry.fromVersion} -> ${entry.toVersion} (" + cases_${index}.length + " case(s))");`);
|
|
247
|
+
checks.push(`\t}`);
|
|
248
|
+
});
|
|
249
|
+
return `${imports.join("\n")}
|
|
250
|
+
|
|
251
|
+
const args = process.argv.slice(2);
|
|
252
|
+
const selectedVersions =
|
|
253
|
+
args[0] === "--all"
|
|
254
|
+
? []
|
|
255
|
+
: args[0] === "--from" && args[1]
|
|
256
|
+
? [args[1]]
|
|
257
|
+
: [];
|
|
258
|
+
|
|
259
|
+
if (deprecated.length !== ${entries.length}) {
|
|
260
|
+
throw new Error("Generated deprecated entries are out of sync with migration registry.");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
${checks.join("\n")}
|
|
264
|
+
|
|
265
|
+
console.log("Migration verification passed for ${state.config.blockName}");
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|