upstreamfix 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/cli.js +167 -0
- package/dist/core/diagnostic-diff.js +41 -0
- package/dist/core/process-cleanup.js +77 -0
- package/dist/core/report.js +131 -0
- package/dist/core/runner.js +826 -0
- package/dist/core/safety.js +7 -0
- package/dist/core/types.js +1 -0
- package/dist/core/verify.js +422 -0
- package/dist/core/write-branch.js +296 -0
- package/dist/migrations/firebase-admin-v14/index.js +597 -0
- package/dist/migrations/firebase-admin-v14/mapping.js +80 -0
- package/dist/migrations/index.js +46 -0
- package/dist/migrations/stripe-shared.js +393 -0
- package/dist/migrations/stripe-v22/index.js +47 -0
- package/dist/migrations/stripe-v23/index.js +55 -0
- package/package.json +49 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { firebaseAdminV14 } from './firebase-admin-v14/index.js';
|
|
4
|
+
import { stripeV22 } from './stripe-v22/index.js';
|
|
5
|
+
import { stripeV23 } from './stripe-v23/index.js';
|
|
6
|
+
/** The shipped migration catalog. Adding a migration requires registering it here, not in core or the CLI. */
|
|
7
|
+
export const migrations = [firebaseAdminV14, stripeV22, stripeV23];
|
|
8
|
+
const ignoredDirectories = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.nuxt', '.turbo', '.cache']);
|
|
9
|
+
export async function findApplicableMigrations(cwd) {
|
|
10
|
+
const manifests = await readManifests(path.resolve(cwd));
|
|
11
|
+
return migrations.filter((migration) => manifests.some((manifest) => {
|
|
12
|
+
const sections = [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies];
|
|
13
|
+
return sections.some((section) => {
|
|
14
|
+
const range = section?.[migration.packageName];
|
|
15
|
+
return typeof range === 'string' && declaredMajor(range) === migration.fromMajor;
|
|
16
|
+
});
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function declaredMajor(range) {
|
|
20
|
+
const match = /^\s*(?:[~^=]|>=?\s*)?(\d+)(?:\.|\s|$)/.exec(range);
|
|
21
|
+
return match ? Number(match[1]) : undefined;
|
|
22
|
+
}
|
|
23
|
+
async function readManifests(root) {
|
|
24
|
+
const manifests = [];
|
|
25
|
+
async function visit(directory) {
|
|
26
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
27
|
+
if (entry.isSymbolicLink())
|
|
28
|
+
continue;
|
|
29
|
+
const absolute = path.join(directory, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (!ignoredDirectories.has(entry.name))
|
|
32
|
+
await visit(absolute);
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile() && entry.name === 'package.json') {
|
|
35
|
+
try {
|
|
36
|
+
manifests.push(JSON.parse(await readFile(absolute, 'utf8')));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// An invalid manifest cannot prove that a migration applies.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await visit(root);
|
|
45
|
+
return manifests;
|
|
46
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
2
|
+
const PACKAGE = 'stripe';
|
|
3
|
+
const REQUEST_OPTION_KEYS = new Set([
|
|
4
|
+
'apiKey', 'apiVersion', 'idempotencyKey', 'maxNetworkRetries', 'stripeAccount', 'timeout', 'host',
|
|
5
|
+
]);
|
|
6
|
+
const REMOVED_RESOURCE_METHODS = new Set([
|
|
7
|
+
'createFullPath', 'createResourcePathWithSymbols', 'extend', 'method', '_joinUrlParts',
|
|
8
|
+
]);
|
|
9
|
+
const ID_FIRST_REQUEST_METHODS = new Set([
|
|
10
|
+
'retrieve', 'update', 'del', 'delete', 'cancel', 'close', 'finalize', 'pay', 'send', 'void',
|
|
11
|
+
'capture', 'confirm', 'resume', 'pause', 'release', 'reverse', 'expire', 'submit', 'reject',
|
|
12
|
+
'accept', 'attach', 'detach', 'increment', 'decrement', 'markUncollectible',
|
|
13
|
+
]);
|
|
14
|
+
const PARAMS_FIRST_REQUEST_METHODS = new Set(['create', 'list', 'search']);
|
|
15
|
+
// Removed from these parameter types in the published Stripe declarations.
|
|
16
|
+
// The destination SDK rejects the old field; replacing it can change payment behavior.
|
|
17
|
+
const REMOVED_PAYMENT_METHOD_TYPES_PARAMS = new Map([
|
|
18
|
+
['paymentIntents.create', 'PaymentIntentCreateParams'],
|
|
19
|
+
['paymentIntents.confirm', 'PaymentIntentConfirmParams'],
|
|
20
|
+
['paymentIntents.update', 'PaymentIntentUpdateParams'],
|
|
21
|
+
['setupIntents.create', 'SetupIntentCreateParams'],
|
|
22
|
+
['setupIntents.update', 'SetupIntentUpdateParams'],
|
|
23
|
+
]);
|
|
24
|
+
export function migrateStripeV22(project) {
|
|
25
|
+
const result = emptyResult();
|
|
26
|
+
for (const sourceFile of project.getSourceFiles())
|
|
27
|
+
migrateV22File(sourceFile, result);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
export function migrateStripeV23(project) {
|
|
31
|
+
const result = emptyResult();
|
|
32
|
+
for (const sourceFile of project.getSourceFiles())
|
|
33
|
+
migrateV23File(sourceFile, result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function emptyResult() {
|
|
37
|
+
return { changes: [], manual: [], filesChanged: [], fileUpdates: [] };
|
|
38
|
+
}
|
|
39
|
+
function migrateV22File(sourceFile, result) {
|
|
40
|
+
const edits = [];
|
|
41
|
+
const before = sourceFile.getFullText();
|
|
42
|
+
const stripeBindings = findStripeBindings(sourceFile);
|
|
43
|
+
const constructorNames = stripeBindings.constructors;
|
|
44
|
+
for (const variable of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
45
|
+
const name = variable.getNameNode();
|
|
46
|
+
const initializer = variable.getInitializer();
|
|
47
|
+
if (!Node.isObjectBindingPattern(name) || !initializer || !isRequireStripe(initializer))
|
|
48
|
+
continue;
|
|
49
|
+
const elements = name.getElements();
|
|
50
|
+
const binding = elements[0];
|
|
51
|
+
const sourceName = binding?.getPropertyNameNode()?.getText() ?? binding?.getNameNode().getText();
|
|
52
|
+
if (elements.length !== 1 || !binding || !['Stripe', 'default'].includes(sourceName ?? '') || !Node.isIdentifier(binding.getNameNode())) {
|
|
53
|
+
addManual(sourceFile, result.manual, variable, 'This destructured CommonJS Stripe import must be rewritten explicitly for v22.', redactedSnippet(variable.getText()));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const alias = binding.getNameNode().getText();
|
|
57
|
+
constructorNames.add(alias);
|
|
58
|
+
stripeBindings.typeNames.add(alias);
|
|
59
|
+
edits.push(edit(sourceFile, name, alias, 'cjs-default-export', name.getText()));
|
|
60
|
+
}
|
|
61
|
+
const clients = findStripeClients(sourceFile, constructorNames);
|
|
62
|
+
for (const property of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
63
|
+
if (!['default', 'Stripe'].includes(property.getName()) || !isRequireStripe(property.getExpression()))
|
|
64
|
+
continue;
|
|
65
|
+
if (property.getParentIfKind(SyntaxKind.CallExpression)?.getExpression() === property)
|
|
66
|
+
continue;
|
|
67
|
+
edits.push(edit(sourceFile, property, property.getExpression().getText(), 'cjs-default-export', property.getText()));
|
|
68
|
+
}
|
|
69
|
+
for (const typeReference of sourceFile.getDescendantsOfKind(SyntaxKind.TypeReference)) {
|
|
70
|
+
const name = typeReference.getTypeName().getText();
|
|
71
|
+
const prefix = [...stripeBindings.typeNames].find((binding) => name === `${binding}.StripeContext`);
|
|
72
|
+
if (prefix)
|
|
73
|
+
edits.push(edit(sourceFile, typeReference, `${prefix}.StripeContextType`, 'stripe-context-type', typeReference.getText()));
|
|
74
|
+
const errorType = [...stripeBindings.typeNames]
|
|
75
|
+
.map((binding) => name.startsWith(`${binding}.errors.`) ? name.slice(`${binding}.errors.`.length) : undefined)
|
|
76
|
+
.find((candidate) => candidate && /^[A-Za-z_$][\w$]*$/.test(candidate));
|
|
77
|
+
if (errorType) {
|
|
78
|
+
const prefix = [...stripeBindings.typeNames].find((binding) => name.startsWith(`${binding}.errors.`));
|
|
79
|
+
edits.push(edit(sourceFile, typeReference, `InstanceType<typeof ${prefix}.errors.${errorType}>`, 'stripe-error-type', typeReference.getText()));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const directive of sourceFile.getTypeReferenceDirectives()) {
|
|
83
|
+
if (directive.getFileName() !== 'stripe/types')
|
|
84
|
+
continue;
|
|
85
|
+
const directivePos = directive.getPos();
|
|
86
|
+
const lineStart = before.lastIndexOf('\n', Math.max(0, directivePos - 1)) + 1;
|
|
87
|
+
const newline = before.indexOf('\n', directivePos);
|
|
88
|
+
const lineEnd = newline < 0 ? before.length : newline + 1;
|
|
89
|
+
const line = before.slice(lineStart, newline < 0 ? before.length : newline).replace(/\r$/, '');
|
|
90
|
+
if (!/^\s*\/\/\/\s*<reference\s+types=(['"])stripe\/types\1\s*\/>\s*$/.test(line))
|
|
91
|
+
continue;
|
|
92
|
+
edits.push({
|
|
93
|
+
start: lineStart,
|
|
94
|
+
end: lineEnd,
|
|
95
|
+
replacement: '',
|
|
96
|
+
rule: 'remove-obsolete-type-reference',
|
|
97
|
+
before: before.slice(lineStart, lineEnd),
|
|
98
|
+
line: sourceFile.getLineAndColumnAtPos(lineStart).line,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
102
|
+
const expression = call.getExpression();
|
|
103
|
+
const [root, ...path] = propertyPath(expression);
|
|
104
|
+
if (root && clients.has(root) && path.length >= 2) {
|
|
105
|
+
const args = call.getArguments();
|
|
106
|
+
const functionArg = args.find((argument) => Node.isArrowFunction(argument) || Node.isFunctionExpression(argument));
|
|
107
|
+
if (functionArg) {
|
|
108
|
+
addManual(sourceFile, result.manual, functionArg, 'Stripe v22 removes callbacks from API service methods. Convert this callback flow to promises/async code and make the containing function async if appropriate.', redactedSnippet(call.getText()));
|
|
109
|
+
}
|
|
110
|
+
const hostArgument = args.find((argument) => Node.isObjectLiteralExpression(argument) && argument.getProperties().some((prop) => Node.isPropertyAssignment(prop) && prop.getName() === 'host'));
|
|
111
|
+
if (hostArgument) {
|
|
112
|
+
addManual(sourceFile, result.manual, hostArgument, 'Stripe v22 removes per-request host overrides. Move the host to the Stripe client configuration after checking that all calls on this client should use it.', redactedSnippet(call.getText()));
|
|
113
|
+
}
|
|
114
|
+
const removedMethod = path.at(-1);
|
|
115
|
+
if (removedMethod && REMOVED_RESOURCE_METHODS.has(removedMethod)) {
|
|
116
|
+
addManual(sourceFile, result.manual, call, `Stripe v22 removes StripeResource.${removedMethod}; there is no safe automatic replacement.`, redactedSnippet(call.getText()));
|
|
117
|
+
}
|
|
118
|
+
// The v22 signature `(id, params?, options?)` needs an explicit params slot
|
|
119
|
+
// only for known ID-first methods called as `(id, options)`. In particular,
|
|
120
|
+
// create/list take `(params?, options?)` and must never gain an `undefined`.
|
|
121
|
+
const method = path.at(-1);
|
|
122
|
+
const optionCandidate = args.length === 2 ? args[1] : undefined;
|
|
123
|
+
if (!hostArgument && method && ID_FIRST_REQUEST_METHODS.has(method) && args.length === 2
|
|
124
|
+
&& isResourceIdArgument(args[0]) && optionCandidate && isRequestOptionsObject(optionCandidate)) {
|
|
125
|
+
edits.push(edit(sourceFile, optionCandidate, `undefined, ${optionCandidate.getText()}`, 'request-options-position', optionCandidate.getText()));
|
|
126
|
+
}
|
|
127
|
+
else if (!hostArgument && method && args.length === 2
|
|
128
|
+
&& ((optionCandidate && containsRequestOptionKey(optionCandidate)
|
|
129
|
+
&& !PARAMS_FIRST_REQUEST_METHODS.has(method)
|
|
130
|
+
&& (!ID_FIRST_REQUEST_METHODS.has(method) || !isResourceIdArgument(args[0]) || !isRequestOptionsObject(optionCandidate)))
|
|
131
|
+
|| (PARAMS_FIRST_REQUEST_METHODS.has(method) && containsRequestOptionKey(args[0])))) {
|
|
132
|
+
addManual(sourceFile, result.manual, call, 'The Stripe request arguments resemble RequestOptions, but their v22 parameter position is ambiguous. Verify this method’s `(id, params?, options?)` or `(params?, options?)` signature and move options manually; no arguments were changed.', redactedSnippet(call.getText()));
|
|
133
|
+
}
|
|
134
|
+
else if (!hostArgument && method && args.length === 1 && containsRequestOptionKey(args[0])) {
|
|
135
|
+
addManual(sourceFile, result.manual, call, 'This Stripe argument may be RequestOptions rather than resource parameters. Confirm the v22 RequestOptions position manually; no arguments were changed.', redactedSnippet(call.getText()));
|
|
136
|
+
}
|
|
137
|
+
const stringApiKey = args.find((argument, index) => index > 0 && isStripeSecretLiteral(argument));
|
|
138
|
+
if (stringApiKey) {
|
|
139
|
+
addManual(sourceFile, result.manual, stringApiKey, 'Stripe v22 no longer accepts a string API key as a service-method argument. Put it in a RequestOptions object under apiKey; the key value is redacted here.', '[API key redacted]');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
144
|
+
const expression = call.getExpression();
|
|
145
|
+
if (Node.isIdentifier(expression) && constructorNames.has(expression.getText())) {
|
|
146
|
+
const parent = call.getParent();
|
|
147
|
+
if (Node.isNewExpression(parent) && parent.getExpression() === expression)
|
|
148
|
+
continue;
|
|
149
|
+
edits.push(edit(sourceFile, expression, `new ${expression.getText()}`, 'stripe-constructor', expression.getText()));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (Node.isPropertyAccessExpression(expression)
|
|
153
|
+
&& ['default', 'Stripe'].includes(expression.getName())
|
|
154
|
+
&& isRequireStripe(expression.getExpression())) {
|
|
155
|
+
edits.push(edit(sourceFile, expression, `new (${expression.getExpression().getText()})`, 'stripe-constructor', expression.getText()));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (isRequireStripe(expression)) {
|
|
159
|
+
edits.push(edit(sourceFile, expression, `new (${expression.getText()})`, 'stripe-constructor', expression.getText()));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
applyEdits(sourceFile, edits, result, before);
|
|
163
|
+
}
|
|
164
|
+
function migrateV23File(sourceFile, result) {
|
|
165
|
+
const edits = [];
|
|
166
|
+
const before = sourceFile.getFullText();
|
|
167
|
+
const stripeBindings = findStripeBindings(sourceFile);
|
|
168
|
+
const clients = findStripeClients(sourceFile, stripeBindings.constructors);
|
|
169
|
+
// Many projects export the client from a separate module, as signet does.
|
|
170
|
+
// A type/value import from stripe in this file anchors the inferred client.
|
|
171
|
+
if (stripeBindings.typeNames.size > 0) {
|
|
172
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
173
|
+
if (!declaration.getModuleSpecifierValue().startsWith('.'))
|
|
174
|
+
continue;
|
|
175
|
+
for (const specifier of declaration.getNamedImports()) {
|
|
176
|
+
if (specifier.getName() === 'stripe')
|
|
177
|
+
clients.add(specifier.getAliasNode()?.getText() ?? specifier.getName());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const typeReference of sourceFile.getDescendantsOfKind(SyntaxKind.TypeReference)) {
|
|
182
|
+
const name = typeReference.getTypeName().getText();
|
|
183
|
+
for (const binding of stripeBindings.typeNames) {
|
|
184
|
+
if (name === `${binding}.ErrorType` || name.startsWith(`${binding}.ErrorType.`)) {
|
|
185
|
+
edits.push(edit(sourceFile, typeReference, name.replace(`${binding}.ErrorType`, `${binding}.errors`), 'error-type-namespace', typeReference.getText()));
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
if (name === `${binding}.AddressParam` || name === `${binding}.JapanAddressParam`) {
|
|
189
|
+
addManual(sourceFile, result.manual, typeReference, 'Stripe v23 uses operation-specific address types for V2 requests. Choose the type from the specific operation; this shared type may still be valid for V1.', typeReference.getText());
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
195
|
+
const expression = call.getExpression();
|
|
196
|
+
const path = propertyPath(expression);
|
|
197
|
+
const [root] = path;
|
|
198
|
+
if (!root || !clients.has(root))
|
|
199
|
+
continue;
|
|
200
|
+
const method = path.at(-1);
|
|
201
|
+
const parameterType = REMOVED_PAYMENT_METHOD_TYPES_PARAMS.get(`${path[1]}.${method}`);
|
|
202
|
+
if (path.length === 3 && parameterType) {
|
|
203
|
+
const params = call.getArguments()[method === 'create' ? 0 : 1];
|
|
204
|
+
if (params && Node.isObjectLiteralExpression(params)) {
|
|
205
|
+
const removed = params.getProperties().find((property) => (Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property))
|
|
206
|
+
&& property.getName() === 'payment_method_types');
|
|
207
|
+
if (removed)
|
|
208
|
+
addManual(sourceFile, result.manual, removed, `Stripe v23 no longer accepts payment_method_types in ${parameterType}. Review the intended payment methods against the new API before changing this payment flow; no replacement was made.`, redactedSnippet(removed.getText()));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (method === 'constructEventWithoutVerification' && path.length === 2) {
|
|
212
|
+
const receiver = Node.isPropertyAccessExpression(expression) ? expression.getExpression() : undefined;
|
|
213
|
+
if (receiver)
|
|
214
|
+
edits.push(edit(sourceFile, expression, `${receiver.getText()}.webhooks.constructEventWithoutVerification`, 'webhook-method-location', expression.getText()));
|
|
215
|
+
}
|
|
216
|
+
if (method === 'verifyHeader' || method === 'verifyHeaderAsync') {
|
|
217
|
+
if (call.getArguments().length < 4) {
|
|
218
|
+
addManual(sourceFile, result.manual, call, 'Stripe v23 now applies DEFAULT_TOLERANCE to webhook signature timestamp checks. Confirm this default is intended, or pass 0 explicitly only if the old no-tolerance behavior is required.', redactedSnippet(call.getText()));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// The v23 SDK changes its default and pinned API version. Review missing, dynamic,
|
|
223
|
+
// or older explicit versions because SDK type declarations are pinned to v23's API.
|
|
224
|
+
for (const creation of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
225
|
+
const expression = creation.getExpression();
|
|
226
|
+
if (!isStripeConstructor(expression, stripeBindings.constructors))
|
|
227
|
+
continue;
|
|
228
|
+
const options = creation.getArguments()?.[1];
|
|
229
|
+
const apiVersionProperty = Node.isObjectLiteralExpression(options)
|
|
230
|
+
? options.getProperties().find((property) => Node.isPropertyAssignment(property) && property.getName() === 'apiVersion')
|
|
231
|
+
: undefined;
|
|
232
|
+
const versionExpression = apiVersionProperty && Node.isPropertyAssignment(apiVersionProperty)
|
|
233
|
+
? apiVersionProperty.getInitializer()
|
|
234
|
+
: undefined;
|
|
235
|
+
const apiVersion = versionExpression && Node.isStringLiteral(versionExpression)
|
|
236
|
+
? versionExpression.getLiteralValue()
|
|
237
|
+
: undefined;
|
|
238
|
+
if (apiVersion !== '2026-09-30.endive') {
|
|
239
|
+
const reason = apiVersion
|
|
240
|
+
? `Stripe v23 is pinned to API version 2026-09-30.endive, while this client explicitly uses ${apiVersion}. Review the Stripe API changelog and update/test the pinned version before upgrading.`
|
|
241
|
+
: 'Stripe v23 changes the default API version to 2026-09-30.endive. Review Stripe’s API changelog and test behavior, or pin an explicit apiVersion before upgrading.';
|
|
242
|
+
addManual(sourceFile, result.manual, creation, reason, redactedSnippet(creation.getText()));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
applyEdits(sourceFile, edits, result, before);
|
|
246
|
+
}
|
|
247
|
+
function findStripeBindings(sourceFile) {
|
|
248
|
+
const constructors = new Set();
|
|
249
|
+
const typeNames = new Set();
|
|
250
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
251
|
+
if (declaration.getModuleSpecifierValue() !== PACKAGE)
|
|
252
|
+
continue;
|
|
253
|
+
const defaultImport = declaration.getDefaultImport();
|
|
254
|
+
if (defaultImport) {
|
|
255
|
+
constructors.add(defaultImport.getText());
|
|
256
|
+
typeNames.add(defaultImport.getText());
|
|
257
|
+
}
|
|
258
|
+
for (const specifier of declaration.getNamedImports()) {
|
|
259
|
+
if (specifier.getName() !== 'Stripe')
|
|
260
|
+
continue;
|
|
261
|
+
const localName = specifier.getAliasNode()?.getText() ?? specifier.getName();
|
|
262
|
+
constructors.add(localName);
|
|
263
|
+
typeNames.add(localName);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const declaration of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
|
|
267
|
+
const reference = declaration.getModuleReference();
|
|
268
|
+
const expression = Node.isExternalModuleReference(reference) ? reference.getExpression() : undefined;
|
|
269
|
+
if (expression && Node.isStringLiteral(expression) && expression.getLiteralValue() === PACKAGE) {
|
|
270
|
+
constructors.add(declaration.getName());
|
|
271
|
+
typeNames.add(declaration.getName());
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const declaration of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
275
|
+
const name = declaration.getNameNode();
|
|
276
|
+
const initializer = declaration.getInitializer();
|
|
277
|
+
if (!Node.isIdentifier(name) || !initializer)
|
|
278
|
+
continue;
|
|
279
|
+
if (isRequireStripe(initializer) || (Node.isPropertyAccessExpression(initializer)
|
|
280
|
+
&& ['default', 'Stripe'].includes(initializer.getName()) && isRequireStripe(initializer.getExpression()))) {
|
|
281
|
+
constructors.add(name.getText());
|
|
282
|
+
typeNames.add(name.getText());
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { constructors, typeNames };
|
|
286
|
+
}
|
|
287
|
+
function findStripeClients(sourceFile, constructors) {
|
|
288
|
+
const clients = new Set();
|
|
289
|
+
for (const declaration of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
290
|
+
const name = declaration.getNameNode();
|
|
291
|
+
const initializer = declaration.getInitializer();
|
|
292
|
+
if (!Node.isIdentifier(name) || !initializer)
|
|
293
|
+
continue;
|
|
294
|
+
if (Node.isNewExpression(initializer) && isStripeConstructor(initializer.getExpression(), constructors)) {
|
|
295
|
+
clients.add(name.getText());
|
|
296
|
+
}
|
|
297
|
+
else if (Node.isCallExpression(initializer)) {
|
|
298
|
+
const expression = initializer.getExpression();
|
|
299
|
+
if ((Node.isIdentifier(expression) && constructors.has(expression.getText())) || isRequireStripe(expression)
|
|
300
|
+
|| (Node.isPropertyAccessExpression(expression) && ['default', 'Stripe'].includes(expression.getName()) && isRequireStripe(expression.getExpression()))) {
|
|
301
|
+
clients.add(name.getText());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return clients;
|
|
306
|
+
}
|
|
307
|
+
function isStripeConstructor(node, constructors) {
|
|
308
|
+
if (Node.isIdentifier(node))
|
|
309
|
+
return constructors.has(node.getText());
|
|
310
|
+
if (Node.isCallExpression(node))
|
|
311
|
+
return isRequireStripe(node);
|
|
312
|
+
if (Node.isParenthesizedExpression(node))
|
|
313
|
+
return isStripeConstructor(node.getExpression(), constructors);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
function isRequireStripe(node) {
|
|
317
|
+
if (!Node.isCallExpression(node) || !Node.isIdentifier(node.getExpression()) || node.getExpression().getText() !== 'require')
|
|
318
|
+
return false;
|
|
319
|
+
const argument = node.getArguments()[0];
|
|
320
|
+
if (!argument || !Node.isStringLiteral(argument))
|
|
321
|
+
return false;
|
|
322
|
+
return argument.getLiteralValue() === PACKAGE;
|
|
323
|
+
}
|
|
324
|
+
function propertyPath(node) {
|
|
325
|
+
if (Node.isIdentifier(node))
|
|
326
|
+
return [node.getText()];
|
|
327
|
+
if (Node.isPropertyAccessExpression(node))
|
|
328
|
+
return [...propertyPath(node.getExpression()), node.getName()];
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
function isRequestOptionsObject(node) {
|
|
332
|
+
if (!Node.isObjectLiteralExpression(node) || node.getProperties().length === 0)
|
|
333
|
+
return false;
|
|
334
|
+
return node.getProperties().every((property) => Node.isPropertyAssignment(property) && REQUEST_OPTION_KEYS.has(property.getName()));
|
|
335
|
+
}
|
|
336
|
+
function containsRequestOptionKey(node) {
|
|
337
|
+
return Node.isObjectLiteralExpression(node)
|
|
338
|
+
&& node.getProperties().some((property) => Node.isPropertyAssignment(property) && REQUEST_OPTION_KEYS.has(property.getName()));
|
|
339
|
+
}
|
|
340
|
+
function isResourceIdArgument(node) {
|
|
341
|
+
return Node.isStringLiteral(node)
|
|
342
|
+
|| Node.isNoSubstitutionTemplateLiteral(node)
|
|
343
|
+
|| Node.isIdentifier(node)
|
|
344
|
+
|| Node.isPropertyAccessExpression(node)
|
|
345
|
+
|| Node.isElementAccessExpression(node);
|
|
346
|
+
}
|
|
347
|
+
function isStripeSecretLiteral(node) {
|
|
348
|
+
if (!Node.isStringLiteral(node) && !Node.isNoSubstitutionTemplateLiteral(node))
|
|
349
|
+
return false;
|
|
350
|
+
return /^sk_(?:test|live)_/.test(node.getLiteralValue());
|
|
351
|
+
}
|
|
352
|
+
function edit(sourceFile, node, replacement, rule, before) {
|
|
353
|
+
return {
|
|
354
|
+
start: node.getStart(),
|
|
355
|
+
end: node.getEnd(),
|
|
356
|
+
replacement,
|
|
357
|
+
rule,
|
|
358
|
+
before,
|
|
359
|
+
line: sourceFile.getLineAndColumnAtPos(node.getStart()).line,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function applyEdits(sourceFile, edits, result, original) {
|
|
363
|
+
if (edits.length === 0)
|
|
364
|
+
return;
|
|
365
|
+
const unique = new Map();
|
|
366
|
+
for (const change of edits)
|
|
367
|
+
unique.set(`${change.start}:${change.end}`, change);
|
|
368
|
+
const ordered = [...unique.values()].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
369
|
+
let migrated = original;
|
|
370
|
+
let boundary = original.length + 1;
|
|
371
|
+
for (const change of ordered) {
|
|
372
|
+
if (change.end > boundary)
|
|
373
|
+
continue;
|
|
374
|
+
migrated = migrated.slice(0, change.start) + change.replacement + migrated.slice(change.end);
|
|
375
|
+
boundary = change.start;
|
|
376
|
+
const after = change.rule === 'request-options-position'
|
|
377
|
+
? `undefined, ${change.before}`
|
|
378
|
+
: change.replacement;
|
|
379
|
+
result.changes.push({ file: sourceFile.getFilePath(), line: change.line, rule: change.rule, before: change.before, after });
|
|
380
|
+
}
|
|
381
|
+
if (migrated === original)
|
|
382
|
+
return;
|
|
383
|
+
result.filesChanged.push(sourceFile.getFilePath());
|
|
384
|
+
result.fileUpdates?.push({ file: sourceFile.getFilePath(), contents: migrated });
|
|
385
|
+
}
|
|
386
|
+
function addManual(sourceFile, findings, node, reason, snippet) {
|
|
387
|
+
if (findings.some((finding) => finding.file === sourceFile.getFilePath() && finding.line === node.getStartLineNumber() && finding.reason === reason))
|
|
388
|
+
return;
|
|
389
|
+
findings.push({ file: sourceFile.getFilePath(), line: node.getStartLineNumber(), reason, snippet: redactedSnippet(snippet) });
|
|
390
|
+
}
|
|
391
|
+
function redactedSnippet(value) {
|
|
392
|
+
return value.replace(/(['"`])sk_(?:test|live)_[A-Za-z0-9]+\1/g, '$1[REDACTED_STRIPE_KEY]$1');
|
|
393
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { migrateStripeV22 } from '../stripe-shared.js';
|
|
2
|
+
export const stripeV22 = {
|
|
3
|
+
id: 'stripe@22',
|
|
4
|
+
packageName: 'stripe',
|
|
5
|
+
fromMajor: 21,
|
|
6
|
+
toMajor: 22,
|
|
7
|
+
releasedAt: '2026-04-03',
|
|
8
|
+
title: 'stripe v21 → v22',
|
|
9
|
+
guideUrl: 'https://github.com/stripe/stripe-node/wiki/Migration-guide-for-v22',
|
|
10
|
+
rules: [
|
|
11
|
+
'stripe-constructor',
|
|
12
|
+
'cjs-default-export',
|
|
13
|
+
'stripe-context-type',
|
|
14
|
+
'stripe-error-type',
|
|
15
|
+
'remove-obsolete-type-reference',
|
|
16
|
+
'request-options-position',
|
|
17
|
+
],
|
|
18
|
+
transform(project) {
|
|
19
|
+
return migrateStripeV22(project);
|
|
20
|
+
},
|
|
21
|
+
updateManifest(manifest) {
|
|
22
|
+
return updateStripeMajor(manifest, 21, 22);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
function updateStripeMajor(manifest, from, to) {
|
|
26
|
+
const notes = [];
|
|
27
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
28
|
+
const current = manifest[field]?.stripe;
|
|
29
|
+
if (!current)
|
|
30
|
+
continue;
|
|
31
|
+
const match = /^\s*[~^]?([0-9]+)(?:\.(?:[0-9]+|x|\*)){0,2}\s*$/i.exec(current);
|
|
32
|
+
if (!match) {
|
|
33
|
+
notes.push('Could not safely interpret `' + field + '.stripe` range `' + current + '`; update it to `^' + to + '.0.0` manually if appropriate.');
|
|
34
|
+
}
|
|
35
|
+
else if (Number(match[1]) === from) {
|
|
36
|
+
notes.push('`' + field + '.stripe`: `' + current + '` → `^' + to + '.0.0`');
|
|
37
|
+
manifest[field].stripe = `^${to}.0.0`;
|
|
38
|
+
}
|
|
39
|
+
else if (Number(match[1]) < from) {
|
|
40
|
+
notes.push('`' + field + '.stripe` is on major ' + match[1] + '; this migration targets v' + from + ' → v' + to + '. Upgrade to v' + from + ' first and review its migration guide.');
|
|
41
|
+
}
|
|
42
|
+
else if (Number(match[1]) > to) {
|
|
43
|
+
notes.push('`' + field + '.stripe` is already on major ' + match[1] + '; it was left unchanged to avoid downgrading it to v' + to + '.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return notes;
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { migrateStripeV23 } from '../stripe-shared.js';
|
|
2
|
+
export const stripeV23 = {
|
|
3
|
+
id: 'stripe@23',
|
|
4
|
+
packageName: 'stripe',
|
|
5
|
+
fromMajor: 22,
|
|
6
|
+
toMajor: 23,
|
|
7
|
+
releasedAt: '2026-10-01',
|
|
8
|
+
title: 'stripe v22 → v23',
|
|
9
|
+
guideUrl: 'https://github.com/stripe/stripe-node/wiki/Migration-guide-for-v23',
|
|
10
|
+
rules: ['error-type-namespace', 'webhook-method-location'],
|
|
11
|
+
transform(project) {
|
|
12
|
+
return migrateStripeV23(project);
|
|
13
|
+
},
|
|
14
|
+
updateManifest(manifest) {
|
|
15
|
+
const notes = [];
|
|
16
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
17
|
+
const current = manifest[field]?.stripe;
|
|
18
|
+
if (!current)
|
|
19
|
+
continue;
|
|
20
|
+
const match = /^\s*[~^]?([0-9]+)(?:\.(?:[0-9]+|x|\*)){0,2}\s*$/i.exec(current);
|
|
21
|
+
if (!match) {
|
|
22
|
+
notes.push('Could not safely interpret `' + field + '.stripe` range `' + current + '`; update it to `^23.0.0` manually if appropriate.');
|
|
23
|
+
}
|
|
24
|
+
else if (Number(match[1]) === 22) {
|
|
25
|
+
notes.push('`' + field + '.stripe`: `' + current + '` → `^23.0.0`');
|
|
26
|
+
manifest[field].stripe = '^23.0.0';
|
|
27
|
+
}
|
|
28
|
+
else if (Number(match[1]) < 22) {
|
|
29
|
+
notes.push('`' + field + '.stripe` is on major ' + match[1] + '; this migration targets v22 → v23. Upgrade to v22 first and review its migration guide.');
|
|
30
|
+
}
|
|
31
|
+
else if (Number(match[1]) > 23) {
|
|
32
|
+
notes.push('`' + field + '.stripe` is already on major ' + match[1] + '; it was left unchanged to avoid downgrading it to v23.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const nodeRange = manifest.engines?.node;
|
|
36
|
+
if (!nodeRange) {
|
|
37
|
+
notes.push('stripe v23 requires Node.js 20 or later; package.json does not declare an engines.node range, so confirm the deployment runtime before upgrading.');
|
|
38
|
+
}
|
|
39
|
+
else if (!supportsNode20(nodeRange)) {
|
|
40
|
+
notes.push(`stripe v23 requires Node.js 20 or later; review engines.node (${nodeRange}) before upgrading.`);
|
|
41
|
+
}
|
|
42
|
+
return notes;
|
|
43
|
+
},
|
|
44
|
+
inspectManifest(manifest) {
|
|
45
|
+
const nodeRange = manifest.engines?.node;
|
|
46
|
+
if (!nodeRange)
|
|
47
|
+
return ['stripe v23 requires Node.js 20 or later; confirm the deployment runtime before upgrading.'];
|
|
48
|
+
if (!supportsNode20(nodeRange))
|
|
49
|
+
return [`stripe v23 requires Node.js 20 or later; review engines.node (${nodeRange}) before upgrading.`];
|
|
50
|
+
return [];
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function supportsNode20(range) {
|
|
54
|
+
return /^(?:>=\s*20(?:\.\d+){0,2}|(?:\^|~)\s*(?:20|2[1-9]|[3-9]\d)(?:\.\d+){0,2}|(?:20|2[1-9]|[3-9]\d)(?:\.\d+){0,2})$/.test(range.trim());
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upstreamfix",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Preview supported migrations for breaking SDK changes.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/ivanDev237/upstreamfix.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/ivanDev237/upstreamfix#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/ivanDev237/upstreamfix/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"migration",
|
|
15
|
+
"firebase-admin",
|
|
16
|
+
"codemod",
|
|
17
|
+
"sdk",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"upstreamfix": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsx src/cli.ts",
|
|
29
|
+
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"benchmark": "tsx scripts/benchmark.ts",
|
|
33
|
+
"new-migration": "tsx scripts/new-migration.ts",
|
|
34
|
+
"prepack": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^15.0.0",
|
|
42
|
+
"diff": "^9.0.0",
|
|
43
|
+
"ts-morph": "^28.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^26.6.5",
|
|
47
|
+
"tsx": "^4.23.15",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^5.0.5"
|
|
50
|
+
}
|
|
51
|
+
}
|