safeinstall-cli 0.1.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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/SUPPORT.md +42 -0
- package/dist/async.js +26 -0
- package/dist/check-flow.js +160 -0
- package/dist/cli-options.js +15 -0
- package/dist/cli.js +97 -0
- package/dist/config.js +129 -0
- package/dist/disk-cache.js +67 -0
- package/dist/evaluations.js +27 -0
- package/dist/init-flow.js +55 -0
- package/dist/install-flow.js +274 -0
- package/dist/output.js +93 -0
- package/dist/package-managers.js +98 -0
- package/dist/policy.js +83 -0
- package/dist/project-discovery.js +90 -0
- package/dist/project-installs/npm.js +93 -0
- package/dist/project-installs/pnpm.js +166 -0
- package/dist/project-installs/shared.js +62 -0
- package/dist/project-installs/types.js +2 -0
- package/dist/project-installs.js +60 -0
- package/dist/project-state.js +101 -0
- package/dist/registry.js +239 -0
- package/dist/signals.js +55 -0
- package/dist/specs.js +185 -0
- package/dist/types.js +2 -0
- package/package.json +60 -0
- package/safeinstall.config.example.json +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHttpUrl = isHttpUrl;
|
|
4
|
+
exports.isTarballLike = isTarballLike;
|
|
5
|
+
exports.extractSemverPrefix = extractSemverPrefix;
|
|
6
|
+
exports.createRegistryRequestedPackage = createRegistryRequestedPackage;
|
|
7
|
+
exports.createNonRegistryRequestedPackage = createNonRegistryRequestedPackage;
|
|
8
|
+
exports.classifyResolvedSource = classifyResolvedSource;
|
|
9
|
+
function isHttpUrl(value) {
|
|
10
|
+
return value.startsWith("http://") || value.startsWith("https://");
|
|
11
|
+
}
|
|
12
|
+
function isTarballLike(value) {
|
|
13
|
+
return value.endsWith(".tgz") || value.endsWith(".tar.gz");
|
|
14
|
+
}
|
|
15
|
+
function extractSemverPrefix(value) {
|
|
16
|
+
const match = value.match(/^(\d+\.\d+\.\d+(?:[-+][^()\s]+)?)/);
|
|
17
|
+
return match?.[1];
|
|
18
|
+
}
|
|
19
|
+
function createRegistryRequestedPackage(name, version) {
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
raw: `${name}@${version}`,
|
|
23
|
+
requested: version,
|
|
24
|
+
sourceType: "registry",
|
|
25
|
+
registrySpecKind: "version"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createNonRegistryRequestedPackage(name, manifestSpec, sourceType, requested) {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
raw: `${name}@${manifestSpec}`,
|
|
32
|
+
requested: requested ?? manifestSpec,
|
|
33
|
+
sourceType
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function classifyResolvedSource(declaredSourceType, resolvedReference, hasIntegrity) {
|
|
37
|
+
if (declaredSourceType !== "unknown" && declaredSourceType !== "registry") {
|
|
38
|
+
return declaredSourceType;
|
|
39
|
+
}
|
|
40
|
+
if (!resolvedReference) {
|
|
41
|
+
return declaredSourceType === "unknown" ? "registry" : declaredSourceType;
|
|
42
|
+
}
|
|
43
|
+
if (resolvedReference.startsWith("git+") ||
|
|
44
|
+
resolvedReference.startsWith("git@") ||
|
|
45
|
+
resolvedReference.includes("github.com:")) {
|
|
46
|
+
return "git";
|
|
47
|
+
}
|
|
48
|
+
if (resolvedReference.startsWith("link:")) {
|
|
49
|
+
return "workspace";
|
|
50
|
+
}
|
|
51
|
+
if (resolvedReference.startsWith("file:")) {
|
|
52
|
+
return "file";
|
|
53
|
+
}
|
|
54
|
+
if (isHttpUrl(resolvedReference)) {
|
|
55
|
+
if (declaredSourceType === "registry" &&
|
|
56
|
+
(hasIntegrity || resolvedReference.includes("registry.npmjs.org") || resolvedReference.includes("registry.yarnpkg.com"))) {
|
|
57
|
+
return "registry";
|
|
58
|
+
}
|
|
59
|
+
return isTarballLike(resolvedReference) ? "tarball" : "url";
|
|
60
|
+
}
|
|
61
|
+
return declaredSourceType === "unknown" ? "unknown" : declaredSourceType;
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadPnpmProjectInstallTargets = exports.loadNpmProjectInstallTargets = void 0;
|
|
4
|
+
exports.loadProjectInstallTargetsForManager = loadProjectInstallTargetsForManager;
|
|
5
|
+
exports.inferProjectInstallTargetsForCheck = inferProjectInstallTargetsForCheck;
|
|
6
|
+
const project_state_1 = require("./project-state");
|
|
7
|
+
const project_discovery_1 = require("./project-discovery");
|
|
8
|
+
const npm_1 = require("./project-installs/npm");
|
|
9
|
+
Object.defineProperty(exports, "loadNpmProjectInstallTargets", { enumerable: true, get: function () { return npm_1.loadNpmProjectInstallTargets; } });
|
|
10
|
+
const pnpm_1 = require("./project-installs/pnpm");
|
|
11
|
+
Object.defineProperty(exports, "loadPnpmProjectInstallTargets", { enumerable: true, get: function () { return pnpm_1.loadPnpmProjectInstallTargets; } });
|
|
12
|
+
async function loadProjectInstallTargetsForManager(effectiveCwd, packageDir, manager) {
|
|
13
|
+
const packageJson = packageDir ? await (0, project_state_1.loadPackageJson)(packageDir) : await (0, project_state_1.loadPackageJson)(effectiveCwd);
|
|
14
|
+
const declaredManager = (0, project_state_1.parseDeclaredPackageManager)(packageJson?.packageManager);
|
|
15
|
+
if (declaredManager && declaredManager !== manager) {
|
|
16
|
+
return {
|
|
17
|
+
targets: [],
|
|
18
|
+
issues: [
|
|
19
|
+
`Project install blocked: package.json declares ${declaredManager} as packageManager, but this command uses ${manager}.`
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (manager === "pnpm") {
|
|
24
|
+
return (0, pnpm_1.loadPnpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
25
|
+
}
|
|
26
|
+
if (manager === "npm") {
|
|
27
|
+
return (0, npm_1.loadNpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
async function inferProjectInstallTargetsForCheck(effectiveCwd, packageDir) {
|
|
32
|
+
const packageJson = packageDir ? await (0, project_state_1.loadPackageJson)(packageDir) : await (0, project_state_1.loadPackageJson)(effectiveCwd);
|
|
33
|
+
const packageManager = (0, project_state_1.parseDeclaredPackageManager)(packageJson?.packageManager);
|
|
34
|
+
const pnpmLockPath = await (0, project_discovery_1.findNearestUpward)(effectiveCwd, "pnpm-lock.yaml");
|
|
35
|
+
const npmLockPath = (await (0, project_discovery_1.findNearestUpward)(effectiveCwd, "package-lock.json")) ??
|
|
36
|
+
(await (0, project_discovery_1.findNearestUpward)(effectiveCwd, "npm-shrinkwrap.json"));
|
|
37
|
+
const hasPnpmLock = Boolean(pnpmLockPath);
|
|
38
|
+
const hasNpmLock = Boolean(npmLockPath);
|
|
39
|
+
if (packageManager === "pnpm") {
|
|
40
|
+
return (0, pnpm_1.loadPnpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
41
|
+
}
|
|
42
|
+
if (packageManager === "npm") {
|
|
43
|
+
return (0, npm_1.loadNpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
44
|
+
}
|
|
45
|
+
if (hasPnpmLock && hasNpmLock) {
|
|
46
|
+
return {
|
|
47
|
+
targets: [],
|
|
48
|
+
issues: [
|
|
49
|
+
"Check blocked: both pnpm-lock.yaml and an npm lockfile exist. Set packageManager in package.json or remove the stale lockfile."
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (hasPnpmLock) {
|
|
54
|
+
return (0, pnpm_1.loadPnpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
55
|
+
}
|
|
56
|
+
if (hasNpmLock) {
|
|
57
|
+
return (0, npm_1.loadNpmProjectInstallTargets)(effectiveCwd, packageDir);
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseDeclaredPackageManager = parseDeclaredPackageManager;
|
|
7
|
+
exports.classifyDeclaredSource = classifyDeclaredSource;
|
|
8
|
+
exports.loadPackageJson = loadPackageJson;
|
|
9
|
+
exports.loadProjectDependencyState = loadProjectDependencyState;
|
|
10
|
+
exports.loadManifestDependencies = loadManifestDependencies;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
|
|
14
|
+
function parseDeclaredPackageManager(packageManager) {
|
|
15
|
+
if (packageManager?.startsWith("pnpm@")) {
|
|
16
|
+
return "pnpm";
|
|
17
|
+
}
|
|
18
|
+
if (packageManager?.startsWith("npm@")) {
|
|
19
|
+
return "npm";
|
|
20
|
+
}
|
|
21
|
+
if (packageManager?.startsWith("bun@")) {
|
|
22
|
+
return "bun";
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function classifyDeclaredSource(spec) {
|
|
27
|
+
if (spec.startsWith("github:") ||
|
|
28
|
+
spec.startsWith("git+ssh://") ||
|
|
29
|
+
spec.startsWith("git+https://") ||
|
|
30
|
+
spec.startsWith("git@")) {
|
|
31
|
+
return "git";
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = (0, npm_package_arg_1.default)(spec);
|
|
35
|
+
switch (result.type) {
|
|
36
|
+
case "git":
|
|
37
|
+
return "git";
|
|
38
|
+
case "remote":
|
|
39
|
+
return result.fetchSpec?.endsWith(".tgz") || result.fetchSpec?.endsWith(".tar.gz") ? "tarball" : "url";
|
|
40
|
+
case "file":
|
|
41
|
+
return "file";
|
|
42
|
+
case "directory":
|
|
43
|
+
return "directory";
|
|
44
|
+
case "tag":
|
|
45
|
+
case "range":
|
|
46
|
+
case "version":
|
|
47
|
+
case "alias":
|
|
48
|
+
return "registry";
|
|
49
|
+
default:
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (spec.startsWith("workspace:")) {
|
|
55
|
+
return "workspace";
|
|
56
|
+
}
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function loadPackageJson(cwd) {
|
|
61
|
+
const packageJsonPath = node_path_1.default.join(cwd, "package.json");
|
|
62
|
+
try {
|
|
63
|
+
const raw = await (0, promises_1.readFile)(packageJsonPath, "utf8");
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function loadProjectDependencyState(cwd, packageName) {
|
|
71
|
+
const packageJson = await loadPackageJson(cwd);
|
|
72
|
+
const declaredSpec = packageJson?.dependencies?.[packageName] ??
|
|
73
|
+
packageJson?.devDependencies?.[packageName] ??
|
|
74
|
+
packageJson?.optionalDependencies?.[packageName];
|
|
75
|
+
let installedVersion;
|
|
76
|
+
try {
|
|
77
|
+
const installedPackagePath = node_path_1.default.join(cwd, "node_modules", packageName, "package.json");
|
|
78
|
+
const installedRaw = await (0, promises_1.readFile)(installedPackagePath, "utf8");
|
|
79
|
+
const installedPackage = JSON.parse(installedRaw);
|
|
80
|
+
installedVersion = installedPackage.version;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
installedVersion = undefined;
|
|
84
|
+
}
|
|
85
|
+
if (!declaredSpec && !installedVersion) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
declaredSpec,
|
|
90
|
+
declaredSourceType: declaredSpec ? classifyDeclaredSource(declaredSpec) : undefined,
|
|
91
|
+
installedVersion
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function loadManifestDependencies(cwd) {
|
|
95
|
+
const packageJson = await loadPackageJson(cwd);
|
|
96
|
+
return {
|
|
97
|
+
...(packageJson?.dependencies ?? {}),
|
|
98
|
+
...(packageJson?.devDependencies ?? {}),
|
|
99
|
+
...(packageJson?.optionalDependencies ?? {})
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RegistryClient = void 0;
|
|
7
|
+
const semver_1 = __importDefault(require("semver"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const disk_cache_1 = require("./disk-cache");
|
|
10
|
+
const signals_1 = require("./signals");
|
|
11
|
+
const ABBREVIATED_METADATA_HEADER = "application/vnd.npm.install-v1+json";
|
|
12
|
+
const REGISTRY_FETCH_TIMEOUT_MS = 15_000;
|
|
13
|
+
const REGISTRY_DISK_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
14
|
+
const VERSION_MANIFEST_CACHE_NAMESPACE = "registry-version-manifests-v1";
|
|
15
|
+
const PUBLISH_TIME_CACHE_NAMESPACE = "registry-publish-times-v1";
|
|
16
|
+
function encodePackageName(name) {
|
|
17
|
+
return name.startsWith("@") ? name.replace("/", "%2f") : name;
|
|
18
|
+
}
|
|
19
|
+
function createTimeoutSignal() {
|
|
20
|
+
return AbortSignal.timeout(REGISTRY_FETCH_TIMEOUT_MS);
|
|
21
|
+
}
|
|
22
|
+
function cacheKey(packageName, version) {
|
|
23
|
+
return `${packageName}@${version}`;
|
|
24
|
+
}
|
|
25
|
+
function scopedCacheKey(registryUrl, packageName, version) {
|
|
26
|
+
return `${registryUrl}|${cacheKey(packageName, version)}`;
|
|
27
|
+
}
|
|
28
|
+
function parsePublishedAt(packageName, version, rawValue) {
|
|
29
|
+
const publishedAt = new Date(rawValue);
|
|
30
|
+
if (Number.isNaN(publishedAt.getTime())) {
|
|
31
|
+
throw new Error(`Registry error: invalid publish time for ${packageName}@${version}.`);
|
|
32
|
+
}
|
|
33
|
+
return publishedAt;
|
|
34
|
+
}
|
|
35
|
+
class RegistryClient {
|
|
36
|
+
registryUrl;
|
|
37
|
+
diskCache;
|
|
38
|
+
signal;
|
|
39
|
+
packageCache = new Map();
|
|
40
|
+
versionCache = new Map();
|
|
41
|
+
publishTimeCache = new Map();
|
|
42
|
+
fullMetadataCache = new Map();
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.registryUrl = (options?.registryUrl ?? config_1.DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
|
|
45
|
+
this.diskCache = new disk_cache_1.DiskCache({
|
|
46
|
+
cacheDir: options?.cacheDir,
|
|
47
|
+
ttlMs: options?.cacheTtlMs ?? REGISTRY_DISK_CACHE_TTL_MS
|
|
48
|
+
});
|
|
49
|
+
this.signal = options?.signal;
|
|
50
|
+
}
|
|
51
|
+
async resolvePackage(requested) {
|
|
52
|
+
const resolvedVersion = requested.registrySpecKind === "version"
|
|
53
|
+
? requested.requested
|
|
54
|
+
: this.resolveVersion(await this.fetchPackageDocument(requested.name), requested);
|
|
55
|
+
const versionDoc = await this.fetchVersionManifest(requested.name, resolvedVersion);
|
|
56
|
+
const publishedAt = await this.fetchPublishedAt(requested.name, resolvedVersion, versionDoc);
|
|
57
|
+
const lifecycleScripts = this.collectLifecycleScripts(versionDoc.scripts);
|
|
58
|
+
return {
|
|
59
|
+
requested,
|
|
60
|
+
resolvedVersion,
|
|
61
|
+
publishedAt,
|
|
62
|
+
lifecycleScripts
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async getLifecycleScripts(packageName, version) {
|
|
66
|
+
try {
|
|
67
|
+
const versionDoc = await this.fetchVersionManifest(packageName, version);
|
|
68
|
+
return this.collectLifecycleScripts(versionDoc.scripts);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (this.isNotFoundError(error)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async fetchPackageDocument(packageName) {
|
|
78
|
+
(0, signals_1.throwIfAborted)(this.signal);
|
|
79
|
+
const cached = this.packageCache.get(packageName);
|
|
80
|
+
if (cached) {
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
const document = await this.fetchJson(`${this.registryUrl}/${encodePackageName(packageName)}`, packageName, {
|
|
84
|
+
headers: {
|
|
85
|
+
Accept: ABBREVIATED_METADATA_HEADER
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.packageCache.set(packageName, document);
|
|
89
|
+
return document;
|
|
90
|
+
}
|
|
91
|
+
async fetchVersionManifest(packageName, version) {
|
|
92
|
+
(0, signals_1.throwIfAborted)(this.signal);
|
|
93
|
+
const versionKey = cacheKey(packageName, version);
|
|
94
|
+
const cached = this.versionCache.get(versionKey);
|
|
95
|
+
if (cached) {
|
|
96
|
+
return cached;
|
|
97
|
+
}
|
|
98
|
+
const persistentKey = scopedCacheKey(this.registryUrl, packageName, version);
|
|
99
|
+
const cachedFromDisk = await this.diskCache.getJson(VERSION_MANIFEST_CACHE_NAMESPACE, persistentKey);
|
|
100
|
+
if (cachedFromDisk) {
|
|
101
|
+
this.versionCache.set(versionKey, cachedFromDisk);
|
|
102
|
+
return cachedFromDisk;
|
|
103
|
+
}
|
|
104
|
+
const manifest = await this.fetchJson(`${this.registryUrl}/${encodePackageName(packageName)}/${encodeURIComponent(version)}`, `${packageName}@${version}`);
|
|
105
|
+
this.versionCache.set(versionKey, manifest);
|
|
106
|
+
await this.diskCache.setJson(VERSION_MANIFEST_CACHE_NAMESPACE, persistentKey, manifest);
|
|
107
|
+
return manifest;
|
|
108
|
+
}
|
|
109
|
+
async fetchPublishedAt(packageName, version, versionDoc) {
|
|
110
|
+
(0, signals_1.throwIfAborted)(this.signal);
|
|
111
|
+
const versionKey = cacheKey(packageName, version);
|
|
112
|
+
const cached = this.publishTimeCache.get(versionKey);
|
|
113
|
+
if (cached) {
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
const persistentKey = scopedCacheKey(this.registryUrl, packageName, version);
|
|
117
|
+
const cachedFromDisk = await this.diskCache.getJson(PUBLISH_TIME_CACHE_NAMESPACE, persistentKey);
|
|
118
|
+
if (cachedFromDisk) {
|
|
119
|
+
const publishedAt = parsePublishedAt(packageName, version, cachedFromDisk);
|
|
120
|
+
this.publishTimeCache.set(versionKey, publishedAt);
|
|
121
|
+
return publishedAt;
|
|
122
|
+
}
|
|
123
|
+
const tarballUrl = versionDoc.dist?.tarball;
|
|
124
|
+
if (tarballUrl) {
|
|
125
|
+
const lastModified = await this.fetchTarballLastModified(packageName, version, tarballUrl);
|
|
126
|
+
if (lastModified) {
|
|
127
|
+
const publishedAt = parsePublishedAt(packageName, version, lastModified);
|
|
128
|
+
this.publishTimeCache.set(versionKey, publishedAt);
|
|
129
|
+
await this.diskCache.setJson(PUBLISH_TIME_CACHE_NAMESPACE, persistentKey, publishedAt.toISOString());
|
|
130
|
+
return publishedAt;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const publishedAt = await this.fetchPublishedAtFromFullMetadata(packageName, version);
|
|
134
|
+
this.publishTimeCache.set(versionKey, publishedAt);
|
|
135
|
+
await this.diskCache.setJson(PUBLISH_TIME_CACHE_NAMESPACE, persistentKey, publishedAt.toISOString());
|
|
136
|
+
return publishedAt;
|
|
137
|
+
}
|
|
138
|
+
async fetchTarballLastModified(packageName, version, tarballUrl) {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(tarballUrl, {
|
|
141
|
+
method: "HEAD",
|
|
142
|
+
signal: this.fetchSignal()
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return response.headers.get("last-modified") ?? undefined;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const shutdownError = (0, signals_1.getShutdownSignalError)(this.signal);
|
|
151
|
+
if (shutdownError) {
|
|
152
|
+
throw shutdownError;
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
155
|
+
throw new Error(`Registry error: timed out while fetching tarball metadata for ${packageName}@${version}.`);
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async fetchPublishedAtFromFullMetadata(packageName, version) {
|
|
161
|
+
const document = await this.fetchFullPackageDocument(packageName);
|
|
162
|
+
const publishedAtRaw = document.time?.[version];
|
|
163
|
+
if (!publishedAtRaw) {
|
|
164
|
+
throw new Error(`Registry error: missing publish time for ${packageName}@${version}.`);
|
|
165
|
+
}
|
|
166
|
+
return parsePublishedAt(packageName, version, publishedAtRaw);
|
|
167
|
+
}
|
|
168
|
+
async fetchFullPackageDocument(packageName) {
|
|
169
|
+
const cached = this.fullMetadataCache.get(packageName);
|
|
170
|
+
if (cached) {
|
|
171
|
+
return cached;
|
|
172
|
+
}
|
|
173
|
+
const document = await this.fetchJson(`${this.registryUrl}/${encodePackageName(packageName)}`, packageName);
|
|
174
|
+
this.fullMetadataCache.set(packageName, document);
|
|
175
|
+
return document;
|
|
176
|
+
}
|
|
177
|
+
async fetchJson(url, packageLabel, init) {
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
...init,
|
|
181
|
+
signal: this.fetchSignal()
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(`Registry error: could not fetch ${packageLabel} (${response.status}).`);
|
|
185
|
+
}
|
|
186
|
+
return (await response.json());
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const shutdownError = (0, signals_1.getShutdownSignalError)(this.signal);
|
|
190
|
+
if (shutdownError) {
|
|
191
|
+
throw shutdownError;
|
|
192
|
+
}
|
|
193
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
194
|
+
throw new Error(`Registry error: timed out while fetching ${packageLabel}.`);
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
fetchSignal() {
|
|
200
|
+
return this.signal ? AbortSignal.any([this.signal, createTimeoutSignal()]) : createTimeoutSignal();
|
|
201
|
+
}
|
|
202
|
+
resolveVersion(document, requested) {
|
|
203
|
+
const versions = Object.keys(document.versions ?? {});
|
|
204
|
+
if (versions.length === 0) {
|
|
205
|
+
throw new Error(`Registry error: package ${requested.name} has no published versions.`);
|
|
206
|
+
}
|
|
207
|
+
if (!requested.registrySpecKind) {
|
|
208
|
+
return document["dist-tags"]?.latest ?? semver_1.default.rsort(versions)[0];
|
|
209
|
+
}
|
|
210
|
+
if (requested.registrySpecKind === "version") {
|
|
211
|
+
if (!document.versions?.[requested.requested]) {
|
|
212
|
+
throw new Error(`Package ${requested.name} does not have version ${requested.requested}.`);
|
|
213
|
+
}
|
|
214
|
+
return requested.requested;
|
|
215
|
+
}
|
|
216
|
+
if (requested.registrySpecKind === "tag") {
|
|
217
|
+
const version = document["dist-tags"]?.[requested.requested];
|
|
218
|
+
if (!version) {
|
|
219
|
+
throw new Error(`Package ${requested.name} does not have dist-tag "${requested.requested}".`);
|
|
220
|
+
}
|
|
221
|
+
return version;
|
|
222
|
+
}
|
|
223
|
+
const resolved = semver_1.default.maxSatisfying(versions, requested.requested, {
|
|
224
|
+
includePrerelease: requested.requested.includes("-")
|
|
225
|
+
});
|
|
226
|
+
if (!resolved) {
|
|
227
|
+
throw new Error(`Package ${requested.name} has no version matching "${requested.requested}".`);
|
|
228
|
+
}
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
collectLifecycleScripts(scripts) {
|
|
232
|
+
const lifecycleNames = ["preinstall", "install", "postinstall"];
|
|
233
|
+
return lifecycleNames.filter((scriptName) => Boolean(scripts?.[scriptName]));
|
|
234
|
+
}
|
|
235
|
+
isNotFoundError(error) {
|
|
236
|
+
return error instanceof Error && /\(404\)\.$/.test(error.message);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.RegistryClient = RegistryClient;
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShutdownSignalError = void 0;
|
|
4
|
+
exports.signalExitCode = signalExitCode;
|
|
5
|
+
exports.getShutdownSignalError = getShutdownSignalError;
|
|
6
|
+
exports.throwIfAborted = throwIfAborted;
|
|
7
|
+
exports.createShutdownController = createShutdownController;
|
|
8
|
+
class ShutdownSignalError extends Error {
|
|
9
|
+
signalName;
|
|
10
|
+
constructor(signalName) {
|
|
11
|
+
super(`Interrupted by ${signalName}.`);
|
|
12
|
+
this.name = "ShutdownSignalError";
|
|
13
|
+
this.signalName = signalName;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.ShutdownSignalError = ShutdownSignalError;
|
|
17
|
+
function signalExitCode(signalName) {
|
|
18
|
+
return signalName === "SIGINT" ? 130 : signalName === "SIGTERM" ? 143 : 1;
|
|
19
|
+
}
|
|
20
|
+
function getShutdownSignalError(signal) {
|
|
21
|
+
const reason = signal?.reason;
|
|
22
|
+
return reason instanceof ShutdownSignalError ? reason : undefined;
|
|
23
|
+
}
|
|
24
|
+
function throwIfAborted(signal) {
|
|
25
|
+
const shutdownError = getShutdownSignalError(signal);
|
|
26
|
+
if (shutdownError) {
|
|
27
|
+
throw shutdownError;
|
|
28
|
+
}
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
throw new Error("Operation aborted.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createShutdownController() {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const handlers = [];
|
|
36
|
+
const register = (signalName) => {
|
|
37
|
+
const handler = () => {
|
|
38
|
+
if (!controller.signal.aborted) {
|
|
39
|
+
controller.abort(new ShutdownSignalError(signalName));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
process.on(signalName, handler);
|
|
43
|
+
handlers.push({ signalName, handler });
|
|
44
|
+
};
|
|
45
|
+
register("SIGINT");
|
|
46
|
+
register("SIGTERM");
|
|
47
|
+
return {
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
dispose() {
|
|
50
|
+
for (const { signalName, handler } of handlers) {
|
|
51
|
+
process.off(signalName, handler);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|