1 | const path = require('path')
|
2 | const webpack = require('webpack')
|
3 | const log = require('debug')('cypress:webpack')
|
4 |
|
5 | const createDeferred = require('./deferred')
|
6 |
|
7 | const bundles = {}
|
8 |
|
9 | // by default, we transform JavaScript (up to anything at stage-4) and JSX
|
10 | const 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 | //
|
38 | const 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
|
165 | preprocessor.defaultOptions = JSON.parse(JSON.stringify(defaultOptions))
|
166 |
|
167 | module.exports = preprocessor
|