Skip to content

Commit

Permalink
added support for virtual bundles
Browse files Browse the repository at this point in the history
this allows compiling bundles programmatically, using strings rather
than files as entry points

the implementation is a little convoluted (cf. inline comments), but
works well with faucet's intentional constraints - see
rollup/rollup#2509 (comment)
for details
  • Loading branch information
FND committed Nov 7, 2018
1 parent d6c7653 commit 3edca46
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
@@ -0,0 +1 @@
/test/unit/expected
43 changes: 43 additions & 0 deletions lib/bundle/diskless.js
@@ -0,0 +1,43 @@
"use strict";

let path = require("path");

let PREFIX = "diskless:";

// Rollup plugin for virtual modules
// `referenceDir` is used for relative imports from diskless modules
// `resolver` is the Rollup plugin responsible for import paths
// `modules` maps file names to source code
module.exports = (referenceDir, resolver, modules = new Map(), prefix = PREFIX) => ({
name: "diskless",
resolveId(importee, importer) {
if(importer && importer.startsWith(prefix)) {
let virtual = path.resolve(referenceDir, importer);
// this is pretty hacky, but necessary because Rollup doesn't
// support combining different plugins' `#resolveId`
return resolver.resolveId(importee, virtual);
}
return importee.startsWith(prefix) ? importee : null;
},
load(id) {
if(!id.startsWith(prefix)) {
return null;
}

let filename = id.substr(prefix.length);
let source = modules.get(filename);
if(source === undefined) {
throw new Error(`missing diskless module: ${filename}`);
}
return source;
},
register(filename, source) {
modules.set(filename, source);
},
deregister(filename) {
return modules.delete(filename);
},
get prefix() {
return prefix;
}
});
48 changes: 48 additions & 0 deletions lib/bundle/virtual.js
@@ -0,0 +1,48 @@
"use strict";

let BasicBundle = require("./basic");
let diskless = require("./diskless");
let crypto = require("crypto");

exports.VirtualBundle = class VirtualBundle extends BasicBundle {
constructor(referenceDir, config, { browsers }) {
super(config, { browsers });
// inject diskless plugin, initializing it with existing resolver
// this is pretty convoluted, but necessary due to Rollup limitations
// (see diskless internals for details)
let { plugins } = this._config.readConfig;
let resolver = plugins.find(plugin => plugin.name === "node-resolve");
let plugin = diskless(referenceDir, resolver);
plugins.unshift(plugin);
this.diskless = plugin;
}

compile(source) {
let { diskless } = this;
// NB: unique-ish ID avoids potential race condition for concurrent
// access with identical sources
// TODO: does file extension matter?
let id = generateHash(new Date().getTime() + source);
let filename = `entry_point_${id}.js`;

diskless.register(filename, source);
let cleanup = () => void diskless.deregister(filename);

return super.compile(diskless.prefix + filename).
then(res => {
cleanup();
return res;
}).
catch(err => {
cleanup();
throw err;
});
}
};

// XXX: duplicates private faucet-core's fingerprinting
function generateHash(str) {
let hash = crypto.createHash("md5");
hash.update(str);
return hash.digest("hex");
}
12 changes: 12 additions & 0 deletions test/unit/expected/virtual_bundle_js1.js
@@ -0,0 +1,12 @@
(function () {
'use strict';

if(typeof global === "undefined" && typeof window !== "undefined") {
window.global = window;
}

var UTIL = "UTIL";

console.log(UTIL);

}());
14 changes: 14 additions & 0 deletions test/unit/expected/virtual_bundle_js2.js
@@ -0,0 +1,14 @@
(function () {
'use strict';

if(typeof global === "undefined" && typeof window !== "undefined") {
window.global = window;
}

var UTIL = "UTIL";

var MYLIB = "MY-LIB";

console.log(UTIL + MYLIB);

}());
32 changes: 32 additions & 0 deletions test/unit/expected/virtual_bundle_jsx.js
@@ -0,0 +1,32 @@
'use strict';

if(typeof global === "undefined" && typeof window !== "undefined") {
window.global = window;
}

var UTIL = "UTIL";

var MYLIB = "MY-LIB";

function createElement(tag, params, ...children) {
return `<${tag} ${JSON.stringify(params)}>${JSON.stringify(children)}</${tag}>`;
}

function Button({
type,
label
}) {
return createElement("button", {
type: type
}, label);
}
function List(_, ...children) {
return createElement("ul", null, children.map(item => createElement("li", null, item)));
}

console.log(createElement(List, null, createElement(Button, {
label: UTIL
}), createElement(Button, {
type: "reset",
label: MYLIB
})));
13 changes: 13 additions & 0 deletions test/unit/fixtures/node_modules/my-lib/components.jsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test/unit/fixtures/node_modules/my-lib/elements.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions test/unit/test_virtual.js
@@ -0,0 +1,62 @@
/* global describe, it */
"use strict";

let { FIXTURES_DIR } = require("./util");
let { VirtualBundle } = require("../../lib/bundle/virtual");
let fs = require("fs");
let path = require("path");
let assert = require("assert");

let assertSame = assert.strictEqual;

let DEFAULT_OPTIONS = {
browsers: {}
};

describe("virtual bundle", _ => {
it("should bundle JavaScript from a source string", async () => {
let bundle = new VirtualBundle(FIXTURES_DIR, null, DEFAULT_OPTIONS);

let res = await bundle.compile(`
import UTIL from "./src/util";
console.log(UTIL);
`);
assertSame(res.error, undefined);
assertSame(res.code, expectedBundle("virtual_bundle_js1.js"));

res = await bundle.compile(`
import UTIL from "./src/util";
import MYLIB from "my-lib";
console.log(UTIL + MYLIB);
`);
assertSame(res.error, undefined);
assertSame(res.code, expectedBundle("virtual_bundle_js2.js"));
});

it("should support JSX", async () => {
let bundle = new VirtualBundle(FIXTURES_DIR, {
format: "CommonJS",
jsx: { pragma: "createElement" }
}, DEFAULT_OPTIONS);

let { code, error } = await bundle.compile(`
import UTIL from "./src/util";
import MYLIB from "my-lib";
import { Button, List } from "my-lib/components";
import createElement from "my-lib/elements";
console.log(<List>
<Button label={UTIL} />
<Button type="reset" label={MYLIB} />
</List>);
`);
assertSame(error, undefined);
assertSame(code, expectedBundle("virtual_bundle_jsx.js"));
});
});

function expectedBundle(filename) {
return fs.readFileSync(path.resolve(__dirname, "expected", filename), "utf8");
}

0 comments on commit 3edca46

Please sign in to comment.