Преглед изворни кода

fix(auto-update): sort prerelease versions safely

Alvin Unreal пре 2 месеци
родитељ
комит
3c413e3b12
2 измењених фајлова са 91 додато и 3 уклоњено
  1. 60 1
      src/hooks/auto-update-checker/checker.test.ts
  2. 31 2
      src/hooks/auto-update-checker/checker.ts

+ 60 - 1
src/hooks/auto-update-checker/checker.test.ts

@@ -259,7 +259,7 @@ describe('auto-update-checker/checker', () => {
         `./checker?test=${importCounter++}`
       );
 
-      const result = await getLatestCompatibleVersion('^1.0.0');
+      const result = await getLatestCompatibleVersion('workspace:*');
 
       expect(result).toEqual({
         latestVersion: null,
@@ -270,6 +270,65 @@ describe('auto-update-checker/checker', () => {
       globalThis.fetch = originalFetch;
     });
 
+    test('parses range prefixes before checking major compatibility', async () => {
+      const originalFetch = globalThis.fetch;
+      globalThis.fetch = mock(async () =>
+        Response.json({
+          'dist-tags': {
+            latest: '1.9.0',
+          },
+          versions: {
+            '1.8.0': {},
+            '1.9.0': {},
+          },
+        }),
+      ) as never;
+
+      const { getLatestCompatibleVersion } = await import(
+        `./checker?test=${importCounter++}`
+      );
+
+      const result = await getLatestCompatibleVersion('^1.0.0');
+
+      expect(result).toEqual({
+        latestVersion: '1.9.0',
+        latestMajorVersion: null,
+        blockedByMajor: false,
+      });
+
+      globalThis.fetch = originalFetch;
+    });
+
+    test('sorts prerelease numeric suffixes numerically', async () => {
+      const originalFetch = globalThis.fetch;
+      globalThis.fetch = mock(async () =>
+        Response.json({
+          'dist-tags': {
+            beta: '1.0.0-beta.10',
+            latest: '1.0.0',
+          },
+          versions: {
+            '1.0.0-beta.2': {},
+            '1.0.0-beta.10': {},
+          },
+        }),
+      ) as never;
+
+      const { getLatestCompatibleVersion } = await import(
+        `./checker?test=${importCounter++}`
+      );
+
+      const result = await getLatestCompatibleVersion('1.0.0-beta.1', 'beta');
+
+      expect(result).toEqual({
+        latestVersion: '1.0.0-beta.10',
+        latestMajorVersion: null,
+        blockedByMajor: false,
+      });
+
+      globalThis.fetch = originalFetch;
+    });
+
     test('fallback dist-tags never return lower-major versions as compatible', async () => {
       const originalFetch = globalThis.fetch;
       globalThis.fetch = mock(async (url: string) => {

+ 31 - 2
src/hooks/auto-update-checker/checker.ts

@@ -51,7 +51,8 @@ function isDistTag(version: string): boolean {
 }
 
 function parseVersion(version: string): ParsedVersion | null {
-  const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?/);
+  const normalized = version.trim().replace(/^[~^=<>\s]+/, '');
+  const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?/);
   if (!match) return null;
 
   return {
@@ -81,7 +82,35 @@ function compareVersions(a: string, b: string): number {
   if (parsedA.prerelease === parsedB.prerelease) return 0;
   if (!parsedA.prerelease) return 1;
   if (!parsedB.prerelease) return -1;
-  return parsedA.prerelease.localeCompare(parsedB.prerelease);
+  return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
+}
+
+function comparePrerelease(a: string, b: string): number {
+  const segmentsA = a.split('.');
+  const segmentsB = b.split('.');
+  const length = Math.max(segmentsA.length, segmentsB.length);
+
+  for (let i = 0; i < length; i++) {
+    const segmentA = segmentsA[i];
+    const segmentB = segmentsB[i];
+    if (segmentA === segmentB) continue;
+    if (segmentA === undefined) return -1;
+    if (segmentB === undefined) return 1;
+
+    const numberA = Number(segmentA);
+    const numberB = Number(segmentB);
+    const numericA = Number.isInteger(numberA);
+    const numericB = Number.isInteger(numberB);
+
+    if (numericA && numericB) return numberA - numberB;
+    if (numericA) return -1;
+    if (numericB) return 1;
+
+    const comparison = segmentA.localeCompare(segmentB);
+    if (comparison !== 0) return comparison;
+  }
+
+  return 0;
 }
 
 function getPrereleaseChannel(version: ParsedVersion): string | null {