├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── README.md ├── esdoc.json ├── package.json ├── src ├── browser-signal.js ├── cli.js ├── compile-cache.js ├── compiler-host.js ├── config-parser.js ├── custom-operators.js ├── digest-for-object.js ├── es6-shim.js ├── file-change-cache.js ├── for-all-files.js ├── index.js ├── initialize-renderer.js ├── live-reload.js ├── packager-cli.js ├── pathwatcher-rx.js ├── promise.js ├── protocol-hook.js ├── read-only-compiler.js ├── require-hook.js ├── rig-mime-types.js ├── sanitize-paths.js └── x-require.js ├── test ├── .eslintrc ├── compile-cache.js ├── compiler-host.js ├── compiler-valid-invalid.js ├── config-parser.js ├── electron-app │ ├── .compilerc │ ├── README.md │ ├── index.html │ ├── lib │ │ └── main.js │ ├── main.js │ ├── package.json │ └── src │ │ ├── main.coffee │ │ ├── utils │ │ └── encoder.coffee │ │ └── views │ │ └── form.coffee ├── electron-smoke-test-app.js ├── electron-smoke-test.html ├── electron-smoke-test.js ├── file-change-cache.js ├── fixtures │ ├── _partial.scss │ ├── alsobinary.webp │ ├── babelrc-noenv │ ├── babelrc-production │ ├── binaryfile.zip │ ├── compilerc-noenv │ ├── compilerc-passthrough │ ├── compilerc-production │ ├── dependency_a.less │ ├── dependency_a.sass │ ├── dependency_a.scss │ ├── dependency_a.styl │ ├── dependency_b.scss │ ├── file-with-dependencies.less │ ├── file-with-dependencies.sass │ ├── file-with-dependencies.scss │ ├── file-with-dependencies.styl │ ├── inline-valid-2.html │ ├── inline-valid-3.html │ ├── inline-valid.html │ ├── invalid.coffee │ ├── invalid.cson │ ├── invalid.css │ ├── invalid.graphql │ ├── invalid.jade │ ├── invalid.js │ ├── invalid.jsx │ ├── invalid.less │ ├── invalid.sass │ ├── invalid.scss │ ├── invalid.styl │ ├── invalid.ts │ ├── invalid.tsx │ ├── invalid.vue │ ├── minified.js │ ├── protourlrigging_1.html │ ├── protourlrigging_2.html │ ├── roboto.html │ ├── source_map.js │ ├── valid.coffee │ ├── valid.cson │ ├── valid.css │ ├── valid.graphql │ ├── valid.jade │ ├── valid.js │ ├── valid.jsx │ ├── valid.less │ ├── valid.sass │ ├── valid.scss │ ├── valid.styl │ ├── valid.ts │ ├── valid.tsx │ ├── valid.vue │ └── x-require-valid.html ├── inline-html-compiler.js ├── packager-cli.js ├── protocol-hook.js └── support.js └── types └── index.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["es2016-node5", "react"], 5 | "plugins": ["transform-async-to-generator", "array-includes", "istanbul"], 6 | "sourceMaps": "inline" 7 | }, 8 | "production": { 9 | "presets": ["es2016-node5", "react"], 10 | "plugins": ["transform-async-to-generator", "array-includes"], 11 | }, 12 | "development": { 13 | "presets": ["es2016-node5", "react"], 14 | "plugins": ["transform-async-to-generator", "array-includes"], 15 | "sourceMaps": "inline" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "strict": 0, 6 | "indent": [ 7 | 2, 8 | 2 9 | ], 10 | "semi": [ 11 | 2, 12 | "always" 13 | ], 14 | "no-console": 0 15 | }, 16 | "env": { 17 | "es6": true, 18 | "node": true, 19 | "browser": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | test/fixtures/* binary 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 27 | node_modules 28 | 29 | /lib 30 | /test-dist 31 | /docs 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "eqeqeq": true, 4 | "eqnull": true, 5 | "expr": true, 6 | "latedef": true, 7 | "onevar": true, 8 | "noarg": true, 9 | "node": true, 10 | "trailing": true, 11 | "undef": true, 12 | "unused": true, 13 | "browser": true 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test-dist/ 2 | test/ 3 | docs/ 4 | src/ 5 | .babelrc 6 | .editorconfig 7 | .eslintrc 8 | .jshintrc 9 | .npmignore 10 | .travis.yml 11 | *.md 12 | esdoc.json 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | 5 | sudo: false 6 | 7 | language: node_js 8 | 9 | node_js: 10 | - "6" 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | 16 | addons: 17 | apt: 18 | packages: 19 | - wine 20 | 21 | before_install: 22 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 23 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install wine; fi 24 | 25 | install: 26 | - npm i -g npm 27 | - npm install 28 | 29 | before_script: 30 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0 ; fi 31 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; sleep 3; fi 32 | 33 | script: 34 | - npm run compile 35 | - npm run test 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", 13 | "windows": { 14 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd" 15 | }, 16 | "runtimeArgs": [ 17 | "-u", 18 | "tdd", 19 | "--timeout", 20 | "999999", 21 | "--compilers", 22 | "js:babel-register", 23 | "--colors", 24 | "${workspaceRoot}/test/*.js" 25 | ], 26 | "internalConsoleOptions": "openOnSessionStart" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at paul@paulbetts.org. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DEPRECATED: electron-compile 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | This project is no longer maintained, pull requests are no longer being reviewed or merged and issues are no longer being responded to. 6 | 7 | --- 8 | 9 | ![](https://img.shields.io/npm/dm/electron-compile.svg) ![](http://electron.github.io/electron-compile/docs/badge.svg) 10 | 11 | electron-compile compiles JS and CSS on the fly with a single call in your app's 'ready' function. 12 | 13 | For JavaScript: 14 | 15 | * JavaScript ES6/ES7 (via Babel) 16 | * TypeScript 17 | * CoffeeScript 18 | * GraphQL 19 | 20 | For CSS: 21 | 22 | * Less 23 | * Sass / SCSS 24 | * Stylus 25 | 26 | For HTML: 27 | 28 | * Jade 29 | * Vue.js 2.0 Single-File Components 30 | 31 | For JSON: 32 | 33 | * CSON 34 | 35 | ### How does it work? (Easiest Way) 36 | 37 | Install `electron-prebuilt-compile` instead of the `electron`: 38 | ```sh 39 | npm install electron-prebuilt-compile --save-dev 40 | ``` 41 | and keep using electron as usual. 42 | 43 | Tada! You did it! 44 | 45 | ### Wait, seriously? 46 | 47 | Yeah. `electron-prebuilt-compile` is like an `electron` that Just Works with all of these languages above. 48 | 49 | ### How does it work? (Slightly Harder Way) 50 | 51 | First, add `electron-compile` and `electron-compilers` as a `devDependency`. 52 | 53 | ```sh 54 | npm install --save electron-compile 55 | npm install --save-dev electron-compilers 56 | ``` 57 | 58 | Create a new file that will be the entry point of your app (perhaps changing 'main' in package.json) - you need to pass in the root directory of your application, which will vary based on your setup. The root directory is the directory that your `package.json` is in. 59 | 60 | ```js 61 | // Assuming this file is ./src/es6-init.js 62 | var appRoot = path.join(__dirname, '..'); 63 | 64 | require('electron-compile').init(appRoot, require.resolve('./main')); 65 | ``` 66 | 67 | 68 | ### I did it, now what? 69 | 70 | From then on, you can now simply include files directly in your HTML, no need for cross-compilation: 71 | 72 | ```html 73 | 74 | 75 | 76 | 77 | ``` 78 | 79 | or just require them in: 80 | 81 | ```js 82 | require('./mylib') // mylib.ts 83 | ``` 84 | 85 | ### Live Reload / Hot Module Reloading 86 | 87 | In your main file, before you create a `BrowserWindow` instance: 88 | 89 | ```js 90 | import {enableLiveReload} from 'electron-compile'; 91 | 92 | enableLiveReload(); 93 | ``` 94 | 95 | #### React Hot Module Loading 96 | 97 | If you are using React, you can also enable Hot Module Reloading for both JavaScript JSX files as well as TypeScript, with a bit of setup: 98 | 99 | 1. `npm install --save react-hot-loader@next` 100 | 1. Call `enableLiveReload({strategy: 'react-hmr'});` in your main file, after `app.ready` (similar to above) 101 | 1. If you're using TypeScript, you're good out-of-the-box. If you're using JavaScript via Babel, add 'react-hot-loader/babel' to your plugins in `.compilerc`: 102 | 103 | ```json 104 | { 105 | "application/javascript": { 106 | "presets": ["react", "es2017-node7"], 107 | "plugins": ["react-hot-loader/babel", "transform-async-to-generator"] 108 | } 109 | } 110 | ``` 111 | 112 | 1. In your `index.html`, replace your initial call to `render`: 113 | 114 | Typical code without React HMR: 115 | 116 | ```js 117 | import * as React from 'react'; 118 | import * as ReactDOM from 'react-dom'; 119 | import { MyApp } from './my-app'; 120 | 121 | ReactDOM.render(, document.getElementById('app')); 122 | ``` 123 | 124 | Rewrite this as: 125 | 126 | ```js 127 | import * as React from 'react'; 128 | import * as ReactDOM from 'react-dom'; 129 | import { AppContainer } from 'react-hot-loader'; 130 | 131 | const render = () => { 132 | // NB: We have to re-require MyApp every time or else this won't work 133 | // We also need to wrap our app in the AppContainer class 134 | const MyApp = require('./myapp').MyApp; 135 | ReactDOM.render(, document.getElementById('app')); 136 | } 137 | 138 | render(); 139 | if (module.hot) { module.hot.accept(render); } 140 | ``` 141 | 142 | 143 | ### Something isn't working / I'm getting weird errors 144 | 145 | electron-compile uses the [debug module](https://github.com/visionmedia/debug), set the DEBUG environment variable to debug what electron-compile is doing: 146 | 147 | ```sh 148 | ## Debug just electron-compile 149 | DEBUG=electron-compile:* npm start 150 | 151 | ## Grab everything except for Babel which is very noisy 152 | DEBUG=*,-babel npm start 153 | ``` 154 | 155 | ### How do I set up (Babel / Less / whatever) the way I want? 156 | 157 | If you've got a `.babelrc` and that's all you want to customize, you can simply use it directly. electron-compile will respect it, even the environment-specific settings. If you want to customize other compilers, use a `.compilerc` or `.compilerc.json` file. Here's an example: 158 | 159 | ```json 160 | { 161 | "application/javascript": { 162 | "presets": ["es2016-node5", "react"], 163 | "sourceMaps": "inline" 164 | }, 165 | "text/less": { 166 | "dumpLineNumbers": "comments" 167 | } 168 | } 169 | ``` 170 | 171 | `.compilerc` also accepts environments with the same syntax as `.babelrc`: 172 | 173 | ```json 174 | { 175 | "env": { 176 | "development": { 177 | "application/javascript": { 178 | "presets": ["es2016-node5", "react"], 179 | "sourceMaps": "inline" 180 | }, 181 | "text/less": { 182 | "dumpLineNumbers": "comments" 183 | } 184 | }, 185 | "production": { 186 | "application/javascript": { 187 | "presets": ["es2016-node5", "react"], 188 | "sourceMaps": "none" 189 | } 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | The opening Object is a list of MIME Types, and options passed to the compiler implementation. These parameters are documented here: 196 | 197 | * Babel - http://babeljs.io/docs/usage/options 198 | * CoffeeScript - https://web.archive.org/web/20160110101607/http://coffeescript.org/documentation/docs/coffee-script.html#section-5 199 | * TypeScript - https://github.com/Microsoft/TypeScript/blob/v1.5.0-beta/bin/typescriptServices.d.ts#L1076 200 | * Less - http://lesscss.org/usage/index.html#command-line-usage-options 201 | * Jade - http://jade-lang.com/api 202 | 203 | ## How can I compile only some file types but not others? 204 | 205 | With `passthrough` enabled, electron-compile will return your source files completely unchanged! 206 | 207 | In this example `.compilerc`, JavaScript files won't be compiled: 208 | 209 | ```json 210 | { 211 | "application/javascript": { 212 | "passthrough": true 213 | }, 214 | "text/less": { 215 | "dumpLineNumbers": "comments" 216 | } 217 | } 218 | ``` 219 | 220 | ## How can I precompile my code for release-time? 221 | 222 | By *far*, the easiest way to do this is via using [electron-forge](https://github.com/electron-userland/electron-forge/). electron-forge handles every aspect of packaging your app on all platforms and helping you publish it. Unless you have a very good reason, you should be using it! 223 | 224 | ## How can I precompile my code for release-time? (the hard way) 225 | 226 | electron-compile comes with a command-line application to pre-create a cache for you. 227 | 228 | ```sh 229 | Usage: electron-compile --appdir [root-app-dir] paths... 230 | 231 | Options: 232 | -a, --appdir The top-level application directory (i.e. where your 233 | package.json is) 234 | -v, --verbose Print verbose information 235 | -h, --help Show help 236 | ``` 237 | 238 | Run `electron-compile` on all of your application assets, even if they aren't strictly code (i.e. your static assets like PNGs). electron-compile will recursively walk the given directories. 239 | 240 | ```sh 241 | electron-compile --appDir /path/to/my/app ./src ./static 242 | ``` 243 | 244 | ### But I use Grunt / Gulp / I want to do Something Interesting 245 | 246 | Compilation also has its own API, check out the [documentation](http://electron-userland.github.io/electron-compile/docs/) for more information. 247 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "includes": ["\\.(js|es6)$"], 5 | "excludes": ["\\.config\\.(js|es6)$"], 6 | "access": ["public", "protected"], 7 | "autoPrivate": true, 8 | "unexportIdentifier": false, 9 | "undocumentIdentifier": true, 10 | "builtinExternal": true, 11 | "index": "./README.md", 12 | "package": "./package.json", 13 | "coverage": true, 14 | "includeSource": true, 15 | "title": "electron-compilers", 16 | "plugins": [ 17 | {"name": "esdoc-es7-plugin"}, 18 | {"name": "esdoc-plugin-async-to-sync"} 19 | ], 20 | "test": { 21 | "type": "mocha", 22 | "source": "./test", 23 | "includes": ["\\.(js|es6)$"] 24 | }, 25 | "lint": true 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-compile", 3 | "version": "6.4.4", 4 | "description": "Electron supporting package to compile JS and CSS in Electron applications", 5 | "scripts": { 6 | "doc": "esdoc -c ./esdoc.json", 7 | "compile": "cross-env NODE_ENV='production' git clean -xdf lib && babel -d lib/ src", 8 | "prepublish": "npm run compile", 9 | "start": "npm run compile && electron ./test-dist/electron-smoke-test.js", 10 | "test": "mocha --compilers js:babel-register test/*.js", 11 | "test-cov": "cross-env NODE_ENV='test' istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-register test/*.js" 12 | }, 13 | "bin": { 14 | "electron-compile": "lib/cli.js", 15 | "electron-packager-compile": "lib/packager-cli.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/paulcbetts/electron-compile" 20 | }, 21 | "keywords": [ 22 | "electron" 23 | ], 24 | "author": "Paul Betts ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/paulcbetts/electron-compile/issues" 28 | }, 29 | "homepage": "https://github.com/paulcbetts/electron-compile", 30 | "main": "lib/index.js", 31 | "types": "types/index.d.ts", 32 | "engines": { 33 | "node": ">= 5.0" 34 | }, 35 | "dependencies": { 36 | "@paulcbetts/mime-types": "^2.1.10", 37 | "@types/node": "^7.0.12", 38 | "btoa": "^1.1.2", 39 | "debug": "^2.5.1", 40 | "lru-cache": "^4.0.1", 41 | "mkdirp": "^0.5.1", 42 | "pify": "^2.3.0", 43 | "rimraf": "^2.5.4", 44 | "rxjs": "^5.1.1", 45 | "spawn-rx": "^2.0.3", 46 | "yargs": "^4.8.1" 47 | }, 48 | "devDependencies": { 49 | "asar": "^0.12.1", 50 | "babel-cli": "^6.11.4", 51 | "babel-eslint": "^6.1.2", 52 | "babel-plugin-array-includes": "^2.0.3", 53 | "babel-plugin-istanbul": "^4.0.0", 54 | "babel-plugin-transform-async-to-generator": "^6.8.0", 55 | "babel-preset-es2016-node5": "^1.1.2", 56 | "babel-preset-react": "^6.11.1", 57 | "babel-register": "^6.11.6", 58 | "chai": "^3.5.0", 59 | "chai-as-promised": "^5.3.0", 60 | "cheerio": "^0.20.0", 61 | "cross-env": "^3.2.4", 62 | "electron-compilers": "^5.8.0", 63 | "electron-packager": "^7.5.1", 64 | "electron-prebuilt": "^1.3.3", 65 | "esdoc": "^0.4.8", 66 | "esdoc-es7-plugin": "0.0.3", 67 | "esdoc-plugin-async-to-sync": "^0.5.0", 68 | "eslint": "^3.3.0", 69 | "istanbul": "^0.4.5", 70 | "mocha": "^3.0.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/browser-signal.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs/Observable'; 2 | import {Subject} from 'rxjs/Subject'; 3 | 4 | import 'rxjs/add/observable/throw'; 5 | 6 | const isElectron = 'type' in process; 7 | const isBrowser = process.type === 'browser'; 8 | 9 | const ipc = !isElectron ? null : 10 | isBrowser ? require('electron').ipcMain : require('electron').ipcRenderer; 11 | 12 | const channelList = {}; 13 | 14 | export function send(channel, ...args) { 15 | if (isElectron && !isBrowser) { 16 | ipc.send(channel, ...args); 17 | return; 18 | } 19 | 20 | if (!(channel in channelList)) return; 21 | 22 | let { subj } = channelList[channel]; 23 | subj.next(args); 24 | } 25 | 26 | export function listen(channel) { 27 | if (isElectron && !isBrowser) return Observable.throw(new Error("Can only call listen from browser")); 28 | 29 | return Observable.create((s) => { 30 | if (!(channel in channelList)) { 31 | let subj = new Subject(); 32 | let ipcListener = (e, ...args) => { subj.next(args); }; 33 | 34 | channelList[channel] = { subj, refcount: 0 }; 35 | if (isElectron && isBrowser) { 36 | ipc.on(channel, ipcListener); 37 | channelList[channel].listener = ipcListener; 38 | } 39 | } 40 | 41 | channelList[channel].refcount++; 42 | 43 | let disp = channelList[channel].subj.subscribe(s); 44 | disp.add(() => { 45 | channelList[channel].refcount--; 46 | if (channelList[channel].refcount > 0) return; 47 | 48 | if (channelList[channel].listener) { 49 | ipc.removeListener(channel, channelList[channel].listener); 50 | } 51 | 52 | delete channelList.channel; 53 | }); 54 | 55 | return disp; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import mkdirp from 'mkdirp'; 5 | 6 | import {createCompilerHostFromProjectRoot} from './config-parser'; 7 | import {forAllFiles} from './for-all-files'; 8 | 9 | process.on('unhandledRejection', (e) => { 10 | d(e.message || e); 11 | d(e.stack || ''); 12 | }); 13 | 14 | process.on('uncaughtException', (e) => { 15 | d(e.message || e); 16 | d(e.stack || ''); 17 | }); 18 | 19 | export async function main(appDir, sourceDirs, cacheDir, sourceMapDir) { 20 | let compilerHost = null; 21 | if (!cacheDir || cacheDir.length < 1) { 22 | cacheDir = '.cache'; 23 | } 24 | 25 | let rootCacheDir = path.join(appDir, cacheDir); 26 | mkdirp.sync(rootCacheDir); 27 | let mapDir = rootCacheDir; 28 | 29 | if (sourceMapDir) { 30 | mapDir = path.join(appDir, sourceMapDir); 31 | d(`specifed separate source map dir at ${mapDir}, creating it`); 32 | mkdirp.sync(mapDir); 33 | } 34 | 35 | if (process.env.NODE_ENV !== 'production') { 36 | console.log(`Using NODE_ENV = ${process.env.NODE_ENV || 'development'}`); 37 | } 38 | 39 | d(`main: ${appDir}, ${JSON.stringify(sourceDirs)}`); 40 | try { 41 | compilerHost = await createCompilerHostFromProjectRoot(appDir, rootCacheDir, sourceMapDir); 42 | } catch (e) { 43 | console.error(`Couldn't set up compilers: ${e.message}`); 44 | d(e.stack); 45 | 46 | throw e; 47 | } 48 | 49 | await Promise.all(sourceDirs.map((dir) => forAllFiles(dir, async (f) => { 50 | try { 51 | d(`Starting compilation for ${f}`); 52 | await compilerHost.compile(f); 53 | } catch (e) { 54 | console.error(`Failed to compile file: ${f}`); 55 | console.error(e.message); 56 | 57 | d(e.stack); 58 | } 59 | }))); 60 | 61 | d('Saving out configuration'); 62 | await compilerHost.saveConfiguration(); 63 | } 64 | 65 | const d = require('debug')('electron-compile'); 66 | 67 | const yargs = require('yargs') 68 | .usage('Usage: electron-compile --appdir [root-app-dir] paths...') 69 | .alias('a', 'appdir') 70 | .describe('a', 'The top-level application directory (i.e. where your package.json is)') 71 | .default('a', process.cwd()) 72 | .alias('c', 'cachedir') 73 | .describe('c', 'The directory to put the cache') 74 | .alias('s', 'sourcemapdir') 75 | .describe('s', 'The directory to store sourcemap if compiler configured to have sourcemap file. Default to cachedir if not specified.') 76 | .help('h') 77 | .alias('h', 'help') 78 | .epilog('Copyright 2015'); 79 | 80 | if (process.mainModule === module) { 81 | const argv = yargs.argv; 82 | 83 | if (!argv._ || argv._.length < 1) { 84 | yargs.showHelp(); 85 | process.exit(-1); 86 | } 87 | 88 | const sourceDirs = argv._; 89 | const appDir = argv.a; 90 | const cacheDir = argv.c; 91 | const sourceMapDir = argv.s; 92 | 93 | main(appDir, sourceDirs, cacheDir, sourceMapDir) 94 | .then(() => process.exit(0)) 95 | .catch((e) => { 96 | console.error(e.message || e); 97 | d(e.stack); 98 | 99 | console.error("Compilation failed!\nFor extra information, set the DEBUG environment variable to '*'"); 100 | process.exit(-1); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/compile-cache.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import zlib from 'zlib'; 4 | import createDigestForObject from './digest-for-object'; 5 | import {pfs, pzlib} from './promise'; 6 | import mkdirp from 'mkdirp'; 7 | 8 | const d = require('debug')('electron-compile:compile-cache'); 9 | 10 | /** 11 | * CompileCache manages getting and setting entries for a single compiler; each 12 | * in-use compiler will have an instance of this class, usually created via 13 | * {@link createFromCompiler}. 14 | * 15 | * You usually will not use this class directly, it is an implementation class 16 | * for {@link CompileHost}. 17 | */ 18 | export default class CompileCache { 19 | /** 20 | * Creates an instance, usually used for testing only. 21 | * 22 | * @param {string} cachePath The root directory to use as a cache path 23 | * 24 | * @param {FileChangedCache} fileChangeCache A file-change cache that is 25 | * optionally pre-loaded. 26 | * @param {string} sourceMapPath The directory to store sourcemap separately if compiler option enabled to emit. 27 | * Default to cachePath if not specified. 28 | */ 29 | constructor(cachePath, fileChangeCache, sourceMapPath = null) { 30 | this.cachePath = cachePath; 31 | this.fileChangeCache = fileChangeCache; 32 | this.sourceMapPath = sourceMapPath || this.cachePath; 33 | } 34 | 35 | /** 36 | * Creates a CompileCache from a class compatible with the CompilerBase 37 | * interface. This method uses the compiler name / version / options to 38 | * generate a unique directory name for cached results 39 | * 40 | * @param {string} cachePath The root path to use for the cache, a directory 41 | * representing the hash of the compiler parameters 42 | * will be created here. 43 | * 44 | * @param {CompilerBase} compiler The compiler to use for version / option 45 | * information. 46 | * 47 | * @param {FileChangedCache} fileChangeCache A file-change cache that is 48 | * optionally pre-loaded. 49 | * 50 | * @param {boolean} readOnlyMode Don't attempt to create the cache directory. 51 | * 52 | * @param {string} sourceMapPath The directory to store sourcemap separately if compiler option enabled to emit. 53 | * Default to cachePath if not specified. 54 | * 55 | * @return {CompileCache} A configured CompileCache instance. 56 | */ 57 | static createFromCompiler(cachePath, compiler, fileChangeCache, readOnlyMode = false, sourceMapPath = null) { 58 | let newCachePath = null; 59 | let getCachePath = () => { 60 | if (newCachePath) return newCachePath; 61 | 62 | const digestObj = { 63 | name: compiler.name || Object.getPrototypeOf(compiler).constructor.name, 64 | version: compiler.getCompilerVersion(), 65 | options: compiler.compilerOptions 66 | }; 67 | 68 | newCachePath = path.join(cachePath, createDigestForObject(digestObj)); 69 | 70 | d(`Path for ${digestObj.name}: ${newCachePath}`); 71 | d(`Set up with parameters: ${JSON.stringify(digestObj)}`); 72 | 73 | if (!readOnlyMode) mkdirp.sync(newCachePath); 74 | return newCachePath; 75 | }; 76 | 77 | let ret = new CompileCache('', fileChangeCache); 78 | ret.getCachePath = getCachePath; 79 | 80 | const newSourceMapPath = sourceMapPath; 81 | ret.getSourceMapPath = () => newSourceMapPath || getCachePath(); 82 | 83 | return ret; 84 | } 85 | 86 | /** 87 | * Returns a file's compiled contents from the cache. 88 | * 89 | * @param {string} filePath The path to the file. FileChangedCache will look 90 | * up the hash and use that as the key in the cache. 91 | * 92 | * @return {Promise} An object with all kinds of information 93 | * 94 | * @property {Object} hashInfo The hash information returned from getHashForPath 95 | * @property {string} code The source code if the file was a text file 96 | * @property {Buffer} binaryData The file if it was a binary file 97 | * @property {string} mimeType The MIME type saved in the cache. 98 | * @property {string[]} dependentFiles The dependent files returned from 99 | * compiling the file, if any. 100 | */ 101 | async get(filePath) { 102 | d(`Fetching ${filePath} from cache`); 103 | let hashInfo = await this.fileChangeCache.getHashForPath(path.resolve(filePath)); 104 | 105 | let code = null; 106 | let mimeType = null; 107 | let binaryData = null; 108 | let dependentFiles = null; 109 | 110 | let cacheFile = null; 111 | try { 112 | cacheFile = path.join(this.getCachePath(), hashInfo.hash); 113 | let result = null; 114 | 115 | if (hashInfo.isFileBinary) { 116 | d("File is binary, reading out info"); 117 | let info = JSON.parse(await pfs.readFile(cacheFile + '.info')); 118 | mimeType = info.mimeType; 119 | dependentFiles = info.dependentFiles; 120 | 121 | binaryData = hashInfo.binaryData; 122 | if (!binaryData) { 123 | binaryData = await pfs.readFile(cacheFile); 124 | binaryData = await pzlib.gunzip(binaryData); 125 | } 126 | } else { 127 | let buf = await pfs.readFile(cacheFile); 128 | let str = (await pzlib.gunzip(buf)).toString('utf8'); 129 | 130 | result = JSON.parse(str); 131 | code = result.code; 132 | mimeType = result.mimeType; 133 | dependentFiles = result.dependentFiles; 134 | } 135 | } catch (e) { 136 | d(`Failed to read cache for ${filePath}, looked in ${cacheFile}: ${e.message}`); 137 | } 138 | 139 | return { hashInfo, code, mimeType, binaryData, dependentFiles }; 140 | } 141 | 142 | 143 | /** 144 | * Saves a compiled result to cache 145 | * 146 | * @param {Object} hashInfo The hash information returned from getHashForPath 147 | * 148 | * @param {string / Buffer} codeOrBinaryData The file's contents, either as 149 | * a string or a Buffer. 150 | * @param {string} mimeType The MIME type returned by the compiler. 151 | * 152 | * @param {string[]} dependentFiles The list of dependent files returned by 153 | * the compiler. 154 | * @return {Promise} Completion. 155 | */ 156 | async save(hashInfo, codeOrBinaryData, mimeType, dependentFiles) { 157 | let buf = null; 158 | let target = path.join(this.getCachePath(), hashInfo.hash); 159 | d(`Saving to ${target}`); 160 | 161 | if (hashInfo.isFileBinary) { 162 | buf = await pzlib.gzip(codeOrBinaryData); 163 | await pfs.writeFile(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8'); 164 | } else { 165 | buf = await pzlib.gzip(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles}))); 166 | } 167 | 168 | await pfs.writeFile(target, buf); 169 | } 170 | 171 | /** 172 | * Attempts to first get a key via {@link get}, then if it fails, call a method 173 | * to retrieve the contents, then save the result to cache. 174 | * 175 | * The fetcher parameter is expected to have the signature: 176 | * 177 | * Promise fetcher(filePath : string, hashInfo : Object); 178 | * 179 | * hashInfo is a value returned from getHashForPath 180 | * The return value of fetcher must be an Object with the properties: 181 | * 182 | * mimeType - the MIME type of the data to save 183 | * code (optional) - the source code as a string, if file is text 184 | * binaryData (optional) - the file contents as a Buffer, if file is binary 185 | * dependentFiles - the dependent files returned by the compiler. 186 | * 187 | * @param {string} filePath The path to the file. FileChangedCache will look 188 | * up the hash and use that as the key in the cache. 189 | * 190 | * @param {Function} fetcher A method which conforms to the description above. 191 | * 192 | * @return {Promise} An Object which has the same fields as the 193 | * {@link get} method return result. 194 | */ 195 | async getOrFetch(filePath, fetcher) { 196 | let cacheResult = await this.get(filePath); 197 | let anyDependenciesChanged = await this.haveAnyDependentFilesChanged(cacheResult); 198 | 199 | if ((cacheResult.code || cacheResult.binaryData) && !anyDependenciesChanged) { 200 | return cacheResult; 201 | } 202 | 203 | let result = await fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo }; 204 | 205 | if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) { 206 | d(`Cache miss: saving out info for ${filePath}`); 207 | await this.save(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles); 208 | 209 | const map = result.sourceMaps; 210 | if (map) { 211 | d(`source map for ${filePath} found, saving it to ${this.getSourceMapPath()}`); 212 | await this.saveSourceMap(cacheResult.hashInfo, filePath, map); 213 | } 214 | } 215 | 216 | result.hashInfo = cacheResult.hashInfo; 217 | return result; 218 | } 219 | 220 | /** 221 | * @private Check if any of a file's dependencies have changed 222 | */ 223 | async haveAnyDependentFilesChanged(cacheResult) { 224 | if (!cacheResult.code || !cacheResult.dependentFiles.length) return false; 225 | 226 | for (let dependentFile of cacheResult.dependentFiles) { 227 | let hasFileChanged = await this.fileChangeCache.hasFileChanged(dependentFile); 228 | if (hasFileChanged) { 229 | return true; 230 | } 231 | 232 | let dependentFileCacheResult = await this.get(dependentFile); 233 | if (dependentFileCacheResult.dependentFiles && dependentFileCacheResult.dependentFiles.length) { 234 | let anySubdependentFilesChanged = await this.haveAnyDependentFilesChanged(dependentFileCacheResult); 235 | if (anySubdependentFilesChanged) return true; 236 | } 237 | } 238 | 239 | return false; 240 | } 241 | 242 | 243 | getSync(filePath) { 244 | d(`Fetching ${filePath} from cache`); 245 | let hashInfo = this.fileChangeCache.getHashForPathSync(path.resolve(filePath)); 246 | 247 | let code = null; 248 | let mimeType = null; 249 | let binaryData = null; 250 | let dependentFiles = null; 251 | 252 | try { 253 | let cacheFile = path.join(this.getCachePath(), hashInfo.hash); 254 | 255 | let result = null; 256 | if (hashInfo.isFileBinary) { 257 | d("File is binary, reading out info"); 258 | let info = JSON.parse(fs.readFileSync(cacheFile + '.info')); 259 | mimeType = info.mimeType; 260 | dependentFiles = info.dependentFiles; 261 | 262 | binaryData = hashInfo.binaryData; 263 | if (!binaryData) { 264 | binaryData = fs.readFileSync(cacheFile); 265 | binaryData = zlib.gunzipSync(binaryData); 266 | } 267 | } else { 268 | let buf = fs.readFileSync(cacheFile); 269 | let str = (zlib.gunzipSync(buf)).toString('utf8'); 270 | 271 | result = JSON.parse(str); 272 | code = result.code; 273 | mimeType = result.mimeType; 274 | dependentFiles = result.dependentFiles; 275 | } 276 | } catch (e) { 277 | d(`Failed to read cache for ${filePath}`); 278 | } 279 | 280 | return { hashInfo, code, mimeType, binaryData, dependentFiles }; 281 | } 282 | 283 | saveSync(hashInfo, codeOrBinaryData, mimeType, dependentFiles) { 284 | let buf = null; 285 | let target = path.join(this.getCachePath(), hashInfo.hash); 286 | d(`Saving to ${target}`); 287 | 288 | if (hashInfo.isFileBinary) { 289 | buf = zlib.gzipSync(codeOrBinaryData); 290 | fs.writeFileSync(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8'); 291 | } else { 292 | buf = zlib.gzipSync(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles}))); 293 | } 294 | 295 | fs.writeFileSync(target, buf); 296 | } 297 | 298 | getOrFetchSync(filePath, fetcher) { 299 | let cacheResult = this.getSync(filePath); 300 | if (cacheResult.code || cacheResult.binaryData) return cacheResult; 301 | 302 | let result = fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo }; 303 | 304 | if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) { 305 | d(`Cache miss: saving out info for ${filePath}`); 306 | this.saveSync(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles); 307 | } 308 | 309 | const map = result.sourceMaps; 310 | if (map) { 311 | d(`source map for ${filePath} found, saving it to ${this.getSourceMapPath()}`); 312 | this.saveSourceMapSync(cacheResult.hashInfo, filePath, map); 313 | } 314 | 315 | result.hashInfo = cacheResult.hashInfo; 316 | return result; 317 | } 318 | 319 | buildSourceMapTarget(hashInfo, filePath) { 320 | const fileName = path.basename(filePath); 321 | const mapFileName = fileName.replace(path.extname(fileName), '.js.map'); 322 | 323 | const target = path.join(this.getSourceMapPath(), mapFileName); 324 | d(`Sourcemap target is: ${target}`); 325 | 326 | return target; 327 | } 328 | 329 | /** 330 | * Saves sourcemap string into cache, or specified separate dir 331 | * 332 | * @param {Object} hashInfo The hash information returned from getHashForPath 333 | * 334 | * @param {string} filePath Path to original file to construct sourcemap file name 335 | 336 | * @param {string} sourceMap Sourcemap data as string 337 | * 338 | * @memberOf CompileCache 339 | */ 340 | async saveSourceMap(hashInfo, filePath, sourceMap) { 341 | const target = this.buildSourceMapTarget(hashInfo, filePath); 342 | await pfs.writeFile(target, sourceMap, 'utf-8'); 343 | } 344 | 345 | saveSourceMapSync(hashInfo, filePath, sourceMap) { 346 | const target = this.buildSourceMapTarget(hashInfo, filePath); 347 | fs.writeFileSync(target, sourceMap, 'utf-8'); 348 | } 349 | 350 | /** 351 | * @private 352 | */ 353 | getCachePath() { 354 | // NB: This is an evil hack so that createFromCompiler can stomp it 355 | // at will 356 | return this.cachePath; 357 | } 358 | 359 | /** 360 | * @private 361 | */ 362 | getSourceMapPath() { 363 | return this.sourceMapPath; 364 | } 365 | 366 | /** 367 | * Returns whether a file should not be compiled. Note that this doesn't 368 | * necessarily mean it won't end up in the cache, only that its contents are 369 | * saved verbatim instead of trying to find an appropriate compiler. 370 | * 371 | * @param {Object} hashInfo The hash information returned from getHashForPath 372 | * 373 | * @return {boolean} True if a file should be ignored 374 | */ 375 | static shouldPassthrough(hashInfo) { 376 | return hashInfo.isMinified || hashInfo.isInNodeModules || hashInfo.hasSourceMap || hashInfo.isFileBinary; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/custom-operators.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs/Observable'; 2 | import {async} from 'rxjs/scheduler/async'; 3 | 4 | import 'rxjs/add/observable/range'; 5 | import 'rxjs/add/observable/throw'; 6 | import 'rxjs/add/observable/timer'; 7 | 8 | import 'rxjs/add/operator/mergeMap'; 9 | import 'rxjs/add/operator/map'; 10 | import 'rxjs/add/operator/retryWhen'; 11 | import 'rxjs/add/operator/switch'; 12 | import 'rxjs/add/operator/zip'; 13 | 14 | function retryWithDelayOrError(errors, maxRetries) { 15 | return Observable.range(1, maxRetries + 1) 16 | .zip(errors, (i, e) => { 17 | return { attempts: i, error: e }; 18 | }) 19 | .mergeMap(({attempts, error}) => { 20 | return attempts <= maxRetries ? 21 | Observable.timer(attempts * 1000) : 22 | Observable.throw(error); 23 | }); 24 | } 25 | 26 | const newCoolOperators = { 27 | guaranteedThrottle: function(time, scheduler = async) { 28 | return this 29 | .map((x) => Observable.timer(time, scheduler).map(() => x)) 30 | .switch(); 31 | }, 32 | 33 | retryAtIntervals: function(maxRetries = 3) { 34 | return this.retryWhen((errors) => retryWithDelayOrError(errors, maxRetries)); 35 | }, 36 | }; 37 | 38 | 39 | for (const key of Object.keys(newCoolOperators)) { 40 | Observable.prototype[key] = newCoolOperators[key]; 41 | } 42 | -------------------------------------------------------------------------------- /src/digest-for-object.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | function updateDigestForJsonValue(shasum, value) { 4 | // Implmentation is similar to that of pretty-printing a JSON object, except: 5 | // * Strings are not escaped. 6 | // * No effort is made to avoid trailing commas. 7 | // These shortcuts should not affect the correctness of this function. 8 | const type = typeof(value); 9 | 10 | if (type === 'string') { 11 | shasum.update('"', 'utf8'); 12 | shasum.update(value, 'utf8'); 13 | shasum.update('"', 'utf8'); 14 | return; 15 | } 16 | 17 | if (type === 'boolean' || type === 'number') { 18 | shasum.update(value.toString(), 'utf8'); 19 | return; 20 | } 21 | 22 | if (!value) { 23 | shasum.update('null', 'utf8'); 24 | return; 25 | } 26 | 27 | if (Array.isArray(value)) { 28 | shasum.update('[', 'utf8'); 29 | for (let i=0; i < value.length; i++) { 30 | updateDigestForJsonValue(shasum, value[i]); 31 | shasum.update(',', 'utf8'); 32 | } 33 | shasum.update(']', 'utf8'); 34 | return; 35 | } 36 | 37 | // value must be an object: be sure to sort the keys. 38 | let keys = Object.keys(value); 39 | keys.sort(); 40 | 41 | shasum.update('{', 'utf8'); 42 | 43 | for (let i=0; i < keys.length; i++) { 44 | updateDigestForJsonValue(shasum, keys[i]); 45 | shasum.update(': ', 'utf8'); 46 | updateDigestForJsonValue(shasum, value[keys[i]]); 47 | shasum.update(',', 'utf8'); 48 | } 49 | 50 | shasum.update('}', 'utf8'); 51 | } 52 | 53 | 54 | /** 55 | * Creates a hash from a JS object 56 | * 57 | * @private 58 | */ 59 | export default function createDigestForObject(obj) { 60 | let sha1 = crypto.createHash('sha1'); 61 | updateDigestForJsonValue(sha1, obj); 62 | 63 | return sha1.digest('hex'); 64 | } 65 | -------------------------------------------------------------------------------- /src/es6-shim.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var electronCompile = require('electron-compile'); 3 | 4 | var packageJson = require('./package.json'); 5 | let initScript = path.resolve(__dirname, packageJson.originalMain); 6 | electronCompile.init(__dirname, initScript); 7 | -------------------------------------------------------------------------------- /src/file-change-cache.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import zlib from 'zlib'; 3 | import crypto from 'crypto'; 4 | import {pfs, pzlib} from './promise'; 5 | import sanitizeFilePath from './sanitize-paths'; 6 | 7 | const d = require('debug')('electron-compile:file-change-cache'); 8 | 9 | /** 10 | * This class caches information about files and determines whether they have 11 | * changed contents or not. Most importantly, this class caches the hash of seen 12 | * files so that at development time, we don't have to recalculate them constantly. 13 | * 14 | * This class is also the core of how electron-compile runs quickly in production 15 | * mode - after precompilation, the cache is serialized along with the rest of the 16 | * data in {@link CompilerHost}, so that when we load the app in production mode, 17 | * we don't end up calculating hashes of file content at all, only using the contents 18 | * of this cache. 19 | */ 20 | export default class FileChangedCache { 21 | constructor(appRoot, failOnCacheMiss=false) { 22 | this.appRoot = sanitizeFilePath(appRoot); 23 | 24 | this.failOnCacheMiss = failOnCacheMiss; 25 | this.changeCache = {}; 26 | } 27 | 28 | static removePrefix(needle, haystack) { 29 | let idx = haystack.toLowerCase().indexOf(needle.toLowerCase()); 30 | if (idx < 0) return haystack; 31 | 32 | return haystack.substring(idx + needle.length); 33 | } 34 | 35 | /** 36 | * Allows you to create a FileChangedCache from serialized data saved from 37 | * {@link getSavedData}. 38 | * 39 | * @param {Object} data Saved data from getSavedData. 40 | * 41 | * @param {string} appRoot The top-level directory for your application (i.e. 42 | * the one which has your package.json). 43 | * 44 | * @param {boolean} failOnCacheMiss (optional) If True, cache misses will throw. 45 | * 46 | * @return {FileChangedCache} 47 | */ 48 | static loadFromData(data, appRoot, failOnCacheMiss=true) { 49 | let ret = new FileChangedCache(appRoot, failOnCacheMiss); 50 | ret.changeCache = data.changeCache; 51 | ret.originalAppRoot = data.appRoot; 52 | 53 | return ret; 54 | } 55 | 56 | 57 | /** 58 | * Allows you to create a FileChangedCache from serialized data saved from 59 | * {@link save}. 60 | * 61 | * @param {string} file Saved data from save. 62 | * 63 | * @param {string} appRoot The top-level directory for your application (i.e. 64 | * the one which has your package.json). 65 | * 66 | * @param {boolean} failOnCacheMiss (optional) If True, cache misses will throw. 67 | * 68 | * @return {Promise} 69 | */ 70 | static async loadFromFile(file, appRoot, failOnCacheMiss=true) { 71 | d(`Loading canned FileChangedCache from ${file}`); 72 | 73 | let buf = await pfs.readFile(file); 74 | return FileChangedCache.loadFromData(JSON.parse(await pzlib.gunzip(buf)), appRoot, failOnCacheMiss); 75 | } 76 | 77 | 78 | /** 79 | * Returns information about a given file, including its hash. This method is 80 | * the main method for this cache. 81 | * 82 | * @param {string} absoluteFilePath The path to a file to retrieve info on. 83 | * 84 | * @return {Promise} 85 | * 86 | * @property {string} hash The SHA1 hash of the file 87 | * @property {boolean} isMinified True if the file is minified 88 | * @property {boolean} isInNodeModules True if the file is in a library directory 89 | * @property {boolean} hasSourceMap True if the file has a source map 90 | * @property {boolean} isFileBinary True if the file is not a text file 91 | * @property {Buffer} binaryData (optional) The buffer that was read if the file 92 | * was binary and there was a cache miss. 93 | * @property {string} code (optional) The string that was read if the file 94 | * was text and there was a cache miss 95 | */ 96 | async getHashForPath(absoluteFilePath) { 97 | let {cacheEntry, cacheKey} = this.getCacheEntryForPath(absoluteFilePath); 98 | 99 | if (this.failOnCacheMiss) { 100 | return cacheEntry.info; 101 | } 102 | 103 | let {ctime, size} = await this.getInfoForCacheEntry(absoluteFilePath); 104 | 105 | if (cacheEntry) { 106 | let fileHasChanged = await this.hasFileChanged(absoluteFilePath, cacheEntry, {ctime, size}); 107 | 108 | if (!fileHasChanged) { 109 | return cacheEntry.info; 110 | } 111 | 112 | d(`Invalidating cache entry: ${cacheEntry.ctime} === ${ctime} && ${cacheEntry.size} === ${size}`); 113 | delete this.changeCache.cacheEntry; 114 | } 115 | 116 | let {digest, sourceCode, binaryData} = await this.calculateHashForFile(absoluteFilePath); 117 | 118 | let info = { 119 | hash: digest, 120 | isMinified: FileChangedCache.contentsAreMinified(sourceCode || ''), 121 | isInNodeModules: FileChangedCache.isInNodeModules(absoluteFilePath), 122 | hasSourceMap: FileChangedCache.hasSourceMap(sourceCode || ''), 123 | isFileBinary: !!binaryData 124 | }; 125 | 126 | this.changeCache[cacheKey] = { ctime, size, info }; 127 | d(`Cache entry for ${cacheKey}: ${JSON.stringify(this.changeCache[cacheKey])}`); 128 | 129 | if (binaryData) { 130 | return Object.assign({binaryData}, info); 131 | } else { 132 | return Object.assign({sourceCode}, info); 133 | } 134 | } 135 | 136 | async getInfoForCacheEntry(absoluteFilePath) { 137 | let stat = await pfs.stat(absoluteFilePath); 138 | if (!stat || !stat.isFile()) throw new Error(`Can't stat ${absoluteFilePath}`); 139 | 140 | return { 141 | stat, 142 | ctime: stat.ctime.getTime(), 143 | size: stat.size 144 | }; 145 | } 146 | 147 | /** 148 | * Gets the cached data for a file path, if it exists. 149 | * 150 | * @param {string} absoluteFilePath The path to a file to retrieve info on. 151 | * 152 | * @return {Object} 153 | */ 154 | getCacheEntryForPath(absoluteFilePath) { 155 | let cacheKey = sanitizeFilePath(absoluteFilePath); 156 | if (this.appRoot) { 157 | cacheKey = cacheKey.replace(this.appRoot, ''); 158 | } 159 | 160 | // NB: We do this because x-require will include an absolute path from the 161 | // original built app and we need to still grok it 162 | if (this.originalAppRoot) { 163 | cacheKey = cacheKey.replace(this.originalAppRoot, ''); 164 | } 165 | 166 | let cacheEntry = this.changeCache[cacheKey]; 167 | 168 | if (this.failOnCacheMiss) { 169 | if (!cacheEntry) { 170 | d(`Tried to read file cache entry for ${absoluteFilePath}`); 171 | d(`cacheKey: ${cacheKey}, appRoot: ${this.appRoot}, originalAppRoot: ${this.originalAppRoot}`); 172 | throw new Error(`Asked for ${absoluteFilePath} but it was not precompiled!`); 173 | } 174 | } 175 | 176 | return {cacheEntry, cacheKey}; 177 | } 178 | 179 | /** 180 | * Checks the file cache to see if a file has changed. 181 | * 182 | * @param {string} absoluteFilePath The path to a file to retrieve info on. 183 | * @param {Object} cacheEntry Cache data from {@link getCacheEntryForPath} 184 | * 185 | * @return {boolean} 186 | */ 187 | async hasFileChanged(absoluteFilePath, cacheEntry=null, fileHashInfo=null) { 188 | cacheEntry = cacheEntry || this.getCacheEntryForPath(absoluteFilePath).cacheEntry; 189 | fileHashInfo = fileHashInfo || await this.getInfoForCacheEntry(absoluteFilePath); 190 | 191 | if (cacheEntry) { 192 | return !(cacheEntry.ctime >= fileHashInfo.ctime && cacheEntry.size === fileHashInfo.size); 193 | } 194 | 195 | return false; 196 | } 197 | 198 | /** 199 | * Returns data that can passed to {@link loadFromData} to rehydrate this cache. 200 | * 201 | * @return {Object} 202 | */ 203 | getSavedData() { 204 | return { changeCache: this.changeCache, appRoot: this.appRoot }; 205 | } 206 | 207 | /** 208 | * Serializes this object's data to a file. 209 | * 210 | * @param {string} filePath The path to save data to. 211 | * 212 | * @return {Promise} Completion. 213 | */ 214 | async save(filePath) { 215 | let toSave = this.getSavedData(); 216 | 217 | let buf = await pzlib.gzip(new Buffer(JSON.stringify(toSave))); 218 | await pfs.writeFile(filePath, buf); 219 | } 220 | 221 | async calculateHashForFile(absoluteFilePath) { 222 | let buf = await pfs.readFile(absoluteFilePath); 223 | let encoding = FileChangedCache.detectFileEncoding(buf); 224 | 225 | if (!encoding) { 226 | let digest = crypto.createHash('sha1').update(buf).digest('hex'); 227 | return { sourceCode: null, digest, binaryData: buf }; 228 | } 229 | 230 | let sourceCode = await pfs.readFile(absoluteFilePath, encoding); 231 | let digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex'); 232 | 233 | return {sourceCode, digest, binaryData: null }; 234 | } 235 | 236 | getHashForPathSync(absoluteFilePath) { 237 | let cacheKey = sanitizeFilePath(absoluteFilePath); 238 | 239 | if (this.appRoot) { 240 | cacheKey = FileChangedCache.removePrefix(this.appRoot, cacheKey); 241 | } 242 | 243 | // NB: We do this because x-require will include an absolute path from the 244 | // original built app and we need to still grok it 245 | if (this.originalAppRoot) { 246 | cacheKey = FileChangedCache.removePrefix(this.originalAppRoot, cacheKey); 247 | } 248 | 249 | let cacheEntry = this.changeCache[cacheKey]; 250 | 251 | if (this.failOnCacheMiss) { 252 | if (!cacheEntry) { 253 | d(`Tried to read file cache entry for ${absoluteFilePath}`); 254 | d(`cacheKey: ${cacheKey}, appRoot: ${this.appRoot}, originalAppRoot: ${this.originalAppRoot}`); 255 | throw new Error(`Asked for ${absoluteFilePath} but it was not precompiled!`); 256 | } 257 | 258 | return cacheEntry.info; 259 | } 260 | 261 | let stat = fs.statSync(absoluteFilePath); 262 | let ctime = stat.ctime.getTime(); 263 | let size = stat.size; 264 | if (!stat || !stat.isFile()) throw new Error(`Can't stat ${absoluteFilePath}`); 265 | 266 | if (cacheEntry) { 267 | if (cacheEntry.ctime >= ctime && cacheEntry.size === size) { 268 | return cacheEntry.info; 269 | } 270 | 271 | d(`Invalidating cache entry: ${cacheEntry.ctime} === ${ctime} && ${cacheEntry.size} === ${size}`); 272 | delete this.changeCache.cacheEntry; 273 | } 274 | 275 | let {digest, sourceCode, binaryData} = this.calculateHashForFileSync(absoluteFilePath); 276 | 277 | let info = { 278 | hash: digest, 279 | isMinified: FileChangedCache.contentsAreMinified(sourceCode || ''), 280 | isInNodeModules: FileChangedCache.isInNodeModules(absoluteFilePath), 281 | hasSourceMap: FileChangedCache.hasSourceMap(sourceCode || ''), 282 | isFileBinary: !!binaryData 283 | }; 284 | 285 | this.changeCache[cacheKey] = { ctime, size, info }; 286 | d(`Cache entry for ${cacheKey}: ${JSON.stringify(this.changeCache[cacheKey])}`); 287 | 288 | if (binaryData) { 289 | return Object.assign({binaryData}, info); 290 | } else { 291 | return Object.assign({sourceCode}, info); 292 | } 293 | } 294 | 295 | saveSync(filePath) { 296 | let toSave = this.getSavedData(); 297 | 298 | let buf = zlib.gzipSync(new Buffer(JSON.stringify(toSave))); 299 | fs.writeFileSync(filePath, buf); 300 | } 301 | 302 | calculateHashForFileSync(absoluteFilePath) { 303 | let buf = fs.readFileSync(absoluteFilePath); 304 | let encoding = FileChangedCache.detectFileEncoding(buf); 305 | 306 | if (!encoding) { 307 | let digest = crypto.createHash('sha1').update(buf).digest('hex'); 308 | return { sourceCode: null, digest, binaryData: buf}; 309 | } 310 | 311 | let sourceCode = fs.readFileSync(absoluteFilePath, encoding); 312 | let digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex'); 313 | 314 | return {sourceCode, digest, binaryData: null}; 315 | } 316 | 317 | 318 | /** 319 | * Determines via some statistics whether a file is likely to be minified. 320 | * 321 | * @private 322 | */ 323 | static contentsAreMinified(source) { 324 | let length = source.length; 325 | if (length > 1024) length = 1024; 326 | 327 | let newlineCount = 0; 328 | 329 | // Roll through the characters and determine the average line length 330 | for(let i=0; i < source.length; i++) { 331 | if (source[i] === '\n') newlineCount++; 332 | } 333 | 334 | // No Newlines? Any file other than a super small one is minified 335 | if (newlineCount === 0) { 336 | return (length > 80); 337 | } 338 | 339 | let avgLineLength = length / newlineCount; 340 | return (avgLineLength > 80); 341 | } 342 | 343 | 344 | /** 345 | * Determines whether a path is in node_modules or the Electron init code 346 | * 347 | * @private 348 | */ 349 | static isInNodeModules(filePath) { 350 | return !!(filePath.match(/(node_modules|bower_components)[\\\/]/i) || filePath.match(/(atom|electron)\.asar/)); 351 | } 352 | 353 | 354 | /** 355 | * Returns whether a file has an inline source map 356 | * 357 | * @private 358 | */ 359 | static hasSourceMap(sourceCode) { 360 | const trimmed = sourceCode.trim(); 361 | return trimmed.lastIndexOf('//# sourceMap') > trimmed.lastIndexOf('\n'); 362 | } 363 | 364 | /** 365 | * Determines the encoding of a file from the two most common encodings by trying 366 | * to decode it then looking for encoding errors 367 | * 368 | * @private 369 | */ 370 | static detectFileEncoding(buffer) { 371 | if (buffer.length < 1) return false; 372 | let buf = (buffer.length < 4096 ? buffer : buffer.slice(0, 4096)); 373 | 374 | const encodings = ['utf8', 'utf16le']; 375 | 376 | let encoding; 377 | if (buffer.length <= 128) { 378 | encoding = encodings.find(x => 379 | Buffer.compare(new Buffer(buffer.toString(), x), buffer) === 0 380 | ); 381 | } else { 382 | encoding = encodings.find(x => !FileChangedCache.containsControlCharacters(buf.toString(x))); 383 | } 384 | 385 | return encoding; 386 | } 387 | 388 | /** 389 | * Determines whether a string is likely to be poorly encoded by looking for 390 | * control characters above a certain threshold 391 | * 392 | * @private 393 | */ 394 | static containsControlCharacters(str) { 395 | let controlCount = 0; 396 | let spaceCount = 0; 397 | let threshold = 2; 398 | if (str.length > 64) threshold = 4; 399 | if (str.length > 512) threshold = 8; 400 | 401 | for (let i=0; i < str.length; i++) { 402 | let c = str.charCodeAt(i); 403 | if (c === 65536 || c < 8) controlCount++; 404 | if (c > 14 && c < 32) controlCount++; 405 | if (c === 32) spaceCount++; 406 | 407 | if (controlCount > threshold) return true; 408 | } 409 | 410 | if (spaceCount < threshold) return true; 411 | 412 | if (controlCount === 0) return false; 413 | return (controlCount / str.length) < 0.02; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/for-all-files.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import {pfs} from './promise'; 4 | 5 | 6 | /** 7 | * Invokes a method on all files in a directory recursively. 8 | * 9 | * @private 10 | */ 11 | export function forAllFiles(rootDirectory, func, ...args) { 12 | let rec = async (dir) => { 13 | let entries = await pfs.readdir(dir); 14 | 15 | for (let name of entries) { 16 | let fullName = path.join(dir, name); 17 | let stats = await pfs.stat(fullName); 18 | 19 | if (stats.isDirectory()) { 20 | await rec(fullName); 21 | } 22 | 23 | if (stats.isFile()) { 24 | await func(fullName, ...args); 25 | } 26 | } 27 | }; 28 | 29 | return rec(rootDirectory); 30 | } 31 | 32 | export function forAllFilesSync(rootDirectory, func, ...args) { 33 | let rec = (dir) => { 34 | fs.readdirSync(dir).forEach((name) => { 35 | let fullName = path.join(dir, name); 36 | let stats = fs.statSync(fullName); 37 | 38 | if (stats.isDirectory()) { 39 | rec(fullName); 40 | return; 41 | } 42 | 43 | if (stats.isFile()) { 44 | func(fullName, ...args); 45 | return; 46 | } 47 | }); 48 | }; 49 | 50 | rec(rootDirectory); 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import CompileCache from './compile-cache'; 2 | import CompilerHost from './compiler-host'; 3 | import * as configParser from './config-parser'; 4 | import FileChangedCache from './file-change-cache'; 5 | import {addBypassChecker} from './protocol-hook'; 6 | 7 | // NB: Patch a bug in Electron that affects electron-prebuilt-compile that 8 | // we can't fix any other way. Yes it _does_ feelbadman.jpg 9 | const fs = require('fs'); 10 | fs.statSyncNoException = fs.statSyncNoException || ((...args) => { 11 | try { 12 | return fs.statSync(...args); 13 | } catch (e) { 14 | return null; 15 | } 16 | }); 17 | 18 | let enableLiveReload = null; 19 | let watchPath = null; 20 | 21 | module.exports = Object.assign({ 22 | // NB: delay-load live-reload so we don't load RxJS in production 23 | enableLiveReload: function(...args) { 24 | enableLiveReload = enableLiveReload || require('./live-reload').enableLiveReload; 25 | return enableLiveReload(...args); 26 | }, 27 | watchPath: function(...args) { 28 | watchPath = watchPath || require('./pathwatcher-rx').watchPath; 29 | return watchPath(...args); 30 | }, 31 | }, 32 | configParser, 33 | { CompilerHost, FileChangedCache, CompileCache, addBypassChecker } 34 | ); 35 | -------------------------------------------------------------------------------- /src/initialize-renderer.js: -------------------------------------------------------------------------------- 1 | import CompilerHost from './compiler-host'; 2 | 3 | // NB: These are duped in protocol-hook so we can save startup time, make 4 | // sure to run both! 5 | const magicGlobalForRootCacheDir = '__electron_compile_root_cache_dir'; 6 | const magicGlobalForAppRootDir = '__electron_compile_app_root_dir'; 7 | 8 | const d = require('debug')('electron-compile:initialize-renderer'); 9 | 10 | let rendererInitialized = false; 11 | 12 | /** 13 | * Called by our rigged script file at the top of every HTML file to set up 14 | * the same compilers as the browser process that created us 15 | * 16 | * @private 17 | */ 18 | export function initializeRendererProcess(readOnlyMode) { 19 | if (rendererInitialized) return; 20 | 21 | let rootCacheDir = require('electron').remote.getGlobal(magicGlobalForRootCacheDir); 22 | let appRoot = require('electron').remote.getGlobal(magicGlobalForAppRootDir); 23 | let compilerHost = null; 24 | 25 | // NB: This has to be synchronous because we need to block HTML parsing 26 | // until we're set up 27 | if (readOnlyMode) { 28 | d(`Setting up electron-compile in precompiled mode with cache dir: ${rootCacheDir}`); 29 | 30 | // NB: React cares SUPER HARD about this, and this is the earliest place 31 | // we can set it up to ensure React picks it up correctly 32 | process.env.NODE_ENV = 'production'; 33 | compilerHost = CompilerHost.createReadonlyFromConfigurationSync(rootCacheDir, appRoot); 34 | } else { 35 | d(`Setting up electron-compile in development mode with cache dir: ${rootCacheDir}`); 36 | const { createCompilers } = require('./config-parser'); 37 | const compilersByMimeType = createCompilers(); 38 | 39 | compilerHost = CompilerHost.createFromConfigurationSync(rootCacheDir, appRoot, compilersByMimeType); 40 | } 41 | 42 | require('./x-require'); 43 | require('./require-hook').default(compilerHost, readOnlyMode); 44 | rendererInitialized = true; 45 | } 46 | -------------------------------------------------------------------------------- /src/live-reload.js: -------------------------------------------------------------------------------- 1 | import FileChangedCache from './file-change-cache'; 2 | import {watchPath} from './pathwatcher-rx'; 3 | import {Observable} from 'rxjs/Observable'; 4 | 5 | import './custom-operators'; 6 | 7 | import 'rxjs/add/observable/defer'; 8 | import 'rxjs/add/observable/empty'; 9 | import 'rxjs/add/observable/fromPromise'; 10 | 11 | import 'rxjs/add/operator/catch'; 12 | import 'rxjs/add/operator/filter'; 13 | import 'rxjs/add/operator/mergeMap'; 14 | import 'rxjs/add/operator/switchMap'; 15 | import 'rxjs/add/operator/timeout'; 16 | 17 | export function enableLiveReload(options={}) { 18 | let { strategy } = options; 19 | 20 | if (process.type !== 'browser' || !global.globalCompilerHost) throw new Error("Call this from the browser process, right after initializing electron-compile"); 21 | 22 | switch(strategy) { 23 | case 'react-hmr': 24 | enableReactHMR(); 25 | break; 26 | case 'naive': 27 | default: 28 | enableLiveReloadNaive(); 29 | } 30 | } 31 | 32 | let BrowserWindow; 33 | if (process.type === 'browser') { 34 | BrowserWindow = require('electron').BrowserWindow; 35 | } 36 | 37 | function reloadAllWindows() { 38 | let ret = BrowserWindow.getAllWindows().map(wnd => { 39 | if (!wnd.isVisible()) return Promise.resolve(true); 40 | 41 | return new Promise((res) => { 42 | wnd.webContents.reloadIgnoringCache(); 43 | wnd.once('ready-to-show', () => res(true)); 44 | }); 45 | }); 46 | 47 | return Promise.all(ret); 48 | } 49 | 50 | function enableLiveReloadNaive() { 51 | let filesWeCareAbout = global.globalCompilerHost.listenToCompileEvents() 52 | .filter(x => !FileChangedCache.isInNodeModules(x.filePath)); 53 | 54 | let weShouldReload = filesWeCareAbout 55 | .mergeMap(x => watchPath(x.filePath).map(() => x)) 56 | .guaranteedThrottle(1*1000); 57 | 58 | return weShouldReload 59 | .switchMap(() => Observable.defer(() => Observable.fromPromise(reloadAllWindows()).timeout(5*1000).catch(() => Observable.empty()))) 60 | .subscribe(() => console.log("Reloaded all windows!")); 61 | } 62 | 63 | function triggerHMRInRenderers() { 64 | BrowserWindow.getAllWindows().forEach((window) => { 65 | window.webContents.send('__electron-compile__HMR'); 66 | }); 67 | 68 | return Promise.resolve(true); 69 | } 70 | 71 | function enableReactHMR() { 72 | global.__electron_compile_hmr_enabled__ = true; 73 | 74 | let filesWeCareAbout = global.globalCompilerHost.listenToCompileEvents() 75 | .filter(x => !FileChangedCache.isInNodeModules(x.filePath)); 76 | 77 | let weShouldReload = filesWeCareAbout 78 | .mergeMap(x => watchPath(x.filePath).map(() => x)) 79 | .guaranteedThrottle(1*1000); 80 | 81 | return weShouldReload 82 | .switchMap(() => Observable.defer(() => Observable.fromPromise(triggerHMRInRenderers()).catch(() => Observable.empty()))) 83 | .subscribe(() => console.log("HMR sent to all windows!")); 84 | } 85 | -------------------------------------------------------------------------------- /src/packager-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import rimraf from 'rimraf'; 5 | 6 | import {pfs} from './promise'; 7 | import {main} from './cli'; 8 | 9 | import {spawnPromise, findActualExecutable} from 'spawn-rx'; 10 | 11 | const d = require('debug')('electron-compile:packager'); 12 | const electronPackager = 'electron-packager'; 13 | 14 | export async function packageDirToResourcesDir(packageDir) { 15 | let appDir = (await pfs.readdir(packageDir)).find((x) => x.match(/\.app$/i)); 16 | if (appDir) { 17 | return path.join(packageDir, appDir, 'Contents', 'Resources', 'app'); 18 | } else { 19 | return path.join(packageDir, 'resources', 'app'); 20 | } 21 | } 22 | 23 | async function copySmallFile(from, to) { 24 | d(`Copying ${from} => ${to}`); 25 | 26 | let buf = await pfs.readFile(from); 27 | await pfs.writeFile(to, buf); 28 | } 29 | 30 | export function splitOutAsarArguments(argv) { 31 | if (argv.find((x) => x.match(/^--asar-unpack$/))) { 32 | throw new Error("electron-compile doesn't support --asar-unpack at the moment, use asar-unpack-dir"); 33 | } 34 | 35 | // Strip --asar altogether 36 | let ret = argv.filter((x) => !x.match(/^--asar/)); 37 | 38 | if (ret.length === argv.length) { return { packagerArgs: ret, asarArgs: null }; } 39 | 40 | let indexOfUnpack = ret.findIndex((x) => x.match(/^--asar-unpack-dir$/)); 41 | if (indexOfUnpack < 0) { 42 | return { packagerArgs: ret, asarArgs: [] }; 43 | } 44 | 45 | let unpackArgs = ret.slice(indexOfUnpack, indexOfUnpack+1); 46 | let notUnpackArgs = ret.slice(0, indexOfUnpack).concat(ret.slice(indexOfUnpack+2)); 47 | 48 | return { packagerArgs: notUnpackArgs, asarArgs: unpackArgs }; 49 | } 50 | 51 | export function parsePackagerOutput(output) { 52 | // NB: Yes, this is fragile as fuck. :-/ 53 | console.log(output); 54 | let lines = output.split('\n'); 55 | 56 | let idx = lines.findIndex((x) => x.match(/Wrote new app/i)); 57 | if (idx < 1) throw new Error(`Packager output is invalid: ${output}`); 58 | lines = lines.splice(idx); 59 | 60 | // Multi-platform case 61 | if (lines[0].match(/Wrote new apps/)) { 62 | return lines.splice(1).filter((x) => x.length > 1); 63 | } else { 64 | return [lines[0].replace(/^.*new app to /, '')]; 65 | } 66 | } 67 | 68 | async function compileAndShim(packageDir) { 69 | let appDir = await packageDirToResourcesDir(packageDir); 70 | 71 | d(`Looking in ${appDir}`); 72 | for (let entry of await pfs.readdir(appDir)) { 73 | if (entry.match(/^(node_modules|bower_components)$/)) continue; 74 | 75 | let fullPath = path.join(appDir, entry); 76 | let stat = await pfs.stat(fullPath); 77 | 78 | if (!stat.isDirectory()) continue; 79 | 80 | d(`Executing electron-compile: ${appDir} => ${entry}`); 81 | await main(appDir, [fullPath]); 82 | } 83 | 84 | d('Copying in es6-shim'); 85 | let packageJson = JSON.parse( 86 | await pfs.readFile(path.join(appDir, 'package.json'), 'utf8')); 87 | 88 | let index = packageJson.main || 'index.js'; 89 | packageJson.originalMain = index; 90 | packageJson.main = 'es6-shim.js'; 91 | 92 | await copySmallFile( 93 | path.join(__dirname, 'es6-shim.js'), 94 | path.join(appDir, 'es6-shim.js')); 95 | 96 | await pfs.writeFile( 97 | path.join(appDir, 'package.json'), 98 | JSON.stringify(packageJson, null, 2)); 99 | } 100 | 101 | export async function runAsarArchive(packageDir, asarUnpackDir) { 102 | let appDir = await packageDirToResourcesDir(packageDir); 103 | 104 | let asarArgs = ['pack', 'app', 'app.asar']; 105 | if (asarUnpackDir) { 106 | asarArgs.push('--unpack-dir', asarUnpackDir); 107 | } 108 | 109 | let { cmd, args } = findExecutableOrGuess('asar', asarArgs); 110 | 111 | d(`Running ${cmd} ${JSON.stringify(args)}`); 112 | await spawnPromise(cmd, args, { cwd: path.join(appDir, '..') }); 113 | rimraf.sync(path.join(appDir)); 114 | } 115 | 116 | export function findExecutableOrGuess(cmdToFind, argsToUse) { 117 | let { cmd, args } = findActualExecutable(cmdToFind, argsToUse); 118 | if (cmd === electronPackager) { 119 | d(`Can't find ${cmdToFind}, falling back to where it should be as a guess!`); 120 | let cmdSuffix = process.platform === 'win32' ? '.cmd' : ''; 121 | return findActualExecutable(path.resolve(__dirname, '..', '..', '.bin', `${cmdToFind}${cmdSuffix}`), argsToUse); 122 | } 123 | 124 | return { cmd, args }; 125 | } 126 | 127 | export async function packagerMain(argv) { 128 | d(`argv: ${JSON.stringify(argv)}`); 129 | argv = argv.splice(2); 130 | 131 | let { packagerArgs, asarArgs } = splitOutAsarArguments(argv); 132 | let { cmd, args } = findExecutableOrGuess(electronPackager, packagerArgs); 133 | 134 | d(`Spawning electron-packager: ${JSON.stringify(args)}`); 135 | let packagerOutput = await spawnPromise(cmd, args); 136 | let packageDirs = parsePackagerOutput(packagerOutput); 137 | 138 | d(`Starting compilation for ${JSON.stringify(packageDirs)}`); 139 | for (let packageDir of packageDirs) { 140 | await compileAndShim(packageDir); 141 | 142 | if (!asarArgs) continue; 143 | 144 | d('Starting ASAR packaging'); 145 | let asarUnpackDir = null; 146 | if (asarArgs.length === 2) { 147 | asarUnpackDir = asarArgs[1]; 148 | } 149 | 150 | await runAsarArchive(packageDir, asarUnpackDir); 151 | } 152 | } 153 | 154 | if (process.mainModule === module) { 155 | packagerMain(process.argv) 156 | .then(() => process.exit(0)) 157 | .catch((e) => { 158 | console.error(e.message || e); 159 | d(e.stack); 160 | 161 | process.exit(-1); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /src/pathwatcher-rx.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {Observable} from 'rxjs/Observable'; 3 | import {Subscription} from 'rxjs/Subscription'; 4 | import LRU from 'lru-cache'; 5 | 6 | import 'rxjs/add/operator/publish'; 7 | 8 | export function watchPathDirect(directory) { 9 | return Observable.create((subj) => { 10 | let dead = false; 11 | 12 | const watcher = fs.watch(directory, {}, (eventType, fileName) => { 13 | if (dead) return; 14 | subj.next({eventType, fileName}); 15 | }); 16 | 17 | watcher.on('error', (e) => { 18 | dead = true; 19 | subj.error(e); 20 | }); 21 | 22 | return new Subscription(() => { if (!dead) { watcher.close(); } }); 23 | }); 24 | } 25 | 26 | const pathCache = new LRU({ length: 256 }); 27 | export function watchPath(directory) { 28 | let ret = pathCache.get(directory); 29 | if (ret) return ret; 30 | 31 | ret = watchPathDirect(directory).publish().refCount(); 32 | pathCache.set(directory, ret); 33 | return ret; 34 | } 35 | -------------------------------------------------------------------------------- /src/promise.js: -------------------------------------------------------------------------------- 1 | import pify from 'pify'; 2 | 3 | // NB: We do this so that every module doesn't have to run pify 4 | // on fs and zlib 5 | 6 | 7 | /** 8 | * @private 9 | */ 10 | export const pfs = pify(require('fs')); 11 | 12 | /** 13 | * @private 14 | */ 15 | export const pzlib = pify(require('zlib')); 16 | -------------------------------------------------------------------------------- /src/protocol-hook.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import fs from 'fs'; 3 | import mime from '@paulcbetts/mime-types'; 4 | import LRU from 'lru-cache'; 5 | 6 | const magicWords = "__magic__file__to__help__electron__compile.js"; 7 | 8 | // NB: These are duped in initialize-renderer so we can save startup time, make 9 | // sure to run both! 10 | const magicGlobalForRootCacheDir = '__electron_compile_root_cache_dir'; 11 | const magicGlobalForAppRootDir = '__electron_compile_app_root_dir'; 12 | 13 | const d = require('debug')('electron-compile:protocol-hook'); 14 | 15 | let protocol = null; 16 | 17 | const mapStatCache = new LRU({length: 512}); 18 | function doesMapFileExist(filePath) { 19 | let ret = mapStatCache.get(filePath); 20 | if (ret !== undefined) return Promise.resolve(ret); 21 | 22 | return new Promise((res) => { 23 | fs.lstat(filePath, (err, s) => { 24 | let failed = (err || !s); 25 | 26 | mapStatCache.set(filePath, !failed); 27 | res(!failed); 28 | }); 29 | }); 30 | } 31 | 32 | /** 33 | * Adds our script header to the top of all HTML files 34 | * 35 | * @private 36 | */ 37 | export function rigHtmlDocumentToInitializeElectronCompile(doc) { 38 | let lines = doc.split("\n"); 39 | let replacement = ``; 40 | let replacedHead = false; 41 | 42 | for (let i=0; i < lines.length; i++) { 43 | if (!lines[i].match(//i)) continue; 44 | 45 | lines[i] = (lines[i]).replace(//i, replacement); 46 | replacedHead = true; 47 | break; 48 | } 49 | 50 | if (!replacedHead) { 51 | replacement = ``; 52 | for (let i=0; i < lines.length; i++) { 53 | if (!lines[i].match(/]+)>/i, replacement); 56 | break; 57 | } 58 | } 59 | 60 | return lines.join("\n"); 61 | } 62 | 63 | function requestFileJob(filePath, finish) { 64 | fs.readFile(filePath, (err, buf) => { 65 | if (err) { 66 | if (err.errno === 34) { 67 | finish(-6); // net::ERR_FILE_NOT_FOUND 68 | return; 69 | } else { 70 | finish(-2); // net::FAILED 71 | return; 72 | } 73 | } 74 | 75 | finish({ 76 | data: buf, 77 | mimeType: mime.lookup(filePath) || 'text/plain' 78 | }); 79 | }); 80 | } 81 | 82 | const bypassCheckers = []; 83 | 84 | /** 85 | * Adds a function that will be called on electron-compile's protocol hook 86 | * used to intercept file requests. Use this to bypass electron-compile 87 | * entirely for certain URI's. 88 | * 89 | * @param {Function} bypassChecker Function that will be called with the file path to determine whether to bypass or not 90 | */ 91 | export function addBypassChecker(bypassChecker) { 92 | bypassCheckers.push(bypassChecker); 93 | } 94 | 95 | /** 96 | * Initializes the protocol hook on file: that allows us to intercept files 97 | * loaded by Chromium and rewrite them. This method along with 98 | * {@link registerRequireExtension} are the top-level methods that electron-compile 99 | * actually uses to intercept code that Electron loads. 100 | * 101 | * @param {CompilerHost} compilerHost The compiler host to use for compilation. 102 | */ 103 | export function initializeProtocolHook(compilerHost) { 104 | protocol = protocol || require('electron').protocol; 105 | 106 | global[magicGlobalForRootCacheDir] = compilerHost.rootCacheDir; 107 | global[magicGlobalForAppRootDir] = compilerHost.appRoot; 108 | 109 | const electronCompileSetupCode = `if (window.require) require('electron-compile/lib/initialize-renderer').initializeRendererProcess(${compilerHost.readOnlyMode});`; 110 | 111 | protocol.interceptBufferProtocol('file', async function(request, finish) { 112 | let uri = url.parse(request.url); 113 | 114 | d(`Intercepting url ${request.url}`); 115 | if (request.url.indexOf(magicWords) > -1) { 116 | finish({ 117 | mimeType: 'application/javascript', 118 | data: new Buffer(electronCompileSetupCode, 'utf8') 119 | }); 120 | 121 | return; 122 | } 123 | 124 | // This is a protocol-relative URL that has gone pear-shaped in Electron, 125 | // let's rewrite it 126 | if (uri.host && uri.host.length > 1) { 127 | //let newUri = request.url.replace(/^file:/, "https:"); 128 | // TODO: Jump off this bridge later 129 | d(`TODO: Found bogus protocol-relative URL, can't fix it up!!`); 130 | finish(-2); 131 | return; 132 | } 133 | 134 | let filePath = decodeURIComponent(uri.pathname); 135 | 136 | // NB: pathname has a leading '/' on Win32 for some reason 137 | if (process.platform === 'win32') { 138 | filePath = filePath.slice(1); 139 | } 140 | 141 | // NB: Special-case files coming from atom.asar or node_modules 142 | if (filePath.match(/[\/\\](atom|electron).asar/) || filePath.match(/[\/\\](node_modules|bower_components)/)) { 143 | // NBs on NBs: If we're loading an HTML file from node_modules, we still have 144 | // to do the HTML document rigging 145 | if (filePath.match(/\.html?$/i)) { 146 | let riggedContents = null; 147 | fs.readFile(filePath, 'utf8', (err, contents) => { 148 | if (err) { 149 | if (err.errno === 34) { 150 | finish(-6); // net::ERR_FILE_NOT_FOUND 151 | return; 152 | } else { 153 | finish(-2); // net::FAILED 154 | return; 155 | } 156 | } 157 | 158 | riggedContents = rigHtmlDocumentToInitializeElectronCompile(contents); 159 | finish({ data: new Buffer(riggedContents), mimeType: 'text/html' }); 160 | return; 161 | }); 162 | 163 | return; 164 | } 165 | 166 | requestFileJob(filePath, finish); 167 | return; 168 | } 169 | 170 | // NB: Chromium will somehow decide that external source map references 171 | // aren't relative to the file that was loaded for node.js modules, but 172 | // relative to the HTML file. Since we can't really figure out what the 173 | // real path is, we just need to squelch it. 174 | if (filePath.match(/\.map$/i) && !(await doesMapFileExist(filePath))) { 175 | finish({ data: new Buffer("", 'utf8'), mimeType: 'text/plain' }); 176 | return; 177 | } 178 | 179 | for (const bypassChecker of bypassCheckers) { 180 | if (bypassChecker(filePath)) { 181 | d('bypassing compilers for:', filePath); 182 | requestFileJob(filePath, finish); 183 | return; 184 | } 185 | } 186 | 187 | try { 188 | let result = await compilerHost.compile(filePath); 189 | 190 | if (result.mimeType === 'text/html') { 191 | result.code = rigHtmlDocumentToInitializeElectronCompile(result.code); 192 | } 193 | 194 | if (result.binaryData || result.code instanceof Buffer) { 195 | finish({ data: result.binaryData || result.code, mimeType: result.mimeType }); 196 | return; 197 | } else { 198 | finish({ data: new Buffer(result.code), mimeType: result.mimeType }); 199 | return; 200 | } 201 | } catch (e) { 202 | let err = `Failed to compile ${filePath}: ${e.message}\n${e.stack}`; 203 | d(err); 204 | 205 | if (e.errno === 34 /*ENOENT*/) { 206 | finish(-6); // net::ERR_FILE_NOT_FOUND 207 | return; 208 | } 209 | 210 | finish({ mimeType: 'text/plain', data: new Buffer(err) }); 211 | return; 212 | } 213 | }); 214 | } 215 | -------------------------------------------------------------------------------- /src/read-only-compiler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReadOnlyCompiler is a compiler which allows the host to inject all of the compiler 3 | * metadata information so that {@link CompileCache} et al are able to recreate the 4 | * hash without having two separate code paths. 5 | */ 6 | export default class ReadOnlyCompiler { 7 | /** 8 | * Creates a ReadOnlyCompiler instance 9 | * 10 | * @private 11 | */ 12 | constructor(name, compilerVersion, compilerOptions, inputMimeTypes) { 13 | Object.assign(this, { name, compilerVersion, compilerOptions, inputMimeTypes }); 14 | } 15 | 16 | async shouldCompileFile() { return true; } 17 | async determineDependentFiles() { return []; } 18 | 19 | async compile() { 20 | throw new Error("Read-only compilers can't compile"); 21 | } 22 | 23 | shouldCompileFileSync() { return true; } 24 | determineDependentFilesSync() { return []; } 25 | 26 | compileSync() { 27 | throw new Error("Read-only compilers can't compile"); 28 | } 29 | 30 | getCompilerVersion() { 31 | return this.compilerVersion; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/require-hook.js: -------------------------------------------------------------------------------- 1 | import mimeTypes from '@paulcbetts/mime-types'; 2 | 3 | let HMR = false; 4 | 5 | const d = require('debug')('electron-compile:require-hook'); 6 | let electron = null; 7 | 8 | if (process.type === 'renderer') { 9 | window.__hot = []; 10 | electron = require('electron'); 11 | HMR = electron.remote.getGlobal('__electron_compile_hmr_enabled__'); 12 | 13 | if (HMR) { 14 | electron.ipcRenderer.on('__electron-compile__HMR', () => { 15 | d("Got HMR signal!"); 16 | 17 | // Reset the module cache 18 | let cache = require('module')._cache; 19 | let toEject = Object.keys(cache).filter(x => x && !x.match(/[\\\/](node_modules|.*\.asar)[\\\/]/i)); 20 | toEject.forEach(x => { 21 | d(`Removing node module entry for ${x}`); 22 | delete cache[x]; 23 | }); 24 | 25 | window.__hot.forEach(fn => fn()); 26 | }); 27 | } 28 | } 29 | 30 | /** 31 | * Initializes the node.js hook that allows us to intercept files loaded by 32 | * node.js and rewrite them. This method along with {@link initializeProtocolHook} 33 | * are the top-level methods that electron-compile actually uses to intercept 34 | * code that Electron loads. 35 | * 36 | * @param {CompilerHost} compilerHost The compiler host to use for compilation. 37 | */ 38 | export default function registerRequireExtension(compilerHost, isProduction) { 39 | if (HMR) { 40 | try { 41 | require('module').prototype.hot = { 42 | accept: (cb) => window.__hot.push(cb) 43 | }; 44 | 45 | require.main.require('react-hot-loader/patch'); 46 | } catch (e) { 47 | console.error(`Couldn't require react-hot-loader/patch, you need to add react-hot-loader@3 as a dependency! ${e.message}`); 48 | } 49 | } 50 | 51 | let mimeTypeList = isProduction ? 52 | Object.keys(compilerHost.mimeTypesToRegister) : 53 | Object.keys(compilerHost.compilersByMimeType); 54 | 55 | mimeTypeList.forEach((mimeType) => { 56 | let ext = mimeTypes.extension(mimeType); 57 | 58 | require.extensions[`.${ext}`] = (module, filename) => { 59 | let {code} = compilerHost.compileSync(filename); 60 | 61 | if (code === null) { 62 | console.error(`null code returned for "${filename}". Please raise an issue on 'electron-compile' with the contents of this file.`); 63 | } 64 | 65 | module._compile(code, filename); 66 | }; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/rig-mime-types.js: -------------------------------------------------------------------------------- 1 | import mimeTypes from '@paulcbetts/mime-types'; 2 | 3 | const typesToRig = { 4 | 'text/typescript': 'ts', 5 | 'text/tsx': 'tsx', 6 | 'text/jade': 'jade', 7 | 'text/cson': 'cson', 8 | 'text/stylus': 'styl', 9 | 'text/sass': 'sass', 10 | 'text/scss': 'scss', 11 | 'text/vue': 'vue', 12 | 'text/graphql': 'graphql', 13 | }; 14 | 15 | /** 16 | * Adds MIME types for types not in the mime-types package 17 | * 18 | * @private 19 | */ 20 | export function init() { 21 | Object.keys(typesToRig).forEach((type) => { 22 | let ext = typesToRig[type]; 23 | 24 | mimeTypes.types[ext] = type; 25 | mimeTypes.extensions[type] = [ext]; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/sanitize-paths.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import LRUCache from 'lru-cache'; 3 | 4 | const d = require('debug')('electron-compile:sanitize-paths'); 5 | const realpathCache = LRUCache({ max: 1024 }); 6 | 7 | function cachedRealpath(p) { 8 | let ret = realpathCache.get(p); 9 | if (ret) return ret; 10 | 11 | ret = fs.realpathSync(p); 12 | d(`Cache miss for cachedRealpath: '${p}' => '${ret}'`); 13 | 14 | realpathCache.set(p, ret); 15 | return ret; 16 | } 17 | 18 | /** 19 | * Electron will sometimes hand us paths that don't match the platform if they 20 | * were derived from a URL (i.e. 'C:/Users/Paul/...'), whereas the cache will have 21 | * saved paths with backslashes. 22 | * 23 | * @private 24 | */ 25 | export default function sanitizeFilePath(file) { 26 | if (!file) return file; 27 | 28 | // NB: Some people add symlinks into system directories. node.js will internally 29 | // call realpath on paths that it finds, which will break our cache resolution. 30 | // We need to catch this scenario and fix it up. The tricky part is, some parts 31 | // of Electron will give us the pre-resolved paths, and others will give us the 32 | // post-resolved one. We need to handle both. 33 | 34 | let realFile = null; 35 | let parts = file.split(/[\\\/]app.asar[\\\/]/); 36 | if (!parts[1]) { 37 | // Not using an ASAR archive 38 | realFile = cachedRealpath(file); 39 | } else { 40 | // We do all this silliness to work around 41 | // https://github.com/atom/electron/issues/4610 42 | realFile = `${cachedRealpath(parts[0])}/app.asar/${parts[1]}`; 43 | } 44 | 45 | return realFile.replace(/[\\\/]/g, '/'); 46 | } 47 | -------------------------------------------------------------------------------- /src/x-require.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | function requireModule(href) { 4 | let filePath = href; 5 | 6 | if (filePath.match(/^file:/i)) { 7 | let theUrl = url.parse(filePath); 8 | filePath = decodeURIComponent(theUrl.pathname); 9 | 10 | if (process.platform === 'win32') { 11 | filePath = filePath.slice(1); 12 | } 13 | } 14 | 15 | // NB: We don't do any path canonicalization here because we rely on 16 | // InlineHtmlCompiler to have already converted any relative paths that 17 | // were used with x-require into absolute paths. 18 | require(filePath); 19 | } 20 | 21 | /** 22 | * @private 23 | */ 24 | export default (() => { 25 | if (process.type !== 'renderer' || !window || !window.document) return null; 26 | 27 | let proto = Object.assign(Object.create(HTMLElement.prototype), { 28 | createdCallback: function() { 29 | let href = this.getAttribute('src'); 30 | if (href && href.length > 0) { 31 | requireModule(href); 32 | } 33 | }, 34 | attributeChangedCallback: function(attrName, oldVal, newVal) { 35 | if (attrName !== 'src') return; 36 | requireModule(newVal); 37 | } 38 | }); 39 | 40 | return document.registerElement('x-require', { prototype: proto }); 41 | })(); 42 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "indent": [ 6 | 2, 7 | 2 8 | ], 9 | "semi": [ 10 | 2, 11 | "always" 12 | ], 13 | "no-console": 0 14 | }, 15 | "env": { 16 | "es6": true, 17 | "node": true, 18 | "browser": true, 19 | "mocha": true 20 | }, 21 | "globals": { 22 | "expect": true, 23 | "chai": true, 24 | }, 25 | "extends": "eslint:recommended" 26 | } 27 | -------------------------------------------------------------------------------- /test/compile-cache.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import rimraf from 'rimraf'; 6 | import mkdirp from 'mkdirp'; 7 | import FileChangeCache from '../src/file-change-cache'; 8 | import CompileCache from '../src/compile-cache'; 9 | import pify from 'pify'; 10 | 11 | const pfs = pify(fs); 12 | 13 | let testCount=0; 14 | 15 | describe('The compile cache', function() { 16 | beforeEach(function() { 17 | this.appRootDir = path.join(__dirname, '..'); 18 | this.fileChangeCache = new FileChangeCache(this.appRootDir); 19 | 20 | this.tempCacheDir = path.join(__dirname, `__compile_cache_${testCount++}`); 21 | mkdirp.sync(this.tempCacheDir); 22 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 23 | }); 24 | 25 | afterEach(function() { 26 | rimraf.sync(this.tempCacheDir); 27 | }); 28 | 29 | it('Should only call compile once for the same file', async function() { 30 | let inputFile = path.resolve(__dirname, '..', 'src', 'compile-cache.js'); 31 | let callCount = 0; 32 | 33 | let fetcher = async function(filePath, hashInfo) { 34 | callCount++; 35 | 36 | let code = hashInfo.sourceCode || await pfs.readFile(filePath, 'utf8'); 37 | let mimeType = 'application/javascript'; 38 | let dependentFiles = []; 39 | return { code, mimeType, dependentFiles }; 40 | }; 41 | 42 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 43 | 44 | expect(result.mimeType).to.equal('application/javascript'); 45 | expect(result.code.length > 10).to.be.ok; 46 | expect(callCount).to.equal(1); 47 | 48 | result = await this.fixture.getOrFetch(inputFile, fetcher); 49 | 50 | expect(result.mimeType).to.equal('application/javascript'); 51 | expect(result.code.length > 10).to.be.ok; 52 | expect(callCount).to.equal(1); 53 | 54 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 55 | 56 | result = await this.fixture.getOrFetch(inputFile, fetcher); 57 | 58 | expect(result.mimeType).to.equal('application/javascript'); 59 | expect(result.code.length > 10).to.be.ok; 60 | expect(callCount).to.equal(1); 61 | }); 62 | 63 | it('Should roundtrip binary files', async function() { 64 | let inputFile = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 65 | let hashInfo = await this.fileChangeCache.getHashForPath(inputFile); 66 | 67 | await this.fixture.save(hashInfo, hashInfo.binaryData, 'application/zip'); 68 | 69 | let fetcher = async function() { 70 | throw new Error("No"); 71 | }; 72 | 73 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 74 | expect(result.mimeType).to.equal('application/zip'); 75 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 76 | 77 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 78 | 79 | result = await this.fixture.getOrFetch(inputFile, fetcher); 80 | expect(result.mimeType).to.equal('application/zip'); 81 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 82 | }); 83 | 84 | it('Should roundtrip binary files synchronously', function() { 85 | let inputFile = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 86 | let hashInfo = this.fileChangeCache.getHashForPathSync(inputFile); 87 | 88 | this.fixture.saveSync(hashInfo, hashInfo.binaryData, 'application/zip'); 89 | 90 | let fetcher = function() { 91 | throw new Error("No"); 92 | }; 93 | 94 | let result = this.fixture.getOrFetchSync(inputFile, fetcher); 95 | expect(result.mimeType).to.equal('application/zip'); 96 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 97 | 98 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 99 | 100 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 101 | expect(result.mimeType).to.equal('application/zip'); 102 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 103 | }); 104 | 105 | it('Should only call compile once for the same file synchronously', function() { 106 | let inputFile = path.resolve(__dirname, '..', 'src', 'compile-cache.js'); 107 | let callCount = 0; 108 | 109 | let fetcher = function(filePath, hashInfo) { 110 | callCount++; 111 | 112 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 113 | let mimeType = 'application/javascript'; 114 | 115 | return { code, mimeType }; 116 | }; 117 | 118 | let result = this.fixture.getOrFetchSync(inputFile, fetcher); 119 | 120 | expect(result.mimeType).to.equal('application/javascript'); 121 | expect(result.code.length > 10).to.be.ok; 122 | expect(callCount).to.equal(1); 123 | 124 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 125 | 126 | expect(result.mimeType).to.equal('application/javascript'); 127 | expect(result.code.length > 10).to.be.ok; 128 | expect(callCount).to.equal(1); 129 | 130 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 131 | 132 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 133 | 134 | expect(result.mimeType).to.equal('application/javascript'); 135 | expect(result.code.length > 10).to.be.ok; 136 | expect(callCount).to.equal(1); 137 | }); 138 | 139 | it('Shouldnt cache compile failures', async function() { 140 | let inputFile = path.resolve(__dirname, '..', 'lib', 'compile-cache.js'); 141 | let callCount = 0; 142 | let weBlewUpCount = 0; 143 | 144 | let fetcher = async function() { 145 | callCount++; 146 | throw new Error("Lolz"); 147 | }; 148 | 149 | try { 150 | await this.fixture.getOrFetch(inputFile, fetcher); 151 | } catch (e) { 152 | weBlewUpCount++; 153 | } 154 | 155 | expect(callCount).to.equal(1); 156 | expect(weBlewUpCount).to.equal(1); 157 | 158 | try { 159 | await this.fixture.getOrFetch(inputFile, fetcher); 160 | } catch (e) { 161 | weBlewUpCount++; 162 | } 163 | 164 | expect(callCount).to.equal(2); 165 | expect(weBlewUpCount).to.equal(2); 166 | }); 167 | 168 | it('should re-compile a file if one of its dependencies have changed', async function() { 169 | let callCount = 0; 170 | let inputFile = path.resolve(__dirname, 'fixtures', 'file-with-dependencies.scss'); 171 | let dependencyPath = path.resolve(__dirname, 'fixtures', '_partial.scss'); 172 | let dependencyCode = await pfs.readFile(dependencyPath); 173 | 174 | let fetcher = function(filePath, hashInfo) { 175 | callCount++; 176 | 177 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 178 | let mimeType = 'text/scss'; 179 | let dependentFiles = [ 180 | dependencyPath 181 | ]; 182 | 183 | return { code, mimeType, dependentFiles }; 184 | }; 185 | 186 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 187 | 188 | expect(result.mimeType).to.equal('text/scss'); 189 | expect(result.code.length > 10).to.be.ok; 190 | expect(callCount).to.equal(1); 191 | 192 | // Try it again - make sure it doesn't run the fetcher 193 | result = await this.fixture.getOrFetch(inputFile, fetcher); 194 | 195 | expect(result.mimeType).to.equal('text/scss'); 196 | expect(result.code.length > 10).to.be.ok; 197 | expect(callCount).to.equal(1); 198 | 199 | // Change a dependency 200 | await pfs.writeFile(dependencyPath, '$background: red;'); 201 | result = await this.fixture.getOrFetch(inputFile, fetcher); 202 | 203 | expect(result.mimeType).to.equal('text/scss'); 204 | expect(result.code.length > 10).to.be.ok; 205 | expect(callCount).to.equal(2); 206 | 207 | await pfs.writeFile(dependencyPath, dependencyCode); 208 | }); 209 | 210 | it('should re-compile a file if one of its nested dependencies have changed', async function() { 211 | let callCount = 0; 212 | let inputFile = path.resolve(__dirname, 'fixtures', 'file-with-dependencies.scss'); 213 | let dependencyPath = path.resolve(__dirname, 'fixtures', 'dependency_b.scss'); 214 | let dependencyCode = await pfs.readFile(dependencyPath); 215 | 216 | let fetcher = function(filePath, hashInfo) { 217 | callCount++; 218 | 219 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 220 | let mimeType = 'text/scss'; 221 | let dependentFiles = [ 222 | dependencyPath 223 | ]; 224 | 225 | return { code, mimeType, dependentFiles }; 226 | }; 227 | 228 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 229 | 230 | expect(result.mimeType).to.equal('text/scss'); 231 | expect(result.code.length > 10).to.be.ok; 232 | expect(callCount).to.equal(1); 233 | 234 | // Try it again - make sure it doesn't run the fetcher 235 | result = await this.fixture.getOrFetch(inputFile, fetcher); 236 | 237 | expect(result.mimeType).to.equal('text/scss'); 238 | expect(result.code.length > 10).to.be.ok; 239 | expect(callCount).to.equal(1); 240 | 241 | // Change a dependency 242 | await pfs.writeFile(dependencyPath, '$black: black;'); // black is back 243 | result = await this.fixture.getOrFetch(inputFile, fetcher); 244 | 245 | expect(result.mimeType).to.equal('text/scss'); 246 | expect(result.code.length > 10).to.be.ok; 247 | expect(callCount).to.equal(2); 248 | 249 | await pfs.writeFile(dependencyPath, dependencyCode); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /test/compiler-host.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import rimraf from 'rimraf'; 6 | import mkdirp from 'mkdirp'; 7 | import mimeTypes from '@paulcbetts/mime-types'; 8 | import FileChangeCache from '../src/file-change-cache'; 9 | import CompilerHost from '../src/compiler-host'; 10 | 11 | const d = require('debug')('test:compiler-host'); 12 | 13 | let testCount=0; 14 | 15 | describe('All available compilers', function() { 16 | it('should have a MIME type in mime-types', function() { 17 | Object.keys(global.compilersByMimeType).forEach((type) => { 18 | d(`Extension for ${type} is ${mimeTypes.extension(type)}`); 19 | expect(mimeTypes.extension(type)).to.be.ok; 20 | }); 21 | }); 22 | }); 23 | 24 | describe('The compiler host', function() { 25 | this.timeout(15*1000); 26 | 27 | beforeEach(function() { 28 | this.appRootDir = path.join(__dirname, '..'); 29 | this.fileChangeCache = new FileChangeCache(this.appRootDir); 30 | 31 | this.tempCacheDir = path.join(__dirname, `__compile_cache_${testCount++}`); 32 | mkdirp.sync(this.tempCacheDir); 33 | 34 | this.compilersByMimeType = Object.keys(global.compilersByMimeType).reduce((acc, type) => { 35 | let Klass = global.compilersByMimeType[type]; 36 | acc[type] = new Klass(); 37 | return acc; 38 | }, {}); 39 | 40 | let InlineHtmlCompiler = Object.getPrototypeOf(this.compilersByMimeType['text/html']).constructor; 41 | this.compilersByMimeType['text/html'] = InlineHtmlCompiler.createFromCompilers(this.compilersByMimeType); 42 | this.compilersByMimeType['application/javascript'].compilerOptions = { 43 | "presets": ["react", "es2016-node5"], 44 | "plugins": ["transform-async-to-generator"], 45 | "sourceMaps": "inline" 46 | }; 47 | 48 | this.compilersByMimeType['text/jsx'].compilerOptions = { 49 | "presets": ["react", "es2016-node5"], 50 | "plugins": ["transform-async-to-generator"], 51 | "sourceMaps": "inline" 52 | }; 53 | 54 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, false); 55 | }); 56 | 57 | afterEach(function() { 58 | rimraf.sync(this.tempCacheDir); 59 | }); 60 | 61 | it('should compile basic HTML and not blow up', function() { 62 | let input = ''; 63 | let inFile = path.join(this.tempCacheDir, 'input.html'); 64 | fs.writeFileSync(inFile, input); 65 | 66 | let result = this.fixture.compileSync(inFile); 67 | 68 | expect(result.mimeType).to.equal('text/html'); 69 | expect(result.code.length > input.length).to.be.ok; 70 | }); 71 | 72 | it('Should compile everything in the fixtures directory', async function() { 73 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 74 | 75 | await this.fixture.compileAll(input, (filePath) => { 76 | if (filePath.match(/invalid/)) return false; 77 | if (filePath.match(/binaryfile/)) return false; 78 | if (filePath.match(/minified/)) return false; 79 | if (filePath.match(/source_map/)) return false; 80 | if (filePath.match(/babelrc/)) return false; 81 | if (filePath.match(/compilerc/)) return false; 82 | 83 | return true; 84 | }); 85 | }); 86 | 87 | it('Should compile everything in the fixtures directory sync', function() { 88 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 89 | 90 | this.fixture.compileAllSync(input, (filePath) => { 91 | if (filePath.match(/invalid/)) return false; 92 | if (filePath.match(/binaryfile/)) return false; 93 | if (filePath.match(/minified/)) return false; 94 | if (filePath.match(/source_map/)) return false; 95 | if (filePath.match(/babelrc/)) return false; 96 | if (filePath.match(/compilerc/)) return false; 97 | 98 | return true; 99 | }); 100 | }); 101 | 102 | it('Should read files from cache once we compile them', async function() { 103 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 104 | 105 | let compileEventCount = 0; 106 | let expectedEventCount = 0; 107 | this.fixture.listenToCompileEvents().subscribe(() => compileEventCount++); 108 | 109 | expect(compileEventCount).to.equal(0); 110 | 111 | await this.fixture.compileAll(input, (filePath) => { 112 | 113 | if (filePath.match(/invalid/)) return false; 114 | if (filePath.match(/binaryfile/)) return false; 115 | if (filePath.match(/minified/)) return false; 116 | if (filePath.match(/source_map/)) return false; 117 | if (filePath.match(/babelrc/)) return false; 118 | if (filePath.match(/compilerc/)) return false; 119 | 120 | expectedEventCount++; 121 | return true; 122 | }); 123 | 124 | expect(compileEventCount).to.equal(expectedEventCount); 125 | expect(expectedEventCount > 0).to.be.ok; 126 | 127 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, true); 128 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 129 | 130 | await this.fixture.compileAll(input, (filePath) => { 131 | if (filePath.match(/invalid/)) return false; 132 | if (filePath.match(/binaryfile/)) return false; 133 | if (filePath.match(/minified/)) return false; 134 | if (filePath.match(/source_map/)) return false; 135 | if (filePath.match(/babelrc/)) return false; 136 | if (filePath.match(/compilerc/)) return false; 137 | 138 | return true; 139 | }); 140 | }); 141 | 142 | it('Should read files from cache once we compile them synchronously', function() { 143 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 144 | 145 | this.fixture.compileAllSync(input, (filePath) => { 146 | if (filePath.match(/invalid/)) return false; 147 | if (filePath.match(/binaryfile/)) return false; 148 | if (filePath.match(/minified/)) return false; 149 | if (filePath.match(/source_map/)) return false; 150 | if (filePath.match(/babelrc/)) return false; 151 | if (filePath.match(/compilerc/)) return false; 152 | 153 | return true; 154 | }); 155 | 156 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, true); 157 | this.fixture.compileUncached = () => { throw new Error("Fail!"); }; 158 | 159 | this.fixture.compileAllSync(input, (filePath) => { 160 | if (filePath.match(/invalid/)) return false; 161 | if (filePath.match(/binaryfile/)) return false; 162 | if (filePath.match(/minified/)) return false; 163 | if (filePath.match(/source_map/)) return false; 164 | if (filePath.match(/babelrc/)) return false; 165 | if (filePath.match(/compilerc/)) return false; 166 | 167 | return true; 168 | }); 169 | }); 170 | 171 | it('Should read files from serialized compiler information', async function() { 172 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 173 | 174 | d("Attempting to run initial compile"); 175 | await this.fixture.compileAll(input, (filePath) => { 176 | if (filePath.match(/invalid/)) return false; 177 | if (filePath.match(/binaryfile/)) return false; 178 | if (filePath.match(/minified/)) return false; 179 | if (filePath.match(/source_map/)) return false; 180 | if (filePath.match(/babelrc/)) return false; 181 | if (filePath.match(/compilerc/)) return false; 182 | 183 | return true; 184 | }); 185 | 186 | expect(Object.keys(this.fixture.mimeTypesToRegister).length > 1).to.be.ok; 187 | 188 | d("Saving configuration"); 189 | await this.fixture.saveConfiguration(); 190 | 191 | d("Recreating from said configuration"); 192 | this.fixture = await CompilerHost.createReadonlyFromConfiguration(this.tempCacheDir, this.appRootDir); 193 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 194 | 195 | expect(Object.keys(this.fixture.mimeTypesToRegister).length > 1).to.be.ok; 196 | 197 | d("Recompiling everything from cached data"); 198 | await this.fixture.compileAll(input, (filePath) => { 199 | if (filePath.match(/invalid/)) return false; 200 | if (filePath.match(/binaryfile/)) return false; 201 | if (filePath.match(/minified/)) return false; 202 | if (filePath.match(/source_map/)) return false; 203 | if (filePath.match(/babelrc/)) return false; 204 | if (filePath.match(/compilerc/)) return false; 205 | 206 | return true; 207 | }); 208 | }); 209 | 210 | it('Should read files from serialized compiler information synchronously', function() { 211 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 212 | 213 | d("Attempting to run initial compile"); 214 | this.fixture.compileAllSync(input, (filePath) => { 215 | if (filePath.match(/invalid/)) return false; 216 | if (filePath.match(/binaryfile/)) return false; 217 | if (filePath.match(/minified/)) return false; 218 | if (filePath.match(/source_map/)) return false; 219 | if (filePath.match(/babelrc/)) return false; 220 | if (filePath.match(/compilerc/)) return false; 221 | 222 | return true; 223 | }); 224 | 225 | d("Saving configuration"); 226 | this.fixture.saveConfigurationSync(); 227 | 228 | d("Recreating from said configuration"); 229 | this.fixture = CompilerHost.createReadonlyFromConfigurationSync(this.tempCacheDir, this.appRootDir); 230 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 231 | 232 | d("Recompiling everything from cached data"); 233 | this.fixture.compileAllSync(input, (filePath) => { 234 | if (filePath.match(/invalid/)) return false; 235 | if (filePath.match(/binaryfile/)) return false; 236 | if (filePath.match(/minified/)) return false; 237 | if (filePath.match(/source_map/)) return false; 238 | if (filePath.match(/babelrc/)) return false; 239 | if (filePath.match(/compilerc/)) return false; 240 | 241 | return true; 242 | }); 243 | }); 244 | 245 | it('Should update the sourceMappingURL with the node_modules path', function() { 246 | //I need to use path.normalize to support run the test on different OS 247 | let nmz = path.normalize; 248 | let fix = CompilerHost.fixNodeModulesSourceMappingSync; 249 | let result = ''; 250 | 251 | let code = `//# sourceMappingURL=index.ts.map`; 252 | let expected = `//# sourceMappingURL=${nmz('node_modules/package/index.ts.map')}`; 253 | 254 | //sourceCode, sourcePath, appRoot 255 | result = fix(code, nmz('/some/folder/node_modules/package/code.js'), nmz('/some/folder')); 256 | expect(result).to.equal(expected); 257 | }); 258 | 259 | it('Should leave the sourceMappingURL as it is when is a data URI', function() { 260 | //I need to use path.normalize to support run the test on different OS 261 | let nmz = path.normalize; 262 | let fix = CompilerHost.fixNodeModulesSourceMappingSync; 263 | let result = ''; 264 | 265 | let code = `//# sourceMappingURL=data:base64;nomething`; 266 | let expected = `//# sourceMappingURL=data:base64;nomething`; 267 | 268 | //sourceCode, sourcePath, appRoot 269 | result = fix(code, nmz('/some/folder/node_modules/package/code.js'), nmz('/some/folder')); 270 | expect(result).to.equal(expected); 271 | }); 272 | 273 | it('Should leave the sourceMappingURL as it is when is used in code', function() { 274 | //I need to use path.normalize to support run the test on different OS 275 | let nmz = path.normalize; 276 | let fix = CompilerHost.fixNodeModulesSourceMappingSync; 277 | let result = ''; 278 | 279 | let code = `var somevar = "//# sourceMappingURL=" + anotherVar`; 280 | let expected = `var somevar = "//# sourceMappingURL=" + anotherVar`; 281 | 282 | //sourceCode, sourcePath, appRoot 283 | result = fix(code, nmz('/some/folder/node_modules/package/code.js'), nmz('/some/folder')); 284 | expect(result).to.equal(expected); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/compiler-valid-invalid.js: -------------------------------------------------------------------------------- 1 | import './support'; 2 | 3 | import pify from 'pify'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import mimeTypes from '@paulcbetts/mime-types'; 7 | 8 | const pfs = pify(fs); 9 | 10 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 11 | let allFixtureFiles = fs.readdirSync(fixtureDir) 12 | .filter((x) => x.match(/invalid\./i)); 13 | 14 | let mimeTypesToTest = allFixtureFiles.reduce((acc,x) => { 15 | if (global.compilersByMimeType[mimeTypes.lookup(x) || '__nope__']) { 16 | acc.push(mimeTypes.lookup(x)); 17 | } 18 | 19 | return acc; 20 | }, []); 21 | 22 | const expectedMimeTypeSpecialCases = { 23 | 'text/less': 'text/css', 24 | 'text/jade': 'text/html', 25 | 'text/cson': 'application/json', 26 | 'text/css': 'text/css', 27 | 'text/stylus': 'text/css', 28 | 'text/scss': 'text/css', 29 | 'text/sass': 'text/css', 30 | 'text/vue': 'application/javascript' 31 | }; 32 | 33 | const mimeTypesWithoutSourceMapSupport = [ 34 | 'text/jade', 35 | 'text/cson', 36 | 'text/css', 37 | 'text/sass', 38 | 'text/scss', 39 | 'text/graphql' 40 | ]; 41 | 42 | const compilerOptionsForMimeType = { 43 | 'application/javascript': { 44 | "presets": ["es2016-node5"], 45 | "plugins": ["transform-async-to-generator"], 46 | "sourceMaps": "inline" 47 | }, 48 | 49 | 'text/jsx': { 50 | "presets": ["es2016-node5", "react"], 51 | "plugins": ["transform-async-to-generator"], 52 | "sourceMaps": "inline" 53 | }, 54 | 55 | 'text/stylus': { 56 | 'import': ['nib'], 57 | 'sourcemap': 'inline' 58 | } 59 | }; 60 | 61 | const mimeTypesWithDependentFilesSupport = [ 62 | 'text/scss', 63 | 'text/sass', 64 | 'text/less', 65 | 'text/stylus', 66 | ]; 67 | 68 | for (let mimeType of mimeTypesToTest) { 69 | let klass = global.compilersByMimeType[mimeType]; 70 | 71 | describe(`The ${klass.name} class for ${mimeType}`, function() { 72 | beforeEach(function() { 73 | if ('createFromCompilers' in klass) { 74 | let innerCompilers = Object.keys(global.compilersByMimeType).reduce((acc, x) => { 75 | if ('createFromCompilers' in global.compilersByMimeType[x]) return acc; 76 | 77 | acc[x] = new global.compilersByMimeType[x](); 78 | if (x in compilerOptionsForMimeType) acc[x].compilerOptions = compilerOptionsForMimeType[x]; 79 | return acc; 80 | }, {}); 81 | 82 | this.fixture = klass.createFromCompilers(innerCompilers); 83 | } else { 84 | this.fixture = new klass(); 85 | } 86 | 87 | if (mimeType in compilerOptionsForMimeType) { 88 | this.fixture.compilerOptions = compilerOptionsForMimeType[mimeType]; 89 | } 90 | }); 91 | 92 | it(`should compile the valid ${mimeType} file`, async function() { 93 | let ext = mimeTypes.extension(mimeType); 94 | let input = path.join(__dirname, '..', 'test', 'fixtures', `valid.${ext}`); 95 | 96 | let ctx = {}; 97 | let shouldCompile = await this.fixture.shouldCompileFile(input, ctx); 98 | expect(shouldCompile).to.be.ok; 99 | 100 | let source = await pfs.readFile(input, 'utf8'); 101 | // let dependentFiles = await this.fixture.determineDependentFiles(source, input, ctx); 102 | // expect(dependentFiles.length).to.equal(0); 103 | 104 | let result = await this.fixture.compile(source, input, ctx); 105 | let expectedMimeType = expectedMimeTypeSpecialCases[mimeType] || 'application/javascript'; 106 | 107 | expect(result.mimeType).to.equal(expectedMimeType); 108 | 109 | // NB: Some compilers don't do source maps 110 | if (!mimeTypesWithoutSourceMapSupport.includes(mimeType)) { 111 | let lines = result.code.split('\n'); 112 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).to.be.ok; 113 | } 114 | 115 | expect(typeof(result.code)).to.equal('string'); 116 | }); 117 | 118 | it(`should fail the invalid ${mimeType} file`, async function() { 119 | let ext = mimeTypes.extension(mimeType); 120 | let input = path.join(__dirname, '..', 'test', 'fixtures', `invalid.${ext}`); 121 | 122 | let ctx = {}; 123 | let shouldCompile = await this.fixture.shouldCompileFile(input, ctx); 124 | expect(shouldCompile).to.be.ok; 125 | 126 | let source = await pfs.readFile(input, 'utf8'); 127 | // let dependentFiles = await this.fixture.determineDependentFiles(source, input, ctx); 128 | // expect(dependentFiles.length).to.equal(0); 129 | 130 | let result = this.fixture.compile(source, input, ctx); 131 | expect(result).to.eventually.throw(); 132 | }); 133 | 134 | if (mimeTypesWithDependentFilesSupport.includes(mimeType)) { 135 | it(`should return a non-empty array of dependent files if a file has dependencies`, async function() { 136 | let ext = mimeTypes.extension(mimeType); 137 | let input = path.join(__dirname, '..', 'test', 'fixtures', `file-with-dependencies.${ext}`); 138 | 139 | let ctx = {}; 140 | let source = await pfs.readFile(input, 'utf8'); 141 | let dependentFiles = await this.fixture.determineDependentFiles(source, input, ctx); 142 | expect(dependentFiles).to.have.length.above(0); 143 | }); 144 | } 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /test/config-parser.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mkdirp from 'mkdirp'; 4 | import rimraf from 'rimraf'; 5 | 6 | import { 7 | createCompilers, 8 | createCompilerHostFromConfiguration, 9 | createCompilerHostFromConfigFile, 10 | createCompilerHostFromBabelRc 11 | } from '../src/config-parser'; 12 | 13 | const d = require('debug')('test:config-parser'); 14 | 15 | let testCount = 0; 16 | 17 | describe('the configuration parser module', function() { 18 | this.timeout(10 * 1000); 19 | 20 | describe('the createCompilers method', function() { 21 | it('should return compilers', function() { 22 | let result = createCompilers(); 23 | expect(Object.keys(result).length > 0).to.be.ok; 24 | }); 25 | 26 | it('should definitely have these compilers', function() { 27 | let result = createCompilers(); 28 | 29 | expect(result['application/javascript']).to.be.ok; 30 | expect(result['text/less']).to.be.ok; 31 | }); 32 | }); 33 | 34 | describe('the createCompilerHostFromConfiguration method', function() { 35 | beforeEach(function() { 36 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 37 | mkdirp.sync(this.tempCacheDir); 38 | }); 39 | 40 | afterEach(function() { 41 | rimraf.sync(this.tempCacheDir); 42 | }); 43 | 44 | it('respects suppressing source maps (scenario test)', async function() { 45 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 46 | 47 | let result = createCompilerHostFromConfiguration({ 48 | appRoot: fixtureDir, 49 | rootCacheDir: this.tempCacheDir, 50 | options: { 51 | 'application/javascript': { 52 | "presets": ["es2016-node5"], 53 | "sourceMaps": false 54 | } 55 | } 56 | }); 57 | 58 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 59 | d(JSON.stringify(compileInfo)); 60 | 61 | expect(compileInfo.mimeType).to.equal('application/javascript'); 62 | 63 | let lines = compileInfo.code.split('\n'); 64 | expect(lines.length > 5).to.be.ok; 65 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 66 | }); 67 | 68 | it('creates a no-op compiler when passthrough is set for a mime type', async function() { 69 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 70 | 71 | let sourceFilePath = path.join(fixtureDir, 'valid.js'); 72 | let sourceFile = fs.readFileSync(sourceFilePath); 73 | 74 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-passthrough')); 75 | 76 | let compileInfo = await result.compile(sourceFilePath); 77 | d(JSON.stringify(compileInfo)); 78 | 79 | expect(compileInfo.mimeType).to.equal('application/javascript'); 80 | 81 | if (compileInfo.code) { 82 | expect(compileInfo.code).to.deep.equal(sourceFile.toString()); 83 | } else { 84 | expect(compileInfo.binaryData).to.equal(sourceFile); 85 | } 86 | }); 87 | }); 88 | 89 | describe('the createCompilerHostFromBabelRc method', function() { 90 | beforeEach(function() { 91 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 92 | mkdirp.sync(this.tempCacheDir); 93 | }); 94 | 95 | afterEach(function() { 96 | rimraf.sync(this.tempCacheDir); 97 | if ('BABEL_ENV' in process.env) { 98 | delete process.env.ELECTRON_COMPILE_ENV; 99 | } 100 | }); 101 | 102 | it('reads from an environment-free file', async function() { 103 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 104 | 105 | let result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-noenv')); 106 | 107 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 108 | d(JSON.stringify(compileInfo)); 109 | 110 | expect(compileInfo.mimeType).to.equal('application/javascript'); 111 | 112 | let lines = compileInfo.code.split('\n'); 113 | expect(lines.length > 5).to.be.ok; 114 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).to.be.ok; 115 | }); 116 | 117 | it('uses the development env when env is unset', async function() { 118 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 119 | let env = process.env.NODE_ENV; 120 | delete process.env.NODE_ENV; 121 | 122 | let result; 123 | try { 124 | result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-production')); 125 | } finally { 126 | process.env.NODE_ENV = env; 127 | } 128 | 129 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 130 | d(JSON.stringify(compileInfo)); 131 | 132 | expect(compileInfo.mimeType).to.equal('application/javascript'); 133 | 134 | let lines = compileInfo.code.split('\n'); 135 | expect(lines.length > 5).to.be.ok; 136 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).to.be.ok; 137 | }); 138 | 139 | it('uses the production env when env is set', async function() { 140 | process.env.BABEL_ENV = 'production'; 141 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 142 | 143 | let result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-production')); 144 | 145 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 146 | d(JSON.stringify(compileInfo)); 147 | 148 | expect(compileInfo.mimeType).to.equal('application/javascript'); 149 | 150 | let lines = compileInfo.code.split('\n'); 151 | expect(lines.length > 5).to.be.ok; 152 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 153 | }); 154 | }); 155 | 156 | describe('the createCompilerHostFromConfigFile method', function() { 157 | beforeEach(function() { 158 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 159 | mkdirp.sync(this.tempCacheDir); 160 | }); 161 | 162 | afterEach(function() { 163 | rimraf.sync(this.tempCacheDir); 164 | if ('ELECTRON_COMPILE_ENV' in process.env) { 165 | delete process.env.ELECTRON_COMPILE_ENV; 166 | } 167 | }); 168 | 169 | it('reads from an environment-free file', async function() { 170 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 171 | 172 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-noenv')); 173 | 174 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 175 | d(JSON.stringify(compileInfo)); 176 | 177 | expect(compileInfo.mimeType).to.equal('application/javascript'); 178 | 179 | let lines = compileInfo.code.split('\n'); 180 | expect(lines.length > 5).to.be.ok; 181 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).to.be.ok; 182 | }); 183 | 184 | it('uses the development env when env is unset', async function() { 185 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 186 | let env = process.env.NODE_ENV; 187 | delete process.env.NODE_ENV; 188 | 189 | let result; 190 | try { 191 | result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-production')); 192 | } finally { 193 | process.env.NODE_ENV = env; 194 | } 195 | 196 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 197 | d(JSON.stringify(compileInfo)); 198 | 199 | expect(compileInfo.mimeType).to.equal('application/javascript'); 200 | 201 | let lines = compileInfo.code.split('\n'); 202 | expect(lines.length > 5).to.be.ok; 203 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).to.be.ok; 204 | }); 205 | 206 | it('uses the production env when env is set', async function() { 207 | process.env.ELECTRON_COMPILE_ENV = 'production'; 208 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 209 | 210 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-production')); 211 | 212 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 213 | d(JSON.stringify(compileInfo)); 214 | 215 | expect(compileInfo.mimeType).to.equal('application/javascript'); 216 | 217 | let lines = compileInfo.code.split('\n'); 218 | expect(lines.length > 5).to.be.ok; 219 | expect(lines.find((x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/electron-app/.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "presets": ["es2016-node5"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/electron-app/README.md: -------------------------------------------------------------------------------- 1 | MP3 Encoder 2 | =============== 3 | 4 | Dummy app to test electron using an external process, coffee-script 5 | and backbone on both windows and OSX 6 | -------------------------------------------------------------------------------- /test/electron-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | MP3 Encoder 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 |

Dummy MP3 encoder

16 |

Pick up a .wav file

17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /test/electron-app/lib/main.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | var EncodeFormView, Encoder, chmodSync, platform, spawn, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | $(function() { 8 | var form; 9 | form = new EncodeFormView({ 10 | logsElem: $("div.logs") 11 | }); 12 | return form.render().$el.appendTo($("div.form")); 13 | }); 14 | 15 | spawn = require("child_process").spawn; 16 | 17 | platform = require("os").platform; 18 | 19 | chmodSync = require("fs").chmodSync; 20 | 21 | Encoder = (function() { 22 | function Encoder(_arg) { 23 | this.source = _arg.source, this.bitrate = _arg.bitrate, this.target = _arg.target, this.log = _arg.log; 24 | this.target || (this.target = this.source.replace(/\.wav$/, ".mp3")); 25 | if (this.target === this.source) { 26 | this.target = this.target.replace(/\.([^.]+)$/, ' encoded.$1'); 27 | } 28 | this.bitrate || (this.bitrate = 128); 29 | switch (platform()) { 30 | case "darwin": 31 | this.pathToBin = __dirname + "/vendor/bin/osx/shineenc"; 32 | break; 33 | case "win32": 34 | this.pathToBin = __dirname + "/vendor/bin/win32/shineenc.exe"; 35 | } 36 | chmodSync(this.pathToBin, 0x1ed); 37 | } 38 | 39 | Encoder.prototype.process = function() { 40 | this.log("Starting encoding process.."); 41 | this.child = spawn(this.pathToBin, ["-b", this.bitrate, this.source, this.target]); 42 | this.child.stdout.on("data", (function(_this) { 43 | return function(data) { 44 | return _this.log("" + (data.toString().replace(/\n/g, '
'))); 45 | }; 46 | })(this)); 47 | this.child.stderr.on("data", (function(_this) { 48 | return function(data) { 49 | return _this.log("ERROR: " + (data.toString().replace(/\n/g, '
')), 'error'); 50 | }; 51 | })(this)); 52 | return this.child.on("exit", (function(_this) { 53 | return function(code) { 54 | var style; 55 | style = code === 0 ? 'good' : 'error'; 56 | return _this.log("Encoding process exited with code: " + code, style); 57 | }; 58 | })(this)); 59 | }; 60 | 61 | return Encoder; 62 | 63 | })(); 64 | 65 | EncodeFormView = (function(_super) { 66 | __extends(EncodeFormView, _super); 67 | 68 | function EncodeFormView() { 69 | return EncodeFormView.__super__.constructor.apply(this, arguments); 70 | } 71 | 72 | EncodeFormView.prototype.tagName = "form"; 73 | 74 | EncodeFormView.prototype.events = { 75 | "submit": "onSubmit" 76 | }; 77 | 78 | EncodeFormView.prototype.initialize = function(_arg) { 79 | this.logsElem = _arg.logsElem; 80 | }; 81 | 82 | EncodeFormView.prototype.render = function() { 83 | this.$el.html("File:
\nBitrate:
\n"); 84 | return this; 85 | }; 86 | 87 | EncodeFormView.prototype.onSubmit = function(e) { 88 | var data; 89 | e.preventDefault(); 90 | data = { 91 | source: this.$("input.file").val(), 92 | bitrate: parseInt(this.$("input.bitrate").val()), 93 | log: (function(_this) { 94 | return function(data, type) { 95 | if (type == null) { 96 | type = 'info'; 97 | } 98 | return _this.logsElem.html("" + (_this.logsElem.html()) + "
" + data + ""); 99 | }; 100 | })(this) 101 | }; 102 | if (data.source == null) { 103 | return console.log("no file!"); 104 | } 105 | return (new Encoder(data)).process(); 106 | }; 107 | 108 | return EncodeFormView; 109 | 110 | })(Backbone.View); 111 | 112 | }).call(this); 113 | -------------------------------------------------------------------------------- /test/electron-app/main.js: -------------------------------------------------------------------------------- 1 | var app = require('app'); // Module to control application life. 2 | var BrowserWindow = require('browser-window'); // Module to create native browser window. 3 | 4 | // Report crashes to our server. 5 | require('crash-reporter').start(); 6 | 7 | // Keep a global reference of the window object, if you don't, the window will 8 | // be closed automatically when the javascript object is GCed. 9 | var mainWindow = null; 10 | 11 | // Quit when all windows are closed. 12 | app.on('window-all-closed', function() { 13 | if (process.platform != 'darwin') 14 | app.quit(); 15 | }); 16 | 17 | // This method will be called when Electron has done everything 18 | // initialization and ready for creating browser windows. 19 | app.on('ready', function() { 20 | // Create the browser window. 21 | mainWindow = new BrowserWindow({width: 800, height: 600}); 22 | 23 | // and load the index.html of the app. 24 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 25 | 26 | // Emitted when the window is closed. 27 | mainWindow.on('closed', function() { 28 | // Dereference the window object, usually you would store windows 29 | // in an array if your app supports multi windows, this is the time 30 | // when you should delete the corresponding element. 31 | mainWindow = null; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/electron-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "mp3-encoder-demo", 3 | "version" : "0.1.0", 4 | "main" : "main.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/electron-app/src/main.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | form = new EncodeFormView 3 | logsElem: $("div.logs") 4 | 5 | form.render().$el.appendTo $("div.form") 6 | -------------------------------------------------------------------------------- /test/electron-app/src/utils/encoder.coffee: -------------------------------------------------------------------------------- 1 | {spawn} = require "child_process" 2 | {platform} = require "os" 3 | {chmodSync} = require "fs" 4 | 5 | 6 | class Encoder 7 | constructor: ({@source, @bitrate, @target, @log}) -> 8 | @target ||= @source.replace /\.wav$/, ".mp3" 9 | if @target == @source 10 | @target = @target.replace(/\.([^.]+)$/, ' encoded.$1') 11 | 12 | @bitrate ||= 128 13 | 14 | switch platform() 15 | when "darwin" 16 | @pathToBin = "vendor/bin/osx/shineenc" 17 | when "win32" 18 | @pathToBin = "vendor/bin/win32/shineenc.exe" 19 | 20 | # binary may not be executable due to zip compression.. 21 | chmodSync @pathToBin, 0o755 22 | 23 | process: -> 24 | @log "Starting encoding process.." 25 | 26 | @child = spawn @pathToBin, ["-b", @bitrate, @source, @target] 27 | 28 | @child.stdout.on "data", (data) => 29 | @log "#{data.toString().replace(/\n/g, '
')}" 30 | 31 | @child.stderr.on "data", (data) => 32 | @log "ERROR: #{data.toString().replace(/\n/g, '
')}", 'error' 33 | 34 | @child.on "exit", (code) => 35 | style = if code == 0 then 'good' else 'error' 36 | @log "Encoding process exited with code: #{code}", style 37 | -------------------------------------------------------------------------------- /test/electron-app/src/views/form.coffee: -------------------------------------------------------------------------------- 1 | class EncodeFormView extends Backbone.View 2 | tagName: "form" 3 | events: 4 | "submit" : "onSubmit" 5 | 6 | initialize: ({@logsElem}) -> 7 | 8 | render: -> 9 | @$el.html """ 10 | File:
11 | Bitrate:
12 | 13 | """ 14 | 15 | this 16 | 17 | onSubmit: (e) -> 18 | e.preventDefault() 19 | 20 | data = 21 | source: @$("input.file").val() 22 | bitrate: parseInt @$("input.bitrate").val() 23 | log: (data, type = 'info') => 24 | @logsElem.html "#{@logsElem.html()}
#{data}" 25 | 26 | return console.log "no file!" unless data.source? 27 | 28 | (new Encoder data).process() 29 | -------------------------------------------------------------------------------- /test/electron-smoke-test-app.js: -------------------------------------------------------------------------------- 1 | // NB: Prevent the test runner from picking this up 2 | if ('type' in process) { 3 | let {app, BrowserWindow} = require('electron'); 4 | 5 | // Quit when all windows are closed. 6 | app.on('window-all-closed', function() { 7 | app.quit(); 8 | }); 9 | 10 | app.on('ready', function() { 11 | global.mainWindow = new BrowserWindow({ 12 | width: 800, 13 | height: 600, 14 | autoHideMenuBar: true 15 | }); 16 | 17 | global.mainWindow.loadURL('file://' + __dirname + '/../test/electron-smoke-test.html'); 18 | global.mainWindow.focus(); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/electron-smoke-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | Electron 8 | 61 | 62 | 63 | 80 | 81 |

82 | 85 |

86 | 87 |

88 | To run your app with Electron, execute the following command under your 89 | Console (or Terminal): 90 |

91 | 92 | 93 | 94 |

95 | The path-to-your-app should be the path to your own Electron 96 | app, you can read the 97 | 102 | guide in Electron's 103 | 108 | on how to write one. 109 |

110 | 111 |

112 | Or you can just drag your app here to run it: 113 |

114 | 115 |
116 | Drag your app here to run it 117 |
118 | 119 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /test/electron-smoke-test.js: -------------------------------------------------------------------------------- 1 | if ('type' in process) { 2 | require('babel-polyfill'); 3 | const init = require('../src/config-parser').init; 4 | 5 | init(__dirname + '/..', './electron-smoke-test-app'); 6 | } 7 | 8 | /* 9 | const path = require('path'); 10 | const app = require('electron').app; 11 | 12 | const createCompilerHostFromProjectRootSync = require('../src/config-parser').createCompilerHostFromProjectRootSync; 13 | 14 | const registerRequireExtension = require('../src/require-hook').default; 15 | const initializeProtocolHook = require('../src/protocol-hook').initializeProtocolHook; 16 | 17 | let compilerHost = createCompilerHostFromProjectRootSync(path.join(__dirname, '..')); 18 | registerRequireExtension(compilerHost); 19 | 20 | let protoify = function() { initializeProtocolHook(compilerHost); }; 21 | if (app.isReady()) { 22 | protoify(); 23 | } else { 24 | app.on('ready', protoify); 25 | } 26 | 27 | require('./electron-smoke-test-app'); 28 | */ 29 | -------------------------------------------------------------------------------- /test/file-change-cache.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import FileChangeCache from '../src/file-change-cache'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | import pify from 'pify'; 7 | 8 | const pfs = pify(fs); 9 | 10 | describe('The file changed cache', function() { 11 | beforeEach(function() { 12 | this.fixture = new FileChangeCache(null); 13 | }); 14 | 15 | it("Correctly computes a file hash for a canned file", async function() { 16 | const expectedInfo = { 17 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 18 | hasSourceMap: false, 19 | isInNodeModules: false, 20 | isMinified: false, 21 | isFileBinary: false 22 | }; 23 | 24 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'valid.js'); 25 | let result = await this.fixture.getHashForPath(input); 26 | 27 | expect(result.sourceCode).to.be.ok; 28 | delete result.sourceCode; 29 | expect(result).to.deep.equal(expectedInfo); 30 | }); 31 | 32 | it("Correctly handles binary files", async function() { 33 | const expectedInfo = { 34 | hash: '83af4f2b5a3e2dda1a322ac75799eee337d569a5', 35 | hasSourceMap: false, 36 | isInNodeModules: false, 37 | isMinified: false, 38 | isFileBinary: true 39 | }; 40 | 41 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 42 | let result = await this.fixture.getHashForPath(input); 43 | expect(result.binaryData).to.be.ok; 44 | expect(result.binaryData.length > 16).to.be.ok; 45 | delete result.binaryData; 46 | expect(result).to.deep.equal(expectedInfo); 47 | }); 48 | 49 | it("Correctly handles webp binary files", async function() { 50 | const expectedInfo = { 51 | hash: '9018bd399fab0dd21f33b23813dad941461cf3cd', 52 | hasSourceMap: false, 53 | isInNodeModules: false, 54 | isMinified: false, 55 | isFileBinary: true 56 | }; 57 | 58 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'alsobinary.webp'); 59 | let result = await this.fixture.getHashForPath(input); 60 | 61 | expect(result.binaryData).to.be.ok; 62 | expect(result.binaryData.length > 16).to.be.ok; 63 | delete result.binaryData; 64 | expect(result).to.deep.equal(expectedInfo); 65 | }); 66 | 67 | 68 | it("Correctly computes a file hash for a canned file syncronously", function() { 69 | const expectedInfo = { 70 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 71 | hasSourceMap: false, 72 | isInNodeModules: false, 73 | isMinified: false, 74 | isFileBinary: false 75 | }; 76 | 77 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'valid.js'); 78 | let result = this.fixture.getHashForPathSync(input); 79 | 80 | expect(result.sourceCode).to.be.ok; 81 | delete result.sourceCode; 82 | expect(result).to.deep.equal(expectedInfo); 83 | }); 84 | 85 | it("Doesn't rerun the file hash if you ask for it twice", async function() { 86 | const expectedInfo = { 87 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 88 | hasSourceMap: false, 89 | isInNodeModules: false, 90 | isMinified: false, 91 | isFileBinary: false 92 | }; 93 | 94 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 95 | let result = await this.fixture.getHashForPath(input); 96 | 97 | expect(result.sourceCode).to.be.ok; 98 | delete result.sourceCode; 99 | expect(result).to.deep.equal(expectedInfo); 100 | 101 | this.fixture.calculateHashForFile = () => Promise.reject(new Error("Didn't work")); 102 | result = await this.fixture.getHashForPath(input); 103 | 104 | // NB: The file hash cache itself shouldn't hold onto file contents, it should 105 | // only opportunistically return it if it had to read the contents anyways 106 | expect(result.sourceCode).to.be.not.ok; 107 | expect(result).to.deep.equal(expectedInfo); 108 | }); 109 | 110 | it("Throws on cache misses in production mode", function() { 111 | this.fixture = new FileChangeCache(null, true); 112 | 113 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 114 | expect(this.fixture.getHashForPath(input)).to.eventually.throw(Error); 115 | }); 116 | 117 | it("Successfully saves and loads its cache information", async function() { 118 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 119 | await this.fixture.getHashForPath(input); 120 | 121 | let targetCache = path.join(__dirname, 'fileChangeCache1.json.gz'); 122 | 123 | try { 124 | await this.fixture.save(targetCache); 125 | 126 | this.fixture = await FileChangeCache.loadFromFile(targetCache, null); 127 | 128 | this.fixture.calculateHashForFile = () => Promise.reject(new Error("Didn't work")); 129 | await this.fixture.getHashForPath(input); 130 | } finally { 131 | fs.unlinkSync(targetCache); 132 | } 133 | }); 134 | 135 | it("Detects changes to files and reruns hash", async function() { 136 | const expectedInfo = { 137 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 138 | hasSourceMap: false, 139 | isInNodeModules: false, 140 | isMinified: false, 141 | isFileBinary: false 142 | }; 143 | 144 | let realInput = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 145 | let input = path.join(__dirname, 'tempfile.tmp'); 146 | let contents = await pfs.readFile(realInput); 147 | await pfs.writeFile(input, contents); 148 | 149 | let stat1 = await pfs.stat(realInput); 150 | let stat2 = await pfs.stat(input); 151 | expect(stat1.size).to.equal(stat2.size); 152 | 153 | try { 154 | let result = await this.fixture.getHashForPath(input); 155 | 156 | expect(result.sourceCode).to.be.ok; 157 | delete result.sourceCode; 158 | expect(result).to.deep.equal(expectedInfo); 159 | 160 | let fd = await pfs.open(input, 'a'); 161 | await pfs.write(fd, '\n\n\n\n'); 162 | await pfs.close(fd); 163 | 164 | // NB: Declaring these as 'var' works around a BabelJS compilation bug 165 | // where it can't deal with let + closure scoping 166 | var realCalc = this.fixture.calculateHashForFile; 167 | var hasCalledCalc = false; 168 | 169 | this.fixture.calculateHashForFile = function(...args) { 170 | hasCalledCalc = true; 171 | return realCalc(...args); 172 | }; 173 | 174 | result = await this.fixture.getHashForPath(input); 175 | 176 | expect(result.sourceCode).to.be.ok; 177 | delete result.sourceCode; 178 | 179 | expect(result).not.to.deep.equal(expectedInfo); 180 | expect(hasCalledCalc).to.be.ok; 181 | } finally { 182 | fs.unlinkSync(input); 183 | } 184 | }); 185 | 186 | it("Successfully finds if a file has a source map", async function() { 187 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'source_map.js'); 188 | let result = await this.fixture.getHashForPath(input); 189 | 190 | expect(result.hasSourceMap).to.be.ok; 191 | }); 192 | 193 | it("Successfully finds if a file has a source map synchronously", function() { 194 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'source_map.js'); 195 | let result = this.fixture.getHashForPathSync(input); 196 | 197 | expect(result.hasSourceMap).to.be.ok; 198 | }); 199 | 200 | it("Successfully finds if a file is minified", async function() { 201 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'minified.js'); 202 | let result = await this.fixture.getHashForPath(input); 203 | 204 | expect(result.isMinified).to.be.ok; 205 | }); 206 | 207 | it("Successfully finds if a file is in node_modules", async function() { 208 | let input = path.join(__dirname, '..', 'node_modules', 'electron-compilers', 'package.json'); 209 | let result = await this.fixture.getHashForPath(input); 210 | 211 | expect(result.isInNodeModules).to.be.ok; 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/fixtures/_partial.scss: -------------------------------------------------------------------------------- 1 | $background: red; -------------------------------------------------------------------------------- /test/fixtures/alsobinary.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-compile/744cf09ed4aa28f8507ba1e6a39f0977993214dc/test/fixtures/alsobinary.webp -------------------------------------------------------------------------------- /test/fixtures/babelrc-noenv: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2016-node5", "react"], 3 | "sourceMaps": "inline" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/babelrc-production: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "presets": ["es2016-node5", "react"], 5 | "sourceMaps": false 6 | }, 7 | "development": { 8 | "presets": ["es2016-node5", "react"], 9 | "sourceMaps": "inline" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/binaryfile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-compile/744cf09ed4aa28f8507ba1e6a39f0977993214dc/test/fixtures/binaryfile.zip -------------------------------------------------------------------------------- /test/fixtures/compilerc-noenv: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "presets": ["es2016-node5", "react"], 4 | "sourceMaps": "inline" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/compilerc-passthrough: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "passthrough": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/compilerc-production: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "application/javascript": { 5 | "presets": ["es2016-node5", "react"], 6 | "sourceMaps": false 7 | } 8 | }, 9 | "development": { 10 | "application/javascript": { 11 | "presets": ["es2016-node5", "react"], 12 | "sourceMaps": "inline" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/dependency_a.less: -------------------------------------------------------------------------------- 1 | @base: red; -------------------------------------------------------------------------------- /test/fixtures/dependency_a.sass: -------------------------------------------------------------------------------- 1 | $importedcolor: red; -------------------------------------------------------------------------------- /test/fixtures/dependency_a.scss: -------------------------------------------------------------------------------- 1 | @import 'dependency_b'; 2 | 3 | $base: red; -------------------------------------------------------------------------------- /test/fixtures/dependency_a.styl: -------------------------------------------------------------------------------- 1 | font-size = 14px -------------------------------------------------------------------------------- /test/fixtures/dependency_b.scss: -------------------------------------------------------------------------------- 1 | $black: black; -------------------------------------------------------------------------------- /test/fixtures/file-with-dependencies.less: -------------------------------------------------------------------------------- 1 | @import 'dependency_a'; 2 | 3 | .box { 4 | color: darken(@base, 40%); 5 | background-color: lighten(@base, 40%); 6 | border: darken(@base, 10%) 2px solid; 7 | padding: 1em; 8 | margin: 0 auto; 9 | } -------------------------------------------------------------------------------- /test/fixtures/file-with-dependencies.sass: -------------------------------------------------------------------------------- 1 | @import './dependency_a.sass' 2 | 3 | #main 4 | color: $importedcolor 5 | font-size: 0.3em 6 | 7 | a 8 | font: 9 | weight: bold 10 | family: serif 11 | &:hover 12 | background-color: #eee 13 | -------------------------------------------------------------------------------- /test/fixtures/file-with-dependencies.scss: -------------------------------------------------------------------------------- 1 | @import './dependency_a.scss'; 2 | @import 'partial'; 3 | 4 | .box { 5 | color: darken($base, 40%); 6 | background-color: lighten($base, 40%); 7 | border: darken($base, 10%) 2px solid; 8 | padding: 1em; 9 | margin: 0 auto; 10 | } 11 | 12 | body { 13 | background-color: $background; 14 | color: $black; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/file-with-dependencies.styl: -------------------------------------------------------------------------------- 1 | @import 'dependency_a'; 2 | 3 | body 4 | font font-size Arial, sans-serif -------------------------------------------------------------------------------- /test/fixtures/inline-valid-2.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 70 | 74 | 75 | 165 | -------------------------------------------------------------------------------- /test/fixtures/inline-valid-3.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 32 | 33 | 79 | 83 | 84 | 174 | -------------------------------------------------------------------------------- /test/fixtures/inline-valid.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 28 | 29 | 48 | 49 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /test/fixtures/invalid.coffee: -------------------------------------------------------------------------------- 1 | element(by.model('query.address')).sendKeys('947') -------------------------------------------------------------------------------- /test/fixtures/invalid.cson: -------------------------------------------------------------------------------- 1 | const class = extend("isn't this JS? lol"); 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-compile/744cf09ed4aa28f8507ba1e6a39f0977993214dc/test/fixtures/invalid.css -------------------------------------------------------------------------------- /test/fixtures/invalid.graphql: -------------------------------------------------------------------------------- 1 | query invalidQuery { 2 | something { 3 | } -------------------------------------------------------------------------------- /test/fixtures/invalid.jade: -------------------------------------------------------------------------------- 1 | #{myMixin 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | WAOEIFKAOWEFKAWOEF@#@#@#@#@#@ 4 | -------------------------------------------------------------------------------- /test/fixtures/invalid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
Hello World!
6 | } 7 | }); -------------------------------------------------------------------------------- /test/fixtures/invalid.less: -------------------------------------------------------------------------------- 1 | @bodyColor: darken(@bodyColor, 30%); 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.sass: -------------------------------------------------------------------------------- 1 | /* this has curly braces! */ 2 | #navbar { 3 | width: 80%; 4 | height: 23px; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/invalid.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: $green; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/invalid.styl: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | WAOEIFKAOWEFKAWOEF@#@#@#@#@#@ 4 | -------------------------------------------------------------------------------- /test/fixtures/invalid.ts: -------------------------------------------------------------------------------- 1 | class Greeter { 2 | constructor(public greeting: string) { } 3 | greet() { 4 | return "

" + this.greeting + "

"; 5 | } 6 | }; 7 | 8 | // NB: This is not of type String! 9 | var greeter = new Greeter({}); 10 | -------------------------------------------------------------------------------- /test/fixtures/invalid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
Hello World!
6 | } 7 | }); -------------------------------------------------------------------------------- /test/fixtures/invalid.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/fixtures/protourlrigging_1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/protourlrigging_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hello World!

5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/roboto.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/source_map.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { 2 | value: true 3 | }); 4 | 5 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 10 | 11 | var _libStore = require('../src/store'); 12 | 13 | var _libStore2 = _interopRequireDefault(_libStore); 14 | 15 | var _lodash = require('lodash'); 16 | 17 | var _lodash2 = _interopRequireDefault(_lodash); 18 | 19 | var _utilsFillShape = require('../utils/fill-shape'); 20 | 21 | var _utilsFillShape2 = _interopRequireDefault(_utilsFillShape); 22 | 23 | var WindowStore = (function () { 24 | function WindowStore() { 25 | _classCallCheck(this, WindowStore); 26 | 27 | this.MAIN_WINDOW = 'MAIN_WINDOW'; 28 | this.SINGLE_TEAM = 'SINGLE_TEAM'; 29 | this.NOTIFICATIONS = 'NOTIFICATIONS'; 30 | this.SSB = 'SSB'; 31 | } 32 | 33 | _createClass(WindowStore, [{ 34 | key: 'getWindows', 35 | value: function getWindows() { 36 | return _libStore2['default'].getEntry('windows'); 37 | } 38 | }, { 39 | key: 'getWindow', 40 | value: function getWindow(id) { 41 | return this.getWindows()[id]; 42 | } 43 | }, { 44 | key: 'getWindowData', 45 | value: function getWindowData(type, params) { 46 | return (0, _utilsFillShape2['default'])(_libStore2['default'].getState(), this.getWindowShape(type, params)); 47 | } 48 | }, { 49 | key: 'getWindowShape', 50 | value: function getWindowShape(type, params) { 51 | switch (type) { 52 | case this.MAIN_WINDOW: 53 | return { 54 | app: true, 55 | settings: true, 56 | teams: true, 57 | events: true 58 | }; 59 | 60 | case this.SINGLE_TEAM: 61 | // params=teamId 62 | var shape = { 63 | app: true, 64 | settings: true, 65 | teams: {} 66 | }; 67 | shape.teams[params] = true; 68 | return shape; 69 | 70 | case this.NOTIFICATIONS: 71 | return { 72 | notifications: true, 73 | teams: true 74 | }; 75 | } 76 | return {}; 77 | } 78 | }, { 79 | key: 'addWindow', 80 | value: function addWindow(windowList, newWindow, type, params) { 81 | var update = {}; 82 | update[newWindow.id] = { 83 | window: newWindow, 84 | type: type, 85 | params: params 86 | }; 87 | return _lodash2['default'].assign({}, windowList, update); 88 | } 89 | }, { 90 | key: 'getShapeForWindow', 91 | value: function getShapeForWindow(winId) { 92 | var windowData = this.getWindows()[winId]; 93 | return this.getWindowShape(windowData.type, windowData.params); 94 | } 95 | }, { 96 | key: 'reduce', 97 | value: function reduce(windows, action) { 98 | if (windows === undefined) windows = {}; 99 | 100 | switch (action.type) { 101 | case 'ADD_WINDOW': 102 | return this.addWindow(windows, action.data.newWindow, action.data.windowType, action.data.params); 103 | default: 104 | return windows; 105 | } 106 | } 107 | }]); 108 | 109 | return WindowStore; 110 | })(); 111 | 112 | exports['default'] = new WindowStore(); 113 | module.exports = exports['default']; 114 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9wYXVsL2NvZGUvdGlueXNwZWNrL3NsYWNrLXdpbnNzYi9zcmMvc3RvcmVzL3dpbmRvdy1zdG9yZS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7O3dCQUFrQixjQUFjOzs7O3NCQUNsQixRQUFROzs7OzhCQUNBLHFCQUFxQjs7OztJQUVyQyxXQUFXO1dBQVgsV0FBVzswQkFBWCxXQUFXOztTQUVmLFdBQVcsR0FBRyxhQUFhO1NBQzNCLFdBQVcsR0FBRyxhQUFhO1NBQzNCLGFBQWEsR0FBRyxlQUFlO1NBQy9CLEdBQUcsR0FBRyxLQUFLOzs7ZUFMUCxXQUFXOztXQU9MLHNCQUFHO0FBQ1gsYUFBTyxzQkFBTSxRQUFRLENBQUMsU0FBUyxDQUFDLENBQUM7S0FDbEM7OztXQUVRLG1CQUFDLEVBQUUsRUFBRTtBQUNaLGFBQU8sSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0tBQzlCOzs7V0FFWSx1QkFBQyxJQUFJLEVBQUUsTUFBTSxFQUFFO0FBQzFCLGFBQU8saUNBQVUsc0JBQU0sUUFBUSxFQUFFLEVBQUUsSUFBSSxDQUFDLGNBQWMsQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQztLQUN2RTs7O1dBRWEsd0JBQUMsSUFBSSxFQUFFLE1BQU0sRUFBRTtBQUMzQixjQUFPLElBQUk7QUFDWCxhQUFLLElBQUksQ0FBQyxXQUFXO0FBQ25CLGlCQUFPO0FBQ0wsZUFBRyxFQUFFLElBQUk7QUFDVCxvQkFBUSxFQUFFLElBQUk7QUFDZCxpQkFBSyxFQUFFLElBQUk7QUFDWCxrQkFBTSxFQUFFLElBQUk7V0FDYixDQUFBOztBQUFBLEFBRUgsYUFBSyxJQUFJLENBQUMsV0FBVzs7QUFDbkIsY0FBSSxLQUFLLEdBQUc7QUFDVixlQUFHLEVBQUUsSUFBSTtBQUNULG9CQUFRLEVBQUUsSUFBSTtBQUNkLGlCQUFLLEVBQUUsRUFBRTtXQUNWLENBQUE7QUFDRCxlQUFLLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxHQUFHLElBQUksQ0FBQztBQUMzQixpQkFBTyxLQUFLLENBQUM7O0FBQUEsQUFFZixhQUFLLElBQUksQ0FBQyxhQUFhO0FBQ3JCLGlCQUFPO0FBQ0wseUJBQWEsRUFBRSxJQUFJO0FBQ25CLGlCQUFLLEVBQUUsSUFBSTtXQUNaLENBQUE7QUFBQSxPQUNGO0FBQ0QsYUFBTyxFQUFFLENBQUM7S0FDWDs7O1dBRVEsbUJBQUMsVUFBVSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFO0FBQzdDLFVBQUksTUFBTSxHQUFHLEVBQUUsQ0FBQztBQUNoQixZQUFNLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQyxHQUFHO0FBQ3JCLGNBQU0sRUFBRSxTQUFTO0FBQ2pCLFlBQUksRUFBRSxJQUFJO0FBQ1YsY0FBTSxFQUFFLE1BQU07T0FDZixDQUFDO0FBQ0YsYUFBTyxvQkFBRSxNQUFNLENBQUMsRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLENBQUMsQ0FBQztLQUN6Qzs7O1dBRWdCLDJCQUFDLEtBQUssRUFBRTtBQUN2QixVQUFJLFVBQVUsR0FBRyxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDMUMsYUFBTyxJQUFJLENBQUMsY0FBYyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0tBQ2hFOzs7V0FFSyxnQkFBQyxPQUFPLEVBQU8sTUFBTSxFQUFFO1VBQXRCLE9BQU8sZ0JBQVAsT0FBTyxHQUFHLEVBQUU7O0FBQ2pCLGNBQU8sTUFBTSxDQUFDLElBQUk7QUFDaEIsYUFBSyxZQUFZO0FBQ2YsaUJBQU8sSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUFBLEFBQ3BHO0FBQ0UsaUJBQU8sT0FBTyxDQUFDO0FBQUEsT0FDbEI7S0FDRjs7O1NBckVHLFdBQVc7OztxQkF3RUYsSUFBSSxXQUFXLEVBQUUiLCJmaWxlIjoiL1VzZXJzL3BhdWwvY29kZS90aW55c3BlY2svc2xhY2std2luc3NiL3NyYy9zdG9yZXMvd2luZG93LXN0b3JlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFN0b3JlIGZyb20gJy4uL2xpYi9zdG9yZSc7XG5pbXBvcnQgXyBmcm9tICdsb2Rhc2gnO1xuaW1wb3J0IGZpbGxTaGFwZSBmcm9tICcuLi91dGlscy9maWxsLXNoYXBlJztcblxuY2xhc3MgV2luZG93U3RvcmUge1xuXG4gIE1BSU5fV0lORE9XID0gJ01BSU5fV0lORE9XJztcbiAgU0lOR0xFX1RFQU0gPSAnU0lOR0xFX1RFQU0nO1xuICBOT1RJRklDQVRJT05TID0gJ05PVElGSUNBVElPTlMnO1xuICBTU0IgPSAnU1NCJztcblxuICBnZXRXaW5kb3dzKCkge1xuICAgIHJldHVybiBTdG9yZS5nZXRFbnRyeSgnd2luZG93cycpO1xuICB9XG5cbiAgZ2V0V2luZG93KGlkKSB7XG4gICAgcmV0dXJuIHRoaXMuZ2V0V2luZG93cygpW2lkXTtcbiAgfVxuXG4gIGdldFdpbmRvd0RhdGEodHlwZSwgcGFyYW1zKSB7XG4gICAgcmV0dXJuIGZpbGxTaGFwZShTdG9yZS5nZXRTdGF0ZSgpLCB0aGlzLmdldFdpbmRvd1NoYXBlKHR5cGUsIHBhcmFtcykpO1xuICB9XG5cbiAgZ2V0V2luZG93U2hhcGUodHlwZSwgcGFyYW1zKSB7XG4gICAgc3dpdGNoKHR5cGUpIHtcbiAgICBjYXNlIHRoaXMuTUFJTl9XSU5ET1c6XG4gICAgICByZXR1cm4ge1xuICAgICAgICBhcHA6IHRydWUsXG4gICAgICAgIHNldHRpbmdzOiB0cnVlLFxuICAgICAgICB0ZWFtczogdHJ1ZSxcbiAgICAgICAgZXZlbnRzOiB0cnVlXG4gICAgICB9XG5cbiAgICBjYXNlIHRoaXMuU0lOR0xFX1RFQU06IC8vIHBhcmFtcz10ZWFtSWRcbiAgICAgIGxldCBzaGFwZSA9IHtcbiAgICAgICAgYXBwOiB0cnVlLFxuICAgICAgICBzZXR0aW5nczogdHJ1ZSxcbiAgICAgICAgdGVhbXM6IHt9XG4gICAgICB9XG4gICAgICBzaGFwZS50ZWFtc1twYXJhbXNdID0gdHJ1ZTtcbiAgICAgIHJldHVybiBzaGFwZTtcblxuICAgIGNhc2UgdGhpcy5OT1RJRklDQVRJT05TOlxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgbm90aWZpY2F0aW9uczogdHJ1ZSxcbiAgICAgICAgdGVhbXM6IHRydWVcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHt9O1xuICB9XG5cbiAgYWRkV2luZG93KHdpbmRvd0xpc3QsIG5ld1dpbmRvdywgdHlwZSwgcGFyYW1zKSB7XG4gICAgbGV0IHVwZGF0ZSA9IHt9O1xuICAgIHVwZGF0ZVtuZXdXaW5kb3cuaWRdID0ge1xuICAgICAgd2luZG93OiBuZXdXaW5kb3csXG4gICAgICB0eXBlOiB0eXBlLFxuICAgICAgcGFyYW1zOiBwYXJhbXNcbiAgICB9O1xuICAgIHJldHVybiBfLmFzc2lnbih7fSwgd2luZG93TGlzdCwgdXBkYXRlKTtcbiAgfVxuXG4gIGdldFNoYXBlRm9yV2luZG93KHdpbklkKSB7XG4gICAgbGV0IHdpbmRvd0RhdGEgPSB0aGlzLmdldFdpbmRvd3MoKVt3aW5JZF07XG4gICAgcmV0dXJuIHRoaXMuZ2V0V2luZG93U2hhcGUod2luZG93RGF0YS50eXBlLCB3aW5kb3dEYXRhLnBhcmFtcyk7XG4gIH1cblxuICByZWR1Y2Uod2luZG93cyA9IHt9LCBhY3Rpb24pIHtcbiAgICBzd2l0Y2goYWN0aW9uLnR5cGUpIHtcbiAgICAgIGNhc2UgJ0FERF9XSU5ET1cnOlxuICAgICAgICByZXR1cm4gdGhpcy5hZGRXaW5kb3cod2luZG93cywgYWN0aW9uLmRhdGEubmV3V2luZG93LCBhY3Rpb24uZGF0YS53aW5kb3dUeXBlLCBhY3Rpb24uZGF0YS5wYXJhbXMpO1xuICAgICAgZGVmYXVsdDpcbiAgICAgICAgcmV0dXJuIHdpbmRvd3M7XG4gICAgfVxuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IG5ldyBXaW5kb3dTdG9yZSgpO1xuIl19 -------------------------------------------------------------------------------- /test/fixtures/valid.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | path = require 'path' 3 | 4 | CoffeeScript = null # defer until used 5 | fs = require 'fs-plus' 6 | 7 | stats = 8 | hits: 0 9 | misses: 0 10 | cacheDirectory = null 11 | 12 | getCachePath = (coffee) -> 13 | digest = crypto.createHash('sha1').update(coffee, 'utf8').digest('hex') 14 | path.join(cacheDirectory, "#{digest}.js") 15 | 16 | getCachedJavaScript = (cachePath) -> 17 | if fs.isFileSync(cachePath) 18 | try 19 | cachedJavaScript = fs.readFileSync(cachePath, 'utf8') 20 | stats.hits++ 21 | return cachedJavaScript 22 | return 23 | 24 | convertFilePath = (filePath) -> 25 | if process.platform is 'win32' 26 | filePath = "/#{path.resolve(filePath).replace(/\\/g, '/')}" 27 | encodeURI(filePath) 28 | 29 | loadCoffeeScript = -> 30 | coffee = require 'coffee-script' 31 | 32 | # Work around for https://github.com/jashkenas/coffeescript/issues/3890 33 | coffeePrepareStackTrace = Error.prepareStackTrace 34 | if coffeePrepareStackTrace? 35 | Error.prepareStackTrace = (error, stack) -> 36 | try 37 | return coffeePrepareStackTrace(error, stack) 38 | catch coffeeError 39 | return stack 40 | 41 | coffee 42 | 43 | compileCoffeeScript = (coffee, filePath, cachePath) -> 44 | CoffeeScript ?= loadCoffeeScript() 45 | {js, v3SourceMap} = CoffeeScript.compile(coffee, filename: filePath, sourceMap: true) 46 | stats.misses++ 47 | 48 | if btoa? and unescape? and encodeURIComponent? 49 | js = """ 50 | #{js} 51 | //# sourceMappingURL=data:application/json;base64,#{btoa unescape encodeURIComponent v3SourceMap} 52 | //# sourceURL=#{convertFilePath(filePath)} 53 | """ 54 | 55 | try 56 | fs.writeFileSync(cachePath, js) 57 | js 58 | 59 | requireCoffeeScript = (module, filePath) -> 60 | coffee = fs.readFileSync(filePath, 'utf8') 61 | cachePath = getCachePath(coffee) 62 | js = getCachedJavaScript(cachePath) ? compileCoffeeScript(coffee, filePath, cachePath) 63 | module._compile(js, filePath) 64 | 65 | exports.register = -> 66 | propertyConfig = 67 | enumerable: true 68 | value: requireCoffeeScript 69 | writable: false 70 | 71 | Object.defineProperty(require.extensions, '.coffee', propertyConfig) 72 | Object.defineProperty(require.extensions, '.litcoffee', propertyConfig) 73 | Object.defineProperty(require.extensions, '.coffee.md', propertyConfig) 74 | 75 | return 76 | 77 | exports.getCacheMisses = -> stats.misses 78 | 79 | exports.getCacheHits = -> stats.hits 80 | 81 | exports.resetCacheStats = -> 82 | stats = 83 | hits: 0 84 | misses: 0 85 | 86 | exports.setCacheDirectory = (newCacheDirectory) -> 87 | cacheDirectory = newCacheDirectory 88 | 89 | exports.getCacheDirectory = -> cacheDirectory 90 | 91 | exports.addPathToCache = (filePath) -> 92 | coffee = fs.readFileSync(filePath, 'utf8') 93 | cachePath = getCachePath(coffee) 94 | compileCoffeeScript(coffee, filePath, cachePath) 95 | return -------------------------------------------------------------------------------- /test/fixtures/valid.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | label: 'Application' 4 | submenu: [ 5 | { 6 | label: 'About' 7 | command: null 8 | } 9 | ] 10 | id: 1 11 | visible: false 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /test/fixtures/valid.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | width: 100%; 5 | height: 100%; 6 | background-color: #000000; 7 | overflow: hidden; 8 | -webkit-user-select: none; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 12px; 11 | cursor: default; 12 | } 13 | 14 | .right { 15 | float: right; 16 | } 17 | 18 | .hidden { 19 | display: none; 20 | } 21 | 22 | #player { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | 27 | #drag-video { 28 | z-index: 99; 29 | position: absolute; 30 | top: 24px; 31 | bottom: 50px; 32 | left: 0; 33 | right: 0; 34 | } 35 | 36 | #splash { 37 | width: 100%; 38 | height: 100%; 39 | background-image: url('splash.gif'); 40 | background-size: 100%; 41 | background-position: -50%; 42 | background-repeat: no-repeat; 43 | -webkit-filter: grayscale(100%); 44 | } 45 | 46 | #overlay { 47 | opacity: 0; 48 | z-index: 10; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | bottom: 0; 54 | transition: opacity 0.3s ease; 55 | } 56 | 57 | #popup { 58 | opacity: 0; 59 | display: none; 60 | position: absolute; 61 | z-index: 20; 62 | right: 5px; 63 | bottom: 55px; 64 | top: 100px; 65 | width: 400px; 66 | background-color: #1F2021; 67 | border-radius: 3px; 68 | font-size: 14px; 69 | transition: opacity 0.3s ease; 70 | } 71 | 72 | #popup.chromecast #chromecast-popup { 73 | display: block; 74 | } 75 | 76 | #popup.chromecast #playlist-popup { 77 | display: none; 78 | } 79 | 80 | #popup.playlist #playlist-popup { 81 | display: block; 82 | } 83 | 84 | #popup.playlist #chromecast-popup { 85 | display: none; 86 | } 87 | 88 | #playlist-entries, #chromecast-entries { 89 | position: absolute; 90 | top: 46px; 91 | bottom: 55px; 92 | right: 0; 93 | left: 0; 94 | overflow: auto; 95 | } 96 | 97 | .playlist-entry, .chromecast-entry { 98 | color: #fff; 99 | font-size: 13px; 100 | padding: 10px 15px; 101 | overflow: hidden; 102 | text-overflow: ellipsis; 103 | white-space: nowrap; 104 | } 105 | 106 | @-webkit-keyframes spin { 107 | to { -webkit-transform: rotate(360deg); } 108 | } 109 | 110 | @-moz-keyframes spin { 111 | to { -moz-transform: rotate(360deg); } 112 | } 113 | 114 | @-ms-keyframes spin { 115 | to { -ms-transform: rotate(360deg); } 116 | } 117 | 118 | @-o-keyframes spin { 119 | to { -o-transform: rotate(360deg); } 120 | } 121 | 122 | @keyframes spin { 123 | to { transform: rotate(360deg); } 124 | } 125 | 126 | .playlist-entry .status { 127 | float: right; 128 | -webkit-animation: spin 1.5s infinite linear; 129 | -moz-animation: spin 1.5s infinite linear; 130 | -ms-animation: spin 1.5s infinite linear; 131 | -o-animation: spin 1.5s infinite linear; 132 | animation: spin 1.5s infinite linear; 133 | } 134 | 135 | .playlist-entry .status:after { 136 | -webkit-transform: rotate(45deg); 137 | -moz-transform: rotate(45deg); 138 | -ms-transform: rotate(45deg); 139 | -o-transform: rotate(45deg); 140 | transform: rotate(45deg); 141 | } 142 | 143 | .playlist-entry.odd, .chromecast-entry.odd { 144 | background-color: #222324; 145 | } 146 | 147 | .playlist-entry.selected, .chromecast-entry.selected { 148 | background-color: #31A357; 149 | } 150 | 151 | #popup .header { 152 | background-color: #363738; 153 | color: #E1E1E1; 154 | font-size: 16px; 155 | line-height: 16px; 156 | padding: 15px; 157 | border-radius: 3px 3px 0 0; 158 | } 159 | 160 | #popup .button { 161 | margin: 10px; 162 | background-color: #31A357; 163 | padding: 10px; 164 | text-align: center; 165 | color: #E1E1E1; 166 | border-radius: 3px; 167 | } 168 | 169 | #controls-timeline-tooltip { 170 | background: #1F2021; 171 | border-radius: 3px; 172 | box-shadow: 0px 1px 2px rgba(0,0,0,.2); 173 | padding: 4px 8px; 174 | color: #fff; 175 | text-align: center; 176 | font-size: 11px; 177 | position: absolute; 178 | bottom: 53px; 179 | z-index: 100; 180 | opacity: 0; 181 | transition: opacity 0.25s; 182 | } 183 | 184 | #controls-timeline-tooltip:after { 185 | width: 0; 186 | height: 0; 187 | top: 100%; 188 | left: 50%; 189 | border-left: 6px solid transparent; 190 | border-right: 6px solid transparent; 191 | border-top: 6px solid #1F2021; 192 | content: ""; 193 | margin-left: -6px; 194 | position: absolute; 195 | } 196 | 197 | #popup .button.bottom { 198 | position: absolute; 199 | bottom: 0; 200 | left: 0; 201 | right: 0; 202 | } 203 | 204 | #idle { 205 | position: absolute; 206 | top: 24px; 207 | bottom: 50px; 208 | left: 0; 209 | right: 0; 210 | } 211 | 212 | .hide-cursor { 213 | cursor: none; 214 | } 215 | 216 | .hide-cursor #overlay { 217 | opacity: 0 !important; 218 | } 219 | 220 | body:hover #overlay, body:hover .titlebar { 221 | opacity: 1; 222 | } 223 | 224 | .titlebar { 225 | background-color: #1F2021; 226 | } 227 | 228 | #controls { 229 | z-index: 11; 230 | position: absolute; 231 | left: 0; 232 | right: 0; 233 | bottom: 0; 234 | height: 50px; 235 | background-color: #1F2021; 236 | color: #727374; 237 | } 238 | 239 | #controls .center { 240 | margin-top: 13px; 241 | } 242 | 243 | #controls-timeline { 244 | background-color: #303233; 245 | height: 10px; 246 | width: 100%; 247 | } 248 | 249 | #controls-timeline-position { 250 | background-color: #31A357; 251 | width: 0%; 252 | height: 10px; 253 | transition: width 0.25s linear; 254 | } 255 | 256 | .controls-secondary { 257 | padding: 6px 10px 0 0; 258 | } 259 | 260 | #player-downloadspeed, #controls-playlist, #controls-chromecast { 261 | margin-right: 11px; 262 | } 263 | 264 | #controls-play { 265 | margin: 6px 9px 6px 14px; 266 | } 267 | 268 | #controls-play, #player-downloadspeed, #controls-fullscreen, #controls-playlist, #controls-chromecast { 269 | float: left; 270 | } 271 | 272 | #controls-play .mega-octicon, #player-downloadspeed .mega-octicon, 273 | #controls-fullscreen .mega-octicon, #controls-playlist .mega-octicon { 274 | /* this is the click buffer */ 275 | padding: 3px 6px; 276 | } 277 | 278 | #controls-play span:hover .mega-octicon, #player-downloadspeed span:hover .mega-octicon, 279 | #controls-fullscreen span:hover .mega-octicon, #controls-playlist span:hover .mega-octicon, 280 | #controls-chromecast span:hover .mega-octicon { 281 | color: #31A357; 282 | } 283 | 284 | #controls-chromecast .chromecast { 285 | background-image: url('chromecast.png'); 286 | background-size: 26px 72px; 287 | background-repeat: no-repeat; 288 | background-position: 0px 0px; 289 | margin-top: 6px; 290 | display: block; 291 | width: 26px; 292 | height: 18px; 293 | } 294 | #controls-chromecast .chromecast:hover, #controls-chromecast.selected .chromecast { 295 | background-position: 0px -18px; 296 | } 297 | .chromecasting #controls-chromecast .chromecast { 298 | background-position: 0px -36px; 299 | } 300 | .chromecasting #controls-chromecast .chromecast:hover, .chromecasting #controls-chromecast.selected .chromecast { 301 | background-position: 0px -54px; 302 | } 303 | 304 | #player-downloadspeed { 305 | margin-top: 4px; 306 | padding: 3px 20px; 307 | } 308 | 309 | #controls-playlist.selected .mega-octicon { 310 | color: #31A357; 311 | } 312 | 313 | .mega-octicon { 314 | color: #F0F0F0; 315 | font-size: 22px; 316 | } 317 | 318 | #controls-play .mega-octicon { 319 | color: #31A357; 320 | } 321 | 322 | #controls-time { 323 | width: 100px; 324 | float: left; 325 | } 326 | 327 | #controls-main { 328 | display: none; 329 | } 330 | 331 | #controls-time-current { 332 | color: #F0F0F0; 333 | } 334 | 335 | #controls-time-current, #controls-time-total { 336 | display: inline-block; 337 | min-width: 33px; 338 | } 339 | 340 | #controls-volume { 341 | margin: 11px 9px 11px 0; 342 | padding: 0; 343 | float: left; 344 | } 345 | 346 | #controls-volume-slider { 347 | -webkit-appearance: none; 348 | background: -webkit-gradient(linear, left top, right top, color-stop(50%, #31A357), color-stop(50%, #727374)); 349 | width: 50px; 350 | height: 3px; 351 | border-radius: 0px; 352 | } 353 | 354 | #controls-volume-slider::-webkit-slider-thumb { 355 | -webkit-appearance: none; 356 | background-color: #31A357; 357 | opacity: 1.0; 358 | width: 7px; 359 | height: 7px; 360 | border-radius: 3.5px; 361 | } 362 | 363 | #controls-volume-slider:focus { 364 | outline: none; 365 | } 366 | 367 | #controls-name { 368 | float: left; 369 | margin-left: 20px; 370 | } 371 | -------------------------------------------------------------------------------- /test/fixtures/valid.graphql: -------------------------------------------------------------------------------- 1 | { 2 | user(id: 5) { 3 | firstName 4 | lastName 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/valid.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | script(type='text/javascript'). 6 | if (foo) { 7 | bar(1 + 5) 8 | } 9 | body 10 | h1 Jade - node template engine 11 | #container.col 12 | if youAreUsingJade 13 | p You are amazing 14 | else 15 | p Get on it! 16 | p. 17 | Jade is a terse and simple 18 | templating language with a 19 | strong focus on performance 20 | and powerful features. 21 | -------------------------------------------------------------------------------- /test/fixtures/valid.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import crypto from 'crypto'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | import mkdirp from 'mkdirp'; 7 | import _ from 'lodash'; 8 | 9 | export default class CompileCache { 10 | constructor() { 11 | this.stats = { 12 | hits: 0, 13 | misses: 0 14 | }; 15 | 16 | this.cacheDir = null; 17 | this.jsCacheDir = null; 18 | this.seenFilePaths = {}; 19 | } 20 | 21 | getCompilerInformation() { 22 | throw new Error("Implement this in a derived class"); 23 | } 24 | 25 | compile(sourceCode, filePath, cachePath) { 26 | throw new Error("Implement this in a derived class"); 27 | } 28 | 29 | getMimeType() { 30 | throw new Error("Implement this in a derived class"); 31 | } 32 | 33 | initializeCompiler() { 34 | throw new Error("Implement this in a derived class"); 35 | } 36 | 37 | shouldCompileFile(sourceCode, fullPath) { 38 | this.ensureInitialized(); 39 | let lowerPath = fullPath.toLowerCase(); 40 | 41 | // NB: require() normally does this for us, but in our protocol hook we 42 | // need to do this ourselves 43 | return _.some( 44 | this.extensions, 45 | (ext) => lowerPath.lastIndexOf(ext) + ext.length === lowerPath.length); 46 | } 47 | 48 | /// 49 | /// shasum - Hash with an update() method. 50 | /// value - Must be a value that could be returned by JSON.parse(). 51 | /// 52 | updateDigestForJsonValue(shasum, value) { 53 | // Implmentation is similar to that of pretty-printing a JSON object, except: 54 | // * Strings are not escaped. 55 | // * No effort is made to avoid trailing commas. 56 | // These shortcuts should not affect the correctness of this function. 57 | const type = typeof(value); 58 | 59 | if (type === 'string') { 60 | shasum.update('"', 'utf8'); 61 | shasum.update(value, 'utf8'); 62 | shasum.update('"', 'utf8'); 63 | return; 64 | } 65 | 66 | if (type === 'boolean' || type === 'number') { 67 | shasum.update(value.toString(), 'utf8'); 68 | return; 69 | } 70 | 71 | if (value === null) { 72 | shasum.update('null', 'utf8'); 73 | return; 74 | } 75 | 76 | if (Array.isArray(value)) { 77 | shasum.update('[', 'utf8'); 78 | for (let i=0; i < value.length; i++) { 79 | this.updateDigestForJsonValue(shasum, value[i]); 80 | shasum.update(',', 'utf8'); 81 | } 82 | shasum.update(']', 'utf8'); 83 | return; 84 | } 85 | 86 | // value must be an object: be sure to sort the keys. 87 | let keys = Object.keys(value); 88 | keys.sort(); 89 | 90 | shasum.update('{', 'utf8'); 91 | 92 | for (let i=0; i < keys.length; i++) { 93 | this.updateDigestForJsonValue(shasum, keys[i]); 94 | shasum.update(': ', 'utf8'); 95 | this.updateDigestForJsonValue(shasum, value[keys[i]]); 96 | shasum.update(',', 'utf8'); 97 | } 98 | 99 | shasum.update('}', 'utf8'); 100 | } 101 | 102 | createDigestForCompilerInformation() { 103 | let sha1 = crypto.createHash('sha1'); 104 | this.updateDigestForJsonValue(sha1, this.getCompilerInformation()); 105 | return sha1.digest('hex'); 106 | } 107 | 108 | getCachePath(sourceCode) { 109 | let digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex'); 110 | 111 | if (!this.jsCacheDir) { 112 | this.jsCacheDir = path.join(this.cacheDir, this.createDigestForCompilerInformation()); 113 | mkdirp.sync(this.jsCacheDir); 114 | } 115 | 116 | return path.join(this.jsCacheDir, `${digest}.js`); 117 | } 118 | 119 | getCachedJavaScript(cachePath) { 120 | try { 121 | let ret = fs.readFileSync(cachePath, 'utf8'); 122 | this.stats.hits++; 123 | 124 | return ret; 125 | } catch (e) { 126 | return null; 127 | } 128 | } 129 | 130 | saveCachedJavaScript(cachePath, js) { 131 | fs.writeFileSync(cachePath, js); 132 | } 133 | 134 | // Function that obeys the contract of an entry in the require.extensions map. 135 | // Returns the transpiled version of the JavaScript code at filePath, which is 136 | // either generated on the fly or pulled from cache. 137 | loadFile(module, filePath, returnOnly=false, sourceCode=null) { 138 | this.ensureInitialized(); 139 | 140 | let fullPath = path.resolve(filePath); 141 | this.seenFilePaths[path.dirname(filePath)] = true; 142 | 143 | sourceCode = sourceCode || fs.readFileSync(filePath, 'utf8'); 144 | 145 | if (!this.shouldCompileFile(sourceCode, fullPath)) { 146 | if (returnOnly) return sourceCode; 147 | return module._compile(sourceCode, filePath); 148 | } 149 | 150 | // NB: We do all of these backflips in order to not load compilers unless 151 | // we actually end up using them, since loading them is typically fairly 152 | // expensive 153 | if (!this.compilerInformation.version) { 154 | this.compilerInformation.version = this.initializeCompiler(); 155 | } 156 | 157 | let cachePath = this.getCachePath(sourceCode); 158 | let js = this.getCachedJavaScript(cachePath); 159 | 160 | if (!js) { 161 | js = this.compile(sourceCode, filePath, cachePath); 162 | this.stats.misses++; 163 | 164 | this.saveCachedJavaScript(cachePath, js); 165 | } 166 | 167 | if (returnOnly) return js; 168 | return module._compile(js, filePath); 169 | } 170 | 171 | register() { 172 | this.ensureInitialized(); 173 | 174 | for (let i=0; i < this.extensions.length; i++) { 175 | Object.defineProperty(require.extensions, `.${this.extensions[i]}`, { 176 | enumerable: true, 177 | writable: false, 178 | value: (module, filePath) => this.loadFile(module, filePath) 179 | }); 180 | } 181 | } 182 | 183 | ensureInitialized() { 184 | if (this.extensions) return; 185 | 186 | let info = this.getCompilerInformation(); 187 | 188 | if (!info.extension && !info.extensions) { 189 | throw new Error("Compiler must register at least one extension in getCompilerInformation"); 190 | } 191 | 192 | this.extensions = (info.extensions ? info.extensions : [info.extension]); 193 | } 194 | 195 | setCacheDirectory(newCacheDir) { 196 | if (this.cacheDir === newCacheDir) return; 197 | 198 | this.cacheDir = newCacheDir; 199 | this.jsCacheDir = null; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /test/fixtures/valid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
Hello World!
6 | } 7 | }); -------------------------------------------------------------------------------- /test/fixtures/valid.less: -------------------------------------------------------------------------------- 1 | @nice-blue: #5B83AD; 2 | @light-blue: @nice-blue + #111; 3 | 4 | #header { 5 | color: @light-blue; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/valid.sass: -------------------------------------------------------------------------------- 1 | #main 2 | color: blue 3 | font-size: 0.3em 4 | 5 | a 6 | font: 7 | weight: bold 8 | family: serif 9 | &:hover 10 | background-color: #eee 11 | -------------------------------------------------------------------------------- /test/fixtures/valid.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/valid.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | 3 | list = red green blue 4 | no-colors = false 5 | 6 | body 7 | color: color for color in list if length(list) > 2 unless no-colors 8 | 9 | mixin() 10 | color: color for color in list if length(list) > 2 unless no-colors 11 | 12 | body 13 | mixin() 14 | -------------------------------------------------------------------------------- /test/fixtures/valid.ts: -------------------------------------------------------------------------------- 1 | declare module WinJS { 2 | class Promise { 3 | then(success?: (value: T) => Promise, error?: (error: any) => Promise, progress?: (progress: any) => void): Promise; 4 | } 5 | } 6 | 7 | declare module Data { 8 | export interface IListItem { 9 | itemIndex: number; 10 | key: any; 11 | data: T; 12 | group: any; 13 | isHeader: boolean; 14 | cached: boolean; 15 | isNonSourceData: boolean; 16 | preventAugmentation: boolean; 17 | } 18 | export interface IVirtualList { 19 | //removeIndices: WinJS.Promise[]>; 20 | removeIndices(indices: number[], options?: any): WinJS.Promise[]>; 21 | } 22 | export class VirtualList implements IVirtualList { 23 | //removeIndices: WinJS.Promise[]>; 24 | public removeIndices(indices: number[], options?: any): WinJS.Promise[]>; 25 | } 26 | } 27 | 28 | class Person { 29 | readonly name: string; 30 | 31 | constructor(name: string) { 32 | if (name.length < 1) { 33 | throw new Error("Empty name!"); 34 | } 35 | 36 | this.name = name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/valid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class SlackApp extends React.Component { 4 | render() { 5 | const vm = this.viewModel; 6 | const shouldShift = vm.isOpen && window.outerWidth > window.outerHeight; 7 | 8 | return 9 |
10 | 11 | 12 | 13 |

I'm in the drawer

14 |
15 | 16 |

I am the main content

17 |
18 |
; 19 | } 20 | } 21 | 22 | export function HelloWorld ({name}) { 23 | return
{`Hi ${name}`}
; 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/valid.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 22 | 23 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /test/fixtures/x-require-valid.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/inline-html-compiler.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import cheerio from 'cheerio'; 6 | import pify from 'pify'; 7 | 8 | const validInputs = [ 9 | 'inline-valid.html', 10 | 'inline-valid-2.html', 11 | 'inline-valid-3.html' 12 | ]; 13 | 14 | const pfs = pify(fs); 15 | const InlineHtmlCompiler = global.compilersByMimeType['text/html']; 16 | 17 | const d = require('debug')('test:inline-html-compiler'); 18 | 19 | describe('The inline HTML compiler', function() { 20 | beforeEach(function() { 21 | let compilers = Object.keys(global.compilersByMimeType).reduce((acc, x) => { 22 | let Klass = global.compilersByMimeType[x]; 23 | acc[x] = new Klass(); 24 | 25 | return acc; 26 | }, {}); 27 | 28 | compilers['application/javascript'].compilerOptions = { 29 | "presets": ["es2016-node5", "react"], 30 | "sourceMaps": "inline" 31 | }; 32 | 33 | compilers['text/coffeescript'].compilerOptions = { sourceMap: true }; 34 | 35 | this.fixture = InlineHtmlCompiler.createFromCompilers(compilers); 36 | }); 37 | 38 | validInputs.forEach((inputFile) => { 39 | it('should compile the valid fixture ' + inputFile, async function() { 40 | let input = path.join(__dirname, '..', 'test', 'fixtures', inputFile); 41 | 42 | let cc = {}; 43 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 44 | 45 | let code = await pfs.readFile(input, 'utf8'); 46 | let df = await this.fixture.determineDependentFiles(input, code, cc); 47 | 48 | expect(df.length).to.equal(0); 49 | 50 | let result = await this.fixture.compile(code, input, cc); 51 | expect(result.mimeType).to.equal('text/html'); 52 | 53 | let $ = cheerio.load(result.code); 54 | let tags = $('script'); 55 | expect(tags.length > 0).to.be.ok; 56 | 57 | $('script').map((__, el) => { 58 | let text = $(el).text(); 59 | if (!text || text.length < 2) return; 60 | 61 | if ($(el).attr('type').match(/handlebars/)) return; 62 | 63 | expect(text.split('\n').find((l) => l.match(/sourceMappingURL/))).to.be.ok; 64 | }); 65 | }); 66 | 67 | it('should compile the valid fixture ' + inputFile + ' synchronously', function() { 68 | let input = path.join(__dirname, '..', 'test', 'fixtures', inputFile); 69 | 70 | let cc = {}; 71 | expect(this.fixture.shouldCompileFileSync(input, cc)).to.be.ok; 72 | 73 | let code = fs.readFileSync(input, 'utf8'); 74 | let df = this.fixture.determineDependentFilesSync(input, code, cc); 75 | 76 | expect(df.length).to.equal(0); 77 | 78 | let result = this.fixture.compileSync(code, input, cc); 79 | expect(result.mimeType).to.equal('text/html'); 80 | 81 | let $ = cheerio.load(result.code); 82 | let tags = $('script'); 83 | expect(tags.length > 0).to.be.ok; 84 | 85 | $('script').map((__, el) => { 86 | let text = $(el).text(); 87 | if (!text || text.length < 2) return; 88 | 89 | d($(el).attr('type')); 90 | if ($(el).attr('type').match(/handlebars/)) return; 91 | 92 | d(text); 93 | expect(text.split('\n').find((l) => l.match(/sourceMappingURL/))).to.be.ok; 94 | }); 95 | }); 96 | }); 97 | 98 | it('should remove protocol-relative URLs because they are dumb', async function() { 99 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'roboto.html'); 100 | 101 | let cc = {}; 102 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 103 | 104 | let code = await pfs.readFile(input, 'utf8'); 105 | let df = await this.fixture.determineDependentFiles(input, code, cc); 106 | 107 | expect(df.length).to.equal(0); 108 | 109 | let result = await this.fixture.compile(code, input, cc); 110 | 111 | expect(result.code.length > 0).to.be.ok; 112 | expect(result.mimeType).to.equal('text/html'); 113 | 114 | let $ = cheerio.load(result.code); 115 | let tags = $('link'); 116 | expect(tags.length === 1).to.be.ok; 117 | expect($(tags[0]).attr('href').match(/^https/i)).to.be.ok; 118 | }); 119 | 120 | it('should canonicalize x-require paths', async function() { 121 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'x-require-valid.html'); 122 | 123 | let cc = {}; 124 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 125 | 126 | let code = await pfs.readFile(input, 'utf8'); 127 | let df = await this.fixture.determineDependentFiles(input, code, cc); 128 | 129 | expect(df.length).to.equal(0); 130 | 131 | let result = await this.fixture.compile(code, input, cc); 132 | 133 | expect(result.code.length > 0).to.be.ok; 134 | expect(result.mimeType).to.equal('text/html'); 135 | 136 | let $ = cheerio.load(result.code); 137 | let tags = $('x-require'); 138 | expect(tags.length === 1).to.be.ok; 139 | 140 | $('x-require').map((__, el) => { 141 | let src = $(el).attr('src'); 142 | expect(src.split(/[\\\/]/).find((x) => x === '.' || x === '..')).not.to.be.ok; 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/packager-cli.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import sfs from 'fs'; 4 | import path from 'path'; 5 | import rimraf from 'rimraf'; 6 | import mkdirp from 'mkdirp'; 7 | 8 | import {packagerMain} from '../src/packager-cli'; 9 | 10 | const d = require('debug')('test:packager-cli'); 11 | 12 | let testCount = 0; 13 | 14 | function statSyncNoException(fsPath) { 15 | if ('statSyncNoException' in sfs) { 16 | return sfs.statSyncNoException(fsPath); 17 | } 18 | 19 | try { 20 | return sfs.statSync(fsPath); 21 | } catch (e) { 22 | return null; 23 | } 24 | } 25 | 26 | describe.skip('the packager CLI', function() { 27 | this.timeout(120 * 1000); 28 | 29 | beforeEach(function() { 30 | this.tempCacheDir = path.join(__dirname, `__packager_cli_${testCount++}`); 31 | mkdirp.sync(this.tempCacheDir); 32 | }); 33 | 34 | afterEach(function() { 35 | rimraf.sync(this.tempCacheDir); 36 | }); 37 | 38 | it('should do the basics of electron-packager', async function() { 39 | let inputApp = path.resolve(__dirname, 'electron-app'); 40 | 41 | // NB: The first two elements are dummies to fake out what would normally 42 | // be the path to node and the path to the script 43 | await packagerMain(['', '', '--platform', 'win32', '--version', '1.3.2', '--arch', 'all', '--out', this.tempCacheDir, inputApp]); 44 | 45 | const toFind = ['node.dll', 'resources', 'resources/app/src/main.coffee']; 46 | let cacheDir = this.tempCacheDir; 47 | 48 | toFind.forEach((name) => { 49 | let file = path.resolve(cacheDir, 'mp3-encoder-demo-win32-ia32', name); 50 | 51 | d(`Looking for ${file}`); 52 | expect(sfs.statSync(file)).to.be.ok; 53 | }); 54 | }); 55 | 56 | it('should run electron-compile', async function() { 57 | let inputApp = path.resolve(__dirname, 'electron-app'); 58 | 59 | // NB: The first two elements are dummies to fake out what would normally 60 | // be the path to node and the path to the script 61 | await packagerMain(['', '', '--platform', 'win32', '--version', '1.3.2', '--arch', 'x64', '--out', this.tempCacheDir, inputApp]); 62 | 63 | const toFind = ['resources/app/.cache', 'resources/app/.compilerc']; 64 | let cacheDir = this.tempCacheDir; 65 | 66 | toFind.forEach((name) => { 67 | let file = path.resolve(cacheDir, 'mp3-encoder-demo-win32-x64', name); 68 | 69 | d(`Looking for ${file}`); 70 | expect(sfs.statSync(file)).to.be.ok; 71 | }); 72 | }); 73 | 74 | it('should replace the init script with es6-shim', async function() { 75 | let inputApp = path.resolve(__dirname, 'electron-app'); 76 | 77 | // NB: The first two elements are dummies to fake out what would normally 78 | // be the path to node and the path to the script 79 | await packagerMain(['', '', '--platform', 'win32', '--version', '1.3.2', '--arch', 'x64', '--out', this.tempCacheDir, inputApp]); 80 | 81 | const toFind = ['resources/app/package.json', 'resources/app/es6-shim.js']; 82 | let cacheDir = this.tempCacheDir; 83 | 84 | toFind.forEach((name) => { 85 | let file = path.resolve(cacheDir, 'mp3-encoder-demo-win32-x64', name); 86 | 87 | d(`Looking for ${file}`); 88 | expect(sfs.statSync(file)).to.be.ok; 89 | }); 90 | 91 | let packageJson = require( 92 | path.join(cacheDir, 'mp3-encoder-demo-win32-x64', 'resources', 'app', 'package.json')); 93 | 94 | expect(packageJson.originalMain).to.equal('main.js'); 95 | expect(packageJson.main).to.equal('es6-shim.js'); 96 | }); 97 | 98 | it('should ASAR archive', async function() { 99 | let inputApp = path.resolve(__dirname, 'electron-app'); 100 | 101 | // NB: The first two elements are dummies to fake out what would normally 102 | // be the path to node and the path to the script 103 | await packagerMain(['', '', '--platform', 'win32', '--version', '1.3.2', '--arch', 'x64', '--asar', '--out', this.tempCacheDir, inputApp]); 104 | 105 | const toFind = ['resources/app.asar']; 106 | let cacheDir = this.tempCacheDir; 107 | 108 | toFind.forEach((name) => { 109 | let file = path.resolve(cacheDir, 'mp3-encoder-demo-win32-x64', name); 110 | 111 | d(`Looking for ${file}`); 112 | expect(statSyncNoException(file)).to.be.ok; 113 | }); 114 | 115 | const toNotFind = ['resources/app']; 116 | toNotFind.forEach((name) => { 117 | let file = path.resolve(cacheDir, 'mp3-encoder-demo-win32-x64', name); 118 | 119 | d(`Looking for ${file}`); 120 | expect(statSyncNoException(file)).not.to.be.ok; 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/protocol-hook.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | import {rigHtmlDocumentToInitializeElectronCompile} from '../src/protocol-hook'; 7 | 8 | describe('protocol hook library', function() { 9 | describe('The HTML include rigging', function() { 10 | it('should rig pages with HEAD tags', function() { 11 | let content = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'protourlrigging_1.html'), 'utf8'); 12 | let result = rigHtmlDocumentToInitializeElectronCompile(content); 13 | 14 | let lines = result.split('\n'); 15 | expect(lines.find((x) => x.match(/head.*__magic__file/i))).to.be.ok; 16 | }); 17 | 18 | it('should rig pages without tags', function() { 19 | let content = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'protourlrigging_2.html'), 'utf8'); 20 | let result = rigHtmlDocumentToInitializeElectronCompile(content); 21 | 22 | let lines = result.split('\n'); 23 | expect(lines.find((x) => x.match(/head.*__magic__file/i))).to.be.ok; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | const allCompilerClasses = require('electron-compilers'); 2 | 3 | let chai = require("chai"); 4 | let chaiAsPromised = require("chai-as-promised"); 5 | 6 | chai.should(); 7 | chai.use(chaiAsPromised); 8 | 9 | global.chai = chai; 10 | global.chaiAsPromised = chaiAsPromised; 11 | global.expect = chai.expect; 12 | global.AssertionError = chai.AssertionError; 13 | global.Assertion = chai.Assertion; 14 | global.assert = chai.assert; 15 | 16 | require('../src/rig-mime-types').init(); 17 | 18 | global.compilersByMimeType = allCompilerClasses.reduce((acc,x) => { 19 | acc = acc || {}; 20 | 21 | for (let type of x.getInputMimeTypes()) { acc[type] = x; } 22 | return acc; 23 | }, {}); 24 | 25 | global.compilersByMimeType['text/css'] = global.compilersByMimeType['text/plain']; 26 | 27 | const VueCompiler = global.compilersByMimeType['text/vue']; 28 | class AutoCreatedVueCompiler extends VueCompiler { 29 | constructor() { 30 | let dummy = VueCompiler.createFromCompilers(Object.keys(global.compilersByMimeType).reduce((acc, x) => { 31 | if ('createFromCompilers' in global.compilersByMimeType[x]) return acc; 32 | 33 | acc[x] = new global.compilersByMimeType[x](); 34 | return acc; 35 | }, {})); 36 | 37 | super(dummy.asyncCompilers, dummy.syncCompilers); 38 | } 39 | } 40 | 41 | global.compilersByMimeType['text/vue'] = AutoCreatedVueCompiler; 42 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare class CompilerHost { 2 | 3 | /** 4 | * Creates a development-mode CompilerHost from the previously saved configuration. 5 | */ 6 | static createFromConfiguration( 7 | rootCacheDir: string, 8 | appRoot: string, 9 | compilersByMimeType: CompilersByMimeType, 10 | fallbackCompiler: CompilerBase): PromiseLike; 11 | 12 | /** 13 | * Creates a development-mode CompilerHost from the previously saved configuration. 14 | */ 15 | static createFromConfigurationSync( 16 | rootCacheDir: string, 17 | appRoot: string, 18 | compilersByMimeType: CompilersByMimeType, 19 | fallbackCompiler: CompilerBase): CompilerHost; 20 | 21 | /** 22 | * Creates a production-mode CompilerHost from the previously saved configuration. 23 | */ 24 | static createReadonlyFromConfiguration( 25 | rootCacheDir: string, 26 | appRoot: string, 27 | fallbackCompiler: CompilerBase): PromiseLike; 28 | 29 | /** 30 | * Creates a production-mode CompilerHost from the previously saved configuration. 31 | */ 32 | static createReadonlyFromConfigurationSync( 33 | rootCacheDir: string, 34 | appRoot: string, 35 | fallbackCompiler: CompilerBase): CompilerHost 36 | 37 | constructor( 38 | rootCacheDir: string, 39 | compilers: CompilersByMimeType, 40 | fileChangeCache: FileChangedCache, 41 | readOnlyMode?: boolean, 42 | fallbackCompiler?: CompilerBase); 43 | 44 | readonly appRoot: string; 45 | readonly cachesForCompilers: Map; 46 | 47 | compile(filePath: string): { 48 | hashinfo: {}, 49 | code: string, 50 | binaryData: Buffer; 51 | mimeType: string; 52 | dependentFiles: string[]; 53 | } 54 | 55 | /** 56 | * Pre-caches an entire directory of files recursively. Usually used for building custom compiler tooling. 57 | */ 58 | compileAll( 59 | filePath: string, 60 | shouldCompile: (path: string) => boolean): PromiseLike; 61 | 62 | /** 63 | * Pre-caches an entire directory of files recursively. Usually used for building custom compiler tooling. 64 | */ 65 | compileAllSync( 66 | filePath: string, 67 | shouldCompile: (path: string) => boolean): void; 68 | 69 | saveConfiguration(): PromiseLike; 70 | 71 | saveConfigurationSync(): void; 72 | } 73 | 74 | export declare function enableLiveReload(options?: { 75 | strategy?: "react-hmr" | "naive"; 76 | }): void; 77 | 78 | export declare function calculateDefaultCompileCacheDirectory(): string; 79 | 80 | export declare function createCompilerHostFromBabelRc(file: string, rootCacheDir?: string): PromiseLike; 81 | 82 | export declare function createCompilerHostFromBabelRcSync(file: string, rootCacheDir?: string): CompilerHost; 83 | 84 | export declare function createCompilerHostFromConfigFile 85 | (file: string, rootCacheDir?: string): PromiseLike; 86 | 87 | export declare function createCompilerHostFromConfigFileSync 88 | (file: string, rootCacheDir?: string): CompilerHost; 89 | 90 | export declare function createCompilerHostFromProjectRoot 91 | (rootDir: string, rootCacheDir?: string): PromiseLike; 92 | 93 | export declare function createCompilerHostFromProjectRootSync 94 | (rootDir: string, rootCacheDir?: string): CompilerHost; 95 | 96 | export declare function createCompilers(): CompilersByMimeType; 97 | 98 | // documentation unclear at this time... 99 | export declare function forAllFilesSync(rootDirectory: string, func: any, ... args: any[]): void; 100 | 101 | export declare function getDefaultConfiguration(): object; 102 | 103 | export declare function init(appRoot: string, mainModule: string, productionMode?: boolean): void; 104 | 105 | export declare function initializeGlobalHooks(compilerHost: CompilerHost): void; 106 | 107 | export declare function initializeProtocolHook(compilerHost: CompilerHost): void; 108 | 109 | export declare function registerRequireExtension(compilerHost: CompilerHost): void; 110 | 111 | export interface CompilerBase { 112 | 113 | } 114 | 115 | export interface CompilersByMimeType { 116 | [mimeType: string]: CompilerBase; 117 | } 118 | 119 | export declare class FileChangedCache { 120 | 121 | static loadFromData( 122 | data: Object, 123 | appRoot: string, 124 | failOnCacheMiss: boolean): FileChangedCache; 125 | 126 | static loadFromFile( 127 | file: string, 128 | appRoot: string, 129 | failOnCacheMiss: boolean): FileChangedCache; 130 | 131 | constructor(appRoot: string, failOnCacheMiss?: boolean); 132 | } 133 | --------------------------------------------------------------------------------