-
Notifications
You must be signed in to change notification settings - Fork 3
feat(cli): add upgrade command with @tailor-platform/sdk-codemod package #893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
6b7ca53
feat(cli): add upgrade command with codemod.com-based architecture
toiroakr 77d102c
test(cli): add tests, docs, and changeset for upgrade command
toiroakr 4859156
feat(cli): add dry-run diff preview to upgrade command
toiroakr 24d052a
refactor(cli): use styles instead of chalk for color detection in upg…
toiroakr de9972e
refactor(cli): make --to optional and remove --interactive from upgra…
toiroakr 2ede2aa
fix(cli): include codemods in package and resolve scripts via SDK pac…
toiroakr d298433
fix(cli): export package.json for codemod script resolution
toiroakr 95e7e13
fix(cli): detect modified files by comparing snapshots in non-dry-run…
toiroakr 7808215
refactor(cli): extract @tailor-platform/sdk-codemod package from upgr…
toiroakr 2cd6940
chore: update changeset for sdk-codemod package
toiroakr e20f951
fix(ci): add vitest and eslint config for sdk-codemod package
toiroakr 8e7f8bb
refactor(sdk-codemod): switch from codemod CLI to @ast-grep/napi for …
toiroakr ca8fe17
feat(sdk-codemod): use diff library for unified diff and print colori…
toiroakr b44069c
refactor(sdk-codemod): use chalk instead of picocolors for consistenc…
toiroakr 87dfdff
fix(ci): update sdk-codemod devDeps to match main and regenerate lock…
toiroakr 6c8f546
fix(sdk-codemod): fix readPackageJSON type error
toiroakr 03ac95a
feat(sdk-codemod): support per-codemod filePatterns for targeted file…
toiroakr e66d785
refactor(sdk-codemod): unify TransformFn to (source, filePath) signat…
toiroakr bca11c9
test(sdk-codemod): add chained transform dry-run and filePatterns tests
toiroakr 6d7d3ed
fix(sdk-codemod): use @ast-grep/napi directly in transform instead of…
toiroakr 583b898
fix(sdk-codemod): set defineGenerators codemod since to 1.12.0 when d…
toiroakr 37eee14
fix(sdk-codemod): revert since to 1.0.0 for defineGenerators codemod
toiroakr c15a8c7
fix: pre-compile codemod transforms to JS, use latest version, and fi…
dqn 9e395d8
fix(sdk-codemod): skip transform when args are not fully migratable, …
dqn 48ec19e
fix(sdk-codemod): use fileURLToPath for package.json, drop variable r…
dqn 9037e9e
fix(sdk-codemod): scope transform to SDK imports, prevent duplicates,…
dqn b33eb86
fix(sdk-codemod): support mixed configs, warn on unmigrated files, su…
dqn 2f5ae1f
fix(sdk-codemod): skip duplicate plugin imports in mixed config migra…
dqn 91acfad
fix(sdk-codemod): check function name for import dedup, respect since…
dqn b951ea0
fix(sdk-codemod): check definePlugins across all SDK imports, set npx…
dqn 7edd6eb
fix(sdk-codemod): forward captured stderr to terminal in upgrade succ…
dqn a9c1c09
fix(sdk-codemod): anchor AST regex to match only main SDK import path
dqn 16b2dc8
fix(sdk-codemod): replace walkDir+picomatch with fs.glob for file sca…
dqn 2ddfbaa
fix(sdk-codemod): include matched rule IDs in unmodified-file warnings
dqn 519f011
chore(sdk-codemod): set initial version to 0.1.0 for first publish
toiroakr 64512a4
chore: set sdk-codemod changeset to patch for pre-1.0
toiroakr 55d39d3
docs: update CLAUDE.md and upgrade.md for definePlugins pattern
toiroakr a4271b9
refactor(sdk-codemod): move legacy pattern detection from runner to C…
toiroakr 38ddb51
fix(sdk-codemod): align devDependencies with main SDK versions to fix…
toiroakr c132bd5
fix(sdk-codemod): update politty to 0.4.13 to match SDK and simplify …
toiroakr 4941b02
fix(sdk-codemod): track per-codemod applied status instead of all-or-…
toiroakr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@tailor-platform/sdk": minor | ||
| "@tailor-platform/sdk-codemod": patch | ||
| --- | ||
|
|
||
| Add `upgrade` command with codemod.com-based architecture for automated SDK version migrations. Codemod execution is handled by the new `@tailor-platform/sdk-codemod` package. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
packages/sdk-codemod/codemods/v2/define-generators-to-plugins/codemod.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| name: "@tailor-platform/define-generators-to-plugins" | ||
| version: "1.0.0" | ||
| description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports" | ||
| engine: jssg | ||
| language: typescript | ||
| since: "1.0.0" | ||
| until: "2.0.0" |
217 changes: 217 additions & 0 deletions
217
packages/sdk-codemod/codemods/v2/define-generators-to-plugins/scripts/transform.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| import { parse, Lang } from "@ast-grep/napi"; | ||
| import type { Edit, SgNode } from "@ast-grep/napi"; | ||
|
|
||
| /** | ||
| * Known plugin mappings from package name to plugin function/import. | ||
| */ | ||
| const PLUGIN_MAP: Record<string, { functionName: string; importPath: string }> = { | ||
| "@tailor-platform/kysely-type": { | ||
| functionName: "kyselyTypePlugin", | ||
| importPath: "@tailor-platform/sdk/plugin/kysely-type", | ||
| }, | ||
| "@tailor-platform/seed": { | ||
| functionName: "seedPlugin", | ||
| importPath: "@tailor-platform/sdk/plugin/seed", | ||
| }, | ||
| "@tailor-platform/enum-constants": { | ||
| functionName: "enumConstantsPlugin", | ||
| importPath: "@tailor-platform/sdk/plugin/enum-constants", | ||
| }, | ||
| "@tailor-platform/file-utils": { | ||
| functionName: "fileUtilsPlugin", | ||
| importPath: "@tailor-platform/sdk/plugin/file-utils", | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Transform defineGenerators() to definePlugins(): | ||
| * | ||
| * 1. Rename `defineGenerators` → `definePlugins` in import and call | ||
| * 2. Transform tuple arguments `["pkg-name", config]` → `pluginFn(config)` | ||
| * 3. Add plugin imports from their respective SDK paths | ||
| * @param source - Source code to transform | ||
| * @returns Transformed source or null if no changes needed | ||
| */ | ||
| export default function transform(source: string): string | null { | ||
| const tree = parse(Lang.TypeScript, source).root(); | ||
|
|
||
| // Only process files that import defineGenerators from the SDK. | ||
| // This prevents modifying unrelated files that happen to contain the identifier. | ||
| if (!source.includes("defineGenerators")) { | ||
| return null; | ||
| } | ||
| if (!source.includes("@tailor-platform/sdk")) { | ||
| return null; | ||
| } | ||
|
|
||
| const edits: Edit[] = []; | ||
| const importsToAdd: Map<string, string> = new Map(); // importPath -> functionName | ||
|
|
||
| // Step 1: Find and transform defineGenerators call arguments (tuples → plugin calls) | ||
| const callNodes = tree.findAll({ | ||
| rule: { | ||
| pattern: "defineGenerators($$$ARGS)", | ||
| }, | ||
| }); | ||
|
|
||
| let totalArgs = 0; | ||
| let migratedArgs = 0; | ||
|
|
||
| for (const callNode of callNodes) { | ||
| // Find array/tuple arguments inside the call | ||
| const args = callNode.getMultipleMatches("ARGS"); | ||
| for (const arg of args) { | ||
| if (!arg.isNamed() || arg.kind() === "comment") continue; | ||
| totalArgs++; | ||
|
|
||
| // Match tuple pattern: ["package-name", config] | ||
| if (arg.kind() === "array") { | ||
| const children = arg | ||
| .children() | ||
| .filter((c: SgNode) => c.isNamed() && c.kind() !== "comment"); | ||
| if (children.length >= 1) { | ||
| const packageNameNode = children[0]!; | ||
| const packageName = packageNameNode.text().replace(/^["']|["']$/g, ""); | ||
| const mapping = PLUGIN_MAP[packageName]; | ||
|
|
||
| if (mapping) { | ||
| migratedArgs++; | ||
| importsToAdd.set(mapping.importPath, mapping.functionName); | ||
| // Build replacement: pluginFn(config) or pluginFn() if no config | ||
| const configNodes = children.slice(1); | ||
| const configText = | ||
| configNodes.length > 0 ? configNodes.map((c: SgNode) => c.text()).join(", ") : ""; | ||
| const replacement = `${mapping.functionName}(${configText})`; | ||
| edits.push(arg.replace(replacement)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // If any arguments could not be migrated, skip the entire transform to avoid | ||
| // producing invalid code (e.g. mixing tuple syntax with plugin calls). | ||
| if (totalArgs > 0 && migratedArgs < totalArgs) { | ||
| return null; | ||
| } | ||
|
|
||
| // Step 2: Rename defineGenerators → definePlugins in call expressions only. | ||
| // Import specifiers are handled separately in step 3 to avoid duplicates. | ||
| const callIdentifiers = tree.findAll({ | ||
| rule: { | ||
| pattern: "defineGenerators", | ||
| kind: "identifier", | ||
| inside: { | ||
| kind: "call_expression", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| for (const id of callIdentifiers) { | ||
| edits.push(id.replace("definePlugins")); | ||
| } | ||
|
|
||
| // Step 3: Handle import specifier for defineGenerators. | ||
| // If the import already contains definePlugins (mixed config), remove the | ||
| // defineGenerators specifier instead of renaming it to avoid duplicates. | ||
| const sdkImportStatements = tree.findAll({ | ||
| rule: { | ||
| kind: "import_statement", | ||
| has: { | ||
| kind: "string", | ||
| regex: "^[\"']@tailor-platform/sdk[\"']$", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Check across ALL SDK import statements whether definePlugins is already | ||
| // imported (may be in a different statement than defineGenerators). | ||
| const hasDefinePlugins = sdkImportStatements.some((stmt) => | ||
| stmt | ||
| .findAll({ rule: { kind: "import_specifier" } }) | ||
| .some((s) => | ||
| s.children().some((c: SgNode) => c.kind() === "identifier" && c.text() === "definePlugins"), | ||
| ), | ||
| ); | ||
|
|
||
| for (const importStmt of sdkImportStatements) { | ||
| const specifiers = importStmt.findAll({ | ||
| rule: { kind: "import_specifier" }, | ||
| }); | ||
|
|
||
| for (const spec of specifiers) { | ||
| const identNode = spec | ||
| .children() | ||
| .find((c: SgNode) => c.kind() === "identifier" && c.text() === "defineGenerators"); | ||
| if (!identNode) continue; | ||
|
|
||
| if (hasDefinePlugins) { | ||
| // Remove the entire specifier (including trailing/leading comma+whitespace) | ||
| // by replacing the specifier text + any adjacent comma | ||
| const specText = spec.text(); | ||
| const importText = importStmt.text(); | ||
| const idx = importText.indexOf(specText); | ||
| if (idx !== -1) { | ||
| // Check for trailing comma+whitespace or leading comma+whitespace | ||
| const afterSpec = importText.slice(idx + specText.length); | ||
| const beforeSpec = importText.slice(0, idx); | ||
| let removeFrom = idx; | ||
| let removeTo = idx + specText.length; | ||
|
|
||
| if (afterSpec.match(/^\s*,/)) { | ||
| removeTo = idx + specText.length + (afterSpec.match(/^\s*,\s*/)![0]?.length ?? 0); | ||
| } else if (beforeSpec.match(/,\s*$/)) { | ||
| removeFrom = idx - (beforeSpec.match(/,\s*$/)![0]?.length ?? 0); | ||
| } | ||
|
|
||
| const cleaned = importText.slice(0, removeFrom) + importText.slice(removeTo); | ||
| edits.push(importStmt.replace(cleaned)); | ||
| } | ||
| } else { | ||
| edits.push(identNode.replace("definePlugins")); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (edits.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| // Apply all edits | ||
| let result = tree.commitEdits(edits); | ||
|
|
||
| // Step 4: Add new import statements for plugin functions (skip already-present ones) | ||
| if (importsToAdd.size > 0) { | ||
| const importLines: string[] = []; | ||
| for (const [importPath, functionName] of importsToAdd) { | ||
| const line = `import { ${functionName} } from "${importPath}";`; | ||
| // Skip if this function is already imported (mixed config scenario). | ||
| // Use a targeted regex to match import statements containing the function | ||
| // name, rather than checking the import path which could match unrelated | ||
| // imports from the same module (e.g. importing a different symbol). | ||
| const importExists = new RegExp(`import\\s*\\{[^}]*\\b${functionName}\\b[^}]*\\}`, "m").test( | ||
| result, | ||
| ); | ||
| if (importExists) continue; | ||
| importLines.push(line); | ||
| } | ||
| // Sort for deterministic output and skip if all imports already present | ||
| importLines.sort(); | ||
| if (importLines.length === 0) { | ||
| return result; | ||
| } | ||
|
|
||
| // Find insertion point: after the @tailor-platform/sdk import line | ||
| const sdkImportRegex = /^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$/m; | ||
| const match = sdkImportRegex.exec(result); | ||
| if (match) { | ||
| const insertPos = (match.index ?? 0) + match[0].length; | ||
| result = result.slice(0, insertPos) + "\n" + importLines.join("\n") + result.slice(insertPos); | ||
| } else { | ||
| // Fallback: prepend imports at the top of the file | ||
| result = importLines.join("\n") + "\n" + result; | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
15 changes: 15 additions & 0 deletions
15
packages/sdk-codemod/codemods/v2/define-generators-to-plugins/tests/basic/expected.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import * as path from "node:path"; | ||
| import * as url from "node:url"; | ||
| import { definePlugins } from "@tailor-platform/sdk"; | ||
| import { enumConstantsPlugin } from "@tailor-platform/sdk/plugin/enum-constants"; | ||
| import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type"; | ||
| import config from "./tailor.config"; | ||
|
|
||
| const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); | ||
| const outDir = path.join(__dirname, "generators-compat-out"); | ||
|
|
||
| export default config; | ||
| export const generators = definePlugins( | ||
| kyselyTypePlugin({ distPath: path.join(outDir, "db.ts") }), | ||
| enumConstantsPlugin({ distPath: path.join(outDir, "enums.ts") }), | ||
| ); |
13 changes: 13 additions & 0 deletions
13
packages/sdk-codemod/codemods/v2/define-generators-to-plugins/tests/basic/input.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import * as path from "node:path"; | ||
| import * as url from "node:url"; | ||
| import { defineGenerators } from "@tailor-platform/sdk"; | ||
| import config from "./tailor.config"; | ||
|
|
||
| const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); | ||
| const outDir = path.join(__dirname, "generators-compat-out"); | ||
|
|
||
| export default config; | ||
| export const generators = defineGenerators( | ||
| ["@tailor-platform/kysely-type", { distPath: path.join(outDir, "db.ts") }], | ||
| ["@tailor-platform/enum-constants", { distPath: path.join(outDir, "enums.ts") }], | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import eslint from "@eslint/js"; | ||
| import { defineConfig, globalIgnores } from "eslint/config"; | ||
| import oxlint from "eslint-plugin-oxlint"; | ||
| import tseslint from "typescript-eslint"; | ||
|
|
||
| export default defineConfig([ | ||
| globalIgnores(["dist/", "codemods/"]), | ||
| eslint.configs.recommended, | ||
| tseslint.configs.recommended, | ||
| ...oxlint.configs["flat/recommended"], | ||
| ]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| { | ||
| "name": "@tailor-platform/sdk-codemod", | ||
| "version": "0.1.0", | ||
| "description": "Codemod runner for Tailor Platform SDK upgrades", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/tailor-platform/sdk.git", | ||
| "directory": "packages/sdk-codemod" | ||
| }, | ||
| "bin": "dist/index.js", | ||
| "files": [ | ||
| "CHANGELOG.md", | ||
| "dist", | ||
| "LICENSE", | ||
| "README.md" | ||
| ], | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "tsdown", | ||
| "lint": "oxlint . && eslint --cache .", | ||
| "lint:fix": "oxlint --fix . && eslint --cache --fix .", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest", | ||
| "prepublish": "pnpm run build", | ||
| "publint": "publint --strict" | ||
| }, | ||
| "dependencies": { | ||
| "@ast-grep/napi": "^0.42.0", | ||
| "chalk": "5.6.2", | ||
| "diff": "8.0.4", | ||
| "pathe": "2.0.3", | ||
| "picomatch": "4.0.4", | ||
| "pkg-types": "2.3.0", | ||
| "politty": "0.4.13", | ||
| "semver": "7.7.4", | ||
| "zod": "4.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "10.0.1", | ||
| "@types/node": "24.12.2", | ||
| "@types/picomatch": "4.0.2", | ||
| "@types/semver": "7.7.1", | ||
| "eslint": "10.2.0", | ||
| "eslint-plugin-oxlint": "1.58.0", | ||
| "oxlint": "1.58.0", | ||
| "tsdown": "0.21.7", | ||
| "typescript": "5.9.3", | ||
| "typescript-eslint": "8.58.0", | ||
| "vitest": "4.1.2" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { parse, Lang } from "@ast-grep/napi"; | ||
| import type { SgRoot } from "@ast-grep/napi"; | ||
|
|
||
| /** | ||
| * Parse TypeScript source code into an ast-grep root. | ||
| * @param source - TypeScript source code | ||
| * @returns Parsed AST root | ||
| */ | ||
| export function parseTS(source: string): SgRoot { | ||
| return parse(Lang.TypeScript, source); | ||
| } | ||
|
|
||
| /** | ||
| * Parse TSX source code into an ast-grep root. | ||
| * @param source - TSX source code | ||
| * @returns Parsed AST root | ||
| */ | ||
| export function parseTSX(source: string): SgRoot { | ||
| return parse(Lang.Tsx, source); | ||
| } | ||
|
|
||
| export type { SgRoot, SgNode, Edit } from "@ast-grep/napi"; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 SDK import regex fails to match multi-line imports, causing plugin imports to be prepended at file top
The
sdkImportRegexat line 205 uses^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$with the/mflag. Since.*does not match newlines, this regex cannot match multi-line SDK imports such as:When the SDK import spans multiple lines, the regex returns no match and the fallback at line 212 prepends the new plugin imports at the very top of the file — before all other imports. This produces valid but poorly ordered output (e.g.,
import { kyselyTypePlugin } ...appearing aboveimport * as path from "node:path").Example of incorrect output for multi-line import
Input:
Actual output (plugin import prepended at top):
Expected output (plugin import after SDK import):
Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.