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 + } + }] + }, +];