diff --git a/eslint.config.mjs b/eslint.config.mjs index e16a18e414..fce7e8c86a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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' }], diff --git a/packages/insomnia/src/entry.main.ts b/packages/insomnia/src/entry.main.ts index 62a948c7c1..ff7bd1122c 100644 --- a/packages/insomnia/src/entry.main.ts +++ b/packages/insomnia/src/entry.main.ts @@ -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(); diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 0d58669a87..ebe6188508 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -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; } diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 04fa9ca4d1..033fe85ba8 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -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', diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index be2b164833..f3806656e2 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -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; writeFile: (options: { path: string; content: string }) => Promise; secureReadFile: (options: { path: string }) => Promise; insecureReadFile: (options: { path: string }) => Promise; @@ -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'); diff --git a/packages/insomnia/src/main/ipc/path.ts b/packages/insomnia/src/main/ipc/path.ts new file mode 100644 index 0000000000..fab2ca3871 --- /dev/null +++ b/packages/insomnia/src/main/ipc/path.ts @@ -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); + }); +} diff --git a/packages/insomnia/src/main/multipart-buffer-to-array.ts b/packages/insomnia/src/main/multipart-buffer-to-array.ts new file mode 100644 index 0000000000..e1bcdef99a --- /dev/null +++ b/packages/insomnia/src/main/multipart-buffer-to-array.ts @@ -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 { + 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(); + }); +} diff --git a/packages/insomnia/src/renderer.ts b/packages/insomnia/src/renderer.ts deleted file mode 100644 index 3d440125a6..0000000000 --- a/packages/insomnia/src/renderer.ts +++ /dev/null @@ -1 +0,0 @@ -import './ui'; diff --git a/packages/insomnia/src/ui/components/base/file-input-button.tsx b/packages/insomnia/src/ui/components/base/file-input-button.tsx index a2db78f5fa..e4127044c4 100644 --- a/packages/insomnia/src/ui/components/base/file-input-button.tsx +++ b/packages/insomnia/src/ui/components/base/file-input-button.tsx @@ -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, '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, diff --git a/packages/insomnia/src/ui/components/design-empty-state.tsx b/packages/insomnia/src/ui/components/design-empty-state.tsx index a3450ccba0..07abf1dd96 100644 --- a/packages/insomnia/src/ui/components/design-empty-state.tsx +++ b/packages/insomnia/src/ui/components/design-empty-state.tsx @@ -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 = ({ onImport }) => { return; } - const contents = String(await readFile(filePath)); + const contents = String(await window.main.insecureReadFile({ path: filePath })); onImport(contents); }, }, diff --git a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx index 398ca5f3f3..b67c814dc7 100644 --- a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx @@ -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 = ({ 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 = ({ 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]); diff --git a/packages/insomnia/src/ui/components/editors/body/file-editor.tsx b/packages/insomnia/src/ui/components/editors/body/file-editor.tsx index a978efec95..b571910806 100644 --- a/packages/insomnia/src/ui/components/editors/body/file-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/file-editor.tsx @@ -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 = ({ 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 (
@@ -43,8 +32,7 @@ export const FileEditor: FC = ({ onChange, path }) => { {pathDescription} - {' '} - ({sizeDescription}) + ) : ( No file selected diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index e8051051f2..37d0881b59 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -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 = ({ } 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'); diff --git a/packages/insomnia/src/ui/components/mcp/event-view.tsx b/packages/insomnia/src/ui/components/mcp/event-view.tsx index 3a94695032..d825211d29 100644 --- a/packages/insomnia/src/ui/components/mcp/event-view.tsx +++ b/packages/insomnia/src/ui/components/mcp/event-view.tsx @@ -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(() => { diff --git a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx index f23d3679a9..d016533d9f 100644 --- a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx +++ b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx @@ -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') || '', + }); }} /> @@ -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, + }); }} /> @@ -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, + }); }} /> diff --git a/packages/insomnia/src/ui/components/modals/mock-route-modal.tsx b/packages/insomnia/src/ui/components/modals/mock-route-modal.tsx index d03aa82b6c..fb189886ee 100644 --- a/packages/insomnia/src/ui/components/modals/mock-route-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/mock-route-modal.tsx @@ -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); } diff --git a/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx b/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx index 519d502931..3c1150e77a 100644 --- a/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/proto-files-modal.tsx @@ -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 = ({ 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 = ({ 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, }); diff --git a/packages/insomnia/src/ui/components/panes/request-test-result-pane.tsx b/packages/insomnia/src/ui/components/panes/request-test-result-pane.tsx index bb0dc87af4..f456b973c1 100644 --- a/packages/insomnia/src/ui/components/panes/request-test-result-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-test-result-pane.tsx @@ -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 = ({ 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 = ({ : 'Unknown'; return ( -
+
{statusTag} diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index dfec3bdfb3..dead53b4b2 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -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 = ({ 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], ); diff --git a/packages/insomnia/src/ui/components/settings/import-export.tsx b/packages/insomnia/src/ui/components/settings/import-export.tsx index 021cdd2e8e..eacbfbaae8 100644 --- a/packages/insomnia/src/ui/components/settings/import-export.tsx +++ b/packages/insomnia/src/ui/components/settings/import-export.tsx @@ -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([]); const [selectedModel, setSelectedModel] = useState(''); const refreshModelsDirectory = useCallback(() => { diff --git a/packages/insomnia/src/ui/components/settings/plugins.tsx b/packages/insomnia/src/ui/components/settings/plugins.tsx index c8f2d14bc0..ade6816f81 100644 --- a/packages/insomnia/src/ui/components/settings/plugins.tsx +++ b/packages/insomnia/src/ui/components/settings/plugins.tsx @@ -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 ( { 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'), ) } > diff --git a/packages/insomnia/src/ui/components/viewers/response-headers-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-headers-viewer.tsx index 919f16b7f7..4df2f59f5f 100644 --- a/packages/insomnia/src/ui/components/viewers/response-headers-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-headers-viewer.tsx @@ -1,5 +1,3 @@ -import { URL } from 'node:url'; - import React, { type FC, Fragment, useMemo } from 'react'; import type { ResponseHeader } from '../../../models/response'; diff --git a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx index ba0ce01bc4..234d6debea 100644 --- a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx @@ -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 = ({ 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 = ({ 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 = ({ } // 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 = ({
); }; - -function multipartBufferToArray({ - bodyBuffer, - contentType, -}: { - bodyBuffer: Buffer | null; - contentType: string; -}): Promise { - 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(); - }); -} diff --git a/packages/insomnia/src/ui/components/websockets/event-view.tsx b/packages/insomnia/src/ui/components/websockets/event-view.tsx index 7e1bbbf103..33dadd92cb 100644 --- a/packages/insomnia/src/ui/components/websockets/event-view.tsx +++ b/packages/insomnia/src/ui/components/websockets/event-view.tsx @@ -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 { @@ -42,20 +39,10 @@ export const MessageEventView: FC { - 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(() => { diff --git a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx index c262b852a9..d35c1b0f73 100644 --- a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx @@ -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 { 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); diff --git a/packages/insomnia/types/global.d.ts b/packages/insomnia/types/global.d.ts index 0a987f7494..745c25639f 100644 --- a/packages/insomnia/types/global.d.ts +++ b/packages/insomnia/types/global.d.ts @@ -12,6 +12,12 @@ declare global { shell: Pick; clipboard: Pick; webUtils: Pick; + path: { + resolve: (...paths: string[]) => string; + dirname: (p: string) => string; + basename: (p: string) => string; + join: (...paths: string[]) => string; + }; showAlert: (options?: Record) => void; showWrapper: (options?: Record) => void; showPrompt: (options?: Record) => void; diff --git a/packages/insomnia/vite.config.ts b/packages/insomnia/vite.config.ts index d78c923632..8f6a2f1627 100644 --- a/packages/insomnia/vite.config.ts +++ b/packages/insomnia/vite.config.ts @@ -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