@preventive/upstream 1.0.0-alpha.9 → 1.0.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/src/package.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict'
2
2
 
3
3
  import { assertion, isRepo, isRepoPath, sameName } from './args.js'
4
- import { githubRepoOfUrl } from './remote.js'
4
+ import { githubRepoOfUrl, withoutDotGit } from './remote.js'
5
5
 
6
6
  const bugsRegex = /^(?i:https?:\/\/github\.com)(?::\d{1,5})?\/(?<repo>[\w-]+\/[\w.-]+)\/issues\/?$/u
7
7
  // npm's `owner/name` shorthand means GitHub. No dots in the owner, so a
@@ -11,9 +11,12 @@ const shorthandRegex = /^(?:github:)?(?<repo>[\w-]+\/[\w.-]+)$/u
11
11
  // The ref is one segment: a branch with a `/` reads as part of the directory.
12
12
  const homepageRegex = /^(?:https?:\/\/)?(?:www\.)?github\.com(?::\d{1,5})?\/(?<repo>[\w-]+\/[\w.-]+?)(?:\.git)?(?:\/(?:tree\/[^/]+(?:\/(?<directory>.*))?)?)?$/iu
13
13
  const str = (value) => (typeof value === 'string' ? value : '')
14
- const urlOf = (field) => str(field?.url ?? field)
14
+ // Read past the whitespace around it, as `homepage` is.
15
+ const urlOf = (field) => str(field?.url ?? field).trim()
15
16
  const homepageUrl = (homepage) => str(homepage).trim().split(/[?#]/u)[0] // npm appends `#readme`.
16
- const repoIn = (regex, text) => regex.exec(text)?.groups.repo
17
+ // Every trailing `.git` dropped, as the URLs' are: no answer ends in one
18
+ // (the npm repo cache refuses one).
19
+ const repoIn = (regex, text) => withoutDotGit(regex.exec(text)?.groups.repo)
17
20
  // A URL's path spells a tree path percent-encoded; one that does not decode names none.
18
21
  function decodePath(path = '') {
19
22
  try {
@@ -33,6 +36,19 @@ function repoSubdirectory(value) {
33
36
  return isRepoPath(path) ? path : undefined // No empty, `.`, `..` or `.git` part: a path inside the repo.
34
37
  }
35
38
 
39
+ // `repository.directory` is a path its author may write with Windows' `\`.
40
+ // A tree path is `/`-separated: a `\` is a name's own only in a homepage's
41
+ // (`%5C`), which is the path itself. Its empty and `.` parts are dropped
42
+ // (`./a//b/.` is `a/b`), and one of nothing else (`./`, `/`, `/.`, `.`, `''`)
43
+ // declares the repo's root, `''`; one with a `..` part names none, whatever
44
+ // it would come to. A package.json that declares none says nothing of where
45
+ // in the repo it sits.
46
+ function declaredDirectory(value) {
47
+ if (typeof value !== 'string') return undefined
48
+ const parts = value.replaceAll('\\', '/').split('/').filter((part) => part !== '' && part !== '.')
49
+ return parts.length === 0 ? '' : repoSubdirectory(parts.join('/'))
50
+ }
51
+
36
52
  export const isRepoDirectory = (value) => typeof value === 'string' && (value === '' || repoSubdirectory(value) === value)
37
53
  export const assertRepoDirectory = assertion('a path inside the repository', isRepoDirectory)
38
54
 
@@ -41,11 +57,12 @@ export function getRepo(pkg) {
41
57
  const { bugs, repository } = pkg
42
58
  const declared = repositoryRepo(urlOf(repository))
43
59
  const homepage = homepageRegex.exec(homepageUrl(pkg.homepage))?.groups
60
+ const homepageRepo = withoutDotGit(homepage?.repo)
44
61
  // `repository` first: it is the field a publisher maintains, where a
45
62
  // `bugs` tracker can be left pointing at a former owner or misspelt.
46
- const github = [declared, repoIn(bugsRegex, urlOf(bugs)), homepage?.repo].find(isRepo)
63
+ const github = [declared, repoIn(bugsRegex, urlOf(bugs)), homepageRepo].find(isRepo)
47
64
  if (github === undefined) return {}
48
- const directory = (sameName(declared, github) && repoSubdirectory(repository?.directory))
49
- || (sameName(homepage?.repo, github) && repoSubdirectory(decodePath(homepage.directory)))
50
- return { github, ...(directory && { directory }), url: `https://github.com/${github}` }
65
+ const directory = (sameName(declared, github) ? declaredDirectory(repository?.directory) : undefined)
66
+ ?? (sameName(homepageRepo, github) ? repoSubdirectory(decodePath(homepage.directory)) : undefined)
67
+ return { github, ...(directory !== undefined && { directory }), url: `https://github.com/${github}` }
51
68
  }
package/src/remote.js CHANGED
@@ -1,15 +1,22 @@
1
1
  import { isRepo } from './args.js'
2
2
 
3
3
  // `user:token@` is skipped, never returned. It is held to the characters
4
- // RFC 3986 allows there — a `#`, `?` or `\` before the `@` ends the
4
+ // RFC 3986 allows there -- a `#`, `?` or `\` before the `@` ends the
5
5
  // authority early, so `https://evil.example#@github.com/a/b` goes to
6
- // evil.example — and URL parsing has to agree the host is github.com.
6
+ // evil.example -- and URL parsing has to agree the host is github.com.
7
7
  const gitUrlRegex = /^(?i:(?:git\+)?(?:https?|git|ssh)):\/\/(?:[\w.~%!$&'()*+,;=:-]*@)?(?i:github\.com)(?::\d{1,5})?\/(?<repo>[\w-]+\/[\w.-]+?)(?:\.git)?\/?$/u
8
8
  const scpRegex = /^(?i:git\+ssh:\/\/)?git@(?i:github\.com):(?<repo>[\w-]+\/[\w.-]+?)(?:\.git)?$/u
9
9
 
10
+ // A repo with every trailing `.git` dropped, so none ends in one: in a loop,
11
+ // as a regex would backtrack over a long run of them in a crafted URL.
12
+ export function withoutDotGit(repo) {
13
+ while (repo?.endsWith('.git')) repo = repo.slice(0, -4)
14
+ return repo
15
+ }
16
+
10
17
  export function githubRepoOfUrl(url) {
11
18
  if (typeof url !== 'string') return undefined
12
19
  const onGitHub = URL.parse(url.replace(/^git\+/iu, ''))?.hostname.toLowerCase() === 'github.com'
13
- const repo = scpRegex.exec(url)?.groups.repo ?? (onGitHub ? gitUrlRegex.exec(url)?.groups.repo : undefined)
20
+ const repo = withoutDotGit(scpRegex.exec(url)?.groups.repo ?? (onGitHub ? gitUrlRegex.exec(url)?.groups.repo : undefined))
14
21
  return isRepo(repo) ? repo : undefined
15
22
  }