diff --git a/bundle/README.md b/bundle/README.md new file mode 100644 index 0000000000..ecf105d15e --- /dev/null +++ b/bundle/README.md @@ -0,0 +1,16 @@ +# bundle + +These are modules that help support bundling with Deno. + +## Usage + +The main usage is to load and run bundles. For example, to run a bundle named +`bundle.js` in your current working directory: + +```sh +deno run https://deno.land/std/bundle/run.ts bundle.js +``` + +--- + +Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. diff --git a/bundle/run.ts b/bundle/run.ts new file mode 100644 index 0000000000..fb848a671e --- /dev/null +++ b/bundle/run.ts @@ -0,0 +1,11 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { evaluate, instantiate, load } from "./utils.ts"; + +async function main(args: string[]): Promise { + const text = await load(args); + const result = evaluate(text); + instantiate(...result); +} + +main(Deno.args); diff --git a/bundle/test.ts b/bundle/test.ts new file mode 100644 index 0000000000..ed9ba62ce5 --- /dev/null +++ b/bundle/test.ts @@ -0,0 +1,111 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { test } from "../testing/mod.ts"; +import { + assert, + AssertionError, + assertStrictEq, + assertThrowsAsync +} from "../testing/asserts.ts"; +import { assertEquals } from "../testing/pretty.ts"; +import { evaluate, instantiate, load, ModuleMetaData } from "./utils.ts"; + +/* eslint-disable @typescript-eslint/no-namespace */ +declare global { + namespace globalThis { + var __results: [string, string] | undefined; + } +} +/* eslint-enable @typescript-eslint/no-namespace */ + +const fixture = ` +define("data", [], { "baz": "qat" }); +define("modB", ["require", "exports", "data"], function(require, exports, data) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.foo = "bar"; + exports.baz = data.baz; +}); +define("modA", ["require", "exports", "modB"], function(require, exports, modB) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + globalThis.__results = [modB.foo, modB.baz]; +}); +`; + +const fixtureQueue = ["data", "modB", "modA"]; +const fixtureModules = new Map(); +fixtureModules.set("data", { + dependencies: [], + factory: { + baz: "qat" + }, + exports: {} +}); +fixtureModules.set("modB", { + dependencies: ["require", "exports", "data"], + factory(_require, exports, data): void { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.foo = "bar"; + exports.baz = data.baz; + }, + exports: {} +}); +fixtureModules.set("modA", { + dependencies: ["require", "exports", "modB"], + factory(_require, exports, modB): void { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + globalThis.__results = [modB.foo, modB.baz]; + }, + exports: {} +}); + +test(async function loadBundle(): Promise { + const result = await load(["", "./bundle/testdata/bundle.js"]); + assert(result != null); + assert( + result.includes( + `define("subdir/print_hello", ["require", "exports"], function(` + ) + ); +}); + +test(async function loadBadArgs(): Promise { + await assertThrowsAsync( + async (): Promise => { + await load(["bundle/test.ts"]); + }, + AssertionError, + "Expected exactly two arguments." + ); +}); + +test(async function loadMissingBundle(): Promise { + await assertThrowsAsync( + async (): Promise => { + await load([".", "bad_bundle.js"]); + }, + AssertionError, + `Expected "bad_bundle.js" to exist.` + ); +}); + +test(async function evaluateBundle(): Promise { + assert(globalThis.define == null, "Expected 'define' to be undefined"); + const [queue, modules] = evaluate(fixture); + assert(globalThis.define == null, "Expected 'define' to be undefined"); + assertEquals(queue, ["data", "modB", "modA"]); + assert(modules.has("modA")); + assert(modules.has("modB")); + assert(modules.has("data")); + assertStrictEq(modules.size, 3); +}); + +test(async function instantiateBundle(): Promise { + assert(globalThis.__results == null); + instantiate(fixtureQueue, fixtureModules); + assertEquals(globalThis.__results, ["bar", "qat"]); + delete globalThis.__results; +}); diff --git a/bundle/testdata/bundle.js b/bundle/testdata/bundle.js new file mode 100644 index 0000000000..6758fd2781 --- /dev/null +++ b/bundle/testdata/bundle.js @@ -0,0 +1,67 @@ +define("subdir/print_hello", ["require", "exports"], function( + require, + exports +) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + function printHello() { + console.log("Hello"); + } + exports.printHello = printHello; +}); +define("subdir/subdir2/mod2", [ + "require", + "exports", + "subdir/print_hello" +], function(require, exports, print_hello_ts_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + function returnsFoo() { + return "Foo"; + } + exports.returnsFoo = returnsFoo; + function printHello2() { + print_hello_ts_1.printHello(); + } + exports.printHello2 = printHello2; +}); +define("subdir/mod1", ["require", "exports", "subdir/subdir2/mod2"], function( + require, + exports, + mod2_ts_1 +) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + function returnsHi() { + return "Hi"; + } + exports.returnsHi = returnsHi; + function returnsFoo2() { + return mod2_ts_1.returnsFoo(); + } + exports.returnsFoo2 = returnsFoo2; + function printHello3() { + mod2_ts_1.printHello2(); + } + exports.printHello3 = printHello3; + function throwsError() { + throw Error("exception from mod1"); + } + exports.throwsError = throwsError; +}); +define("005_more_imports", ["require", "exports", "subdir/mod1"], function( + require, + exports, + mod1_ts_1 +) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + mod1_ts_1.printHello3(); + if (mod1_ts_1.returnsHi() !== "Hi") { + throw Error("Unexpected"); + } + if (mod1_ts_1.returnsFoo2() !== "Foo") { + throw Error("Unexpected"); + } +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvc3ViZGlyL3ByaW50X2hlbGxvLnRzIiwiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvc3ViZGlyL3N1YmRpcjIvbW9kMi50cyIsImZpbGU6Ly8vVXNlcnMva2tlbGx5L2dpdGh1Yi9kZW5vL3Rlc3RzL3N1YmRpci9tb2QxLnRzIiwiZmlsZTovLy9Vc2Vycy9ra2VsbHkvZ2l0aHViL2Rlbm8vdGVzdHMvMDA1X21vcmVfaW1wb3J0cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7SUFBQSxTQUFnQixVQUFVO1FBQ3hCLE9BQU8sQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdkIsQ0FBQztJQUZELGdDQUVDOzs7OztJQ0FELFNBQWdCLFVBQVU7UUFDeEIsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRkQsZ0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLDJCQUFVLEVBQUUsQ0FBQztJQUNmLENBQUM7SUFGRCxrQ0FFQzs7Ozs7SUNORCxTQUFnQixTQUFTO1FBQ3ZCLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUZELDhCQUVDO0lBRUQsU0FBZ0IsV0FBVztRQUN6QixPQUFPLG9CQUFVLEVBQUUsQ0FBQztJQUN0QixDQUFDO0lBRkQsa0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLHFCQUFXLEVBQUUsQ0FBQztJQUNoQixDQUFDO0lBRkQsa0NBRUM7SUFFRCxTQUFnQixXQUFXO1FBQ3pCLE1BQU0sS0FBSyxDQUFDLHFCQUFxQixDQUFDLENBQUM7SUFDckMsQ0FBQztJQUZELGtDQUVDOzs7OztJQ2RELHFCQUFXLEVBQUUsQ0FBQztJQUVkLElBQUksbUJBQVMsRUFBRSxLQUFLLElBQUksRUFBRTtRQUN4QixNQUFNLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQztLQUMzQjtJQUVELElBQUkscUJBQVcsRUFBRSxLQUFLLEtBQUssRUFBRTtRQUMzQixNQUFNLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQztLQUMzQiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBwcmludEhlbGxvKCk6IHZvaWQge1xuICBjb25zb2xlLmxvZyhcIkhlbGxvXCIpO1xufVxuIiwiaW1wb3J0IHsgcHJpbnRIZWxsbyB9IGZyb20gXCIuLi9wcmludF9oZWxsby50c1wiO1xuXG5leHBvcnQgZnVuY3Rpb24gcmV0dXJuc0ZvbygpOiBzdHJpbmcge1xuICByZXR1cm4gXCJGb29cIjtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHByaW50SGVsbG8yKCk6IHZvaWQge1xuICBwcmludEhlbGxvKCk7XG59XG4iLCJpbXBvcnQgeyByZXR1cm5zRm9vLCBwcmludEhlbGxvMiB9IGZyb20gXCIuL3N1YmRpcjIvbW9kMi50c1wiO1xuXG5leHBvcnQgZnVuY3Rpb24gcmV0dXJuc0hpKCk6IHN0cmluZyB7XG4gIHJldHVybiBcIkhpXCI7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZXR1cm5zRm9vMigpOiBzdHJpbmcge1xuICByZXR1cm4gcmV0dXJuc0ZvbygpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcHJpbnRIZWxsbzMoKTogdm9pZCB7XG4gIHByaW50SGVsbG8yKCk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiB0aHJvd3NFcnJvcigpOiB2b2lkIHtcbiAgdGhyb3cgRXJyb3IoXCJleGNlcHRpb24gZnJvbSBtb2QxXCIpO1xufVxuIiwiaW1wb3J0IHsgcmV0dXJuc0hpLCByZXR1cm5zRm9vMiwgcHJpbnRIZWxsbzMgfSBmcm9tIFwiLi9zdWJkaXIvbW9kMS50c1wiO1xuXG5wcmludEhlbGxvMygpO1xuXG5pZiAocmV0dXJuc0hpKCkgIT09IFwiSGlcIikge1xuICB0aHJvdyBFcnJvcihcIlVuZXhwZWN0ZWRcIik7XG59XG5cbmlmIChyZXR1cm5zRm9vMigpICE9PSBcIkZvb1wiKSB7XG4gIHRocm93IEVycm9yKFwiVW5leHBlY3RlZFwiKTtcbn1cbiJdfQ== diff --git a/bundle/utils.ts b/bundle/utils.ts new file mode 100644 index 0000000000..93983e47c3 --- /dev/null +++ b/bundle/utils.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { assertStrictEq, assert } from "../testing/asserts.ts"; +import { exists } from "../fs/exists.ts"; + +export interface DefineFactory { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (...args: any): object | void; +} + +export interface ModuleMetaData { + dependencies: string[]; + factory?: DefineFactory | object; + exports: object; +} + +type Define = ( + id: string, + dependencies: string[], + factory: DefineFactory +) => void; + +/* eslint-disable @typescript-eslint/no-namespace */ +declare global { + namespace globalThis { + var define: Define | undefined; + } +} +/* eslint-enable @typescript-eslint/no-namespace */ + +/** Evaluate the bundle, returning a queue of module IDs and their data to + * instantiate. + */ +export function evaluate( + text: string +): [string[], Map] { + const queue: string[] = []; + const modules = new Map(); + + globalThis.define = function define( + id: string, + dependencies: string[], + factory: DefineFactory + ): void { + modules.set(id, { + dependencies, + factory, + exports: {} + }); + queue.push(id); + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Deno as any).core.evalContext(text); + // Deleting `define()` so it isn't accidentally there when the modules + // instantiate. + delete globalThis.define; + + return [queue, modules]; +} + +/** Drain the queue of module IDs while instantiating the modules. */ +export function instantiate( + queue: string[], + modules: Map +): void { + let id: string | undefined; + while ((id = queue.shift())) { + const module = modules.get(id)!; + assert(module != null); + assert(module.factory != null); + + const dependencies = module.dependencies.map( + (id): object => { + if (id === "require") { + // TODO(kitsonk) support dynamic import by passing a `require()` that + // can return a local module or dynamically import one. + return (): void => {}; + } else if (id === "exports") { + return module.exports; + } + const dep = modules.get(id)!; + assert(dep != null); + return dep.exports; + } + ); + + if (typeof module.factory === "function") { + module.factory!(...dependencies); + } else if (module.factory) { + // when bundling JSON, TypeScript just emits it as an object/array as the + // third argument of the `define()`. + module.exports = module.factory; + } + delete module.factory; + } +} + +/** Load the bundle and return the contents asynchronously. */ +export async function load(args: string[]): Promise { + // TODO(kitsonk) allow loading of remote bundles via fetch. + assertStrictEq(args.length, 2, "Expected exactly two arguments."); + const [, bundleFileName] = args; + assert( + await exists(bundleFileName), + `Expected "${bundleFileName}" to exist.` + ); + return new TextDecoder().decode(await Deno.readFile(bundleFileName)); +} diff --git a/test.ts b/test.ts index edaca7a213..864f1b5112 100755 --- a/test.ts +++ b/test.ts @@ -2,6 +2,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import "./archive/tar_test.ts"; import "./bytes/test.ts"; +import "./bundle/test.ts"; import "./colors/test.ts"; import "./datetime/test.ts"; import "./encoding/test.ts";