Socket
Socket
Sign inDemoInstall

publint

Package Overview
Dependencies
Maintainers
1
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

publint - npm Package Compare versions

Comparing version 0.1.16 to 0.2.0

27

index.d.ts

@@ -47,3 +47,3 @@ export type MessageType = 'suggestion' | 'warning' | 'error'

>
| BaseMessage<'FILE_DOES_NOT_EXIST', { filePath: string }> // TODO: remove `filePath`
| BaseMessage<'FILE_DOES_NOT_EXIST'>
| BaseMessage<'FILE_NOT_PUBLISHED'>

@@ -67,3 +67,20 @@ | BaseMessage<'MODULE_SHOULD_BE_ESM'>

| BaseMessage<'USE_EXPORTS_BROWSER'>
| BaseMessage<'TYPES_NOT_EXPORTED', { typesFilePath: string }>
| BaseMessage<
'TYPES_NOT_EXPORTED',
{
typesFilePath: string
actualExtension?: string
expectExtension?: string
}
>
| BaseMessage<
'EXPORT_TYPES_INVALID_FORMAT',
{
condition: string
actualFormat: string
expectFormat: string
actualExtension: string
expectExtension: string
}
>

@@ -105,2 +122,6 @@ export interface Options {

export declare function publint(options?: Options): Promise<Message[]>
export interface Result {
messages: Message[]
}
export declare function publint(options?: Options): Promise<Result>

10

lib/cli.js

@@ -10,3 +10,3 @@ #!/usr/bin/env node

import { publint } from './node.js'
import { printMessage } from '../src/message.js'
import { formatMessage } from '../src/message.js'
import { createPromiseQueue } from '../src/utils.js'

@@ -123,3 +123,3 @@

const pkgName = rootPkg.name || path.basename(pkgDir)
const messages = await publint({ pkgDir, level, strict })
const { messages } = await publint({ pkgDir, level, strict })

@@ -131,3 +131,3 @@ if (messages.length) {

suggestions.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
logs.push(c.dim(`${i + 1}. `) + formatMessage(m, rootPkg))
)

@@ -140,3 +140,3 @@ }

warnings.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
logs.push(c.dim(`${i + 1}. `) + formatMessage(m, rootPkg))
)

@@ -149,3 +149,3 @@ }

errors.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
logs.push(c.dim(`${i + 1}. `) + formatMessage(m, rootPkg))
)

@@ -152,0 +152,0 @@ process.exitCode = 1

export { formatMessagePath, getPkgPathValue } from '../src/utils.js'
export { printMessage } from '../src/message.js'
export { formatMessage } from '../src/message.js'

@@ -5,6 +5,6 @@ export { formatMessagePath, getPkgPathValue } from '../src/utils.js'

* no-op. leave users to implement themselves.
* @type {import('../utils.d.ts').printMessage}
* @type {import('../utils.d.ts').formatMessage}
*/
export function printMessage() {
export function formatMessage() {
return
}
{
"name": "publint",
"version": "0.1.16",
"version": "0.2.0",
"description": "Lint packaging errors",

@@ -13,14 +13,10 @@ "type": "module",

"types": "./index.d.ts",
"import": {
"browser": "./lib/browser.js",
"node": "./lib/node.js",
"default": "./src/index.js"
}
"browser": "./lib/browser.js",
"node": "./lib/node.js",
"default": "./src/index.js"
},
"./utils": {
"types": "./utils.d.ts",
"import": {
"node": "./lib/utils-node.js",
"default": "./lib/utils.js"
}
"node": "./lib/utils-node.js",
"default": "./lib/utils.js"
}

@@ -27,0 +23,0 @@ },

@@ -47,3 +47,3 @@ <br>

const messages = await publint({
const { messages } = await publint({
/**

@@ -78,3 +78,3 @@ * Path to your package that contains a package.json file.

```js
import { printMessage } from 'publint/utils'
import { formatMessage } from 'publint/utils'
import fs from 'node:fs/promises'

@@ -89,3 +89,3 @@

// Useful for re-implementing the CLI in a programmatic way.
printMessage(message, pkg)
console.log(formatMessage(message, pkg))
}

@@ -92,0 +92,0 @@ ```

@@ -11,3 +11,8 @@ import {

isFilePathLintable,
isFileContentLintable
isFileContentLintable,
getAdjacentDtsPath,
resolveExports,
isDtsFile,
getDtsFilePathFormat,
getDtsCodeFormatExtension
} from './utils.js'

@@ -26,3 +31,3 @@

* @param {Options} options
* @returns {Promise<import('../index.js').Message[]>}
* @returns {Promise<import('../index.d.ts').Result>}
*/

@@ -39,3 +44,3 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) {

const rootPkgContent = await readFile(rootPkgPath, [])
if (rootPkgContent === false) return messages
if (rootPkgContent === false) return { messages }
const rootPkg = JSON.parse(rootPkgContent)

@@ -73,3 +78,3 @@ const [main, mainPkgPath] = getPublishedField(rootPkg, 'main')

code: 'FILE_DOES_NOT_EXIST',
args: { filePath: path },
args: {},
path: pkgPath,

@@ -308,8 +313,8 @@ type: 'error'

if (level === 'warning') {
return messages.filter((m) => m.type !== 'suggestion')
return { messages: messages.filter((m) => m.type !== 'suggestion') }
} else if (level === 'error') {
return messages.filter((m) => m.type === 'error')
return { messages: messages.filter((m) => m.type === 'error') }
}
return messages
return { messages }

@@ -559,3 +564,3 @@ /**

// else this `exports` may have multiple export entrypoints, check for '.'
// TODO: check for other entrypoints
// TODO: check for other entrypoints, move logic into `crawlExports`
else if ('.' in exports) {

@@ -577,15 +582,104 @@ checkTypesExported('.')

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'
})
// detect if this package intend to ship types
if (typesFilePath) {
const exportsPath = exportsRootKey
? exportsPkgPath.concat(exportsRootKey)
: exportsPkgPath
// keyed strings for seen resolved paths, so we don't trigger duplicate messages for the same thing
const seenResolvedKeys = new Set()
// NOTE: got lazy. here we check for the import/require result in different environments
// to make sure we cover possible cases. however, a better way it to resolve the exports
// and scan also the possible environment conditions, and return an array instead.
for (const env of [undefined, 'node', 'browser', 'worker']) {
for (const format of ['import', 'require']) {
const result = resolveExports(
exportsRootValue,
// @ts-expect-error till this day, ts still doesn't understand `filter(Boolean)`
['types', format, env].filter(Boolean),
exportsPath
)
if (!result) continue
// check if we've seen this resolve before. we also key by format as we want to distinguish
// incorrect exports, but only when the "exports -> path" contains that format, otherwise
// it's intentional fallback behaviour by libraries and we don't want to trigger a false alarm.
// e.g. libraries that only `"exports": "./index.mjs"` means it's ESM only, so we don't key
// the format, so the next run with `"require"` condition is skipped.
// different env can share the same key as code can usually be used for multiple environments.
const seenKey =
result.path.join('.') + (result.dualPublish ? format : '')
if (seenResolvedKeys.has(seenKey)) continue
seenResolvedKeys.add(seenKey)
const resolvedPath = vfs.pathJoin(pkgDir, result.value)
// if path doesn't exist, let the missing file error message take over instead
if (!(await vfs.isPathExist(resolvedPath))) continue
if (isDtsFile(result.value)) {
// if we have resolve to a dts file, it might not be ours because typescript requires
// `.d.mts` and `.d.cts` for esm and cjs (`.js` and nearest type: module behaviour applies).
// check if we're hitting this case :(
const dtsActualFormat = await getDtsFilePathFormat(
resolvedPath,
vfs
)
// get the intended format from the conditions. yes, while the `import` condition can actually
// point to a CJS file, what we're checking here is the intent of the expected format.
// otherwise the package could currently have the wrong format (which we would emit a message above),
// but when we reach here, we don't want to base on that incorrect format.
const dtsExpectFormat = format === 'import' ? 'ESM' : 'CJS'
if (dtsActualFormat !== dtsExpectFormat) {
messages.push({
code: 'EXPORT_TYPES_INVALID_FORMAT',
args: {
condition: format,
actualFormat: dtsActualFormat,
expectFormat: dtsExpectFormat,
actualExtension: vfs.getExtName(result.value),
expectExtension: getDtsCodeFormatExtension(dtsExpectFormat)
},
path: result.path,
type: 'warning'
})
}
} else {
// adjacent dts file here is always in the correct format
const hasAdjacentDtsFile = await vfs.isPathExist(
vfs.pathJoin(pkgDir, getAdjacentDtsPath(result.value))
)
// if there's no adjacent dts file, it's likely they don't support moduleResolution: bundler.
// try to provide a warning.
if (!hasAdjacentDtsFile) {
// before we recommend using `typesFilePath` for this export condition, we need to make sure
// it's of a matching format
const dtsActualFormat = await getDtsFilePathFormat(
vfs.pathJoin(pkgDir, typesFilePath),
vfs
)
const dtsExpectFormat = format === 'import' ? 'ESM' : 'CJS'
// if it's a matching format, we can recommend using the types file for this exports condition too.
// if not, we need to tell them to create a `.d.[mc]ts` file and not use `typesFilePath`.
// this is signalled in `matchingFormat`, where the message handler should check for it.
const isMatchingFormat = dtsActualFormat === dtsExpectFormat
messages.push({
code: 'TYPES_NOT_EXPORTED',
args: {
typesFilePath,
actualExtension: isMatchingFormat
? undefined
: vfs.getExtName(typesFilePath),
expectExtension: isMatchingFormat
? undefined
: getDtsCodeFormatExtension(dtsExpectFormat)
},
path: result.path,
type: 'warning'
})
}
}
}
}
}

@@ -592,0 +686,0 @@ })

@@ -8,3 +8,3 @@ import c from 'picocolors'

*/
export function printMessage(m, pkg) {
export function formatMessage(m, pkg) {
/** @param {string[]} path */

@@ -77,10 +77,26 @@ const pv = (path) => getPkgPathValue(pkg, path)

case 'TYPES_NOT_EXPORTED': {
let target = fp(m.path)
if (target.endsWith('.exports')) {
target = 'The library'
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(typesFilePath.replace(m.args.actualExtension, m.args.expectExtension))}"`
} else {
target = c.bold(target)
// 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 'EXPORT_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)
}
// 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.`
return `${c.bold(fp(m.path))} types is an invalid format when resolving with the "${c.bold(m.args.condition)}" condition. 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(pv(m.path).replace(m.args.actualExtension, m.args.expectExtension))}"`
}

@@ -91,1 +107,11 @@ default:

}
/**
* 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
}

@@ -149,3 +149,3 @@ /**

* @param {import('../index.d.ts').Vfs} vfs
* @returns {Promise<CodeFormat>}
* @returns {Promise<Exclude<CodeFormat, 'mixed' | 'unknown'>>}
*/

@@ -166,2 +166,14 @@ export async function getFilePathFormat(filePath, vfs) {

/**
* @param {string} filePath
* @param {import('../index.d.ts').Vfs} vfs
* @returns {Promise<Exclude<CodeFormat, 'mixed' | 'unknown'>>}
*/
export async function getDtsFilePathFormat(filePath, vfs) {
if (filePath.endsWith('.d.mts')) return 'ESM'
if (filePath.endsWith('.d.cts')) return 'CJS'
const nearestPkg = await getNearestPkg(filePath, vfs)
return nearestPkg?.type === 'module' ? 'ESM' : 'CJS'
}
/**
* @param {CodeFormat} format

@@ -181,2 +193,16 @@ */

/**
* @param {CodeFormat} format
*/
export function getDtsCodeFormatExtension(format) {
switch (format) {
case 'ESM':
return '.mts'
case 'CJS':
return '.cts'
default:
return '.ts'
}
}
/**
* @param {string} path

@@ -304,1 +330,66 @@ */

}
/**
* @param {string} filePath
*/
export function getAdjacentDtsPath(filePath) {
// foo.js -> foo.d.ts
// foo.mjs -> foo.d.mts
// foo.cjs -> foo.d.cts
// foo.jsx -> foo.d.ts
return filePath.replace(/\.([mc]?)jsx?$/, '.d.$1ts')
}
const DTS_RE = /\.d\.[mc]?ts$/
/**
* @param {string} filePath
*/
export function isDtsFile(filePath) {
return filePath.endsWith('.d.ts')
}
/**
* simplified `exports` field resolver that expects `exportsValue` to be the path value directly.
* no path matching will happen. `exportsValue` should be an object that contains only conditions
* and their values, or a string
* @param {Record<string, any> | string | string[]} exportsValue
* @param {string[]} conditions
* @param {string[]} [currentPath] matched conditions while resolving the exports
* @param {{ dualPublish: boolean }} [_metadata]
* @returns {{ value: string, path: string[], dualPublish: boolean } | undefined}
*/
export function resolveExports(
exportsValue,
conditions,
currentPath = [],
_metadata = { dualPublish: false }
) {
if (typeof exportsValue === 'string') {
// prettier-ignore
return { value: exportsValue, path: currentPath, dualPublish: _metadata.dualPublish }
} else if (Array.isArray(exportsValue)) {
// prettier-ignore
return { value: exportsValue[0], path: currentPath, dualPublish: _metadata.dualPublish }
}
// while traversing the exports object, also keep info it the path we're traversing
// intends to dual export. helpful for better logging heuristics.
if (
_metadata.dualPublish === false &&
'import' in exportsValue &&
'require' in exportsValue
) {
_metadata.dualPublish = true
}
for (const key in exportsValue) {
if (conditions.includes(key) || key === 'default') {
return resolveExports(
exportsValue[key],
conditions,
currentPath.concat(key),
_metadata
)
}
}
}

@@ -27,3 +27,2 @@ import fs from 'node:fs'

},
// TODO: Manually create these
pathJoin(...parts) {

@@ -30,0 +29,0 @@ return nodePath.join(...parts)

@@ -1,2 +0,2 @@

import { Message } from './index.js'
import type { Message } from './index.js'

@@ -7,2 +7,5 @@ type Pkg = Record<string, any>

export declare function getPkgPathValue(pkg: Pkg, path: string[]): any
export declare function printMessage(msg: Message, pkg: Pkg): string | undefined
export declare function formatMessage(
msg: Message,
pkg: Pkg
): string | undefined
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc