0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

Make bundles fully standalone (#3325)

- Bundles are fully standalone. They now include the shared loader with
  `deno_typescript`.
- Refactor of the loader in `deno_typescript` to perform module
  instantiation in a more
- Change of behaviour when an output file is not specified on the CLI.
  Previously a default name was determined and the bundle written to that
  file, now the bundle will be sent to `stdout`.
- Refactors in the TypeScript compiler to be able to support the concept
  of a request type.  This provides a cleaner abstraction and makes it
  easier to support things like single module transpiles to the userland.
- Remove a "dangerous" circular dependency between `os.ts` and `deno.ts`,
  and define `pid` and `noColor` in a better way.
- Don't bind early to `console` in `repl.ts`.
- Add an integration test for generating a bundle.
This commit is contained in:
Kitson Kelly 2019-11-14 02:35:56 +11:00 committed by Ry Dahl
parent ee1b8dc883
commit 8d03397293
21 changed files with 335 additions and 479 deletions

View file

@ -156,20 +156,23 @@ impl CompiledFileMetadata {
}
/// Creates the JSON message send to compiler.ts's onmessage.
fn req(
request_type: msg::CompilerRequestType,
root_names: Vec<String>,
compiler_config: CompilerConfig,
bundle: Option<String>,
out_file: Option<String>,
) -> Buf {
let j = match (compiler_config.path, compiler_config.content) {
(Some(config_path), Some(config_data)) => json!({
"type": request_type as i32,
"rootNames": root_names,
"bundle": bundle,
"outFile": out_file,
"configPath": config_path,
"config": str::from_utf8(&config_data).unwrap(),
}),
_ => json!({
"type": request_type as i32,
"rootNames": root_names,
"bundle": bundle,
"outFile": out_file,
}),
};
@ -250,7 +253,7 @@ impl TsCompiler {
self: &Self,
global_state: ThreadSafeGlobalState,
module_name: String,
out_file: String,
out_file: Option<String>,
) -> impl Future<Item = (), Error = ErrBox> {
debug!(
"Invoking the compiler to bundle. module_name: {}",
@ -258,7 +261,12 @@ impl TsCompiler {
);
let root_names = vec![module_name.clone()];
let req_msg = req(root_names, self.config.clone(), Some(out_file));
let req_msg = req(
msg::CompilerRequestType::Bundle,
root_names,
self.config.clone(),
out_file,
);
let worker = TsCompiler::setup_worker(global_state.clone());
let worker_ = worker.clone();
@ -360,7 +368,12 @@ impl TsCompiler {
);
let root_names = vec![module_url.to_string()];
let req_msg = req(root_names, self.config.clone(), None);
let req_msg = req(
msg::CompilerRequestType::Compile,
root_names,
self.config.clone(),
None,
);
let worker = TsCompiler::setup_worker(global_state.clone());
let worker_ = worker.clone();
@ -709,7 +722,7 @@ mod tests {
.bundle_async(
state.clone(),
module_name,
String::from("$deno$/bundle.js"),
Some(String::from("$deno$/bundle.js")),
)
.then(|result| {
assert!(result.is_ok());

View file

@ -6,7 +6,6 @@ use clap::Arg;
use clap::ArgMatches;
use clap::Shell;
use clap::SubCommand;
use deno::ModuleSpecifier;
use log::Level;
use std;
use std::str;
@ -259,11 +258,16 @@ compiler.",
SubCommand::with_name("bundle")
.about("Bundle module and dependencies into single file")
.long_about(
"Output a single JavaScript file with all dependencies
"Output a single JavaScript file with all dependencies.
If a out_file argument is omitted, the output of the bundle will be sent to
standard out.
Example:
deno bundle https://deno.land/std/examples/colors.ts"
deno bundle https://deno.land/std/examples/colors.ts
deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js"
)
.arg(Arg::with_name("source_file").takes_value(true).required(true))
.arg(Arg::with_name("out_file").takes_value(true).required(false)),
@ -793,32 +797,6 @@ pub enum DenoSubcommand {
Version,
}
fn get_default_bundle_filename(source_file: &str) -> String {
let specifier = ModuleSpecifier::resolve_url_or_path(source_file).unwrap();
let path_segments = specifier.as_url().path_segments().unwrap();
let file_name = path_segments.filter(|s| !s.is_empty()).last().unwrap();
let file_stem = file_name.trim_end_matches(".ts").trim_end_matches(".js");
format!("{}.bundle.js", file_stem)
}
#[test]
fn test_get_default_bundle_filename() {
assert_eq!(get_default_bundle_filename("blah.ts"), "blah.bundle.js");
assert_eq!(
get_default_bundle_filename("http://example.com/blah.ts"),
"blah.bundle.js"
);
assert_eq!(get_default_bundle_filename("blah.js"), "blah.bundle.js");
assert_eq!(
get_default_bundle_filename("http://example.com/blah.js"),
"blah.bundle.js"
);
assert_eq!(
get_default_bundle_filename("http://zombo.com/stuff/"),
"stuff.bundle.js"
);
}
pub fn flags_from_vec(
args: Vec<String>,
) -> (DenoFlags, DenoSubcommand, Vec<String>) {
@ -835,11 +813,13 @@ pub fn flags_from_vec(
("bundle", Some(bundle_match)) => {
flags.allow_write = true;
let source_file: &str = bundle_match.value_of("source_file").unwrap();
let out_file = bundle_match
.value_of("out_file")
.map(String::from)
.unwrap_or_else(|| get_default_bundle_filename(source_file));
argv.extend(vec![source_file.to_string(), out_file.to_string()]);
let out_file = bundle_match.value_of("out_file").map(String::from);
match out_file {
Some(out_file) => {
argv.extend(vec![source_file.to_string(), out_file.to_string()])
}
_ => argv.extend(vec![source_file.to_string()]),
}
DenoSubcommand::Bundle
}
("completions", Some(completions_match)) => {

View file

@ -31,6 +31,13 @@ enum MediaType {
Unknown = 5
}
// Warning! The values in this enum are duplicated in cli/msg.rs
// Update carefully!
enum CompilerRequestType {
Compile = 0,
Bundle = 1
}
// Startup boilerplate. This is necessary because the compiler has its own
// snapshot. (It would be great if we could remove these things or centralize
// them somewhere else.)
@ -44,16 +51,23 @@ window["denoMain"] = denoMain;
const ASSETS = "$asset$";
const OUT_DIR = "$deno$";
const BUNDLE_LOADER = "bundle_loader.js";
/** The format of the work message payload coming from the privileged side */
interface CompilerReq {
type CompilerRequest = {
rootNames: string[];
bundle?: string;
// TODO(ry) add compiler config to this interface.
// options: ts.CompilerOptions;
configPath?: string;
config?: string;
}
} & (
| {
type: CompilerRequestType.Compile;
}
| {
type: CompilerRequestType.Bundle;
outFile?: string;
});
interface ConfigureResponse {
ignoredOptions?: string[];
@ -271,7 +285,7 @@ function fetchSourceFiles(
async function processImports(
specifiers: Array<[string, string]>,
referrer = ""
): Promise<void> {
): Promise<SourceFileJson[]> {
if (!specifiers.length) {
return;
}
@ -287,6 +301,7 @@ async function processImports(
await processImports(sourceFile.imports(), sourceFile.url);
}
}
return sourceFiles;
}
/** Utility function to turn the number of bytes into a human readable
@ -314,16 +329,36 @@ function cache(extension: string, moduleId: string, contents: string): void {
const encoder = new TextEncoder();
/** Given a fileName and the data, emit the file to the file system. */
function emitBundle(fileName: string, data: string): void {
function emitBundle(
rootNames: string[],
fileName: string | undefined,
data: string,
sourceFiles: readonly ts.SourceFile[]
): void {
// For internal purposes, when trying to emit to `$deno$` just no-op
if (fileName.startsWith("$deno$")) {
if (fileName && fileName.startsWith("$deno$")) {
console.warn("skipping emitBundle", fileName);
return;
}
const encodedData = encoder.encode(data);
console.log(`Emitting bundle to "${fileName}"`);
writeFileSync(fileName, encodedData);
console.log(`${humanFileSize(encodedData.length)} emitted.`);
const loader = fetchAsset(BUNDLE_LOADER);
// when outputting to AMD and a single outfile, TypeScript makes up the module
// specifiers which are used to define the modules, and doesn't expose them
// publicly, so we have to try to replicate
const sources = sourceFiles.map(sf => sf.fileName);
const sharedPath = util.commonPath(sources);
rootNames = rootNames.map(id =>
id.replace(sharedPath, "").replace(/\.\w+$/i, "")
);
const instantiate = `instantiate(${JSON.stringify(rootNames)});\n`;
const bundle = `${loader}\n${data}\n${instantiate}`;
if (fileName) {
const encodedData = encoder.encode(bundle);
console.warn(`Emitting bundle to "${fileName}"`);
writeFileSync(fileName, encodedData);
console.warn(`${humanFileSize(encodedData.length)} emitted.`);
} else {
console.log(bundle);
}
}
/** Returns the TypeScript Extension enum for a given media type. */
@ -380,17 +415,23 @@ class Host implements ts.CompilerHost {
/** Provides the `ts.HostCompiler` interface for Deno.
*
* @param _rootNames A set of modules that are the ones that should be
* instantiated first. Used when generating a bundle.
* @param _bundle Set to a string value to configure the host to write out a
* bundle instead of caching individual files.
*/
constructor(private _bundle?: string) {
if (this._bundle) {
constructor(
private _requestType: CompilerRequestType,
private _rootNames: string[],
private _outFile?: string
) {
if (this._requestType === CompilerRequestType.Bundle) {
// options we need to change when we are generating a bundle
const bundlerOptions: ts.CompilerOptions = {
module: ts.ModuleKind.AMD,
inlineSourceMap: true,
outDir: undefined,
outFile: `${OUT_DIR}/bundle.js`,
// disabled until we have effective way to modify source maps
sourceMap: false
};
Object.assign(this._options, bundlerOptions);
@ -531,10 +572,11 @@ class Host implements ts.CompilerHost {
): void {
util.log("compiler::host.writeFile", fileName);
try {
if (this._bundle) {
emitBundle(this._bundle, data);
assert(sourceFiles != null);
if (this._requestType === CompilerRequestType.Bundle) {
emitBundle(this._rootNames, this._outFile, data, sourceFiles!);
} else {
assert(sourceFiles != null && sourceFiles.length == 1);
assert(sourceFiles.length == 1);
const url = sourceFiles![0].fileName;
const sourceFile = SourceFile.get(url);
@ -579,16 +621,29 @@ class Host implements ts.CompilerHost {
// lazy instantiating the compiler web worker
window.compilerMain = function compilerMain(): void {
// workerMain should have already been called since a compiler is a worker.
window.onmessage = async ({ data }: { data: CompilerReq }): Promise<void> => {
const { rootNames, configPath, config, bundle } = data;
util.log(">>> compile start", { rootNames, bundle });
window.onmessage = async ({
data: request
}: {
data: CompilerRequest;
}): Promise<void> => {
const { rootNames, configPath, config } = request;
util.log(">>> compile start", {
rootNames,
type: CompilerRequestType[request.type]
});
// This will recursively analyse all the code for other imports, requesting
// those from the privileged side, populating the in memory cache which
// will be used by the host, before resolving.
await processImports(rootNames.map(rootName => [rootName, rootName]));
const resolvedRootModules = (await processImports(
rootNames.map(rootName => [rootName, rootName])
)).map(info => info.url);
const host = new Host(bundle);
const host = new Host(
request.type,
resolvedRootModules,
request.type === CompilerRequestType.Bundle ? request.outFile : undefined
);
let emitSkipped = true;
let diagnostics: ts.Diagnostic[] | undefined;
@ -642,8 +697,9 @@ window.compilerMain = function compilerMain(): void {
// We will only proceed with the emit if there are no diagnostics.
if (diagnostics && diagnostics.length === 0) {
if (bundle) {
console.log(`Bundling "${bundle}"`);
if (request.type === CompilerRequestType.Bundle) {
// warning so it goes to stderr instead of stdout
console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`);
}
const emitResult = program.emit();
emitSkipped = emitResult.emitSkipped;
@ -662,7 +718,10 @@ window.compilerMain = function compilerMain(): void {
postMessage(result);
util.log("<<< compile end", { rootNames, bundle });
util.log("<<< compile end", {
rootNames,
type: CompilerRequestType[request.type]
});
// The compiler isolate exits after a single message.
workerClose();

View file

@ -112,9 +112,3 @@ export let pid: number;
/** Reflects the NO_COLOR environment variable: https://no-color.org/ */
export let noColor: boolean;
// TODO(ry) This should not be exposed to Deno.
export function _setGlobals(pid_: number, noColor_: boolean): void {
pid = pid_;
noColor = noColor_;
}

View file

@ -7,9 +7,6 @@ import * as util from "./util.ts";
import { window } from "./window.ts";
import { OperatingSystem, Arch } from "./build.ts";
// builtin modules
import { _setGlobals } from "./deno.ts";
/** Check if running in terminal.
*
* console.log(Deno.isTTY().stdout);
@ -103,14 +100,15 @@ export function start(preserveDenoNamespace = true, source?: string): Start {
// First we send an empty `Start` message to let the privileged side know we
// are ready. The response should be a `StartRes` message containing the CLI
// args and other info.
const s = sendSync(dispatch.OP_START);
const startResponse = sendSync(dispatch.OP_START);
const { pid, noColor, debugFlag } = startResponse;
util.setLogDebug(s.debugFlag, source);
util.setLogDebug(debugFlag, source);
// pid and noColor need to be set in the Deno module before it's set to be
// frozen.
_setGlobals(s.pid, s.noColor);
delete window.Deno._setGlobals;
util.immutableDefine(window.Deno, "pid", pid);
util.immutableDefine(window.Deno, "noColor", noColor);
Object.freeze(window.Deno);
if (preserveDenoNamespace) {
@ -126,7 +124,7 @@ export function start(preserveDenoNamespace = true, source?: string): Start {
assert(window.Deno === undefined);
}
return s;
return startResponse;
}
/**

View file

@ -8,8 +8,6 @@ import { stringifyArgs } from "./console.ts";
import * as dispatch from "./dispatch.ts";
import { sendSync, sendAsync } from "./dispatch_json.ts";
const { console } = window;
/**
* REPL logging.
* In favor of console.log to avoid unwanted indentation
@ -106,6 +104,7 @@ function evaluate(code: string): boolean {
// @internal
export async function replLoop(): Promise<void> {
const { console } = window;
Object.defineProperties(window, replCommands);
const historyFile = "deno_history.txt";

View file

@ -223,3 +223,33 @@ export function splitNumberToParts(n: number): number[] {
const higher = (n - lower) / 0x100000000;
return [lower, higher];
}
/** Return the common path shared by the `paths`.
*
* @param paths The set of paths to compare.
* @param sep An optional separator to use. Defaults to `/`.
* @internal
*/
export function commonPath(paths: string[], sep = "/"): string {
const [first = "", ...remaining] = paths;
if (first === "" || remaining.length === 0) {
return "";
}
const parts = first.split(sep);
let endOfPrefix = parts.length;
for (const path of remaining) {
const compare = path.split(sep);
for (let i = 0; i < endOfPrefix; i++) {
if (compare[i] !== parts[i]) {
endOfPrefix = i;
}
}
if (endOfPrefix === 0) {
return "";
}
}
const prefix = parts.slice(0, endOfPrefix).join(sep);
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
}

View file

@ -325,8 +325,11 @@ fn bundle_command(flags: DenoFlags, argv: Vec<String>) {
let (worker, state) = create_worker_and_state(flags, argv);
let main_module = state.main_module.as_ref().unwrap().clone();
assert!(state.argv.len() >= 3);
let out_file = state.argv[2].clone();
assert!(state.argv.len() >= 2);
let out_file = match state.argv.len() {
3 => Some(state.argv[2].clone()),
_ => None,
};
debug!(">>>>> bundle_async START");
// NOTE: we need to poll `worker` otherwise TS compiler worker won't run properly
let main_future = lazy(move || {

View file

@ -87,3 +87,13 @@ pub fn enum_name_media_type(mt: MediaType) -> &'static str {
MediaType::Unknown => "Unknown",
}
}
// Warning! The values in this enum are duplicated in js/compiler.ts
// Update carefully!
#[allow(non_camel_case_types)]
#[repr(i8)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum CompilerRequestType {
Compile = 0,
Bundle = 1,
}

18
cli/tests/bundle.test.out Normal file
View file

@ -0,0 +1,18 @@
[WILDCARD]
let define;
[WILDCARD]
let instantiate;
[WILDCARD]
(function() {
[WILDCARD]
})();
define("print_hello", ["require", "exports"], function (require, exports) {
[WILDCARD]
});
define("mod1", ["require", "exports", "subdir2/mod2"], function (require, exports, mod2_ts_1) {
[WILDCARD]
});
instantiate(["mod1"]);

View file

@ -391,6 +391,11 @@ itest!(async_error {
output: "async_error.ts.out",
});
itest!(bundle {
args: "bundle subdir/mod1.ts",
output: "bundle.test.out",
});
itest!(circular1 {
args: "run --reload circular1.js",
output: "circular1.js.out",

View file

@ -1,54 +0,0 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
// A very very basic AMD preamble to support the output of TypeScript outFile
// bundles.
/**
* @type {(name: string) => any}
*/
let require;
/**
* @type {(name: string, deps: ReadonlyArray<string>, factory: (...deps: any[]) => void) => void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let define;
(function() {
/**
* @type {Map<string, { name: string, exports: any }>}
*/
const modules = new Map();
/**
* @param {string} name
*/
function createOrLoadModule(name) {
let m = modules.get(name);
if (!m) {
m = { name, exports: {} };
modules.set(name, m);
}
return m;
}
require = name => {
return createOrLoadModule(name).exports;
};
define = (name, deps, factory) => {
const currentModule = createOrLoadModule(name);
const localExports = currentModule.exports;
const args = deps.map(dep => {
if (dep === "require") {
return require;
} else if (dep === "exports") {
return localExports;
} else {
const depModule = createOrLoadModule(dep);
return depModule.exports;
}
});
factory(...args);
};
})();

View file

@ -0,0 +1,124 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
// A script preamble that provides the ability to load a single outfile
// TypeScript "bundle" where a main module is loaded which recursively
// instantiates all the other modules in the bundle. This code is used to load
// bundles when creating snapshots, but is also used when emitting bundles from
// Deno cli.
/**
* @type {(name: string, deps: ReadonlyArray<string>, factory: (...deps: any[]) => void) => void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let define;
/**
* @type {(mod: string | string[]) => void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let instantiate;
/**
* @callback Factory
* @argument {...any[]} args
* @returns {object | void}
*/
/**
* @typedef ModuleMetaData
* @property {ReadonlyArray<string>} dependencies
* @property {(Factory | object)=} factory
* @property {object} exports
*/
(function() {
/**
* @type {Map<string, ModuleMetaData>}
*/
const modules = new Map();
/**
* Bundles in theory can support "dynamic" imports, but for internal bundles
* we can't go outside to fetch any modules that haven't been statically
* defined.
* @param {string[]} deps
* @param {(...deps: any[]) => void} resolve
* @param {(err: any) => void} reject
*/
const require = (deps, resolve, reject) => {
try {
if (deps.length !== 1) {
throw new TypeError("Expected only a single module specifier.");
}
if (!modules.has(deps[0])) {
throw new RangeError(`Module "${deps[0]}" not defined.`);
}
resolve(getExports(deps[0]));
} catch (e) {
if (reject) {
reject(e);
} else {
throw e;
}
}
};
define = (id, dependencies, factory) => {
if (modules.has(id)) {
throw new RangeError(`Module "${id}" has already been defined.`);
}
modules.set(id, {
dependencies,
factory,
exports: {}
});
};
/**
* @param {string} id
* @returns {any}
*/
function getExports(id) {
const module = modules.get(id);
if (!module) {
// because `$deno$/ts_global.d.ts` looks like a real script, it doesn't
// get erased from output as an import, but it doesn't get defined, so
// we don't have a cache for it, so because this is an internal bundle
// we can just safely return an empty object literal.
return {};
}
if (!module.factory) {
return module.exports;
} else if (module.factory) {
const { factory, exports } = module;
delete module.factory;
if (typeof factory === "function") {
const dependencies = module.dependencies.map(id => {
if (id === "require") {
return require;
} else if (id === "exports") {
return exports;
}
return getExports(id);
});
factory(...dependencies);
} else {
Object.assign(exports, factory);
}
return exports;
}
}
instantiate = dep => {
define = undefined;
if (Array.isArray(dep)) {
for (const d of dep) {
getExports(d);
}
} else {
getExports(dep);
}
// clean up, or otherwise these end up in the runtime environment
instantiate = undefined;
};
})();

View file

@ -22,7 +22,7 @@ use std::sync::Mutex;
static TYPESCRIPT_CODE: &str = include_str!("typescript/lib/typescript.js");
static COMPILER_CODE: &str = include_str!("compiler_main.js");
static AMD_RUNTIME_CODE: &str = include_str!("amd_runtime.js");
static BUNDLE_LOADER: &str = include_str!("bundle_loader.js");
pub fn ts_version() -> String {
let data = include_str!("typescript/package.json");
@ -181,11 +181,13 @@ pub fn mksnapshot_bundle(
let source_code_vec = std::fs::read(bundle)?;
let source_code = std::str::from_utf8(&source_code_vec)?;
js_check(runtime_isolate.execute("amd_runtime.js", AMD_RUNTIME_CODE));
js_check(runtime_isolate.execute("bundle_loader.js", BUNDLE_LOADER));
js_check(runtime_isolate.execute(&bundle.to_string_lossy(), &source_code));
let main = state.lock().unwrap().main_module_name();
js_check(runtime_isolate.execute("anon", &format!("require('{}')", main)));
js_check(
runtime_isolate.execute("anon", &format!("instantiate('{}')", main)),
);
write_snapshot(runtime_isolate, bundle)?;
@ -202,12 +204,14 @@ pub fn mksnapshot_bundle_ts(
let source_code_vec = std::fs::read(bundle)?;
let source_code = std::str::from_utf8(&source_code_vec)?;
js_check(runtime_isolate.execute("amd_runtime.js", AMD_RUNTIME_CODE));
js_check(runtime_isolate.execute("bundle_loader.js", BUNDLE_LOADER));
js_check(runtime_isolate.execute("typescript.js", TYPESCRIPT_CODE));
js_check(runtime_isolate.execute(&bundle.to_string_lossy(), &source_code));
let main = state.lock().unwrap().main_module_name();
js_check(runtime_isolate.execute("anon", &format!("require('{}')", main)));
js_check(
runtime_isolate.execute("anon", &format!("instantiate('{}')", main)),
);
write_snapshot(runtime_isolate, bundle)?;
@ -249,6 +253,7 @@ pub fn get_asset(name: &str) -> Option<&'static str> {
};
}
match name {
"bundle_loader.js" => Some(include_str!("bundle_loader.js")),
"lib.deno_core.d.ts" => Some(include_str!("lib.deno_core.d.ts")),
"typescript.d.ts" => inc!("typescript.d.ts"),
"lib.esnext.d.ts" => inc!("lib.esnext.d.ts"),

View file

@ -1,16 +0,0 @@
# 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.

View file

@ -1,8 +0,0 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { evaluate, instantiate, load } from "./utils.ts";
const args = Deno.args;
const text = await load(args);
const result = evaluate(text);
instantiate(...result);

View file

@ -1,116 +0,0 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test } from "../testing/mod.ts";
import {
assert,
AssertionError,
assertEquals,
assertThrowsAsync
} from "../testing/asserts.ts";
import { instantiate, load, ModuleMetaData } from "./utils.ts";
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace globalThis {
// eslint-disable-next-line no-var
var __results: [string, string] | undefined;
}
}
/* eslint-disable max-len */
/* 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];
});
`;
*/
/* eslint-enable max-len */
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", "--foo"]);
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 at least 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.`
);
});
/* TODO re-enable test
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;
});

View file

@ -1,67 +0,0 @@
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==

View file

@ -1,109 +0,0 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { 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 {
// eslint-disable-next-line no-var
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.
assert(args.length >= 2, "Expected at least two arguments.");
const [, bundleFileName] = args;
assert(
await exists(bundleFileName),
`Expected "${bundleFileName}" to exist.`
);
return new TextDecoder().decode(await Deno.readFile(bundleFileName));
}

View file

@ -793,9 +793,8 @@ Particularly useful ones:
### Bundling
`deno bundle [URL]` will output a single JavaScript file, using
[AMD](https://en.wikipedia.org/wiki/Asynchronous_module_definition), which
includes all dependencies of the specified input.
`deno bundle [URL]` will output a single JavaScript file, which includes all
dependencies of the specified input. For example:
```
> deno bundle https://deno.land/std/examples/colors.ts
@ -804,32 +803,21 @@ Emitting bundle to "colors.bundle.js"
9.2 kB emitted.
```
To run then bundle in Deno use
The bundle can just be run as any other module in Deno would:
```
deno https://deno.land/std/bundle/run.ts colors.bundle.js
deno colors.bundle.js
```
Bundles can also be loaded in the web browser with the assistance of
[RequireJS](https://requirejs.org/). Suppose we have a bundle called
`website.bundle.js`, then the following HTML should be able to load it:
Bundles can also be loaded in the web browser. For example:
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script src="website.bundle.js"></script>
<script>
requirejs(["website"], website => website.main());
</script>
```
Here we assume there's an exported function `main()` from `website.ts`.
```js
// website.ts
export main() {
console.log("hello from the web browser");
}
```
Bundles, whether loaded in the web browser, or in Deno, would run the root
module which is specified on the command line when creating the bundle, so put
any initiation logic in that module.
### Installing executable scripts

View file

@ -201,8 +201,8 @@ def bundle_benchmark(deno_exe):
for name, url in bundles.items():
# bundle
run([deno_exe, "bundle", url])
path = name + ".bundle.js"
run([deno_exe, "bundle", url, path])
# get size of bundle
assert os.path.exists(path)
sizes[name] = os.path.getsize(path)