All files / wasm / bundle.ts

40.00% Branches 8/20
90.65% Lines 97/107
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
x1
x3
x3
x3
x3
x3
x3
x3
 
x3
 
 
x1
 
 
 
 
 
 
 
 
 
 
 
x3
x3
x18
 
x6
x6
x42
 
 
 
 
 
 
 
 
 
x6
x6
x8
x8
x56
 
x8
x8
x32
x8
x8
x8
 
x6
x6
x60
x8
x8
 
x8
x8
x8
x8
x8
x8
x8
x8
 
x8
x8
x8
 
x8
x8
x32
x8
x40
x8
x6
 
x1
 
x3
x3
x5
x5
x5
x5
x5
x40
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
x15
x15
x21
x21
x21
x15
x17
x17
x17
x17
x17
x17
x68
 
x17
x17
x15
x5
x5
x5
x5
x25
x5
x5
x5
I






























I












I










































































I








// Imports
import { encodeBase64 } from "@std/encoding/base64"
import { bundle as bundle_ts } from "../ts/bundle.ts"
import { assert } from "@std/assert"
import { UntarStream } from "@std/tar/untar-stream"
import { ensureFile } from "@std/fs"
import { basename, dirname, resolve, toFileUrl } from "@std/path"
import { Logger } from "@libs/logger"
import type { record } from "@libs/typing"
import { command } from "@libs/run/command"
export type { record }

/**
 * Build WASM, bundle and minify JavaScript.
 *
 * The resulting WASM file is injected as a base64 string in the JavaScript output to avoid the need of `--allow-read` permission.
 *
 * @example
 * ```ts
 * import { bundle } from "./bundle.ts"
 * await bundle("path/to/rust/project")
 * ```
 *
 * @module
 */
export async function bundle(project: string, { bin = "wasm-pack", autoinstall = false, banner, logger: log = new Logger(), env = {} } = {} as { bin?: string; autoinstall?: boolean; banner?: string; logger?: Logger; env?: record<string> }): Promise<void> {
  log = log.with({ project })

  // Check that cargo is installed
  try {
    await command("cargo", ["--version"], { logger: log, env, throw: true })
  } catch (error) {
    if (error instanceof Deno.errors.NotFound) {
      log.error("cargo binary not found in PATH, is it installed?")
      log.error("note that even with `autoinstall` option enabled, cargo must be installed manually")
      log.error("https://doc.rust-lang.org/cargo/getting-started/installation.html")
      throw error
    }
  }

  // Autoinstall wasm-pack if needed
  if (autoinstall) {
    log.trace(`checking if ${bin} is installed`)
    try {
      await command(bin, ["--version"], { logger: log, env, throw: true })
    } catch (error) {
      if (error instanceof Deno.errors.NotFound) {
        log.wdebug(`${bin} not found, installing`)
        bin = await install({ log, path: dirname(bin) })
      }
    }
  }

  // Build wasm
  log.debug("building wasm")
  const { success, stderr } = await command(bin, ["build", "--release", "--target", "web"], { logger: log, cwd: project, env })
  assert(success, `wasm build failed: ${stderr}`)
  log.ok("built wasm")

  // Inject wasm so it can be loaded without permissions, and export a source function so it can be loaded synchronously using `initSync()`
  log.debug("injecting base64 wasm to js")
  const name = basename(project)
  const wasm = await fetch(toFileUrl(resolve(project, `pkg/${name}_bg.wasm`))).then((response) => response.arrayBuffer())
  let js = await fetch(toFileUrl(resolve(project, `pkg/${name}.js`))).then((response) => response.text())
  js = js.replace(`'${name}_bg.wasm'`, "`data:application/wasm;base64,${source('base64')}`")
  js += `export function source(format) {
    const b64 = '${encodeBase64(wasm)}'
    return format === 'base64' ? b64 : new Uint8Array(Array.from(atob(b64), c => c.charCodeAt(0))).buffer
  }`
  await Deno.writeTextFile(resolve(project, `./${name}.js`), js)
  log.ok("injecting base64 wasm to js")

  // Minify output
  log.debug("minifying js")
  const minified = await bundle_ts(new URL(toFileUrl(resolve(project, `./${name}.js`))), { minify: "terser", banner })
  await Deno.writeTextFile(resolve(project, `./${name}.js`), minified)
  log.with({ size: `${new Blob([minified]).size}b` }).log()
  log.ok("minified js")
}

/**
 * Install wasm-pack.
 */
async function install({ log, path }: { log: Logger; path: string }) {
  // List releases and select asset for current platform
  log.debug("looking for latest release of wasm-pack")
  const { tag_name, assets: assets } = await fetch("https://api.github.com/repos/rustwasm/wasm-pack/releases/latest").then((response) => response.json())
  log.info(`found version ${tag_name}`)
  const packaged = assets
    .map(({ name, browser_download_url: url }: { name: string; browser_download_url: string }) => ({ name, url }))
    .find(({ name }: { name: string }) => name.includes(Deno.build.os) && name.includes(Deno.build.arch))
  assert(packaged, `cannot find suitable release for ${Deno.build.os}-${Deno.build.arch}`)
  log.info(`found binary ${packaged.name} for ${Deno.build.os}-${Deno.build.arch}`)
  // Download archive
  log.debug("downloading release")
  const response = await fetch(packaged.url)
  log.ok("downloaded release")
  // Extract archive
  log.debug("extracting release")
  const entries = response.body!
    .pipeThrough(new DecompressionStream("gzip"))
    .pipeThrough(new UntarStream())
  let found = false
  for await (const entry of entries) {
    const filename = basename(entry.path)
    if (!filename.startsWith("wasm-pack")) {
      await entry.readable?.cancel()
      continue
    }
    if (entry.readable === undefined) {
      continue
    }
    found = true
    path = resolve(path, filename)
    log.trace(`extracted ${path}`)
    await ensureFile(path)
    using file = await Deno.open(path, { write: true, truncate: true })
    await Deno.chmod(path, 0o755).catch(() => null)
    await entry.readable.pipeTo(file.writable)
    break
  }
  assert(found, "wasm-pack binary not found in archive")
  log.ok("extracted release")
  // Check installation
  log.debug("checking installation")
  const { success } = await command(path, ["--version"], { logger: log })
  assert(success, "wasm-pack could not be executed")
  return path
}