From b6ed0bb5b40e8b77c08b67178c742e14913f2dfb Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Mon, 8 Jun 2026 12:30:58 -0700 Subject: [PATCH] fix: packaged inso segfault patch via pkg-safe consola reporter (#10035) (cherry picked from commit 7f609226ac621fcb1619227f94593afc526537d7) --- .github/workflows/release-build.yml | 3 +- package-lock.json | 29 +++ packages/insomnia-inso/package.json | 2 + packages/insomnia-inso/src/logger.ts | 3 + .../__snapshots__/fancy-reporter.test.ts.snap | 189 ++++++++++++++++++ .../src/reporters/fancy-reporter.test.ts | 98 +++++++++ .../src/reporters/fancy-reporter.ts | 159 +++++++++++++++ .../insomnia-inso/src/scripts/verify-pkg.js | 49 +++-- 8 files changed, 511 insertions(+), 21 deletions(-) create mode 100644 packages/insomnia-inso/src/reporters/__snapshots__/fancy-reporter.test.ts.snap create mode 100644 packages/insomnia-inso/src/reporters/fancy-reporter.test.ts create mode 100644 packages/insomnia-inso/src/reporters/fancy-reporter.ts diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index cafe2cbf41..e0f01bd9cc 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -237,8 +237,7 @@ jobs: - name: Package inso run: | - echo "Replacing electron binary with node binary" - node_modules/.bin/node-pre-gyp install --update-binary --directory node_modules/@getinsomnia/node-libcurl + npm run install-libcurl-node npm run build:production -w insomnia-inso npm run package -w insomnia-inso env: diff --git a/package-lock.json b/package-lock.json index 9379611db2..6d3aa3ab8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16582,6 +16582,21 @@ "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -29405,6 +29420,8 @@ "consola": "^3.4.2", "cosmiconfig": "^9.0.0", "enquirer": "^2.4.1", + "fast-string-width": "^3.0.2", + "is-unicode-supported": "^2.1.0", "picocolors": "^1.1.1", "string-argv": "^0.3.2", "yaml": "^2.7.1" @@ -29435,6 +29452,18 @@ "node": "^12.20 || >=14.13" } }, + "packages/insomnia-inso/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/insomnia-scripting-environment": { "version": "13.0.0-beta.1", "license": "Apache-2.0", diff --git a/packages/insomnia-inso/package.json b/packages/insomnia-inso/package.json index 7108e2e2fc..bbe0ae2273 100644 --- a/packages/insomnia-inso/package.json +++ b/packages/insomnia-inso/package.json @@ -59,6 +59,8 @@ "@stoplight/types": "^14.1.1", "commander": "^12.1.0", "consola": "^3.4.2", + "fast-string-width": "^3.0.2", + "is-unicode-supported": "^2.1.0", "cosmiconfig": "^9.0.0", "enquirer": "^2.4.1", "picocolors": "^1.1.1", diff --git a/packages/insomnia-inso/src/logger.ts b/packages/insomnia-inso/src/logger.ts index d7e2bf14f9..7f27b93b4b 100644 --- a/packages/insomnia-inso/src/logger.ts +++ b/packages/insomnia-inso/src/logger.ts @@ -1,6 +1,8 @@ import type { ConsolaOptions, LogObject, LogType } from 'consola'; import { createConsola } from 'consola'; +import { FancyReporter } from './reporters/fancy-reporter'; + type LogsByType = Partial>; type ModifiedConsola = ReturnType & { __getLogs: () => LogsByType }; @@ -9,6 +11,7 @@ const consolaLogger = createConsola({ formatOptions: { date: false, }, + reporters: [new FancyReporter()], }); (consolaLogger as ModifiedConsola).__getLogs = () => ({}); diff --git a/packages/insomnia-inso/src/reporters/__snapshots__/fancy-reporter.test.ts.snap b/packages/insomnia-inso/src/reporters/__snapshots__/fancy-reporter.test.ts.snap new file mode 100644 index 0000000000..84feae28c0 --- /dev/null +++ b/packages/insomnia-inso/src/reporters/__snapshots__/fancy-reporter.test.ts.snap @@ -0,0 +1,189 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FancyReporter > character-formats \`backticks\` and _underscores_ 1`] = ` +{ + "stderr": "", + "stdout": "ℹ use foo and bar properly +", +} +`; + +exports[`FancyReporter > falls back to logObj.icon for types without a built-in icon 1`] = ` +{ + "stderr": "", + "stdout": "★ hello +", +} +`; + +exports[`FancyReporter > formats Error args with stack 1`] = ` +{ + "stderr": " + ERROR boom + at + +", + "stdout": "", +} +`; + +exports[`FancyReporter > formats chained Error.cause 1`] = ` +{ + "stderr": " + ERROR outer + at + + [cause]: inner + at + +", + "stdout": "", +} +`; + +exports[`FancyReporter > honors the badge override on the log object 1`] = ` +{ + "stderr": "", + "stdout": " + INFO hello + +", +} +`; + +exports[`FancyReporter > honors the badge override on the log object 2`] = ` +{ + "stderr": "✖ hello +", + "stdout": "", +} +`; + +exports[`FancyReporter > preserves additional lines after the first 1`] = ` +{ + "stderr": "", + "stdout": "ℹ line one +line two +line three +", +} +`; + +exports[`FancyReporter > renders log type: box 1`] = ` +{ + "stderr": "", + "stdout": "box hello from box +", +} +`; + +exports[`FancyReporter > renders log type: debug 1`] = ` +{ + "stderr": "", + "stdout": "⚙ hello from debug +", +} +`; + +exports[`FancyReporter > renders log type: error 1`] = ` +{ + "stderr": " + ERROR hello from error + +", + "stdout": "", +} +`; + +exports[`FancyReporter > renders log type: fail 1`] = ` +{ + "stderr": "", + "stdout": "✖ hello from fail +", +} +`; + +exports[`FancyReporter > renders log type: fatal 1`] = ` +{ + "stderr": " + FATAL hello from fatal + +", + "stdout": "", +} +`; + +exports[`FancyReporter > renders log type: info 1`] = ` +{ + "stderr": "", + "stdout": "ℹ hello from info +", +} +`; + +exports[`FancyReporter > renders log type: log 1`] = ` +{ + "stderr": "", + "stdout": "hello from log +", +} +`; + +exports[`FancyReporter > renders log type: ready 1`] = ` +{ + "stderr": "", + "stdout": "✔ hello from ready +", +} +`; + +exports[`FancyReporter > renders log type: start 1`] = ` +{ + "stderr": "", + "stdout": "◐ hello from start +", +} +`; + +exports[`FancyReporter > renders log type: success 1`] = ` +{ + "stderr": "", + "stdout": "✔ hello from success +", +} +`; + +exports[`FancyReporter > renders log type: trace 1`] = ` +{ + "stderr": "", + "stdout": "→ hello from trace + at +", +} +`; + +exports[`FancyReporter > renders log type: warn 1`] = ` +{ + "stderr": " + WARN hello from warn + +", + "stdout": "", +} +`; + +exports[`FancyReporter > renders tag and date in a two-column layout when columns are wide 1`] = ` +{ + "stderr": "", + "stdout": "ℹ hello cli 12:34:56 PM +", +} +`; + +exports[`FancyReporter > renders tag inline when columns is 0 1`] = ` +{ + "stderr": "", + "stdout": "[cli] ℹ hello +", +} +`; diff --git a/packages/insomnia-inso/src/reporters/fancy-reporter.test.ts b/packages/insomnia-inso/src/reporters/fancy-reporter.test.ts new file mode 100644 index 0000000000..f072709bcf --- /dev/null +++ b/packages/insomnia-inso/src/reporters/fancy-reporter.test.ts @@ -0,0 +1,98 @@ +import type { ConsolaOptions, LogObject, LogType } from 'consola'; +import { LogTypes } from 'consola'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('is-unicode-supported', () => ({ default: () => true })); + +import { FancyReporter } from './fancy-reporter'; + +const render = ( + input: Partial, + columns = 0, + formatOptions: ConsolaOptions['formatOptions'] = {}, +) => { + let stdout = ''; + let stderr = ''; + const sink = (target: 'stdout' | 'stderr') => + ({ + write: (chunk: string) => { + if (target === 'stdout') stdout += chunk; + else stderr += chunk; + return true; + }, + columns, + }) as unknown as NodeJS.WriteStream; + + const logObj = { + date: new Date('2026-01-01T12:34:56'), + args: ['hello'], + type: 'info', + level: 3, + tag: '', + ...input, + } as LogObject; + // toLocaleTimeString is locale-dependent — pin it for stable snapshots. + vi.spyOn(logObj.date, 'toLocaleTimeString').mockReturnValue('12:34:56 PM'); + + new FancyReporter().log(logObj, { + options: { + stdout: sink('stdout'), + stderr: sink('stderr'), + formatOptions: { date: false, ...formatOptions }, + } as ConsolaOptions, + }); + + // Stack frames are non-deterministic — collapse them so snapshots are stable. + const normalize = (s: string) => s.replace(/(\n\s{2,}at [^\n]+)+/g, '\n at '); + return { stdout: normalize(stdout), stderr: normalize(stderr) }; +}; + +describe('FancyReporter', () => { + const types = (Object.keys(LogTypes) as LogType[]).filter(t => t !== 'silent' && t !== 'verbose'); + + it.each(types)('renders log type: %s', type => { + expect(render({ type, level: LogTypes[type].level as number, args: [`hello from ${type}`] })).toMatchSnapshot(); + }); + + it('renders tag and date in a two-column layout when columns are wide', () => { + expect(render({ tag: 'cli' }, 120, { date: true })).toMatchSnapshot(); + }); + + it('renders tag inline when columns is 0', () => { + expect(render({ tag: 'cli' })).toMatchSnapshot(); + }); + + it('character-formats `backticks` and _underscores_', () => { + expect(render({ args: ['use `foo` and _bar_ properly'] })).toMatchSnapshot(); + }); + + it('preserves additional lines after the first', () => { + expect(render({ args: ['line one\nline two\nline three'] })).toMatchSnapshot(); + }); + + it('formats Error args with stack', () => { + const err = Object.assign(new Error('boom'), { + stack: 'Error: boom\n at foo (/repo/file.ts:1:1)\n at bar (/repo/file.ts:2:2)', + }); + expect(render({ type: 'error', level: 0, args: [err] })).toMatchSnapshot(); + }); + + it('formats chained Error.cause', () => { + const inner = Object.assign(new Error('inner'), { + stack: 'Error: inner\n at inner (/repo/inner.ts:5:5)', + }); + const outer = Object.assign(new Error('outer', { cause: inner }), { + stack: 'Error: outer\n at outer (/repo/outer.ts:3:3)', + }); + expect(render({ type: 'error', level: 0, args: [outer] })).toMatchSnapshot(); + }); + + it('honors the badge override on the log object', () => { + expect(render({ badge: true } as Partial)).toMatchSnapshot(); + expect(render({ type: 'error', level: 0, badge: false } as Partial)).toMatchSnapshot(); + }); + + it('falls back to logObj.icon for types without a built-in icon', () => { + expect(render({ type: 'verbose' as LogType, level: 5, icon: '★' } as Partial)).toMatchSnapshot(); + }); +}); diff --git a/packages/insomnia-inso/src/reporters/fancy-reporter.ts b/packages/insomnia-inso/src/reporters/fancy-reporter.ts new file mode 100644 index 0000000000..1fd31a09fe --- /dev/null +++ b/packages/insomnia-inso/src/reporters/fancy-reporter.ts @@ -0,0 +1,159 @@ +import path from 'node:path'; +import { formatWithOptions } from 'node:util'; + +import type { ConsolaOptions, ConsolaReporter, FormatOptions, LogLevel, LogObject, LogType } from 'consola'; +import { type ColorName, colors, getColor } from 'consola/utils'; +import fastStringWidth from 'fast-string-width'; +import isUnicodeSupported from 'is-unicode-supported'; + +const TYPE_COLOR_MAP: Partial> = { + info: 'cyan', + fail: 'red', + success: 'green', + ready: 'green', + start: 'magenta', +}; + +const LEVEL_COLOR_MAP: Partial> = { + 0: 'red', + 1: 'yellow', +}; + +const unicode = isUnicodeSupported(); +const icon = (value: string, fallback: string) => (unicode ? value : fallback); + +const TYPE_ICONS: Partial> = { + error: icon('✖', '×'), + fatal: icon('✖', '×'), + ready: icon('✔', '√'), + warn: icon('⚠', '‼'), + info: icon('ℹ', 'i'), + success: icon('✔', '√'), + debug: icon('⚙', 'D'), + trace: icon('→', '→'), + fail: icon('✖', '×'), + start: icon('◐', 'o'), + log: '', +}; + +function getBgColor(color: ColorName) { + const bgKey = `bg${color[0].toUpperCase()}${color.slice(1)}` as ColorName; + return colors[bgKey] ?? colors.bgWhite; +} + +function parseStack(stack: string, message: string) { + const cwd = process.cwd() + path.sep; + return stack + .split('\n') + .splice(message.split('\n').length) + .map(l => l.trim().replace('file://', '').replace(cwd, '')); +} + +function characterFormat(str: string) { + return str + .replace(/`([^`]+)`/gm, (_, match: string) => colors.cyan(match)) + .replace(/\s+_([^_]+)_\s+/gm, (_, match: string) => ` ${colors.underline(match)} `); +} + +function writeStream(data: string, stream: NodeJS.WriteStream) { + const write = (stream as NodeJS.WriteStream & { __write?: typeof stream.write }).__write || stream.write; + return write.call(stream, data); +} + +function formatType(logObj: LogObject, isBadge: boolean) { + const typeColor = TYPE_COLOR_MAP[logObj.type] || LEVEL_COLOR_MAP[logObj.level] || 'gray'; + + if (isBadge) { + return getBgColor(typeColor)(colors.black(` ${logObj.type.toUpperCase()} `)); + } + + const typeIcon = + typeof TYPE_ICONS[logObj.type] === 'string' + ? TYPE_ICONS[logObj.type] + : (logObj as LogObject & { icon?: string }).icon || logObj.type; + + return typeIcon ? getColor(typeColor)(typeIcon) : ''; +} + +function formatTraceStack(stack: string, message: string, errorLevel = 0) { + const indent = ' '.repeat(errorLevel + 1); + return ( + `\n${indent}` + + parseStack(stack, message) + .map( + line => + ' ' + + line + .replace(/^at +/, match => colors.gray(match)) + .replace(/\((.+)\)/, (_, match: string) => `(${colors.cyan(match)})`), + ) + .join(`\n${indent}`) + ); +} + +function formatError(err: Error & { cause?: unknown }, opts: FormatOptions): string { + const message = err.message ?? formatWithOptions(opts, err); + const stack = err.stack + ? ' '.repeat((opts.errorLevel || 0) + 1) + + parseStack(err.stack, message).join(`\n${' '.repeat((opts.errorLevel || 0) + 1)}`) + : ''; + const level = opts.errorLevel || 0; + const causedPrefix = level > 0 ? `${' '.repeat(level)}[cause]: ` : ''; + const causedError = err.cause + ? '\n\n' + formatError(err.cause as Error & { cause?: unknown }, { ...opts, errorLevel: level + 1 }) + : ''; + return causedPrefix + message + '\n' + stack + causedError; +} + +function formatArgs(args: unknown[], opts: FormatOptions) { + const formattedArgs = args.map(arg => { + if (arg && typeof arg === 'object' && 'stack' in arg && typeof (arg as Error).stack === 'string') { + return formatError(arg as Error & { cause?: unknown }, opts); + } + return arg; + }); + return formatWithOptions(opts, ...formattedArgs); +} + +function formatLogObj(logObj: LogObject, opts: FormatOptions) { + const [message, ...additional] = formatArgs(logObj.args, opts).split('\n'); + const coloredDate = opts.date ? colors.gray(logObj.date.toLocaleTimeString()) : ''; + const isBadge = (logObj.badge as boolean | undefined) ?? logObj.level < 2; + const type = formatType(logObj, isBadge); + const tag = logObj.tag ? colors.gray(logObj.tag) : ''; + const join = (parts: unknown[]) => parts.filter(Boolean).join(' '); + + const left = join([type, characterFormat(message)]); + const right = join(opts.columns ? [tag, coloredDate] : [tag]); + const space = (opts.columns || 0) - fastStringWidth(left) - fastStringWidth(right) - 2; + + let line = + space > 0 && (opts.columns || 0) >= 80 + ? left + ' '.repeat(space) + right + : (right ? `${colors.gray(`[${right}]`)} ` : '') + left; + + line += characterFormat(additional.length > 0 ? '\n' + additional.join('\n') : ''); + + if (logObj.type === 'trace') { + const err = new Error('Trace: ' + logObj.message); + line += formatTraceStack(err.stack || '', err.message); + } + + return isBadge ? '\n' + line + '\n' : line; +} + +export class FancyReporter implements ConsolaReporter { + log(logObj: LogObject, ctx: { options: ConsolaOptions }) { + const line = formatLogObj(logObj, { + columns: (ctx.options.stdout as NodeJS.WriteStream | undefined)?.columns || 0, + ...ctx.options.formatOptions, + }); + + return writeStream( + line + '\n', + logObj.level < 2 + ? (ctx.options.stderr as NodeJS.WriteStream) || process.stderr + : (ctx.options.stdout as NodeJS.WriteStream) || process.stdout, + ); + } +} diff --git a/packages/insomnia-inso/src/scripts/verify-pkg.js b/packages/insomnia-inso/src/scripts/verify-pkg.js index d77ae5c49a..605e8381dc 100644 --- a/packages/insomnia-inso/src/scripts/verify-pkg.js +++ b/packages/insomnia-inso/src/scripts/verify-pkg.js @@ -1,20 +1,31 @@ -const spawn = require('child_process').spawn; -const resolve = require('path').resolve; -const basePath = resolve('binaries/inso'); -const childProcess = spawn(basePath, ['--help']); -childProcess.stdout.on('data', data => { - console.log(`stdout: ${data}`); -}); -childProcess.stderr.on('data', data => { - console.log(`stderr: ${data}`); -}); -childProcess.on('error', err => { - console.error(`Error: ${err.message}`); -}); -childProcess.on('exit', (code, signal) => { - if (code !== 0) { - console.error(`Child process exited with code ${code} and signal ${signal}`); - } else { - console.log('Child process finished successfully'); +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); + +const binary = path.resolve('binaries/inso'); +const lintFixture = path.resolve('src/commands/fixtures/openapi-spec.yaml'); +const env = { ...process.env }; +delete env.CI; + +const commands = [['--help'], ['lint', 'spec', lintFixture]]; + +for (const args of commands) { + const result = spawnSync(binary, args, { env, encoding: 'utf8' }); + + if (result.error) { + console.error(`Failed to run ${binary} ${args.join(' ')}: ${result.error.message}`); + process.exit(1); } -}); + + if (result.status !== 0) { + console.error(`Command failed (exit ${result.status}): ${binary} ${args.join(' ')}`); + if (result.stdout) { + console.error(result.stdout); + } + if (result.stderr) { + console.error(result.stderr); + } + process.exit(result.status ?? 1); + } +} + +console.log('Packaged binary smoke tests passed');