1 | ;
|
2 | var typescript_overrides_1 = require("./lib/typescript-overrides");
|
3 | var _ = require("lodash");
|
4 | var webpack = require("webpack");
|
5 | var deferred_1 = require("./deferred");
|
6 | var path = require('path');
|
7 | var debug = require('debug')('cypress:webpack');
|
8 | var debugStats = require('debug')('cypress:webpack:stats');
|
9 | // bundle promises from input spec filename to output bundled file paths
|
10 | var 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
|
13 | var 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 | };
|
35 | var 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 | };
|
43 | var 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 | };
|
53 | var 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 | };
|
61 | var 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
|
77 | var 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
|
245 | Object.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
|
256 | preprocessor.__reset = function () {
|
257 | bundles = {};
|
258 | };
|
259 | function cleanseError(err) {
|
260 | return err.replace(/\n\s*at.*/g, '').replace(/From previous event:\n?/g, '');
|
261 | }
|
262 | module.exports = preprocessor;
|