Skip to content

Commit d7ded44

Browse files
committed
feat: implement semver parsing and comparison for improved version handling
1 parent 4b0b5bd commit d7ded44

2 files changed

Lines changed: 190 additions & 5 deletions

File tree

packages/intent/src/scanner.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,72 @@ function getPackageDepth(packageRoot: string, projectRoot: string): number {
199199
return relative(projectRoot, packageRoot).split(sep).length
200200
}
201201

202+
interface ParsedSemver {
203+
major: number
204+
minor: number
205+
patch: number
206+
prerelease: Array<string | number>
207+
}
208+
209+
function parseSemver(version: string): ParsedSemver | null {
210+
const match =
211+
/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
212+
version,
213+
)
214+
if (!match) return null
215+
216+
const prerelease = match[4]
217+
? match[4].split('.').map((identifier) => {
218+
return /^\d+$/.test(identifier) ? Number(identifier) : identifier
219+
})
220+
: []
221+
222+
return {
223+
major: Number(match[1]),
224+
minor: Number(match[2]),
225+
patch: Number(match[3]),
226+
prerelease,
227+
}
228+
}
229+
230+
function comparePrereleaseIdentifiers(
231+
a: string | number | undefined,
232+
b: string | number | undefined,
233+
): number {
234+
if (a === undefined) return b === undefined ? 0 : 1
235+
if (b === undefined) return -1
236+
237+
if (typeof a === 'number' && typeof b === 'number') {
238+
return a - b
239+
}
240+
241+
if (typeof a === 'number') return -1
242+
if (typeof b === 'number') return 1
243+
244+
return a.localeCompare(b)
245+
}
246+
202247
function comparePackageVersions(a: string, b: string): number {
203-
const aMatch = /^(\d+)\.(\d+)\.(\d+)/.exec(a)
204-
const bMatch = /^(\d+)\.(\d+)\.(\d+)/.exec(b)
205-
if (!aMatch || !bMatch) return 0
248+
const parsedA = parseSemver(a)
249+
const parsedB = parseSemver(b)
206250

207-
for (let i = 1; i <= 3; i++) {
208-
const diff = Number(aMatch[i]) - Number(bMatch[i])
251+
if (!parsedA || !parsedB) {
252+
if (parsedA) return 1
253+
if (parsedB) return -1
254+
return 0
255+
}
256+
257+
for (const key of ['major', 'minor', 'patch'] as const) {
258+
const diff = parsedA[key] - parsedB[key]
259+
if (diff !== 0) return diff
260+
}
261+
262+
const length = Math.max(parsedA.prerelease.length, parsedB.prerelease.length)
263+
for (let i = 0; i < length; i++) {
264+
const diff = comparePrereleaseIdentifiers(
265+
parsedA.prerelease[i],
266+
parsedB.prerelease[i],
267+
)
209268
if (diff !== 0) return diff
210269
}
211270

packages/intent/tests/scanner.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,132 @@ describe('scanForIntents', () => {
416416
expect(versionWarning).toContain('across 3 versions')
417417
expect(versionWarning).toContain('Using 5.0.0')
418418
})
419+
420+
it('prefers stable releases over prereleases at the same depth', async () => {
421+
writeJson(join(root, 'package.json'), {
422+
name: 'app',
423+
private: true,
424+
dependencies: {
425+
'consumer-a': '1.0.0',
426+
'consumer-b': '1.0.0',
427+
},
428+
})
429+
430+
const consumerADir = createDir(root, 'node_modules', 'consumer-a')
431+
const consumerBDir = createDir(root, 'node_modules', 'consumer-b')
432+
433+
writeJson(join(consumerADir, 'package.json'), {
434+
name: 'consumer-a',
435+
version: '1.0.0',
436+
dependencies: { '@tanstack/query': '5.0.0-beta.1' },
437+
})
438+
writeJson(join(consumerBDir, 'package.json'), {
439+
name: 'consumer-b',
440+
version: '1.0.0',
441+
dependencies: { '@tanstack/query': '5.0.0' },
442+
})
443+
444+
const prereleaseDir = createDir(
445+
consumerADir,
446+
'node_modules',
447+
'@tanstack',
448+
'query',
449+
)
450+
const stableDir = createDir(
451+
consumerBDir,
452+
'node_modules',
453+
'@tanstack',
454+
'query',
455+
)
456+
457+
writeJson(join(prereleaseDir, 'package.json'), {
458+
name: '@tanstack/query',
459+
version: '5.0.0-beta.1',
460+
intent: { version: 1, repo: 'TanStack/query', docs: 'docs/' },
461+
})
462+
writeJson(join(stableDir, 'package.json'), {
463+
name: '@tanstack/query',
464+
version: '5.0.0',
465+
intent: { version: 1, repo: 'TanStack/query', docs: 'docs/' },
466+
})
467+
writeSkillMd(createDir(prereleaseDir, 'skills', 'fetching'), {
468+
name: 'fetching',
469+
description: 'Prerelease query skill',
470+
})
471+
writeSkillMd(createDir(stableDir, 'skills', 'fetching'), {
472+
name: 'fetching',
473+
description: 'Stable query skill',
474+
})
475+
476+
const result = await scanForIntents(root)
477+
478+
expect(result.packages).toHaveLength(1)
479+
expect(result.packages[0]!.version).toBe('5.0.0')
480+
expect(result.packages[0]!.packageRoot).toBe(stableDir)
481+
})
482+
483+
it('prefers valid semver versions over invalid ones at the same depth', async () => {
484+
writeJson(join(root, 'package.json'), {
485+
name: 'app',
486+
private: true,
487+
dependencies: {
488+
'consumer-a': '1.0.0',
489+
'consumer-b': '1.0.0',
490+
},
491+
})
492+
493+
const consumerADir = createDir(root, 'node_modules', 'consumer-a')
494+
const consumerBDir = createDir(root, 'node_modules', 'consumer-b')
495+
496+
writeJson(join(consumerADir, 'package.json'), {
497+
name: 'consumer-a',
498+
version: '1.0.0',
499+
dependencies: { '@tanstack/query': 'workspace-dev' },
500+
})
501+
writeJson(join(consumerBDir, 'package.json'), {
502+
name: 'consumer-b',
503+
version: '1.0.0',
504+
dependencies: { '@tanstack/query': '5.0.0' },
505+
})
506+
507+
const invalidDir = createDir(
508+
consumerADir,
509+
'node_modules',
510+
'@tanstack',
511+
'query',
512+
)
513+
const validDir = createDir(
514+
consumerBDir,
515+
'node_modules',
516+
'@tanstack',
517+
'query',
518+
)
519+
520+
writeJson(join(invalidDir, 'package.json'), {
521+
name: '@tanstack/query',
522+
version: 'workspace-dev',
523+
intent: { version: 1, repo: 'TanStack/query', docs: 'docs/' },
524+
})
525+
writeJson(join(validDir, 'package.json'), {
526+
name: '@tanstack/query',
527+
version: '5.0.0',
528+
intent: { version: 1, repo: 'TanStack/query', docs: 'docs/' },
529+
})
530+
writeSkillMd(createDir(invalidDir, 'skills', 'fetching'), {
531+
name: 'fetching',
532+
description: 'Invalid version query skill',
533+
})
534+
writeSkillMd(createDir(validDir, 'skills', 'fetching'), {
535+
name: 'fetching',
536+
description: 'Valid version query skill',
537+
})
538+
539+
const result = await scanForIntents(root)
540+
541+
expect(result.packages).toHaveLength(1)
542+
expect(result.packages[0]!.version).toBe('5.0.0')
543+
expect(result.packages[0]!.packageRoot).toBe(validDir)
544+
})
419545
})
420546

421547
describe('package manager detection', () => {

0 commit comments

Comments
 (0)