Skip to content

Commit a94f663

Browse files
Namcheebluwy
andauthored
feat: add INVALID_REPOSITORY_VALUE rule (#106)
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
1 parent 17464b8 commit a94f663

File tree

14 files changed

+332
-4
lines changed

14 files changed

+332
-4
lines changed

pkg/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ export type Message =
109109
}
110110
>
111111
| BaseMessage<'DEPRECATED_FIELD_JSNEXT'>
112+
| BaseMessage<
113+
'INVALID_REPOSITORY_VALUE',
114+
{
115+
type:
116+
| 'invalid-string-shorthand'
117+
| 'invalid-git-url'
118+
| 'deprecated-github-git-protocol'
119+
| 'shorthand-git-sites'
120+
}
121+
>
112122

113123
export interface Options {
114124
/**

pkg/src/index.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
getPkgPathValue,
2323
replaceLast,
2424
isRelativePath,
25-
isAbsolutePath
25+
isAbsolutePath,
26+
isGitUrl,
27+
isShorthandRepositoryUrl,
28+
isShorthandGitHubOrGitLabUrl,
29+
isDeprecatedGitHubGitUrl
2630
} from './utils.js'
2731

2832
/**
@@ -234,6 +238,12 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) {
234238
}
235239
}
236240

241+
// if `repository` field exist, check if the value is valid
242+
// `repository` might be a shorthand string of URL or an object
243+
if ('repository' in rootPkg) {
244+
promiseQueue.push(() => checkRepositoryField(rootPkg.repository))
245+
}
246+
237247
// check file existence for other known package fields
238248
const knownFields = [
239249
'types',
@@ -436,6 +446,55 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) {
436446
}
437447
}
438448

449+
// https://docs.npmjs.com/cli/v10/configuring-npm/package-json#repository
450+
/**
451+
* @param {Record<string, string> | string} repository
452+
*/
453+
async function checkRepositoryField(repository) {
454+
if (!ensureTypeOfField(repository, ['string', 'object'], ['repository']))
455+
return
456+
457+
if (typeof repository === 'string') {
458+
// the string field accepts shorthands only. if this doesn't look like a shorthand,
459+
// and looks like a git URL, recommend using the object form.
460+
if (!isShorthandRepositoryUrl(repository)) {
461+
messages.push({
462+
code: 'INVALID_REPOSITORY_VALUE',
463+
args: { type: 'invalid-string-shorthand' },
464+
path: ['repository'],
465+
type: 'warning'
466+
})
467+
}
468+
} else if (
469+
typeof repository === 'object' &&
470+
repository.url &&
471+
repository.type === 'git'
472+
) {
473+
if (!isGitUrl(repository.url)) {
474+
messages.push({
475+
code: 'INVALID_REPOSITORY_VALUE',
476+
args: { type: 'invalid-git-url' },
477+
path: ['repository', 'url'],
478+
type: 'warning'
479+
})
480+
} else if (isDeprecatedGitHubGitUrl(repository.url)) {
481+
messages.push({
482+
code: 'INVALID_REPOSITORY_VALUE',
483+
args: { type: 'deprecated-github-git-protocol' },
484+
path: ['repository', 'url'],
485+
type: 'suggestion'
486+
})
487+
} else if (isShorthandGitHubOrGitLabUrl(repository.url)) {
488+
messages.push({
489+
code: 'INVALID_REPOSITORY_VALUE',
490+
args: { type: 'shorthand-git-sites' },
491+
path: ['repository', 'url'],
492+
type: 'suggestion'
493+
})
494+
}
495+
}
496+
}
497+
439498
/**
440499
* @param {string[]} pkgPath
441500
*/

pkg/src/message.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,32 @@ export function formatMessage(m, pkg) {
158158
case 'DEPRECATED_FIELD_JSNEXT':
159159
// prettier-ignore
160160
return `${c.bold(fp(m.path))} is deprecated. ${c.bold('pkg.module')} should be used instead.`
161+
case 'INVALID_REPOSITORY_VALUE':
162+
switch (m.args.type) {
163+
case 'invalid-string-shorthand':
164+
// prettier-ignore
165+
return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which isn't a valid shorthand value supported by npm. Consider using an object that references a repository.`
166+
case 'invalid-git-url':
167+
// prettier-ignore
168+
return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which isn't a valid git URL. A valid git URL is usually in the form of "${c.bold('git+https://example.com/user/repo.git')}".`
169+
case 'deprecated-github-git-protocol':
170+
// prettier-ignore
171+
return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which uses the git:// protocol that is deprecated by GitHub due to security concerns. Consider replacing the protocol with https://.`
172+
case 'shorthand-git-sites': {
173+
let fullUrl = pv(m.path)
174+
if (fullUrl[fullUrl.length - 1] === '/') {
175+
fullUrl = fullUrl.slice(0, -1)
176+
}
177+
if (!fullUrl.startsWith('git+')) {
178+
fullUrl = 'git+' + fullUrl
179+
}
180+
if (!fullUrl.endsWith('.git')) {
181+
fullUrl += '.git'
182+
}
183+
// prettier-ignore
184+
return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but could be a full git URL like "${c.bold(fullUrl)}".`
185+
}
186+
}
161187
default:
162188
return
163189
}

pkg/src/utils.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { lintableFileExtensions } from './constants.js'
66
* main: string,
77
* module: string,
88
* exports: Record<string, string>,
9+
* repository: Record<string, string> | string,
910
* type: 'module' | 'commonjs'
1011
* }} Pkg
1112
*/
@@ -45,6 +46,59 @@ export function stripComments(code) {
4546
.replace(SINGLELINE_COMMENTS_RE, '')
4647
}
4748

49+
// Reference: https://git-scm.com/docs/git-clone#_git_urls and https://github.com/npm/hosted-git-info
50+
const GIT_URL_RE =
51+
/^(git\+https?|git\+ssh|https?|ssh|git):\/\/(?:[\w._-]+@)?([\w.-]+)(?::([\w\d-]+))?(\/[\w._/-]+)\/?$/
52+
/**
53+
* @param {string} url
54+
*/
55+
export function isGitUrl(url) {
56+
return GIT_URL_RE.test(url)
57+
}
58+
/**
59+
* @param {string} url
60+
*/
61+
export function isShorthandGitHubOrGitLabUrl(url) {
62+
const tokens = url.match(GIT_URL_RE)
63+
if (tokens) {
64+
const host = tokens[2]
65+
const path = tokens[4]
66+
67+
if (/(github|gitlab)/.test(host)) {
68+
return !url.startsWith('git+') || !path.endsWith('.git')
69+
}
70+
}
71+
72+
return false
73+
}
74+
/**
75+
* Reference: https://github.blog/security/application-security/improving-git-protocol-security-github/
76+
* @param {string} url
77+
*/
78+
export function isDeprecatedGitHubGitUrl(url) {
79+
const tokens = url.match(GIT_URL_RE)
80+
if (tokens) {
81+
const protocol = tokens[1]
82+
const host = tokens[2]
83+
84+
if (/github/.test(host) && protocol === 'git') {
85+
return true
86+
}
87+
}
88+
89+
return false
90+
}
91+
92+
// Reference: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#repository
93+
const SHORTHAND_REPOSITORY_URL_RE =
94+
/^(?:(?:github|bitbucket|gitlab):[\w\-]+\/[\w\-]+|gist:\w+|[\w\-]+\/[\w\-]+)$/
95+
/**
96+
* @param {string} url
97+
*/
98+
export function isShorthandRepositoryUrl(url) {
99+
return SHORTHAND_REPOSITORY_URL_RE.test(url)
100+
}
101+
48102
/**
49103
* @param {string} code
50104
* @returns {CodeFormat}

pkg/tests/fixtures/invalid-field-types/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"type": "commonjs",
66
"main": 0,
77
"module": true,
8-
"jsnext:main": false
8+
"jsnext:main": false,
9+
"repository": 123
910
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "publint-invalid-repository-value-object-deprecatec",
3+
"version": "0.0.1",
4+
"type": "commonjs",
5+
"repository": {
6+
"type": "git",
7+
"url": "git://www.github.com/bluwy/publint"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "publint-invalid-repository-value-object-not-git-url",
3+
"version": "0.0.1",
4+
"type": "commonjs",
5+
"repository": {
6+
"type": "git",
7+
"url": "imap://fake.com/"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "publint-invalid-repository-value-object-shorthand-site",
3+
"version": "0.0.1",
4+
"type": "commonjs",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://www.github.com/bluwy/publint"
8+
}
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "publint-invalid-repository-value-shorthand",
3+
"version": "0.0.1",
4+
"type": "commonjs",
5+
"repository": "npm/npm"
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "publint-invalid-repository-value-string-not-url",
3+
"version": "0.0.1",
4+
"type": "commonjs",
5+
"repository": "not_an_url"
6+
}

0 commit comments

Comments
 (0)