Skip to content

Commit dfde39c

Browse files
committed
feat: add support for bundling preload files, fixes #613
1 parent f3272be commit dfde39c

File tree

4 files changed

+230
-25
lines changed

4 files changed

+230
-25
lines changed

__tests__/commands.spec.js

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,53 @@ describe('electron:build', () => {
391391
'customDist'
392392
)
393393
})
394+
395+
test('Preload file is bundled if set', async () => {
396+
const mockRun = jest
397+
.fn()
398+
.mockImplementation((cb) => cb(undefined, { hasErrors: () => false }))
399+
webpack.mockReturnValue({run: mockRun})
400+
await runCommand('electron:build', {
401+
pluginOptions: {
402+
electronBuilder: {
403+
preload: 'preloadFile'
404+
}
405+
}
406+
})
407+
// Both main process and preload file should have been built
408+
expect(webpack).toBeCalledTimes(2)
409+
const preloadWebpackCall = webpack.mock.calls[1][0]
410+
expect(preloadWebpackCall.target).toBe('electron-preload')
411+
expect(preloadWebpackCall.entry).toEqual({preload: ['projectPath/preloadFile']})
412+
// Make sure preload bundle has been run
413+
expect(mockRun).toHaveBeenCalledTimes(2)
414+
webpack.mockClear()
415+
})
416+
417+
test('Multiple preload files can be bundled', async () => {
418+
const mockRun = jest
419+
.fn()
420+
.mockImplementation((cb) => cb(undefined, { hasErrors: () => false }))
421+
webpack.mockReturnValue({run: mockRun})
422+
await runCommand('electron:build', {
423+
pluginOptions: {
424+
electronBuilder: {
425+
preload: {firstPreload: 'preload1', secondPreload: 'preload2'}
426+
}
427+
}
428+
})
429+
// Both main process and preload file should have been built
430+
expect(webpack).toBeCalledTimes(2)
431+
const preloadWebpackCall = webpack.mock.calls[1][0]
432+
expect(preloadWebpackCall.target).toBe('electron-preload')
433+
expect(preloadWebpackCall.entry).toEqual({
434+
firstPreload: ['projectPath/preload1'],
435+
secondPreload: ['projectPath/preload2']
436+
})
437+
// Make sure preload bundle has been run
438+
expect(mockRun).toHaveBeenCalledTimes(2)
439+
webpack.mockClear()
440+
})
394441
})
395442

396443
describe('electron:serve', () => {
@@ -557,7 +604,7 @@ describe('electron:serve', () => {
557604
})
558605

559606
// Proper file is watched
560-
expect(chokidar.watch.mock.calls[0][0]).toBe('projectPath/customBackground')
607+
expect(chokidar.watch.mock.calls[0][0]).toEqual(['projectPath/customBackground'])
561608
// Child has not yet been killed or unwatched
562609
expect(mockExeca.send).not.toBeCalled()
563610
expect(mockExeca.kill).not.toBeCalled()
@@ -596,11 +643,13 @@ describe('electron:serve', () => {
596643
// So we can make sure it wasn't called
597644
jest.spyOn(process, 'exit')
598645
let watchCb = {}
599-
chokidar.watch.mockImplementation(file => {
646+
chokidar.watch.mockImplementation(files => {
600647
return {
601648
on: (type, cb) => {
602-
// Set callback to be called later
603-
watchCb[file] = cb
649+
files.forEach(file => {
650+
// Set callback to be called later
651+
watchCb[file] = cb
652+
})
604653
}
605654
}
606655
})
@@ -616,8 +665,7 @@ describe('electron:serve', () => {
616665
})
617666

618667
// Proper file is watched
619-
expect(chokidar.watch.mock.calls[0][0]).toBe('projectPath/customBackground')
620-
expect(chokidar.watch.mock.calls[1][0]).toBe('projectPath/listFile')
668+
expect(chokidar.watch.mock.calls[0][0]).toEqual(['projectPath/customBackground', 'projectPath/listFile'])
621669
// Child has not yet been killed or unwatched
622670
expect(mockExeca.send).not.toBeCalled()
623671
expect(mockExeca.kill).not.toBeCalled()
@@ -676,6 +724,20 @@ describe('electron:serve', () => {
676724
expect(execa).toHaveBeenCalledTimes(3)
677725
})
678726

727+
test('Preload file is watched for changes', async () => {
728+
await runCommand('electron:serve', {
729+
pluginOptions: {
730+
electronBuilder: {
731+
// Set preload file
732+
preload: 'preloadFile'
733+
}
734+
}
735+
})
736+
737+
// Proper file is watched
738+
expect(chokidar.watch.mock.calls[0][0]).toContain('projectPath/preloadFile')
739+
})
740+
679741
test('Junk output is stripped from electron child process', async () => {
680742
await runCommand('electron:serve')
681743

@@ -785,6 +847,53 @@ describe('electron:serve', () => {
785847
const args = execa.mock.calls[0][1]
786848
expect(args).toContain('--expected')
787849
})
850+
851+
test('Preload file is bundled if set', async () => {
852+
const mockRun = jest
853+
.fn()
854+
.mockImplementation((cb) => cb(undefined, { hasErrors: () => false }))
855+
webpack.mockReturnValue({run: mockRun})
856+
await runCommand('electron:build', {
857+
pluginOptions: {
858+
electronBuilder: {
859+
preload: 'preloadFile'
860+
}
861+
}
862+
})
863+
// Both main process and preload file should have been built
864+
expect(webpack).toBeCalledTimes(2)
865+
const preloadWebpackCall = webpack.mock.calls[1][0]
866+
expect(preloadWebpackCall.target).toBe('electron-preload')
867+
expect(preloadWebpackCall.entry).toEqual({preload: ['projectPath/preloadFile']})
868+
// Make sure preload bundle has been run
869+
expect(mockRun).toHaveBeenCalledTimes(2)
870+
webpack.mockClear()
871+
})
872+
873+
test('Multiple preload files can be bundled', async () => {
874+
const mockRun = jest
875+
.fn()
876+
.mockImplementation((cb) => cb(undefined, { hasErrors: () => false }))
877+
webpack.mockReturnValue({run: mockRun})
878+
await runCommand('electron:build', {
879+
pluginOptions: {
880+
electronBuilder: {
881+
preload: {firstPreload: 'preload1', secondPreload: 'preload2'}
882+
}
883+
}
884+
})
885+
// Both main process and preload file should have been built
886+
expect(webpack).toBeCalledTimes(2)
887+
const preloadWebpackCall = webpack.mock.calls[1][0]
888+
expect(preloadWebpackCall.target).toBe('electron-preload')
889+
expect(preloadWebpackCall.entry).toEqual({
890+
firstPreload: ['projectPath/preload1'],
891+
secondPreload: ['projectPath/preload2']
892+
})
893+
// Make sure preload bundle has been run
894+
expect(mockRun).toHaveBeenCalledTimes(2)
895+
webpack.mockClear()
896+
})
788897
})
789898

790899
describe('Custom webpack chain', () => {

docs/guide/guide.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@ console.log(fileContents)
7474
</script>
7575
```
7676

77+
## Preload Files
78+
79+
Preload files allow you to execute JS with [Node integration](/guide/configuration.html#node-integration) in the context of your Vue App (shared `window` variable). Create a preload file and update your `vue.config.js` as so:
80+
81+
```js
82+
module.exports = {
83+
pluginOptions: {
84+
electronBuilder: {
85+
preload: 'src/preload.js',
86+
// Or, for multiple preload files:
87+
preload: { preload: 'src/preload.js', otherPreload: 'src/preload2.js' }
88+
}
89+
}
90+
}
91+
```
92+
93+
Then, update the `new BrowserWindow` call in your main process file (`src/background.(js|ts)` by default) to include the preload option:
94+
95+
```diff
96+
win = new BrowserWindow({
97+
width: 800,
98+
height: 600,
99+
webPreferences: {
100+
// Use pluginOptions.nodeIntegration, leave this alone
101+
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#node-integration for more info
102+
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
103+
+ preload: path.join(__dirname, 'preload.js')
104+
}
105+
})
106+
```
107+
77108
## Folder Structure
78109

79110
```

index.js

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
} = require('@vue/cli-shared-utils')
1717
const formatStats = require('@vue/cli-service/lib/commands/build/formatStats')
1818
const { chainWebpack, getExternals } = require('./lib/webpackConfig')
19+
const webpackMerge = require('webpack-merge')
1920

2021
module.exports = (api, options) => {
2122
// If plugin options are provided in vue.config.js, those will be used. Otherwise it is empty object
@@ -162,7 +163,7 @@ module.exports = (api, options) => {
162163

163164
if (bundleMainProcess) {
164165
// Build the main process into the renderer process output dir
165-
const bundle = bundleMain({
166+
const { mainBundle, preloadBundle } = bundleMain({
166167
mode: 'build',
167168
api,
168169
args,
@@ -173,7 +174,7 @@ module.exports = (api, options) => {
173174
usesTypescript
174175
})
175176
logWithSpinner('Bundling main process...')
176-
bundle.run((err, stats) => {
177+
mainBundle.run((err, stats) => {
177178
stopSpinner(false)
178179
if (err) {
179180
return reject(err)
@@ -188,7 +189,28 @@ module.exports = (api, options) => {
188189
)
189190
log(formatStats(stats, targetDirShort, api))
190191

191-
buildApp()
192+
if (preloadBundle) {
193+
logWithSpinner('Bundling preload files...')
194+
preloadBundle.run((err, stats) => {
195+
stopSpinner(false)
196+
if (err) {
197+
return reject(err)
198+
}
199+
if (stats.hasErrors()) {
200+
// eslint-disable-next-line prefer-promise-reject-errors
201+
return reject(`Build failed with errors.`)
202+
}
203+
const targetDirShort = path.relative(
204+
api.service.context,
205+
`${outputDir}/bundled`
206+
)
207+
log(formatStats(stats, targetDirShort, api))
208+
209+
buildApp()
210+
})
211+
} else {
212+
buildApp()
213+
}
192214
})
193215
} else {
194216
info(
@@ -240,9 +262,11 @@ module.exports = (api, options) => {
240262
// Use custom config for webpack
241263
process.env.IS_ELECTRON = true
242264
const execa = require('execa')
265+
let preload = pluginOptions.preload || {}
243266
const mainProcessWatch = [
244267
mainProcessFile,
245-
...(pluginOptions.mainProcessWatch || [])
268+
...(pluginOptions.mainProcessWatch || []),
269+
...(typeof preload === 'string' ? [preload] : Object.values(preload))
246270
]
247271
const mainProcessArgs = pluginOptions.mainProcessArgs || []
248272

@@ -267,7 +291,7 @@ module.exports = (api, options) => {
267291
queuedBuilds++
268292
if (bundleMainProcess) {
269293
// Build the main process
270-
const bundle = bundleMain({
294+
const { mainBundle, preloadBundle } = bundleMain({
271295
mode: 'serve',
272296
api,
273297
args,
@@ -279,7 +303,7 @@ module.exports = (api, options) => {
279303
server
280304
})
281305
logWithSpinner('Bundling main process...')
282-
bundle.run((err, stats) => {
306+
mainBundle.run((err, stats) => {
283307
stopSpinner(false)
284308
if (err) {
285309
throw err
@@ -290,7 +314,24 @@ module.exports = (api, options) => {
290314
}
291315
const targetDirShort = path.relative(api.service.context, outputDir)
292316
log(formatStats(stats, targetDirShort, api))
293-
launchElectron()
317+
318+
if (preloadBundle) {
319+
preloadBundle.run((err, stats) => {
320+
stopSpinner(false)
321+
if (err) {
322+
throw err
323+
}
324+
if (stats.hasErrors()) {
325+
error(`Build failed with errors.`)
326+
process.exit(1)
327+
}
328+
const targetDirShort = path.relative(api.service.context, outputDir)
329+
log(formatStats(stats, targetDirShort, api))
330+
launchElectron()
331+
})
332+
} else {
333+
launchElectron()
334+
}
294335
})
295336
} else {
296337
info(
@@ -342,13 +383,11 @@ module.exports = (api, options) => {
342383
startElectron()
343384
// Restart on main process file change
344385
const chokidar = require('chokidar')
345-
mainProcessWatch.forEach(file => {
346-
chokidar.watch(api.resolve(file)).on('all', () => {
347-
// This function gets triggered on first launch
348-
if (firstBundleCompleted) {
349-
startElectron()
350-
}
351-
})
386+
chokidar.watch(mainProcessWatch.map(file => api.resolve(file))).on('all', () => {
387+
// This function gets triggered on first launch
388+
if (firstBundleCompleted) {
389+
startElectron()
390+
}
352391
})
353392

354393
// Attempt to kill gracefully on SIGINT and SIGTERM
@@ -529,7 +568,6 @@ function bundleMain({
529568
const config = new Config()
530569
config
531570
.mode(NODE_ENV)
532-
.target('electron-main')
533571
.node.set('__dirname', false)
534572
.set('__filename', false)
535573
// Set externals
@@ -579,9 +617,7 @@ function bundleMain({
579617
}
580618
])
581619
}
582-
config
583-
.entry(isBuild ? 'background' : 'index')
584-
.add(api.resolve(mainProcessFile))
620+
585621
const {
586622
transformer,
587623
formatter
@@ -606,7 +642,35 @@ function bundleMain({
606642
.options({ transpileOnly: !mainProcessTypeChecking })
607643
}
608644
mainProcessChain(config)
609-
return webpack(config.toConfig())
645+
646+
// Create mainConfig and preloadConfig and set unique configuration
647+
let mainConfig = new Config()
648+
let preloadConfig = new Config()
649+
650+
mainConfig.target('electron-main')
651+
mainConfig
652+
.entry(isBuild ? 'background' : 'index')
653+
.add(api.resolve(mainProcessFile))
654+
655+
preloadConfig.target('electron-preload')
656+
let preload = pluginOptions.preload
657+
if (preload) {
658+
// Add preload files if they are set in pluginOptions
659+
if (typeof preload === 'string') {
660+
preload = { preload }
661+
}
662+
Object.keys(preload).forEach((k) => {
663+
preloadConfig.entry(k).add(api.resolve(preload[k]))
664+
})
665+
}
666+
667+
mainConfig = webpackMerge(config.toConfig(), mainConfig.toConfig())
668+
preloadConfig = webpackMerge(config.toConfig(), preloadConfig.toConfig())
669+
670+
return {
671+
mainBundle: webpack(mainConfig),
672+
preloadBundle: preload ? webpack(preloadConfig) : undefined
673+
}
610674
}
611675

612676
module.exports.defaultModes = {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"unzip-crx": "^0.2.0",
4747
"webpack": "^4.18.0",
4848
"webpack-chain": "^6.0.0",
49+
"webpack-merge": "^4.2.2",
4950
"yargs": "^14.0.0"
5051
},
5152
"devDependencies": {

0 commit comments

Comments
 (0)