diff --git a/frappe/index.js b/frappe/index.js index cff1d5a2..395eca73 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -9,6 +9,9 @@ const { const { markRaw } = require('vue'); module.exports = { + isElectron: false, + isServer: false, + initializeAndRegister(customModels = {}, force = false) { this.init(force); const common = require('frappe/common'); diff --git a/main/registerProcessListeners.ts b/main/registerProcessListeners.ts index c1519a01..885d913d 100644 --- a/main/registerProcessListeners.ts +++ b/main/registerProcessListeners.ts @@ -1,5 +1,6 @@ import { app } from 'electron'; import { Main } from '../main'; +import { handleError } from '../src/errorHandling'; export default function registerProcessListeners(main: Main) { if (main.isDevelopment) { @@ -15,4 +16,12 @@ export default function registerProcessListeners(main: Main) { }); } } + + process.on('unhandledRejection', (error: Error) => { + handleError(true, error); + }); + + process.on('uncaughtException', (error) => { + handleError(true, error, {}, () => process.exit(1)); + }); } diff --git a/src/errorHandling.ts b/src/errorHandling.ts index 32013ac7..777fce6e 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -16,7 +16,7 @@ interface ErrorLog { name: string; message: string; stack?: string; - more?: object; + more?: Record; } function getCanLog(): boolean { @@ -71,7 +71,10 @@ function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) { return props; } -export function getErrorLogObject(error: Error, more: object = {}): ErrorLog { +export function getErrorLogObject( + error: Error, + more: Record +): ErrorLog { const { name, stack, message } = error; const errorLogObj = { name, stack, message, more }; @@ -84,7 +87,7 @@ export function getErrorLogObject(error: Error, more: object = {}): ErrorLog { export function handleError( shouldLog: boolean, error: Error, - more: object = {}, + more?: Record, cb?: Function ) { telemetry.error(error.name); @@ -96,7 +99,7 @@ export function handleError( return; } - const errorLogObj = getErrorLogObject(error, more); + const errorLogObj = getErrorLogObject(error, more ?? {}); // @ts-ignore const canLog = getCanLog(); diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 92d91900..00000000 --- a/src/main.js +++ /dev/null @@ -1,168 +0,0 @@ -import { ipcRenderer } from 'electron'; -import frappe from 'frappe'; -import { createApp } from 'vue'; -import models from '../models'; -import App from './App'; -import FeatherIcon from './components/FeatherIcon'; -import config, { ConfigKeys } from './config'; -import { getErrorHandled, handleError } from './errorHandling'; -import { IPC_CHANNELS, IPC_MESSAGES } from './messages'; -import router from './router'; -import telemetry from './telemetry/telemetry'; -import { outsideClickDirective } from './ui'; -import { setLanguageMap, showToast, stringifyCircular } from './utils'; -(async () => { - const language = config.get('language'); - if (language) { - await setLanguageMap(language); - } - - if (process.env.NODE_ENV === 'development') { - window.config = config; - } - - frappe.isServer = true; - frappe.isElectron = true; - frappe.initializeAndRegister(models, language); - frappe.fetch = window.fetch.bind(); - - ipcRenderer.send = getErrorHandled(ipcRenderer.send); - ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke); - - window.frappe = frappe; - - window.onerror = (message, source, lineno, colno, error) => { - error = error ?? new Error('triggered in window.onerror'); - handleError(true, error, { message, source, lineno, colno }); - }; - - process.on('unhandledRejection', (error) => { - handleError(true, error); - }); - - process.on('uncaughtException', (error) => { - handleError(true, error, () => process.exit(1)); - }); - - registerIpcRendererListeners(); - - const app = createApp({ - template: '', - }); - app.use(router); - app.component('App', App); - app.component('feather-icon', FeatherIcon); - app.directive('on-outside-click', outsideClickDirective); - app.mixin({ - computed: { - frappe() { - return frappe; - }, - platform() { - return { - win32: 'Windows', - darwin: 'Mac', - linux: 'Linux', - }[process.platform]; - }, - }, - methods: { - t: frappe.t, - T: frappe.T, - }, - }); - - app.config.errorHandler = (err, vm, info) => { - const more = { - info, - }; - - if (vm) { - const { fullPath, params } = vm.$route; - more.fullPath = fullPath; - more.params = stringifyCircular(params ?? {}); - more.data = stringifyCircular(vm.$data ?? {}, true, true); - more.props = stringifyCircular(vm.$props ?? {}, true, true); - } - - handleError(false, err, more); - console.error(err, vm, info); - }; - - incrementOpenCount(); - app.mount('body'); -})(); - -function incrementOpenCount() { - let openCount = config.get(ConfigKeys.OpenCount); - if (typeof openCount !== 'number') { - openCount = 1; - } else { - openCount += 1; - } - - config.set(ConfigKeys.OpenCount, openCount); -} - -function registerIpcRendererListeners() { - ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => { - Object.assign(window.frappe.store, message); - }); - - ipcRenderer.on(IPC_CHANNELS.CHECKING_FOR_UPDATE, (_) => { - showToast({ message: frappe.t`Checking for updates` }); - }); - - ipcRenderer.on(IPC_CHANNELS.UPDATE_AVAILABLE, (_, version) => { - const message = version - ? frappe.t`Version ${version} available` - : frappe.t`New version available`; - const action = () => { - ipcRenderer.send(IPC_MESSAGES.DOWNLOAD_UPDATE); - showToast({ message: frappe.t`Downloading update` }); - }; - - showToast({ - message, - action, - actionText: frappe.t`Download Update`, - duration: 10000, - type: 'success', - }); - }); - - ipcRenderer.on(IPC_CHANNELS.UPDATE_NOT_AVAILABLE, (_) => { - showToast({ message: frappe.t`No updates available` }); - }); - - ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, (_) => { - const action = () => { - ipcRenderer.send(IPC_MESSAGES.INSTALL_UPDATE); - }; - showToast({ - message: frappe.t`Update downloaded`, - action, - actionText: frappe.t`Install Update`, - duration: 10000, - type: 'success', - }); - }); - - ipcRenderer.on(IPC_CHANNELS.UPDATE_ERROR, (_, error) => { - error.name = 'Updation Error'; - handleError(true, error); - }); - - document.addEventListener('visibilitychange', function () { - const { visibilityState } = document; - if (visibilityState === 'visible' && !telemetry.started) { - telemetry.start(); - } - - if (visibilityState !== 'hidden') { - return; - } - - telemetry.stop(); - }); -} diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 00000000..7afcd436 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,95 @@ +import { ipcRenderer } from 'electron'; +import frappe from 'frappe'; +import { createApp } from 'vue'; +import models from '../models'; +import App from './App.vue'; +import FeatherIcon from './components/FeatherIcon.vue'; +import config, { ConfigKeys } from './config'; +import { getErrorHandled, handleError } from './errorHandling'; +import { incrementOpenCount } from './renderer/helpers'; +import registerIpcRendererListeners from './renderer/registerIpcRendererListeners'; +import router from './router'; +import { outsideClickDirective } from './ui'; +import { setLanguageMap, stringifyCircular } from './utils'; + +(async () => { + const language = config.get(ConfigKeys.Language); + if (language) { + await setLanguageMap(language); + } + + if (process.env.NODE_ENV === 'development') { + // @ts-ignore + window.config = config; + } + + frappe.isServer = true; + frappe.isElectron = true; + + frappe.initializeAndRegister(models); + + ipcRenderer.send = getErrorHandled(ipcRenderer.send); + ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke); + + // @ts-ignore + window.frappe = frappe; + + window.onerror = (message, source, lineno, colno, error) => { + error = error ?? new Error('triggered in window.onerror'); + handleError(true, error, { message, source, lineno, colno }); + }; + + registerIpcRendererListeners(); + + const app = createApp({ + template: '', + }); + + app.use(router); + app.component('App', App); + app.component('feather-icon', FeatherIcon); + app.directive('on-outside-click', outsideClickDirective); + app.mixin({ + computed: { + frappe() { + return frappe; + }, + platform(): string { + switch (process.platform) { + case 'win32': + return 'Windows'; + case 'darwin': + return 'Mac'; + case 'linux': + return 'Linux'; + default: + return 'Linux'; + } + }, + }, + methods: { + t: frappe.t, + T: frappe.T, + }, + }); + + app.config.errorHandler = (err, vm, info) => { + const more: Record = { + info, + }; + + if (vm) { + const { fullPath, params } = vm.$route; + more.fullPath = fullPath; + more.params = stringifyCircular(params ?? {}); + more.data = stringifyCircular(vm.$data ?? {}, true, true); + more.props = stringifyCircular(vm.$props ?? {}, true, true); + } + + handleError(false, err as Error, more); + console.error(err, vm, info); + }; + + incrementOpenCount(); + app.mount('body'); +})(); diff --git a/src/renderer/helpers.ts b/src/renderer/helpers.ts new file mode 100644 index 00000000..6dd74ec7 --- /dev/null +++ b/src/renderer/helpers.ts @@ -0,0 +1,12 @@ +import config, { ConfigKeys } from '@/config'; + +export function incrementOpenCount() { + let openCount = config.get(ConfigKeys.OpenCount); + if (typeof openCount !== 'number') { + openCount = 1; + } else { + openCount += 1; + } + + config.set(ConfigKeys.OpenCount, openCount); +} diff --git a/src/renderer/registerIpcRendererListeners.ts b/src/renderer/registerIpcRendererListeners.ts new file mode 100644 index 00000000..a6ac8b47 --- /dev/null +++ b/src/renderer/registerIpcRendererListeners.ts @@ -0,0 +1,64 @@ +import { handleError } from '@/errorHandling'; +import { IPC_CHANNELS, IPC_MESSAGES } from '@/messages'; +import telemetry from '@/telemetry/telemetry'; +import { showToast } from '@/utils'; +import { ipcRenderer } from 'electron'; +import frappe from 'frappe'; + +export default function registerIpcRendererListeners() { + ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => { + Object.assign(frappe.store, message); + }); + + ipcRenderer.on(IPC_CHANNELS.CHECKING_FOR_UPDATE, (_) => { + showToast({ message: frappe.t`Checking for updates` }); + }); + + ipcRenderer.on(IPC_CHANNELS.UPDATE_AVAILABLE, (_, version) => { + const message = version + ? frappe.t`Version ${version} available` + : frappe.t`New version available`; + const action = () => { + ipcRenderer.send(IPC_MESSAGES.DOWNLOAD_UPDATE); + showToast({ message: frappe.t`Downloading update` }); + }; + + showToast({ + message, + action, + actionText: frappe.t`Download Update`, + duration: 10000, + type: 'success', + }); + }); + + ipcRenderer.on(IPC_CHANNELS.UPDATE_NOT_AVAILABLE, (_) => { + showToast({ message: frappe.t`No updates available` }); + }); + + ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, (_) => { + const action = () => { + ipcRenderer.send(IPC_MESSAGES.INSTALL_UPDATE); + }; + showToast({ + message: frappe.t`Update downloaded`, + action, + actionText: frappe.t`Install Update`, + duration: 10000, + type: 'success', + }); + }); + + ipcRenderer.on(IPC_CHANNELS.UPDATE_ERROR, (_, error) => { + error.name = 'Updation Error'; + handleError(true, error); + }); + + document.addEventListener('visibilitychange', function () { + if (document.visibilityState !== 'hidden') { + return; + } + + telemetry.stop(); + }); +} diff --git a/src/router.js b/src/router.js index d64e37b6..e97f6c51 100644 --- a/src/router.js +++ b/src/router.js @@ -1,16 +1,14 @@ -import ChartOfAccounts from '@/pages/ChartOfAccounts'; -// standard views -import Dashboard from '@/pages/Dashboard/Dashboard'; -import DataImport from '@/pages/DataImport'; -// custom views -import GetStarted from '@/pages/GetStarted'; -import InvoiceForm from '@/pages/InvoiceForm'; -import JournalEntryForm from '@/pages/JournalEntryForm'; -import ListView from '@/pages/ListView/ListView'; -import PrintView from '@/pages/PrintView/PrintView'; -import QuickEditForm from '@/pages/QuickEditForm'; -import Report from '@/pages/Report'; -import Settings from '@/pages/Settings/Settings'; +import ChartOfAccounts from '@/pages/ChartOfAccounts.vue'; +import Dashboard from '@/pages/Dashboard/Dashboard.vue'; +import DataImport from '@/pages/DataImport.vue'; +import GetStarted from '@/pages/GetStarted.vue'; +import InvoiceForm from '@/pages/InvoiceForm.vue'; +import JournalEntryForm from '@/pages/JournalEntryForm.vue'; +import ListView from '@/pages/ListView/ListView.vue'; +import PrintView from '@/pages/PrintView/PrintView.vue'; +import QuickEditForm from '@/pages/QuickEditForm.vue'; +import Report from '@/pages/Report.vue'; +import Settings from '@/pages/Settings/Settings.vue'; import { createRouter, createWebHistory } from 'vue-router'; import telemetry from './telemetry/telemetry'; import { NounEnum, Verb } from './telemetry/types'; diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts index d9f24faa..13a865fd 100644 --- a/src/shims-vue.d.ts +++ b/src/shims-vue.d.ts @@ -1,4 +1,6 @@ declare module '*.vue' { - import Vue from 'vue' - export default Vue + import type { DefineComponent } from 'vue'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any>; + export default component; } diff --git a/src/utils.js b/src/utils.js index 87b8a803..416275f6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,5 @@ -import Avatar from '@/components/Avatar'; -import Toast from '@/components/Toast'; +import Avatar from '@/components/Avatar.vue'; +import Toast from '@/components/Toast.vue'; import router from '@/router'; import { ipcRenderer } from 'electron'; import frappe, { t } from 'frappe'; diff --git a/vue.config.js b/vue.config.js index c0dacf04..3dd31d45 100644 --- a/vue.config.js +++ b/vue.config.js @@ -6,6 +6,7 @@ module.exports = { electronBuilder: { nodeIntegration: true, mainProcessFile: 'main.ts', + rendererProcessFile: 'src/renderer.ts', disableMainProcessTypescript: false, mainProcessTypeChecking: true, chainWebpackRendererProcess: (config) => {