├── 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 |
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