diff --git a/docz/docs/03-demos/19-desktop/01-electron.md b/docz/docs/03-demos/19-desktop/01-electron.md index 8c3c2f2..6e0efee 100644 --- a/docz/docs/03-demos/19-desktop/01-electron.md +++ b/docz/docs/03-demos/19-desktop/01-electron.md @@ -40,22 +40,44 @@ app to read and write workbooks. The app will look like the screenshots below: ## Integration Details -The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be -imported from the main or the renderer thread. +Electron uses a multi-process architecture, with the main process handling system level operations and I/O, and the renderer process handling UI and web content. -The SheetJS `readFile` and `writeFile` methods will use the Electron `fs` module -where available. +**Renderer Process Limitations** -
- Renderer Configuration (click to show) +The renderer process is sandboxed and cannot run any non-browser code. -Electron 9 and later require the preference `nodeIntegration: true` in order to -`require('xlsx')` in the renderer process. +**Main Process Limitations** -Electron 12 and later also require `worldSafeExecuteJavascript: true` and -`contextIsolation: true`. +The main process can run any NodeJS code, but it cannot access the DOM or any browser APIs. -
+To allow communication between the main and renderer processes, Electron recommends building a [context bridge](https://www.electronjs.org/docs/latest/api/context-bridge) to expose low-level system calls and NodeJS APIs to the renderer process. + +```js +// preload.js +const { contextBridge, ipcRenderer, shell } = require('electron'); +// import nodejs modules we wish to expose APIs from. +const path = require('path'); +const XLSX = require('xlsx'); + +// The contextBridge API allows us to expose APIs to the renderer process. +contextBridge.exposeInMainWorld('SheetJSDemoAPI', { + // request OS file dialogs from the main process + openFile: (filters) => ipcRenderer.invoke('dialog:openFile', filters), + saveFile: (filters) => ipcRenderer.invoke('dialog:saveFile', filters), + message: (msg) => ipcRenderer.invoke('dialog:message', msg), + // open external links in the default browser + openExternal: (url) => shell.openExternal(url), + // listen for file open events from the main process + onFileOpened: (cb) => ipcRenderer.on('file-opened', (_e, fp) => cb(fp)), + + // You can use this to expose nodejs APIs to the renderer process. + basename: (p) => path.basename(p), + extname: (p) => path.extname(p), + + // Here for example we are exposing the sheetjs package to the renderer process. + xlsx: XLSX, +}); +``` ### Reading Files @@ -118,88 +140,72 @@ document.getElementById("drop").addEventListener("drop", handleDrop, false); [`XLSX.readFile`](/docs/api/parse-options) reads workbooks from the file system. `showOpenDialog` shows a Save As dialog and returns the selected file name. -Unlike the Web APIs, the `showOpenDialog` flow can be initiated by app code: + +We can now use the exposed APIs from our preload script above to show the open dialog and try to parse the workbook from within the renderer process. ```js -/* from the renderer thread */ -const electron = require('@electron/remote'); +// index.js - renderer process + +// our exposed bridge APIs are available as SheetJSDemoAPI on the window object +const openFile = SheetJSDemoAPI.openFile; // request the open file dialog from the main process +// We can also access the SheetJS package from the exposed bridge APIs +const XLSX = SheetJSDemoAPI.xlsx; /* this function will show the open dialog and try to parse the workbook */ async function importFile() { - /* show Save As dialog */ - const result = await electron.dialog.showOpenDialog({ - title: 'Select a file', - filters: [{ + /* show open file dialog */ + const result = await openFile([{ name: "Spreadsheets", extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */] - }] - }); + }]); /* result.filePaths is an array of selected files */ if(result.filePaths.length == 0) throw new Error("No file was selected!"); // highlight-next-line return XLSX.readFile(result.filePaths[0]); } ``` - -:::note pass - -`showOpenDialog` originally returned an array of paths: +The actual implementation of the `openFile` function is handled within the main process in `main.js`. ```js -var dialog = require('electron').remote.dialog; +// main.js - main process +const { ipcMain, dialog } = require('electron'); -function importFile(workbook) { - var result = dialog.showOpenDialog({ properties: ['openFile'] }); - return XLSX.readFile(result[0]); -} +ipcMain.handle('dialog:openFile', (_e, filters) => + dialog.showOpenDialog({ title: 'Select a file', filters, properties: ['openFile'] }) +); ``` -This method was renamed to `showOpenDialogSync` in Electron 6. - -::: - ### Writing Files [`XLSX.writeFile`](/docs/api/write-options) writes workbooks to the file system. `showSaveDialog` shows a Save As dialog and returns the selected file name: +The implementation for saving files looks very similar to the one above thanks to our bridge API. ```js -/* from the renderer thread */ -const electron = require('@electron/remote'); +// index.js - renderer process +// our exposed bridge APIs are available as SheetJSDemoAPI on the window object +const saveFile = window.SheetJSDemoAPI.saveFile; // request the save file dialog from the main process +const XLSX = window.SheetJSDemoAPI.xlsx; -/* this function will show the save dialog and try to write the workbook */ async function exportFile(workbook) { - /* show Save As dialog */ - const result = await electron.dialog.showSaveDialog({ - title: 'Save file as', - filters: [{ + const result = await saveFile([{ name: "Spreadsheets", extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */] - }] - }); - /* write file */ - // highlight-next-line - XLSX.writeFile(workbook, result.filePath); + }]); + if(result.filePaths.length == 0) throw new Error("No file was selected!"); + XLSX.writeFile(workbook, result.filePaths[0]); } ``` - -:::note pass - -`showSaveDialog` originally returned the selected path: - +And here is the implementation of the `saveFile` function in `main.js`: ```js -var dialog = require('electron').remote.dialog; +// main.js - main process +const { ipcMain, dialog } = require('electron'); -function exportFile(workbook) { - var result = dialog.showSaveDialog(); - XLSX.writeFile(workbook, result); -} +ipcMain.handle('dialog:saveFile', (_e, filters) => + dialog.showSaveDialog({ title: 'Save file as', filters }) +); ``` -This method was renamed to `showSaveDialogSync` in Electron 6. - -::: - ## Complete Example :::note Tested Deployments @@ -217,21 +223,17 @@ This demo was tested in the following environments: ::: -This demo includes a drag-and-drop box as well as a file input box, mirroring -the [SheetJS Data Preview Live Demo](https://oss.sheetjs.com/sheetjs/) - -The core data in this demo is an editable HTML table. The readers build up the -table using `sheet_to_html` (with `editable:true` option) and the writers scrape -the table using `table_to_book`. - The demo project is wired for `electron-forge` to build the standalone binary. +You can also use `electron-builder` to build a packaged installer binary. + 1) Download the demo files: - [`package.json`](pathname:///electron/package.json) : project structure - [`main.js`](pathname:///electron/main.js) : main process script - [`index.html`](pathname:///electron/index.html) : window page - [`index.js`](pathname:///electron/index.js) : script loaded in render context +- [`preload.js`](pathname:///electron/preload.js) : preload script (ContextBridge API worker) - [`styles.css`](pathname:///electron/styles.css) : stylesheet :::caution pass @@ -250,6 +252,7 @@ curl -LO https://docs.sheetjs.com/electron/package.json curl -LO https://docs.sheetjs.com/electron/main.js curl -LO https://docs.sheetjs.com/electron/index.html curl -LO https://docs.sheetjs.com/electron/index.js +curl -LO https://docs.sheetjs.com/electron/preload.js curl -LO https://docs.sheetjs.com/electron/styles.css ``` @@ -268,6 +271,7 @@ curl.exe -LO https://docs.sheetjs.com/electron/package.json curl.exe -LO https://docs.sheetjs.com/electron/main.js curl.exe -LO https://docs.sheetjs.com/electron/index.html curl.exe -LO https://docs.sheetjs.com/electron/index.js +curl.exe -LO https://docs.sheetjs.com/electron/preload.js curl.exe -LO https://docs.sheetjs.com/electron/styles.css ``` @@ -293,8 +297,13 @@ The app will run. ```bash npm run make ``` +or +```bash +npm run dist +``` +if you want to generate an installer binary. -This will create a package in the `out\make` folder and a standalone binary. +This will create a package in the `out\make` folder and a standalone binary, or an installer binary in `/dist` if you used `npm run dist`. :::caution pass @@ -308,9 +317,11 @@ When the demo was last tested on Windows ARM, the generated binary targeted x64. The program will run on ARM64 Windows. ::: + ### Working with OS level file open events. + The demo has been preconfigured to handle OS level file open events, such as the "open with" context menu or `open` CLI command for all file types SheetJS supports. -In order to register your application as a handler for any other file types, it is necessary to modify the `package.json` file as such. +In order to pre-register your application as a handler for any other file types, it is necessary to modify the `package.json` file as such. ```json // ...existing content @@ -334,10 +345,6 @@ In order to register your application as a handler for any other file types, it ``` this snippet makes it possible to generate installers for MacOS, Windows and Linux which will automatically register the application as a handler for the specified file types. -```sh -npm run dist # generate installers for macos, windows and linux -``` - :::info pass It is also possible to open files using the "open with" context menu without registering the application as a handler for the specified file types. This however, requires manually selecting the application binary as a target to open the file with. @@ -365,7 +372,7 @@ navigate to the Downloads folder and select `pres.numbers`. The application should show data in a table. -8) Click "Export Data!" and click "Save" in the popup. By default, it will try +8) Click "Export" and click "Save" in the popup. By default, it will try to write to `Untitled.xls` in the Downloads folder. :::note pass @@ -379,27 +386,40 @@ If there is no default name, enter `Untitled.xls` and click "Save". The app will show a popup once the data is exported. Open the file in a spreadsheet editor and compare the data to the table shown in the application. -#### Drag and Drop - +#### Open with menu 9) Close the application, end the terminal process and re-launch (see step 6) 10) Open the Downloads folder in a file explorer or finder window. -11) Click and drag the `pres.numbers` file from the Downloads folder to the +11) Right-click the `pres.numbers` file and select "Open with". + +12) Select your application binary by navigating to the folder where the application was built (see step 4). + + + +The application should show data in a table. + +#### Drag and Drop + +13) Close the application, end the terminal process and re-launch (see step 6) + +14) Open the Downloads folder in a file explorer or finder window. + +15) Click and drag the `pres.numbers` file from the Downloads folder to the bordered "Drop a spreadsheet file" box. The file data should be displayed. #### File Input Element -12) Close the application, end the terminal process and re-launch (see step 6) +16) Close the application, end the terminal process and re-launch (see step 6) -13) Click "Choose File". With the file picker, navigate to the Downloads folder +17) Click "Choose File". With the file picker, navigate to the Downloads folder and select `pres.numbers`. ## Electron Breaking Changes The first version of this demo used Electron `1.7.5`. The current demo includes -the required changes for Electron `35.1.2`. +the required changes for Electron `36.1.0`. There are no Electron-specific workarounds in the library, but Electron broke backwards compatibility multiple times. A summary of changes is noted below. @@ -425,6 +445,10 @@ Electron 12 and later also require `worldSafeExecuteJavascript: true` and Electron 14 and later must use `@electron/remote` instead of `remote`. An `initialize` call is required to enable Developer Tools in the window. +For demos built on top of Electron 36 and later we isolate the processes entirely and the demo no longer requires `@electron/remote`. +However, `nodeIntegration: false` by default now means that the renderer process no longer has access to NodeJS APIs. +To expose NodeJS APIs to the renderer process, we use the contextBridge API to expose APIs from the main process to the renderer process. [See more](https://www.electronjs.org/docs/latest/api/context-bridge). This has been best practice since Electron 25. ::: + [^1]: See ["Makers"](https://www.electronforge.io/config/makers) in the Electron Forge documentation. On Linux, the demo generates `rpm` and `deb` distributables. On Arch Linux and the Steam Deck, `sudo pacman -Syu rpm-tools dpkg fakeroot` installed required packages. On Debian and Ubuntu, `sudo apt-get install rpm` sufficed. \ No newline at end of file diff --git a/docz/static/electron/index.js b/docz/static/electron/index.js index 9cf5f8c..365cd91 100644 --- a/docz/static/electron/index.js +++ b/docz/static/electron/index.js @@ -1,33 +1,32 @@ -const XLSX = require("xlsx"); -// TODO: Replace deprecated @electron/remote with contextBridge‑based IPC in production. -const electron = require("@electron/remote"); -const { ipcRenderer } = require("electron"); -const path = require("path"); +const XLSX = window.SheetJSDemoAPI.xlsx; +const basename = window.SheetJSDemoAPI.basename; +const extname = window.SheetJSDemoAPI.extname; +const onFileOpened = window.SheetJSDemoAPI.onFileOpened; // --------------------------------------------------------------------------- // Supported file extensions // --------------------------------------------------------------------------- const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split( - "|", + "|" ); // --------------------------------------------------------------------------- // DOM references // --------------------------------------------------------------------------- -const dropContainer = document.getElementById("drop-container"); -const fileStatus = document.getElementById("fileStatus"); -const exportBtn = document.getElementById("exportBtn"); +const dropContainer = document.getElementById("drop-container"); +const fileStatus = document.getElementById("fileStatus"); +const exportBtn = document.getElementById("exportBtn"); const spinnerOverlay = document.getElementById("spinner-overlay"); -const htmlout = document.getElementById("htmlout"); -const onError = document.getElementById("onError"); +const htmlout = document.getElementById("htmlout"); +const onError = document.getElementById("onError"); // --------------------------------------------------------------------------- // State & helpers // --------------------------------------------------------------------------- -let currentWorkbook = null; // SheetJS workbook in memory +let currentWorkbook = null; // SheetJS workbook in memory const isSpreadsheet = (ext) => EXTENSIONS.includes(ext.toLowerCase()); -const nextPaint = () => new Promise(requestAnimationFrame); +const nextPaint = () => new Promise(requestAnimationFrame); // --------------------------------------------------------------------------- // Open external links in default browser (security) @@ -35,7 +34,7 @@ const nextPaint = () => new Promise(requestAnimationFrame); document.addEventListener("click", (e) => { if (e.target.tagName === "A" && e.target.href.startsWith("http")) { e.preventDefault(); - electron.shell.openExternal(e.target.href); + window.SheetJSDemoAPI.openExternal(e.target.href); } }); @@ -45,16 +44,18 @@ document.addEventListener("click", (e) => { async function exportWorkbookAsFile() { if (!currentWorkbook) return displayError("No workbook loaded!"); // -- 1. use electron save as dialog to get file path - const { filePath, canceled } = await electron.dialog.showSaveDialog({ - title: "Save file as", - filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], - }); + const { filePath, canceled } = await window.SheetJSDemoAPI.saveFile([ + { + name: "Spreadsheets", + extensions: EXTENSIONS, + }, + ]); // -- 2. if canceled or no file path, return if (canceled || !filePath) return; // -- 3. write workbook to file try { XLSX.writeFile(currentWorkbook, filePath); - electron.dialog.showMessageBox({ message: `Exported to ${filePath}` }); + window.SheetJSDemoAPI.message(`Exported to ${filePath}`); } catch (err) { // -- 4. if error, display error displayError(`Failed to export: ${err.message}`); @@ -83,14 +84,19 @@ function renderWorkbookToTables(wb) { // --------------------------------------------------------------------------- // Generic UI helpers // --------------------------------------------------------------------------- -const displayError = (msg) => (onError ? ((onError.textContent = msg), (onError.hidden = false)) : console.error(msg)); -const hideDropUI = () => dropContainer && (dropContainer.style.display = "none"); -const showDropUI = () => dropContainer && (dropContainer.style.display = "block"); -const hideExportBtn = () => (exportBtn.disabled = true); -const showExportBtn = () => (exportBtn.disabled = false); -const showSpinner = () => (spinnerOverlay.style.display = "flex"); -const hideSpinner = () => (spinnerOverlay.style.display = "none"); -const hideOutputUI = () => (htmlout.innerHTML = ""); +const displayError = (msg) => + onError + ? ((onError.textContent = msg), (onError.hidden = false)) + : console.error(msg); +const hideDropUI = () => + dropContainer && (dropContainer.style.display = "none"); +const showDropUI = () => + dropContainer && (dropContainer.style.display = "block"); +const hideExportBtn = () => (exportBtn.disabled = true); +const showExportBtn = () => (exportBtn.disabled = false); +const showSpinner = () => (spinnerOverlay.style.display = "flex"); +const hideSpinner = () => (spinnerOverlay.style.display = "none"); +const hideOutputUI = () => (htmlout.innerHTML = ""); const hideLoadedFileUI = () => (fileStatus.innerHTML = ""); const getLoadedFileUI = (fileName) => `
${fileName} @@ -121,24 +127,26 @@ fileStatus.addEventListener("click", (e) => { // --------------------------------------------------------------------------- async function handleReadBtn() { // -- 1. show file open dialog to get the file path - const { filePaths, canceled } = await electron.dialog.showOpenDialog({ - title: "Select a file", - filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], - properties: ["openFile"], - }); + const { filePaths, canceled } = await window.SheetJSDemoAPI.openFile([ + { + name: "Spreadsheets", + extensions: EXTENSIONS, + }, + ]); // -- 2. if canceled or no file path, return if (canceled || !filePaths.length) return; // -- 3. if multiple files selected, return error - if (filePaths.length !== 1) return displayError("Please choose a single file."); + if (filePaths.length !== 1) + return displayError("Please choose a single file."); showSpinner(); - await nextPaint(); // ensure spinner paints + await nextPaint(); // ensure spinner paints try { // -- 4. read the first selected file const filePath = filePaths[0]; currentWorkbook = XLSX.readFile(filePath); renderWorkbookToTables(currentWorkbook); - showLoadedFileUI(path.basename(filePath)); + showLoadedFileUI(basename(filePath)); } finally { hideSpinner(); hideDropUI(); @@ -192,7 +200,7 @@ async function readFile(files) { // -- 2. get the first file const file = files[0]; // -- 3. if not a spreadsheet, return error - const ext = path.extname(file.name).slice(1); + const ext = extname(file.name).slice(1); if (!isSpreadsheet(ext)) return displayError(`Unsupported file type .${ext}`); showSpinner(); @@ -216,10 +224,10 @@ async function readFile(files) { // --------------------------------------------------------------------------- attachFileListeners(); // the file-opened event is sent from the main process when a file is opened using "open with" -ipcRenderer.on("file-opened", async (_e, filePath) => { +onFileOpened(async (_e, filePath) => { showSpinner(); await nextPaint(); // ensure spinner paints - currentWorkbook = XLSX.readFile(filePath); + currentWorkbook = XLSX.readFile(filePath); renderWorkbookToTables(currentWorkbook); showLoadedFileUI(path.basename(filePath)); hideSpinner(); diff --git a/docz/static/electron/main.js b/docz/static/electron/main.js index a5ed378..874ff8d 100644 --- a/docz/static/electron/main.js +++ b/docz/static/electron/main.js @@ -1,85 +1,120 @@ /* sheetjs (C) 2013-present SheetJS -- https://sheetjs.com */ -var electron = require('electron'); -var XLSX = require('xlsx'); -var app = electron.app; -require('@electron/remote/main').initialize(); // required for Electron 14+ - -var win = null; +const { app, BrowserWindow, ipcMain, dialog } = require('electron'); +const path = require('path'); +const XLSX = require('xlsx'); const EXT_REGEX = /\.(xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers)$/i; -const pendingPaths = []; // any paths that arrive before the window exists +const pendingPaths = []; // paths of files that were opened before the window was created. These are queued and loaded once the window is created. +let win = null; // reference to the main window, to make sure we don't create multiple windows. -// send file opening events to renderer -function sendToRenderer(filePath) { - if (win && win.webContents) { - win.webContents.send('file-opened', filePath); - } else { - pendingPaths.push(filePath); - } +/* In electron, the main process is the only process that can directly interface with the operating system. + The renderer process is sandboxed and cannot run any non-browser code. + To allow the renderer process to interface with the operating system, we use the contextBridge API to expose the API to the renderer process. + https://www.electronjs.org/docs/latest/api/context-bridge +*/ + +/* ----------------------------------------------------------------------------- */ +/* IPC handlers that allow communication between main and renderer processes */ +/* ----------------------------------------------------------------------------- */ + +/* These three functions can be used to interface with the operating system from the renderer process. + the ipcMain.handle() function is used to register a handler for a specific event. + when the renderer process calls the corresponding function, the main process will receive the event and execute the handler. + + In this case, we are listening to events which allow the renderer process to open/save file dialogs and show modal messages. +*/ +ipcMain.handle('dialog:openFile', (_e, filters) => + dialog.showOpenDialog({ title: 'Select a file', filters, properties: ['openFile'] }) +); + +ipcMain.handle('dialog:saveFile', (_e, filters) => + dialog.showSaveDialog({ title: 'Save file as', filters }) +); + +ipcMain.handle('dialog:message', (_e, msg) => + dialog.showMessageBox({ message: msg, buttons: ['OK'] }) +); + +/* ----------------------------------------------------------------------------- */ +/* Utility functions */ +/* ----------------------------------------------------------------------------- */ +function sendToRenderer(fp) { + if (win && win.webContents) win.webContents.send('file-opened', fp); + else pendingPaths.push(fp); } +/* + On Windows and Linux, opening a file using the "open with" menu option or `open` command will pass the file path as a startup argument to the app. + We need to parse it, and test if it is a spreadsheet file. +*/ function firstSpreadsheetFromArgv() { - const args = process.defaultApp // dev: electron . - ? process.argv.slice(2) // skip electron executable & dir - : process.argv.slice(1); // skip packaged exe - return args.find(a => EXT_REGEX.test(a)); // undefined if none + const args = process.defaultApp ? process.argv.slice(2) : process.argv.slice(1); + return args.find((a) => EXT_REGEX.test(a)); } -/* ---- single-instance guard (needed for Windows / Linux) ---- */ -// on windows and linux, opening a file opens a new instance of the app, this prevents that. -// https://www.electronjs.org/docs/latest/api/app#event-second-instance -const gotLock = app.requestSingleInstanceLock(); -if (!gotLock) app.quit(); +/* ----------------------------------------------------------------------------- */ +/* Single-instance guard */ +/* ----------------------------------------------------------------------------- */ + +// Windows and Linux only: If the app is already running, we need to prevent a new instance from launching when opening a file via the "open with" menu option or `open` command. +if (!app.requestSingleInstanceLock()) app.quit(); else { - app.on('second-instance', (_e, argv) => { // emitted in *primary* instance - const fp = argv.find(a => EXT_REGEX.test(a)); + app.on('second-instance', (_e, argv) => { + const fp = argv.find((a) => EXT_REGEX.test(a)); if (fp) sendToRenderer(fp); if (win) { win.show(); win.focus(); } }); } -/* ---- platform-specific “open file” hooks (macOS) ---- */ -// https://www.electronjs.org/docs/latest/api/app#event-open-file-macos -app.on('open-file', (event, fp) => { // macOS Dock / Finder - event.preventDefault(); - sendToRenderer(fp); -}); -// https://www.electronjs.org/docs/latest/api/app#event-open-url-macos -app.on('open-url', (event, url) => { // you can add a custom protocol if you want to handle URLs - event.preventDefault(); - sendToRenderer(url.replace('file://', '')); // crude, adjust if you keep deep-links -}); +// macOS file / url events +app.on('open-file', (evt, fp) => { evt.preventDefault(); sendToRenderer(fp); }); +app.on('open-url', (evt, url) => { evt.preventDefault(); sendToRenderer(url.replace('file://', '')); }); -/* ---- normal start-up, harvest argv (Windows & Linux) ---- */ -app.whenReady().then(() => { - const fp = firstSpreadsheetFromArgv(); // Windows & Linux first launch - if (fp) pendingPaths.push(fp); - createWindow(); -}); - -/* ---- create the window ---- */ +/* ----------------------------------------------------------------------------- */ +/* Create the window */ +/* ----------------------------------------------------------------------------- */ function createWindow() { if (win) return; - win = new electron.BrowserWindow({ - width: 800, height: 600, + + win = new BrowserWindow({ + width: 800, + height: 600, webPreferences: { - worldSafeExecuteJavaScript: true, // required for Electron 12+ - contextIsolation: false, // required for Electron 12+ - nodeIntegration: true, - enableRemoteModule: true + preload: path.join(__dirname, './preload.js'), // preload script that will be executed in the renderer process before the page is loaded and act as a bridge between the main and renderer processes within a worker thread. + contextIsolation: true, // isolate and enable bridge, keeping the renderer process sandboxed and separated from the main process. + nodeIntegration: false, // no Node.js in renderer process. + nodeIntegrationInWorker: true, // enable Node.js in worker threads. + worldSafeExecuteJavaScript: true } }); - win.loadURL("file://" + __dirname + "/index.html"); - require('@electron/remote/main').enable(win.webContents); // required for Electron 14 - if (process.env.NODE_ENV === 'development') win.webContents.openDevTools(); // only open devtools in development - win.on('closed', function () { win = null; }); + + win.loadFile('index.html'); + if (process.env.NODE_ENV === 'development') win.webContents.openDevTools(); + + win.on('closed', () => { win = null; }); + win.webContents.once('did-finish-load', () => { pendingPaths.splice(0).forEach(sendToRenderer); }); } -if (app.setAboutPanelOptions) app.setAboutPanelOptions({ applicationName: 'sheetjs-electron', applicationVersion: "XLSX " + XLSX.version, copyright: "(C) 2017-present SheetJS LLC" }); -app.on('ready', createWindow); +/* ----------------------------------------------------------------------------- */ +/* App lifecycle */ +/* ----------------------------------------------------------------------------- */ +app.whenReady().then(() => { + const fp = firstSpreadsheetFromArgv(); + if (fp) pendingPaths.push(fp); + createWindow(); +}); + +if (app.setAboutPanelOptions) { + app.setAboutPanelOptions({ + applicationName: 'sheetjs-electron', + applicationVersion: `XLSX ${XLSX.version}`, + copyright: '(C) 2017‑present SheetJS LLC' + }); +} + app.on('activate', createWindow); -app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit(); }); +app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); \ No newline at end of file diff --git a/docz/static/electron/package.json b/docz/static/electron/package.json index a8fcee0..84c5a00 100644 --- a/docz/static/electron/package.json +++ b/docz/static/electron/package.json @@ -6,7 +6,6 @@ "version": "0.0.0", "main": "main.js", "dependencies": { - "@electron/remote": "2.1.2", "xlsx": "https://sheet.lol/balls/xlsx-0.20.3.tgz" }, "scripts": { diff --git a/docz/static/electron/preload.js b/docz/static/electron/preload.js new file mode 100644 index 0000000..cc91b6b --- /dev/null +++ b/docz/static/electron/preload.js @@ -0,0 +1,20 @@ +const { contextBridge, ipcRenderer, shell } = require('electron'); +const path = require('path'); +const XLSX = require('xlsx'); + +// Because the main process is sandboxed, we need to use the contextBridge API to expose the API to the renderer process. +// https://www.electronjs.org/docs/latest/api/context-bridge +contextBridge.exposeInMainWorld('SheetJSDemoAPI', { + openFile: (filters) => ipcRenderer.invoke('dialog:openFile', filters), + saveFile: (filters) => ipcRenderer.invoke('dialog:saveFile', filters), + message: (msg) => ipcRenderer.invoke('dialog:message', msg), + openExternal: (url) => shell.openExternal(url), + // expose file-opened event + onFileOpened: (cb) => ipcRenderer.on('file-opened', (_e, fp) => cb(fp)), + // expose basename from path package + basename: (p) => path.basename(p), + // expose extname from path package + extname: (p) => path.extname(p), + // expose sheetjs package functions + xlsx: XLSX, +}); \ No newline at end of file