From 3edca468f5d4a57646cbca2dbac596ebee92a299 Mon Sep 17 00:00:00 2001 From: FND Date: Wed, 31 Oct 2018 18:07:27 +0100 Subject: [PATCH] added support for virtual bundles 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 https://github.com/rollup/rollup/issues/2509#issuecomment-434387083 for details --- .eslintignore | 1 + lib/bundle/diskless.js | 43 +++++++++++++ lib/bundle/virtual.js | 48 ++++++++++++++ test/unit/expected/virtual_bundle_js1.js | 12 ++++ test/unit/expected/virtual_bundle_js2.js | 14 +++++ test/unit/expected/virtual_bundle_jsx.js | 32 ++++++++++ .../node_modules/my-lib/components.jsx | 13 ++++ .../fixtures/node_modules/my-lib/elements.js | 3 + test/unit/test_virtual.js | 62 +++++++++++++++++++ 9 files changed, 228 insertions(+) create mode 100644 .eslintignore create mode 100644 lib/bundle/diskless.js create mode 100644 lib/bundle/virtual.js create mode 100644 test/unit/expected/virtual_bundle_js1.js create mode 100644 test/unit/expected/virtual_bundle_js2.js create mode 100644 test/unit/expected/virtual_bundle_jsx.js create mode 100644 test/unit/fixtures/node_modules/my-lib/components.jsx create mode 100644 test/unit/fixtures/node_modules/my-lib/elements.js create mode 100644 test/unit/test_virtual.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..cf7ad1b --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/test/unit/expected diff --git a/lib/bundle/diskless.js b/lib/bundle/diskless.js new file mode 100644 index 0000000..fc428b6 --- /dev/null +++ b/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; + } +}); diff --git a/lib/bundle/virtual.js b/lib/bundle/virtual.js new file mode 100644 index 0000000..4d9bb28 --- /dev/null +++ b/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"); +} diff --git a/test/unit/expected/virtual_bundle_js1.js b/test/unit/expected/virtual_bundle_js1.js new file mode 100644 index 0000000..e589967 --- /dev/null +++ b/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); + +}()); diff --git a/test/unit/expected/virtual_bundle_js2.js b/test/unit/expected/virtual_bundle_js2.js new file mode 100644 index 0000000..1856a77 --- /dev/null +++ b/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); + +}()); diff --git a/test/unit/expected/virtual_bundle_jsx.js b/test/unit/expected/virtual_bundle_jsx.js new file mode 100644 index 0000000..66b3c80 --- /dev/null +++ b/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)}`; +} + +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 +}))); diff --git a/test/unit/fixtures/node_modules/my-lib/components.jsx b/test/unit/fixtures/node_modules/my-lib/components.jsx new file mode 100644 index 0000000..3b04aec --- /dev/null +++ b/test/unit/fixtures/node_modules/my-lib/components.jsx @@ -0,0 +1,13 @@ +import createElement from "./elements"; + +export function Button({ type, label }) { + return ; +} + +export function List(_, ...children) { + return ; +} diff --git a/test/unit/fixtures/node_modules/my-lib/elements.js b/test/unit/fixtures/node_modules/my-lib/elements.js new file mode 100644 index 0000000..99c4f2d --- /dev/null +++ b/test/unit/fixtures/node_modules/my-lib/elements.js @@ -0,0 +1,3 @@ +export default function createElement(tag, params, ...children) { + return `<${tag} ${JSON.stringify(params)}>${JSON.stringify(children)}`; +} diff --git a/test/unit/test_virtual.js b/test/unit/test_virtual.js new file mode 100644 index 0000000..7e4827b --- /dev/null +++ b/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( +