@tailor-platform/sdk-codemod 0.1.5 → 0.2.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/CHANGELOG.md +17 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +49 -0
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +152 -0
- package/dist/codemods/v2/cli-rename/scripts/transform.js +54 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +470 -0
- package/dist/codemods/v2/sdk-skills-shim/scripts/transform.js +51 -0
- package/dist/codemods/v2/test-run-arg-input/scripts/transform.js +144 -0
- package/dist/index.js +88 -10
- package/package.json +9 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @tailor-platform/sdk-codemod
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1104](https://github.com/tailor-platform/sdk/pull/1104) [`3c1571c`](https://github.com/tailor-platform/sdk/commit/3c1571cd76d125854b8379dfb8edcb58c2f517a4) Thanks [@dqn](https://github.com/dqn)! - Add three v2 codemods that the upgrade runner can apply when migrating across the 1.x → 2.x boundary:
|
|
8
|
+
|
|
9
|
+
- `v2/test-run-arg-input` strips the deprecated `{ "input": ... }` wrapper from `tailor-sdk function test-run --arg` JSON inside `package.json` scripts, shell scripts, and Markdown code blocks.
|
|
10
|
+
- `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files.
|
|
11
|
+
- `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses.
|
|
12
|
+
- `v2/apply-to-deploy` rewrites `tailor-sdk apply` invocations in `package.json` scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins (`tailor-sdk@latest`, `tailor-sdk@1.45.2`) are preserved.
|
|
13
|
+
- `v2/cli-rename` rewrites `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across `package.json` scripts, shell scripts, CI YAML, and Markdown. Optional `@version` pins are preserved.
|
|
14
|
+
- `v2/auth-invoker-unwrap` replaces `auth.invoker("name")` calls with the bare `"name"` string literal and drops the `auth` import when it has no other reference. Calls whose argument is not a literal string (`auth.invoker(variable)`, template literals) are left untouched so the author can decide.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [#1181](https://github.com/tailor-platform/sdk/pull/1181) [`3da6be2`](https://github.com/tailor-platform/sdk/commit/3da6be28a9df97b7633ada4923564d3c18afbf49) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency @ast-grep/napi to v0.42.2
|
|
19
|
+
|
|
3
20
|
## 0.1.5
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as path from "pathe";
|
|
2
|
+
//#region codemods/v2/apply-to-deploy/scripts/transform.ts
|
|
3
|
+
const APPLY_PATTERN = /\btailor-sdk(@[^\s'"`]+)?(\s+)apply(?![-\w])/g;
|
|
4
|
+
function replaceApply(value) {
|
|
5
|
+
return value.replace(APPLY_PATTERN, (_match, ver, sep) => `tailor-sdk${ver ?? ""}${sep}deploy`);
|
|
6
|
+
}
|
|
7
|
+
function transformText(source) {
|
|
8
|
+
if (!APPLY_PATTERN.test(source)) return null;
|
|
9
|
+
APPLY_PATTERN.lastIndex = 0;
|
|
10
|
+
const updated = replaceApply(source);
|
|
11
|
+
return updated === source ? null : updated;
|
|
12
|
+
}
|
|
13
|
+
function transformPackageJson(source) {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(source);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
let modified = false;
|
|
21
|
+
const scripts = parsed.scripts;
|
|
22
|
+
if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
|
|
23
|
+
if (typeof value !== "string") continue;
|
|
24
|
+
if (!value.includes("tailor-sdk")) continue;
|
|
25
|
+
const updated = replaceApply(value);
|
|
26
|
+
if (updated !== value) {
|
|
27
|
+
scripts[name] = updated;
|
|
28
|
+
modified = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!modified) return null;
|
|
32
|
+
const trailing = source.endsWith("\n") ? "\n" : "";
|
|
33
|
+
return JSON.stringify(parsed, null, 2) + trailing;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Replace `tailor-sdk apply` invocations with `tailor-sdk deploy`.
|
|
37
|
+
*
|
|
38
|
+
* `deploy` is a v1 alias of `apply` and the recommended name going forward.
|
|
39
|
+
* @param source - File contents
|
|
40
|
+
* @param filePath - Absolute path to the file (used to dispatch package.json vs text)
|
|
41
|
+
* @returns Transformed source or null when nothing matched.
|
|
42
|
+
*/
|
|
43
|
+
function transform(source, filePath) {
|
|
44
|
+
if (!source.includes("tailor-sdk")) return null;
|
|
45
|
+
if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
|
|
46
|
+
return transformText(source);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { transform as default };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
//#region codemods/v2/auth-invoker-unwrap/scripts/transform.ts
|
|
3
|
+
const QUICK_FILTER_NEEDLE = "auth.invoker";
|
|
4
|
+
function quickFilter(source) {
|
|
5
|
+
return source.includes(QUICK_FILTER_NEEDLE);
|
|
6
|
+
}
|
|
7
|
+
function isInsideImportStatement(node) {
|
|
8
|
+
let current = node.parent();
|
|
9
|
+
while (current) {
|
|
10
|
+
if (current.kind() === "import_statement") return true;
|
|
11
|
+
current = current.parent();
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find every `auth.invoker(<stringLiteral>)` call in `root`. Calls whose
|
|
17
|
+
* argument is not a literal string (e.g. `auth.invoker(name)`,
|
|
18
|
+
* `auth.invoker(\`x\${y}\`)`) are intentionally ignored: only the literal form
|
|
19
|
+
* is safely replaceable.
|
|
20
|
+
*/
|
|
21
|
+
function findInvokerCalls(root) {
|
|
22
|
+
const matches = root.findAll({ rule: { pattern: "auth.invoker($NAME)" } });
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const match of matches) {
|
|
25
|
+
const arg = match.getMatch("NAME");
|
|
26
|
+
if (!arg) continue;
|
|
27
|
+
if (arg.kind() !== "string") continue;
|
|
28
|
+
const r = match.range();
|
|
29
|
+
out.push({
|
|
30
|
+
callNode: match,
|
|
31
|
+
argText: arg.text(),
|
|
32
|
+
range: [r.start.index, r.end.index]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Count `auth` identifier references that are not part of an import statement
|
|
39
|
+
* and not part of any of the `auth.invoker(...)` calls already scheduled for
|
|
40
|
+
* replacement. A non-zero return means the `auth` import must be preserved.
|
|
41
|
+
*/
|
|
42
|
+
function countRemainingAuthRefs(root, scheduledCallRanges) {
|
|
43
|
+
const idents = root.findAll({ rule: {
|
|
44
|
+
kind: "identifier",
|
|
45
|
+
regex: "^auth$"
|
|
46
|
+
} });
|
|
47
|
+
let count = 0;
|
|
48
|
+
for (const node of idents) {
|
|
49
|
+
if (isInsideImportStatement(node)) continue;
|
|
50
|
+
const start = node.range().start.index;
|
|
51
|
+
if (scheduledCallRanges.some(([s, e]) => start >= s && start < e)) continue;
|
|
52
|
+
count++;
|
|
53
|
+
}
|
|
54
|
+
return count;
|
|
55
|
+
}
|
|
56
|
+
function* iterateImportSpecs(importStmt) {
|
|
57
|
+
const specs = importStmt.findAll({ rule: { kind: "import_specifier" } });
|
|
58
|
+
for (const spec of specs) {
|
|
59
|
+
const idents = spec.children().filter((c) => c.kind() === "identifier");
|
|
60
|
+
if (idents.length === 0) continue;
|
|
61
|
+
const importedName = idents[0].text();
|
|
62
|
+
yield {
|
|
63
|
+
spec,
|
|
64
|
+
importedName,
|
|
65
|
+
localName: idents[1]?.text() ?? importedName
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build an Edit that removes the `auth` specifier from `importStmt`. Returns
|
|
71
|
+
* null if the statement does not import `auth`. When `auth` is the only
|
|
72
|
+
* specifier the entire import line is removed (including a trailing newline);
|
|
73
|
+
* otherwise just the `auth,` / `, auth` fragment is dropped.
|
|
74
|
+
*/
|
|
75
|
+
function buildAuthImportRemovalEdit(source, importStmt) {
|
|
76
|
+
const specs = Array.from(iterateImportSpecs(importStmt));
|
|
77
|
+
const authSpec = specs.find((s) => s.localName === "auth" && s.importedName === "auth");
|
|
78
|
+
if (!authSpec) return null;
|
|
79
|
+
if (specs.length === 1) {
|
|
80
|
+
const r = importStmt.range();
|
|
81
|
+
let end = r.end.index;
|
|
82
|
+
while (end < source.length && (source[end] === "\n" || source[end] === "\r")) end++;
|
|
83
|
+
return {
|
|
84
|
+
startPos: r.start.index,
|
|
85
|
+
endPos: end,
|
|
86
|
+
insertedText: ""
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const r = authSpec.spec.range();
|
|
90
|
+
let start = r.start.index;
|
|
91
|
+
let end = r.end.index;
|
|
92
|
+
while (end < source.length && (source[end] === " " || source[end] === " ")) end++;
|
|
93
|
+
if (source[end] === ",") {
|
|
94
|
+
end++;
|
|
95
|
+
while (end < source.length && (source[end] === " " || source[end] === " ")) end++;
|
|
96
|
+
return {
|
|
97
|
+
startPos: start,
|
|
98
|
+
endPos: end,
|
|
99
|
+
insertedText: ""
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
while (start > 0 && (source[start - 1] === " " || source[start - 1] === " ")) start--;
|
|
103
|
+
if (source[start - 1] === ",") {
|
|
104
|
+
start--;
|
|
105
|
+
while (start > 0 && (source[start - 1] === " " || source[start - 1] === " ")) start--;
|
|
106
|
+
return {
|
|
107
|
+
startPos: start,
|
|
108
|
+
endPos: end,
|
|
109
|
+
insertedText: ""
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
startPos: r.start.index,
|
|
114
|
+
endPos: r.end.index,
|
|
115
|
+
insertedText: ""
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function findAuthImports(root) {
|
|
119
|
+
return root.findAll({ rule: { kind: "import_statement" } }).filter((stmt) => {
|
|
120
|
+
for (const { localName, importedName } of iterateImportSpecs(stmt)) if (localName === "auth" && importedName === "auth") return true;
|
|
121
|
+
return false;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Replace `auth.invoker("name")` calls with the bare `"name"` string literal.
|
|
126
|
+
* If no other `auth` references remain after the rewrite, drop the `auth`
|
|
127
|
+
* specifier (or the entire import line when `auth` was its sole specifier).
|
|
128
|
+
*
|
|
129
|
+
* `auth.invoker()` was deprecated in favor of passing the machine user name
|
|
130
|
+
* directly; carrying the `auth` import only for `.invoker()` would otherwise
|
|
131
|
+
* pull config-layer (Node-only) modules into runtime bundles.
|
|
132
|
+
* @param source - File contents
|
|
133
|
+
* @param filePath - Absolute path to the file (kept for the runner signature)
|
|
134
|
+
* @returns Transformed source or null when nothing matched.
|
|
135
|
+
*/
|
|
136
|
+
function transform(source, _filePath) {
|
|
137
|
+
if (!quickFilter(source)) return null;
|
|
138
|
+
const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
|
|
139
|
+
const calls = findInvokerCalls(root);
|
|
140
|
+
if (calls.length === 0) return null;
|
|
141
|
+
const edits = calls.map((c) => c.callNode.replace(c.argText));
|
|
142
|
+
if (countRemainingAuthRefs(root, calls.map((c) => c.range)) === 0) for (const importStmt of findAuthImports(root)) {
|
|
143
|
+
const edit = buildAuthImportRemovalEdit(source, importStmt);
|
|
144
|
+
if (edit) edits.push(edit);
|
|
145
|
+
}
|
|
146
|
+
if (edits.length === 0) return null;
|
|
147
|
+
let result = root.commitEdits(edits);
|
|
148
|
+
result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
149
|
+
return result === source ? null : result;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { transform as default };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as path from "pathe";
|
|
2
|
+
//#region codemods/v2/cli-rename/scripts/transform.ts
|
|
3
|
+
const COMMAND_RENAMES = [["crash-report", "crashreport"]];
|
|
4
|
+
const COMMAND_PATTERN = new RegExp(`\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g");
|
|
5
|
+
const COMMAND_MAP = new Map(COMMAND_RENAMES);
|
|
6
|
+
function replaceAll(value) {
|
|
7
|
+
return value.replace(COMMAND_PATTERN, (_match, ver, sep, cmd) => `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`);
|
|
8
|
+
}
|
|
9
|
+
function transformText(source) {
|
|
10
|
+
if (!COMMAND_PATTERN.test(source)) return null;
|
|
11
|
+
COMMAND_PATTERN.lastIndex = 0;
|
|
12
|
+
const updated = replaceAll(source);
|
|
13
|
+
return updated === source ? null : updated;
|
|
14
|
+
}
|
|
15
|
+
function transformPackageJson(source) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(source);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let modified = false;
|
|
23
|
+
const scripts = parsed.scripts;
|
|
24
|
+
if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
|
|
25
|
+
if (typeof value !== "string") continue;
|
|
26
|
+
const updated = replaceAll(value);
|
|
27
|
+
if (updated !== value) {
|
|
28
|
+
scripts[name] = updated;
|
|
29
|
+
modified = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!modified) return null;
|
|
33
|
+
const trailing = source.endsWith("\n") ? "\n" : "";
|
|
34
|
+
return JSON.stringify(parsed, null, 2) + trailing;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Apply v2 CLI naming conventions: multi-word commands collapse into a single
|
|
38
|
+
* word (`crash-report` → `crashreport`). Optional `@version` pins on the binary
|
|
39
|
+
* (`tailor-sdk@latest`) are preserved.
|
|
40
|
+
*
|
|
41
|
+
* Long options (`--executionId`, `--executorName`, `--jobId`) and the
|
|
42
|
+
* positional argument keys with the same names are intentionally not rewritten:
|
|
43
|
+
* those tokens are positional in the SDK CLI and never appear as long flags in
|
|
44
|
+
* user scripts, so a transform here would have no real-world target.
|
|
45
|
+
* @param source - File contents
|
|
46
|
+
* @param filePath - Absolute path to the file (used to dispatch package.json vs text)
|
|
47
|
+
* @returns Transformed source or null when nothing matched.
|
|
48
|
+
*/
|
|
49
|
+
function transform(source, filePath) {
|
|
50
|
+
if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
|
|
51
|
+
return transformText(source);
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { transform as default };
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
//#region codemods/v2/principal-unify/scripts/transform.ts
|
|
3
|
+
const TYPE_RENAME_MAP = {
|
|
4
|
+
TailorUser: "TailorPrincipal",
|
|
5
|
+
TailorActor: "TailorPrincipal",
|
|
6
|
+
TailorInvoker: "TailorPrincipal"
|
|
7
|
+
};
|
|
8
|
+
const UNAUTHENTICATED = "unauthenticatedTailorUser";
|
|
9
|
+
const QUICK_FILTER_NEEDLES = [
|
|
10
|
+
...Object.keys(TYPE_RENAME_MAP),
|
|
11
|
+
UNAUTHENTICATED,
|
|
12
|
+
"createResolver"
|
|
13
|
+
];
|
|
14
|
+
function quickFilter(source) {
|
|
15
|
+
if (!source.includes("@tailor-platform/sdk")) return false;
|
|
16
|
+
return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle));
|
|
17
|
+
}
|
|
18
|
+
function isInsideImportStatement(node) {
|
|
19
|
+
let current = node.parent();
|
|
20
|
+
while (current) {
|
|
21
|
+
if (current.kind() === "import_statement") return true;
|
|
22
|
+
current = current.parent();
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function isMemberExpressionObject(node) {
|
|
27
|
+
const parent = node.parent();
|
|
28
|
+
if (!parent || parent.kind() !== "member_expression") return false;
|
|
29
|
+
const obj = parent.field("object");
|
|
30
|
+
if (!obj) return false;
|
|
31
|
+
const r = node.range();
|
|
32
|
+
const or = obj.range();
|
|
33
|
+
return r.start.index === or.start.index && r.end.index === or.end.index;
|
|
34
|
+
}
|
|
35
|
+
function extractModuleSource(importText) {
|
|
36
|
+
return importText.match(/from\s+(["'])([^"']+)\1/)?.[2] ?? "@tailor-platform/sdk";
|
|
37
|
+
}
|
|
38
|
+
function escapeRegex(s) {
|
|
39
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Yield each import specifier in `importStmt` along with its imported name and
|
|
43
|
+
* optional alias. `import { Foo as Bar }` produces `{ importedName: "Foo",
|
|
44
|
+
* aliasNode: <Bar>, localName: "Bar" }`; `import { Foo }` produces
|
|
45
|
+
* `{ importedName: "Foo", aliasNode: undefined, localName: "Foo" }`.
|
|
46
|
+
*/
|
|
47
|
+
function* iterateImportSpecs(importStmt) {
|
|
48
|
+
const specs = importStmt.findAll({ rule: { kind: "import_specifier" } });
|
|
49
|
+
for (const spec of specs) {
|
|
50
|
+
const idents = spec.children().filter((c) => c.kind() === "identifier");
|
|
51
|
+
if (idents.length === 0) continue;
|
|
52
|
+
const importedName = idents[0].text();
|
|
53
|
+
const aliasNode = idents[1];
|
|
54
|
+
yield {
|
|
55
|
+
spec,
|
|
56
|
+
importedName,
|
|
57
|
+
aliasNode,
|
|
58
|
+
localName: aliasNode?.text() ?? importedName
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function rebuildImportStatement(importStmt, globalEmittedRenamed, unauthenticatedLocalNames) {
|
|
63
|
+
const importText = importStmt.text();
|
|
64
|
+
const isImportType = /^\s*import\s+type\b/.test(importText);
|
|
65
|
+
const trailingSemi = importText.trimEnd().endsWith(";") ? ";" : "";
|
|
66
|
+
const sourceRaw = extractModuleSource(importText);
|
|
67
|
+
const newSpecTexts = [];
|
|
68
|
+
const seenLocal = /* @__PURE__ */ new Set();
|
|
69
|
+
let touched = false;
|
|
70
|
+
for (const { spec, importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) {
|
|
71
|
+
const specText = spec.text();
|
|
72
|
+
const isTypeOnly = /^\s*type\s+/.test(specText);
|
|
73
|
+
const renamed = TYPE_RENAME_MAP[importedName];
|
|
74
|
+
if (renamed) {
|
|
75
|
+
touched = true;
|
|
76
|
+
const finalLocal = aliasNode?.text() ?? renamed;
|
|
77
|
+
if (seenLocal.has(finalLocal)) continue;
|
|
78
|
+
if (!aliasNode && globalEmittedRenamed.has(renamed)) continue;
|
|
79
|
+
seenLocal.add(finalLocal);
|
|
80
|
+
if (!aliasNode) globalEmittedRenamed.add(renamed);
|
|
81
|
+
const asPart = aliasNode ? ` as ${aliasNode.text()}` : "";
|
|
82
|
+
newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`);
|
|
83
|
+
} else if (importedName === UNAUTHENTICATED) {
|
|
84
|
+
touched = true;
|
|
85
|
+
unauthenticatedLocalNames.add(localName);
|
|
86
|
+
} else {
|
|
87
|
+
if (seenLocal.has(localName)) continue;
|
|
88
|
+
seenLocal.add(localName);
|
|
89
|
+
newSpecTexts.push(specText);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!touched) return {
|
|
93
|
+
newText: importText,
|
|
94
|
+
touched: false
|
|
95
|
+
};
|
|
96
|
+
if (newSpecTexts.length === 0) return {
|
|
97
|
+
newText: "",
|
|
98
|
+
touched: true
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
newText: `${isImportType ? "import type " : "import "}{ ${newSpecTexts.join(", ")} } from "${sourceRaw}"${trailingSemi}`,
|
|
102
|
+
touched: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const SCOPE_KINDS = new Set([
|
|
106
|
+
"statement_block",
|
|
107
|
+
"function_body",
|
|
108
|
+
"for_statement",
|
|
109
|
+
"for_in_statement",
|
|
110
|
+
"for_of_statement",
|
|
111
|
+
"arrow_function",
|
|
112
|
+
"function_expression",
|
|
113
|
+
"function_declaration",
|
|
114
|
+
"method_definition"
|
|
115
|
+
]);
|
|
116
|
+
const NESTED_FN_KINDS = [
|
|
117
|
+
"arrow_function",
|
|
118
|
+
"function_expression",
|
|
119
|
+
"function_declaration",
|
|
120
|
+
"method_definition"
|
|
121
|
+
];
|
|
122
|
+
function isInsideAnyRange(pos, ranges) {
|
|
123
|
+
return ranges.some(([s, e]) => pos >= s && pos < e);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Walk up from `decl` and return the byte range of its enclosing scope, or
|
|
127
|
+
* null if no recognized scope ancestor exists.
|
|
128
|
+
*/
|
|
129
|
+
function enclosingScopeRange(decl) {
|
|
130
|
+
let scope = decl.parent();
|
|
131
|
+
while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent();
|
|
132
|
+
if (!scope) return null;
|
|
133
|
+
const range = scope.range();
|
|
134
|
+
return [range.start.index, range.end.index];
|
|
135
|
+
}
|
|
136
|
+
function patternBindsName(pat, name) {
|
|
137
|
+
const k = pat.kind();
|
|
138
|
+
if (k === "identifier") return pat.text() === name;
|
|
139
|
+
if (k === "object_pattern") for (const child of pat.children()) {
|
|
140
|
+
const ck = child.kind();
|
|
141
|
+
if (ck === "shorthand_property_identifier_pattern" && child.text() === name) return true;
|
|
142
|
+
if (ck === "pair_pattern") {
|
|
143
|
+
const value = child.field("value");
|
|
144
|
+
if (value && patternBindsName(value, name)) return true;
|
|
145
|
+
}
|
|
146
|
+
if (ck === "object_assignment_pattern") {
|
|
147
|
+
const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
|
|
148
|
+
if (inner && inner.text() === name) return true;
|
|
149
|
+
}
|
|
150
|
+
if (ck === "rest_pattern") {
|
|
151
|
+
const inner = child.children().find((c) => c.kind() === "identifier");
|
|
152
|
+
if (inner && inner.text() === name) return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (k === "array_pattern") {
|
|
156
|
+
for (const child of pat.children()) if (patternBindsName(child, name)) return true;
|
|
157
|
+
} else if (k === "assignment_pattern") {
|
|
158
|
+
const left = pat.field("left");
|
|
159
|
+
if (left && patternBindsName(left, name)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
function functionRebindsName(fn, name) {
|
|
164
|
+
const single = fn.field("parameter");
|
|
165
|
+
if (single && patternBindsName(single, name)) return true;
|
|
166
|
+
const params = fn.field("parameters") ?? fn.children().find((c) => c.kind() === "formal_parameters");
|
|
167
|
+
if (!params) return false;
|
|
168
|
+
for (const child of params.children()) {
|
|
169
|
+
const k = child.kind();
|
|
170
|
+
if (k === "identifier" && child.text() === name) return true;
|
|
171
|
+
if (k === "object_pattern" || k === "array_pattern") {
|
|
172
|
+
if (patternBindsName(child, name)) return true;
|
|
173
|
+
}
|
|
174
|
+
if (k === "required_parameter" || k === "optional_parameter") {
|
|
175
|
+
const pat = child.field("pattern");
|
|
176
|
+
if (pat && patternBindsName(pat, name)) return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Collect byte ranges of inner functions that re-bind `ctxName` as a parameter.
|
|
183
|
+
*
|
|
184
|
+
* Member-accesses to `ctxName.user` whose start byte falls inside any of these
|
|
185
|
+
* ranges refer to the inner function's parameter, not the resolver context, and
|
|
186
|
+
* must not be renamed.
|
|
187
|
+
* @param body - The resolver body node.
|
|
188
|
+
* @param ctxName - The context parameter identifier name.
|
|
189
|
+
* @param resolverArrow - The resolver's outer arrow/function expression to exclude.
|
|
190
|
+
*/
|
|
191
|
+
function collectCtxShadowRanges(body, ctxName, resolverArrow) {
|
|
192
|
+
const ranges = [];
|
|
193
|
+
const ar = resolverArrow.range();
|
|
194
|
+
for (const k of NESTED_FN_KINDS) {
|
|
195
|
+
const fns = body.findAll({ rule: { kind: k } });
|
|
196
|
+
for (const fn of fns) {
|
|
197
|
+
const r = fn.range();
|
|
198
|
+
if (r.start.index === ar.start.index && r.end.index === ar.end.index) continue;
|
|
199
|
+
if (functionRebindsName(fn, ctxName)) ranges.push([r.start.index, r.end.index]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const declarators = body.findAll({ rule: { kind: "variable_declarator" } });
|
|
203
|
+
for (const decl of declarators) {
|
|
204
|
+
const nameNode = decl.field("name");
|
|
205
|
+
if (!nameNode || nameNode.kind() !== "identifier") continue;
|
|
206
|
+
if (nameNode.text() !== ctxName) continue;
|
|
207
|
+
const range = enclosingScopeRange(decl);
|
|
208
|
+
if (range) ranges.push(range);
|
|
209
|
+
}
|
|
210
|
+
return ranges;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Collect every byte range across the file where `name` is locally re-bound,
|
|
214
|
+
* so identifier references inside the range are treated as shadowed.
|
|
215
|
+
*
|
|
216
|
+
* Combines variable declarations (var/let/const, including object-pattern
|
|
217
|
+
* shorthand declarations) with function-parameter bindings.
|
|
218
|
+
*/
|
|
219
|
+
function collectAllShadowRanges(root, name) {
|
|
220
|
+
const ranges = [];
|
|
221
|
+
const declarators = root.findAll({ rule: { kind: "variable_declarator" } });
|
|
222
|
+
for (const decl of declarators) {
|
|
223
|
+
const nameNode = decl.field("name");
|
|
224
|
+
if (!nameNode) continue;
|
|
225
|
+
if (!patternBindsName(nameNode, name)) continue;
|
|
226
|
+
const range = enclosingScopeRange(decl);
|
|
227
|
+
if (range) ranges.push(range);
|
|
228
|
+
}
|
|
229
|
+
for (const k of NESTED_FN_KINDS) {
|
|
230
|
+
const fns = root.findAll({ rule: { kind: k } });
|
|
231
|
+
for (const fn of fns) if (functionRebindsName(fn, name)) {
|
|
232
|
+
const range = fn.range();
|
|
233
|
+
ranges.push([range.start.index, range.end.index]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return ranges;
|
|
237
|
+
}
|
|
238
|
+
function findResolverBodyArrow(call) {
|
|
239
|
+
const args = call.field("arguments");
|
|
240
|
+
if (!args) return null;
|
|
241
|
+
const objArg = args.children().find((c) => c.kind() === "object");
|
|
242
|
+
if (!objArg) return null;
|
|
243
|
+
const pairs = objArg.findAll({ rule: { kind: "pair" } });
|
|
244
|
+
for (const pair of pairs) {
|
|
245
|
+
if (pair.field("key")?.text() !== "body") continue;
|
|
246
|
+
const value = pair.field("value");
|
|
247
|
+
if (!value) continue;
|
|
248
|
+
if (value.kind() === "arrow_function" || value.kind() === "function_expression") return value;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Look for any binding named `caller` in the resolver body or pattern. When
|
|
254
|
+
* one exists, renaming `user` → `caller` would either shadow it, collide with
|
|
255
|
+
* a duplicate `let`/`const`, or alias an unrelated value, so the codemod
|
|
256
|
+
* leaves the body alone for manual migration instead.
|
|
257
|
+
*/
|
|
258
|
+
function hasCallerBindingConflict(pattern, body) {
|
|
259
|
+
for (const child of pattern.children()) {
|
|
260
|
+
const k = child.kind();
|
|
261
|
+
if (k === "shorthand_property_identifier_pattern" && child.text() === "caller") return true;
|
|
262
|
+
if (k === "pair_pattern") {
|
|
263
|
+
const key = child.field("key");
|
|
264
|
+
if (key && key.text() === "caller") return true;
|
|
265
|
+
const value = child.field("value");
|
|
266
|
+
if (value && value.kind() === "identifier" && value.text() === "caller") return true;
|
|
267
|
+
}
|
|
268
|
+
if (k === "object_assignment_pattern") {
|
|
269
|
+
const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
|
|
270
|
+
if (inner && inner.text() === "caller") return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (body.findAll({ rule: {
|
|
274
|
+
kind: "identifier",
|
|
275
|
+
regex: "^caller$",
|
|
276
|
+
inside: { kind: "variable_declarator" }
|
|
277
|
+
} }).length > 0) return true;
|
|
278
|
+
if (body.findAll({ rule: {
|
|
279
|
+
kind: "shorthand_property_identifier_pattern",
|
|
280
|
+
regex: "^caller$",
|
|
281
|
+
inside: { kind: "variable_declarator" }
|
|
282
|
+
} }).length > 0) return true;
|
|
283
|
+
for (const k of NESTED_FN_KINDS) {
|
|
284
|
+
const fns = body.findAll({ rule: { kind: k } });
|
|
285
|
+
for (const fn of fns) if (functionRebindsName(fn, "caller")) return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
function transformResolverBody(arrowNode, edits) {
|
|
290
|
+
const params = arrowNode.field("parameters") ?? arrowNode.field("parameter") ?? arrowNode.children().find((c) => c.kind() === "formal_parameters");
|
|
291
|
+
const body = arrowNode.field("body");
|
|
292
|
+
if (!params || !body) return;
|
|
293
|
+
const firstParam = params.children().find((c) => c.kind() === "required_parameter" || c.kind() === "optional_parameter" || c.kind() === "identifier" || c.kind() === "object_pattern");
|
|
294
|
+
if (!firstParam) return;
|
|
295
|
+
let pattern;
|
|
296
|
+
if (firstParam.kind() === "object_pattern" || firstParam.kind() === "identifier") pattern = firstParam;
|
|
297
|
+
else {
|
|
298
|
+
const inner = firstParam.field("pattern");
|
|
299
|
+
if (inner) pattern = inner;
|
|
300
|
+
}
|
|
301
|
+
if (!pattern) return;
|
|
302
|
+
if (pattern.kind() === "object_pattern") {
|
|
303
|
+
if (hasCallerBindingConflict(pattern, body)) return;
|
|
304
|
+
let renamedShorthandUser = false;
|
|
305
|
+
for (const child of pattern.children()) {
|
|
306
|
+
const kind = child.kind();
|
|
307
|
+
if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") {
|
|
308
|
+
edits.push(child.replace("caller"));
|
|
309
|
+
renamedShorthandUser = true;
|
|
310
|
+
} else if (kind === "pair_pattern") {
|
|
311
|
+
const key = child.field("key");
|
|
312
|
+
if (key && key.text() === "user") edits.push(key.replace("caller"));
|
|
313
|
+
} else if (kind === "object_assignment_pattern") {
|
|
314
|
+
const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
|
|
315
|
+
if (inner && inner.text() === "user") {
|
|
316
|
+
edits.push(inner.replace("caller"));
|
|
317
|
+
renamedShorthandUser = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (renamedShorthandUser) {
|
|
322
|
+
const shadowRanges = collectAllShadowRanges(body, "user");
|
|
323
|
+
const refs = body.findAll({ rule: {
|
|
324
|
+
kind: "identifier",
|
|
325
|
+
regex: "^user$"
|
|
326
|
+
} });
|
|
327
|
+
for (const ref of refs) {
|
|
328
|
+
const pos = ref.range().start.index;
|
|
329
|
+
if (isInsideAnyRange(pos, shadowRanges)) continue;
|
|
330
|
+
edits.push(ref.replace("caller"));
|
|
331
|
+
}
|
|
332
|
+
const shortRefs = body.findAll({ rule: {
|
|
333
|
+
kind: "shorthand_property_identifier",
|
|
334
|
+
regex: "^user$"
|
|
335
|
+
} });
|
|
336
|
+
for (const ref of shortRefs) {
|
|
337
|
+
const pos = ref.range().start.index;
|
|
338
|
+
if (isInsideAnyRange(pos, shadowRanges)) continue;
|
|
339
|
+
edits.push(ref.replace("user: caller"));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const ctxName = pattern.text();
|
|
345
|
+
const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, arrowNode);
|
|
346
|
+
const propertyAccesses = body.findAll({ rule: {
|
|
347
|
+
kind: "property_identifier",
|
|
348
|
+
regex: "^user$"
|
|
349
|
+
} });
|
|
350
|
+
for (const propId of propertyAccesses) {
|
|
351
|
+
const parent = propId.parent();
|
|
352
|
+
if (!parent || parent.kind() !== "member_expression") continue;
|
|
353
|
+
const obj = parent.field("object");
|
|
354
|
+
if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue;
|
|
355
|
+
const pos = obj.range().start.index;
|
|
356
|
+
if (isInsideAnyRange(pos, ctxShadowRanges)) continue;
|
|
357
|
+
edits.push(propId.replace("caller"));
|
|
358
|
+
}
|
|
359
|
+
const ctxDestructures = body.findAll({ rule: {
|
|
360
|
+
kind: "variable_declarator",
|
|
361
|
+
has: {
|
|
362
|
+
field: "value",
|
|
363
|
+
kind: "identifier",
|
|
364
|
+
regex: `^${escapeRegex(ctxName)}$`
|
|
365
|
+
}
|
|
366
|
+
} });
|
|
367
|
+
for (const decl of ctxDestructures) {
|
|
368
|
+
const pos = decl.range().start.index;
|
|
369
|
+
if (isInsideAnyRange(pos, ctxShadowRanges)) continue;
|
|
370
|
+
const pat = decl.field("name");
|
|
371
|
+
if (!pat || pat.kind() !== "object_pattern") continue;
|
|
372
|
+
for (const child of pat.children()) {
|
|
373
|
+
const k = child.kind();
|
|
374
|
+
if (k === "shorthand_property_identifier_pattern" && child.text() === "user") edits.push(child.replace("caller: user"));
|
|
375
|
+
else if (k === "pair_pattern") {
|
|
376
|
+
const key = child.field("key");
|
|
377
|
+
if (key && key.text() === "user") edits.push(key.replace("caller"));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal.
|
|
384
|
+
*
|
|
385
|
+
* - Renames `TailorUser` / `TailorActor` / `TailorInvoker` type references to `TailorPrincipal`.
|
|
386
|
+
* - Rewrites SDK imports (including the `/test` subpath) to use `TailorPrincipal` (deduped
|
|
387
|
+
* across statements) and drops `unauthenticatedTailorUser`.
|
|
388
|
+
* - Replaces standalone references to `unauthenticatedTailorUser` with `null`. Member-access
|
|
389
|
+
* forms like `unauthenticatedTailorUser.id` are left alone on purpose so the resulting TS
|
|
390
|
+
* error after the import is removed points the author at the broken access.
|
|
391
|
+
* - Renames `user` to `caller` for top-level destructured resolver bodies (`{ input, user }`),
|
|
392
|
+
* handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and
|
|
393
|
+
* rewrites `<ctx>.user` for non-destructured single-param bodies — respecting variable
|
|
394
|
+
* shadowing in both directions.
|
|
395
|
+
* @param source - TypeScript source text.
|
|
396
|
+
* @returns Transformed source or null when nothing matched.
|
|
397
|
+
*/
|
|
398
|
+
function transform(source) {
|
|
399
|
+
if (!quickFilter(source)) return null;
|
|
400
|
+
const tree = parse(Lang.TypeScript, source).root();
|
|
401
|
+
const edits = [];
|
|
402
|
+
const sdkImports = tree.findAll({ rule: {
|
|
403
|
+
kind: "import_statement",
|
|
404
|
+
has: {
|
|
405
|
+
kind: "string",
|
|
406
|
+
regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$"
|
|
407
|
+
}
|
|
408
|
+
} });
|
|
409
|
+
const sdkRenameSourceNames = /* @__PURE__ */ new Set();
|
|
410
|
+
for (const importStmt of sdkImports) for (const { importedName, aliasNode } of iterateImportSpecs(importStmt)) if (TYPE_RENAME_MAP[importedName] && !aliasNode) sdkRenameSourceNames.add(importedName);
|
|
411
|
+
const typeIdents = tree.findAll({ rule: {
|
|
412
|
+
kind: "type_identifier",
|
|
413
|
+
not: { inside: { kind: "import_statement" } }
|
|
414
|
+
} });
|
|
415
|
+
for (const id of typeIdents) {
|
|
416
|
+
if (!sdkRenameSourceNames.has(id.text())) continue;
|
|
417
|
+
const newName = TYPE_RENAME_MAP[id.text()];
|
|
418
|
+
edits.push(id.replace(newName));
|
|
419
|
+
}
|
|
420
|
+
let importRemoved = false;
|
|
421
|
+
const globalEmittedRenamed = /* @__PURE__ */ new Set();
|
|
422
|
+
const unauthenticatedLocalNames = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const importStmt of sdkImports) {
|
|
424
|
+
const { newText, touched } = rebuildImportStatement(importStmt, globalEmittedRenamed, unauthenticatedLocalNames);
|
|
425
|
+
if (!touched) continue;
|
|
426
|
+
edits.push(importStmt.replace(newText));
|
|
427
|
+
if (newText === "") importRemoved = true;
|
|
428
|
+
}
|
|
429
|
+
for (const localName of unauthenticatedLocalNames) {
|
|
430
|
+
const shadowRanges = collectAllShadowRanges(tree, localName);
|
|
431
|
+
const ids = tree.findAll({ rule: {
|
|
432
|
+
kind: "identifier",
|
|
433
|
+
regex: `^${escapeRegex(localName)}$`
|
|
434
|
+
} });
|
|
435
|
+
for (const id of ids) {
|
|
436
|
+
if (isInsideImportStatement(id)) continue;
|
|
437
|
+
if (isMemberExpressionObject(id)) continue;
|
|
438
|
+
const pos = id.range().start.index;
|
|
439
|
+
if (isInsideAnyRange(pos, shadowRanges)) continue;
|
|
440
|
+
edits.push(id.replace("null"));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const createResolverLocalNames = /* @__PURE__ */ new Set();
|
|
444
|
+
for (const importStmt of sdkImports) for (const { importedName, localName } of iterateImportSpecs(importStmt)) if (importedName === "createResolver") createResolverLocalNames.add(localName);
|
|
445
|
+
for (const localName of createResolverLocalNames) {
|
|
446
|
+
const shadowRanges = collectAllShadowRanges(tree, localName);
|
|
447
|
+
const calls = tree.findAll({ rule: {
|
|
448
|
+
kind: "call_expression",
|
|
449
|
+
has: {
|
|
450
|
+
field: "function",
|
|
451
|
+
kind: "identifier",
|
|
452
|
+
regex: `^${escapeRegex(localName)}$`
|
|
453
|
+
}
|
|
454
|
+
} });
|
|
455
|
+
for (const call of calls) {
|
|
456
|
+
const callee = call.field("function");
|
|
457
|
+
if (!callee) continue;
|
|
458
|
+
const pos = callee.range().start.index;
|
|
459
|
+
if (isInsideAnyRange(pos, shadowRanges)) continue;
|
|
460
|
+
const arrow = findResolverBodyArrow(call);
|
|
461
|
+
if (arrow) transformResolverBody(arrow, edits);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (edits.length === 0) return null;
|
|
465
|
+
let result = tree.commitEdits(edits);
|
|
466
|
+
if (importRemoved) result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
export { transform as default };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as path from "pathe";
|
|
2
|
+
//#region codemods/v2/sdk-skills-shim/scripts/transform.ts
|
|
3
|
+
const SHIM_PATTERN = /\btailor-sdk-skills(?:@[^\s'"`]+)?(?:[ \t]+install)?\b(?!-)/g;
|
|
4
|
+
const REPLACEMENT = "tailor-sdk skills install";
|
|
5
|
+
function replaceShim(value) {
|
|
6
|
+
return value.replace(SHIM_PATTERN, REPLACEMENT);
|
|
7
|
+
}
|
|
8
|
+
function transformText(source) {
|
|
9
|
+
if (!SHIM_PATTERN.test(source)) return null;
|
|
10
|
+
SHIM_PATTERN.lastIndex = 0;
|
|
11
|
+
const updated = replaceShim(source);
|
|
12
|
+
return updated === source ? null : updated;
|
|
13
|
+
}
|
|
14
|
+
function transformPackageJson(source) {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(source);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let modified = false;
|
|
22
|
+
const scripts = parsed.scripts;
|
|
23
|
+
if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
|
|
24
|
+
if (typeof value !== "string") continue;
|
|
25
|
+
if (!value.includes("tailor-sdk-skills")) continue;
|
|
26
|
+
const updated = replaceShim(value);
|
|
27
|
+
if (updated !== value) {
|
|
28
|
+
scripts[name] = updated;
|
|
29
|
+
modified = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!modified) return null;
|
|
33
|
+
const trailing = source.endsWith("\n") ? "\n" : "";
|
|
34
|
+
return JSON.stringify(parsed, null, 2) + trailing;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Replace `tailor-sdk-skills` invocations with `tailor-sdk skills install`.
|
|
38
|
+
*
|
|
39
|
+
* The standalone `tailor-sdk-skills` binary is removed in v2; users must call
|
|
40
|
+
* the subcommand on the main `tailor-sdk` CLI instead.
|
|
41
|
+
* @param source - File contents
|
|
42
|
+
* @param filePath - Absolute path to the file (used to dispatch package.json vs text)
|
|
43
|
+
* @returns Transformed source or null when nothing matched.
|
|
44
|
+
*/
|
|
45
|
+
function transform(source, filePath) {
|
|
46
|
+
if (!source.includes("tailor-sdk-skills")) return null;
|
|
47
|
+
if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
|
|
48
|
+
return transformText(source);
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { transform as default };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as path from "pathe";
|
|
2
|
+
//#region codemods/v2/test-run-arg-input/scripts/transform.ts
|
|
3
|
+
const COMMAND_PATTERN = /\btailor-sdk\s+function\s+test-run\b/;
|
|
4
|
+
const JOIN_MARKER = "SDK_CODEMOD_JOIN";
|
|
5
|
+
const SHELL_ARG_PATTERN = new RegExp(`(--arg|-a)(\\s*=\\s*|(?:\\s|${JOIN_MARKER})+)(['"\`])((?:\\\\.|(?!\\3)[^\\\\])*)\\3`, "g");
|
|
6
|
+
function isInputWrapper(parsed) {
|
|
7
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && Object.keys(parsed).length === 1 && "input" in parsed;
|
|
8
|
+
}
|
|
9
|
+
function unwrapJsonString(body, quote) {
|
|
10
|
+
const decoded = quote === "\"" ? body.replace(/\\(["\\])/g, "$1") : body;
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(decoded);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!isInputWrapper(parsed)) return null;
|
|
18
|
+
const inner = JSON.stringify(parsed.input);
|
|
19
|
+
if (quote === "\"") return inner.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
20
|
+
return inner;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Apply the unwrap to one shell command segment. Quotes are tracked so command
|
|
24
|
+
* boundary characters inside strings (e.g. `'a;b'`) are not split.
|
|
25
|
+
*/
|
|
26
|
+
function applyUnwrapToSegment(segment) {
|
|
27
|
+
return segment.replace(SHELL_ARG_PATTERN, (match, flag, sep, quote, body) => {
|
|
28
|
+
const unwrapped = unwrapJsonString(body, quote);
|
|
29
|
+
if (unwrapped == null) return match;
|
|
30
|
+
return `${flag}${sep}${quote}${unwrapped}${quote}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Walk the line splitting on unquoted shell command boundaries (`;`, `&&`,
|
|
35
|
+
* `||`, `|`, `&`) and only run the unwrap on segments that actually invoke
|
|
36
|
+
* `tailor-sdk function test-run`. Without this, a chained line like
|
|
37
|
+
* `tailor-sdk function test-run ... --arg '{"input":...}' && other-cli --arg '{"input":...}'`
|
|
38
|
+
* would have the unrelated `other-cli` argument unwrapped too.
|
|
39
|
+
*/
|
|
40
|
+
function transformShellLine(line) {
|
|
41
|
+
if (!COMMAND_PATTERN.test(line)) return line;
|
|
42
|
+
let result = "";
|
|
43
|
+
let segBuf = "";
|
|
44
|
+
let i = 0;
|
|
45
|
+
let quoteChar = null;
|
|
46
|
+
const N = line.length;
|
|
47
|
+
const flushSegment = () => {
|
|
48
|
+
if (COMMAND_PATTERN.test(segBuf)) result += applyUnwrapToSegment(segBuf);
|
|
49
|
+
else result += segBuf;
|
|
50
|
+
segBuf = "";
|
|
51
|
+
};
|
|
52
|
+
while (i < N) {
|
|
53
|
+
const ch = line[i];
|
|
54
|
+
if (quoteChar) {
|
|
55
|
+
if (ch === "\\" && quoteChar !== "'" && i + 1 < N) {
|
|
56
|
+
segBuf += line.slice(i, i + 2);
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === quoteChar) quoteChar = null;
|
|
61
|
+
segBuf += ch;
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
66
|
+
quoteChar = ch;
|
|
67
|
+
segBuf += ch;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const two = line.slice(i, i + 2);
|
|
72
|
+
if (two === "&&" || two === "||") {
|
|
73
|
+
flushSegment();
|
|
74
|
+
result += two;
|
|
75
|
+
i += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === ";" || ch === "|" || ch === "&") {
|
|
79
|
+
flushSegment();
|
|
80
|
+
result += ch;
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
segBuf += ch;
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
flushSegment();
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
function transformShellLikeText(source) {
|
|
91
|
+
if (!COMMAND_PATTERN.test(source)) return null;
|
|
92
|
+
const joined = source.replace(/\\\n/g, JOIN_MARKER);
|
|
93
|
+
let modified = false;
|
|
94
|
+
const lines = joined.split("\n");
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const line = lines[i];
|
|
97
|
+
const transformed = transformShellLine(line);
|
|
98
|
+
if (transformed !== line) {
|
|
99
|
+
lines[i] = transformed;
|
|
100
|
+
modified = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!modified) return null;
|
|
104
|
+
return lines.join("\n").split(JOIN_MARKER).join("\\\n");
|
|
105
|
+
}
|
|
106
|
+
function transformPackageJson(source) {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(source);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const scripts = parsed.scripts;
|
|
114
|
+
if (typeof scripts !== "object" || scripts == null || Array.isArray(scripts)) return null;
|
|
115
|
+
let modified = false;
|
|
116
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
117
|
+
if (typeof value !== "string") continue;
|
|
118
|
+
if (!COMMAND_PATTERN.test(value)) continue;
|
|
119
|
+
const updated = transformShellLikeText(value);
|
|
120
|
+
if (updated != null) {
|
|
121
|
+
scripts[name] = updated;
|
|
122
|
+
modified = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!modified) return null;
|
|
126
|
+
const trailing = source.endsWith("\n") ? "\n" : "";
|
|
127
|
+
return JSON.stringify(parsed, null, 2) + trailing;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Strip `{ "input": ... }` wrappers from `tailor-sdk function test-run --arg` JSON.
|
|
131
|
+
*
|
|
132
|
+
* In v2 the resolver `--arg` JSON must be the input fields directly. Old format
|
|
133
|
+
* wrapped them under `input` and is removed.
|
|
134
|
+
* @param source - File contents
|
|
135
|
+
* @param filePath - Absolute path to the file (used to dispatch by extension)
|
|
136
|
+
* @returns Transformed source or null when nothing matched.
|
|
137
|
+
*/
|
|
138
|
+
function transform(source, filePath) {
|
|
139
|
+
if (!source.includes("tailor-sdk")) return null;
|
|
140
|
+
if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
|
|
141
|
+
return transformShellLikeText(source);
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export { transform as default };
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import * as url from "node:url";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import * as path from "pathe";
|
|
5
|
-
import { arg, defineCommand, runMain } from "politty";
|
|
6
5
|
import { readPackageJSON } from "pkg-types";
|
|
6
|
+
import { arg, defineCommand, runMain } from "politty";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { gte, lt, valid } from "semver";
|
|
9
9
|
import * as fs from "node:fs";
|
|
@@ -13,15 +13,93 @@ import { structuredPatch } from "diff";
|
|
|
13
13
|
import picomatch from "picomatch";
|
|
14
14
|
//#region src/registry.ts
|
|
15
15
|
const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods");
|
|
16
|
-
const allCodemods = [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const allCodemods = [
|
|
17
|
+
{
|
|
18
|
+
id: "v2/define-generators-to-plugins",
|
|
19
|
+
name: "defineGenerators → definePlugins",
|
|
20
|
+
description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports",
|
|
21
|
+
since: "1.0.0",
|
|
22
|
+
until: "2.0.0",
|
|
23
|
+
scriptPath: "v2/define-generators-to-plugins/scripts/transform.js",
|
|
24
|
+
legacyPatterns: ["defineGenerators"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "v2/test-run-arg-input",
|
|
28
|
+
name: "function test-run --arg input unwrap",
|
|
29
|
+
description: "Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs",
|
|
30
|
+
since: "1.0.0",
|
|
31
|
+
until: "2.0.0",
|
|
32
|
+
scriptPath: "v2/test-run-arg-input/scripts/transform.js",
|
|
33
|
+
filePatterns: [
|
|
34
|
+
"**/package.json",
|
|
35
|
+
"**/*.{sh,bash,zsh}",
|
|
36
|
+
"**/*.md"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "v2/sdk-skills-shim",
|
|
41
|
+
name: "tailor-sdk-skills → tailor-sdk skills install",
|
|
42
|
+
description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`",
|
|
43
|
+
since: "1.0.0",
|
|
44
|
+
until: "2.0.0",
|
|
45
|
+
scriptPath: "v2/sdk-skills-shim/scripts/transform.js",
|
|
46
|
+
filePatterns: [
|
|
47
|
+
"**/package.json",
|
|
48
|
+
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
49
|
+
"**/*.md"
|
|
50
|
+
],
|
|
51
|
+
legacyPatterns: ["tailor-sdk-skills"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "v2/principal-unify",
|
|
55
|
+
name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal",
|
|
56
|
+
description: "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`",
|
|
57
|
+
since: "1.0.0",
|
|
58
|
+
until: "2.0.0",
|
|
59
|
+
scriptPath: "v2/principal-unify/scripts/transform.js",
|
|
60
|
+
legacyPatterns: [
|
|
61
|
+
"TailorUser",
|
|
62
|
+
"TailorActor",
|
|
63
|
+
"TailorInvoker",
|
|
64
|
+
"unauthenticatedTailorUser"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "v2/apply-to-deploy",
|
|
69
|
+
name: "tailor-sdk apply → tailor-sdk deploy",
|
|
70
|
+
description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the v2-recommended `tailor-sdk deploy` alias",
|
|
71
|
+
since: "1.0.0",
|
|
72
|
+
until: "2.0.0",
|
|
73
|
+
scriptPath: "v2/apply-to-deploy/scripts/transform.js",
|
|
74
|
+
filePatterns: [
|
|
75
|
+
"**/package.json",
|
|
76
|
+
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
77
|
+
"**/*.md"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "v2/cli-rename",
|
|
82
|
+
name: "v2 CLI rename (single-word commands)",
|
|
83
|
+
description: "Rewrite `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across package.json scripts, shell scripts, CI configs, and docs",
|
|
84
|
+
since: "1.0.0",
|
|
85
|
+
until: "2.0.0",
|
|
86
|
+
scriptPath: "v2/cli-rename/scripts/transform.js",
|
|
87
|
+
filePatterns: [
|
|
88
|
+
"**/package.json",
|
|
89
|
+
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
90
|
+
"**/*.md"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "v2/auth-invoker-unwrap",
|
|
95
|
+
name: "auth.invoker(\"name\") → \"name\"",
|
|
96
|
+
description: "Replace `auth.invoker(\"name\")` calls with the bare `\"name\"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.",
|
|
97
|
+
since: "1.0.0",
|
|
98
|
+
until: "2.0.0",
|
|
99
|
+
scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js",
|
|
100
|
+
legacyPatterns: ["auth.invoker"]
|
|
101
|
+
}
|
|
102
|
+
];
|
|
25
103
|
/**
|
|
26
104
|
* Resolve the absolute path to a codemod script.
|
|
27
105
|
* @param scriptPath - Relative path from the codemods root
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tailor-platform/sdk-codemod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Codemod runner for Tailor Platform SDK upgrades",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"url": "git+https://github.com/tailor-platform/sdk.git",
|
|
9
9
|
"directory": "packages/sdk-codemod"
|
|
10
10
|
},
|
|
11
|
-
"bin": "dist/index.js",
|
|
12
11
|
"files": [
|
|
13
12
|
"CHANGELOG.md",
|
|
14
13
|
"dist",
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
],
|
|
18
17
|
"type": "module",
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@ast-grep/napi": "0.42.
|
|
19
|
+
"@ast-grep/napi": "0.42.2",
|
|
21
20
|
"chalk": "5.6.2",
|
|
22
21
|
"diff": "9.0.0",
|
|
23
22
|
"pathe": "2.0.3",
|
|
@@ -28,25 +27,24 @@
|
|
|
28
27
|
"zod": "4.3.6"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@types/node": "24.12.2",
|
|
30
|
+
"@types/node": "24.12.3",
|
|
33
31
|
"@types/picomatch": "4.0.3",
|
|
34
32
|
"@types/semver": "7.7.1",
|
|
35
|
-
"eslint": "10.3.0",
|
|
36
|
-
"eslint-plugin-oxlint": "1.61.0",
|
|
37
33
|
"oxlint": "1.61.0",
|
|
38
|
-
"tsdown": "0.
|
|
34
|
+
"tsdown": "0.22.0",
|
|
39
35
|
"typescript": "5.9.3",
|
|
40
|
-
"typescript-eslint": "8.59.1",
|
|
41
36
|
"vitest": "4.1.5"
|
|
42
37
|
},
|
|
43
38
|
"scripts": {
|
|
44
39
|
"build": "tsdown",
|
|
45
|
-
"lint": "oxlint .
|
|
46
|
-
"lint:fix": "oxlint
|
|
40
|
+
"lint": "oxlint .",
|
|
41
|
+
"lint:fix": "oxlint . --fix",
|
|
47
42
|
"typecheck": "tsc --noEmit",
|
|
48
43
|
"test": "vitest",
|
|
49
44
|
"prepublish": "pnpm run build",
|
|
50
45
|
"publint": "publint --strict"
|
|
46
|
+
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"sdk-codemod": "dist/index.js"
|
|
51
49
|
}
|
|
52
50
|
}
|