@tailor-platform/sdk-codemod 0.3.0-next.1 → 0.3.0-next.2

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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # @tailor-platform/sdk-codemod
2
2
 
3
+ ## 0.3.0-next.2
4
+ ### Minor Changes
5
+
6
+
7
+
8
+ - [#1473](https://github.com/tailor-platform/sdk/pull/1473) [`7ddf3c7`](https://github.com/tailor-platform/sdk/commit/7ddf3c716adf85a66a75d554da7730b5406f84b1) Thanks [@dqn](https://github.com/dqn)! - Add the `v2/principal-unify` codemod so `tailor-sdk upgrade` can migrate SDK principal APIs to `TailorPrincipal`.
9
+
10
+
11
+ ### Patch Changes
12
+
13
+
14
+
15
+ - [#1515](https://github.com/tailor-platform/sdk/pull/1515) [`dcf66a1`](https://github.com/tailor-platform/sdk/commit/dcf66a1e648f5287eaea9ea330eb4ad726a4d363) Thanks [@dqn](https://github.com/dqn)! - Rewrite `tailor-sdk apply` to `tailor-sdk deploy` in source files that contain embedded CLI command strings.
16
+
17
+
18
+
19
+ - [#1482](https://github.com/tailor-platform/sdk/pull/1482) [`8b5870e`](https://github.com/tailor-platform/sdk/commit/8b5870e85db1efec7647acb98226f8161e3d1583) Thanks [@toiroakr](https://github.com/toiroakr)! - Add LLM-assisted review support to the codemod runner. A codemod can declare `suspiciousPatterns` plus a `prompt`; after running, files whose post-transform content still matches a suspicious pattern are reported as `llmReviews` (in the JSON output and on stderr) together with the codemod's migration prompt. This surfaces the cases a deterministic transform cannot safely complete (e.g. a value reached through a variable) so they can be finished with an LLM. The `auth.invoker(...)` codemod adopts this for its non-literal-argument cases.
20
+
21
+
22
+
23
+ - [#1495](https://github.com/tailor-platform/sdk/pull/1495) [`6234022`](https://github.com/tailor-platform/sdk/commit/6234022d7dc03813b8dade831b86f63a5f7a20e6) Thanks [@toiroakr](https://github.com/toiroakr)! - Generate the v2 migration guide (`packages/sdk/docs/migration/v2.md`) from the codemod registry, which is the single source of truth. Each entry renders its name, automation level (Automatic / Partially automatic / Manual), description, optional before/after `examples`, and — for changes the codemods cannot fully migrate on their own — the LLM/manual migration prompt. Run `pnpm codemod:docs:update` to regenerate and `pnpm codemod:docs:check` (wired into `pnpm check`) to verify it is in sync.
24
+
25
+ `scriptPath` is now optional, so the registry can also describe codemod-less ("manual") migrations that ship only guidance (`examples` / `prompt` / `suspiciousPatterns`) with no automatic transform. A manual entry with a `prompt` but no scoping pattern is surfaced as a project-wide `llmReviews` entry at runtime.
26
+
27
+ Add a `sdk-codemod list` command that prints every registered rule (id, name, kind, version range).
28
+
29
+
30
+ - [#1517](https://github.com/tailor-platform/sdk/pull/1517) [`a649764`](https://github.com/tailor-platform/sdk/commit/a6497649be2786b3f6e410c8aa98c4247a599258) Thanks [@dqn](https://github.com/dqn)! - Reduce false-positive v2 codemod warnings and LLM-review prompts from source comments, string literals, and identifier substring matches.
31
+
32
+
33
+
34
+ - [#1476](https://github.com/tailor-platform/sdk/pull/1476) [`fa83075`](https://github.com/tailor-platform/sdk/commit/fa83075f5e0e91085c0ef0cb44b7058a28a79ec3) Thanks [@toiroakr](https://github.com/toiroakr)! - `executeScript` now takes its `arg` as a JSON-serializable value instead of a pre-serialized JSON string. Pass the value directly (e.g. `arg: { a: 1 }`) instead of `arg: JSON.stringify({ a: 1 })`.
35
+
36
+ Add the `v2/execute-script-arg` codemod, which unwraps `JSON.stringify(...)` passed as the `executeScript` `arg` option. Indirect forms (a stringified value held in a variable, etc.) cannot be rewritten automatically and are surfaced as an LLM-assisted review task with a migration prompt.
37
+
38
+
39
+ - [#1518](https://github.com/tailor-platform/sdk/pull/1518) [`ab10b1f`](https://github.com/tailor-platform/sdk/commit/ab10b1fea309ec5496e09bdca394d46d58603f5f) Thanks [@dqn](https://github.com/dqn)! - Reduce noisy `executeScript` LLM-review prompts by flagging files only when unresolved `arg` stringification remains likely.
40
+
41
+
42
+
43
+ - [#1509](https://github.com/tailor-platform/sdk/pull/1509) [`7cadaa7`](https://github.com/tailor-platform/sdk/commit/7cadaa7c4987b81130ca80ba80bc5d5b26276394) Thanks [@dqn](https://github.com/dqn)! - Rename resolver, executor, workflow trigger, and typed workflow start machine-user options from `authInvoker` to `invoker`.
44
+
45
+ Update create-sdk templates and the v2 auth invoker codemod to generate the new `invoker` option.
46
+
47
+
48
+ - [#1519](https://github.com/tailor-platform/sdk/pull/1519) [`4e3fa47`](https://github.com/tailor-platform/sdk/commit/4e3fa47d24e6bb1145eac13c355e976f2d594851) Thanks [@dqn](https://github.com/dqn)! - Limit the `openDownloadStream` migration review prompt to files that reference deprecated download stream APIs.
49
+
50
+
51
+
52
+ - [#1521](https://github.com/tailor-platform/sdk/pull/1521) [`2d0689e`](https://github.com/tailor-platform/sdk/commit/2d0689e8ac0079473294fab367799a5431c130f4) Thanks [@dqn](https://github.com/dqn)! - Flag files that need project-specific review after the v2 principal migration, including resolver helper adapters and nullable `caller` follow-ups.
53
+
54
+
55
+
56
+ - [#1525](https://github.com/tailor-platform/sdk/pull/1525) [`425a19d`](https://github.com/tailor-platform/sdk/commit/425a19dd58da6e373b739d3b3e838c2ff3d1736a) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency semver to v7.8.5
57
+
58
+
59
+
60
+ - [#1520](https://github.com/tailor-platform/sdk/pull/1520) [`ed3d338`](https://github.com/tailor-platform/sdk/commit/ed3d338ce71d68904ef1fb83afbbd06a7e5f6973) Thanks [@dqn](https://github.com/dqn)! - Flag files that still reference ambient Tailor runtime globals so the v2 migration can opt them into `@tailor-platform/sdk/runtime/globals`.
61
+
62
+
63
+
64
+ - [#1439](https://github.com/tailor-platform/sdk/pull/1439) [`c5b10d2`](https://github.com/tailor-platform/sdk/commit/c5b10d2841ded08927285bce538c05220cde5e4c) Thanks [@dqn](https://github.com/dqn)! - Unify function principal context around `TailorPrincipal`.
65
+
66
+ Resolver contexts now use `caller` and `invoker` as `TailorPrincipal | null`, workflow and executor invokers also use `TailorPrincipal | null`, and event executor `actor` uses `TailorPrincipal | null` with `id`/`type` fields. The legacy `TailorUser`, `TailorInvoker`, `TailorActor`, `TailorActorType`, and `unauthenticatedTailorUser` exports are removed.
67
+
3
68
  ## 0.3.0-next.1
4
69
  ### Patch Changes
5
70
 
@@ -3,6 +3,16 @@ import * as path from "pathe";
3
3
  const ARG_VALUE = `(?:[^\\s'"\`;&|]+|'[^']*'|"(?:(?:\\\\.)|[^"\\\\])*")`;
4
4
  const GLOBAL_ARG_PATTERN = `(?:(?:\\s+(?:--verbose|--json|-j))|(?:\\s+(?:--env-file|--env-file-if-exists|-e)(?:=${ARG_VALUE}|\\s+${ARG_VALUE})))*`;
5
5
  const APPLY_PATTERN = new RegExp(`(?<![\\w-])tailor-sdk(?:@[^\\s'"\`]+)?(?![\\w-])(${GLOBAL_ARG_PATTERN}\\s+)apply(?![-\\w])`, "g");
6
+ const SOURCE_EXTENSIONS = new Set([
7
+ ".ts",
8
+ ".tsx",
9
+ ".mts",
10
+ ".cts",
11
+ ".js",
12
+ ".jsx",
13
+ ".mjs",
14
+ ".cjs"
15
+ ]);
6
16
  function replaceApply(value) {
7
17
  return value.replace(APPLY_PATTERN, (match) => `${match.slice(0, -5)}deploy`);
8
18
  }
@@ -12,6 +22,10 @@ function transformText(source) {
12
22
  const updated = replaceApply(source);
13
23
  return updated === source ? null : updated;
14
24
  }
25
+ function transformSourceText(source) {
26
+ const updated = source.split(/(\r\n|\n|\r)/).map((part, index) => index % 2 === 0 ? replaceApply(part) : part).join("");
27
+ return updated === source ? null : updated;
28
+ }
15
29
  function transformPackageJson(source) {
16
30
  let parsed;
17
31
  try {
@@ -44,7 +58,9 @@ function transformPackageJson(source) {
44
58
  */
45
59
  function transform(source, filePath) {
46
60
  if (!source.includes("tailor-sdk")) return null;
47
- if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
61
+ const ext = path.extname(filePath).toLowerCase();
62
+ if (ext === ".json") return transformPackageJson(source);
63
+ if (SOURCE_EXTENSIONS.has(ext)) return transformSourceText(source);
48
64
  return transformText(source);
49
65
  }
50
66
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import { Lang, parse } from "@ast-grep/napi";
2
2
  //#region codemods/v2/auth-invoker-unwrap/scripts/transform.ts
3
- const QUICK_FILTER_NEEDLE = "auth.invoker";
3
+ const QUICK_FILTER_NEEDLES = ["auth.invoker", "authInvoker"];
4
4
  function quickFilter(source) {
5
- return source.includes(QUICK_FILTER_NEEDLE);
5
+ return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle));
6
6
  }
7
7
  function isInsideImportStatement(node) {
8
8
  let current = node.parent();
@@ -121,14 +121,101 @@ function findAuthImports(root) {
121
121
  return false;
122
122
  });
123
123
  }
124
+ function sameRange(a, b) {
125
+ const ar = a.range();
126
+ const br = b.range();
127
+ return ar.start.index === br.start.index && ar.end.index === br.end.index;
128
+ }
129
+ function keyText(node) {
130
+ if (!node) return null;
131
+ return node.text().replace(/^['"]|['"]$/g, "");
132
+ }
133
+ function expressionArguments(args) {
134
+ return args.children().filter((child) => ![
135
+ "(",
136
+ ")",
137
+ ","
138
+ ].includes(child.kind()));
139
+ }
140
+ function argumentCallForObject(objectNode) {
141
+ const args = objectNode.parent();
142
+ const call = args?.parent();
143
+ if (args?.kind() !== "arguments" || call?.kind() !== "call_expression") return null;
144
+ const index = expressionArguments(args).findIndex((arg) => sameRange(arg, objectNode));
145
+ return index === -1 ? null : {
146
+ call,
147
+ index
148
+ };
149
+ }
150
+ function calleeText(call) {
151
+ return call.field("function")?.text() ?? "";
152
+ }
153
+ function isCreateCallOptionObject(objectNode, functionName) {
154
+ const callInfo = argumentCallForObject(objectNode);
155
+ return callInfo?.index === 0 && callInfo.call.field("function")?.kind() === "identifier" && calleeText(callInfo.call) === functionName;
156
+ }
157
+ function isExecutorOperationObject(objectNode) {
158
+ const operationPair = objectNode.parent();
159
+ if (operationPair?.kind() !== "pair" || keyText(operationPair.field("key")) !== "operation") return false;
160
+ const configObject = operationPair.parent();
161
+ return configObject?.kind() === "object" && isCreateCallOptionObject(configObject, "createExecutor");
162
+ }
163
+ function isSupportedInvokerOptionObject(objectNode) {
164
+ return isCreateCallOptionObject(objectNode, "createResolver") || isCreateCallOptionObject(objectNode, "startWorkflow") || isExecutorOperationObject(objectNode);
165
+ }
166
+ function optionObjectForPairKey(node) {
167
+ const parent = node.parent();
168
+ if (!parent || parent.kind() !== "pair") return null;
169
+ const key = parent.field("key");
170
+ if (!key || !sameRange(key, node)) return null;
171
+ const objectNode = parent.parent();
172
+ return objectNode?.kind() === "object" ? objectNode : null;
173
+ }
174
+ function isSupportedInvokerOptionKey(node) {
175
+ const objectNode = optionObjectForPairKey(node) ?? node.parent();
176
+ return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode);
177
+ }
178
+ function isSupportedInvokerValueCall(node) {
179
+ const pair = node.parent();
180
+ if (pair?.kind() !== "pair") return false;
181
+ const value = pair.field("value");
182
+ if (!value || !sameRange(value, node)) return false;
183
+ const key = keyText(pair.field("key"));
184
+ if (key !== "authInvoker" && key !== "invoker") return false;
185
+ const objectNode = pair.parent();
186
+ return objectNode?.kind() === "object" && isSupportedInvokerOptionObject(objectNode);
187
+ }
188
+ function findAuthInvokerShorthands(root) {
189
+ return root.findAll({ rule: {
190
+ kind: "shorthand_property_identifier",
191
+ regex: "^authInvoker$"
192
+ } }).filter(isSupportedInvokerOptionKey);
193
+ }
194
+ function findAuthInvokerPropertyKeys(root) {
195
+ return root.findAll({ rule: {
196
+ kind: "property_identifier",
197
+ regex: "^authInvoker$"
198
+ } }).filter(isSupportedInvokerOptionKey);
199
+ }
200
+ function findQuotedAuthInvokerPropertyKeys(root) {
201
+ return root.findAll({ rule: {
202
+ kind: "string",
203
+ regex: "^['\"]authInvoker['\"]$"
204
+ } }).filter(isSupportedInvokerOptionKey);
205
+ }
206
+ function renameQuotedKey(node) {
207
+ const quote = node.text().startsWith("'") ? "'" : "\"";
208
+ return `${quote}invoker${quote}`;
209
+ }
124
210
  /**
125
- * Replace `auth.invoker("name")` calls with the bare `"name"` string literal.
211
+ * Replace `auth.invoker("name")` calls with the bare `"name"` string literal
212
+ * and rename `authInvoker:` option keys to `invoker:`.
126
213
  * If no other `auth` references remain after the rewrite, drop the `auth`
127
214
  * specifier (or the entire import line when `auth` was its sole specifier).
128
215
  *
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.
216
+ * `auth.invoker()` was removed in favor of passing the machine user name
217
+ * directly to `invoker`; carrying the `auth` import only for `.invoker()`
218
+ * would otherwise pull config-layer modules into runtime bundles.
132
219
  * @param source - File contents
133
220
  * @param filePath - Absolute path to the file (kept for the runner signature)
134
221
  * @returns Transformed source or null when nothing matched.
@@ -136,15 +223,16 @@ function findAuthImports(root) {
136
223
  function transform(source, _filePath) {
137
224
  if (!quickFilter(source)) return null;
138
225
  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;
226
+ const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode));
141
227
  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)) {
228
+ edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker")));
229
+ edits.push(...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))));
230
+ edits.push(...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")));
231
+ if (calls.length > 0 && countRemainingAuthRefs(root, calls.map((c) => c.range)) === 0) for (const importStmt of findAuthImports(root)) {
143
232
  const edit = buildAuthImportRemovalEdit(source, importStmt);
144
233
  if (edit) edits.push(edit);
145
234
  }
146
- if (edits.length === 0) return null;
147
- let result = root.commitEdits(edits);
235
+ let result = edits.length === 0 ? source : root.commitEdits(edits);
148
236
  result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n");
149
237
  return result === source ? null : result;
150
238
  }
@@ -0,0 +1,60 @@
1
+ import { Lang, parse } from "@ast-grep/napi";
2
+ //#region codemods/v2/execute-script-arg/scripts/transform.ts
3
+ const NEEDLE = "executeScript";
4
+ function quickFilter(source) {
5
+ return source.includes(NEEDLE) && source.includes("JSON.stringify");
6
+ }
7
+ function pairKeyText(pair) {
8
+ const key = pair.children()[0];
9
+ if (!key) return null;
10
+ return key.text().replace(/^['"]|['"]$/g, "");
11
+ }
12
+ /**
13
+ * True when `stringifyCall` is the value of a top-level `arg:` property in the
14
+ * object literal passed directly to `executeScript(...)`. The chain checked is
15
+ * `JSON.stringify(...)` → pair (`arg:`) → object → arguments → `executeScript`
16
+ * call, so a nested `arg:` (e.g. `executeScript({ opts: { arg: ... } })`) or an
17
+ * unrelated `JSON.stringify` is left untouched.
18
+ */
19
+ function isExecuteScriptArg(stringifyCall) {
20
+ const pair = stringifyCall.parent();
21
+ if (!pair || pair.kind() !== "pair") return false;
22
+ if (pairKeyText(pair) !== "arg") return false;
23
+ const obj = pair.parent();
24
+ if (!obj || obj.kind() !== "object") return false;
25
+ const args = obj.parent();
26
+ if (!args || args.kind() !== "arguments") return false;
27
+ const call = args.parent();
28
+ if (!call || call.kind() !== "call_expression") return false;
29
+ const callee = call.children()[0];
30
+ return !!callee && callee.text() === NEEDLE;
31
+ }
32
+ /**
33
+ * Rewrite `executeScript({ ..., arg: JSON.stringify(X), ... })` to
34
+ * `executeScript({ ..., arg: X, ... })`.
35
+ *
36
+ * In v2 the `executeScript` `arg` option takes a JSON-serializable value and
37
+ * serializes it internally, so a pre-stringified argument double-encodes. Only
38
+ * the literal `arg: JSON.stringify(<single expr>)` form is rewritten; indirect
39
+ * forms (a stringified value held in a variable, `JSON.stringify(x, null, 2)`,
40
+ * etc.) are left for manual migration.
41
+ * @param source - File contents
42
+ * @param _filePath - Absolute path to the file (kept for the runner signature)
43
+ * @returns Transformed source or null when nothing matched.
44
+ */
45
+ function transform(source, _filePath) {
46
+ if (!quickFilter(source)) return null;
47
+ const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
48
+ const edits = [];
49
+ for (const match of root.findAll({ rule: { pattern: "JSON.stringify($X)" } })) {
50
+ if (!isExecuteScriptArg(match)) continue;
51
+ const inner = match.getMatch("X");
52
+ if (!inner) continue;
53
+ edits.push(match.replace(inner.text()));
54
+ }
55
+ if (edits.length === 0) return null;
56
+ const result = root.commitEdits(edits);
57
+ return result === source ? null : result;
58
+ }
59
+ //#endregion
60
+ export { transform as default };