UNPKG

mendel-middleware

Version:

Mendel middleware that uses manifests to output multilayer resolution of isomorphic applications.

337 lines (295 loc) 10 kB
/* Copyright 2015, Yahoo Inc. Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms. */ var pathToRegexp = require('path-to-regexp'); var bpack = require('browser-pack'); var MendelTrees = require('mendel-core'); var MendelLoader = require('mendel-loader'); var debug = require('debug')('mendel:middleware'); module.exports = MendelMiddleware; function MendelMiddleware(opts) { var trees = MendelTrees(opts); var route = trees.config.routeConfig.hash || '/mendel/:hash/:bundle.js'; var getPath = pathToRegexp.compile(route); var keys = []; var bundleRoute = pathToRegexp(route, keys); var bundles = trees.config.bundles.reduce(function (acc, bundle) { acc[bundle.id] = bundle; return acc; }, {}); var loader = new MendelLoader(trees, { parentModule: module.parent, }); return function (req, res, next) { req.mendel = req.mendel || { bundleCache: {}, variations: false, }; req.mendel.getBundleEntries = function (bundleId) { const bundleDeps = trees.bundles[bundleId].bundles; return bundleDeps .filter((dep) => !!dep.expose || !!dep.entry) .map((dep) => dep.expose || dep.id); }; req.mendel.setVariations = function (variations) { if (req.mendel.variations === false) { var varsAndChains = trees.variationsAndChains(variations); req.mendel.variations = varsAndChains.matchingVariations; req.mendel.lookupChains = varsAndChains.lookupChains; } return req.mendel.variations; }; req.mendel.getBundle = function (bundle) { if (!req.mendel.variations) { throw new Error('Please call req.mendel.setVariations first'); } if (!req.mendel.bundleCache[bundle]) { var tree = trees.findTreeForVariations( bundle, req.mendel.lookupChains ); req.mendel.bundleCache[bundle] = tree; } return req.mendel.bundleCache[bundle]; }; req.mendel.getURL = function (bundle, variations) { if (!req.mendel.variations && variations) { console.warn( '[DEPRECATED] Please replace use of ' + 'mendel.getURL(bundle, variations).' + '\nUse mendel.setVariations(variations) followed by' + ' mendel.getURL(bundle) instead.' ); req.mendel.setVariations(variations); } var tree = req.mendel.getBundle(bundle); return getPath({ bundle: bundle, hash: tree.hash }); }; req.mendel.resolver = function (bundles, variations) { if (!req.mendel.variations && variations) { console.warn( '[DEPRECATED] Please replace use of ' + 'mendel.resolver(bundle, variations).' + '\nUse mendel.setVariations(variations) followed by' + ' mendel.resolver(bundle) instead.' ); req.mendel.setVariations(variations); } return loader.resolver(bundles, req.mendel.lookupChains); }; req.mendel.isSsrReady = loader.isSsrReady.bind(loader); // Match bundle route var reqParams = bundleRoute.exec(req.url); if (!reqParams) { return next(); } var params = namedParams(keys, reqParams); if (!(params.bundle && params.hash && bundles[params.bundle])) { return next(); } var decodedResults = trees.findTreeForHash(params.bundle, params.hash); if (!decodedResults || decodedResults.error) { return bundleError( res, 404, decodedResults && decodedResults.error, params ); } var bundle = bundles[params.bundle]; if (bundle.options && bundle.options.serveAs === 'css') { res.set({ 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'public, max-age=31536000', }); decodedResults.deps .sort((a, b) => a.order - b.order) .forEach(function (dep) { res.write(dep.source + '\n'); }); res.end(); } else { var pack = bpack({ raw: true, hasExports: true }); var modules = indexedDeps(decodedResults.deps.filter(Boolean)); if (!modules.length) { // Something wrong, modules shouldn't be zero return bundleError( res, 500, { code: 'EMPTYBUNDLE', message: 'Tree contents are empty', }, params ); } // Serve bundle res.set({ 'Content-Type': 'application/javascript', 'Cache-Control': 'public, max-age=31536000', }); pack.pipe(res); for (var i = 0; i < modules.length; i++) { pack.write(modules[i]); } pack.end(); } }; } /* Here is a piece of hard to read JavaScript. This compresses the bundle by renaming all dependency indexes from file paths to a numbered index. Here is a sample transformation: [ { "entry": true, "id": "/User/me/projects/site/src/main.js", "deps": { "./colors.js": "/User/me/projects/site/src/colors.js", "./shared.js": "/User/me/projects/site/src/shared.js" } }, { "id": "/User/me/projects/site/src/colors.js", "deps": { "external-lib": false } }, { "expose": "shared", "id": "/User/me/projects/site/src/shared.js", "deps": {} } ] Should become: [ { "entry": true, "id": 1, "deps": { "./colors.js": 2, "./shared.js": "shared" } }, { "id": 2, "deps": { "external-lib": false } }, { "expose": "shared", "id": "shared", "deps": {} } ] */ function indexedDeps(mods) { // the index can't be ever 0 because 0 is false for browserify var newModIndex = [0]; // indexes are created first, because deps can come unordered mods.forEach(function (mod) { if (!mod.expose) newModIndex.push(mod.id); }); // create a new array of modified modules return mods.map(function (oldMod) { return Object.keys(oldMod).reduce(function (newMod, prop) { if (prop === 'deps') { // deps needs to be reindexed newMod.deps = Object.keys(oldMod.deps).reduce(function ( newDeps, name ) { var id = oldMod.deps[name]; var index = newModIndex.indexOf(id); if (index > -1) { newDeps[name] = index; } else { // deps not indexed are exposed or external newDeps[name] = id; } return newDeps; }, {}); } else if (prop === 'id') { // id needs to be reindexed var index = newModIndex.indexOf(oldMod.id); if (index > -1) { newMod.id = index; } else { // unless it is entry or exposed newMod.id = oldMod.expose || oldMod.id; } } else { // for all other props we just copy over newMod[prop] = oldMod[prop]; } return newMod; }, {}); }); } /****** Here is a non-functional but more performant implementation of the same transformation above. I don't think it would pay off in performance, but I am keeping it here since I didn't benchmark. The above should be more maintainable. function indexedDeps(mods) { var i, key, deps, index, newId, newDeps, newMod, map, newMods; // indexes can't start with 0 because 0 is false for browserify runtime map = ['']; // stores indexes for (i = 0; i < mods.length; i++) { if (mods[i].expose) continue; map.push(mods[i].id); } // create new mods newMods = []; for (i = 0; i < mods.length; i++) { // shallow copy original deps newMod = {}; for(key in mods[i]) { newMod[key] = mods[i][key]; } // create new deps deps = mods[i].deps; newDeps = {}; for(key in deps) { index = map.indexOf(deps[key]); if (index > -1) { newDeps[key] = index; } else { newDeps[key] = deps[key]; } } // replace props on new mod newId = map.indexOf(newMod.id); if (newId > -1) newMod.id = newId; newMod.deps = newDeps; newMods.push(newMod); } return newMods; } ****/ function bundleError(res, statusCode, error, params) { var message = 'Mendel: '; if (!error) { message += statusCode === 404 ? 'Bundle not found' : 'Unable to create Bundle'; } else { message += error.code + ' - ' + error.message; } debug( [ error ? error.code : 'UNKNOWN', 'hash=' + params.hash, 'bundle=' + params.bundle, ].join(' ') ); res.status(statusCode).send(message); } function namedParams(keys, reqParams) { return keys.reduce(function (params, param, index) { params[param.name] = reqParams[index + 1]; return params; }, {}); }