Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

publint

Package Overview
Dependencies
Maintainers
1
Versions
32
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.5 to 0.1.6

190

lib/cli.js
#!/usr/bin/env node
import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import path from 'node:path'

@@ -10,9 +11,24 @@ import { createRequire } from 'node:module'

import { printMessage } from '../src/message.js'
import { createPromiseQueue } from '../src/utils.js'
const version = createRequire(import.meta.url)('../package.json').version
const cli = sade('publint', false).version(version)
sade('publint [dir]', true)
.version(version)
cli
.command('run [dir]', 'Lint a directory (defaults to current directory)', {
default: true
})
.action(async (dir) => {
const pkgDir = dir ? path.resolve(dir) : process.cwd()
const { logs, success } = await lintDir(pkgDir)
if (!success) process.exitCode = 1
logs.forEach((l) => console.log(l))
})
cli
.command('deps [dir]', 'Lint dependencies declared in package.json')
.option('-P, --prod', 'Only check dependencies')
.option('-D, --dev', 'Only check devDependencies')
.action(async (dir, opts) => {
const pkgDir = dir ? path.resolve(dir) : process.cwd()
const rootPkgContent = await fs

@@ -23,38 +39,152 @@ .readFile(path.join(pkgDir, 'package.json'), 'utf8')

})
if (!rootPkgContent) return
if (!rootPkgContent) {
process.exitCode = 1
return
}
const rootPkg = JSON.parse(rootPkgContent)
const messages = await publint({ pkgDir })
/** @type {string[]} */
const deps = []
if (!opts.dev) deps.push(...Object.keys(rootPkg.dependencies || {}))
if (!opts.prod) deps.push(...Object.keys(rootPkg.devDependencies || {}))
console.log(`${c.bold(rootPkg.name)} lint results:`)
if (deps.length === 0) {
console.log(c.yellow('No dependencies found'))
return
}
if (messages.length) {
const suggestions = messages.filter((v) => v.type === 'suggestion')
if (suggestions.length) {
console.log(c.bold(c.blue('Suggestions:')))
suggestions.forEach((m, i) =>
console.log(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
const pq = createPromiseQueue()
let waitingDepIndex = 0
const waitingDepIndexListeners = []
const listenWaitingDepIndex = (cb) => {
waitingDepIndexListeners.push(cb)
// unlisten
return () => {
const i = waitingDepIndexListeners.indexOf(cb)
if (i > -1) waitingDepIndexListeners.splice(i, 1)
}
}
const warnings = messages.filter((v) => v.type === 'warning')
if (warnings.length) {
console.log(c.bold(c.yellow('Warnings:')))
warnings.forEach((m, i) =>
console.log(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
}
// lint deps in parallel, but log results in order asap
for (let i = 0; i < deps.length; i++) {
pq.push(async () => {
const depDir = await findDepPath(deps[i], pkgDir)
const { logs, success } = await lintDir(depDir, true)
if (!success) process.exitCode = 1
// log this lint result
const log = () => {
logs.forEach((l, j) => console.log((j > 0 ? ' ' : '') + l))
waitingDepIndex++
waitingDepIndexListeners.forEach((cb) => cb())
}
// log when it's our turn so that the results are ordered alphabetically,
// though all deps are linted in parallel
if (waitingDepIndex === i) {
log()
} else {
const unlisten = listenWaitingDepIndex(() => {
if (waitingDepIndex === i) {
log()
unlisten()
}
})
}
})
}
const errors = messages.filter((v) => v.type === 'error')
if (errors.length) {
console.log(c.bold(c.red('Errors:')))
errors.forEach((m, i) =>
console.log(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
}
await pq.wait()
})
process.exitCode = 1
cli.parse(process.argv)
/**
* @param {string} pkgDir
* @param {boolean} [compact]
*/
async function lintDir(pkgDir, compact = false) {
/** @type {string[]} */
const logs = []
const rootPkgContent = await fs
.readFile(path.join(pkgDir, 'package.json'), 'utf8')
.catch(() => {
logs.push(c.red(`Unable to read package.json at ${pkgDir}`))
})
if (!rootPkgContent) return { logs, success: false }
const rootPkg = JSON.parse(rootPkgContent)
const messages = await publint({ pkgDir })
if (messages.length) {
const suggestions = messages.filter((v) => v.type === 'suggestion')
if (suggestions.length) {
logs.push(c.bold(c.blue('Suggestions:')))
suggestions.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
}
const warnings = messages.filter((v) => v.type === 'warning')
if (warnings.length) {
logs.push(c.bold(c.yellow('Warnings:')))
warnings.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
}
const errors = messages.filter((v) => v.type === 'error')
if (errors.length) {
logs.push(c.bold(c.red('Errors:')))
errors.forEach((m, i) =>
logs.push(c.dim(`${i + 1}. `) + printMessage(m, rootPkg))
)
}
if (compact) {
logs.unshift(`${c.red('x')} ${c.bold(rootPkg.name)}`)
} else {
console.log(c.bold(c.green('All good!')))
logs.unshift(`${c.bold(rootPkg.name)} lint results:`)
}
})
.parse(process.argv)
return { logs, success: false }
} else {
if (compact) {
logs.unshift(`${c.green('✓')} ${c.bold(rootPkg.name)}`)
} else {
logs.unshift(`${c.bold(rootPkg.name)} lint results:`)
logs.push(c.bold(c.green('All good!')))
}
return { logs, success: true }
}
}
/** @type {import('pnpapi')} */
let pnp
if (process.versions.pnp) {
try {
const { createRequire } = (await import('module')).default
pnp = createRequire(import.meta.url)('pnpapi')
} catch {}
}
/**
*
* @param {string} dep
* @param {string} parent
* @returns
*/
async function findDepPath(dep, parent) {
if (pnp) {
const depRoot = pnp.resolveToUnqualified(dep, parent)
if (!depRoot) return undefined
} else {
const depRoot = path.join(parent, 'node_modules', dep)
try {
await fs.access(depRoot)
return fsSync.realpathSync(depRoot)
} catch {
return undefined
}
}
}

2

lib/index.d.ts

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

// TODO: Check nodejs modules usage
export type Message =

@@ -64,2 +63,3 @@ | BaseMessage<

| BaseMessage<'EXPORTS_MODULE_SHOULD_BE_ESM'>
| BaseMessage<'EXPORTS_VALUE_INVALID', { suggestValue: string }>
| BaseMessage<'USE_EXPORTS_BROWSER'>

@@ -66,0 +66,0 @@

{
"name": "publint",
"version": "0.1.5",
"version": "0.1.6",
"description": "Lint packaging errors",

@@ -32,3 +32,3 @@ "type": "module",

"funding": "https://bjornlu.com/sponsor",
"homepage": "https://publint.bjornlu.com",
"homepage": "https://publint.dev",
"repository": {

@@ -35,0 +35,0 @@ "type": "git",

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

<p align="center">
<a href="https://publint.bjornlu.com">
<a href="https://publint.dev">
<strong>Try it online</strong>

@@ -24,3 +24,3 @@ </a>

This package contains a CLI and API to lint packages locally. The package to be linted must exist and be built locally for the lint to succeed. To test other npm packages, try https://publint.bjornlu.com.
This package contains a CLI and API to lint packages locally. The package to be linted must exist and be built locally for the lint to succeed. To test other npm packages, try https://publint.dev.

@@ -32,12 +32,14 @@ ## Usage

```bash
$ publint --help
# Lint your library project
$ npx publint
Usage
$ publint [dir] [options]
# Lint a dependency
$ npx publint ./node_modules/some-lib
Options
-v, --version Displays current version
-h, --help Displays this message
# Lint your project's dependencies based on package.json
$ npx publint deps
```
Use `npx publint --help` for more information.
### API

@@ -53,3 +55,3 @@

*/
pkgDir: './path/to/package'
pkgDir: './path/to/package',
/**

@@ -56,0 +58,0 @@ * A virtual file-system object that handles fs/path operations.

@@ -6,3 +6,4 @@ import {

getCodeFormatExtension,
isExplicitExtension
isExplicitExtension,
createPromiseQueue
} from './utils.js'

@@ -262,10 +263,2 @@

function createPromiseQueue() {
const promises = []
return {
push: (fn) => promises.push(fn()),
wait: () => Promise.all(promises)
}
}
/**

@@ -304,2 +297,3 @@ * @param {string | Record<string, any>} fieldValue

promiseQueue.push(async () => {
// warn deprecated subpath mapping
// https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings

@@ -323,2 +317,14 @@ if (exports.endsWith('/')) {

// error incorrect exports value
if (!exports.startsWith('./')) {
messages.push({
code: 'EXPORTS_VALUE_INVALID',
args: {
suggestValue: './' + exports.replace(/^[\/]+/, '')
},
path: currentPath,
type: 'error'
})
}
const isGlob = exports.includes('*')

@@ -394,3 +400,5 @@ const exportsFiles = await getExportsFiles(exports)

})
} else {
}
// `exports` could be null to disallow exports of globs from another key
else if (exports) {
const exportsKeys = Object.keys(exports)

@@ -397,0 +405,0 @@

@@ -19,4 +19,8 @@ import c from 'picocolors'

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 `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} and 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(relativePath.replace('.js', m.args.expectExtension))}`
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(relativePath.replace('.js', m.args.expectExtension))}`
}

@@ -26,4 +30,8 @@ case 'FILE_INVALID_EXPLICIT_FORMAT': {

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 `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} which 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(relativePath.replace(m.args.actualExtension, m.args.expectExtension))}`
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(relativePath.replace(m.args.actualExtension, m.args.expectExtension))}`
}

@@ -57,2 +65,5 @@ case 'FILE_DOES_NOT_EXIST':

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 'USE_EXPORTS_BROWSER':

@@ -59,0 +70,0 @@ // prettier-ignore

@@ -179,1 +179,9 @@ /**

}
export function createPromiseQueue() {
const promises = []
return {
push: (fn) => promises.push(fn()),
wait: () => Promise.all(promises)
}
}

Sorry, the diff of this file is not supported yet

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