Skip to content

Commit

Permalink
Merge pull request #521 from parcel-bundler/config-cache
Browse files Browse the repository at this point in the history
Invalidate cache on config and environment variable changes
  • Loading branch information
devongovett authored Jan 10, 2018
2 parents 5572e88 + eceebeb commit 6c3d34f
Show file tree
Hide file tree
Showing 19 changed files with 138 additions and 45 deletions.
19 changes: 19 additions & 0 deletions src/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const objectHash = require('./utils/objectHash');
const md5 = require('./utils/md5');
const isURL = require('./utils/is-url');
const sanitizeFilename = require('sanitize-filename');
const config = require('./utils/config');

let ASSET_ID = 1;

Expand Down Expand Up @@ -33,6 +34,11 @@ class Asset {
this.depAssets = new Map();
this.parentBundle = null;
this.bundles = new Set();
this.cacheData = {};
}

shouldInvalidate() {
return false;
}

async loadIfNeeded() {
Expand Down Expand Up @@ -82,6 +88,19 @@ class Asset {
.generateBundleName();
}

async getConfig(filenames) {
// Resolve the config file
let conf = await config.resolve(this.name, filenames);
if (conf) {
// Add as a dependency so it is added to the watcher and invalidates
// this asset when the config changes.
this.addDependency(conf, {includedInParent: true});
return await config.load(this.name, filenames);
}

return null;
}

mightHaveDependencies() {
return true;
}
Expand Down
77 changes: 55 additions & 22 deletions src/Bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Bundler extends EventEmitter {

this.pending = false;
this.loadedAssets = new Map();
this.watchedAssets = new Map();
this.farm = null;
this.watcher = null;
this.hmr = null;
Expand Down Expand Up @@ -267,11 +268,35 @@ class Bundler extends EventEmitter {
let asset = this.parser.getAsset(path, pkg, this.options);
this.loadedAssets.set(path, asset);

if (this.watcher) {
this.watch(path, asset);
return asset;
}

watch(path, asset) {
if (!this.watcher) {
return;
}

if (!this.watchedAssets.has(path)) {
this.watcher.add(path);
this.watchedAssets.set(path, new Set());
}

return asset;
this.watchedAssets.get(path).add(asset);
}

unwatch(path, asset) {
if (!this.watchedAssets.has(path)) {
return;
}

let watched = this.watchedAssets.get(path);
watched.delete(asset);

if (watched.size === 0) {
this.watchedAssets.delete(path);
this.watcher.unwatch(path);
}
}

async resolveDep(asset, dep) {
Expand Down Expand Up @@ -317,7 +342,7 @@ 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) {
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
processed = await this.farm.run(asset.name, asset.package, this.options);
if (this.cache) {
this.cache.write(asset.name, processed);
Expand All @@ -339,26 +364,25 @@ class Bundler extends EventEmitter {
// Resolve and load asset dependencies
let assetDeps = await Promise.all(
dependencies.map(async dep => {
let assetDep = await this.resolveDep(asset, dep);
if (!dep.includedInParent) {
if (dep.includedInParent) {
// This dependency is already included in the parent's generated output,
// so no need to load it. We map the name back to the parent asset so
// that changing it triggers a recompile of the parent.
this.watch(dep.name, asset);
} else {
let assetDep = await this.resolveDep(asset, dep);
await this.loadAsset(assetDep);
return assetDep;
}

return assetDep;
})
);

// Store resolved assets in their original order
dependencies.forEach((dep, i) => {
asset.dependencies.set(dep.name, dep);
let assetDep = assetDeps[i];
if (dep.includedInParent) {
// This dependency is already included in the parent's generated output,
// so no need to load it. We map the name back to the parent asset so
// that changing it triggers a recompile of the parent.
this.loadedAssets.set(dep.name, asset);
} else {
asset.dependencies.set(dep.name, dep);
asset.depAssets.set(dep.name, assetDep);
if (assetDep) {
asset.depAssets.set(dep, assetDep);
}
});

Expand Down Expand Up @@ -421,8 +445,7 @@ class Bundler extends EventEmitter {

asset.parentBundle = bundle;

for (let dep of asset.dependencies.values()) {
let assetDep = asset.depAssets.get(dep.name);
for (let [dep, assetDep] of asset.depAssets) {
this.createBundleTree(assetDep, dep, bundle);
}

Expand Down Expand Up @@ -463,21 +486,31 @@ class Bundler extends EventEmitter {
unloadAsset(asset) {
this.loadedAssets.delete(asset.name);
if (this.watcher) {
this.watcher.unwatch(asset.name);
this.unwatch(asset.name, asset);

// Unwatch all included dependencies that map to this asset
for (let dep of asset.dependencies.values()) {
if (dep.includedInParent) {
this.unwatch(dep.name, asset);
}
}
}
}

async onChange(path) {
let asset = this.loadedAssets.get(path);
if (!asset) {
let assets = this.watchedAssets.get(path);
if (!assets || !assets.size) {
return;
}

this.logger.clear();
this.logger.status(emoji.progress, `Building ${asset.basename}...`);
this.logger.status(emoji.progress, `Building ${Path.basename(path)}...`);

// Add the asset to the rebuild queue, and reset the timeout.
this.buildQueue.add(asset);
for (let asset of assets) {
this.buildQueue.add(asset);
}

clearTimeout(this.rebuildTimeout);

this.rebuildTimeout = setTimeout(async () => {
Expand Down
5 changes: 2 additions & 3 deletions src/HMRServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ class HMRServer {
type: 'update',
assets: assets.map(asset => {
let deps = {};
for (let dep of asset.dependencies.values()) {
let mod = asset.depAssets.get(dep.name);
deps[dep.name] = mod.id;
for (let [dep, depAsset] of asset.depAssets) {
deps[dep.name] = depAsset.id;
}

return {
Expand Down
14 changes: 12 additions & 2 deletions src/assets/JSAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const fsVisitor = require('../visitors/fs');
const babel = require('../transforms/babel');
const generate = require('babel-generator').default;
const uglify = require('../transforms/uglify');
const config = require('../utils/config');

const IMPORT_RE = /\b(?:import\b|export\b|require\s*\()/;
const GLOBAL_RE = /\b(?:process|__dirname|__filename|global|Buffer)\b/;
Expand All @@ -26,6 +25,17 @@ class JSAsset extends Asset {
this.isAstDirty = false;
this.isES6Module = false;
this.outputCode = null;
this.cacheData.env = {};
}

shouldInvalidate(cacheData) {
for (let key in cacheData.env) {
if (cacheData.env[key] !== process.env[key]) {
return true;
}
}

return false;
}

mightHaveDependencies() {
Expand Down Expand Up @@ -55,7 +65,7 @@ class JSAsset extends Asset {
// Check if there is a babel config file. If so, determine which parser plugins to enable
this.babelConfig =
(this.package && this.package.babel) ||
(await config.load(this.name, ['.babelrc', '.babelrc.js']));
(await this.getConfig(['.babelrc', '.babelrc.js']));
if (this.babelConfig) {
const file = new BabelFile({filename: this.name});
options.plugins.push(...file.parserOpts.plugins);
Expand Down
3 changes: 1 addition & 2 deletions src/assets/LESSAsset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const CSSAsset = require('./CSSAsset');
const config = require('../utils/config');
const localRequire = require('../utils/localRequire');
const promisify = require('../utils/promisify');

Expand All @@ -11,7 +10,7 @@ class LESSAsset extends CSSAsset {

let opts =
this.package.less ||
(await config.load(this.name, ['.lessrc', '.lessrc.js'])) ||
(await this.getConfig(['.lessrc', '.lessrc.js'])) ||
{};
opts.filename = this.name;
opts.plugins = (opts.plugins || []).concat(urlPlugin(this));
Expand Down
3 changes: 1 addition & 2 deletions src/assets/SASSAsset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const CSSAsset = require('./CSSAsset');
const config = require('../utils/config');
const localRequire = require('../utils/localRequire');
const promisify = require('../utils/promisify');
const path = require('path');
Expand All @@ -12,7 +11,7 @@ class SASSAsset extends CSSAsset {

let opts =
this.package.sass ||
(await config.load(this.name, ['.sassrc', '.sassrc.js'])) ||
(await this.getConfig(['.sassrc', '.sassrc.js'])) ||
{};
opts.includePaths = (opts.includePaths || []).concat(
path.dirname(this.name)
Expand Down
3 changes: 1 addition & 2 deletions src/assets/StylusAsset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const CSSAsset = require('./CSSAsset');
const config = require('../utils/config');
const localRequire = require('../utils/localRequire');
const Resolver = require('../Resolver');

Expand All @@ -11,7 +10,7 @@ class StylusAsset extends CSSAsset {
let stylus = await localRequire('stylus', this.name);
let opts =
this.package.stylus ||
(await config.load(this.name, ['.stylusrc', '.stylusrc.js']));
(await this.getConfig(['.stylusrc', '.stylusrc.js']));
let style = stylus(code, opts);
style.set('filename', this.name);
style.set('include css', true);
Expand Down
3 changes: 1 addition & 2 deletions src/assets/TypeScriptAsset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const JSAsset = require('./JSAsset');
const config = require('../utils/config');
const localRequire = require('../utils/localRequire');

class TypeScriptAsset extends JSAsset {
Expand All @@ -14,7 +13,7 @@ class TypeScriptAsset extends JSAsset {
fileName: this.basename
};

let tsconfig = await config.load(this.name, ['tsconfig.json']);
let tsconfig = await this.getConfig(['tsconfig.json']);

// Overwrite default if config is found
if (tsconfig) {
Expand Down
4 changes: 1 addition & 3 deletions src/packagers/JSPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ class JSPackager extends Packager {
}

let deps = {};
for (let dep of asset.dependencies.values()) {
let mod = asset.depAssets.get(dep.name);

for (let [dep, mod] of asset.depAssets) {
// For dynamic dependencies, list the child bundles to load along with the module id
if (dep.dynamic && this.bundle.childBundles.has(mod.parentBundle)) {
let bundles = [path.basename(mod.parentBundle.name)];
Expand Down
3 changes: 1 addition & 2 deletions src/transforms/babel.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const babel = require('babel-core');
const config = require('../utils/config');

module.exports = async function(asset) {
if (!await shouldTransform(asset)) {
Expand Down Expand Up @@ -40,6 +39,6 @@ async function shouldTransform(asset) {
return true;
}

let babelrc = await config.resolve(asset.name, ['.babelrc', '.babelrc.js']);
let babelrc = await asset.getConfig(['.babelrc', '.babelrc.js']);
return !!babelrc;
}
3 changes: 1 addition & 2 deletions src/transforms/uglify.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
const {minify} = require('uglify-es');
const config = require('../utils/config');

module.exports = async function(asset) {
await asset.parseIfNeeded();

// Convert AST into JS
let code = asset.generate().js;

let customConfig = await config.load(asset.name, ['.uglifyrc']);
let customConfig = await asset.getConfig(['.uglifyrc']);
let options = {
warnings: true,
mangle: {
Expand Down
2 changes: 0 additions & 2 deletions src/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ async function resolve(filepath, filenames, root = path.parse(filepath).root) {
existsCache.set(file, true);
return file;
}

existsCache.set(file, false);
}

return resolve(filepath, filenames, root);
Expand Down
1 change: 1 addition & 0 deletions src/visitors/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
let val = types.valueToNode(process.env[key.value]);
morph(node, val);
asset.isAstDirty = true;
asset.cacheData.env[key.value] = process.env[key.value];
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ exports.run = async function(path, pkg, options, callback) {
callback(null, {
dependencies: Array.from(asset.dependencies.values()),
generated: asset.generated,
hash: asset.hash
hash: asset.hash,
cacheData: asset.cacheData
});
} catch (err) {
let returned = err;
Expand Down
7 changes: 7 additions & 0 deletions test/integration/babel/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"presets": [["env", {
"targets": {
"browsers": ["last 2 Chrome versions"]
}
}]]
}
6 changes: 6 additions & 0 deletions test/integration/babel/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module"
}
}
1 change: 1 addition & 0 deletions test/integration/babel/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class Foo {}
4 changes: 4 additions & 0 deletions test/integration/babel/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Foo from './foo';

export {Foo};
export class Bar {}
Loading

0 comments on commit 6c3d34f

Please sign in to comment.