From c4e13810aa557a3fcaaec1f42d296c0a15673e35 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 16 Dec 2024 15:34:07 -0800 Subject: [PATCH] feat: add `HostedGitInfo.fromManifest` This encapsulates the logic used in `npm repo` --- lib/index.js | 51 +++++++++++++++++++++++++++++++++ test/github.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/gitlab.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) diff --git a/lib/index.js b/lib/index.js index 0c9d0b0..d297b8c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,27 @@ const parseUrl = require('./parse-url.js') const cache = new LRUCache({ max: 1000 }) +function unknownHostedUrl (url) { + try { + const { + protocol, + hostname, + pathname, + } = new URL(url) + + /* istanbul ignore next - URL ctor should prevent this */ + if (!protocol || !hostname) { + return null + } + + const proto = /(?:git\+)http:$/.test(protocol) ? 'http:' : 'https:' + const path = pathname.replace(/\.git$/, '') + return `${proto}//${hostname}${path}` + } catch { + return null + } +} + class GitHost { constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) { Object.assign(this, GitHost.#gitHosts[type], { @@ -56,6 +77,36 @@ class GitHost { return cache.get(key) } + static fromManifest (manifest, opts = {}) { + if (!manifest || typeof manifest !== 'object') { + return + } + + const r = manifest.repository + // TODO: look into also checking the `bugs`/`homepage` URLs + + const rurl = r && ( + typeof r === 'string' + ? r + : typeof r === 'object' && typeof r.url === 'string' + ? r.url + : null + ) + + if (!rurl) { + throw new Error('no repository') + } + + const info = (rurl && this.fromUrl(rurl.replace(/^git\+/, ''), opts)) || null + const url = info ? info.browse(r.directory) : unknownHostedUrl(rurl) + + if (!url) { + throw new Error('no repository: could not get URL') + } + + return url + } + static parseUrl (url) { return parseUrl(url) } diff --git a/test/github.js b/test/github.js index 6c01068..666a352 100644 --- a/test/github.js +++ b/test/github.js @@ -270,3 +270,80 @@ t.test('string methods populate correctly', t => { t.end() }) + +t.test('from manifest', t => { + t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') + t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') + t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined') + t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined') + + const unknownHostRepo = { + name: 'foo', + repository: { + url: 'https://nope.com', + }, + } + t.equal(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/') + + const insecureUnknownHostRepo = { + name: 'foo', + repository: { + url: 'http://nope.com', + }, + } + t.equal(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/') + + const insecureGitUnknownHostRepo = { + name: 'foo', + repository: { + url: 'git+http://nope.com', + }, + } + t.equal(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com') + + const badRepo = { + name: 'foo', + repository: { + url: '#', + }, + } + t.throws(() => HostedGit.fromManifest(badRepo), Error, 'bad repo throws') + + const manifest = { + name: 'foo', + repository: { + type: 'git', + url: 'git+ssh://github.com/foo/bar.git', + }, + } + + const parsed = HostedGit.fromManifest(manifest) + t.equal(parsed, 'https://github.com/foo/bar') + + const monorepo = { + name: 'clowncar', + repository: { + type: 'git', + url: 'git+ssh://github.com/foo/bar.git', + directory: 'packages/foo', + }, + } + + const honk = HostedGit.fromManifest(monorepo) + t.equal(honk, 'https://github.com/foo/bar/tree/HEAD/packages/foo') + + const stringRepo = { + name: 'foo', + repository: 'git+ssh://github.com/foo/bar.git', + } + const stringRepoParsed = HostedGit.fromManifest(stringRepo) + t.equal(stringRepoParsed, 'https://github.com/foo/bar') + + const nonStringRepo = { + name: 'foo', + repository: 42, + } + t.throws(() => HostedGit.fromManifest(nonStringRepo)) + + t.end() +}) diff --git a/test/gitlab.js b/test/gitlab.js index 685bd06..db56419 100644 --- a/test/gitlab.js +++ b/test/gitlab.js @@ -321,3 +321,80 @@ t.test('string methods populate correctly', t => { t.end() }) + +t.test('from manifest', t => { + t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') + t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') + t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined') + t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined') + + const unknownHostRepo = { + name: 'foo', + repository: { + url: 'https://nope.com', + }, + } + t.equal(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/') + + const insecureUnknownHostRepo = { + name: 'foo', + repository: { + url: 'http://nope.com', + }, + } + t.equal(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/') + + const insecureGitUnknownHostRepo = { + name: 'foo', + repository: { + url: 'git+http://nope.com', + }, + } + t.equal(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com') + + const badRepo = { + name: 'foo', + repository: { + url: '#', + }, + } + t.throws(() => HostedGit.fromManifest(badRepo), Error, 'bad repo throws') + + const manifest = { + name: 'foo', + repository: { + type: 'git', + url: 'git+ssh://gitlab.com/foo/bar.git', + }, + } + + const parsed = HostedGit.fromManifest(manifest) + t.equal(parsed, 'https://gitlab.com/foo/bar') + + const monorepo = { + name: 'clowncar', + repository: { + type: 'git', + url: 'git+ssh://gitlab.com/foo/bar.git', + directory: 'packages/foo', + }, + } + + const honk = HostedGit.fromManifest(monorepo) + t.equal(honk, 'https://gitlab.com/foo/bar/tree/HEAD/packages/foo') + + const stringRepo = { + name: 'foo', + repository: 'git+ssh://gitlab.com/foo/bar.git', + } + const stringRepoParsed = HostedGit.fromManifest(stringRepo) + t.equal(stringRepoParsed, 'https://gitlab.com/foo/bar') + + const nonStringRepo = { + name: 'foo', + repository: 42, + } + t.throws(() => HostedGit.fromManifest(nonStringRepo)) + + t.end() +})