Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 47 additions & 64 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,6 @@
help="Build and run the integration tests (default is --no-integration-tests)",
)

parser.add_argument(
"--web-only",
action=argparse.BooleanOptionalAction,
default=False,
help="Build only the web version of the wasm package",
)

parser.add_argument(
"--ci-bench",
action=argparse.BooleanOptionalAction,
Expand Down Expand Up @@ -135,7 +128,6 @@
npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"

build_type = "debug" if args.debug else "release"
wasm_targets = ["web", "nodejs"] if not args.web_only else ["web"]
run_tests = args.test

root_dir = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -576,66 +568,57 @@ def run_ci_historic_benchmark():
subprocess.run(cargo_args, check=True, text=True, cwd=wasm_src)

# Next, create the JavaScript glue code using wasm-bindgen with something like:
# wasm-bindgen --target <nodejs|web> [--debug] --out-dir ./target/wasm32/{release,debug}/{nodejs|web} <infile>
# wasm-bindgen --target web [--debug] --out-dir ./target/wasm32/{release,debug}/web <infile>
#
# The output will be written to {out-dir}/qsc_wasm_bg.wasm
for target_platform in wasm_targets:
out_dir = os.path.join(wasm_bld, target_platform)
in_file = os.path.join(
root_dir, "target", "wasm32-unknown-unknown", build_type, "qsc_wasm.wasm"
)
out_dir = os.path.join(wasm_bld, "web")
in_file = os.path.join(
root_dir, "target", "wasm32-unknown-unknown", build_type, "qsc_wasm.wasm"
)

wasm_bindgen_args = [
"wasm-bindgen",
"--target",
"web",
"--out-dir",
out_dir,
]
if build_type == "debug":
wasm_bindgen_args.append("--debug")
wasm_bindgen_args.append(in_file)

subprocess.run(wasm_bindgen_args, check=True, text=True, cwd=wasm_src)

wasm_bindgen_args = [
"wasm-bindgen",
"--target",
target_platform,
"--out-dir",
out_dir,
# Also run wasm-opt to optimize the wasm file for release builds with:
# wasm-opt -Oz --enable-{<as needed>} --output <outfile> <infile>
#
# -Oz does extra size optimizations, and features are enabled to match Rust defaults
# to avoid mismatch errors, as wasm-opt currently disables some of these by default.
# See <https://doc.rust-lang.org/rustc/platform-support/wasm32-unknown-unknown.html#enabled-webassembly-features>
#
# This updates the wasm file in place.
#
# Note: wasm-opt is not needed for debug builds, so we only run it for release builds.
if build_type == "release":
wasm_file = os.path.join(out_dir, "qsc_wasm_bg.wasm")
wasm_opt_args = [
"wasm-opt",
"-Oz",
"--enable-bulk-memory",
"--enable-nontrapping-float-to-int",
"--output",
wasm_file,
wasm_file,
]
if build_type == "debug":
wasm_bindgen_args.append("--debug")
wasm_bindgen_args.append(in_file)

subprocess.run(wasm_bindgen_args, check=True, text=True, cwd=wasm_src)

# Also run wasm-opt to optimize the wasm file for release builds with:
# wasm-opt -Oz --enable-{<as needed>} --output <outfile> <infile>
#
# -Oz does extra size optimizations, and features are enabled to match Rust defaults
# to avoid mismatch errors, as wasm-opt currently disables some of these by default.
# See <https://doc.rust-lang.org/rustc/platform-support/wasm32-unknown-unknown.html#enabled-webassembly-features>
#
# This updates the wasm file in place.
#
# Note: wasm-opt is not needed for debug builds, so we only run it for release builds.
if build_type == "release":
wasm_file = os.path.join(out_dir, "qsc_wasm_bg.wasm")
wasm_opt_args = [
"wasm-opt",
"-Oz",
"--enable-bulk-memory",
"--enable-nontrapping-float-to-int",
"--output",
wasm_file,
wasm_file,
]
subprocess.run(wasm_opt_args, check=True, text=True, cwd=wasm_src)

# After building, copy the artifacts to the npm location
lib_dir = os.path.join(npm_src, "lib", target_platform)
os.makedirs(lib_dir, exist_ok=True)

for filename in ["qsc_wasm_bg.wasm", "qsc_wasm.d.ts", "qsc_wasm.js"]:
fullpath = os.path.join(out_dir, filename)

# To make the node files CommonJS modules, the extension needs to change
# (This is because the package is set to ECMAScript modules by default)
if target_platform == "nodejs" and filename == "qsc_wasm.js":
filename = "qsc_wasm.cjs"
if target_platform == "nodejs" and filename == "qsc_wasm.d.ts":
filename = "qsc_wasm.d.cts"

shutil.copy2(fullpath, os.path.join(lib_dir, filename))
subprocess.run(wasm_opt_args, check=True, text=True, cwd=wasm_src)

# After building, copy the artifacts to the npm location
lib_dir = os.path.join(npm_src, "lib", "web")
os.makedirs(lib_dir, exist_ok=True)

for filename in ["qsc_wasm_bg.wasm", "qsc_wasm.d.ts", "qsc_wasm.js"]:
fullpath = os.path.join(out_dir, filename)
shutil.copy2(fullpath, os.path.join(lib_dir, filename))

step_end()

Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 5 additions & 22 deletions source/npm/qsharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,11 @@ to it when calling the `loadWasmModule` method so it may be located and loaded.

## Node and browser support

wasm-bindgen generates different files for the browser and Node.js environments. The wasm is slightly
different, and the loader code is quite different. This can be seen in `./lib/web/qsc_wasm.cjs`
and `./lib/nodejs/qsc_wasm.js` files respectively. Specifically, the web environment loads the wasm
file using async web APIs such as `fetch` with a URI, and Node.js uses `require` to load the `fs` module
and calls to `readFileSync`. Once the wasm module is loaded however, the exported APIs are used
in a similar manner.

To support using this npm package from both environments, the package uses "conditional exports"
<https://nodejs.org/dist/latest-v18.x/docs/api/packages.html#conditional-exports> to expose one
entry point for Node.js, and another for browsers. The distinct entry points uses their respective
loader to load the wasm module for the platform, and then expose functionality that uses the
loaded module via common code.

When bundling for the web, bundlers such as esbuild will automatically use the default entry point,
whereas when loaded as a Node.js module, it will use the "node" entry point.

Note that TypeScript seems to add the ['import', 'types', 'node'] conditions by default when
searching the Node.js `exports`, and so will always find the 'node' export before the 'default'
export. To resolve this, a 'browser' condition was added (which is same as 'default' but earlier
than 'node') and the tsconfig compiler option `"customConditions": ["browser"]` should be added
(requires TypeScript 5.0 or later). esbuild also adds the 'browser' condition when bundling for
the browser (see <https://esbuild.github.io/api/#how-conditions-work>).
This package is platform-agnostic, using a single entry point (`main.ts`) for both browser
and Node.js environments. The wasm module and JavaScript glue code can be found in `./lib/web/`.
Consumers must bundle the package for their target platform so that external
dependencies (such as the `web-worker` package) are resolved correctly for the
runtime environment.

## Design

Expand Down
9 changes: 7 additions & 2 deletions source/npm/qsharp/generate_docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

// @ts-check

import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { generate_docs } from "./lib/nodejs/qsc_wasm.cjs";
import initWasm, { generate_docs } from "./lib/web/qsc_wasm.js";

const scriptDirPath = dirname(fileURLToPath(import.meta.url));
const docsDirPath = join(scriptDirPath, "docs");
Expand All @@ -16,6 +16,11 @@ if (!existsSync(docsDirPath)) {
mkdirSync(docsDirPath);
}

// Initialize wasm before calling any exported functions
const wasmPath = join(scriptDirPath, "lib", "web", "qsc_wasm_bg.wasm");
const wasmBytes = readFileSync(wasmPath);
await initWasm({ module_or_path: wasmBytes });

// 'filename' will be of the format 'namespace/api.md' (except for 'toc.yaml')
// 'metadata' will be the metadata that will appear at the top of the file
// 'contents' will contain the non-metadata markdown expected
Expand Down
17 changes: 8 additions & 9 deletions source/npm/qsharp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,10 @@
"directory": "npm"
},
"exports": {
".": {
"browser": "./dist/browser.js",
"node": "./dist/main.js",
"default": "./dist/browser.js"
},
"./compiler-worker": "./dist/compiler/worker-browser.js",
"./language-service-worker": "./dist/language-service/worker-browser.js",
"./debug-service-worker": "./dist/debug-service/worker-browser.js",
".": "./dist/main.js",
"./compiler-worker": "./dist/compiler/worker.js",
"./language-service-worker": "./dist/language-service/worker.js",
"./debug-service-worker": "./dist/debug-service/worker.js",
"./katas": "./dist/katas.js",
"./katas-md": "./dist/katas-md.js",
"./state-viz": "./ux/circuit-vis/state-viz/worker/index.ts",
Expand All @@ -44,5 +40,8 @@
"docs",
"lib",
"ux"
]
],
"dependencies": {
"web-worker": "^1.5.0"
}
}
Loading
Loading