diff --git a/package.json b/package.json index cc2f6a4a59e..09e780cb0bf 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "babylon": "^6.17.4", "babylon-walk": "^1.0.2", "browser-resolve": "^1.11.2", + "canonical-json": "0.0.4", "chalk": "^2.1.0", "chokidar": "^1.7.0", "commander": "^2.11.0", diff --git a/src/Asset.js b/src/Asset.js index 793f0f49de8..71cd14ab737 100644 --- a/src/Asset.js +++ b/src/Asset.js @@ -33,6 +33,7 @@ class Asset { this.depAssets = new Map(); this.parentBundle = null; this.bundles = new Set(); + this.config = {}; } async loadIfNeeded() { @@ -90,6 +91,11 @@ class Asset { return await fs.readFile(this.name, this.encoding); } + async getConfig() { + // do nothing + return this.config; + } + parse() { // do nothing by default } @@ -112,6 +118,7 @@ class Asset { async process() { if (!this.generated) { + await this.getConfig(); await this.loadIfNeeded(); await this.pretransform(); await this.getDependencies(); diff --git a/src/Bundler.js b/src/Bundler.js index ad92a5dd9fa..2966a3650f1 100644 --- a/src/Bundler.js +++ b/src/Bundler.js @@ -13,6 +13,8 @@ const Logger = require('./Logger'); const PackagerRegistry = require('./packagers'); const localRequire = require('./utils/localRequire'); const config = require('./utils/config'); +const configCache = require('./utils/configCache'); +const objectHash = require('./utils/objectHash'); const emoji = require('./utils/emoji'); /** @@ -41,6 +43,7 @@ class Bundler extends EventEmitter { this.errored = false; this.buildQueue = new Set(); this.rebuildTimeout = null; + this.plugins = []; } normalizeOptions(options) { @@ -98,6 +101,10 @@ class Bundler extends EventEmitter { let deps = Object.assign({}, pkg.dependencies, pkg.devDependencies); for (let dep in deps) { if (dep.startsWith('parcel-plugin-')) { + this.plugins.push({ + name: dep, + version: deps[dep] + }); let plugin = await localRequire(dep, this.mainFile); plugin(this); } @@ -169,12 +176,27 @@ class Bundler extends EventEmitter { } } + async createVersionHash() { + let pkg = await config.load(this.mainFile, ['package.json']); + if (!pkg) { + return ''; + } + + let versionObject = { + plugins: this.plugins, + version: pkg.version, + name: pkg.name + }; + return objectHash(versionObject); + } + async start() { if (this.farm) { return; } await this.loadPlugins(); + this.versionHash = await this.createVersionHash(); this.options.extensions = Object.assign({}, this.parser.extensions); this.farm = WorkerFarm.getShared(this.options); @@ -313,8 +335,16 @@ class Bundler extends EventEmitter { // First try the cache, otherwise load and compile in the background let processed = this.cache && (await this.cache.read(asset.name)); - if (!processed) { + let sameConfig = false; + if (processed) { + sameConfig = await configCache.compare(asset, processed.configCache); + sameConfig = sameConfig + ? processed.versionHash === this.versionHash + : false; + } + if (!processed || !sameConfig) { processed = await this.farm.run(asset.name, asset.package, this.options); + processed.versionHash = this.versionHash; if (this.cache) { this.cache.write(asset.name, processed); } diff --git a/src/assets/CSSAsset.js b/src/assets/CSSAsset.js index 06c1d182805..3ef74dc96df 100644 --- a/src/assets/CSSAsset.js +++ b/src/assets/CSSAsset.js @@ -22,6 +22,12 @@ class CSSAsset extends Asset { ); } + async getConfig() { + await postcssTransform.getConfig(this); + + return this.config; + } + parse(code) { let root = postcss.parse(code, {from: this.name, to: this.name}); return new CSSAst(code, root); @@ -85,7 +91,7 @@ class CSSAsset extends Asset { } async transform() { - await postcssTransform(this); + await postcssTransform.parse(this); } getCSSAst() { diff --git a/src/assets/HTMLAsset.js b/src/assets/HTMLAsset.js index 3e4c613cea8..d4e99158d6f 100644 --- a/src/assets/HTMLAsset.js +++ b/src/assets/HTMLAsset.js @@ -30,6 +30,12 @@ class HTMLAsset extends Asset { this.isAstDirty = false; } + async getConfig() { + await posthtmlTransform.getConfig(this); + + return this.config; + } + parse(code) { let res = parse(code); res.walk = api.walk; @@ -58,7 +64,7 @@ class HTMLAsset extends Asset { } async transform() { - await posthtmlTransform(this); + await posthtmlTransform.parse(this); } generate() { diff --git a/src/assets/JSAsset.js b/src/assets/JSAsset.js index 683248c5b96..b538935cf78 100644 --- a/src/assets/JSAsset.js +++ b/src/assets/JSAsset.js @@ -34,9 +34,13 @@ class JSAsset extends Asset { ); } - async getParserOptions() { + async getConfig() { + if (this.config.babelOptions && this.config.babelConfig) { + return this.config; + } + // Babylon options. We enable a few plugins by default. - const options = { + this.config.babelOptions = { filename: this.name, allowReturnOutsideFunction: true, allowHashBang: true, @@ -48,21 +52,19 @@ class JSAsset extends Asset { }; // Check if there is a babel config file. If so, determine which parser plugins to enable - this.babelConfig = + this.config.babelConfig = (this.package && this.package.babel) || (await config.load(this.name, ['.babelrc', '.babelrc.js'])); - if (this.babelConfig) { + if (this.config.babelConfig) { const file = new BabelFile({filename: this.name}); - options.plugins.push(...file.parserOpts.plugins); + this.config.babelOptions.plugins.push(...file.parserOpts.plugins); } - return options; + return this.config; } async parse(code) { - const options = await this.getParserOptions(); - - return babylon.parse(code, options); + return babylon.parse(code, this.config.babelOptions); } traverse(visitor) { diff --git a/src/assets/JSONAsset.js b/src/assets/JSONAsset.js index 7766dbb6847..198917772a1 100644 --- a/src/assets/JSONAsset.js +++ b/src/assets/JSONAsset.js @@ -5,6 +5,9 @@ class JSONAsset extends JSAsset { return 'module.exports = ' + (await super.load()) + ';'; } + async getConfig() { + return this.config; + } parse() {} collectDependencies() {} pretransform() {} diff --git a/src/assets/LESSAsset.js b/src/assets/LESSAsset.js index 88e06d1209c..df24f393b6f 100644 --- a/src/assets/LESSAsset.js +++ b/src/assets/LESSAsset.js @@ -4,19 +4,31 @@ const localRequire = require('../utils/localRequire'); const promisify = require('../utils/promisify'); class LESSAsset extends CSSAsset { - async parse(code) { - // less should be installed locally in the module that's being required - let less = await localRequire('less', this.name); - let render = promisify(less.render.bind(less)); + async getConfig() { + await super.getConfig(); - let opts = + if (this.config.less) { + return this.config; + } + + this.config.less = this.package.less || (await config.load(this.name, ['.lessrc', '.lessrc.js'])) || {}; - opts.filename = this.name; - opts.plugins = (opts.plugins || []).concat(urlPlugin(this)); + this.config.less.filename = this.name; + this.config.less.plugins = (this.config.less.plugins || []).concat( + urlPlugin(this) + ); + + return this.config; + } + + async parse(code) { + // less should be installed locally in the module that's being required + let less = await localRequire('less', this.name); + let render = promisify(less.render.bind(less)); - let res = await render(code, opts); + let res = await render(code, this.config.less); res.render = () => res.css; return res; } diff --git a/src/assets/SASSAsset.js b/src/assets/SASSAsset.js index 3f961ae065a..9b053c5dfcc 100644 --- a/src/assets/SASSAsset.js +++ b/src/assets/SASSAsset.js @@ -5,23 +5,36 @@ const promisify = require('../utils/promisify'); const path = require('path'); class SASSAsset extends CSSAsset { + async getConfig() { + await super.getConfig(); + + if (this.config.sass) { + return this.config; + } + + this.config.sass = + this.package.sass || + (await config.load(this.name, ['.sassrc', '.sassrc.js'])) || + {}; + this.config.sass.includePaths = ( + this.config.sass.includePaths || [] + ).concat(path.dirname(this.name)); + + this.config.sass.indentedSyntax = + typeof this.config.sass.indentedSyntax === 'boolean' + ? this.config.sass.indentedSyntax + : path.extname(this.name).toLowerCase() === '.sass'; + + return this.config; + } + async parse(code) { // node-sass should be installed locally in the module that's being required let sass = await localRequire('node-sass', this.name); let render = promisify(sass.render.bind(sass)); - let opts = - this.package.sass || - (await config.load(this.name, ['.sassrc', '.sassrc.js'])) || - {}; - opts.includePaths = (opts.includePaths || []).concat( - path.dirname(this.name) - ); + let opts = this.config.sass; opts.data = code; - opts.indentedSyntax = - typeof opts.indentedSyntax === 'boolean' - ? opts.indentedSyntax - : path.extname(this.name).toLowerCase() === '.sass'; opts.functions = Object.assign({}, opts.functions, { url: node => { diff --git a/src/assets/StylusAsset.js b/src/assets/StylusAsset.js index bdce6802b02..5bb2137bfca 100644 --- a/src/assets/StylusAsset.js +++ b/src/assets/StylusAsset.js @@ -6,13 +6,24 @@ const Resolver = require('../Resolver'); const URL_RE = /^(?:url\s*\(\s*)?['"]?(?:[#/]|(?:https?:)?\/\/)/i; class StylusAsset extends CSSAsset { + async getConfig() { + await super.getConfig(); + + if (this.config.stylus) { + return this.config; + } + + this.config.stylus = + this.package.stylus || + (await config.load(this.name, ['.stylusrc', '.stylusrc.js'])); + + return this.config; + } + async parse(code) { // stylus should be installed locally in the module that's being required let stylus = await localRequire('stylus', this.name); - let opts = - this.package.stylus || - (await config.load(this.name, ['.stylusrc', '.stylusrc.js'])); - let style = stylus(code, opts); + let style = stylus(code, this.config.stylus); style.set('filename', this.name); style.set('include css', true); style.set('Evaluator', await createEvaluator(this)); diff --git a/src/assets/TypeScriptAsset.js b/src/assets/TypeScriptAsset.js index 450e6e3799f..42158182c44 100644 --- a/src/assets/TypeScriptAsset.js +++ b/src/assets/TypeScriptAsset.js @@ -3,10 +3,15 @@ const config = require('../utils/config'); const localRequire = require('../utils/localRequire'); class TypeScriptAsset extends JSAsset { - async parse(code) { - // require typescript, installed locally in the app + async getConfig() { + await super.getConfig(); let typescript = await localRequire('typescript', this.name); - let transpilerOptions = { + + if (this.config.typescript) { + return this.config; + } + + this.config.typescript = { compilerOptions: { module: typescript.ModuleKind.CommonJS, jsx: typescript.JsxEmit.Preserve @@ -18,19 +23,26 @@ class TypeScriptAsset extends JSAsset { // Overwrite default if config is found if (tsconfig) { - transpilerOptions.compilerOptions = Object.assign( - transpilerOptions.compilerOptions, + this.config.typescript.compilerOptions = Object.assign( + this.config.typescript.compilerOptions, tsconfig.compilerOptions ); } - transpilerOptions.compilerOptions.noEmit = false; + this.config.typescript.compilerOptions.noEmit = false; + + return this.config.typescript; + } + + async parse(code) { + let typescript = await localRequire('typescript', this.name); // Transpile Module using TypeScript and parse result as ast format through babylon this.contents = typescript.transpileModule( code, - transpilerOptions + this.config.typescript ).outputText; - return await super.parse(this.contents); + + return super.parse(this.contents); } } diff --git a/src/transforms/babel.js b/src/transforms/babel.js index 009614519a9..0a901c5d6d3 100644 --- a/src/transforms/babel.js +++ b/src/transforms/babel.js @@ -1,5 +1,4 @@ const babel = require('babel-core'); -const config = require('../utils/config'); module.exports = async function(asset) { if (!await shouldTransform(asset)) { @@ -30,13 +29,14 @@ async function shouldTransform(asset) { } if (asset.ast) { - return !!asset.babelConfig; + return !!asset.config.babelConfig; } if (asset.package && asset.package.babel) { return true; } - let babelrc = await config.resolve(asset.name, ['.babelrc', '.babelrc.js']); + await asset.getConfig(); + let babelrc = asset.config.babelConfig; return !!babelrc; } diff --git a/src/transforms/postcss.js b/src/transforms/postcss.js index 7376be333b8..a20bf1d0087 100644 --- a/src/transforms/postcss.js +++ b/src/transforms/postcss.js @@ -4,57 +4,73 @@ const postcss = require('postcss'); const Config = require('../utils/config'); const cssnano = require('cssnano'); -module.exports = async function(asset) { - let config = await getConfig(asset); - if (!config) { +async function parse(asset) { + if (!asset.config.postcss) { return; } await asset.parseIfNeeded(); - let res = await postcss(config.plugins).process(asset.getCSSAst(), config); + let res = await postcss(asset.config.postcss.plugins).process( + asset.getCSSAst(), + asset.config.postcss + ); asset.ast.css = res.css; asset.ast.dirty = false; -}; +} async function getConfig(asset) { - let config = + if (asset.config.postcss) { + return asset.config; + } + + asset.config.postcss = asset.package.postcss || (await Config.load(asset.name, [ '.postcssrc', '.postcssrc.js', 'postcss.config.js' ])); - if (!config && !asset.options.minify) { + if (!asset.config.postcss && !asset.options.minify) { return; } - config = config || {}; + asset.config.postcss = asset.config.postcss || {}; let postcssModulesConfig = { getJSON: (filename, json) => (asset.cssModules = json) }; - if (config.plugins && config.plugins['postcss-modules']) { + if ( + asset.config.postcss.plugins && + asset.config.postcss.plugins['postcss-modules'] + ) { postcssModulesConfig = Object.assign( - config.plugins['postcss-modules'], + asset.config.postcss.plugins['postcss-modules'], postcssModulesConfig ); - delete config.plugins['postcss-modules']; + delete asset.config.postcss.plugins['postcss-modules']; } - config.plugins = await loadPlugins(config.plugins, asset.name); + asset.config.postcss.plugins = await loadPlugins( + asset.config.postcss.plugins, + asset.name + ); - if (config.modules) { + if (asset.config.postcss.modules) { let postcssModules = await localRequire('postcss-modules', asset.name); - config.plugins.push(postcssModules(postcssModulesConfig)); + asset.config.postcss.plugins.push(postcssModules(postcssModulesConfig)); } if (asset.options.minify) { - config.plugins.push(cssnano()); + asset.config.postcss.plugins.push(cssnano()); } - config.from = asset.name; - config.to = asset.name; - return config; + asset.config.postcss.from = asset.name; + asset.config.postcss.to = asset.name; + + return asset.config; } + +module.exports.getConfig = getConfig; +module.exports.parse = parse; diff --git a/src/transforms/posthtml.js b/src/transforms/posthtml.js index b17b1525b16..ea713f8e1d6 100644 --- a/src/transforms/posthtml.js +++ b/src/transforms/posthtml.js @@ -3,41 +3,54 @@ const posthtml = require('posthtml'); const Config = require('../utils/config'); const htmlnano = require('htmlnano'); -module.exports = async function(asset) { - let config = await getConfig(asset); - if (!config) { +async function parse(asset) { + if (!asset.config.posthtml) { return; } await asset.parseIfNeeded(); - let res = await posthtml(config.plugins).process(asset.ast, config); + let res = await posthtml(asset.config.posthtml.plugins).process( + asset.ast, + asset.config.posthtml + ); asset.ast = res.tree; asset.isAstDirty = true; -}; +} async function getConfig(asset) { - let config = + if (asset.config.posthtml) { + return asset.config; + } + + asset.config.posthtml = asset.package.posthtml || (await Config.load(asset.name, [ '.posthtmlrc', '.posthtmlrc.js', 'posthtml.config.js' ])); - if (!config && !asset.options.minify) { + if (!asset.config.posthtml && !asset.options.minify) { return; } - config = config || {}; - config.plugins = await loadPlugins(config.plugins, asset.name); + asset.config.posthtml = asset.config.posthtml || {}; + asset.config.posthtml.plugins = await loadPlugins( + asset.config.posthtml.plugins, + asset.name + ); if (asset.options.minify) { const htmlNanoOptions = { collapseWhitespace: 'conservative' }; - config.plugins.push(htmlnano(htmlNanoOptions)); + asset.config.posthtml.plugins.push(htmlnano(htmlNanoOptions)); } - config.skipParse = true; - return config; + asset.config.posthtml.skipParse = true; + + return asset.config; } + +module.exports.getConfig = getConfig; +module.exports.parse = parse; diff --git a/src/utils/configCache.js b/src/utils/configCache.js new file mode 100644 index 00000000000..0aeb916a75f --- /dev/null +++ b/src/utils/configCache.js @@ -0,0 +1,14 @@ +const objectHash = require('./objectHash'); + +const getConfigHash = async function(asset) { + return objectHash(await asset.getConfig()); +}; + +const compare = async function(asset, configHash) { + let assetHash = await getConfigHash(asset); + + return assetHash === configHash; +}; + +module.exports.getConfigHash = getConfigHash; +module.exports.compare = compare; diff --git a/src/utils/objectHash.js b/src/utils/objectHash.js index fb7574b59e7..9767f9f6595 100644 --- a/src/utils/objectHash.js +++ b/src/utils/objectHash.js @@ -1,10 +1,11 @@ const crypto = require('crypto'); +const canonicalJson = require('canonical-json'); module.exports = function(object) { let hash = crypto.createHash('md5'); - for (let key of Object.keys(object).sort()) { - hash.update(key + object[key]); - } + + // Use canonical JSON to ensure same json returns the exact same string => exact same hash + hash.update(canonicalJson(object)); return hash.digest('hex'); }; diff --git a/src/worker.js b/src/worker.js index a988a2161bc..058edad2955 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,5 +1,6 @@ require('v8-compile-cache'); const Parser = require('./Parser'); +const configCache = require('./utils/configCache'); let parser; @@ -13,10 +14,13 @@ exports.run = async function(path, pkg, options, callback) { var asset = parser.getAsset(path, pkg, options); await asset.process(); + let configHash = await configCache.getConfigHash(asset); + callback(null, { dependencies: Array.from(asset.dependencies.values()), generated: asset.generated, - hash: asset.hash + hash: asset.hash, + configCache: configHash }); } catch (err) { let returned = err; diff --git a/yarn.lock b/yarn.lock index c55bb245edc..eec8a565c15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -997,6 +997,10 @@ caniuse-lite@^1.0.30000780: version "1.0.30000782" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000782.tgz#5b82b8c385f25348745c471ca51320afb1b7f254" +canonical-json@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/canonical-json/-/canonical-json-0.0.4.tgz#6579c072c3db5c477ec41dc978fbf2b8f41074a3" + caseless@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"