From f156f734940bb66afe59fb8477d8bfb41c44bd0b Mon Sep 17 00:00:00 2001
From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Wed, 11 Dec 2019 23:55:14 -0500
Subject: [PATCH] feat: add typescript-webpack template

---
 packages/api/core/package.json                |  1 +
 packages/api/core/test/slow/api_spec_slow.ts  | 52 +++++++++-
 .../template/typescript-webpack/package.json  | 21 ++++
 .../src/TypeScriptWebpackTemplate.ts          | 96 +++++++++++++++++++
 .../template/typescript-webpack/tmpl/index.ts | 58 +++++++++++
 .../typescript-webpack/tmpl/renderer.ts       | 31 ++++++
 .../typescript-webpack/tmpl/tsconfig.json     | 20 ++++
 .../typescript-webpack/tmpl/tslint.json       | 14 +++
 .../tmpl/webpack.main.config.js               | 14 +++
 .../tmpl/webpack.plugins.js                   |  7 ++
 .../tmpl/webpack.renderer.config.js           | 17 ++++
 .../typescript-webpack/tmpl/webpack.rules.js  | 27 ++++++
 12 files changed, 354 insertions(+), 4 deletions(-)
 create mode 100644 packages/template/typescript-webpack/package.json
 create mode 100644 packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts
 create mode 100644 packages/template/typescript-webpack/tmpl/index.ts
 create mode 100644 packages/template/typescript-webpack/tmpl/renderer.ts
 create mode 100644 packages/template/typescript-webpack/tmpl/tsconfig.json
 create mode 100644 packages/template/typescript-webpack/tmpl/tslint.json
 create mode 100644 packages/template/typescript-webpack/tmpl/webpack.main.config.js
 create mode 100644 packages/template/typescript-webpack/tmpl/webpack.plugins.js
 create mode 100644 packages/template/typescript-webpack/tmpl/webpack.renderer.config.js
 create mode 100644 packages/template/typescript-webpack/tmpl/webpack.rules.js

diff --git a/packages/api/core/package.json b/packages/api/core/package.json
index ecb9eaa693..2bb4d66371 100644
--- a/packages/api/core/package.json
+++ b/packages/api/core/package.json
@@ -39,6 +39,7 @@
     "@electron-forge/shared-types": "6.0.0-beta.46",
     "@electron-forge/template-webpack": "6.0.0-beta.46",
     "@electron-forge/template-typescript": "6.0.0-beta.46",
+    "@electron-forge/template-typescript-webpack": "6.0.0-beta.46",
     "@electron/get": "^1.6.0",
     "colors": "^1.4.0",
     "cross-spawn-promise": "^0.10.1",
diff --git a/packages/api/core/test/slow/api_spec_slow.ts b/packages/api/core/test/slow/api_spec_slow.ts
index 911c092f04..0a5354804a 100644
--- a/packages/api/core/test/slow/api_spec_slow.ts
+++ b/packages/api/core/test/slow/api_spec_slow.ts
@@ -179,16 +179,60 @@ describe(`electron-forge API (with installer=${nodeInstaller})`, () => {
       const expectedFiles = [
         'tsconfig.json',
         'tslint.json',
+        path.join('src', 'index.ts'),
       ];
+
       for (const filename of expectedFiles) {
         await expectProjectPathExists(filename, 'file');
       }
     });
 
-    it('should convert the main process file to typescript', async () => {
-      await expectProjectPathNotExists(path.join('src', 'index.js'), 'file');
-      await expectProjectPathExists(path.join('src', 'index.ts'), 'file');
-      expect((await fs.readFile(path.join(dir, 'src', 'index.ts'))).toString()).to.match(/Electron.BrowserWindow/);
+    describe('lint', () => {
+      it('should initially pass the linting process', async () => {
+        try {
+          await forge.lint({ dir });
+        } catch (err) {
+          if (err.stdout) {
+            // eslint-disable-next-line no-console
+            console.error('STDOUT:', err.stdout.toString());
+            // eslint-disable-next-line no-console
+            console.error('STDERR:', err.stderr.toString());
+          }
+          throw err;
+        }
+      });
+    });
+
+    after(async () => {
+      await fs.remove(dir);
+    });
+  });
+
+  describe('init (with typescript-webpack templater)', () => {
+    before(ensureTestDirIsNonexistent);
+
+    it('should succeed in initializing the typescript template', async () => {
+      await forge.init({
+        dir,
+        template: 'typescript-webpack',
+      });
+    });
+
+    it('should copy the appropriate template files', async () => {
+      const expectedFiles = [
+        'tsconfig.json',
+        'tslint.json',
+        'webpack.main.config.js',
+        'webpack.renderer.config.js',
+        'webpack.rules.js',
+        'webpack.plugins.js',
+        path.join('src', 'index.ts'),
+        path.join('src', 'renderer.ts'),
+      ];
+
+      for (const filename of expectedFiles) {
+        await expectProjectPathExists(filename, 'file');
+      }
     });
 
     describe('lint', () => {
diff --git a/packages/template/typescript-webpack/package.json b/packages/template/typescript-webpack/package.json
new file mode 100644
index 0000000000..99177785e6
--- /dev/null
+++ b/packages/template/typescript-webpack/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "@electron-forge/template-typescript-webpack",
+  "version": "6.0.0-beta.46",
+  "description": "Typescript-Webpack template for Electron Forge",
+  "repository": "https://github.com/electron-userland/electron-forge",
+  "author": "Shelley Vohr <shelley.vohr@gmail.com>",
+  "license": "MIT",
+  "main": "dist/TypeScriptWebpackTemplate.js",
+  "typings": "dist/TypeScriptWebpackTemplate.d.ts",
+  "scripts": {
+    "test": "echo No Tests"
+  },
+  "engines": {
+    "node": ">= 8.0"
+  },
+  "dependencies": {
+    "@electron-forge/async-ora": "6.0.0-beta.46",
+    "@electron-forge/shared-types": "6.0.0-beta.46",
+    "fs-extra": "^8.1.0"
+  }
+}
diff --git a/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts b/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts
new file mode 100644
index 0000000000..7b6fd7cdcc
--- /dev/null
+++ b/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts
@@ -0,0 +1,96 @@
+import { ForgeTemplate } from '@electron-forge/shared-types';
+import { asyncOra } from '@electron-forge/async-ora';
+
+import fs from 'fs-extra';
+import path from 'path';
+
+const currentVersion = require('../package').version;
+
+const copyTemplateFile = async (destDir: string, basename: string) => {
+  const templateDir = path.resolve(__dirname, '..', 'tmpl');
+  await fs.copy(path.join(templateDir, basename), path.resolve(destDir, basename));
+};
+
+const updateFileByLine = async (
+  inputPath: string,
+  lineHandler: (line: string) => string,
+  outputPath: string | undefined = undefined,
+) => {
+  const fileContents = (await fs.readFile(inputPath, 'utf8')).split('\n').map(lineHandler).join('\n');
+  await fs.writeFile(outputPath || inputPath, fileContents);
+  if (outputPath !== undefined) {
+    await fs.remove(inputPath);
+  }
+};
+
+class TypeScriptWebpackTemplate implements ForgeTemplate {
+  public devDependencies = [
+    `@electron-forge/plugin-webpack@${currentVersion}`,
+    '@marshallofsound/webpack-asset-relocator-loader@^0.5.0',
+    'css-loader@^3.0.0',
+    'node-loader@^0.6.0',
+    'ts-loader@^6.2.1',
+    'style-loader@^0.23.1',
+    'typescript@^3.7.0',
+    'tslint@^5.20.0',
+    'fork-ts-checker-webpack-plugin@^3.1.1',
+  ];
+
+  public initializeTemplate = async (directory: string) => {
+    await asyncOra('Setting up Forge configuration', async () => {
+      const packageJSONPath = path.resolve(directory, 'package.json');
+      const packageJSON = await fs.readJson(packageJSONPath);
+
+      packageJSON.main = '.webpack/main';
+      packageJSON.config.forge.plugins = packageJSON.config.forge.plugins || [];
+      packageJSON.config.forge.plugins.push([
+        '@electron-forge/plugin-webpack',
+        {
+          mainConfig: './webpack.main.config.js',
+          renderer: {
+            config: './webpack.renderer.config.js',
+            entryPoints: [{
+              html: './src/index.html',
+              js: './src/renderer.ts',
+              name: 'main_window',
+            }],
+          },
+        },
+      ]);
+
+      // Configure scripts for TS template
+      packageJSON.scripts.lint = 'tslint -c tslint.json -p tsconfig.json';
+
+      await fs.writeJson(packageJSONPath, packageJSON, { spaces: 2 });
+    });
+
+    await asyncOra('Setting up TypeScript configuration', async () => {
+      const filePath = (fileName: string) => path.join(directory, 'src', fileName);
+
+      // Copy Webpack files
+      await copyTemplateFile(directory, 'webpack.main.config.js');
+      await copyTemplateFile(directory, 'webpack.renderer.config.js');
+      await copyTemplateFile(directory, 'webpack.rules.js');
+      await copyTemplateFile(directory, 'webpack.plugins.js');
+
+      await updateFileByLine(path.resolve(directory, 'src', 'index.html'), (line) => {
+        if (line.includes('link rel="stylesheet"')) return '';
+        return line;
+      });
+
+      // Copy tsconfig with a small set of presets
+      await copyTemplateFile(directory, 'tsconfig.json');
+
+      // Copy tslint config with recommended settings
+      await copyTemplateFile(directory, 'tslint.json');
+
+      // Remove index.js and replace with index.ts
+      await fs.remove(filePath('index.js'));
+      await copyTemplateFile(path.join(directory, 'src'), 'index.ts');
+
+      await copyTemplateFile(path.join(directory, 'src'), 'renderer.ts');
+    });
+  }
+}
+
+export default new TypeScriptWebpackTemplate();
diff --git a/packages/template/typescript-webpack/tmpl/index.ts b/packages/template/typescript-webpack/tmpl/index.ts
new file mode 100644
index 0000000000..2fcad01598
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/index.ts
@@ -0,0 +1,58 @@
+import { app, BrowserWindow } from 'electron';
+declare const MAIN_WINDOW_WEBPACK_ENTRY: any;
+
+// Handle creating/removing shortcuts on Windows when installing/uninstalling.
+if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
+  app.quit();
+}
+
+// Keep a global reference of the window object, if you don't, the window will
+// be closed automatically when the JavaScript object is garbage collected.
+let mainWindow: Electron.BrowserWindow;
+
+const createWindow = () => {
+  // Create the browser window.
+  mainWindow = new BrowserWindow({
+    height: 600,
+    width: 800,
+  });
+
+  // and load the index.html of the app.
+  mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
+
+  // Open the DevTools.
+  mainWindow.webContents.openDevTools();
+
+  // Emitted when the window is closed.
+  mainWindow.on('closed', () => {
+    // Dereference the window object, usually you would store windows
+    // in an array if your app supports multi windows, this is the time
+    // when you should delete the corresponding element.
+    mainWindow = null;
+  });
+};
+
+// 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.on('ready', createWindow);
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () => {
+  // On OS X it is common for applications and their menu bar
+  // to stay active until the user quits explicitly with Cmd + Q
+  if (process.platform !== 'darwin') {
+    app.quit();
+  }
+});
+
+app.on('activate', () => {
+  // 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 (mainWindow === null) {
+    createWindow();
+  }
+});
+
+// In this file you can include the rest of your app's specific main process
+// code. You can also put them in separate files and import them here.
diff --git a/packages/template/typescript-webpack/tmpl/renderer.ts b/packages/template/typescript-webpack/tmpl/renderer.ts
new file mode 100644
index 0000000000..4a3534745d
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/renderer.ts
@@ -0,0 +1,31 @@
+/**
+ * This file will automatically be loaded by webpack and run in the "renderer" context.
+ * To learn more about the differences between the "main" and the "renderer" context in
+ * Electron, visit:
+ *
+ * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes
+ *
+ * By default, Node.js integration in this file is disabled. When enabling Node.js integration
+ * in a renderer process, please be aware of potential security implications. You can read
+ * more about security risks here:
+ *
+ * https://electronjs.org/docs/tutorial/security
+ *
+ * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
+ * flag:
+ *
+ * ```
+ *  // Create the browser window.
+ *  mainWindow = new BrowserWindow({
+ *    width: 800,
+ *    height: 600,
+ *    webPreferences: {
+ *      nodeIntegration: true
+ *    }
+ *  });
+ * ```
+ */
+
+import './index.css';
+
+console.log('👋 This message is being logged by "renderer.js", included via webpack');
diff --git a/packages/template/typescript-webpack/tmpl/tsconfig.json b/packages/template/typescript-webpack/tmpl/tsconfig.json
new file mode 100644
index 0000000000..7b63cff3b6
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "allowJs": true,
+    "module": "commonjs",
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "noImplicitAny": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "outDir": "dist",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "paths": {
+      "*": ["node_modules/*"]
+    }
+  },
+  "include": [
+    "src/**/*"
+  ]
+}
diff --git a/packages/template/typescript-webpack/tmpl/tslint.json b/packages/template/typescript-webpack/tmpl/tslint.json
new file mode 100644
index 0000000000..f11b68a7f1
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/tslint.json
@@ -0,0 +1,14 @@
+{
+  "defaultSeverity": "error",
+  "extends": [
+    "tslint:latest"
+  ],
+  "jsRules": {},
+  "rules": {
+    "no-implicit-dependencies": [true, "dev"],
+    "no-var-requires": false,
+    "quotemark": [true, "single"],
+    "no-console": false
+  },
+  "rulesDirectory": []
+}
diff --git a/packages/template/typescript-webpack/tmpl/webpack.main.config.js b/packages/template/typescript-webpack/tmpl/webpack.main.config.js
new file mode 100644
index 0000000000..1d755595c7
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/webpack.main.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+  /**
+   * This is the main entry point for your application, it's the first file
+   * that runs in the main process.
+   */
+  entry: './src/index.ts',
+  // Put your normal webpack config below here
+  module: {
+    rules: require('./webpack.rules'),
+  },
+  resolve: {
+    extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json']
+  },
+};
\ No newline at end of file
diff --git a/packages/template/typescript-webpack/tmpl/webpack.plugins.js b/packages/template/typescript-webpack/tmpl/webpack.plugins.js
new file mode 100644
index 0000000000..4aa1aaad17
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/webpack.plugins.js
@@ -0,0 +1,7 @@
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
+
+module.exports = [
+  new ForkTsCheckerWebpackPlugin({
+    async: false
+  })
+];
diff --git a/packages/template/typescript-webpack/tmpl/webpack.renderer.config.js b/packages/template/typescript-webpack/tmpl/webpack.renderer.config.js
new file mode 100644
index 0000000000..fee66bdc1c
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/webpack.renderer.config.js
@@ -0,0 +1,17 @@
+const rules = require('./webpack.rules');
+const plugins = require('./webpack.plugins');
+
+rules.push({
+  test: /\.css$/,
+  use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
+});
+
+module.exports = {
+  module: {
+    rules,
+  },
+  plugins: plugins,
+  resolve: {
+    extensions: ['.js', '.ts', '.jsx', '.tsx', '.css']
+  },
+};
diff --git a/packages/template/typescript-webpack/tmpl/webpack.rules.js b/packages/template/typescript-webpack/tmpl/webpack.rules.js
new file mode 100644
index 0000000000..25dd17a28d
--- /dev/null
+++ b/packages/template/typescript-webpack/tmpl/webpack.rules.js
@@ -0,0 +1,27 @@
+module.exports = [
+  // Add support for native node modules
+  {
+    test: /\.node$/,
+    use: 'node-loader',
+  },
+  {
+    test: /\.(m?js|node)$/,
+    parser: { amd: false },
+    use: {
+      loader: '@marshallofsound/webpack-asset-relocator-loader',
+      options: {
+        outputAssetBase: 'native_modules',
+      },
+    },
+  },
+  {
+    test: /\.tsx?$/,
+    exclude: /(node_modules|.webpack)/,
+    loaders: [{
+      loader: 'ts-loader',
+      options: {
+        transpileOnly: true
+      }
+    }]
+  },
+];