fix: packaged inso segfault patch via pkg-safe consola reporter (#10035)

(cherry picked from commit 7f609226ac)
This commit is contained in:
Ryan Willis
2026-06-08 12:30:58 -07:00
committed by Insomnia
parent d50e4f3eaf
commit b6ed0bb5b4
8 changed files with 511 additions and 21 deletions

View File

@@ -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:

29
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -1,6 +1,8 @@
import type { ConsolaOptions, LogObject, LogType } from 'consola';
import { createConsola } from 'consola';
import { FancyReporter } from './reporters/fancy-reporter';
type LogsByType = Partial<Record<LogType, string[]>>;
type ModifiedConsola = ReturnType<typeof createConsola> & { __getLogs: () => LogsByType };
@@ -9,6 +11,7 @@ const consolaLogger = createConsola({
formatOptions: {
date: false,
},
reporters: [new FancyReporter()],
});
(consolaLogger as ModifiedConsola).__getLogs = () => ({});

View File

@@ -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 <stack-frame>
",
"stdout": "",
}
`;
exports[`FancyReporter > formats chained Error.cause 1`] = `
{
"stderr": "
ERROR outer
at <stack-frame>
[cause]: inner
at <stack-frame>
",
"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 <stack-frame>
",
}
`;
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
",
}
`;

View File

@@ -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<LogObject>,
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 <stack-frame>');
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<LogObject>)).toMatchSnapshot();
expect(render({ type: 'error', level: 0, badge: false } as Partial<LogObject>)).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<LogObject>)).toMatchSnapshot();
});
});

View File

@@ -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<Record<LogType, ColorName>> = {
info: 'cyan',
fail: 'red',
success: 'green',
ready: 'green',
start: 'magenta',
};
const LEVEL_COLOR_MAP: Partial<Record<LogLevel, ColorName>> = {
0: 'red',
1: 'yellow',
};
const unicode = isUnicodeSupported();
const icon = (value: string, fallback: string) => (unicode ? value : fallback);
const TYPE_ICONS: Partial<Record<LogType, string>> = {
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,
);
}
}

View File

@@ -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');