Skip to content

Commit

Permalink
Content hash output file names (#1025)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Mar 21, 2018
1 parent 8002684 commit 76fa1ed
Show file tree
Hide file tree
Showing 24 changed files with 244 additions and 82 deletions.
26 changes: 4 additions & 22 deletions src/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class Asset {
await this.getDependencies();
await this.transform();
this.generated = await this.generate();
this.hash = this.generateHash();
this.hash = await this.generateHash();
}

return this.generated;
Expand All @@ -175,27 +175,9 @@ class Asset {
}

generateBundleName() {
const ext = '.' + this.type;

const isEntryPoint = this.name === this.options.mainFile;

// If this is the entry point of the root bundle, use outFile filename if provided
if (isEntryPoint && this.options.outFile) {
return (
path.basename(
this.options.outFile,
path.extname(this.options.outFile)
) + ext
);
}

// If this is the entry point of the root bundle, use the original filename
if (isEntryPoint) {
return path.basename(this.name, path.extname(this.name)) + ext;
}

// Otherwise generate a unique name
return md5(this.name) + ext;
// Generate a unique name. This will be replaced with a nicer
// name later as part of content hashing.
return md5(this.name) + '.' + this.type;
}

generateErrorMessage(err) {
Expand Down
54 changes: 54 additions & 0 deletions src/Bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,59 @@ class Bundle {
return this.assets.size === 0;
}

getBundleNameMap(contentHash, hashes = new Map()) {
let hashedName = this.getHashedBundleName(contentHash);
hashes.set(Path.basename(this.name), hashedName);
this.name = Path.join(Path.dirname(this.name), hashedName);

for (let child of this.childBundles.values()) {
child.getBundleNameMap(contentHash, hashes);
}

return hashes;
}

getHashedBundleName(contentHash) {
// If content hashing is enabled, generate a hash from all assets in the bundle.
// Otherwise, use a hash of the filename so it remains consistent across builds.
let ext = Path.extname(this.name);
let hash = (contentHash
? this.getHash()
: Path.basename(this.name, ext)
).slice(-8);
let entryAsset = this.entryAsset || this.parentBundle.entryAsset;
let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name));
let isMainEntry = entryAsset.name === entryAsset.options.mainFile;
let isEntry =
isMainEntry || Array.from(entryAsset.parentDeps).some(dep => dep.entry);

// If this is the main entry file, use the output file option as the name if provided.
if (isMainEntry && entryAsset.options.outFile) {
name = entryAsset.options.outFile;
}

// If this is an entry asset, don't hash. Return a relative path
// from the main file so we keep the original file paths.
if (isEntry) {
return Path.join(
Path.relative(
Path.dirname(entryAsset.options.mainFile),
Path.dirname(entryAsset.name)
),
name + ext
);
}

// If this is an index file, use the parent directory name instead
// which is probably more descriptive.
if (name === 'index') {
name = Path.basename(Path.dirname(entryAsset.name));
}

// Add the content hash and extension.
return name + '.' + hash + ext;
}

async package(bundler, oldHashes, newHashes = new Map()) {
if (this.isEmpty) {
return newHashes;
Expand Down Expand Up @@ -125,6 +178,7 @@ class Bundle {
let packager = new Packager(this, bundler);

let startTime = Date.now();
await packager.setup();
await packager.start();

let included = new Set();
Expand Down
22 changes: 16 additions & 6 deletions src/Bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ class Bundler extends EventEmitter {
options.hmrHostname ||
(options.target === 'electron' ? 'localhost' : ''),
detailedReport: options.detailedReport || false,
autoinstall: (options.autoinstall || false) && !isProduction
autoinstall: (options.autoinstall || false) && !isProduction,
contentHash:
typeof options.contentHash === 'boolean'
? options.contentHash
: isProduction
};
}

Expand Down Expand Up @@ -198,8 +202,14 @@ class Bundler extends EventEmitter {
}

// Create a new bundle tree and package everything up.
let bundle = this.createBundleTree(this.mainAsset);
this.bundleHashes = await bundle.package(this, this.bundleHashes);
this.mainBundle = this.createBundleTree(this.mainAsset);
this.bundleNameMap = this.mainBundle.getBundleNameMap(
this.options.contentHash
);
this.bundleHashes = await this.mainBundle.package(
this,
this.bundleHashes
);

// Unload any orphaned assets
this.unloadOrphanedAssets();
Expand All @@ -208,11 +218,11 @@ class Bundler extends EventEmitter {
let time = prettifyTime(buildTime);
logger.status(emoji.success, `Built in ${time}.`, 'green');
if (!this.watcher) {
bundleReport(bundle, this.options.detailedReport);
bundleReport(this.mainBundle, this.options.detailedReport);
}

this.emit('bundled', bundle);
return bundle;
this.emit('bundled', this.mainBundle);
return this.mainBundle;
} catch (err) {
this.errored = true;
logger.error(err);
Expand Down
6 changes: 5 additions & 1 deletion src/Logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class Logger {
? options.color
: chalk.supportsColor;
this.chalk = new chalk.constructor({enabled: this.color});
this.isTest =
options && typeof options.isTest === 'boolean'
? options.isTest
: process.env.NODE_ENV === 'test';
}

countLines(message) {
Expand Down Expand Up @@ -87,7 +91,7 @@ class Logger {
}

clear() {
if (!this.color) {
if (!this.color || this.isTest) {
return;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const serverErrors = require('./utils/customErrors').serverErrors;
const generateCertificate = require('./utils/generateCertificate');
const getCertificate = require('./utils/getCertificate');
const logger = require('./Logger');
const path = require('path');

serveStatic.mime.define({
'application/wasm': ['wasm']
Expand Down Expand Up @@ -56,8 +57,8 @@ function middleware(bundler) {

function sendIndex() {
// If the main asset is an HTML file, serve it
if (bundler.mainAsset.type === 'html') {
req.url = `/${bundler.mainAsset.generateBundleName()}`;
if (bundler.mainBundle.type === 'html') {
req.url = `/${path.basename(bundler.mainBundle.name)}`;
serve(req, res, send404);
} else {
send404();
Expand Down
26 changes: 21 additions & 5 deletions src/assets/HTMLAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ const META = {
]
};

// Options to be passed to `addURLDependency` for certain tags + attributes
const OPTIONS = {
a: {
href: {entry: true}
},
iframe: {
src: {entry: true}
}
};

class HTMLAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
Expand All @@ -75,20 +85,20 @@ class HTMLAsset extends Asset {
return res;
}

processSingleDependency(path) {
let assetPath = this.addURLDependency(decodeURIComponent(path));
processSingleDependency(path, opts) {
let assetPath = this.addURLDependency(decodeURIComponent(path), opts);
if (!isURL(assetPath)) {
assetPath = urlJoin(this.options.publicURL, assetPath);
}
return assetPath;
}

collectSrcSetDependencies(srcset) {
collectSrcSetDependencies(srcset, opts) {
const newSources = [];
for (const source of srcset.split(',')) {
const pair = source.trim().split(' ');
if (pair.length === 0) continue;
pair[0] = this.processSingleDependency(pair[0]);
pair[0] = this.processSingleDependency(pair[0], opts);
newSources.push(pair.join(' '));
}
return newSources.join(',');
Expand Down Expand Up @@ -121,9 +131,15 @@ class HTMLAsset extends Asset {
if (node.tag === 'a' && node.attrs[attr].lastIndexOf('.') < 1) {
continue;
}

if (elements && elements.includes(node.tag)) {
let depHandler = this.getAttrDepHandler(attr);
node.attrs[attr] = depHandler.call(this, node.attrs[attr]);
let options = OPTIONS[node.tag];
node.attrs[attr] = depHandler.call(
this,
node.attrs[attr],
options && options[attr]
);
this.isAstDirty = true;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/assets/RawAsset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Asset = require('../Asset');
const urlJoin = require('../utils/urlJoin');
const md5 = require('../utils/md5');

class RawAsset extends Asset {
// Don't load raw assets. They will be copied by the RawPackager directly.
Expand All @@ -21,6 +22,10 @@ class RawAsset extends Asset {
js: `module.exports=${JSON.stringify(pathToAsset)};`
};
}

async generateHash() {
return await md5.file(this.name);
}
}

module.exports = RawAsset;
2 changes: 1 addition & 1 deletion src/packagers/CSSPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class CSSPackager extends Packager {
css = `@media ${media.join(', ')} {\n${css.trim()}\n}\n`;
}

await this.dest.write(css);
await this.write(css);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/packagers/HTMLPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class HTMLPackager extends Packager {
).process(html, {sync: true}).html;
}

await this.dest.write(html);
await this.write(html);
}

addBundlesToTree(bundles, tree) {
Expand Down
8 changes: 4 additions & 4 deletions src/packagers/JSPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class JSPackager extends Packager {
this.options.hmrHostname
)};` + preludeCode;
}
await this.dest.write(preludeCode + '({');
await this.write(preludeCode + '({');
this.lineOffset = lineCounter(preludeCode);
}

Expand Down Expand Up @@ -101,7 +101,7 @@ class JSPackager extends Packager {
wrapped += ']';

this.first = false;
await this.dest.write(wrapped);
await this.write(wrapped);

// Use the pre-computed line count from the source map if possible
let lineCount = map && map.lineCount ? map.lineCount : lineCounter(code);
Expand Down Expand Up @@ -198,10 +198,10 @@ class JSPackager extends Packager {
entry.push(this.bundle.entryAsset.id);
}

await this.dest.write('},{},' + JSON.stringify(entry) + ')');
await this.write('},{},' + JSON.stringify(entry) + ')');
if (this.options.sourceMaps) {
// Add source map url
await this.dest.write(
await this.write(
`\n//# sourceMappingURL=${urlJoin(
this.options.publicURL,
path.basename(this.bundle.name, '.js') + '.map'
Expand Down
23 changes: 21 additions & 2 deletions src/packagers/Packager.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
const fs = require('fs');
const promisify = require('../utils/promisify');
const path = require('path');
const {mkdirp} = require('../utils/fs');

class Packager {
constructor(bundle, bundler) {
this.bundle = bundle;
this.bundler = bundler;
this.options = bundler.options;
this.setup();
}

setup() {
async setup() {
// Create sub-directories if needed
if (this.bundle.name.includes(path.sep)) {
await mkdirp(path.dirname(this.bundle.name));
}

this.dest = fs.createWriteStream(this.bundle.name);
this.dest.write = promisify(this.dest.write.bind(this.dest));
this.dest.end = promisify(this.dest.end.bind(this.dest));
}

async write(string) {
await this.dest.write(this.replaceBundleNames(string));
}

replaceBundleNames(string) {
// Replace temporary bundle names in the output with the final content-hashed names.
for (let [name, map] of this.bundler.bundleNameMap) {
string = string.split(name).join(map);
}

return string;
}

async start() {}

// eslint-disable-next-line no-unused-vars
Expand Down
13 changes: 1 addition & 12 deletions src/packagers/RawPackager.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
const Packager = require('./Packager');
const fs = require('../utils/fs');
const path = require('path');
const url = require('url');

class RawPackager extends Packager {
// Override so we don't create a file for this bundle.
// Each asset will be emitted as a separate file instead.
setup() {}

async addAsset(asset) {
// Use the bundle name if this is the entry asset, otherwise generate one.
let name = this.bundle.name;
if (asset !== this.bundle.entryAsset) {
name = url.resolve(
path.join(path.dirname(this.bundle.name), asset.generateBundleName()),
''
);
}

let contents = asset.generated[asset.type];
if (!contents || (contents && contents.path)) {
contents = await fs.readFile(contents ? contents.path : asset.name);
}

this.size = contents.length;
await fs.writeFile(name, contents);
await fs.writeFile(this.bundle.name, contents);
}

getSize() {
Expand Down
Loading

0 comments on commit 76fa1ed

Please sign in to comment.