1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-22 15:06:54 -05:00

feat: Support named exports on bundles. (#3352)

This commit is contained in:
Kitson Kelly 2019-11-21 03:02:08 +11:00 committed by Ry Dahl
parent 1912ed6740
commit 8d977d0117
7 changed files with 222 additions and 72 deletions

113
cli/js/bundler.ts Normal file
View file

@ -0,0 +1,113 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Console } from "./console.ts";
import * as dispatch from "./dispatch.ts";
import { sendSync } from "./dispatch_json.ts";
import { TextEncoder } from "./text_encoding.ts";
import { assert, commonPath, humanFileSize } from "./util.ts";
import { writeFileSync } from "./write_file.ts";
declare global {
const console: Console;
}
const BUNDLE_LOADER = "bundle_loader.js";
const encoder = new TextEncoder();
let bundleLoader: string;
let rootExports: string[] | undefined;
/** Given a fileName and the data, emit the file to the file system. */
export function emitBundle(
rootNames: string[],
fileName: string | undefined,
data: string,
sourceFiles: readonly ts.SourceFile[]
): void {
// if the fileName is set to an internal value, just noop
if (fileName && fileName.startsWith("$deno$")) {
return;
}
// This should never happen at the moment, but this code can't currently
// support it
assert(
rootNames.length === 1,
"Only single root modules supported for bundling."
);
if (!bundleLoader) {
bundleLoader = sendSync(dispatch.OP_FETCH_ASSET, { name: 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 = commonPath(sources);
const rootName = rootNames[0].replace(sharedPath, "").replace(/\.\w+$/i, "");
let instantiate: string;
if (rootExports && rootExports.length) {
instantiate = `const __rootExports = instantiate("${rootName}");\n`;
for (const rootExport of rootExports) {
if (rootExport === "default") {
instantiate += `export default __rootExports["${rootExport}"];\n`;
} else {
instantiate += `export const ${rootExport} = __rootExports["${rootExport}"];\n`;
}
}
} else {
instantiate = `instantiate("${rootName}");\n`;
}
const bundle = `${bundleLoader}\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);
}
}
/** Set the rootExports which will by the `emitBundle()` */
export function setRootExports(
program: ts.Program,
rootModules: string[]
): void {
// get a reference to the type checker, this will let us find symbols from
// the AST.
const checker = program.getTypeChecker();
assert(rootModules.length === 1);
// get a reference to the main source file for the bundle
const mainSourceFile = program.getSourceFile(rootModules[0]);
assert(mainSourceFile);
// retrieve the internal TypeScript symbol for this AST node
const mainSymbol = checker.getSymbolAtLocation(mainSourceFile);
if (!mainSymbol) {
return;
}
rootExports = checker
.getExportsOfModule(mainSymbol)
// .getExportsOfModule includes type only symbols which are exported from
// the module, so we need to try to filter those out. While not critical
// someone looking at the bundle would think there is runtime code behind
// that when there isn't. There appears to be no clean way of figuring that
// out, so inspecting SymbolFlags that might be present that are type only
.filter(
sym =>
!(
sym.flags & ts.SymbolFlags.Interface ||
sym.flags & ts.SymbolFlags.TypeLiteral ||
sym.flags & ts.SymbolFlags.Signature ||
sym.flags & ts.SymbolFlags.TypeParameter ||
sym.flags & ts.SymbolFlags.TypeAlias ||
sym.flags & ts.SymbolFlags.Type ||
sym.flags & ts.SymbolFlags.Namespace ||
sym.flags & ts.SymbolFlags.InterfaceExcludes ||
sym.flags & ts.SymbolFlags.TypeParameterExcludes ||
sym.flags & ts.SymbolFlags.TypeAliasExcludes
)
)
.map(sym => sym.getName());
}

View file

@ -4,6 +4,7 @@
import "./globals.ts";
import "./ts_global.d.ts";
import { emitBundle, setRootExports } from "./bundler.ts";
import { bold, cyan, yellow } from "./colors.ts";
import { Console } from "./console.ts";
import { core } from "./core.ts";
@ -12,13 +13,11 @@ import { cwd } from "./dir.ts";
import * as dispatch from "./dispatch.ts";
import { sendAsync, sendSync } from "./dispatch_json.ts";
import * as os from "./os.ts";
import { TextEncoder } from "./text_encoding.ts";
import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts";
import { assert, notImplemented } from "./util.ts";
import * as util from "./util.ts";
import { window } from "./window.ts";
import { postMessage, workerClose, workerMain } from "./workers.ts";
import { writeFileSync } from "./write_file.ts";
// Warning! The values in this enum are duplicated in cli/msg.rs
// Update carefully!
@ -52,7 +51,6 @@ 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 */
type CompilerRequest = {
@ -188,6 +186,12 @@ class SourceFile {
throw new Error("SourceFile has already been processed.");
}
assert(this.sourceCode != null);
// we shouldn't process imports for files which contain the nocheck pragma
// (like bundles)
if (this.sourceCode.match(/\/{2}\s+@ts-nocheck/)) {
util.log(`Skipping imports for "${this.filename}"`);
return [];
}
const preProcessedFileInfo = ts.preProcessFile(this.sourceCode, true, true);
this.processed = true;
const files = (this.importedFiles = [] as Array<[string, string]>);
@ -302,63 +306,12 @@ async function processImports(
return sourceFiles;
}
/** Utility function to turn the number of bytes into a human readable
* unit */
function humanFileSize(bytes: number): string {
const thresh = 1000;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return `${bytes.toFixed(1)} ${units[u]}`;
}
/** Ops to rest for caching source map and compiled js */
function cache(extension: string, moduleId: string, contents: string): void {
util.log("compiler::cache", { extension, moduleId });
sendSync(dispatch.OP_CACHE, { extension, moduleId, contents });
}
const encoder = new TextEncoder();
/** Given a fileName and the data, emit the file to the file system. */
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 && fileName.startsWith("$deno$")) {
console.warn("skipping emitBundle", fileName);
return;
}
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. */
function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
switch (mediaType) {
@ -577,7 +530,7 @@ class Host implements ts.CompilerHost {
try {
assert(sourceFiles != null);
if (this._requestType === CompilerRequestType.Bundle) {
emitBundle(this._rootNames, this._outFile, data, sourceFiles!);
emitBundle(this._rootNames, this._outFile, data, sourceFiles);
} else {
assert(sourceFiles.length == 1);
const url = sourceFiles[0].fileName;
@ -704,6 +657,9 @@ window.compilerMain = function compilerMain(): void {
// warning so it goes to stderr instead of stdout
console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`);
}
if (request.type === CompilerRequestType.Bundle) {
setRootExports(program, resolvedRootModules);
}
const emitResult = program.emit();
emitSkipped = emitResult.emitSkipped;
// emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned

View file

@ -248,3 +248,19 @@ export function commonPath(paths: string[], sep = "/"): string {
const prefix = parts.slice(0, endOfPrefix).join(sep);
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
}
/** Utility function to turn the number of bytes into a human readable
* unit */
export function humanFileSize(bytes: number): string {
const thresh = 1000;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return `${bytes.toFixed(1)} ${units[u]}`;
}

View file

@ -14,5 +14,9 @@ define("mod1", ["require", "exports", "subdir2/mod2"], function (require, export
[WILDCARD]
});
instantiate(["mod1"]);
const __rootExports = instantiate("mod1");
export const returnsHi = __rootExports["returnsHi"];
export const returnsFoo2 = __rootExports["returnsFoo2"];
export const printHello3 = __rootExports["printHello3"];
export const throwsError = __rootExports["throwsError"];

View file

@ -55,6 +55,47 @@ fn js_unit_tests() {
drop(g);
}
#[test]
fn bundle_exports() {
use tempfile::TempDir;
// First we have to generate a bundle of some module that has exports.
let mod1 = util::root_path().join("cli/tests/subdir/mod1.ts");
assert!(mod1.is_file());
let t = TempDir::new().expect("tempdir fail");
let bundle = t.path().join("mod1.bundle.js");
let mut deno = util::deno_cmd()
.current_dir(util::root_path())
.arg("bundle")
.arg(mod1)
.arg(&bundle)
.spawn()
.expect("failed to spawn script");
let status = deno.wait().expect("failed to wait for the child process");
assert!(status.success());
assert!(bundle.is_file());
// Now we try to use that bundle from another module.
let test = t.path().join("test.js");
std::fs::write(
&test,
"
import { printHello3 } from \"./mod1.bundle.js\";
printHello3(); ",
)
.expect("error writing file");
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg(&test)
.output()
.expect("failed to spawn script");
// check the output of the test.ts program.
assert_eq!(std::str::from_utf8(&output.stdout).unwrap().trim(), "Hello");
assert_eq!(output.stderr, b"");
}
// TODO(#2933): Rewrite this test in rust.
#[test]
fn repl_test() {

View file

@ -6,14 +6,16 @@
// bundles when creating snapshots, but is also used when emitting bundles from
// Deno cli.
// @ts-nocheck
/**
* @type {(name: string, deps: ReadonlyArray<string>, factory: (...deps: any[]) => void) => void}
* @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}
* @type {(mod: string) => any=}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let instantiate;
@ -111,14 +113,9 @@ let instantiate;
instantiate = dep => {
define = undefined;
if (Array.isArray(dep)) {
for (const d of dep) {
getExports(d);
}
} else {
getExports(dep);
}
const result = getExports(dep);
// clean up, or otherwise these end up in the runtime environment
instantiate = undefined;
return result;
};
})();

View file

@ -797,27 +797,50 @@ Particularly useful ones:
dependencies of the specified input. For example:
```
> deno bundle https://deno.land/std/examples/colors.ts
> deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js
Bundling "colors.bundle.js"
Emitting bundle to "colors.bundle.js"
9.2 kB emitted.
```
If you omit the out file, the bundle will be sent to `stdout`.
The bundle can just be run as any other module in Deno would:
```
deno colors.bundle.js
```
Bundles can also be loaded in the web browser. For example:
The output is a self contained ES Module, which any exports from the main module
supplied on the command line will be available. For example if the main module
looked something like this:
```html
<script src="website.bundle.js"></script>
```ts
export { foo } from "./foo.js";
export const bar = "bar";
```
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.
It could be imported like this:
```ts
import { foo, bar } from "./lib.bundle.js";
```
Bundles can also be loaded in the web browser. The bundle is a self-contained ES
module, and so the attribute of `type` must be set to `"module"`. For example:
```html
<script type="module" src="website.bundle.js"></script>
```
Or you could import it into another ES module to consume:
```html
<script type="module">
import * as website from "website.bundle.js";
</script>
```
### Installing executable scripts