├── .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 [![CircleCI](https://circleci.com/gh/cypress-io/cypress-browserify-preprocessor/tree/master.svg?style=svg)](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 | --------------------------------------------------------------------------------