Skip to content

Commit

Permalink
feat: Initial functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
medikoo committed Mar 11, 2022
1 parent f0d8254 commit a854ef9
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -4,12 +4,25 @@

# npm-registry-utilities

## Utilities for retrieving data from npm registry

### Installation

```bash
npm install npm-registry-utilities
```

### Available utilities

- [`resolve-package-metadata`](docs/resolve-package-metadata.md) - Resolves meta data for given package name
- [`resolve-version-metadata`](docs/resolve-version-metadata.md) - Resolves meta data for version of given package name that matches provided version range

### Tests

```bash
npm test
```

[build-image]: https://github.com/medikoo/npm-registry-utilities/workflows/Integrate/badge.svg
[build-url]: https://github.com/medikoo/npm-registry-utilities/actions?query=workflow%3AIntegrate
[cov-image]: https://img.shields.io/codecov/c/github/medikoo/npm-registry-utilities.svg
Expand Down
17 changes: 17 additions & 0 deletions docs/resolve-package-metadata.md
@@ -0,0 +1,17 @@
# `resolve-package-metadata(packageName[, options])`

Resolve metadata for provided package name.

## Supported options

- `registryUrl` - Alternative registryUrl to query
- `authToken` - Auth token for registry url

## Usage

```javascript
const resolvePackageMetadata = require("npm-registry-utilities/resolve-package-metadata");

const packageMetaData = await resolvePackageMetaData("npm-registry-utilities");
// { name: "npm-registry-utilities", 'dist-tags': { latest: ... }, versions: { ... } }
```
19 changes: 19 additions & 0 deletions docs/resolve-version-metadata.md
@@ -0,0 +1,19 @@
# `resolve-version-metadata(packageName[, versionRange[, options]])`

Resolve metadata for provided package name.

`versionRange` defaults to `latest`

## Supported options

_Same as for [`resolve-package-metadata`](docs/resolve-package-metadata.md)_

## Usage

```javascript
const resolveVersionMetadata = require("npm-registry-utilities/resolve-version-metadata");

const versionMetaData = await resolvePackageMetaData("npm-registry-utilities", "1");
// Version metadata for latest v1 release
// { name: "npm-registry-utilities", version: .... dist: ..., .... } }
```
20 changes: 20 additions & 0 deletions ensure-package-name.js
@@ -0,0 +1,20 @@
"use strict";

const ensureString = require("type/string/ensure")
, validatePackageName = require("validate-npm-package-name");

module.exports = packageName => {
packageName = ensureString(packageName, { errorCode: "INVALID_PACKAGE_NAME" });
const validationResult = validatePackageName(packageName);
if (!validationResult.validForOldPackages) {
throw Object.assign(
new Error(
`Invalid package name "${ packageName }":\n\t${
validationResult.errors.join("\n\t")
}`
),
{ code: "INVALID_PACKAGE_NAME" }
);
}
return packageName;
};
14 changes: 14 additions & 0 deletions ensure-version-range.js
@@ -0,0 +1,14 @@
"use strict";

const ensureString = require("type/string/ensure")
, semver = require("semver");

module.exports = versionRange => {
versionRange = ensureString(versionRange, { errorCode: "INVALID_VERSION_RANGE" });
if (!semver.validRange(versionRange)) {
throw Object.assign(new Error(`Invalid version range "${ versionRange }"`), {
code: "INVALID_VERSION_RANGE"
});
}
return versionRange;
};
26 changes: 26 additions & 0 deletions lib/cache.js
@@ -0,0 +1,26 @@
"use strict";

const random = require("ext/string/random")
, path = require("path")
, os = require("os")
, fsp = require("fs").promises
, readFile = require("fs2/read-file")
, writeFile = require("fs2/write-file");

const cacheDirname = path.resolve(os.homedir(), ".npm-registry-utils");

module.exports = {
async get(name) {
const content = await readFile(path.resolve(cacheDirname, name), { loose: true });
if (!content) return null;
try { return JSON.parse(content); }
catch { return null; }
},
async set(name, value) {
const filename = path.resolve(cacheDirname, name)
, tmpFilename = path.resolve(cacheDirname, `${ name }. ${ random() }tmp`);
await writeFile(tmpFilename, JSON.stringify(value), { intermediate: true });
await fsp.rename(tmpFilename, filename);
},
path: cacheDirname
};
10 changes: 10 additions & 0 deletions package.json
Expand Up @@ -9,7 +9,17 @@
"registry"
],
"repository": "medikoo/npm-registry-utilities",
"dependencies": {
"ext": "^1.6.0",
"fs2": "^0.3.9",
"memoizee": "^0.4.15",
"node-fetch": "^2.6.7",
"semver": "^7.3.5",
"type": "^2.6.0",
"validate-npm-package-name": "^3.0.0"
},
"devDependencies": {
"chai": "^4.3.6",
"eslint": "^7.32.0",
"eslint-config-medikoo": "^4.1.0",
"git-list-updated": "^1.2.1",
Expand Down
34 changes: 34 additions & 0 deletions resolve-package-metadata.js
@@ -0,0 +1,34 @@
"use strict";

const ensureString = require("type/string/ensure")
, isObject = require("type/object/is")
, memoizee = require("memoizee")
, fetch = require("node-fetch")
, ensurePackageName = require("./ensure-package-name")
, cache = require("./lib/cache");

module.exports = memoizee(
async (name, options = {}) => {
name = ensurePackageName(name);
if (!isObject(options)) options = {};
const registryUrl = ensureString(options.registryUrl, {
default: "https://registry.npmjs.org"
});
const authToken = ensureString(options.authToken, { isOptional: true });
const cached = (await cache.get(name)) || {};
const response = await fetch(`${ registryUrl }/${ name }`, {
headers: {
accept: "application/vnd.npm.install-v1+json",
...(authToken && { authorization: `Bearer ${ authToken }` }),
// We do not rely on got's cache
// as it's not effective with 'authorization' header
...(cached.etag && { "if-none-match": cached.etag })
}
});
if (response.status === 304) return cached.data;
const result = await response.json();
cache.set(`${ name }/registry.json`, { etag: response.headers.etag, data: result });
return result;
},
{ primitive: true, promise: true }
);
29 changes: 29 additions & 0 deletions resolve-version-metadata.js
@@ -0,0 +1,29 @@
"use strict";

const isObject = require("type/object/is")
, isValue = require("type/value/is")
, semver = require("semver")
, ensurePackageName = require("./ensure-package-name")
, ensureVersionRange = require("./ensure-version-range")
, resolvePackageMetadata = require("./resolve-package-metadata");

module.exports = async (name, versionRange = null, options = {}) => {
name = ensurePackageName(name);
if (isValue(versionRange)) versionRange = ensureVersionRange(versionRange);
if (!isObject(options)) options = {};
const metadata = await resolvePackageMetadata(name, options);
if (!versionRange) versionRange = metadata["dist-tags"].latest;
if (semver.valid(versionRange)) return metadata.versions[versionRange] || null;
const allVersions = [], stableVersions = [];
for (const [version, meta] of Object.entries(metadata.versions)) {
const versionData = semver.parse(version);
if (versionData.prerelease.length) continue;
allVersions.push(version);
if (!meta.deprecated) stableVersions.push(version);
}
const resultVersion =
semver.maxSatisfying(stableVersions, versionRange) ||
semver.maxSatisfying(allVersions, versionRange);
if (!resultVersion) return null;
return metadata.versions[resultVersion];
};
12 changes: 12 additions & 0 deletions test/ensure-package-name.test.js
@@ -0,0 +1,12 @@
"use strict";

const { assert } = require("chai");

const ensurePackageName = require("../ensure-package-name");

describe("ensure-package-name.js", () => {
it("should pass through valid package name", () =>
assert.equal(ensurePackageName("npm-registry-utilities"), "npm-registry-utilities"));
it("should reject invalid package name", () =>
assert.throws(() => ensurePackageName("invalid name"), "Invalid package name"));
});
12 changes: 12 additions & 0 deletions test/ensure-version-range.test.js
@@ -0,0 +1,12 @@
"use strict";

const { assert } = require("chai");

const ensureVersionRange = require("../ensure-version-range");

describe("ensure-version-range.js", () => {
it("should pass through valid version range", () =>
assert.equal(ensureVersionRange("^1.1.1"), "^1.1.1"));
it("should reject invalid version range", () =>
assert.throws(() => ensureVersionRange("invalid range"), "Invalid version range"));
});
Empty file removed test/index.test.js
Empty file.
13 changes: 13 additions & 0 deletions test/resolve-package-metadata.test.js
@@ -0,0 +1,13 @@
"use strict";

const { assert } = require("chai");

const resolvePackageMetadata = require("../resolve-package-metadata");

describe("resolve-package-metadata.js", () => {
it("should resolve npm package metadata", async () => {
const result = await resolvePackageMetadata("d");
assert.equal(result.name, "d");
assert.equal(typeof result.versions[Object.keys(result.versions)[0]].version, "string");
});
});
22 changes: 22 additions & 0 deletions test/resolve-version-metadata.test.js
@@ -0,0 +1,22 @@
"use strict";

const { assert } = require("chai");

const resolveVersionMetadata = require("../resolve-version-metadata")
, resolvePackageMetadata = require("../resolve-package-metadata");

describe("resolve-version-metadata.js", () => {
it("should resolve latest version metadata when no version range provided", async () => {
const result = await resolveVersionMetadata("d");
const metadata = await resolvePackageMetadata("d");
assert.equal(result.version, metadata["dist-tags"].latest);
});
it("should resolve specific version metadata", async () => {
const result = await resolveVersionMetadata("d", "1.0.0");
assert.equal(result.version, "1.0.0");
});
it("should resolve latest version in a range", async () => {
const result = await resolveVersionMetadata("d", "^0.1");
assert.equal(result.version, "0.1.1");
});
});

0 comments on commit a854ef9

Please sign in to comment.