Skip to content

Commit

Permalink
Add bundle loader (denoland/std#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk authored and ry committed Jun 10, 2019
1 parent 87d3b9b commit 0ed3046
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 0 deletions.
16 changes: 16 additions & 0 deletions bundle/README.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions bundle/run.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const text = await load(args);
const result = evaluate(text);
instantiate(...result);
}

main(Deno.args);
111 changes: 111 additions & 0 deletions bundle/test.ts
Original file line number Diff line number Diff line change
@@ -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<string, ModuleMetaData>();
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<void> {
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<void> {
await assertThrowsAsync(
async (): Promise<void> => {
await load(["bundle/test.ts"]);
},
AssertionError,
"Expected exactly two arguments."
);
});

test(async function loadMissingBundle(): Promise<void> {
await assertThrowsAsync(
async (): Promise<void> => {
await load([".", "bad_bundle.js"]);
},
AssertionError,
`Expected "bad_bundle.js" to exist.`
);
});

test(async function evaluateBundle(): Promise<void> {
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<void> {
assert(globalThis.__results == null);
instantiate(fixtureQueue, fixtureModules);
assertEquals(globalThis.__results, ["bar", "qat"]);
delete globalThis.__results;
});
67 changes: 67 additions & 0 deletions bundle/testdata/bundle.js

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

108 changes: 108 additions & 0 deletions bundle/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string, ModuleMetaData>] {
const queue: string[] = [];
const modules = new Map<string, ModuleMetaData>();

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<string, ModuleMetaData>
): 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<string> {
// 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));
}
1 change: 1 addition & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit 0ed3046

Please sign in to comment.