@tailor-platform/sdk-codemod 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.
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
//#region codemods/v2/define-generators-to-plugins/scripts/transform.ts
|
|
3
|
+
/**
|
|
4
|
+
* Known plugin mappings from package name to plugin function/import.
|
|
5
|
+
*/
|
|
6
|
+
const PLUGIN_MAP = {
|
|
7
|
+
"@tailor-platform/kysely-type": {
|
|
8
|
+
functionName: "kyselyTypePlugin",
|
|
9
|
+
importPath: "@tailor-platform/sdk/plugin/kysely-type"
|
|
10
|
+
},
|
|
11
|
+
"@tailor-platform/seed": {
|
|
12
|
+
functionName: "seedPlugin",
|
|
13
|
+
importPath: "@tailor-platform/sdk/plugin/seed"
|
|
14
|
+
},
|
|
15
|
+
"@tailor-platform/enum-constants": {
|
|
16
|
+
functionName: "enumConstantsPlugin",
|
|
17
|
+
importPath: "@tailor-platform/sdk/plugin/enum-constants"
|
|
18
|
+
},
|
|
19
|
+
"@tailor-platform/file-utils": {
|
|
20
|
+
functionName: "fileUtilsPlugin",
|
|
21
|
+
importPath: "@tailor-platform/sdk/plugin/file-utils"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Transform defineGenerators() to definePlugins():
|
|
26
|
+
*
|
|
27
|
+
* 1. Rename `defineGenerators` → `definePlugins` in import and call
|
|
28
|
+
* 2. Transform tuple arguments `["pkg-name", config]` → `pluginFn(config)`
|
|
29
|
+
* 3. Add plugin imports from their respective SDK paths
|
|
30
|
+
* @param source - Source code to transform
|
|
31
|
+
* @returns Transformed source or null if no changes needed
|
|
32
|
+
*/
|
|
33
|
+
function transform(source) {
|
|
34
|
+
const tree = parse(Lang.TypeScript, source).root();
|
|
35
|
+
if (!source.includes("defineGenerators")) return null;
|
|
36
|
+
if (!source.includes("@tailor-platform/sdk")) return null;
|
|
37
|
+
const edits = [];
|
|
38
|
+
const importsToAdd = /* @__PURE__ */ new Map();
|
|
39
|
+
const callNodes = tree.findAll({ rule: { pattern: "defineGenerators($$$ARGS)" } });
|
|
40
|
+
let totalArgs = 0;
|
|
41
|
+
let migratedArgs = 0;
|
|
42
|
+
for (const callNode of callNodes) {
|
|
43
|
+
const args = callNode.getMultipleMatches("ARGS");
|
|
44
|
+
for (const arg of args) {
|
|
45
|
+
if (!arg.isNamed() || arg.kind() === "comment") continue;
|
|
46
|
+
totalArgs++;
|
|
47
|
+
if (arg.kind() === "array") {
|
|
48
|
+
const children = arg.children().filter((c) => c.isNamed() && c.kind() !== "comment");
|
|
49
|
+
if (children.length >= 1) {
|
|
50
|
+
const mapping = PLUGIN_MAP[children[0].text().replace(/^["']|["']$/g, "")];
|
|
51
|
+
if (mapping) {
|
|
52
|
+
migratedArgs++;
|
|
53
|
+
importsToAdd.set(mapping.importPath, mapping.functionName);
|
|
54
|
+
const configNodes = children.slice(1);
|
|
55
|
+
const configText = configNodes.length > 0 ? configNodes.map((c) => c.text()).join(", ") : "";
|
|
56
|
+
const replacement = `${mapping.functionName}(${configText})`;
|
|
57
|
+
edits.push(arg.replace(replacement));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (totalArgs > 0 && migratedArgs < totalArgs) return null;
|
|
64
|
+
const callIdentifiers = tree.findAll({ rule: {
|
|
65
|
+
pattern: "defineGenerators",
|
|
66
|
+
kind: "identifier",
|
|
67
|
+
inside: { kind: "call_expression" }
|
|
68
|
+
} });
|
|
69
|
+
for (const id of callIdentifiers) edits.push(id.replace("definePlugins"));
|
|
70
|
+
const sdkImportStatements = tree.findAll({ rule: {
|
|
71
|
+
kind: "import_statement",
|
|
72
|
+
has: {
|
|
73
|
+
kind: "string",
|
|
74
|
+
regex: "^[\"']@tailor-platform/sdk[\"']$"
|
|
75
|
+
}
|
|
76
|
+
} });
|
|
77
|
+
const hasDefinePlugins = sdkImportStatements.some((stmt) => stmt.findAll({ rule: { kind: "import_specifier" } }).some((s) => s.children().some((c) => c.kind() === "identifier" && c.text() === "definePlugins")));
|
|
78
|
+
for (const importStmt of sdkImportStatements) {
|
|
79
|
+
const specifiers = importStmt.findAll({ rule: { kind: "import_specifier" } });
|
|
80
|
+
for (const spec of specifiers) {
|
|
81
|
+
const identNode = spec.children().find((c) => c.kind() === "identifier" && c.text() === "defineGenerators");
|
|
82
|
+
if (!identNode) continue;
|
|
83
|
+
if (hasDefinePlugins) {
|
|
84
|
+
const specText = spec.text();
|
|
85
|
+
const importText = importStmt.text();
|
|
86
|
+
const idx = importText.indexOf(specText);
|
|
87
|
+
if (idx !== -1) {
|
|
88
|
+
const afterSpec = importText.slice(idx + specText.length);
|
|
89
|
+
const beforeSpec = importText.slice(0, idx);
|
|
90
|
+
let removeFrom = idx;
|
|
91
|
+
let removeTo = idx + specText.length;
|
|
92
|
+
if (afterSpec.match(/^\s*,/)) removeTo = idx + specText.length + (afterSpec.match(/^\s*,\s*/)[0]?.length ?? 0);
|
|
93
|
+
else if (beforeSpec.match(/,\s*$/)) removeFrom = idx - (beforeSpec.match(/,\s*$/)[0]?.length ?? 0);
|
|
94
|
+
const cleaned = importText.slice(0, removeFrom) + importText.slice(removeTo);
|
|
95
|
+
edits.push(importStmt.replace(cleaned));
|
|
96
|
+
}
|
|
97
|
+
} else edits.push(identNode.replace("definePlugins"));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (edits.length === 0) return null;
|
|
101
|
+
let result = tree.commitEdits(edits);
|
|
102
|
+
if (importsToAdd.size > 0) {
|
|
103
|
+
const importLines = [];
|
|
104
|
+
for (const [importPath, functionName] of importsToAdd) {
|
|
105
|
+
const line = `import { ${functionName} } from "${importPath}";`;
|
|
106
|
+
if (new RegExp(`import\\s*\\{[^}]*\\b${functionName}\\b[^}]*\\}`, "m").test(result)) continue;
|
|
107
|
+
importLines.push(line);
|
|
108
|
+
}
|
|
109
|
+
importLines.sort();
|
|
110
|
+
if (importLines.length === 0) return result;
|
|
111
|
+
const match = /^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$/m.exec(result);
|
|
112
|
+
if (match) {
|
|
113
|
+
const insertPos = (match.index ?? 0) + match[0].length;
|
|
114
|
+
result = result.slice(0, insertPos) + "\n" + importLines.join("\n") + result.slice(insertPos);
|
|
115
|
+
} else result = importLines.join("\n") + "\n" + result;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { transform as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as url from "node:url";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as path from "pathe";
|
|
5
|
+
import { arg, defineCommand, runMain } from "politty";
|
|
6
|
+
import { readPackageJSON } from "pkg-types";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { gte, lt, valid } from "semver";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import { glob } from "node:fs/promises";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { structuredPatch } from "diff";
|
|
13
|
+
import picomatch from "picomatch";
|
|
14
|
+
//#region src/registry.ts
|
|
15
|
+
const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods");
|
|
16
|
+
const allCodemods = [{
|
|
17
|
+
id: "v2/define-generators-to-plugins",
|
|
18
|
+
name: "defineGenerators → definePlugins",
|
|
19
|
+
description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports",
|
|
20
|
+
since: "1.0.0",
|
|
21
|
+
until: "2.0.0",
|
|
22
|
+
scriptPath: "v2/define-generators-to-plugins/scripts/transform.js"
|
|
23
|
+
}];
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the absolute path to a codemod script.
|
|
26
|
+
* @param scriptPath - Relative path from the codemods root
|
|
27
|
+
* @returns Absolute path to the script file
|
|
28
|
+
*/
|
|
29
|
+
function resolveCodemodScript(scriptPath) {
|
|
30
|
+
return path.resolve(CODEMODS_ROOT, scriptPath);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get codemod packages applicable for a version range.
|
|
34
|
+
* A codemod applies when: since <= fromVersion < until <= toVersion
|
|
35
|
+
* @param fromVersion - Current SDK version (semver)
|
|
36
|
+
* @param toVersion - Target SDK version (semver)
|
|
37
|
+
* @returns Array of applicable codemod packages in registration order
|
|
38
|
+
*/
|
|
39
|
+
function getApplicableCodemods(fromVersion, toVersion) {
|
|
40
|
+
if (!valid(fromVersion)) throw new Error(`Invalid fromVersion: ${fromVersion}`);
|
|
41
|
+
if (!valid(toVersion)) throw new Error(`Invalid toVersion: ${toVersion}`);
|
|
42
|
+
return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, codemod.until) && gte(toVersion, codemod.until));
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/runner.ts
|
|
46
|
+
/** Default file patterns for TypeScript files. */
|
|
47
|
+
const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"];
|
|
48
|
+
/** Directory names always excluded from file scanning. */
|
|
49
|
+
const EXCLUDE_DIRS = new Set([
|
|
50
|
+
"node_modules",
|
|
51
|
+
"dist",
|
|
52
|
+
".git"
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Print a colorized unified diff for a single file to stderr.
|
|
56
|
+
* @param filePath - Absolute path to the file
|
|
57
|
+
* @param before - Original content
|
|
58
|
+
* @param after - Transformed content
|
|
59
|
+
*/
|
|
60
|
+
function printDiff(filePath, before, after) {
|
|
61
|
+
const patch = structuredPatch(filePath, filePath, before, after, "", "", { context: 3 });
|
|
62
|
+
if (patch.hunks.length === 0) return;
|
|
63
|
+
process.stderr.write(`\n${chalk.bold(`--- ${filePath}`)}\n`);
|
|
64
|
+
process.stderr.write(`${chalk.bold(`+++ ${filePath}`)}\n`);
|
|
65
|
+
for (const hunk of patch.hunks) {
|
|
66
|
+
process.stderr.write(chalk.cyan(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n`));
|
|
67
|
+
for (const line of hunk.lines) if (line.startsWith("+")) process.stderr.write(`${chalk.green(line)}\n`);
|
|
68
|
+
else if (line.startsWith("-")) process.stderr.write(`${chalk.red(line)}\n`);
|
|
69
|
+
else process.stderr.write(`${line}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Load a transform module from a TypeScript file path.
|
|
74
|
+
* Expects the module to have a default export that is a TransformFn.
|
|
75
|
+
* @param scriptPath - Absolute path to the transform script
|
|
76
|
+
* @returns The transform function
|
|
77
|
+
*/
|
|
78
|
+
async function loadTransform(scriptPath) {
|
|
79
|
+
const mod = await import(url.pathToFileURL(scriptPath).href);
|
|
80
|
+
if (typeof mod.default !== "function") throw new Error(`Transform at ${scriptPath} does not have a default export function`);
|
|
81
|
+
return mod.default;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Run multiple codemods on a project directory using in-memory chaining.
|
|
85
|
+
* Each file is processed through all transforms whose filePatterns match it.
|
|
86
|
+
* Later transforms see earlier transforms' output — even in dry-run mode.
|
|
87
|
+
*
|
|
88
|
+
* In dry-run mode, colorized diffs are printed to stderr.
|
|
89
|
+
* @param codemods - Codemod packages to run (with resolved script paths)
|
|
90
|
+
* @param targetPath - Project directory to transform
|
|
91
|
+
* @param dryRun - Whether to preview changes without writing
|
|
92
|
+
* @returns Combined result of all codemod executions
|
|
93
|
+
*/
|
|
94
|
+
async function runCodemods(codemods, targetPath, dryRun) {
|
|
95
|
+
const loaded = [];
|
|
96
|
+
for (const { codemod, scriptPath } of codemods) {
|
|
97
|
+
const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS;
|
|
98
|
+
loaded.push({
|
|
99
|
+
id: codemod.id,
|
|
100
|
+
transform: await loadTransform(scriptPath),
|
|
101
|
+
matches: picomatch(patterns)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const allPatterns = /* @__PURE__ */ new Set();
|
|
105
|
+
for (const { codemod } of codemods) for (const p of codemod.filePatterns ?? DEFAULT_FILE_PATTERNS) allPatterns.add(p);
|
|
106
|
+
const filesModified = [];
|
|
107
|
+
const warnings = [];
|
|
108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const pattern of allPatterns) for await (const relative of glob(pattern, {
|
|
110
|
+
cwd: targetPath,
|
|
111
|
+
exclude: (name) => EXCLUDE_DIRS.has(name)
|
|
112
|
+
})) {
|
|
113
|
+
const absolute = path.resolve(targetPath, relative);
|
|
114
|
+
if (seen.has(absolute)) continue;
|
|
115
|
+
seen.add(absolute);
|
|
116
|
+
let original;
|
|
117
|
+
try {
|
|
118
|
+
original = await fs.promises.readFile(absolute, "utf-8");
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let current = original;
|
|
123
|
+
const matchedRules = [];
|
|
124
|
+
for (const { id, transform, matches } of loaded) {
|
|
125
|
+
if (!matches(relative)) continue;
|
|
126
|
+
matchedRules.push(id);
|
|
127
|
+
const result = await transform(current, absolute);
|
|
128
|
+
if (result != null) current = result;
|
|
129
|
+
}
|
|
130
|
+
if (current !== original) {
|
|
131
|
+
filesModified.push(absolute);
|
|
132
|
+
if (dryRun) printDiff(absolute, original, current);
|
|
133
|
+
else await fs.promises.writeFile(absolute, current, "utf-8");
|
|
134
|
+
} else if (matchedRules.length > 0 && original.includes("defineGenerators")) warnings.push(`${relative}: contains defineGenerators but was not migrated automatically (matched rules: ${matchedRules.join(", ")}). Manual migration may be needed.`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
changed: filesModified.length > 0,
|
|
138
|
+
filesModified,
|
|
139
|
+
warnings
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/index.ts
|
|
144
|
+
const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/..");
|
|
145
|
+
runMain(defineCommand({
|
|
146
|
+
name: packageJson.name ?? "sdk-codemod",
|
|
147
|
+
description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
|
|
148
|
+
args: z.object({
|
|
149
|
+
from: arg(z.string(), { description: "Source SDK version (the version before upgrade)" }),
|
|
150
|
+
to: arg(z.string(), { description: "Target SDK version (the version after upgrade)" }),
|
|
151
|
+
target: arg(z.string().default("."), { description: "Project directory to transform" }),
|
|
152
|
+
"dry-run": arg(z.boolean().default(false), {
|
|
153
|
+
alias: "d",
|
|
154
|
+
description: "Preview changes without modifying files"
|
|
155
|
+
})
|
|
156
|
+
}).strict(),
|
|
157
|
+
run: async (args) => {
|
|
158
|
+
const targetPath = path.resolve(args.target);
|
|
159
|
+
const dryRun = args["dry-run"];
|
|
160
|
+
const codemods = getApplicableCodemods(args.from, args.to);
|
|
161
|
+
const output = {
|
|
162
|
+
codemodsApplied: 0,
|
|
163
|
+
codemodsSkipped: 0,
|
|
164
|
+
filesModified: [],
|
|
165
|
+
warnings: [],
|
|
166
|
+
errors: []
|
|
167
|
+
};
|
|
168
|
+
if (codemods.length === 0) {
|
|
169
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const codemodEntries = codemods.map((codemod) => ({
|
|
173
|
+
codemod,
|
|
174
|
+
scriptPath: resolveCodemodScript(codemod.scriptPath)
|
|
175
|
+
}));
|
|
176
|
+
for (const { codemod } of codemodEntries) process.stderr.write(`Running: ${codemod.name} - ${codemod.description}\n`);
|
|
177
|
+
try {
|
|
178
|
+
const result = await runCodemods(codemodEntries, targetPath, dryRun);
|
|
179
|
+
output.codemodsApplied = result.changed ? codemods.length : 0;
|
|
180
|
+
output.codemodsSkipped = result.changed ? 0 : codemods.length;
|
|
181
|
+
output.filesModified = result.filesModified;
|
|
182
|
+
output.warnings = result.warnings;
|
|
183
|
+
if (result.changed) process.stderr.write(` ${result.filesModified.length} file(s) modified\n`);
|
|
184
|
+
else process.stderr.write(" No changes needed\n");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
output.errors.push({
|
|
188
|
+
codemodId: "pipeline",
|
|
189
|
+
message
|
|
190
|
+
});
|
|
191
|
+
process.stderr.write(` Failed: ${message}\n`);
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
194
|
+
if (output.errors.length > 0) process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}), { version: packageJson.version });
|
|
197
|
+
//#endregion
|
|
198
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tailor-platform/sdk-codemod",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codemod runner for Tailor Platform SDK upgrades",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tailor-platform/sdk.git",
|
|
9
|
+
"directory": "packages/sdk-codemod"
|
|
10
|
+
},
|
|
11
|
+
"bin": "dist/index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"lint": "oxlint . && eslint --cache .",
|
|
22
|
+
"lint:fix": "oxlint --fix . && eslint --cache --fix .",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"prepublish": "pnpm run build",
|
|
26
|
+
"publint": "publint --strict"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@ast-grep/napi": "^0.42.0",
|
|
30
|
+
"chalk": "5.6.2",
|
|
31
|
+
"diff": "8.0.4",
|
|
32
|
+
"pathe": "2.0.3",
|
|
33
|
+
"picomatch": "4.0.4",
|
|
34
|
+
"pkg-types": "2.3.0",
|
|
35
|
+
"politty": "0.4.12",
|
|
36
|
+
"semver": "7.7.4",
|
|
37
|
+
"zod": "4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "10.0.1",
|
|
41
|
+
"@types/node": "24.12.0",
|
|
42
|
+
"@types/picomatch": "4.0.2",
|
|
43
|
+
"@types/semver": "7.7.1",
|
|
44
|
+
"eslint": "10.1.0",
|
|
45
|
+
"eslint-plugin-oxlint": "1.57.0",
|
|
46
|
+
"oxlint": "1.57.0",
|
|
47
|
+
"tsdown": "0.21.7",
|
|
48
|
+
"typescript": "5.9.3",
|
|
49
|
+
"typescript-eslint": "8.57.2",
|
|
50
|
+
"vitest": "4.1.2"
|
|
51
|
+
}
|
|
52
|
+
}
|