diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 107bb19..e49ffec 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -24,7 +24,8 @@ jobs: with: no-lockfile: true npm-test-script: 'test-ci' - node-versions: '14,16,18,19' + # Node 16 has a SegFault from C8 when using --test + node-versions: '18,19' # We currently have some issues on Windows that will have to wait to be fixed # os: 'ubuntu-latest,windows-latest' os: 'ubuntu-latest' diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a209a01 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +"prettier-config-x-standard" \ No newline at end of file diff --git a/lib/commands/login/index.js b/lib/commands/login/index.js index 19c0d2c..ebfb595 100644 --- a/lib/commands/login/index.js +++ b/lib/commands/login/index.js @@ -43,6 +43,9 @@ export const login = { if (!isInteractive()) { throw new InputError('cannot prompt for credentials in a non-interactive shell') } + /** + * @type {{ apiKey: string }} + */ const result = await prompts({ type: 'password', name: 'apiKey', @@ -91,6 +94,9 @@ export const login = { let enforcedOrgs = [] if (enforcedChoices.length > 1) { + /** + * @type { {id: string} } + */ const { id } = await prompts({ type: 'select', name: 'id', @@ -104,6 +110,9 @@ export const login = { }) if (id) enforcedOrgs = [id] } else if (enforcedChoices.length) { + /** + * @type { {confirmOrg: boolean} } + */ const { confirmOrg } = await prompts({ type: 'confirm', name: 'confirmOrg', @@ -112,7 +121,10 @@ export const login = { onState: promptAbortHandler }) if (confirmOrg) { - enforcedOrgs = [enforcedChoices[0]?.value] + const existing = /** @type {undefined | {value: string}} */(enforcedChoices[0]) + if (existing) { + enforcedOrgs = [existing.value] + } } } // MUST DO all updateSetting ON SAME TICK TO AVOID PARTIAL WRITE diff --git a/lib/commands/report/create.js b/lib/commands/report/create.js index 6797d60..e0dca88 100644 --- a/lib/commands/report/create.js +++ b/lib/commands/report/create.js @@ -118,13 +118,13 @@ async function setupCommand (name, description, argv, importMeta) { Options ${printFlagList({ - '--all': 'Include all issues', - '--debug': 'Output debug information', - '--dry-run': 'Only output what will be done without actually doing it', - '--json': 'Output result as json', - '--markdown': 'Output result as markdown', - '--strict': 'Exits with an error code if any matching issues are found', - '--view': 'Will wait for and return the created report' + 'all': 'Include all issues', + 'debug': 'Output debug information', + 'dry-run': 'Only output what will be done without actually doing it', + 'json': 'Output result as json', + 'markdown': 'Output result as markdown', + 'strict': 'Exits with an error code if any matching issues are found', + 'view': 'Will wait for and return the created report' }, 6)} Examples @@ -185,9 +185,11 @@ async function setupCommand (name, description, argv, importMeta) { .then(res => { if (!res.success) handleUnsuccessfulApiResponse('getReportSupportedFiles', res, ora()) return res.data - }).catch(cause => { - throw new ErrorWithCause('Failed getting supported files for report', { cause }) - }) + }).catch( + /** @type {(cause: Error) => never} */ + (cause) => { + throw new ErrorWithCause('Failed getting supported files for report', { cause }) + }) const packagePaths = await getPackageFiles(cwd, cli.input, config, supportedFiles, debugLog) diff --git a/lib/shadow/npm-injection.cjs b/lib/shadow/npm-injection.cjs index 252ff8b..51ffcf6 100644 --- a/lib/shadow/npm-injection.cjs +++ b/lib/shadow/npm-injection.cjs @@ -9,13 +9,17 @@ const path = require('path') const rl = require('readline') const { PassThrough } = require('stream') +const config = require('@socketsecurity/config') + const oraPromise = import('ora') const isInteractivePromise = import('is-interactive') const chalkPromise = import('chalk') const chalkMarkdownPromise = import('../utils/chalk-markdown.js') const settingsPromise = import('../utils/settings.js') const sdkPromise = import('../utils/sdk.js') -const ipc_version = require('../../package.json').version +const createTTYServer = require('./tty-server.cjs') +const { createIssueUXLookup } = require('../utils/issue-rules.cjs') +const { isErrnoException } = require('../utils/type-helpers.cjs') try { // due to update-notifier pkg being ESM only we actually spawn a subprocess sadly @@ -37,19 +41,25 @@ try { const pubTokenPromise = sdkPromise.then(({ getDefaultKey, FREE_API_KEY }) => getDefaultKey() || FREE_API_KEY) const apiKeySettingsPromise = sdkPromise.then(async ({ setupSdk }) => { - const sdk = await setupSdk() + const sdk = await setupSdk(await pubTokenPromise) const orgResult = await sdk.getOrganizations() if (!orgResult.success) { - throw new Error('Failed to fetch organizations info: ' + orgResult.error.message) + throw new Error('Failed to fetch Socket organization info: ' + orgResult.error.message) } - const orgs = Object.values(orgResult.data.organizations) - const settingsSelectors = [] - for (const org of orgs) { + /** + * @type {(Exclude)[]} + */ + const orgs = [] + for (const org of Object.values(orgResult.data.organizations)) { if (org) { - settingsSelectors.push({ organization: org.id }) + orgs.push(org) } } - const result = await sdk.postSettings(settingsSelectors) + const result = await sdk.postSettings(orgs.map(org => { + return { + organization: org.id + } + })) if (!result.success) { throw new Error('Failed to fetch API key settings: ' + result.error.message) } @@ -59,91 +69,88 @@ const apiKeySettingsPromise = sdkPromise.then(async ({ setupSdk }) => { } }) -/** @type {Promise<{ defaultRules: import('../utils/settings.js').IssueRules, orgRules: { id: string, issueRules: import('../utils/settings.js').IssueRules }[] }>} */ -const orgSettingsPromise = settingsPromise.then(async ({ getSetting }) => { - const enforcedOrgs = getSetting('enforcedOrgs') - const { orgs, settings } = await apiKeySettingsPromise - - /** - * @param {import('../utils/settings.js').IssueRules[string]} rule - * @returns {number} - */ - const ruleStrength = (rule) => { - if (typeof rule === 'boolean') return rule ? 3 : 1 - switch (rule.action) { - case 'error': return 3 - case 'warn': return 2 - case 'ignore': return 1 - case 'defer': return 0 - } - } - - /** - * - * @param {import('../utils/settings.js').IssueRules} a - * @param {import('../utils/settings.js').IssueRules} b - * @returns {import('../utils/settings.js').IssueRules} - */ - const mergeRules = (a, b) => { - const merged = { ...a } - for (const rule in b) { - if ( - !merged[rule] || - ruleStrength(b[rule]) > ruleStrength(merged[rule]) - ) { - merged[rule] = b[rule] +/** + * + */ +async function findSocketYML () { + let prevDir = null + let dir = process.cwd() + const fs = require('fs/promises') + while (dir !== prevDir) { + const ymlPath = path.join(dir, 'socket.yml') + // mark as handled + const yml = fs.readFile(ymlPath, 'utf-8').catch(() => {}) + const yamlPath = path.join(dir, 'socket.yaml') + // mark as handled + const yaml = fs.readFile(yamlPath, 'utf-8').catch(() => {}) + try { + const txt = await yml + if (txt != null) { + return { + path: ymlPath, + parsed: config.parseSocketConfig(txt) + } + } + } catch (e) { + if (isErrnoException(e)) { + if (e.code !== 'ENOENT' && e.code !== 'EISDIR') { + throw e + } + } else { + throw new Error('Found file but was unable to parse ' + ymlPath) } } - return merged - } - - const mergeDefaults = (a, b) => { - const merged = { ...a } - for (const rule in b) { - const defaultedRule = merged[rule] - if ( - !(rule in merged) || ( - typeof defaultedRule === 'object' && - defaultedRule.action === 'defer' - )) { - merged[rule] = b[rule] + try { + const txt = await yaml + if (txt != null) { + return { + path: yamlPath, + parsed: config.parseSocketConfig(txt) + } + } + } catch (e) { + if (isErrnoException(e)) { + if (e.code !== 'ENOENT' && e.code !== 'EISDIR') { + throw e } + } else { + throw new Error('Found file but was unable to parse ' + yamlPath) + } } - return merged + prevDir = dir + dir = path.join(dir, '..') } + return null +} + +/** + * @type {Promise>} + */ +const uxLookupPromise = settingsPromise.then(async ({ getSetting }) => { + const enforcedOrgs = getSetting('enforcedOrgs') ?? [] + const { orgs, settings } = await apiKeySettingsPromise - /** @type {Record} */ - const baseOrgRules = {} - for (let i = 0; i < orgs.length; ++i) { - const orgID = orgs[i].id - const entry = settings.entries[i] - /** @type {import('../utils/settings.js').IssueRules} */ - let issueRules = {} - let target = entry.start - while (target !== null) { - issueRules = mergeDefaults(issueRules, entry.settings[target].issueRules) - target = entry.settings[target].deferTo + // remove any organizations not being enforced + for (const [i, org] of orgs.entries()) { + if (!enforcedOrgs.includes(org.id)) { + settings.entries.splice(i, 1) } - baseOrgRules[orgID] = issueRules } - const defaults = settings.defaults.issueRules - - const enforcedRules = enforcedOrgs - .map(org => baseOrgRules[org]) - .filter(rules => rules) - .reduce((a, b) => mergeRules(a, b), {}) - - return { - defaultRules: mergeDefaults(enforcedRules, defaults), - orgRules: orgs.map(({ id, name }) => { - return { - id, - name, - issueRules: mergeDefaults(mergeRules(baseOrgRules[id], enforcedRules), defaults) + const socketYml = await findSocketYML() + if (socketYml) { + settings.entries.push({ + start: socketYml.path, + // @ts-ignore + settings: { + [socketYml.path]: { + deferTo: null, + issueRules: socketYml.parsed.issueRules + } } }) } + return createIssueUXLookup(settings) }) // shadow `npm` and `npx` to mitigate subshells @@ -218,8 +225,10 @@ let translations = null */ let formatter = null -const ttyServerPromise = chalkPromise.then(chalk => { - return createTTYServer(chalk.default.level) +const ttyServerPromise = chalkPromise.then(async (chalk) => { + return createTTYServer(chalk.default.level, (await isInteractivePromise).default({ + stream: process.stdin + }), npmlog) }) const npmEntrypoint = fs.realpathSync(`${process.argv[1]}`) @@ -240,6 +249,10 @@ function findRoot (filepath) { const npmDir = findRoot(path.dirname(npmEntrypoint)) const arboristLibClassPath = path.join(npmDir, 'node_modules', '@npmcli', 'arborist', 'lib', 'arborist', 'index.js') const npmlog = require(path.join(npmDir, 'node_modules', 'npmlog', 'lib', 'log.js')) +/** + * @type {import('pacote')} + */ +const pacote = require(path.join(npmDir, 'node_modules', 'pacote')) /** * @type {typeof import('@npmcli/arborist')} @@ -289,10 +302,10 @@ class SafeArborist extends Arborist { } args[0] ??= {} const old = { - ...args[0], dryRun: false, save: Boolean(args[0].save ?? true), - saveBundle: Boolean(args[0].saveBundle ?? false) + saveBundle: Boolean(args[0].saveBundle ?? false), + ...args[0] } // @ts-expect-error types are wrong args[0].dryRun = true @@ -312,7 +325,7 @@ class SafeArborist extends Arborist { } const ttyServer = await ttyServerPromise const proceed = await ttyServer.captureTTY(async (input, output, colorLevel) => { - if (input) { + if (input && output) { const chalkNS = await chalkPromise chalkNS.default.level = colorLevel const oraNS = await oraPromise @@ -327,7 +340,7 @@ class SafeArborist extends Arborist { spinner: oraNS.spinners.dots, }) } - const risky = await packagesHaveRiskyIssues(this.registry, diff, ora, input, output) + const risky = await packagesHaveRiskyIssues(this, this.registry, diff, ora, input, output) if (!risky) { return true } @@ -359,11 +372,12 @@ class SafeArborist extends Arborist { rli.close() } } else { - if (await packagesHaveRiskyIssues(this.registry, diff, null, null, output)) { - throw new Error('Socket npm unable to prompt to accept risk, need TTY to do so') + if (await packagesHaveRiskyIssues(this, this.registry, diff, null, null, output)) { + throw new Error('Socket npm Unable to prompt to accept risk, need TTY to do so') } return true } + // @ts-ignore paranoia // eslint-disable-next-line return false }) @@ -469,14 +483,15 @@ function walk (diff, needInfoOn = []) { } /** - * @param {string} registry + * @param {SafeArborist} safeArb + * @param {string} _registry * @param {InstallEffect[]} pkgs * @param {import('ora')['default'] | null} ora - * @param {Readable | null} input - * @param {Writable} output + * @param {Readable | null} [_input] + * @param {Writable | null} [output] * @returns {Promise} */ -async function packagesHaveRiskyIssues (registry, pkgs, ora = null, input, output) { +async function packagesHaveRiskyIssues (safeArb, _registry, pkgs, ora = null, _input, output) { let failed = false if (pkgs.length) { let remaining = pkgs.length @@ -490,75 +505,81 @@ async function packagesHaveRiskyIssues (registry, pkgs, ora = null, input, outpu const spinner = ora ? ora().start(getText()) : null const pkgDatas = [] try { - const orgSettings = await orgSettingsPromise - if (orgSettings.orgRules.length > 1) { - throw new Error('multi-organization API keys not supported') - } - // TODO: determine org based on cwd - const rules = orgSettings.orgRules.length - ? orgSettings.orgRules[0].issueRules - : orgSettings.defaultRules + // TODO: determine org based on cwd, pass in + const uxLookup = await uxLookupPromise for await (const pkgData of batchScan(pkgs.map(pkg => pkg.pkgid))) { + /** + * @type {Array} + */ let failures = [] - let warns = [] + let displayWarning = false + const name = pkgData.pkg + const version = pkgData.ver + let blocked = false if (pkgData.type === 'missing') { + failed = true failures.push({ type: 'missingDependency' }) continue - } - for (const issue of (pkgData.value?.issues ?? [])) { - if (rules[issue.type]) { - if ((typeof rules[issue.type] === 'boolean' && rules[issue.type]) || rules[issue.type].action === 'error') { - failures.push(issue) - } else if (rules[issue.type].action === 'warn') { - warns.push(issue) + } else { + for (const failure of pkgData.value.issues) { + const ux = await uxLookup({ package: { name, version }, issue: { type: failure.type } }) + if (ux.display || ux.block) { + failures.push({ raw: failure, block: ux.block }) + // before we ask about problematic issues, check to see if they already existed in the old version + // if they did, be quiet + const pkg = pkgs.find(pkg => pkg.pkgid === `${pkgData.pkg}@${pkgData.ver}` && pkg.existing?.startsWith(pkgData.pkg + '@')) + if (pkg?.existing) { + for await (const oldPkgData of batchScan([pkg.existing])) { + if (oldPkgData.type === 'success') { + failures = failures.filter( + issue => oldPkgData.value.issues.find(oldIssue => oldIssue.type === issue.raw.type) == null + ) + } + } + } + } + if (ux.block) { + failed = true + blocked = true + } + if (ux.display) { + displayWarning = true } } } - // before we ask about problematic issues, check to see if they already existed in the old version - // if they did, be quiet - if (failures.length || warns.length) { - const pkg = pkgs.find(pkg => pkg.pkgid === `${pkgData.pkg}@${pkgData.ver}` && pkg.existing?.startsWith(pkgData.pkg)) - if (pkg?.existing) { - for await (const oldPkgData of batchScan([pkg.existing])) { - if (oldPkgData.type === 'success') { - const issueFilter = issue => oldPkgData.value.issues.find(oldIssue => oldIssue.type === issue.type) == null - failures = failures.filter(issueFilter) - warns = warns.filter(issueFilter) - } - } + if (!blocked) { + const pkg = pkgs.find(pkg => pkg.pkgid === `${pkgData.pkg}@${pkgData.ver}`) + if (pkg) { + pacote.tarball.stream(pkg.pkgid, (stream) => { + stream.resume() + // @ts-ignore pacote does a naughty + return stream.promise() + }, { ...safeArb[kCtorArgs][0] }) } } - if (failures.length || warns.length) { - failed ||= failures.length > 0 - spinner?.stop() + if (displayWarning) { translations ??= JSON.parse(fs.readFileSync(path.join(__dirname, '/translations.json'), 'utf-8')) formatter ??= new ((await chalkMarkdownPromise).ChalkOrMarkdown)(false) - const name = pkgData.pkg - const version = pkgData.ver - output.write(`(socket) ${formatter.hyperlink(`${name}@${version}`, `https://socket.dev/npm/package/${name}/overview/${version}`)} contains risks:\n`) - if (translations) { - for (const failure of failures) { + spinner?.stop() + output?.write(`(socket) ${formatter.hyperlink(`${name}@${version}`, `https://socket.dev/npm/package/${name}/overview/${version}`)} contains risks:\n`) + const lines = new Set() + for (const failure of failures.sort((a, b) => a.raw.type < b.raw.type ? -1 : 1)) { + const type = failure.raw.type + if (type) { // @ts-ignore - const issueTypeTranslation = translations.issues[failure.type] + const issueTypeTranslation = translations.issues[type] // TODO: emoji seems to misalign terminals sometimes // @ts-ignore - const msg = ` ${formatter.bold(issueTypeTranslation.title)} - ${issueTypeTranslation.description}\n` - output.write(msg) - } - for (const warn of warns) { - // @ts-ignore - const issueTypeTranslation = translations.issues[warn.type] - // @ts-ignore - const msg = ` ${issueTypeTranslation.title} - ${issueTypeTranslation.description}\n` - output.write(msg) + lines.add(` ${issueTypeTranslation?.title ?? type}${failure.block ? '' : ' (non-blocking)'} - ${issueTypeTranslation?.description ?? ''}\n`) } } + for (const line of lines) { + output?.write(line) + } spinner?.start() - } else { - // TODO: have pacote/cacache download non-problematic files while waiting } remaining-- if (remaining !== 0) { @@ -581,195 +602,3 @@ async function packagesHaveRiskyIssues (registry, pkgs, ora = null, input, outpu return false } } - -/** - * @param {import('chalk')['default']['level']} colorLevel - * @returns {Promise<{ captureTTY(mutexFn: (input: Readable | null, output: Writable, colorLevel: import('chalk')['default']['level']) => Promise): Promise }>} - */ -async function createTTYServer (colorLevel) { - const TTY_IPC = process.env.SOCKET_SECURITY_TTY_IPC - const net = require('net') - /** - * @type {import('readline')} - */ - let readline - const isSTDINInteractive = (await isInteractivePromise).default({ - stream: process.stdin - }) - if (!isSTDINInteractive && TTY_IPC) { - return { - async captureTTY (mutexFn) { - return new Promise((resolve, reject) => { - const conn = net.createConnection({ - path: TTY_IPC - }).on('error', reject) - let captured = false - const bufs = [] - conn.on('data', function awaitCapture (chunk) { - bufs.push(chunk) - let lineBuff = Buffer.concat(bufs) - try { - if (!captured) { - const EOL = lineBuff.indexOf('\n'.charCodeAt(0)) - if (EOL !== -1) { - conn.removeListener('data', awaitCapture) - conn.push(lineBuff.slice(EOL + 1)) - lineBuff = null - captured = true - const { - ipc_version: remote_ipc_version, - capabilities: { input: hasInput, output: hasOutput, colorLevel: ipcColorLevel } - } = JSON.parse(lineBuff.slice(0, EOL).toString('utf-8')) - if (remote_ipc_version !== ipc_version) { - throw new Error('Mismatched STDIO tunnel IPC version, ensure you only have 1 version of socket CLI being called.') - } - const input = hasInput ? new PassThrough() : null - input.pause() - conn.pipe(input) - const output = hasOutput ? new PassThrough() : null - output.pipe(conn) - // make ora happy - // @ts-ignore - output.isTTY = true - // @ts-ignore - output.cursorTo = function cursorTo (x, y, callback) { - readline = readline || require('readline') - readline.cursorTo(this, x, y, callback) - } - // @ts-ignore - output.clearLine = function clearLine (dir, callback) { - readline = readline || require('readline') - readline.clearLine(this, dir, callback) - } - mutexFn(hasInput ? input : null, hasOutput ? output : null, ipcColorLevel) - .then(resolve, reject) - .finally(() => { - conn.unref() - conn.end() - input.end() - output.end() - // process.exit(13) - }) - } - } - } catch (e) { - reject(e) - } - }) - }) - } - } - } - const pendingCaptures = [] - let captured = false - const sock = path.join(require('os').tmpdir(), `socket-security-tty-${process.pid}.sock`) - process.env.SOCKET_SECURITY_TTY_IPC = sock - try { - await require('fs/promises').unlink(sock) - } catch (e) { - if (e.code !== 'ENOENT') { - throw e - } - } - process.on('exit', () => { - ttyServer.close() - try { - require('fs').unlinkSync(sock) - } catch (e) { - if (e.code !== 'ENOENT') { - throw e - } - } - }) - const input = isSTDINInteractive ? process.stdin : null - const output = process.stderr - const ttyServer = await new Promise((resolve, reject) => { - const server = net.createServer(async (conn) => { - if (captured) { - const captured = new Promise((resolve) => { - pendingCaptures.push({ - resolve - }) - }) - await captured - } else { - captured = true - } - const wasProgressEnabled = npmlog.progressEnabled - npmlog.pause() - if (wasProgressEnabled) { - npmlog.disableProgress() - } - conn.write(`${JSON.stringify({ - ipc_version, - capabilities: { - input: Boolean(input), - output: true, - colorLevel - } - })}\n`) - conn.on('data', (data) => { - output.write(data) - }) - conn.on('error', (e) => { - output.write(`there was an error prompting from a subshell (${e.message}), socket npm closing`) - process.exit(1) - }) - input.on('data', (data) => { - conn.write(data) - }) - input.on('end', () => { - conn.unref() - conn.end() - if (wasProgressEnabled) { - npmlog.enableProgress() - } - npmlog.resume() - nextCapture() - }) - }).listen(sock, (err) => { - if (err) reject(err) - else resolve(server) - }).unref() - }) - /** - * - */ - function nextCapture () { - if (pendingCaptures.length > 0) { - const nextCapture = pendingCaptures.shift() - nextCapture.resolve() - } else { - captured = false - } - } - return { - async captureTTY (mutexFn) { - if (captured) { - const captured = new Promise((resolve) => { - pendingCaptures.push({ - resolve - }) - }) - await captured - } else { - captured = true - } - const wasProgressEnabled = npmlog.progressEnabled - try { - npmlog.pause() - if (wasProgressEnabled) { - npmlog.disableProgress() - } - // need await here for proper finally timing - return await mutexFn(input, output, colorLevel) - } finally { - if (wasProgressEnabled) { - npmlog.enableProgress() - } - npmlog.resume() - nextCapture() - } - } - } -} diff --git a/lib/shadow/tty-server.cjs b/lib/shadow/tty-server.cjs new file mode 100644 index 0000000..4769600 --- /dev/null +++ b/lib/shadow/tty-server.cjs @@ -0,0 +1,221 @@ +const path = require('path') +const { PassThrough } = require('stream') +const { isErrnoException } = require('../utils/type-helpers.cjs') +const ipc_version = require('../../package.json').version + +/** + * @typedef {import('stream').Readable} Readable + */ +/** + * @typedef {import('stream').Writable} Writable + */ +/** + * @param {import('chalk')['default']['level']} colorLevel + * @param {boolean} isInteractive + * @param {any} npmlog + * @returns {Promise<{ captureTTY(mutexFn: (input: Readable | null, output?: Writable, colorLevel: import('chalk')['default']['level']) => Promise): Promise }>} + */ +module.exports = async function createTTYServer (colorLevel, isInteractive, npmlog) { + const TTY_IPC = process.env['SOCKET_SECURITY_TTY_IPC'] + const net = require('net') + /** + * @type {import('readline')} + */ + let readline + const isSTDINInteractive = true || isInteractive + if (!isSTDINInteractive && TTY_IPC) { + return { + async captureTTY (mutexFn) { + return new Promise((resolve, reject) => { + const conn = net.createConnection({ + path: TTY_IPC + }).on('error', reject) + let captured = false + /** + * @type {Array} + */ + const bufs = [] + conn.on('data', function awaitCapture (chunk) { + bufs.push(chunk) + /** + * @type {Buffer | null} + */ + let lineBuff = Buffer.concat(bufs) + try { + if (!captured) { + const EOL = lineBuff.indexOf('\n'.charCodeAt(0)) + if (EOL !== -1) { + conn.removeListener('data', awaitCapture) + conn.push(lineBuff.slice(EOL + 1)) + const { + ipc_version: remote_ipc_version, + capabilities: { input: hasInput, output: hasOutput, colorLevel: ipcColorLevel } + } = JSON.parse(lineBuff.slice(0, EOL).toString('utf-8')) + lineBuff = null + captured = true + if (remote_ipc_version !== ipc_version) { + throw new Error('Mismatched STDIO tunnel IPC version, ensure you only have 1 version of socket CLI being called.') + } + const input = hasInput ? new PassThrough() : null + input?.pause() + if (input) conn.pipe(input) + const output = hasOutput ? new PassThrough() : null + output?.pipe(conn) + // make ora happy + // @ts-ignore + output.isTTY = true + // @ts-ignore + output.cursorTo = function cursorTo (x, y, callback) { + readline = readline || require('readline') + // @ts-ignore + readline.cursorTo(this, x, y, callback) + } + // @ts-ignore + output.clearLine = function clearLine (dir, callback) { + readline = readline || require('readline') + // @ts-ignore + readline.clearLine(this, dir, callback) + } + mutexFn(hasInput ? input : null, hasOutput ? /** @type {Writable} */(output) : undefined, ipcColorLevel) + .then(resolve, reject) + .finally(() => { + conn.unref() + conn.end() + input?.end() + output?.end() + // process.exit(13) + }) + } + } + } catch (e) { + reject(e) + } + }) + }) + } + } + } + /** + * @type {Array<{resolve(): void}>}} + */ + const pendingCaptures = [] + let captured = false + const sock = path.join(require('os').tmpdir(), `socket-security-tty-${process.pid}.sock`) + process.env['SOCKET_SECURITY_TTY_IPC'] = sock + try { + await require('fs/promises').unlink(sock) + } catch (e) { + if (isErrnoException(e) && e.code !== 'ENOENT') { + throw e + } + } + const input = isSTDINInteractive ? process.stdin : null + const output = process.stderr + if (input) { + await new Promise((resolve, reject) => { + const server = net.createServer(async (conn) => { + if (captured) { + const captured = new Promise((resolve) => { + pendingCaptures.push({ + resolve () { + resolve(undefined) + } + }) + }) + await captured + } else { + captured = true + } + const wasProgressEnabled = npmlog.progressEnabled + npmlog.pause() + if (wasProgressEnabled) { + npmlog.disableProgress() + } + conn.write(`${JSON.stringify({ + ipc_version, + capabilities: { + input: Boolean(input), + output: true, + colorLevel + } + })}\n`) + conn.on('data', (data) => { + output.write(data) + }) + conn.on('error', (e) => { + output.write(`there was an error prompting from a subshell (${e.message}), socket npm closing`) + process.exit(1) + }) + input.on('data', (data) => { + conn.write(data) + }) + input.on('end', () => { + conn.unref() + conn.end() + if (wasProgressEnabled) { + npmlog.enableProgress() + } + npmlog.resume() + nextCapture() + }) + }).listen(sock, () => resolve(server)).on('error', (err) => { + reject(err) + }).unref() + process.on('exit', () => { + server.close() + try { + require('fs').unlinkSync(sock) + } catch (e) { + if (isErrnoException(e) && e.code !== 'ENOENT') { + throw e + } + } + }) + resolve(server) + }) + } + /** + * + */ + function nextCapture () { + if (pendingCaptures.length > 0) { + const nextCapture = pendingCaptures.shift() + if (nextCapture) { + nextCapture.resolve() + } + } else { + captured = false + } + } + return { + async captureTTY (mutexFn) { + if (captured) { + const captured = new Promise((resolve) => { + pendingCaptures.push({ + resolve () { + resolve(undefined) + } + }) + }) + await captured + } else { + captured = true + } + const wasProgressEnabled = npmlog.progressEnabled + try { + npmlog.pause() + if (wasProgressEnabled) { + npmlog.disableProgress() + } + // need await here for proper finally timing + return await mutexFn(input, output, colorLevel) + } finally { + if (wasProgressEnabled) { + npmlog.enableProgress() + } + npmlog.resume() + nextCapture() + } + } + } +} diff --git a/lib/utils/issue-rules.cjs b/lib/utils/issue-rules.cjs new file mode 100644 index 0000000..642d4ca --- /dev/null +++ b/lib/utils/issue-rules.cjs @@ -0,0 +1,180 @@ +//#region UX Constants +/** + * @typedef {{block: boolean, display: boolean}} RuleActionUX + */ +const IGNORE_UX = { + block: false, + display: false +} +const WARN_UX = { + block: false, + display: true +} +const ERROR_UX = { + block: true, + display: true +} +//#endregion +//#region utils +/** + * @typedef { NonNullable> & {success: true})['data']['entries'][number]['settings'][string]>['issueRules']>>[string] | boolean } NonNormalizedIssueRule + */ +/** + * @typedef { (NonNullable> & {success: true})['data']['defaults']['issueRules']>[string]> & { action: string }) | boolean } NonNormalizedResolvedIssueRule + */ +/** + * Iterates over all entries with ordered issue rule for deferal + * Iterates over all issue rules and finds the first defined value that does not defer otherwise uses the defaultValue + * Takes the value and converts into a UX workflow + * + * @param {Iterable>} entriesOrderedIssueRules + * @param {NonNormalizedResolvedIssueRule} defaultValue + * @returns {RuleActionUX} + */ +function resolveIssueRuleUX (entriesOrderedIssueRules, defaultValue) { + if (defaultValue === true || defaultValue == null) { + defaultValue = { + action: 'error' + } + } else if (defaultValue === false) { + defaultValue = { + action: 'ignore' + } + } + let block = false + let display = false + let needDefault = true + iterate_entries: + for (const issueRuleArr of entriesOrderedIssueRules) { + for (const rule of issueRuleArr) { + if (issueRuleValueDoesNotDefer(rule)) { + // there was a rule, even if a defer, don't narrow to the default + needDefault = false + const narrowingFilter = uxForDefinedNonDeferValue(rule) + block = block || narrowingFilter.block + display = display || narrowingFilter.display + continue iterate_entries + } + } + // all rules defer, narrow + const narrowingFilter = uxForDefinedNonDeferValue(defaultValue) + block = block || narrowingFilter.block + display = display || narrowingFilter.display + } + if (needDefault) { + // no config set a + const narrowingFilter = uxForDefinedNonDeferValue(defaultValue) + block = block || narrowingFilter.block + display = display || narrowingFilter.display + } + return { + block, + display + } +} + +/** + * Negative form because it is narrowing the type + * + * @type {(issueRuleValue: NonNormalizedIssueRule) => issueRuleValue is NonNormalizedResolvedIssueRule} + */ +function issueRuleValueDoesNotDefer (issueRule) { + if (issueRule === undefined) { + return false + } else if (typeof issueRule === 'object' && issueRule) { + const { action } = issueRule + if (action === undefined || action === 'defer') { + return false + } + } + return true +} + +/** + * Handles booleans for backwards compatibility + * + * @param {NonNormalizedResolvedIssueRule} issueRuleValue + * @returns {RuleActionUX} + */ +function uxForDefinedNonDeferValue (issueRuleValue) { + if (typeof issueRuleValue === 'boolean') { + return issueRuleValue ? ERROR_UX : IGNORE_UX + } + const { action } = issueRuleValue + if (action === 'warn') { + return WARN_UX + } else if (action === 'ignore') { + return IGNORE_UX + } + return ERROR_UX +} +//#endregion +//#region exports +module.exports = { + /** + * + * @param {(Awaited> & {success: true})['data']} settings + * @returns {(context: {package: {name: string, version: string}, issue: {type: string}}) => RuleActionUX} + */ + createIssueUXLookup (settings) { + /** + * @type {Map} + */ + const cachedUX = new Map() + return (context) => { + const key = context.issue.type + /** + * @type {RuleActionUX | undefined} + */ + let ux = cachedUX.get(key) + if (ux) { + return ux + } + /** + * @type {Array>} + */ + const entriesOrderedIssueRules = [] + for (const settingsEntry of settings.entries) { + /** + * @type {Array} + */ + const orderedIssueRules = [] + let target = settingsEntry.start + while (target !== null) { + const resolvedTarget = settingsEntry.settings[target] + if (!resolvedTarget) { + break + } + const issueRuleValue = resolvedTarget.issueRules?.[key] + if (typeof issueRuleValue !== 'undefined') { + orderedIssueRules.push(issueRuleValue) + } + target = resolvedTarget.deferTo ?? null + } + entriesOrderedIssueRules.push(orderedIssueRules) + } + const defaultValue = settings.defaults.issueRules[key] + /** + * @type {NonNormalizedResolvedIssueRule} + */ + let resolvedDefaultValue = { + action: 'error' + } + // @ts-ignore backcompat, cover with tests + if (defaultValue === false) { + resolvedDefaultValue = { + action: 'ignore' + } + // @ts-ignore backcompat, cover with tests + } else if (defaultValue && defaultValue !== true) { + resolvedDefaultValue = { + action: defaultValue.action ?? 'error' + } + } + ux = resolveIssueRuleUX(entriesOrderedIssueRules, resolvedDefaultValue) + cachedUX.set(key, ux) + return ux + } + } +} +//#endregion diff --git a/lib/utils/misc.js b/lib/utils/misc.js index d6a6cb0..5d11c96 100644 --- a/lib/utils/misc.js +++ b/lib/utils/misc.js @@ -7,7 +7,7 @@ import { logSymbols } from './chalk-markdown.js' export function createDebugLogger (printDebugLogs) { return printDebugLogs // eslint-disable-next-line no-console - ? (...params) => console.error(logSymbols.info, ...params) + ? /** @type { (...params: unknown[]) => void } */(...params) => console.error(logSymbols.info, ...params) : () => {} } diff --git a/lib/utils/path-resolve.js b/lib/utils/path-resolve.js index 68855d3..cbf15ed 100644 --- a/lib/utils/path-resolve.js +++ b/lib/utils/path-resolve.js @@ -9,7 +9,7 @@ import micromatch from 'micromatch' import { ErrorWithCause } from 'pony-cause' import { InputError } from './errors.js' -import { isErrnoException } from './type-helpers.js' +import { isErrnoException } from './type-helpers.cjs' /** * There are a lot of possible folders that we should not be looking in and "ignore-by-default" helps us with defining those diff --git a/lib/utils/sdk.js b/lib/utils/sdk.js index 04b0de5..4090cef 100644 --- a/lib/utils/sdk.js +++ b/lib/utils/sdk.js @@ -30,6 +30,9 @@ export function getDefaultKey () { */ export async function setupSdk (apiKey = getDefaultKey()) { if (apiKey == null && isInteractive()) { + /** + * @type {{ apiKey: string }} + */ const input = await prompts({ type: 'password', name: 'apiKey', diff --git a/lib/utils/type-helpers.js b/lib/utils/type-helpers.cjs similarity index 79% rename from lib/utils/type-helpers.js rename to lib/utils/type-helpers.cjs index 947ba58..6782df5 100644 --- a/lib/utils/type-helpers.js +++ b/lib/utils/type-helpers.cjs @@ -2,7 +2,7 @@ * @param {unknown} value * @returns {value is NodeJS.ErrnoException} */ -export function isErrnoException (value) { +exports.isErrnoException = function isErrnoException (value) { if (!(value instanceof Error)) { return false } diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js index 1780666..044127e 100644 --- a/lib/utils/update-notifier.js +++ b/lib/utils/update-notifier.js @@ -6,6 +6,9 @@ import updateNotifier from 'update-notifier' export function initUpdateNotifier () { readFile(new URL('../../package.json', import.meta.url), 'utf8') .then(rawPkg => { + /** + * @type {Exclude[0], undefined>['pkg']} + */ const pkg = JSON.parse(rawPkg) updateNotifier({ pkg }).notify() }) diff --git a/package-lock.json b/package-lock.json index ffa043e..8e8cfb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@apideck/better-ajv-errors": "^0.3.6", "@socketsecurity/config": "^2.0.0", - "@socketsecurity/sdk": "^0.7.0", + "@socketsecurity/sdk": "^0.7.2", "chalk": "^5.1.2", "globby": "^13.1.3", "hpagent": "^1.2.0", @@ -41,7 +41,7 @@ "@types/micromatch": "^4.0.2", "@types/mocha": "^10.0.1", "@types/mock-fs": "^4.13.1", - "@types/node": "^14.18.31", + "@types/node": "^20.4.2", "@types/npm": "^7.19.0", "@types/npmcli__arborist": "^5.6.1", "@types/prompts": "^2.4.1", @@ -50,8 +50,6 @@ "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "c8": "^8.0.0", - "chai": "^4.3.6", - "chai-as-promised": "^7.1.1", "dependency-check": "^5.0.0-7", "eslint": "^8.34.0", "eslint-config-standard": "^17.0.0", @@ -66,7 +64,6 @@ "eslint-plugin-unicorn": "^45.0.2", "husky": "^8.0.1", "installed-check": "^6.0.5", - "mocha": "^10.0.0", "mock-fs": "^5.2.0", "nock": "^13.3.0", "npm-run-all2": "^6.0.2", @@ -498,9 +495,9 @@ } }, "node_modules/@socketsecurity/sdk": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-0.7.0.tgz", - "integrity": "sha512-ErId4QAviK8uMuOEzuGNMRGjNc8/bZR9oO36Jh534tEjXMFEkGdNVziC2pLskRLxrhHjFT3q7MVkKc926v6Uqg==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-0.7.2.tgz", + "integrity": "sha512-FDA6bbF8kOIgD3bnBHRKSLp2xqWUlZ+030brFJ1vaJgeG/qt5NZoVC6sjPKcTDBf38aJbnzQkW7sl81mEqFZsw==", "dependencies": { "formdata-node": "^5.0.0", "got": "^12.5.3", @@ -616,9 +613,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.51", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.51.tgz", - "integrity": "sha512-P9bsdGFPpVtofEKlhWMVS2qqx1A/rt9QBfihWlklfHHpUpjtYse5AzFz6j4DWrARLYh6gRnw9+5+DJcrq3KvBA==", + "version": "20.4.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz", + "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==", "dev": true }, "node_modules/@types/node-fetch": { @@ -1135,15 +1132,6 @@ "node": ">=8" } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -1188,19 +1176,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1305,15 +1280,6 @@ "node": ">=0.10.0" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/ast-module-types": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-3.0.0.tgz", @@ -1375,15 +1341,6 @@ "node": ">=0.6" } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -1448,12 +1405,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1625,36 +1576,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "dependencies": { - "check-error": "^1.0.2" - }, - "peerDependencies": { - "chai": ">= 2.1.2 < 5" - } - }, "node_modules/chalk": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", @@ -1666,54 +1587,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -2099,18 +1972,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2658,15 +2519,6 @@ "node": ">=4.2.0" } }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3741,15 +3593,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -3838,20 +3681,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3906,15 +3735,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -4241,15 +4061,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/hosted-git-info": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", @@ -4674,18 +4485,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -5352,83 +5151,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5441,15 +5163,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -5806,97 +5519,6 @@ "node": ">= 6" } }, - "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/mock-fs": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", @@ -5928,18 +5550,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6533,15 +6143,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6870,15 +6471,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -7107,18 +6699,6 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/redent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", @@ -7536,15 +7116,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8091,15 +7662,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -8360,12 +7922,6 @@ "node": ">=0.10.0" } }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -8484,54 +8040,6 @@ "node": ">=10" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index a6bc4ad..6a55c52 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,14 @@ "lib/shadow/**" ], "scripts": { - "check:dependency-check": "dependency-check '*.js' 'lib/shadow/*.cjs' '*.mjs' 'test/**/*.js' --no-dev", + "check:dependency-check": "dependency-check '*.js' 'lib/shadow/*.cjs' '*.mjs' 'test/**/*.js' --no-dev --ignore-module node:test --ignore-module node:assert/strict", "check:installed-check": "installed-check -i eslint-plugin-jsdoc", "check:lint": "eslint --report-unused-disable-directives .", "check:tsc": "tsc", "check:type-coverage": "type-coverage --detail --strict --at-least 95 --ignore-files 'test/*'", "check": "run-p -c --aggregate-output check:*", "prepare": "husky install", - "test:mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js'", + "test:unit": "c8 --reporter=lcov --reporter text node --test", "test-ci": "run-s test:*", "test": "run-s check test:*" }, @@ -50,7 +50,7 @@ "@types/micromatch": "^4.0.2", "@types/mocha": "^10.0.1", "@types/mock-fs": "^4.13.1", - "@types/node": "^14.18.31", + "@types/node": "^20.4.2", "@types/npm": "^7.19.0", "@types/npmcli__arborist": "^5.6.1", "@types/prompts": "^2.4.1", @@ -59,8 +59,6 @@ "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "c8": "^8.0.0", - "chai": "^4.3.6", - "chai-as-promised": "^7.1.1", "dependency-check": "^5.0.0-7", "eslint": "^8.34.0", "eslint-config-standard": "^17.0.0", @@ -75,7 +73,6 @@ "eslint-plugin-unicorn": "^45.0.2", "husky": "^8.0.1", "installed-check": "^6.0.5", - "mocha": "^10.0.0", "mock-fs": "^5.2.0", "nock": "^13.3.0", "npm-run-all2": "^6.0.2", @@ -85,7 +82,7 @@ "dependencies": { "@apideck/better-ajv-errors": "^0.3.6", "@socketsecurity/config": "^2.0.0", - "@socketsecurity/sdk": "^0.7.0", + "@socketsecurity/sdk": "^0.7.2", "chalk": "^5.1.2", "globby": "^13.1.3", "hpagent": "^1.2.0", diff --git a/test/issue-rule-ux.test.js b/test/issue-rule-ux.test.js new file mode 100644 index 0000000..f32cec5 --- /dev/null +++ b/test/issue-rule-ux.test.js @@ -0,0 +1,317 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import * as ux from '../lib/utils/issue-rules.cjs' + +describe('Issue Rule UX', () => { + it('should properly defer', () => { + const noEntriesLookup = ux.createIssueUXLookup({ + defaults: { + issueRules: { + fromDeferString: { + action: 'warn' + }, + fromUndefinedAction: { + action: 'warn' + }, + fromUndefinedIssueRule: { + action: 'warn' + }, + willError: { + action: 'error' + }, + willIgnore: { + action: 'ignore' + }, + willWarn: { + action: 'warn' + } + } + }, + entries: [{ + start: 'organization', + settings: { + organization: { + deferTo: 'repository', + issueRules: { + fromDeferString: { action: 'defer' }, + // @ts-ignore paranoia + fromUndefinedAction: { } + } + }, + repository: { + deferTo: null, + issueRules: { + fromMiddleConfig: { + action: 'warn' + } + } + } + } + }] + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'willError' } + }), { + block: true, + display: true, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'willIgnore' } + }), { + block: false, + display: false, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'willWarn' } + }), { + block: false, + display: true, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'fromDeferString' } + }), { + block: false, + display: true, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'fromUndefinedAction' } + }), { + block: false, + display: true, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'fromUndefinedIssueRule' } + }), { + block: false, + display: true, + }) + assert.deepEqual(noEntriesLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'fromMiddleConfig' } + }), { + block: false, + display: true, + }) + }) + it('should use error UX when missing keys', () => { + const emptyLookup = ux.createIssueUXLookup({ + defaults: { + issueRules: { + } + }, + entries: [] + }) + assert.deepEqual(emptyLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: '404' } + }), { + block: true, + display: true, + }) + }) + it('should use error/ignore UX when having boolean values instead of config', () => { + const booleanLookup = ux.createIssueUXLookup({ + defaults: { + issueRules: { + // @ts-ignore backcompat + defaultTrue: true, + // @ts-ignore backcompat + defaultFalse: false + } + }, + entries: [{ + start: 'organization', + settings: { + organization: { + issueRules: { + // @ts-ignore backcompat + orgTrue: true, + // @ts-ignore backcompat + orgFalse: false + } + } + } + }] + }) + assert.deepEqual(booleanLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'defaultTrue' } + }), { + block: true, + display: true, + }) + assert.deepEqual(booleanLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'orgTrue' } + }), { + block: true, + display: true, + }) + assert.deepEqual(booleanLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'defaultFalse' } + }), { + block: false, + display: false, + }) + assert.deepEqual(booleanLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'orgFalse' } + }), { + block: false, + display: false, + }) + }) + it('should use the maximal strength on multiple settings entries', () => { + const multiSettings = ux.createIssueUXLookup({ + defaults: { + issueRules: { + } + }, + entries: [ + { + start: 'start', + settings: { + start: { + deferTo: null, + issueRules: { + warn_then_error: { + action: 'warn' + }, + ignore_then_missing: { + action: 'ignore' + }, + ignore_then_defer: { + action: 'ignore' + } + } + } + } + }, + { + start: 'start', + settings: { + start: { + deferTo: null, + issueRules: { + warn_then_error: { + action: 'error' + }, + ignore_then_defer: { + action: 'defer' + } + } + } + } + } + ] + }) + assert.deepEqual(multiSettings({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'warn_then_error' } + }), { + block: true, + display: true, + }) + assert.deepEqual(multiSettings({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'ignore_then_missing' } + }), { + block: true, + display: true, + }) + assert.deepEqual(multiSettings({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'ignore_then_defer' } + }), { + block: true, + display: true, + }) + }) + it('should shadow defaults', () => { + const shadowedLookup = ux.createIssueUXLookup({ + defaults: { + issueRules: { + willWarn: { + action: 'warn' + } + } + }, + entries: [{ + start: 'organization', + settings: { + organization: { + deferTo: null, + issueRules: { + willWarn: { + action: 'ignore' + } + } + } + } + }] + }) + assert.deepEqual(shadowedLookup({ + package: { + name: 'bar', + version: '0.0.0' + }, + issue: { type: 'willWarn' } + }), { + block: false, + display: false, + }) + }) +}) diff --git a/test/path-resolve.spec.js b/test/path-resolve.test.js similarity index 80% rename from test/path-resolve.spec.js rename to test/path-resolve.test.js index efd95b6..15f7195 100644 --- a/test/path-resolve.spec.js +++ b/test/path-resolve.test.js @@ -1,6 +1,6 @@ -/// -import chai from 'chai' -import chaiAsPromised from 'chai-as-promised' +import assert from 'node:assert/strict' +import { afterEach, beforeEach, describe, it } from 'node:test' + import mockFs from 'mock-fs' import nock from 'nock' @@ -12,9 +12,6 @@ import { mapGlobResultToFiles, } from '../lib/utils/path-resolve.js' -chai.use(chaiAsPromised) -chai.should() - const globPatterns = { general: { readme: { @@ -104,16 +101,17 @@ describe('Path Resolve', () => { }) it('should handle found files', async () => { - await fileExists('foo.txt').should.eventually.be.true + assert.equal(await fileExists('foo.txt'), true) }) it('should handle missing files', async () => { - await fileExists('missing.txt').should.eventually.be.false + assert.equal(await fileExists('missing.txt'), false) }) it('should throw when finding a folder', async () => { - await fileExists('some-dir') - .should.be.rejectedWith(InputError, 'Expected \'some-dir\' to be a file') + await assert.rejects(fileExists('some-dir'), (e) => { + return e instanceof InputError && e.message.includes('Expected \'some-dir\' to be a file') + }) }) }) @@ -123,15 +121,16 @@ describe('Path Resolve', () => { mockFs({ '/foo.txt': 'some content', }) - await sortedMapGlobEntry('/foo.txt', globPatterns).should.eventually.become([]) + assert.deepEqual(await sortedMapGlobEntry('/foo.txt', globPatterns), []) }) it('should throw on errors', async () => { mockFs({ '/package.json': { /* Empty directory */ }, }) - await sortedMapGlobEntry('/', globPatterns) - .should.eventually.be.rejectedWith(InputError, 'Expected \'/package.json\' to be a file') + await assert.rejects(sortedMapGlobEntry('/', globPatterns), (e) => { + return e instanceof InputError && e.message.includes('Expected \'/package.json\' to be a file') + }) }) }) @@ -141,7 +140,7 @@ describe('Path Resolve', () => { '/package-lock.json': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/', globPatterns), [ '/package-lock.json', '/package.json' ]) @@ -151,14 +150,14 @@ describe('Path Resolve', () => { mockFs({ '/package.json': '{}', }) - await sortedMapGlobEntry('/', globPatterns).should.eventually.become(['/package.json']) + assert.deepEqual(await sortedMapGlobEntry('/', globPatterns), ['/package.json']) }) it('should not resolve lock file without package', async () => { mockFs({ '/package-lock.json': '{}', }) - await sortedMapGlobEntry('/', globPatterns).should.eventually.become([]) + assert.deepEqual(await sortedMapGlobEntry('/', globPatterns), []) }) it('should support alternative lock files', async () => { @@ -166,7 +165,7 @@ describe('Path Resolve', () => { '/yarn.lock': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/', globPatterns), [ '/package.json', '/yarn.lock' ]) @@ -179,7 +178,7 @@ describe('Path Resolve', () => { '/package-lock.json': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/package.json', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/package.json', globPatterns), [ '/package-lock.json', '/package.json' ]) @@ -189,19 +188,19 @@ describe('Path Resolve', () => { mockFs({ '/package.json': '{}', }) - await sortedMapGlobEntry('/package.json', globPatterns).should.eventually.become(['/package.json']) + assert.strict.deepEqual(await sortedMapGlobEntry('/package.json', globPatterns), ['/package.json']) }) it('should not validate the input file', async () => { mockFs({}) - await sortedMapGlobEntry('/package.json', globPatterns).should.eventually.become(['/package.json']) + assert.deepEqual(await sortedMapGlobEntry('/package.json', globPatterns), ['/package.json']) }) it('should not validate the input file, but still add a complementary lock file', async () => { mockFs({ '/package-lock.json': '{}', }) - await sortedMapGlobEntry('/package.json', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/package.json', globPatterns), [ '/package-lock.json', '/package.json' ]) @@ -212,7 +211,7 @@ describe('Path Resolve', () => { '/yarn.lock': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/package.json', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/package.json', globPatterns), [ '/package.json', '/yarn.lock' ]) @@ -225,7 +224,7 @@ describe('Path Resolve', () => { '/package-lock.json': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/package-lock.json', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/package-lock.json', globPatterns), [ '/package-lock.json', '/package.json' ]) @@ -236,7 +235,7 @@ describe('Path Resolve', () => { '/yarn.lock': '{}', '/package.json': '{}', }) - await sortedMapGlobEntry('/yarn.lock', globPatterns).should.eventually.become([ + assert.deepEqual(await sortedMapGlobEntry('/yarn.lock', globPatterns), [ '/package.json', '/yarn.lock' ]) @@ -256,13 +255,13 @@ describe('Path Resolve', () => { '/abc/package.json': '{}', }) - await sortedMapGlobResult([ + assert.deepEqual(await sortedMapGlobResult([ '/', '/foo/package-lock.json', '/bar/package.json', '/abc/', '/abc/package.json' - ], globPatterns).should.eventually.become([ + ], globPatterns), [ '/abc/package.json', '/bar/package.json', '/bar/yarn.lock', @@ -286,13 +285,13 @@ describe('Path Resolve', () => { '/abc/package.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['**/*'], undefined, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/abc/package.json', '/bar/package.json', '/bar/yarn.lock', @@ -308,13 +307,13 @@ describe('Path Resolve', () => { '/package.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['.'], undefined, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/package.json', ]) }) @@ -327,7 +326,7 @@ describe('Path Resolve', () => { '/foo/package.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['**/*'], { @@ -341,7 +340,7 @@ describe('Path Resolve', () => { }, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/bar/package.json', '/foo/package-lock.json', '/foo/package.json' @@ -357,13 +356,13 @@ describe('Path Resolve', () => { '/foo/package.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['**/*'], undefined, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/foo/package-lock.json', '/foo/package.json' ]) @@ -384,13 +383,13 @@ describe('Path Resolve', () => { '/foo/package.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['**/*'], undefined, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/foo/package-lock.json', '/foo/package.json' ]) @@ -404,13 +403,13 @@ describe('Path Resolve', () => { '/foo/random.json': '{}', }) - await sortedGetPackageFiles( + assert.deepEqual(await sortedGetPackageFiles( '/', ['**/*'], undefined, globPatterns, () => {} - ).should.eventually.become([ + ), [ '/foo/package-lock.json', '/foo/package.json' ]) diff --git a/test/test-helpers.test.js b/test/test-helpers.test.js new file mode 100644 index 0000000..c54e16d --- /dev/null +++ b/test/test-helpers.test.js @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import { describe, it } from 'node:test' + +import * as helpers from '../lib/utils/type-helpers.cjs' + +describe('Error Narrowing', () => { + it('should properly detect node errors', () => { + try { + fs.readFileSync(new URL('./enoent', import.meta.url)) + } catch (e) { + assert.equal(helpers.isErrnoException(e), true) + } + }) + it('should properly only detect node errors', () => { + assert.equal(helpers.isErrnoException(new Error()), false) + assert.equal(helpers.isErrnoException({ ...new Error() }), false) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 2f5140f..22fd744 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "cli.js" ], "include": [ + "lib/utils/issue-rules.cjs", "lib/**/*", "test/**/*" ],