Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
const isPlainObj = require('is-plain-obj'); | ||
const {isObject, isString} = require('../validators/type'); | ||
const LintIssue = require('../LintIssue'); | ||
const {exists} = require('../validators/property'); | ||
|
||
const lintId = 'exports-valid'; | ||
const nodeName = 'exports'; | ||
const ruleType = 'standard'; | ||
|
||
const getKeyType = (key) => { | ||
if (key.startsWith('/')) return 'invalid'; | ||
|
||
return key.startsWith('.') ? 'entry' : 'condition'; | ||
}; | ||
|
||
const isValidPath = (value) => { | ||
if (typeof value !== 'string') return false; | ||
|
||
if (['', '.', './', '..', '../'].includes(value)) return false; | ||
|
||
if (value.includes('../')) return false; | ||
|
||
if (value.startsWith('/')) return false; | ||
|
||
return true; | ||
}; | ||
|
||
const traverse = (exports, path = []) => { | ||
const fails = []; | ||
|
||
if (typeof exports === 'string') { | ||
if (!isValidPath(exports)) { | ||
fails.push([path, 'String value should be a valid export path']); | ||
} | ||
} else if (isPlainObj(exports)) { | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const [key, value] of Object.entries(exports)) { | ||
if (getKeyType(key) === 'invalid') { | ||
fails.push([path, 'Key should not be relative path or "condition"']); | ||
} else { | ||
fails.push(...traverse(value)); | ||
} | ||
} | ||
} else { | ||
fails.push([path, 'Value should be string or object']); | ||
} | ||
|
||
if (Array.isArray(exports)) { | ||
fails.push([path, 'Array']); | ||
} | ||
|
||
return fails.map(([arrayOfKeys, message]) => [arrayOfKeys.join('.'), message]); | ||
}; | ||
|
||
const lint = (packageJsonData, severity) => { | ||
if (!exists(packageJsonData, nodeName)) return true; | ||
|
||
const fails = traverse(packageJsonData[nodeName]); | ||
|
||
if (fails.length > 0) { | ||
const failsListString = fails.map(([path, message]) => `\`${path}\`: ${message}`); | ||
|
||
return new LintIssue(lintId, severity, nodeName, `Invalid paths: ${failsListString}`); | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
module.exports = { | ||
lint, | ||
ruleType, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
const ruleModule = require('../../../src/rules/exports-valid'); | ||
|
||
const {lint, ruleType} = ruleModule; | ||
|
||
describe('exports-valid Unit Tests', () => { | ||
describe('a rule type value should be exported', () => { | ||
test('it should equal "standard"', () => { | ||
expect(ruleType).toStrictEqual('standard'); | ||
}); | ||
}); | ||
|
||
describe('when package.json has invalid node', () => { | ||
const failures = [ | ||
{ | ||
title: 'root is `true`', | ||
input: true, | ||
message: 'Value of `exports` field should be string or object', | ||
}, | ||
{ | ||
title: 'root is a number', | ||
input: 4, | ||
message: 'Value of `exports` field should be string or object', | ||
}, | ||
{ | ||
title: 'key is `/`', | ||
input: {'/': 'foo.js'}, | ||
message: 'Unsupported condition key `/`. Supported conditions are `[]`', | ||
}, | ||
{ | ||
title: 'key starts with `/`', | ||
input: {'/foo': 'foo.js'}, | ||
message: 'Unsupported condition key `/foo`. Supported conditions are `[]`', | ||
}, | ||
{ | ||
title: 'key is short relative path', | ||
input: {foo: 'foo.js'}, | ||
message: 'Unsupported condition key `foo`. Supported conditions are `[]`', | ||
}, | ||
{ | ||
title: 'main-only sugar path starts with `/`', | ||
input: '/main.js', | ||
message: 'Invalid path `/main.js`. Paths must start with `./`', | ||
}, | ||
{ | ||
title: 'main-only sugar path short form relative', | ||
input: 'main.js', | ||
message: 'Invalid path `main.js`. Paths must start with `./`', | ||
}, | ||
{ | ||
title: 'short form relative path', | ||
input: {'./a': 'a.js'}, | ||
message: 'Invalid path `a.js`. Paths must start with `./`', | ||
}, | ||
{ | ||
title: 'unsupported condition', | ||
config: {conditions: ['foo']}, | ||
input: {bar: './main.js'}, | ||
message: "Unsupported condition `bar`. Supported conditions are `['foo']`", | ||
}, | ||
{ | ||
title: 'folder mapped to file', | ||
input: {'./': './a.js'}, | ||
message: 'The value of the folder mapping key `./` must end with `/`', | ||
}, | ||
|
||
// conditional import key `node` must be after `import` and `require` if any of them exists | ||
|
||
// conditional import key `default` must be last | ||
|
||
// support fallbacks. at least one of the values must be valid | ||
]; | ||
failures.forEach(({title, input, fails}) => { | ||
// eslint-disable-next-line jest/valid-title | ||
test(title, () => { | ||
const packageJsonData = {exports: input}; | ||
const response = lint(packageJsonData, 'error'); | ||
|
||
expect(response.lintId).toStrictEqual('exports-valid'); | ||
expect(response.severity).toStrictEqual('error'); | ||
expect(response.node).toStrictEqual('exports'); | ||
const failsListString = fails.map(([path, message]) => `\`${path}\`: ${message}`); | ||
expect(response.lintMessage).toStrictEqual(`Invalid paths: ${failsListString}`); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('when package.json has valid node', () => { | ||
const valids = [ | ||
{ | ||
title: 'main-only sugar', | ||
input: './main.js', | ||
}, | ||
{ | ||
title: 'supported condition', | ||
config: {conditions: ['foo']}, | ||
input: {foo: './main.js'}, | ||
}, | ||
]; | ||
}); | ||
|
||
describe('when package.json does not have node', () => { | ||
test('true should be returned', () => { | ||
const packageJsonData = {}; | ||
const response = lint(packageJsonData, 'error'); | ||
|
||
expect(response).toBe(true); | ||
}); | ||
}); | ||
}); |