Comparing version 0.1.10 to 0.1.11
@@ -65,2 +65,3 @@ export type MessageType = 'suggestion' | 'warning' | 'error' | ||
| BaseMessage<'USE_EXPORTS_BROWSER'> | ||
| BaseMessage<'TYPES_NOT_EXPORTED', { typesFilePath: string }> | ||
@@ -67,0 +68,0 @@ export interface Options { |
{ | ||
"name": "publint", | ||
"version": "0.1.10", | ||
"version": "0.1.11", | ||
"description": "Lint packaging errors", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -8,3 +8,4 @@ import { | ||
createPromiseQueue, | ||
getPublishedField | ||
getPublishedField, | ||
objectHasKeyNested | ||
} from './utils.js' | ||
@@ -53,11 +54,12 @@ | ||
/** | ||
* @param {string} path | ||
* @param {string[]} pkgPath | ||
* @param {string[]} tryExtensions | ||
* @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, tryExtensions = []) { | ||
async function readFile(path, pkgPath = undefined, tryExtensions = []) { | ||
try { | ||
const content = await vfs.readFile(path) | ||
if (_packedFiles && !_packedFiles.includes(path)) { | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
@@ -70,3 +72,3 @@ } | ||
const content = await vfs.readFile(path + ext) | ||
if (_packedFiles && !_packedFiles.includes(path)) { | ||
if (pkgPath && _packedFiles && !_packedFiles.includes(path)) { | ||
fileNotPublished(pkgPath) | ||
@@ -77,8 +79,10 @@ } | ||
} | ||
messages.push({ | ||
code: 'FILE_DOES_NOT_EXIST', | ||
args: { filePath: path }, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
if (pkgPath) { | ||
messages.push({ | ||
code: 'FILE_DOES_NOT_EXIST', | ||
args: { filePath: path }, | ||
path: pkgPath, | ||
type: 'error' | ||
}) | ||
} | ||
return false | ||
@@ -243,2 +247,4 @@ } | ||
crawlExports(exports, exportsPkgPath) | ||
// make sure types are exported for moduleResolution bundler | ||
doCheckTypesExported() | ||
} else { | ||
@@ -500,2 +506,67 @@ // all files can be accessed. verify them all | ||
} | ||
function doCheckTypesExported() { | ||
if (typeof exports === 'string') { | ||
checkTypesExported() | ||
} else if (typeof exports === 'object') { | ||
const exportsKeys = Object.keys(exports) | ||
if (exportsKeys.length === 0) return | ||
// check if the `exports` directly map to condition keys (doesn't start with '.'). | ||
// if so, we work on it directly. | ||
if (!exportsKeys[0].startsWith('.')) { | ||
checkTypesExported() | ||
} | ||
// else this `exports` may have multiple export entrypoints, check for '.' | ||
// TODO: check for other entrypoints | ||
else if ('.' in exports) { | ||
checkTypesExported('.') | ||
} | ||
} | ||
} | ||
/** | ||
* @param {string | undefined} exportsRootKey | ||
*/ | ||
function checkTypesExported(exportsRootKey = undefined) { | ||
promiseQueue.push(async () => { | ||
const typesFilePath = await findTypesFilePath(exportsRootKey) | ||
const exportsRootValue = exportsRootKey | ||
? exports[exportsRootKey] | ||
: exports | ||
if ( | ||
typesFilePath && // check has existing types? | ||
(typeof exportsRootValue === 'string' || // if the root value is just a string, types is not exported | ||
!objectHasKeyNested(exportsRootValue, 'types')) // else if object, check has types condition | ||
) { | ||
messages.push({ | ||
code: 'TYPES_NOT_EXPORTED', | ||
args: { typesFilePath }, | ||
path: exportsRootKey | ||
? exportsPkgPath.concat(exportsRootKey) | ||
: exportsPkgPath, | ||
type: 'warning' | ||
}) | ||
} | ||
}) | ||
} | ||
/** | ||
* @param {string | undefined} exportsKey | ||
*/ | ||
async function findTypesFilePath(exportsKey) { | ||
let typesFilePath | ||
if (exportsKey == null || exportsKey === '.') { | ||
const [types] = getPublishedField(rootPkg, 'types') | ||
if (types) { | ||
typesFilePath = types | ||
} else if (await readFile(vfs.pathJoin(pkgDir, './index.d.ts'))) { | ||
typesFilePath = './index.d.ts' | ||
} | ||
} else { | ||
// TODO: handle nested exports key | ||
} | ||
return typesFilePath | ||
} | ||
} |
@@ -71,2 +71,12 @@ import c from 'picocolors' | ||
return `${c.bold('pkg.browser')} can be refactored to use ${c.bold('pkg.exports')} and the ${c.bold('browser')} condition instead to declare browser-specific exports. (This will be a breaking change)` | ||
case 'TYPES_NOT_EXPORTED': { | ||
let target = fp(m.path) | ||
if (target.endsWith('.exports')) { | ||
target = 'The library' | ||
} else { | ||
target = c.bold(target) | ||
} | ||
// prettier-ignore | ||
return `${target} has types at ${c.bold(m.args.typesFilePath)} but it is not exported from ${c.bold('pkg.exports')}. Consider adding it to ${c.bold(fp(m.path) + '.types')} to be compatible with TypeScript's ${c.bold('"moduleResolution": "bundler"')} compiler option.` | ||
} | ||
default: | ||
@@ -73,0 +83,0 @@ return |
@@ -240,1 +240,14 @@ /** | ||
} | ||
/** | ||
* @param {Record<string, any>} obj | ||
* @param {string} key | ||
*/ | ||
export function objectHasKeyNested(obj, key) { | ||
for (const k in obj) { | ||
if (k === key) return true | ||
if (typeof obj[k] === 'object' && objectHasKeyNested(obj[k], key)) | ||
return true | ||
} | ||
return false | ||
} |
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
44343
1201