UNPKG

raptor

Version:

RaptorJS provides an AMD module loader that works in Node, Rhino and the web browser. It also includes various sub-modules to support building optimized web applications.

1,095 lines (870 loc) 45.5 kB
var util = require('util'); var util = require('util'); var util = require('util'); var nodePath = require('path'); var q = require('q'); function getResourceUrlCacheKey(path, baseResource) { var key = path; if (baseResource) { key += '|' + baseResource.getURL(); } return key; } define.Class( 'raptor/optimizer/PageOptimizer', ['raptor'], function(raptor, require, exports, module) { "use strict"; var packaging = require('raptor/packaging'); var Cache = require('raptor/optimizer/Cache'); var OptimizerWriterMixins = require('raptor/optimizer/OptimizerWriterMixins'); var OptimizerFileWriter = require('raptor/optimizer/OptimizerFileWriter'); var FileUrlBuilder = require('raptor/optimizer/FileUrlBuilder'); var OptimizedPage = require('raptor/optimizer/OptimizedPage'); var SlotTracker = require('raptor/optimizer/SlotTracker'); var File = require('raptor/files/File'); var promises = require('raptor/promises'); var escapeXmlAttr = require('raptor/xml/utils').escapeXmlAttr; var logger = module.logger(); var crypto = require('crypto'); var forEach = raptor.forEach; var resources = require('raptor/resources'); var mime = require('raptor/mime'); /* * Bundle wrappers are objects that describe code prefixes and suffixes that * will be written for a bundle when enabled. In in-place-deployment mode, * a bundle for a source file might not be written so these wrappers will have * no impact on those files. */ var DEFAULT_BUNDLE_WRAPPERS = [ { /* * This wrapper puts all bundle code in the scope of a function that is provided * a define function, require function, and "raptorNoConflict" flag. This allows * the raptor module to not to have to put define and require functions in the global * scope which might conflict with other AMD-like implementations (such as RequireJS). */ id: 'raptor-no-conflict', prefix: '(function(define, require, raptorNoConflict) {\n', suffix: '\n})(window.raptorDefine, window.raptorRequire, true);', contentType: 'application/javascript' } ]; // This function is moved outside the optimagePageCached method to avoid memory // leak from closures function evictOptimizedPageFromCache(cache, cacheKey, ttl) { logger.info('Scheduling cached object with key "' + cacheKey + '" to be evicted' + ttl + 'ms'); setTimeout(function() { logger.info('Evicting cached object with key "' + cacheKey + '". TTL configured to be ' + ttl + 'ms'); cache.removeOptimizedPage(cacheKey); }, ttl); } var PageOptimizer = function(config) { if (!config) { throw new Error('config is required'); } this.config = config; this.cacheLookup = {}; this.cacheProvider = config.cacheProvider || require('raptor/caching').getDefaultProvider(); this.config.notifyPlugins('pageOptimizerConfigured', { config: this.config, pageOptimizer: this }); // Look for bundle wrappers from the config object or use the defaults. // NOTE: Bundle wrappers have to be explicitly enabled (they're disabled by default). // Bundle wrappers can be enabled in the PageOptimizer Config or BundleConfig. var bundleWrappers; if (config.wrappers === undefined) { bundleWrappers = DEFAULT_BUNDLE_WRAPPERS; } if (bundleWrappers) { // create copy of input bundle wrappers array this.bundleWrappers = bundleWrappers.slice(); } }; PageOptimizer.prototype = { getPageBundleSetConfig: function(pageName) { var pageConfig = this.config.getPageConfig(pageName), bundleSetConfig = null; if (pageConfig) { bundleSetConfig = pageConfig.bundleSetConfig; } if (!bundleSetConfig) { bundleSetConfig = this.config.getBundleSetConfig("default"); if (!bundleSetConfig) { bundleSetConfig = this.config.addBundleSetConfig({ name: "default" }); } } return bundleSetConfig; }, /** * This enables localization which triggers special bundle and module creation for enabled locales * * @param context the optimization context (whose i18n property will be initialized to a new I18nContext) * @param bundleSetConfig the bundle set configuration that is in use which will have new i18n * bundle configurations added to it */ enableLocalization: function(context, bundleSetConfig) { // create the i18n context and configure the bundles for each locale context.i18n = require('raptor/optimizer/i18n').createContext({ locales: this.config.getLocales() }); }, buildPageBundles: function(options, context) { var PageBundles = require('raptor/optimizer/PageBundles'), pageName = options.name || options.pageName, config = this.config, enabledExtensions = packaging.createExtensionCollection(options.enabledExtensions), _this = this; if (!pageName) { throw raptor.createError(new Error('"name" property is required')); } /* * If there are any globally enabled extensions then add those */ enabledExtensions.addAll(this.config.getEnabledExtensions()); context.enabledExtensions = enabledExtensions; var bundleSetConfig = this.getPageBundleSetConfig(pageName); if (this.config.enabledLocales) { this.enableLocalization(context, bundleSetConfig); } var packageManifest = options.packageManifest; if (!packageManifest) { var packageResource = options.packageResource; if (packageResource) { packageManifest = packaging.getPackageManifest(packageResource); } else { var packageFile = options.packageFile; if (packageFile) { if (typeof packageFile === 'string') { var packageFilePath = packageFile; packageFile = new File(packageFilePath); if (!packageFile.exists()) { throw raptor.createError(new Error("Provided package file does not exist: " + packageFilePath)); } } packageResource = require('raptor/resources').createFileResource(packageFile); packageManifest = packaging.getPackageManifest(packageResource); } else { var dependencies = options.dependencies; if (dependencies) { packageManifest = packaging.createPackageManifest(); packageManifest.setDependencies(dependencies); } else { module = options.module; if (module) { packageManifest = require('raptor/packaging').getModuleManifest(module); } } } } } else if (!packaging.isPackageManifest(packageManifest)) { throw raptor.createError(new Error("Invalid package manifest: " + packageManifest)); } if (!packageManifest) { throw raptor.createError(new Error("Package manifest for page not provided. One of the following properties is required: packageManifest, packageResource, packageFile, dependencies, module")); } var sourceUrlResolver = config.hasServerSourceMappings() ? function(path) { return config.getUrlForSourceFile(path); } : null; var startTime = Date.now(); var bundleMappingsPromise = config.isBundlingEnabled() ? this.getBundleMappingsCached(bundleSetConfig, context) : //Only load the bundle mappings if bundling is enabled null; function buildPageBundles(bundleMappings) { var startTime = Date.now(); var pageBundles = new PageBundles({ pageName: pageName, bundleMappings: bundleMappings, bundlingEnabled: config.isBundlingEnabled(), inPlaceDeploymentEnabled: config.isInPlaceDeploymentEnabled(), sourceUrlResolver: sourceUrlResolver, enabledExtensions: enabledExtensions, packageManifest: packageManifest, checksumsEnabled: config.isChecksumsEnabled(), context: context }); var pageBundlesPromise = pageBundles.build(); pageBundlesPromise.then(function() { logger.info('Page bundles for "' + pageName + '" built in ' + (Date.now() - startTime) + 'ms'); }); return pageBundlesPromise; } if (bundleMappingsPromise) { bundleMappingsPromise.then( function() { logger.info('Bundle mappings for page "' + pageName + '" built in ' + (Date.now() - startTime) + 'ms'); }); return bundleMappingsPromise.then(buildPageBundles); } else { return buildPageBundles(null); } }, getBundleMappingsCached: function(bundleSetConfig, context) { var cache = this.getCache(context); var cacheKey = bundleSetConfig._id; var bundleMappingsPromise = cache.getBundleMappings(cacheKey); if (!bundleMappingsPromise) { bundleMappingsPromise = this.config.createBundleMappings(bundleSetConfig, context); cache.addBundleMappings(bundleSetConfig._id, bundleMappingsPromise); } return bundleMappingsPromise; }, uncacheBundleMappings: function(bundleSetConfig, context) { var cache = this.getCache(context); var cacheKey = bundleSetConfig._id; cache.removeBundleMappings(bundleSetConfig._id); }, buildCacheKey: function(context) { var config = this.getConfig(); var cacheKey = null; function cacheKey_add(str) { if (cacheKey) { cacheKey += '|' + str; } else { cacheKey = str; } } config.notifyPlugins('buildCacheKey', { context: context, config: config, pageOptimizer: this, cacheKey: { add: cacheKey_add } }); var enabledExtensions = context.enabledExtensions; if (enabledExtensions) { cacheKey_add(enabledExtensions.getKey()); } return cacheKey || ''; }, getCache: function(context) { var key = this.buildCacheKey(context); return this.cacheLookup[key] || (this.cacheLookup[key] = new Cache(this.cacheProvider, context, key)); }, clearCache: function() { raptor.forEachEntry(this.cacheLookup, function(key, cache) { cache.clear(); }); }, clearBundleMappingsCache: function() { raptor.forEachEntry(this.cacheLookup, function(key, cache) { logger.info("Removing bundle mappings for cache " + key); cache.removeAllBundleMappings(); }); }, getConfig: function() { return this.config; }, getWriter: function() { return this.writer || this.createWriter(); }, createWriter: function(context) { var Writer = this.config.getWriter(); if (Writer) { if (typeof Writer === 'string') { Writer = require(Writer); } else if (typeof Writer !== 'function') { return Writer; } } else { Writer = OptimizerFileWriter; } var writer = new Writer(this); this.writer = writer; return writer; }, applyFilters: function(code, contentType, context) { var deferred = promises.defer(); var filters = this.getConfig().getFilters(); if (!filters || filters.length === 0) { deferred.resolve(code); return deferred.promise; } var promiseChain = null; var errorHandled = false; function onError(e) { if (errorHandled) { return; } errorHandled = true; deferred.reject(e); } forEach(filters, function(filterFunc) { if (filterFunc.contentType && filterFunc.contentType !== contentType) { return; } function applyFilter(code) { try { var startTime = Date.now(); var output = filterFunc(code, contentType, context); if (output == null) { output = code; } var finishTime = function(code) { var elapsedTime = Date.now() - startTime; if (elapsedTime > 1000) { logger.debug('Filter (' + filterFunc._name + ') completed in ' + (elapsedTime) + 'ms. Code:\n' + code); } }; if (promises.isPromise(output)) { output.then(finishTime); } else { finishTime(); var deferred = promises.defer(); deferred.resolve(output); output = deferred.promise; } return output; } catch(e) { onError(e); } } if (promiseChain) { promiseChain = promiseChain.then( applyFilter, onError); } else { promiseChain = applyFilter(code); } }, this); if (!promiseChain) { deferred.resolve(code); return deferred.promise; } else { promiseChain.then( function(code) { deferred.resolve(code); }, onError); promiseChain.fail(onError); } return deferred.promise; }, getJavaScriptDependencyHtml: function(url) { return '<script type="text/javascript" src="' + escapeXmlAttr(url) + '"></script>'; }, getCSSDependencyHtml: function(url) { return '<link rel="stylesheet" type="text/css" href="' + escapeXmlAttr(url) + '">'; }, calculateChecksum: function(code, restrictLength) { var shasum = crypto.createHash('sha1'); shasum.update(code); var checksum = shasum.digest('hex'), checksumLength = this.config.getChecksumLength(); if (restrictLength !== false && checksumLength > 0 && checksum.length > checksumLength) { checksum = checksum.substring(0, checksumLength); } return checksum; }, readDependency: function(dependency, context) { var deferred = promises.defer(); function onError(e) { deferred.reject(e); } var contentType = dependency.getContentType(); var filterContext = context ? raptor.extend({}, context) : {}; filterContext.contentType = contentType; filterContext.dependency = dependency; var _this = this; function applyFilters(code) { var promise = _this.applyFilters(code, contentType, filterContext); promise.then( function(code) { deferred.resolve(code); }, onError); } try { var code = dependency.getCode(context); if (promises.isPromise(code)) { var codePromise = code; codePromise.then(applyFilters, onError); } else { applyFilters(code); } } catch(e) { logger.error('Error getting code for dependency ' + dependency, e); onError(e); } return deferred.promise; }, readBundle: function(bundle, context, options) { var startTime = Date.now(); options = options || {}; var includeDependencies = options.includeDependencies === true; logger.debug("Reading bundle: " + bundle.getKey()); var dependencies = bundle.getDependencies(); var deferred = promises.defer(); var contentType = bundle.getContentType(); var _this = this; var outputPromise = deferred.promise; var checksumsEnabled = options.includeChecksums === true || bundle.checksumsEnabled; if (checksumsEnabled === undefined) { // checksumsEnabled not set for bundle so check optimizer config checksumsEnabled = (this.config.checksumsEnabled !== false) || bundle.requireChecksum; } checksumsEnabled = checksumsEnabled === true; var errorHandled = false; function onError(e) { if (errorHandled) { return; } errorHandled = true; logger.error('Unable to read bundle ' + bundle.getKey() + '. Exception: ' + (e.stack || e)); deferred.reject(e); } // Step 1 function filterCode() { var filteredCodeArray = new Array(dependencies.length); var pending = dependencies.length; var filterCodeDeferred = promises.defer(); var errorHandled = false; function filterCodeOnError(e) { if (errorHandled) { return; } errorHandled = true; filterCodeDeferred.reject(e); } dependencies.forEach(function(dependency, i) { // Each filter needs its own context since we update the context with the // current dependency and each dependency is filtered in parallel var filterContext = context ? raptor.extend({}, context) : {}; filterContext.bundle = bundle; filterContext.contentType = contentType; filterContext.dependency = dependency; var code = dependency.getCode(context); function applyFilters(code) { var promise = _this.applyFilters(code, contentType, filterContext); promise .then(function(code) { filteredCodeArray[i] = code; if (--pending === 0) { filterCodeDeferred.resolve(filteredCodeArray); } }) .fail(filterCodeOnError); } if (promises.isPromise(code)) { var codePromise = code; codePromise .then(applyFilters) .fail(filterCodeOnError); } else { applyFilters(code); } }); return filterCodeDeferred.promise; } // Step 2 function finish(filteredCodeArray) { var bundleCode = filteredCodeArray.join("\n"); var bundleChecksum = checksumsEnabled ? _this.calculateChecksum(bundleCode) : null; logger.info('Bundle ' + bundle.getKey() + ' read in ' + (Date.now() - startTime) + 'ms'); var dependencyInfoArray; if (includeDependencies) { dependencyInfoArray = new Array(dependencies.length); filteredCodeArray.forEach(function(filteredCode, i) { var dependencyChecksum = checksumsEnabled ? _this.calculateChecksum(filteredCode) : null; dependencyInfoArray[i] = { code: filteredCode, checksum: dependencyChecksum, dependency: dependencies[i] }; }); } deferred.resolve({ code: bundleCode, checksum: bundleChecksum, dependencies: dependencyInfoArray }); } try { filterCode() .then(finish) .fail(onError); } catch(e) { onError(e); } return deferred.promise; }, optimizePageCached: function(context, cacheKey, options) { if (!context) { context = {}; } var cache = this.getCache(context); var optimizedPage = cache.getOptimizedPage(cacheKey); var _this = this; var rebuildCacheTimeout = -1; var cacheTTL = -1; function handleRebuildCacheTimeout() { if (rebuildCacheTimeout !== -1) { logger.debug('Scheduling optimized page to be rebuilt in ' + rebuildCacheTimeout + 'ms'); setTimeout(function () { logger.debug('Rebuilding optimizer cache...'); try { _this.optimizePage(options) .then( cacheOptimizedPage, handleRebuildCacheTimeout); } catch(e) { logger.error("Error in handleRebuildCacheTimeout: ", e); } }, rebuildCacheTimeout); rebuildCacheTimeout = -1; } } function handleCacheTimeToLive() { if (cacheTTL !== -1) { evictOptimizedPageFromCache(cache, cacheKey, cacheTTL); cacheTTL = -1; } } function cacheOptimizedPage(optimizedPage) { handleRebuildCacheTimeout(); handleCacheTimeToLive(); } if (!optimizedPage) { if (typeof options === 'function') { options = options(); } context.setRebuildCacheTimeout = function(newCacheTimeout) { rebuildCacheTimeout = newCacheTimeout; }; context.setCacheTimeToLive = function(newTTL) { cacheTTL = newTTL; }; context.isOptimizedPageCached = function() { var cachedOptimizedPage = cache.getOptimizedPage(cacheKey); return cachedOptimizedPage && !promises.isPromise(cachedOptimizedPage); }; var optimizedPagePromise = this.optimizePage(options); optimizedPage = optimizedPagePromise; cache.addOptimizedPage(cacheKey, optimizedPagePromise); optimizedPagePromise .then(cacheOptimizedPage) .fail(function() { // Remove the failed promise so that we can try again next time cache.removeOptimizedPage(cacheKey); }); } return optimizedPage; }, optimizePage: function(options) { var startTime = new Date().getTime(); var deferred = promises.defer(); // This will be used to generate the returned promise // if we create a new context then make sure we put it // back into the options object for reference later var context = options.context || (options.context = {}); var config = this.getConfig(); if (options.basePath) { context.basePath = options.basePath; } var pluginContext = { context: context, config: config, options: options, pageOptimizer: this }; config.notifyPlugins('beforeOptimizePage', pluginContext); var writer = options.writer || this.getWriter(); writer.config = config; OptimizerWriterMixins.addMixins(writer); writer.setPageOptimizer(this); writer.setConfig(config); var optimizedPage = new OptimizedPage(); var slotTracker = new SlotTracker(); var _this = this; context.config = config; context.writer = writer; context.optimizer = this; if (!context.attributes) { context.attributes = {}; } var errorHandled = false; function onError(e) { if (errorHandled) { return; } errorHandled = true; deferred.reject(e); } if (config.bundleWrappers) { logger.info('Enabled bundle wrappers: ' + Object.keys(config.bundleWrappers).join(', ') + ' (these can be overridden at the bundle level)'); } else { logger.info('No bundle wrappers enabled (this can be overridden at the bundle level)'); } function buildLoaderMetadata(pageBundles) { var loaderMetadata = {}; if (pageBundles.hasAsyncRequires()) { pageBundles.forEachAsyncRequire(function(asyncRequire) { var entry = loaderMetadata[asyncRequire.getName()] = { requires: [], css: [], js: [] }; forEach(asyncRequire.getRequires(), function(require) { entry.requires.push(require); }); forEach(asyncRequire.getBundles(), function(bundle) { if (bundle.isJavaScript()) { entry.js.push(bundle.getUrl(context)); } else if (bundle.isStyleSheet()) { entry.css.push(bundle.getUrl(context)); } else { throw raptor.createError(new Error("Invalid bundle content type: " + bundle.getContentType())); } }); if (!entry.requires.length) { delete entry.requires; } if (!entry.js.length) { delete entry.js; } if (!entry.css.length) { delete entry.css; } }); } context.loaderMetadata = loaderMetadata; optimizedPage.setLoaderMetadata(loaderMetadata); } function registerBundle(bundle, async) { if (!async) { optimizedPage.addUrl(bundle.getUrl(context), bundle.getSlot(), bundle.getContentType()); if (bundle.outputFile) { optimizedPage.addFile(bundle.outputFile, bundle.getContentType()); } else if (bundle.sourceResource) { if (bundle.sourceResource.isFileResource()) { optimizedPage.addFile(bundle.sourceResource.getFilePath(), bundle.getContentType()); } } } else { // TODO: Should we track URLs and files for async-only bundles? } } function onBundleWritten(bundle) { registerBundle(bundle, false); } function onAsyncBundleWritten(bundle) { registerBundle(bundle, true); } function buildHtmlSlots(pageBundles) { pageBundles.forEachBundle(function(bundle) { var html, url; if (bundle.isInline() && !bundle.inPlaceDeployment) { if (bundle.isMergeInline()) { slotTracker.addContent(bundle.getSlot(), bundle.getContentType(), bundle.getCode(), true /* inline */); } else { slotTracker.addContentBlock(bundle.getSlot(), bundle.getContentType(), bundle.getCode()); } } else { url = bundle.getUrl(context); if (bundle.isJavaScript()) { html = _this.getJavaScriptDependencyHtml(url); } else if (bundle.isStyleSheet()) { html = _this.getCSSDependencyHtml(url); } else { throw raptor.createError(new Error("Invalid bundle content type: " + bundle.getContentType())); } slotTracker.addContent(bundle.getSlot(), bundle.getContentType(), html, (!bundle.inPlaceDeployment && bundle.isInline())); } }); optimizedPage.setHtmlBySlot(slotTracker.getHtmlBySlot()); } var pageBundlesPromise = this.buildPageBundles(options, context); pageBundlesPromise .then( function(pageBundles) { // First write out all of the async bundles writer.writeBundles(pageBundles.forEachAsyncBundleIter(), onAsyncBundleWritten, context) .then( function() { try { // Now that we have build the async bundles we can build the // loader metadata so that it can be added to page bundles buildLoaderMetadata(pageBundles); // Now write out all of the non-async bundles return writer.writeBundles(pageBundles.forEachBundleIter(), onBundleWritten, context); } catch(e) { onError(e); } }) .then( function() { // All of the bundles have now been persisted, now we can // generate all of the HTML for the page buildHtmlSlots(pageBundles); var pageName = options.name || options.pageName; logger.info('Optimized page "' + pageName + '" in ' + (Date.now() - startTime) + 'ms'); //All done! Resolve the promise with the optimized page deferred.resolve(optimizedPage); }) .fail(onError); }) .fail(onError); return deferred.promise; }, getBundleWrappers: function() { return this.bundleWrappers; }, isWrapperEnabledForBundle: function(wrapper, bundle) { var wrapperId = wrapper.id; if (wrapper.contentType !== bundle.getContentType()) { logger.debug('Bundle wrapper "' + wrapperId + '" with contentType "' + wrapper.contentType + '" does not match bundle content type of "' + bundle.getContentType() + '"'); return false; } // are there any wrappers explicitly configured for the bundle? var enabledWrappers = bundle.getWrappers(); if (bundle.wrappers === undefined) { // no wrappers set at the bundle level so check for which wrappers have been "globally" enabled enabledWrappers = this.config.bundleWrappers; } return (enabledWrappers && (enabledWrappers[wrapperId] === true)); }, resolveResourceUrlCached: function(path, baseResource, context) { var cache = this.getCache(context); var cacheKey = getResourceUrlCacheKey(path, baseResource); var resourceUrl = cache.getResourceUrl(cacheKey); if (resourceUrl) { return resourceUrl; } resourceUrl = this.resolveResourceUrl(path, baseResource, context); cache.addResourceUrl(cacheKey, resourceUrl); resourceUrl .fail(function() { // Remove the cached resource URL if the promise // is rejected in order to give the system // a chance to recover cache.removeResourceUrl(cacheKey); }); return resourceUrl; }, /** * Supported properties for the context argument: * <ul> * <li>inPlaceFromDir: Directory for calculating relative paths for in-place deployment (if enabled, optional) * <li>relativeFromDir: Directory for calculating relative path for final URL (optional) * </ul> * * @param {String} path The resource path to resolve to a URL * @param {raptor/resources/Resource} baseResource The base resource (required for relative resource paths) * @param {Object} context Additional contextual information * @return {Promise} The promise that will eventually resolve to the URL */ resolveResourceUrl: function(path, baseResource, context) { if (path.startsWith("http://") || path.startsWith("https://") || path.startsWith("//")) { return path; } context = context || {}; var inPlaceFromDir = context.inPlaceFromDir; var relativeFromDir = context.relativeFromDir; var writer = this.getWriter(); var config = this.getConfig(); var inputPath = path; var hashString = '', hashStart = path.indexOf('#'); if (hashStart != -1) { hashString = path.substring(hashStart); path = path.substring(0, hashStart); } var queryString = '', queryStart = path.indexOf('?'); if (queryStart != -1) { queryString = path.substring(queryStart); path = path.substring(0, queryStart); } var resource = resources.resolveResource(baseResource, path); if (resource && resource.isFileResource() && resource.exists()) { if (inPlaceFromDir && config.isInPlaceDeploymentEnabled()) { // This code block is for in-place deployment, but the CSS resource needs to be // compiled (such as LESS). While we can't do in-place deployment for the // LESS file (since it needs to be compiled), we can at least rewrite the // image URLs to point to the original location. Therefore, we need to modify // the relative path based on the fact that the final CSS file will be // in a new directory. path = writer.urlBuilder.getInPlaceResourceUrl( resource.getFilePath(), inPlaceFromDir); if (path) { path += queryString + hashString; logger.debug("Resolved URL: " + inputPath + ' --> ' + path); return q(path); } } var base64Encode = queryString === '?base64'; if (base64Encode && writer.base64EncodeSupported !== true) { // We only do the Base64 encoding if the writer prefers not // to do the Base64 encoding or does not support Base64 encoding path = 'data:' + mime.lookup(resource.getPath()) + ';base64,' + resource.readAsBinary().toString('base64'); return q(path); } else { context = raptor.extend({}, context); // Record that base 64 encoding was requested for this resource (this might be helpful to the writer) context.base64EncodeUrl = base64Encode; return writer.writeResource(resource, context) .then(function(outputFileInfo) { if (relativeFromDir && outputFileInfo.file) { // The resource was written to disk, we can calculate // a relative path from the optimizer output directory (where this CSS file will reside) // to the output image/resource path = nodePath.relative( relativeFromDir, outputFileInfo.file.getAbsolutePath()) + queryString + hashString; } else if (outputFileInfo.url) { path = outputFileInfo.url; if (path.startsWith("data:") === false) { path = path + queryString + hashString; } } else { throw new Error('Invalid output from "writer.writeResource": ' + require('util').inspect(outputFileInfo)); } logger.debug("Resolved URL: ", inputPath, ' --> ', path); return path; }); } } path = inputPath; logger.debug("Resolved URL: ", inputPath, ' --> ', path); return q(path); } }; return PageOptimizer; });