├── .npmignore ├── test ├── fixtures │ ├── invalid.jade │ ├── invalid.less │ ├── invalid.coffee │ ├── invalid.js │ ├── babelrc-noenv │ ├── binaryfile.zip │ ├── valid.less │ ├── compilerc-noenv │ ├── invalid.jsx │ ├── valid.jsx │ ├── protourlrigging_2.html │ ├── invalid.ts │ ├── babelrc-production │ ├── compilerc-production │ ├── valid.jade │ ├── roboto.html │ ├── x-require-valid.html │ ├── valid.ts │ ├── protourlrigging_1.html │ ├── inline-valid.html │ ├── valid.coffee │ ├── inline-valid-2.html │ ├── inline-valid-3.html │ ├── valid.js │ ├── source_map.js │ └── minified.js ├── .eslintrc ├── electron-smoke-test-app.js ├── support.js ├── electron-smoke-test.js ├── protocol-hook.js ├── compiler-valid-invalid.js ├── electron-smoke-test.html ├── inline-html-compiler.js ├── compile-cache.js ├── file-change-cache.js ├── config-parser.js └── compiler-host.js ├── .babelrc ├── src ├── babel-maybefill.js ├── promise.js ├── index.js ├── rig-mime-types.js ├── sanitize-paths.js ├── require-hook.js ├── read-only-compiler.js ├── x-require.js ├── for-all-files.js ├── digest-for-object.js ├── cli.js ├── protocol-hook.js ├── compile-cache.js ├── file-change-cache.js ├── config-parser.js └── compiler-host.js ├── .jshintrc ├── .eslintrc ├── .gitignore ├── esdoc.json ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test-dist/ 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.jade: -------------------------------------------------------------------------------- 1 | #{myMixin 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.less: -------------------------------------------------------------------------------- 1 | @bodyColor: darken(@bodyColor, 30%); 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.coffee: -------------------------------------------------------------------------------- 1 | element(by.model('query.address')).sendKeys('947') -------------------------------------------------------------------------------- /test/fixtures/invalid.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | WAOEIFKAOWEFKAWOEF@#@#@#@#@#@ 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015", "react"], 3 | "sourceMaps": "inline" 4 | } 5 | -------------------------------------------------------------------------------- /src/babel-maybefill.js: -------------------------------------------------------------------------------- 1 | if (!('regeneratorRuntime' in global)) { 2 | require('babel-polyfill'); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/babelrc-noenv: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015", "react"], 3 | "sourceMaps": "inline" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/binaryfile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/electron-compile/master/test/fixtures/binaryfile.zip -------------------------------------------------------------------------------- /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/compilerc-noenv: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "presets": ["stage-0", "es2015", "react"], 4 | "sourceMaps": "inline" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/valid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
Hello World!
6 | } 7 | }); -------------------------------------------------------------------------------- /test/fixtures/protourlrigging_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hello World!

5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/invalid.ts: -------------------------------------------------------------------------------- 1 | class Z { 2 | public x = ""; 3 | } 4 | 5 | var a1: Z[] = []; 6 | var a2 = new Z[]; 7 | var a3 = new Z[](); 8 | var a4: Z[] = new Z[]; 9 | var a5: Z[] = new Z[](); 10 | var a6: Z[][] = new Z [ ] [ ]; -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/fixtures/babelrc-production: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "presets": ["stage-0", "es2015", "react"], 5 | "sourceMaps": false 6 | }, 7 | "development": { 8 | "presets": ["stage-0", "es2015", "react"], 9 | "sourceMaps": "inline" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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/index.js: -------------------------------------------------------------------------------- 1 | import './babel-maybefill'; 2 | import _ from 'lodash'; 3 | 4 | import * as configParser from './config-parser'; 5 | 6 | import CompilerHost from './compiler-host'; 7 | import FileChangedCache from './file-change-cache'; 8 | import CompileCache from './compile-cache'; 9 | 10 | module.exports = _.assign({}, 11 | configParser, 12 | { CompilerHost, FileChangedCache, CompileCache } 13 | ); 14 | -------------------------------------------------------------------------------- /test/fixtures/compilerc-production: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "application/javascript": { 5 | "presets": ["stage-0", "es2015", "react"], 6 | "sourceMaps": false 7 | } 8 | }, 9 | "development": { 10 | "application/javascript": { 11 | "presets": ["stage-0", "es2015", "react"], 12 | "sourceMaps": "inline" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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 | }, 20 | "extends": "eslint:recommended" 21 | } 22 | -------------------------------------------------------------------------------- /src/rig-mime-types.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mimeTypes from 'mime-types'; 3 | 4 | const typesToRig = { 5 | 'text/typescript': 'ts', 6 | 'text/jade': 'jade' 7 | }; 8 | 9 | 10 | /** 11 | * Adds MIME types for types not in the mime-types package 12 | * 13 | * @private 14 | */ 15 | export function init() { 16 | _.each(Object.keys(typesToRig), (type) => { 17 | let ext = typesToRig[type]; 18 | 19 | mimeTypes.types[ext] = type; 20 | mimeTypes.extensions[type] = _.clone([ext], true); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/sanitize-paths.js: -------------------------------------------------------------------------------- 1 | //const d = require('debug')('electron-compile:sanitize-paths'); 2 | 3 | /** 4 | * Electron will sometimes hand us paths that don't match the platform if they 5 | * were derived from a URL (i.e. 'C:/Users/Paul/...'), whereas the cache will have 6 | * saved paths with backslashes. 7 | * 8 | * @private 9 | */ 10 | export default function sanitizeFilePath(file) { 11 | // d(file); 12 | if (!file) return file; 13 | 14 | let ret = file.replace(/[\\\/]/g, '/'); 15 | return ret.toLowerCase(); 16 | } 17 | -------------------------------------------------------------------------------- /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/.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/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/fixtures/roboto.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/fixtures/x-require-valid.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import _ from 'lodash'; 4 | 5 | const allCompilerClasses = require('electron-compilers'); 6 | 7 | let chai = require("chai"); 8 | let chaiAsPromised = require("chai-as-promised"); 9 | 10 | chai.should(); 11 | chai.use(chaiAsPromised); 12 | 13 | global.chai = chai; 14 | global.chaiAsPromised = chaiAsPromised; 15 | global.expect = chai.expect; 16 | global.AssertionError = chai.AssertionError; 17 | global.Assertion = chai.Assertion; 18 | global.assert = chai.assert; 19 | 20 | require('../src/rig-mime-types').init(); 21 | 22 | global.compilersByMimeType = _.reduce(allCompilerClasses, (acc,x) => { 23 | acc = acc || {}; 24 | 25 | for (let type of x.getInputMimeTypes()) { acc[type] = x; } 26 | return acc; 27 | }, {}); 28 | -------------------------------------------------------------------------------- /src/require-hook.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mimeTypes from 'mime-types'; 3 | 4 | /** 5 | * Initializes the node.js hook that allows us to intercept files loaded by 6 | * node.js and rewrite them. This method along with {@link initializeProtocolHook} 7 | * are the top-level methods that electron-compile actually uses to intercept 8 | * code that Electron loads. 9 | * 10 | * @param {CompilerHost} compilerHost The compiler host to use for compilation. 11 | */ 12 | export default function registerRequireExtension(compilerHost) { 13 | _.each(Object.keys(compilerHost.compilersByMimeType), (mimeType) => { 14 | let ext = mimeTypes.extension(mimeType); 15 | 16 | require.extensions[`.${ext}`] = (module, filename) => { 17 | let {code} = compilerHost.compileSync(filename); 18 | module._compile(code, filename); 19 | }; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /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/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 | declare module Data { 7 | export interface IListItem { 8 | itemIndex: number; 9 | key: any; 10 | data: T; 11 | group: any; 12 | isHeader: boolean; 13 | cached: boolean; 14 | isNonSourceData: boolean; 15 | preventAugmentation: boolean; 16 | } 17 | export interface IVirtualList { 18 | //removeIndices: WinJS.Promise[]>; 19 | removeIndices(indices: number[], options?: any): WinJS.Promise[]>; 20 | } 21 | export class VirtualList implements IVirtualList { 22 | //removeIndices: WinJS.Promise[]>; 23 | public removeIndices(indices: number[], options?: any): WinJS.Promise[]>; 24 | } 25 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/read-only-compiler.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | 4 | /** 5 | * ReadOnlyCompiler is a compiler which allows the host to inject all of the compiler 6 | * metadata information so that {@link CompileCache} et al are able to recreate the 7 | * hash without having two separate code paths. 8 | */ 9 | export default class ReadOnlyCompiler { 10 | /** 11 | * Creates a ReadOnlyCompiler instance 12 | * 13 | * @private 14 | */ 15 | constructor(name, compilerVersion, compilerOptions, inputMimeTypes) { 16 | _.assign(this, { name, compilerVersion, compilerOptions, inputMimeTypes }); 17 | } 18 | 19 | async shouldCompileFile() { return true; } 20 | async determineDependentFiles() { return []; } 21 | 22 | async compile() { 23 | throw new Error("Read-only compilers can't compile"); 24 | } 25 | 26 | shouldCompileFileSync() { return true; } 27 | determineDependentFilesSync() { return []; } 28 | 29 | compileSync() { 30 | throw new Error("Read-only compilers can't compile"); 31 | } 32 | 33 | getCompilerVersion() { 34 | return this.compilerVersion; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/protocol-hook.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import _ from 'lodash'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | 7 | import {rigHtmlDocumentToInitializeElectronCompile} from '../src/protocol-hook'; 8 | 9 | describe('protocol hook library', function() { 10 | describe('The HTML include rigging', function() { 11 | it('should rig pages with HEAD tags', function() { 12 | let content = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'protourlrigging_1.html'), 'utf8'); 13 | let result = rigHtmlDocumentToInitializeElectronCompile(content); 14 | 15 | let lines = result.split('\n'); 16 | expect(_.any(lines, (x) => x.match(/head.*__magic__file/i))).to.be.ok; 17 | }); 18 | 19 | it('should rig pages without tags', function() { 20 | let content = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'protourlrigging_2.html'), 'utf8'); 21 | let result = rigHtmlDocumentToInitializeElectronCompile(content); 22 | 23 | let lines = result.split('\n'); 24 | expect(_.any(lines, (x) => x.match(/head.*__magic__file/i))).to.be.ok; 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixtures/inline-valid.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 28 | 29 | 48 | 49 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /src/x-require.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import url from 'url'; 3 | 4 | function requireModule(href) { 5 | let filePath = href; 6 | 7 | if (filePath.match(/^file:/i)) { 8 | let theUrl = url.parse(filePath); 9 | filePath = decodeURIComponent(theUrl.pathname); 10 | 11 | if (process.platform === 'win32') { 12 | filePath = filePath.slice(1); 13 | } 14 | } 15 | 16 | // NB: We don't do any path canonicalization here because we rely on 17 | // InlineHtmlCompiler to have already converted any relative paths that 18 | // were used with x-require into absolute paths. 19 | require(filePath); 20 | } 21 | 22 | /** 23 | * @private 24 | */ 25 | export default (() => { 26 | if (process.type !== 'renderer' || !window || !window.document) return null; 27 | 28 | let proto = _.extend(Object.create(HTMLElement.prototype), { 29 | createdCallback: function() { 30 | let href = this.getAttribute('src'); 31 | if (href && href.length > 0) { 32 | requireModule(href); 33 | } 34 | }, 35 | attributeChangedCallback: function(attrName, oldVal, newVal) { 36 | if (attrName !== 'src') return; 37 | requireModule(newVal); 38 | } 39 | }); 40 | 41 | return document.registerElement('x-require', { prototype: proto }); 42 | })(); 43 | -------------------------------------------------------------------------------- /src/for-all-files.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import {pfs} from './promise'; 5 | 6 | 7 | /** 8 | * Invokes a method on all files in a directory recursively. 9 | * 10 | * @private 11 | */ 12 | export function forAllFiles(rootDirectory, func, ...args) { 13 | let rec = async (dir) => { 14 | let entries = await pfs.readdir(dir); 15 | 16 | for (let name of entries) { 17 | let fullName = path.join(dir, name); 18 | let stats = await pfs.stat(fullName); 19 | 20 | if (stats.isDirectory()) { 21 | await rec(fullName); 22 | } 23 | 24 | if (stats.isFile()) { 25 | await func(fullName, ...args); 26 | } 27 | } 28 | }; 29 | 30 | return rec(rootDirectory); 31 | } 32 | 33 | export function forAllFilesSync(rootDirectory, func, ...args) { 34 | let rec = (dir) => { 35 | _.each(fs.readdirSync(dir), (name) => { 36 | let fullName = path.join(dir, name); 37 | let stats = fs.statSync(fullName); 38 | 39 | if (stats.isDirectory()) { 40 | rec(fullName); 41 | return; 42 | } 43 | 44 | if (stats.isFile()) { 45 | func(fullName, ...args); 46 | return; 47 | } 48 | }); 49 | }; 50 | 51 | rec(rootDirectory); 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-compile", 3 | "version": "2.0.5", 4 | "description": "Electron supporting package to compile JS and CSS in Electron applications", 5 | "scripts": { 6 | "doc": "esdoc -c ./esdoc.json", 7 | "compile": "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 | }, 12 | "bin": { 13 | "electron-compile": "lib/cli.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/paulcbetts/electron-compile" 18 | }, 19 | "keywords": [ 20 | "electron" 21 | ], 22 | "author": "Paul Betts ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/paulcbetts/electron-compile/issues" 26 | }, 27 | "homepage": "https://github.com/paulcbetts/electron-compile", 28 | "main": "lib/index.js", 29 | "dependencies": { 30 | "babel-polyfill": "^6.3.14", 31 | "btoa": "^1.1.2", 32 | "debug": "^2.2.0", 33 | "lodash": "^3.10.1", 34 | "mime-types": "^2.1.8", 35 | "mkdirp": "^0.5.1", 36 | "pify": "^2.3.0", 37 | "yargs": "^3.31.0" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.3.17", 41 | "babel-eslint": "^5.0.0-beta6", 42 | "babel-preset-es2015": "^6.3.13", 43 | "babel-preset-react": "^6.3.13", 44 | "babel-preset-stage-0": "^6.3.13", 45 | "babel-register": "^6.3.13", 46 | "chai": "^3.4.1", 47 | "chai-as-promised": "^5.2.0", 48 | "cheerio": "^0.19.0", 49 | "electron-compilers": "^2.0.0", 50 | "electron-prebuilt": "^0.36.2", 51 | "esdoc": "^0.4.3", 52 | "esdoc-es7-plugin": "0.0.3", 53 | "esdoc-plugin-async-to-sync": "^0.5.0", 54 | "eslint": "^1.10.3", 55 | "mocha": "^2.3.4", 56 | "rimraf": "^2.5.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import './babel-maybefill'; 4 | import path from 'path'; 5 | import mkdirp from 'mkdirp'; 6 | import _ from 'lodash'; 7 | 8 | import {createCompilerHostFromProjectRoot} from './config-parser'; 9 | import {forAllFiles} from './for-all-files'; 10 | 11 | process.on('unhandledRejection', (e) => { 12 | d(e.message || e); 13 | d(e.stack || ''); 14 | }); 15 | 16 | process.on('uncaughtException', (e) => { 17 | d(e.message || e); 18 | d(e.stack || ''); 19 | }); 20 | 21 | async function main(appDir, sourceDirs) { 22 | let compilerHost = null; 23 | let rootCacheDir = path.join(appDir, '.cache'); 24 | mkdirp.sync(rootCacheDir); 25 | 26 | if (process.env.NODE_ENV !== 'production') { 27 | console.log(`Using NODE_ENV = ${process.env.NODE_ENV || 'development'}`); 28 | } 29 | 30 | d(`main: ${appDir}, ${JSON.stringify(sourceDirs)}`); 31 | try { 32 | compilerHost = await createCompilerHostFromProjectRoot(appDir, rootCacheDir); 33 | } catch (e) { 34 | console.error(`Couldn't set up compilers: ${e.message}`); 35 | d(e.stack); 36 | 37 | throw e; 38 | } 39 | 40 | await Promise.all(_.map(sourceDirs, (dir) => forAllFiles(dir, async (f) => { 41 | try { 42 | d(`Starting compilation for ${f}`); 43 | await compilerHost.compile(f); 44 | } catch (e) { 45 | console.error(`Failed to compile file: ${f}`); 46 | console.error(e.message); 47 | 48 | d(e.stack); 49 | } 50 | }))); 51 | 52 | d('Saving out configuration'); 53 | await compilerHost.saveConfiguration(); 54 | } 55 | 56 | const d = require('debug')('electron-compile'); 57 | 58 | const yargs = require('yargs') 59 | .usage('Usage: electron-compile --appdir [root-app-dir] paths...') 60 | .alias('a', 'appdir') 61 | .describe('a', 'The top-level application directory (i.e. where your package.json is)') 62 | .help('h') 63 | .alias('h', 'help') 64 | .epilog('Copyright 2015'); 65 | 66 | const argv = yargs.argv; 67 | 68 | if (!argv._ || argv._.length < 1) { 69 | yargs.showHelp(); 70 | process.exit(-1); 71 | } 72 | 73 | const sourceDirs = argv._; 74 | const appDir = argv.a || process.env.PWD; 75 | 76 | main(appDir, sourceDirs) 77 | .then(() => process.exit(0)) 78 | .catch((e) => { 79 | console.error(e.message || e); 80 | d(e.stack); 81 | 82 | console.error("Compilation failed!\nFor extra information, set the DEBUG environment variable to '*'"); 83 | process.exit(-1); 84 | }); 85 | -------------------------------------------------------------------------------- /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 _ from 'lodash'; 7 | import mimeTypes from 'mime-types'; 8 | 9 | const pfs = pify(fs); 10 | 11 | let allFixtureFiles = _.filter( 12 | fs.readdirSync(path.join(__dirname, '..', 'test', 'fixtures')), 13 | (x) => x.match(/invalid\./i)); 14 | 15 | let mimeTypesToTest = _.reduce(allFixtureFiles, (acc,x) => { 16 | if (global.compilersByMimeType[mimeTypes.lookup(x) || '__nope__']) { 17 | acc.push(mimeTypes.lookup(x)); 18 | } 19 | 20 | return acc; 21 | }, []); 22 | 23 | const expectedMimeTypeSpecialCases = { 24 | 'text/less': 'text/css', 25 | 'text/jade': 'text/html' 26 | }; 27 | 28 | for (let mimeType of mimeTypesToTest) { 29 | let klass = global.compilersByMimeType[mimeType]; 30 | 31 | describe(`The ${klass.name} class for ${mimeType}`, function() { 32 | beforeEach(function() { 33 | this.fixture = new klass(); 34 | }); 35 | 36 | it(`should compile the valid ${mimeType} file`, async function() { 37 | let ext = mimeTypes.extension(mimeType); 38 | let input = path.join(__dirname, '..', 'test', 'fixtures', `valid.${ext}`); 39 | 40 | let ctx = {}; 41 | let shouldCompile = await this.fixture.shouldCompileFile(input, ctx); 42 | expect(shouldCompile).to.be.ok; 43 | 44 | let source = await pfs.readFile(input, 'utf8'); 45 | let dependentFiles = await this.fixture.determineDependentFiles(source, input, ctx); 46 | expect(dependentFiles.length).to.equal(0); 47 | 48 | let result = await this.fixture.compile(source, input, ctx); 49 | let expectedMimeType = expectedMimeTypeSpecialCases[mimeType] || 'application/javascript'; 50 | 51 | expect(result.mimeType).to.equal(expectedMimeType); 52 | 53 | // NB: Jade doesn't do source maps 54 | if (mimeType !== 'text/jade') { 55 | let lines = result.code.split('\n'); 56 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).to.be.ok; 57 | } 58 | }); 59 | 60 | it(`should fail the invalid ${mimeType} file`, async function() { 61 | let ext = mimeTypes.extension(mimeType); 62 | let input = path.join(__dirname, '..', 'test', 'fixtures', `invalid.${ext}`); 63 | 64 | let ctx = {}; 65 | let shouldCompile = await this.fixture.shouldCompileFile(input, ctx); 66 | expect(shouldCompile).to.be.ok; 67 | 68 | let source = await pfs.readFile(input, 'utf8'); 69 | let dependentFiles = await this.fixture.determineDependentFiles(source, input, ctx); 70 | expect(dependentFiles.length).to.equal(0); 71 | 72 | let result = this.fixture.compile(source, input, ctx); 73 | expect(result).to.eventually.throw(); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## electron-compile 2 | 3 | ![](https://img.shields.io/npm/dm/electron-compile.svg) ![](http://electronjs.github.io/electron-compile/docs/badge.svg) 4 | 5 | electron-compile compiles JS and CSS on the fly with a single call in your app's 'ready' function. 6 | 7 | For JavaScript: 8 | 9 | * JavaScript ES6/ES7 (via Babel) 10 | * TypeScript 11 | * CoffeeScript 12 | 13 | For CSS: 14 | 15 | * LESS 16 | 17 | For HTML: 18 | 19 | * Jade 20 | 21 | ### How does it work? (Easiest Way) 22 | 23 | Change your reference to `electron-prebuilt` to `electron-prebuilt-compile`. Tada! You did it. 24 | 25 | ### Wait, seriously? 26 | 27 | Yeah. `electron-prebuilt-compile` is like an `electron-prebuilt` that Just Works with all of these languages above. 28 | 29 | ### How does it work? (Slightly Harder Way) 30 | 31 | First, add `electron-compile` and `electron-compilers` as a `devDependency`. 32 | 33 | ```sh 34 | npm install --save electron-compile 35 | npm install --save-dev electron-compilers 36 | ``` 37 | 38 | 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. 39 | 40 | ```js 41 | // Assuming this file is ./src/es6-init.js 42 | var appRoot = path.join(__dirname, '..'); 43 | 44 | // ...and that your main app is called ./src/main.js. This is written as if 45 | // you were going to `require` the file from here. 46 | require('electron-compile').init(appRoot, './main'); 47 | ``` 48 | 49 | 50 | ### I did it, now what? 51 | 52 | From then on, you can now simply include files directly in your HTML, no need for cross-compilation: 53 | 54 | ```html 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | or just require them in: 62 | 63 | ```js 64 | require('./mylib') // mylib.ts 65 | ``` 66 | 67 | ### Something isn't working / I'm getting weird errors 68 | 69 | electron-compile uses the [debug module](https://github.com/visionmedia/debug), set the DEBUG environment variable to debug what electron-compile is doing: 70 | 71 | ```sh 72 | ## Debug just electron-compile 73 | DEBUG=electron-compile:* npm start 74 | 75 | ## Grab everything except for Babel which is very noisy 76 | DEBUG=*,-babel npm start 77 | ``` 78 | 79 | ### How do I set up (Babel / LESS / whatever) the way I want? 80 | 81 | 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` file. Here's an example: 82 | 83 | ```js 84 | { 85 | "application/javascript": { 86 | "presets": ["stage-0", "es2015", "react"], 87 | "sourceMaps": "inline" 88 | }, 89 | "text/less": { 90 | "dumpLineNumbers": "comments" 91 | } 92 | } 93 | ``` 94 | 95 | `.compilerc` also accepts environments with the same syntax as `.babelrc`: 96 | 97 | ```js 98 | { 99 | "env": { 100 | "development": { 101 | "application/javascript": { 102 | "presets": ["stage-0", "es2015", "react"], 103 | "sourceMaps": "inline" 104 | }, 105 | "text/less": { 106 | "dumpLineNumbers": "comments" 107 | } 108 | }, 109 | "production": { 110 | "application/javascript": { 111 | "presets": ["stage-0", "es2015", "react"] 112 | "sourceMaps": "none" 113 | } 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | The opening Object is a list of MIME Types, and options passed to the compiler implementation. These parameters are documented here: 120 | 121 | * Babel - http://babeljs.io/docs/usage/options 122 | * CoffeeScript - http://coffeescript.org/documentation/docs/coffee-script.html#section-5 123 | * TypeScript - https://github.com/Microsoft/TypeScript/blob/v1.5.0-beta/bin/typescriptServices.d.ts#L1076 124 | * LESS - http://lesscss.org/usage/index.html#command-line-usage-options 125 | * Jade - http://jade-lang.com/api 126 | 127 | ## How can I precompile my code for release-time? 128 | 129 | electron-compile comes with a command-line application to pre-create a cache for you. 130 | 131 | ```sh 132 | Usage: electron-compile --appDir [root-app-dir] paths... 133 | 134 | Options: 135 | -a, --appdir The top-level application directory (i.e. where your 136 | package.json is) 137 | -v, --verbose Print verbose information 138 | -h, --help Show help 139 | ``` 140 | 141 | 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. 142 | 143 | ```sh 144 | electron-compile --appDir /path/to/my/app ./src ./static 145 | ``` 146 | 147 | ### But I use Grunt / Gulp / I want to do Something Interesting 148 | 149 | Compilation also has its own API, check out the [documentation](http://electronjs.github.io/electron-compile/docs/badge.svg) for more information. 150 | -------------------------------------------------------------------------------- /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 | import _ from 'lodash'; 8 | 9 | const validInputs = [ 10 | 'inline-valid.html', 11 | 'inline-valid-2.html', 12 | 'inline-valid-3.html' 13 | ]; 14 | 15 | const pfs = pify(fs); 16 | const InlineHtmlCompiler = global.compilersByMimeType['text/html']; 17 | 18 | const d = require('debug')('test:inline-html-compiler'); 19 | 20 | describe('The inline HTML compiler', function() { 21 | beforeEach(function() { 22 | let compilers = _.reduce(Object.keys(global.compilersByMimeType), (acc, x) => { 23 | let Klass = global.compilersByMimeType[x]; 24 | acc[x] = new Klass(); 25 | 26 | return acc; 27 | }, {}); 28 | 29 | compilers['application/javascript'].compilerOptions = { 30 | "presets": ["stage-0", "es2015", "react"], 31 | "sourceMaps": "inline" 32 | }; 33 | 34 | compilers['text/coffeescript'].compilerOptions = { sourceMap: true }; 35 | 36 | this.fixture = InlineHtmlCompiler.createFromCompilers(compilers); 37 | }); 38 | 39 | _.each(validInputs, (inputFile) => { 40 | it('should compile the valid fixture ' + inputFile, async function() { 41 | let input = path.join(__dirname, '..', 'test', 'fixtures', inputFile); 42 | 43 | let cc = {}; 44 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 45 | 46 | let code = await pfs.readFile(input, 'utf8'); 47 | let df = await this.fixture.determineDependentFiles(input, code, cc); 48 | 49 | expect(df.length).to.equal(0); 50 | 51 | let result = await this.fixture.compile(code, input, cc); 52 | expect(result.mimeType).to.equal('text/html'); 53 | 54 | let $ = cheerio.load(result.code); 55 | let tags = $('script'); 56 | expect(tags.length > 0).to.be.ok; 57 | 58 | $('script').map((__, el) => { 59 | let text = $(el).text(); 60 | if (!text || text.length < 2) return; 61 | 62 | if ($(el).attr('type').match(/handlebars/)) return; 63 | 64 | expect(_.find(text.split('\n'), (l) => l.match(/sourceMappingURL/))).to.be.ok; 65 | }); 66 | }); 67 | 68 | it('should compile the valid fixture ' + inputFile + ' synchronously', function() { 69 | let input = path.join(__dirname, '..', 'test', 'fixtures', inputFile); 70 | 71 | let cc = {}; 72 | expect(this.fixture.shouldCompileFileSync(input, cc)).to.be.ok; 73 | 74 | let code = fs.readFileSync(input, 'utf8'); 75 | let df = this.fixture.determineDependentFilesSync(input, code, cc); 76 | 77 | expect(df.length).to.equal(0); 78 | 79 | let result = this.fixture.compileSync(code, input, cc); 80 | expect(result.mimeType).to.equal('text/html'); 81 | 82 | let $ = cheerio.load(result.code); 83 | let tags = $('script'); 84 | expect(tags.length > 0).to.be.ok; 85 | 86 | $('script').map((__, el) => { 87 | let text = $(el).text(); 88 | if (!text || text.length < 2) return; 89 | 90 | d($(el).attr('type')); 91 | if ($(el).attr('type').match(/handlebars/)) return; 92 | 93 | d(text); 94 | expect(_.find(text.split('\n'), (l) => l.match(/sourceMappingURL/))).to.be.ok; 95 | }); 96 | }); 97 | }); 98 | 99 | it('should remove protocol-relative URLs because they are dumb', async function() { 100 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'roboto.html'); 101 | 102 | let cc = {}; 103 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 104 | 105 | let code = await pfs.readFile(input, 'utf8'); 106 | let df = await this.fixture.determineDependentFiles(input, code, cc); 107 | 108 | expect(df.length).to.equal(0); 109 | 110 | let result = await this.fixture.compile(code, input, cc); 111 | 112 | expect(result.code.length > 0).to.be.ok; 113 | expect(result.mimeType).to.equal('text/html'); 114 | 115 | let $ = cheerio.load(result.code); 116 | let tags = $('link'); 117 | expect(tags.length === 1).to.be.ok; 118 | expect($(tags[0]).attr('href').match(/^https/i)).to.be.ok; 119 | }); 120 | 121 | it('should canonicalize x-require paths', async function() { 122 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'x-require-valid.html'); 123 | 124 | let cc = {}; 125 | expect(await this.fixture.shouldCompileFile(input, cc)).to.be.ok; 126 | 127 | let code = await pfs.readFile(input, 'utf8'); 128 | let df = await this.fixture.determineDependentFiles(input, code, cc); 129 | 130 | expect(df.length).to.equal(0); 131 | 132 | let result = await this.fixture.compile(code, input, cc); 133 | 134 | expect(result.code.length > 0).to.be.ok; 135 | expect(result.mimeType).to.equal('text/html'); 136 | 137 | let $ = cheerio.load(result.code); 138 | let tags = $('x-require'); 139 | expect(tags.length === 1).to.be.ok; 140 | 141 | $('x-require').map((__, el) => { 142 | let src = $(el).attr('src'); 143 | expect(_.find(src.split(/[\\\/]/), (x) => x === '.' || x === '..')).not.to.be.ok; 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /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 | return { code, mimeType }; 39 | }; 40 | 41 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 42 | 43 | expect(result.mimeType).to.equal('application/javascript'); 44 | expect(result.code.length > 10).to.be.ok; 45 | expect(callCount).to.equal(1); 46 | 47 | result = await this.fixture.getOrFetch(inputFile, fetcher); 48 | 49 | expect(result.mimeType).to.equal('application/javascript'); 50 | expect(result.code.length > 10).to.be.ok; 51 | expect(callCount).to.equal(1); 52 | 53 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 54 | 55 | result = await this.fixture.getOrFetch(inputFile, fetcher); 56 | 57 | expect(result.mimeType).to.equal('application/javascript'); 58 | expect(result.code.length > 10).to.be.ok; 59 | expect(callCount).to.equal(1); 60 | }); 61 | 62 | it('Should roundtrip binary files', async function() { 63 | let inputFile = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 64 | let hashInfo = await this.fileChangeCache.getHashForPath(inputFile); 65 | 66 | await this.fixture.save(hashInfo, hashInfo.binaryData, 'application/zip'); 67 | 68 | let fetcher = async function() { 69 | throw new Error("No"); 70 | }; 71 | 72 | let result = await this.fixture.getOrFetch(inputFile, fetcher); 73 | expect(result.mimeType).to.equal('application/zip'); 74 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 75 | 76 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 77 | 78 | result = await this.fixture.getOrFetch(inputFile, fetcher); 79 | expect(result.mimeType).to.equal('application/zip'); 80 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 81 | }); 82 | 83 | it('Should roundtrip binary files synchronously', function() { 84 | let inputFile = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 85 | let hashInfo = this.fileChangeCache.getHashForPathSync(inputFile); 86 | 87 | this.fixture.saveSync(hashInfo, hashInfo.binaryData, 'application/zip'); 88 | 89 | let fetcher = function() { 90 | throw new Error("No"); 91 | }; 92 | 93 | let result = this.fixture.getOrFetchSync(inputFile, fetcher); 94 | expect(result.mimeType).to.equal('application/zip'); 95 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 96 | 97 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 98 | 99 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 100 | expect(result.mimeType).to.equal('application/zip'); 101 | expect(result.binaryData.length).to.equal(hashInfo.binaryData.length); 102 | }); 103 | 104 | it('Should only call compile once for the same file synchronously', function() { 105 | let inputFile = path.resolve(__dirname, '..', 'src', 'compile-cache.js'); 106 | let callCount = 0; 107 | 108 | let fetcher = function(filePath, hashInfo) { 109 | callCount++; 110 | 111 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 112 | let mimeType = 'application/javascript'; 113 | 114 | return { code, mimeType }; 115 | }; 116 | 117 | let result = this.fixture.getOrFetchSync(inputFile, fetcher); 118 | 119 | expect(result.mimeType).to.equal('application/javascript'); 120 | expect(result.code.length > 10).to.be.ok; 121 | expect(callCount).to.equal(1); 122 | 123 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 124 | 125 | expect(result.mimeType).to.equal('application/javascript'); 126 | expect(result.code.length > 10).to.be.ok; 127 | expect(callCount).to.equal(1); 128 | 129 | this.fixture = new CompileCache(this.tempCacheDir, this.fileChangeCache); 130 | 131 | result = this.fixture.getOrFetchSync(inputFile, fetcher); 132 | 133 | expect(result.mimeType).to.equal('application/javascript'); 134 | expect(result.code.length > 10).to.be.ok; 135 | expect(callCount).to.equal(1); 136 | }); 137 | 138 | it('Shouldnt cache compile failures', async function() { 139 | let inputFile = path.resolve(__dirname, '..', 'lib', 'compile-cache.js'); 140 | let callCount = 0; 141 | let weBlewUpCount = 0; 142 | 143 | let fetcher = async function() { 144 | callCount++; 145 | throw new Error("Lolz"); 146 | }; 147 | 148 | try { 149 | await this.fixture.getOrFetch(inputFile, fetcher); 150 | } catch (e) { 151 | weBlewUpCount++; 152 | } 153 | 154 | expect(callCount).to.equal(1); 155 | expect(weBlewUpCount).to.equal(1); 156 | 157 | try { 158 | await this.fixture.getOrFetch(inputFile, fetcher); 159 | } catch (e) { 160 | weBlewUpCount++; 161 | } 162 | 163 | expect(callCount).to.equal(2); 164 | expect(weBlewUpCount).to.equal(2); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/protocol-hook.js: -------------------------------------------------------------------------------- 1 | import './babel-maybefill'; 2 | import url from 'url'; 3 | import fs from 'fs'; 4 | import mime from 'mime-types'; 5 | 6 | import CompilerHost from './compiler-host'; 7 | 8 | const magicWords = "__magic__file__to__help__electron__compile.js"; 9 | const magicGlobalForRootCacheDir = '__electron_compile_root_cache_dir'; 10 | const magicGlobalForAppRootDir = '__electron_compile_app_root_dir'; 11 | 12 | const d = require('debug')('electron-compile:protocol-hook'); 13 | 14 | let protocol = null; 15 | 16 | /** 17 | * Adds our script header to the top of all HTML files 18 | * 19 | * @private 20 | */ 21 | export function rigHtmlDocumentToInitializeElectronCompile(doc) { 22 | let lines = doc.split("\n"); 23 | let replacement = ``; 24 | let replacedHead = false; 25 | 26 | for (let i=0; i < lines.length; i++) { 27 | if (!lines[i].match(//i)) continue; 28 | 29 | lines[i] = (lines[i]).replace(//i, replacement); 30 | replacedHead = true; 31 | break; 32 | } 33 | 34 | if (!replacedHead) { 35 | replacement = ``; 36 | for (let i=0; i < lines.length; i++) { 37 | if (!lines[i].match(/]+)>/i, replacement); 40 | break; 41 | } 42 | } 43 | 44 | return lines.join("\n"); 45 | } 46 | 47 | function requestFileJob(filePath, finish) { 48 | fs.readFile(filePath, (err, buf) => { 49 | if (err) { 50 | if (err.errno === 34) { 51 | finish(-6); // net::ERR_FILE_NOT_FOUND 52 | return; 53 | } else { 54 | finish(-2); // net::FAILED 55 | return; 56 | } 57 | } 58 | 59 | finish({ 60 | data: buf, 61 | mimeType: mime.lookup(filePath) || 'text/plain' 62 | }); 63 | }); 64 | } 65 | 66 | let rendererInitialized = false; 67 | 68 | /** 69 | * Called by our rigged script file at the top of every HTML file to set up 70 | * the same compilers as the browser process that created us 71 | * 72 | * @private 73 | */ 74 | export function initializeRendererProcess(readOnlyMode) { 75 | if (rendererInitialized) return; 76 | 77 | // NB: If we don't do this, we'll get a renderer crash if you enable debug 78 | require('debug/browser'); 79 | 80 | let rootCacheDir = require('remote').getGlobal(magicGlobalForRootCacheDir); 81 | let appRoot = require('remote').getGlobal(magicGlobalForAppRootDir); 82 | let compilerHost = null; 83 | 84 | // NB: This has to be synchronous because we need to block HTML parsing 85 | // until we're set up 86 | if (readOnlyMode) { 87 | d(`Setting up electron-compile in precompiled mode with cache dir: ${rootCacheDir}`); 88 | compilerHost = CompilerHost.createReadonlyFromConfigurationSync(rootCacheDir, appRoot); 89 | } else { 90 | d(`Setting up electron-compile in development mode with cache dir: ${rootCacheDir}`); 91 | const { createCompilers } = require('./config-parser'); 92 | const compilersByMimeType = createCompilers(); 93 | 94 | compilerHost = CompilerHost.createFromConfigurationSync(rootCacheDir, appRoot, compilersByMimeType); 95 | } 96 | 97 | require('./x-require'); 98 | require('./require-hook').default(compilerHost); 99 | rendererInitialized = true; 100 | } 101 | 102 | 103 | /** 104 | * Initializes the protocol hook on file: that allows us to intercept files 105 | * loaded by Chromium and rewrite them. This method along with 106 | * {@link registerRequireExtension} are the top-level methods that electron-compile 107 | * actually uses to intercept code that Electron loads. 108 | * 109 | * @param {CompilerHost} compilerHost The compiler host to use for compilation. 110 | */ 111 | export function initializeProtocolHook(compilerHost) { 112 | protocol = protocol || require('protocol'); 113 | 114 | global[magicGlobalForRootCacheDir] = compilerHost.rootCacheDir; 115 | global[magicGlobalForAppRootDir] = compilerHost.appRoot; 116 | 117 | const electronCompileSetupCode = `if (window.require) require('electron-compile/lib/protocol-hook').initializeRendererProcess(${compilerHost.readOnlyMode});`; 118 | 119 | protocol.interceptBufferProtocol('file', async function(request, finish) { 120 | let uri = url.parse(request.url); 121 | 122 | d(`Intercepting url ${request.url}`); 123 | if (request.url.indexOf(magicWords) > -1) { 124 | finish({ 125 | mimeType: 'application/javascript', 126 | data: new Buffer(electronCompileSetupCode, 'utf8') 127 | }); 128 | 129 | return; 130 | } 131 | 132 | // This is a protocol-relative URL that has gone pear-shaped in Electron, 133 | // let's rewrite it 134 | if (uri.host && uri.host.length > 1) { 135 | //let newUri = request.url.replace(/^file:/, "https:"); 136 | // TODO: Jump off this bridge later 137 | d(`TODO: Found bogus protocol-relative URL, can't fix it up!!`); 138 | finish(-2); 139 | } 140 | 141 | let filePath = decodeURIComponent(uri.pathname); 142 | 143 | // NB: pathname has a leading '/' on Win32 for some reason 144 | if (process.platform === 'win32') { 145 | filePath = filePath.slice(1); 146 | } 147 | 148 | // NB: Special-case files coming from atom.asar or node_modules 149 | if (filePath.match(/[\/\\]atom.asar/) || filePath.match(/[\/\\]node_modules/)) { 150 | requestFileJob(filePath, finish); 151 | return; 152 | } 153 | 154 | try { 155 | let result = await compilerHost.compile(filePath); 156 | 157 | if (result.mimeType === 'text/html') { 158 | result.code = rigHtmlDocumentToInitializeElectronCompile(result.code); 159 | } 160 | 161 | if (result.binaryData || result.code instanceof Buffer) { 162 | finish({ data: result.binaryData || result.code, mimeType: result.mimeType }); 163 | return; 164 | } else { 165 | finish({ data: new Buffer(result.code), mimeType: result.mimeType }); 166 | return; 167 | } 168 | } catch (e) { 169 | let err = `Failed to compile ${filePath}: ${e.message}\n${e.stack}`; 170 | d(err); 171 | 172 | if (e.errno === 34 /*ENOENT*/) { 173 | finish(-6); // net::ERR_FILE_NOT_FOUND 174 | return; 175 | } 176 | 177 | finish({ mimeType: 'text/plain', data: new Buffer(err) }); 178 | return; 179 | } 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /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 | const pfs = pify(fs); 8 | 9 | describe('The file changed cache', function() { 10 | beforeEach(function() { 11 | this.fixture = new FileChangeCache(null); 12 | }); 13 | 14 | it("Correctly computes a file hash for a canned file", async function() { 15 | const expectedInfo = { 16 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 17 | hasSourceMap: false, 18 | isInNodeModules: false, 19 | isMinified: false, 20 | isFileBinary: false 21 | }; 22 | 23 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'valid.js'); 24 | let result = await this.fixture.getHashForPath(input); 25 | 26 | expect(result.sourceCode).to.be.ok; 27 | delete result.sourceCode; 28 | expect(result).to.deep.equal(expectedInfo); 29 | }); 30 | 31 | it("Correctly handles binary files", async function() { 32 | const expectedInfo = { 33 | hash: '83af4f2b5a3e2dda1a322ac75799eee337d569a5', 34 | hasSourceMap: false, 35 | isInNodeModules: false, 36 | isMinified: false, 37 | isFileBinary: true 38 | }; 39 | 40 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'binaryfile.zip'); 41 | let result = await this.fixture.getHashForPath(input); 42 | expect(result.binaryData).to.be.ok; 43 | expect(result.binaryData.length > 16).to.be.ok; 44 | delete result.binaryData; 45 | expect(result).to.deep.equal(expectedInfo); 46 | }); 47 | 48 | 49 | it("Correctly computes a file hash for a canned file syncronously", function() { 50 | const expectedInfo = { 51 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 52 | hasSourceMap: false, 53 | isInNodeModules: false, 54 | isMinified: false, 55 | isFileBinary: false 56 | }; 57 | 58 | let input = path.resolve(__dirname, '..', 'test', 'fixtures', 'valid.js'); 59 | let result = this.fixture.getHashForPathSync(input); 60 | 61 | expect(result.sourceCode).to.be.ok; 62 | delete result.sourceCode; 63 | expect(result).to.deep.equal(expectedInfo); 64 | }); 65 | 66 | it("Doesn't rerun the file hash if you ask for it twice", async function() { 67 | const expectedInfo = { 68 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 69 | hasSourceMap: false, 70 | isInNodeModules: false, 71 | isMinified: false, 72 | isFileBinary: false 73 | }; 74 | 75 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 76 | let result = await this.fixture.getHashForPath(input); 77 | 78 | expect(result.sourceCode).to.be.ok; 79 | delete result.sourceCode; 80 | expect(result).to.deep.equal(expectedInfo); 81 | 82 | this.fixture.calculateHashForFile = () => Promise.reject(new Error("Didn't work")); 83 | result = await this.fixture.getHashForPath(input); 84 | 85 | // NB: The file hash cache itself shouldn't hold onto file contents, it should 86 | // only opportunistically return it if it had to read the contents anyways 87 | expect(result.sourceCode).to.be.not.ok; 88 | expect(result).to.deep.equal(expectedInfo); 89 | }); 90 | 91 | it("Throws on cache misses in production mode", function() { 92 | this.fixture = new FileChangeCache(null, true); 93 | 94 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 95 | expect(this.fixture.getHashForPath(input)).to.eventually.throw(Error); 96 | }); 97 | 98 | it("Successfully saves and loads its cache information", async function() { 99 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 100 | await this.fixture.getHashForPath(input); 101 | 102 | let targetCache = path.join(__dirname, 'fileChangeCache1.json.gz'); 103 | 104 | try { 105 | await this.fixture.save(targetCache); 106 | 107 | this.fixture = await FileChangeCache.loadFromFile(targetCache, null); 108 | 109 | this.fixture.calculateHashForFile = () => Promise.reject(new Error("Didn't work")); 110 | await this.fixture.getHashForPath(input); 111 | } finally { 112 | fs.unlinkSync(targetCache); 113 | } 114 | }); 115 | 116 | it("Detects changes to files and reruns hash", async function() { 117 | const expectedInfo = { 118 | hash: '4a92e95074156e8b46869519c43ddf10b59299a4', 119 | hasSourceMap: false, 120 | isInNodeModules: false, 121 | isMinified: false, 122 | isFileBinary: false 123 | }; 124 | 125 | let realInput = path.join(__dirname, '..', 'test', 'fixtures', 'valid.js'); 126 | let input = path.join(__dirname, 'tempfile.tmp'); 127 | let contents = await pfs.readFile(realInput); 128 | await pfs.writeFile(input, contents); 129 | 130 | let stat1 = await pfs.stat(realInput); 131 | let stat2 = await pfs.stat(input); 132 | expect(stat1.size).to.equal(stat2.size); 133 | 134 | try { 135 | let result = await this.fixture.getHashForPath(input); 136 | 137 | expect(result.sourceCode).to.be.ok; 138 | delete result.sourceCode; 139 | expect(result).to.deep.equal(expectedInfo); 140 | 141 | let fd = await pfs.open(input, 'a'); 142 | await pfs.write(fd, '\n\n\n\n'); 143 | await pfs.close(fd); 144 | 145 | // NB: Declaring these as 'var' works around a BabelJS compilation bug 146 | // where it can't deal with let + closure scoping 147 | var realCalc = this.fixture.calculateHashForFile; 148 | var hasCalledCalc = false; 149 | 150 | this.fixture.calculateHashForFile = function(...args) { 151 | hasCalledCalc = true; 152 | return realCalc(...args); 153 | }; 154 | 155 | result = await this.fixture.getHashForPath(input); 156 | 157 | expect(result.sourceCode).to.be.ok; 158 | delete result.sourceCode; 159 | 160 | expect(result).not.to.deep.equal(expectedInfo); 161 | expect(hasCalledCalc).to.be.ok; 162 | } finally { 163 | fs.unlinkSync(input); 164 | } 165 | }); 166 | 167 | it("Successfully finds if a file has a source map", async function() { 168 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'source_map.js'); 169 | let result = await this.fixture.getHashForPath(input); 170 | 171 | expect(result.hasSourceMap).to.be.ok; 172 | }); 173 | 174 | it("Successfully finds if a file has a source map synchronously", function() { 175 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'source_map.js'); 176 | let result = this.fixture.getHashForPathSync(input); 177 | 178 | expect(result.hasSourceMap).to.be.ok; 179 | }); 180 | 181 | it("Successfully finds if a file is minified", async function() { 182 | let input = path.join(__dirname, '..', 'test', 'fixtures', 'minified.js'); 183 | let result = await this.fixture.getHashForPath(input); 184 | 185 | expect(result.isMinified).to.be.ok; 186 | }); 187 | 188 | it("Successfully finds if a file is in node_modules", async function() { 189 | let input = path.join(__dirname, '..', 'node_modules', 'electron-compilers', 'package.json'); 190 | let result = await this.fixture.getHashForPath(input); 191 | 192 | expect(result.isInNodeModules).to.be.ok; 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/config-parser.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 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 | describe('the createCompilers method', function() { 19 | it('should return compilers', function() { 20 | let result = createCompilers(); 21 | expect(Object.keys(result).length > 0).to.be.ok; 22 | }); 23 | 24 | it('should definitely have these compilers', function() { 25 | let result = createCompilers(); 26 | 27 | expect(result['application/javascript']).to.be.ok; 28 | expect(result['text/less']).to.be.ok; 29 | }); 30 | }); 31 | 32 | describe('the createCompilerHostFromConfiguration method', function() { 33 | beforeEach(function() { 34 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 35 | mkdirp.sync(this.tempCacheDir); 36 | }); 37 | 38 | afterEach(function() { 39 | rimraf.sync(this.tempCacheDir); 40 | }); 41 | 42 | it('respects suppressing source maps (scenario test)', async function() { 43 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 44 | 45 | let result = createCompilerHostFromConfiguration({ 46 | appRoot: fixtureDir, 47 | rootCacheDir: this.tempCacheDir, 48 | options: { 49 | 'application/javascript': { 50 | "presets": ["stage-0", "es2015"], 51 | "sourceMaps": false 52 | } 53 | } 54 | }); 55 | 56 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 57 | d(JSON.stringify(compileInfo)); 58 | 59 | expect(compileInfo.mimeType).to.equal('application/javascript'); 60 | 61 | let lines = compileInfo.code.split('\n'); 62 | expect(lines.length > 5).to.be.ok; 63 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 64 | }); 65 | }); 66 | 67 | describe('the createCompilerHostFromBabelRc method', function() { 68 | beforeEach(function() { 69 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 70 | mkdirp.sync(this.tempCacheDir); 71 | }); 72 | 73 | afterEach(function() { 74 | rimraf.sync(this.tempCacheDir); 75 | if ('BABEL_ENV' in process.env) { 76 | delete process.env.ELECTRON_COMPILE_ENV; 77 | } 78 | }); 79 | 80 | it('reads from an environment-free file', async function() { 81 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 82 | 83 | let result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-noenv')); 84 | 85 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 86 | d(JSON.stringify(compileInfo)); 87 | 88 | expect(compileInfo.mimeType).to.equal('application/javascript'); 89 | 90 | let lines = compileInfo.code.split('\n'); 91 | expect(lines.length > 5).to.be.ok; 92 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).to.be.ok; 93 | }); 94 | 95 | it('uses the development env when env is unset', async function() { 96 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 97 | 98 | let result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-production')); 99 | 100 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 101 | d(JSON.stringify(compileInfo)); 102 | 103 | expect(compileInfo.mimeType).to.equal('application/javascript'); 104 | 105 | let lines = compileInfo.code.split('\n'); 106 | expect(lines.length > 5).to.be.ok; 107 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).to.be.ok; 108 | }); 109 | 110 | it('uses the production env when env is set', async function() { 111 | process.env.BABEL_ENV = 'production'; 112 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 113 | 114 | let result = await createCompilerHostFromBabelRc(path.join(fixtureDir, 'babelrc-production')); 115 | 116 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 117 | d(JSON.stringify(compileInfo)); 118 | 119 | expect(compileInfo.mimeType).to.equal('application/javascript'); 120 | 121 | let lines = compileInfo.code.split('\n'); 122 | expect(lines.length > 5).to.be.ok; 123 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 124 | }); 125 | }); 126 | 127 | describe('the createCompilerHostFromConfigFile method', function() { 128 | beforeEach(function() { 129 | this.tempCacheDir = path.join(__dirname, `__create_compiler_host_${testCount++}`); 130 | mkdirp.sync(this.tempCacheDir); 131 | }); 132 | 133 | afterEach(function() { 134 | rimraf.sync(this.tempCacheDir); 135 | if ('ELECTRON_COMPILE_ENV' in process.env) { 136 | delete process.env.ELECTRON_COMPILE_ENV; 137 | } 138 | }); 139 | 140 | it('reads from an environment-free file', async function() { 141 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 142 | 143 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-noenv')); 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(_.any(lines, (x) => x.match(/sourceMappingURL=/))).to.be.ok; 153 | }); 154 | 155 | it('uses the development env when env is unset', async function() { 156 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 157 | 158 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-production')); 159 | 160 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 161 | d(JSON.stringify(compileInfo)); 162 | 163 | expect(compileInfo.mimeType).to.equal('application/javascript'); 164 | 165 | let lines = compileInfo.code.split('\n'); 166 | expect(lines.length > 5).to.be.ok; 167 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).to.be.ok; 168 | }); 169 | 170 | it('uses the production env when env is set', async function() { 171 | process.env.ELECTRON_COMPILE_ENV = 'production'; 172 | let fixtureDir = path.join(__dirname, '..', 'test', 'fixtures'); 173 | 174 | let result = await createCompilerHostFromConfigFile(path.join(fixtureDir, 'compilerc-production')); 175 | 176 | let compileInfo = await result.compile(path.join(fixtureDir, 'valid.js')); 177 | d(JSON.stringify(compileInfo)); 178 | 179 | expect(compileInfo.mimeType).to.equal('application/javascript'); 180 | 181 | let lines = compileInfo.code.split('\n'); 182 | expect(lines.length > 5).to.be.ok; 183 | expect(_.any(lines, (x) => x.match(/sourceMappingURL=/))).not.to.be.ok; 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /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/compiler-host.js: -------------------------------------------------------------------------------- 1 | import './support.js'; 2 | 3 | import _ from 'lodash'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | import rimraf from 'rimraf'; 7 | import mkdirp from 'mkdirp'; 8 | import mimeTypes from 'mime-types'; 9 | import FileChangeCache from '../src/file-change-cache'; 10 | import CompilerHost from '../src/compiler-host'; 11 | 12 | const d = require('debug')('test:compiler-host'); 13 | 14 | let testCount=0; 15 | 16 | describe('All available compilers', function() { 17 | it('should have a MIME type in mime-types', function() { 18 | _.each(Object.keys(global.compilersByMimeType), (type) => { 19 | d(`Extension for ${type} is ${mimeTypes.extension(type)}`); 20 | expect(mimeTypes.extension(type)).to.be.ok; 21 | }); 22 | }); 23 | }); 24 | 25 | describe('The compiler host', function() { 26 | this.timeout(15*1000); 27 | 28 | beforeEach(function() { 29 | this.appRootDir = path.join(__dirname, '..'); 30 | this.fileChangeCache = new FileChangeCache(this.appRootDir); 31 | 32 | this.tempCacheDir = path.join(__dirname, `__compile_cache_${testCount++}`); 33 | mkdirp.sync(this.tempCacheDir); 34 | 35 | this.compilersByMimeType = _.reduce(Object.keys(global.compilersByMimeType), (acc, type) => { 36 | let Klass = global.compilersByMimeType[type]; 37 | acc[type] = new Klass(); 38 | return acc; 39 | }, {}); 40 | 41 | let InlineHtmlCompiler = Object.getPrototypeOf(this.compilersByMimeType['text/html']).constructor; 42 | this.compilersByMimeType['text/html'] = InlineHtmlCompiler.createFromCompilers(this.compilersByMimeType); 43 | 44 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, false); 45 | }); 46 | 47 | afterEach(function() { 48 | rimraf.sync(this.tempCacheDir); 49 | }); 50 | 51 | it('should compile basic HTML and not blow up', function() { 52 | let input = ''; 53 | let inFile = path.join(this.tempCacheDir, 'input.html'); 54 | fs.writeFileSync(inFile, input); 55 | 56 | let result = this.fixture.compileSync(inFile); 57 | 58 | expect(result.mimeType).to.equal('text/html'); 59 | expect(result.code.length > input.length).to.be.ok; 60 | }); 61 | 62 | it('Should compile everything in the fixtures directory', async function() { 63 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 64 | 65 | await this.fixture.compileAll(input, (filePath) => { 66 | if (filePath.match(/invalid/)) return false; 67 | if (filePath.match(/binaryfile/)) return false; 68 | if (filePath.match(/minified/)) return false; 69 | if (filePath.match(/source_map/)) return false; 70 | if (filePath.match(/babelrc/)) return false; 71 | if (filePath.match(/compilerc/)) return false; 72 | 73 | return true; 74 | }); 75 | }); 76 | 77 | it('Should compile everything in the fixtures directory sync', function() { 78 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 79 | 80 | this.fixture.compileAllSync(input, (filePath) => { 81 | if (filePath.match(/invalid/)) return false; 82 | if (filePath.match(/binaryfile/)) return false; 83 | if (filePath.match(/minified/)) return false; 84 | if (filePath.match(/source_map/)) return false; 85 | if (filePath.match(/babelrc/)) return false; 86 | if (filePath.match(/compilerc/)) return false; 87 | 88 | return true; 89 | }); 90 | }); 91 | 92 | it('Should read files from cache once we compile them', async function() { 93 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 94 | 95 | await this.fixture.compileAll(input, (filePath) => { 96 | if (filePath.match(/invalid/)) return false; 97 | if (filePath.match(/binaryfile/)) return false; 98 | if (filePath.match(/minified/)) return false; 99 | if (filePath.match(/source_map/)) return false; 100 | if (filePath.match(/babelrc/)) return false; 101 | if (filePath.match(/compilerc/)) return false; 102 | 103 | return true; 104 | }); 105 | 106 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, true); 107 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 108 | 109 | await this.fixture.compileAll(input, (filePath) => { 110 | if (filePath.match(/invalid/)) return false; 111 | if (filePath.match(/binaryfile/)) return false; 112 | if (filePath.match(/minified/)) return false; 113 | if (filePath.match(/source_map/)) return false; 114 | if (filePath.match(/babelrc/)) return false; 115 | if (filePath.match(/compilerc/)) return false; 116 | 117 | return true; 118 | }); 119 | }); 120 | 121 | it('Should read files from cache once we compile them synchronously', function() { 122 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 123 | 124 | this.fixture.compileAllSync(input, (filePath) => { 125 | if (filePath.match(/invalid/)) return false; 126 | if (filePath.match(/binaryfile/)) return false; 127 | if (filePath.match(/minified/)) return false; 128 | if (filePath.match(/source_map/)) return false; 129 | if (filePath.match(/babelrc/)) return false; 130 | if (filePath.match(/compilerc/)) return false; 131 | 132 | return true; 133 | }); 134 | 135 | this.fixture = new CompilerHost(this.tempCacheDir, this.compilersByMimeType, this.fileChangeCache, true); 136 | this.fixture.compileUncached = () => { throw new Error("Fail!"); }; 137 | 138 | this.fixture.compileAllSync(input, (filePath) => { 139 | if (filePath.match(/invalid/)) return false; 140 | if (filePath.match(/binaryfile/)) return false; 141 | if (filePath.match(/minified/)) return false; 142 | if (filePath.match(/source_map/)) return false; 143 | if (filePath.match(/babelrc/)) return false; 144 | if (filePath.match(/compilerc/)) return false; 145 | 146 | return true; 147 | }); 148 | }); 149 | 150 | it('Should read files from serialized compiler information', async function() { 151 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 152 | 153 | d("Attempting to run initial compile"); 154 | await this.fixture.compileAll(input, (filePath) => { 155 | if (filePath.match(/invalid/)) return false; 156 | if (filePath.match(/binaryfile/)) return false; 157 | if (filePath.match(/minified/)) return false; 158 | if (filePath.match(/source_map/)) return false; 159 | if (filePath.match(/babelrc/)) return false; 160 | if (filePath.match(/compilerc/)) return false; 161 | 162 | return true; 163 | }); 164 | 165 | d("Saving configuration"); 166 | await this.fixture.saveConfiguration(); 167 | 168 | d("Recreating from said configuration"); 169 | this.fixture = await CompilerHost.createReadonlyFromConfiguration(this.tempCacheDir, this.appRootDir); 170 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 171 | 172 | d("Recompiling everything from cached data"); 173 | await this.fixture.compileAll(input, (filePath) => { 174 | if (filePath.match(/invalid/)) return false; 175 | if (filePath.match(/binaryfile/)) return false; 176 | if (filePath.match(/minified/)) return false; 177 | if (filePath.match(/source_map/)) return false; 178 | if (filePath.match(/babelrc/)) return false; 179 | if (filePath.match(/compilerc/)) return false; 180 | 181 | return true; 182 | }); 183 | }); 184 | 185 | it('Should read files from serialized compiler information synchronously', function() { 186 | let input = path.join(__dirname, '..', 'test', 'fixtures'); 187 | 188 | d("Attempting to run initial compile"); 189 | this.fixture.compileAllSync(input, (filePath) => { 190 | if (filePath.match(/invalid/)) return false; 191 | if (filePath.match(/binaryfile/)) return false; 192 | if (filePath.match(/minified/)) return false; 193 | if (filePath.match(/source_map/)) return false; 194 | if (filePath.match(/babelrc/)) return false; 195 | if (filePath.match(/compilerc/)) return false; 196 | 197 | return true; 198 | }); 199 | 200 | d("Saving configuration"); 201 | this.fixture.saveConfigurationSync(); 202 | 203 | d("Recreating from said configuration"); 204 | this.fixture = CompilerHost.createReadonlyFromConfigurationSync(this.tempCacheDir, this.appRootDir); 205 | this.fixture.compileUncached = () => Promise.reject(new Error("Fail!")); 206 | 207 | d("Recompiling everything from cached data"); 208 | this.fixture.compileAllSync(input, (filePath) => { 209 | if (filePath.match(/invalid/)) return false; 210 | if (filePath.match(/binaryfile/)) return false; 211 | if (filePath.match(/minified/)) return false; 212 | if (filePath.match(/source_map/)) return false; 213 | if (filePath.match(/babelrc/)) return false; 214 | if (filePath.match(/compilerc/)) return false; 215 | 216 | return true; 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /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 | */ 27 | constructor(cachePath, fileChangeCache) { 28 | this.cachePath = cachePath; 29 | this.fileChangeCache = fileChangeCache; 30 | } 31 | 32 | /** 33 | * Creates a CompileCache from a class compatible with the CompilerBase 34 | * interface. This method uses the compiler name / version / options to 35 | * generate a unique directory name for cached results 36 | * 37 | * @param {string} cachePath The root path to use for the cache, a directory 38 | * representing the hash of the compiler parameters 39 | * will be created here. 40 | * 41 | * @param {CompilerBase} compiler The compiler to use for version / option 42 | * information. 43 | * 44 | * @param {FileChangedCache} fileChangeCache A file-change cache that is 45 | * optionally pre-loaded. 46 | * 47 | * @param {boolean} readOnlyMode Don't attempt to create the cache directory. 48 | * 49 | * @return {CompileCache} A configured CompileCache instance. 50 | */ 51 | static createFromCompiler(cachePath, compiler, fileChangeCache, readOnlyMode=false) { 52 | let newCachePath = null; 53 | let getCachePath = () => { 54 | if (newCachePath) return newCachePath; 55 | 56 | const digestObj = { 57 | name: compiler.name || Object.getPrototypeOf(compiler).constructor.name, 58 | version: compiler.getCompilerVersion(), 59 | options: compiler.compilerOptions 60 | }; 61 | 62 | newCachePath = path.join(cachePath, createDigestForObject(digestObj)); 63 | 64 | d(`Path for ${digestObj.name}: ${newCachePath}`); 65 | d(`Set up with parameters: ${JSON.stringify(digestObj)}`); 66 | 67 | if (!readOnlyMode) mkdirp.sync(newCachePath); 68 | return newCachePath; 69 | }; 70 | 71 | let ret = new CompileCache('', fileChangeCache); 72 | ret.getCachePath = getCachePath; 73 | 74 | return ret; 75 | } 76 | 77 | /** 78 | * Returns a file's compiled contents from the cache. 79 | * 80 | * @param {string} filePath The path to the file. FileChangedCache will look 81 | * up the hash and use that as the key in the cache. 82 | * 83 | * @return {Promise} An object with all kinds of information 84 | * 85 | * @property {Object} hashInfo The hash information returned from getHashForPath 86 | * @property {string} code The source code if the file was a text file 87 | * @property {Buffer} binaryData The file if it was a binary file 88 | * @property {string} mimeType The MIME type saved in the cache. 89 | * @property {string[]} dependentFiles The dependent files returned from 90 | * compiling the file, if any. 91 | */ 92 | async get(filePath) { 93 | d(`Fetching ${filePath} from cache`); 94 | let hashInfo = await this.fileChangeCache.getHashForPath(path.resolve(filePath)); 95 | 96 | let code = null; 97 | let mimeType = null; 98 | let binaryData = null; 99 | let dependentFiles = null; 100 | 101 | let cacheFile = null; 102 | try { 103 | cacheFile = path.join(this.getCachePath(), hashInfo.hash); 104 | let result = null; 105 | 106 | if (hashInfo.isFileBinary) { 107 | d("File is binary, reading out info"); 108 | let info = JSON.parse(await pfs.readFile(cacheFile + '.info')); 109 | mimeType = info.mimeType; 110 | dependentFiles = info.dependentFiles; 111 | 112 | binaryData = hashInfo.binaryData; 113 | if (!binaryData) { 114 | binaryData = await pfs.readFile(cacheFile); 115 | binaryData = await pzlib.gunzip(binaryData); 116 | } 117 | } else { 118 | let buf = await pfs.readFile(cacheFile); 119 | let str = (await pzlib.gunzip(buf)).toString('utf8'); 120 | 121 | result = JSON.parse(str); 122 | code = result.code; 123 | mimeType = result.mimeType; 124 | dependentFiles = result.dependentFiles; 125 | } 126 | } catch (e) { 127 | d(`Failed to read cache for ${filePath}, looked in ${cacheFile}: ${e.message}`); 128 | } 129 | 130 | return { hashInfo, code, mimeType, binaryData, dependentFiles }; 131 | } 132 | 133 | 134 | /** 135 | * Saves a compiled result to cache 136 | * 137 | * @param {Object} hashInfo The hash information returned from getHashForPath 138 | * 139 | * @param {string / Buffer} codeOrBinaryData The file's contents, either as 140 | * a string or a Buffer. 141 | * @param {string} mimeType The MIME type returned by the compiler. 142 | * 143 | * @param {string[]} dependentFiles The list of dependent files returned by 144 | * the compiler. 145 | * @return {Promise} Completion. 146 | */ 147 | async save(hashInfo, codeOrBinaryData, mimeType, dependentFiles) { 148 | let buf = null; 149 | let target = path.join(this.getCachePath(), hashInfo.hash); 150 | d(`Saving to ${target}`); 151 | 152 | if (hashInfo.isFileBinary) { 153 | buf = await pzlib.gzip(codeOrBinaryData); 154 | await pfs.writeFile(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8'); 155 | } else { 156 | buf = await pzlib.gzip(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles}))); 157 | } 158 | 159 | await pfs.writeFile(target, buf); 160 | } 161 | 162 | /** 163 | * Attempts to first get a key via {@link get}, then if it fails, call a method 164 | * to retrieve the contents, then save the result to cache. 165 | * 166 | * The fetcher parameter is expected to have the signature: 167 | * 168 | * Promise fetcher(filePath : string, hashInfo : Object); 169 | * 170 | * hashInfo is a value returned from getHashForPath 171 | * The return value of fetcher must be an Object with the properties: 172 | * 173 | * mimeType - the MIME type of the data to save 174 | * code (optional) - the source code as a string, if file is text 175 | * binaryData (optional) - the file contents as a Buffer, if file is binary 176 | * dependentFiles - the dependent files returned by the compiler. 177 | * 178 | * @param {string} filePath The path to the file. FileChangedCache will look 179 | * up the hash and use that as the key in the cache. 180 | * 181 | * @param {Function} fetcher A method which conforms to the description above. 182 | * 183 | * @return {Promise} An Object which has the same fields as the 184 | * {@link get} method return result. 185 | */ 186 | async getOrFetch(filePath, fetcher) { 187 | let cacheResult = await this.get(filePath); 188 | if (cacheResult.code || cacheResult.binaryData) return cacheResult; 189 | 190 | let result = await fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo }; 191 | 192 | if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) { 193 | d(`Cache miss: saving out info for ${filePath}`); 194 | await this.save(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles); 195 | } 196 | 197 | result.hashInfo = cacheResult.hashInfo; 198 | return result; 199 | } 200 | 201 | getSync(filePath) { 202 | d(`Fetching ${filePath} from cache`); 203 | let hashInfo = this.fileChangeCache.getHashForPathSync(path.resolve(filePath)); 204 | 205 | let code = null; 206 | let mimeType = null; 207 | let binaryData = null; 208 | let dependentFiles = null; 209 | 210 | try { 211 | let cacheFile = path.join(this.getCachePath(), hashInfo.hash); 212 | 213 | let result = null; 214 | if (hashInfo.isFileBinary) { 215 | d("File is binary, reading out info"); 216 | let info = JSON.parse(fs.readFileSync(cacheFile + '.info')); 217 | mimeType = info.mimeType; 218 | dependentFiles = info.dependentFiles; 219 | 220 | binaryData = hashInfo.binaryData; 221 | if (!binaryData) { 222 | binaryData = fs.readFileSync(cacheFile); 223 | binaryData = zlib.gunzipSync(binaryData); 224 | } 225 | } else { 226 | let buf = fs.readFileSync(cacheFile); 227 | let str = (zlib.gunzipSync(buf)).toString('utf8'); 228 | 229 | result = JSON.parse(str); 230 | code = result.code; 231 | mimeType = result.mimeType; 232 | dependentFiles = result.dependentFiles; 233 | } 234 | } catch (e) { 235 | d(`Failed to read cache for ${filePath}`); 236 | } 237 | 238 | return { hashInfo, code, mimeType, binaryData, dependentFiles }; 239 | } 240 | 241 | saveSync(hashInfo, codeOrBinaryData, mimeType, dependentFiles) { 242 | let buf = null; 243 | let target = path.join(this.getCachePath(), hashInfo.hash); 244 | d(`Saving to ${target}`); 245 | 246 | if (hashInfo.isFileBinary) { 247 | buf = zlib.gzipSync(codeOrBinaryData); 248 | fs.writeFileSync(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8'); 249 | } else { 250 | buf = zlib.gzipSync(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles}))); 251 | } 252 | 253 | fs.writeFileSync(target, buf); 254 | } 255 | 256 | getOrFetchSync(filePath, fetcher) { 257 | let cacheResult = this.getSync(filePath); 258 | if (cacheResult.code || cacheResult.binaryData) return cacheResult; 259 | 260 | let result = fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo }; 261 | 262 | if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) { 263 | d(`Cache miss: saving out info for ${filePath}`); 264 | this.saveSync(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles); 265 | } 266 | 267 | result.hashInfo = cacheResult.hashInfo; 268 | return result; 269 | } 270 | 271 | 272 | /** 273 | * @private 274 | */ 275 | getCachePath() { 276 | // NB: This is an evil hack so that createFromCompiler can stomp it 277 | // at will 278 | return this.cachePath; 279 | } 280 | 281 | 282 | /** 283 | * Returns whether a file should not be compiled. Note that this doesn't 284 | * necessarily mean it won't end up in the cache, only that its contents are 285 | * saved verbatim instead of trying to find an appropriate compiler. 286 | * 287 | * @param {Object} hashInfo The hash information returned from getHashForPath 288 | * 289 | * @return {boolean} True if a file should be ignored 290 | */ 291 | static shouldPassthrough(hashInfo) { 292 | return hashInfo.isMinified || hashInfo.isInNodeModules || hashInfo.hasSourceMap || hashInfo.isFileBinary; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /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 _ from 'lodash'; 6 | import sanitizeFilePath from './sanitize-paths'; 7 | 8 | const d = require('debug')('electron-compile:file-change-cache'); 9 | 10 | /** 11 | * This class caches information about files and determines whether they have 12 | * changed contents or not. Most importantly, this class caches the hash of seen 13 | * files so that at development time, we don't have to recalculate them constantly. 14 | * 15 | * This class is also the core of how electron-compile runs quickly in production 16 | * mode - after precompilation, the cache is serialized along with the rest of the 17 | * data in {@link CompilerHost}, so that when we load the app in production mode, 18 | * we don't end up calculating hashes of file content at all, only using the contents 19 | * of this cache. 20 | */ 21 | export default class FileChangedCache { 22 | constructor(appRoot, failOnCacheMiss=false) { 23 | this.appRoot = sanitizeFilePath(appRoot); 24 | this.failOnCacheMiss = failOnCacheMiss; 25 | this.changeCache = {}; 26 | } 27 | 28 | /** 29 | * Allows you to create a FileChangedCache from serialized data saved from 30 | * {@link getSavedData}. 31 | * 32 | * @param {Object} data Saved data from getSavedData. 33 | * 34 | * @param {string} appRoot The top-level directory for your application (i.e. 35 | * the one which has your package.json). 36 | * 37 | * @param {boolean} failOnCacheMiss (optional) If True, cache misses will throw. 38 | * 39 | * @return {FileChangedCache} 40 | */ 41 | static loadFromData(data, appRoot, failOnCacheMiss=true) { 42 | let ret = new FileChangedCache(appRoot, failOnCacheMiss); 43 | ret.changeCache = data.changeCache; 44 | ret.originalAppRoot = data.appRoot; 45 | 46 | return ret; 47 | } 48 | 49 | 50 | /** 51 | * Allows you to create a FileChangedCache from serialized data saved from 52 | * {@link save}. 53 | * 54 | * @param {string} file Saved data from save. 55 | * 56 | * @param {string} appRoot The top-level directory for your application (i.e. 57 | * the one which has your package.json). 58 | * 59 | * @param {boolean} failOnCacheMiss (optional) If True, cache misses will throw. 60 | * 61 | * @return {Promise} 62 | */ 63 | static async loadFromFile(file, appRoot, failOnCacheMiss=true) { 64 | d(`Loading canned FileChangedCache from ${file}`); 65 | 66 | let buf = await pfs.readFile(file); 67 | return FileChangedCache.loadFromData(JSON.parse(await pzlib.gunzip(buf)), appRoot, failOnCacheMiss); 68 | } 69 | 70 | 71 | /** 72 | * Returns information about a given file, including its hash. This method is 73 | * the main method for this cache. 74 | * 75 | * @param {string} absoluteFilePath The path to a file to retrieve info on. 76 | * 77 | * @return {Promise} 78 | * 79 | * @property {string} hash The SHA1 hash of the file 80 | * @property {boolean} isMinified True if the file is minified 81 | * @property {boolean} isInNodeModules True if the file is in a library directory 82 | * @property {boolean} hasSourceMap True if the file has a source map 83 | * @property {boolean} isFileBinary True if the file is not a text file 84 | * @property {Buffer} binaryData (optional) The buffer that was read if the file 85 | * was binary and there was a cache miss. 86 | * @property {string} code (optional) The string that was read if the file 87 | * was text and there was a cache miss 88 | */ 89 | async getHashForPath(absoluteFilePath) { 90 | let cacheKey = sanitizeFilePath(absoluteFilePath); 91 | if (this.appRoot) { 92 | cacheKey = cacheKey.replace(this.appRoot, ''); 93 | } 94 | 95 | // NB: We do this because x-require will include an absolute path from the 96 | // original built app and we need to still grok it 97 | if (this.originalAppRoot) { 98 | cacheKey = cacheKey.replace(this.originalAppRoot, ''); 99 | } 100 | 101 | let cacheEntry = this.changeCache[cacheKey]; 102 | 103 | if (this.failOnCacheMiss) { 104 | if (!cacheEntry) { 105 | d(`Tried to read file cache entry for ${absoluteFilePath}`); 106 | d(`cacheKey: ${cacheKey}, appRoot: ${this.appRoot}, originalAppRoot: ${this.originalAppRoot}`); 107 | throw new Error(`Asked for ${absoluteFilePath} but it was not precompiled!`); 108 | } 109 | 110 | return cacheEntry.info; 111 | } 112 | 113 | let stat = await pfs.stat(absoluteFilePath); 114 | let ctime = stat.ctime.getTime(); 115 | let size = stat.size; 116 | if (!stat || !stat.isFile()) throw new Error(`Can't stat ${absoluteFilePath}`); 117 | 118 | if (cacheEntry) { 119 | if (cacheEntry.ctime >= ctime && cacheEntry.size === size) { 120 | return cacheEntry.info; 121 | } 122 | 123 | d(`Invalidating cache entry: ${cacheEntry.ctime} === ${ctime} && ${cacheEntry.size} === ${size}`); 124 | delete this.changeCache.cacheEntry; 125 | } 126 | 127 | let {digest, sourceCode, binaryData} = await this.calculateHashForFile(absoluteFilePath); 128 | 129 | let info = { 130 | hash: digest, 131 | isMinified: FileChangedCache.contentsAreMinified(sourceCode || ''), 132 | isInNodeModules: FileChangedCache.isInNodeModules(absoluteFilePath), 133 | hasSourceMap: FileChangedCache.hasSourceMap(sourceCode || ''), 134 | isFileBinary: !!binaryData 135 | }; 136 | 137 | this.changeCache[cacheKey] = { ctime, size, info }; 138 | d(`Cache entry for ${cacheKey}: ${JSON.stringify(this.changeCache[cacheKey])}`); 139 | 140 | if (binaryData) { 141 | return _.extend({binaryData}, info); 142 | } else { 143 | return _.extend({sourceCode}, info); 144 | } 145 | } 146 | 147 | 148 | /** 149 | * Returns data that can passed to {@link loadFromData} to rehydrate this cache. 150 | * 151 | * @return {Object} 152 | */ 153 | getSavedData() { 154 | return { changeCache: this.changeCache, appRoot: this.appRoot }; 155 | } 156 | 157 | /** 158 | * Serializes this object's data to a file. 159 | * 160 | * @param {string} filePath The path to save data to. 161 | * 162 | * @return {Promise} Completion. 163 | */ 164 | async save(filePath) { 165 | let toSave = this.getSavedData(); 166 | 167 | let buf = await pzlib.gzip(new Buffer(JSON.stringify(toSave))); 168 | await pfs.writeFile(filePath, buf); 169 | } 170 | 171 | async calculateHashForFile(absoluteFilePath) { 172 | let buf = await pfs.readFile(absoluteFilePath); 173 | let encoding = FileChangedCache.detectFileEncoding(buf); 174 | 175 | if (!encoding) { 176 | let digest = crypto.createHash('sha1').update(buf).digest('hex'); 177 | return { sourceCode: null, digest, binaryData: buf }; 178 | } 179 | 180 | let sourceCode = await pfs.readFile(absoluteFilePath, encoding); 181 | let digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex'); 182 | 183 | return {sourceCode, digest, binaryData: null }; 184 | } 185 | 186 | getHashForPathSync(absoluteFilePath) { 187 | let cacheKey = sanitizeFilePath(absoluteFilePath); 188 | if (this.appRoot) { 189 | cacheKey = cacheKey.replace(this.appRoot, ''); 190 | } 191 | 192 | // NB: We do this because x-require will include an absolute path from the 193 | // original built app and we need to still grok it 194 | if (this.originalAppRoot) { 195 | cacheKey = cacheKey.replace(this.originalAppRoot, ''); 196 | } 197 | 198 | let cacheEntry = this.changeCache[cacheKey]; 199 | 200 | if (this.failOnCacheMiss) { 201 | if (!cacheEntry) { 202 | d(`Tried to read file cache entry for ${absoluteFilePath}`); 203 | d(`cacheKey: ${cacheKey}, appRoot: ${this.appRoot}, originalAppRoot: ${this.originalAppRoot}`); 204 | throw new Error(`Asked for ${absoluteFilePath} but it was not precompiled!`); 205 | } 206 | 207 | return cacheEntry.info; 208 | } 209 | 210 | let stat = fs.statSync(absoluteFilePath); 211 | let ctime = stat.ctime.getTime(); 212 | let size = stat.size; 213 | if (!stat || !stat.isFile()) throw new Error(`Can't stat ${absoluteFilePath}`); 214 | 215 | if (cacheEntry) { 216 | if (cacheEntry.ctime >= ctime && cacheEntry.size === size) { 217 | return cacheEntry.info; 218 | } 219 | 220 | d(`Invalidating cache entry: ${cacheEntry.ctime} === ${ctime} && ${cacheEntry.size} === ${size}`); 221 | delete this.changeCache.cacheEntry; 222 | } 223 | 224 | let {digest, sourceCode, binaryData} = this.calculateHashForFileSync(absoluteFilePath); 225 | 226 | let info = { 227 | hash: digest, 228 | isMinified: FileChangedCache.contentsAreMinified(sourceCode || ''), 229 | isInNodeModules: FileChangedCache.isInNodeModules(absoluteFilePath), 230 | hasSourceMap: FileChangedCache.hasSourceMap(sourceCode || ''), 231 | isFileBinary: !!binaryData 232 | }; 233 | 234 | this.changeCache[cacheKey] = { ctime, size, info }; 235 | d(`Cache entry for ${cacheKey}: ${JSON.stringify(this.changeCache[cacheKey])}`); 236 | 237 | if (binaryData) { 238 | return _.extend({binaryData}, info); 239 | } else { 240 | return _.extend({sourceCode}, info); 241 | } 242 | } 243 | 244 | saveSync(filePath) { 245 | let toSave = this.getSavedData(); 246 | 247 | let buf = zlib.gzipSync(new Buffer(JSON.stringify(toSave))); 248 | fs.writeFileSync(filePath, buf); 249 | } 250 | 251 | calculateHashForFileSync(absoluteFilePath) { 252 | let buf = fs.readFileSync(absoluteFilePath); 253 | let encoding = FileChangedCache.detectFileEncoding(buf); 254 | 255 | if (!encoding) { 256 | let digest = crypto.createHash('sha1').update(buf).digest('hex'); 257 | return { sourceCode: null, digest, binaryData: buf}; 258 | } 259 | 260 | let sourceCode = fs.readFileSync(absoluteFilePath, encoding); 261 | let digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex'); 262 | 263 | return {sourceCode, digest, binaryData: null}; 264 | } 265 | 266 | 267 | /** 268 | * Determines via some statistics whether a file is likely to be minified. 269 | * 270 | * @private 271 | */ 272 | static contentsAreMinified(source) { 273 | let length = source.length; 274 | if (length > 1024) length = 1024; 275 | 276 | let newlineCount = 0; 277 | 278 | // Roll through the characters and determine the average line length 279 | for(let i=0; i < source.length; i++) { 280 | if (source[i] === '\n') newlineCount++; 281 | } 282 | 283 | // No Newlines? Any file other than a super small one is minified 284 | if (newlineCount === 0) { 285 | return (length > 80); 286 | } 287 | 288 | let avgLineLength = length / newlineCount; 289 | return (avgLineLength > 80); 290 | } 291 | 292 | 293 | /** 294 | * Determines whether a path is in node_modules or the Electron init code 295 | * 296 | * @private 297 | */ 298 | static isInNodeModules(filePath) { 299 | return !!(filePath.match(/node_modules[\\\/]/i) || filePath.match(/atom\.asar/)); 300 | } 301 | 302 | 303 | /** 304 | * Returns whether a file has an inline source map 305 | * 306 | * @private 307 | */ 308 | static hasSourceMap(sourceCode) { 309 | return sourceCode.lastIndexOf('//# sourceMap') > sourceCode.lastIndexOf('\n'); 310 | } 311 | 312 | /** 313 | * Determines the encoding of a file from the two most common encodings by trying 314 | * to decode it then looking for encoding errors 315 | * 316 | * @private 317 | */ 318 | static detectFileEncoding(buffer) { 319 | if (buffer.length < 1) return false; 320 | let buf = (buffer.length < 4096 ? buffer : buffer.slice(0, 4096)); 321 | 322 | const encodings = ['utf8', 'utf16le']; 323 | 324 | let encoding = _.find( 325 | encodings, 326 | (x) => !FileChangedCache.containsControlCharacters(buf.toString(x))); 327 | 328 | return encoding; 329 | } 330 | 331 | /** 332 | * Determines whether a string is likely to be poorly encoded by looking for 333 | * control characters above a certain threshold 334 | * 335 | * @private 336 | */ 337 | static containsControlCharacters(str) { 338 | let controlCount = 0; 339 | let threshold = (str.length < 64 ? 2 : 16); 340 | 341 | for (let i=0; i < str.length; i++) { 342 | let c = str.charCodeAt(i); 343 | if (c === 65536 || c < 8) controlCount++; 344 | 345 | if (controlCount > threshold) return true; 346 | } 347 | 348 | if (controlCount === 0) return false; 349 | return (controlCount / str.length) < 0.02; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/config-parser.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import mkdirp from 'mkdirp'; 5 | import {pfs} from './promise'; 6 | 7 | import FileChangedCache from './file-change-cache'; 8 | import CompilerHost from './compiler-host'; 9 | import { initializeProtocolHook } from './protocol-hook'; 10 | import registerRequireExtension from './require-hook'; 11 | 12 | const d = require('debug')('electron-compile:config-parser'); 13 | 14 | // NB: We intentionally delay-load this so that in production, you can create 15 | // cache-only versions of these compilers 16 | let allCompilerClasses = null; 17 | 18 | function statSyncNoException(fsPath) { 19 | if ('statSyncNoException' in fs) { 20 | return fs.statSyncNoException(fsPath); 21 | } 22 | 23 | try { 24 | return fs.statSync(fsPath); 25 | } catch (e) { 26 | return null; 27 | } 28 | } 29 | 30 | 31 | /** 32 | * Initialize the global hooks (protocol hook for file:, node.js hook) 33 | * independent of initializing the compiler. This method is usually called by 34 | * init instead of directly 35 | * 36 | * @param {CompilerHost} compilerHost The compiler host to use. 37 | * 38 | */ 39 | export function initializeGlobalHooks(compilerHost) { 40 | let globalVar = (global || window); 41 | globalVar.globalCompilerHost = compilerHost; 42 | 43 | registerRequireExtension(compilerHost); 44 | 45 | if ('type' in process && process.type === 'browser') { 46 | const { app } = require('electron'); 47 | 48 | let protoify = function() { initializeProtocolHook(compilerHost); }; 49 | if (app.isReady()) { 50 | protoify(); 51 | } else { 52 | app.on('ready', protoify); 53 | } 54 | } 55 | } 56 | 57 | 58 | /** 59 | * Initialize electron-compile and set it up, either for development or 60 | * production use. This is almost always the only method you need to use in order 61 | * to use electron-compile. 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 {string} mainModule The module to require in, relative to the module 67 | * calling init, that will start your app. Write this 68 | * as if you were writing a require call from here. 69 | * 70 | * @param {bool} productionMode If explicitly True/False, will set read-only 71 | * mode to be disabled/enabled. If not, we'll 72 | * guess based on the presence of a production 73 | * cache. 74 | */ 75 | export function init(appRoot, mainModule, productionMode = null) { 76 | let compilerHost = null; 77 | let cacheDir = path.join(appRoot, '.cache'); 78 | 79 | if (productionMode === null) { 80 | productionMode = !!statSyncNoException(cacheDir); 81 | } 82 | 83 | if (productionMode) { 84 | // In read-only mode, we'll assume that everything is in `appRoot/.cache` 85 | compilerHost = CompilerHost.createReadonlyFromConfigurationSync(cacheDir, appRoot); 86 | } else { 87 | compilerHost = createCompilerHostFromProjectRootSync(appRoot); 88 | } 89 | 90 | initializeGlobalHooks(compilerHost); 91 | require.main.require(mainModule); 92 | } 93 | 94 | 95 | /** 96 | * Creates a {@link CompilerHost} with the given information. This method is 97 | * usually called by {@link createCompilerHostFromProjectRoot}. 98 | * 99 | * @private 100 | */ 101 | export function createCompilerHostFromConfiguration(info) { 102 | let compilers = createCompilers(); 103 | let rootCacheDir = info.rootCacheDir || calculateDefaultCompileCacheDirectory(); 104 | 105 | d(`Creating CompilerHost: ${JSON.stringify(info)}, rootCacheDir = ${rootCacheDir}`); 106 | let fileChangeCache = new FileChangedCache(info.appRoot); 107 | let ret = new CompilerHost(rootCacheDir, compilers, fileChangeCache, false, compilers['text/plain']); 108 | 109 | _.each(Object.keys(info.options || {}), (x) => { 110 | let opts = info.options[x]; 111 | if (!(x in compilers)) { 112 | throw new Error(`Found compiler settings for missing compiler: ${x}`); 113 | } 114 | 115 | d(`Setting options for ${x}: ${JSON.stringify(opts)}`); 116 | compilers[x].compilerOptions = opts; 117 | }); 118 | 119 | // NB: It's super important that we guarantee that the configuration is saved 120 | // out, because we'll need to re-read it in the renderer process 121 | d(`Created compiler host with options: ${JSON.stringify(info)}`); 122 | ret.saveConfigurationSync(); 123 | return ret; 124 | } 125 | 126 | /** 127 | * Creates a compiler host from a .babelrc file. This method is usually called 128 | * from {@link createCompilerHostFromProjectRoot} instead of used directly. 129 | * 130 | * @param {string} file The path to a .babelrc file 131 | * 132 | * @param {string} rootCacheDir (optional) The directory to use as a cache. 133 | * 134 | * @return {Promise} A set-up compiler host 135 | */ 136 | export async function createCompilerHostFromBabelRc(file, rootCacheDir=null) { 137 | let info = JSON.parse(await pfs.readFile(file, 'utf8')); 138 | 139 | // package.json 140 | if ('babel' in info) { 141 | info = info.babel; 142 | } 143 | 144 | if ('env' in info) { 145 | let ourEnv = process.env.BABEL_ENV || process.env.NODE_ENV || 'development'; 146 | info = info.env[ourEnv]; 147 | } 148 | 149 | // Are we still package.json (i.e. is there no babel info whatsoever?) 150 | if ('name' in info && 'version' in info) { 151 | return createCompilerHostFromConfiguration({ 152 | appRoot: path.dirname(file), 153 | options: getDefaultConfiguration(), 154 | rootCacheDir 155 | }); 156 | } 157 | 158 | return createCompilerHostFromConfiguration({ 159 | appRoot: path.dirname(file), 160 | options: { 161 | 'application/javascript': info 162 | }, 163 | rootCacheDir 164 | }); 165 | } 166 | 167 | 168 | /** 169 | * Creates a compiler host from a .compilerc file. This method is usually called 170 | * from {@link createCompilerHostFromProjectRoot} instead of used directly. 171 | * 172 | * @param {string} file The path to a .compilerc file 173 | * 174 | * @param {string} rootCacheDir (optional) The directory to use as a cache. 175 | * 176 | * @return {Promise} A set-up compiler host 177 | */ 178 | export async function createCompilerHostFromConfigFile(file, rootCacheDir=null) { 179 | let info = JSON.parse(await pfs.readFile(file, 'utf8')); 180 | 181 | if ('env' in info) { 182 | let ourEnv = process.env.ELECTRON_COMPILE_ENV || process.env.NODE_ENV || 'development'; 183 | info = info.env[ourEnv]; 184 | } 185 | 186 | return createCompilerHostFromConfiguration({ 187 | appRoot: path.dirname(file), 188 | options: info, 189 | rootCacheDir 190 | }); 191 | } 192 | 193 | 194 | /** 195 | * Creates a configured {@link CompilerHost} instance from the project root 196 | * directory. This method first searches for a .compilerc, then falls back to the 197 | * default locations for Babel configuration info. If neither are found, defaults 198 | * to standard settings 199 | * 200 | * @param {string} rootDir The root application directory (i.e. the directory 201 | * that has the app's package.json) 202 | * 203 | * @param {string} rootCacheDir (optional) The directory to use as a cache. 204 | * 205 | * @return {Promise} A set-up compiler host 206 | */ 207 | export async function createCompilerHostFromProjectRoot(rootDir, rootCacheDir=null) { 208 | let compilerc = path.join(rootDir, '.compilerc'); 209 | if (statSyncNoException(compilerc)) { 210 | d(`Found a .compilerc at ${compilerc}, using it`); 211 | return await createCompilerHostFromConfigFile(compilerc, rootCacheDir); 212 | } 213 | 214 | let babelrc = path.join(rootDir, '.babelrc'); 215 | if (statSyncNoException(babelrc)) { 216 | d(`Found a .babelrc at ${babelrc}, using it`); 217 | return await createCompilerHostFromBabelRc(babelrc, rootCacheDir); 218 | } 219 | 220 | d(`Using package.json or default parameters at ${rootDir}`); 221 | return await createCompilerHostFromBabelRc(path.join(rootDir, 'package.json'), rootCacheDir); 222 | } 223 | 224 | export function createCompilerHostFromBabelRcSync(file, rootCacheDir=null) { 225 | let info = JSON.parse(fs.readFileSync(file, 'utf8')); 226 | 227 | // package.json 228 | if ('babel' in info) { 229 | info = info.babel; 230 | } 231 | 232 | if ('env' in info) { 233 | let ourEnv = process.env.BABEL_ENV || process.env.NODE_ENV || 'development'; 234 | info = info.env[ourEnv]; 235 | } 236 | 237 | // Are we still package.json (i.e. is there no babel info whatsoever?) 238 | if ('name' in info && 'version' in info) { 239 | return createCompilerHostFromConfiguration({ 240 | appRoot: path.dirname(file), 241 | options: getDefaultConfiguration(), 242 | rootCacheDir 243 | }); 244 | } 245 | 246 | return createCompilerHostFromConfiguration({ 247 | appRoot: path.dirname(file), 248 | options: { 249 | 'application/javascript': info 250 | }, 251 | rootCacheDir 252 | }); 253 | } 254 | 255 | export function createCompilerHostFromConfigFileSync(file, rootCacheDir=null) { 256 | let info = JSON.parse(fs.readFileSync(file, 'utf8')); 257 | 258 | if ('env' in info) { 259 | let ourEnv = process.env.ELECTRON_COMPILE_ENV || process.env.NODE_ENV || 'development'; 260 | info = info.env[ourEnv]; 261 | } 262 | 263 | return createCompilerHostFromConfiguration({ 264 | appRoot: path.dirname(file), 265 | options: info, 266 | rootCacheDir 267 | }); 268 | } 269 | 270 | export function createCompilerHostFromProjectRootSync(rootDir, rootCacheDir=null) { 271 | let compilerc = path.join(rootDir, '.compilerc'); 272 | if (statSyncNoException(compilerc)) { 273 | d(`Found a .compilerc at ${compilerc}, using it`); 274 | return createCompilerHostFromConfigFileSync(compilerc, rootCacheDir); 275 | } 276 | 277 | let babelrc = path.join(rootDir, '.babelrc'); 278 | if (statSyncNoException(babelrc)) { 279 | d(`Found a .babelrc at ${babelrc}, using it`); 280 | return createCompilerHostFromBabelRcSync(babelrc, rootCacheDir); 281 | } 282 | 283 | d(`Using package.json or default parameters at ${rootDir}`); 284 | return createCompilerHostFromBabelRcSync(path.join(rootDir, 'package.json'), rootCacheDir); 285 | } 286 | 287 | /** 288 | * Returns what electron-compile would use as a default rootCacheDir. Usually only 289 | * used for debugging purposes 290 | * 291 | * @return {string} A path that may or may not exist where electron-compile would 292 | * set up a development mode cache. 293 | */ 294 | export function calculateDefaultCompileCacheDirectory() { 295 | let tmpDir = process.env.TEMP || process.env.TMPDIR || '/tmp'; 296 | let hash = require('crypto').createHash('md5').update(process.execPath).digest('hex'); 297 | 298 | let cacheDir = path.join(tmpDir, `compileCache_${hash}`); 299 | mkdirp.sync(cacheDir); 300 | 301 | d(`Using default cache directory: ${cacheDir}`); 302 | return cacheDir; 303 | } 304 | 305 | 306 | /** 307 | * Returns the default .configrc if no configuration information can be found. 308 | * 309 | * @return {Object} A list of default config settings for electron-compiler. 310 | */ 311 | export function getDefaultConfiguration() { 312 | return { 313 | 'application/javascript': { 314 | "presets": ["stage-0", "es2015", "react"], 315 | "sourceMaps": "inline" 316 | } 317 | }; 318 | } 319 | 320 | /** 321 | * Allows you to create new instances of all compilers that are supported by 322 | * electron-compile and use them directly. Currently supports Babel, CoffeeScript, 323 | * TypeScript, LESS, and Jade. 324 | * 325 | * @return {Object} An Object whose Keys are MIME types, and whose values 326 | * are instances of @{link CompilerBase}. 327 | */ 328 | export function createCompilers() { 329 | if (!allCompilerClasses) { 330 | // First we want to see if electron-compilers itself has been installed with 331 | // devDependencies. If that's not the case, check to see if 332 | // electron-compilers is installed as a peer dependency (probably as a 333 | // devDependency of the root project). 334 | const locations = ['electron-compilers', '../../electron-compilers']; 335 | 336 | for (let location of locations) { 337 | try { 338 | allCompilerClasses = require(location); 339 | } catch (e) { 340 | // Yolo 341 | } 342 | } 343 | 344 | if (!allCompilerClasses) { 345 | throw new Error("Electron compilers not found but were requested to be loaded"); 346 | } 347 | } 348 | 349 | // NB: Note that this code is carefully set up so that InlineHtmlCompiler 350 | // (i.e. classes with `createFromCompilers`) initially get an empty object, 351 | // but will have a reference to the final result of what we return, which 352 | // resolves the circular dependency we'd otherwise have here. 353 | let ret = {}; 354 | let instantiatedClasses = _.map(allCompilerClasses, (Klass) => { 355 | if ('createFromCompilers' in Klass) { 356 | return Klass.createFromCompilers(ret); 357 | } else { 358 | return new Klass(); 359 | } 360 | }); 361 | 362 | _.reduce(instantiatedClasses, (acc,x) => { 363 | let Klass = Object.getPrototypeOf(x).constructor; 364 | 365 | for (let type of Klass.getInputMimeTypes()) { acc[type] = x; } 366 | return acc; 367 | }, ret); 368 | 369 | return ret; 370 | } 371 | -------------------------------------------------------------------------------- /src/compiler-host.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mimeTypes from 'mime-types'; 3 | import fs from 'fs'; 4 | import zlib from 'zlib'; 5 | import path from 'path'; 6 | import {pfs, pzlib} from './promise'; 7 | 8 | import {forAllFiles, forAllFilesSync} from './for-all-files'; 9 | import CompileCache from './compile-cache'; 10 | import FileChangedCache from './file-change-cache'; 11 | import ReadOnlyCompiler from './read-only-compiler'; 12 | 13 | const d = require('debug')('electron-compile:compiler-host'); 14 | 15 | // This isn't even my 16 | const finalForms = { 17 | 'text/javascript': true, 18 | 'application/javascript': true, 19 | 'text/html': true, 20 | 'text/css': true, 21 | 'image/svg+xml': true 22 | }; 23 | 24 | /** 25 | * This class is the top-level class that encapsulates all of the logic of 26 | * compiling and caching application code. If you're looking for a "Main class", 27 | * this is it. 28 | * 29 | * This class can be created directly but it is usually created via the methods 30 | * in config-parser, which will among other things, set up the compiler options 31 | * given a project root. 32 | * 33 | * CompilerHost is also the top-level class that knows how to serialize all of the 34 | * information necessary to recreate itself, either as a development host (i.e. 35 | * will allow cache misses and actual compilation), or as a read-only version of 36 | * itself for production. 37 | */ 38 | export default class CompilerHost { 39 | /** 40 | * Creates an instance of CompilerHost. You probably want to use the methods 41 | * in config-parser for development, or {@link createReadonlyFromConfiguration} 42 | * for production instead. 43 | * 44 | * @param {string} rootCacheDir The root directory to use for the cache 45 | * 46 | * @param {Object} compilers an Object whose keys are input MIME types and 47 | * whose values are instances of CompilerBase. Create 48 | * this via the {@link createCompilers} method in 49 | * config-parser. 50 | * 51 | * @param {FileChangedCache} fileChangeCache A file-change cache that is 52 | * optionally pre-loaded. 53 | * 54 | * @param {boolean} readOnlyMode If True, cache misses will fail and 55 | * compilation will not be attempted. 56 | * 57 | * @param {CompilerBase} fallbackCompiler (optional) When a file is compiled 58 | * which doesn't have a matching compiler, 59 | * this compiler will be used instead. If 60 | * null, will fail compilation. A good 61 | * alternate fallback is the compiler for 62 | * 'text/plain', which is guaranteed to be 63 | * present. 64 | */ 65 | constructor(rootCacheDir, compilers, fileChangeCache, readOnlyMode, fallbackCompiler = null) { 66 | let compilersByMimeType = _.assign({}, compilers); 67 | _.assign(this, {rootCacheDir, compilersByMimeType, fileChangeCache, readOnlyMode, fallbackCompiler}); 68 | this.appRoot = this.fileChangeCache.appRoot; 69 | 70 | this.cachesForCompilers = _.reduce(Object.keys(compilersByMimeType), (acc, x) => { 71 | let compiler = compilersByMimeType[x]; 72 | if (acc.has(compiler)) return acc; 73 | 74 | acc.set( 75 | compiler, 76 | CompileCache.createFromCompiler(rootCacheDir, compiler, fileChangeCache, readOnlyMode)); 77 | return acc; 78 | }, new Map()); 79 | } 80 | 81 | /** 82 | * Creates a production-mode CompilerHost from the previously saved 83 | * configuration 84 | * 85 | * @param {string} rootCacheDir The root directory to use for the cache. This 86 | * cache must have cache information saved via 87 | * {@link saveConfiguration} 88 | * 89 | * @param {string} appRoot The top-level directory for your application (i.e. 90 | * the one which has your package.json). 91 | * 92 | * @param {CompilerBase} fallbackCompiler (optional) When a file is compiled 93 | * which doesn't have a matching compiler, 94 | * this compiler will be used instead. If 95 | * null, will fail compilation. A good 96 | * alternate fallback is the compiler for 97 | * 'text/plain', which is guaranteed to be 98 | * present. 99 | * 100 | * @return {Promise} A read-only CompilerHost 101 | */ 102 | static async createReadonlyFromConfiguration(rootCacheDir, appRoot, fallbackCompiler=null) { 103 | let target = path.join(rootCacheDir, 'compiler-info.json.gz'); 104 | let buf = await pfs.readFile(target); 105 | let info = JSON.parse(await pzlib.gunzip(buf)); 106 | 107 | let fileChangeCache = FileChangedCache.loadFromData(info.fileChangeCache, appRoot, true); 108 | 109 | let compilers = _.reduce(Object.keys(info.compilers), (acc, x) => { 110 | let cur = info.compilers[x]; 111 | acc[x] = new ReadOnlyCompiler(cur.name, cur.compilerVersion, cur.compilerOptions, cur.inputMimeTypes); 112 | 113 | return acc; 114 | }, {}); 115 | 116 | return new CompilerHost(rootCacheDir, compilers, fileChangeCache, true, fallbackCompiler); 117 | } 118 | 119 | /** 120 | * Creates a development-mode CompilerHost from the previously saved 121 | * configuration. 122 | * 123 | * @param {string} rootCacheDir The root directory to use for the cache. This 124 | * cache must have cache information saved via 125 | * {@link saveConfiguration} 126 | * 127 | * @param {string} appRoot The top-level directory for your application (i.e. 128 | * the one which has your package.json). 129 | * 130 | * @param {Object} compilersByMimeType an Object whose keys are input MIME 131 | * types and whose values are instances 132 | * of CompilerBase. Create this via the 133 | * {@link createCompilers} method in 134 | * config-parser. 135 | * 136 | * @param {CompilerBase} fallbackCompiler (optional) When a file is compiled 137 | * which doesn't have a matching compiler, 138 | * this compiler will be used instead. If 139 | * null, will fail compilation. A good 140 | * alternate fallback is the compiler for 141 | * 'text/plain', which is guaranteed to be 142 | * present. 143 | * 144 | * @return {Promise} A read-only CompilerHost 145 | */ 146 | static async createFromConfiguration(rootCacheDir, appRoot, compilersByMimeType, fallbackCompiler=null) { 147 | let target = path.join(rootCacheDir, 'compiler-info.json.gz'); 148 | let buf = await pfs.readFile(target); 149 | let info = JSON.parse(await pzlib.gunzip(buf)); 150 | 151 | let fileChangeCache = FileChangedCache.loadFromData(info.fileChangeCache, appRoot, false); 152 | 153 | _.each(Object.keys(info.compilers), (x) => { 154 | let cur = info.compilers[x]; 155 | compilersByMimeType[x].compilerOptions = cur.compilerOptions; 156 | }); 157 | 158 | return new CompilerHost(rootCacheDir, compilersByMimeType, fileChangeCache, false, fallbackCompiler); 159 | } 160 | 161 | 162 | /** 163 | * Saves the current compiler configuration to a file that 164 | * {@link createReadonlyFromConfiguration} can use to recreate the current 165 | * compiler environment 166 | * 167 | * @return {Promise} Completion 168 | */ 169 | async saveConfiguration() { 170 | let serializedCompilerOpts = _.reduce(Object.keys(this.compilersByMimeType), (acc, x) => { 171 | let compiler = this.compilersByMimeType[x]; 172 | let Klass = Object.getPrototypeOf(compiler).constructor; 173 | 174 | let val = { 175 | name: Klass.name, 176 | inputMimeTypes: Klass.getInputMimeTypes(), 177 | compilerOptions: compiler.compilerOptions, 178 | compilerVersion: compiler.getCompilerVersion() 179 | }; 180 | 181 | acc[x] = val; 182 | return acc; 183 | }, {}); 184 | 185 | let info = { 186 | fileChangeCache: this.fileChangeCache.getSavedData(), 187 | compilers: serializedCompilerOpts 188 | }; 189 | 190 | let target = path.join(this.rootCacheDir, 'compiler-info.json.gz'); 191 | let buf = await pzlib.gzip(new Buffer(JSON.stringify(info))); 192 | await pfs.writeFile(target, buf); 193 | } 194 | 195 | /** 196 | * Compiles a file and returns the compiled result. 197 | * 198 | * @param {string} filePath The path to the file to compile 199 | * 200 | * @return {Promise} An Object with the compiled result 201 | * 202 | * @property {Object} hashInfo The hash information returned from getHashForPath 203 | * @property {string} code The source code if the file was a text file 204 | * @property {Buffer} binaryData The file if it was a binary file 205 | * @property {string} mimeType The MIME type saved in the cache. 206 | * @property {string[]} dependentFiles The dependent files returned from 207 | * compiling the file, if any. 208 | */ 209 | compile(filePath) { 210 | return (this.readOnlyMode ? this.compileReadOnly(filePath) : this.fullCompile(filePath)); 211 | } 212 | 213 | 214 | /** 215 | * Handles compilation in read-only mode 216 | * 217 | * @private 218 | */ 219 | async compileReadOnly(filePath) { 220 | // We guarantee that node_modules are always shipped directly 221 | let type = mimeTypes.lookup(filePath); 222 | if (FileChangedCache.isInNodeModules(filePath)) { 223 | return { 224 | mimeType: type || 'application/javascript', 225 | code: await pfs.readFile(filePath, 'utf8') 226 | }; 227 | } 228 | 229 | let hashInfo = await this.fileChangeCache.getHashForPath(filePath); 230 | 231 | // NB: Here, we're basically only using the compiler here to find 232 | // the appropriate CompileCache 233 | let compiler = CompilerHost.shouldPassthrough(hashInfo) ? 234 | this.getPassthroughCompiler() : 235 | this.compilersByMimeType[type || '__lolnothere']; 236 | 237 | if (!compiler) { 238 | compiler = this.fallbackCompiler; 239 | 240 | let { code, binaryData, mimeType } = await compiler.get(filePath); 241 | return { code: code || binaryData, mimeType }; 242 | } 243 | 244 | let cache = this.cachesForCompilers.get(compiler); 245 | let {code, binaryData, mimeType} = await cache.get(filePath); 246 | 247 | code = code || binaryData; 248 | if (!code || !mimeType) { 249 | throw new Error(`Asked to compile ${filePath} in production, is this file not precompiled?`); 250 | } 251 | 252 | return { code, mimeType }; 253 | } 254 | 255 | /** 256 | * Handles compilation in read-write mode 257 | * 258 | * @private 259 | */ 260 | async fullCompile(filePath) { 261 | d(`Compiling ${filePath}`); 262 | 263 | let hashInfo = await this.fileChangeCache.getHashForPath(filePath); 264 | let type = mimeTypes.lookup(filePath); 265 | 266 | if (hashInfo.isInNodeModules) { 267 | let code = hashInfo.sourceCode || await pfs.readFile(filePath, 'utf8'); 268 | return { code, mimeType: type }; 269 | } 270 | 271 | let compiler = CompilerHost.shouldPassthrough(hashInfo) ? 272 | this.getPassthroughCompiler() : 273 | this.compilersByMimeType[type || '__lolnothere']; 274 | 275 | if (!compiler) { 276 | d(`Falling back to passthrough compiler for ${filePath}`); 277 | compiler = this.fallbackCompiler; 278 | } 279 | 280 | if (!compiler) { 281 | throw new Error(`Couldn't find a compiler for ${filePath}`); 282 | } 283 | 284 | let cache = this.cachesForCompilers.get(compiler); 285 | return await cache.getOrFetch( 286 | filePath, 287 | (filePath, hashInfo) => this.compileUncached(filePath, hashInfo, compiler)); 288 | } 289 | 290 | /** 291 | * Handles invoking compilers independent of caching 292 | * 293 | * @private 294 | */ 295 | async compileUncached(filePath, hashInfo, compiler) { 296 | let inputMimeType = mimeTypes.lookup(filePath); 297 | 298 | if (hashInfo.isFileBinary) { 299 | return { 300 | binaryData: hashInfo.binaryData || await pfs.readFile(filePath), 301 | mimeType: inputMimeType, 302 | dependentFiles: [] 303 | }; 304 | } 305 | 306 | let ctx = {}; 307 | let code = hashInfo.sourceCode || await pfs.readFile(filePath, 'utf8'); 308 | 309 | if (!(await compiler.shouldCompileFile(code, ctx))) { 310 | d(`Compiler returned false for shouldCompileFile: ${filePath}`); 311 | return { code, mimeType: mimeTypes.lookup(filePath), dependentFiles: [] }; 312 | } 313 | 314 | let dependentFiles = await compiler.determineDependentFiles(code, filePath, ctx); 315 | 316 | d(`Using compiler options: ${JSON.stringify(compiler.compilerOptions)}`); 317 | let result = await compiler.compile(code, filePath, ctx); 318 | 319 | let shouldInlineHtmlify = 320 | inputMimeType !== 'text/html' && 321 | result.mimeType === 'text/html'; 322 | 323 | let isPassthrough = 324 | result.mimeType === 'text/plain' || 325 | !result.mimeType || 326 | CompilerHost.shouldPassthrough(hashInfo); 327 | 328 | if ((finalForms[result.mimeType] && !shouldInlineHtmlify) || isPassthrough) { 329 | // Got something we can use in-browser, let's return it 330 | return _.assign(result, {dependentFiles}); 331 | } else { 332 | d(`Recursively compiling result of ${filePath} with non-final MIME type ${result.mimeType}, input was ${inputMimeType}`); 333 | 334 | hashInfo = _.assign({ sourceCode: result.code, mimeType: result.mimeType }, hashInfo); 335 | compiler = this.compilersByMimeType[result.mimeType || '__lolnothere']; 336 | 337 | if (!compiler) { 338 | d(`Recursive compile failed - intermediate result: ${JSON.stringify(result)}`); 339 | 340 | throw new Error(`Compiling ${filePath} resulted in a MIME type of ${result.mimeType}, which we don't know how to handle`); 341 | } 342 | 343 | return await this.compileUncached( 344 | `${filePath}.${mimeTypes.extension(result.mimeType || 'txt')}`, 345 | hashInfo, compiler); 346 | } 347 | } 348 | 349 | /** 350 | * Pre-caches an entire directory of files recursively. Usually used for 351 | * building custom compiler tooling. 352 | * 353 | * @param {string} rootDirectory The top-level directory to compile 354 | * 355 | * @param {Function} shouldCompile (optional) A Function which allows the 356 | * caller to disable compiling certain files. 357 | * It takes a fully-qualified path to a file, 358 | * and should return a Boolean. 359 | * 360 | * @return {Promise} Completion. 361 | */ 362 | async compileAll(rootDirectory, shouldCompile=null) { 363 | let should = shouldCompile || function() {return true;}; 364 | 365 | await forAllFiles(rootDirectory, (f) => { 366 | if (!should(f)) return; 367 | 368 | d(`Compiling ${f}`); 369 | return this.compile(f, this.compilersByMimeType); 370 | }); 371 | } 372 | 373 | /* 374 | * Sync Methods 375 | */ 376 | 377 | compileSync(filePath) { 378 | return (this.readOnlyMode ? this.compileReadOnlySync(filePath) : this.fullCompileSync(filePath)); 379 | } 380 | 381 | static createReadonlyFromConfigurationSync(rootCacheDir, appRoot, fallbackCompiler=null) { 382 | let target = path.join(rootCacheDir, 'compiler-info.json.gz'); 383 | let buf = fs.readFileSync(target); 384 | let info = JSON.parse(zlib.gunzipSync(buf)); 385 | 386 | let fileChangeCache = FileChangedCache.loadFromData(info.fileChangeCache, appRoot, true); 387 | 388 | let compilers = _.reduce(Object.keys(info.compilers), (acc, x) => { 389 | let cur = info.compilers[x]; 390 | acc[x] = new ReadOnlyCompiler(cur.name, cur.compilerVersion, cur.compilerOptions, cur.inputMimeTypes); 391 | 392 | return acc; 393 | }, {}); 394 | 395 | return new CompilerHost(rootCacheDir, compilers, fileChangeCache, true, fallbackCompiler); 396 | } 397 | 398 | static createFromConfigurationSync(rootCacheDir, appRoot, compilersByMimeType, fallbackCompiler=null) { 399 | let target = path.join(rootCacheDir, 'compiler-info.json.gz'); 400 | let buf = fs.readFileSync(target); 401 | let info = JSON.parse(zlib.gunzipSync(buf)); 402 | 403 | let fileChangeCache = FileChangedCache.loadFromData(info.fileChangeCache, appRoot, false); 404 | 405 | _.each(Object.keys(info.compilers), (x) => { 406 | let cur = info.compilers[x]; 407 | compilersByMimeType[x].compilerOptions = cur.compilerOptions; 408 | }); 409 | 410 | return new CompilerHost(rootCacheDir, compilersByMimeType, fileChangeCache, false, fallbackCompiler); 411 | } 412 | 413 | saveConfigurationSync() { 414 | let serializedCompilerOpts = _.reduce(Object.keys(this.compilersByMimeType), (acc, x) => { 415 | let compiler = this.compilersByMimeType[x]; 416 | let Klass = Object.getPrototypeOf(compiler).constructor; 417 | 418 | let val = { 419 | name: Klass.name, 420 | inputMimeTypes: Klass.getInputMimeTypes(), 421 | compilerOptions: compiler.compilerOptions, 422 | compilerVersion: compiler.getCompilerVersion() 423 | }; 424 | 425 | acc[x] = val; 426 | return acc; 427 | }, {}); 428 | 429 | let info = { 430 | fileChangeCache: this.fileChangeCache.getSavedData(), 431 | compilers: serializedCompilerOpts 432 | }; 433 | 434 | let target = path.join(this.rootCacheDir, 'compiler-info.json.gz'); 435 | let buf = zlib.gzipSync(new Buffer(JSON.stringify(info))); 436 | fs.writeFileSync(target, buf); 437 | } 438 | 439 | compileReadOnlySync(filePath) { 440 | // We guarantee that node_modules are always shipped directly 441 | let type = mimeTypes.lookup(filePath); 442 | if (FileChangedCache.isInNodeModules(filePath)) { 443 | return { 444 | mimeType: type || 'application/javascript', 445 | code: fs.readFileSync(filePath, 'utf8') 446 | }; 447 | } 448 | 449 | let hashInfo = this.fileChangeCache.getHashForPathSync(filePath); 450 | 451 | // We guarantee that node_modules are always shipped directly 452 | if (hashInfo.isInNodeModules) { 453 | return { 454 | mimeType: type, 455 | code: hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8') 456 | }; 457 | } 458 | 459 | // NB: Here, we're basically only using the compiler here to find 460 | // the appropriate CompileCache 461 | let compiler = CompilerHost.shouldPassthrough(hashInfo) ? 462 | this.getPassthroughCompiler() : 463 | this.compilersByMimeType[type || '__lolnothere']; 464 | 465 | if (!compiler) { 466 | compiler = this.fallbackCompiler; 467 | 468 | let { code, binaryData, mimeType } = compiler.getSync(filePath); 469 | return { code: code || binaryData, mimeType }; 470 | } 471 | 472 | let cache = this.cachesForCompilers.get(compiler); 473 | let {code, binaryData, mimeType} = cache.getSync(filePath); 474 | 475 | code = code || binaryData; 476 | if (!code || !mimeType) { 477 | throw new Error(`Asked to compile ${filePath} in production, is this file not precompiled?`); 478 | } 479 | 480 | return { code, mimeType }; 481 | } 482 | 483 | fullCompileSync(filePath) { 484 | d(`Compiling ${filePath}`); 485 | 486 | let hashInfo = this.fileChangeCache.getHashForPathSync(filePath); 487 | let type = mimeTypes.lookup(filePath); 488 | 489 | if (hashInfo.isInNodeModules) { 490 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 491 | return { code, mimeType: type }; 492 | } 493 | 494 | let compiler = CompilerHost.shouldPassthrough(hashInfo) ? 495 | this.getPassthroughCompiler() : 496 | this.compilersByMimeType[type || '__lolnothere']; 497 | 498 | if (!compiler) { 499 | d(`Falling back to passthrough compiler for ${filePath}`); 500 | compiler = this.fallbackCompiler; 501 | } 502 | 503 | if (!compiler) { 504 | throw new Error(`Couldn't find a compiler for ${filePath}`); 505 | } 506 | 507 | let cache = this.cachesForCompilers.get(compiler); 508 | return cache.getOrFetchSync( 509 | filePath, 510 | (filePath, hashInfo) => this.compileUncachedSync(filePath, hashInfo, compiler)); 511 | } 512 | 513 | compileUncachedSync(filePath, hashInfo, compiler) { 514 | let inputMimeType = mimeTypes.lookup(filePath); 515 | 516 | if (hashInfo.isFileBinary) { 517 | return { 518 | binaryData: hashInfo.binaryData || fs.readFileSync(filePath), 519 | mimeType: inputMimeType, 520 | dependentFiles: [] 521 | }; 522 | } 523 | 524 | let ctx = {}; 525 | let code = hashInfo.sourceCode || fs.readFileSync(filePath, 'utf8'); 526 | 527 | if (!(compiler.shouldCompileFileSync(code, ctx))) { 528 | d(`Compiler returned false for shouldCompileFile: ${filePath}`); 529 | return { code, mimeType: mimeTypes.lookup(filePath), dependentFiles: [] }; 530 | } 531 | 532 | let dependentFiles = compiler.determineDependentFilesSync(code, filePath, ctx); 533 | 534 | let result = compiler.compileSync(code, filePath, ctx); 535 | 536 | let shouldInlineHtmlify = 537 | inputMimeType !== 'text/html' && 538 | result.mimeType === 'text/html'; 539 | 540 | let isPassthrough = 541 | result.mimeType === 'text/plain' || 542 | !result.mimeType || 543 | CompilerHost.shouldPassthrough(hashInfo); 544 | 545 | if ((finalForms[result.mimeType] && !shouldInlineHtmlify) || isPassthrough) { 546 | // Got something we can use in-browser, let's return it 547 | return _.assign(result, {dependentFiles}); 548 | } else { 549 | d(`Recursively compiling result of ${filePath} with non-final MIME type ${result.mimeType}, input was ${inputMimeType}`); 550 | 551 | hashInfo = _.assign({ sourceCode: result.code, mimeType: result.mimeType }, hashInfo); 552 | compiler = this.compilersByMimeType[result.mimeType || '__lolnothere']; 553 | 554 | if (!compiler) { 555 | d(`Recursive compile failed - intermediate result: ${JSON.stringify(result)}`); 556 | 557 | throw new Error(`Compiling ${filePath} resulted in a MIME type of ${result.mimeType}, which we don't know how to handle`); 558 | } 559 | 560 | return this.compileUncachedSync( 561 | `${filePath}.${mimeTypes.extension(result.mimeType || 'txt')}`, 562 | hashInfo, compiler); 563 | } 564 | } 565 | 566 | compileAllSync(rootDirectory, shouldCompile=null) { 567 | let should = shouldCompile || function() {return true;}; 568 | 569 | forAllFilesSync(rootDirectory, (f) => { 570 | if (!should(f)) return; 571 | return this.compileSync(f, this.compilersByMimeType); 572 | }); 573 | } 574 | 575 | /* 576 | * Other stuff 577 | */ 578 | 579 | 580 | /** 581 | * Returns the passthrough compiler 582 | * 583 | * @private 584 | */ 585 | getPassthroughCompiler() { 586 | return this.compilersByMimeType['text/plain']; 587 | } 588 | 589 | 590 | /** 591 | * Determines whether we should even try to compile the content. Note that in 592 | * some cases, content will still be in cache even if this returns true, and 593 | * in other cases (isInNodeModules), we'll know explicitly to not even bother 594 | * looking in the cache. 595 | * 596 | * @private 597 | */ 598 | static shouldPassthrough(hashInfo) { 599 | return hashInfo.isMinified || hashInfo.isInNodeModules || hashInfo.hasSourceMap || hashInfo.isFileBinary; 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /test/fixtures/minified.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9 | */ 10 | // @version 0.7.15 11 | !function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",u=0,l="",w=!1,_=!1,g=[];e:for(;(e[u-1]!=p||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[u+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),l+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=f(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;nf&&(h=s[f]);f++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),w=v?"complete":"interactive",_="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=i.getResponseHeader("Location"),a=null;if(n)var a="/"===n.substr(0,1)?location.origin+n:n;r.call(o,!t.ok(i)&&i,i.response||i.responseText,a)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return t(e)&&void 0===e.__doc?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(o)}var n=e.initializeModules,r=e.isIE;if(!e.useNative){(!window.CustomEvent||r&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){_.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(_.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e,a)}),T(e.removedNodes,function(e){e.localName&&c(e)}))}),_.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),_.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),_.dom&&console.groupEnd()}function w(e){b(e,v)}var _=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=window.MutationObserver._isPolyfilled&&_["throttle-attached"];e.hasPolyfillMutations=y,e.hasThrottledAttached=y;var E=!1,L=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=w,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&w(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){ 12 | r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return _(e),e}}var m,v=e.isIE,w=e.upgradeDocumentTree,_=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},T="http://www.w3.org/1999/xhtml",M=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){a(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules,o=e.isIE;if(n){var i=function(){};e.watchShadow=i,e.upgrade=i,e.upgradeAll=i,e.upgradeDocumentTree=i,e.upgradeSubtree=i,e.takeRecords=i,e["instanceof"]=function(e,t){return e instanceof t}}else r();var a=e.upgradeDocumentTree,s=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&s(wrap(e["import"]))}),(!window.CustomEvent||o&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var c=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(c,t)}else t()}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){function e(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=r.createDocumentFragment());for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}())}(window.WebComponents),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); --------------------------------------------------------------------------------