From c2944e9faf80e3fce3502bb81a5871f98c4164aa Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:16:51 -0400 Subject: [PATCH 1/2] fix(core): depend on the current @nodevu/parsefiles --- core/package.json | 2 +- core/test/alpha.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/package.json b/core/package.json index 57f96ec..cc8b2c8 100644 --- a/core/package.json +++ b/core/package.json @@ -22,7 +22,7 @@ "/util/prod" ], "dependencies": { - "@nodevu/parsefiles": "^0.0.3", + "@nodevu/parsefiles": "^1.1.0", "luxon": "^3.5.0", "semver": "^7.6.3" }, diff --git a/core/test/alpha.js b/core/test/alpha.js index 59e8d74..9e867bd 100644 --- a/core/test/alpha.js +++ b/core/test/alpha.js @@ -85,7 +85,10 @@ describe('check to make sure that Alpha releases work as expected', async () => 'node-v27.0.0-alpha.1.0.0-linux-x64.tar.gz', 'node-v27.0.0-alpha.1.0.0-linux-x64.tar.xz', ]); - deepStrictEqual(source[0].files, ['node-v27.0.0-alpha.1.0.0.tar.xz']); + deepStrictEqual(source[0].files, [ + 'node-v27.0.0-alpha.1.0.0.tar.gz', + 'node-v27.0.0-alpha.1.0.0.tar.xz', + ]); }); it('should have correct support values for a release line with an Alpha phase', async () => { From 31dfb2d087b57c4a7abc202cfbb5756bbc50f768 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:16:53 -0400 Subject: [PATCH 2/2] feat: ship TypeScript types for every package --- .c8rc.json | 2 +- .github/workflows/ci.yml | 29 +++- .oxfmtrc.json | 3 +- .oxlintrc.json | 3 +- aliases/index.js | 9 + aliases/package.json | 7 +- aliases/tsconfig.json | 5 + aliases/types/index.d.ts | 11 ++ core/index.js | 191 ++++++++++++++++++--- core/package.json | 7 +- core/tsconfig.json | 5 + core/types/index.d.ts | 213 ++++++++++++++++++++++++ core/types/util/prod/optionsParser.d.ts | 21 +++ core/types/util/prod/schedule.d.ts | 26 +++ core/types/util/prod/versions.d.ts | 41 +++++ core/util/prod/optionsParser.js | 12 ++ core/util/prod/schedule.js | 17 ++ core/util/prod/versions.js | 21 +++ fetchindex/index.js | 25 +++ fetchindex/package.json | 6 +- fetchindex/tsconfig.json | 5 + fetchindex/types/index.d.ts | 50 ++++++ newest/index.js | 23 +++ newest/package.json | 6 +- newest/tsconfig.json | 5 + newest/types/index.d.ts | 29 ++++ oldest/index.js | 23 +++ oldest/package.json | 6 +- oldest/tsconfig.json | 5 + oldest/types/index.d.ts | 29 ++++ opt/index.js | 15 ++ opt/package.json | 6 +- opt/tsconfig.json | 5 + opt/types/index.d.ts | 34 ++++ package.json | 9 +- parsefiles/index.js | 20 ++- parsefiles/package.json | 6 +- parsefiles/tsconfig.json | 5 + parsefiles/types/index.d.ts | 35 ++++ ranges/index.js | 36 +++- ranges/package.json | 6 +- ranges/tsconfig.json | 5 + ranges/types/index.d.ts | 36 ++++ static/index.js | 27 ++- static/json.d.ts | 6 + static/package.json | 12 +- static/tsconfig.json | 9 + static/types/index.d.ts | 8 + test/types.mts | 42 +++++ translate/index.js | 12 +- translate/package.json | 7 +- translate/tsconfig.json | 5 + translate/types/index.d.ts | 17 ++ tsconfig.base.json | 17 ++ tsconfig.json | 22 +++ 55 files changed, 1177 insertions(+), 60 deletions(-) create mode 100644 aliases/tsconfig.json create mode 100644 aliases/types/index.d.ts create mode 100644 core/tsconfig.json create mode 100644 core/types/index.d.ts create mode 100644 core/types/util/prod/optionsParser.d.ts create mode 100644 core/types/util/prod/schedule.d.ts create mode 100644 core/types/util/prod/versions.d.ts create mode 100644 fetchindex/tsconfig.json create mode 100644 fetchindex/types/index.d.ts create mode 100644 newest/tsconfig.json create mode 100644 newest/types/index.d.ts create mode 100644 oldest/tsconfig.json create mode 100644 oldest/types/index.d.ts create mode 100644 opt/tsconfig.json create mode 100644 opt/types/index.d.ts create mode 100644 parsefiles/tsconfig.json create mode 100644 parsefiles/types/index.d.ts create mode 100644 ranges/tsconfig.json create mode 100644 ranges/types/index.d.ts create mode 100644 static/json.d.ts create mode 100644 static/tsconfig.json create mode 100644 static/types/index.d.ts create mode 100644 test/types.mts create mode 100644 translate/tsconfig.json create mode 100644 translate/types/index.d.ts create mode 100644 tsconfig.base.json create mode 100644 tsconfig.json diff --git a/.c8rc.json b/.c8rc.json index 0768307..94d3a2b 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -1,6 +1,6 @@ { "all": true, - "exclude": ["coverage/**", "test/**"], + "exclude": ["coverage/**", "test/**", "**/*.d.ts"], "check-coverage": true, "branches": 100, "functions": 100, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02678b..bdec5d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,33 @@ jobs: done exit "$status" + # The published types are generated from the JSDoc, and committed since + # releases pack without installing anything, so check they're current + types: + if: github.repository == 'cutenode/nodevu' + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: current + # the packages import each other, so link the workspaces + - name: Run npm install + run: npm install -ws --include-workspace-root + - name: Build the types + run: npm run types + - name: Check the committed types match the source + run: | + if [ -n "$(git status --porcelain)" ]; then + git status --porcelain + echo "::error::the committed types are out of date, run npm run types and commit the result" + exit 1 + fi + test: needs: changes if: needs.changes.outputs.packages != '[]' @@ -131,7 +158,7 @@ jobs: # required check that never reports blocks the merge ci-ok: if: always() - needs: [changes, lint, test] + needs: [changes, lint, types, test] runs-on: ubuntu-latest steps: - name: Fail if any job failed or was cancelled diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 4c2faa6..791c9ec 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -17,6 +17,7 @@ ".pi/**", ".roo/**", ".windsurf/**", - "tools/oxlint/anti-slop/**" + "tools/oxlint/anti-slop/**", + "*/types/**" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index e5776f0..007569d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,7 +12,8 @@ ".pi/**", ".roo/**", ".windsurf/**", - "tools/oxlint/anti-slop/**" + "tools/oxlint/anti-slop/**", + "*/types/**" ], "jsPlugins": [ { "name": "anti-slop", "specifier": "./tools/oxlint/anti-slop/index.ts" } diff --git a/aliases/index.js b/aliases/index.js index 584945b..91dc010 100644 --- a/aliases/index.js +++ b/aliases/index.js @@ -1,3 +1,12 @@ +/** + * @typedef {'alpha' | 'current' | 'lts/latest' | 'lts/active' | 'lts/maintenance' | 'eol' | 'all'} Alias + */ + +/** + * The names of the ranges of release lines `@nodevu/ranges` works out. + * + * @type {Alias[]} + */ const aliases = [ 'alpha', 'current', diff --git a/aliases/package.json b/aliases/package.json index afbfc43..79f81a5 100644 --- a/aliases/package.json +++ b/aliases/package.json @@ -23,5 +23,10 @@ "test": "", "updates:check": "npx npm-check-updates", "updates:update": "npx npm-check-updates -u" - } + }, + "types": "types/index.d.ts", + "files": [ + "index.js", + "/types" + ] } diff --git a/aliases/tsconfig.json b/aliases/tsconfig.json new file mode 100644 index 0000000..9767eef --- /dev/null +++ b/aliases/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [] +} diff --git a/aliases/types/index.d.ts b/aliases/types/index.d.ts new file mode 100644 index 0000000..1b1a98e --- /dev/null +++ b/aliases/types/index.d.ts @@ -0,0 +1,11 @@ +/** + * @typedef {'alpha' | 'current' | 'lts/latest' | 'lts/active' | 'lts/maintenance' | 'eol' | 'all'} Alias + */ +export = aliases; +export type Alias = 'alpha' | 'current' | 'lts/latest' | 'lts/active' | 'lts/maintenance' | 'eol' | 'all'; +/** + * The names of the ranges of release lines `@nodevu/ranges` works out. + * + * @type {Alias[]} + */ +declare const aliases: Alias[]; diff --git a/core/index.js b/core/index.js index 298a378..26ec5bd 100644 --- a/core/index.js +++ b/core/index.js @@ -5,25 +5,129 @@ const optionsParser = require('./util/prod/optionsParser'); const versionFetcher = require('./util/prod/versions'); const scheduleFetcher = require('./util/prod/schedule'); +/** + * @typedef {object} Options + * @property {typeof fetch} [fetch] The `fetch` implementation to request the + * data with. Defaults to `globalThis.fetch`. + * @property {string | import('luxon').DateTime} [now] An ISO 8601 date to work + * out each release line's phase against. Defaults to now. + * @property {object} [urls] + * @property {string} [urls.index] The full URL of the release index. + * @property {string} [urls.schedule] The full URL of the release schedule. + */ + +/** + * @typedef {'alpha' | 'start' | 'lts' | 'maintenance' | 'end'} Phase + */ + +/** + * The start date of each phase of a release line, such as `'2021-04-20'`. + * + * @typedef {object} PhaseDates + * @property {string} [alpha] + * @property {string} [start] + * @property {string} [lts] + * @property {string} [maintenance] + * @property {string} [end] + */ + +/** + * @typedef {object} Support + * @property {string} [codename] The LTS codename, such as `'Iron'`. + * @property {{ newest?: string, oldest?: string }} [lts] The newest and first + * releases flagged as LTS. + * @property {object} phases + * @property {PhaseDates} phases.dates + * @property {Phase | Record} phases.current The phase the line + * is in on `now`, or an empty object when none has started yet. + */ + +/** + * @typedef {object} Security + * @property {string[]} all Every security release in the line, newest first. + * @property {string} [newest] + * @property {string} [oldest] + */ + +/** + * The files of one identifier from a release's `files`. + * + * @typedef {object} FileLink + * @property {string} id The identifier, such as `'linux-x64'`. + * @property {string[]} files The file names for the identifier. + * @property {string} [architecture] The architecture, such as `'x64'`. + */ + +/** + * @typedef {object} Release + * @property {object} semver + * @property {string} semver.raw The version without its `v`. + * @property {number} semver.major + * @property {number} semver.minor + * @property {number} semver.patch + * @property {string} semver.line The release line, such as `'v17'`. + * @property {string} [releaseDate] Such as `'2022-02-22'`. + * @property {{ version?: string }} modules The ABI version. + * @property {object} dependencies The versions of the bundled dependencies. + * @property {string} [dependencies.npm] + * @property {string} [dependencies.v8] + * @property {string} [dependencies.uv] + * @property {string} [dependencies.zlib] + * @property {string} [dependencies.openssl] + * @property {object} files + * @property {string[]} files.available The identifiers the index lists. + * @property {Record} files.links The files of each + * identifier, grouped by type, such as `linux` or `windows`. + * @property {{ isLts: boolean }} lts + * @property {{ isAlpha: boolean }} alpha + * @property {{ isSecurity: boolean }} security + */ + +/** + * @typedef {object} ReleaseLine + * @property {Record} releases Every release in the line, + * newest first, keyed by version, such as `v17.6.0`. + * @property {Support} [support] Only present when the release schedule has a + * start date for the line. + * @property {Security} security + */ + +/** + * Every release line, newest first, keyed by name, such as `v22` or `v0.10`. + * + * @typedef {Record} Data + */ + +/** + * Fetches the Node.js release index and release schedule and merges them into + * one object keyed by release line. + * + * @param {Options} [options] + * @returns {Promise} + */ async function core(options) { // parse our user's options and set up our fetch/DateTime implementations const parsedOptions = optionsParser(options); - const now = DateTime.fromISO(parsedOptions.now); + const now = DateTime.fromISO(String(parsedOptions.now)); // collect and configure our data sources const versions = await versionFetcher(parsedOptions); const schedule = await scheduleFetcher(parsedOptions); // instantiate our data object, to be expanded upon + /** @type {Data} */ const data = {}; - for (const version of Object.keys(versions)) { + for (const version of versions.keys()) { // keep the pre-release identifiers, otherwise every Alpha release (like // `v27.0.0-alpha.0.0.0`) would be coerced into `v27.0.0` - const versionSemver = semver.coerce(versions[version].version, { - includePrerelease: true, - }); + // every version in index.json parses, so this is never null + const versionSemver = /** @type {import('semver').SemVer} */ ( + semver.coerce(versions[version].version, { + includePrerelease: true, + }) + ); const name = versionSemver.major !== 0 @@ -32,14 +136,15 @@ async function core(options) { // define the shape of the object we're going to use if (!data[name]) { - data[name] = {}; + data[name] = /** @type {ReleaseLine} */ ({}); } if (!data[name].releases) { data[name].releases = {}; } - data[name].releases[`v${versionSemver.version}`] = {}; + data[name].releases[`v${versionSemver.version}`] = + /** @type {Release} */ ({}); // define semver object const semverToReturn = { @@ -76,7 +181,8 @@ async function core(options) { versions[version].openssl ?? undefined; // surface file information - data[name].releases[`v${versionSemver.version}`].files = {}; + data[name].releases[`v${versionSemver.version}`].files = + /** @type {Release['files']} */ ({}); // TODO: parse versions[version].files and convert them to URLs that can be directly accessed data[name].releases[`v${versionSemver.version}`].files.available = @@ -93,16 +199,20 @@ async function core(options) { for (const filename of Object.keys(availableShorthand)) { const id = data[name].releases[`v${versionSemver.version}`].files.available[ - filename + Number(filename) ]; const parsedFile = parsefiles(id, versionSemver.version); - if (!linksShorthand[parsedFile.type]) { - linksShorthand[parsedFile.type] = []; + // every identifier in the index has a type so far. one parsefiles + // doesn't know yet is grouped under 'undefined' + const type = String(parsedFile.type); + + if (!linksShorthand[type]) { + linksShorthand[type] = []; } - linksShorthand[parsedFile.type].push({ + linksShorthand[type].push({ id: parsedFile.id, files: parsedFile.files, architecture: parsedFile.architecture, @@ -115,7 +225,7 @@ async function core(options) { // hack-y way to skip this logic on releases that don't have a listed start if (!data[name].support) { // check to see if we've already written it. if we have, we don't need to waste time on it. - data[name].support = {}; + data[name].support = /** @type {Support} */ ({}); data[name].support.codename = schedule[name]?.codename ?? undefined; data[name].support.lts = schedule[name]?.lts ? {} : undefined; @@ -129,7 +239,7 @@ async function core(options) { data[name].support.lts.newest = versionSemver.version; } - data[name].support.phases = {}; + data[name].support.phases = /** @type {Support['phases']} */ ({}); data[name].support.phases.dates = {}; data[name].support.phases.dates.alpha = schedule[name]?.alpha ?? undefined; @@ -158,7 +268,8 @@ async function core(options) { } // ## define the lts object in each specific version - data[name].releases[`v${versionSemver.version}`].lts = {}; + data[name].releases[`v${versionSemver.version}`].lts = + /** @type {{ isLts: boolean }} */ ({}); data[name].releases[`v${versionSemver.version}`].lts.isLts = !!versions[version].lts; @@ -168,7 +279,8 @@ async function core(options) { // // Alpha releases are the pre-releases a release line gets before its // first Current release, versioned as `vN.0.0-alpha.X.Y.Z` - data[name].releases[`v${versionSemver.version}`].alpha = {}; + data[name].releases[`v${versionSemver.version}`].alpha = + /** @type {{ isAlpha: boolean }} */ ({}); data[name].releases[`v${versionSemver.version}`].alpha.isAlpha = versionSemver.prerelease[0] === 'alpha'; @@ -177,7 +289,7 @@ async function core(options) { // ## define the release-line specific security object if (!data[name].security) { // check to see if we've already written it. if we have, we don't need to waste time on it. - data[name].security = {}; + data[name].security = /** @type {Security} */ ({}); data[name].security.all = []; } @@ -199,7 +311,8 @@ async function core(options) { } // ## define the security object in each specfic version - data[name].releases[`v${versionSemver.version}`].security = {}; + data[name].releases[`v${versionSemver.version}`].security = + /** @type {{ isSecurity: boolean }} */ ({}); data[name].releases[`v${versionSemver.version}`].security.isSecurity = versions[version].security ?? false; @@ -208,26 +321,50 @@ async function core(options) { return data; } +/** + * @param {import('luxon').DateTime} now + * @param {PhaseDates} [dates] + * @returns {Phase | undefined} + */ function determineCurrentReleasePhase(now, dates = {}) { // here we figure out if the dates for each release line passed is in the past or future // `true` is in the past // `false` is in the future - // `undefined` is a missing date + // `undefined` is a missing date, which parses as an invalid DateTime const isoified = { - alpha: isInPast(DateTime.fromISO(dates.alpha).diff(now).toMillis()), - start: isInPast(DateTime.fromISO(dates.start).diff(now).toMillis()), - lts: isInPast(DateTime.fromISO(dates.lts).diff(now).toMillis()), + alpha: isInPast( + DateTime.fromISO(dates.alpha ?? '') + .diff(now) + .toMillis(), + ), + start: isInPast( + DateTime.fromISO(dates.start ?? '') + .diff(now) + .toMillis(), + ), + lts: isInPast( + DateTime.fromISO(dates.lts ?? '') + .diff(now) + .toMillis(), + ), maintenance: isInPast( - DateTime.fromISO(dates.maintenance).diff(now).toMillis(), + DateTime.fromISO(dates.maintenance ?? '') + .diff(now) + .toMillis(), + ), + end: isInPast( + DateTime.fromISO(dates.end ?? '') + .diff(now) + .toMillis(), ), - end: isInPast(DateTime.fromISO(dates.end).diff(now).toMillis()), }; // set up our result to return + /** @type {Phase | undefined} */ let result; // iterate over the past/future object and set the above variable to whatever the first date is in the future. - for (const phase of Object.keys(isoified)) { + for (const phase of /** @type {Phase[]} */ (Object.keys(isoified))) { // since we're looping, the last true is the current phase // since the start (or alpha) date will always be in the past if (isoified[phase] === true) { @@ -244,6 +381,10 @@ function determineCurrentReleasePhase(now, dates = {}) { // DateTime.fromISO(DATE).diffNow().toMillis() // // where DATE is your date value +/** + * @param {number} number + * @returns {boolean | undefined} + */ function isInPast(number) { const sign = Math.sign(number); diff --git a/core/package.json b/core/package.json index cc8b2c8..69beb09 100644 --- a/core/package.json +++ b/core/package.json @@ -19,10 +19,12 @@ "files": [ "index.js", "LICENSE", - "/util/prod" + "/util/prod", + "/types" ], "dependencies": { "@nodevu/parsefiles": "^1.1.0", + "@types/luxon": "^3.7.6", "luxon": "^3.5.0", "semver": "^7.6.3" }, @@ -34,5 +36,6 @@ "repository": { "type": "git", "url": "git+https://github.com/cutenode/nodevu.git" - } + }, + "types": "types/index.d.ts" } diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 0000000..78be28d --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js", "util/prod/*.js"], + "references": [{ "path": "../parsefiles" }] +} diff --git a/core/types/index.d.ts b/core/types/index.d.ts new file mode 100644 index 0000000..0f62620 --- /dev/null +++ b/core/types/index.d.ts @@ -0,0 +1,213 @@ +export = core; +export type Options = { + /** + * The `fetch` implementation to request the + * data with. Defaults to `globalThis.fetch`. + */ + fetch?: typeof fetch; + /** + * An ISO 8601 date to work + * out each release line's phase against. Defaults to now. + */ + now?: string | import('luxon').DateTime; + urls?: { + index?: string; + schedule?: string; + }; +}; +export type Phase = 'alpha' | 'start' | 'lts' | 'maintenance' | 'end'; +export type PhaseDates = { + alpha?: string; + start?: string; + lts?: string; + maintenance?: string; + end?: string; +}; +export type Support = { + /** + * The LTS codename, such as `'Iron'`. + */ + codename?: string; + /** + * The newest and first + * releases flagged as LTS. + */ + lts?: { + newest?: string; + oldest?: string; + }; + phases: { + dates: PhaseDates; + current: Phase | Record; + }; +}; +export type Security = { + /** + * Every security release in the line, newest first. + */ + all: string[]; + newest?: string; + oldest?: string; +}; +export type FileLink = { + /** + * The identifier, such as `'linux-x64'`. + */ + id: string; + /** + * The file names for the identifier. + */ + files: string[]; + /** + * The architecture, such as `'x64'`. + */ + architecture?: string; +}; +export type Release = { + semver: { + raw: string; + major: number; + minor: number; + patch: number; + line: string; + }; + /** + * Such as `'2022-02-22'`. + */ + releaseDate?: string; + /** + * The ABI version. + */ + modules: { + version?: string; + }; + /** + * The versions of the bundled dependencies. + */ + dependencies: { + npm?: string; + v8?: string; + uv?: string; + zlib?: string; + openssl?: string; + }; + files: { + available: string[]; + links: Record; + }; + lts: { + isLts: boolean; + }; + alpha: { + isAlpha: boolean; + }; + security: { + isSecurity: boolean; + }; +}; +export type ReleaseLine = { + /** + * Every release in the line, + * newest first, keyed by version, such as `v17.6.0`. + */ + releases: Record; + /** + * Only present when the release schedule has a + * start date for the line. + */ + support?: Support; + security: Security; +}; +export type Data = Record; +/** + * @typedef {object} Options + * @property {typeof fetch} [fetch] The `fetch` implementation to request the + * data with. Defaults to `globalThis.fetch`. + * @property {string | import('luxon').DateTime} [now] An ISO 8601 date to work + * out each release line's phase against. Defaults to now. + * @property {object} [urls] + * @property {string} [urls.index] The full URL of the release index. + * @property {string} [urls.schedule] The full URL of the release schedule. + */ +/** + * @typedef {'alpha' | 'start' | 'lts' | 'maintenance' | 'end'} Phase + */ +/** + * The start date of each phase of a release line, such as `'2021-04-20'`. + * + * @typedef {object} PhaseDates + * @property {string} [alpha] + * @property {string} [start] + * @property {string} [lts] + * @property {string} [maintenance] + * @property {string} [end] + */ +/** + * @typedef {object} Support + * @property {string} [codename] The LTS codename, such as `'Iron'`. + * @property {{ newest?: string, oldest?: string }} [lts] The newest and first + * releases flagged as LTS. + * @property {object} phases + * @property {PhaseDates} phases.dates + * @property {Phase | Record} phases.current The phase the line + * is in on `now`, or an empty object when none has started yet. + */ +/** + * @typedef {object} Security + * @property {string[]} all Every security release in the line, newest first. + * @property {string} [newest] + * @property {string} [oldest] + */ +/** + * The files of one identifier from a release's `files`. + * + * @typedef {object} FileLink + * @property {string} id The identifier, such as `'linux-x64'`. + * @property {string[]} files The file names for the identifier. + * @property {string} [architecture] The architecture, such as `'x64'`. + */ +/** + * @typedef {object} Release + * @property {object} semver + * @property {string} semver.raw The version without its `v`. + * @property {number} semver.major + * @property {number} semver.minor + * @property {number} semver.patch + * @property {string} semver.line The release line, such as `'v17'`. + * @property {string} [releaseDate] Such as `'2022-02-22'`. + * @property {{ version?: string }} modules The ABI version. + * @property {object} dependencies The versions of the bundled dependencies. + * @property {string} [dependencies.npm] + * @property {string} [dependencies.v8] + * @property {string} [dependencies.uv] + * @property {string} [dependencies.zlib] + * @property {string} [dependencies.openssl] + * @property {object} files + * @property {string[]} files.available The identifiers the index lists. + * @property {Record} files.links The files of each + * identifier, grouped by type, such as `linux` or `windows`. + * @property {{ isLts: boolean }} lts + * @property {{ isAlpha: boolean }} alpha + * @property {{ isSecurity: boolean }} security + */ +/** + * @typedef {object} ReleaseLine + * @property {Record} releases Every release in the line, + * newest first, keyed by version, such as `v17.6.0`. + * @property {Support} [support] Only present when the release schedule has a + * start date for the line. + * @property {Security} security + */ +/** + * Every release line, newest first, keyed by name, such as `v22` or `v0.10`. + * + * @typedef {Record} Data + */ +/** + * Fetches the Node.js release index and release schedule and merges them into + * one object keyed by release line. + * + * @param {Options} [options] + * @returns {Promise} + */ +declare function core(options?: Options): Promise; diff --git a/core/types/util/prod/optionsParser.d.ts b/core/types/util/prod/optionsParser.d.ts new file mode 100644 index 0000000..6caad4a --- /dev/null +++ b/core/types/util/prod/optionsParser.d.ts @@ -0,0 +1,21 @@ +export = parseOptions; +import { DateTime } from 'luxon'; +export type ParsedOptions = { + fetch: typeof fetch; + now: string | DateTime; + urls: { + index: string; + schedule: string; + }; +}; +/** + * @typedef {object} ParsedOptions + * @property {typeof fetch} fetch + * @property {string | DateTime} now + * @property {{ index: string, schedule: string }} urls + */ +/** + * @param {import('../../index').Options} [options] + * @returns {ParsedOptions} + */ +declare function parseOptions(options?: import('../../index').Options): ParsedOptions; diff --git a/core/types/util/prod/schedule.d.ts b/core/types/util/prod/schedule.d.ts new file mode 100644 index 0000000..fa58aeb --- /dev/null +++ b/core/types/util/prod/schedule.d.ts @@ -0,0 +1,26 @@ +/** + * The Node.js release schedule, keyed by release line. + * + * @typedef {Record} Schedule + */ +export = schedule; +export type Schedule = Record; +/** + * @param {import('./optionsParser').ParsedOptions} options + * @returns {Promise} + */ +declare function schedule(options: import('./optionsParser').ParsedOptions): Promise; diff --git a/core/types/util/prod/versions.d.ts b/core/types/util/prod/versions.d.ts new file mode 100644 index 0000000..dd81f75 --- /dev/null +++ b/core/types/util/prod/versions.d.ts @@ -0,0 +1,41 @@ +/** + * A release as the Node.js release index lists it. + * + * @typedef {object} IndexRelease + * @property {string} version Such as `'v17.6.0'`. + * @property {string} date + * @property {string[]} files + * @property {string} [npm] + * @property {string} [v8] + * @property {string} [uv] + * @property {string} [zlib] + * @property {string} [openssl] + * @property {string} [modules] + * @property {string | false} lts The LTS codename, or `false`. + * @property {boolean} security + */ +export = versions; +export type IndexRelease = { + /** + * Such as `'v17.6.0'`. + */ + version: string; + date: string; + files: string[]; + npm?: string; + v8?: string; + uv?: string; + zlib?: string; + openssl?: string; + modules?: string; + /** + * The LTS codename, or `false`. + */ + lts: string | false; + security: boolean; +}; +/** + * @param {import('./optionsParser').ParsedOptions} options + * @returns {Promise} + */ +declare function versions(options: import('./optionsParser').ParsedOptions): Promise; diff --git a/core/util/prod/optionsParser.js b/core/util/prod/optionsParser.js index 8324436..d47e683 100644 --- a/core/util/prod/optionsParser.js +++ b/core/util/prod/optionsParser.js @@ -1,11 +1,23 @@ const { DateTime } = require('luxon'); +/** + * @typedef {object} ParsedOptions + * @property {typeof fetch} fetch + * @property {string | DateTime} now + * @property {{ index: string, schedule: string }} urls + */ + // this function allows us to parse user-passed options. // // this is particularly useful for tests so we can reduce variables // and ensure that our test suite is able to be consistent. +/** + * @param {import('../../index').Options} [options] + * @returns {ParsedOptions} + */ function parseOptions(options) { // set up our defaults + /** @type {ParsedOptions} */ const parsedOptions = { fetch: globalThis.fetch, now: DateTime.now(), diff --git a/core/util/prod/schedule.js b/core/util/prod/schedule.js index 0a0435b..bc47c99 100644 --- a/core/util/prod/schedule.js +++ b/core/util/prod/schedule.js @@ -1,3 +1,20 @@ +/** + * The Node.js release schedule, keyed by release line. + * + * @typedef {Record} Schedule + */ + +/** + * @param {import('./optionsParser').ParsedOptions} options + * @returns {Promise} + */ async function schedule(options) { // parse our options and set up fetch if a custom fetch is passed const fetch = options.fetch; diff --git a/core/util/prod/versions.js b/core/util/prod/versions.js index 51102d2..38f0b9a 100644 --- a/core/util/prod/versions.js +++ b/core/util/prod/versions.js @@ -1,3 +1,24 @@ +/** + * A release as the Node.js release index lists it. + * + * @typedef {object} IndexRelease + * @property {string} version Such as `'v17.6.0'`. + * @property {string} date + * @property {string[]} files + * @property {string} [npm] + * @property {string} [v8] + * @property {string} [uv] + * @property {string} [zlib] + * @property {string} [openssl] + * @property {string} [modules] + * @property {string | false} lts The LTS codename, or `false`. + * @property {boolean} security + */ + +/** + * @param {import('./optionsParser').ParsedOptions} options + * @returns {Promise} + */ async function versions(options) { // parse our options and set up fetch if a custom fetch is passed const fetch = options.fetch; diff --git a/fetchindex/index.js b/fetchindex/index.js index 51102d2..26fc1a3 100644 --- a/fetchindex/index.js +++ b/fetchindex/index.js @@ -1,3 +1,28 @@ +/** + * A release as the Node.js release index lists it. + * + * @typedef {object} IndexRelease + * @property {string} version Such as `'v17.6.0'`. + * @property {string} date + * @property {string[]} files + * @property {string} [npm] + * @property {string} [v8] + * @property {string} [uv] + * @property {string} [zlib] + * @property {string} [openssl] + * @property {string} [modules] + * @property {string | false} lts The LTS codename, or `false`. + * @property {boolean} security + */ + +/** + * Fetches the Node.js release index. + * + * @param {object} options + * @param {typeof fetch} options.fetch The `fetch` implementation to use. + * @param {{ index: string }} options.urls The full URL of the index. + * @returns {Promise} Every release, newest first. + */ async function versions(options) { // parse our options and set up fetch if a custom fetch is passed const fetch = options.fetch; diff --git a/fetchindex/package.json b/fetchindex/package.json index 3b90cd7..1cbdbba 100644 --- a/fetchindex/package.json +++ b/fetchindex/package.json @@ -5,7 +5,8 @@ "main": "index.js", "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "scripts": { "lint": "oxlint && oxfmt --check", @@ -33,5 +34,6 @@ "c8": "^12.0.0", "luxon": "^3.5.0", "undici": "^8.12.2" - } + }, + "types": "types/index.d.ts" } diff --git a/fetchindex/tsconfig.json b/fetchindex/tsconfig.json new file mode 100644 index 0000000..9767eef --- /dev/null +++ b/fetchindex/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [] +} diff --git a/fetchindex/types/index.d.ts b/fetchindex/types/index.d.ts new file mode 100644 index 0000000..a4bee25 --- /dev/null +++ b/fetchindex/types/index.d.ts @@ -0,0 +1,50 @@ +/** + * A release as the Node.js release index lists it. + * + * @typedef {object} IndexRelease + * @property {string} version Such as `'v17.6.0'`. + * @property {string} date + * @property {string[]} files + * @property {string} [npm] + * @property {string} [v8] + * @property {string} [uv] + * @property {string} [zlib] + * @property {string} [openssl] + * @property {string} [modules] + * @property {string | false} lts The LTS codename, or `false`. + * @property {boolean} security + */ +export = versions; +export type IndexRelease = { + /** + * Such as `'v17.6.0'`. + */ + version: string; + date: string; + files: string[]; + npm?: string; + v8?: string; + uv?: string; + zlib?: string; + openssl?: string; + modules?: string; + /** + * The LTS codename, or `false`. + */ + lts: string | false; + security: boolean; +}; +/** + * Fetches the Node.js release index. + * + * @param {object} options + * @param {typeof fetch} options.fetch The `fetch` implementation to use. + * @param {{ index: string }} options.urls The full URL of the index. + * @returns {Promise} Every release, newest first. + */ +declare function versions(options: { + fetch: typeof globalThis.fetch; + urls: { + index: string; + }; +}): Promise; diff --git a/newest/index.js b/newest/index.js index f721030..dcedcad 100644 --- a/newest/index.js +++ b/newest/index.js @@ -1,5 +1,14 @@ const nodevu = require('@nodevu/core'); +/** + * Finds the newest LTS or security release of a release line. + * + * @param {string} name The release line, such as `'v16'` or `'v0.10'`. + * @param {'lts' | 'security'} type Which kind of release to find. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} The version without its `v`, or + * `undefined` when the line has no such release. + */ async function newest(name, type, options) { const data = await nodevu(options); @@ -15,10 +24,24 @@ async function newest(name, type, options) { } } +/** + * Finds the newest LTS release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ async function lts(name, options) { return await newest(name, 'lts', options); } +/** + * Finds the newest security release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ async function security(name, options) { return await newest(name, 'security', options); } diff --git a/newest/package.json b/newest/package.json index d4a6b22..07d6b0e 100644 --- a/newest/package.json +++ b/newest/package.json @@ -5,7 +5,8 @@ "main": "index.js", "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "scripts": { "lint": "oxlint && oxfmt --check", @@ -34,5 +35,6 @@ }, "devDependencies": { "c8": "^12.0.0" - } + }, + "types": "types/index.d.ts" } diff --git a/newest/tsconfig.json b/newest/tsconfig.json new file mode 100644 index 0000000..9171bea --- /dev/null +++ b/newest/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [{ "path": "../core" }] +} diff --git a/newest/types/index.d.ts b/newest/types/index.d.ts new file mode 100644 index 0000000..cd3d5e8 --- /dev/null +++ b/newest/types/index.d.ts @@ -0,0 +1,29 @@ +export { newest }; +export { lts }; +export { security }; +/** + * Finds the newest LTS or security release of a release line. + * + * @param {string} name The release line, such as `'v16'` or `'v0.10'`. + * @param {'lts' | 'security'} type Which kind of release to find. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} The version without its `v`, or + * `undefined` when the line has no such release. + */ +declare function newest(name: string, type: 'lts' | 'security', options?: import('@nodevu/core').Options): Promise; +/** + * Finds the newest LTS release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ +declare function lts(name: string, options?: import('@nodevu/core').Options): Promise; +/** + * Finds the newest security release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ +declare function security(name: string, options?: import('@nodevu/core').Options): Promise; diff --git a/oldest/index.js b/oldest/index.js index 186c34a..2d45490 100644 --- a/oldest/index.js +++ b/oldest/index.js @@ -1,5 +1,14 @@ const nodevu = require('@nodevu/core'); +/** + * Finds the oldest LTS or security release of a release line. + * + * @param {string} name The release line, such as `'v16'` or `'v0.10'`. + * @param {'lts' | 'security'} type Which kind of release to find. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} The version without its `v`, or + * `undefined` when the line has no such release. + */ async function oldest(name, type, options) { const data = await nodevu(options); @@ -15,10 +24,24 @@ async function oldest(name, type, options) { } } +/** + * Finds the oldest LTS release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ async function lts(name, options) { return await oldest(name, 'lts', options); } +/** + * Finds the oldest security release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ async function security(name, options) { return await oldest(name, 'security', options); } diff --git a/oldest/package.json b/oldest/package.json index 03f4667..90eecfa 100644 --- a/oldest/package.json +++ b/oldest/package.json @@ -5,7 +5,8 @@ "main": "index.js", "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "scripts": { "lint": "oxlint && oxfmt --check", @@ -34,5 +35,6 @@ }, "devDependencies": { "c8": "^12.0.0" - } + }, + "types": "types/index.d.ts" } diff --git a/oldest/tsconfig.json b/oldest/tsconfig.json new file mode 100644 index 0000000..9171bea --- /dev/null +++ b/oldest/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [{ "path": "../core" }] +} diff --git a/oldest/types/index.d.ts b/oldest/types/index.d.ts new file mode 100644 index 0000000..ebe7986 --- /dev/null +++ b/oldest/types/index.d.ts @@ -0,0 +1,29 @@ +export { oldest }; +export { lts }; +export { security }; +/** + * Finds the oldest LTS or security release of a release line. + * + * @param {string} name The release line, such as `'v16'` or `'v0.10'`. + * @param {'lts' | 'security'} type Which kind of release to find. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} The version without its `v`, or + * `undefined` when the line has no such release. + */ +declare function oldest(name: string, type: 'lts' | 'security', options?: import('@nodevu/core').Options): Promise; +/** + * Finds the oldest LTS release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ +declare function lts(name: string, options?: import('@nodevu/core').Options): Promise; +/** + * Finds the oldest security release of a release line. + * + * @param {string} name The release line, such as `'v16'`. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise} + */ +declare function security(name: string, options?: import('@nodevu/core').Options): Promise; diff --git a/opt/index.js b/opt/index.js index a28c0d0..e34c8a8 100644 --- a/opt/index.js +++ b/opt/index.js @@ -1,11 +1,26 @@ const { DateTime } = require('luxon'); +/** + * @typedef {object} Options + * @property {typeof fetch} fetch + * @property {string | DateTime} now An ISO 8601 date, or a Luxon `DateTime`. + * @property {{ index: string, schedule: string }} urls + */ + // this function allows us to parse user-passed options. // // this is particularly useful for tests so we can reduce variables // and ensure that our test suite is able to be consistent. +/** + * Builds the options `@nodevu/core` takes, filling in a default for each one + * left out. + * + * @param {{ fetch?: typeof fetch, now?: string | DateTime, urls?: { index?: string, schedule?: string } }} [inputOptions] + * @returns {Options} + */ function opt(inputOptions) { // set up our defaults + /** @type {Options} */ const result = { fetch: globalThis.fetch, now: DateTime.now(), diff --git a/opt/package.json b/opt/package.json index e77908f..fb79db3 100644 --- a/opt/package.json +++ b/opt/package.json @@ -5,7 +5,8 @@ "main": "index.js", "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "scripts": { "lint": "oxlint && oxfmt --check", @@ -33,5 +34,6 @@ "c8": "^12.0.0", "luxon": "^3.5.0", "undici": "^8.12.2" - } + }, + "types": "types/index.d.ts" } diff --git a/opt/tsconfig.json b/opt/tsconfig.json new file mode 100644 index 0000000..9767eef --- /dev/null +++ b/opt/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [] +} diff --git a/opt/types/index.d.ts b/opt/types/index.d.ts new file mode 100644 index 0000000..62f9148 --- /dev/null +++ b/opt/types/index.d.ts @@ -0,0 +1,34 @@ +export = opt; +import { DateTime } from 'luxon'; +export type Options = { + fetch: typeof fetch; + /** + * An ISO 8601 date, or a Luxon `DateTime`. + */ + now: string | DateTime; + urls: { + index: string; + schedule: string; + }; +}; +/** + * @typedef {object} Options + * @property {typeof fetch} fetch + * @property {string | DateTime} now An ISO 8601 date, or a Luxon `DateTime`. + * @property {{ index: string, schedule: string }} urls + */ +/** + * Builds the options `@nodevu/core` takes, filling in a default for each one + * left out. + * + * @param {{ fetch?: typeof fetch, now?: string | DateTime, urls?: { index?: string, schedule?: string } }} [inputOptions] + * @returns {Options} + */ +declare function opt(inputOptions?: { + fetch?: typeof fetch; + now?: string | DateTime; + urls?: { + index?: string; + schedule?: string; + }; +}): Options; diff --git a/package.json b/package.json index c963d51..7b80a0d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lint:write": "oxlint --fix && oxfmt", "coverage": "npm run coverage --ws --if-present", "test": "npm run test --ws", - "updates": "npm run updates --ws" + "updates": "npm run updates --ws", + "types": "tsc -b" }, "repository": { "type": "git", @@ -38,7 +39,11 @@ ], "devDependencies": { "@oxlint/plugins": "1.89.0", + "@types/luxon": "3.7.6", + "@types/node": "22.20.5", + "@types/semver": "7.8.0", "oxfmt": "0.74.0", - "oxlint": "1.89.0" + "oxlint": "1.89.0", + "typescript": "7.0.2" } } diff --git a/parsefiles/index.js b/parsefiles/index.js index a95ad92..68f61ab 100644 --- a/parsefiles/index.js +++ b/parsefiles/index.js @@ -1,5 +1,23 @@ -// file should be a string +/** + * @typedef {object} ParsedFile + * @property {string} id The identifier passed in, such as `'linux-x64'`. + * @property {string[]} files The file names that should exist for the + * identifier. + * @property {'aix' | 'headers' | 'linux' | 'macos' | 'source' | 'sunos' | 'windows'} [type] + * The platform, or what kind of files these are. + * @property {'x86' | 'x64' | 'arm64' | 'armv7l' | 'armv6l' | 'power' | 'z'} [architecture] + */ + +/** + * Parses an identifier from the `files` of a release in the Node.js release + * index into the file names it stands for, its platform, and its architecture. + * + * @param {string} file An identifier, such as `'win-x64-msi'`. + * @param {string} version The release's version, without its `v`. + * @returns {ParsedFile} + */ function parseFilename(file, version) { + /** @type {ParsedFile} */ const info = { id: file, files: [file], diff --git a/parsefiles/package.json b/parsefiles/package.json index 4c9d855..198aaf6 100644 --- a/parsefiles/package.json +++ b/parsefiles/package.json @@ -19,7 +19,8 @@ ], "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "author": "Tierney Cyren ", "license": "MIT", @@ -29,5 +30,6 @@ "repository": { "type": "git", "url": "git+https://github.com/cutenode/nodevu.git" - } + }, + "types": "types/index.d.ts" } diff --git a/parsefiles/tsconfig.json b/parsefiles/tsconfig.json new file mode 100644 index 0000000..9767eef --- /dev/null +++ b/parsefiles/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [] +} diff --git a/parsefiles/types/index.d.ts b/parsefiles/types/index.d.ts new file mode 100644 index 0000000..5a5c49f --- /dev/null +++ b/parsefiles/types/index.d.ts @@ -0,0 +1,35 @@ +/** + * @typedef {object} ParsedFile + * @property {string} id The identifier passed in, such as `'linux-x64'`. + * @property {string[]} files The file names that should exist for the + * identifier. + * @property {'aix' | 'headers' | 'linux' | 'macos' | 'source' | 'sunos' | 'windows'} [type] + * The platform, or what kind of files these are. + * @property {'x86' | 'x64' | 'arm64' | 'armv7l' | 'armv6l' | 'power' | 'z'} [architecture] + */ +export = parseFilename; +export type ParsedFile = { + /** + * The identifier passed in, such as `'linux-x64'`. + */ + id: string; + /** + * The file names that should exist for the + * identifier. + */ + files: string[]; + /** + * The platform, or what kind of files these are. + */ + type?: 'aix' | 'headers' | 'linux' | 'macos' | 'source' | 'sunos' | 'windows'; + architecture?: 'x86' | 'x64' | 'arm64' | 'armv7l' | 'armv6l' | 'power' | 'z'; +}; +/** + * Parses an identifier from the `files` of a release in the Node.js release + * index into the file names it stands for, its platform, and its architecture. + * + * @param {string} file An identifier, such as `'win-x64-msi'`. + * @param {string} version The release's version, without its `v`. + * @returns {ParsedFile} + */ +declare function parseFilename(file: string, version: string): ParsedFile; diff --git a/ranges/index.js b/ranges/index.js index 00ca6e8..4d28827 100644 --- a/ranges/index.js +++ b/ranges/index.js @@ -67,6 +67,29 @@ const defaultRangesObject = { // If you figure one out, or there's one in the future, please feel free // to submit a PR to update this code. +/** + * A range of versions. The fields a range doesn't have, or has nothing to + * point to, are `undefined`. + * + * @typedef {object} Range + * @property {string[]} versions The versions in the range, newest first. + * @property {string} [newest] + * @property {string} [oldest] + * @property {string} [newestLts] + * @property {string} [oldestLts] + * @property {string} [newestSecurity] + * @property {string} [oldestSecurity] + */ + +/** + * Works out ranges of Node.js versions, named by `@nodevu/aliases`. + * + * @param {import('@nodevu/aliases').Alias | readonly import('@nodevu/aliases').Alias[]} [filter] + * The ranges to work out. Defaults to `'all'`, every range. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise>>} + * A key per range. A range with no releases is left out. + */ async function generateRanges(filter, options) { let localFilter = filter; @@ -106,6 +129,7 @@ async function generateRanges(filter, options) { // // modifying the default object has some super wonky side effects // on subsequent calls to this function that we don't want. + /** @type {Record} */ const ranges = JSON.parse(JSON.stringify(defaultRangesObject)); for (const line in data) { @@ -185,8 +209,8 @@ async function generateRanges(filter, options) { // only the first LTS line fills these in, same as 'current' ranges['lts/latest'].oldestSecurity = data[line].security.oldest; ranges['lts/latest'].newestSecurity = data[line].security.newest; - ranges['lts/latest'].oldestLts = data[line].support.lts.oldest; - ranges['lts/latest'].newestLts = data[line].support.lts.newest; + ranges['lts/latest'].oldestLts = data[line].support.lts?.oldest; + ranges['lts/latest'].newestLts = data[line].support.lts?.newest; } } } @@ -209,18 +233,20 @@ async function generateRanges(filter, options) { if (ranges['lts/maintenance'].versions.length === 1) { ranges['lts/maintenance'].newestSecurity = data[line].security.newest; - ranges['lts/maintenance'].newestLts = data[line].support.lts.newest; + ranges['lts/maintenance'].newestLts = + data[line].support.lts?.newest; } if ( ranges['lts/maintenance'].oldestSecurity !== data[line].security.oldest || ranges['lts/maintenance'].oldestLts !== - data[line].support.lts.oldest + data[line].support.lts?.oldest ) { ranges['lts/maintenance'].oldestSecurity = data[line].security.oldest; - ranges['lts/maintenance'].oldestLts = data[line].support.lts.oldest; + ranges['lts/maintenance'].oldestLts = + data[line].support.lts?.oldest; } } } diff --git a/ranges/package.json b/ranges/package.json index 27b44ce..d744921 100644 --- a/ranges/package.json +++ b/ranges/package.json @@ -5,7 +5,8 @@ "main": "index.js", "files": [ "index.js", - "LICENSE" + "LICENSE", + "/types" ], "scripts": { "lint": "oxlint && oxfmt --check", @@ -31,5 +32,6 @@ }, "devDependencies": { "c8": "^12.0.0" - } + }, + "types": "types/index.d.ts" } diff --git a/ranges/tsconfig.json b/ranges/tsconfig.json new file mode 100644 index 0000000..39e5407 --- /dev/null +++ b/ranges/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [{ "path": "../core" }, { "path": "../aliases" }] +} diff --git a/ranges/types/index.d.ts b/ranges/types/index.d.ts new file mode 100644 index 0000000..799c761 --- /dev/null +++ b/ranges/types/index.d.ts @@ -0,0 +1,36 @@ +export = generateRanges; +export type Range = { + /** + * The versions in the range, newest first. + */ + versions: string[]; + newest?: string; + oldest?: string; + newestLts?: string; + oldestLts?: string; + newestSecurity?: string; + oldestSecurity?: string; +}; +/** + * A range of versions. The fields a range doesn't have, or has nothing to + * point to, are `undefined`. + * + * @typedef {object} Range + * @property {string[]} versions The versions in the range, newest first. + * @property {string} [newest] + * @property {string} [oldest] + * @property {string} [newestLts] + * @property {string} [oldestLts] + * @property {string} [newestSecurity] + * @property {string} [oldestSecurity] + */ +/** + * Works out ranges of Node.js versions, named by `@nodevu/aliases`. + * + * @param {import('@nodevu/aliases').Alias | readonly import('@nodevu/aliases').Alias[]} [filter] + * The ranges to work out. Defaults to `'all'`, every range. + * @param {import('@nodevu/core').Options} [options] Passed to `@nodevu/core`. + * @returns {Promise>>} + * A key per range. A range with no releases is left out. + */ +declare function generateRanges(filter?: import('@nodevu/aliases').Alias | readonly import('@nodevu/aliases').Alias[], options?: import('@nodevu/core').Options): Promise>>; diff --git a/static/index.js b/static/index.js index 4f5f415..714e40b 100644 --- a/static/index.js +++ b/static/index.js @@ -3,10 +3,29 @@ const releasesData = require('./data/releases.json'); const securityData = require('./data/security.json'); const supportData = require('./data/support.json'); -module.exports.default = defaultData; +/** + * Everything `@nodevu/core` returns. + */ +module.exports.default = /** @type {import('@nodevu/core').Data} */ ( + defaultData +); -module.exports.releases = releasesData; +/** + * The releases of each release line. + */ +module.exports.releases = + /** @type {Record} */ ( + releasesData + ); -module.exports.security = securityData; +/** + * The security releases of each release line. + */ +module.exports.security = + /** @type {Record} */ (securityData); -module.exports.support = supportData; +/** + * The support phases of each release line the release schedule lists. + */ +module.exports.support = + /** @type {Record} */ (supportData); diff --git a/static/json.d.ts b/static/json.d.ts new file mode 100644 index 0000000..6424701 --- /dev/null +++ b/static/json.d.ts @@ -0,0 +1,6 @@ +// the data files are typed where index.js exports them, so they're unknown +// here instead of inferred from megabytes of JSON +declare module '*.json' { + const value: unknown; + export = value; +} diff --git a/static/package.json b/static/package.json index c8cbcda..cf11914 100644 --- a/static/package.json +++ b/static/package.json @@ -29,7 +29,8 @@ "files": [ "/data", "index.js", - "LICENSE" + "LICENSE", + "/types" ], "author": "Tierney Cyren ", "license": "MIT", @@ -40,5 +41,14 @@ "devDependencies": { "@nodevu/core": "^1.1.0", "c8": "^12.0.0" + }, + "types": "types/index.d.ts", + "peerDependencies": { + "@nodevu/core": "^1.1.0" + }, + "peerDependenciesMeta": { + "@nodevu/core": { + "optional": true + } } } diff --git a/static/tsconfig.json b/static/tsconfig.json new file mode 100644 index 0000000..ac607a7 --- /dev/null +++ b/static/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + // json.d.ts types the data files instead + "resolveJsonModule": false + }, + "include": ["index.js", "json.d.ts"], + "references": [{ "path": "../core" }] +} diff --git a/static/types/index.d.ts b/static/types/index.d.ts new file mode 100644 index 0000000..1c0bdde --- /dev/null +++ b/static/types/index.d.ts @@ -0,0 +1,8 @@ +/** + * Everything `@nodevu/core` returns. + */ +declare const _default: import("@nodevu/core").Data; +export default _default; +export declare var releases: Record>; +export declare var security: Record; +export declare var support: Record; diff --git a/test/types.mts b/test/types.mts new file mode 100644 index 0000000..ea97287 --- /dev/null +++ b/test/types.mts @@ -0,0 +1,42 @@ +// compiled by `npm run types`: each package's published types, used the way +// a TypeScript project would. every @ts-expect-error has to stay an error +import aliases, { type Alias } from '@nodevu/aliases'; +import core, { type Data, type Phase } from '@nodevu/core'; +import fetchindex, { type IndexRelease } from '@nodevu/fetchindex'; +import newest from '@nodevu/newest'; +import oldest from '@nodevu/oldest'; +import opt from '@nodevu/opt'; +import parsefiles, { type ParsedFile } from '@nodevu/parsefiles'; +import ranges, { type Range } from '@nodevu/ranges'; +import nodevuStatic from '@nodevu/static'; +import translate from '@nodevu/translate'; + +export async function check(): Promise { + const data: Data = await core({ now: '2025-09-01' }); + + const phase: Phase | Record | undefined = + data.v20?.support?.phases.current; + + const major: number | undefined = data.v20?.releases['v20.0.0']?.semver.major; + // @ts-expect-error now is a date string + await core({ now: 2025 }); + + const lts: string | undefined = await oldest.lts('v16'); + const security: string | undefined = await newest.newest('v16', 'security'); + // @ts-expect-error the type is 'lts' or 'security' + await oldest.oldest('v16', 'stable'); + + const all: Alias[] = aliases; + const supported = await ranges(await translate('supported')); + const current: Range | undefined = supported.current; + // @ts-expect-error not an alias + await ranges('stable'); + // @ts-expect-error not a package support term + await translate('stable'); + + const file: ParsedFile = parsefiles('linux-x64', '17.6.0'); + const index: IndexRelease[] = await fetchindex(opt()); + const offline: Data = nodevuStatic.default; + + console.log(phase, major, lts, security, all, current, file, index, offline); +} diff --git a/translate/index.js b/translate/index.js index f5f96db..8dcd2f8 100644 --- a/translate/index.js +++ b/translate/index.js @@ -1,12 +1,20 @@ // ordered from most recent coverage to oldest coverage -const translations = { +const translations = /** @type {const} */ ({ current: ['current'], lts_latest: ['lts/latest'], lts: ['lts/active', 'lts/maintenance'], supported: ['current', 'lts/active', 'lts/maintenance'], all: ['current', 'lts/active', 'lts/maintenance', 'eol'], -}; +}); +/** + * Translates a term of the Node.js package support document to the aliases + * `@nodevu/ranges` takes. + * + * @template {keyof typeof translations} Term + * @param {Term} legacyName The term, such as `'supported'`. + * @returns {Promise<(typeof translations)[Term]>} + */ async function translate(legacyName) { if (!translations[legacyName]) { throw new Error(`Unknown value: ${legacyName}`); diff --git a/translate/package.json b/translate/package.json index 5a7c466..d34a315 100644 --- a/translate/package.json +++ b/translate/package.json @@ -34,5 +34,10 @@ }, "engines": { "node": ">=18.0.0" - } + }, + "types": "types/index.d.ts", + "files": [ + "index.js", + "/types" + ] } diff --git a/translate/tsconfig.json b/translate/tsconfig.json new file mode 100644 index 0000000..9767eef --- /dev/null +++ b/translate/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["index.js"], + "references": [] +} diff --git a/translate/types/index.d.ts b/translate/types/index.d.ts new file mode 100644 index 0000000..8abb362 --- /dev/null +++ b/translate/types/index.d.ts @@ -0,0 +1,17 @@ +export = translate; +declare const translations: { + readonly current: readonly ['current']; + readonly lts_latest: readonly ['lts/latest']; + readonly lts: readonly ['lts/active', 'lts/maintenance']; + readonly supported: readonly ['current', 'lts/active', 'lts/maintenance']; + readonly all: readonly ['current', 'lts/active', 'lts/maintenance', 'eol']; +}; +/** + * Translates a term of the Node.js package support document to the aliases + * `@nodevu/ranges` takes. + * + * @template {keyof typeof translations} Term + * @param {Term} legacyName The term, such as `'supported'`. + * @returns {Promise<(typeof translations)[Term]>} + */ +declare function translate(legacyName: Term): Promise<(typeof translations)[Term]>; diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..6c6e9ce --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "composite": true, + "declaration": true, + "emitDeclarationOnly": true, + "strict": true, + "module": "nodenext", + "target": "es2022", + "types": ["node"], + "skipLibCheck": true, + "rootDir": "${configDir}", + "outDir": "${configDir}/types", + "tsBuildInfoFile": "${configDir}/node_modules/.cache/types.tsbuildinfo" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..225cbbc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "target": "es2022", + "types": ["node"], + "noEmit": true + }, + "files": ["test/types.mts"], + "references": [ + { "path": "aliases" }, + { "path": "core" }, + { "path": "fetchindex" }, + { "path": "newest" }, + { "path": "oldest" }, + { "path": "opt" }, + { "path": "parsefiles" }, + { "path": "ranges" }, + { "path": "static" }, + { "path": "translate" } + ] +}