broccoli-concat
Version:
Fast, good-enough concatenation with source maps.
188 lines (153 loc) • 4.7 kB
JavaScript
'use strict';
const findIndex = require('find-index');
function contentMapper(entry) {
return entry.content;
}
function notUndefined(content) {
return content !== undefined;
}
class Entry {
constructor(file, contentLimit, fs) {
this._content = undefined;
this._contentLimit = contentLimit || 10000;
this._fs = fs;
this.file = file;
}
get content() {
if (this._content !== undefined) {
return this._content;
}
let content = this._fs.readFileSync(`${this.file}`, 'utf-8');
if (content.length < this._contentLimit) {
this._content = content;
}
return content;
}
set content(value) {
// if value is less than content limit, set it; otherwise invalidate
// previous content so we don't unexpectedly read from it later and
// get the old content
if (value.length < this._contentLimit) {
this._content = value;
} else {
this._content = undefined;
}
}
}
class SimpleConcat {
constructor(attrs) {
this.separator = attrs.separator || '';
this.header = attrs.header;
this.headerFiles = attrs.headerFiles || [];
this.footerFiles = attrs.footerFiles || [];
this.footer = attrs.footer;
this.contentLimit = attrs.contentLimit;
this.fs = attrs.fs;
// Internally, we represent the concatenation as a series of entries. These
// entries have a 'file' attribute for lookup/sorting and a 'content' property
// which represents the value to be used in the concatenation.
this._internal = [];
// We represent the header/footer files as empty at first so that we don't
// have to figure out order when patching
this._internalHeaderFiles = this.headerFiles.map(file => { return { file: file, content: '' }; });
this._internalFooterFiles = this.footerFiles.map(file => { return { file: file, content: '' }; });
}
/**
* Finds the index of the given file in the internal data structure.
*/
_findIndexOf(file) {
return findIndex(this._internal, entry => entry.file === file);
}
/**
* Updates the contents of a header file.
*/
_updateHeaderFile(fileIndex, content) {
this._internalHeaderFiles[fileIndex].content = content;
}
/**
* Updates the contents of a footer file.
*/
_updateFooterFile(fileIndex, content) {
this._internalFooterFiles[fileIndex].content = content;
}
/**
* Determines if the given file is a header or footer file, and if so updates
* it with the given contents.
*/
_handleHeaderOrFooterFile(file, content) {
let headerFileIndex = this.headerFiles.indexOf(file);
if (headerFileIndex !== -1) {
this._updateHeaderFile(headerFileIndex, content);
return true;
}
let footerFileIndex = this.footerFiles.indexOf(file);
if (footerFileIndex !== -1) {
this._updateFooterFile(footerFileIndex, content);
return true;
}
return false;
}
addFile(file, content) {
if (this._handleHeaderOrFooterFile(file, content)) {
return;
}
let entry = new Entry(file, this.contentLimit, this.fs);
let index = findIndex(this._internal, entry => entry.file > file);
if (index === -1) {
this._internal.push(entry);
} else {
this._internal.splice(index, 0, entry);
}
}
updateFile(file, content) {
if (this._handleHeaderOrFooterFile(file, content)) {
return;
}
let index = this._findIndexOf(file);
if (index === -1) {
throw new Error('Trying to update ' + file + ' but it has not been read before');
}
this._internal[index].content = content;
}
removeFile(file) {
if (this._handleHeaderOrFooterFile(file, undefined)) {
return;
}
let index = this._findIndexOf(file);
if (index === -1) {
throw new Error('Trying to remove ' + file + ' but it did not previously exist');
}
this._internal.splice(index, 1);
}
fileSizes() {
return [].concat(
this._internalHeaderFiles,
this._internal,
this._internalFooterFiles
).reduce((sizes, entry) => {
sizes[entry.file] = entry.content.length;
return sizes;
}, {});
}
result() {
let fileContents = [].concat(
this._internalHeaderFiles.map(contentMapper),
this._internal.map(contentMapper),
this._internalFooterFiles.map(contentMapper)
).filter(notUndefined);
if (!fileContents.length) {
return;
}
let content = [].concat(
this.header,
fileContents,
this.footer
).filter(notUndefined).join(this.separator);
if (this.footer) {
content += '\n';
}
return content;
}
}
SimpleConcat.isPatchBased = true;
module.exports = SimpleConcat;