UNPKG

vue-cli-plugin-electron-builder-gz

Version:

A Vue Cli 3 plugin for Electron with no required configuration - fork with packages updates

614 lines (579 loc) 20.7 kB
const TerserPlugin = require('terser-webpack-plugin') const webpack = require('webpack') const Config = require('webpack-chain') const merge = require('lodash.merge') const fs = require('fs-extra') const path = require('path') const readline = require('readline') const { log, done, info, logWithSpinner, stopSpinner, warn, error } = require('@vue/cli-shared-utils') const formatStats = require('@vue/cli-service/lib/commands/build/formatStats') const { chainWebpack, getExternals } = require('./lib/webpackConfig') module.exports = (api, options) => { // If plugin options are provided in vue.config.js, those will be used. Otherwise it is empty object const pluginOptions = options.pluginOptions && options.pluginOptions.electronBuilder ? options.pluginOptions.electronBuilder : {} // If option is not set in pluginOptions, default is used const usesTypescript = pluginOptions.disableMainProcessTypescript ? false : api.hasPlugin('typescript') const mainProcessFile = pluginOptions.mainProcessFile || (usesTypescript ? 'src/background.ts' : 'src/background.js') const mainProcessChain = pluginOptions.chainWebpackMainProcess || (config => config) const bundleMainProcess = pluginOptions.bundleMainProcess == null ? true : pluginOptions.bundleMainProcess const removeArg = (arg, count, rawArgs) => { const index = rawArgs.indexOf(arg) if (index !== -1) rawArgs.splice(index, count) } // Apply custom webpack config api.chainWebpack(async config => { chainWebpack(api, pluginOptions, config) }) api.registerCommand( 'electron:build', { description: 'build app with electron-builder', usage: 'vue-cli-service build:electron [electron-builder options]', details: `All electron-builder command line options are supported.\n` + `See https://www.electron.build/cli for cli options\n` + `See https://nklayman.github.io/vue-cli-plugin-electron-builder/ for more details about this plugin.` }, (args, rawArgs) => new Promise(async (resolve, reject) => { // Use custom config for webpack process.env.IS_ELECTRON = true const builder = require('electron-builder') const yargs = require('yargs') // Import the yargs options from electron-builder const configureBuildCommand = require('electron-builder/out/builder') .configureBuildCommand // Prevent custom args from interfering with electron-builder removeArg('--mode', 2, rawArgs) removeArg('--dest', 2, rawArgs) removeArg('--legacy', 1, rawArgs) removeArg('--dashboard', 1, rawArgs) removeArg('--skipBundle', 1, rawArgs) removeArg('--report', 1, rawArgs) removeArg('--report-json', 1, rawArgs) // Parse the raw arguments using electron-builder yargs config const builderArgs = yargs .command(['build', '*'], 'Build', configureBuildCommand) .parse(rawArgs) // Base config used in electron-builder build const outputDir = args.dest || pluginOptions.outputDir || 'dist_electron' const defaultBuildConfig = { directories: { output: outputDir, app: `${outputDir}/bundled` }, files: ['**'], extends: null } // User-defined electron-builder config, overwrites/adds to default config const userBuildConfig = pluginOptions.builderOptions || {} if (args.skipBundle) { console.log('Not bundling app as --skipBundle was passed') // Build with electron-builder buildApp() } else { const bundleOutputDir = path.join(outputDir, 'bundled') // Arguments to be passed to renderer build const vueArgs = { _: [], // For the cli-ui webpack dashboard dashboard: args.dashboard, // Make sure files are outputted to proper directory dest: bundleOutputDir, // Enable modern mode unless --legacy is passed modern: !args.legacy, // --report and --report-json args report: args.report, 'report-json': args['report-json'] } // With @vue/cli-service v3.4.1+, we can bypass legacy build process.env.VUE_CLI_MODERN_BUILD = !args.legacy // If the legacy builded is skipped the output dir won't be cleaned fs.removeSync(bundleOutputDir) fs.ensureDirSync(bundleOutputDir) // Mock data from legacy build const pages = options.pages || { index: '' } Object.keys(pages).forEach(page => { if (pages[page].filename) { // If page is configured as an object, use the filename (without .html) page = pages[page].filename.replace(/\.html$/, '') } fs.writeFileSync( path.join(bundleOutputDir, `legacy-assets-${page}.html.json`), '[]' ) }) // Set the base url so that the app protocol is used options.baseUrl = pluginOptions.customFileProtocol || 'app://./' // Set publicPath as well (replaced baseUrl since @vue/cli 3.3.0) options.publicPath = pluginOptions.customFileProtocol || 'app://./' info('Bundling render process:') // Build the render process with the custom args try { await api.service.run('build', vueArgs) } catch (e) { error( 'Vue CLI build failed. Please resolve any issues with your build and try again.' ) process.exit(1) } // Copy package.json to output dir const pkg = JSON.parse( fs.readFileSync(api.resolve('./package.json'), 'utf8') ) const externals = getExternals(api, pluginOptions) // https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/223 // Strip non-externals from dependencies so they won't be copied into app.asar Object.keys(pkg.dependencies).forEach(dependency => { if (!Object.keys(externals).includes(dependency)) { delete pkg.dependencies[dependency] } }) fs.writeFileSync( `${outputDir}/bundled/package.json`, JSON.stringify(pkg, 2) ) // Prevent electron-builder from installing app deps fs.ensureDirSync(`${outputDir}/bundled/node_modules`) // Copy fonts to css/fonts. Fixes some issues with static font imports if (fs.existsSync(api.resolve(outputDir + '/bundled/fonts'))) { fs.ensureDirSync(api.resolve(outputDir + '/bundled/css/fonts')) fs.copySync( api.resolve(outputDir + '/bundled/fonts'), api.resolve(outputDir + '/bundled/css/fonts') ) } if (bundleMainProcess) { // Build the main process into the renderer process output dir const bundle = bundleMain({ mode: 'build', api, args, pluginOptions, outputDir, mainProcessFile, mainProcessChain, usesTypescript }) logWithSpinner('Bundling main process...') bundle.run((err, stats) => { stopSpinner(false) if (err) { return reject(err) } if (stats.hasErrors()) { // eslint-disable-next-line prefer-promise-reject-errors return reject(`Build failed with errors.`) } const targetDirShort = path.relative( api.service.context, `${outputDir}/bundled` ) log(formatStats(stats, targetDirShort, api)) buildApp() }) } else { info( 'Not bundling main process as bundleMainProcess was set to false in plugin options' ) // Copy main process file instead of bundling it fs.copySync( api.resolve(mainProcessFile), api.resolve(`${outputDir}/bundled/background.js`) ) buildApp() } } function buildApp () { info('Building app with electron-builder:') // Build the app using electron builder builder .build( merge({ config: merge( defaultBuildConfig, // User-defined config overwrites defaults userBuildConfig ), // Args parsed with yargs ...builderArgs }) ) .then(() => { // handle result done('Build complete!') resolve() }) .catch(err => { // handle error return reject(err) }) } }) ) api.registerCommand( 'electron:serve', { description: 'serve app and launch electron', usage: 'vue-cli-service serve:electron', details: `See https://nklayman.github.io/vue-cli-plugin-electron-builder/ for more details about this plugin.` }, async (args, rawArgs) => { // Use custom config for webpack process.env.IS_ELECTRON = true const execa = require('execa') const mainProcessWatch = [ mainProcessFile, ...(pluginOptions.mainProcessWatch || []) ] const mainProcessArgs = pluginOptions.mainProcessArgs || [] // Don't pass command args to electron removeArg('--dashboard', 1, rawArgs) removeArg('--debug', 1, rawArgs) removeArg('--headless', 1, rawArgs) // Run the serve command const server = await api.service.run('serve', { _: [], // Use dashboard if called from ui dashboard: args.dashboard }) const outputDir = pluginOptions.outputDir || 'dist_electron' // Copy package.json so electron can detect app's name fs.copySync(api.resolve('./package.json'), `${outputDir}/package.json`) // Function to bundle main process and start Electron const startElectron = () => { queuedBuilds++ if (bundleMainProcess) { // Build the main process const bundle = bundleMain({ mode: 'serve', api, args, pluginOptions, outputDir, mainProcessFile, mainProcessChain, usesTypescript, server }) logWithSpinner('Bundling main process...') bundle.run((err, stats) => { stopSpinner(false) if (err) { throw err } if (stats.hasErrors()) { error(`Build failed with errors.`) process.exit(1) } const targetDirShort = path.relative(api.service.context, outputDir) log(formatStats(stats, targetDirShort, api)) launchElectron() }) } else { info( 'Not bundling main process as bundleMainProcess was set to false in plugin options' ) // Copy main process file instead of bundling it fs.copySync( api.resolve(mainProcessFile), api.resolve(`${outputDir}/index.js`) ) launchElectron() } } // Prevent multiple restarts at the same time let queuedBuilds = 0 // Electron process let child let firstBundleCompleted = false // Function to kill Electron process const killElectron = () => { if (!child) { return } const currentChild = child // Attempt to kill gracefully if (process.platform === 'win32') { currentChild.send('graceful-exit') } else { currentChild.kill('SIGTERM') } // Kill unconditionally after 2 seconds if unsuccessful setTimeout(() => { if (!currentChild.killed) { warn(`Force killing Electron (process #${currentChild.pid})`) currentChild.kill('SIGKILL') } }, 2000) } // Initial start of Electron startElectron() // Restart on main process file change const chokidar = require('chokidar') mainProcessWatch.forEach(file => { chokidar.watch(api.resolve(file)).on('all', () => { // This function gets triggered on first launch if (firstBundleCompleted) { startElectron() } }) }) // Attempt to kill gracefully on SIGINT and SIGTERM const signalHandler = () => { if (!child) { process.exit(0) } killElectron() } if (!process.env.IS_TEST) process.on('SIGINT', signalHandler) if (!process.env.IS_TEST) process.on('SIGTERM', signalHandler) // Handle Ctrl+C on Windows if (process.platform === 'win32' && !process.env.IS_TEST) { readline .createInterface({ input: process.stdin, output: process.stdout }) .on('SIGINT', () => { process.emit('SIGINT') }) } function launchElectron () { firstBundleCompleted = true // Don't exit process when electron is killed if (child) { child.removeListener('exit', onChildExit) } // Kill existing instances killElectron() // Don't launch if a new background file is being bundled queuedBuilds-- if (queuedBuilds > 0) return if (args.debug) { // Do not launch electron and provide instructions on launching through debugger info( 'Not launching electron as debug argument was passed. You must launch electron through your debugger.' ) info( `If you are using Spectron, make sure to set the IS_TEST env variable to true.` ) info( 'Learn more about debugging the main process at https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/testingAndDebugging.html#debugging.' ) } else if (args.headless) { // Log information for spectron console.log(`$outputDir=${outputDir}`) console.log(`$WEBPACK_DEV_SERVER_URL=${server.url}`) } else { // Launch electron with execa if (mainProcessArgs.length > 0) { info( 'Launching Electron with arguments: "' + mainProcessArgs.join(' ') + ' ' + rawArgs.join(' ') + '" ...' ) } else { info('Launching Electron...') } let stdioConfig = [null, null, null] // Use an IPC on Windows for graceful exit if (process.platform === 'win32') stdioConfig.push('ipc') child = execa( require('electron'), [ // Have it load the main process file built with webpack outputDir, // Append other arguments specified in plugin options ...mainProcessArgs, // Append args passed to command ...rawArgs ], { cwd: api.resolve('.'), env: { ...process.env, // Disable electron security warnings ELECTRON_DISABLE_SECURITY_WARNINGS: true }, stdio: stdioConfig } ) if (pluginOptions.removeElectronJunk === false) { // Pipe output to console child.stdout.pipe(process.stdout) child.stderr.pipe(process.stderr) } else { // Remove junk terminal output (#60) child.stdout .pipe(require('./lib/removeJunk.js')()) .pipe(process.stdout) child.stderr .pipe(require('./lib/removeJunk.js')()) .pipe(process.stderr) } child.on('exit', onChildExit) } } function onChildExit () { process.exit(0) } } ) api.registerCommand( 'build:electron', { description: '[deprecated, use electron:build instead] build app with electron-builder', usage: 'vue-cli-service build:electron [electron-builder options]', details: `All electron-builder command line options are supported.\n` + `See https://www.electron.build/cli for cli options\n` + `See https://nklayman.github.io/vue-cli-plugin-electron-builder/ for more details about this plugin.` }, (args, rawArgs) => { warn('This command is deprecated. Please use electron:build instead.') return api.service.run( 'electron:build', { ...args, _: ['First arg is removed', ...args._] }, ['First arg is removed', ...rawArgs] ) } ) api.registerCommand( 'serve:electron', { description: '[deprecated, use electron:serve instead] serve app and launch electron', usage: 'vue-cli-service serve:electron', details: `See https://nklayman.github.io/vue-cli-plugin-electron-builder/ for more details about this plugin.` }, (args, rawArgs) => { warn('This command is deprecated. Please use electron:serve instead.') return api.service.run( 'electron:serve', { ...args, _: ['First arg is removed', ...args._] }, ['First arg is removed', ...rawArgs] ) } ) } function bundleMain ({ mode, api, args, pluginOptions, outputDir, mainProcessFile, mainProcessChain, usesTypescript, server }) { const mainProcessTypeChecking = pluginOptions.mainProcessTypeChecking || false const isBuild = mode === 'build' const NODE_ENV = process.env.NODE_ENV const config = new Config() config .mode(NODE_ENV) .target('electron-main') .node.set('__dirname', false) .set('__filename', false) // Set externals config.externals(getExternals(api, pluginOptions)) config.output .path(api.resolve(outputDir + (isBuild ? '/bundled' : ''))) // Electron will not detect background.js on dev server, only index.js .filename('[name].js') const envVars = {} if (isBuild) { // Set __static to __dirname (files in public get copied here) config .plugin('define') .use(webpack.DefinePlugin, [{ __static: '__dirname' }]) } else { // Set __static to public folder config.plugin('define').use(webpack.DefinePlugin, [ { __static: JSON.stringify(api.resolve('./public')) } ]) // Dev server url envVars['WEBPACK_DEV_SERVER_URL'] = server.url // Path to node_modules (for externals in development) envVars['NODE_MODULES_PATH'] = api.resolve('./node_modules') } // Add all env vars prefixed with VUE_APP_ Object.keys(process.env).forEach(k => { if (/^VUE_APP_/.test(k)) { envVars[k] = process.env[k] } }) config.plugin('env').use(webpack.EnvironmentPlugin, [envVars]) if (args.debug) { // Enable source maps for debugging config.devtool('source-map') } else if (NODE_ENV === 'production') { // Minify for better performance config.plugin('uglify').use(TerserPlugin, [ { parallel: true } ]) } config .entry(isBuild ? 'background' : 'index') .add(api.resolve(mainProcessFile)) const { transformer, formatter } = require('@vue/cli-service/lib/util/resolveLoaderError') config .plugin('friendly-errors') .use(require('friendly-errors-webpack-plugin'), [ { additionalTransformers: [transformer], additionalFormatters: [formatter] } ]) config.resolve.alias.set('@', api.resolve('src')) if (usesTypescript) { config.resolve.extensions.merge(['.js', '.ts']) config.module .rule('ts') .test(/\.ts$/) .use('ts-loader') .loader('ts-loader') .options({ transpileOnly: !mainProcessTypeChecking }) } mainProcessChain(config) return webpack(config.toConfig()) } module.exports.defaultModes = { 'build:electron': 'production', 'serve:electron': 'development', 'electron:build': 'production', 'electron:serve': 'development' } module.exports.testWithSpectron = require('./lib/testWithSpectron')