import 'core-js/stable' ; import 'regenerator-runtime/runtime' ; import { fork , ChildProcess , Serializable } from 'child_process' ; import path from 'path' ; import fs from 'fs' ; import { app , BrowserWindow , BrowserView , shell , ipcMain , dialog , Menu } from 'electron' ; import dotenv from 'dotenv' ; import log from 'electron-log' ; import * as Sentry from '@sentry/electron' ; import semver from 'semver' ; import fetch from 'node-fetch' ; import type { AlertStatus } from '@chakra-ui/react' ; import findOpenSocket from './utils/find-open-socket' ; import MenuBuilder from './menu' ; import store from './store' ; import { ElementPayload , ElementActualStatus , BreadcrumbPayload , MainIpcChannel , ElementTag } from './interface' ; import { version } from './package.json' ; dotenv . config ( ) ; const mainLog = log . scope ( 'main' ) ; const isDarwin = process . platform === 'darwin' ; const isLinux = process . platform === 'linux' ; const isWin = process . platform === 'win32' ; const isProd = process . env . NODE_ENV === 'production' ; const isProdDebug = process . env . DEBUG_PROD === 'true' ; const isDebug = ! isProd || isProdDebug ; const isStudio = process . env . STUDIO === 'true' ; let socketId ; app . commandLine . appendSwitch ( 'ignore-certificate-errors' , 'true' ) ; app . commandLine . appendSwitch ( 'allow-insecure-localhost' , 'true' ) ; Sentry . init ( { dsn : process . env . SENTRYDNS , enableNative : true } ) ; Sentry . setContext ( 'character' , { runtime : 'main' , isDebug , isDarwin , isStudio , version , } ) ; process . on ( 'uncaughtException' , ( err ) => { Sentry . captureException ( err ) ; dialog . showMessageBox ( { type : 'error' , title : 'Sorry' , message : 'Uncaught exception occurred' , detail : err . message , } ) ; } ) ; if ( isDebug ) { const sourceMapSupport = require ( 'source-map-support' ) ; sourceMapSupport . install ( ) ; require ( 'electron-debug' ) ( ) ; } const installExtensions = async ( ) => { const installer = require ( 'electron-devtools-installer' ) ; const forceDownload = ! ! process . env . UPGRADE_EXTENSIONS ; const extensions = [ 'REACT_DEVELOPER_TOOLS' ] ; return installer . default ( extensions . map ( ( name ) => installer [ name ] ) , forceDownload ) . catch ( mainLog . error ) ; } ; let menuBuilder = null ; let mainWindow = null ; let mainMenu = null ; let pagesMenu ; const checkForUpdates = async ( silent = false ) => { const resp = await fetch ( 'https://cdn.socode.pro/latest.json' , { headers : { 'content-type' : 'application/json' , } , } ) ; if ( resp . ok ) { const matrix = await resp . json ( ) ; let latest = isStudio ? matrix . win_studio : matrix . win ; if ( isDarwin ) { latest = isStudio ? matrix . mac_studio : matrix . mac ; } if ( semver . gt ( latest , version ) ) { const { response } = await dialog . showMessageBox ( { message : 'The updated version is available for download. Sure to download?' , buttons : [ 'Yes' , 'Cancel' ] , } ) ; if ( response === 0 ) { await shell . openExternal ( `https://cdn.socode.pro/Derealize${ isStudio ? '-studio' : '' }-${ latest }.${ isDarwin ? 'dmg' : 'exe' }` ) ; } } else if ( ! silent ) { mainWindow ?. webContents . send ( MainIpcChannel . Toast , 'There are currently no updates available.' , 'success' as AlertStatus ) ; } } else if ( ! silent ) { mainWindow ?. webContents . send ( MainIpcChannel . Toast , `check update request fail:${ resp . statusText }` , 'warning' as AlertStatus ) ; } } ; const about = async ( ) => { dialog . showMessageBox ( { type : 'info' , title : 'About' , message : `Derealize${ isStudio ? ' Studio' : '' }` , detail : `version: ${ version }` , } ) ; } ; const topbarHeight = 42 ; const setBrowserViewBounds = ( mainwin ) => { const browserView = mainwin . getBrowserView ( ) ; if ( ! browserView ) return ; const barwidth = ( store . get ( 'barWidth' ) as number ) || 300 ; const rectangle = mainwin . getBounds ( ) ; const xaxis = barwidth + 5 ; const yaxis = ( mainwin . isMaximized ( ) ? 34 : 46 ) + topbarHeight + 1 ; browserView . setBounds ( { x : xaxis , y : yaxis , width : rectangle . width - xaxis , height : rectangle . height - yaxis } ) ; } ; interface ProjectHost { view : BrowserView ; baseUrl : string ; pages : Array < string > ; isWeapp : boolean ; loadFail ? : boolean ; activeSelector ? : string ; } const projects = new Map < string , ProjectHost > ( ) ; const frontMain = ( ) => { if ( ! mainWindow ) return ; projects . forEach ( ( p ) => mainWindow ?. removeBrowserView ( p . view ) ) ; } ; ipcMain . on ( MainIpcChannel . FrontView , ( event , projectId , baseUrl , pages , isWeapp ) => { if ( ! mainWindow ) return ; if ( ! projectId ) { frontMain ( ) ; return ; } const project = projects . get ( projectId ) ; if ( project ) { mainWindow . setBrowserView ( project . view ) ; } else { const view = new BrowserView ( { webPreferences : { nodeIntegration : false , enableRemoteModule : false , contextIsolation : true , preload : path . resolve ( __dirname , isProd ? 'preload-inject.prod.js' : 'preload-inject.js' ) , allowRunningInsecureContent : true , } , } ) ; view . setBackgroundColor ( '#fff' ) ; projects . set ( projectId , { view , baseUrl , pages , isWeapp } ) ; mainWindow . setBrowserView ( view ) ; setBrowserViewBounds ( mainWindow ) ; if ( isDebug ) { view . webContents . openDevTools ( ) ; } view . webContents . on ( 'did-start-loading' , ( ) => { const pj = projects . get ( projectId ) ; if ( ! pj ) return ; pj . loadFail = undefined ; mainWindow ?. webContents . send ( MainIpcChannel . LoadStart , projectId ) ; } ) . on ( 'did-finish-load' , ( e ) => { const pj = projects . get ( projectId ) ; if ( ! pj || pj . loadFail ) return ; pj . view . webContents . send ( MainIpcChannel . LoadFinish , socketId , projectId , isWeapp , pj . activeSelector ) ; mainWindow ?. webContents . send ( MainIpcChannel . LoadFinish , projectId , true ) ; } ) . on ( 'did-fail-load' , ( ) => { const pj = projects . get ( projectId ) ; if ( ! pj ) return ; pj . loadFail = true ; mainWindow ?. webContents . send ( MainIpcChannel . LoadFinish , projectId , false ) ; } ) . on ( 'page-favicon-updated' , ( e , favicons ) => { mainWindow ?. webContents . send ( MainIpcChannel . Favicon , projectId , favicons [ 0 ] ) ; } ) ; } pagesMenu = menuBuilder ?. buildPagesMenu ( projectId , pages ) ; } ) ; ipcMain . on ( MainIpcChannel . DeviceEmulation , ( event , projectId , swidth ) => { const project = projects . get ( projectId ) ; if ( project ) { const rectangle = project . view . getBounds ( ) ; const width = swidth === 0 ? rectangle . width : swidth ; project . view . webContents . enableDeviceEmulation ( { screenPosition : swidth === 0 ? 'desktop' : 'mobile' , screenSize : { width , height : rectangle . height } , viewSize : { width , height : rectangle . height } , viewPosition : { x : 0 , y : 0 } , deviceScaleFactor : 0 , scale : 1 , } ) ; } } ) ; const loadURL = ( projectId , url ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . loadURL ( path . join ( project . baseUrl , url ) ) ; } } ; ipcMain . on ( MainIpcChannel . LoadURL , ( event , projectId , url ) => { loadURL ( projectId , url ) ; } ) ; ipcMain . on ( MainIpcChannel . DestroyProjectView , ( event , projectId ) => { if ( ! mainWindow ) return ; const project = projects . get ( projectId ) ; if ( project ) { mainWindow . removeBrowserView ( project . view ) ; ( project . view . webContents as any ) . destroy ( ) ; } projects . delete ( projectId ) ; } ) ; const sendIsMaximized = ( ) => { if ( ! mainWindow ) return ; mainWindow . webContents . send ( 'isMaximized' , mainWindow . isMaximized ( ) ) ; setBrowserViewBounds ( mainWindow ) ; } ; const createWindow = async ( ) => { if ( isDebug ) { await installExtensions ( ) ; } const RESOURCES_PATH = app . isPackaged ? path . join ( process . resourcesPath , 'assets' ) : path . join ( __dirname , '../assets' ) ; const getAssetPath = ( ... paths ) : string => { return path . join ( RESOURCES_PATH , ... paths ) ; } ; mainWindow = new BrowserWindow ( { show : false , width : 1280 , height : 800 , icon : isStudio ? getAssetPath ( 'icon.studio.png' ) : getAssetPath ( 'icon.png' ) , frame : false , webPreferences : { nodeIntegration : false , enableRemoteModule : false , contextIsolation : true , preload : path . resolve ( __dirname , isProd ? 'preload.prod.js' : 'preload.js' ) , } , } ) ; if ( isDebug ) { mainWindow . webContents . openDevTools ( ) ; } mainWindow . loadURL ( `file://${ __dirname }/index.html` ) ; mainWindow . webContents . on ( 'did-finish-load' , ( ) => { if ( ! mainWindow ) { throw new Error ( 'mainWindow is not defined' ) ; } if ( process . env . START_MINIMIZED ) { mainWindow . minimize ( ) ; } else { mainWindow . show ( ) ; mainWindow . focus ( ) ; } mainWindow . webContents . send ( 'setParams' , socketId ) ; } ) ; mainWindow . on ( 'closed' , ( ) => { mainWindow = null ; } ) ; menuBuilder = new MenuBuilder ( mainWindow , frontMain , loadURL , checkForUpdates , about ) ; mainMenu = menuBuilder . buildMenu ( ) ; mainWindow . webContents . on ( 'new-window' , ( event , url ) => { event . preventDefault ( ) ; shell . openExternal ( url ) ; } ) ; mainWindow . on ( 'maximize' , sendIsMaximized ) ; mainWindow . on ( 'unmaximize' , sendIsMaximized ) ; mainWindow . on ( 'resize' , ( ) => { if ( ! mainWindow ) return ; setBrowserViewBounds ( mainWindow ) ; } ) ; } ; app . on ( 'window-all-closed' , ( ) => { if ( ! isDarwin ) { app . quit ( ) ; } } ) ; app . on ( 'activate' , async ( ) => { if ( mainWindow === null ) createWindow ( ) ; } ) ; let backendProcess ; const createBackendProcess = ( ) => { if ( process . env . BACKEND_SUBPROCESS === 'true' ) { backendProcess = fork ( path . join ( __dirname , 'backend/backend.ts' ) , [ '--subprocess' , socketId , '--version' , version ] , { execArgv : [ '-r' , './.derealize/scripts/BabelRegister' ] , } ) ; } else if ( isProd ) { backendProcess = fork ( path . join ( __dirname , 'backend.prod.js' ) , [ '--subprocess' , socketId , '--version' , version ] , { stdio : isProdDebug ? [ 'ignore' , fs . openSync ( './out.log' , 'a' ) , fs . openSync ( './err.log' , 'a' ) , 'ipc' ] : 'pipe' , } ) ; } else { app . allowRendererProcessReuse = false ; const backendWin = new BrowserWindow ( { x : 400 , y : 400 , width : 1200 , height : 800 , show : true , webPreferences : { nodeIntegration : true , enableRemoteModule : true , contextIsolation : false , } , } ) ; backendWin . loadURL ( `file://${ __dirname }/backend/backend.html` ) ; backendWin . webContents . openDevTools ( ) ; backendWin . webContents . on ( 'did-finish-load' , ( ) => { backendWin . webContents . send ( 'setParams' , { socketId } ) ; } ) ; } if ( backendProcess ) { backendProcess . on ( 'message' , ( message ) => { mainLog . info ( 'backend ipc' , message ) ; } ) . on ( 'error' , ( err ) => { mainLog . error ( 'backend ipc err' , err ) ; if ( isProd ) Sentry . captureException ( err ) ; } ) ; } } ; ipcMain . on ( MainIpcChannel . GetStore , ( event , key ) => { const value = store . get ( key ) ; event . returnValue = value ; } ) ; ipcMain . on ( MainIpcChannel . SetStore , ( event , payload ) => { store . set ( payload ) ; } ) ; ipcMain . on ( MainIpcChannel . Controls , ( event , payload ) => { if ( ! mainWindow ) return ; switch ( payload ) { case 'minimize' : mainWindow . minimize ( ) ; break ; case 'maximize' : mainWindow . maximize ( ) ; break ; case 'unmaximize' : mainWindow . unmaximize ( ) ; break ; case 'close' : app . quit ( ) ; break ; default : break ; } } ) ; ipcMain . on ( MainIpcChannel . MainMenu , ( ) => { if ( ! mainWindow || ! mainMenu ) return ; mainMenu . popup ( { window : mainWindow , x : 228 , y : mainWindow . isMaximized ( ) ? 34 : 38 } ) ; } ) ; ipcMain . on ( MainIpcChannel . PagesMenu , ( ) => { if ( ! mainWindow || ! pagesMenu ) return ; pagesMenu . popup ( { window : mainWindow , x : 5 , y : mainWindow . isMaximized ( ) ? 76 : 80 } ) ; } ) ; ipcMain . on ( MainIpcChannel . SelectDirs , async ( event , arg ) => { if ( ! mainWindow ) return ; const result = await dialog . showOpenDialog ( mainWindow , { properties : [ 'openDirectory' ] , } ) ; event . returnValue = result . filePaths ; } ) ; ipcMain . on ( MainIpcChannel . OpenPath , ( event , folderPath , filePath , preAppDir = false ) => { let folder = folderPath ; if ( preAppDir ) { folder = path . resolve ( __dirname , '..' , folderPath ) ; } if ( ! fs . existsSync ( folder ) ) { fs . mkdirSync ( folder ) ; } if ( filePath ) { const opath = path . resolve ( folder , filePath ) ; shell . openPath ( opath ) ; } else { shell . openPath ( folder ) ; } } ) ; ipcMain . on ( MainIpcChannel . FocusElement , ( event , payload ) => { const project = projects . get ( payload . projectId ) ; if ( project ) { project . activeSelector = payload . selector ; } mainWindow ?. webContents . send ( MainIpcChannel . FocusElement , payload ) ; } ) ; ipcMain . on ( MainIpcChannel . RespElementStatus , ( event , payload ) => { mainWindow ?. webContents . send ( MainIpcChannel . RespElementStatus , payload ) ; } ) ; ipcMain . on ( MainIpcChannel . BlurElement , ( event , projectId ) => { const project = projects . get ( projectId ) ; if ( project ) { project . activeSelector = undefined ; mainWindow ?. webContents . send ( MainIpcChannel . BlurElement , projectId ) ; } } ) ; ipcMain . on ( MainIpcChannel . LiveUpdateClass , ( event , projectId , selector , className , needRespStatus ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . send ( MainIpcChannel . LiveUpdateClass , selector , className , needRespStatus ) ; } } ) ; ipcMain . on ( MainIpcChannel . LiveUpdateText , ( event , projectId , selector , text ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . send ( MainIpcChannel . LiveUpdateText , selector , text ) ; } } ) ; ipcMain . on ( MainIpcChannel . LiveUpdateTag , ( event , projectId , selector , tag ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . send ( MainIpcChannel . LiveUpdateTag , selector , tag ) ; } } ) ; ipcMain . on ( MainIpcChannel . SelectBreadcrumb , ( event , payload ) => { const project = projects . get ( payload . projectId ) ; if ( project ) { project . view . webContents . send ( MainIpcChannel . SelectBreadcrumb , payload ) ; } } ) ; ipcMain . on ( MainIpcChannel . Refresh , ( event , projectId ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . reloadIgnoringCache ( ) ; } } ) ; ipcMain . on ( MainIpcChannel . Forward , ( event , projectId ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . goForward ( ) ; } } ) ; ipcMain . on ( MainIpcChannel . Backward , ( event , projectId ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . goBack ( ) ; } } ) ; ipcMain . on ( MainIpcChannel . DisableLink , ( event , projectId , isDisable ) => { const project = projects . get ( projectId ) ; if ( project ) { project . view . webContents . send ( MainIpcChannel . DisableLink , isDisable ) ; } } ) ; ipcMain . on ( MainIpcChannel . ElectronLog , ( event , message ) => { log . debug ( message ) ; } ) ; if ( ! isProd && isWin ) { app . setAsDefaultProtocolClient ( 'derealize' , process . execPath , [ path . resolve ( process . argv [ 1 ] ) ] ) ; } else { app . setAsDefaultProtocolClient ( 'derealize' ) ; } const extractRx = /derealize:\/\/template-(\w+)\//g ; const sendTemplateName = ( deeplinkingUrl ) => { const rxr = extractRx . exec ( deeplinkingUrl ) ; if ( rxr && rxr . length > 1 ) { mainLog . info ( 'sendTemplateName rxr[1]' , rxr [ 1 ] ) ; mainWindow ?. webContents . send ( MainIpcChannel . OpenImport , rxr [ 1 ] ) ; } } ; app . on ( 'open-url' , ( event , deeplinkingUrl ) => { event . preventDefault ( ) ; mainLog . info ( 'open-url' , deeplinkingUrl ) ; if ( deeplinkingUrl ) sendTemplateName ( deeplinkingUrl ) ; } ) ; if ( isWin ) { if ( app . requestSingleInstanceLock ( ) ) { app . on ( 'second-instance' , ( e , argv ) => { const deeplinkingUrl = argv . find ( ( arg ) => arg . startsWith ( 'derealize://' ) ) ; mainLog . info ( 'second-instance' , deeplinkingUrl ) ; if ( deeplinkingUrl ) sendTemplateName ( deeplinkingUrl ) ; if ( mainWindow ) { if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ; mainWindow . focus ( ) ; } } ) ; } else { app . quit ( ) ; } } app . whenReady ( ) . then ( async ( ) => { mainLog . info ( `${ app . getName ( ) };isStudio:${ isStudio };userData:${ app . getPath ( 'userData' ) }` ) ; socketId = await findOpenSocket ( ) ; createBackendProcess ( ) ; createWindow ( ) ; checkForUpdates ( true ) ; return null ; } ) . catch ( Sentry . captureException ) ;