├── .node-version
├── test
├── fixtures
│ ├── dom.js
│ ├── mul.js
│ ├── divide.js
│ ├── sub.js
│ ├── math.js
│ ├── typescript
│ │ ├── component.tsx
│ │ ├── math.ts
│ │ ├── simple.spec.tsx
│ │ ├── react_spec.tsx
│ │ └── math_spec.ts
│ ├── example_spec.js
│ ├── dom_spec.js
│ ├── divide_spec.js
│ ├── sub_spec.js
│ ├── mul_spec.js
│ ├── math_spec.js
│ └── require_spec.js
├── .eslintrc.json
├── e2e
│ └── e2e_spec.js
└── unit
│ └── index_spec.js
├── .gitignore
├── .npmrc
├── .estlintignore
├── .eslintrc.json
├── .vscode
└── settings.json
├── issue_template.md
├── circle.yml
├── renovate.json
├── LICENSE.md
├── lib
└── simple_tsify.js
├── __snapshots__
└── e2e_spec.js
├── package.json
├── README.md
└── index.js
/.node-version:
--------------------------------------------------------------------------------
1 | 16.13.0
2 |
--------------------------------------------------------------------------------
/test/fixtures/dom.js:
--------------------------------------------------------------------------------
1 | export default 'dom'
2 |
--------------------------------------------------------------------------------
/test/fixtures/mul.js:
--------------------------------------------------------------------------------
1 | module.exports = (a, b) => a * b
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | npm-debug.log
4 | _test-output
5 |
--------------------------------------------------------------------------------
/test/fixtures/divide.js:
--------------------------------------------------------------------------------
1 | // named export
2 | export const divide = (a, b) => a/b
3 |
--------------------------------------------------------------------------------
/test/fixtures/sub.js:
--------------------------------------------------------------------------------
1 | const sub = (a, b) => a - b
2 |
3 | module.exports = {sub}
4 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:@cypress/dev/tests"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/math.js:
--------------------------------------------------------------------------------
1 | export default {
2 | add: (a, b) => {
3 | return a + b
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 | save-exact=true
3 | progress=false
4 | legacy-peer-deps=true
5 |
--------------------------------------------------------------------------------
/.estlintignore:
--------------------------------------------------------------------------------
1 | # don't ignore hidden files, useful for formatting json config files
2 | !.*.json
3 | **/node_modules
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@cypress/dev"
4 | ],
5 | "extends": [
6 | "plugin:@cypress/dev/general"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": true,
3 | // enable for eslint-plugin json-format
4 | "eslint.validate": [
5 | "json"
6 | ],
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default () => {
4 | return (
5 |
icon
6 | )
7 | }
--------------------------------------------------------------------------------
/test/fixtures/example_spec.js:
--------------------------------------------------------------------------------
1 | it('is a test', () => {
2 | const [a, b] = [1, 2]
3 |
4 | expect(a).to.equal(1)
5 | expect(b).to.equal(2)
6 | expect(Math.min(...[3, 4])).to.equal(3)
7 | })
8 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/math.ts:
--------------------------------------------------------------------------------
1 | export const multiply = (a: number, b: number): number => {
2 | return a * b
3 | }
4 |
5 | export default {
6 | add: (a: number, b: number) => {
7 | return a + b
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/test/fixtures/dom_spec.js:
--------------------------------------------------------------------------------
1 | const dom = require('./dom')
2 |
3 | context('imports default string', function () {
4 | it('works', () => {
5 | expect(dom, 'dom').to.be.a('string')
6 | expect(dom).to.equal('dom')
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/test/fixtures/divide_spec.js:
--------------------------------------------------------------------------------
1 | import { divide } from './divide'
2 |
3 | context('ES6 named export and import', function () {
4 | it('works', () => {
5 | expect(divide, 'divide').to.be.a('function')
6 | expect(divide(10, 2)).to.eq(5)
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/test/fixtures/sub_spec.js:
--------------------------------------------------------------------------------
1 | import { sub } from './sub'
2 |
3 | context('sub.js', function () {
4 | it('imports function', () => {
5 | expect(sub, 'sub').to.be.a('function')
6 | })
7 | it('can subtract numbers', function () {
8 | expect(sub(1, 2)).to.eq(-1)
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/fixtures/mul_spec.js:
--------------------------------------------------------------------------------
1 | import mul from './mul'
2 |
3 | context('mul.js imports default', function () {
4 | it('imports function', () => {
5 | expect(mul, 'mul').to.be.a('function')
6 | })
7 | it('can multiply numbers', function () {
8 | expect(mul(3, 2)).to.eq(6)
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | - Operating System:
4 | - Cypress Version:
5 | - Browser Version:
6 |
7 | ### Is this a Feature or Bug?
8 |
9 |
10 | ### Current behavior:
11 |
12 |
13 | ### Desired behavior:
14 |
15 |
16 | ### How to reproduce:
17 |
18 |
19 | ### Additional Info (images, stack traces, etc)
20 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/simple.spec.tsx:
--------------------------------------------------------------------------------
1 | import { multiply } from './math'
2 |
3 | describe('simple .tsx spec', () => {
4 | it('can import another module and add', () => {
5 | const EXPECTED = 6
6 | const result = multiply(2, 3)
7 |
8 | if (result !== EXPECTED) {
9 | throw new Error(`multiplying 2*3 did not equal ${EXPECTED}. received: ${result}`)
10 | }
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/test/fixtures/math_spec.js:
--------------------------------------------------------------------------------
1 | // math exports default object
2 | // so if we want a property, first we need to grab the default
3 | import math from './math'
4 | const {add} = math
5 |
6 | context('math.js', function () {
7 | it('imports function', () => {
8 | expect(add, 'add').to.be.a('function')
9 | })
10 | it('can add numbers', function () {
11 | expect(add(1, 2)).to.eq(3)
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/test/fixtures/require_spec.js:
--------------------------------------------------------------------------------
1 | context('non-top-level requires', function () {
2 | const math = require('./math')
3 | const dom = require('./dom')
4 |
5 | it('imports proper types of values', () => {
6 | expect(math.add, 'add').to.be.a('function')
7 | expect(dom, 'dom').to.be.a('string')
8 | })
9 |
10 | it('values are correct', function () {
11 | expect(math.add(1, 2)).to.eq(3)
12 | expect(dom, 'dom').to.equal('dom')
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/react_spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect } from 'chai'
3 |
4 | import MyComponent from './component'
5 |
6 | describe('', () => {
7 | it('renders an `.icon-star`', () => {
8 | const component =
9 |
10 | expect(component.type().type).to.equal('div')
11 | expect(component.type().props.className).to.equal('icon-star')
12 | expect(component.type().props.children).to.equal('icon')
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | # https://circleci.com/orbs/registry/orb/circleci/node
2 | version: 2.1
3 | orbs:
4 | node: circleci/node@4.7.0
5 | jobs:
6 | build:
7 | docker:
8 | - image: cimg/node:16.13.0
9 | steps:
10 | - checkout
11 | - run:
12 | name: Environment Details
13 | command: |
14 | echo "Node.js version"
15 | node --version
16 | echo "npm version"
17 | npm --version
18 | - node/install-packages
19 | - run:
20 | name: Run Tests
21 | command: npm run test
22 | - run:
23 | name: Semantic Release
24 | command: npm run semantic-release || true
25 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "automerge": true,
6 | "commitMessage": "{{semanticPrefix}}Update {{depName}} to {{newVersion}} 🌟",
7 | "prTitle": "{{semanticPrefix}}{{#if isPin}}Pin{{else}}Update{{/if}} dependency {{depName}} to version {{newVersion}} 🌟",
8 | "major": {
9 | "automerge": false
10 | },
11 | "minor": {
12 | "automerge": false
13 | },
14 | "prConcurrentLimit": 3,
15 | "prHourlyLimit": 2,
16 | "schedule": [
17 | "after 2am and before 3am on saturday"
18 | ],
19 | "updateNotScheduled": false,
20 | "timezone": "America/New_York",
21 | "lockFileMaintenance": {
22 | "enabled": true
23 | },
24 | "separatePatchReleases": true,
25 | "separateMultipleMajor": true,
26 | "masterIssue": true,
27 | "labels": [
28 | "type: dependencies",
29 | "renovate"
30 | ],
31 | "packageRules": [
32 | {
33 | "packageNames": ["cypress"],
34 | "groupName": "cypress",
35 | "schedule": "before 2am"
36 | },
37 | {
38 | "packagePatterns": "^eslint",
39 | "groupName": "eslint"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/math_spec.ts:
--------------------------------------------------------------------------------
1 | // math exports default object
2 | // so if we want a property, first we need to grab the default
3 | import math from './math'
4 | const { add } = math
5 |
6 | const x: number = 3
7 |
8 | // ensures that generics can be properly compiled and not treated
9 | // as react components in `.ts` files.
10 | // https://github.com/cypress-io/cypress-browserify-preprocessor/issues/44
11 | const isKeyOf = (obj: T, key: any): key is keyof T => {
12 | return typeof key === 'string' && key in obj;
13 | }
14 |
15 | context('math.ts', function () {
16 | it('imports function', () => {
17 | expect(add, 'add').to.be.a('function')
18 | })
19 | it('can add numbers', function () {
20 | expect(add(1, 2)).to.eq(3)
21 | })
22 | it('test ts-typed variable', function () {
23 | expect(x).to.eq(3)
24 | })
25 | it('test iterator', () => {
26 | const arr = [...Array(100).keys()]
27 |
28 | expect(arr[0] + arr[1]).to.eq(1)
29 | })
30 | it('Test generic', () => {
31 | const x = {
32 | key: 'value'
33 | }
34 |
35 | expect(isKeyOf(x, 'key')).to.eq(true)
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## MIT License
2 |
3 | Copyright (c) 2017 Cypress.io https://cypress.io
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the "Software"), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/lib/simple_tsify.js:
--------------------------------------------------------------------------------
1 | const through = require('through2')
2 | const path = require('path')
3 |
4 | const isJson = (code) => {
5 | try {
6 | JSON.parse(code)
7 | } catch (e) {
8 | return false
9 | }
10 |
11 | return true
12 | }
13 |
14 | // tsify doesn't have transpile-only option like ts-node or ts-loader.
15 | // It means it should check types whenever spec file is changed
16 | // and it slows down the test speed a lot.
17 | // We skip this slow type-checking process by using transpileModule() api.
18 | module.exports = function (fileName, opts) {
19 | const ts = opts.typescript
20 | const chunks = []
21 | const ext = path.extname(fileName)
22 |
23 | return through(
24 | (buf, enc, next) => {
25 | chunks.push(buf.toString())
26 | next()
27 | },
28 | function (next) {
29 | const text = chunks.join('')
30 |
31 | if (isJson(text)) {
32 | this.push(text)
33 | } else {
34 | this.push(
35 | ts.transpileModule(text, {
36 | // explicitly name the file here
37 | // for sourcemaps
38 | fileName,
39 | compilerOptions: {
40 | esModuleInterop: true,
41 | // inline the source maps into the file
42 | // https://github.com/cypress-io/cypress-browserify-preprocessor/issues/48
43 | inlineSourceMap: true,
44 | inlineSources: true,
45 | jsx:
46 | ext === '.tsx' || ext === '.jsx' || ext === '.js'
47 | ? 'react'
48 | : undefined,
49 | downlevelIteration: true,
50 | },
51 | }).outputText,
52 | )
53 | }
54 |
55 | next()
56 | },
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/__snapshots__/e2e_spec.js:
--------------------------------------------------------------------------------
1 | exports['browserify preprocessor - e2e correctly preprocesses the file 1'] = `
2 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=8"
70 | },
71 | "license": "MIT",
72 | "repository": {
73 | "type": "git",
74 | "url": "https://github.com/cypress-io/cypress-browserify-preprocessor.git"
75 | },
76 | "homepage": "https://github.com/cypress-io/cypress-browserify-preprocessor#readme",
77 | "author": "Chris Breiding ",
78 | "bugs": "https://github.com/cypress-io/cypress-browserify-preprocessor/issues",
79 | "keywords": [
80 | "cypress",
81 | "browserify",
82 | "cypress-plugin",
83 | "cypress-preprocessor"
84 | ],
85 | "release": {
86 | "analyzeCommits": {
87 | "preset": "angular",
88 | "releaseRules": [
89 | {
90 | "type": "break",
91 | "release": "major"
92 | },
93 | {
94 | "type": "major",
95 | "release": "major"
96 | },
97 | {
98 | "type": "minor",
99 | "release": "minor"
100 | },
101 | {
102 | "type": "patch",
103 | "release": "patch"
104 | }
105 | ]
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/test/e2e/e2e_spec.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const chai = require('chai')
3 | const fs = require('fs-extra')
4 | const path = require('path')
5 | const snapshot = require('snap-shot-it')
6 | const Bluebird = require('bluebird')
7 |
8 | process.env.__TESTING__ = true
9 |
10 | const preprocessor = require('../../index')
11 |
12 | /* eslint-disable-next-line no-unused-vars */
13 | const expect = chai.expect
14 |
15 | const typescript = require.resolve('typescript')
16 |
17 | beforeEach(() => {
18 | fs.removeSync(path.join(__dirname, '_test-output'))
19 | preprocessor.reset()
20 | })
21 |
22 | // do not generate source maps by default
23 | const DEFAULT_OPTIONS = { browserifyOptions: { debug: false } }
24 |
25 | const bundle = (fixtureName, options = DEFAULT_OPTIONS) => {
26 | const on = () => {}
27 | const filePath = path.join(__dirname, '..', 'fixtures', fixtureName)
28 | const outputPath = path.join(__dirname, '..', '_test-output', fixtureName)
29 |
30 | return preprocessor(options)({ filePath, outputPath, on }).then(() => {
31 | return fs.readFileSync(outputPath).toString()
32 | })
33 | }
34 |
35 | const parseSourceMap = (output) => {
36 | return _
37 | .chain(output)
38 | .split('//# sourceMappingURL=data:application/json;charset=utf-8;base64,')
39 | .last()
40 | .thru((str) => {
41 | const base64 = Buffer.from(str, 'base64').toString()
42 |
43 | return JSON.parse(base64)
44 | })
45 | .value()
46 | }
47 |
48 | const verifySourceContents = ({ sources, sourcesContent }) => {
49 | const zippedArrays = _.zip(sources, sourcesContent)
50 |
51 | return Bluebird.map(zippedArrays, ([sourcePath, sourceContent]) => {
52 | return fs.readFile(sourcePath, 'utf8')
53 | .then((str) => {
54 | expect(str).to.eq(sourceContent)
55 | })
56 | })
57 | }
58 |
59 | describe('browserify preprocessor - e2e', () => {
60 | it('correctly preprocesses the file', () => {
61 | return bundle('example_spec.js').then((output) => {
62 | snapshot(output)
63 | })
64 | })
65 |
66 | describe('imports and exports', () => {
67 | it('handles imports and exports', () => {
68 | return bundle('math_spec.js').then((output) => {
69 | // check that bundled tests work
70 | eval(output)
71 | })
72 | })
73 |
74 | it('named ES6', () => {
75 | return bundle('divide_spec.js').then((output) => {
76 | // check that bundled tests work
77 | eval(output)
78 | })
79 | })
80 |
81 | it('handles module.exports and import', () => {
82 | return bundle('sub_spec.js').then((output) => {
83 | // check that bundled tests work
84 | eval(output)
85 | snapshot('sub import', output)
86 | })
87 | })
88 |
89 | it('handles module.exports and default import', () => {
90 | return bundle('mul_spec.js').then((output) => {
91 | // check that bundled tests work
92 | eval(output)
93 | // for some reason, this bundle included full resolved path
94 | // to interop require module
95 | // which on CI generates different path.
96 | // so as long as eval works, do not snapshot it
97 | })
98 | })
99 |
100 | it('handles default string import', () => {
101 | return bundle('dom_spec.js').then((output) => {
102 | // check that bundled tests work
103 | eval(output)
104 | })
105 | })
106 |
107 | it('handles non-top-level require', () => {
108 | return bundle('require_spec.js').then((output) => {
109 | // check that bundled tests work
110 | eval(output)
111 | })
112 | })
113 | })
114 |
115 | describe('typescript', () => {
116 | it('handles .ts file when the path is given', () => {
117 | return bundle('typescript/math_spec.ts', {
118 | typescript,
119 | }).then((output) => {
120 | // check that bundled tests work
121 | eval(output)
122 |
123 | const sourceMap = parseSourceMap(output)
124 |
125 | expect(sourceMap.sources).to.deep.eq([
126 | 'node_modules/browser-pack/_prelude.js',
127 | 'test/fixtures/typescript/math.ts',
128 | 'test/fixtures/typescript/math_spec.ts',
129 | ])
130 |
131 | return verifySourceContents(sourceMap)
132 | })
133 | })
134 |
135 | it('handles simple .tsx file with imports', () => {
136 | return bundle('typescript/simple.spec.tsx', {
137 | typescript,
138 | }).then((output) => {
139 | // check that bundled tests work
140 | eval(output)
141 |
142 | const sourceMap = parseSourceMap(output)
143 |
144 | expect(sourceMap.sources).to.deep.eq([
145 | 'node_modules/browser-pack/_prelude.js',
146 | 'test/fixtures/typescript/math.ts',
147 | 'test/fixtures/typescript/simple.spec.tsx',
148 | ])
149 |
150 | return verifySourceContents(sourceMap)
151 | })
152 | })
153 |
154 | it('handles .tsx file when the path is given', () => {
155 | return bundle('typescript/react_spec.tsx', {
156 | typescript,
157 | }).then((output) => {
158 | // check that bundled tests work
159 | eval(output)
160 | })
161 | })
162 | })
163 | })
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Note: This plugin is deprecated and Cypress will not be moving forward with development of the plugin.
2 |
3 | # Cypress Browserify Preprocessor [](https://circleci.com/gh/cypress-io/cypress-browserify-preprocessor/tree/master)
4 |
5 | Cypress preprocessor for bundling JavaScript via browserify.
6 |
7 | Modifying the default options allows you to add support for things like:
8 |
9 | - TypeScript
10 | - Babel Plugins
11 | - ES Presets
12 |
13 | ## Installation
14 |
15 | Requires [Node](https://nodejs.org/en/) version 6.5.0 or above.
16 |
17 | ```sh
18 | npm install --save-dev @cypress/browserify-preprocessor
19 | ```
20 |
21 | ## Usage
22 |
23 | In your project's [plugins file](https://on.cypress.io/plugins-guide):
24 |
25 | ```javascript
26 | const browserify = require('@cypress/browserify-preprocessor')
27 |
28 | module.exports = (on) => {
29 | on('file:preprocessor', browserify())
30 | }
31 | ```
32 |
33 | ## Options
34 |
35 | Pass in options as the second argument to `browserify`:
36 |
37 | ```javascript
38 | module.exports = (on) => {
39 | const options = {
40 | // options here
41 | }
42 |
43 | on('file:preprocessor', browserify(options))
44 | }
45 | ```
46 |
47 | ### browserifyOptions
48 |
49 | Object of options passed to [browserify](https://github.com/browserify/browserify#browserifyfiles--opts).
50 |
51 | ```javascript
52 | // example
53 | browserify({
54 | browserifyOptions: {
55 | extensions: ['.js', '.ts'],
56 | plugin: [
57 | ['tsify']
58 | ]
59 | }
60 | })
61 | ```
62 |
63 | If you pass one of the top-level options in (`extensions`, `transform`, etc), it will override the default. In the above example, browserify will process `.js` and `.ts` files, but not `.jsx` or `.coffee`. If you wish to add to or modify existing options, read about [modifying the default options](#modifying-default-options).
64 |
65 | [watchify](https://github.com/browserify/watchify) is automatically configured as a plugin (as needed), so it's not necessary to manually specify it.
66 |
67 | Source maps are always enabled unless explicitly disabled by specifying `debug: false`.
68 |
69 | **Default**:
70 |
71 | ```javascript
72 | {
73 | extensions: ['.js', '.jsx', '.coffee'],
74 | transform: [
75 | [
76 | 'coffeeify',
77 | {}
78 | ],
79 | [
80 | 'babelify',
81 | {
82 | ast: false,
83 | babelrc: false,
84 | plugins: [
85 | '@babel/plugin-transform-modules-commonjs',
86 | '@babel/plugin-proposal-class-properties',
87 | '@babel/plugin-proposal-object-rest-spread',
88 | '@babel/plugin-transform-runtime',
89 | ],
90 | presets: [
91 | '@babel/preset-env',
92 | '@babel/preset-react',
93 | ]
94 | },
95 | ]
96 | ],
97 | debug: true,
98 | plugin: [],
99 | cache: {},
100 | packageCache: {}
101 | }
102 | ```
103 |
104 | *Note*: `cache` and `packageCache` are always set to `{}` and cannot be overridden. Otherwise, file watching would not function correctly.
105 |
106 | ### watchifyOptions
107 |
108 | Object of options passed to [watchify](https://github.com/browserify/watchify#options)
109 |
110 | ```javascript
111 | // example
112 | browserify({
113 | watchifyOptions: {
114 | delay: 500
115 | }
116 | })
117 | ```
118 |
119 | **Default**:
120 |
121 | ```javascript
122 | {
123 | ignoreWatch: [
124 | '**/.git/**',
125 | '**/.nyc_output/**',
126 | '**/.sass-cache/**',
127 | '**/bower_components/**',
128 | '**/coverage/**',
129 | '**/node_modules/**'
130 | ],
131 | }
132 | ```
133 |
134 | ### onBundle
135 |
136 | A function that is called with the [browserify instance](https://github.com/browserify/browserify#browserifyfiles--opts). This allows you to specify external files and plugins. See the [browserify docs](https://github.com/browserify/browserify#baddfile-opts) for methods available.
137 |
138 | ```javascript
139 | // example
140 | browserify({
141 | onBundle (bundle) {
142 | bundle.external('react')
143 | bundle.plugin('some-plugin')
144 | bundle.ignore('pg-native')
145 | }
146 | })
147 | ```
148 |
149 | ### typescript
150 |
151 | When the path to the TypeScript package is given, Cypress will automatically transpile `.ts` spec, plugin, support files. Note that this **DOES NOT** check types.
152 |
153 | ```javascript
154 | browserify({
155 | typescript: require.resolve('typescript')
156 | })
157 | ```
158 |
159 | **Default**: `undefined`
160 |
161 | ## Modifying default options
162 |
163 | The default options are provided as `browserify.defaultOptions` so they can be more easily modified.
164 |
165 | If, for example, you want to update the options for the `babelify` transform to turn on `babelrc` loading, you could do the following:
166 |
167 | ```javascript
168 | const browserify = require('@cypress/browserify-preprocessor')
169 |
170 | module.exports = (on) => {
171 | const options = browserify.defaultOptions
172 | options.browserifyOptions.transform[1][1].babelrc = true
173 |
174 | on('file:preprocessor', browserify(options))
175 | }
176 | ```
177 |
178 | ## Debugging
179 |
180 | Execute code with `DEBUG=cypress:browserify` environment variable.
181 |
182 | ## Contributing
183 |
184 | Run all tests once:
185 |
186 | ```shell
187 | npm test
188 | ```
189 |
190 | Run tests in watch mode:
191 |
192 | ```shell
193 | npm run test-watch
194 | ```
195 |
196 | ## License
197 |
198 | This project is licensed under the terms of the [MIT license](/LICENSE.md).
199 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const Promise = require('bluebird')
5 | const fs = require('fs-extra')
6 |
7 | const cloneDeep = require('lodash.clonedeep')
8 | const browserify = require('browserify')
9 | const watchify = require('watchify')
10 |
11 | const debug = require('debug')('cypress:browserify')
12 |
13 | const typescriptExtensionRegex = /\.tsx?$/
14 | const errorTypes = {
15 | TYPESCRIPT_AND_TSIFY: 'TYPESCRIPT_AND_TSIFY',
16 | TYPESCRIPT_NONEXISTENT: 'TYPESCRIPT_NONEXISTENT',
17 | TYPESCRIPT_NOT_CONFIGURED: 'TYPESCRIPT_NOT_CONFIGURED',
18 | TYPESCRIPT_NOT_STRING: 'TYPESCRIPT_NOT_STRING',
19 | }
20 |
21 | const bundles = {}
22 |
23 | // by default, we transform JavaScript (including some proposal features),
24 | // JSX, & CoffeeScript
25 | const defaultOptions = {
26 | browserifyOptions: {
27 | extensions: ['.js', '.jsx', '.coffee'],
28 | transform: [
29 | [
30 | require.resolve('coffeeify'),
31 | {},
32 | ],
33 | [
34 | require.resolve('babelify'),
35 | {
36 | ast: false,
37 | babelrc: false,
38 | plugins: [
39 | ...[
40 | 'babel-plugin-add-module-exports',
41 | '@babel/plugin-proposal-class-properties',
42 | '@babel/plugin-proposal-object-rest-spread',
43 | ].map(require.resolve),
44 | [require.resolve('@babel/plugin-transform-runtime'), {
45 | absoluteRuntime: path.dirname(require.resolve('@babel/runtime/package')),
46 | }],
47 | ],
48 | presets: [
49 | '@babel/preset-env',
50 | '@babel/preset-react',
51 | ].map(require.resolve),
52 | },
53 | ],
54 | ],
55 | plugin: [],
56 | },
57 | watchifyOptions: {
58 | // ignore watching the following or the user's system can get bogged down
59 | // by watchers
60 | ignoreWatch: [
61 | '**/.git/**',
62 | '**/.nyc_output/**',
63 | '**/.sass-cache/**',
64 | '**/bower_components/**',
65 | '**/coverage/**',
66 | '**/node_modules/**',
67 | ],
68 | },
69 | }
70 |
71 | const throwError = ({ message, type }) => {
72 | const prefix = 'Error running @cypress/browserify-preprocessor:\n\n'
73 |
74 | const err = new Error(`${prefix}${message}`)
75 |
76 | if (type) err.type = type
77 |
78 | throw err
79 | }
80 |
81 | const getBrowserifyOptions = async (entry, userBrowserifyOptions = {}, typescriptPath = null) => {
82 | let browserifyOptions = cloneDeep(defaultOptions.browserifyOptions)
83 |
84 | // allow user to override default options
85 | browserifyOptions = Object.assign(browserifyOptions, userBrowserifyOptions, {
86 | // these must always be new objects or 'update' events will not fire
87 | cache: {},
88 | packageCache: {},
89 | })
90 |
91 | // unless user has explicitly turned off source map support, always enable it
92 | // so we can use it to point user to the source code
93 | if (userBrowserifyOptions.debug !== false) {
94 | browserifyOptions.debug = true
95 | }
96 |
97 | // we need to override and control entries
98 | Object.assign(browserifyOptions, {
99 | entries: [entry],
100 | })
101 |
102 | if (typescriptPath) {
103 | if (typeof typescriptPath !== 'string') {
104 | throwError({
105 | type: errorTypes.TYPESCRIPT_NOT_STRING,
106 | message: `The 'typescript' option must be a string. You passed: ${typescriptPath}`,
107 | })
108 | }
109 |
110 | const pathExists = await fs.pathExists(typescriptPath)
111 |
112 | if (!pathExists) {
113 | throwError({
114 | type: errorTypes.TYPESCRIPT_NONEXISTENT,
115 | message: `The 'typescript' option must be a valid path to your TypeScript installation. We could not find anything at the following path: ${typescriptPath}`,
116 | })
117 | }
118 |
119 | const transform = browserifyOptions.transform
120 | const hasTsifyTransform = transform.some((stage) => Array.isArray(stage) && stage[0].includes('tsify'))
121 | const hastsifyPlugin = browserifyOptions.plugin.includes('tsify')
122 |
123 | if (hasTsifyTransform || hastsifyPlugin) {
124 | const type = hasTsifyTransform ? 'transform' : 'plugin'
125 |
126 | throwError({
127 | type: errorTypes.TYPESCRIPT_AND_TSIFY,
128 | message: `It looks like you passed the 'typescript' option and also specified a browserify ${type} for TypeScript. This may cause conflicts.
129 |
130 | Please do one of the following:
131 |
132 | 1) Pass in the 'typescript' option and omit the browserify ${type} (Recommmended)
133 | 2) Omit the 'typescript' option and continue to use your own browserify ${type}`,
134 | })
135 | }
136 |
137 | browserifyOptions.extensions.push('.ts', '.tsx')
138 | // remove babelify setting
139 | browserifyOptions.transform = transform.filter((stage) => !Array.isArray(stage) || !stage[0].includes('babelify'))
140 | // add typescript compiler
141 | browserifyOptions.transform.push([
142 | path.join(__dirname, './lib/simple_tsify'), {
143 | typescript: require(typescriptPath),
144 | },
145 | ])
146 | }
147 |
148 | debug('browserifyOptions: %o', browserifyOptions)
149 |
150 | return browserifyOptions
151 | }
152 |
153 | // export a function that returns another function, making it easy for users
154 | // to configure like so:
155 | //
156 | // on('file:preprocessor', browserify(options))
157 | //
158 | const preprocessor = (options = {}) => {
159 | debug('received user options: %o', options)
160 |
161 | // we return function that accepts the arguments provided by
162 | // the event 'file:preprocessor'
163 | //
164 | // this function will get called for the support file when a project is loaded
165 | // (if the support file is not disabled)
166 | // it will also get called for a spec file when that spec is requested by
167 | // the Cypress runner
168 | //
169 | // when running in the GUI, it will likely get called multiple times
170 | // with the same filePath, as the user could re-run the tests, causing
171 | // the supported file and spec file to be requested again
172 | return async (file) => {
173 | const filePath = file.filePath
174 |
175 | debug('get:', filePath)
176 |
177 | // since this function can get called multiple times with the same
178 | // filePath, we return the cached bundle promise if we already have one
179 | // since we don't want or need to re-initiate browserify/watchify for it
180 | if (bundles[filePath]) {
181 | debug('already have bundle for:', filePath)
182 |
183 | return bundles[filePath]
184 | }
185 |
186 | // we're provided a default output path that lives alongside Cypress's
187 | // app data files so we don't have to worry about where to put the bundled
188 | // file on disk
189 | const outputPath = file.outputPath
190 |
191 | debug('input:', filePath)
192 | debug('output:', outputPath)
193 |
194 | const browserifyOptions = await getBrowserifyOptions(filePath, options.browserifyOptions, options.typescript)
195 | const watchifyOptions = Object.assign({}, defaultOptions.watchifyOptions, options.watchifyOptions)
196 |
197 | if (!options.typescript && typescriptExtensionRegex.test(filePath)) {
198 | throwError({
199 | type: errorTypes.TYPESCRIPT_NOT_CONFIGURED,
200 | message: `You are attempting to preprocess a TypeScript file, but do not have TypeScript configured. Pass the 'typescript' option to enable TypeScript support.
201 |
202 | The file: ${filePath}`,
203 | })
204 | }
205 |
206 | const bundler = browserify(browserifyOptions)
207 |
208 | if (file.shouldWatch) {
209 | debug('watching')
210 | bundler.plugin(watchify, watchifyOptions)
211 | }
212 |
213 | // yield the bundle if onBundle is specified so the user can modify it
214 | // as need via `bundle.external()`, `bundle.plugin()`, etc
215 | const onBundle = options.onBundle
216 |
217 | if (typeof onBundle === 'function') {
218 | onBundle(bundler)
219 | }
220 |
221 | // this kicks off the bundling and wraps it up in a promise. the promise
222 | // is what is ultimately returned from this function
223 | // it resolves with the outputPath so Cypress knows where to serve
224 | // the file from
225 | const bundle = () => {
226 | return new Promise((resolve, reject) => {
227 | debug(`making bundle ${outputPath}`)
228 |
229 | const onError = (err) => {
230 | err.filePath = filePath
231 | // backup the original stack before its
232 | // potentially modified from bluebird
233 | err.originalStack = err.stack
234 | debug(`errored bundling: ${outputPath}`, err)
235 | reject(err)
236 | }
237 |
238 | const ws = fs.createWriteStream(outputPath)
239 |
240 | ws.on('finish', () => {
241 | debug('finished bundling:', outputPath)
242 | resolve(outputPath)
243 | })
244 |
245 | ws.on('error', onError)
246 |
247 | bundler
248 | .bundle()
249 | .on('error', onError)
250 | .pipe(ws)
251 | })
252 | }
253 |
254 | // when we're notified of an update via watchify, signal for Cypress to
255 | // rerun the spec
256 | bundler.on('update', () => {
257 | debug('update:', filePath)
258 | // we overwrite the cached bundle promise, so on subsequent invocations
259 | // it gets the latest bundle
260 | const bundlePromise = bundle().finally(() => {
261 | debug('- update finished for:', filePath)
262 | file.emit('rerun')
263 | })
264 |
265 | bundles[filePath] = bundlePromise
266 | // we suppress unhandled rejections so they don't bubble up to the
267 | // unhandledRejection handler and crash the app. Cypress will eventually
268 | // take care of the rejection when the file is requested
269 | bundlePromise.suppressUnhandledRejections()
270 | })
271 |
272 | const bundlePromise = fs
273 | .ensureDir(path.dirname(outputPath))
274 | .then(bundle)
275 |
276 | // cache the bundle promise, so it can be returned if this function
277 | // is invoked again with the same filePath
278 | bundles[filePath] = bundlePromise
279 |
280 | // when the spec or project is closed, we need to clean up the cached
281 | // bundle promise and stop the watcher via `bundler.close()`
282 | file.on('close', () => {
283 | debug('close:', filePath)
284 | delete bundles[filePath]
285 | if (file.shouldWatch) {
286 | bundler.close()
287 | }
288 | })
289 |
290 | // return the promise, which will resolve with the outputPath or reject
291 | // with any error encountered
292 | return bundlePromise
293 | }
294 | }
295 |
296 | // provide a clone of the default options
297 | preprocessor.defaultOptions = JSON.parse(JSON.stringify(defaultOptions))
298 |
299 | preprocessor.errorTypes = errorTypes
300 |
301 | if (process.env.__TESTING__) {
302 | preprocessor.reset = () => {
303 | for (let filePath in bundles) {
304 | delete bundles[filePath]
305 | }
306 | }
307 | }
308 |
309 | module.exports = preprocessor
310 |
--------------------------------------------------------------------------------
/test/unit/index_spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chai = require('chai')
4 | const fs = require('fs-extra')
5 | const mockery = require('mockery')
6 | const sinon = require('sinon')
7 | const watchify = require('watchify')
8 |
9 | const expect = chai.expect
10 |
11 | chai.use(require('sinon-chai'))
12 |
13 | const sandbox = sinon.sandbox.create()
14 | const browserify = sandbox.stub()
15 |
16 | mockery.enable({
17 | warnOnUnregistered: false,
18 | })
19 |
20 | mockery.registerMock('browserify', browserify)
21 |
22 | const streamApi = {
23 | pipe () {
24 | return streamApi
25 | },
26 | }
27 |
28 | streamApi.on = sandbox.stub().returns(streamApi)
29 |
30 | process.env.__TESTING__ = true
31 |
32 | const preprocessor = require('../../index')
33 |
34 | describe('browserify preprocessor', function () {
35 | beforeEach(function () {
36 | sandbox.restore()
37 |
38 | const bundlerApi = this.bundlerApi = {
39 | bundle: sandbox.stub().returns(streamApi),
40 | external () {
41 | return bundlerApi
42 | },
43 | close: sandbox.spy(),
44 | plugin: sandbox.stub(),
45 | }
46 |
47 | bundlerApi.transform = sandbox.stub().returns(bundlerApi)
48 | bundlerApi.on = sandbox.stub().returns(bundlerApi)
49 |
50 | browserify.returns(bundlerApi)
51 |
52 | this.createWriteStreamApi = {
53 | on: sandbox.stub(),
54 | }
55 |
56 | sandbox.stub(fs, 'createWriteStream').returns(this.createWriteStreamApi)
57 | sandbox.stub(fs, 'ensureDir').resolves()
58 |
59 | this.options = {}
60 | this.file = {
61 | filePath: 'path/to/file.js',
62 | outputPath: 'output/output.js',
63 | shouldWatch: false,
64 | on: sandbox.stub(),
65 | emit: sandbox.spy(),
66 | }
67 |
68 | this.run = () => {
69 | return preprocessor(this.options)(this.file)
70 | }
71 | })
72 |
73 | describe('exported function', function () {
74 | it('receives user options and returns a preprocessor function', function () {
75 | expect(preprocessor(this.options)).to.be.a('function')
76 | })
77 |
78 | it('has defaultOptions attached to it', function () {
79 | expect(preprocessor.defaultOptions).to.be.an('object')
80 | expect(preprocessor.defaultOptions.browserifyOptions).to.be.an('object')
81 | })
82 | })
83 |
84 | describe('preprocessor function', function () {
85 | beforeEach(function () {
86 | preprocessor.reset()
87 | })
88 |
89 | describe('when it finishes cleanly', function () {
90 | beforeEach(function () {
91 | this.createWriteStreamApi.on.withArgs('finish').yields()
92 | })
93 |
94 | it('runs browserify', function () {
95 | return this.run().then(() => {
96 | expect(browserify).to.be.called
97 | })
98 | })
99 |
100 | it('returns existing bundle if called again with same filePath', function () {
101 | browserify.reset()
102 | browserify.returns(this.bundlerApi)
103 |
104 | const run = preprocessor(this.options)
105 |
106 | return run(this.file)
107 | .then(() => {
108 | return run(this.file)
109 | })
110 | .then(() => {
111 | expect(browserify).to.be.calledOnce
112 | })
113 | })
114 |
115 | it('specifies the entry file', function () {
116 | return this.run().then(() => {
117 | expect(browserify.lastCall.args[0].entries[0]).to.equal(this.file.filePath)
118 | })
119 | })
120 |
121 | it('specifies default extensions if none provided', function () {
122 | return this.run().then(() => {
123 | expect(browserify.lastCall.args[0].extensions).to.eql(['.js', '.jsx', '.coffee'])
124 | })
125 | })
126 |
127 | it('uses provided extensions', function () {
128 | this.options.browserifyOptions = { extensions: ['.coffee'] }
129 |
130 | return this.run().then(() => {
131 | expect(browserify.lastCall.args[0].extensions).to.eql(['.coffee'])
132 | })
133 | })
134 |
135 | it('starts with clean cache and packageCache', function () {
136 | browserify.reset()
137 | browserify.returns(this.bundlerApi)
138 |
139 | const run = preprocessor(this.options)
140 |
141 | return run(this.file)
142 | .then(() => {
143 | browserify.lastCall.args[0].cache.foo = 'bar'
144 | browserify.lastCall.args[0].packageCache.foo = 'bar'
145 | this.file.on.withArgs('close').yield()
146 |
147 | return run(this.file)
148 | })
149 | .then(() => {
150 | expect(browserify).to.be.calledTwice
151 | expect(browserify.lastCall.args[0].cache).to.eql({})
152 | expect(browserify.lastCall.args[0].packageCache).to.eql({})
153 | })
154 | })
155 |
156 | it('watches when shouldWatch is true', function () {
157 | this.file.shouldWatch = true
158 |
159 | return this.run().then(() => {
160 | expect(this.bundlerApi.plugin).to.be.calledWith(watchify)
161 | })
162 | })
163 |
164 | it('use default watchifyOptions if not provided', function () {
165 | this.file.shouldWatch = true
166 |
167 | return this.run().then(() => {
168 | expect(this.bundlerApi.plugin).to.be.calledWith(watchify, {
169 | ignoreWatch: [
170 | '**/.git/**',
171 | '**/.nyc_output/**',
172 | '**/.sass-cache/**',
173 | '**/bower_components/**',
174 | '**/coverage/**',
175 | '**/node_modules/**',
176 | ],
177 | })
178 | })
179 | })
180 |
181 | it('includes watchifyOptions if provided', function () {
182 | this.file.shouldWatch = true
183 | this.options.watchifyOptions = { ignoreWatch: ['node_modules'] }
184 |
185 | return this.run().then(() => {
186 | expect(this.bundlerApi.plugin).to.be.calledWith(watchify, {
187 | ignoreWatch: ['node_modules'],
188 | })
189 | })
190 | })
191 |
192 | it('does not watch when shouldWatch is false', function () {
193 | return this.run().then(() => {
194 | expect(this.bundlerApi.plugin).not.to.be.called
195 | })
196 | })
197 |
198 | it('calls onBundle callback with bundler', function () {
199 | this.options.onBundle = sandbox.spy()
200 |
201 | return this.run().then(() => {
202 | expect(this.options.onBundle).to.be.calledWith(this.bundlerApi)
203 | })
204 | })
205 |
206 | it('uses transforms if provided', function () {
207 | const transform = [() => { }, {}]
208 |
209 | this.options.browserifyOptions = { transform }
210 |
211 | return this.run().then(() => {
212 | expect(browserify.lastCall.args[0].transform).to.eql(transform)
213 | })
214 | })
215 |
216 | it('ensures directory for output is created', function () {
217 | return this.run().then(() => {
218 | expect(fs.ensureDir).to.be.calledWith('output')
219 | })
220 | })
221 |
222 | it('creates write stream to output path', function () {
223 | return this.run().then(() => {
224 | expect(fs.createWriteStream).to.be.calledWith(this.file.outputPath)
225 | })
226 | })
227 |
228 | it('bundles', function () {
229 | return this.run().then(() => {
230 | expect(this.bundlerApi.bundle).to.be.called
231 | })
232 | })
233 |
234 | it('resolves with the output path', function () {
235 | return this.run().then((outputPath) => {
236 | expect(outputPath).to.equal(this.file.outputPath)
237 | })
238 | })
239 |
240 | it('re-bundles when there is an update', function () {
241 | this.bundlerApi.on.withArgs('update').yields()
242 |
243 | return this.run().then(() => {
244 | expect(this.bundlerApi.bundle).to.be.calledTwice
245 | })
246 | })
247 |
248 | it('emits `rerun` when there is an update', function () {
249 | this.bundlerApi.on.withArgs('update').yields()
250 |
251 | return this.run().then(() => {
252 | expect(this.file.emit).to.be.calledWith('rerun')
253 | })
254 | })
255 |
256 | it('closes bundler when shouldWatch is true and `close` is emitted', function () {
257 | this.file.shouldWatch = true
258 |
259 | return this.run().then(() => {
260 | this.file.on.withArgs('close').yield()
261 | expect(this.bundlerApi.close).to.be.called
262 | })
263 | })
264 |
265 | it('does not close bundler when shouldWatch is false and `close` is emitted', function () {
266 | return this.run().then(() => {
267 | this.file.on.withArgs('close').yield()
268 | expect(this.bundlerApi.close).not.to.be.called
269 | })
270 | })
271 |
272 | describe('source maps', () => {
273 | describe('browerifyOptions.debug', function () {
274 | it('true by default', function () {
275 | return this.run().then(() => {
276 | expect(browserify.lastCall.args[0].debug).to.be.true
277 | })
278 | })
279 |
280 | it('false if user has explicitly set to false', function () {
281 | this.options.browserifyOptions = { debug: false }
282 |
283 | return this.run().then(() => {
284 | expect(browserify.lastCall.args[0].debug).to.be.false
285 | })
286 | })
287 | })
288 | })
289 | })
290 |
291 | describe('when it errors', function () {
292 | beforeEach(function () {
293 | this.err = {
294 | stack: 'Failed to preprocess...',
295 | }
296 | })
297 |
298 | it('errors if write stream fails', function () {
299 | this.createWriteStreamApi.on.withArgs('error').yields(this.err)
300 |
301 | return this.run().catch((err) => {
302 | expect(err.stack).to.equal(this.err.stack)
303 | })
304 | })
305 |
306 | it('errors if bundling fails', function () {
307 | streamApi.on.withArgs('error').yields(this.err)
308 |
309 | return this.run().catch((err) => {
310 | expect(err.stack).to.equal(this.err.stack)
311 | })
312 | })
313 |
314 | it('backs up stack as originalStack', function () {
315 | this.createWriteStreamApi.on.withArgs('error').yields(this.err)
316 |
317 | return this.run().catch((err) => {
318 | expect(err.originalStack).to.equal(this.err.stack)
319 | })
320 | })
321 |
322 | it('does not trigger unhandled rejection when bundle errors after update', function (done) {
323 | const handler = sandbox.spy()
324 |
325 | process.on('unhandledRejection', handler)
326 | this.createWriteStreamApi.on.withArgs('finish').onFirstCall().yields()
327 |
328 | this.file.emit = () => {
329 | setTimeout(() => {
330 | expect(handler).not.to.be.called
331 | process.removeListener('unhandledRejection', handler)
332 | done()
333 | }, 500)
334 | }
335 |
336 | this.run().then(() => {
337 | streamApi.on.withArgs('error').yieldsAsync(new Error('bundle error')).returns({ pipe () { } })
338 | this.bundlerApi.on.withArgs('update').yield()
339 | })
340 | })
341 |
342 | it('rejects subsequent request after and update bundle errors', function () {
343 | this.createWriteStreamApi.on.withArgs('finish').onFirstCall().yields()
344 | const run = preprocessor(this.options)
345 |
346 | return run(this.file)
347 | .then(() => {
348 | streamApi.on.withArgs('error').yieldsAsync(new Error('bundle error')).returns({ pipe () { } })
349 | this.bundlerApi.on.withArgs('update').yield()
350 |
351 | return run(this.file)
352 | })
353 | .then(() => {
354 | throw new Error('should not resolve')
355 | })
356 | .catch((err) => {
357 | expect(err.message).to.contain('bundle error')
358 | })
359 | })
360 | })
361 |
362 | describe('typescript support', function () {
363 | beforeEach(function () {
364 | this.options.typescript = require.resolve('typescript')
365 | })
366 |
367 | it('adds tsify transform', function () {
368 | this.createWriteStreamApi.on.withArgs('finish').yields()
369 |
370 | return this.run().then(() => {
371 | expect(browserify.lastCall.args[0].transform[1][0]).to.include('simple_tsify')
372 | })
373 | })
374 |
375 | it('adds to extensions', function () {
376 | this.createWriteStreamApi.on.withArgs('finish').yields()
377 |
378 | return this.run().then(() => {
379 | expect(browserify.lastCall.args[0].extensions).to.eql(['.js', '.jsx', '.coffee', '.ts', '.tsx'])
380 | })
381 | })
382 |
383 | // Regression test for cypress-io/cypress-browserify-preprocessor#56
384 | it('handles transforms defined as functions', function () {
385 | this.createWriteStreamApi.on.withArgs('finish').yields()
386 |
387 | const transform = [() => { }, {}]
388 |
389 | this.options.browserifyOptions = { transform }
390 |
391 | return this.run().then(() => {
392 | transform.forEach((stage, stageIndex) => {
393 | expect(browserify.lastCall.args[0].transform[stageIndex]).to.eql(stage)
394 | })
395 | })
396 | })
397 |
398 | it('removes babelify transform', function () {
399 | this.createWriteStreamApi.on.withArgs('finish').yields()
400 |
401 | return this.run().then(() => {
402 | const transforms = browserify.lastCall.args[0].transform
403 |
404 | expect(transforms).to.have.length(2)
405 | expect(transforms[1][0]).not.to.include('babelify')
406 | })
407 | })
408 |
409 | it('does not change browserify options without typescript option', function () {
410 | this.createWriteStreamApi.on.withArgs('finish').yields()
411 |
412 | this.options.typescript = undefined
413 |
414 | return this.run().then(() => {
415 | expect(browserify.lastCall.args[0].transform[1][0]).to.include('babelify')
416 | expect(browserify.lastCall.args[0].transform[1][0]).not.to.include('simple_tsify')
417 | expect(browserify.lastCall.args[0].extensions).to.eql(['.js', '.jsx', '.coffee'])
418 | })
419 | })
420 |
421 | it('removes babelify transform even if it is not the last item', function () {
422 | this.createWriteStreamApi.on.withArgs('finish').yields()
423 |
424 | const { browserifyOptions } = preprocessor.defaultOptions
425 |
426 | this.options.browserifyOptions = {
427 | ...browserifyOptions,
428 | transform: [
429 | browserifyOptions.transform[1],
430 | browserifyOptions.transform[0],
431 | ],
432 | }
433 |
434 | return this.run().then(() => {
435 | const transforms = browserify.lastCall.args[0].transform
436 |
437 | expect(transforms).to.have.length(2)
438 | expect(transforms[1][0]).not.to.include('babelify')
439 | })
440 | })
441 |
442 | describe('validation', function () {
443 | const shouldntResolve = () => {
444 | throw new Error('Should error, should not resolve')
445 | }
446 |
447 | const verifyErrorIncludesPrefix = (err) => {
448 | expect(err.message).to.include('Error running @cypress/browserify-preprocessor:')
449 | }
450 |
451 | it('throws error when typescript path is not a string', function () {
452 | this.options.typescript = true
453 |
454 | return this.run()
455 | .then(shouldntResolve)
456 | .catch((err) => {
457 | verifyErrorIncludesPrefix(err)
458 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_NOT_STRING)
459 | expect(err.message).to.include(`The 'typescript' option must be a string. You passed: true`)
460 | })
461 | })
462 |
463 | it('throws error when nothing exists at typescript path', function () {
464 | this.options.typescript = '/nothing/here'
465 |
466 | return this.run()
467 | .then(shouldntResolve)
468 | .catch((err) => {
469 | verifyErrorIncludesPrefix(err)
470 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_NONEXISTENT)
471 | expect(err.message).to.include(`The 'typescript' option must be a valid path to your TypeScript installation. We could not find anything at the following path: /nothing/here`)
472 | })
473 | })
474 |
475 | it('throws error when typescript path and tsify plugin are specified', function () {
476 | this.options.browserifyOptions = {
477 | plugin: ['tsify'],
478 | }
479 |
480 | return this.run()
481 | .then(shouldntResolve)
482 | .catch((err) => {
483 | verifyErrorIncludesPrefix(err)
484 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_AND_TSIFY)
485 | expect(err.message).to.include(`It looks like you passed the 'typescript' option and also specified a browserify plugin for TypeScript. This may cause conflicts`)
486 | })
487 | })
488 |
489 | it('throws error when typescript path and tsify transform are specified', function () {
490 | this.options.browserifyOptions = {
491 | transform: [
492 | ['path/to/tsify', {}],
493 | ],
494 | }
495 |
496 | return this.run()
497 | .then(shouldntResolve)
498 | .catch((err) => {
499 | verifyErrorIncludesPrefix(err)
500 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_AND_TSIFY)
501 | expect(err.message).to.include(`It looks like you passed the 'typescript' option and also specified a browserify transform for TypeScript. This may cause conflicts`)
502 | })
503 | })
504 |
505 | it('throws error when processing .ts file and typescript option is not set', function () {
506 | this.options.typescript = undefined
507 | this.file.filePath = 'path/to/file.ts'
508 |
509 | return this.run()
510 | .then(shouldntResolve)
511 | .catch((err) => {
512 | verifyErrorIncludesPrefix(err)
513 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_NOT_CONFIGURED)
514 | expect(err.message).to.include(`You are attempting to preprocess a TypeScript file, but do not have TypeScript configured. Pass the 'typescript' option to enable TypeScript support`)
515 | expect(err.message).to.include('path/to/file.ts')
516 | })
517 | })
518 |
519 | it('throws error when processing .tsx file and typescript option is not set', function () {
520 | this.options.typescript = undefined
521 | this.file.filePath = 'path/to/file.tsx'
522 |
523 | return this.run()
524 | .then(shouldntResolve)
525 | .catch((err) => {
526 | verifyErrorIncludesPrefix(err)
527 | expect(err.type).to.equal(preprocessor.errorTypes.TYPESCRIPT_NOT_CONFIGURED)
528 | expect(err.message).to.include(`You are attempting to preprocess a TypeScript file, but do not have TypeScript configured. Pass the 'typescript' option to enable TypeScript support`)
529 | expect(err.message).to.include('path/to/file.tsx')
530 | })
531 | })
532 | })
533 | })
534 | })
535 | })
536 |
--------------------------------------------------------------------------------