diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 727b2f2..f3d2f53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - name: clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dsherret/rust-toolchain-file@v1 @@ -37,8 +37,23 @@ jobs: if: contains(matrix.os, 'ubuntu') run: deno fmt --check && cargo fmt -- --check + - name: Check lint + if: contains(matrix.os, 'ubuntu') + run: deno lint && cargo clippy + - name: Check Wasm up-to-date run: deno task build --check - name: Test run: deno task test + + - name: Get tag version + if: contains(matrix.os, 'ubuntu') && startsWith(github.ref, 'refs/tags/') + id: get_tag_version + run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> "$GITHUB_OUTPUT" + + - name: Publish + if: contains(matrix.os, 'ubuntu') && startsWith(github.ref, 'refs/tags/') + run: | + deno run -A ./scripts/update_deno_json_version.ts ${{steps.get_tag_version.outputs.TAG_VERSION}} + deno publish diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index e25cf22..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Publish -on: [push, pull_request] - -jobs: - publish: - runs-on: ubuntu-latest - - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - - - name: Publish package - if: ${{ github.ref == 'refs/heads/main' }} - run: deno publish diff --git a/deno.jsonc b/deno.json similarity index 98% rename from deno.jsonc rename to deno.json index a0e6d0a..d08748e 100644 --- a/deno.jsonc +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@deno/wasmbuild", - "version": "0.15.6", + "version": "0.0.0", "exports": "./main.ts", "exclude": [ "./tests/target", diff --git a/lib/args.ts b/lib/args.ts index a982a56..f592e4c 100644 --- a/lib/args.ts +++ b/lib/args.ts @@ -15,7 +15,7 @@ export type LoaderKind = "sync" | "async" | "async-with-cache"; export interface CommonBuild { outDir: string; - bindingJsFileExt: string; + bindingJsFileExt: "js" | "mjs"; profile: "debug" | "release"; project: string | undefined; loaderKind: LoaderKind; @@ -68,11 +68,19 @@ export function parseArgs(rawArgs: string[]): Command { : "async-with-cache", isOpt: !(flags["skip-opt"] ?? flags.debug == "debug"), outDir: flags.out ?? "./lib", - bindingJsFileExt: flags["js-ext"] ?? `js`, + bindingJsFileExt: getBindingJsFileExt(), cargoFlags: getCargoFlags(), }; } + function getBindingJsFileExt() { + const ext: string = flags["js-ext"] ?? `js`; + if (ext !== "js" && ext !== "mjs") { + throw new Error("js-ext must be 'js' or 'mjs'"); + } + return ext; + } + function getCargoFlags() { const cargoFlags = []; diff --git a/lib/bindgen.ts b/lib/bindgen.ts index 7a08fee..65b452e 100644 --- a/lib/bindgen.ts +++ b/lib/bindgen.ts @@ -5,6 +5,7 @@ import * as path from "@std/path"; export interface BindgenOutput { js: string; + ts: string; snippets: Map; localModules: Map; wasmBytes: number[]; @@ -52,6 +53,7 @@ async function generateForSelfBuild(filePath: string): Promise { ); return { js: await Deno.readTextFile(path.join(tempPath, "wasmbuild.js")), + ts: await Deno.readTextFile(path.join(tempPath, "wasmbuild.d.ts")), localModules: new Map(), snippets: new Map(), wasmBytes: Array.from(wasmBytes), diff --git a/lib/commands/build_command.ts b/lib/commands/build_command.ts index c4fd2f1..80a283f 100644 --- a/lib/commands/build_command.ts +++ b/lib/commands/build_command.ts @@ -14,8 +14,10 @@ export async function runBuildCommand(args: BuildCommand) { await ensureDir(args.outDir); await writeSnippets(); - console.log(` write ${colors.yellow(output.bindingJsPath)}`); - await Deno.writeTextFile(output.bindingJsPath, output.bindingJsText); + console.log(` write ${colors.yellow(output.bindingJs.path)}`); + await Deno.writeTextFile(output.bindingJs.path, output.bindingJs.text); + console.log(` write ${colors.yellow(output.bindingDts.path)}`); + await Deno.writeTextFile(output.bindingDts.path, output.bindingDts.text); if (output.wasmFileName != null) { const wasmDest = path.join(args.outDir, output.wasmFileName); diff --git a/lib/commands/check_command.ts b/lib/commands/check_command.ts index 51bb4ed..c43fa29 100644 --- a/lib/commands/check_command.ts +++ b/lib/commands/check_command.ts @@ -23,7 +23,7 @@ export async function runCheckCommand(args: CheckCommand) { async function getOriginalSourceHash() { try { return getSourceHashFromText( - await Deno.readTextFile(output.bindingJsPath), + await Deno.readTextFile(output.bindingJs.path), ); } catch (err) { if (err instanceof Deno.errors.NotFound) { diff --git a/lib/loader_text.generated.ts b/lib/loader_text.generated.ts index aadb704..542946a 100644 --- a/lib/loader_text.generated.ts +++ b/lib/loader_text.generated.ts @@ -1,4 +1,4 @@ -// Copyright 2018-2022 the Deno authors. MIT license. +// Copyright 2018-2024 the Deno authors. MIT license. export const loaderText = 'export async function cacheToLocalDir(url, decompress) {\n\ diff --git a/lib/pre_build.ts b/lib/pre_build.ts index f866efb..524395a 100644 --- a/lib/pre_build.ts +++ b/lib/pre_build.ts @@ -15,8 +15,14 @@ import { loaderText as generatedLoaderText } from "./loader_text.generated.ts"; export interface PreBuildOutput { bindgen: BindgenOutput; - bindingJsText: string; - bindingJsPath: string; + bindingJs: { + path: string; + text: string; + }; + bindingDts: { + path: string; + text: string; + }; sourceHash: string; wasmFileName: string | undefined; } @@ -104,7 +110,6 @@ export async function runPreBuild( const bindingJsFileName = `${crate.libName}.generated.${args.bindingJsFileExt}`; - const bindingJsPath = path.join(args.outDir, bindingJsFileName); const { bindingJsText, sourceHash } = await getBindingJsOutput( args, @@ -114,8 +119,14 @@ export async function runPreBuild( return { bindgen: bindgenOutput, - bindingJsText, - bindingJsPath, + bindingJs: { + path: path.join(args.outDir, bindingJsFileName), + text: bindingJsText, + }, + bindingDts: { + path: path.join(args.outDir, getDtsFileName(args, crate)), + text: getBindgenDtsOutput(args, bindgenOutput), + }, sourceHash, wasmFileName: args.loaderKind === "sync" ? undefined @@ -123,6 +134,12 @@ export async function runPreBuild( }; } +function getDtsFileName(args: CheckCommand | BuildCommand, crate: WasmCrate) { + return `${crate.libName}.generated.${ + args.bindingJsFileExt === "mjs" ? "d.mts" : "d.ts" + }`; +} + async function getBindingJsOutput( args: CheckCommand | BuildCommand, crate: WasmCrate, @@ -132,7 +149,9 @@ async function getBindingJsOutput( const header = `// @generated file from wasmbuild -- do not edit // @ts-nocheck: generated // deno-lint-ignore-file -// deno-fmt-ignore-file`; +// deno-fmt-ignore-file +/// +`; const genText = bindgenOutput.js.replace( /\bconst\swasm_url\s.+/ms, getLoaderText(args, crate, bindgenOutput), @@ -223,24 +242,12 @@ function getLoaderText( function getSyncLoaderText(bindgenOutput: BindgenOutput) { const exportNames = getExportNames(bindgenOutput); return ` -/** Instantiates an instance of the Wasm module returning its functions. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - */ export function instantiate() { return instantiateWithInstance().exports; } let instanceWithExports; -/** Instantiates an instance of the Wasm module along with its exports. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - * @returns {{ - * instance: WebAssembly.Instance; - * exports: { ${exportNames.map((n) => `${n}: typeof ${n}`).join("; ")} } - * }} - */ export function instantiateWithInstance() { if (instanceWithExports == null) { const instance = instantiateInstance(); @@ -255,7 +262,6 @@ export function instantiateWithInstance() { return instanceWithExports; } -/** Gets if the Wasm module has been instantiated. */ export function isInstantiated() { return instanceWithExports != null; } @@ -307,34 +313,11 @@ const loader = new WasmBuildLoader({ imports, cache: ${cacheText}, }) -`; - - loaderText += `/** - * Options for instantiating a Wasm instance. - * @typedef {Object} InstantiateOptions - * @property {URL=} url - Optional url to the Wasm file to instantiate. - * @property {DecompressCallback=} decompress - Callback to decompress the - * raw Wasm file bytes before instantiating. - */ -/** Instantiates an instance of the Wasm module returning its functions. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - * @param {InstantiateOptions=} opts - */ export async function instantiate(opts) { return (await instantiateWithInstance(opts)).exports; } -/** Instantiates an instance of the Wasm module along with its exports. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - * @param {InstantiateOptions=} opts - * @returns {Promise<{ - * instance: WebAssembly.Instance; - * exports: { ${exportNames.map((n) => `${n}: typeof ${n}`).join("; ")} } - * }>} - */ export async function instantiateWithInstance(opts) { const {instance } = await loader.load( opts?.url ?? new URL("${getWasmFileNameFromCrate(crate)}", import.meta.url), @@ -353,65 +336,31 @@ function getWasmInstanceExports() { return { ${exportNames.join(", ")} }; } -/** Gets if the Wasm module has been instantiated. */ export function isInstantiated() { return loader.instance != null; } `; - return loaderText + " " + generatedLoaderText; + return loaderText + generatedLoaderText; function getWasmbuildLoaderText() { - return `/** -* @callback WasmBuildDecompressCallback -* @param {Uint8Array} compressed -* @returns {Uint8Array} decompressed -*/ - -/** -* @callback WasmBuildCacheCallback -* @param {URL} url -* @param {WasmBuildDecompressCallback | undefined} decompress -* @returns {Promise} -*/ - -/** -* @typedef WasmBuildLoaderOptions -* @property {WebAssembly.Imports | undefined} imports - The Wasm module's imports. -* @property {WasmBuildCacheCallback} [cache] - A function that caches the Wasm module to -* a local path so that a network request isn't required on every load. -* -* Returns an ArrayBuffer with the bytes on download success, but cache save failure. -*/ - -class WasmBuildLoader { - /** @type {WasmBuildLoaderOptions} */ + return `class WasmBuildLoader { #options; - /** @type {Promise | undefined} */ #lastLoadPromise; - /** @type {WebAssembly.WebAssemblyInstantiatedSource | undefined} */ #instantiated; - /** @param {WasmBuildLoaderOptions} options */ constructor(options) { this.#options = options; } - /** @returns {WebAssembly.Instance | undefined} */ get instance() { return this.#instantiated?.instance; } - /** @returns {WebAssembly.Module | undefined} */ get module() { return this.#instantiated?.module; } - /** - * @param {URL} url - * @param {WasmBuildDecompressCallback | undefined} decompress - * @returns {Promise} - */ load( url, decompress, @@ -431,10 +380,6 @@ class WasmBuildLoader { return this.#lastLoadPromise; } - /** - * @param {URL} url - * @param {WasmBuildDecompressCallback | undefined} decompress - */ async #instantiate(url, decompress) { const imports = this.#options.imports; if (this.#options.cache != null && url.protocol !== "file:") { @@ -457,8 +402,7 @@ class WasmBuildLoader { const isFile = url.protocol === "file:"; // make file urls work in Node via dnt - const isNode = - (/** @type {any} */ (globalThis)).process?.versions?.node != null; + const isNode = globalThis.process?.versions?.node != null; if (isFile && typeof Deno !== "object") { throw new Error( "Loading local files are not supported in this environment", @@ -487,12 +431,7 @@ class WasmBuildLoader { wasmResponse.headers.get("content-type")?.toLowerCase() .startsWith("application/wasm") ) { - return WebAssembly.instantiateStreaming( - // Cast to any so there's no type checking issues with dnt - // (https://github.com/denoland/wasmbuild/issues/92) - /** @type {any} */ (wasmResponse), - imports, - ); + return WebAssembly.instantiateStreaming(wasmResponse, imports); } else { return WebAssembly.instantiate( await wasmResponse.arrayBuffer(), @@ -509,6 +448,84 @@ class WasmBuildLoader { } } +function getBindgenDtsOutput( + args: CheckCommand | BuildCommand, + bindgenOutput: BindgenOutput, +) { + switch (args.loaderKind) { + case "sync": + return getDtsSyncLoaderText(bindgenOutput); + case "async": + case "async-with-cache": + return getDtsAsyncLoaderText(bindgenOutput); + } +} + +function getDtsSyncLoaderText(bindgenOutput: BindgenOutput) { + return `${getCommonDtsLoaderText(bindgenOutput)} + +/** Instantiates an instance of the Wasm module returning its functions. +* @remarks It is safe to call this multiple times and once successfully +* loaded it will always return a reference to the same object. */ +export function instantiate(): InstantiateResult["exports"]; + +/** Instantiates an instance of the Wasm module along with its exports. + * @remarks It is safe to call this multiple times and once successfully + * loaded it will always return a reference to the same object. */ +export function instantiateWithInstance(): InstantiateResult; + +${getLibraryDts(bindgenOutput)}`; +} + +function getDtsAsyncLoaderText(bindgenOutput: BindgenOutput) { + return `${getCommonDtsLoaderText(bindgenOutput)} +/** Options for instantiating a Wasm instance. */ +export interface InstantiateOptions { + /** Optional url to the Wasm file to instantiate. */ + url?: URL; + /** Callback to decompress the raw Wasm file bytes before instantiating. */ + decompress?: (bytes: Uint8Array) => Uint8Array; +} + +/** Instantiates an instance of the Wasm module returning its functions. +* @remarks It is safe to call this multiple times and once successfully +* loaded it will always return a reference to the same object. */ +export function instantiate(opts?: InstantiateOptions): Promise; + +/** Instantiates an instance of the Wasm module along with its exports. + * @remarks It is safe to call this multiple times and once successfully + * loaded it will always return a reference to the same object. */ +export function instantiateWithInstance(opts?: InstantiateOptions): Promise; + +${getLibraryDts(bindgenOutput)}`; +} + +function getCommonDtsLoaderText(bindgenOutput: BindgenOutput) { + const exportNames = getExportNames(bindgenOutput); + return `// deno-lint-ignore-file +// deno-fmt-ignore-file + +export interface InstantiateResult { + instance: WebAssembly.Instance; + exports: { + ${exportNames.map((n) => `${n}: typeof ${n}`).join(";\n ")} + }; +} + +/** Gets if the Wasm module has been instantiated. */ +export function isInstantiated(): boolean; +`; +} + +function getLibraryDts(bindgenOutput: BindgenOutput) { + return bindgenOutput.ts.replace( + `/* tslint:disable */ +/* eslint-disable */ +`, + "", + ); +} + function getExportNames(bindgenOutput: BindgenOutput) { return Array.from(bindgenOutput.js.matchAll( /export (function|class) ([^({]+)[({]/g, diff --git a/lib/wasmbuild.generated.d.ts b/lib/wasmbuild.generated.d.ts new file mode 100644 index 0000000..2e40d0a --- /dev/null +++ b/lib/wasmbuild.generated.d.ts @@ -0,0 +1,37 @@ +// deno-lint-ignore-file +// deno-fmt-ignore-file + +export interface InstantiateResult { + instance: WebAssembly.Instance; + exports: { + generate_bindgen: typeof generate_bindgen + }; +} + +/** Gets if the Wasm module has been instantiated. */ +export function isInstantiated(): boolean; + +/** Options for instantiating a Wasm instance. */ +export interface InstantiateOptions { + /** Optional url to the Wasm file to instantiate. */ + url?: URL; + /** Callback to decompress the raw Wasm file bytes before instantiating. */ + decompress?: (bytes: Uint8Array) => Uint8Array; +} + +/** Instantiates an instance of the Wasm module returning its functions. +* @remarks It is safe to call this multiple times and once successfully +* loaded it will always return a reference to the same object. */ +export function instantiate(opts?: InstantiateOptions): Promise; + +/** Instantiates an instance of the Wasm module along with its exports. + * @remarks It is safe to call this multiple times and once successfully + * loaded it will always return a reference to the same object. */ +export function instantiateWithInstance(opts?: InstantiateOptions): Promise; + +/** +* @param {string} name +* @param {Uint8Array} wasm_bytes +* @returns {any} +*/ +export function generate_bindgen(name: string, wasm_bytes: Uint8Array): any; diff --git a/lib/wasmbuild.generated.js b/lib/wasmbuild.generated.js index f7db1cf..e7d65d0 100644 --- a/lib/wasmbuild.generated.js +++ b/lib/wasmbuild.generated.js @@ -2,7 +2,9 @@ // @ts-nocheck: generated // deno-lint-ignore-file // deno-fmt-ignore-file -// source-hash: ecca7e30c14f111590665815a71d35fed87d3f4f +/// + +// source-hash: 2767f1ac698c443db098ed8323a9d83ed8cff26e let wasm; const heap = new Array(128).fill(undefined); @@ -224,56 +226,23 @@ const imports = { }, }; -/** - * @callback WasmBuildDecompressCallback - * @param {Uint8Array} compressed - * @returns {Uint8Array} decompressed - */ - -/** - * @callback WasmBuildCacheCallback - * @param {URL} url - * @param {WasmBuildDecompressCallback | undefined} decompress - * @returns {Promise} - */ - -/** - * @typedef WasmBuildLoaderOptions - * @property {WebAssembly.Imports | undefined} imports - The Wasm module's imports. - * @property {WasmBuildCacheCallback} [cache] - A function that caches the Wasm module to - * a local path so that a network request isn't required on every load. - * - * Returns an ArrayBuffer with the bytes on download success, but cache save failure. - */ - class WasmBuildLoader { - /** @type {WasmBuildLoaderOptions} */ #options; - /** @type {Promise | undefined} */ #lastLoadPromise; - /** @type {WebAssembly.WebAssemblyInstantiatedSource | undefined} */ #instantiated; - /** @param {WasmBuildLoaderOptions} options */ constructor(options) { this.#options = options; } - /** @returns {WebAssembly.Instance | undefined} */ get instance() { return this.#instantiated?.instance; } - /** @returns {WebAssembly.Module | undefined} */ get module() { return this.#instantiated?.module; } - /** - * @param {URL} url - * @param {WasmBuildDecompressCallback | undefined} decompress - * @returns {Promise} - */ load( url, decompress, @@ -293,10 +262,6 @@ class WasmBuildLoader { return this.#lastLoadPromise; } - /** - * @param {URL} url - * @param {WasmBuildDecompressCallback | undefined} decompress - */ async #instantiate(url, decompress) { const imports = this.#options.imports; if (this.#options.cache != null && url.protocol !== "file:") { @@ -319,8 +284,7 @@ class WasmBuildLoader { const isFile = url.protocol === "file:"; // make file urls work in Node via dnt - const isNode = - (/** @type {any} */ (globalThis)).process?.versions?.node != null; + const isNode = globalThis.process?.versions?.node != null; if (isFile && typeof Deno !== "object") { throw new Error( "Loading local files are not supported in this environment", @@ -349,12 +313,7 @@ class WasmBuildLoader { wasmResponse.headers.get("content-type")?.toLowerCase() .startsWith("application/wasm") ) { - return WebAssembly.instantiateStreaming( - // Cast to any so there's no type checking issues with dnt - // (https://github.com/denoland/wasmbuild/issues/92) - /** @type {any} */ (wasmResponse), - imports, - ); + return WebAssembly.instantiateStreaming(wasmResponse, imports); } else { return WebAssembly.instantiate( await wasmResponse.arrayBuffer(), @@ -375,32 +334,11 @@ const loader = new WasmBuildLoader({ imports, cache: isNodeOrDeno ? cacheToLocalDir : undefined, }); -/** - * Options for instantiating a Wasm instance. - * @typedef {Object} InstantiateOptions - * @property {URL=} url - Optional url to the Wasm file to instantiate. - * @property {DecompressCallback=} decompress - Callback to decompress the - * raw Wasm file bytes before instantiating. - */ -/** Instantiates an instance of the Wasm module returning its functions. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - * @param {InstantiateOptions=} opts - */ export async function instantiate(opts) { return (await instantiateWithInstance(opts)).exports; } -/** Instantiates an instance of the Wasm module along with its exports. - * @remarks It is safe to call this multiple times and once successfully - * loaded it will always return a reference to the same object. - * @param {InstantiateOptions=} opts - * @returns {Promise<{ - * instance: WebAssembly.Instance; - * exports: { generate_bindgen: typeof generate_bindgen } - * }>} - */ export async function instantiateWithInstance(opts) { const { instance } = await loader.load( opts?.url ?? new URL("wasmbuild_bg.wasm", import.meta.url), @@ -419,7 +357,6 @@ function getWasmInstanceExports() { return { generate_bindgen }; } -/** Gets if the Wasm module has been instantiated. */ export function isInstantiated() { return loader.instance != null; } diff --git a/lib/wasmbuild_bg.wasm b/lib/wasmbuild_bg.wasm index e94750b..7d061cc 100644 Binary files a/lib/wasmbuild_bg.wasm and b/lib/wasmbuild_bg.wasm differ diff --git a/rs_lib/src/lib.rs b/rs_lib/src/lib.rs index 07f7d64..1105bf2 100644 --- a/rs_lib/src/lib.rs +++ b/rs_lib/src/lib.rs @@ -7,6 +7,7 @@ use wasm_bindgen::prelude::*; #[serde(rename_all = "camelCase")] pub struct Output { pub js: String, + pub ts: Option, pub snippets: HashMap>, pub local_modules: HashMap, pub wasm_bytes: Vec, @@ -27,11 +28,13 @@ pub fn generate_bindgen( fn inner(name: &str, wasm_bytes: Vec) -> Result { let mut x = wasm_bindgen_cli_support::Bindgen::new() .deno(true)? + .typescript(true) .input_bytes(name, wasm_bytes) .generate_output()?; Ok(Output { js: x.js().to_string(), + ts: x.ts().map(|t| t.to_string()), snippets: x.snippets().clone(), local_modules: x.local_modules().clone(), wasm_bytes: x.wasm_mut().emit_wasm(), diff --git a/scripts/update_deno_json_version.ts b/scripts/update_deno_json_version.ts new file mode 100644 index 0000000..b3f2153 --- /dev/null +++ b/scripts/update_deno_json_version.ts @@ -0,0 +1,16 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +import * as path from "@std/path"; + +// temporary until https://github.com/denoland/deno/issues/22663 is implemented + +const version = Deno.args[0]; +if (version == null || version.length === 0) { + throw new Error("Please provide a version."); +} + +const rootDir = path.dirname(import.meta.dirname!); +const denoJsonPath = path.join(rootDir, "/deno.json"); +const data = JSON.parse(Deno.readTextFileSync(denoJsonPath)); +data.version = version; +Deno.writeTextFileSync(denoJsonPath, JSON.stringify(data, undefined, 2) + "\n");