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.
@@ -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.dependencies?.['firebase-functions'];
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
- const normalized = range.trim();
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 textEdits = edits.map((edit) => ({ start: edit.start, end: edit.end, text: edit.text }));
173
- for (const edit of edits) {
174
- result.changes.push({ file, line: edit.line, rule: edit.rule, before: edit.before, after: edit.text });
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();
@@ -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' && declaredMajor(range) === migration.fromMajor;
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 (!ignoredDirectories.has(entry.name))
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
- applyEdits(sourceFile, edits, result, before);
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
- applyEdits(sourceFile, edits, result, before);
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 applyEdits(sourceFile, edits, result, original) {
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
- unique.set(`${change.start}:${change.end}`, change);
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
- continue;
374
- migrated = migrated.slice(0, change.start) + change.replacement + migrated.slice(change.end);
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 /^(?:>=\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
+ return guaranteesNodeMajor(range, 20);
55
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upstreamfix",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Preview supported migrations for breaking SDK changes.",
5
5
  "repository": {
6
6
  "type": "git",