Skip to content

Commit d753ce6

Browse files
jonasgeilerjycouet
andauthored
fix(sv): align eslint version to 10 accross all addons
Co-authored-by: jycouet <jycouet@gmail.com>
1 parent 92d7ca1 commit d753ce6

17 files changed

Lines changed: 232 additions & 89 deletions

File tree

.changeset/free-ducks-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/sv-utils': patch
3+
---
4+
5+
add `minVersion` & `coerceVersion` from `semver`. Deprecate `splitVersion`

.changeset/tiny-pears-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': minor
3+
---
4+
5+
fix(sv): align eslint version to `10` accross all addons

packages/sv-utils/api-surface.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,8 +725,16 @@ type Version = {
725725
major?: number;
726726
minor?: number;
727727
patch?: number;
728+
version?: string;
728729
};
730+
731+
declare function minVersion(range: string): string;
732+
/**
733+
* @deprecated Use `coerceVersion` instead.
734+
*/
729735
declare function splitVersion(str: string): Version;
736+
737+
declare function coerceVersion(str: string): Version;
730738
declare function isVersionUnsupportedBelow(
731739
versionStr: string,
732740
belowStr: string
@@ -796,6 +804,7 @@ export {
796804
type TransformFn,
797805
index_d_exports as Walker,
798806
type YamlDocument,
807+
coerceVersion,
799808
color,
800809
constructCommand,
801810
createPrinter,
@@ -810,6 +819,7 @@ export {
810819
json_d_exports as json,
811820
loadFile,
812821
loadPackageJson,
822+
minVersion,
813823
parse,
814824
pnpm_d_exports as pnpm,
815825
resolveCommand,

packages/sv-utils/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
},
2727
"devDependencies": {
2828
"@types/estree": "^1.0.8",
29+
"@types/semver": "^7.7.1",
2930
"decircular": "^1.0.0",
3031
"dedent": "^1.7.0",
3132
"esrap": "^2.2.2",
3233
"package-manager-detector": "^1.6.0",
34+
"semver": "^7.7.4",
3335
"silver-fleece": "^1.2.1",
3436
"smol-toml": "^1.5.2",
3537
"svelte": "^5.53.0",

packages/sv-utils/src/common.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

packages/sv-utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const parse = {
6767
};
6868

6969
// Utilities
70-
export { splitVersion, isVersionUnsupportedBelow } from './common.ts';
70+
export { splitVersion, coerceVersion, isVersionUnsupportedBelow, minVersion } from './semver.ts';
7171
export { createPrinter } from './utils.ts';
7272
export { sanitizeName } from './sanitize.ts';
7373
export { downloadJson } from './downloadJson.ts';

packages/sv-utils/src/semver.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import semverCoerce from 'semver/functions/coerce.js';
2+
import semverLt from 'semver/functions/lt.js';
3+
import semverMinVersion from 'semver/ranges/min-version.js';
4+
5+
type Version = {
6+
major?: number;
7+
minor?: number;
8+
patch?: number;
9+
/** The clean `major.minor.patch` string. Only populated by `coerceVersion`. */
10+
version?: string;
11+
};
12+
13+
/**
14+
* Returns the lowest version that satisfies the given range, e.g.
15+
* `^9.0.0` -> `9.0.0`, `~1.2.3` -> `1.2.3`, `workspace:^5.4.3` -> `5.4.3`.
16+
* Throws on unparseable inputs like `latest` or `workspace:*`.
17+
*/
18+
export function minVersion(range: string): string {
19+
const cleaned = range.replace(/^workspace:/, '');
20+
if (cleaned === '*' || cleaned === '') {
21+
throw new Error(`Cannot determine min version from range: ${range}`);
22+
}
23+
const min = semverMinVersion(cleaned);
24+
if (!min) throw new Error(`Cannot determine min version from range: ${range}`);
25+
return min.version;
26+
}
27+
28+
/**
29+
* @deprecated Use `coerceVersion` instead.
30+
*/
31+
export function splitVersion(str: string): Version {
32+
const [major, minor, patch] = str?.split('.') ?? [];
33+
34+
function toVersionNumber(val: string | undefined): number | undefined {
35+
return val !== undefined && val !== '' && !isNaN(Number(val)) ? Number(val) : undefined;
36+
}
37+
38+
return {
39+
major: toVersionNumber(major),
40+
minor: toVersionNumber(minor),
41+
patch: toVersionNumber(patch)
42+
};
43+
}
44+
45+
/**
46+
* Parses a version-ish string into `{ major, minor, patch, version }` using `semver.coerce`.
47+
* `version` is the clean `major.minor.patch` string (e.g. `"9.0.0"` for `^9.0.0`).
48+
* Understands ranges (`^9.0.0`), partial versions (`18.13`), and `workspace:` prefixes.
49+
* Returns all-undefined for unparseable input.
50+
*/
51+
export function coerceVersion(str: string): Version {
52+
const c = semverCoerce(str);
53+
if (!c) return { major: undefined, minor: undefined, patch: undefined, version: undefined };
54+
return { major: c.major, minor: c.minor, patch: c.patch, version: c.version };
55+
}
56+
57+
export function isVersionUnsupportedBelow(
58+
versionStr: string,
59+
belowStr: string
60+
): boolean | undefined {
61+
const version = semverCoerce(versionStr);
62+
const below = semverCoerce(belowStr);
63+
if (!version || !below) return undefined;
64+
return semverLt(version, below);
65+
}
Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, describe, it } from 'vitest';
2-
import { splitVersion, isVersionUnsupportedBelow } from '../common.ts';
2+
import { splitVersion, coerceVersion, isVersionUnsupportedBelow, minVersion } from '../semver.ts';
33

44
describe('versionSplit', () => {
55
const combinationsVersionSplit = [
@@ -19,6 +19,37 @@ describe('versionSplit', () => {
1919
);
2020
});
2121

22+
describe('coerceVersion', () => {
23+
const combinationsCoerceVersion = [
24+
{ version: '18.13.0', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
25+
// semver.coerce regex-shifts: first numeric run becomes major
26+
{ version: 'x.13.0', expected: { major: 13, minor: 0, patch: 0, version: '13.0.0' } },
27+
// missing/non-numeric parts are filled with 0
28+
{ version: '18.y.0', expected: { major: 18, minor: 0, patch: 0, version: '18.0.0' } },
29+
{ version: '18.13.z', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
30+
{ version: '18', expected: { major: 18, minor: 0, patch: 0, version: '18.0.0' } },
31+
{ version: '18.13', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
32+
// ranges and `workspace:` prefix are understood
33+
{ version: '^9.0.0', expected: { major: 9, minor: 0, patch: 0, version: '9.0.0' } },
34+
{ version: '~1.2.3', expected: { major: 1, minor: 2, patch: 3, version: '1.2.3' } },
35+
{
36+
version: 'workspace:^5.4.3',
37+
expected: { major: 5, minor: 4, patch: 3, version: '5.4.3' }
38+
},
39+
// unparseable input
40+
{
41+
version: 'invalid',
42+
expected: { major: undefined, minor: undefined, patch: undefined, version: undefined }
43+
}
44+
];
45+
it.each(combinationsCoerceVersion)(
46+
'should return the correct version for $version',
47+
({ version, expected }) => {
48+
expect(coerceVersion(version)).toEqual(expected);
49+
}
50+
);
51+
});
52+
2253
describe('minimumRequirement', () => {
2354
const combinationsMinimumRequirement = [
2455
{ version: '17', below: '18.3.0', expected: true },
@@ -46,3 +77,18 @@ describe('minimumRequirement', () => {
4677
}
4778
);
4879
});
80+
81+
describe('minVersion', () => {
82+
it('returns the lowest version that satisfies the range', () => {
83+
expect(minVersion('^9.0.0')).toBe('9.0.0');
84+
expect(minVersion('~1.2.3')).toBe('1.2.3');
85+
expect(minVersion('workspace:^5.4.3')).toBe('5.4.3');
86+
expect(minVersion('2.x')).toBe('2.0.0');
87+
expect(minVersion('>=1.0.0 || >=2.3.1 <2.4.5')).toBe('1.0.0');
88+
});
89+
90+
it('throws on unparseable ranges', () => {
91+
expect(() => minVersion('latest')).toThrow();
92+
expect(() => minVersion('workspace:*')).toThrow();
93+
});
94+
});

packages/sv/src/addons/common.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { type SvelteAst, type TransformFn, transforms } from '@sveltejs/sv-utils';
22
import process from 'node:process';
33

4+
// This is in common because the eslint addon installs this version,
5+
// and the prettier addon uses this to check if the installed major version of
6+
// eslint is supported by `addEslintConfigPrettier(...)`.
7+
export const ESLINT_VERSION = /* update-deps: eslint */ '^10.2.0';
8+
49
export const addEslintConfigPrettier = transforms.script(({ ast, js }) => {
510
// if a default import for `eslint-plugin-svelte` already exists, then we'll use their specifier's name instead
611
const importNodes = ast.body.filter((n) => n.type === 'ImportDeclaration');

packages/sv/src/addons/eslint.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { log } from '@clack/prompts';
22
import { type AstTypes, transforms } from '@sveltejs/sv-utils';
33
import { defineAddon } from '../core/config.ts';
4-
import { addEslintConfigPrettier, getNodeTypesVersion } from './common.ts';
4+
import { addEslintConfigPrettier, ESLINT_VERSION, getNodeTypesVersion } from './common.ts';
55

66
export default defineAddon({
77
id: 'eslint',
@@ -12,7 +12,7 @@ export default defineAddon({
1212
const typescript = language === 'ts';
1313
const prettierInstalled = Boolean(dependencyVersion('prettier'));
1414

15-
sv.devDependency('eslint', '^10.2.0');
15+
sv.devDependency('eslint', ESLINT_VERSION);
1616
sv.devDependency('@eslint/compat', '^2.0.4');
1717
sv.devDependency('eslint-plugin-svelte', '^3.17.0');
1818
sv.devDependency('globals', '^17.4.0');

0 commit comments

Comments
 (0)