This repository has been archived by the owner on Jan 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
/
index.js
309 lines (254 loc) · 9.58 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
'use strict'
const path = require('path')
const Promise = require('bluebird')
const fs = require('fs-extra')
const cloneDeep = require('lodash.clonedeep')
const browserify = require('browserify')
const watchify = require('watchify')
const debug = require('debug')('cypress:browserify')
const typescriptExtensionRegex = /\.tsx?$/
const errorTypes = {
TYPESCRIPT_AND_TSIFY: 'TYPESCRIPT_AND_TSIFY',
TYPESCRIPT_NONEXISTENT: 'TYPESCRIPT_NONEXISTENT',
TYPESCRIPT_NOT_CONFIGURED: 'TYPESCRIPT_NOT_CONFIGURED',
TYPESCRIPT_NOT_STRING: 'TYPESCRIPT_NOT_STRING',
}
const bundles = {}
// by default, we transform JavaScript (including some proposal features),
// JSX, & CoffeeScript
const defaultOptions = {
browserifyOptions: {
extensions: ['.js', '.jsx', '.coffee'],
transform: [
[
require.resolve('coffeeify'),
{},
],
[
require.resolve('babelify'),
{
ast: false,
babelrc: false,
plugins: [
...[
'babel-plugin-add-module-exports',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
].map(require.resolve),
[require.resolve('@babel/plugin-transform-runtime'), {
absoluteRuntime: path.dirname(require.resolve('@babel/runtime/package')),
}],
],
presets: [
'@babel/preset-env',
'@babel/preset-react',
].map(require.resolve),
},
],
],
plugin: [],
},
watchifyOptions: {
// ignore watching the following or the user's system can get bogged down
// by watchers
ignoreWatch: [
'**/.git/**',
'**/.nyc_output/**',
'**/.sass-cache/**',
'**/bower_components/**',
'**/coverage/**',
'**/node_modules/**',
],
},
}
const throwError = ({ message, type }) => {
const prefix = 'Error running @cypress/browserify-preprocessor:\n\n'
const err = new Error(`${prefix}${message}`)
if (type) err.type = type
throw err
}
const getBrowserifyOptions = async (entry, userBrowserifyOptions = {}, typescriptPath = null) => {
let browserifyOptions = cloneDeep(defaultOptions.browserifyOptions)
// allow user to override default options
browserifyOptions = Object.assign(browserifyOptions, userBrowserifyOptions, {
// these must always be new objects or 'update' events will not fire
cache: {},
packageCache: {},
})
// unless user has explicitly turned off source map support, always enable it
// so we can use it to point user to the source code
if (userBrowserifyOptions.debug !== false) {
browserifyOptions.debug = true
}
// we need to override and control entries
Object.assign(browserifyOptions, {
entries: [entry],
})
if (typescriptPath) {
if (typeof typescriptPath !== 'string') {
throwError({
type: errorTypes.TYPESCRIPT_NOT_STRING,
message: `The 'typescript' option must be a string. You passed: ${typescriptPath}`,
})
}
const pathExists = await fs.pathExists(typescriptPath)
if (!pathExists) {
throwError({
type: errorTypes.TYPESCRIPT_NONEXISTENT,
message: `The 'typescript' option must be a valid path to your TypeScript installation. We could not find anything at the following path: ${typescriptPath}`,
})
}
const transform = browserifyOptions.transform
const hasTsifyTransform = transform.some((stage) => Array.isArray(stage) && stage[0].includes('tsify'))
const hastsifyPlugin = browserifyOptions.plugin.includes('tsify')
if (hasTsifyTransform || hastsifyPlugin) {
const type = hasTsifyTransform ? 'transform' : 'plugin'
throwError({
type: errorTypes.TYPESCRIPT_AND_TSIFY,
message: `It looks like you passed the 'typescript' option and also specified a browserify ${type} for TypeScript. This may cause conflicts.
Please do one of the following:
1) Pass in the 'typescript' option and omit the browserify ${type} (Recommmended)
2) Omit the 'typescript' option and continue to use your own browserify ${type}`,
})
}
browserifyOptions.extensions.push('.ts', '.tsx')
// remove babelify setting
browserifyOptions.transform = transform.filter((stage) => !Array.isArray(stage) || !stage[0].includes('babelify'))
// add typescript compiler
browserifyOptions.transform.push([
path.join(__dirname, './lib/simple_tsify'), {
typescript: require(typescriptPath),
},
])
}
debug('browserifyOptions: %o', browserifyOptions)
return browserifyOptions
}
// export a function that returns another function, making it easy for users
// to configure like so:
//
// on('file:preprocessor', browserify(options))
//
const preprocessor = (options = {}) => {
debug('received user options: %o', options)
// we return function that accepts the arguments provided by
// the event 'file:preprocessor'
//
// this function will get called for the support file when a project is loaded
// (if the support file is not disabled)
// it will also get called for a spec file when that spec is requested by
// the Cypress runner
//
// when running in the GUI, it will likely get called multiple times
// with the same filePath, as the user could re-run the tests, causing
// the supported file and spec file to be requested again
return async (file) => {
const filePath = file.filePath
debug('get:', filePath)
// since this function can get called multiple times with the same
// filePath, we return the cached bundle promise if we already have one
// since we don't want or need to re-initiate browserify/watchify for it
if (bundles[filePath]) {
debug('already have bundle for:', filePath)
return bundles[filePath]
}
// we're provided a default output path that lives alongside Cypress's
// app data files so we don't have to worry about where to put the bundled
// file on disk
const outputPath = file.outputPath
debug('input:', filePath)
debug('output:', outputPath)
const browserifyOptions = await getBrowserifyOptions(filePath, options.browserifyOptions, options.typescript)
const watchifyOptions = Object.assign({}, defaultOptions.watchifyOptions, options.watchifyOptions)
if (!options.typescript && typescriptExtensionRegex.test(filePath)) {
throwError({
type: errorTypes.TYPESCRIPT_NOT_CONFIGURED,
message: `You are attempting to preprocess a TypeScript file, but do not have TypeScript configured. Pass the 'typescript' option to enable TypeScript support.
The file: ${filePath}`,
})
}
const bundler = browserify(browserifyOptions)
if (file.shouldWatch) {
debug('watching')
bundler.plugin(watchify, watchifyOptions)
}
// yield the bundle if onBundle is specified so the user can modify it
// as need via `bundle.external()`, `bundle.plugin()`, etc
const onBundle = options.onBundle
if (typeof onBundle === 'function') {
onBundle(bundler)
}
// this kicks off the bundling and wraps it up in a promise. the promise
// is what is ultimately returned from this function
// it resolves with the outputPath so Cypress knows where to serve
// the file from
const bundle = () => {
return new Promise((resolve, reject) => {
debug(`making bundle ${outputPath}`)
const onError = (err) => {
err.filePath = filePath
// backup the original stack before its
// potentially modified from bluebird
err.originalStack = err.stack
debug(`errored bundling: ${outputPath}`, err)
reject(err)
}
const ws = fs.createWriteStream(outputPath)
ws.on('finish', () => {
debug('finished bundling:', outputPath)
resolve(outputPath)
})
ws.on('error', onError)
bundler
.bundle()
.on('error', onError)
.pipe(ws)
})
}
// when we're notified of an update via watchify, signal for Cypress to
// rerun the spec
bundler.on('update', () => {
debug('update:', filePath)
// we overwrite the cached bundle promise, so on subsequent invocations
// it gets the latest bundle
const bundlePromise = bundle().finally(() => {
debug('- update finished for:', filePath)
file.emit('rerun')
})
bundles[filePath] = bundlePromise
// we suppress unhandled rejections so they don't bubble up to the
// unhandledRejection handler and crash the app. Cypress will eventually
// take care of the rejection when the file is requested
bundlePromise.suppressUnhandledRejections()
})
const bundlePromise = fs
.ensureDir(path.dirname(outputPath))
.then(bundle)
// cache the bundle promise, so it can be returned if this function
// is invoked again with the same filePath
bundles[filePath] = bundlePromise
// when the spec or project is closed, we need to clean up the cached
// bundle promise and stop the watcher via `bundler.close()`
file.on('close', () => {
debug('close:', filePath)
delete bundles[filePath]
if (file.shouldWatch) {
bundler.close()
}
})
// return the promise, which will resolve with the outputPath or reject
// with any error encountered
return bundlePromise
}
}
// provide a clone of the default options
preprocessor.defaultOptions = JSON.parse(JSON.stringify(defaultOptions))
preprocessor.errorTypes = errorTypes
if (process.env.__TESTING__) {
preprocessor.reset = () => {
for (let filePath in bundles) {
delete bundles[filePath]
}
}
}
module.exports = preprocessor