upstreamfix 0.1.0 → 0.1.3
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/README.md +21 -4
- package/dist/cli.js +109 -29
- package/dist/core/diagnostic-diff.js +12 -9
- package/dist/core/diff-api.js +211 -0
- package/dist/core/docker-mount.js +6 -0
- package/dist/core/impact.js +72 -0
- package/dist/core/manifest.js +4 -0
- package/dist/core/node-range.js +19 -0
- package/dist/core/probe.js +155 -0
- package/dist/core/project-discovery.js +10 -0
- package/dist/core/report.js +70 -10
- package/dist/core/runner.js +136 -19
- package/dist/core/verify.js +110 -78
- package/dist/core/watch.js +81 -0
- package/dist/core/write-branch.js +45 -27
- package/dist/migrations/firebase-admin-v14/index.js +122 -10
- package/dist/migrations/index.js +3 -7
- package/dist/migrations/stripe-shared.js +75 -9
- package/dist/migrations/stripe-v23/index.js +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Node, SyntaxKind, } from 'ts-morph';
|
|
2
|
+
import { guaranteesNodeMajor } from '../../core/node-range.js';
|
|
2
3
|
import { APP_MODULE, CREDENTIAL_FACTORIES, REMOVED_SERVICES, ROOT_TYPES, SERVICES, } from './mapping.js';
|
|
3
4
|
const PKG = 'firebase-admin';
|
|
4
5
|
const NODE_RUNTIME_GUIDANCE = 'firebase-admin v14 requires Node.js 22 or later; package.json does not declare engines.node. Add `"engines": { "node": ">=22" }` or use `--node-runtime 22` after confirming the deployment runtime.';
|
|
@@ -24,6 +25,9 @@ export const firebaseAdminV14 = {
|
|
|
24
25
|
.find((value) => typeof value === 'string');
|
|
25
26
|
if (functionsTest)
|
|
26
27
|
notes.push(`firebase-functions-test "${functionsTest}" is installed. In a benchmarked project, the v14 typecheck failed because this package's declarations import firebase-admin exports removed in v14 (including app and firestore). Check whether a compatible release is available; otherwise update the test setup, then rerun the tests before upgrading.`);
|
|
28
|
+
const functionsVersion = dependencyVersion(manifest, 'firebase-functions');
|
|
29
|
+
if (functionsVersion)
|
|
30
|
+
notes.push(`firebase-functions \`${functionsVersion}\` is installed: check that its firebase-admin peer range accepts v14.`);
|
|
27
31
|
return notes;
|
|
28
32
|
},
|
|
29
33
|
transform(project) {
|
|
@@ -85,16 +89,20 @@ export const firebaseAdminV14 = {
|
|
|
85
89
|
else if (!runtimeReady) {
|
|
86
90
|
notes.push(`firebase-admin v14 requires Node.js 22 or later; review \`engines.node\` (${engine}).`);
|
|
87
91
|
}
|
|
88
|
-
const functionsVersion = manifest
|
|
92
|
+
const functionsVersion = dependencyVersion(manifest, 'firebase-functions');
|
|
89
93
|
if (functionsVersion) {
|
|
90
94
|
notes.push(`firebase-functions \`${functionsVersion}\` is installed: check that its firebase-admin peer range accepts v14 (firebase-functions#1902).`);
|
|
91
95
|
}
|
|
92
96
|
return notes;
|
|
93
97
|
},
|
|
94
98
|
};
|
|
99
|
+
function dependencyVersion(manifest, name) {
|
|
100
|
+
return [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies]
|
|
101
|
+
.map((section) => section?.[name])
|
|
102
|
+
.find((value) => typeof value === 'string');
|
|
103
|
+
}
|
|
95
104
|
function supportsNode22(range) {
|
|
96
|
-
|
|
97
|
-
return /^(?:>=\s*22(?:\.\d+){0,2}|(?:\^|~)\s*22(?:\.\d+){0,2}|22(?:\.\d+){0,2})$/.test(normalized);
|
|
105
|
+
return guaranteesNodeMajor(range, 22);
|
|
98
106
|
}
|
|
99
107
|
function migrateFile(sourceFile, result) {
|
|
100
108
|
const file = sourceFile.getFilePath();
|
|
@@ -104,6 +112,34 @@ function migrateFile(sourceFile, result) {
|
|
|
104
112
|
const aliases = findAliases(sourceFile);
|
|
105
113
|
if (aliases.length === 0)
|
|
106
114
|
return false;
|
|
115
|
+
const aliasNames = new Set(aliases.map((alias) => alias.name));
|
|
116
|
+
for (const declaration of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
117
|
+
const initializer = declaration.getInitializer();
|
|
118
|
+
const nameNode = declaration.getNameNode();
|
|
119
|
+
if (!Node.isCallExpression(initializer) || !Node.isIdentifier(nameNode))
|
|
120
|
+
continue;
|
|
121
|
+
const factory = initializer.getExpression();
|
|
122
|
+
if (!Node.isPropertyAccessExpression(factory) || factory.getName() !== 'app')
|
|
123
|
+
continue;
|
|
124
|
+
const root = factory.getExpression();
|
|
125
|
+
if (!Node.isIdentifier(root) || !aliasNames.has(root.getText()))
|
|
126
|
+
continue;
|
|
127
|
+
for (const reference of nameNode.findReferencesAsNodes()) {
|
|
128
|
+
if (reference === nameNode || reference.getSourceFile() !== sourceFile)
|
|
129
|
+
continue;
|
|
130
|
+
const access = reference.getParent();
|
|
131
|
+
if (!Node.isPropertyAccessExpression(access) || access.getExpression() !== reference)
|
|
132
|
+
continue;
|
|
133
|
+
const service = access.getName();
|
|
134
|
+
if (!Object.hasOwn(SERVICES, service) && !Object.hasOwn(REMOVED_SERVICES, service))
|
|
135
|
+
continue;
|
|
136
|
+
result.manual.push({ file, line: access.getStartLineNumber(), reason: `Stored admin.app() result calls app.${service}; choose the modular service getter manually.`, snippet: snippetOf(access) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (result.manual.length > manualBefore) {
|
|
140
|
+
sortManual(result.manual, manualBefore);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
107
143
|
const edits = [];
|
|
108
144
|
const aliasesWithManualUses = new Set();
|
|
109
145
|
for (const alias of aliases) {
|
|
@@ -129,19 +165,37 @@ function migrateFile(sourceFile, result) {
|
|
|
129
165
|
});
|
|
130
166
|
continue;
|
|
131
167
|
}
|
|
168
|
+
if (alias.kind === 'cjs') {
|
|
169
|
+
for (const type of sourceFile.getDescendantsOfKind(SyntaxKind.TypeReference)) {
|
|
170
|
+
if (!aliasMemberPattern(alias.name).test(type.getText()))
|
|
171
|
+
continue;
|
|
172
|
+
aliasesWithManualUses.add(alias.name);
|
|
173
|
+
result.manual.push({ file, line: type.getStartLineNumber(), reason: 'A type-only Firebase reference from require needs a manual import type; no runtime require was synthesized.', snippet: snippetOf(type) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
132
176
|
const references = alias.nameNode
|
|
133
177
|
.findReferencesAsNodes()
|
|
134
178
|
.filter(Node.isIdentifier)
|
|
135
179
|
.filter((reference) => reference.getSourceFile() === sourceFile)
|
|
136
180
|
.filter((reference) => reference !== alias.nameNode);
|
|
137
181
|
for (const ref of references) {
|
|
138
|
-
const classification = classify(ref);
|
|
139
182
|
const line = ref.getStartLineNumber();
|
|
183
|
+
if (alias.kind === 'cjs' && ref.getFirstAncestorByKind(SyntaxKind.TypeReference)) {
|
|
184
|
+
aliasesWithManualUses.add(alias.name);
|
|
185
|
+
result.manual.push({ file, line, reason: 'A type-only Firebase reference from require needs a manual import type; no runtime require was synthesized.', snippet: snippetOf(ref) });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const classification = classify(ref);
|
|
140
189
|
if ('reason' in classification) {
|
|
141
190
|
aliasesWithManualUses.add(alias.name);
|
|
142
191
|
result.manual.push({ file, line, reason: classification.reason, snippet: snippetOf(ref) });
|
|
143
192
|
continue;
|
|
144
193
|
}
|
|
194
|
+
if (alias.kind === 'cjs' && classification.imports.some((imp) => imp.typeOnly)) {
|
|
195
|
+
aliasesWithManualUses.add(alias.name);
|
|
196
|
+
result.manual.push({ file, line, reason: 'A type-only Firebase reference from require needs a manual import type; no runtime require was synthesized.', snippet: snippetOf(ref) });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
145
199
|
const collision = classification.imports.find((imp) => hasImportCollision(sourceFile, imp));
|
|
146
200
|
if (collision) {
|
|
147
201
|
aliasesWithManualUses.add(alias.name);
|
|
@@ -164,15 +218,33 @@ function migrateFile(sourceFile, result) {
|
|
|
164
218
|
});
|
|
165
219
|
}
|
|
166
220
|
}
|
|
221
|
+
if (result.manual.slice(manualBefore).some((item) => item.reason.startsWith('App-scoped ') && item.reason.includes('arguments'))) {
|
|
222
|
+
sortManual(result.manual, manualBefore);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
167
225
|
if (edits.length === 0) {
|
|
168
226
|
if (result.manual.length > manualBefore)
|
|
169
227
|
sortManual(result.manual, manualBefore);
|
|
170
228
|
return false;
|
|
171
229
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
const orderedEdits = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
231
|
+
if (hasOverlappingEdits(orderedEdits)) {
|
|
232
|
+
for (let index = 1; index < orderedEdits.length; index++) {
|
|
233
|
+
const previous = orderedEdits[index - 1];
|
|
234
|
+
const current = orderedEdits[index];
|
|
235
|
+
if (current.start >= previous.end)
|
|
236
|
+
continue;
|
|
237
|
+
result.manual.push({
|
|
238
|
+
file,
|
|
239
|
+
line: current.line,
|
|
240
|
+
reason: 'Automatic Firebase edits overlap in this file; review the nested usage manually before migrating it.',
|
|
241
|
+
snippet: current.before,
|
|
242
|
+
});
|
|
243
|
+
sortManual(result.manual, manualBefore);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
175
246
|
}
|
|
247
|
+
const textEdits = edits.map((edit) => ({ start: edit.start, end: edit.end, text: edit.text }));
|
|
176
248
|
const importMap = mergeImports(edits.flatMap((edit) => edit.imports));
|
|
177
249
|
const kind = aliases.some((alias) => alias.kind === 'esm') ? 'esm' : 'cjs';
|
|
178
250
|
const importText = renderImports(sourceFile, kind, importMap);
|
|
@@ -182,12 +254,41 @@ function migrateFile(sourceFile, result) {
|
|
|
182
254
|
}
|
|
183
255
|
textEdits.push(...aliasDeclarationRemovals(sourceFile, aliases, aliasesWithManualUses));
|
|
184
256
|
const transformed = applyTextEdits(sourceFile.getFullText(), textEdits);
|
|
257
|
+
for (const alias of aliases) {
|
|
258
|
+
if (!aliasesWithManualUses.has(alias.name) && aliasMemberPattern(alias.name).test(transformed)) {
|
|
259
|
+
result.manual.push({
|
|
260
|
+
file,
|
|
261
|
+
line: alias.nameNode.getStartLineNumber(),
|
|
262
|
+
reason: `The ${alias.name} namespace still has references after migration; its import cannot be removed safely. Review this file manually.`,
|
|
263
|
+
snippet: snippetOf(alias.nameNode),
|
|
264
|
+
});
|
|
265
|
+
sortManual(result.manual, manualBefore);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const edit of edits) {
|
|
270
|
+
result.changes.push({ file, line: edit.line, rule: edit.rule, before: edit.before, after: edit.text });
|
|
271
|
+
}
|
|
185
272
|
if (transformed !== sourceFile.getFullText())
|
|
186
273
|
result.fileUpdates?.push({ file, contents: transformed });
|
|
187
274
|
if (result.manual.length > manualBefore)
|
|
188
275
|
sortManual(result.manual, manualBefore);
|
|
189
276
|
return true;
|
|
190
277
|
}
|
|
278
|
+
export function hasOverlappingEdits(edits) {
|
|
279
|
+
const ordered = [...edits].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
280
|
+
let boundary = -1;
|
|
281
|
+
for (const edit of ordered) {
|
|
282
|
+
if (edit.start < boundary)
|
|
283
|
+
return true;
|
|
284
|
+
boundary = Math.max(boundary, edit.end);
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function aliasMemberPattern(alias) {
|
|
289
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
290
|
+
return new RegExp(`(?<![\\w$])${escaped}\\s*[.\\[]`);
|
|
291
|
+
}
|
|
191
292
|
function classify(ref) {
|
|
192
293
|
const parent = ref.getParent();
|
|
193
294
|
if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref)
|
|
@@ -251,6 +352,9 @@ function classifyAccess(access) {
|
|
|
251
352
|
const serviceName = serviceAccess.getName();
|
|
252
353
|
const nestedService = Object.hasOwn(SERVICES, serviceName) ? SERVICES[serviceName] : undefined;
|
|
253
354
|
if (nestedService) {
|
|
355
|
+
if (serviceCall.getArguments().length > 0) {
|
|
356
|
+
return { reason: `App-scoped ${serviceName} call has arguments; choose the equivalent modular API manually so its target is preserved.` };
|
|
357
|
+
}
|
|
254
358
|
return {
|
|
255
359
|
node: serviceCall,
|
|
256
360
|
text: `${nestedService.getter}()`,
|
|
@@ -515,7 +619,7 @@ function renderImports(sourceFile, kind, byModule) {
|
|
|
515
619
|
const newline = fullText.includes('\r\n') ? '\r\n' : '\n';
|
|
516
620
|
const before = fullText.slice(0, position);
|
|
517
621
|
const after = fullText.slice(position);
|
|
518
|
-
const prefix = before.endsWith('\n') ? '' : newline;
|
|
622
|
+
const prefix = before.length === 0 || before.endsWith('\n') ? '' : newline;
|
|
519
623
|
const suffix = after.startsWith('\n') ? '' : newline;
|
|
520
624
|
return `${prefix}${lines.join(newline)}${suffix}`;
|
|
521
625
|
}
|
|
@@ -568,7 +672,7 @@ function aliasDeclarationRemovals(sourceFile, aliases, aliasesWithManualUses) {
|
|
|
568
672
|
importDecl.getNamedImports().length === 0) {
|
|
569
673
|
const key = `${importDecl.getStart()}:${importDecl.getEnd()}`;
|
|
570
674
|
if (!seen.has(key))
|
|
571
|
-
removals.push({ start: importDecl.getStart(), end: importDecl.getEnd(), text: '' });
|
|
675
|
+
removals.push({ start: importDecl.getStart(), end: endIncludingLineBreak(sourceFile, importDecl.getEnd()), text: '' });
|
|
572
676
|
seen.add(key);
|
|
573
677
|
}
|
|
574
678
|
continue;
|
|
@@ -580,12 +684,20 @@ function aliasDeclarationRemovals(sourceFile, aliases, aliasesWithManualUses) {
|
|
|
580
684
|
if (removable) {
|
|
581
685
|
const key = `${removable.getStart()}:${removable.getEnd()}`;
|
|
582
686
|
if (!seen.has(key))
|
|
583
|
-
removals.push({ start: removable.getStart(), end: removable.getEnd(), text: '' });
|
|
687
|
+
removals.push({ start: removable.getStart(), end: endIncludingLineBreak(sourceFile, removable.getEnd()), text: '' });
|
|
584
688
|
seen.add(key);
|
|
585
689
|
}
|
|
586
690
|
}
|
|
587
691
|
return removals;
|
|
588
692
|
}
|
|
693
|
+
function endIncludingLineBreak(sourceFile, end) {
|
|
694
|
+
const text = sourceFile.getFullText();
|
|
695
|
+
if (text.slice(end, end + 2) === '\r\n')
|
|
696
|
+
return end + 2;
|
|
697
|
+
if (text[end] === '\n')
|
|
698
|
+
return end + 1;
|
|
699
|
+
return end;
|
|
700
|
+
}
|
|
589
701
|
function snippetOf(node) {
|
|
590
702
|
const statement = node.getFirstAncestor((n) => Node.isStatement(n)) ?? node;
|
|
591
703
|
const text = statement.getText().split('\n')[0].trim();
|
package/dist/migrations/index.js
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { IGNORED_PROJECT_DIRECTORIES, isSupportedSourceMajor } from '../core/project-discovery.js';
|
|
3
4
|
import { firebaseAdminV14 } from './firebase-admin-v14/index.js';
|
|
4
5
|
import { stripeV22 } from './stripe-v22/index.js';
|
|
5
6
|
import { stripeV23 } from './stripe-v23/index.js';
|
|
6
7
|
/** The shipped migration catalog. Adding a migration requires registering it here, not in core or the CLI. */
|
|
7
8
|
export const migrations = [firebaseAdminV14, stripeV22, stripeV23];
|
|
8
|
-
const ignoredDirectories = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.nuxt', '.turbo', '.cache']);
|
|
9
9
|
export async function findApplicableMigrations(cwd) {
|
|
10
10
|
const manifests = await readManifests(path.resolve(cwd));
|
|
11
11
|
return migrations.filter((migration) => manifests.some((manifest) => {
|
|
12
12
|
const sections = [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies];
|
|
13
13
|
return sections.some((section) => {
|
|
14
14
|
const range = section?.[migration.packageName];
|
|
15
|
-
return typeof range === 'string' &&
|
|
15
|
+
return typeof range === 'string' && isSupportedSourceMajor(range, migration.fromMajor);
|
|
16
16
|
});
|
|
17
17
|
}));
|
|
18
18
|
}
|
|
19
|
-
function declaredMajor(range) {
|
|
20
|
-
const match = /^\s*(?:[~^=]|>=?\s*)?(\d+)(?:\.|\s|$)/.exec(range);
|
|
21
|
-
return match ? Number(match[1]) : undefined;
|
|
22
|
-
}
|
|
23
19
|
async function readManifests(root) {
|
|
24
20
|
const manifests = [];
|
|
25
21
|
async function visit(directory) {
|
|
@@ -28,7 +24,7 @@ async function readManifests(root) {
|
|
|
28
24
|
continue;
|
|
29
25
|
const absolute = path.join(directory, entry.name);
|
|
30
26
|
if (entry.isDirectory()) {
|
|
31
|
-
if (!
|
|
27
|
+
if (!IGNORED_PROJECT_DIRECTORIES.has(entry.name))
|
|
32
28
|
await visit(absolute);
|
|
33
29
|
}
|
|
34
30
|
else if (entry.isFile() && entry.name === 'package.json') {
|
|
@@ -37,6 +37,9 @@ function emptyResult() {
|
|
|
37
37
|
return { changes: [], manual: [], filesChanged: [], fileUpdates: [] };
|
|
38
38
|
}
|
|
39
39
|
function migrateV22File(sourceFile, result) {
|
|
40
|
+
if (withholdInvalidSource(sourceFile, result))
|
|
41
|
+
return;
|
|
42
|
+
reportUnsupportedStripeImports(sourceFile, result);
|
|
40
43
|
const edits = [];
|
|
41
44
|
const before = sourceFile.getFullText();
|
|
42
45
|
const stripeBindings = findStripeBindings(sourceFile);
|
|
@@ -44,6 +47,10 @@ function migrateV22File(sourceFile, result) {
|
|
|
44
47
|
for (const variable of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
45
48
|
const name = variable.getNameNode();
|
|
46
49
|
const initializer = variable.getInitializer();
|
|
50
|
+
if (Node.isObjectBindingPattern(name) && initializer && Node.isPropertyAccessExpression(initializer)
|
|
51
|
+
&& initializer.getName() === 'default' && isRequireStripe(initializer.getExpression())) {
|
|
52
|
+
addManual(sourceFile, result.manual, variable, 'Destructuring require("stripe").default is not rewritten automatically; inspect this import shape manually.', variable.getText());
|
|
53
|
+
}
|
|
47
54
|
if (!Node.isObjectBindingPattern(name) || !initializer || !isRequireStripe(initializer))
|
|
48
55
|
continue;
|
|
49
56
|
const elements = name.getElements();
|
|
@@ -100,6 +107,10 @@ function migrateV22File(sourceFile, result) {
|
|
|
100
107
|
}
|
|
101
108
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
102
109
|
const expression = call.getExpression();
|
|
110
|
+
if (expression.getText().startsWith('this.stripe.')
|
|
111
|
+
&& call.getArguments().some((argument) => Node.isArrowFunction(argument) || Node.isFunctionExpression(argument))) {
|
|
112
|
+
addManual(sourceFile, result.manual, call, 'Stripe v22 removes callbacks from client methods reached through this.stripe; convert this call manually to promises.', call.getText());
|
|
113
|
+
}
|
|
103
114
|
const [root, ...path] = propertyPath(expression);
|
|
104
115
|
if (root && clients.has(root) && path.length >= 2) {
|
|
105
116
|
const args = call.getArguments();
|
|
@@ -159,9 +170,12 @@ function migrateV22File(sourceFile, result) {
|
|
|
159
170
|
edits.push(edit(sourceFile, expression, `new (${expression.getText()})`, 'stripe-constructor', expression.getText()));
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
|
-
|
|
173
|
+
applyStripeEdits(sourceFile, edits, result, before);
|
|
163
174
|
}
|
|
164
175
|
function migrateV23File(sourceFile, result) {
|
|
176
|
+
if (withholdInvalidSource(sourceFile, result))
|
|
177
|
+
return;
|
|
178
|
+
reportUnsupportedStripeImports(sourceFile, result);
|
|
165
179
|
const edits = [];
|
|
166
180
|
const before = sourceFile.getFullText();
|
|
167
181
|
const stripeBindings = findStripeBindings(sourceFile);
|
|
@@ -242,7 +256,19 @@ function migrateV23File(sourceFile, result) {
|
|
|
242
256
|
addManual(sourceFile, result.manual, creation, reason, redactedSnippet(creation.getText()));
|
|
243
257
|
}
|
|
244
258
|
}
|
|
245
|
-
|
|
259
|
+
applyStripeEdits(sourceFile, edits, result, before);
|
|
260
|
+
}
|
|
261
|
+
function reportUnsupportedStripeImports(sourceFile, result) {
|
|
262
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
263
|
+
if (declaration.getModuleSpecifierValue() !== PACKAGE)
|
|
264
|
+
continue;
|
|
265
|
+
if (declaration.getNamespaceImport()) {
|
|
266
|
+
addManual(sourceFile, result.manual, declaration, 'A namespace Stripe import needs manual review before this migration.', declaration.getText());
|
|
267
|
+
}
|
|
268
|
+
if (declaration.getNamedImports().some((specifier) => specifier.getName() === 'default')) {
|
|
269
|
+
addManual(sourceFile, result.manual, declaration, 'A named default Stripe import needs manual review before this migration.', declaration.getText());
|
|
270
|
+
}
|
|
271
|
+
}
|
|
246
272
|
}
|
|
247
273
|
function findStripeBindings(sourceFile) {
|
|
248
274
|
const constructors = new Set();
|
|
@@ -359,20 +385,31 @@ function edit(sourceFile, node, replacement, rule, before) {
|
|
|
359
385
|
line: sourceFile.getLineAndColumnAtPos(node.getStart()).line,
|
|
360
386
|
};
|
|
361
387
|
}
|
|
362
|
-
function
|
|
388
|
+
export function applyStripeEdits(sourceFile, edits, result, original) {
|
|
363
389
|
if (edits.length === 0)
|
|
364
390
|
return;
|
|
365
391
|
const unique = new Map();
|
|
366
|
-
for (const change of edits)
|
|
367
|
-
|
|
392
|
+
for (const change of edits) {
|
|
393
|
+
const key = `${change.start}:${change.end}`;
|
|
394
|
+
const previous = unique.get(key);
|
|
395
|
+
if (previous && previous.replacement !== change.replacement) {
|
|
396
|
+
reportOverlap(sourceFile, result, change);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
unique.set(key, change);
|
|
400
|
+
}
|
|
368
401
|
const ordered = [...unique.values()].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
369
|
-
let migrated = original;
|
|
370
402
|
let boundary = original.length + 1;
|
|
371
403
|
for (const change of ordered) {
|
|
372
|
-
if (change.end > boundary)
|
|
373
|
-
|
|
374
|
-
|
|
404
|
+
if (change.end > boundary) {
|
|
405
|
+
reportOverlap(sourceFile, result, change);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
375
408
|
boundary = change.start;
|
|
409
|
+
}
|
|
410
|
+
let migrated = original;
|
|
411
|
+
for (const change of ordered) {
|
|
412
|
+
migrated = migrated.slice(0, change.start) + change.replacement + migrated.slice(change.end);
|
|
376
413
|
const after = change.rule === 'request-options-position'
|
|
377
414
|
? `undefined, ${change.before}`
|
|
378
415
|
: change.replacement;
|
|
@@ -383,6 +420,35 @@ function applyEdits(sourceFile, edits, result, original) {
|
|
|
383
420
|
result.filesChanged.push(sourceFile.getFilePath());
|
|
384
421
|
result.fileUpdates?.push({ file: sourceFile.getFilePath(), contents: migrated });
|
|
385
422
|
}
|
|
423
|
+
function withholdInvalidSource(sourceFile, result) {
|
|
424
|
+
const diagnostics = sourceFile.compilerNode.parseDiagnostics ?? [];
|
|
425
|
+
if (diagnostics.length === 0)
|
|
426
|
+
return false;
|
|
427
|
+
const isStripeSpecifier = (value) => value === PACKAGE || value.startsWith(`${PACKAGE}/`);
|
|
428
|
+
const hasImport = sourceFile.getImportDeclarations().some((declaration) => isStripeSpecifier(declaration.getModuleSpecifierValue()));
|
|
429
|
+
const hasRequire = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
430
|
+
const expression = call.getExpression();
|
|
431
|
+
if (!(Node.isIdentifier(expression) && expression.getText() === 'require') && expression.getKind() !== SyntaxKind.ImportKeyword)
|
|
432
|
+
return false;
|
|
433
|
+
const argument = call.getArguments()[0];
|
|
434
|
+
return Node.isStringLiteral(argument) && isStripeSpecifier(argument.getLiteralValue());
|
|
435
|
+
});
|
|
436
|
+
if (!hasImport && !hasRequire)
|
|
437
|
+
return true;
|
|
438
|
+
result.manual.push({
|
|
439
|
+
file: sourceFile.getFilePath(), line: 1,
|
|
440
|
+
reason: `This Stripe source file has ${diagnostics.length} syntax error(s); automatic edits were withheld until the file parses cleanly.`,
|
|
441
|
+
snippet: redactedSnippet(sourceFile.getFullText().split(/\r?\n/, 1)[0] ?? ''),
|
|
442
|
+
});
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
function reportOverlap(sourceFile, result, change) {
|
|
446
|
+
result.manual.push({
|
|
447
|
+
file: sourceFile.getFilePath(), line: change.line,
|
|
448
|
+
reason: 'Two Stripe migration edits overlap; automatic edits for this file were withheld for manual review.',
|
|
449
|
+
snippet: redactedSnippet(change.before),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
386
452
|
function addManual(sourceFile, findings, node, reason, snippet) {
|
|
387
453
|
if (findings.some((finding) => finding.file === sourceFile.getFilePath() && finding.line === node.getStartLineNumber() && finding.reason === reason))
|
|
388
454
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { migrateStripeV23 } from '../stripe-shared.js';
|
|
2
|
+
import { guaranteesNodeMajor } from '../../core/node-range.js';
|
|
2
3
|
export const stripeV23 = {
|
|
3
4
|
id: 'stripe@23',
|
|
4
5
|
packageName: 'stripe',
|
|
@@ -51,5 +52,5 @@ export const stripeV23 = {
|
|
|
51
52
|
},
|
|
52
53
|
};
|
|
53
54
|
function supportsNode20(range) {
|
|
54
|
-
return
|
|
55
|
+
return guaranteesNodeMajor(range, 20);
|
|
55
56
|
}
|