lasso
Version:
Lasso.js is a build tool and runtime library for building and bundling all of the resources needed by a web application
645 lines (531 loc) • 20.6 kB
JavaScript
const nodePath = require('path');
const condition = require('../condition');
const CONTENT_TYPE_CSS = require('../content-types').CSS;
const CONTENT_TYPE_JS = require('../content-types').JS;
const CONTENT_TYPE_NONE = require('../content-types').NONE;
const util = require('../util');
const ok = require('assert').ok;
const equal = require('assert').equal;
const Readable = require('stream').Readable;
const manifestLoader = require('../manifest-loader');
const logger = require('raptor-logging').logger(module);
const lastModified = require('../last-modified');
const AsyncValue = require('raptor-async/AsyncValue');
const EventEmitter = require('events').EventEmitter;
const hasOwn = Object.prototype.hasOwnProperty;
const NON_KEY_PROPERTIES = {
inline: true,
slot: true,
'js-slot': true,
'css-slot': true,
getDefaultBundleName: true
};
function getPackagePath(d) {
return d.__filename ? d.__filename : '(unknown)';
}
function doCalculateFingerprint (dependency, lassoContext) {
return new Promise((resolve, reject) => {
const input = dependency.read(lassoContext);
const cachingReadStream = util.createCachingStream();
const fingerprintStream = util.createFingerprintStream()
.on('error', reject)
.on('fingerprint', function(fingerprint) {
dependency._cachingReadStream = cachingReadStream;
if (logger.isDebugEnabled()) {
logger.debug('Dependency ' + dependency.toString() + ' fingerprint key: ' + fingerprint);
}
resolve(fingerprint);
});
// Don't buffer the data so that the stream can be drained
fingerprintStream.resume();
input
.on('error', function(e) {
let message = 'Unable to read dependency "' + dependency + '" referenced in "' + dependency.getParentManifestPath() + '". ';
if (e.code === 'ENOENT' && e.path) {
message += 'File does not exist: ' + e.path;
} else {
message += 'Error: ' + (e.stack || e);
}
e = new Error(message);
e.dependency = dependency;
reject(e);
})
.pipe(cachingReadStream)
.pipe(fingerprintStream);
});
};
// This is a simple stream implementation that either has code available
// immediately or it is waiting for code to be made available upon
// callback completion
function DependencyReadable() {
}
require('util').inherits(DependencyReadable, Readable);
DependencyReadable.prototype._read = function() {
// don't need to actually implement _read because
};
function Dependency(dependencyConfig, dirname, filename) {
ok(dependencyConfig != null, '"dependencyConfig" is a required argument');
equal(typeof dependencyConfig, 'object', '"dependencyConfig" should be an object');
ok(dirname, '"dirname" is a required argument');
equal(typeof dirname, 'string', '"dirname" should be a string');
this._resolvedInit = undefined;
this._keyAsyncValue = undefined;
this._lastModifiedAsyncValue = undefined;
this._cachingReadStream = undefined;
// The directory associated with this dependency.
// If the dependency was defined in an browser.json
// file then the directory is the directory in which
// browser.json is found.
this.__dirname = dirname;
this.__filename = filename;
this.type = dependencyConfig.type;
this._condition = condition.fromObject(dependencyConfig);
this._events = undefined;
this.set(dependencyConfig);
}
Dependency.prototype = {
__Dependency: true,
properties: {
type: 'string',
attributes: 'object',
inline: 'string',
slot: 'string',
'css-slot': 'string',
'js-slot': 'string',
// TODO: Change: Should these be removed?
if: 'string',
'if-extension': 'string', /* DEPRECRATED */
'if-not-extension': 'string', /* DEPRECRATED */
'if-flag': 'string',
'if-not-flag': 'string',
getDefaultBundleName: 'function'
},
async init (lassoContext) {
this._context = lassoContext;
if (this._resolvedInit) {
return;
}
await this.doInit(lassoContext);
this._resolvedInit = true;
},
async doInit (lassoContext) {},
createReadStream: function(lassoContext) {
if (this._cachingReadStream) {
return this._cachingReadStream.createReplayStream();
}
return this.doRead(lassoContext);
},
/**
*
* @deprecated use createReadStream instead
*/
read: function(lassoContext) {
return this.createReadStream(lassoContext);
},
set: function(props) {
const propertyTypes = this.properties;
for (const k in props) {
if (hasOwn.call(props, k)) {
let v = props[k];
if (propertyTypes) {
const type = propertyTypes[k];
if (!type && !k.startsWith('_')) {
throw new Error('Dependency of type "' + this.type + '" does not support property "' + k + '". Package: ' + getPackagePath(this));
}
if (type && typeof v === 'string') {
if (type === 'boolean') {
v = (v === 'true');
} else if (type === 'int' || type === 'integer') {
v = parseInt(v, 10);
} else if (type === 'float' || type === 'number') {
v = parseFloat(v);
} else if (type === 'path') {
v = this.resolvePath(v);
}
}
}
this[k] = v;
}
}
},
/*
* This resolve method will use the module search path to resolve
* the given path if the path does not begin with "." or "/".
*
* For example, dependency.resolvePath('some-module/a.js')
* will use the NodeJS module search path starting from this dependencies directory.
* If resolution fails using the NodeJS module search path, then an attempt
* will be made to resolve the path as a relative path.
*
* dependency.resolvePath('./a.js') we be resolved relative to the
* directory of this dependency.
*
* dependency.resolvePath('/a.js') is assumed to be absolute
* (possibly because it was already resolved).
*/
resolvePath: function(path, from) {
const result = this._context.resolve(path, from || this.__dirname, {
moduleFallbackToRelative: true
});
return result && result.path;
},
getParentManifestDir: function() {
return this.__dirname;
},
getParentManifestPath: function() {
return this.__filename;
},
isPackageDependency: function() {
return this._packageDependency === true;
},
/**
* This method is called to determine if a depednency can be added to a shared
* application bundle or if it can only be added to a page bundle.
* This method can be overridden, but the default behavior is to return
* false to indicate that it can be added to either a shared application
* bundle or a page-specific bundle.
* @return {boolean} Returns true if this is a page-bundle only dependency. False, otherwise.s
*/
isPageBundleOnlyDependency: function() {
return false;
},
onAddToPageBundle: function(bundle, lassoContext) {
// subclasses can override
},
onAddToAsyncPageBundle: function(bundle, lassoContext) {
// subclasses can override
},
async getPackageManifest (lassoContext) {
if (!this.isPackageDependency()) {
throw new Error('getPackageManifest() failed. Dependency is not a package: ' + this.toString());
}
let manifest;
if (typeof this.loadPackageManifest === 'function') {
const packageManifestResult = await this.loadPackageManifest(lassoContext);
if (!packageManifestResult) {
return null;
}
const dependencyRegistry = this.__dependencyRegistry;
const LassoManifest = require('../LassoManifest');
if (typeof packageManifestResult === 'string') {
const manifestPath = packageManifestResult;
const from = this.getParentManifestDir();
try {
manifest = this.createPackageManifest(manifestLoader.load(manifestPath, from));
} catch (e) {
let err;
if (e.fileNotFound) {
err = new Error('Lasso manifest not found for path "' +
manifestPath + '" (searching from "' + from + '"). Dependency: ' +
this.toString());
} else {
err = new Error('Unable to load lasso manifest for path "' +
manifestPath + '". Dependency: ' + this.toString() + '. Exception: ' +
(e.stack || e));
}
throw err;
}
} else if (!LassoManifest.isLassoManifest(packageManifestResult)) {
manifest = new LassoManifest({
manifest: packageManifestResult,
dependencyRegistry,
dirname: this.getParentManifestDir(),
filename: this.getParentManifestPath()
});
} else {
manifest = packageManifestResult;
}
return manifest;
} else if (typeof this.getDependencies === 'function') {
const dependencies = await this.getDependencies(lassoContext);
if (dependencies) {
manifest = this.createPackageManifest(dependencies);
}
return manifest;
} else {
throw new Error('getPackageManifest() failed. "getDependencies" or "loadPackageManifest" expected: ' + this.toString());
}
},
_getKeyPropertyNames: function() {
return Object.keys(this)
.filter(function(k) {
return !k.startsWith('_') && k !== 'type' && !hasOwn.call(NON_KEY_PROPERTIES, k);
}, this)
.sort();
},
getKey: function() {
if (!this._keyAsyncValue || !this._keyAsyncValue.isResolved()) {
return null;
// throw new Error('getKey() was called before key was calculated');
}
return this._keyAsyncValue.data;
},
/**
* getReadCacheKey() must be a unique key across all lasso context since
* it is flattened to single level and shared by multiple lasso instances.
*/
getReadCacheKey: function() {
return this.getPropsKey();
},
getPropsKey: function() {
return this._propsKey || (this._propsKey = this.calculateKeyFromProps());
},
/**
* calculateKey() is used to calculate a unique key for this dependency
* that is unique within the given lasso context.
*
* getReadCacheKey() must be a unique key across all lasso context since
* it is flattened to single level and shared by multiple lasso instances.
*/
calculateKey (lassoContext) {
// TODO: Change to fully use async/await
return new Promise((resolve, reject) => {
function callback (err, res) {
return err ? reject(err) : resolve(res);
}
if (this._key !== undefined) {
return callback(null, this._key);
}
if (this._keyAsyncValue) {
// Attach a listener to the current in-progres check
return this._keyAsyncValue.done(callback);
}
// no data holder so let's create one
let keyAsyncValue;
this._keyAsyncValue = keyAsyncValue = new AsyncValue();
this._keyAsyncValue.done(callback);
const handleKey = (key) => {
if (key === null) {
key = this.type + '|' + lassoContext.uniqueId();
} else if (typeof key !== 'string') {
keyAsyncValue.reject(new Error('Invalid key: ' + key));
return;
}
if (logger.isDebugEnabled()) {
logger.debug('Calculated key for ' + this.toString() + ': ' + key);
}
// Store resolve key in "_key" for quick lookup
this._key = key;
keyAsyncValue.resolve(key);
};
const keyResult = this.doCalculateKey(lassoContext);
if ((typeof keyResult === 'string') && !keyAsyncValue.isSettled()) {
handleKey(keyResult);
} else if (keyResult && typeof keyResult === 'object' && keyResult.then) {
keyResult
.then((key) => {
handleKey(key);
})
.catch((err) => {
keyAsyncValue.reject(err);
});
} else {
return handleKey(keyResult);
}
});
},
doCalculateKey (lassoContext) {
if (this.isPackageDependency()) {
return this.calculateKeyFromProps();
} else if (this.isExternalResource()) {
const url = this.getUrl ? this.getUrl(lassoContext) : this.url;
return url;
} else {
if (lassoContext.cache) {
return this.getLastModified(lassoContext)
.then((lastModified) => {
if (!lastModified) {
return doCalculateFingerprint(this, lassoContext);
}
return lassoContext.cache.getDependencyFingerprint(
// cache key
this.getPropsKey(),
// last modified timestamp (if cache entry is older than this then builder will be called)
lastModified,
// builder
doCalculateFingerprint.bind(null, this, lassoContext));
});
} else {
return doCalculateFingerprint(this, lassoContext);
}
}
},
calculateKeyFromProps: function() {
const key = this._getKeyPropertyNames()
.map(function(k) {
return k + '=' + this[k];
}, this)
.join('|');
return this.type + '|' + key;
},
hasContent: function() {
return this.contentType !== CONTENT_TYPE_NONE;
},
isJavaScript: function() {
return this.contentType === CONTENT_TYPE_JS;
},
isStyleSheet: function() {
return this.contentType === CONTENT_TYPE_CSS;
},
getSlot: function() {
if (this.slot) {
return this.slot;
}
if (this.isStyleSheet()) {
return this.getStyleSheetSlot();
} else {
return this.getJavaScriptSlot();
}
},
hasModifiedFingerprint: function() {
return false;
},
getContentType: function() {
return this.contentType;
},
isCompiled: function() {
return false;
},
isInPlaceDeploymentAllowed: function() {
return this.type === 'js' || this.type === 'css';
},
isExternalResource: function() {
return false;
},
getJavaScriptSlot: function() {
return this['js-slot'] || this.slot;
},
getStyleSheetSlot: function() {
return this['css-slot'] || this.slot;
},
async getLastModified (lassoContext) {
if (this._lastModifiedValue) {
return this._lastModifiedValue;
}
const getLastModified = (lastModified) =>
lastModified == null || lastModified < 0 ? 0 : lastModified;
const lastModified = this._lastModifiedValue =
getLastModified(await this.doGetLastModified(lassoContext));
return lastModified;
},
async doGetLastModified (lassoContext, callback) {
const sourceFile = this.getSourceFile();
if (sourceFile) {
return this.getFileLastModified(sourceFile);
} else {
return 0;
}
},
async getFileLastModified (path) {
return lastModified.forFile(path);
},
createPackageManifest: function(manifest, dirname, filename) {
const LassoManifest = require('../LassoManifest');
if (Array.isArray(manifest) || !manifest) {
manifest = {
dependencies: manifest || []
};
} else {
dirname = manifest.dirname;
filename = manifest.filename;
}
return new LassoManifest({
manifest,
dependencyRegistry: this.__dependencyRegistry,
dirname: dirname || this.getParentManifestDir(),
filename: filename || this.getParentManifestPath()
});
},
getDir: function() {
const sourceFile = this.getSourceFile();
// if (!sourceFile) {
// throw new Error('Unable to determine directory that dependency is associated with because getSourceFile() returned null and getDir() is not implemented. Dependency: ' + this.toString());
// }
return (sourceFile) ? nodePath.dirname(sourceFile) : null;
},
getSourceFile: function() {
return null;
},
toString() {
const entries = [];
const type = this.type;
for (const k in this) {
if (hasOwn.call(this, k) && !k.startsWith('_') && k !== 'type') {
const v = this[k];
entries.push(k + '=' + JSON.stringify(v));
}
}
// var packagePath = this.getParentManifestPath();
// if (packagePath) {
// entries.push('packageFile="' + packagePath + '"');
// }
return '[' + type + (entries.length ? (': ' + entries.join(', ')) : '') + ']';
},
shouldCache: function(lassoContext) {
let cacheable = true;
let isStatic = false;
const cacheConfig = this.cacheConfig;
if (cacheConfig) {
cacheable = cacheConfig.cacheable !== false;
isStatic = cacheConfig.static === true;
} else {
cacheable = this.cache !== false;
}
if (isStatic) {
const transformer = lassoContext.transformer;
if (!transformer || transformer.hasTransforms() === false) {
// Don't bother caching a dependency if it is static and there are no transforms
return false;
}
}
return cacheable;
},
emit: function() {
if (!this._events) {
// No listeners
return;
}
return this._events.emit.apply(this._events, arguments);
},
on: function(event, listener) {
if (!this._events) {
this._events = new EventEmitter();
}
return this._events.on(event, listener);
},
once: function(event, listener) {
if (!this._events) {
this._events = new EventEmitter();
}
return this._events.once(event, listener);
},
removeListener: function(event, listener) {
if (!this._events) {
// Nothing to remove
return;
}
return this._events.removeListener.apply(this._events, arguments);
},
removeAllListeners: function(event) {
if (!this._events) {
// Nothing to remove
return;
}
return this._events.removeAllListeners.apply(this._events, arguments);
},
getDefaultBundleName: function(pageBundleName, lassoContext) {
return this.defaultBundleName;
},
inspect: function() {
const inspected = {
type: this.type
};
this._getKeyPropertyNames()
.forEach((k) => {
inspected[k] = this[k];
});
return inspected;
}
};
Dependency.prototype.addListener = Dependency.prototype.on;
module.exports = Dependency;