UNPKG

publint

Version:

Lint packaging errors

224 lines (216 loc) 13.2 kB
import picocolors from 'picocolors' import { formatMessagePath as fp, getPkgPathValue, replaceLast, } from '../shared/utils.js' /** @type { import('picocolors/types.js').Colors | undefined } */ let _picocolorsHasColors /** @type { import('picocolors/types.js').Colors | undefined } */ let _picocolorsNoColors /** * @param {import('../index.d.ts').Message} m * @param {import('../shared/utils.js').Pkg} pkg * @param {import('../utils.d.ts').FormatMessageOptions} opts */ export function formatMessage(m, pkg, opts = {}) { /** @type { import('picocolors/types.js').Colors } */ let c = picocolors if (opts.color === true) { c = _picocolorsHasColors ??= picocolors.createColors(true) } else if (opts.color === false) { c = _picocolorsNoColors ??= picocolors.createColors(false) } /** @param {string[]} path */ const pv = (path) => getPkgPathValue(pkg, path) switch (m.code) { case 'IMPLICIT_INDEX_JS_INVALID_FORMAT': return `index.js should be ${m.args.expectFormat} but it is ${m.args.actualFormat}` case 'FILE_INVALID_FORMAT': { const is = pv(m.path).includes('*') ? 'matches' : 'is' const relativePath = m.args.actualFilePath ?? pv(m.path) const start = m.path[0] === 'name' ? c.bold(relativePath) : `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} and` // prettier-ignore return `${start} is written in ${c.yellow(m.args.actualFormat)}, but is interpreted as ${c.yellow(m.args.expectFormat)}. Consider using the ${c.yellow(m.args.expectExtension)} extension, e.g. ${c.bold(replaceLast(relativePath, '.js', m.args.expectExtension))}` } case 'FILE_INVALID_EXPLICIT_FORMAT': { const is = pv(m.path).includes('*') ? 'matches' : 'is' const relativePath = m.args.actualFilePath ?? pv(m.path) const start = m.path[0] === 'name' ? c.bold(relativePath) : `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} which` // prettier-ignore return `${start} ends with the ${c.yellow(m.args.actualExtension)} extension, but the code is written in ${c.yellow(m.args.actualFormat)}. Consider using the ${c.yellow(m.args.expectExtension)} extension, e.g. ${c.bold(replaceLast(relativePath,m.args.actualExtension, m.args.expectExtension))}` } case 'FILE_INVALID_JSX_EXTENSION': { const is = m.args.globbedFilePath ? 'matches' : 'is' const relativePath = m.args.globbedFilePath ?? pv(m.path) const start = m.path[0] === 'name' ? c.bold(relativePath) : `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} which` // prettier-ignore return `${start} uses an invalid ${c.bold(m.args.actualExtension)} extension. You don't need to split ESM and CJS formats for JSX. You should write a single file in ESM with the ${c.bold('.jsx')} extension instead, e.g. ${c.bold(replaceLast(pv(m.path), m.args.actualExtension, '.jsx'))}` } case 'FILE_DOES_NOT_EXIST': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but the file does not exist.` case 'FILE_NOT_PUBLISHED': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but the file is not published. Is it specified in ${c.bold('pkg.files')}?` case 'HAS_ESM_MAIN_BUT_NO_EXPORTS': // prettier-ignore return `${c.bold('pkg.main')} is an ESM file, but it is usually better to use ${c.bold('pkg.exports')} instead. If you don't support Node.js 12.6 and below, you can also remove ${c.bold('pkg.main')}. (This will be a breaking change)` case 'HAS_MODULE_BUT_NO_EXPORTS': // prettier-ignore return `${c.bold('pkg.module')} is used to output ESM, but ${c.bold('pkg.exports')} is not defined. As Node.js doesn't read ${c.bold('pkg.module')}, the ESM output may be skipped. Consider adding ${c.bold('pkg.exports')} to export the ESM output. ${c.bold('pkg.module')} can usually be removed alongside too. (This will be a breaking change)` case 'MODULE_SHOULD_BE_ESM': // prettier-ignore return `${c.bold('pkg.module')} should be ESM, but the code is written in CJS.` case 'EXPORTS_GLOB_NO_MATCHED_FILES': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but does not match any files.` case 'EXPORTS_GLOB_NO_DEPRECATED_SUBPATH_MAPPING': // prettier-ignore return `${c.bold(fp(m.path))} maps to a path that ends with ${c.bold('/')} which is deprecated. Use ${c.bold(fp(m.args.expectPath))}: "${c.bold(m.args.expectValue)}" instead.` case 'EXPORTS_TYPES_SHOULD_BE_FIRST': // prettier-ignore return `${c.bold(fp(m.path))} should be the first in the object as conditions are order-sensitive so it can be resolved by TypeScript.` case 'EXPORTS_MODULE_SHOULD_PRECEDE_REQUIRE': { // prettier-ignore return `${c.bold(fp(m.path))} should come before the "require" condition so it can take precedence when used by a bundler.` } case 'EXPORTS_DEFAULT_SHOULD_BE_LAST': // prettier-ignore return `${c.bold(fp(m.path))} should be the last in the object so it doesn't take precedence over the keys following it.` case 'EXPORTS_MODULE_SHOULD_BE_ESM': // prettier-ignore return `${c.bold(fp(m.path))} should be ESM, but the code is written in CJS.` case 'EXPORTS_VALUE_INVALID': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but is invalid as it does not start with "${c.bold('./')}". Use ${c.bold(m.args.suggestValue)} instead.` case 'EXPORTS_MISSING_ROOT_ENTRYPOINT': { const mainField = m.args.mainFields[0] // prettier-ignore return `${c.bold(fp(m.path))} is missing the root entrypoint export, which is defined in ${c.bold('pkg.' + mainField)}. Environments that support the ${c.bold('"exports"')} field will ignore ${c.bold('pkg.' + mainField)} as ${c.bold('"exports"')} takes the highest priority. Consider adding ${c.bold(fp(m.path.concat('.')))}: "${c.bold(pv([mainField]))}".` } case 'USE_EXPORTS_BROWSER': // prettier-ignore return `${c.bold('pkg.browser')} with a string value can be refactored to use ${c.bold('pkg.exports')} and the ${c.bold('"browser"')} condition to declare browser-specific exports. ` + `e.g. ${c.bold('pkg.exports["."].browser')}: "${c.bold(pv(m.path))}". (This will be a breaking change)` case 'USE_EXPORTS_OR_IMPORTS_BROWSER': // prettier-ignore return `${c.bold('pkg.browser')} with an object value can be refactored to use ${c.bold('pkg.exports')}/${c.bold('pkg.imports')} and the ${c.bold('"browser"')} condition to declare browser-specific exports. (This will be a breaking change)` case 'USE_FILES': // prettier-ignore return `The package ${c.bold('publishes internal tests or config files')}. You can use ${c.bold('pkg.files')} to only publish certain files and save user bandwidth.` case 'USE_TYPE': // prettier-ignore return `The package does not specify the ${c.bold('"type"')} field. Node.js may attempt to detect the package type causing a small performance hit. Consider adding ${c.bold('"type"')}: "${c.bold('commonjs')}".` case 'USE_LICENSE': // prettier-ignore return `The package does not specify the ${c.bold('"license"')} field but a license file was detected at ${c.bold(m.args.licenseFilePath)}. Consider adding a ${c.bold('"license"')} field so it's displayed on npm.` case 'TYPES_NOT_EXPORTED': { const typesFilePath = exportsRel(m.args.typesFilePath) if (m.args.actualExtension && m.args.expectExtension) { // prettier-ignore return `${c.bold(fp(m.path))} types is not exported. Consider adding ${c.bold(fp(m.path) + '.types')} to be compatible with TypeScript's ${c.bold('"moduleResolution": "bundler"')} compiler option. ` + `Note that you cannot use "${c.bold(typesFilePath)}" because it has a mismatching format. Instead, you can duplicate the file and use the ${c.bold(m.args.expectExtension)} extension, e.g. ` + `${c.bold(fp(m.path) + '.types')}: "${c.bold(replaceLast(typesFilePath, m.args.actualExtension, m.args.expectExtension))}"` } else { // prettier-ignore return `${c.bold(fp(m.path))} types is not exported. Consider adding ${c.bold(fp(m.path) + '.types')}: "${c.bold(typesFilePath)}" to be compatible with TypeScript's ${c.bold('"moduleResolution": "bundler"')} compiler option.` } } case 'EXPORTS_TYPES_INVALID_FORMAT': { // convert ['exports', 'types'] -> ['exports', '<condition>', 'types'] // convert ['exports', 'types', 'node'] -> ['exports', 'types', 'node', '<condition>'] const expectPath = m.path.slice() const typesIndex = m.path.findIndex((p) => p === 'types') if (typesIndex === m.path.length - 1) { expectPath.splice(typesIndex, 0, m.args.condition) } else { expectPath.push(m.args.condition) } let additionalMessage = '' // ambiguous default export if (m.args.expectFormat === 'ESM' && m.args.actualFormat === 'CJS') { additionalMessage = `This causes the types to be ambiguous when default importing the package due to its implied interop. ` } // incorrect dynamic import restriction else if (m.args.expectFormat === 'CJS' && m.args.actualFormat === 'ESM') { additionalMessage = `This causes the types to only work when dynamically importing the package, even though the package exports CJS. ` } // prettier-ignore return `${c.bold(fp(m.path))} types is interpreted as ${m.args.actualFormat} when resolving with the "${c.bold(m.args.condition)}" condition. ` + additionalMessage + `Consider splitting out two ${c.bold('"types"')} conditions for ${c.bold('"import"')} and ${c.bold('"require"')}, and use the ${c.yellow(m.args.expectExtension)} extension, ` + `e.g. ${c.bold(fp(expectPath))}: "${c.bold(replaceLast(pv(m.path), m.args.actualExtension, m.args.expectExtension))}"` } case 'FIELD_INVALID_VALUE_TYPE': { let expectStr = m.args.expectTypes[0] for (let i = 1; i < m.args.expectTypes.length; i++) { if (i === m.args.expectTypes.length - 1) { expectStr += ` or ${m.args.expectTypes[i]}` } else { expectStr += `, ${m.args.expectTypes[i]}` } } // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which is an invalid ${c.bold(m.args.actualType)} type. Expected a ${c.bold(expectStr)} type instead.` } case 'EXPORTS_VALUE_CONFLICTS_WITH_BROWSER': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which also matches ${c.bold(fp(m.args.browserPath))}: "${c.bold(pv(m.args.browserPath))}", which overrides the path when building the library with the "${c.bold(m.args.browserishCondition)}" condition. This is usually unintentional and may cause build issues. Consider using a different file name for ${c.bold(pv(m.path))}.` case 'DEPRECATED_FIELD_JSNEXT': // prettier-ignore return `${c.bold(fp(m.path))} is deprecated. ${c.bold('pkg.module')} should be used instead.` case 'INVALID_REPOSITORY_VALUE': switch (m.args.type) { case 'invalid-string-shorthand': // prettier-ignore 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.` case 'invalid-git-url': // prettier-ignore 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')}".` case 'deprecated-github-git-protocol': // prettier-ignore 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://.` case 'shorthand-git-sites': { let fullUrl = pv(m.path) if (fullUrl[fullUrl.length - 1] === '/') { fullUrl = fullUrl.slice(0, -1) } if (!fullUrl.startsWith('git+')) { fullUrl = 'git+' + fullUrl } if (!fullUrl.endsWith('.git')) { fullUrl += '.git' } // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but could be a full git URL like "${c.bold(fullUrl)}".` } } case 'LOCAL_DEPENDENCY': // prettier-ignore return `The "${c.bold(m.path[m.path.length - 1])}" dependency references "${c.bold(pv(m.path))}" that will likely not work when installed by end-users.` case 'BIN_FILE_NOT_EXECUTABLE': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but the file is not executable. It should start with shebang, e.g. ${c.bold('#!/usr/bin/env node')}.` default: return } } /** * Make sure s is an `"exports"` compatible relative path * @param {string} s */ function exportsRel(s) { if (s[0] === '.') return s if (s[0] === '/') return '.' + s return './' + s }