add lint warning about node modules (#9439)

* add lint warning about node modules

* eliminate hash function from test case

* use bridge for read file

* use main bridges for fs

* add header

* fix types

* remove node:url

* path bridge

* multipart bridge

* clean up eslint file

* add naive warning count in order to see impact
This commit is contained in:
Jack Kavanagh
2025-12-08 15:27:48 +01:00
committed by GitHub
parent 1d625ba1c9
commit dfcb8ebace
28 changed files with 269 additions and 284 deletions

View File

@@ -1,3 +1,5 @@
import { builtinModules } from 'node:module';
import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
@@ -8,10 +10,13 @@ import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default defineConfig([
// https://typescript-eslint.io/getting-started#additional-configs
eslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
// Unicorn section
eslintPluginUnicorn.configs.unopinionated,
{
rules: {
@@ -48,6 +53,7 @@ export default defineConfig([
'unicorn/prefer-switch': 'off', // TODO: delete me
},
},
// Playwright section
{
...playwright.configs['flat/recommended'],
files: ['packages/insomnia-smoke-test/tests/**/*.ts'],
@@ -62,7 +68,22 @@ export default defineConfig([
'playwright/no-wait-for-timeout': 'error',
},
},
// nodeIntegration: false section
{
files: [
'packages/insomnia/src/ui/**/*.{ts,tsx}',
// TODO: 'packages/insomnia/src/common/**/*.{ts,tsx}',
],
rules: {
'no-restricted-imports': [
'error',
{
paths: builtinModules.map(m => `node:${m}`),
},
],
},
},
// React hooks section
{
files: ['packages/insomnia/src/**/*.{ts,tsx}'],
plugins: { 'react-hooks': reactHooksPlugin },
@@ -75,6 +96,7 @@ export default defineConfig([
'react-hooks/incompatible-library': 'off', //TODO(use react-aria virtualizer): delete me
},
},
// React section
{
files: ['packages/insomnia/src/**/*.{ts,tsx}'],
...reactPlugin.configs.flat.recommended,
@@ -111,6 +133,7 @@ export default defineConfig([
'react/no-array-index-key': 'error',
},
},
// simple-import-sort section
{
plugins: {
'simple-import-sort': simpleImportSortPlugin,
@@ -119,6 +142,7 @@ export default defineConfig([
'simple-import-sort/imports': 'error',
},
},
// General ESLint rules
{
rules: {
'no-restricted-imports': [
@@ -148,6 +172,7 @@ export default defineConfig([
'no-useless-escape': 'off', // TODO: delete me
},
},
// TypeScript ESLint rules
{
rules: {
'@typescript-eslint/array-type': ['error', { default: 'array', readonly: 'array' }],

View File

@@ -7,6 +7,7 @@ import contextMenu from 'electron-context-menu';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
import { configureFetch } from 'insomnia-api';
import { registerPathHandlers } from '~/main/ipc/path';
import { registerLLMConfigServiceAPI } from '~/main/llm-config-service';
import { insomniaFetch } from '~/ui/insomnia-fetch';
@@ -75,6 +76,7 @@ app.on('ready', async () => {
registerElectronHandlers();
// @TODO - Maybe move the register stuff in the registerMainHandlers function
registerMainHandlers();
registerPathHandlers();
registergRPCHandlers();
registerGitServiceAPI();
registerLLMConfigServiceAPI();

View File

@@ -181,6 +181,7 @@ const main: Window['main'] = {
onDefaultBrowserOAuthRedirect: options => ipcRenderer.invoke('onDefaultBrowserOAuthRedirect', options),
cancelAuthorizationInDefaultBrowser: options => ipcRenderer.invoke('cancelAuthorizationInDefaultBrowser', options),
setMenuBarVisibility: options => ipcRenderer.send('setMenuBarVisibility', options),
multipartBufferToArray: options => ipcRenderer.invoke('multipartBufferToArray', options),
installPlugin: (lookupName: string, allowScopedPackageNames = false) =>
ipcRenderer.invoke('installPlugin', lookupName, allowScopedPackageNames),
curlRequest: options => ipcRenderer.invoke('curlRequest', options),
@@ -263,7 +264,12 @@ ipcRenderer.on('hidden-browser-window-response-listener', event => {
ports.set('hiddenWindowPort', port);
ipcRenderer.invoke('main-window-script-port-ready');
});
const path: Window['path'] = {
dirname: (p: string) => ipcRenderer.sendSync('path.dirname', p),
basename: (p: string) => ipcRenderer.sendSync('path.basename', p),
join: (...paths: string[]) => ipcRenderer.sendSync('path.join', ...paths),
resolve: (...paths: string[]) => ipcRenderer.sendSync('path.resolve', ...paths),
};
const dialog: Window['dialog'] = {
showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options),
showSaveDialog: options => ipcRenderer.invoke('showSaveDialog', options),
@@ -291,6 +297,7 @@ if (process.contextIsolated) {
contextBridge.exposeInMainWorld('shell', shell);
contextBridge.exposeInMainWorld('clipboard', clipboard);
contextBridge.exposeInMainWorld('webUtils', webUtils);
contextBridge.exposeInMainWorld('path', path);
} else {
window.main = main;
window.dialog = dialog;
@@ -298,4 +305,5 @@ if (process.contextIsolated) {
window.shell = shell;
window.clipboard = clipboard;
window.webUtils = webUtils;
window.path = path;
}

View File

@@ -24,30 +24,30 @@ export type HandleChannels =
| 'authorizeUserInWindow'
| 'backup'
| 'cancelAuthorizationInDefaultBrowser'
| 'generateMockRouteDataFromSpec'
| 'generateCommitsFromDiff'
| 'curl.event.findMany'
| 'curl.open'
| 'curl.readyState'
| 'curlRequest'
| 'database.caCertificate.create'
| 'extractJsonFileFromPostmanDataDumpArchive'
| 'generateCommitsFromDiff'
| 'generateMockRouteDataFromSpec'
| 'getExecution'
| 'getLocalStorageDataFromFileOrigin'
| 'git.abortMerge'
| 'git.canPushLoader'
| 'git.checkoutGitBranch'
| 'git.cloneGitRepo'
| 'git.commitAndPushToGitRepo'
| 'git.commitToGitRepo'
| 'git.multipleCommitToGitRepo'
| 'git.completeSignInToGitHub'
| 'git.completeSignInToGitLab'
| 'git.continueMerge'
| 'git.createNewGitBranch'
| 'git.deleteGitBranch'
| 'git.diff'
| 'git.diffFileLoader'
| 'git.discardChanges'
| 'git.abortMerge'
| 'git.fetchGitRemoteBranches'
| 'git.getGitBranches'
| 'git.getGitHubRepositories'
@@ -57,13 +57,13 @@ export type HandleChannels =
| 'git.gitFetchAction'
| 'git.gitLogLoader'
| 'git.gitStatus'
| 'git.diff'
| 'git.initGitRepoClone'
| 'git.initSignInToGitHub'
| 'git.initSignInToGitLab'
| 'git.loadGitRepository'
| 'git.mergeGitBranch'
| 'git.migrateLegacyInsomniaFolderToFile'
| 'git.multipleCommitToGitRepo'
| 'git.pullFromGitRemote'
| 'git.pushToGitRemote'
| 'git.resetGitRepo'
@@ -74,33 +74,53 @@ export type HandleChannels =
| 'git.updateGitRepo'
| 'grpc.loadMethods'
| 'grpc.loadMethodsFromReflection'
| 'installPlugin'
| 'lintSpec'
| 'llm.getActiveBackend'
| 'llm.setActiveBackend'
| 'llm.clearActiveBackend'
| 'llm.getBackendConfig'
| 'llm.updateBackendConfig'
| 'llm.getAllConfigurations'
| 'llm.getCurrentConfig'
| 'llm.getAIFeatureEnabled'
| 'llm.setAIFeatureEnabled'
| 'onDefaultBrowserOAuthRedirect'
| 'open-channel-to-hidden-browser-window'
| 'parseImport'
| 'openPath'
| 'readCurlResponse'
| 'readOrCreateDataDir'
| 'readDir'
| 'insecureReadFile'
| 'insecureReadFileWithEncoding'
| 'secureReadFile'
| 'installPlugin'
| 'lintSpec'
| 'llm.clearActiveBackend'
| 'llm.getActiveBackend'
| 'llm.getAIFeatureEnabled'
| 'llm.getAllConfigurations'
| 'llm.getBackendConfig'
| 'llm.getCurrentConfig'
| 'llm.setActiveBackend'
| 'llm.setAIFeatureEnabled'
| 'llm.updateBackendConfig'
| 'mcp.client.cancelRequest'
| 'mcp.client.hasRequestResponded'
| 'mcp.close'
| 'mcp.connect'
| 'mcp.event.findMany'
| 'mcp.event.findNotifications'
| 'mcp.event.findPendingEvents'
| 'mcp.notification.rootListChange'
| 'mcp.notification.rootListChange'
| 'mcp.primitive.callTool'
| 'mcp.primitive.getPrompt'
| 'mcp.primitive.listPrompts'
| 'mcp.primitive.listResources'
| 'mcp.primitive.listResourceTemplates'
| 'mcp.primitive.listTools'
| 'mcp.primitive.readResource'
| 'mcp.primitive.subscribeResource'
| 'mcp.primitive.unsubscribeResource'
| 'mcp.readyState'
| 'multipartBufferToArray'
| 'onDefaultBrowserOAuthRedirect'
| 'open-channel-to-hidden-browser-window'
| 'openPath'
| 'parseImport'
| 'readCurlResponse'
| 'readDir'
| 'readOrCreateDataDir'
| 'restoreBackup'
| 'secretStorage.decryptString'
| 'secretStorage.deleteSecret'
| 'secretStorage.encryptString'
| 'secretStorage.getSecret'
| 'secretStorage.setSecret'
| 'secureReadFile'
| 'showOpenDialog'
| 'showSaveDialog'
| 'socketIO.event.findMany'
@@ -111,26 +131,7 @@ export type HandleChannels =
| 'webSocket.event.send'
| 'webSocket.open'
| 'webSocket.readyState'
| 'writeFile'
| 'mcp.connect'
| 'mcp.primitive.listTools'
| 'mcp.primitive.callTool'
| 'mcp.primitive.listPrompts'
| 'mcp.primitive.getPrompt'
| 'mcp.primitive.listResources'
| 'mcp.primitive.listResourceTemplates'
| 'mcp.primitive.readResource'
| 'mcp.primitive.subscribeResource'
| 'mcp.primitive.unsubscribeResource'
| 'mcp.notification.rootListChange'
| 'mcp.readyState'
| 'mcp.event.findMany'
| 'mcp.event.findNotifications'
| 'mcp.event.findPendingEvents'
| 'mcp.notification.rootListChange'
| 'mcp.client.hasRequestResponded'
| 'mcp.client.cancelRequest'
| 'mcp.close';
| 'writeFile';
export const ipcMainHandle = (
channel: HandleChannels,
@@ -154,6 +155,10 @@ export type MainOnChannels =
| 'manualUpdateCheck'
| 'openDeepLink'
| 'openInBrowser'
| 'path.basename'
| 'path.dirname'
| 'path.join'
| 'path.resolve'
| 'readText'
| 'restart'
| 'set-hidden-window-busy-status'
@@ -220,6 +225,7 @@ const getTemplateValue = (arg: NunjucksParsedTagArg) => {
}
return arg.defaultValue;
};
export function registerElectronHandlers() {
ipcMainOn(
'show-nunjucks-context-menu',

View File

@@ -19,6 +19,7 @@ import iconv from 'iconv-lite';
import { AI_PLUGIN_NAME } from '~/common/constants';
import { convert } from '~/main/importers/convert';
import { getCurrentConfig, type LLMConfigServiceAPI } from '~/main/llm-config-service';
import { multipartBufferToArray, type Part } from '~/main/multipart-buffer-to-array';
import { insecureReadFile, insecureReadFileWithEncoding, secureReadFile } from '~/main/secure-read-file';
import type { GenerateCommitsFromDiffFunction, MockRouteData, ModelConfig } from '~/plugins/types';
@@ -97,6 +98,7 @@ export interface RendererToMainBridgeAPI {
setMenuBarVisibility: (visible: boolean) => void;
installPlugin: typeof installPlugin;
parseImport: typeof convert;
multipartBufferToArray: (options: { bodyBuffer: Buffer; contentType: string }) => Promise<Part[]>;
writeFile: (options: { path: string; content: string }) => Promise<string>;
secureReadFile: (options: { path: string }) => Promise<string>;
insecureReadFile: (options: { path: string }) => Promise<string>;
@@ -183,6 +185,9 @@ export function registerMainHandlers() {
ipcMainHandle('database.caCertificate.create', async (_, options: { parentId: string; path: string }) => {
return models.caCertificate.create(options);
});
ipcMainHandle('multipartBufferToArray', async (_, options) => {
return multipartBufferToArray(options);
});
ipcMainOn('loginStateChange', async () => {
BrowserWindow.getAllWindows().forEach(w => {
w.webContents.send('loggedIn');

View File

@@ -0,0 +1,18 @@
import path from 'node:path';
import { ipcMainOn } from '~/main/ipc/electron';
export function registerPathHandlers() {
ipcMainOn('path.basename', (event, p: string) => {
event.returnValue = path.basename(p);
});
ipcMainOn('path.dirname', (event, p: string) => {
event.returnValue = path.dirname(p);
});
ipcMainOn('path.join', (event, ...paths: string[]) => {
event.returnValue = path.join(...paths);
});
ipcMainOn('path.resolve', (event, ...paths: string[]) => {
event.returnValue = path.resolve(...paths);
});
}

View File

@@ -0,0 +1,71 @@
import { PassThrough } from 'node:stream';
import multiparty from 'multiparty';
export interface Part {
id: number;
title: string;
name: string;
bytes: number;
value: Buffer;
filename: string | null;
headers: { name: string; value: string }[];
}
export function multipartBufferToArray({
bodyBuffer,
contentType,
}: {
bodyBuffer: Buffer | null;
contentType: string;
}): Promise<Part[]> {
return new Promise((resolve, reject) => {
const parts: Part[] = [];
if (!bodyBuffer) {
return resolve(parts);
}
const fakeReq = new PassThrough();
// @ts-expect-error -- TSCONVERSION investigate `stream` types
fakeReq.headers = {
'content-type': contentType,
};
const form = new multiparty.Form();
let id = 0;
form.on('part', part => {
const dataBuffers: any[] = [];
part.on('data', data => {
dataBuffers.push(data);
});
part.on('error', err => {
reject(new Error(`Failed to parse part: ${err.message}`));
});
part.on('end', () => {
const title = part.filename ? `${part.name} (${part.filename})` : part.name;
parts.push({
id,
title,
value: dataBuffers ? Buffer.concat(dataBuffers) : Buffer.from(''),
name: part.name,
filename: part.filename || null,
bytes: part.byteCount,
headers: Object.keys(part.headers).map(name => ({
name,
value: part.headers[name],
})),
});
id += 1;
});
});
form.on('error', err => {
reject(err);
});
form.on('close', () => {
resolve(parts);
});
// @ts-expect-error -- TSCONVERSION
form.parse(fakeReq);
fakeReq.write(bodyBuffer);
fakeReq.end();
});
}

View File

@@ -1 +0,0 @@
import './ui';

View File

@@ -1,5 +1,3 @@
import nodePath from 'node:path';
import React, { type HTMLAttributes, useCallback } from 'react';
import { selectFileOrFolder } from '../../../common/select-file-or-folder';
@@ -18,7 +16,7 @@ interface Props extends Omit<HTMLAttributes<HTMLButtonElement>, 'onChange'> {
export const FileInputButton = (props: Props) => {
const { showFileName, showFileIcon, path, name, onChange, itemtypes, extensions, disabled, ...extraProps } = props;
// NOTE: Basename fails if path is not a string, so let's make sure it is
const fileName = typeof path === 'string' ? nodePath.basename(path) : null;
const fileName = typeof path === 'string' ? window.path.basename(path) : null;
const _handleChooseFile = useCallback(async () => {
const { canceled, filePath } = await selectFileOrFolder({
itemTypes: itemtypes,

View File

@@ -1,5 +1,3 @@
import { readFile } from 'node:fs/promises';
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import React, { type FC } from 'react';
import { Button, Heading, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components';
@@ -60,7 +58,7 @@ export const DesignEmptyState: FC<Props> = ({ onImport }) => {
return;
}
const contents = String(await readFile(filePath));
const contents = String(await window.main.insecureReadFile({ path: filePath }));
onImport(contents);
},
},

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs';
import React, { type FC, useCallback } from 'react';
import { Button } from 'react-aria-components';
@@ -46,11 +44,11 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
if (!filePath) {
return;
}
const to = fs.createWriteStream(filePath);
to.on('error', err => {
console.warn('Failed to export har', err);
await window.main.writeFile({
path: filePath,
content: har,
});
to.end(har);
}, [activeRequest, activeResponse]);
const exportDebugFile = useCallback(async () => {
@@ -74,14 +72,11 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
if (canceled) {
return;
}
const readStream = models.response.getBodyStream(activeResponse);
if (readStream && filePath && typeof readStream !== 'string') {
const to = fs.createWriteStream(filePath);
to.write(headers);
readStream.pipe(to);
to.on('error', err => {
console.warn('Failed to save full response', err);
if (filePath && activeResponse.bodyBuffer) {
await window.main.writeFile({
path: filePath,
content: headers + '\n' + activeResponse.bodyBuffer.toString('utf8') || '',
});
}
}, [activeRequest, activeResponse]);

View File

@@ -1,8 +1,5 @@
import fs from 'node:fs';
import React, { type FC, useCallback } from 'react';
import * as misc from '../../../../common/misc';
import { FileInputButton } from '../../base/file-input-button';
import { PromptButton } from '../../base/prompt-button';
@@ -26,14 +23,6 @@ export const FileEditor: FC<Props> = ({ onChange, path }) => {
// Replace home path with ~/ to make the path shorter
const homeDirectory = window.app.getPath('home');
const pathDescription = path.replace(homeDirectory, '~');
let sizeDescription = '';
try {
const bytes = fs.statSync(path).size;
sizeDescription = misc.describeByteSize(bytes);
} catch {
sizeDescription = '';
}
return (
<div className="text-center">
@@ -43,8 +32,7 @@ export const FileEditor: FC<Props> = ({ onChange, path }) => {
<code className="txt-sm block">
<span className="force-wrap selectable" title={path}>
{pathDescription}
</span>{' '}
<span className="no-wrap">({sizeDescription})</span>
</span>
</code>
) : (
<code className="super-faint txt-sm block">No file selected</code>

View File

@@ -1,5 +1,3 @@
import { readFileSync } from 'node:fs';
import type { LintOptions, ShowHintOptions, TextMarker } from 'codemirror';
import type { GraphQLHintOptions } from 'codemirror-graphql/hint';
import type { GraphQLInfoOptions } from 'codemirror-graphql/info';
@@ -432,7 +430,7 @@ export const GraphQLEditor: FC<Props> = ({
}
try {
const filePath = filePaths[0]; // showOpenDialog is single select
const file = readFileSync(filePath);
const file = await window.main.insecureReadFile({ path: filePath });
const content = JSON.parse(file.toString());
if (!content.data) {
throw new Error('JSON file should have a data field with the introspection results');

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs';
import { CallToolResultSchema, ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { type RJSFSchema, type UiSchema } from '@rjsf/utils';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@@ -23,7 +21,6 @@ import {
useRequestLoaderData,
} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { CodeEditor, type CodeEditorHandle } from '../../components/.client/codemirror/code-editor';
import { showError } from '../../components/modals';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
@@ -63,20 +60,10 @@ export const MessageEventView = ({ event }: Props) => {
if (canceled || !outputPath) {
return;
}
const to = fs.createWriteStream(outputPath);
to.on('error', err => {
showError({
title: 'Save Failed',
message: 'Failed to save response body',
error: err,
});
await window.main.writeFile({
path: outputPath,
content: raw,
});
to.write(raw);
to.end();
}, [raw]);
const handleCopyResponseToClipboard = useCallback(() => {

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs';
import type * as Har from 'har-format';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { Button, Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-aria-components';
@@ -334,17 +332,19 @@ const PreviewModeDropdown = ({
icon="save"
label="Export raw response"
onClick={async () => {
const bodyBuffer = await models.response.getBodyBuffer(activeResponse);
const { canceled, filePath } = await window.dialog.showSaveDialog({
title: 'Save Full Response',
buttonLabel: 'Save',
defaultPath: `response-${Date.now()}.txt`,
});
if (canceled || !filePath || !bodyBuffer) {
if (canceled || !filePath || !activeResponse.bodyBuffer) {
return;
}
fs.promises.writeFile(filePath, bodyBuffer.toString('utf8'));
await window.main.writeFile({
path: filePath,
content: activeResponse.bodyBuffer?.toString('utf8') || '',
});
}}
/>
</DropdownItem>
@@ -364,7 +364,10 @@ const PreviewModeDropdown = ({
if (canceled || !filePath || !bodyBuffer) {
return;
}
fs.promises.writeFile(filePath, jsonPrettify(bodyBuffer.toString('utf8')));
await window.main.writeFile({
path: filePath,
content: jsonPrettify(activeResponse.bodyBuffer?.toString('utf8')) || '',
});
}}
/>
)}
@@ -389,7 +392,10 @@ const PreviewModeDropdown = ({
.map(v => v.value)
.join('');
fs.promises.writeFile(filePath, headers);
await window.main.writeFile({
path: filePath,
content: headers,
});
}}
/>
</DropdownItem>
@@ -411,7 +417,10 @@ const PreviewModeDropdown = ({
const data = await exportHarCurrentRequest(activeRequest, activeResponse);
const har = JSON.stringify(data, null, '\t');
fs.promises.writeFile(filePath, har);
await window.main.writeFile({
path: filePath,
content: har,
});
}}
/>
</DropdownItem>

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs/promises';
import React from 'react';
import {
Button,
@@ -76,8 +74,7 @@ export const MockRouteModal = ({
let body = '';
if (responseData?.bodyPath) {
try {
const bodyBuffer = await fs.readFile(responseData.bodyPath);
body = bodyBuffer.toString();
body = await window.main.secureReadFile({ path: responseData.bodyPath });
} catch (error) {
console.error('Failed to read response body:', error);
}

View File

@@ -1,5 +1,3 @@
import path from 'node:path';
import * as protoLoader from '@grpc/proto-loader';
import React, { type FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';
@@ -232,7 +230,7 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave }) => {
const protoText = await window.main.insecureReadFile({ path: filePath });
const updatedFile = await models.protoFile.update(protoFile, {
name: path.basename(filePath),
name: window.path.basename(filePath),
protoText,
});
const impacted = await models.grpcRequest.findByProtoFileId(updatedFile._id);
@@ -287,7 +285,7 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave }) => {
const protoText = await window.main.insecureReadFile({ path: filePath });
const newFile = await models.protoFile.create({
name: path.basename(filePath),
name: window.path.basename(filePath),
parentId: workspaceId,
protoText,
});

View File

@@ -1,5 +1,3 @@
import crypto from 'node:crypto';
import React, { type FC, useState } from 'react';
import { Toolbar } from 'react-aria-components';
@@ -59,9 +57,7 @@ export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
return Boolean(fuzzyMatch(resultFilter, result.testCase, { splitSpace: false, loose: true })?.indexes);
})
.map((result, i: number) => {
const key = crypto.createHash('sha1').update(`${result.testCase}"-${i}`).digest('hex');
.map(result => {
const statusText = {
passed: 'PASS',
failed: 'FAIL',
@@ -99,7 +95,7 @@ export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
: 'Unknown';
return (
<div key={key} data-testid="test-result-row">
<div key={result.testCase} data-testid="test-result-row">
<div className="my-3 flex w-full text-base">
<div className="m-auto mx-1 leading-4">
<span className="mr-2 ml-2">{statusTag}</span>

View File

@@ -1,10 +1,9 @@
import fs from 'node:fs';
import { extension as mimeExtension } from 'mime-types';
import React, { type FC, useCallback, useMemo } from 'react';
import { Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-aria-components';
import { useRootLoaderData } from '~/root';
import { jsonPrettify } from '~/utils/prettify/json';
import { PREVIEW_MODE_SOURCE } from '../../../common/constants';
import { getSetCookieHeaders } from '../../../common/misc';
@@ -14,14 +13,12 @@ import {
type RequestLoaderData,
useRequestLoaderData,
} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { jsonPrettify } from '../../../utils/prettify/json';
import { useExecutionState } from '../../hooks/use-execution-state';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
import { MockResponseExtractor } from '../editors/mock-response-extractor';
import { ErrorBoundary } from '../error-boundary';
import { showError } from '../modals';
import { ResponseTimer } from '../response-timer';
import { SizeTag } from '../tags/size-tag';
import { StatusTag } from '../tags/status-tag';
@@ -85,34 +82,14 @@ export const ResponsePane: FC<Props> = ({ activeRequestId }) => {
if (canceled) {
return;
}
const readStream = models.response.getBodyStream(activeResponse);
const dataBuffers: any[] = [];
if (readStream && outputPath && typeof readStream !== 'string') {
readStream.on('data', data => {
dataBuffers.push(data);
});
readStream.on('end', () => {
const to = fs.createWriteStream(outputPath);
const finalBuffer = Buffer.concat(dataBuffers);
to.on('error', err => {
showError({
title: 'Save Failed',
message: 'Failed to save response body',
error: err,
});
});
if (prettify && contentType.includes('json')) {
to.write(jsonPrettify(finalBuffer.toString('utf8')));
} else {
to.write(finalBuffer);
}
to.end();
if (prettify && contentType.includes('json')) {
await window.main.writeFile({
path: outputPath,
content: jsonPrettify(activeResponse.bodyBuffer?.toString('utf8')) || '',
});
return;
}
await window.main.writeFile({ path: outputPath, content: activeResponse.bodyBuffer?.toString('utf8') || '' });
},
[activeRequest, activeResponse],
);

View File

@@ -1,6 +1,3 @@
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { format } from 'date-fns';
import { getProductName } from 'insomnia/src/common/constants';
import { database } from 'insomnia/src/common/database';
@@ -108,7 +105,7 @@ const showSaveExportedFileDialog = async ({
const options = {
title: 'Export Insomnia Data',
buttonLabel: 'Export',
defaultPath: `${path.join(dir, `${name}_${date}`)}.${selectedFormat}`,
defaultPath: `${window.path.join(dir, `${name}_${date}`)}.${selectedFormat}`,
};
const { filePath } = await window.dialog.showSaveDialog(options);
return filePath || null;
@@ -131,8 +128,11 @@ const showSaveExportedFolderDialog = async () => {
async function writeExportedFileToFileSystem(filename: string, data: string) {
// Remember last exported path
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
await writeFile(filename, data);
window.localStorage.setItem('insomnia.lastExportPath', window.path.dirname(filename));
await window.main.writeFile({
path: filename,
content: data,
});
}
export const exportProjectToFile = (activeProjectName: string, workspacesForActiveProject: Workspace[]) => {
@@ -196,12 +196,14 @@ export const exportProjectToFile = (activeProjectName: string, workspacesForActi
}
const projectName = activeProjectName.replace(/ /g, '-');
const insomniaProjectExportFolder = path.join(dirPath, `insomnia-export.${projectName}.${Date.now()}`);
await mkdir(insomniaProjectExportFolder);
const insomniaProjectExportFolder = window.path.join(
dirPath,
`insomnia-export.${projectName}.${Date.now()}`,
);
for (const workspace of workspacesForActiveProject) {
const workspaceName = workspace.name.replace(/ /g, '-');
const fileName = path.join(insomniaProjectExportFolder, `${workspaceName}-${workspace._id}.yaml`);
const fileName = window.path.join(insomniaProjectExportFolder, `${workspaceName}-${workspace._id}.yaml`);
const stringifiedExport = await getInsomniaV5DataExport({
workspaceId: workspace._id,
includePrivateEnvironments: shouldExportPrivateEnvironments,
@@ -379,7 +381,7 @@ export async function exportWorkspaceData({
try {
const workspaceName = workspace.name.replace(/ /g, '-');
const filePath = path.join(dirPath, `${workspaceName}-${workspace._id}.yaml`);
const filePath = window.path.join(dirPath, `${workspaceName}-${workspace._id}.yaml`);
await writeExportedFileToFileSystem(filePath, insomniaExport);
} catch (error) {
console.error(error);
@@ -404,8 +406,7 @@ export async function exportAllData({ dirPath }: { dirPath: string }): Promise<v
includePrivateEnvironments = await showExportPrivateEnvironmentsModal();
}
const insomniaExportFolder = path.join(dirPath, `insomnia-export.${Date.now()}`);
await mkdir(insomniaExportFolder);
const insomniaExportFolder = window.path.join(dirPath, `insomnia-export.${Date.now()}`);
for (const workspace of workspacesWithoutMcp) {
await exportWorkspaceData({

View File

@@ -1,5 +1,3 @@
import nodePath from 'node:path';
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
import { Button, Input, Text } from 'react-aria-components';
import z from 'zod/v4';
@@ -41,8 +39,8 @@ export const GGUF = ({
});
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const userDataPath = nodePath.resolve(window.app.getPath('userData'));
const llmsFolder = nodePath.resolve(userDataPath, LLMS_FOLDER_NAME);
const userDataPath = window.path.resolve(window.app.getPath('userData'));
const llmsFolder = window.path.resolve(userDataPath, LLMS_FOLDER_NAME);
const [availableLLMs, setAvailableLLMs] = useState<string[]>([]);
const [selectedModel, setSelectedModel] = useState<string>('');
const refreshModelsDirectory = useCallback(() => {

View File

@@ -1,5 +1,3 @@
import nodePath from 'node:path';
import React, { type FC, useEffect, useState } from 'react';
import {
Button,
@@ -403,10 +401,9 @@ export const Plugins: FC = () => {
)}
>
{plugin => {
const link = nodePath.resolve(
plugin.name.startsWith('insomnia-plugin-') ? PLUGIN_HUB_BASE : NPM_PACKAGE_BASE,
plugin.name,
);
const link = plugin.name.startsWith('insomnia-plugin-')
? PLUGIN_HUB_BASE
: NPM_PACKAGE_BASE + '/' + plugin.name;
return (
<GridListItem
@@ -490,7 +487,7 @@ export const Plugins: FC = () => {
className="text-(--color-surprise) underline"
onPress={() =>
window.shell.showItemInFolder(
nodePath.resolve(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'), 'plugins'),
window.path.resolve(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'), 'plugins'),
)
}
>

View File

@@ -1,5 +1,3 @@
import { URL } from 'node:url';
import React, { type FC, Fragment, useMemo } from 'react';
import type { ResponseHeader } from '../../../models/response';

View File

@@ -1,32 +1,18 @@
import fs from 'node:fs';
import path from 'node:path';
import { PassThrough } from 'node:stream';
import { format } from 'date-fns';
import type { SaveDialogOptions } from 'electron';
import { extension as mimeExtension } from 'mime-types';
import multiparty from 'multiparty';
import React, { type FC, useCallback, useEffect, useState } from 'react';
import { Button } from 'react-aria-components';
import type { Part } from '~/main/multipart-buffer-to-array';
import { getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY } from '../../../common/constants';
import type { ResponseHeader } from '../../../models/response';
import { Dropdown, DropdownItem, ItemContent } from '../base/dropdown';
import { showModal } from '../modals/index';
import { WrapperModal } from '../modals/wrapper-modal';
import { ResponseHeadersViewer } from './response-headers-viewer';
import { ResponseViewer } from './response-viewer';
interface Part {
id: number;
title: string;
name: string;
bytes: number;
value: Buffer;
filename: string | null;
headers: ResponseHeader[];
}
interface Props {
download: (...args: any[]) => any;
responseId: string;
@@ -58,8 +44,11 @@ export const ResponseMultipartViewer: FC<Props> = ({
useEffect(() => {
const init = async () => {
if (!bodyBuffer || !contentType) {
return;
}
try {
const parts = await multipartBufferToArray({ bodyBuffer, contentType });
const parts = await window.main.multipartBufferToArray({ bodyBuffer, contentType });
setParts(parts);
setSelectedPart(parts[0]);
} catch (err) {
@@ -96,7 +85,7 @@ export const ResponseMultipartViewer: FC<Props> = ({
const options: SaveDialogOptions = {
title: 'Save as File',
buttonLabel: 'Save',
defaultPath: path.join(dir, filename),
defaultPath: window.path.join(dir, filename),
filters: [
// @ts-expect-error https://github.com/electron/electron/pull/29322
{
@@ -111,11 +100,13 @@ export const ResponseMultipartViewer: FC<Props> = ({
}
// Remember last exported path
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
window.localStorage.setItem('insomnia.lastExportPath', window.path.dirname(filename));
// Save the file
try {
await fs.promises.writeFile(filePath, selectedPart.value);
await window.main.writeFile({
path: filePath,
content: selectedPart.value.toString('utf8'),
});
} catch (err) {
console.warn('Failed to save multipart to file', err);
}
@@ -217,62 +208,3 @@ export const ResponseMultipartViewer: FC<Props> = ({
</div>
);
};
function multipartBufferToArray({
bodyBuffer,
contentType,
}: {
bodyBuffer: Buffer | null;
contentType: string;
}): Promise<Part[]> {
return new Promise((resolve, reject) => {
const parts: Part[] = [];
if (!bodyBuffer) {
return resolve(parts);
}
const fakeReq = new PassThrough();
// @ts-expect-error -- TSCONVERSION investigate `stream` types
fakeReq.headers = {
'content-type': contentType,
};
const form = new multiparty.Form();
let id = 0;
form.on('part', part => {
const dataBuffers: any[] = [];
part.on('data', data => {
dataBuffers.push(data);
});
part.on('error', err => {
reject(new Error(`Failed to parse part: ${err.message}`));
});
part.on('end', () => {
const title = part.filename ? `${part.name} (${part.filename})` : part.name;
parts.push({
id,
title,
value: dataBuffers ? Buffer.concat(dataBuffers) : Buffer.from(''),
name: part.name,
filename: part.filename || null,
bytes: part.byteCount,
headers: Object.keys(part.headers).map(name => ({
name,
value: part.headers[name],
})),
});
id += 1;
});
});
form.on('error', err => {
reject(err);
});
form.on('close', () => {
resolve(parts);
});
// @ts-expect-error -- TSCONVERSION
form.parse(fakeReq);
fakeReq.write(bodyBuffer);
fakeReq.end();
});
}

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs';
import React, { type FC, useCallback, useRef } from 'react';
import { useParams } from 'react-router';
@@ -11,7 +9,6 @@ import type { SocketIOEvent } from '../../../main/network/socket-io';
import type { WebSocketEvent, WebSocketMessageEvent } from '../../../main/network/websocket';
import { useRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { showError } from '../modals';
import { WebSocketPreviewModeDropdown } from './websocket-preview-dropdown';
interface Props<T> {
@@ -42,20 +39,10 @@ export const MessageEventView: FC<Props<CurlMessageEvent | WebSocketMessageEvent
if (canceled || !outputPath) {
return;
}
const to = fs.createWriteStream(outputPath);
to.on('error', err => {
showError({
title: 'Save Failed',
message: 'Failed to save response body',
error: err,
});
await window.main.writeFile({
path: outputPath,
content: raw,
});
to.write(raw);
to.end();
}, [raw]);
const handleCopyResponseToClipboard = useCallback(() => {

View File

@@ -1,5 +1,3 @@
import fs from 'node:fs';
import classnames from 'classnames';
import React, { type FC, useEffect, useMemo, useState } from 'react';
import { Button, Input, SearchField, Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
@@ -206,18 +204,10 @@ const RealtimeActiveResponsePane: FC<RealtimeActiveResponsePaneProps & { readySt
useEffect(() => {
let isMounted = true;
const fn = async () => {
try {
await fs.promises.stat(response.timelinePath);
} catch (err) {
if (err.code === 'ENOENT') {
return setTimeline([]);
}
}
// allow to read the file as it is chosen by user
const content = await window.main.secureReadFile({
path: response.timelinePath,
});
const timelineParsed = deserializeNDJSON(content);
if (isMounted) {
setTimeline(timelineParsed);

View File

@@ -12,6 +12,12 @@ declare global {
shell: Pick<Electron.Shell, 'showItemInFolder' | 'openPath'>;
clipboard: Pick<Electron.Clipboard, 'readText' | 'writeText' | 'clear'>;
webUtils: Pick<Electron.WebUtils, 'getPathForFile'>;
path: {
resolve: (...paths: string[]) => string;
dirname: (p: string) => string;
basename: (p: string) => string;
join: (...paths: string[]) => string;
};
showAlert: (options?: Record<string, any>) => void;
showWrapper: (options?: Record<string, any>) => void;
showPrompt: (options?: Record<string, any>) => void;

View File

@@ -70,7 +70,7 @@ export default defineConfig(({ mode }) => {
},
};
});
let totalWarnings = 0;
function DetectNodeBuiltinImports() {
const builtins = new Set(builtinModules);
@@ -85,7 +85,8 @@ function DetectNodeBuiltinImports() {
// If the import target is a Node builtin module
if (builtins.has(source) || builtins.has(source.replace('virtual:external:node:', ''))) {
const file = path.relative(process.cwd(), importer);
console.warn(`⚠️ File "${file}" imports Node builtin module "${source}"`);
totalWarnings += 1;
console.warn(`⚠️ ${totalWarnings} File "${file}" imports Node builtin module "${source}"`);
}
return null; // Let Vite handle the actual resolution