UNPKG

lasso

Version:

Lasso.js is a build tool and runtime library for building and bundling all of the resources needed by a web application

515 lines (425 loc) 18.3 kB
const nodePath = require('path'); const extend = require('raptor-util').extend; const inherit = require('raptor-util').inherit; const Dependency = require('./Dependency'); const CONTENT_TYPE_CSS = require('../content-types').CSS; const CONTENT_TYPE_JS = require('../content-types').JS; const ok = require('assert').ok; const typePathRegExp = /^([A-Za-z0-9_\-]{2,})\s*:\s*(.+)$/; // Hack: {2,} is used because Windows file system paths start with "c:\" const readStream = require('../util').readStream; const RequireHandler = require('./RequireHandler'); const equal = require('assert').equal; const globNormalizer = require('./glob').normalizer; const dependencyResource = require('./dependency-resource'); const logger = require('raptor-logging').logger(module); const slice = Array.prototype.slice; const hasOwn = Object.prototype.hasOwnProperty; function createDefaultNormalizer(registry) { function parsePath(path) { const typePathMatches = typePathRegExp.exec(path); if (typePathMatches) { return { type: typePathMatches[1], path: typePathMatches[2] }; } else { let type = registry.typeForPath(path); if (!type) { type = 'package'; } return { type, path }; } } return function(dependency) { if (typeof dependency === 'string') { dependency = parsePath(dependency); } else if (!Array.isArray(dependency)) { dependency = Object.assign({}, dependency); // the dependency doesn't have a type so try to infer it from the path if (!dependency.type) { if (dependency.package) { dependency.type = 'package'; dependency.path = dependency.package; delete dependency.package; } else if (dependency.path) { const parsed = parsePath(dependency.path); dependency.type = parsed.type; dependency.path = parsed.path; } else if (dependency.intersection) { dependency.type = 'intersection'; dependency.dependencies = dependency.intersection; delete dependency.intersection; } else if (dependency.dependencies) { dependency.type = 'dependencies'; } } } return dependency; }; } function DependencyRegistry() { this.registeredTypes = {}; this.extensions = {}; this.requireExtensions = {}; this._normalizers = []; this._finalNormalizers = []; this.addNormalizer(createDefaultNormalizer(this)); this.registerDefaults(); this.requireExtensions = {}; this.requireExtensionNames = undefined; } DependencyRegistry.prototype = { __DependencyRegistry: true, registerDefaults: function() { this.registerStyleSheetType('css', require('./dependency-resource')); this.registerJavaScriptType('js', require('./dependency-resource')); this.registerJavaScriptType('comment', require('./dependency-comment')); this.registerPackageType('package', require('./dependency-package')); this.registerPackageType('intersection', require('./dependency-intersection')); this.registerPackageType('dependencies', require('./dependency-dependencies')); this.registerExtension('browser.json', 'package'); this.registerExtension('optimizer.json', 'package'); }, typeForPath: function(path) { // Find the type from the longest matching file extension. // For example if we are trying to infer the type of "jquery-1.8.3.js" then we will try: // a) "8.3.js" // b) "3.js" // c) "js" path = nodePath.basename(path); let type = this.extensions[path]; if (type) { // This is to handle the case where the extension // is the actual filename. For example: "browser.json" return type; } let dotPos = path.indexOf('.'); if (dotPos === -1) { return null; } do { type = path.substring(dotPos + 1); if (hasOwn.call(this.extensions, type)) { return this.extensions[type]; } // move to the next dot position dotPos = path.indexOf('.', dotPos + 1); } while (dotPos !== -1); const lastDot = path.lastIndexOf('.'); return path.substring(lastDot + 1); }, addNormalizer: function(normalizerFunc) { ok(typeof normalizerFunc === 'function', 'function expected'); this._normalizers.unshift(normalizerFunc); // Always run the glob normalizer first this._finalNormalizers = [globNormalizer].concat(this._normalizers); }, registerType: function(type, mixins) { equal(typeof type, 'string', '"type" should be a string'); equal(typeof mixins, 'object', '"mixins" should be a object'); const isPackageDependency = mixins._packageDependency === true; const hasReadFunc = mixins.read; if (isPackageDependency && hasReadFunc) { throw new Error('Manifest dependency of type "' + type + '" is not expected to have a read() method.'); } if (mixins.init) { mixins.doInit = mixins.init; delete mixins.init; } mixins = extend({}, mixins); const properties = mixins.properties || {}; const childProperties = Object.create(Dependency.prototype.properties); extend(childProperties, properties); mixins.properties = childProperties; const calculateKey = mixins.calculateKey; if (calculateKey) { mixins.doCalculateKey = calculateKey; delete mixins.calculateKey; } const getLastModified = mixins.getLastModified || mixins.lastModified; if (getLastModified) { mixins.doGetLastModified = getLastModified; delete mixins.getLastModified; delete mixins.lastModified; } if (!isPackageDependency && mixins.read) { // Wrap the read method to ensure that it always returns a stream // instead of possibly using a callback const oldRead = mixins.read; delete mixins.read; mixins.doRead = function(lassoContext) { return readStream(() => { return oldRead.call(this, lassoContext); }); }; } const _this = this; function Ctor(dependencyConfig, dirname, filename) { this.__dependencyRegistry = _this; Dependency.call(this, dependencyConfig, dirname, filename); } inherit(Ctor, Dependency); extend(Ctor.prototype, mixins); this.registeredTypes[type] = Ctor; }, registerRequireExtension: function(ext, options) { this.requireExtensionNames = undefined; equal(typeof ext, 'string', '"ext" should be a string'); if (ext.charAt(0) === '.') { ext = ext.substring(1); } if (typeof options === 'function') { options = { read: options }; } ok(options.read || options.createReadStream, '"read" or "createReadStream" is required'); this.requireExtensions[ext] = options; }, /** * In addition to registering a require extension using the "registerRequireExtension", * this method also registers a new dependency type with possibly additional properties. * * For example, if you just use "registerRequireExtension('foo', ...)", then only the following is supported: * - var foo = require('./hello.foo'); * - "require: ./hello.foo" * * However, if you use registerRequireType with custom proeprties then all of the following are supported: * - var foo = require('./hello.foo'); * - "require: ./hello.foo" * - "hello.foo", * - { "type": "foo", "path": "hello.foo", "hello": "world" } * * For an example, please see: * https://github.com/lasso-js/lasso-marko/blob/master/lasso-marko-plugin.js * * * dependency that can be required. Howev * @param {String} type The extension/type to register * @param {Object} mixins [description] */ registerRequireType: function(type, options) { equal(typeof type, 'string', '"type" should be a string'); equal(typeof options, 'object', '"options" should be a object'); const userRead = options.read; const userCreateReadStream = options.createReadStream; const userGetLastModified = options.getLastModified; const extensionOptions = extend({}, options); if (userRead) { extensionOptions.read = function(path, lassoContext, callback) { // Chop off the first path argument return userRead.apply(this, slice.call(arguments, 1)); }; } if (userCreateReadStream) { extensionOptions.userCreateReadStream = function(path, lassoContext) { // Chop off the first path argument return userCreateReadStream.apply(this, slice.call(arguments, 1)); }; } if (userGetLastModified) { extensionOptions.getLastModified = async function (path, lassoContext) { // Chop off the first path argument return userGetLastModified.apply(this, slice.call(arguments, 1)); }; } this.registerRequireExtension(type, extensionOptions); this.registerPackageType(type, { properties: { path: 'string' }, async init(lassoContext) { this.path = this.resolvePath(this.path); }, getSourceFile() { return this.path; }, async getDependencies (lassoContext) { const path = this.path; return [ { type: 'require', path } ]; } }); }, getRequireExtensionNames() { if (this.requireExtensionNames === undefined) { const extensionsLookup = {}; // eslint-disable-next-line n/no-deprecated-api const nodeRequireExtensions = require.extensions; for (const ext in nodeRequireExtensions) { if (ext !== '.node') { extensionsLookup[ext] = true; } } for (let ext in this.requireExtensions) { if (ext.charAt(0) !== '.') { ext = '.' + ext; } extensionsLookup[ext] = true; } this.requireExtensionNames = Object.keys(extensionsLookup); } return this.requireExtensionNames; }, createRequireHandler(path, lassoContext, userOptions) { ok(path, '"path" is required'); ok(lassoContext, '"lassoContext" is required'); ok(userOptions, '"userOptions" is required'); ok(typeof path === 'string', '"path" should be a string'); ok(typeof lassoContext === 'object', '"lassoContext" should be an object'); return new RequireHandler(userOptions, lassoContext, path); }, getRequireHandler: function(path, lassoContext) { ok(path, '"path" is required'); ok(lassoContext, '"lassoContext" is required'); ok(typeof path === 'string', '"path" should be a string'); ok(typeof lassoContext === 'object', '"lassoContext" should be an object'); const basename = nodePath.basename(path); const lastDot = basename.lastIndexOf('.'); if (lastDot === -1) { return null; } const ext = basename.substring(lastDot + 1); const userOptions = this.requireExtensions[ext]; if (!userOptions) { return null; } return new RequireHandler(userOptions, lassoContext, path); }, registerJavaScriptType: function(type, mixins) { equal(typeof type, 'string', '"type" should be a string'); equal(typeof mixins, 'object', '"mixins" should be a object'); mixins.contentType = CONTENT_TYPE_JS; this.registerType(type, mixins); }, registerStyleSheetType: function(type, mixins) { equal(typeof type, 'string', '"type" should be a string'); equal(typeof mixins, 'object', '"mixins" should be a object'); mixins.contentType = CONTENT_TYPE_CSS; this.registerType(type, mixins); }, registerPackageType: function(type, mixins) { equal(typeof type, 'string', '"type" should be a string'); equal(typeof mixins, 'object', '"mixins" should be a object'); mixins._packageDependency = true; this.registerType(type, mixins); }, registerExtension: function(extension, type) { equal(typeof extension, 'string', '"extension" should be a string'); equal(typeof type, 'string', '"type" should be a string'); this.extensions[extension] = type; }, getType: function(type) { return this.registeredTypes[type]; }, createDependency: function(config, dirname, filename) { ok(config, '"config" is required'); ok(dirname, '"dirname" is required'); equal(typeof config, 'object', 'Invalid dependency: ' + require('util').inspect(config)); const type = config.type; const Ctor = this.registeredTypes[type]; if (!Ctor) { throw new Error('Dependency of type "' + type + '" is not supported. (dependency=' + require('util').inspect(config) + ', package="' + filename + '"). Registered types:\n' + Object.keys(this.registeredTypes).join(', ')); } return new Ctor(config, dirname, filename); }, async normalizeDependencies (dependencies, dirname, filename) { logger.debug('normalizeDependencies() BEGIN: ', dependencies, 'count:', dependencies.length); if (dependencies.length === 0) { return dependencies; } let i = 0; let j = 0; dependencies = dependencies.concat([]); const normalizers = this._finalNormalizers; const normalizerCount = normalizers.length; const context = { dirname, filename }; function handleNormalizedDependency (dependencies, i, normalizedDependency) { if (normalizedDependency) { if (Array.isArray(normalizedDependency)) { // Remove one dependencies.splice.apply(dependencies, [i, 1].concat(normalizedDependency)); j = 0; // Continue at the same dependency index, but restart normalizing at the beginning return null; } else { dependencies[i] = normalizedDependency; } } j++; return normalizedDependency; }; const handleDependencyNormalization = async () => { while (i < dependencies.length) { let dependency = dependencies[i]; if (!dependency.__Dependency) { if (j < normalizerCount) { const normalizeFunc = normalizers[j]; const normalizedDependency = await normalizeFunc(dependency, context); const handledNormalizedDep = handleNormalizedDependency(dependencies, i, normalizedDependency); dependency = handledNormalizedDep || dependency; // Stop looping and we will pick up where we left off when // the async normalizer finishes return handleDependencyNormalization(); } // Restart with the first normalizer for the next dependency j = 0; // Convert the dependency object to an actual Dependency instance dependency = this.createDependency(dependency, dirname, filename); } dependencies[i] = dependency; i++; } logger.debug('normalizeDependencies() DONE!'); return dependencies; }; return handleDependencyNormalization(); }, /** * This method is used to create a new JavaScript or CSS * type that allows the code to be transformed using a custom * transform function. This was introduced because we wanted to * be able to easily use the babel transpiler on individual * JS dependencies without registering a global transform. */ createResourceTransformType (transformFunc) { const transformType = extend({}, dependencyResource); extend(transformType, { isExternalResource: function() { return false; }, async read (context) { const readResult = await dependencyResource.read.call(this, {}); return new Promise((resolve, reject) => { function callback (err, res) { return err ? reject(err) : resolve(res); } if (typeof readResult === 'string') { return transformFunc(readResult, callback); } else if (readResult) { let code = ''; readResult .on('data', function(data) { code += data; }) .on('end', function() { transformFunc(code, callback); }); } }); } }); return transformType; } }; module.exports = DependencyRegistry;