UNPKG

5.58 kBJavaScriptView Raw
1const cloneDeep = require('lodash.clonedeep')
2const path = require('path')
3const webpack = require('webpack')
4const log = require('debug')('cypress:webpack')
5
6const createDeferred = require('./deferred')
7
8const bundles = {}
9
10// by default, we transform JavaScript (up to anything at stage-4) and JSX
11const defaultOptions = {
12 webpackOptions: {
13 module: {
14 rules: [
15 {
16 test: /\.jsx?$/,
17 exclude: [/node_modules/],
18 use: [
19 {
20 loader: require.resolve('babel-loader'),
21 options: {
22 presets: ['babel-preset-env', 'babel-preset-react'].map(require.resolve),
23 },
24 },
25 ],
26 },
27 ],
28 },
29 },
30 watchOptions: {},
31}
32
33// export a function that returns another function, making it easy for users
34// to configure like so:
35//
36// on('file:preprocessor', webpack(options))
37//
38const preprocessor = (options = {}) => {
39 log('user options:', options)
40
41 // we return function that accepts the arguments provided by
42 // the event 'file:preprocessor'
43 //
44 // this function will get called for the support file when a project is loaded
45 // (if the support file is not disabled)
46 // it will also get called for a spec file when that spec is requested by
47 // the Cypress runner
48 //
49 // when running in the GUI, it will likely get called multiple times
50 // with the same filePath, as the user could re-run the tests, causing
51 // the supported file and spec file to be requested again
52 return (file) => {
53 const filePath = file.filePath
54 log('get', filePath)
55
56 // since this function can get called multiple times with the same
57 // filePath, we return the cached bundle promise if we already have one
58 // since we don't want or need to re-initiate webpack for it
59 if (bundles[filePath]) {
60 log(`already have bundle for ${filePath}`)
61 return bundles[filePath]
62 }
63
64 // user can override the default options
65 let webpackOptions = Object.assign({}, defaultOptions.webpackOptions, options.webpackOptions)
66 let watchOptions = Object.assign({}, defaultOptions.watchOptions, options.watchOptions)
67
68 // we're provided a default output path that lives alongside Cypress's
69 // app data files so we don't have to worry about where to put the bundled
70 // file on disk
71 const outputPath = file.outputPath
72
73 // we need to set entry and output
74 webpackOptions = Object.assign(webpackOptions, {
75 entry: filePath,
76 output: {
77 path: path.dirname(outputPath),
78 filename: path.basename(outputPath),
79 },
80 })
81
82 log(`input: ${filePath}`)
83 log(`output: ${outputPath}`)
84
85 const compiler = webpack(webpackOptions)
86
87 // we keep a reference to the latest bundle in this scope
88 // it's a deferred object that will be resolved or rejected in
89 // the `handle` function below and its promise is what is ultimately
90 // returned from this function
91 let latestBundle = createDeferred()
92 // cache the bundle promise, so it can be returned if this function
93 // is invoked again with the same filePath
94 bundles[filePath] = latestBundle.promise
95
96 const rejectWithErr = (err) => {
97 err.filePath = filePath
98 // backup the original stack before it's potentially modified by bluebird
99 err.originalStack = err.stack
100 log(`errored bundling ${outputPath}`, err)
101 latestBundle.reject(err)
102 }
103
104 // this function is called when bundling is finished, once at the start
105 // and, if watching, each time watching triggers a re-bundle
106 const handle = (err, stats) => {
107 if (err) {
108 return rejectWithErr(err)
109 }
110
111 const jsonStats = stats.toJson()
112
113 if (stats.hasErrors()) {
114 err = new Error('Webpack Compilation Error')
115 err.stack = jsonStats.errors.join('\n\n')
116 return rejectWithErr(err)
117 }
118
119 // these stats are really only useful for debugging
120 if (jsonStats.warnings.length > 0) {
121 log(`warnings for ${outputPath}`)
122 log(jsonStats.warnings)
123 }
124
125 log('finished bundling', outputPath)
126 // resolve with the outputPath so Cypress knows where to serve
127 // the file from
128 latestBundle.resolve(outputPath)
129 }
130
131 // this event is triggered when watching and a file is saved
132 compiler.plugin('compile', () => {
133 log('compile', filePath)
134 // we overwrite the latest bundle, so that a new call to this function
135 // returns a promise that resolves when the bundling is finished
136 latestBundle = createDeferred()
137 bundles[filePath] = latestBundle.promise.tap(() => {
138 log('- compile finished for', filePath)
139 // when the bundling is finished, we call `util.fileUpdated`
140 // to let Cypress know to re-run the spec
141 file.emit('rerun')
142 })
143 })
144
145 if (file.shouldWatch) {
146 log('watching')
147 }
148
149 const bundler = file.shouldWatch
150 ? compiler.watch(watchOptions, handle)
151 : compiler.run(handle)
152
153 // when the spec or project is closed, we need to clean up the cached
154 // bundle promise and stop the watcher via `bundler.close()`
155 file.on('close', () => {
156 log('close', filePath)
157 delete bundles[filePath]
158
159 if (file.shouldWatch) {
160 bundler.close()
161 }
162 })
163
164 // return the promise, which will resolve with the outputPath or reject
165 // with any error encountered
166 return bundles[filePath]
167 }
168}
169
170// provide a clone of the default options
171preprocessor.defaultOptions = cloneDeep(defaultOptions)
172
173module.exports = preprocessor