This commit introduces native operating system file dialogs in the desktop application replacing the existing web-based dialogs. It lays the foundation for future enhancements such as: - Providing error messages when saving or executing files, addressing #264. - Creating system restore points, addressing #50. Documentation updates: - Update `desktop-vs-web-features.md` with added functionality. - Update `README.md` with security feature highlights. - Update home page documentation to emphasize security features. Other supporting changes include: - Integrate IPC communication channels for secure Electron dialog API interactions. - Refactor `IpcRegistration` for more type-safety and simplicity. - Introduce a Vue hook to encapsulate dialog functionality. - Improve errors during IPC registration for easier troubleshooting. - Move `ClientLoggerFactory` for consistency in hooks organization and remove `LoggerFactory` interface for simplicity. - Add tests for the save file dialog in the browser context. - Add `Blob` polyfill in tests to compensate for the missing `blob.text()` function in `jsdom` (see jsdom/jsdom#2555). Improve environment detection logic: - Treat test environment as browser environments to correctly activate features based on the environment. This resolves issues where the environment is misidentified as desktop, but Electron preloader APIs are missing. - Rename `isDesktop` environment identification variable to `isRunningAsDesktopApplication` for better clarity and to avoid confusion with desktop environments in web/browser/test environments. - Simplify `BrowserRuntimeEnvironment` to consistently detect non-desktop application environments. - Improve environment detection for Electron main process (electron/electron#2288).
This commit is contained in:
@@ -11,9 +11,10 @@ import {
|
||||
} from '@/presentation/injectionSymbols';
|
||||
import { PropertyKeys } from '@/TypeHelpers';
|
||||
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||
import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
|
||||
import { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
|
||||
import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
||||
|
||||
export function provideDependencies(
|
||||
context: IApplicationContext,
|
||||
@@ -67,6 +68,10 @@ export function provideDependencies(
|
||||
InjectionKeys.useCodeRunner,
|
||||
useCodeRunner,
|
||||
),
|
||||
useDialog: (di) => di.provide(
|
||||
InjectionKeys.useDialog,
|
||||
useDialog,
|
||||
),
|
||||
};
|
||||
registerAll(Object.values(resolvers), api);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ClientLoggerFactory } from '@/presentation/components/Shared/Hooks/Log/ClientLoggerFactory';
|
||||
import { Bootstrapper } from '../Bootstrapper';
|
||||
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||
|
||||
export class AppInitializationLogger implements Bootstrapper {
|
||||
constructor(
|
||||
|
||||
8
src/presentation/common/Dialog.ts
Normal file
8
src/presentation/common/Dialog.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Dialog {
|
||||
saveFile(fileContents: string, fileName: string, type: FileType): Promise<void>;
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
BatchFile,
|
||||
ShellScript,
|
||||
}
|
||||
@@ -19,10 +19,14 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState);
|
||||
const { os, isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
||||
|
||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||
const canRun = computed<boolean>(() => getCanRunState(
|
||||
currentState.value.os,
|
||||
isRunningAsDesktopApplication,
|
||||
os,
|
||||
));
|
||||
|
||||
async function executeCode() {
|
||||
if (!codeRunner) { throw new Error('missing code runner'); }
|
||||
@@ -33,7 +37,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktopVersion: isDesktop,
|
||||
canRun,
|
||||
executeCode,
|
||||
};
|
||||
@@ -42,10 +45,10 @@ export default defineComponent({
|
||||
|
||||
function getCanRunState(
|
||||
selectedOs: OperatingSystem,
|
||||
isDesktopVersion: boolean,
|
||||
isRunningAsDesktopApplication: boolean,
|
||||
hostOs: OperatingSystem | undefined,
|
||||
): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<IconButton
|
||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
||||
:text="isRunningAsDesktopApplication ? 'Save' : 'Download'"
|
||||
:icon-name="isRunningAsDesktopApplication ? 'floppy-disk' : 'file-arrow-down'"
|
||||
@click="saveCode"
|
||||
/>
|
||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||
@@ -16,12 +16,11 @@ import {
|
||||
defineComponent, ref, computed,
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptFileName } from '@/application/CodeRunner/ScriptFileName';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import InstructionList from './Instructions/InstructionList.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
@@ -35,7 +34,8 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
@@ -44,13 +44,17 @@ export default defineComponent({
|
||||
fileName.value,
|
||||
));
|
||||
|
||||
function saveCode() {
|
||||
saveCodeToDisk(fileName.value, currentState.value);
|
||||
async function saveCode() {
|
||||
await dialog.saveFile(
|
||||
currentState.value.code.current,
|
||||
fileName.value,
|
||||
getType(currentState.value.collection.scripting.language),
|
||||
);
|
||||
areInstructionsVisible.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktopVersion: isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
instructions,
|
||||
fileName,
|
||||
areInstructionsVisible,
|
||||
@@ -59,12 +63,6 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
SaveFileDialog.saveFile(content, fileName, type);
|
||||
}
|
||||
|
||||
function getType(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile:
|
||||
|
||||
@@ -188,6 +188,7 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
||||
.appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
|
||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||
.appendCommentLine(' ✔️ Committed to your safety with strong security measures.')
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog';
|
||||
|
||||
export function determineDialogBasedOnEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog,
|
||||
browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(),
|
||||
): Dialog {
|
||||
if (!environment.isRunningAsDesktopApplication) {
|
||||
return browserDialogFactory();
|
||||
}
|
||||
const dialog = windowInjectedDialogFactory();
|
||||
if (!dialog) {
|
||||
throw new Error([
|
||||
'The Dialog API could not be retrieved from the window object.',
|
||||
'This may indicate that the Dialog API is either not implemented or not correctly exposed in the current desktop environment.',
|
||||
].join('\n'));
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
export type WindowDialogCreationFunction = () => Dialog | undefined;
|
||||
|
||||
export type BrowserDialogCreationFunction = () => Dialog;
|
||||
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { determineDialogBasedOnEnvironment } from './ClientDialogFactory';
|
||||
|
||||
export function useDialog(
|
||||
factory: DialogFactory = () => determineDialogBasedOnEnvironment(CurrentEnvironment),
|
||||
) {
|
||||
const dialog = factory();
|
||||
return {
|
||||
dialog,
|
||||
};
|
||||
}
|
||||
|
||||
export type DialogFactory = () => Dialog;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
export class ClientLoggerFactory implements LoggerFactory {
|
||||
public static readonly Current: LoggerFactory = new ClientLoggerFactory();
|
||||
@@ -22,7 +22,7 @@ export class ClientLoggerFactory implements LoggerFactory {
|
||||
this.logger = noopLoggerFactory(); // keep the test outputs clean
|
||||
return;
|
||||
}
|
||||
if (environment.isDesktop) {
|
||||
if (environment.isRunningAsDesktopApplication) {
|
||||
this.logger = windowInjectedLoggerFactory();
|
||||
return;
|
||||
}
|
||||
@@ -49,5 +49,5 @@ function isUnitOrIntegrationTests(
|
||||
the global window object. If we're in a desktop (Node) environment and the logger isn't
|
||||
injected, it indicates a testing environment.
|
||||
*/
|
||||
return environment.isDesktop && !windowAccessor()?.log;
|
||||
return environment.isRunningAsDesktopApplication && !windowAccessor()?.log;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
|
||||
export interface LoggerFactory {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ClientLoggerFactory } from './ClientLoggerFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="privacy-policy">
|
||||
<div v-if="!isDesktop" class="line">
|
||||
<div v-if="!isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
🚫🍪
|
||||
</div>
|
||||
<div>No cookies!</div>
|
||||
</div>
|
||||
<div v-if="isDesktop" class="line">
|
||||
<div v-if="isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
🚫🌐
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
of the <a :href="repositoryUrl" target="_blank" rel="noopener noreferrer">source code</a> with no changes.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isDesktop" class="line">
|
||||
<div v-if="!isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
📈
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ import { injectKey } from '@/presentation/injectionSymbols';
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { info } = injectKey((keys) => keys.useApplication);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
|
||||
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||
@@ -68,7 +68,7 @@ export default defineComponent({
|
||||
return {
|
||||
repositoryUrl,
|
||||
feedbackUrl,
|
||||
isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="footer">
|
||||
<div class="footer__section">
|
||||
<span v-if="isDesktop" class="footer__section__item">
|
||||
<span v-if="isRunningAsDesktopApplication" class="footer__section__item">
|
||||
<AppIcon class="icon" icon="globe" />
|
||||
<span>
|
||||
Online version at <a :href="homepageUrl" target="_blank" rel="noopener noreferrer">{{ homepageUrl }}</a>
|
||||
@@ -68,7 +68,7 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { info } = injectKey((keys) => keys.useApplication);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
|
||||
const isPrivacyDialogVisible = ref(false);
|
||||
|
||||
@@ -87,7 +87,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
isPrivacyDialogVisible,
|
||||
showPrivacyDialog,
|
||||
version,
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import { ScriptFileCodeRunner } from '@/infrastructure/CodeRunner/ScriptFileCodeRunner';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ElectronDialog } from '@/infrastructure/Dialog/Electron/ElectronDialog';
|
||||
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
||||
import { registerIpcChannel } from '../shared/IpcBridging/IpcProxy';
|
||||
import { IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions';
|
||||
import { ChannelDefinitionKey, IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions';
|
||||
|
||||
export function registerAllIpcChannels(
|
||||
createCodeRunner: CodeRunnerFactory = () => new ScriptFileCodeRunner(),
|
||||
registrar: IpcRegistrar = registerIpcChannel,
|
||||
createDialog: DialogFactory = () => new ElectronDialog(),
|
||||
registrar: IpcChannelRegistrar = registerIpcChannel,
|
||||
) {
|
||||
const registrars: Record<keyof typeof IpcChannelDefinitions, () => void> = {
|
||||
CodeRunner: () => registrar(IpcChannelDefinitions.CodeRunner, createCodeRunner()),
|
||||
const ipcInstanceCreators: IpcChannelRegistrars = {
|
||||
CodeRunner: () => createCodeRunner(),
|
||||
Dialog: () => createDialog(),
|
||||
};
|
||||
Object.entries(registrars).forEach(([name, register]) => {
|
||||
Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => {
|
||||
try {
|
||||
register();
|
||||
const definition = IpcChannelDefinitions[name];
|
||||
const instance = instanceFactory();
|
||||
registrar(definition, instance);
|
||||
} catch (err) {
|
||||
throw new AggregateError(`main: Failed to register IPC channel "${name}"`, err);
|
||||
throw new AggregateError([err], `main: Failed to register IPC channel "${name}":\n${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export type CodeRunnerFactory = () => CodeRunner;
|
||||
export type DialogFactory = () => Dialog;
|
||||
export type IpcChannelRegistrar = typeof registerIpcChannel;
|
||||
|
||||
export type IpcRegistrar = typeof registerIpcChannel;
|
||||
type RegistrationChannel<T extends ChannelDefinitionKey> = (typeof IpcChannelDefinitions)[T];
|
||||
type ExtractChannelServiceType<T> = T extends IpcChannel<infer U> ? U : never;
|
||||
type IpcChannelRegistrars = {
|
||||
[K in ChannelDefinitionKey]: () => ExtractChannelServiceType<RegistrationChannel<K>>;
|
||||
};
|
||||
|
||||
@@ -12,14 +12,20 @@ export function provideWindowVariables(
|
||||
createApiFacade: ApiFacadeFactory = createSecureFacade,
|
||||
ipcConsumerCreator: IpcConsumerProxyCreator = createIpcConsumerProxy,
|
||||
): WindowVariables {
|
||||
return {
|
||||
isDesktop: true,
|
||||
// Enforces mandatory variable availability at compile time
|
||||
const variables: RequiredWindowVariables = {
|
||||
isRunningAsDesktopApplication: true,
|
||||
log: createApiFacade(createLogger(), ['info', 'debug', 'warn', 'error']),
|
||||
os: convertToOs(process.platform),
|
||||
codeRunner: ipcConsumerCreator(IpcChannelDefinitions.CodeRunner),
|
||||
dialog: ipcConsumerCreator(IpcChannelDefinitions.Dialog),
|
||||
};
|
||||
return variables;
|
||||
}
|
||||
|
||||
type RequiredWindowVariables = PartiallyRequired<WindowVariables, 'os' /* | 'anotherOptionalKey'.. */>;
|
||||
type PartiallyRequired<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>;
|
||||
|
||||
export type LoggerFactory = () => Logger;
|
||||
|
||||
export type ApiFacadeFactory = typeof createSecureFacade;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { FunctionKeys } from '@/TypeHelpers';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { IpcChannel } from './IpcChannel';
|
||||
|
||||
export const IpcChannelDefinitions = {
|
||||
CodeRunner: defineElectronIpcChannel<CodeRunner>('code-run', ['runCode']),
|
||||
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['saveFile']),
|
||||
} as const;
|
||||
|
||||
export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions;
|
||||
|
||||
function defineElectronIpcChannel<T>(
|
||||
name: string,
|
||||
functionNames: readonly FunctionKeys<T>[],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ipcMain } from 'electron/main';
|
||||
import { ipcRenderer } from 'electron/renderer';
|
||||
import { isFunction } from '@/TypeHelpers';
|
||||
import { FunctionKeys, isFunction } from '@/TypeHelpers';
|
||||
import { IpcChannel } from './IpcChannel';
|
||||
|
||||
export function createIpcConsumerProxy<T>(
|
||||
@@ -25,9 +25,7 @@ export function registerIpcChannel<T>(
|
||||
) {
|
||||
channel.accessibleMembers.forEach((functionKey) => {
|
||||
const originalFunction = originalObject[functionKey];
|
||||
if (!isFunction(originalFunction)) {
|
||||
throw new Error('Non-function members are not yet supported');
|
||||
}
|
||||
validateIpcFunction(functionKey, originalFunction, channel);
|
||||
const ipcChannel = getIpcChannelIdentifier(channel.namespace, functionKey as string);
|
||||
electronIpcMain.handle(ipcChannel, (_event, ...args: unknown[]) => {
|
||||
return originalFunction.apply(originalObject, args);
|
||||
@@ -35,6 +33,28 @@ export function registerIpcChannel<T>(
|
||||
});
|
||||
}
|
||||
|
||||
function validateIpcFunction<T>(
|
||||
functionKey: FunctionKeys<T>,
|
||||
functionValue: T[FunctionKeys<T>],
|
||||
channel: IpcChannel<T>,
|
||||
): asserts functionValue is T[FunctionKeys<T>] & ((...args: unknown[]) => unknown) {
|
||||
const functionName = functionKey.toString();
|
||||
if (functionValue === undefined) {
|
||||
throwErrorWithContext(`The function "${functionName}" is not found on the target object.`);
|
||||
}
|
||||
if (!isFunction(functionValue)) {
|
||||
throwErrorWithContext('Non-function members are not yet supported.');
|
||||
}
|
||||
function throwErrorWithContext(message: string): never {
|
||||
throw new Error([
|
||||
message,
|
||||
`Channel: ${JSON.stringify(channel)}.`,
|
||||
`Function key: ${functionName}.`,
|
||||
`Value: ${JSON.stringify(functionValue)}`,
|
||||
].join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
function getIpcChannelIdentifier(namespace: string, key: string) {
|
||||
return `proxy:${namespace}:${key}`;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipbo
|
||||
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||
import type { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
|
||||
import type { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
|
||||
import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
|
||||
import type { useDialog } from './components/Shared/Hooks/Dialog/UseDialog';
|
||||
|
||||
export const InjectionKeys = {
|
||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||
@@ -19,6 +20,7 @@ export const InjectionKeys = {
|
||||
useUserSelectionState: defineTransientKey<ReturnType<typeof useUserSelectionState>>('useUserSelectionState'),
|
||||
useLogger: defineTransientKey<ReturnType<typeof useLogger>>('useLogger'),
|
||||
useCodeRunner: defineTransientKey<ReturnType<typeof useCodeRunner>>('useCodeRunner'),
|
||||
useDialog: defineTransientKey<ReturnType<typeof useDialog>>('useDialog'),
|
||||
};
|
||||
|
||||
export interface InjectionKeyWithLifetime<T> {
|
||||
|
||||
Reference in New Issue
Block a user