Comparing version 0.2.2 to 0.2.3
@@ -47,2 +47,6 @@ export type MessageType = 'suggestion' | 'warning' | 'error' | ||
> | ||
| BaseMessage< | ||
'FILE_INVALID_JSX_EXTENSION', | ||
{ actualExtension: string; globbedFilePath?: string } | ||
> | ||
| BaseMessage<'FILE_DOES_NOT_EXIST'> | ||
@@ -68,2 +72,3 @@ | BaseMessage<'FILE_NOT_PUBLISHED'> | ||
| BaseMessage<'USE_EXPORTS_OR_IMPORTS_BROWSER'> | ||
| BaseMessage<'USE_FILES'> | ||
| BaseMessage< | ||
@@ -88,2 +93,20 @@ 'TYPES_NOT_EXPORTED', | ||
> | ||
| BaseMessage< | ||
'FIELD_INVALID_VALUE_TYPE', | ||
{ | ||
actualType: string | ||
expectTypes: string[] | ||
} | ||
> | ||
| BaseMessage< | ||
'EXPORTS_VALUE_CONFLICTS_WITH_BROWSER', | ||
{ | ||
/** | ||
* The path to the key inside the `"browser"` field that conflicts with | ||
* the current path `"exports"` field | ||
*/ | ||
browserPath: string[] | ||
browserishCondition: string | ||
} | ||
> | ||
@@ -90,0 +113,0 @@ export interface Options { |
{ | ||
"name": "publint", | ||
"version": "0.2.2", | ||
"version": "0.2.3", | ||
"description": "Lint packaging errors", | ||
@@ -5,0 +5,0 @@ "type": "module", |
230
src/index.js
import { | ||
commonInternalPaths, | ||
invalidJsxExtensions, | ||
knownBrowserishConditions | ||
} from './constants.js' | ||
import { | ||
exportsGlob, | ||
@@ -50,51 +55,17 @@ getCodeFormat, | ||
/** | ||
* @param {string} path file path to read | ||
* @param {string[]} [pkgPath] current path that tries to read this file. | ||
* pass `undefined` to prevent error reporting if the file is missing. | ||
* @param {string[]} tryExtensions list of extensions to try before giving up | ||
* @returns {Promise<string | false>} | ||
*/ | ||
async function readFile(path, pkgPath = undefined, tryExtensions = []) { | ||
try { | ||
const content = await vfs.readFile(path) | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
} | ||
return content | ||
} catch { | ||
for (let ext of tryExtensions) { | ||
// remove duplicated slashes | ||
if (ext[0] === '/' && path[path.length - 1] === '/') { | ||
ext = ext.slice(1) | ||
// Check if package published internal tests or config files | ||
if (rootPkg.files == null) { | ||
promiseQueue.push(async () => { | ||
for (const p of commonInternalPaths) { | ||
const internalPath = vfs.pathJoin(pkgDir, p) | ||
if (await vfs.isPathExist(internalPath)) { | ||
messages.push({ | ||
code: 'USE_FILES', | ||
args: {}, | ||
path: ['name'], | ||
type: 'suggestion' | ||
}) | ||
break | ||
} | ||
try { | ||
const content = await vfs.readFile(path + ext) | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
} | ||
return content | ||
} catch {} | ||
} | ||
if (pkgPath) { | ||
messages.push({ | ||
code: 'FILE_DOES_NOT_EXIST', | ||
args: {}, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
} | ||
return false | ||
} | ||
} | ||
/** | ||
* @param {string[]} pkgPath | ||
*/ | ||
function fileNotPublished(pkgPath) { | ||
messages.push({ | ||
code: 'FILE_NOT_PUBLISHED', | ||
args: {}, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
@@ -106,3 +77,3 @@ } | ||
// LOAD_INDEX(X) | ||
if (!main && !module && !exports) { | ||
if (main == null && module == null && exports == null) { | ||
promiseQueue.push(async () => { | ||
@@ -140,4 +111,5 @@ // check index.js only, others aren't our problem | ||
*/ | ||
if (main) { | ||
if (main != null) { | ||
promiseQueue.push(async () => { | ||
if (!ensureTypeOfField(main, ['string'], mainPkgPath)) return | ||
const mainPath = vfs.pathJoin(pkgDir, main) | ||
@@ -149,2 +121,4 @@ const mainContent = await readFile(mainPath, mainPkgPath, [ | ||
if (mainContent === false) return | ||
if (hasInvalidJsxExtension(main, mainPkgPath)) return | ||
if (!isFilePathLintable(main)) return | ||
const actualFormat = getCodeFormat(mainContent) | ||
@@ -172,3 +146,3 @@ const expectFormat = await getFilePathFormat(mainPath, vfs) | ||
} | ||
if (expectFormat === 'ESM' && !exports) { | ||
if (actualFormat === 'ESM' && exports == null) { | ||
messages.push({ | ||
@@ -190,4 +164,5 @@ code: 'HAS_ESM_MAIN_BUT_NO_EXPORTS', | ||
*/ | ||
if (module) { | ||
if (module != null) { | ||
promiseQueue.push(async () => { | ||
if (!ensureTypeOfField(module, ['string'], modulePkgPath)) return | ||
const modulePath = vfs.pathJoin(pkgDir, module) | ||
@@ -199,2 +174,4 @@ const moduleContent = await readFile(modulePath, modulePkgPath, [ | ||
if (moduleContent === false) return | ||
if (hasInvalidJsxExtension(module, modulePkgPath)) return | ||
if (!isFilePathLintable(module)) return | ||
const actualFormat = getCodeFormat(moduleContent) | ||
@@ -238,3 +215,6 @@ if (actualFormat === 'CJS') { | ||
const [fieldValue, fieldPkgPath] = getPublishedField(rootPkg, field) | ||
if (typeof fieldValue === 'string') { | ||
if ( | ||
fieldValue != null && | ||
ensureTypeOfField(fieldValue, ['string'], fieldPkgPath) | ||
) { | ||
promiseQueue.push(async () => { | ||
@@ -287,2 +267,10 @@ const fieldPath = vfs.pathJoin(pkgDir, fieldValue) | ||
for (const filePath of files) { | ||
if ( | ||
hasInvalidJsxExtension( | ||
filePath, | ||
['name'], | ||
'/' + vfs.pathRelative(pkgDir, filePath) | ||
) | ||
) | ||
continue | ||
if (!isFilePathLintable(filePath)) continue | ||
@@ -360,2 +348,98 @@ pq.push(async () => { | ||
/** | ||
* @param {string} path file path to read | ||
* @param {string[]} [pkgPath] current path that tries to read this file. | ||
* pass `undefined` to prevent error reporting if the file is missing. | ||
* @param {string[]} tryExtensions list of extensions to try before giving up | ||
* @returns {Promise<string | false>} | ||
*/ | ||
async function readFile(path, pkgPath = undefined, tryExtensions = []) { | ||
try { | ||
const content = await vfs.readFile(path) | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
} | ||
return content | ||
} catch { | ||
for (let ext of tryExtensions) { | ||
// remove duplicated slashes | ||
if (ext[0] === '/' && path[path.length - 1] === '/') { | ||
ext = ext.slice(1) | ||
} | ||
try { | ||
const content = await vfs.readFile(path + ext) | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
} | ||
return content | ||
} catch {} | ||
} | ||
if (pkgPath) { | ||
messages.push({ | ||
code: 'FILE_DOES_NOT_EXIST', | ||
args: {}, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
} | ||
return false | ||
} | ||
} | ||
/** | ||
* @param {string[]} pkgPath | ||
*/ | ||
function fileNotPublished(pkgPath) { | ||
messages.push({ | ||
code: 'FILE_NOT_PUBLISHED', | ||
args: {}, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
} | ||
/** | ||
* @param {string} filePath | ||
* @param {string[]} currentPath | ||
* @param {string} [globbedFilePath] only needed for globs | ||
*/ | ||
function hasInvalidJsxExtension(filePath, currentPath, globbedFilePath) { | ||
const matched = invalidJsxExtensions.find((ext) => filePath.endsWith(ext)) | ||
if (matched) { | ||
messages.push({ | ||
code: 'FILE_INVALID_JSX_EXTENSION', | ||
args: { | ||
actualExtension: matched, | ||
globbedFilePath | ||
}, | ||
path: currentPath, | ||
type: 'error' | ||
}) | ||
return true | ||
} | ||
return false | ||
} | ||
/** | ||
* @param {any} fieldValue | ||
* @param {('string' | 'number' | 'boolean' | 'object')[]} expectTypes | ||
* @param {string[]} pkgPath | ||
*/ | ||
function ensureTypeOfField(fieldValue, expectTypes, pkgPath) { | ||
// @ts-expect-error typeof doesn't need to match `expectedTypes` type but TS panics | ||
if (!expectTypes.includes(typeof fieldValue)) { | ||
messages.push({ | ||
code: 'FIELD_INVALID_VALUE_TYPE', | ||
args: { | ||
actualType: typeof fieldValue, | ||
expectTypes | ||
}, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
return false | ||
} | ||
return true | ||
} | ||
/** | ||
* @param {string | Record<string, any>} fieldValue | ||
@@ -457,2 +541,22 @@ * @param {string[]} currentPath | ||
// if the exports value matches a key in `pkg.browser` (meaning it'll be remapped | ||
// if in a browser-ish environment), check if this is a browser-ish environment/condition. | ||
// if so, warn about this conflict as it's often unexpected behaviour. | ||
if (typeof browser === 'object' && exportsValue in browser) { | ||
const browserishCondition = knownBrowserishConditions.find((c) => | ||
currentPath.includes(c) | ||
) | ||
if (browserishCondition) { | ||
messages.push({ | ||
code: 'EXPORTS_VALUE_CONFLICTS_WITH_BROWSER', | ||
args: { | ||
browserPath: browserPkgPath.concat(exportsValue), | ||
browserishCondition | ||
}, | ||
path: currentPath, | ||
type: 'warning' | ||
}) | ||
} | ||
} | ||
const pq = createPromiseQueue() | ||
@@ -462,2 +566,10 @@ | ||
for (const filePath of exportsFiles) { | ||
if ( | ||
hasInvalidJsxExtension( | ||
filePath, | ||
currentPath, | ||
isGlob ? './' + vfs.pathRelative(pkgDir, filePath) : undefined | ||
) | ||
) | ||
return | ||
// TODO: maybe check .ts in the future | ||
@@ -507,8 +619,10 @@ if (!isFilePathLintable(filePath)) continue | ||
// NOTE: only relax this for globbed files, as they're implicitly exported. | ||
const expectFilePath = replaceLast( | ||
filePath, | ||
actualExtension, | ||
expectExtension | ||
) | ||
if (await vfs.isPathExist(expectFilePath)) return | ||
if (isGlob) { | ||
const expectFilePath = replaceLast( | ||
filePath, | ||
actualExtension, | ||
expectExtension | ||
) | ||
if (await vfs.isPathExist(expectFilePath)) return | ||
} | ||
@@ -515,0 +629,0 @@ messages.push({ |
@@ -39,2 +39,12 @@ import c from 'picocolors' | ||
} | ||
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': | ||
@@ -84,2 +94,5 @@ // 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 'TYPES_NOT_EXPORTED': { | ||
@@ -124,2 +137,17 @@ const typesFilePath = exportsRel(m.args.typesFilePath) | ||
} | ||
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))}.` | ||
default: | ||
@@ -126,0 +154,0 @@ return |
@@ -0,1 +1,3 @@ | ||
import { lintableFileExtensions } from './constants.js' | ||
/** | ||
@@ -216,7 +218,3 @@ * @typedef {{ | ||
export function isFilePathLintable(filePath) { | ||
return ( | ||
filePath.endsWith('.js') || | ||
filePath.endsWith('.mjs') || | ||
filePath.endsWith('.cjs') | ||
) | ||
return lintableFileExtensions.some((ext) => filePath.endsWith(ext)) | ||
} | ||
@@ -223,0 +221,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
70051
15
1829