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:
parent
1912ed6740
commit
8d977d0117
7 changed files with 222 additions and 72 deletions
113
cli/js/bundler.ts
Normal file
113
cli/js/bundler.ts
Normal 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());
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
import "./globals.ts";
|
import "./globals.ts";
|
||||||
import "./ts_global.d.ts";
|
import "./ts_global.d.ts";
|
||||||
|
|
||||||
|
import { emitBundle, setRootExports } from "./bundler.ts";
|
||||||
import { bold, cyan, yellow } from "./colors.ts";
|
import { bold, cyan, yellow } from "./colors.ts";
|
||||||
import { Console } from "./console.ts";
|
import { Console } from "./console.ts";
|
||||||
import { core } from "./core.ts";
|
import { core } from "./core.ts";
|
||||||
|
@ -12,13 +13,11 @@ import { cwd } from "./dir.ts";
|
||||||
import * as dispatch from "./dispatch.ts";
|
import * as dispatch from "./dispatch.ts";
|
||||||
import { sendAsync, sendSync } from "./dispatch_json.ts";
|
import { sendAsync, sendSync } from "./dispatch_json.ts";
|
||||||
import * as os from "./os.ts";
|
import * as os from "./os.ts";
|
||||||
import { TextEncoder } from "./text_encoding.ts";
|
|
||||||
import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts";
|
import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts";
|
||||||
import { assert, notImplemented } from "./util.ts";
|
import { assert, notImplemented } from "./util.ts";
|
||||||
import * as util from "./util.ts";
|
import * as util from "./util.ts";
|
||||||
import { window } from "./window.ts";
|
import { window } from "./window.ts";
|
||||||
import { postMessage, workerClose, workerMain } from "./workers.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
|
// Warning! The values in this enum are duplicated in cli/msg.rs
|
||||||
// Update carefully!
|
// Update carefully!
|
||||||
|
@ -52,7 +51,6 @@ window["denoMain"] = denoMain;
|
||||||
|
|
||||||
const ASSETS = "$asset$";
|
const ASSETS = "$asset$";
|
||||||
const OUT_DIR = "$deno$";
|
const OUT_DIR = "$deno$";
|
||||||
const BUNDLE_LOADER = "bundle_loader.js";
|
|
||||||
|
|
||||||
/** The format of the work message payload coming from the privileged side */
|
/** The format of the work message payload coming from the privileged side */
|
||||||
type CompilerRequest = {
|
type CompilerRequest = {
|
||||||
|
@ -188,6 +186,12 @@ class SourceFile {
|
||||||
throw new Error("SourceFile has already been processed.");
|
throw new Error("SourceFile has already been processed.");
|
||||||
}
|
}
|
||||||
assert(this.sourceCode != null);
|
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);
|
const preProcessedFileInfo = ts.preProcessFile(this.sourceCode, true, true);
|
||||||
this.processed = true;
|
this.processed = true;
|
||||||
const files = (this.importedFiles = [] as Array<[string, string]>);
|
const files = (this.importedFiles = [] as Array<[string, string]>);
|
||||||
|
@ -302,63 +306,12 @@ async function processImports(
|
||||||
return sourceFiles;
|
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 */
|
/** Ops to rest for caching source map and compiled js */
|
||||||
function cache(extension: string, moduleId: string, contents: string): void {
|
function cache(extension: string, moduleId: string, contents: string): void {
|
||||||
util.log("compiler::cache", { extension, moduleId });
|
util.log("compiler::cache", { extension, moduleId });
|
||||||
sendSync(dispatch.OP_CACHE, { extension, moduleId, contents });
|
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. */
|
/** Returns the TypeScript Extension enum for a given media type. */
|
||||||
function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
|
function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
|
@ -577,7 +530,7 @@ class Host implements ts.CompilerHost {
|
||||||
try {
|
try {
|
||||||
assert(sourceFiles != null);
|
assert(sourceFiles != null);
|
||||||
if (this._requestType === CompilerRequestType.Bundle) {
|
if (this._requestType === CompilerRequestType.Bundle) {
|
||||||
emitBundle(this._rootNames, this._outFile, data, sourceFiles!);
|
emitBundle(this._rootNames, this._outFile, data, sourceFiles);
|
||||||
} else {
|
} else {
|
||||||
assert(sourceFiles.length == 1);
|
assert(sourceFiles.length == 1);
|
||||||
const url = sourceFiles[0].fileName;
|
const url = sourceFiles[0].fileName;
|
||||||
|
@ -704,6 +657,9 @@ window.compilerMain = function compilerMain(): void {
|
||||||
// warning so it goes to stderr instead of stdout
|
// warning so it goes to stderr instead of stdout
|
||||||
console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`);
|
console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`);
|
||||||
}
|
}
|
||||||
|
if (request.type === CompilerRequestType.Bundle) {
|
||||||
|
setRootExports(program, resolvedRootModules);
|
||||||
|
}
|
||||||
const emitResult = program.emit();
|
const emitResult = program.emit();
|
||||||
emitSkipped = emitResult.emitSkipped;
|
emitSkipped = emitResult.emitSkipped;
|
||||||
// emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
|
// emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
|
||||||
|
|
|
@ -248,3 +248,19 @@ export function commonPath(paths: string[], sep = "/"): string {
|
||||||
const prefix = parts.slice(0, endOfPrefix).join(sep);
|
const prefix = parts.slice(0, endOfPrefix).join(sep);
|
||||||
return prefix.endsWith(sep) ? prefix : `${prefix}${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]}`;
|
||||||
|
}
|
||||||
|
|
|
@ -14,5 +14,9 @@ define("mod1", ["require", "exports", "subdir2/mod2"], function (require, export
|
||||||
[WILDCARD]
|
[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"];
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,47 @@ fn js_unit_tests() {
|
||||||
drop(g);
|
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.
|
// TODO(#2933): Rewrite this test in rust.
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_test() {
|
fn repl_test() {
|
||||||
|
|
|
@ -6,14 +6,16 @@
|
||||||
// bundles when creating snapshots, but is also used when emitting bundles from
|
// bundles when creating snapshots, but is also used when emitting bundles from
|
||||||
// Deno cli.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
let define;
|
let define;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(mod: string | string[]) => void}
|
* @type {(mod: string) => any=}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
let instantiate;
|
let instantiate;
|
||||||
|
@ -111,14 +113,9 @@ let instantiate;
|
||||||
|
|
||||||
instantiate = dep => {
|
instantiate = dep => {
|
||||||
define = undefined;
|
define = undefined;
|
||||||
if (Array.isArray(dep)) {
|
const result = getExports(dep);
|
||||||
for (const d of dep) {
|
|
||||||
getExports(d);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
getExports(dep);
|
|
||||||
}
|
|
||||||
// clean up, or otherwise these end up in the runtime environment
|
// clean up, or otherwise these end up in the runtime environment
|
||||||
instantiate = undefined;
|
instantiate = undefined;
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -797,27 +797,50 @@ Particularly useful ones:
|
||||||
dependencies of the specified input. For example:
|
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"
|
Bundling "colors.bundle.js"
|
||||||
Emitting bundle to "colors.bundle.js"
|
Emitting bundle to "colors.bundle.js"
|
||||||
9.2 kB emitted.
|
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:
|
The bundle can just be run as any other module in Deno would:
|
||||||
|
|
||||||
```
|
```
|
||||||
deno colors.bundle.js
|
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
|
```ts
|
||||||
<script src="website.bundle.js"></script>
|
export { foo } from "./foo.js";
|
||||||
|
|
||||||
|
export const bar = "bar";
|
||||||
```
|
```
|
||||||
|
|
||||||
Bundles, whether loaded in the web browser, or in Deno, would run the root
|
It could be imported like this:
|
||||||
module which is specified on the command line when creating the bundle, so put
|
|
||||||
any initiation logic in that module.
|
```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
|
### Installing executable scripts
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue