feat/docs: [Electron Demo] - full context isolation between processes.

This commit is contained in:
syntaxbullet 2025-05-02 18:35:16 +02:00
parent 603b49a9fd
commit 418f16a872
5 changed files with 261 additions and 175 deletions

@ -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**
<details>
<summary><b>Renderer Configuration</b> (click to show)</summary>
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.
</details>
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.

@ -1,33 +1,32 @@
const XLSX = require("xlsx");
// TODO: Replace deprecated @electron/remote with contextBridgebased 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) => `<div class="file-loaded">
<span class="file-name text-muted text-small">${fileName}</span>
@ -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();

@ -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) 2017present 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(); });

@ -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": {

@ -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,
});