summaryrefslogtreecommitdiff
path: root/cli/js
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2019-11-21 03:02:08 +1100
committerRy Dahl <ry@tinyclouds.org>2019-11-20 11:02:08 -0500
commit8d977d0117c2b61e6714cec9e4238f4ba0a56195 (patch)
tree5992f83fa50067f59aec1f1c9008d6c3fe6e921d /cli/js
parent1912ed674097588adb7b83e7b78043b2168821f3 (diff)
feat: Support named exports on bundles. (#3352)
Diffstat (limited to 'cli/js')
-rw-r--r--cli/js/bundler.ts113
-rw-r--r--cli/js/compiler.ts66
-rw-r--r--cli/js/util.ts16
3 files changed, 140 insertions, 55 deletions
diff --git a/cli/js/bundler.ts b/cli/js/bundler.ts
new file mode 100644
index 000000000..4285b61ad
--- /dev/null
+++ b/cli/js/bundler.ts
@@ -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());
+}
diff --git a/cli/js/compiler.ts b/cli/js/compiler.ts
index 1779f76c3..4ad4ae8a4 100644
--- a/cli/js/compiler.ts
+++ b/cli/js/compiler.ts
@@ -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
diff --git a/cli/js/util.ts b/cli/js/util.ts
index 54230af1f..b046a34f4 100644
--- a/cli/js/util.ts
+++ b/cli/js/util.ts
@@ -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]}`;
+}