All files / wasm / bundle.ts

40.00% Branches 8/20
90.48% Lines 95/105
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
x1
x2
x2
x2
x2
x2
x2
x2
x2
 
x2
 
 
x1
 
 
 
 
 
 
 
 
 
 
 
x2
x2
x15
 
x5
x5
x35
 
 
 
 
 
 
 
 
 
x5
x5
x7
x7
x49
 
x7
x7
x28
x7
x7
x7
 
x5
x5
x50
x7
x7
 
x7
x7
x7
x7
x7
x7
x7
x7
 
x7
x7
x7
 
x7
x7
x28
x7
x35
x7
x5
 
x1
 
x2
x2
x4
x4
x4
x4
x4
x36
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x14
x14
x20
x20
x14
x16
x16
x16
x16
x16
x16
x64
 
x16
x16
x14
x4
x4
x4
x4
x20
x4
x4
x4
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 { Untar } from "@std/archive/untar"
import { copy, readerFromStreamReader } from "@std/io"
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 reader = readerFromStreamReader(await fetch(packaged.url).then((response) => response.body!.pipeThrough(new DecompressionStream("gzip")).getReader()))
  log.ok("downloaded release")
  // Extract archive
  log.debug("extracting release")
  const untar = new Untar(reader)
  let found = false
  for await (const entry of untar) {
    const filename = basename(entry.fileName)
    if (!filename.startsWith("wasm-pack")) {
      continue
    }
    if (entry.type !== "file") {
      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 copy(entry, file)
    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
}