@cosmicdrift/kumiko-guards 0.362.0 → 0.364.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/package.json +2 -2
- package/src/_lib/agent-docs.ts +425 -0
- package/src/_lib/bun-lock-packages.ts +1 -1
- package/src/_lib/dependency-cli.ts +27 -0
- package/src/_lib/dependency-exceptions.ts +135 -0
- package/src/_lib/direct-dependencies.ts +80 -0
- package/src/_lib/guard-kit.ts +7 -2
- package/src/_lib/installed-packages.ts +146 -0
- package/src/_lib/npm-registry.ts +168 -0
- package/src/_lib/security-audit.ts +390 -0
- package/src/changes.json +41 -5
- package/src/check-as-casts.ts +93 -47
- package/src/check-complexity.ts +1 -1
- package/src/check-licenses.ts +288 -0
- package/src/check-outdated.ts +234 -0
- package/src/check-runtime-isolation.ts +3 -1
- package/src/check-security.ts +182 -0
- package/src/check-single-runtime-instance.ts +10 -4
- package/src/cli.ts +27 -10
- package/src/dependency-license-exceptions.json +77 -0
- package/src/dependency-security-exceptions.json +4 -0
- package/src/guard-admin-api.ts +3 -3
- package/src/guard-agent-docs-drift.ts +137 -0
- package/src/guard-comment-lang.ts +76 -40
- package/src/guard-direct-entity-writes.ts +3 -0
- package/src/guard-html-escape.ts +2 -2
- package/src/guard-pre-es-patterns.ts +3 -0
- package/src/guard-public-exports-changelog.ts +22 -9
- package/src/guard-raw-sql.ts +1 -1
- package/src/guard-thin-wrappers.ts +11 -5
- package/src/guard-upgrade-state.ts +55 -31
- package/src/index.ts +1 -0
- package/src/pre-push.sh +19 -0
- package/src/run-repo-checks.ts +2 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct dependencies declared by one repo: its root package.json and every workspace member.
|
|
3
|
+
*/
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readPackageJson, workspaceMemberDirs } from "./installed-packages";
|
|
6
|
+
|
|
7
|
+
const DEP_FIELDS = [
|
|
8
|
+
"dependencies",
|
|
9
|
+
"devDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
"peerDependencies",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type DirectDependency = {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly version: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isFirstParty(name: string, spec: string): boolean {
|
|
20
|
+
return (
|
|
21
|
+
name.startsWith("@cosmicdrift/") ||
|
|
22
|
+
name.startsWith("@cosmicdriftgamestudio/") ||
|
|
23
|
+
name.startsWith("@app/") ||
|
|
24
|
+
spec.startsWith("workspace:")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringEntries(section: unknown): [string, string][] {
|
|
29
|
+
if (typeof section !== "object" || section === null) return [];
|
|
30
|
+
return Object.entries(section).filter(
|
|
31
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Every declared (name, spec) pair across the repo, duplicates kept so each differing range stays visible. */
|
|
36
|
+
export function collectDeclaredDependencies(repoRoot: string): DirectDependency[] {
|
|
37
|
+
const deps: DirectDependency[] = [];
|
|
38
|
+
for (const dir of [repoRoot, ...workspaceMemberDirs(repoRoot)]) {
|
|
39
|
+
const pkg = readPackageJson(join(dir, "package.json"));
|
|
40
|
+
if (!pkg) continue;
|
|
41
|
+
for (const field of DEP_FIELDS) {
|
|
42
|
+
for (const [name, version] of stringEntries(pkg[field])) {
|
|
43
|
+
if (!isFirstParty(name, version)) deps.push({ name, version });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return deps;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Unique direct dependencies by name (first declaration wins), sorted. */
|
|
51
|
+
export function collectDirectDependencies(repoRoot: string): DirectDependency[] {
|
|
52
|
+
const byName = new Map<string, DirectDependency>();
|
|
53
|
+
for (const dep of collectDeclaredDependencies(repoRoot)) {
|
|
54
|
+
if (!byName.has(dep.name)) byName.set(dep.name, dep);
|
|
55
|
+
}
|
|
56
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Every declared spec per name, so an install satisfying any one of several differing ranges counts as direct. */
|
|
60
|
+
export function collectDeclaredSpecsByName(
|
|
61
|
+
repoRoot: string,
|
|
62
|
+
): ReadonlyMap<string, readonly string[]> {
|
|
63
|
+
const byName = new Map<string, string[]>();
|
|
64
|
+
for (const dep of collectDeclaredDependencies(repoRoot)) {
|
|
65
|
+
const specs = byName.get(dep.name);
|
|
66
|
+
if (specs) specs.push(dep.version);
|
|
67
|
+
else byName.set(dep.name, [dep.version]);
|
|
68
|
+
}
|
|
69
|
+
return byName;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Pins from the root package.json `overrides` / `resolutions` (string values only). */
|
|
73
|
+
export function collectRootOverrides(repoRoot: string): ReadonlyMap<string, string> {
|
|
74
|
+
const pkg = readPackageJson(join(repoRoot, "package.json"));
|
|
75
|
+
const overrides = new Map<string, string>();
|
|
76
|
+
for (const field of ["overrides", "resolutions"] as const) {
|
|
77
|
+
for (const [name, value] of stringEntries(pkg?.[field])) overrides.set(name, value);
|
|
78
|
+
}
|
|
79
|
+
return overrides;
|
|
80
|
+
}
|
package/src/_lib/guard-kit.ts
CHANGED
|
@@ -583,8 +583,13 @@ function guardKitVersion(): string {
|
|
|
583
583
|
const pkgPath = resolve(import.meta.dir, "../../package.json");
|
|
584
584
|
// The version only decorates the banner; it must never abort a guard run.
|
|
585
585
|
try {
|
|
586
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
587
|
-
return pkg
|
|
586
|
+
const pkg: unknown = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
587
|
+
return typeof pkg === "object" &&
|
|
588
|
+
pkg !== null &&
|
|
589
|
+
"version" in pkg &&
|
|
590
|
+
typeof pkg.version === "string"
|
|
591
|
+
? pkg.version
|
|
592
|
+
: "unknown";
|
|
588
593
|
} catch {
|
|
589
594
|
return "unknown";
|
|
590
595
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed third-party packages of one repo, found the way Bun resolves them:
|
|
3
|
+
* node_modules of the repo root, of its workspace members and of every
|
|
4
|
+
* ancestor directory (a hoisted parent workspace or a `.wt/` worktree has no
|
|
5
|
+
* node_modules of its own).
|
|
6
|
+
*/
|
|
7
|
+
import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export type InstalledPackage = {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly version: string;
|
|
13
|
+
readonly license: string | null;
|
|
14
|
+
readonly path: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const FIRST_PARTY_SCOPES = ["@cosmicdrift/", "@cosmicdriftgamestudio/", "@app/"];
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readPackageJson(file: string): Record<string, unknown> | undefined {
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(readFileSync(file, "utf-8"));
|
|
26
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Workspace member directories declared by the repo's package.json `workspaces`. */
|
|
33
|
+
export function workspaceMemberDirs(repoRoot: string): string[] {
|
|
34
|
+
const workspaces = readPackageJson(join(repoRoot, "package.json"))?.["workspaces"];
|
|
35
|
+
const patterns = Array.isArray(workspaces)
|
|
36
|
+
? workspaces
|
|
37
|
+
: isRecord(workspaces) && Array.isArray(workspaces["packages"])
|
|
38
|
+
? workspaces["packages"]
|
|
39
|
+
: [];
|
|
40
|
+
const dirs = new Set<string>();
|
|
41
|
+
for (const pattern of patterns) {
|
|
42
|
+
if (typeof pattern !== "string" || pattern.startsWith("!")) continue;
|
|
43
|
+
for (const match of new Bun.Glob(pattern).scanSync({ cwd: repoRoot, onlyFiles: false })) {
|
|
44
|
+
const dir = join(repoRoot, match);
|
|
45
|
+
if (existsSync(join(dir, "package.json"))) dirs.add(dir);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...dirs];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `node_modules` directories Bun would load from for this repo, nearest first. */
|
|
52
|
+
export function nodeModulesDirs(repoRoot: string): string[] {
|
|
53
|
+
const dirs: string[] = [];
|
|
54
|
+
const add = (dir: string): void => {
|
|
55
|
+
const nodeModules = join(dir, "node_modules");
|
|
56
|
+
if (!dirs.includes(nodeModules) && existsSync(nodeModules)) dirs.push(nodeModules);
|
|
57
|
+
};
|
|
58
|
+
add(repoRoot);
|
|
59
|
+
for (const member of workspaceMemberDirs(repoRoot)) add(member);
|
|
60
|
+
for (let dir = dirname(repoRoot); ; dir = dirname(dir)) {
|
|
61
|
+
add(dir);
|
|
62
|
+
if (dirname(dir) === dir) break;
|
|
63
|
+
}
|
|
64
|
+
return dirs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Directories whose node_modules a scan of `repoRoot` reads — also where their exception files live. */
|
|
68
|
+
export function installationRoots(repoRoot: string): string[] {
|
|
69
|
+
return [repoRoot, ...nodeModulesDirs(repoRoot).map((dir) => dirname(dir))];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function collectInstalledPackages(repoRoot: string): InstalledPackage[] {
|
|
73
|
+
const seen = new Map<string, InstalledPackage>();
|
|
74
|
+
const visited = new Set<string>();
|
|
75
|
+
for (const dir of nodeModulesDirs(repoRoot)) walkNodeModules(dir, seen, visited);
|
|
76
|
+
return [...seen.values()].sort((a, b) =>
|
|
77
|
+
`${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readUnvisitedDir(dir: string, visited: Set<string>): Dirent[] {
|
|
82
|
+
try {
|
|
83
|
+
const real = realpathSync(dir);
|
|
84
|
+
if (visited.has(real)) return [];
|
|
85
|
+
visited.add(real);
|
|
86
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function walkNodeModules(
|
|
93
|
+
dir: string,
|
|
94
|
+
seen: Map<string, InstalledPackage>,
|
|
95
|
+
visited: Set<string>,
|
|
96
|
+
): void {
|
|
97
|
+
const entries = readUnvisitedDir(dir, visited);
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
100
|
+
if (entry.name.startsWith(".")) continue;
|
|
101
|
+
const full = join(dir, entry.name);
|
|
102
|
+
if (entry.name.startsWith("@")) {
|
|
103
|
+
walkNodeModules(full, seen, visited);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const info = readPackageInfo(join(full, "package.json"));
|
|
107
|
+
if (info && !seen.has(`${info.name}@${info.version}`)) {
|
|
108
|
+
seen.set(`${info.name}@${info.version}`, info);
|
|
109
|
+
}
|
|
110
|
+
walkNodeModules(join(full, "node_modules"), seen, visited);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readPackageInfo(file: string): InstalledPackage | undefined {
|
|
115
|
+
const pkg = readPackageJson(file);
|
|
116
|
+
if (!pkg) return undefined;
|
|
117
|
+
const { name, version } = pkg;
|
|
118
|
+
if (typeof name !== "string" || typeof version !== "string") return undefined;
|
|
119
|
+
// Workspace members and the repo's own scopes are first-party, not third-party licences.
|
|
120
|
+
if (pkg["private"] === true || FIRST_PARTY_SCOPES.some((scope) => name.startsWith(scope))) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
name,
|
|
125
|
+
version,
|
|
126
|
+
license: normalizeLicense(pkg["license"] ?? pkg["licenses"]),
|
|
127
|
+
path: file,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function licenseTypeOf(entry: unknown): string | undefined {
|
|
132
|
+
if (typeof entry === "string") return entry.trim() || undefined;
|
|
133
|
+
if (isRecord(entry) && typeof entry["type"] === "string")
|
|
134
|
+
return entry["type"].trim() || undefined;
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** SPDX string from package.json `license` (string | {type}) or legacy `licenses` (array). */
|
|
139
|
+
export function normalizeLicense(raw: unknown): string | null {
|
|
140
|
+
if (Array.isArray(raw)) {
|
|
141
|
+
const parts = raw.map(licenseTypeOf).filter((part): part is string => part !== undefined);
|
|
142
|
+
if (parts.length === 0) return null;
|
|
143
|
+
return parts.length === 1 ? (parts[0] ?? null) : `(${parts.join(" OR ")})`;
|
|
144
|
+
}
|
|
145
|
+
return licenseTypeOf(raw) ?? null;
|
|
146
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Registry lookups go through `bun info` (honors the project's registry/auth config) instead of raw HTTP.
|
|
2
|
+
|
|
3
|
+
export type LatestRelease = {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly publishedAt: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DeprecationLookup =
|
|
9
|
+
| { readonly kind: "deprecated"; readonly message: string }
|
|
10
|
+
| { readonly kind: "current" }
|
|
11
|
+
| { readonly kind: "unknown"; readonly reason: string };
|
|
12
|
+
|
|
13
|
+
export type RegistryClient = {
|
|
14
|
+
/** Deprecation status of one exact version; `unknown` when the registry could not answer. */
|
|
15
|
+
deprecation(name: string, version: string): Promise<DeprecationLookup>;
|
|
16
|
+
/** Latest dist-tag with its publish date; `null` when unknown. */
|
|
17
|
+
latestRelease(name: string): Promise<LatestRelease | null>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const BUN_INFO_TIMEOUT_MS = 30_000;
|
|
21
|
+
|
|
22
|
+
export type BunInfoResult =
|
|
23
|
+
| { readonly ok: true; readonly exitCode: number; readonly value: unknown }
|
|
24
|
+
| { readonly ok: false; readonly reason: string };
|
|
25
|
+
|
|
26
|
+
async function bunInfoJson(cwd: string, args: readonly string[]): Promise<BunInfoResult> {
|
|
27
|
+
try {
|
|
28
|
+
const proc = Bun.spawn(["bun", "info", ...args, "--json"], {
|
|
29
|
+
cwd,
|
|
30
|
+
stdout: "pipe",
|
|
31
|
+
stderr: "pipe",
|
|
32
|
+
timeout: BUN_INFO_TIMEOUT_MS,
|
|
33
|
+
});
|
|
34
|
+
const [out, err, exitCode] = await Promise.all([
|
|
35
|
+
new Response(proc.stdout).text(),
|
|
36
|
+
new Response(proc.stderr).text(),
|
|
37
|
+
proc.exited,
|
|
38
|
+
]);
|
|
39
|
+
if (out.trim() === "") {
|
|
40
|
+
const firstErrorLine = err.trim().split("\n")[0];
|
|
41
|
+
return { ok: false, reason: firstErrorLine || `bun info exited ${exitCode} without output` };
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, exitCode, value: JSON.parse(out) };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// `bun info <pkg>@<v> deprecated` answers a version without the field with exit 1 and {"error":"Property not found"}.
|
|
54
|
+
function isPropertyNotFound(value: unknown): boolean {
|
|
55
|
+
return isRecord(value) && value["error"] === "Property not found";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function interpretDeprecationLookup(result: BunInfoResult): DeprecationLookup {
|
|
59
|
+
if (!result.ok) return { kind: "unknown", reason: result.reason };
|
|
60
|
+
if (isPropertyNotFound(result.value)) return { kind: "current" };
|
|
61
|
+
if (result.exitCode === 0 && typeof result.value === "string") {
|
|
62
|
+
return result.value === ""
|
|
63
|
+
? { kind: "current" }
|
|
64
|
+
: { kind: "deprecated", message: result.value };
|
|
65
|
+
}
|
|
66
|
+
return { kind: "unknown", reason: `unexpected bun info answer (exit ${result.exitCode})` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `cwd` must contain a package.json — `bun info` refuses to run without one. */
|
|
70
|
+
export function createBunInfoClient(cwd: string): RegistryClient {
|
|
71
|
+
return {
|
|
72
|
+
async deprecation(name, version) {
|
|
73
|
+
return interpretDeprecationLookup(
|
|
74
|
+
await bunInfoJson(cwd, [`${name}@${version}`, "deprecated"]),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
async latestRelease(name) {
|
|
78
|
+
const version = await bunInfoJson(cwd, [name, "dist-tags.latest"]);
|
|
79
|
+
if (!version.ok || version.exitCode !== 0 || typeof version.value !== "string") return null;
|
|
80
|
+
const time = await bunInfoJson(cwd, [name, "time"]);
|
|
81
|
+
const publishedAt = time.ok && isRecord(time.value) ? time.value[version.value] : undefined;
|
|
82
|
+
return typeof publishedAt === "string" ? { version: version.value, publishedAt } : null;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Memoizes per call key, so a package listed twice costs one lookup. */
|
|
88
|
+
export function memoizeRegistryClient(client: RegistryClient): RegistryClient {
|
|
89
|
+
const deprecation = new Map<string, Promise<DeprecationLookup>>();
|
|
90
|
+
const latest = new Map<string, Promise<LatestRelease | null>>();
|
|
91
|
+
return {
|
|
92
|
+
deprecation(name, version) {
|
|
93
|
+
const key = `${name}@${version}`;
|
|
94
|
+
const cached = deprecation.get(key);
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
const pending = client.deprecation(name, version);
|
|
97
|
+
deprecation.set(key, pending);
|
|
98
|
+
return pending;
|
|
99
|
+
},
|
|
100
|
+
latestRelease(name) {
|
|
101
|
+
const cached = latest.get(name);
|
|
102
|
+
if (cached) return cached;
|
|
103
|
+
const pending = client.latestRelease(name);
|
|
104
|
+
latest.set(name, pending);
|
|
105
|
+
return pending;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseMajor(version: string): number | null {
|
|
111
|
+
const match = version.match(/^(\d+)/);
|
|
112
|
+
return match?.[1] === undefined ? null : Number.parseInt(match[1], 10);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type MajorLagWarning = {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly current: string;
|
|
118
|
+
readonly latest: string;
|
|
119
|
+
readonly latestPublished: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function findMajorLagWarning(
|
|
123
|
+
name: string,
|
|
124
|
+
currentVersion: string,
|
|
125
|
+
release: LatestRelease,
|
|
126
|
+
lagMonths: number,
|
|
127
|
+
nowMs: number,
|
|
128
|
+
): MajorLagWarning | null {
|
|
129
|
+
const currentMajor = parseMajor(currentVersion);
|
|
130
|
+
const latestMajor = parseMajor(release.version);
|
|
131
|
+
if (currentMajor === null || latestMajor === null || latestMajor <= currentMajor) return null;
|
|
132
|
+
|
|
133
|
+
let published: Temporal.Instant;
|
|
134
|
+
try {
|
|
135
|
+
published = Temporal.Instant.from(release.publishedAt);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const cutoff = Temporal.Instant.fromEpochMilliseconds(nowMs)
|
|
140
|
+
.toZonedDateTimeISO("UTC")
|
|
141
|
+
.subtract({ months: lagMonths });
|
|
142
|
+
if (Temporal.Instant.compare(published, cutoff.toInstant()) > 0) return null;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
current: currentVersion,
|
|
147
|
+
latest: release.version,
|
|
148
|
+
latestPublished: release.publishedAt,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function mapWithConcurrency<T, R>(
|
|
153
|
+
items: readonly T[],
|
|
154
|
+
concurrency: number,
|
|
155
|
+
fn: (item: T) => Promise<R>,
|
|
156
|
+
): Promise<R[]> {
|
|
157
|
+
const results: R[] = new Array(items.length);
|
|
158
|
+
let index = 0;
|
|
159
|
+
async function worker(): Promise<void> {
|
|
160
|
+
while (index < items.length) {
|
|
161
|
+
const i = index++;
|
|
162
|
+
const item = items[i];
|
|
163
|
+
if (item !== undefined) results[i] = await fn(item);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
167
|
+
return results;
|
|
168
|
+
}
|