Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/free-ducks-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/sv-utils': patch
---

add `minVersion` & `coerceVersion` from `semver`. Deprecate `splitVersion`
5 changes: 5 additions & 0 deletions .changeset/tiny-pears-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': minor
---

fix(sv): align eslint version to `10` accross all addons
10 changes: 10 additions & 0 deletions packages/sv-utils/api-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -725,8 +725,16 @@ type Version = {
major?: number;
minor?: number;
patch?: number;
version?: string;
};

declare function minVersion(range: string): string;
/**
* @deprecated Use `coerceVersion` instead.
*/
declare function splitVersion(str: string): Version;

declare function coerceVersion(str: string): Version;
declare function isVersionUnsupportedBelow(
versionStr: string,
belowStr: string
Expand Down Expand Up @@ -796,6 +804,7 @@ export {
type TransformFn,
index_d_exports as Walker,
type YamlDocument,
coerceVersion,
color,
constructCommand,
createPrinter,
Expand All @@ -810,6 +819,7 @@ export {
json_d_exports as json,
loadFile,
loadPackageJson,
minVersion,
parse,
pnpm_d_exports as pnpm,
resolveCommand,
Expand Down
2 changes: 2 additions & 0 deletions packages/sv-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
},
"devDependencies": {
"@types/estree": "^1.0.8",
"@types/semver": "^7.7.1",
"decircular": "^1.0.0",
"dedent": "^1.7.0",
"esrap": "^2.2.2",
"package-manager-detector": "^1.6.0",
"semver": "^7.7.4",
"silver-fleece": "^1.2.1",
"smol-toml": "^1.5.2",
"svelte": "^5.53.0",
Expand Down
48 changes: 0 additions & 48 deletions packages/sv-utils/src/common.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/sv-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const parse = {
};

// Utilities
export { splitVersion, isVersionUnsupportedBelow } from './common.ts';
export { splitVersion, coerceVersion, isVersionUnsupportedBelow, minVersion } from './semver.ts';
export { createPrinter } from './utils.ts';
export { sanitizeName } from './sanitize.ts';
export { downloadJson } from './downloadJson.ts';
Expand Down
65 changes: 65 additions & 0 deletions packages/sv-utils/src/semver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import semverCoerce from 'semver/functions/coerce.js';
import semverLt from 'semver/functions/lt.js';
import semverMinVersion from 'semver/ranges/min-version.js';

type Version = {
major?: number;
minor?: number;
patch?: number;
/** The clean `major.minor.patch` string. Only populated by `coerceVersion`. */
version?: string;
};

/**
* Returns the lowest version that satisfies the given range, e.g.
* `^9.0.0` -> `9.0.0`, `~1.2.3` -> `1.2.3`, `workspace:^5.4.3` -> `5.4.3`.
* Throws on unparseable inputs like `latest` or `workspace:*`.
*/
export function minVersion(range: string): string {
const cleaned = range.replace(/^workspace:/, '');
if (cleaned === '*' || cleaned === '') {
throw new Error(`Cannot determine min version from range: ${range}`);
}
const min = semverMinVersion(cleaned);
if (!min) throw new Error(`Cannot determine min version from range: ${range}`);
return min.version;
}

/**
* @deprecated Use `coerceVersion` instead.
*/
export function splitVersion(str: string): Version {
const [major, minor, patch] = str?.split('.') ?? [];

function toVersionNumber(val: string | undefined): number | undefined {
return val !== undefined && val !== '' && !isNaN(Number(val)) ? Number(val) : undefined;
}

return {
major: toVersionNumber(major),
minor: toVersionNumber(minor),
patch: toVersionNumber(patch)
};
}

/**
* Parses a version-ish string into `{ major, minor, patch, version }` using `semver.coerce`.
* `version` is the clean `major.minor.patch` string (e.g. `"9.0.0"` for `^9.0.0`).
* Understands ranges (`^9.0.0`), partial versions (`18.13`), and `workspace:` prefixes.
* Returns all-undefined for unparseable input.
*/
export function coerceVersion(str: string): Version {
const c = semverCoerce(str);
if (!c) return { major: undefined, minor: undefined, patch: undefined, version: undefined };
return { major: c.major, minor: c.minor, patch: c.patch, version: c.version };
}

export function isVersionUnsupportedBelow(
versionStr: string,
belowStr: string
): boolean | undefined {
const version = semverCoerce(versionStr);
const below = semverCoerce(belowStr);
if (!version || !below) return undefined;
return semverLt(version, below);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, describe, it } from 'vitest';
import { splitVersion, isVersionUnsupportedBelow } from '../common.ts';
import { splitVersion, coerceVersion, isVersionUnsupportedBelow, minVersion } from '../semver.ts';

describe('versionSplit', () => {
const combinationsVersionSplit = [
Expand All @@ -19,6 +19,37 @@ describe('versionSplit', () => {
);
});

describe('coerceVersion', () => {
const combinationsCoerceVersion = [
{ version: '18.13.0', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
// semver.coerce regex-shifts: first numeric run becomes major
{ version: 'x.13.0', expected: { major: 13, minor: 0, patch: 0, version: '13.0.0' } },
// missing/non-numeric parts are filled with 0
{ version: '18.y.0', expected: { major: 18, minor: 0, patch: 0, version: '18.0.0' } },
{ version: '18.13.z', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
{ version: '18', expected: { major: 18, minor: 0, patch: 0, version: '18.0.0' } },
{ version: '18.13', expected: { major: 18, minor: 13, patch: 0, version: '18.13.0' } },
// ranges and `workspace:` prefix are understood
{ version: '^9.0.0', expected: { major: 9, minor: 0, patch: 0, version: '9.0.0' } },
{ version: '~1.2.3', expected: { major: 1, minor: 2, patch: 3, version: '1.2.3' } },
{
version: 'workspace:^5.4.3',
expected: { major: 5, minor: 4, patch: 3, version: '5.4.3' }
},
// unparseable input
{
version: 'invalid',
expected: { major: undefined, minor: undefined, patch: undefined, version: undefined }
}
];
it.each(combinationsCoerceVersion)(
'should return the correct version for $version',
({ version, expected }) => {
expect(coerceVersion(version)).toEqual(expected);
}
);
});

describe('minimumRequirement', () => {
const combinationsMinimumRequirement = [
{ version: '17', below: '18.3.0', expected: true },
Expand Down Expand Up @@ -46,3 +77,18 @@ describe('minimumRequirement', () => {
}
);
});

describe('minVersion', () => {
it('returns the lowest version that satisfies the range', () => {
expect(minVersion('^9.0.0')).toBe('9.0.0');
expect(minVersion('~1.2.3')).toBe('1.2.3');
expect(minVersion('workspace:^5.4.3')).toBe('5.4.3');
expect(minVersion('2.x')).toBe('2.0.0');
expect(minVersion('>=1.0.0 || >=2.3.1 <2.4.5')).toBe('1.0.0');
});

it('throws on unparseable ranges', () => {
expect(() => minVersion('latest')).toThrow();
expect(() => minVersion('workspace:*')).toThrow();
});
});
5 changes: 5 additions & 0 deletions packages/sv/src/addons/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type SvelteAst, type TransformFn, transforms } from '@sveltejs/sv-utils';
import process from 'node:process';

// This is in common because the eslint addon installs this version,
// and the prettier addon uses this to check if the installed major version of
// eslint is supported by `addEslintConfigPrettier(...)`.
export const ESLINT_VERSION = /* update-deps: eslint */ '^10.2.0';

export const addEslintConfigPrettier = transforms.script(({ ast, js }) => {
// if a default import for `eslint-plugin-svelte` already exists, then we'll use their specifier's name instead
const importNodes = ast.body.filter((n) => n.type === 'ImportDeclaration');
Expand Down
4 changes: 2 additions & 2 deletions packages/sv/src/addons/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log } from '@clack/prompts';
import { type AstTypes, transforms } from '@sveltejs/sv-utils';
import { defineAddon } from '../core/config.ts';
import { addEslintConfigPrettier, getNodeTypesVersion } from './common.ts';
import { addEslintConfigPrettier, ESLINT_VERSION, getNodeTypesVersion } from './common.ts';

export default defineAddon({
id: 'eslint',
Expand All @@ -12,7 +12,7 @@ export default defineAddon({
const typescript = language === 'ts';
const prettierInstalled = Boolean(dependencyVersion('prettier'));

sv.devDependency('eslint', '^10.2.0');
sv.devDependency('eslint', ESLINT_VERSION);
sv.devDependency('@eslint/compat', '^2.0.4');
sv.devDependency('eslint-plugin-svelte', '^3.17.0');
sv.devDependency('globals', '^17.4.0');
Expand Down
22 changes: 11 additions & 11 deletions packages/sv/src/addons/prettier.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log } from '@clack/prompts';
import { color, dedent, transforms } from '@sveltejs/sv-utils';
import { color, coerceVersion, dedent, transforms } from '@sveltejs/sv-utils';
import { defineAddon } from '../core/config.ts';
import { addEslintConfigPrettier } from './common.ts';
import { addEslintConfigPrettier, ESLINT_VERSION } from './common.ts';

export default defineAddon({
id: 'prettier',
Expand Down Expand Up @@ -70,8 +70,7 @@ export default defineAddon({
)
);

const eslintVersion = dependencyVersion('eslint');
const eslintInstalled = hasEslint(eslintVersion);
const eslintInfo = checkEslint(dependencyVersion('eslint'));

sv.file(
file.package,
Expand All @@ -88,23 +87,24 @@ export default defineAddon({
})
);

if (eslintVersion?.startsWith(SUPPORTED_ESLINT_VERSION) === false) {
if (eslintInfo.installed && !eslintInfo.supported) {
log.warn(
`An older major version of ${color.warning(
`An unsupported major version of ${color.warning(
'eslint'
)} was detected. Skipping ${color.warning('eslint-config-prettier')} installation.`
);
}

if (eslintInstalled) {
if (eslintInfo.supported) {
sv.devDependency('eslint-config-prettier', '^10.1.8');
sv.file('eslint.config.js', addEslintConfigPrettier);
}
}
});

const SUPPORTED_ESLINT_VERSION = '9';

function hasEslint(version: string | undefined): boolean {
return !!version && version.startsWith(SUPPORTED_ESLINT_VERSION);
function checkEslint(version: string | undefined): { installed: boolean; supported: boolean } {
if (!version) return { installed: false, supported: false };
const supportedMajor = coerceVersion(ESLINT_VERSION).major;
const installedMajor = coerceVersion(version).major;
return { installed: true, supported: installedMajor === supportedMajor };
Comment thread
jycouet marked this conversation as resolved.
}
Loading
Loading