mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-04 20:14:55 -04:00
refactor: break main-process circular imports (git-service/plugin-window/templating-worker-database)
This commit is contained in:
18 files changed
+123
-55
No files matched your search
@@ -42,6 +42,7 @@ vi.mock('electron', () => ({
|
||||
|
||||
vi.mock('../window-utils', () => ({
|
||||
getMainWindow: vi.fn(() => fakeMainWindow),
|
||||
registerPluginWindowControls: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../prompt-bridge', () => ({
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
@@ -37,6 +37,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
@@ -30,6 +30,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
@@ -30,6 +30,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
@@ -33,6 +33,7 @@ vi.mock('~/plugins', () => ({
|
||||
getPluginCommonContext: vi.fn(),
|
||||
getTemplateTags: vi.fn().mockResolvedValue([]),
|
||||
getPlugins: vi.fn().mockResolvedValue([]),
|
||||
registerUserPluginExportDiscovery: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/common/cookies', () => ({ jarFromCookies: vi.fn() }));
|
||||
vi.mock('../common/database', () => ({ database: {} }));
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
type ProviderEmail,
|
||||
type ProviderRepository,
|
||||
} from '~/sync/git/providers';
|
||||
import type { FileIssue, FileIssueKind } from '~/sync/git/repo-file-watcher';
|
||||
import type { FileIssue, WorkspaceFileIssue } from '~/sync/git/repo-file-watcher';
|
||||
|
||||
import { INSOMNIA_GITLAB_API_URL } from '../common/constants';
|
||||
import { database } from '../common/database';
|
||||
@@ -174,14 +174,6 @@ export function vcsEventProperties(type: 'git', action: VCSAction, error?: strin
|
||||
return { type, action, error };
|
||||
}
|
||||
|
||||
export interface WorkspaceFileIssue {
|
||||
workspaceId: string;
|
||||
gitRepositoryId: string;
|
||||
relPath: string;
|
||||
kind: FileIssueKind;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface GetProjectGitFileIssuesOptions {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
|
||||
@@ -3,8 +3,10 @@ import path from 'node:path';
|
||||
|
||||
import { app, BrowserWindow, ipcMain, type IpcMainEvent, type IpcMainInvokeEvent } from 'electron';
|
||||
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
|
||||
import { requestPromptFromRenderer } from './prompt-bridge';
|
||||
import { getMainWindow } from './window-utils';
|
||||
import { getMainWindow, registerPluginWindowControls } from './window-utils';
|
||||
|
||||
let pluginWindow: BrowserWindow | null = null;
|
||||
let windowReady = false;
|
||||
@@ -16,6 +18,19 @@ const pendingRequests = new Map<
|
||||
let cachedHasRequestHooks: boolean | null = null;
|
||||
let cachedHasResponseHooks: boolean | null = null;
|
||||
|
||||
// Registered at module load so `window-utils` (its dev menu and main-window load hook) can drive
|
||||
// the plugin window without importing this module, which would be a circular dependency.
|
||||
registerPluginWindowControls({
|
||||
toggleVisibility: () => {
|
||||
invariant(pluginWindow, 'pluginWindow is not defined');
|
||||
pluginWindow.isVisible() ? pluginWindow.hide() : pluginWindow.show();
|
||||
},
|
||||
destroyOrCreate: () => {
|
||||
pluginWindow ? destroyPluginWindow() : createPluginWindow();
|
||||
},
|
||||
createAfterMainLoad: () => createPluginWindow(),
|
||||
});
|
||||
|
||||
// Bridge observability counters. Kept in-memory and exposed via the
|
||||
// `plugins.getBridgeMetrics` IPC handler so devs / smoke tests / support
|
||||
// dumps can read the live state without scraping logs.
|
||||
|
||||
@@ -20,7 +20,12 @@ import type {
|
||||
PluginTemplateTagContext,
|
||||
PluginToMainAPIPaths,
|
||||
} from '~/common/templating/types';
|
||||
import { getPluginCommonContext, getPlugins, getTemplateTags } from '~/plugins';
|
||||
import {
|
||||
getPluginCommonContext,
|
||||
getPlugins,
|
||||
getTemplateTags,
|
||||
registerUserPluginExportDiscovery,
|
||||
} from '~/plugins';
|
||||
import {
|
||||
HOOK_REQUEST_FIELDS,
|
||||
type PluginExportManifest,
|
||||
@@ -472,6 +477,11 @@ export const discoverUserPluginExportsForLoader = async (body: {
|
||||
});
|
||||
};
|
||||
|
||||
// Register the sandbox-backed user-plugin discovery with the plugin loader (plugins/index.ts),
|
||||
// which can't import this main-only module without creating a circular dependency. Loading this
|
||||
// module in main always precedes plugin discovery, since getPlugins/getTemplateTags live here.
|
||||
registerUserPluginExportDiscovery(discoverUserPluginExportsForLoader);
|
||||
|
||||
const pickHookRequestFields = (req: Record<string, any>): Record<string, any> => {
|
||||
const out: Record<string, any> = {};
|
||||
for (const key of HOOK_REQUEST_FIELDS) {
|
||||
|
||||
@@ -25,7 +25,6 @@ import { docsBase } from '../common/documentation';
|
||||
import { getElectronStorage } from './electron-storage';
|
||||
import { ipcMainOn } from './ipc/electron';
|
||||
import { getLogDirectory } from './log';
|
||||
import { createPluginWindow, destroyPluginWindow, getPluginWindow } from './plugin-window';
|
||||
import { isTrustedAppOrigin } from './trusted-origin';
|
||||
import { MAIN_WINDOW_SECURITY } from './window-security';
|
||||
|
||||
@@ -34,9 +33,26 @@ const DEFAULT_HEIGHT = 720;
|
||||
const MINIMUM_WIDTH = 500;
|
||||
const MINIMUM_HEIGHT = 400;
|
||||
const browserWindows = new Map<'Insomnia' | 'HiddenBrowserWindow', ElectronBrowserWindow>();
|
||||
|
||||
// The plugin window lives in `plugin-window`, which depends on `getMainWindow` below — importing
|
||||
// it from here would be a circular dependency. Instead, `plugin-window` registers its controls
|
||||
// at module load and this module calls through the registration.
|
||||
interface PluginWindowControls {
|
||||
toggleVisibility: () => void;
|
||||
destroyOrCreate: () => void;
|
||||
createAfterMainLoad: () => void;
|
||||
}
|
||||
|
||||
let pluginWindowControls: PluginWindowControls | null = null;
|
||||
|
||||
export function registerPluginWindowControls(controls: PluginWindowControls): void {
|
||||
pluginWindowControls = controls;
|
||||
}
|
||||
|
||||
export function getMainWindow(): ElectronBrowserWindow | null {
|
||||
return browserWindows.get('Insomnia') ?? null;
|
||||
}
|
||||
|
||||
let hiddenWindowIsBusy = false;
|
||||
interface Bounds {
|
||||
height?: number;
|
||||
@@ -658,16 +674,15 @@ export function createWindow(): ElectronBrowserWindow {
|
||||
{
|
||||
label: 'Show/hide plugin browser window ',
|
||||
click: () => {
|
||||
const pluginWindow = getPluginWindow();
|
||||
invariant(pluginWindow, 'pluginWindow is not defined');
|
||||
pluginWindow.isVisible() ? pluginWindow.hide() : pluginWindow.show();
|
||||
invariant(pluginWindowControls, 'pluginWindow is not defined');
|
||||
pluginWindowControls.toggleVisibility();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Stop/start plugin browser window ',
|
||||
click: () => {
|
||||
const pluginWindow = getPluginWindow();
|
||||
pluginWindow ? destroyPluginWindow() : createPluginWindow();
|
||||
invariant(pluginWindowControls, 'pluginWindow is not defined');
|
||||
pluginWindowControls.destroyOrCreate();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -852,8 +867,9 @@ export function createWindowsAndReturnMain() {
|
||||
// that Playwright's firstWindow() always returns the main app window. Creating
|
||||
// it on did-finish-load still parses the 12 MB bundle well before any user
|
||||
// plugin call would occur.
|
||||
mainWindow.webContents.once('did-finish-load', () => createPluginWindow());
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
invariant(pluginWindowControls, 'pluginWindow controls are not registered');
|
||||
pluginWindowControls.createAfterMainLoad();
|
||||
});
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export { destroyPluginWindow };
|
||||
+28
-23
@@ -22,31 +22,36 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { services } from 'insomnia-data';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
vi.mock('~/main/templating-worker-database', () => ({
|
||||
discoverUserPluginExportsForLoader: async () => {
|
||||
// Stands in for the real function's eventual `await import('../templating/sandbox/
|
||||
// plugin-tag-sandbox')` → `getQuickJSModule()` call chain, without needing to also mock
|
||||
// `buildSandboxBridge`/`readPluginModuleMap`/`electron.app.getVersion()` for no added
|
||||
// evidentiary value — the property under test is what `getQuickJSModule()` itself resolves to,
|
||||
// not the rest of real discovery's plumbing.
|
||||
const { getQuickJSModule } = await import('~/templating/sandbox/quickjs-runtime');
|
||||
(globalThis as any).__sandboxedPluginResolvedQuickJSModule = await getQuickJSModule();
|
||||
return {
|
||||
templateTags: [],
|
||||
requestHooks: 0,
|
||||
responseHooks: 0,
|
||||
requestActions: [],
|
||||
requestGroupActions: [],
|
||||
workspaceActions: [],
|
||||
documentActions: [],
|
||||
themes: [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
// plugins/index.ts no longer dynamically imports `~/main/templating-worker-database`; the main
|
||||
// process now hands it the sandbox-backed discovery via `registerUserPluginExportDiscovery`. Mock
|
||||
// that module would never register anything, so register a stub discovery through the real
|
||||
// registration hook instead — standing in for the real function's eventual
|
||||
// `await import('../templating/sandbox/plugin-tag-sandbox')` → `getQuickJSModule()` call chain,
|
||||
// without needing to also mock `buildSandboxBridge`/`readPluginModuleMap`/`electron.app.getVersion()`
|
||||
// for no added evidentiary value — the property under test is what `getQuickJSModule()` itself
|
||||
// resolves to, not the rest of real discovery's plumbing.
|
||||
import { _testOnlySetPlugins, getPlugins, registerUserPluginExportDiscovery } from '../index';
|
||||
|
||||
import { _testOnlySetPlugins, getPlugins } from '../index';
|
||||
registerUserPluginExportDiscovery(async () => {
|
||||
// Deliberately dynamic: this test's premise is that the process's first-ever
|
||||
// `quickjs-emscripten` resolution happens *during* the sandboxed sibling's discovery call, so a
|
||||
// static import would defeat what's being proven. The assignment below avoids a cast; the test
|
||||
// assertions read the flag back off `globalThis`.
|
||||
const { getQuickJSModule } = await import('~/templating/sandbox/quickjs-runtime');
|
||||
Object.assign(globalThis, { __sandboxedPluginResolvedQuickJSModule: await getQuickJSModule() });
|
||||
return {
|
||||
templateTags: [],
|
||||
requestHooks: 0,
|
||||
responseHooks: 0,
|
||||
requestActions: [],
|
||||
requestGroupActions: [],
|
||||
workspaceActions: [],
|
||||
documentActions: [],
|
||||
themes: [],
|
||||
};
|
||||
});
|
||||
|
||||
const originalProcessType = process.type;
|
||||
|
||||
|
||||
@@ -64,6 +64,21 @@ export async function init() {
|
||||
// discovery has to reach the sandbox from either. In main we call the sandbox directly — the
|
||||
// `insomnia-templating-worker-database://` protocol is a renderer<->main channel and main's own
|
||||
// `fetch` can't resolve it. In a renderer (the plugin window) we go over that protocol.
|
||||
type UserPluginExportDiscovery = (body: {
|
||||
directory: string;
|
||||
name: string;
|
||||
permissions?: { modules?: string[]; capabilities?: string[] };
|
||||
}) => Promise<PluginExportManifest>;
|
||||
|
||||
// The main process registers the sandbox-backed discovery here at module load; importing
|
||||
// `main/templating-worker-database` directly would be a circular dependency (that module loads
|
||||
// this one for getPlugins/getTemplateTags).
|
||||
let discoverUserPluginExportsInMain: UserPluginExportDiscovery | null = null;
|
||||
|
||||
export function registerUserPluginExportDiscovery(discover: UserPluginExportDiscovery): void {
|
||||
discoverUserPluginExportsInMain = discover;
|
||||
}
|
||||
|
||||
async function discoverUserPluginExports(
|
||||
directory: string,
|
||||
name: string,
|
||||
@@ -73,8 +88,10 @@ async function discoverUserPluginExports(
|
||||
if (__IS_RENDERER__) {
|
||||
return (await fetchFromTemplateWorkerDatabase('plugin.discoverUserPluginExports', body)) as PluginExportManifest;
|
||||
}
|
||||
const { discoverUserPluginExportsForLoader } = await import('~/main/templating-worker-database');
|
||||
return discoverUserPluginExportsForLoader(body);
|
||||
if (!discoverUserPluginExportsInMain) {
|
||||
throw new Error('User plugin export discovery is not registered; main/templating-worker-database did not load in the main process');
|
||||
}
|
||||
return discoverUserPluginExportsInMain(body);
|
||||
}
|
||||
|
||||
function buildUserPluginModuleFromManifest(pluginName: string, manifest: PluginExportManifest): Plugin['module'] {
|
||||
|
||||
@@ -44,8 +44,6 @@ import path from 'node:path';
|
||||
import type { BaseModel, Workspace, WorkspaceMeta } from 'insomnia-data';
|
||||
import { models, services } from 'insomnia-data';
|
||||
|
||||
import type { WorkspaceFileIssue } from '~/main/git-service';
|
||||
|
||||
import { database as db } from '../../common/database';
|
||||
import { InsomniaFileTypeValues } from '../../common/import-v5-parser';
|
||||
import { getInsomniaV5DataExport, tryImportV5Data } from '../../common/insomnia-v5';
|
||||
@@ -69,6 +67,19 @@ export interface FileIssue {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file issue scoped to its workspace and git repository, as reported over IPC to the
|
||||
* renderer. Lives here (not in `main/git-service`) to avoid a circular dependency:
|
||||
* git-service loads this module for the watcher registry.
|
||||
*/
|
||||
export interface WorkspaceFileIssue {
|
||||
workspaceId: string;
|
||||
gitRepositoryId: string;
|
||||
relPath: string;
|
||||
kind: FileIssueKind;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface FileProblemsChangedPayload {
|
||||
repoId: string;
|
||||
problems: FileIssue[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { useCallback } from 'react';
|
||||
import { Button, GridListItem } from 'react-aria-components';
|
||||
|
||||
import { scopeToBgColorMap, scopeToIconMap, scopeToTextColorMap } from '~/common/get-workspace-label';
|
||||
import type { WorkspaceFileIssue } from '~/main/git-service';
|
||||
import type { WorkspaceFileIssue } from '~/sync/git/repo-file-watcher';
|
||||
import { scrollElementIntoView } from '~/ui/utils';
|
||||
|
||||
import { type BaseTab, TAB_CONTEXT_MENU_COMMAND, type TabType, useInsomniaTabContext } from '../../context/app/insomnia-tab-context';
|
||||
|
||||
@@ -10,8 +10,7 @@ import React, {
|
||||
} from 'react';
|
||||
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import type { WorkspaceFileIssue } from '~/main/git-service';
|
||||
import type { FileProblemsChangedPayload } from '~/sync/git/repo-file-watcher';
|
||||
import type { FileProblemsChangedPayload, WorkspaceFileIssue } from '~/sync/git/repo-file-watcher';
|
||||
|
||||
const mapIssuesByWorkspaceId = (issues: WorkspaceFileIssue[]) => {
|
||||
return Object.fromEntries(issues.map(issue => [issue.workspaceId, issue])) as Record<string, WorkspaceFileIssue>;
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
"insomnia": [
|
||||
"packages/insomnia/src/common/import.ts -> packages/insomnia/src/common/insomnia-v5.ts",
|
||||
"packages/insomnia/src/common/render.ts -> packages/insomnia/src/network/network.ts",
|
||||
"packages/insomnia/src/main/git-service.ts -> packages/insomnia/src/sync/git/repo-file-watcher.ts",
|
||||
"packages/insomnia/src/main/plugin-window.ts -> packages/insomnia/src/main/prompt-bridge.ts -> packages/insomnia/src/main/window-utils.ts",
|
||||
"packages/insomnia/src/main/plugin-window.ts -> packages/insomnia/src/main/window-utils.ts",
|
||||
"packages/insomnia/src/main/templating-worker-database.ts -> packages/insomnia/src/plugins/index.ts",
|
||||
"packages/insomnia/src/main/templating-worker-database.ts -> packages/insomnia/src/templating/index.ts -> packages/insomnia/src/plugins/index.ts",
|
||||
"packages/insomnia/src/sync/git/providers/github.ts -> packages/insomnia/src/sync/git/utils.ts -> packages/insomnia/src/sync/git/providers/index.ts",
|
||||
"packages/insomnia/src/sync/git/providers/gitlab.ts -> packages/insomnia/src/sync/git/utils.ts -> packages/insomnia/src/sync/git/providers/index.ts",
|
||||
"packages/insomnia/src/ui/components/modals/export-requests-modal.tsx -> packages/insomnia/src/ui/components/settings/import-export.tsx",
|
||||
|
||||
Reference in new issue
Block a user