Skip to content
Closed
Show file tree
Hide file tree
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 Apr 2, 2026
77d102c
test(cli): add tests, docs, and changeset for upgrade command
toiroakr Apr 2, 2026
4859156
feat(cli): add dry-run diff preview to upgrade command
toiroakr Apr 2, 2026
24d052a
refactor(cli): use styles instead of chalk for color detection in upg…
toiroakr Apr 2, 2026
de9972e
refactor(cli): make --to optional and remove --interactive from upgra…
toiroakr Apr 2, 2026
2ede2aa
fix(cli): include codemods in package and resolve scripts via SDK pac…
toiroakr Apr 2, 2026
d298433
fix(cli): export package.json for codemod script resolution
toiroakr Apr 2, 2026
95e7e13
fix(cli): detect modified files by comparing snapshots in non-dry-run…
toiroakr Apr 2, 2026
7808215
refactor(cli): extract @tailor-platform/sdk-codemod package from upgr…
toiroakr Apr 2, 2026
2cd6940
chore: update changeset for sdk-codemod package
toiroakr Apr 2, 2026
e20f951
fix(ci): add vitest and eslint config for sdk-codemod package
toiroakr Apr 2, 2026
8e7f8bb
refactor(sdk-codemod): switch from codemod CLI to @ast-grep/napi for …
toiroakr Apr 3, 2026
ca8fe17
feat(sdk-codemod): use diff library for unified diff and print colori…
toiroakr Apr 3, 2026
b44069c
refactor(sdk-codemod): use chalk instead of picocolors for consistenc…
toiroakr Apr 3, 2026
87dfdff
fix(ci): update sdk-codemod devDeps to match main and regenerate lock…
toiroakr Apr 3, 2026
6c8f546
fix(sdk-codemod): fix readPackageJSON type error
toiroakr Apr 3, 2026
03ac95a
feat(sdk-codemod): support per-codemod filePatterns for targeted file…
toiroakr Apr 3, 2026
e66d785
refactor(sdk-codemod): unify TransformFn to (source, filePath) signat…
toiroakr Apr 3, 2026
bca11c9
test(sdk-codemod): add chained transform dry-run and filePatterns tests
toiroakr Apr 3, 2026
6d7d3ed
fix(sdk-codemod): use @ast-grep/napi directly in transform instead of…
toiroakr Apr 3, 2026
583b898
fix(sdk-codemod): set defineGenerators codemod since to 1.12.0 when d…
toiroakr Apr 3, 2026
37eee14
fix(sdk-codemod): revert since to 1.0.0 for defineGenerators codemod
toiroakr Apr 3, 2026
c15a8c7
fix: pre-compile codemod transforms to JS, use latest version, and fi…
dqn Apr 4, 2026
9e395d8
fix(sdk-codemod): skip transform when args are not fully migratable, …
dqn Apr 4, 2026
48ec19e
fix(sdk-codemod): use fileURLToPath for package.json, drop variable r…
dqn Apr 4, 2026
9037e9e
fix(sdk-codemod): scope transform to SDK imports, prevent duplicates,…
dqn Apr 4, 2026
b33eb86
fix(sdk-codemod): support mixed configs, warn on unmigrated files, su…
dqn Apr 4, 2026
2f5ae1f
fix(sdk-codemod): skip duplicate plugin imports in mixed config migra…
dqn Apr 4, 2026
91acfad
fix(sdk-codemod): check function name for import dedup, respect since…
dqn Apr 4, 2026
b951ea0
fix(sdk-codemod): check definePlugins across all SDK imports, set npx…
dqn Apr 4, 2026
7edd6eb
fix(sdk-codemod): forward captured stderr to terminal in upgrade succ…
dqn Apr 4, 2026
a9c1c09
fix(sdk-codemod): anchor AST regex to match only main SDK import path
dqn Apr 5, 2026
16b2dc8
fix(sdk-codemod): replace walkDir+picomatch with fs.glob for file sca…
dqn Apr 5, 2026
2ddfbaa
fix(sdk-codemod): include matched rule IDs in unmodified-file warnings
dqn Apr 5, 2026
519f011
chore(sdk-codemod): set initial version to 0.1.0 for first publish
toiroakr Apr 8, 2026
64512a4
chore: set sdk-codemod changeset to patch for pre-1.0
toiroakr Apr 8, 2026
55d39d3
docs: update CLAUDE.md and upgrade.md for definePlugins pattern
toiroakr Apr 8, 2026
a4271b9
refactor(sdk-codemod): move legacy pattern detection from runner to C…
toiroakr Apr 9, 2026
38ddb51
fix(sdk-codemod): align devDependencies with main SDK versions to fix…
toiroakr Apr 9, 2026
c132bd5
fix(sdk-codemod): update politty to 0.4.13 to match SDK and simplify …
toiroakr Apr 9, 2026
4941b02
fix(sdk-codemod): track per-codemod applied status instead of all-or-…
toiroakr Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/codemod-infra-upgrade-command.md
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.
7 changes: 3 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Refer to `example/` for working implementations of all patterns (config, models,

Key files:

- `example/tailor.config.ts` - Configuration with defineConfig, defineAuth, defineIdp, defineStaticWebSite, defineGenerators
- `example/tailor.config.ts` - Configuration with defineConfig, defineAuth, defineIdp, defineStaticWebSite, definePlugins
- `example/tailordb/*.ts` - Model definitions with `db.type()`
- `example/resolvers/*.ts` - Resolver implementations with `createResolver`
- `example/executors/*.ts` - Executor implementations with `createExecutor`
Expand Down Expand Up @@ -74,13 +74,12 @@ Multi-event trigger variants handle multiple events in one executor:

Args include `event` (short name like `"created"`) and `rawEvent` (full event type like `"tailordb.type_record.created"`) for runtime type narrowing.

### Generators
### Plugins

`defineGenerators()` takes tuples as rest arguments (see `example/tailor.config.ts`). The `@tailor-platform/kysely-type` generator (built into the SDK) is required for `getDB()` in resolvers/executors/workflows.
`definePlugins()` takes plugin instances as rest arguments (see `example/tailor.config.ts`). The `kyselyTypePlugin` from `@tailor-platform/sdk/plugin/kysely-type` is required for `getDB()` in resolvers/executors/workflows. `defineGenerators()` is deprecated — use `definePlugins()` instead.

### Configuration

- `definePlugins()` is available for reusable type/resolver/executor generation
- Static website `.url` property is resolved at deployment time — use it in CORS and redirect URIs

## Developer Guides
Expand Down
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"
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);
Comment on lines +205 to +209
Copy link
Copy Markdown
Contributor

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 sdkImportRegex at line 205 uses ^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$ with the /m flag. Since .* does not match newlines, this regex cannot match multi-line SDK imports such as:

import {
  defineConfig,
  defineGenerators,
} from "@tailor-platform/sdk";

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 above import * as path from "node:path").

Example of incorrect output for multi-line import

Input:

import {
  defineConfig,
  defineGenerators,
} from "@tailor-platform/sdk";
import config from "./tailor.config";

Actual output (plugin import prepended at top):

import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
import {
  defineConfig,
  definePlugins,
} from "@tailor-platform/sdk";
import config from "./tailor.config";

Expected output (plugin import after SDK import):

import {
  defineConfig,
  definePlugins,
} from "@tailor-platform/sdk";
import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
import config from "./tailor.config";
Prompt for agents
In packages/sdk-codemod/codemods/v2/define-generators-to-plugins/scripts/transform.ts, the sdkImportRegex on line 205 only matches single-line SDK imports because .* does not cross newlines even with the /m flag. When the SDK import spans multiple lines (e.g., import { defineConfig, definePlugins } from "@tailor-platform/sdk" spread across lines), the regex fails to match and the fallback prepends plugin imports at the top of the file.

To fix this, use a regex that can match multi-line imports. One approach is to use [\s\S] instead of .* to allow matching across newlines:
  const sdkImportRegex = /^(import\s+[\s\S]*?from\s+["']@tailor-platform\/sdk["'];?)$/m;

However, this could be fragile with greedy matching across lines. A more robust approach would be to find the last line containing 'from "@tailor-platform/sdk"' (or the SDK import path) and insert after that line, using a simple string search rather than a single-line regex. For example, find the index of the closing of the SDK import statement by searching for the from clause pattern line-by-line.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

} else {
// Fallback: prepend imports at the top of the file
result = importLines.join("\n") + "\n" + result;
}
}

return result;
}
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") }),
);
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") }],
);
11 changes: 11 additions & 0 deletions packages/sdk-codemod/eslint.config.js
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"],
]);
52 changes: 52 additions & 0 deletions packages/sdk-codemod/package.json
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"
}
}
22 changes: 22 additions & 0 deletions packages/sdk-codemod/src/helpers.ts
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";
Loading
Loading