Skip to content

Commit

Permalink
fix: verify @types/node version with registry
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Molisani <[email protected]>
  • Loading branch information
molisani committed Oct 29, 2024
1 parent b1df5cf commit 1a762b0
Show file tree
Hide file tree
Showing 28 changed files with 3,156 additions and 82 deletions.
1 change: 1 addition & 0 deletions packages/create-app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { buildApplication, buildCommand } from "@stricli/core";
import packageJson from "../package.json";

const command = buildCommand({
/* c8 ignore next */
loader: async () => import("./impl"),
parameters: {
positional: {
Expand Down
15 changes: 8 additions & 7 deletions packages/create-app/src/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Distributed under the terms of the Apache 2.0 license.
import type { PackageJson, TsConfigJson } from "type-fest";
import self from "../package.json";
import type { LocalContext } from "./context";
import {
binBashCompleteModuleText,
binBashCompleteScriptText,
Expand All @@ -19,8 +20,8 @@ import {
singleCommandAppText,
singleCommandImplText,
} from "./files";
import { calculateAcceptableNodeVersions, type NodeVersions } from "./node";
import srcTsconfig from "./tsconfig.json";
import type { LocalContext } from "./context";

interface TsupConfig {
entry?: string[];
Expand All @@ -45,7 +46,7 @@ type PackageJsonTemplateValues = Pick<PackageJson.PackageJsonStandard, "name"> &
function buildPackageJson(
values: PackageJsonTemplateValues,
commandName: string,
nodeMajorVersion: number,
nodeVersions: NodeVersions,
): LocalPackageJson {
return {
...values,
Expand All @@ -55,7 +56,7 @@ function buildPackageJson(
[commandName]: "dist/cli.js",
},
engines: {
node: `>=${nodeMajorVersion}`,
node: nodeVersions.engine,
},
scripts: {
prebuild: "tsc -p src/tsconfig.json",
Expand All @@ -74,7 +75,7 @@ function buildPackageJson(
"@stricli/core": self.dependencies["@stricli/core"],
},
devDependencies: {
"@types/node": `${nodeMajorVersion}.x`,
"@types/node": nodeVersions.types,
tsup: self.devDependencies.tsup,
typescript: self.devDependencies.typescript,
},
Expand Down Expand Up @@ -135,8 +136,8 @@ export default async function (this: LocalContext, flags: CreateProjectFlags, di

const packageName = flags.name ?? path.basename(directoryPath);
const commandName = flags.command ?? packageName;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodeMajorVersion = this.process.versions.node.split(".").map(Number)[0]!;

const nodeVersions = await calculateAcceptableNodeVersions(this.process);

let packageJson = buildPackageJson(
{
Expand All @@ -147,7 +148,7 @@ export default async function (this: LocalContext, flags: CreateProjectFlags, di
type: flags.type,
},
commandName,
nodeMajorVersion,
nodeVersions,
);

const bashCommandName = calculateBashCompletionCommand(commandName);
Expand Down
58 changes: 58 additions & 0 deletions packages/create-app/src/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2024 Bloomberg Finance L.P.
// Distributed under the terms of the Apache 2.0 license.
import { discoverPackageRegistry, fetchPackageVersions } from "./registry";

export interface NodeVersions {
readonly engine: string;
readonly types: string;
}

const MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION = 22;

export async function calculateAcceptableNodeVersions(process: NodeJS.Process): Promise<NodeVersions> {
const majorVersion = Number(process.versions.node.split(".")[0]);
let typesVersion: string | undefined;

if (majorVersion > MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION) {
// To avoid hitting the registry every time, only run when higher than a statically-known maximum safe value.
const registry = discoverPackageRegistry(process);
const versions = registry && (await fetchPackageVersions(registry, "@types/node"));
if (versions?.includes(process.versions.node)) {
typesVersion = `^${process.versions.node}`;
} else if (versions) {
const typeMajors = new Set(versions.map((version) => Number(version.split(".")[0])));
if (typeMajors.has(majorVersion)) {
// Previously unknown major version exists, which means MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION should be updated.
typesVersion = `${majorVersion}.x`;
} else {
// Filter available major versions to just even (LTS) and pick highest.
// This assumes that types will exist for the LTS version just prior to the current unknown major version.
const highestEvenTypeMajor = [...typeMajors]
.filter((major) => major % 2 === 0)
.toSorted()
.at(-1);
if (highestEvenTypeMajor) {
typesVersion = `${highestEvenTypeMajor}.x`;
process.stderr.write(
`No version of @types/node found with major ${majorVersion}, falling back to ${typesVersion}\n`,
);
}
}
}
} else {
typesVersion = `${majorVersion}.x`;
}

if (!typesVersion) {
typesVersion = `${majorVersion}.x`;
// Should only be hit if something went wrong determining registry URL or fetching from registry.
process.stderr.write(
`Unable to determine version of @types/node for ${process.versions.node}, assuming ${typesVersion}\n`,
);
}

return {
engine: `>=${majorVersion}`,
types: typesVersion,
};
}
29 changes: 29 additions & 0 deletions packages/create-app/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024 Bloomberg Finance L.P.
// Distributed under the terms of the Apache 2.0 license.
import child_process from "node:child_process";

export function discoverPackageRegistry(process: NodeJS.Process): string | undefined {
if (process.env["NPM_CONFIG_REGISTRY"]) {
return process.env["NPM_CONFIG_REGISTRY"];
}

if (process.env["NPM_EXECPATH"]) {
return child_process
.execFileSync(process.execPath, [process.env["NPM_EXECPATH"], "config", "get", "registry"], {
encoding: "utf-8",
})
.trim();
}
}

export async function fetchPackageVersions(
registry: string,
packageName: string,
): Promise<readonly string[] | undefined> {
const input = registry + (registry.endsWith("/") ? packageName : `/${packageName}`);
const response = await fetch(input);
const data = await response.json();
if (typeof data === "object" && data && "versions" in data && typeof data.versions === "object") {
return Object.keys(data.versions ?? {});
}
}
Loading

0 comments on commit 1a762b0

Please sign in to comment.