UNPKG

11 kBJavaScriptView Raw
1"use strict";
2var typescript_overrides_1 = require("./lib/typescript-overrides");
3var _ = require("lodash");
4var webpack = require("webpack");
5var deferred_1 = require("./deferred");
6var path = require('path');
7var debug = require('debug')('cypress:webpack');
8var debugStats = require('debug')('cypress:webpack:stats');
9// bundle promises from input spec filename to output bundled file paths
10var bundles = {};
11// we don't automatically load the rules, so that the babel dependencies are
12// not required if a user passes in their own configuration
13var getDefaultWebpackOptions = function () {
14 debug('load default options');
15 return {
16 mode: 'development',
17 module: {
18 rules: [
19 {
20 test: /\.jsx?$/,
21 exclude: [/node_modules/],
22 use: [
23 {
24 loader: 'babel-loader',
25 options: {
26 presets: ['@babel/preset-env'],
27 },
28 },
29 ],
30 },
31 ],
32 },
33 };
34};
35var replaceErrMessage = function (err, partToReplace, replaceWith) {
36 if (replaceWith === void 0) { replaceWith = ''; }
37 err.message = _.trim(err.message.replace(partToReplace, replaceWith));
38 if (err.stack) {
39 err.stack = _.trim(err.stack.replace(partToReplace, replaceWith));
40 }
41 return err;
42};
43var cleanModuleNotFoundError = function (err) {
44 var message = err.message;
45 if (!message.includes('Module not found'))
46 return err;
47 var startIndex = message.lastIndexOf('resolve ');
48 var endIndex = message.lastIndexOf("doesn't exist") + "doesn't exist".length;
49 var partToReplace = message.substring(startIndex, endIndex);
50 var newMessagePart = "Looked for and couldn't find the file at the following paths:";
51 return replaceErrMessage(err, partToReplace, newMessagePart);
52};
53var cleanMultiNonsense = function (err) {
54 var message = err.message;
55 var startIndex = message.indexOf('@ multi');
56 if (startIndex < 0)
57 return err;
58 var partToReplace = message.substring(startIndex);
59 return replaceErrMessage(err, partToReplace);
60};
61var quietErrorMessage = function (err) {
62 if (!err || !err.message)
63 return err;
64 err = cleanModuleNotFoundError(err);
65 err = cleanMultiNonsense(err);
66 return err;
67};
68/**
69 * Webpack preprocessor configuration function. Takes configuration object
70 * and returns file preprocessor.
71 * @example
72 ```
73 on('file:preprocessor', webpackPreprocessor(options))
74 ```
75 */
76// @ts-ignore
77var preprocessor = function (options) {
78 if (options === void 0) { options = {}; }
79 debug('user options: %o', options);
80 // we return function that accepts the arguments provided by
81 // the event 'file:preprocessor'
82 //
83 // this function will get called for the support file when a project is loaded
84 // (if the support file is not disabled)
85 // it will also get called for a spec file when that spec is requested by
86 // the Cypress runner
87 //
88 // when running in the GUI, it will likely get called multiple times
89 // with the same filePath, as the user could re-run the tests, causing
90 // the supported file and spec file to be requested again
91 return function (file) {
92 var filePath = file.filePath;
93 debug('get', filePath);
94 // since this function can get called multiple times with the same
95 // filePath, we return the cached bundle promise if we already have one
96 // since we don't want or need to re-initiate webpack for it
97 if (bundles[filePath]) {
98 debug("already have bundle for " + filePath);
99 return bundles[filePath];
100 }
101 var defaultWebpackOptions = getDefaultWebpackOptions();
102 // we're provided a default output path that lives alongside Cypress's
103 // app data files so we don't have to worry about where to put the bundled
104 // file on disk
105 var outputPath = path.extname(file.outputPath) === '.js'
106 ? file.outputPath
107 : file.outputPath + ".js";
108 var entry = [filePath].concat(options.additionalEntries || []);
109 var watchOptions = options.watchOptions || {};
110 // user can override the default options
111 var webpackOptions = _
112 .chain(options.webpackOptions)
113 .defaultTo(defaultWebpackOptions)
114 .defaults({
115 mode: defaultWebpackOptions.mode,
116 })
117 .assign({
118 // we need to set entry and output
119 entry: entry,
120 output: {
121 path: path.dirname(outputPath),
122 filename: path.basename(outputPath),
123 },
124 })
125 .tap(function (opts) {
126 if (opts.devtool === false) {
127 // disable any overrides if
128 // we've explictly turned off sourcemaps
129 typescript_overrides_1.overrideSourceMaps(false);
130 return;
131 }
132 debug('setting devtool to inline-source-map');
133 opts.devtool = 'inline-source-map';
134 // override typescript to always generate
135 // proper source maps
136 typescript_overrides_1.overrideSourceMaps(true);
137 })
138 .value();
139 debug('webpackOptions: %o', webpackOptions);
140 debug('watchOptions: %o', watchOptions);
141 debug("input: " + filePath);
142 debug("output: " + outputPath);
143 var compiler = webpack(webpackOptions);
144 // we keep a reference to the latest bundle in this scope
145 // it's a deferred object that will be resolved or rejected in
146 // the `handle` function below and its promise is what is ultimately
147 // returned from this function
148 var latestBundle = deferred_1.createDeferred();
149 // cache the bundle promise, so it can be returned if this function
150 // is invoked again with the same filePath
151 bundles[filePath] = latestBundle.promise;
152 var rejectWithErr = function (err) {
153 err = quietErrorMessage(err);
154 // @ts-ignore
155 err.filePath = filePath;
156 debug("errored bundling " + outputPath, err.message);
157 latestBundle.reject(err);
158 };
159 // this function is called when bundling is finished, once at the start
160 // and, if watching, each time watching triggers a re-bundle
161 var handle = function (err, stats) {
162 if (err) {
163 debug('handle - had error', err.message);
164 return rejectWithErr(err);
165 }
166 var jsonStats = stats.toJson();
167 if (stats.hasErrors()) {
168 err = new Error('Webpack Compilation Error');
169 var errorsToAppend = jsonStats.errors
170 // remove stack trace lines since they're useless for debugging
171 .map(cleanseError)
172 // multiple errors separated by newline
173 .join('\n\n');
174 err.message += "\n" + errorsToAppend;
175 debug('stats had error(s)');
176 return rejectWithErr(err);
177 }
178 // these stats are really only useful for debugging
179 if (jsonStats.warnings.length > 0) {
180 debug("warnings for " + outputPath);
181 debug(jsonStats.warnings);
182 }
183 debug('finished bundling', outputPath);
184 if (debugStats.enabled) {
185 /* eslint-disable-next-line no-console */
186 console.error(stats.toString({ colors: true }));
187 }
188 // resolve with the outputPath so Cypress knows where to serve
189 // the file from
190 latestBundle.resolve(outputPath);
191 };
192 // this event is triggered when watching and a file is saved
193 var plugin = { name: 'CypressWebpackPreprocessor' };
194 var onCompile = function () {
195 debug('compile', filePath);
196 // we overwrite the latest bundle, so that a new call to this function
197 // returns a promise that resolves when the bundling is finished
198 latestBundle = deferred_1.createDeferred();
199 bundles[filePath] = latestBundle.promise;
200 bundles[filePath].finally(function () {
201 debug('- compile finished for', filePath);
202 // when the bundling is finished, emit 'rerun' to let Cypress
203 // know to rerun the spec
204 file.emit('rerun');
205 })
206 // we suppress unhandled rejections so they don't bubble up to the
207 // unhandledRejection handler and crash the process. Cypress will
208 // eventually take care of the rejection when the file is requested.
209 // note that this does not work if attached to latestBundle.promise
210 // for some reason. it only works when attached after .tap ¯\_(ツ)_/¯
211 .suppressUnhandledRejections();
212 };
213 // when we should watch, we hook into the 'compile' hook so we know when
214 // to rerun the tests
215 if (file.shouldWatch) {
216 debug('watching');
217 if (compiler.hooks) {
218 // TODO compile.tap takes "string | Tap"
219 // so seems we just need to pass plugin.name
220 // @ts-ignore
221 compiler.hooks.compile.tap(plugin, onCompile);
222 }
223 else {
224 compiler.plugin('compile', onCompile);
225 }
226 }
227 var bundler = file.shouldWatch ? compiler.watch(watchOptions, handle) : compiler.run(handle);
228 // when the spec or project is closed, we need to clean up the cached
229 // bundle promise and stop the watcher via `bundler.close()`
230 file.on('close', function (cb) {
231 if (cb === void 0) { cb = function () { }; }
232 debug('close', filePath);
233 delete bundles[filePath];
234 if (file.shouldWatch) {
235 // in this case the bundler is webpack.Compiler.Watching
236 bundler.close(cb);
237 }
238 });
239 // return the promise, which will resolve with the outputPath or reject
240 // with any error encountered
241 return bundles[filePath];
242 };
243};
244// provide a clone of the default options
245Object.defineProperty(preprocessor, 'defaultOptions', {
246 get: function () {
247 debug('get default options');
248 return {
249 webpackOptions: getDefaultWebpackOptions(),
250 watchOptions: {},
251 };
252 },
253});
254// for testing purposes, but do not add this to the typescript interface
255// @ts-ignore
256preprocessor.__reset = function () {
257 bundles = {};
258};
259function cleanseError(err) {
260 return err.replace(/\n\s*at.*/g, '').replace(/From previous event:\n?/g, '');
261}
262module.exports = preprocessor;