├── src ├── cli │ ├── ilk-watch.js │ ├── ilk.js │ ├── ilk-build.js │ └── ilk-server.js ├── compile │ ├── construct │ │ ├── templates │ │ │ ├── iife.jst │ │ │ ├── module-set.jst │ │ │ ├── register-urls.jst │ │ │ ├── common-module.jst │ │ │ ├── .eslintrc │ │ │ └── runtime.jst │ │ └── index.js │ ├── modules │ │ ├── generate-id.js │ │ ├── generate-maps.js │ │ ├── get-seeds.js │ │ ├── update-references.js │ │ ├── parse.js │ │ ├── hash.js │ │ ├── resolve.js │ │ ├── transform.js │ │ ├── load.js │ │ ├── transform-amd.js │ │ └── compile.js │ ├── bundles │ │ ├── init.js │ │ ├── interpolate-filename.js │ │ ├── get-seeds.js │ │ ├── hash.js │ │ ├── dedupe-implicit.js │ │ ├── dedupe-explicit.js │ │ ├── generate.js │ │ └── generate-raw.js │ └── index.js ├── util │ ├── object.js │ ├── ast.js │ ├── file.js │ └── template.js ├── profiler.js ├── options │ ├── shared.js │ ├── server.js │ ├── index.js │ └── compile.js ├── optimizations │ └── file-cache │ │ └── index.js ├── server │ └── server.js ├── resolve.js └── index.js ├── .eslintignore ├── .babelrc ├── .npmignore ├── example ├── preset.js ├── app │ ├── shared │ │ ├── required-but-not-assigned.js │ │ ├── lib-c.js │ │ ├── lib-b.js │ │ └── lib-a.js │ ├── entry-b.js │ └── entry-a.js ├── package.json ├── example-plugin.js ├── ilk-config.js └── build.js ├── spec ├── .eslintrc ├── run.js ├── util.js └── src │ ├── resolve.spec.js │ ├── compile │ └── construct.spec.js │ └── index.spec.js ├── circle.yml ├── scripts ├── validate-docs.sh └── generate-docs.js ├── index.js ├── .gitignore ├── .eslintrc ├── LICENSE ├── README.md ├── package.json └── docs └── extensibility.md /src/cli/ilk-watch.js: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/ 2 | lib/ 3 | docs/ 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["nodejs-lts"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | example/ 3 | scripts/ 4 | spec/ 5 | src/ 6 | -------------------------------------------------------------------------------- /example/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pretty: false, 3 | includeComments: false 4 | }; 5 | -------------------------------------------------------------------------------- /example/app/shared/required-but-not-assigned.js: -------------------------------------------------------------------------------- 1 | module.exports = "this will never be used... :("; 2 | -------------------------------------------------------------------------------- /src/compile/construct/templates/iife.jst: -------------------------------------------------------------------------------- 1 | (function () { BODY; })(); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /example/app/shared/lib-c.js: -------------------------------------------------------------------------------- 1 | const jsxExpression =
some text
; 2 | export const thing = "C!"; 3 | -------------------------------------------------------------------------------- /src/compile/construct/templates/module-set.jst: -------------------------------------------------------------------------------- 1 | window[GLOBAL_NAME].load(MODULES_HASH); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /src/compile/construct/templates/register-urls.jst: -------------------------------------------------------------------------------- 1 | window[GLOBAL_NAME].registerUrls(URLS); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /example/app/shared/lib-b.js: -------------------------------------------------------------------------------- 1 | function thing () { 2 | return "this should be transformed into a function expr!"; 3 | } 4 | thing(); 5 | 6 | module.exports = "B!"; 7 | -------------------------------------------------------------------------------- /example/app/entry-b.js: -------------------------------------------------------------------------------- 1 | var libB = require("./shared/lib-b"); 2 | import libC from "./shared/lib-c"; 3 | 4 | console.log("entry-b:libB", libB); 5 | console.log("entry-b:libC", libC.thing); 6 | -------------------------------------------------------------------------------- /example/app/entry-a.js: -------------------------------------------------------------------------------- 1 | var libA = require("./shared/lib-a"); 2 | var libB = require("./shared/lib-b"); 3 | require("lodash"); 4 | 5 | console.log("entry-a:libA", libA); 6 | console.log("entra-a:libB", libB); 7 | -------------------------------------------------------------------------------- /src/compile/construct/templates/common-module.jst: -------------------------------------------------------------------------------- 1 | ({ 2 | deps: DEPS, // eslint-disable-line no-undef 3 | fn: function (require, module, exports) { MODULE_BODY; } // eslint-disable-line no-undef 4 | }); 5 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - "../.eslintrc" 3 | 4 | env: 5 | mocha: true 6 | 7 | globals: 8 | sinon: true 9 | assert: false 10 | expect: true 11 | 12 | rules: 13 | max-nested-callbacks: "off" 14 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: v6.3.0 4 | 5 | test: 6 | override: 7 | - npm run check 8 | 9 | notify: 10 | webhooks: 11 | - url: https://webhooks.gitter.im/e/ec8fbe5ef76183d4a766 12 | -------------------------------------------------------------------------------- /scripts/validate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run generate-docs || exit 1 3 | 4 | if [ -n "$(git status --short)" ]; then 5 | echo -e "\033[0;31m[error]\033[0m Changes found in auto-generated docs. Run 'npm run generate-docs', commit, and try again." 6 | exit 1 7 | else 8 | exit 0 9 | fi -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interlock-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "clean": "rm -fr ./dist/*.js ./dist/*.map", 7 | "build": "node build.js" 8 | }, 9 | "author": "Dale Bustad ", 10 | "license": "MIT", 11 | "dependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /example/app/shared/lib-a.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "./lib-b", 3 | "./required-but-not-assigned" 4 | ], function (b) { 5 | return "A! " + b; 6 | }); 7 | 8 | /* 9 | Expected output: 10 | module.exports = (function () { 11 | var b = require("hash-for-lib-b"); 12 | require("hash-for-required-but-not-assigned"); 13 | return "A! " + b; 14 | })(); 15 | */ 16 | -------------------------------------------------------------------------------- /src/compile/construct/templates/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - "formidable/configurations/es5-browser" 3 | 4 | rules: 5 | no-unused-expressions: "off" 6 | no-extra-parens: "off" 7 | no-unused-vars: "off" 8 | max-len: "off" 9 | valid-jsdoc: "off" 10 | no-return-assign: "off" 11 | func-style: "off" 12 | no-magic-numbers: "off" 13 | space-before-function-paren: 14 | - 2 15 | - anonymous: "always" 16 | named: "always" 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var useTranspiled = true; // eslint-disable-line no-var 2 | 3 | try { 4 | require.resolve("./lib"); 5 | } catch (e) { 6 | useTranspiled = false; 7 | } 8 | 9 | /* eslint-disable global-require */ 10 | if (useTranspiled) { 11 | require("babel-polyfill"); 12 | module.exports = require("./lib"); 13 | } else { 14 | require("babel-core/register"); 15 | module.exports = require("./src"); 16 | } 17 | /* eslint-enable global-require */ 18 | -------------------------------------------------------------------------------- /example/example-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function (babel) { 2 | var t = babel.types; 3 | return { 4 | visitor: { 5 | FunctionDeclaration: function (path) { 6 | var id = path.node.id; 7 | path.node.type = "FunctionExpression"; 8 | path.node.id = null; 9 | 10 | path.replaceWith(t.variableDeclaration("var", [ 11 | t.variableDeclarator(id, path.node) 12 | ])); 13 | } 14 | } 15 | }; 16 | } -------------------------------------------------------------------------------- /spec/run.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require("babel-core/register"); 4 | 5 | const requireDir = require("require-dir"); 6 | const chai = require("chai"); 7 | const sinonChai = require("sinon-chai"); 8 | global.sinon = require("sinon"); 9 | 10 | chai.config.includeStack = true; 11 | chai.use(sinonChai); 12 | 13 | global.expect = chai.expect; 14 | global.AssertionError = chai.AssertionError; 15 | global.Assertion = chai.Assertion; 16 | global.assert = chai.assert; 17 | 18 | requireDir("./src", { recurse: true }); 19 | -------------------------------------------------------------------------------- /example/ilk-config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | srcRoot: __dirname, 5 | destRoot: path.join(__dirname, "dist"), 6 | 7 | entry: { 8 | "./app/entry-a.js": "entry-a.bundle.js", 9 | "./app/entry-b.js": { dest: "entry-b.bundle.js" } 10 | }, 11 | split: { 12 | "./app/shared/lib-a.js": "[setHash].js" 13 | }, 14 | 15 | includeComments: true, 16 | sourceMaps: true, 17 | 18 | plugins: [], 19 | 20 | presets: [require("./preset")], 21 | 22 | babelConfig: { 23 | plugins: [require.resolve("./example-plugin")] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /example/build.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | var Interlock = require(".."); 4 | 5 | var ilk = new Interlock(require("./ilk-config")); 6 | 7 | 8 | // ilk.watch(function (buildEvent) { 9 | // var patchModules = buildEvent.patchModules; 10 | // var compilation = buildEvent.compilation; 11 | 12 | // if (patchModules) { 13 | // const paths = patchModules.map(function (module) { return module.path; }); 14 | // console.log("the following modules have been updated:", paths); 15 | // } 16 | // if (compilation) { 17 | // console.log("a new compilation has completed"); 18 | // } 19 | // }, { save: true }); 20 | 21 | ilk.build(); 22 | -------------------------------------------------------------------------------- /src/compile/modules/generate-id.js: -------------------------------------------------------------------------------- 1 | import { assign } from "lodash"; 2 | 3 | import { pluggable } from "pluggable"; 4 | 5 | /** 6 | * Given a mostly-compiled module, generate an ID for that module 7 | * and resolve the same module with an `id` property. 8 | * 9 | * @param {Object} module Module that needs an ID. 10 | * 11 | * @return {Object} Module that now has an `id` property. 12 | */ 13 | export default pluggable(function generateModuleId (module) { 14 | if (!module.hash) { 15 | throw new Error(`Cannot generate module ID for module at path: ${module.path}`); 16 | } 17 | 18 | return assign({}, module, { 19 | id: module.hash 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/util/object.js: -------------------------------------------------------------------------------- 1 | import { assign, isArray } from "lodash"; 2 | 3 | 4 | export function* entries (obj) { 5 | for (const key of Object.keys(obj)) { 6 | yield [key, obj[key]]; 7 | } 8 | } 9 | 10 | function _deepAssign (obj, keyPath, newVal) { 11 | const key = keyPath[0]; 12 | const modified = assign({}, obj); 13 | if (keyPath.length === 1) { 14 | modified[key] = newVal; 15 | } else { 16 | modified[key] = _deepAssign(obj[key], keyPath.slice(1), newVal); 17 | } 18 | return modified; 19 | } 20 | 21 | export function deepAssign (obj, keyPath, newVal) { 22 | keyPath = isArray(keyPath) ? keyPath : keyPath.split("."); 23 | return _deepAssign(obj, keyPath, newVal); 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -------------------- 2 | # OSX Files 3 | # -------------------- 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | Icon 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | 12 | # -------------------- 13 | # Sublime Text Files 14 | # -------------------- 15 | *.sublime-project 16 | *.sublime-workspace 17 | 18 | # -------------------- 19 | # IntelliJ Files 20 | # -------------------- 21 | *.iml 22 | *.ipr 23 | *.iws 24 | .idea/ 25 | out/ 26 | 27 | # -------------------- 28 | # Eclipse Files 29 | # -------------------- 30 | .project 31 | .metadata 32 | *.bak 33 | .classpath 34 | .settings/ 35 | 36 | # -------------------- 37 | # App Files 38 | # -------------------- 39 | npm-debug.log 40 | node_modules/ 41 | dist/ 42 | lib/ 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "babel-eslint" 3 | 4 | extends: 5 | - "formidable/configurations/es6-node" 6 | 7 | parserOptions: 8 | ecmaVersion: 6 9 | sourceType: "module" 10 | 11 | rules: 12 | no-invalid-this: "off" 13 | func-style: "off" 14 | arrow-parens: "off" 15 | consistent-return: "off" 16 | space-before-function-paren: 17 | - "error" 18 | - anonymous: "always" 19 | named: "always" 20 | generator-star-spacing: 21 | - "error" 22 | - before: false 23 | after: true 24 | no-magic-numbers: "off" 25 | max-params: 26 | - "warn" 27 | - 5 28 | no-return-assign: "off" 29 | quotes: 30 | - "error" 31 | - "double" 32 | - "avoid-escape" 33 | no-unused-expressions: "off" 34 | prefer-arrow-callback: "off" 35 | -------------------------------------------------------------------------------- /src/cli/ilk.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from "yargs"; 4 | 5 | import * as options from "../options"; 6 | 7 | 8 | const y = yargs 9 | .strict() 10 | .usage(`Usage: $0 [options] 11 | 12 | This is where additional information will go.`); 13 | 14 | options.buildArgs(y, options.shared) 15 | .command( 16 | "build", 17 | "Transform source files to destination JS.", 18 | require("./ilk-build") 19 | ) 20 | .command( 21 | "server", 22 | "Run a development server with live-updated assets.", 23 | require("./ilk-server") 24 | ) 25 | 26 | .demand(1, "You must specify an Interlock command.") 27 | 28 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk.") 29 | .help() 30 | .parse(process.argv); 31 | -------------------------------------------------------------------------------- /src/compile/bundles/init.js: -------------------------------------------------------------------------------- 1 | import { pluggable } from "pluggable"; 2 | 3 | 4 | export default pluggable(function initBundle (opts = {}) { 5 | const { 6 | dest = this.opts.implicitBundleDest, 7 | module, 8 | moduleHashes = [], 9 | modules = [], 10 | isEntryPt = false, 11 | type = "javascript", 12 | excludeRuntime = false 13 | } = opts; 14 | const includeRuntime = isEntryPt && !excludeRuntime; 15 | 16 | if (type !== "javascript") { 17 | throw new Error("Cannot create JS bundle for non-JavaScript module. " + 18 | "Please configure appropriate plugin."); 19 | } 20 | 21 | return { 22 | module, 23 | moduleHashes, 24 | modules, 25 | dest, 26 | type, 27 | isEntryPt, 28 | includeRuntime 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /src/compile/modules/generate-maps.js: -------------------------------------------------------------------------------- 1 | import { reduce } from "lodash"; 2 | 3 | import { pluggable } from "pluggable"; 4 | 5 | 6 | /** 7 | * Given a set of fully compiled modules, generate and return two 8 | * hashmaps of those modules, indexed by their hash and their 9 | * absolute path. 10 | * 11 | * @param {Array} modules Fully compiles modules. 12 | * 13 | * @return {Object} Fully compiled modules, indexed by hash and 14 | * absolute path. 15 | */ 16 | export default pluggable(function generateModuleMaps (modules) { 17 | return reduce(modules, (moduleMaps, module) => { 18 | moduleMaps.byHash[module.hash] = module; 19 | moduleMaps.byAbsPath[module.path] = module; 20 | return moduleMaps; 21 | }, { 22 | byHash: {}, 23 | byAbsPath: {} 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/compile/bundles/interpolate-filename.js: -------------------------------------------------------------------------------- 1 | import { assign } from "lodash"; 2 | import { pluggable } from "pluggable"; 3 | 4 | 5 | /** 6 | * Given a bundle, determine its ultimate output filepath by replacing 7 | * supported placeholders with their dynamic equivalents. 8 | * 9 | * @param {Object} bundle Late-stage bundle object. 10 | * 11 | * @return {Object} Bundle with interpolated `dest` property. 12 | */ 13 | export default pluggable(function interpolateFilename (bundle) { 14 | let dest = bundle.dest 15 | .replace("[setHash]", bundle.setHash) 16 | .replace("[hash]", bundle.hash); 17 | if (bundle.module) { 18 | dest = dest.replace("[primaryModuleHash]", bundle.module.hash); 19 | dest = dest.replace("[primaryModuleId]", bundle.module.id); 20 | } 21 | 22 | return assign({}, bundle, { dest }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/compile/modules/get-seeds.js: -------------------------------------------------------------------------------- 1 | import { keys, fromPairs } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import resolveModule from "./resolve"; 6 | 7 | 8 | /** 9 | * Inspect the compilation options for bundle definitions (provided as 10 | * key/value pairs to options.entry and options.split), resolve references, 11 | * and return an object of the early-stage modules indexed by their path 12 | * relative to the compilation context. 13 | * 14 | * @return {Object} Early-stage modules indexed by relative path. 15 | */ 16 | export default pluggable(function getModuleSeeds () { 17 | return Promise.all( 18 | [].concat(keys(this.opts.entry), keys(this.opts.split)) 19 | .map(relPath => this.resolveModule(relPath) 20 | .then(module => [relPath, module]) 21 | )) 22 | .then(fromPairs); 23 | }, { resolveModule }); 24 | -------------------------------------------------------------------------------- /src/cli/ilk-build.js: -------------------------------------------------------------------------------- 1 | import Interlock from ".."; 2 | import * as options from "../options"; 3 | import { assign } from "lodash"; 4 | 5 | 6 | export const builder = yargs => { 7 | return options 8 | .buildArgs(yargs, options.compile) 9 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk-build."); 10 | }; 11 | 12 | export const handler = argv => { 13 | const config = argv.config ? options.loadConfig(argv.config) : {}; 14 | const logger = options.getLogger(argv.verbose); 15 | 16 | const compileOpts = options.getInterlockOpts( 17 | argv, 18 | options.compile, 19 | config 20 | ); 21 | const sharedOpts = options.getInterlockOpts( 22 | argv, 23 | options.shared, 24 | config 25 | ); 26 | 27 | const opts = assign({}, sharedOpts, compileOpts); 28 | 29 | let ilk; 30 | try { 31 | ilk = new Interlock(opts); 32 | ilk.build(); 33 | } catch (err) { 34 | logger.warn(err.stack) || logger.error("Error:", err.message); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /scripts/generate-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { join } from "path"; 3 | import generateDocs from "pluggable/lib/generate-docs"; 4 | 5 | const PREAMBLE = `# Extensibility 6 | 7 | Bacon ipsum dolor amet jerky jowl meatloaf ribeye beef. Doner chicken bacon tongue picanha 8 | landjaeger pork chop brisket leberkas fatback ball tip meatball corned beef. Drumstick turkey 9 | salami fatback ham hock venison tenderloin pork chop short ribs t-bone beef ribs hamburger 10 | shankle. 11 | 12 | Chuck pastrami bresaola salami, pork flank porchetta ground round filet mignon tongue corned 13 | beef. Pork belly spare ribs kielbasa chicken ribeye turducken, jerky pig doner flank. 14 | 15 | Hamburger tail landjaeger ball tip, porchetta fatback drumstick kielbasa shankle frankfurter. 16 | 17 | Something about Pluggable.CONTINUE... 18 | 19 | `; 20 | 21 | generateDocs({ 22 | rootPath: join(__dirname, ".."), 23 | outputPath: "docs/extensibility.md", 24 | jsonOutputPath: "docs/compilation.json", 25 | sources: "src/**/*.js", 26 | preamble: PREAMBLE 27 | }); 28 | -------------------------------------------------------------------------------- /src/util/ast.js: -------------------------------------------------------------------------------- 1 | import { isArray, isObject, isFunction, isNumber, isString } from "lodash"; 2 | import * as t from "babel-types"; 3 | import { parse } from "babylon"; 4 | 5 | 6 | function fromVal (val) { 7 | if (isArray(val)) { 8 | return fromArray(val); // eslint-disable-line no-use-before-define 9 | } else if (isObject(val)) { 10 | return fromObject(val); // eslint-disable-line no-use-before-define 11 | } else if (isFunction(val)) { 12 | return fromFunction(val); // eslint-disable-line no-use-before-define 13 | } else if (isNumber(val)) { 14 | return t.numericLiteral(val); 15 | } else if (isString(val)) { 16 | return t.stringLiteral(val); 17 | } 18 | throw new Error("Cannot transform value into AST.", val); 19 | } 20 | 21 | export function fromObject (obj) { 22 | return t.objectExpression(Object.keys(obj).map(key => 23 | t.objectProperty(t.stringLiteral(key), fromVal(obj[key])) 24 | )); 25 | } 26 | 27 | export function fromArray (arr) { 28 | return t.arrayExpression(arr.map(fromVal)); 29 | } 30 | 31 | export function fromFunction (fn) { 32 | return parse(`(${fn.toString()})`).body[0].expression; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dale Bustad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # interlock 2 | 3 | npm version 4 | 5 | *** 6 | 7 | For information about Interlock and how it works, check out our [website](http://interlockjs.com). To learn how to extend Interlock through plugins, we recommend you [read the docs](http://interlockjs.com/docs/extensibility). 8 | 9 | ## License 10 | 11 | [MIT License](http://opensource.org/licenses/MIT) 12 | -------------------------------------------------------------------------------- /spec/util.js: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import traverse from "babel-traverse"; 3 | 4 | const QUERY_FORMAT = /(([A-Za-z]+)?)((\[[A-Za-z\.]+=[A-Za-z0-9+-=_\.]+\])*)/; 5 | const NUMBER_FORMAT = /[0-9\.]+/; 6 | const NOT_FOUND = Symbol(); 7 | 8 | export function query (ast, queryStr) { 9 | const queryMatch = QUERY_FORMAT.exec(queryStr); 10 | if (!queryMatch) { return null; } 11 | const [ , nodeType, , keyValsStr ] = queryMatch; 12 | const keyVals = !keyValsStr ? 13 | [] : 14 | keyValsStr 15 | .slice(1, keyValsStr.length - 1) 16 | .split("][") 17 | .map(keyValPair => keyValPair.split("=")) 18 | .map(([keypath, val]) => ({ 19 | keypath, 20 | val, 21 | altVal: NUMBER_FORMAT.test(val) ? Number(val) : undefined 22 | })); 23 | 24 | const matches = []; 25 | traverse.cheap(ast, node => { 26 | if (!nodeType || node.type === nodeType) { 27 | const keyValsMatch = keyVals.reduce((isMatch, { keypath, val, altVal }) => { 28 | const valAtPath = get(node, keypath, NOT_FOUND); 29 | return isMatch && valAtPath === val || valAtPath === altVal; 30 | }, true); 31 | if (keyValsMatch) { matches.push(node); } 32 | } 33 | }); 34 | return matches; 35 | } 36 | -------------------------------------------------------------------------------- /src/compile/modules/update-references.js: -------------------------------------------------------------------------------- 1 | import traverse from "babel-traverse"; 2 | 3 | import { pluggable } from "pluggable"; 4 | import { assign } from "lodash"; 5 | 6 | 7 | /** 8 | * Given a module whose dependencies have been identified and compiled, 9 | * replace all original references with run-time references. In the case 10 | * of JavaScript, this will mean updating references like `path/to/dep` 11 | * or `./sibling-dep` with each dependency's module ID. 12 | * 13 | * @param {Object} module Module with AST containing original require expressions. 14 | * 15 | * @return {Object} Module with AST containing require expressions whose 16 | * arguments have been replaced with corresponding dependency 17 | * module hashes. 18 | */ 19 | export default pluggable(function updateReferences (module) { 20 | traverse.cheap(module.ast, node => { 21 | if ( 22 | node.type === "CallExpression" && 23 | node.callee.name === "require" 24 | ) { 25 | const originalVal = node.arguments[0].value; 26 | const correspondingModule = module.dependenciesByInternalRef[originalVal]; 27 | node.arguments[0].value = correspondingModule.hash; 28 | node.arguments[0].raw = `"${correspondingModule.hash}"`; 29 | } 30 | }); 31 | 32 | return assign({}, module, { ast: module.ast }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/compile/bundles/get-seeds.js: -------------------------------------------------------------------------------- 1 | import { assign, map } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import initBundle from "./init"; 6 | 7 | 8 | /** 9 | * Given the set of early-stage modules (originally generated from the bundle definitions) 10 | * and the set of fully compiled modules (indexed by their absolute path), return an array 11 | * of early-stage bundles. These bundles do not yet know about which modules they contain, 12 | * but do hold a reference to the root module of their branch of the dependency graph. 13 | * 14 | * @param {Object} moduleSeeds Early-stage modules, indexed by path relative to 15 | * the compilation context. 16 | * @param {Object} modulesByPath Fully compiled modules, indexed by absolute path. 17 | * 18 | * @return {Array} Early-stage bundles with `module` property. 19 | */ 20 | export default pluggable(function getBundleSeeds (moduleSeeds, modulesByPath) { 21 | return Promise.all([].concat( 22 | map(this.opts.entry, (bundleDef, relPath) => this.initBundle(assign({}, bundleDef, { 23 | module: modulesByPath[moduleSeeds[relPath].path], 24 | isEntryPt: true 25 | }))), 26 | map(this.opts.split, (bundleDef, relPath) => this.initBundle(assign({}, bundleDef, { 27 | module: modulesByPath[moduleSeeds[relPath].path], 28 | isEntryPt: false 29 | }))) 30 | )); 31 | }, { initBundle }); 32 | -------------------------------------------------------------------------------- /src/util/file.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { chain } from "lodash"; 5 | 6 | 7 | const WINDOWS = process.platform === "win32"; 8 | 9 | 10 | export function isFile (absPath) { 11 | let stat; 12 | try { stat = fs.statSync(absPath); } catch (e) { return false; } 13 | return stat.isFile() || stat.isFIFO(); 14 | } 15 | 16 | export function isDir (absPath) { 17 | let stat; 18 | try { stat = fs.statSync(absPath); } catch (e) { return false; } 19 | return stat.isDirectory(); 20 | } 21 | 22 | export function getPossiblePaths (deepPath, desiredFilename) { 23 | const prefix = WINDOWS && /^[A-Za-z]:[\/\\]/.test(deepPath) ? 24 | deepPath.slice(0, 3) : 25 | "/"; 26 | const nestedDirs = deepPath.split(WINDOWS ? /[\/\\]+/ : /\/+/); 27 | 28 | return chain(0) 29 | .range(nestedDirs.length + 1) 30 | .map(idx => { 31 | const pathPieces = nestedDirs.slice(0, nestedDirs.length - idx); 32 | return path.resolve(prefix, ...pathPieces, desiredFilename); 33 | }) 34 | .value(); 35 | } 36 | 37 | export function getPackageJson (filePath) { 38 | for (const pjPath of getPossiblePaths(filePath, "package.json")) { 39 | if (isFile(pjPath)) { 40 | try { 41 | return require(pjPath); // eslint-disable-line global-require 42 | } catch (err) { 43 | throw new Error(`Invalid package.json found at '${pjPath}'.`); 44 | } 45 | } 46 | } 47 | throw new Error(`Could not find package.json for '${filePath}'.`); 48 | } 49 | -------------------------------------------------------------------------------- /src/profiler.js: -------------------------------------------------------------------------------- 1 | export const PROFILER_ACTIVE = process.env.INTERLOCK_PROFILER === "true"; 2 | 3 | const invocations = []; 4 | 5 | function getStats () { 6 | const deduped = {}; 7 | invocations.forEach(({ name, sec, nsec }) => { 8 | if (!(name in deduped)) { 9 | deduped[name] = { total: 0, events: 0 }; 10 | } 11 | deduped[name].total += sec * 1000000000 + nsec; 12 | deduped[name].events += 1; 13 | }); 14 | 15 | const sorted = {}; 16 | Object.keys(deduped) 17 | .sort((fnA, fnB) => deduped[fnB].total - deduped[fnA].total) 18 | .map(name => sorted[name] = deduped[name]) 19 | .forEach(record => { 20 | record.avg = record.total / record.events; 21 | }); 22 | 23 | return sorted; 24 | } 25 | 26 | function printReport () { 27 | const stats = getStats(); 28 | Object.keys(stats).forEach(name => { 29 | const stat = stats[name]; 30 | const totalMs = (stat.total / 1000000).toFixed(3); 31 | const avg = stat.avg.toFixed(); 32 | const avgMs = (stat.avg / 1000000).toFixed(3); 33 | 34 | /* eslint-disable no-console */ 35 | console.log(` 36 | ${name}: 37 | number of events: ${stat.events} 38 | total time: ~${totalMs} ms (${stat.total} ns) 39 | average time: ${avgMs} ms (${avg} ns)`); 40 | /* eslint-enable no-console */ 41 | }); 42 | 43 | } 44 | 45 | if (PROFILER_ACTIVE) { 46 | process.on("exit", function (err) { 47 | if (err) { 48 | console.log(err); // eslint-disable-line no-console 49 | } 50 | printReport(); 51 | }); 52 | } 53 | 54 | export function createEvent (name) { 55 | const startTime = process.hrtime(); 56 | return function () { 57 | const [sec, nsec] = process.hrtime(startTime); 58 | invocations.push({ name, sec, nsec }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/compile/bundles/hash.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { assign } from "lodash"; 3 | 4 | import { pluggable } from "pluggable"; 5 | 6 | 7 | /** 8 | * Calculate the bundle's hash by invoking `update` with data from the bundle. 9 | * `update` should be called with string data only. 10 | * 11 | * @param {Function} update Updates the ongoing computation of bundle hash. 12 | * @param {Object} bundle The bundle object. 13 | */ 14 | const updateBundleHash = pluggable(function updateBundleHash (update, bundle) { 15 | update(JSON.stringify(bundle.moduleHashes), "utf-8"); 16 | update(JSON.stringify(!!bundle.entry), "utf-8"); 17 | update(JSON.stringify(!!bundle.includeRuntime), "utf-8"); 18 | }); 19 | 20 | /** 21 | * Given an otherwise prepared bundle, generate a hash for that bundle and resolve 22 | * to that same bundle with a new `hash` property. 23 | * 24 | * @param {Object} bundle Unhashed bundle. 25 | * 26 | * @returns {Object} Bundle plus new `hash` property, a 40-character SHA1 27 | * that uniquely identifies the bundle. 28 | */ 29 | function hashBundle (bundle) { 30 | // Node v0.10.x cannot re-use crypto instances after digest is called. 31 | bundle.setHash = crypto.createHash("sha1") 32 | .update(JSON.stringify(bundle.moduleHashes), "utf-8") 33 | .digest("hex"); 34 | 35 | const shasum = crypto.createHash("sha1"); 36 | const update = shasum.update.bind(shasum); 37 | 38 | return this.updateBundleHash(update, bundle) 39 | .then(() => assign({}, bundle, { 40 | hash: shasum.digest("base64") 41 | .replace(/\//g, "_") 42 | .replace(/\+/g, "-") 43 | .replace(/=+$/, "") 44 | })); 45 | } 46 | 47 | export default pluggable(hashBundle, { updateBundleHash }); 48 | -------------------------------------------------------------------------------- /src/compile/modules/parse.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import Promise from "bluebird"; 3 | 4 | import { parse } from "babylon"; 5 | import { assign } from "lodash"; 6 | 7 | import { pluggable } from "pluggable"; 8 | 9 | 10 | /** 11 | * Parse the source of the provided early-stage module. Resolves to the same 12 | * module object, with additional `ast` and `sourcePath` properties (or equivalent 13 | * for non-JavaScript modules). 14 | * 15 | * @param {Object} module Unparsed module with rawSource property. 16 | * 17 | * @return {Object} Parsed module with new `ast` and `sourcePath` properties. 18 | */ 19 | export default pluggable(function parseModule (module) { 20 | if (module.type !== "javascript") { 21 | throw new Error("Cannot parse non-JavaScript. Please configure appropriate plugin."); 22 | } 23 | 24 | try { 25 | const sourcePath = path.join(module.ns, module.nsPath); 26 | const ast = parse(module.rawSource, { 27 | sourceType: "module", 28 | sourceFilename: sourcePath, 29 | // See: https://github.com/babel/babel/tree/master/packages/babylon#plugins 30 | plugins: [ 31 | "jsx", 32 | "flow", 33 | "asyncFunctions", 34 | "classConstructorCall", 35 | "doExpressions", 36 | "trailingFunctionCommas", 37 | "objectRestSpread", 38 | "decorators", 39 | "classProperties", 40 | "exportExtensions", 41 | "exponentiationOperator", 42 | "asyncGenerators", 43 | "functionBind", 44 | "functionSent" 45 | ] 46 | }).program; 47 | 48 | return assign({}, module, { ast, sourcePath }); 49 | } catch (err) { 50 | return Promise.reject(`Unable to parse file: ${module.path}\n${err.stack}`); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/options/shared.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { isObject, isArray } from "lodash"; 4 | 5 | 6 | /* eslint-disable no-console */ 7 | export function getLogger (verbosity) { 8 | return { 9 | error: (...msgs) => console.error("ERROR:", ...msgs) || true, 10 | warn: verbosity >= 1 ? (...msgs) => console.warn("WARNING:", ...msgs) || true : () => false, 11 | info: verbosity >= 2 ? (...msgs) => console.info("INFO:", ...msgs) || true : () => false, 12 | debug: verbosity >= 3 ? (...msgs) => console.log("DEBUG:", ...msgs) || true : () => false 13 | }; 14 | } 15 | /* eslint-enable no-console */ 16 | 17 | export const shared = [{ 18 | key: "log", 19 | default: () => getLogger(1), 20 | schema: isObject, 21 | 22 | flagType: "count", 23 | flags: ["verbose", "v"], 24 | flagTransform: getLogger, 25 | cmdOpts: { global: true }, 26 | 27 | description: { 28 | short: "Compiler verbosity (sent to STDOUT).", 29 | full: "TODO" 30 | } 31 | }, { 32 | key: "config", 33 | 34 | flagType: "string", 35 | flags: ["config", "c"], 36 | cmdOpts: { global: true }, 37 | 38 | description: { 39 | short: "Path to Interlock config file.", 40 | full: "TODO" 41 | } 42 | }, { 43 | key: "presets", 44 | default: () => [], 45 | schema: presets => { 46 | return isArray(presets) && presets.reduce((isValid, preset) => { 47 | return isValid && isObject(preset); 48 | }, true); 49 | }, 50 | 51 | flags: ["preset"], 52 | flagType: "string", 53 | // eslint-disable-next-line global-require 54 | flagTransform: (val, cwd) => require(path.resolve(cwd, val)), 55 | cmdOpts: { global: true }, 56 | 57 | description: { 58 | short: "Pull in pre-determined Interlock configuration options.", 59 | full: "TODO" 60 | } 61 | }]; 62 | -------------------------------------------------------------------------------- /src/compile/modules/hash.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | import { assign } from "lodash"; 4 | 5 | import { pluggable } from "pluggable"; 6 | 7 | 8 | /** 9 | * Use data from the provided module to generate a hash, utilizing the provided 10 | * update function. Only string values should be passed to the update function. 11 | * The resulting hash should be deterministic for the same inputs in the same order. 12 | * 13 | * @param {Object} module Module that needs a hash property. 14 | * @param {Function} update Function to be invoked with data that uniquely 15 | * identifies the module (or, more precisely, the 16 | * run-time behavior of the module). 17 | */ 18 | const updateModuleHash = pluggable(function updateModuleHash (update, module) { 19 | const dependencyHashes = module.dependencies.map(dep => dep.hash); 20 | dependencyHashes.sort(); 21 | 22 | update(module.rawSource); 23 | update(module.ns); 24 | update(module.nsPath); 25 | dependencyHashes.forEach(update); 26 | }); 27 | 28 | /** 29 | * Given a mostly-compiled module, generate a hash for that module and resolve 30 | * to that module with a new `hash` property. 31 | * 32 | * @param {Object} module Module that needs to be hashed hash. 33 | * 34 | * @return {Object} Module that now has a hash property. 35 | */ 36 | export default pluggable(function hashModule (module) { 37 | if (module.hash) { return module; } 38 | 39 | const shasum = crypto.createHash("sha1"); 40 | const update = shasum.update.bind(shasum); 41 | 42 | return this.updateModuleHash(update, module) 43 | .then(() => shasum.digest("base64") 44 | .replace(/\//g, "_") 45 | .replace(/\+/g, "-") 46 | .replace(/=+$/, "")) 47 | .then(hash => assign({}, module, { hash })); 48 | }, { updateModuleHash }); 49 | -------------------------------------------------------------------------------- /src/options/server.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { 4 | isInteger, 5 | isString, 6 | isObject, 7 | isRegExp, 8 | isBoolean, 9 | chain 10 | } from "lodash"; 11 | 12 | 13 | export const server = [{ 14 | key: "port", 15 | default: () => 1337, 16 | schema: portNum => isInteger(portNum) && portNum > 0, 17 | 18 | flagType: "number", 19 | flags: ["port"], 20 | 21 | description: { 22 | short: "Port on which to run dev server.", 23 | full: "TODO" 24 | } 25 | }, { 26 | key: "retryTimeout", 27 | default: () => 3000, 28 | schema: timeout => isInteger(timeout) && timeout > 0, 29 | 30 | flagType: "number", 31 | flags: ["retry-timeout"], 32 | 33 | description: { 34 | short: "Delay before hot-reload clients attempt reconnection.", 35 | full: "TODO" 36 | } 37 | }, { 38 | key: "staticResources", 39 | default: () => {}, 40 | schema: resources => 41 | isObject(resources) && 42 | Object.keys(resources).reduce((allEntriesOkay, fullPath) => { 43 | return allEntriesOkay && isString(fullPath) && isRegExp(resources[fullPath]); 44 | }, true), 45 | 46 | flagType: "string", 47 | flags: ["mount"], 48 | flagTransform: (val, cwd) => chain(val) 49 | .chunk(2) 50 | .map(([urlPattern, localPath]) => { 51 | return [ 52 | path.resolve(cwd, localPath), 53 | new RegExp(`^${urlPattern}`) 54 | ]; 55 | }) 56 | .fromPairs() 57 | .value(), 58 | cmdOpts: { nargs: 2 }, 59 | 60 | description: { 61 | short: "Pairs of URL patterns and the local filepaths they should resolve to.", 62 | full: "TODO" 63 | } 64 | }, { 65 | key: "hot", 66 | default: () => false, 67 | schema: isBoolean, 68 | 69 | flagType: "boolean", 70 | flags: ["hot"], 71 | 72 | description: { 73 | short: "Enable hot-reloading of client modules.", 74 | long: "TODO" 75 | } 76 | }]; 77 | -------------------------------------------------------------------------------- /src/compile/modules/resolve.js: -------------------------------------------------------------------------------- 1 | import { pluggable } from "pluggable"; 2 | import resolve from "../../resolve"; 3 | 4 | 5 | /** 6 | * Transform the require string before it is resolved to a file on disk. 7 | * No transformations occur by default - the output is the same as the input. 8 | * 9 | * @param {String} requireStr Require string or comparable value. 10 | * 11 | * @return {String} Transformed require string. 12 | */ 13 | const preresolve = pluggable(function preresolve (requireStr) { 14 | return requireStr; 15 | }); 16 | 17 | /** 18 | * Given a require string and some context, resolve that require string 19 | * to a file on disk, returning a module seed. 20 | * 21 | * @param {String} requireStr Require string or comparable value. 22 | * @param {String} contextPath Absolute path from which to resolve any relative 23 | * paths. 24 | * @param {String} ns Namespace to set on module seed if the resolved 25 | * module is of the same namespace as its context. 26 | * @param {String} nsRoot Absolute path of default namespace. 27 | * @param {Array} extensions Array of file extension strings, including the leading 28 | * dot. 29 | * 30 | * @return {Object} Module seed. 31 | */ 32 | function resolveModule (requireStr, contextPath, ns, nsRoot, extensions) { 33 | contextPath = contextPath || this.opts.srcRoot; 34 | 35 | return this.preresolve(requireStr, contextPath) 36 | .then(requireStrToResolve => { 37 | const resolved = resolve( 38 | requireStrToResolve, 39 | contextPath || this.opts.srcRoot, 40 | ns || this.opts.ns, 41 | nsRoot || this.opts.srcRoot, 42 | extensions || this.opts.extensions 43 | ); 44 | 45 | if (!resolved) { 46 | throw new Error(`Cannot resolve ${requireStr} from ${contextPath}.`); 47 | } 48 | 49 | return resolved; 50 | }); 51 | } 52 | 53 | export default pluggable(resolveModule, { preresolve }); 54 | -------------------------------------------------------------------------------- /src/compile/modules/transform.js: -------------------------------------------------------------------------------- 1 | import { assign, uniq } from "lodash"; 2 | import { transformFromAst } from "babel-core"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import transformAmd from "./transform-amd"; 6 | 7 | 8 | /** 9 | * Transforms the module's AST, returning a module object with transformed 10 | * `ast` property as well as a new `synchronousRequires` property. If the 11 | * module is not of type "javascript", transformations to type-specific 12 | * intermediate representation should occur at this step. 13 | * 14 | * @param {Object} module Module object, with `ast` property. 15 | * 16 | * @return {Object} Module object with transformed `ast` property 17 | * and new `synchronousRequires` property. 18 | */ 19 | export default pluggable(function transformModule (module) { 20 | if (module.type !== "javascript") { 21 | throw new Error("Cannot transform non-JS module. Please activate appropriate plugin."); 22 | } 23 | const babelUserConfig = this.opts.babelConfig || {}; 24 | let synchronousRequires = []; 25 | 26 | const getRequires = { 27 | visitor: { 28 | CallExpression (path) { 29 | if (path.node.callee.name === "require") { 30 | if (path.node.arguments.length === 0) { 31 | throw new Error("Require expressions must include a target."); 32 | } 33 | synchronousRequires.push(path.node.arguments[0].value); 34 | } 35 | } 36 | } 37 | }; 38 | 39 | const config = assign({}, babelUserConfig, { 40 | filename: module.path, 41 | code: false, 42 | ast: true, 43 | plugins: [ 44 | ...(babelUserConfig.plugins || []), 45 | require.resolve("babel-plugin-transform-es2015-modules-commonjs"), 46 | transformAmd(), 47 | getRequires 48 | ] 49 | }); 50 | 51 | const { ast } = transformFromAst(module.ast, null, config); 52 | synchronousRequires = uniq(synchronousRequires); 53 | 54 | return assign({}, module, { 55 | synchronousRequires, 56 | ast: ast.program 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/cli/ilk-server.js: -------------------------------------------------------------------------------- 1 | import Interlock from ".."; 2 | import * as options from "../options"; 3 | import { createServer } from "../server/server"; 4 | import { assign, keys } from "lodash"; 5 | 6 | 7 | export const builder = yargs => { 8 | return options 9 | .buildArgs(yargs, options.server, options.compile) 10 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk-build."); 11 | }; 12 | 13 | export const handler = argv => { 14 | const config = argv.config ? options.loadConfig(argv.config) : {}; 15 | const logger = options.getLogger(argv.verbose); 16 | 17 | const compileOpts = options.getInterlockOpts( 18 | argv, 19 | options.compile, 20 | config 21 | ); 22 | const sharedOpts = options.getInterlockOpts( 23 | argv, 24 | options.shared, 25 | config 26 | ); 27 | const opts = assign({}, sharedOpts, compileOpts); 28 | 29 | let serverOpts = options.getInterlockOpts(argv, options.server); 30 | serverOpts = options.validate(serverOpts, options.server); 31 | 32 | const { 33 | setDynamicAssets, 34 | notify, 35 | pause 36 | } = createServer(serverOpts); 37 | 38 | try { 39 | const ilk = new Interlock(opts); 40 | 41 | let resume = pause(); 42 | ilk.watch(buildEvent => { 43 | const { change, patchModules, compilation } = buildEvent; 44 | 45 | if (change) { 46 | resume = pause(); 47 | notify("recompiling", { filename: buildEvent.change }); 48 | return; 49 | } 50 | 51 | if (patchModules) { 52 | notify("update", { update: true }); 53 | } else if (compilation) { 54 | const newAssets = keys(compilation.bundles).reduce((assets, filename) => { 55 | assets[`/${filename}`] = compilation.bundles[filename].raw; 56 | return assets; 57 | }, {}); 58 | setDynamicAssets(newAssets); 59 | resume(); 60 | notify("compilation", { compilation: true }); 61 | } 62 | }); 63 | } catch (err) { 64 | logger.warn(err.stack) || logger.error("Error:", err.message); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/compile/bundles/dedupe-implicit.js: -------------------------------------------------------------------------------- 1 | import { assign, difference, intersection, filter } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import initBundle from "./init"; 6 | 7 | 8 | const genBundlesWithImplicit = Promise.coroutine(function* (bundles) { 9 | bundles = bundles.slice(); 10 | 11 | for (let a = 0; a < bundles.length; a++) { 12 | const bundleA = bundles[a]; 13 | const bundleLengthAtIteration = bundles.length; 14 | 15 | for (let b = a + 1; b < bundleLengthAtIteration; b++) { 16 | const bundleB = bundles[b]; 17 | const commonHashes = intersection(bundleA.moduleHashes, bundleB.moduleHashes); 18 | 19 | if (commonHashes.length) { 20 | const moduleHashesA = difference(bundleA.moduleHashes, commonHashes); 21 | const moduleHashesB = difference(bundleB.moduleHashes, commonHashes); 22 | bundles[a] = assign({}, bundleA, { moduleHashes: moduleHashesA }); 23 | bundles[b] = assign({}, bundleB, { moduleHashes: moduleHashesB }); 24 | 25 | bundles.push(yield this.initBundle({ 26 | moduleHashes: commonHashes, 27 | type: bundles[a].type, 28 | excludeRuntime: true 29 | })); 30 | } 31 | } 32 | } 33 | 34 | return filter(bundles, bundle => bundle.moduleHashes.length); 35 | }); 36 | 37 | /** 38 | * Given an array of explicitly defined bundles, generate a new array of bundles 39 | * including new implicit bundles. These implicit bundles will be generated from 40 | * the intersections of two (or more) bundles' module hashes. 41 | * 42 | * This ensures that no module is included in more than one bundle. It further 43 | * ensures that any module that is depended upon by more than one bundle will be 44 | * split off into its own new bundle. 45 | * 46 | * @param {Array} explicitBundles Bundles with module and moduleHashes properties. 47 | * 48 | * @return {Array} Explicit bundles plus new implicit bundles. 49 | */ 50 | export default pluggable(function dedupeImplicit (explicitBundles) { 51 | return genBundlesWithImplicit.call(this, explicitBundles); 52 | }, { initBundle }); 53 | -------------------------------------------------------------------------------- /src/compile/bundles/dedupe-explicit.js: -------------------------------------------------------------------------------- 1 | import { assign, includes, difference } from "lodash"; 2 | 3 | import { pluggable } from "pluggable"; 4 | 5 | 6 | /** 7 | * First, update the bundle's `module` property to refer to the compiled 8 | * version of the module. Then generate a moduleHashes property for each 9 | * of the bundles, containing all hashes for all modules in the bundle's 10 | * dependency branch. 11 | * 12 | * Then, identify bundles that include the entry module from another bundle. 13 | * When found, remove all of the second module's bundles from the first. 14 | * 15 | * This will ensure that for any explicitly-defined bundle, other bundles 16 | * will not include its module or module-dependencies. This avoids copies 17 | * of a module from appearing in multiple bundles. 18 | * 19 | * @param {Array} bundleSeeds Early-stage bundle objects without module 20 | * or moduleHashes properties. 21 | * @param {Object} modulesByAbsPath Map of absolute paths to compiled modules. 22 | * 23 | * @return {Array} Bundle objects with explicit intersections 24 | * removed and new module and moduleHashes 25 | * properties. 26 | */ 27 | function dedupeExplicit (bundleSeeds, modulesByAbsPath) { 28 | const bundles = bundleSeeds.map(bundle => { 29 | // Generate flat, naive dependency arrays. 30 | const module = modulesByAbsPath[bundle.module.path]; 31 | return assign({}, bundle, { 32 | moduleHashes: [module.hash, ...module.deepDependencies.map((dep) => dep.hash)], 33 | module 34 | }); 35 | }); 36 | 37 | // For each explicitly-defined bundle, remove that bundle's entry module 38 | // and other deep dependencies from other bundles' module arrays. 39 | return bundles.map(bundleA => { 40 | bundleA = assign({}, bundleA); 41 | 42 | bundles.forEach(bundleB => { 43 | if (bundleA.module.path !== bundleB.module.path && 44 | includes(bundleA.moduleHashes, bundleB.module.hash)) { 45 | bundleA.moduleHashes = difference(bundleA.moduleHashes, bundleB.moduleHashes); 46 | } 47 | }); 48 | 49 | return bundleA; 50 | }); 51 | } 52 | 53 | export default pluggable(dedupeExplicit); 54 | -------------------------------------------------------------------------------- /src/compile/modules/load.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import Promise from "bluebird"; 4 | import { assign } from "lodash"; 5 | import { pluggable } from "pluggable"; 6 | 7 | 8 | const readFilePromise = Promise.promisify(fs.readFile, fs); 9 | 10 | 11 | /** 12 | * This function is invoked whenever the compiler attempts to read a source-file 13 | * from the disk. It takes an raw-module object as its only input. The properties 14 | * available on that object are as follows: 15 | * 16 | * - `path` - the absolute path of the file 17 | * - `ns` - the namespace of the module (either the default ns, or borrowed from its 18 | * containing package) 19 | * - `nsRoot` - the absolute path to the root of the namespace 20 | * - `nsPath` - the file's path relative to the root of the namespace 21 | * 22 | * The function should output an object with the same properties, plus one additional 23 | * property: `rawSource`. This property should be the string-value of the module 24 | * source. 25 | * 26 | * @param {Object} module Module object. 27 | * 28 | * @return {Object} Module object + `rawSource`. 29 | */ 30 | export const readSource = pluggable(function readSource (module) { 31 | return readFilePromise(module.path, "utf-8") 32 | .then(rawSource => assign({}, module, { rawSource })); 33 | }); 34 | 35 | /** 36 | * Given the early-stage module (module seed + rawSource property), determine and set 37 | * its type. This value defaults to "javascript" and is used to determine whether 38 | * default behaviors for parsing and processing modules should be used on the module. 39 | * 40 | * @param {Object} module Early-stage module. 41 | * 42 | * @return {Object} Module with new `type` property. 43 | */ 44 | export const setModuleType = pluggable(function setModuleType (module) { 45 | return assign({}, module, { type: "javascript" }); 46 | }); 47 | 48 | /** 49 | * Given a module seed, read the module from disk and determine its type. 50 | * 51 | * @param {Object} module Module seed. 52 | * 53 | * @return {Object} Module seed plus `rawSource` and `type` properties. 54 | */ 55 | const loadModule = pluggable(function loadModule (module) { 56 | if (module.type) { return module; } 57 | 58 | return this.readSource(module) 59 | .then(this.setModuleType); 60 | }, { readSource, setModuleType }); 61 | 62 | export default loadModule; 63 | -------------------------------------------------------------------------------- /src/optimizations/file-cache/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { tmpdir } from "os"; 3 | import fs from "fs"; 4 | 5 | import Promise from "bluebird"; 6 | import { assign } from "lodash"; 7 | import { sync as mkdirp } from "mkdirp"; 8 | import farmhash from "farmhash"; 9 | 10 | 11 | const DEFAULT_SUBDIR = "ilk_fcache"; 12 | 13 | const readFile = Promise.promisify(fs.readFile); 14 | const writeFile = Promise.promisify(fs.writeFile); 15 | 16 | 17 | function getCacheDir (cacheDir) { 18 | if (!cacheDir) { 19 | const tmpDir = tmpdir(); 20 | try { 21 | fs.accessSync(tmpDir, fs.R_OK | fs.W_OK); // eslint-disable-line no-bitwise 22 | } catch (err) { 23 | throw new Error( 24 | "Unable to access temp directory for caching. " + 25 | "Please check user permissions or specify `cacheDir` explicitly." 26 | ); 27 | } 28 | 29 | cacheDir = path.join(tmpDir, DEFAULT_SUBDIR); 30 | } 31 | 32 | try { 33 | mkdirp(cacheDir); 34 | fs.accessSync(cacheDir, fs.R_OK | fs.W_OK); // eslint-disable-line no-bitwise 35 | } catch (err) { 36 | throw new Error( 37 | "Unable to access cache directory. Please check user permissions.\n" + 38 | `CACHE DIR: ${cacheDir}` 39 | ); 40 | } 41 | 42 | return cacheDir; 43 | } 44 | 45 | 46 | export default function (opts = {}) { 47 | const cacheDir = getCacheDir(opts.cacheDir); 48 | 49 | const getCachePath = rawSource => path.join(cacheDir, `${farmhash.hash64(rawSource)}.ast`); 50 | 51 | return (override, transform) => { 52 | override("parseModule", function (module) { 53 | if (module.type !== "javascript") { return override.CONTINUE; } 54 | 55 | const cachePath = getCachePath(module.rawSource); 56 | 57 | return readFile(cachePath) 58 | .then(astJson => { 59 | const ast = JSON.parse(astJson); 60 | const sourcePath = path.join(module.ns, module.nsPath); 61 | return assign({}, module, { ast, sourcePath }); 62 | }) 63 | .catch(() => override.CONTINUE); 64 | }); 65 | 66 | transform("parseModule", function (module) { 67 | if (module.type !== "javascript") { return module; } 68 | 69 | const cachePath = getCachePath(module.rawSource); 70 | const astJson = JSON.stringify(module.ast); 71 | 72 | return writeFile(cachePath, astJson).then(() => module); 73 | }); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interlock", 3 | "version": "0.10.7", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --ext .js --ext .jst .", 8 | "test": "mocha spec/run.js", 9 | "build": "babel -d lib/ src/ && npm run copy-templates && npm run prep-cli", 10 | "watch": "watch 'npm run build' src/ -d", 11 | "copy-templates": "cp -R src/compile/construct/templates lib/compile/construct/", 12 | "prep-cli": "chmod u+x lib/cli/ilk.js", 13 | "prepublish": "npm run build", 14 | "preversion": "npm run check && npm run build", 15 | "check": "npm run lint && npm run test && ./scripts/validate-docs.sh", 16 | "generate-docs": "babel-node ./scripts/generate-docs.js" 17 | }, 18 | "bin": { 19 | "ilk": "./lib/cli/ilk.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/interlockjs/interlock" 24 | }, 25 | "author": "Dale Bustad ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/interlockjs/interlock/issues" 29 | }, 30 | "homepage": "https://github.com/interlockjs/interlock", 31 | "dependencies": { 32 | "babel-cli": "^6.14.0", 33 | "babel-core": "^6.14.0", 34 | "babel-eslint": "^6.1.2", 35 | "babel-generator": "^6.14.0", 36 | "babel-plugin-transform-es2015-modules-commonjs": "^6.14.0", 37 | "babel-polyfill": "^6.13.0", 38 | "babel-preset-nodejs-lts": "^2.0.1", 39 | "babel-traverse": "^6.15.0", 40 | "babel-types": "^6.15.0", 41 | "babylon": "^6.9.2", 42 | "bluebird": "^3.4.6", 43 | "chokidar": "^1.6.0", 44 | "eslint": "^3.4.0", 45 | "eslint-config-defaults": "^9.0.0", 46 | "eslint-plugin-filenames": "^1.1.0", 47 | "farmhash": "^1.2.1", 48 | "lodash": "^4.15.0", 49 | "mime": "*", 50 | "mkdirp": "*", 51 | "mocha": "^3.0.2", 52 | "pluggable": "^1.1.4", 53 | "sinon": "^1.17.5", 54 | "source-map": "*", 55 | "watch": "^0.19.2", 56 | "yargs": "^5.0.0" 57 | }, 58 | "devDependencies": { 59 | "babel-cli": "^6.4.5", 60 | "babel-eslint": "^6.1.2", 61 | "babel-preset-nodejs-lts": "^2.0.1", 62 | "chai": "^3.2.0", 63 | "eslint": "^3.5.0", 64 | "eslint-config-formidable": "^1.0.1", 65 | "eslint-plugin-filenames": "^1.1.0", 66 | "eslint-plugin-import": "^1.14.0", 67 | "mocha": "^3.0.2", 68 | "require-dir": "^0.3.0", 69 | "sinon": "^1.14.1", 70 | "sinon-chai": "^2.8.0", 71 | "watch": "^0.19.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /spec/src/resolve.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks,no-new */ 2 | 3 | import path from "path"; 4 | 5 | import resolve from "../../src/resolve"; 6 | 7 | const baseDir = path.resolve(__dirname, "../.."); 8 | 9 | 10 | describe("src/resolve", function () { 11 | function attemptResolve (requireStr, extentions) { 12 | return resolve( 13 | requireStr, 14 | path.join(baseDir, "src"), 15 | "interlock", 16 | baseDir, 17 | extentions || [".js"] 18 | ); 19 | } 20 | 21 | it("resolves file in same directory", function () { 22 | const resolved = attemptResolve("./index"); 23 | expect(resolved).to.have.property("ns", "interlock"); 24 | expect(resolved).to.have.property("nsPath", "src/index.js"); 25 | expect(resolved.path).to.equal(path.join(baseDir, "src/index.js")); 26 | }); 27 | 28 | it("resolves file in sub-directory", function () { 29 | const resolved = attemptResolve("./compile/construct/index"); 30 | expect(resolved).to.have.property("ns", "interlock"); 31 | expect(resolved).to.have.property("nsPath", "src/compile/construct/index.js"); 32 | expect(resolved.path).to.equal(path.join(baseDir, "src/compile/construct/index.js")); 33 | }); 34 | 35 | it("resolves current directory", function () { 36 | const resolved = attemptResolve("./"); 37 | expect(resolved).to.have.property("ns", "interlock"); 38 | expect(resolved).to.have.property("nsPath", "src/index.js"); 39 | expect(resolved.path).to.equal(path.join(baseDir, "src/index.js")); 40 | }); 41 | 42 | it("resolves a file in a separate directory branch", function () { 43 | const resolved = attemptResolve("../example/build"); 44 | expect(resolved).to.have.property("ns", "interlock"); 45 | expect(resolved).to.have.property("nsPath", "example/build.js"); 46 | expect(resolved.path).to.equal(path.join(baseDir, "example/build.js")); 47 | }); 48 | 49 | it("resolves a node_modules package", function () { 50 | const resolved = attemptResolve("lodash"); 51 | expect(resolved).to.have.property("ns", "lodash"); 52 | expect(resolved).to.have.property("nsPath", "lodash.js"); 53 | expect(resolved.path) 54 | .to.equal(path.join(baseDir, "node_modules/lodash", "lodash.js")); 55 | }); 56 | 57 | it("resolves a file in node_modules package", function () { 58 | const resolved = attemptResolve("lodash/slice.js"); 59 | expect(resolved).to.have.property("ns", "lodash"); 60 | expect(resolved).to.have.property("nsPath", "slice.js"); 61 | expect(resolved.path) 62 | .to.equal(path.join(baseDir, "node_modules/lodash/slice.js")); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/src/compile/construct.spec.js: -------------------------------------------------------------------------------- 1 | import { parse } from "babylon"; 2 | import { query } from "../../util"; 3 | import generate from "babel-generator"; 4 | import { constructCommonModule } from "../../../src/compile/construct"; 5 | 6 | function render (ast) { 7 | return generate(ast, { 8 | compact: false, 9 | comments: true, 10 | quotes: "double" 11 | }).code; 12 | } 13 | 14 | describe("src/compile/construct", () => { 15 | describe("common module", () => { 16 | function simpleModule () { 17 | const origModuleBody = parse("module.exports = 'hello';").program.body; 18 | 19 | const dependencies = [{ id: "ddb179" }, { id: "aa527f" }]; 20 | 21 | return Promise.all([ 22 | constructCommonModule(origModuleBody, dependencies), 23 | origModuleBody 24 | ]); 25 | } 26 | 27 | it("outputs an object literal with two properties", () => { 28 | return simpleModule() 29 | .then(([ast]) => { 30 | const objLiterals = query(ast, "ObjectExpression"); 31 | expect(objLiterals).to.have.length(1); 32 | expect(objLiterals[0].properties).to.have.length(2); 33 | }); 34 | }); 35 | 36 | it("includes dependency IDs", () => { 37 | return simpleModule() 38 | .then(([ast]) => { 39 | const depsArray = query(ast, "[key.name=deps]")[0]; 40 | expect(depsArray).to.have.deep.property("value.type", "ArrayExpression"); 41 | expect(depsArray.value.elements).to.have.length(2); 42 | expect(query(depsArray, "StringLiteral[value=ddb179]")).to.have.length(1); 43 | expect(query(depsArray, "StringLiteral[value=aa527f]")).to.have.length(1); 44 | }); 45 | }); 46 | 47 | it("includes the wrapped module body", () => { 48 | return simpleModule() 49 | .then(([ast, origBody]) => { 50 | const moduleFn = query(ast, "ObjectProperty[key.name=fn]")[0]; 51 | expect(moduleFn).to.have.deep.property("value.type", "FunctionExpression"); 52 | expect(moduleFn.value.params).to.have.length(3); 53 | 54 | const constructedModuleFnBody = query(moduleFn, "BlockStatement")[0].body; 55 | expect(constructedModuleFnBody).to.eql(origBody); 56 | }); 57 | }); 58 | 59 | it("outputs correct JS when rendered", () => { 60 | return simpleModule() 61 | .then(([ast]) => { 62 | expect(render(ast)).to.eql([ 63 | "{", 64 | " deps: [\"ddb179\", \"aa527f\"],", 65 | " fn: function (require, module, exports) {", 66 | " module.exports = 'hello';", 67 | " }", 68 | "}" 69 | ].join("\n")); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/options/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { assign, flatten } from "lodash"; 4 | 5 | 6 | export { compile } from "./compile"; 7 | export { shared, getLogger } from "./shared"; 8 | export { server } from "./server"; 9 | 10 | 11 | export function loadConfig (configPath) { 12 | configPath = path.resolve(process.cwd(), configPath); 13 | 14 | try { 15 | return require(configPath); // eslint-disable-line global-require 16 | } catch (err) { 17 | console.log("ERROR: Unable to load config file.\n"); // eslint-disable-line no-console 18 | console.log(err.stack); // eslint-disable-line no-console 19 | console.log(""); // eslint-disable-line no-console 20 | throw new Error("Please correct your config file and try again."); 21 | } 22 | } 23 | 24 | export function buildArgs (yargs, ...optionsDefs) { 25 | return flatten(optionsDefs).reduce((_yargs, option) => { 26 | if (!option.flags) { 27 | return _yargs; 28 | } 29 | return _yargs.option(option.flags[0], { 30 | alias: option.flags ? option.flags.slice(1) : [], 31 | describe: option.description.short, 32 | type: option.flagType, 33 | ...(option.cmdOpts || {}) // eslint-disable-line no-extra-parens 34 | }); 35 | }, yargs); 36 | } 37 | 38 | export function getInterlockOpts (argv, optionsDef, defaults = {}) { 39 | const cwd = process.cwd(); 40 | 41 | return optionsDef.reduce((opts, option) => { 42 | const argKey = option.flags[0]; 43 | let val = argv[argKey]; 44 | 45 | if (val) { 46 | if (option.flagTransform) { 47 | val = option.flagTransform(val, cwd); 48 | } 49 | opts[option.key] = val; 50 | } 51 | 52 | return opts; 53 | }, defaults); 54 | } 55 | 56 | function markAsDefault (opts, key) { 57 | opts.__defaults = opts.__defaults || {}; 58 | opts.__defaults[key] = true; 59 | } 60 | 61 | export function validate (options, optionsDef) { 62 | const cwd = process.cwd(); 63 | 64 | options = optionsDef.reduce((opts, option) => { 65 | const hasKey = option.key in options; 66 | let val = options[option.key]; 67 | 68 | if (!hasKey && option.default) { 69 | markAsDefault(opts, option.key); 70 | val = opts[option.key] = option.default(cwd); 71 | } 72 | 73 | if (!hasKey && option.required) { 74 | throw new Error(`A value is required for option '${option.key}'.`); 75 | } 76 | if (hasKey && option.schema && !option.schema(val)) { 77 | throw new Error(`Received invalid value for option '${option.key}': ${JSON.stringify(val)}.`); 78 | } 79 | 80 | return opts; 81 | }, assign({}, options)); 82 | 83 | if (optionsDef.or) { 84 | optionsDef.or.forEach(alternatives => { 85 | const oneIsPresent = alternatives.reduce((isPresent, alternative) => { 86 | return isPresent || !!options[alternative]; 87 | }, false); 88 | 89 | if (!oneIsPresent) { 90 | throw new Error( 91 | `Expected at least one of the following options: ${alternatives.join(", ")}.` 92 | ); 93 | } 94 | }); 95 | } 96 | 97 | return options; 98 | } 99 | -------------------------------------------------------------------------------- /src/util/template.js: -------------------------------------------------------------------------------- 1 | import { has, cloneDeep } from "lodash"; 2 | import * as babylon from "babylon"; 3 | import * as t from "babel-types"; 4 | import traverse from "babel-traverse"; 5 | 6 | 7 | const FROM_TEMPLATE = "_fromTemplate"; 8 | const TEMPLATE_SKIP = Symbol(); 9 | const CLEAR_KEYS = [ "tokens", "start", "end", "loc", "raw", "rawValue" ]; 10 | 11 | 12 | // Adapted from `babel-traverse`'s `traverse.clearNode`. 13 | function clearNode (node) { 14 | CLEAR_KEYS.forEach(key => { 15 | if (node[key] !== null) { node[key] = undefined; } 16 | }); 17 | 18 | for (const key in node) { 19 | if (key[0] === "_" && node[key] !== null) { node[key] = undefined; } 20 | } 21 | 22 | Object.getOwnPropertySymbols(node).forEach(sym => { 23 | node[sym] = null; 24 | }); 25 | } 26 | 27 | 28 | const templateVisitor = { 29 | noScope: true, 30 | 31 | enter (path, replacements) { 32 | let { node } = path; 33 | 34 | if (node[TEMPLATE_SKIP]) { return path.skip(); } 35 | if (t.isExpressionStatement(node)) { node = node.expression; } 36 | 37 | let replacement; 38 | 39 | if (t.isIdentifier(node) && node[FROM_TEMPLATE]) { 40 | if (has(replacements, node.name)) { 41 | replacement = replacements[node.name]; 42 | } 43 | } 44 | 45 | if (replacement === null) { path.remove(); } 46 | 47 | if (replacement) { 48 | replacement[TEMPLATE_SKIP] = true; 49 | path.replaceInline(replacement); 50 | } 51 | } 52 | }; 53 | 54 | 55 | function useTemplate (ast, replacements) { 56 | ast = cloneDeep(ast); 57 | const { program } = ast; 58 | 59 | traverse(ast, templateVisitor, null, replacements); 60 | 61 | if (program.body.length > 1) { 62 | return program.body; 63 | } else { 64 | return program.body[0]; 65 | } 66 | } 67 | 68 | // Adapted from `babel-template`. The existing package was deliberately 69 | // removing comments and other node meta-data from templates and from 70 | // replacement nodes. 71 | export default function template (code) { 72 | let stack; 73 | try { 74 | throw new Error(); 75 | } catch (error) { 76 | stack = error.stack.split("\n").slice(1).join("\n"); 77 | } 78 | 79 | let getAst = function () { 80 | let ast; 81 | 82 | try { 83 | ast = babylon.parse(code, { 84 | allowReturnOutsideFunction: true, 85 | allowSuperOutsideMethod: true 86 | }); 87 | 88 | traverse.cheap(ast, function (node) { 89 | clearNode(node); 90 | if (node.leadingComments) { node.leadingComments.forEach(clearNode); } 91 | if (node.trailingComments) { node.trailingComments.forEach(clearNode); } 92 | node[FROM_TEMPLATE] = true; 93 | }); 94 | } catch (err) { 95 | err.stack = `${err.stack}from\n${stack}`; 96 | throw err; 97 | } 98 | 99 | getAst = function () { 100 | return ast; 101 | }; 102 | 103 | return ast; 104 | }; 105 | 106 | return function (replacements) { 107 | return useTemplate(getAst(), replacements); 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import url from "url"; 5 | 6 | import mime from "mime"; 7 | import { findKey } from "lodash"; 8 | 9 | 10 | function fileNotFound (res) { 11 | res.writeHead(404); 12 | res.end(); 13 | } 14 | 15 | function respondStatic (res, basePath, relativePath) { 16 | const contentType = mime.lookup(relativePath); 17 | const realPath = path.join(basePath, relativePath); 18 | 19 | fs.readFile(realPath, (err, data) => { 20 | if (err) { 21 | res.writeHead(404); 22 | } else { 23 | res.writeHead(200, { "content-type": contentType }); 24 | res.write(data); 25 | } 26 | res.end(); 27 | }); 28 | } 29 | 30 | function sendEvent (connections, id, eventName, data, retryTimeout) { 31 | const connection = connections[id]; 32 | data = JSON.stringify(data); 33 | connection.write(`id: ${id}\n`); 34 | connection.write(`event: ${eventName}\n`); 35 | connection.write(`retry: ${retryTimeout}\n`); 36 | connection.write(`data: ${data}\n\n`); 37 | } 38 | 39 | 40 | export function createServer (opts = {}) { 41 | const eventsUrl = opts.eventsUrl || "/ilk/events"; 42 | const connections = {}; 43 | let nextConnectionID = 0; 44 | 45 | let dynamicResources = {}; 46 | let shouldRespond = Promise.resolve(); 47 | 48 | function pause () { 49 | let resume; 50 | shouldRespond = new Promise(_resolve => resume = _resolve); 51 | return resume; 52 | } 53 | 54 | function setDynamicAssets (assets) { dynamicResources = assets; } 55 | 56 | function notify (eventName, data) { 57 | Object.keys(connections).forEach(key => { 58 | if (!connections[key]) { return; } 59 | sendEvent(connections, key, eventName, data, opts.retryTimeout); 60 | }); 61 | } 62 | 63 | const server = http.createServer((req, res) => { 64 | shouldRespond.then(() => { // eslint-disable-line max-statements 65 | const acceptType = req.headers.accept; 66 | let requestUrl = url.parse(req.url).pathname.toLowerCase(); 67 | if (!(requestUrl in dynamicResources)) { 68 | requestUrl = path.join(requestUrl, "index.html"); 69 | } 70 | const requestedResource = dynamicResources[requestUrl]; 71 | 72 | if (requestUrl === eventsUrl && acceptType === "text/event-stream") { 73 | const id = ++nextConnectionID; 74 | connections[id] = res; 75 | res.writeHead(200, { 76 | "Content-Type": "text/event-stream", 77 | "Cache-Control": "no-cache", 78 | "Connection": "keep-alive" 79 | }); 80 | 81 | res.on("close", function () { 82 | connections[id] = null; 83 | }); 84 | 85 | return; 86 | } else if (requestedResource) { 87 | const contentType = mime.lookup(requestUrl); 88 | res.writeHead(200, { "content-type": contentType }); 89 | res.write(requestedResource); 90 | res.end(); 91 | return; 92 | } 93 | const staticMatch = findKey(opts.staticResources, pattern => { 94 | return pattern.test(requestUrl); 95 | }); 96 | 97 | if (!staticMatch) { 98 | fileNotFound(res); 99 | return; 100 | } 101 | 102 | const relPath = requestUrl.replace(opts.staticResources[staticMatch], ""); 103 | respondStatic(res, staticMatch, relPath); 104 | }); 105 | }); 106 | 107 | server.listen(opts.port); 108 | 109 | return { 110 | server, 111 | setDynamicAssets, 112 | notify, 113 | pause 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/compile/bundles/generate.js: -------------------------------------------------------------------------------- 1 | import { assign } from "lodash"; 2 | import { pluggable } from "pluggable"; 3 | import Promise from "bluebird"; 4 | 5 | import getBundleSeeds from "./get-seeds"; 6 | import dedupeExplicit from "./dedupe-explicit"; 7 | import dedupeImplicit from "./dedupe-implicit"; 8 | import hashBundle from "./hash"; 9 | import interpolateFilename from "./interpolate-filename"; 10 | 11 | 12 | /** 13 | * Define the canonical modules array for a bundle. This should occur after 14 | * bundle module hashes are deduped. 15 | * 16 | * @param {Object} bundle The bundle object, with no modules property. 17 | * @param {Object} moduleMaps Has two properties - byAbsPath and byHash - 18 | * where each of these map to the compiled module 19 | * via the respective value. 20 | * 21 | * @return {Object} The bundle object, with modules property. 22 | */ 23 | const populateBundleModules = pluggable(function populateBundleModules (bundle, moduleMaps) { 24 | return assign({}, bundle, { 25 | modules: bundle.moduleHashes.map(hash => moduleMaps.byHash[hash]) 26 | }); 27 | }); 28 | 29 | /** 30 | * Given a set of module seeds and the set of fully generated modules, generate 31 | * a finalized array of bundles. These bundles will be early-stage and should 32 | * not be populated with the actual modules. Instead, each bundle will be defined 33 | * by the module hashes (unique IDs) of the modules that comprise the bundle. 34 | * 35 | * @param {Object} moduleSeeds Early-stage module objects, indexed by their 36 | * path relative to the compilation context. 37 | * @param {Object} moduleMaps Maps of fully compiled modules, indexed by both 38 | * absolute path and hash. 39 | * 40 | * @return {Array} Early-stage bundles. 41 | */ 42 | const partitionBundles = pluggable(function partitionBundles (moduleSeeds, moduleMaps) { 43 | return this.getBundleSeeds(moduleSeeds, moduleMaps.byAbsPath) 44 | .then(seedBundles => this.dedupeExplicit(seedBundles, moduleMaps.byAbsPath)) 45 | .then(this.dedupeImplicit); 46 | }, { getBundleSeeds, dedupeExplicit, dedupeImplicit }); 47 | 48 | /** 49 | * Given a set of module seeds - originally generated from the bundle definitions 50 | * passed into the Interlock constructor - and the set of fully generated modules, 51 | * generate the full set of bundles that should be emitted, populate them with 52 | * module objects, hash them, and interpolate any output filenames. 53 | * 54 | * Bundles outputted from this function should be ready to be transformed into 55 | * strings using AST->source transformation, and then written to disk. 56 | * 57 | * @param {Object} moduleSeeds Early-stage module objects, indexed by their 58 | * path relative to the compilation context. 59 | * @param {Object} moduleMaps Maps of fully compiled modules, indexed by both 60 | * absolute path and hash. 61 | * 62 | * @return {Array} Fully compiled bundles. 63 | */ 64 | export default pluggable(function generateBundles (moduleSeeds, moduleMaps) { 65 | return this.partitionBundles(moduleSeeds, moduleMaps) 66 | .then(bundles => Promise.all( 67 | bundles.map(bundle => this.populateBundleModules(bundle, moduleMaps))) 68 | ) 69 | .then(bundles => Promise.all(bundles.map(this.hashBundle))) 70 | .then(bundles => Promise.all(bundles.map(this.interpolateFilename))); 71 | }, { partitionBundles, hashBundle, interpolateFilename, populateBundleModules }); 72 | -------------------------------------------------------------------------------- /src/compile/bundles/generate-raw.js: -------------------------------------------------------------------------------- 1 | import generate from "babel-generator"; 2 | import { assign } from "lodash"; 3 | 4 | import { pluggable } from "pluggable"; 5 | 6 | 7 | /** 8 | * Given an AST and a set of options, generate the corresponding JavaScript 9 | * source and optional sourcemap string. 10 | * 11 | * @param {Object} opts The generation options. 12 | * @param {AST} opts.ast The AST to render. 13 | * @param {Boolean} opts.sourceMaps Whether to render a source-map. 14 | * @param {String} opts.sourceMapTarget The output filename. 15 | * @param {Boolean} opts.pretty Whether to output formatted JS. 16 | * @param {Boolean} opts.includeComments Whether to include comments in the output. 17 | * @param {Object} opts.sources A hash of source filenames to source content. 18 | * 19 | * @return {Object} An object with `code` and `map` strings, 20 | * where `map` can be null. 21 | */ 22 | const generateJsCode = pluggable(function generateJsCode (opts) { 23 | const { 24 | ast, 25 | sourceMaps, 26 | sourceMapTarget, 27 | pretty, 28 | includeComments, 29 | sources 30 | } = opts; 31 | 32 | const { code, map } = generate(ast, { 33 | sourceMaps, 34 | sourceMapTarget, 35 | comments: includeComments, 36 | compact: !pretty, 37 | quotes: "double" 38 | }, sources); 39 | 40 | return { 41 | code, 42 | map: sourceMaps ? JSON.stringify(map) : null 43 | }; 44 | }); 45 | 46 | /** 47 | * Given a compiled bundle object, return an array of one or more bundles with 48 | * new `raw` property. This raw property should be generated from the bundle's 49 | * AST or equivalent intermediate representation. 50 | * 51 | * This is a one-to-many transformation because it is quite possible for multiple 52 | * output files to result from a single bundle object. The canonical example (and 53 | * default behavior of this function, when sourcemaps are enabled) is for one 54 | * bundle to result in a `.js` file and a `.map` file. 55 | * 56 | * @param {Object} bundle Bundle object without `raw` property. 57 | * 58 | * @return {Array} Array of bundle objects. At minimum, these bundle 59 | * objects should have a `raw` property - a string 60 | * representation of the file to be written to disk - 61 | * and a `dest` property - the relative filepath 62 | * of the file to be written to disk. 63 | */ 64 | export default pluggable(function generateRawBundles (bundle) { 65 | if (bundle.type !== "javascript") { 66 | throw new Error("Cannot generate JS source for non-JavaScript bundle."); 67 | } 68 | 69 | const bundleSources = bundle.modules.reduce((hash, module) => { 70 | hash[module.sourcePath] = module.rawSource; 71 | return hash; 72 | }, {}); 73 | 74 | const ast = bundle.ast.type === "Program" || bundle.ast.type === "File" ? 75 | bundle.ast : 76 | { type: "Program", body: [].concat(bundle.ast) }; 77 | 78 | return this.generateJsCode({ 79 | ast, 80 | sourceMaps: !!this.opts.sourceMaps, 81 | sourceMapTarget: this.opts.sourceMaps && bundle.dest, 82 | pretty: !this.opts.pretty, 83 | includeComments: !!this.opts.includeComments, 84 | sources: bundleSources 85 | }).then(({ code, map }) => { 86 | const outputBundle = assign({}, bundle, { raw: code }); 87 | 88 | return this.opts.sourceMaps ? 89 | [ outputBundle, { raw: map, dest: `${bundle.dest}.map` } ] : 90 | [ outputBundle ]; 91 | }); 92 | }, { generateJsCode }); 93 | -------------------------------------------------------------------------------- /src/resolve.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { some } from "lodash"; 4 | 5 | import { isFile, isDir, getPossiblePaths } from "./util/file"; 6 | 7 | 8 | function select (items, fn) { 9 | for (const item of items) { 10 | const result = fn(item); 11 | if (result) { return result; } 12 | } 13 | return null; 14 | } 15 | 16 | function resolveFile (absPath, extensions) { 17 | let altPath; 18 | 19 | if (isFile(absPath)) { return absPath; } 20 | 21 | return some(extensions, function (ext) { 22 | altPath = absPath + ext; 23 | return isFile(altPath) && altPath; 24 | }) && altPath || null; 25 | } 26 | 27 | function resolveDir (absPath, extensions) { 28 | if (!isDir(absPath)) { return null; } 29 | 30 | const packageJsonPath = path.join(absPath, "package.json"); 31 | if (isFile(packageJsonPath)) { 32 | const main = require(packageJsonPath).main; // eslint-disable-line global-require 33 | if (main) { 34 | const mainAbsPath = path.join(absPath, main); 35 | return resolveFile(mainAbsPath, extensions) || resolveDir(mainAbsPath, extensions); 36 | } 37 | } 38 | 39 | const fallback = path.join(absPath, "index.js"); 40 | return isFile(fallback) && fallback || null; 41 | } 42 | 43 | function resolveSimple (requireStr, contextPath, nsRoot, extensions) { 44 | const absPath = path.resolve(contextPath, requireStr); 45 | 46 | const resolvedPath = resolveFile(absPath, extensions) || resolveDir(absPath, extensions); 47 | if (resolvedPath) { 48 | return { 49 | resolvedPath, 50 | nsPath: path.relative(nsRoot, resolvedPath) 51 | }; 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * Evaluates a require string to an absolute path, along with that file's 58 | * namespace meta-data. 59 | * 60 | * @param {String} requireStr The value passed to the `require()` statement. 61 | * @param {String} contextPath The directory from where relative requires are evaluated. 62 | * @param {String} ns The default/active namespace. 63 | * @param {String} nsRoot The root directory of the default/active namespace. 64 | * @param {Array} extensions All possible file extensions to try. 65 | * @param {Array} searchPaths Additional directories in which to attempt to locate 66 | * specified resource. 67 | * 68 | * @returns {Object} Object with three properties: 69 | * - path: the resolved absolute path of the resource; 70 | * - ns: the namespace associated with that resource; 71 | * - nsPath: the root path of the namespace. 72 | * 73 | * @returns {null} Will return null if resolve fails. 74 | */ 75 | export default function resolve (requireStr, contextPath, ns, nsRoot, extensions, searchPaths = []) { // eslint-disable-line max-len,max-params 76 | const resolvedSimple = resolveSimple(requireStr, contextPath, nsRoot, extensions); 77 | 78 | if (resolvedSimple) { 79 | return { 80 | path: resolvedSimple.resolvedPath, 81 | ns, 82 | nsPath: resolvedSimple.nsPath, 83 | nsRoot, 84 | uri: `${ns}:${resolvedSimple.nsPath}` 85 | }; 86 | } 87 | 88 | if (/^(\.\.?)?\//.test(requireStr)) { return null; } 89 | 90 | ns = requireStr.split("/")[0]; 91 | const resolvedPath = select(searchPaths.concat(getPossiblePaths(contextPath, "node_modules")), searchPath => { // eslint-disable-line max-len 92 | const searchCandidate = path.join(searchPath, requireStr); 93 | nsRoot = path.join(searchPath, ns); 94 | return resolveFile(searchCandidate, extensions) || resolveDir(searchCandidate, extensions); 95 | }); 96 | 97 | if (resolvedPath) { 98 | const nsPath = path.relative(nsRoot, resolvedPath); 99 | return { 100 | path: resolvedPath, 101 | ns, 102 | nsPath, 103 | nsRoot, 104 | uri: `${ns}:${nsPath}` 105 | }; 106 | } 107 | 108 | return null; 109 | } 110 | -------------------------------------------------------------------------------- /src/compile/modules/transform-amd.js: -------------------------------------------------------------------------------- 1 | import { zip } from "lodash"; 2 | import * as t from "babel-types"; 3 | 4 | 5 | /** 6 | * Return the AST equivalent of `require(requireStr)`, where requireStr is 7 | * the provided value. 8 | * 9 | * Example: 10 | * "./path/to/thing" --> `require("./path/to/thing")` 11 | * 12 | * @param {String} requireStr Require string. 13 | * 14 | * @return {AST} Call expression node. 15 | */ 16 | function requireCallExpression (requireStr) { 17 | return t.callExpression(t.identifier("require"), [t.stringLiteral(requireStr)]); 18 | } 19 | 20 | /** 21 | * Return the provided expression node wrapped in an expression statement node. 22 | * 23 | * Example: 24 | * `some.expression()` --> `some.expression();` 25 | * 26 | * @param {AST} expr Expression node to wrap. 27 | * 28 | * @return {AST} Expression statement node. 29 | */ 30 | function expressionStmt (expr) { 31 | return t.expressionStatement(expr); 32 | } 33 | 34 | /* 35 | 36 | */ 37 | /** 38 | * Return an assignment expression, setting module.exports to an IIFE containing 39 | * the provided require statements and module body. 40 | * 41 | * Example: 42 | * module.exports = (function () { 43 | * // ... require statements 44 | * // ... module body 45 | * })(); 46 | * 47 | * @param {Array} requireStatements Array of call expression statements. 48 | * @param {Array} moduleBody Array of AST nodes. 49 | * 50 | * @return {AST} Assignment expression AST node. 51 | */ 52 | function commonJsTemplate (requireStatements, moduleBody) { 53 | return t.assignmentExpression( 54 | "=", 55 | t.memberExpression(t.identifier("module"), t.identifier("exports")), 56 | t.callExpression( 57 | t.functionExpression(null, [], t.blockStatement( 58 | requireStatements.concat(moduleBody) 59 | )), 60 | [] 61 | ) 62 | ); 63 | } 64 | 65 | /** 66 | * Given a desired variable name and require string, generate a new 67 | * call expression statement. If no variable name is provided, return 68 | * a simple call expression statement. 69 | * 70 | * Examples: 71 | * `var myVar = require("the-provided-requireStr");` 72 | * `require("the-provided-requireStr-that-is-not-assigned");` 73 | * 74 | * @param {String} requireVar Variable name. 75 | * @param {String} requireStr Require string. 76 | * 77 | * @return {AST} Variable declaration or call expression 78 | * statement, depending on presence of 79 | * requireVar. 80 | */ 81 | function toRequire (requireVar, requireStr) { 82 | if (requireVar) { 83 | return t.variableDeclaration("var", [t.variableDeclarator( 84 | t.identifier(requireVar), 85 | requireCallExpression(requireStr) 86 | )]); 87 | } 88 | 89 | return expressionStmt(requireCallExpression(requireStr)); 90 | } 91 | 92 | /** 93 | * Given the AST node representing a define function call's dependency array 94 | * and the AST node representing that same call's callback function, generate 95 | * the common JS equivalent as AST. 96 | * 97 | * @param {AST} defineArray AST node of define dependency array. 98 | * @param {AST} defineFunction AST node of define callback. 99 | * 100 | * @return {AST} AST node of common JS equivalent to 101 | * AMD-style module. 102 | */ 103 | function toCommonJs (defineArray, defineFunction) { 104 | const requireStrings = defineArray.elements.map(el => el.value); 105 | const requireVars = defineFunction.params.map(param => param.name); 106 | const requireStatements = zip(requireVars, requireStrings) 107 | .map(([requireVar, requireStr]) => toRequire(requireVar, requireStr)); 108 | return commonJsTemplate(requireStatements, defineFunction.body.body); 109 | } 110 | 111 | export default function () { 112 | let topLevelNode; 113 | 114 | return { 115 | visitor: { 116 | ExpressionStatement (nodePath) { 117 | if (nodePath.parent.type === "Program") { 118 | topLevelNode = nodePath.node; 119 | } 120 | }, 121 | CallExpression (nodePath) { 122 | if (nodePath.parent === topLevelNode && nodePath.node.callee.name === "define") { 123 | const args = nodePath.node.arguments; 124 | if (args.length === 2 && 125 | args[0].type === "ArrayExpression" && 126 | args[1].type === "FunctionExpression") { 127 | nodePath.replaceWith(toCommonJs(args[0], args[1])); 128 | return; 129 | } else if (args.length === 3 && 130 | args[1].type === "ArrayExpression" && 131 | args[1].type === "FunctionExpression") { 132 | nodePath.replaceWith(toCommonJs(args[1], args[2])); 133 | return; 134 | } 135 | throw new Error("Could not parse `define` block."); 136 | } 137 | } 138 | } 139 | }; 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/compile/modules/compile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import path from "path"; 3 | 4 | import { assign, chain, fromPairs } from "lodash"; 5 | import Promise from "bluebird"; 6 | import { pluggable } from "pluggable"; 7 | 8 | import resolveModule from "./resolve"; 9 | import loadModule from "./load"; 10 | import hashModule from "./hash"; 11 | import generateModuleId from "./generate-id"; 12 | import parseModule from "./parse"; 13 | import transformModule from "./transform"; 14 | import updateReferences from "./update-references"; 15 | 16 | 17 | /** 18 | * Because the `compileModule` and `generateDependencies` functions interact 19 | * recursively, defining a stand-in pluggable for `compileModule` allows for 20 | * plugins to utilize `compileModule` from within an overridden `generateDependencies`. 21 | * 22 | * For true behavior, please see documentation for `compileModule`. 23 | * 24 | * @return {Promise} Resolves to compiled module. 25 | */ 26 | const compileModuleR = pluggable(function compileModuleR () { 27 | return compileModule.apply(this, arguments); 28 | }); 29 | 30 | /** 31 | * Given a module whose dependency references (like require strings) have been 32 | * determined, recursively compile all dependencies and return the module with 33 | * new dependency properties. 34 | * 35 | * @param {Object} module Module for whom dependencies should be compiled. 36 | * 37 | * @return {Object} Module with new dependency properties. 38 | */ 39 | const generateDependencies = pluggable(function generateDependencies (module) { 40 | 41 | // Given a require string or similar absolute/relative path reference, resolve 42 | // that reference and compile the dependency (recursively). 43 | const getDependency = (requireStr, contextPath, contextNs, contextNsRoot) => { 44 | return this.resolveModule(requireStr, contextPath, contextNs, contextNsRoot) 45 | .then(dependency => this.compileModuleR(dependency)) 46 | .then(childModule => [requireStr, childModule]); 47 | }; 48 | 49 | // Given an array of compiled module dependencies, generate a recursively flattened 50 | // list of all module dependencies. 51 | const getDeepDependencies = dependencies => chain(dependencies) 52 | .map(([, dep]) => dep.deepDependencies.concat(dep)) 53 | .flatten() 54 | .value(); 55 | 56 | const contextPath = path.dirname(module.path); 57 | const directDependencies = Promise.all(module.synchronousRequires.map( 58 | requireStr => getDependency(requireStr, contextPath, module.ns, module.nsRoot) 59 | )); 60 | 61 | return Promise.all([directDependencies, directDependencies.then(getDeepDependencies)]) 62 | .then(([depTuples, deepDependencies]) => assign({}, module, { 63 | // De-dupe any (deep-)dependencies by their hash. 64 | deepDependencies: chain(deepDependencies).keyBy("hash").values().value(), 65 | dependencies: chain(depTuples) 66 | .map(([, dependency]) => dependency) 67 | .keyBy("hash") 68 | .values() 69 | .value(), 70 | // Generate a mapping between the original require strings and the modules 71 | // they resolved to. 72 | dependenciesByInternalRef: fromPairs(depTuples) 73 | })); 74 | }, { resolveModule, compileModuleR }); 75 | 76 | /** 77 | * Given an unprocess module that has been loaded from disk, return a promise 78 | * that resolves to the same module in a processed/compiled state, and whose 79 | * dependencies have also been processed/compiled. 80 | * 81 | * @param {Object} module Seed module. 82 | * 83 | * @return {Promise} Resolves to compiled module. 84 | */ 85 | const compileModule = pluggable(function compileModule (module) { 86 | const modulesByAbsPath = this.cache.modulesByAbsPath; 87 | 88 | if (module.path in modulesByAbsPath) { 89 | return modulesByAbsPath[module.path]; 90 | } 91 | 92 | return modulesByAbsPath[module.path] = this.loadModule(module) 93 | .then(this.parseModule) 94 | .then(this.transformModule) 95 | .then(this.generateDependencies) 96 | .then(this.hashModule) 97 | .then(this.generateModuleId) 98 | .then(this.updateReferences); 99 | }, { 100 | loadModule, 101 | parseModule, 102 | transformModule, 103 | generateDependencies, 104 | hashModule, 105 | generateModuleId, 106 | updateReferences 107 | }); 108 | 109 | /** 110 | * Given one or more module seeds, traverse their dependency graph, collecting any and 111 | * all dependency modules, and then parse, transform, and hash those modules. Return 112 | * a promise that resolves to the full set of modules, once they have been correctly 113 | * gathered and compiled. 114 | * 115 | * @param {Array} moduleSeeds Module seeds, i.e. modules that have not yet been 116 | * populated with properties such as ast, `dependencies`, 117 | * etc. Module objects _should_ have path, rawSource, 118 | * and namespace values. 119 | * 120 | * @return {Promise} Resolves to array of all compiled modules. 121 | */ 122 | const compileModules = pluggable(function compileModules (moduleSeeds) { 123 | return Promise.all(moduleSeeds.map(this.compileModule.bind(this))) 124 | .then(compiledSeedModules => chain(compiledSeedModules) 125 | .map(seedModule => seedModule.deepDependencies.concat(seedModule)) 126 | .flatten() 127 | .uniq() 128 | .value() 129 | ); 130 | }, { compileModule }); 131 | 132 | export default compileModules; 133 | -------------------------------------------------------------------------------- /src/compile/index.js: -------------------------------------------------------------------------------- 1 | import { assign, chain, flatten, values } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable, getBaseContext } from "pluggable"; 5 | 6 | import { constructBundleAst } from "./construct"; 7 | import getModuleSeeds from "./modules/get-seeds"; 8 | import generateModuleMaps from "./modules/generate-maps"; 9 | import compileModules from "./modules/compile"; 10 | import generateBundles from "./bundles/generate"; 11 | import generateRawBundles from "./bundles/generate-raw"; 12 | 13 | import fcachePlugin from "../optimizations/file-cache"; 14 | 15 | 16 | /** 17 | * Given an array of bundles, generate a lookup dictionary of module hashes 18 | * to the destination path of the bundles that contains them. 19 | * 20 | * @param {Array} bundles Compiled bundles. 21 | * 22 | * @return {Object} moduleHash-to-URL lookup dictionary. 23 | */ 24 | export const getUrls = pluggable(function getUrls (bundles) { 25 | return bundles.reduce((urls, bundle) => { 26 | bundle.moduleHashes.forEach(hash => urls[hash] = `/${bundle.dest}`); 27 | return urls; 28 | }, {}); 29 | }); 30 | 31 | /** 32 | * Given a compiled bundle and moduleHash-to-URL lookup object, output 33 | * the same bundle with generated AST. 34 | * 35 | * @param {Object} bundle Fully compiled bundle, ready to be outputed. 36 | * @param {Object} urls moduleHash-to-URL lookup dictionary. 37 | * 38 | * @return {Object} Bundle with new `ast` property. 39 | */ 40 | export const constructBundle = pluggable(function constructBundle (bundle, urls) { 41 | return this.constructBundleAst({ 42 | modules: bundle.modules, 43 | includeRuntime: bundle.includeRuntime, 44 | urls: bundle.isEntryPt ? urls : null, 45 | entryModuleId: bundle.isEntryPt && bundle.module && bundle.module.id || null 46 | }) 47 | .then(ast => assign({}, bundle, { ast })); 48 | }, { constructBundleAst }); 49 | 50 | /** 51 | * Given an array of compiled bundles and a moduleHash-to-URL lookup dictionary, 52 | * generate a new array of bundles with new `ast` and `raw` properties. 53 | * 54 | * Some compiled bundles (as internally represented) will result in more than 55 | * one output file. The canonical example of this is a JS file and its source-map. 56 | * Plugins may also implement mechanisms to output multiple files per bundle. 57 | * 58 | * This one-to-many relationship is defined by the generateRawBundles method, which 59 | * may output an array of raw bundles. 60 | * 61 | * @param {Array} bundlesArr Compiled bundles. 62 | * @param {Object} urls moduleHash-to-URL lookup dictionary. 63 | * 64 | * @return {Array} Bundles with new `raw` properties. 65 | */ 66 | export const emitRawBundles = pluggable(function emitRawBundles (bundlesArr, urls) { 67 | return Promise.all(bundlesArr.map(bundle => 68 | this.constructBundle(bundle, urls) 69 | .then(this.generateRawBundles) 70 | )) 71 | // generateRawBundles returns arrays of bundles. This allows, for example, a 72 | // source map to also be emitted along with its bundle JS. 73 | .then(flatten); 74 | }, { constructBundle, generateRawBundles }); 75 | 76 | /** 77 | * Reduces an array of compiled bundles into a compilation object. This compilation 78 | * object will have three key/value pairs: 79 | * 80 | * - **cache:** populated from the compilation process 81 | * - **bundles:** a mapping of destination paths to `raw` code 82 | * - **opts:** the original options passed to the compilation 83 | * 84 | * @param {Array} bundles Compiled bundles, generated by generateBundles. 85 | * 86 | * @return {Promise} Compilation object. 87 | */ 88 | export const buildOutput = pluggable(function buildOutput (bundles) { 89 | return this.getUrls(bundles) 90 | .then(urls => this.emitRawBundles(bundles, urls)) 91 | .then(rawBundles => chain(rawBundles) 92 | .map(rawBundle => [rawBundle.dest, rawBundle]) 93 | .fromPairs() 94 | .value()) 95 | .then(bundlesByDest => ({ 96 | bundles: bundlesByDest, 97 | opts: this.opts, 98 | cache: this.cache 99 | })); 100 | }, { getUrls, emitRawBundles }); 101 | 102 | /** 103 | * Loads, transforms, and bundles an application using the provided options. 104 | * Modules are collected and transformed, bundles are formed from those modules, 105 | * and those bundles are finally converted into a format that can be written 106 | * to disk or served over HTTP. 107 | * 108 | * @return {Promise} Resolves to an object with three properties: `bundles`, 109 | * `opts`, and `cache`. 110 | */ 111 | const compile = pluggable(function compile () { 112 | return this.getModuleSeeds() 113 | .then(moduleSeeds => Promise.all([ 114 | moduleSeeds, 115 | this.compileModules(values(moduleSeeds)).then(this.generateModuleMaps.bind(this)) 116 | ])) 117 | .then(([moduleSeeds, moduleMaps]) => this.generateBundles(moduleSeeds, moduleMaps)) 118 | .then(this.buildOutput); 119 | }, { getModuleSeeds, compileModules, generateModuleMaps, generateBundles, buildOutput }); 120 | 121 | 122 | export default function (opts) { 123 | const plugins = [].concat(opts.plugins); 124 | 125 | if (opts.fcache) { 126 | const cacheDir = opts.fcache === true ? null : opts.fcache; 127 | plugins.push(fcachePlugin({ cacheDir })); 128 | } 129 | 130 | return compile.call(getBaseContext({ 131 | cache: { 132 | modulesByAbsPath: Object.create(null) 133 | }, 134 | opts: Object.freeze(opts) 135 | }, plugins)); 136 | } 137 | -------------------------------------------------------------------------------- /src/options/compile.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { 4 | isString, 5 | isObject, 6 | isArray, 7 | isBoolean, 8 | chain 9 | } from "lodash"; 10 | 11 | import { getPackageJson } from "../util/file"; 12 | 13 | 14 | function evalOption (errorMsg) { 15 | return val => { 16 | try { 17 | return new Function("require", `return ${val};`)(require); // eslint-disable-line no-new-func 18 | } catch (err) { 19 | throw new Error(errorMsg); 20 | } 21 | }; 22 | } 23 | 24 | 25 | export const compile = [{ 26 | key: "srcRoot", 27 | default: cwd => path.join(cwd, "src"), 28 | schema: isString, 29 | 30 | flags: ["src"], 31 | flagType: "string", 32 | flagTransform: (val, cwd) => path.resolve(cwd, val), 33 | cmdOpts: { normalize: true }, 34 | 35 | description: { 36 | short: "Path to source directory.", 37 | full: "TODO" 38 | } 39 | }, { 40 | key: "destRoot", 41 | default: cwd => path.join(cwd, "dist"), 42 | schema: isString, 43 | 44 | flags: ["dest"], 45 | flagType: "string", 46 | flagTransform: (val, cwd) => path.resolve(cwd, val), 47 | cmdOpts: { normalize: true }, 48 | 49 | description: { 50 | short: "Path to output directory.", 51 | full: "TODO" 52 | } 53 | }, { 54 | key: "entry", 55 | schema: entryObj => { 56 | return isObject(entryObj) && Object.keys(entryObj).reduce((isValid, key) => { 57 | return isValid && 58 | isString(entryObj[key]) || 59 | isObject(entryObj[key]) && 60 | isString(entryObj[key].dest); 61 | }, true); 62 | }, 63 | 64 | flags: ["entry", "e"], 65 | flagType: "string", 66 | flagTransform: val => chain(val).chunk(2).fromPairs().value(), 67 | cmdOpts: { nargs: 2 }, 68 | 69 | description: { 70 | short: "Your application entry point, followed by its output bundle filename.", 71 | full: "TODO" 72 | } 73 | }, { 74 | key: "split", 75 | schema: splitObj => { 76 | return isObject(splitObj) && Object.keys(splitObj).reduce((isValid, key) => { 77 | return isValid && 78 | isString(splitObj[key]) || 79 | isObject(splitObj[key]) && 80 | isString(splitObj[key].dest); 81 | }, true); 82 | }, 83 | 84 | flags: ["split", "s"], 85 | flagType: "string", 86 | flagTransform: val => chain(val).chunk(2).fromPairs().value(), 87 | cmdOpts: { nargs: 2 }, 88 | 89 | description: { 90 | short: "Your application split point, followed by its output bundle filename.", 91 | full: "TODO" 92 | } 93 | }, { 94 | key: "extensions", 95 | default: () => [".js", ".jsx", ".es6"], 96 | schema: val => 97 | isArray(val) && 98 | val.reduce((result, entry) => result && isString(entry), true), 99 | 100 | flags: ["ext"], 101 | flagType: "string", 102 | flagTransform: val => Array.isArray(val) ? val : [val], 103 | 104 | description: { 105 | short: "Extensions to use for require() resolution.", 106 | full: "TODO" 107 | } 108 | }, { 109 | key: "ns", 110 | default: cwd => getPackageJson(cwd).name, 111 | schema: isString, 112 | 113 | flags: ["namespace"], 114 | flagType: "string", 115 | 116 | description: { 117 | short: "Namespace to use for your project.", 118 | full: "TODO" 119 | } 120 | }, { 121 | key: "implicitBundleDest", 122 | default: () => "[setHash].js", 123 | schema: isString, 124 | 125 | flags: ["implicit-bundle-dest"], 126 | flagType: "string", 127 | 128 | description: { 129 | short: "Filename pattern for discovered/implicit bundles.", 130 | full: "TODO" 131 | } 132 | }, { 133 | key: "sourceMaps", 134 | default: () => false, 135 | schema: isBoolean, 136 | 137 | flags: ["sourcemaps"], 138 | flagType: "boolean", 139 | 140 | description: { 141 | short: "Output sourcemaps along with bundled code.", 142 | full: "TODO" 143 | } 144 | }, { 145 | key: "includeComments", 146 | default: () => false, 147 | schema: isBoolean, 148 | 149 | flagType: "boolean", 150 | flags: ["comments"], 151 | 152 | description: { 153 | short: "Include comments in output JS.", 154 | full: "TODO" 155 | } 156 | }, { 157 | key: "pretty", 158 | default: () => false, 159 | schema: isBoolean, 160 | 161 | flags: ["pretty"], 162 | flagType: "boolean", 163 | 164 | description: { 165 | short: "Output bundles in non-compact mode.", 166 | full: "TODO" 167 | } 168 | }, { 169 | key: "babelConfig", 170 | schema: isObject, 171 | 172 | flags: ["babel-config"], 173 | flagType: "string", 174 | flagTransform: evalOption("You supplied an invalid 'babel-config' value."), 175 | 176 | description: { 177 | short: "Babel config. Should take the form of a JS object.", 178 | full: "TODO" 179 | } 180 | }, { 181 | key: "plugins", 182 | default: () => [], 183 | schema: isArray, 184 | 185 | flags: ["plugins"], 186 | flagType: "string", 187 | flagTransform: evalOption("You supplied an invalid 'plugins' value."), 188 | 189 | description: { 190 | short: "Plugins array. Should take the form of a JS array.", 191 | full: "TODO" 192 | } 193 | }, { 194 | "key": "multiprocess", 195 | default: () => false, 196 | schema: isBoolean, 197 | 198 | flags: ["multiprocess"], 199 | flagType: "boolean", 200 | 201 | description: { 202 | short: "Distribute work over multiple processors.", 203 | full: "TODO" 204 | } 205 | }, { 206 | "key": "workers", 207 | schema: val => Number.isInteger(val) && val > 0, 208 | 209 | flags: ["workers"], 210 | flagType: "number", 211 | 212 | description: { 213 | short: "Number of processes to use (implies multiprocess).", 214 | full: "TODO" 215 | } 216 | }, { 217 | "key": "fcache", 218 | default: () => false, 219 | schema: val => isBoolean(val) || isString(val), 220 | 221 | flags: ["fcache"], 222 | flagType: "boolean", 223 | 224 | description: { 225 | short: "Enable caching of compilation assets for improved build times.", 226 | full: "TODO" 227 | } 228 | }]; 229 | 230 | compile.or = [ 231 | ["entry", "split"] 232 | ]; 233 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | 4 | import { watch } from "chokidar"; 5 | import { sync as mkdirp } from "mkdirp"; 6 | import { assign, isArray, merge, chain, isString, keys } from "lodash"; 7 | 8 | import compile from "./compile"; 9 | import * as options from "./options"; 10 | import { entries } from "./util/object"; 11 | import compileModules from "./compile/modules/compile"; 12 | 13 | 14 | function normalizeEntryPoints (entryPoints) { 15 | return chain(entryPoints || {}) 16 | .map((entryDefinition, entrySrcPath) => { 17 | entryDefinition = isString(entryDefinition) ? 18 | { dest: entryDefinition } : 19 | entryDefinition; 20 | return [entrySrcPath, entryDefinition]; 21 | }) 22 | .fromPairs() 23 | .value(); 24 | } 25 | 26 | function flattenPresets (opts) { 27 | if (!isArray(opts.presets)) { 28 | throw new Error("Provided `presets` option is not an array. This check is performed recursively."); // eslint-disable-line max-len 29 | } 30 | 31 | return opts.presets.reduce((_opts, preset) => { 32 | if (preset.presets) { preset = flattenPresets(preset); } 33 | return merge({}, preset, _opts); 34 | }, opts); 35 | } 36 | 37 | 38 | /** 39 | * The entry point for the Interlock application 40 | * 41 | * @param {Object} opts Compilation options. 42 | * 43 | * @param {Object} opts.entry A hash where keys are input files, relative to 44 | * srcRoot and treated as entry points, and values 45 | * are entry definitions. Entry definitions can be 46 | * string paths relative to destRoot, or objects with 47 | * `dest` value and other config. 48 | * @param {Object} opts.split A hash where keys are input files, relative to 49 | * srcRoot, and values are split definitions. Split 50 | * definitions can be string paths relative to 51 | * destRoot, or objects with `dest` value and other 52 | * config. 53 | * @param {String} opts.srcRoot The absolute path from which all relative source 54 | * paths will be resolved. 55 | * @param {String} opts.destRoot The absolute path from which all relative destination 56 | * paths will be resolved. 57 | * @param {String} opts.context Interlock's working directory. 58 | * @param {Array} opts.extensions The list of file extentions that Interlock will 59 | * automatically append to require-strings when 60 | * attempting to resolve that require-string. 61 | * @param {String} opts.ns The namespace for the build. If omitted, the value 62 | * will be borrowed from `name` in package.json. 63 | * @param {Boolean} opts.sourceMaps Emit source maps with the bundles. 64 | * @param {String} opts.globalName Name to use for run-time global variable. 65 | * @param {Array} opts.plugins An Array of interlock Plugins. 66 | * @param {Array} opts.presets An Array for valid compilation options objects. 67 | * @param {Boolean} opts.includeComments Include comments in the compiled bundles 68 | * @param {String} opts.implicitBundleDest The location to emit shared dependency bundles 69 | * 70 | * @returns {void} 71 | */ 72 | export default function Interlock (opts) { 73 | // The ordering of validation/flattening is important here. Presets are defined as 74 | // a shared option - so that validation should occur first. Once all nested presets 75 | // have been flattened, validation must occur on the compilation options that have 76 | // been flattened. 77 | opts = options.validate(opts, options.shared); 78 | opts = flattenPresets(opts, opts.presets); 79 | opts = options.validate(opts, options.compile); 80 | 81 | this.options = assign({}, opts, { 82 | globalName: "__interlock__", 83 | entry: normalizeEntryPoints(opts.entry), 84 | split: normalizeEntryPoints(opts.split) 85 | }); 86 | } 87 | 88 | /** 89 | * @return {Promise} Resolves to the compilation output. 90 | */ 91 | Interlock.prototype.build = function () { 92 | return compile(this.options) 93 | .then(this._saveBundles) 94 | .catch(function (err) { 95 | console.log("*** BUILD FAILED ***"); // eslint-disable-line no-console 96 | if (err && err.stack) { 97 | console.log(err.stack); // eslint-disable-line no-console 98 | } else { 99 | console.log(err); // eslint-disable-line no-console 100 | } 101 | throw err; 102 | }); 103 | }; 104 | 105 | Interlock.prototype._saveBundles = function (compilation) { 106 | for (const [bundleDest, bundle] of entries(compilation.bundles)) { 107 | const bundleOutput = bundle.raw; 108 | const destRoot = path.join(compilation.opts.destRoot, bundleDest); 109 | mkdirp(path.dirname(destRoot)); 110 | fs.writeFileSync(destRoot, bundleOutput); 111 | } 112 | return compilation; 113 | }; 114 | 115 | function getRefreshedAsset (compilation, changedFilePath) { 116 | return compilation.cache.modulesByAbsPath[changedFilePath] 117 | .then(origAsset => assign({}, origAsset, { 118 | type: null, 119 | rawSource: null, 120 | ast: null, 121 | requireNodes: null, 122 | dependencies: null, 123 | hash: null 124 | })); 125 | } 126 | 127 | Interlock.prototype.watch = function (cb, opts = {}) { 128 | const self = this; 129 | let lastCompilation = null; 130 | const absPathToModuleHash = Object.create(null); 131 | 132 | const watcher = watch([], {}); 133 | 134 | function onCompileComplete (compilation) { 135 | lastCompilation = compilation; 136 | for (const [, bundleObj] of entries(compilation.bundles)) { 137 | for (const module of bundleObj.modules || []) { 138 | watcher.add(module.path); 139 | absPathToModuleHash[module.path] = module.hash; 140 | } 141 | } 142 | if (opts.save) { self._saveBundles(compilation); } 143 | // Emit compilation. 144 | cb({ compilation }); 145 | } 146 | 147 | watcher.on("change", changedFilePath => { 148 | cb({ change: changedFilePath }); // eslint-disable-line callback-return 149 | 150 | for (const modulePath of keys(absPathToModuleHash)) { watcher.unwatch(modulePath); } 151 | 152 | getRefreshedAsset(lastCompilation, changedFilePath) 153 | .then(refreshedAsset => { 154 | delete lastCompilation.cache.modulesByAbsPath[changedFilePath]; 155 | return compileModules.call(lastCompilation, [refreshedAsset]); 156 | }) 157 | .then(patchModules => { 158 | cb({ patchModules, changedFilePath }); // eslint-disable-line callback-return 159 | return compile(lastCompilation.opts).then(onCompileComplete); 160 | }); 161 | }); 162 | 163 | compile(this.options).then(onCompileComplete); 164 | }; 165 | -------------------------------------------------------------------------------- /src/compile/construct/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { assign } from "lodash"; 5 | import Promise from "bluebird"; 6 | import * as t from "babel-types"; 7 | import template from "../../util/template"; 8 | 9 | import { pluggable } from "pluggable"; 10 | import { fromObject } from "../../util/ast"; 11 | 12 | 13 | function getTemplate (templateName, transform) { 14 | transform = transform || (node => node); 15 | const absPath = path.join(__dirname, `templates/${templateName}.jst`); 16 | const templateStr = fs.readFileSync(absPath, "utf-8") 17 | // Remove ESlint rule exclusions from parsed templates. 18 | .replace(/\s*\/\/\s*eslint-disable-line.*/g, ""); 19 | const _template = template(templateStr); 20 | return opts => transform(_template(opts)); 21 | } 22 | 23 | const commonModuleTmpl = getTemplate("common-module", node => node.expression); 24 | const moduleSetTmpl = getTemplate("module-set"); 25 | const runtimeTmpl = getTemplate("runtime"); 26 | const registerUrlsTmpl = getTemplate("register-urls"); 27 | const iifeTmpl = getTemplate("iife"); 28 | 29 | /** 30 | * Given an array of AST nodes from a module's body along with that module's 31 | * dependencies, construct an AST object expression that represents its run-time 32 | * equivalent. 33 | * 34 | * @param {Array} moduleBody Array of AST nodes. 35 | * @param {Array} deps Array of modules upon which module is dependent. 36 | * 37 | * @return {ASTnode} Object expression AST node. 38 | */ 39 | export const constructCommonModule = pluggable( 40 | function constructCommonModule (moduleBody, deps) { 41 | return commonModuleTmpl({ 42 | MODULE_BODY: moduleBody, 43 | DEPS: t.arrayExpression(deps.map(dep => t.stringLiteral(dep.id))) 44 | }); 45 | } 46 | ); 47 | 48 | function markAsEntry (moduleAst) { 49 | return assign({}, moduleAst, { 50 | properties: moduleAst.properties.concat( 51 | t.objectProperty(t.identifier("entry"), t.booleanLiteral(true)) 52 | ) 53 | }); 54 | } 55 | 56 | /** 57 | * Given an array of compiled modules, construct the AST for JavaScript that would 58 | * register those modules for consumption by the Interlock run-time. 59 | * 60 | * @param {Array} modules Array of compiled modules. 61 | * @param {String} globalName Global variable name of the Interlock run-time. 62 | * @param {String} entryModuleId Module-hash of the entry module. 63 | * 64 | * @return {Array} Array of AST nodes to be emitted as JavaScript. 65 | */ 66 | export const constructModuleSet = pluggable( 67 | function constructModuleSet (modules, globalName, entryModuleId) { 68 | return Promise.all(modules.map(module => 69 | this.constructCommonModule(module.ast.body, module.dependencies) 70 | .then(moduleAst => module.id === entryModuleId ? 71 | markAsEntry(moduleAst) : 72 | moduleAst 73 | ) 74 | .then(moduleAst => t.objectProperty(t.stringLiteral(module.id), moduleAst)) 75 | )) 76 | .then(moduleProps => moduleSetTmpl({ 77 | GLOBAL_NAME: t.stringLiteral(globalName), 78 | MODULES_HASH: t.objectExpression(moduleProps) 79 | })); 80 | }, 81 | { constructCommonModule } 82 | ); 83 | 84 | /** 85 | * Construct the guts of the Interlock run-time for inclusion in file output. 86 | * 87 | * @param {String} globalName Global variable name of Interlock run-time. 88 | * 89 | * @return {Array} Array of AST nodes. 90 | */ 91 | export const constructRuntime = pluggable(function constructRuntime (globalName) { 92 | return runtimeTmpl({ 93 | GLOBAL_NAME: t.stringLiteral(globalName) 94 | }); 95 | }); 96 | 97 | /** 98 | * Transforms a map of module-hashes-to-URLs to the AST equivalent. 99 | * 100 | * @param {Object} urls Keys are module hashes, values are URL strings. 101 | * @param {String} globalName Global variable name of Interlock run-time. 102 | * 103 | * @return {ASTnode} Single AST node. 104 | */ 105 | export const constructRegisterUrls = pluggable( 106 | function constructRegisterUrls (urls, globalName) { 107 | return registerUrlsTmpl({ 108 | GLOBAL_NAME: t.stringLiteral(globalName), 109 | URLS: fromObject(urls) 110 | }); 111 | } 112 | ); 113 | 114 | /** 115 | * Builds body of output bundle, to be inserted into the IIFE. 116 | * 117 | * @param {Object} opts Same options object passed to constructBundleBody. 118 | * 119 | * @return {Array} Body of bundle. 120 | */ 121 | export const constructBundleBody = pluggable(function constructBundleBody (opts) { 122 | return Promise.all([ 123 | opts.includeRuntime && this.constructRuntime(this.opts.globalName), 124 | opts.urls && this.constructRegisterUrls(opts.urls, this.opts.globalName), 125 | opts.modules && this.constructModuleSet( 126 | opts.modules, 127 | this.opts.globalName, 128 | opts.entryModuleId 129 | ) 130 | ]) 131 | .then(([runtime, urls, moduleSet, loadEntry]) => 132 | [].concat(runtime, urls, moduleSet, loadEntry)); 133 | }, { constructModuleSet, constructRuntime, constructRegisterUrls }); 134 | 135 | /** 136 | * Construct the AST for an output bundle. A number of optional options-args are 137 | * allowed, to give flexibility to the compiler for what sort of bundle should be 138 | * constructed. 139 | * 140 | * For example, in the case of a bundle with an entry module, you'll want everything 141 | * to be included. The run-time is needed, because there is no guarantee another 142 | * bundle has already loaded the run-time. The module-hash-to-bundle-URLs object 143 | * should be included, as again there is no guarantee another bundle has already 144 | * set those values. The modules of the bundle itself need to be included, etc. 145 | * 146 | * However, you might instead generate a specialized bundle that only contains the 147 | * run-time and URLs. This bundle might be inlined into the page, or guaranteed 148 | * to be loaded first, so that redundant copies of the run-time be included in 149 | * every other bundle generated. 150 | * 151 | * The output for this function should be a root AST node, ready to be transformed 152 | * back into JavaScript code. 153 | * 154 | * @param {Object} opts Options. 155 | * @param {Boolean} opts.includeRuntime Indicates whether Interlock run-time should be emitted. 156 | * @param {Object} opts.urls Optional. If included, map of module hashes to URLs 157 | * will be emitted. 158 | * @param {Array} opts.modules Optional. If included, the module objects will be 159 | * transformed into output module AST and emitted. 160 | * @param {String} opts.entryModuleId Optional. If included, a statement will be rendered 161 | * to invoke the specified module on load. 162 | * 163 | * @return {ASTnode} Single program AST node. 164 | */ 165 | export const constructBundleAst = pluggable(function constructBundleAst (opts) { 166 | return this.constructBundleBody(opts) 167 | .then(body => iifeTmpl({ 168 | BODY: body.filter(x => x) 169 | })); 170 | }, { constructBundleBody }); 171 | -------------------------------------------------------------------------------- /spec/src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks,no-new */ 2 | 3 | import path from "path"; 4 | 5 | import _ from "lodash"; 6 | 7 | import Interlock from "../../src/index.js"; 8 | 9 | const minimalValidConfig = { 10 | entry: { "./index.js": "bundle.js" }, 11 | srcRoot: path.join(__dirname, "/../..") 12 | }; 13 | 14 | describe("src/index", () => { 15 | describe("constructor", function () { 16 | // TODO: Test for [] and undefined. _.merge ignores those values. 17 | it("throws an Error if not passed invalid options", function () { // eslint-disable-line max-statements,max-len 18 | // Missing or empty config 19 | expect(() => { new Interlock(); }).to.throw(Error); 20 | expect(() => { new Interlock({}); }).to.throw(Error); 21 | 22 | // Invalid options.entry 23 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: true })); }) 24 | .to.throw(Error, "Received invalid value for option 'entry': true."); 25 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: 1 })); }) 26 | .to.throw(Error, "Received invalid value for option 'entry': 1."); 27 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: null })); }) 28 | .to.throw(Error, "Received invalid value for option 'entry': null."); 29 | expect(() => { 30 | const invalidConfig = _.merge({}, minimalValidConfig, 31 | { entry: null }, 32 | { entry: { "fakepath": {} } } 33 | ); 34 | new Interlock(invalidConfig); 35 | }).to.throw(Error, "Received invalid value for option 'entry': {\"fakepath\":{}}."); 36 | expect(() => { 37 | const invalidConfig = _.merge({}, minimalValidConfig, 38 | { entry: null }, 39 | { entry: { "fakepath": { dest: true } } } 40 | ); 41 | new Interlock(invalidConfig); 42 | }).to.throw( 43 | Error, 44 | "Received invalid value for option 'entry': {\"fakepath\":{\"dest\":true}}." 45 | ); 46 | 47 | // Invalid options.split 48 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: true })); }) 49 | .to.throw(Error, "Received invalid value for option 'split': true."); 50 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: 1 })); }) 51 | .to.throw(Error, "Received invalid value for option 'split': 1."); 52 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: null })); }) 53 | .to.throw(Error, "Received invalid value for option 'split': null."); 54 | expect(() => { 55 | const invalidConfig = _.merge({}, minimalValidConfig, { split: { "fakepath": {} } }); 56 | new Interlock(invalidConfig); 57 | }).to.throw(Error, "Received invalid value for option \'split\': {\"fakepath\":{}}."); 58 | expect(() => { 59 | const invalidConfig = _.merge({}, minimalValidConfig, 60 | { split: { "fakepath": { dest: true } } }); 61 | new Interlock(invalidConfig); 62 | }).to.throw( 63 | Error, 64 | "Received invalid value for option \'split\': {\"fakepath\":{\"dest\":true}}." 65 | ); 66 | 67 | // Conditional options.split || options.entry requirement 68 | expect(() => { 69 | new Interlock({ 70 | entry: { "./index.js": "bundle.js" }, 71 | srcRoot: path.join(__dirname, "/../..") 72 | }); 73 | }) 74 | .to.not.throw(Error); 75 | expect(() => { 76 | new Interlock({ 77 | split: { "./index.js": "bundle.js" }, 78 | srcRoot: path.join(__dirname, "/../..") 79 | }); 80 | }) 81 | .to.not.throw(Error); 82 | expect(() => { 83 | new Interlock({ 84 | split: { "./index.js": "bundle.js" }, 85 | entry: { "./index.js": "bundle.js" }, 86 | srcRoot: path.join(__dirname, "/../..") 87 | }); 88 | }) 89 | .to.not.throw(Error); 90 | expect(() => { 91 | new Interlock({ srcRoot: path.join(__dirname, "/../..") }); 92 | }).to.throw(Error); 93 | 94 | // Invalid options.srcRoot 95 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: true })); }) 96 | .to.throw(Error); 97 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: 1 })); }) 98 | .to.throw(Error); 99 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: null })); }) 100 | .to.throw(Error); 101 | }); 102 | 103 | it("fills in default values when not passed in", function () { 104 | const ilk = new Interlock(minimalValidConfig); 105 | 106 | // Can't do deep-equal comparison on function objects. 107 | delete ilk.options.log; 108 | 109 | expect(ilk.options).to.deep.equal({ 110 | entry: { "./index.js": { dest: "bundle.js" } }, 111 | split: {}, 112 | globalName: "__interlock__", 113 | srcRoot: path.join(__dirname, "/../.."), 114 | destRoot: path.join(__dirname, "../..", "dist"), 115 | extensions: [ ".js", ".jsx", ".es6" ], 116 | ns: "interlock", 117 | implicitBundleDest: "[setHash].js", 118 | includeComments: false, 119 | multiprocess: false, 120 | plugins: [], 121 | pretty: false, 122 | sourceMaps: false, 123 | fcache: false, 124 | presets: [], 125 | __defaults: { 126 | destRoot: true, 127 | extensions: true, 128 | fcache: true, 129 | implicitBundleDest: true, 130 | includeComments: true, 131 | log: true, 132 | multiprocess: true, 133 | ns: true, 134 | plugins: true, 135 | presets: true, 136 | pretty: true, 137 | sourceMaps: true 138 | } 139 | }); 140 | }); 141 | 142 | it("allows overrides to the default config", function () { 143 | const ilk = new Interlock({ 144 | entry: { "./index.js": "bundle.js" }, 145 | srcRoot: path.join(__dirname, "/../.."), 146 | context: "custom context", 147 | destRoot: "custom destRoot", 148 | extensions: [".custom"], 149 | ns: "custom-namespace", 150 | implicitBundleDest: "custom-dest" 151 | }); 152 | 153 | // Can't do deep-equal comparison on function objects. 154 | delete ilk.options.log; 155 | 156 | expect(ilk.options).to.deep.equal({ 157 | entry: { "./index.js": { "dest": "bundle.js" } }, 158 | split: {}, 159 | globalName: "__interlock__", 160 | srcRoot: path.join(__dirname, "/../.."), 161 | context: "custom context", 162 | destRoot: "custom destRoot", 163 | extensions: [".custom"], 164 | ns: "custom-namespace", 165 | implicitBundleDest: "custom-dest", 166 | includeComments: false, 167 | multiprocess: false, 168 | plugins: [], 169 | pretty: false, 170 | sourceMaps: false, 171 | fcache: false, 172 | presets: [], 173 | __defaults: { 174 | fcache: true, 175 | includeComments: true, 176 | log: true, 177 | multiprocess: true, 178 | plugins: true, 179 | presets: true, 180 | pretty: true, 181 | sourceMaps: true 182 | } 183 | }); 184 | }); 185 | }); 186 | 187 | describe("build", function () { 188 | it("return a Promise"); 189 | it("resolves to the compilation output"); 190 | }); 191 | describe("watch", function () { 192 | it("rebuilds on file change"); 193 | }); 194 | describe("_saveBundles", function () { 195 | it("saves output from compilation to destRoot"); 196 | it("prefixes bundles with namespace"); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /src/compile/construct/templates/runtime.jst: -------------------------------------------------------------------------------- 1 | /** 2 | * Behavior identical to _.extend. Copies key/value pairs from source 3 | * objects to destination object. 4 | * 5 | * @param {Object} dest Destination for key/value pairs. 6 | * @param {Object} sources One or more sources for key/value pairs. 7 | * 8 | * @return {Object} Destination object. 9 | */ 10 | function copyProps (dest/*, sources... */) { 11 | var len = arguments.length; 12 | if (arguments.length < 2 || dest === null) { return dest; } 13 | Array.prototype.splice.call(arguments, 1).forEach(function (src) { 14 | Object.keys(src).forEach(function (key) { dest[key] = src[key]; }); 15 | }); 16 | return dest; 17 | } 18 | 19 | /** 20 | * This object is exposed to the browser environment and is accessible 21 | * to chunks that are loaded asynchronously. It represents the core 22 | * of the Interlock runtime. 23 | */ 24 | var r = window[GLOBAL_NAME] = window[GLOBAL_NAME] || { // eslint-disable-line no-undef 25 | modules: {}, 26 | urls: {}, 27 | providers: [ 28 | /** 29 | * Default module provider. Builds a