UNPKG

5.49 kBJavaScriptView Raw
1const path = require('path')
2const webpack = require('webpack')
3const log = require('debug')('cypress:webpack')
4
5const createDeferred = require('./deferred')
6
7const bundles = {}
8
9// by default, we transform JavaScript (up to anything at stage-4) and JSX
10const defaultOptions = {
11 webpackOptions: {
12 module: {
13 rules: [
14 {
15 test: /\.jsx?$/,
16 exclude: [/node_modules/],
17 use: [{
18 loader: require.resolve('babel-loader'),
19 options: {
20 presets: [
21 'babel-preset-env',
22 'babel-preset-react',
23 ].map(require.resolve),
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(config, 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 (config) => {
53 const filePath = config.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 = config.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 // this function is called when bundling is finished, once at the start
97 // and, if watching, each time watching triggers a re-bundle
98 const handle = (err, stats) => {
99 if (err) {
100 err.filePath = filePath
101 // backup the original stack before it's
102 // potentially modified from bluebird
103 err.originalStack = err.stack
104 log(`errored bundling ${outputPath}`, err)
105 return latestBundle.reject(err)
106 }
107
108 // these stats are really only useful for debugging
109 const jsonStats = stats.toJson()
110 if (jsonStats.errors.length > 0) {
111 log(`soft errors for ${outputPath}`)
112 log(jsonStats.errors)
113 }
114 if (jsonStats.warnings.length > 0) {
115 log(`warnings for ${outputPath}`)
116 log(jsonStats.warnings)
117 }
118
119 log('finished bundling', outputPath)
120 // resolve with the outputPath so Cypress knows where to serve
121 // the file from
122 latestBundle.resolve(outputPath)
123 }
124
125 // this event is triggered when watching and a file is saved
126 compiler.plugin('compile', () => {
127 log('compile', filePath)
128 // we overwrite the latest bundle, so that a new call to this function
129 // returns a promise that resolves when the bundling is finished
130 latestBundle = createDeferred()
131 bundles[filePath] = latestBundle.promise.tap(() => {
132 log('- compile finished for', filePath)
133 // when the bundling is finished, we call `util.fileUpdated`
134 // to let Cypress know to re-run the spec
135 config.emit('rerun')
136 })
137 })
138
139 if (config.shouldWatch) {
140 log('watching')
141 }
142
143 const bundler = config.shouldWatch ?
144 compiler.watch(watchOptions, handle) :
145 compiler.run(handle)
146
147 // when the spec or project is closed, we need to clean up the cached
148 // bundle promise and stop the watcher via `bundler.close()`
149 config.on('close', () => {
150 log('close', filePath)
151 delete bundles[filePath]
152
153 if (config.shouldWatch) {
154 bundler.close()
155 }
156 })
157
158 // return the promise, which will resolve with the outputPath or reject
159 // with any error encountered
160 return bundles[filePath]
161 }
162}
163
164// provide a clone of the default options
165preprocessor.defaultOptions = JSON.parse(JSON.stringify(defaultOptions))
166
167module.exports = preprocessor