|
| 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