summaryrefslogtreecommitdiff
path: root/cli/js/bundler.ts
blob: 4285b61ad499c922881784c6e3b98e966f1d76ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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());
}