Repository: electron/fiddle (ts)

Issue:
Security: follow best practices, maybe update the default fiddle to electron-quick-start?
I just started using Fiddle today after working with Electron for a while. I find it worrying that the default example when you start Fiddle does not follow best-practices and purposely makes the code less secure. Sure it's cool to demonstrate `nodeIntegration` in the `index.html`, but isn't that advanced stuff? Stuff that you should only do once you need to and when you absolutely know what you're doing? (From my experience it is never needed, since you can expose a minimal api via preload).  I must assume many newcomers will first come into contact with Electron via Fiddle and might use the code as a starting point. They might not be aware of the immense implications of `nodeIntegration` and might get comfortable with doing it the wrong way (blindly using Node.js inside renderer, instead of exposing well defined APIs from preload).

Is there a reason Fiddle doesn't just use the same code https://github.com/electron/electron-quick-start does? It looks like a sane starting point, e.g. Node.js integration is disabled and it demonstrates how to safely use Node.js inside preload while still interacting with the page https://github.com/electron/electron-quick-start/blob/d2db0d4cc1f7ebcf6c562eeb5d5e3a8eabd0432e/preload.js

I've searched the issues and haven't seen this come up yet if I haven't missed anything.
Add support for Electron 12 with contextIsolation enabled by default
The default app template is showing errors when running on Electron v12.0.0
<img width="912" alt="Screen Shot 2021-03-02 at 11 37 12 PM" src="https://user-images.githubusercontent.com/1281234/109724707-4199ef80-7bb0-11eb-9e0e-aacfe49946b3.png">


Pull request:
chore: update default app to work w/Electron 12.0.0
Fixes #585.
Closes #377.

Update the default app to match [`electron-quick-start`](https://github.com/electron/electron-quick-start) because the previous default app no longer works out-of-the box in Electron 12.0.0. This change requires that `preload.js` is visible by default, so startup now shows a four-pane layout.

Patch:
diff --git a/.husky/pre-commit b/.husky/pre-commit
old mode 100644
new mode 100755
diff --git a/src/content/html.ts b/src/content/html.ts
index a3caa491..9b5bae1c 100644
--- a/src/content/html.ts
+++ b/src/content/html.ts
@@ -2,19 +2,18 @@ export const html = `<!DOCTYPE html>
 <html>
   <head>
     <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
     <title>Hello World!</title>
-    <link rel="stylesheet" type="text/css" href="./styles.css">
   </head>
   <body>
     <h1>Hello World!</h1>
-    <!-- All of the Node.js APIs are available in this renderer process. -->
-    We are using Node.js <script>document.write(process.versions.node)</script>,
-    Chromium <script>document.write(process.versions.chrome)</script>,
-    and Electron <script>document.write(process.versions.electron)</script>.
+    We are using Node.js <span id="node-version"></span>,
+    Chromium <span id="chrome-version"></span>,
+    and Electron <span id="electron-version"></span>.
 
-    <script>
-      // You can also require other files to run in this process
-      require('./renderer.js')
-    </script>
+    <!-- You can also require other files to run in this process -->
+    <script src="./renderer.js"></script>
   </body>
 </html>`;
diff --git a/src/content/main.ts b/src/content/main.ts
index 8df38d03..85dd420f 100644
--- a/src/content/main.ts
+++ b/src/content/main.ts
@@ -1,5 +1,6 @@
 export const main = `// Modules to control application life and create native browser window
 const {app, BrowserWindow} = require('electron')
+const path = require('path')
 
 function createWindow () {
   // Create the browser window.
@@ -7,7 +8,7 @@ function createWindow () {
     width: 800,
     height: 600,
     webPreferences: {
-      nodeIntegration: true
+      preload: path.join(__dirname, 'preload.js')
     }
   })
 
@@ -21,23 +22,21 @@ function createWindow () {
 // This method will be called when Electron has finished
 // initialization and is ready to create browser windows.
 // Some APIs can only be used after this event occurs.
-app.whenReady().then(createWindow)
+app.whenReady().then(() => {
+  createWindow()
+  
+  app.on('activate', function () {
+    // On macOS it's common to re-create a window in the app when the
+    // dock icon is clicked and there are no other windows open.
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
 
 // Quit when all windows are closed, except on macOS. There, it's common
 // for applications and their menu bar to stay active until the user quits
 // explicitly with Cmd + Q.
 app.on('window-all-closed', function () {
-  if (process.platform !== 'darwin') {
-    app.quit()
-  }
-})
-
-app.on('activate', function () {
-  // On OS X it's common to re-create a window in the app when the
-  // dock icon is clicked and there are no other windows open.
-  if (BrowserWindow.getAllWindows().length === 0) {
-    createWindow()
-  }
+  if (process.platform !== 'darwin') app.quit()
 })
 
 // In this file you can include the rest of your app's specific main process
diff --git a/src/content/preload.ts b/src/content/preload.ts
new file mode 100644
index 00000000..af253282
--- /dev/null
+++ b/src/content/preload.ts
@@ -0,0 +1,12 @@
+export const preload = `// All of the Node.js APIs are available in the preload process.
+// It has the same sandbox as a Chrome extension.
+window.addEventListener('DOMContentLoaded', () => {
+  const replaceText = (selector, text) => {
+    const element = document.getElementById(selector)
+    if (element) element.innerText = text
+  }
+
+  for (const type of ['chrome', 'node', 'electron']) {
+    replaceText(\`\${type}-version\`, process.versions[type])
+  }
+})`;
diff --git a/src/content/renderer.ts b/src/content/renderer.ts
index 605b40f3..e3d475ea 100644
--- a/src/content/renderer.ts
+++ b/src/content/renderer.ts
@@ -1,3 +1,6 @@
 export const renderer = `// This file is required by the index.html file and will
 // be executed in the renderer process for that window.
-// All of the Node.js APIs are available in this process.`;
+// No Node.js APIs are available in this process because
+// \`nodeIntegration\` is turned off. Use \`preload.js\` to
+// selectively enable features needed in the rendering
+// process.`;
diff --git a/src/renderer/components/editors-non-ideal-state.tsx b/src/renderer/components/editors-non-ideal-state.tsx
index fe2f0983..490892f3 100644
--- a/src/renderer/components/editors-non-ideal-state.tsx
+++ b/src/renderer/components/editors-non-ideal-state.tsx
@@ -6,7 +6,7 @@ import { EditorId } from '../../interfaces';
 import { AppState } from '../state';
 
 export function renderNonIdealState(appState: AppState) {
-  const allEditors = [EditorId.html, EditorId.main, EditorId.renderer];
+  const allEditors = [EditorId.html, EditorId.main, EditorId.renderer, EditorId.preload];
   const resolveButton = (
     <Button
       text="Open all editors"
diff --git a/src/renderer/components/editors.tsx b/src/renderer/components/editors.tsx
index c37298ab..da50affc 100644
--- a/src/renderer/components/editors.tsx
+++ b/src/renderer/components/editors.tsx
@@ -98,6 +98,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
         {
           main: await getContent(EditorId.main, version),
           renderer: await getContent(EditorId.renderer, version),
+          preload: await getContent(EditorId.preload, version),
           html: await getContent(EditorId.html, version),
         },
         {},
diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts
index b9e27411..16e9780d 100644
--- a/src/renderer/constants.ts
+++ b/src/renderer/constants.ts
@@ -15,10 +15,14 @@ export const CONFIG_PATH = path.join(
 
 export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
   direction: 'row',
-  first: EditorId.main,
+  first: {
+    direction: 'column',
+    first: EditorId.main,
+    second: EditorId.renderer,
+  },
   second: {
     direction: 'column',
-    first: EditorId.renderer,
+    first: EditorId.preload,
     second: EditorId.html,
   },
 };
@@ -28,7 +32,6 @@ export const DEFAULT_CLOSED_PANELS: Partial<Record<
   EditorBackup | true
 >> = {
   docsDemo: true,
-  preload: getEditorBackup(EditorId.preload),
   css: getEditorBackup(EditorId.css),
 };
 
diff --git a/src/renderer/content.ts b/src/renderer/content.ts
index daa9d734..3154a8b7 100644
--- a/src/renderer/content.ts
+++ b/src/renderer/content.ts
@@ -20,6 +20,10 @@ export async function getContent(
     return (await import('../content/renderer')).renderer;
   }
 
+  if (name === EditorId.preload) {
+    return (await import('../content/preload')).preload;
+  }
+
   if (name === EditorId.main) {
     // We currently only distinguish between loadFile
     // and loadURL. TODO: Properly version the quick-start.
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index a6257c55..338be5e0 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -73,6 +73,7 @@ window.ElectronFiddle =
       main: null,
       renderer: null,
       html: null,
+      preload: null,
     },
     app: null,
   } as any);


Test patch:
diff --git a/tests/mocks/editors.ts b/tests/mocks/editors.ts
index 7470343b..1ed8c954 100644
--- a/tests/mocks/editors.ts
+++ b/tests/mocks/editors.ts
@@ -18,4 +18,5 @@ export class EditorsMock {
   public main = new EditorMock('main');
   public renderer = new EditorMock('renderer');
   public html = new EditorMock('html');
+  public preload = new EditorMock('preload');
 }
diff --git a/tests/renderer/state-spec.ts b/tests/renderer/state-spec.ts
index 977952cf..91a6fc41 100644
--- a/tests/renderer/state-spec.ts
+++ b/tests/renderer/state-spec.ts
@@ -608,7 +608,11 @@ describe('AppState', () => {
       expect(appState.mosaicArrangement).toEqual({
         direction: 'row',
         first: EditorId.renderer,
-        second: EditorId.html,
+        second: {
+          direction: 'column',
+          first: EditorId.preload,
+          second: EditorId.html,
+        }
       });
       expect(appState.closedPanels[EditorId.main]).toBeTruthy();
       expect(appState.closedPanels[EditorId.renderer]).toBeUndefined();
@@ -636,7 +640,11 @@ describe('AppState', () => {
       expect(appState.mosaicArrangement).toEqual({
         direction: 'row',
         first: EditorId.renderer,
-        second: EditorId.html,
+        second: {
+          direction: 'column',
+          first: EditorId.preload,
+          second: EditorId.html,
+        }
       });
 
       appState.resetEditorLayout();
diff --git a/tests/utils/editor-mounted-spec.ts b/tests/utils/editor-mounted-spec.ts
index 3686092b..94220e76 100644
--- a/tests/utils/editor-mounted-spec.ts
+++ b/tests/utils/editor-mounted-spec.ts
@@ -6,14 +6,16 @@ describe('waitforEditorsToMount', () => {
   it('resolves when editors match list', async () => {
     // these editors should be mounted by default.
     const { ElectronFiddle: fiddle } = window as any;
-    await waitForEditorsToMount([
+    const ids = [
       EditorId.html,
       EditorId.main,
+      EditorId.preload,
       EditorId.renderer,
-    ]);
-    expect(fiddle.editors).toHaveProperty(EditorId.html);
-    expect(fiddle.editors).toHaveProperty(EditorId.main);
-    expect(fiddle.editors).toHaveProperty(EditorId.renderer);
+    ];
+    await waitForEditorsToMount(ids);
+    for (const id of ids) {
+      expect(fiddle.editors).toHaveProperty(id);
+    }
   });
 
   it('rejects if editors fail to match list', async () => {
diff --git a/tests/utils/editors-mosaic-arrangement-spec.ts b/tests/utils/editors-mosaic-arrangement-spec.ts
index 0bfabd62..cd9225d1 100644
--- a/tests/utils/editors-mosaic-arrangement-spec.ts
+++ b/tests/utils/editors-mosaic-arrangement-spec.ts
@@ -26,10 +26,11 @@ describe('Mosaic Arrangement Utilities', () => {
       });
     });
 
-    it('creates the correct arrangement for three visible panels', () => {
+    it('creates the correct arrangement for the default visible panels', () => {
       const result = createMosaicArrangement([
         EditorId.main,
         EditorId.renderer,
+        EditorId.preload,
         EditorId.html,
       ]);
 
@@ -60,10 +61,10 @@ describe('Mosaic Arrangement Utilities', () => {
       expect(result).toEqual([EditorId.main, EditorId.renderer]);
     });
 
-    it('returns the correct array for three visible panels', () => {
+    it('returns the correct array for the default mosaic', () => {
       const result = getVisibleMosaics(DEFAULT_MOSAIC_ARRANGEMENT);
 
-      expect(result).toEqual(['main', 'renderer', 'html']);
+      expect(result).toEqual(['main', 'renderer', 'preload', 'html']);
     });
   });
 });