Skip to content

Commit 434176c

Browse files
committed
new rule exports valid
1 parent 00fac18 commit 434176c

File tree

2 files changed

+403
-0
lines changed

2 files changed

+403
-0
lines changed

src/rules/exports-valid.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const isPlainObj = require('is-plain-obj');
2+
const LintIssue = require('../LintIssue');
3+
const {exists} = require('../validators/property');
4+
5+
const lintId = 'exports-valid';
6+
const nodeName = 'exports';
7+
const ruleType = 'standard';
8+
9+
const isValidPathKey = (key) => key.startsWith('.') || key.startsWith('./');
10+
11+
const isValidPath = (value) => value.startsWith('./');
12+
13+
const validateFallbacks = (fallbacks) => {
14+
if (fallbacks.length === 0) return {error: 'fallbackEmpty'};
15+
16+
const {validIndexes, invalidIndexes, hasNonString, invalidFollowingValid} = fallbacks.reduce(
17+
(acc, cur, i) => {
18+
if (typeof cur === 'string') {
19+
const isValid = isValidPath(cur);
20+
21+
acc[isValid ? 'validIndexes' : 'invalidIndexes'].push(i);
22+
if (!isValid && acc.validIndexes.length) {
23+
acc.invalidFollowingValid = true;
24+
}
25+
} else {
26+
acc.hasNonString = true;
27+
}
28+
29+
return acc;
30+
},
31+
{validIndexes: [], invalidIndexes: [], hasNonString: false}
32+
);
33+
34+
if (validIndexes.length === 0) {
35+
return {error: 'fallbackNoValidPath'};
36+
}
37+
38+
if (invalidIndexes.length === 0) {
39+
return {error: 'fallbackNoInvalids'};
40+
}
41+
42+
if (validIndexes.length > 1) {
43+
return {error: 'fallbackUnreachableValid'};
44+
}
45+
46+
if (invalidFollowingValid) {
47+
return {error: 'fallbackUnreachableInvalid'};
48+
}
49+
50+
if (hasNonString) {
51+
return {error: 'fallbackHasNonString'};
52+
}
53+
54+
return true;
55+
};
56+
57+
// eslint-disable-next-line max-lines-per-function
58+
const lint = (packageJsonData, severity, config = {conditions: []}) => {
59+
const conditions = [...(config.conditions || []), 'default'];
60+
61+
if (!exists(packageJsonData, nodeName)) return true;
62+
63+
// eslint-disable-next-line complexity,max-statements,max-lines-per-function
64+
const traverse = (parentKey, parentType, exports) => {
65+
if (typeof exports === 'string') {
66+
if (!isValidPath(exports)) {
67+
return {error: 'invalidPath', str: exports};
68+
}
69+
70+
if (parentKey.endsWith('/') && !exports.endsWith('/')) {
71+
return {error: 'folderMappedToFile', str: parentKey};
72+
}
73+
74+
return true;
75+
}
76+
77+
if (Array.isArray(exports)) {
78+
// https://nodejs.org/api/esm.html#esm_package_exports_fallbacks
79+
// eslint-disable-next-line no-restricted-syntax
80+
return validateFallbacks(exports);
81+
}
82+
83+
if (!isPlainObj(exports)) {
84+
return {error: 'unexpectedType', str: typeof exports};
85+
}
86+
87+
// either a paths object or a conditions object
88+
let objectType;
89+
90+
const entries = Object.entries(exports);
91+
92+
for (let i = 0; i < entries.length; i += 1) {
93+
const [key, value] = entries[i];
94+
95+
if (isValidPathKey(key)) {
96+
if (objectType === 'conditions') {
97+
return {error: 'pathInConditions', str: key};
98+
}
99+
100+
if (parentType === 'paths') {
101+
return {error: 'nestedPaths', str: parentKey};
102+
}
103+
104+
objectType = 'paths';
105+
106+
const result = traverse(key, objectType, value);
107+
108+
if (result !== true) return result;
109+
} else {
110+
// `key` interpreted as a condition
111+
if (!conditions.includes(key)) {
112+
return {error: 'unsupportedCondition', str: key};
113+
}
114+
115+
if (objectType === 'paths') {
116+
return {error: 'conditionInPaths', str: key};
117+
}
118+
119+
objectType = 'conditions';
120+
if (key === 'default' && i + 1 < entries.length) {
121+
return {error: 'defaultConditionNotLast'};
122+
}
123+
124+
const result = traverse(key, objectType, value);
125+
126+
if (result !== true) return result;
127+
}
128+
}
129+
130+
return true;
131+
};
132+
133+
const result = traverse(nodeName, 'root', packageJsonData[nodeName]);
134+
135+
if (result !== true) {
136+
const message = {
137+
invalidPath: `invalid path \`${result.str}\` must start with \`./\``,
138+
pathInConditions: `found path key \`${result.str}\` in a conditions object`,
139+
nestedPaths: `key \`${result.str}\` has paths object vaule but only conditions may be nested`,
140+
unsupportedCondition: `condition \`${result.str}\` not in supported conditions \`${conditions}\``,
141+
conditionInPaths: `found condition key \`${result.str}\` in a paths object`,
142+
unexpectedType: `unexpected \`${result.str}\``,
143+
defaultConditionNotLast: 'condition `default` must be the last key',
144+
folderMappedToFile: `the value of the folder mapping key \`${result.str}\` must end with \`/\``,
145+
fallbackEmpty: 'empty fallback array',
146+
fallbackNoValidPath: 'fallback array has no valid path',
147+
fallbackNoInvalids: 'fallback array has no invalid values',
148+
fallbackUnreachableValid: 'fallback array has multiple valid paths',
149+
fallbackUnreachableInvalid: 'found invalid value following a valid path',
150+
fallbackHasNonString: 'fallback array must have only strings',
151+
}[result.error];
152+
153+
return new LintIssue(lintId, severity, nodeName, message);
154+
}
155+
156+
return true;
157+
};
158+
159+
module.exports = {
160+
lint,
161+
ruleType,
162+
};

0 commit comments

Comments
 (0)