diff --git a/.changeset/great-bags-smoke.md b/.changeset/great-bags-smoke.md new file mode 100644 index 00000000..97529e4b --- /dev/null +++ b/.changeset/great-bags-smoke.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat(add): Add minimum Node.js version warning if below 16. diff --git a/packages/cli/utils/common.ts b/packages/cli/utils/common.ts index b2282f97..857850f5 100644 --- a/packages/cli/utils/common.ts +++ b/packages/cli/utils/common.ts @@ -4,6 +4,7 @@ import * as p from '@clack/prompts'; import type { Argument, HelpConfiguration, Option } from 'commander'; import { UnsupportedError } from './errors.ts'; import process from 'node:process'; +import { minimumRequirement } from '@sveltejs/cli-core'; const NO_PREFIX = '--no-'; let options: readonly Option[] = []; @@ -75,6 +76,14 @@ type MaybePromise = () => Promise | void; export async function runCommand(action: MaybePromise): Promise { try { p.intro(`Welcome to the Svelte CLI! ${pc.gray(`(v${pkg.version})`)}`); + + const unsupported = minimumRequirement('18.3').for(process.versions.node); + if (unsupported) { + p.log.warn( + `You are using Node.js ${pc.red(process.versions.node)}, please upgrade to Node.js 18.3 or higher.` + ); + } + await action(); p.outro("You're all set!"); } catch (e) { diff --git a/packages/core/common.ts b/packages/core/common.ts new file mode 100644 index 00000000..64d0a5c0 --- /dev/null +++ b/packages/core/common.ts @@ -0,0 +1,57 @@ +type Version = { + major?: number; + minor?: number; + patch?: number; +}; + +export function versionSplit(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) + }; +} + +function versionUnsupportedBelow(version_str: string, below_str: string): boolean | undefined { + const version = versionSplit(version_str); + const below = versionSplit(below_str); + + if (version.major === undefined || below.major === undefined) return undefined; + if (version.major < below.major) return true; + if (version.major > below.major) return false; + + if (version.minor === undefined || below.minor === undefined) { + if (version.major === below.major) return false; + else return true; + } + if (version.minor < below.minor) return true; + if (version.minor > below.minor) return false; + + if (version.patch === undefined || below.patch === undefined) { + if (version.minor === below.minor) return false; + else return true; + } + if (version.patch < below.patch) return true; + if (version.patch > below.patch) return false; + if (version.patch === below.patch) return false; + + return undefined; +} + +/** + * @example + * const unsupported = minimumRequirement('18.3').for(process.versions.node); + */ +export function minimumRequirement(version: string): { + for: (target: string) => boolean | undefined; +} { + return { + for: (target: string) => versionUnsupportedBelow(target, version) + }; +} diff --git a/packages/core/index.ts b/packages/core/index.ts index e22448cb..90d3fa07 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -3,6 +3,7 @@ export { log } from '@clack/prompts'; export { default as colors } from 'picocolors'; export { default as dedent } from 'dedent'; export * as utils from './utils.ts'; +export { minimumRequirement, versionSplit } from './common.ts'; export type * from './addon/processors.ts'; export type * from './addon/options.ts'; diff --git a/packages/core/tests/common.ts b/packages/core/tests/common.ts new file mode 100644 index 00000000..fdf6be33 --- /dev/null +++ b/packages/core/tests/common.ts @@ -0,0 +1,50 @@ +import { expect, describe, it } from 'vitest'; +import { versionSplit, minimumRequirement } from '../common.ts'; + +describe('versionSplit', () => { + const combinationsVersionSplit = [ + { version: '18.13.0', expected: { major: 18, minor: 13, patch: 0 } }, + { version: 'x.13.0', expected: { major: undefined, minor: 13, patch: 0 } }, + { version: '18.y.0', expected: { major: 18, minor: undefined, patch: 0 } }, + { version: '18.13.z', expected: { major: 18, minor: 13, patch: undefined } }, + { version: '18', expected: { major: 18, minor: undefined, patch: undefined } }, + { version: '18.13', expected: { major: 18, minor: 13, patch: undefined } }, + { version: 'invalid', expected: { major: undefined, minor: undefined, patch: undefined } } + ]; + it.each(combinationsVersionSplit)( + 'should return the correct version for $version', + ({ version, expected }) => { + expect(versionSplit(version)).toEqual(expected); + } + ); +}); + +describe('minimumRequirement', () => { + const combinationsMinimumRequirement = [ + { version: '17', below: '18.3.0', expected: true }, + { version: '18.2', below: '18.3.0', expected: true }, + { version: '18.3.0', below: '18.3.1', expected: true }, + { version: '18.3.1', below: '18.3.0', expected: false }, + { version: '18.3.0', below: '18.3.0', expected: false }, + { version: '18.3.0', below: '18.3', expected: false }, + { version: '18.3.1', below: '18.3', expected: false }, + { version: '18.3.1', below: '18', expected: false }, + { version: '18', below: '18', expected: false }, + { version: 'a', below: 'b', expected: undefined }, + { version: '18.3', below: '18.3', expected: false }, + { version: '18.4', below: '18.3', expected: false }, + { version: '18.2', below: '18.3', expected: true }, + + // if it's undefined, we can't say anything... + { version: undefined!, below: '18.3', expected: undefined }, + { version: '', below: '18.3', expected: undefined } + ] as const; + it.each(combinationsMinimumRequirement)( + '($version below $below) should be $expected', + ({ version, below, expected }) => { + expect(minimumRequirement(below).for(version)).toEqual(expected); + } + ); +}); + +// minimumRequirement(4.0).for(nodeVersion)