Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bundle loader #480

Merged
merged 5 commits into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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