@tailor-platform/sdk-codemod 0.3.9 → 0.4.0-next.10
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 +301 -0
- package/dist/codemods/ast-grep-helpers-CXtWn3RB.js +171 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +22 -4
- package/dist/codemods/v2/auth-attributes-rename/scripts/transform.js +213 -0
- package/dist/codemods/v2/auth-connection-token-helper/scripts/transform.js +243 -0
- package/dist/codemods/v2/auth-invoker-call-unwrap/scripts/transform.js +7 -0
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +108 -13
- package/dist/codemods/v2/cli-rename/scripts/transform.js +373 -14
- package/dist/codemods/v2/db-type-to-table/scripts/transform.js +383 -0
- package/dist/codemods/v2/env-var-rename/scripts/transform.js +88 -0
- package/dist/codemods/v2/erd-site-to-plugin/scripts/transform.js +195 -0
- package/dist/codemods/v2/exec-job-function-rename/scripts/transform.js +95 -0
- package/dist/codemods/v2/execute-script-arg/scripts/transform.js +60 -0
- package/dist/codemods/v2/forward-relation-name/scripts/transform.js +115 -0
- package/dist/codemods/v2/idp-publish-events-rename/scripts/transform.js +186 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +1555 -44
- package/dist/codemods/v2/rename-bin/scripts/transform.js +1087 -0
- package/dist/codemods/v2/runtime-globals-opt-in/scripts/transform.js +103 -0
- package/dist/codemods/v2/runtime-subpath-namespace/scripts/transform.js +792 -0
- package/dist/codemods/v2/sdk-skills-shim/scripts/transform.js +3 -3
- package/dist/codemods/v2/seed-exec-to-cli-plugin/scripts/transform.js +115 -0
- package/dist/codemods/v2/tailor-output-ignore-dir/scripts/transform.js +14 -0
- package/dist/codemods/v2/tailordb-namespace/scripts/transform.js +5 -4
- package/dist/codemods/v2/wait-point-rename/scripts/transform.js +126 -0
- package/dist/codemods/v2/workflow-trigger-rename/scripts/transform.js +123 -0
- package/dist/index.js +1962 -51
- package/package.json +3 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { a as importSource, c as localDeclarationNames, i as importBindings, o as importSpecNames, r as findImportStatements } from "../../../ast-grep-helpers-CXtWn3RB.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v2/exec-job-function-rename/scripts/transform.ts
|
|
4
|
+
const DEPRECATED_METHOD = "startJobFunction";
|
|
5
|
+
const CANONICAL_METHOD = "execJobFunction";
|
|
6
|
+
const DEPRECATED_OPTIONS = "StartJobFunctionOptions";
|
|
7
|
+
const CANONICAL_OPTIONS = "ExecJobFunctionOptions";
|
|
8
|
+
const WORKFLOW_MODULE_SOURCES = /* @__PURE__ */ new Set(["@tailor-platform/sdk/runtime", "@tailor-platform/sdk/runtime/workflow"]);
|
|
9
|
+
function quickFilter(source) {
|
|
10
|
+
return source.includes(DEPRECATED_METHOD) || source.includes(DEPRECATED_OPTIONS);
|
|
11
|
+
}
|
|
12
|
+
function sourceLang(filePath, source) {
|
|
13
|
+
const lower = filePath.toLowerCase();
|
|
14
|
+
if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) return Lang.Tsx;
|
|
15
|
+
return source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
|
|
16
|
+
}
|
|
17
|
+
function collectWorkflowLocals(root, imports) {
|
|
18
|
+
const locals = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const importStmt of imports) for (const binding of importBindings(importStmt)) if (binding.importedName === "workflow" && WORKFLOW_MODULE_SOURCES.has(binding.source)) locals.add(binding.localName);
|
|
20
|
+
const declaredNames = localDeclarationNames(root);
|
|
21
|
+
for (const name of locals) if (declaredNames.has(name)) locals.delete(name);
|
|
22
|
+
return locals;
|
|
23
|
+
}
|
|
24
|
+
function tailorIsAmbientGlobal(root, imports) {
|
|
25
|
+
if (localDeclarationNames(root).has("tailor")) return false;
|
|
26
|
+
return !imports.some((stmt) => importBindings(stmt).some((b) => b.localName === "tailor"));
|
|
27
|
+
}
|
|
28
|
+
function isAmbientTailorWorkflow(object) {
|
|
29
|
+
if (!object || object.kind() !== "member_expression") return false;
|
|
30
|
+
const base = object.field("object");
|
|
31
|
+
const property = object.field("property");
|
|
32
|
+
return base?.kind() === "identifier" && base.text() === "tailor" && property?.text() === "workflow";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rename `StartJobFunctionOptions` import specifiers from the runtime module,
|
|
36
|
+
* plus the type references that resolve to them when the import is not aliased.
|
|
37
|
+
* @param root - Parsed file root
|
|
38
|
+
* @param imports - Top-level import statements
|
|
39
|
+
* @returns Edits renaming the deprecated options type
|
|
40
|
+
*/
|
|
41
|
+
function optionsTypeEdits(root, imports) {
|
|
42
|
+
const edits = [];
|
|
43
|
+
for (const importStmt of imports) {
|
|
44
|
+
const source = importSource(importStmt);
|
|
45
|
+
if (source === null || !WORKFLOW_MODULE_SOURCES.has(source)) continue;
|
|
46
|
+
const specs = importStmt.findAll({ rule: { kind: "import_specifier" } });
|
|
47
|
+
if (specs.some((spec) => importSpecNames(spec)?.importedName === CANONICAL_OPTIONS)) continue;
|
|
48
|
+
for (const spec of specs) {
|
|
49
|
+
const names = importSpecNames(spec);
|
|
50
|
+
if (names?.importedName !== DEPRECATED_OPTIONS) continue;
|
|
51
|
+
const importedNode = spec.children().find((child) => child.kind() === "identifier");
|
|
52
|
+
if (!importedNode) continue;
|
|
53
|
+
edits.push(importedNode.replace(CANONICAL_OPTIONS));
|
|
54
|
+
if (names.localName !== DEPRECATED_OPTIONS) continue;
|
|
55
|
+
for (const reference of root.findAll({ rule: {
|
|
56
|
+
kind: "type_identifier",
|
|
57
|
+
regex: `^${DEPRECATED_OPTIONS}$`
|
|
58
|
+
} })) edits.push(reference.replace(CANONICAL_OPTIONS));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return edits;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite `.startJobFunction(` member accesses — on the ambient
|
|
65
|
+
* `tailor.workflow` global or on a `workflow` value imported from
|
|
66
|
+
* `@tailor-platform/sdk/runtime(/workflow)` — to the canonical
|
|
67
|
+
* `execJobFunction`, and rename the `StartJobFunctionOptions` type alias.
|
|
68
|
+
* @param source - File contents
|
|
69
|
+
* @param filePath - Absolute path to the file
|
|
70
|
+
* @returns Transformed source or null when nothing matched.
|
|
71
|
+
*/
|
|
72
|
+
function transform(source, filePath) {
|
|
73
|
+
if (!quickFilter(source)) return null;
|
|
74
|
+
const root = parse(sourceLang(filePath ?? "", source), source).root();
|
|
75
|
+
const imports = findImportStatements(root);
|
|
76
|
+
const workflowLocals = collectWorkflowLocals(root, imports);
|
|
77
|
+
const tailorIsAmbient = tailorIsAmbientGlobal(root, imports);
|
|
78
|
+
const edits = optionsTypeEdits(root, imports);
|
|
79
|
+
for (const member of root.findAll({ rule: { kind: "member_expression" } })) {
|
|
80
|
+
const property = member.field("property");
|
|
81
|
+
if (!property || property.kind() !== "property_identifier") continue;
|
|
82
|
+
if (property.text() !== DEPRECATED_METHOD) continue;
|
|
83
|
+
const object = member.field("object");
|
|
84
|
+
if (!object) continue;
|
|
85
|
+
const isWorkflowImportReceiver = object.kind() === "identifier" && workflowLocals.has(object.text());
|
|
86
|
+
const isAmbientReceiver = tailorIsAmbient && isAmbientTailorWorkflow(object);
|
|
87
|
+
if (!isWorkflowImportReceiver && !isAmbientReceiver) continue;
|
|
88
|
+
edits.push(property.replace(CANONICAL_METHOD));
|
|
89
|
+
}
|
|
90
|
+
if (edits.length === 0) return null;
|
|
91
|
+
const result = root.commitEdits(edits);
|
|
92
|
+
return result === source ? null : result;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { transform as default };
|
|
@@ -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 };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { l as stringValue } from "../../../ast-grep-helpers-CXtWn3RB.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v2/forward-relation-name/scripts/transform.ts
|
|
4
|
+
function sourceLang(filePath, source) {
|
|
5
|
+
const lowerPath = filePath.toLowerCase();
|
|
6
|
+
if (/\.(?:ts|mts|cts)$/u.test(lowerPath)) return Lang.TypeScript;
|
|
7
|
+
if (/\.(?:tsx|jsx|js)$/u.test(lowerPath)) return Lang.Tsx;
|
|
8
|
+
return source.includes("</") ? Lang.Tsx : Lang.TypeScript;
|
|
9
|
+
}
|
|
10
|
+
function isRelationCall(call, aliases) {
|
|
11
|
+
const callee = call.children()[0];
|
|
12
|
+
if (!callee) return false;
|
|
13
|
+
if (callee.kind() === "identifier") return aliases.has(callee.text());
|
|
14
|
+
if (callee.kind() === "subscript_expression") {
|
|
15
|
+
const property = literalStringValue(callee.field("index"));
|
|
16
|
+
return property === null || property === "relation";
|
|
17
|
+
}
|
|
18
|
+
if (callee.kind() !== "member_expression") return false;
|
|
19
|
+
return callee.children().findLast((child) => child.kind() === "property_identifier" || child.kind() === "identifier")?.text() === "relation";
|
|
20
|
+
}
|
|
21
|
+
function relationBindingName(pattern) {
|
|
22
|
+
if (pattern.kind() !== "object_pattern") return null;
|
|
23
|
+
for (const child of pattern.children()) {
|
|
24
|
+
if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "relation") return child.text();
|
|
25
|
+
if (child.kind() === "pair_pattern" && stringValue(child.field("key")) === "relation") {
|
|
26
|
+
const value = child.field("value");
|
|
27
|
+
return value?.kind() === "identifier" ? value.text() : null;
|
|
28
|
+
}
|
|
29
|
+
if (child.kind() === "object_assignment_pattern") {
|
|
30
|
+
const binding = child.children().find((node) => node.kind() === "shorthand_property_identifier_pattern");
|
|
31
|
+
if (binding?.text() === "relation") return binding.text();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function relationAliases(root) {
|
|
37
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
38
|
+
for (const pattern of root.findAll({ rule: { kind: "object_pattern" } })) {
|
|
39
|
+
const name = relationBindingName(pattern);
|
|
40
|
+
if (name) aliases.add(name);
|
|
41
|
+
}
|
|
42
|
+
return aliases;
|
|
43
|
+
}
|
|
44
|
+
function callArgument(call) {
|
|
45
|
+
const args = call.children().find((child) => child.kind() === "arguments");
|
|
46
|
+
if (!args) return null;
|
|
47
|
+
const values = args.children().filter((child) => {
|
|
48
|
+
const kind = child.kind();
|
|
49
|
+
return kind !== "(" && kind !== ")" && kind !== "," && kind !== "comment";
|
|
50
|
+
});
|
|
51
|
+
return values.length === 1 ? values[0] : null;
|
|
52
|
+
}
|
|
53
|
+
function pairKey(pair) {
|
|
54
|
+
const key = pair.children()[0];
|
|
55
|
+
return stringValue(key ?? null);
|
|
56
|
+
}
|
|
57
|
+
function pairValue(pair) {
|
|
58
|
+
const children = pair.children();
|
|
59
|
+
const colonIndex = children.findIndex((child) => child.kind() === ":");
|
|
60
|
+
if (colonIndex === -1) return null;
|
|
61
|
+
return children.slice(colonIndex + 1).find((child) => child.kind() !== "comment") ?? null;
|
|
62
|
+
}
|
|
63
|
+
function objectPair(object, key) {
|
|
64
|
+
return object.children().find((child) => child.kind() === "pair" && pairKey(child) === key) ?? null;
|
|
65
|
+
}
|
|
66
|
+
function literalStringValue(node) {
|
|
67
|
+
if (node?.kind() !== "string") return null;
|
|
68
|
+
return stringValue(node);
|
|
69
|
+
}
|
|
70
|
+
function hasDynamicProperties(object) {
|
|
71
|
+
return object.children().some((child) => {
|
|
72
|
+
const kind = child.kind();
|
|
73
|
+
if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false;
|
|
74
|
+
if (kind !== "pair") return true;
|
|
75
|
+
const keyKind = child.children()[0]?.kind();
|
|
76
|
+
return keyKind !== "property_identifier" && keyKind !== "string";
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function needsReview(call) {
|
|
80
|
+
const config = callArgument(call);
|
|
81
|
+
if (config?.kind() !== "object") return config != null;
|
|
82
|
+
if (hasDynamicProperties(config)) return true;
|
|
83
|
+
const relationType = objectPair(config, "type");
|
|
84
|
+
const toward = objectPair(config, "toward");
|
|
85
|
+
if (!relationType || !toward) return false;
|
|
86
|
+
if (literalStringValue(pairValue(relationType)) === "keyOnly") return false;
|
|
87
|
+
const towardConfig = pairValue(toward);
|
|
88
|
+
if (towardConfig?.kind() !== "object") return towardConfig != null;
|
|
89
|
+
if (hasDynamicProperties(towardConfig)) return true;
|
|
90
|
+
const targetType = objectPair(towardConfig, "type");
|
|
91
|
+
if (targetType && literalStringValue(pairValue(targetType)) === "self") return false;
|
|
92
|
+
const as = objectPair(towardConfig, "as");
|
|
93
|
+
if (as) {
|
|
94
|
+
const explicitName = literalStringValue(pairValue(as));
|
|
95
|
+
return explicitName === null || explicitName.length === 0;
|
|
96
|
+
}
|
|
97
|
+
if (!targetType) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function transform(_source, _filePath) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function reviewFindings(source, filePath, relativePath) {
|
|
104
|
+
if (!source.includes("relation")) return [];
|
|
105
|
+
const root = parse(sourceLang(filePath, source), source).root();
|
|
106
|
+
const aliases = relationAliases(root);
|
|
107
|
+
return root.findAll({ rule: { kind: "call_expression" } }).filter((call) => isRelationCall(call, aliases) && needsReview(call)).map((call) => ({
|
|
108
|
+
file: relativePath,
|
|
109
|
+
line: call.range().start.line + 1,
|
|
110
|
+
message: "Review the v2 forward GraphQL field name or add an explicit toward.as.",
|
|
111
|
+
excerpt: call.text().split("\n", 1)[0].trim()
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { transform as default, reviewFindings };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { a as importSource, c as localDeclarationNames, i as importBindings, l as stringValue, r as findImportStatements, s as isTypeOnlyImport } from "../../../ast-grep-helpers-CXtWn3RB.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v2/idp-publish-events-rename/scripts/transform.ts
|
|
4
|
+
const SDK_MODULE = "@tailor-platform/sdk";
|
|
5
|
+
const DEFINE_IDP = "defineIdp";
|
|
6
|
+
const LEGACY_KEY = "publishUserEvents";
|
|
7
|
+
const NEW_KEY = "publishEvents";
|
|
8
|
+
function sourceLang(filePath, source) {
|
|
9
|
+
return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes("</") ? Lang.Tsx : Lang.TypeScript;
|
|
10
|
+
}
|
|
11
|
+
function collectDefineIdpNames(root) {
|
|
12
|
+
const direct = /* @__PURE__ */ new Set();
|
|
13
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const importStmt of findImportStatements(root)) {
|
|
15
|
+
if (importSource(importStmt) !== SDK_MODULE || isTypeOnlyImport(importStmt)) continue;
|
|
16
|
+
for (const binding of importBindings(importStmt)) if (binding.importedName === DEFINE_IDP) direct.add(binding.localName);
|
|
17
|
+
for (const namespaceImport of importStmt.findAll({ rule: { kind: "namespace_import" } })) for (const identifier of namespaceImport.children()) if (identifier.kind() === "identifier") namespaces.add(identifier.text());
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
direct,
|
|
21
|
+
namespaces
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function memberProperty(member) {
|
|
25
|
+
return member.children().findLast((child) => child.kind() === "property_identifier" || child.kind() === "identifier") ?? null;
|
|
26
|
+
}
|
|
27
|
+
function isDefineIdpCall(call, names) {
|
|
28
|
+
const callee = call.children()[0];
|
|
29
|
+
if (!callee) return false;
|
|
30
|
+
if (callee.kind() === "identifier") return names.direct.has(callee.text());
|
|
31
|
+
if (callee.kind() !== "member_expression") return false;
|
|
32
|
+
const object = callee.field("object");
|
|
33
|
+
return object?.kind() === "identifier" && names.namespaces.has(object.text()) && memberProperty(callee)?.text() === DEFINE_IDP;
|
|
34
|
+
}
|
|
35
|
+
function objectArguments(call) {
|
|
36
|
+
const args = call.children().find((child) => child.kind() === "arguments");
|
|
37
|
+
if (!args) return [];
|
|
38
|
+
return args.children().filter((child) => child.kind() === "object");
|
|
39
|
+
}
|
|
40
|
+
function findLegacyEntries(object) {
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const child of object.children()) {
|
|
43
|
+
if (child.kind() === "shorthand_property_identifier" && child.text() === LEGACY_KEY) {
|
|
44
|
+
entries.push({
|
|
45
|
+
node: child,
|
|
46
|
+
shorthand: true
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (child.kind() !== "pair") continue;
|
|
51
|
+
const key = child.children()[0];
|
|
52
|
+
if (!key) continue;
|
|
53
|
+
if (key.kind() !== "property_identifier" && key.kind() !== "string") continue;
|
|
54
|
+
if (stringValue(key) !== LEGACY_KEY) continue;
|
|
55
|
+
entries.push({
|
|
56
|
+
node: key,
|
|
57
|
+
shorthand: false
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
function renameEdit(entry) {
|
|
63
|
+
if (entry.shorthand) return entry.node.replace(`${NEW_KEY}: ${LEGACY_KEY}`);
|
|
64
|
+
const text = entry.node.text();
|
|
65
|
+
if (entry.node.kind() !== "string") return entry.node.replace(NEW_KEY);
|
|
66
|
+
const quote = text.startsWith("'") ? "'" : text.startsWith("`") ? "`" : "\"";
|
|
67
|
+
return entry.node.replace(`${quote}${NEW_KEY}${quote}`);
|
|
68
|
+
}
|
|
69
|
+
function parseRoot(source, filePath) {
|
|
70
|
+
try {
|
|
71
|
+
return parse(sourceLang(filePath, source), source).root();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function shadowsDefineIdp(root, names) {
|
|
77
|
+
const declared = localDeclarationNames(root);
|
|
78
|
+
return [...names.direct, ...names.namespaces].some((name) => declared.has(name));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rename the `publishUserEvents` option to `publishEvents` on `defineIdp` calls.
|
|
82
|
+
* @param source - File contents
|
|
83
|
+
* @param filePath - Path to the file being transformed
|
|
84
|
+
* @returns Transformed source, or null when nothing matched
|
|
85
|
+
*/
|
|
86
|
+
function transform(source, filePath = "") {
|
|
87
|
+
if (!source.includes(LEGACY_KEY) || !source.includes(SDK_MODULE)) return null;
|
|
88
|
+
const root = parseRoot(source, filePath);
|
|
89
|
+
if (!root) return null;
|
|
90
|
+
const names = collectDefineIdpNames(root);
|
|
91
|
+
if (names.direct.size === 0 && names.namespaces.size === 0) return null;
|
|
92
|
+
if (shadowsDefineIdp(root, names)) return null;
|
|
93
|
+
const edits = [];
|
|
94
|
+
for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
|
|
95
|
+
if (!isDefineIdpCall(call, names)) continue;
|
|
96
|
+
for (const object of objectArguments(call)) for (const entry of findLegacyEntries(object)) edits.push(renameEdit(entry));
|
|
97
|
+
}
|
|
98
|
+
return edits.length > 0 ? root.commitEdits(edits) : null;
|
|
99
|
+
}
|
|
100
|
+
function lineOf(node) {
|
|
101
|
+
return node.range().start.line + 1;
|
|
102
|
+
}
|
|
103
|
+
function excerptOf(node) {
|
|
104
|
+
return node.text().split("\n", 1)[0].trim();
|
|
105
|
+
}
|
|
106
|
+
function coversIndex(ranges, index) {
|
|
107
|
+
return ranges.some((range) => index >= range.start && index < range.end);
|
|
108
|
+
}
|
|
109
|
+
/** Quoted keys, e.g. `{ "publishUserEvents": true }`, whose node kind is a string. */
|
|
110
|
+
function quotedLegacyKeys(root) {
|
|
111
|
+
return root.findAll({ rule: { kind: "pair" } }).flatMap((pair) => {
|
|
112
|
+
const key = pair.children()[0];
|
|
113
|
+
return key?.kind() === "string" && stringValue(key) === LEGACY_KEY ? [key] : [];
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Computed keys whose value cannot be read statically, e.g. `[key]: value`. */
|
|
117
|
+
function computedKeyEntries(object) {
|
|
118
|
+
return object.children().filter((child) => child.kind() === "pair").flatMap((pair) => {
|
|
119
|
+
const key = pair.children()[0];
|
|
120
|
+
return key?.kind() === "computed_property_name" ? [key] : [];
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Report `publishUserEvents` occurrences the transform cannot rewrite.
|
|
125
|
+
*
|
|
126
|
+
* Occurrences nested deeper inside a `defineIdp` options object belong to a
|
|
127
|
+
* different option shape (e.g. `userAuthPolicy`) and are left alone, so only
|
|
128
|
+
* what the transform genuinely missed is reported.
|
|
129
|
+
* @param source - File contents
|
|
130
|
+
* @param filePath - Path to the file being reviewed
|
|
131
|
+
* @param relativePath - Repository-relative path reported to the user
|
|
132
|
+
* @returns Findings for occurrences needing a manual rename
|
|
133
|
+
*/
|
|
134
|
+
function reviewFindings(source, filePath, relativePath) {
|
|
135
|
+
if (!source.includes(LEGACY_KEY) || !source.includes(SDK_MODULE)) return [];
|
|
136
|
+
const root = parseRoot(source, filePath);
|
|
137
|
+
if (!root) return [];
|
|
138
|
+
const names = collectDefineIdpNames(root);
|
|
139
|
+
const hasDefineIdp = names.direct.size > 0 || names.namespaces.size > 0;
|
|
140
|
+
if (hasDefineIdp && shadowsDefineIdp(root, names)) return [{
|
|
141
|
+
file: relativePath,
|
|
142
|
+
line: 1,
|
|
143
|
+
message: `A local declaration shadows the SDK ${DEFINE_IDP} import; rename ${LEGACY_KEY} to ${NEW_KEY} by hand.`,
|
|
144
|
+
excerpt: LEGACY_KEY
|
|
145
|
+
}];
|
|
146
|
+
const rewritten = /* @__PURE__ */ new Set();
|
|
147
|
+
const optionRanges = [];
|
|
148
|
+
const findings = [];
|
|
149
|
+
if (hasDefineIdp) for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
|
|
150
|
+
if (!isDefineIdpCall(call, names)) continue;
|
|
151
|
+
for (const object of objectArguments(call)) {
|
|
152
|
+
const range = object.range();
|
|
153
|
+
optionRanges.push({
|
|
154
|
+
start: range.start.index,
|
|
155
|
+
end: range.end.index
|
|
156
|
+
});
|
|
157
|
+
for (const entry of findLegacyEntries(object)) rewritten.add(entry.node.range().start.index);
|
|
158
|
+
for (const key of computedKeyEntries(object)) findings.push({
|
|
159
|
+
file: relativePath,
|
|
160
|
+
line: lineOf(key),
|
|
161
|
+
message: `A computed ${DEFINE_IDP} option key may be ${LEGACY_KEY}; rename it to ${NEW_KEY} if so.`,
|
|
162
|
+
excerpt: excerptOf(key)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const residual = [...root.findAll({ rule: { any: [{
|
|
167
|
+
kind: "property_identifier",
|
|
168
|
+
regex: `^${LEGACY_KEY}$`
|
|
169
|
+
}, {
|
|
170
|
+
kind: "shorthand_property_identifier",
|
|
171
|
+
regex: `^${LEGACY_KEY}$`
|
|
172
|
+
}] } }), ...quotedLegacyKeys(root)];
|
|
173
|
+
for (const node of residual) {
|
|
174
|
+
const index = node.range().start.index;
|
|
175
|
+
if (rewritten.has(index) || coversIndex(optionRanges, index)) continue;
|
|
176
|
+
findings.push({
|
|
177
|
+
file: relativePath,
|
|
178
|
+
line: lineOf(node),
|
|
179
|
+
message: `Rename the IdP option ${LEGACY_KEY} to ${NEW_KEY}.`,
|
|
180
|
+
excerpt: excerptOf(node)
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { transform as default, reviewFindings };
|