Skip to content

Commit

Permalink
new rule exports-valid
Browse files Browse the repository at this point in the history
  • Loading branch information
mightyiam committed Sep 15, 2020
1 parent 00fac18 commit 6c399a0
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
72 changes: 72 additions & 0 deletions src/rules/exports-valid.js
@@ -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,
};
109 changes: 109 additions & 0 deletions test/unit/rules/exports-valid.test.js
@@ -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);
});
});
});

0 comments on commit 6c399a0

Please sign in to comment.