@cfasim-ui/wasm 0.8.13 → 0.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/vitePlugin.js +36 -2
- package/src/vitePlugin.test.ts +85 -0
- package/src/wasm.worker.ts +21 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfasim-ui/wasm",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WebAssembly integration for cfasim-ui",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"./vite": "./src/vitePlugin.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cfasim-ui/shared": "0.8.
|
|
23
|
+
"@cfasim-ui/shared": "0.8.15"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"vue": "^3.5.0"
|
package/src/vitePlugin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import { basename, resolve } from "node:path";
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -7,10 +9,35 @@ import { basename, resolve } from "node:path";
|
|
|
7
9
|
* @property {string} [name] Output name used for the wasm module (default: project directory name)
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
/** Global injected via Vite `define`; the worker appends it as `?v=` to wasm URLs. */
|
|
13
|
+
export const WASM_VERSIONS_DEFINE = "__CFASIM_WASM_VERSIONS__";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hash the wasm-pack output for a model so its URLs change whenever the
|
|
17
|
+
* compiled module or its JS glue changes. Both are served from fixed paths
|
|
18
|
+
* under public/, so without this a CDN or browser cache can pair a new app
|
|
19
|
+
* bundle with a stale wasm build.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} outDir wasm-pack output directory
|
|
22
|
+
* @param {string} name wasm module name
|
|
23
|
+
* @returns {string} 12 hex chars of sha256 over `<name>_bg.wasm` + `<name>.js`
|
|
24
|
+
*/
|
|
25
|
+
export function hashWasmOutput(outDir, name) {
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
for (const file of [`${name}_bg.wasm`, `${name}.js`]) {
|
|
28
|
+
hash.update(readFileSync(resolve(outDir, file)));
|
|
29
|
+
}
|
|
30
|
+
return hash.digest("hex").slice(0, 12);
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
/**
|
|
11
34
|
* Vite plugin that builds a Rust model to WebAssembly via wasm-pack
|
|
12
35
|
* and outputs it to public/wasm/{name}/.
|
|
13
36
|
*
|
|
37
|
+
* The build runs in the `config` hook so the output hash can be exposed to
|
|
38
|
+
* the app through `define` as `__CFASIM_WASM_VERSIONS__` (a map of model
|
|
39
|
+
* name to hash). Several instances merge into the same map.
|
|
40
|
+
*
|
|
14
41
|
* @param {CfasimWasmOptions} [options]
|
|
15
42
|
* @returns {import("vite").Plugin}
|
|
16
43
|
*/
|
|
@@ -24,12 +51,19 @@ export function cfasimWasm(options) {
|
|
|
24
51
|
cwd: root,
|
|
25
52
|
stdio: "pipe",
|
|
26
53
|
});
|
|
54
|
+
return { name, version: hashWasmOutput(outDir, name) };
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
return {
|
|
30
58
|
name: "cfasim-wasm",
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
config(userConfig) {
|
|
60
|
+
// Mirrors Vite's own root resolution, which has not happened yet.
|
|
61
|
+
const root = userConfig.root ? resolve(userConfig.root) : process.cwd();
|
|
62
|
+
const { name, version } = build(root);
|
|
63
|
+
const existing = userConfig.define?.[WASM_VERSIONS_DEFINE];
|
|
64
|
+
const versions = typeof existing === "string" ? JSON.parse(existing) : {};
|
|
65
|
+
versions[name] = version;
|
|
66
|
+
return { define: { [WASM_VERSIONS_DEFINE]: JSON.stringify(versions) } };
|
|
33
67
|
},
|
|
34
68
|
};
|
|
35
69
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
cfasimWasm,
|
|
8
|
+
hashWasmOutput,
|
|
9
|
+
WASM_VERSIONS_DEFINE,
|
|
10
|
+
} from "./vitePlugin.js";
|
|
11
|
+
|
|
12
|
+
vi.mock("node:child_process", () => ({ execSync: vi.fn() }));
|
|
13
|
+
|
|
14
|
+
let root: string;
|
|
15
|
+
|
|
16
|
+
function writeOutput(name: string, wasm: string, js: string) {
|
|
17
|
+
const dir = resolve(root, "public", "wasm", name);
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
writeFileSync(resolve(dir, `${name}_bg.wasm`), wasm);
|
|
20
|
+
writeFileSync(resolve(dir, `${name}.js`), js);
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runConfig(
|
|
25
|
+
plugin: ReturnType<typeof cfasimWasm>,
|
|
26
|
+
userConfig: Record<string, unknown>,
|
|
27
|
+
): Record<string, string> {
|
|
28
|
+
const hook = plugin.config as (c: unknown) => {
|
|
29
|
+
define: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
return JSON.parse(hook(userConfig).define[WASM_VERSIONS_DEFINE]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
root = mkdtempSync(resolve(tmpdir(), "cfasim-wasm-"));
|
|
36
|
+
vi.mocked(execSync).mockClear();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
rmSync(root, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("hashWasmOutput", () => {
|
|
44
|
+
it("changes when either the wasm or the glue changes", () => {
|
|
45
|
+
const dir = writeOutput("m", "wasm-a", "js-a");
|
|
46
|
+
const base = hashWasmOutput(dir, "m");
|
|
47
|
+
expect(base).toMatch(/^[0-9a-f]{12}$/);
|
|
48
|
+
expect(hashWasmOutput(dir, "m")).toBe(base);
|
|
49
|
+
|
|
50
|
+
writeOutput("m", "wasm-b", "js-a");
|
|
51
|
+
const wasmChanged = hashWasmOutput(dir, "m");
|
|
52
|
+
expect(wasmChanged).not.toBe(base);
|
|
53
|
+
|
|
54
|
+
writeOutput("m", "wasm-b", "js-b");
|
|
55
|
+
expect(hashWasmOutput(dir, "m")).not.toBe(wasmChanged);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("cfasimWasm", () => {
|
|
60
|
+
it("builds with wasm-pack and defines the output hash by model name", () => {
|
|
61
|
+
const dir = writeOutput("my_sim", "wasm", "js");
|
|
62
|
+
const versions = runConfig(cfasimWasm({ model: "..", name: "my-sim" }), {
|
|
63
|
+
root,
|
|
64
|
+
});
|
|
65
|
+
expect(versions).toEqual({ my_sim: hashWasmOutput(dir, "my_sim") });
|
|
66
|
+
expect(execSync).toHaveBeenCalledWith(
|
|
67
|
+
`wasm-pack build .. --target web --out-dir ${dir}`,
|
|
68
|
+
expect.objectContaining({ cwd: root }),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("merges into a versions map defined by an earlier instance", () => {
|
|
73
|
+
const a = writeOutput("a", "wasm-a", "js-a");
|
|
74
|
+
const b = writeOutput("b", "wasm-b", "js-b");
|
|
75
|
+
const first = runConfig(cfasimWasm({ name: "a" }), { root });
|
|
76
|
+
const second = runConfig(cfasimWasm({ name: "b" }), {
|
|
77
|
+
root,
|
|
78
|
+
define: { [WASM_VERSIONS_DEFINE]: JSON.stringify(first) },
|
|
79
|
+
});
|
|
80
|
+
expect(second).toEqual({
|
|
81
|
+
a: hashWasmOutput(a, "a"),
|
|
82
|
+
b: hashWasmOutput(b, "b"),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/wasm.worker.ts
CHANGED
|
@@ -10,13 +10,31 @@ const modulePromises = new Map<string, Promise<Record<string, any>>>();
|
|
|
10
10
|
|
|
11
11
|
const baseUrl = import.meta.env.BASE_URL ?? "/";
|
|
12
12
|
|
|
13
|
+
// Injected by the `cfasimWasm` Vite plugin (model name -> hash of the
|
|
14
|
+
// wasm-pack output). Absent when an app ships prebuilt wasm without it.
|
|
15
|
+
declare const __CFASIM_WASM_VERSIONS__: Record<string, string> | undefined;
|
|
16
|
+
|
|
17
|
+
function wasmVersion(model: string): string | undefined {
|
|
18
|
+
if (typeof __CFASIM_WASM_VERSIONS__ === "undefined") return undefined;
|
|
19
|
+
return __CFASIM_WASM_VERSIONS__?.[model];
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
23
|
function ensureModule(model: string): Promise<Record<string, any>> {
|
|
15
24
|
if (!modulePromises.has(model)) {
|
|
16
25
|
const promise = (async () => {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
26
|
+
const dir = `${self.location.origin}${baseUrl}wasm/${model}/`;
|
|
27
|
+
const version = wasmVersion(model);
|
|
28
|
+
const query = version ? `?v=${version}` : "";
|
|
29
|
+
const mod = await import(/* @vite-ignore */ `${dir}${model}.js${query}`);
|
|
30
|
+
// The glue resolves `<model>_bg.wasm` against its own URL, which drops
|
|
31
|
+
// the query, so the wasm path is passed explicitly when versioned.
|
|
32
|
+
// The object form needs wasm-bindgen >= 0.2.93.
|
|
33
|
+
if (version) {
|
|
34
|
+
await mod.default({ module_or_path: `${dir}${model}_bg.wasm${query}` });
|
|
35
|
+
} else {
|
|
36
|
+
await mod.default();
|
|
37
|
+
}
|
|
20
38
|
return mod;
|
|
21
39
|
})();
|
|
22
40
|
promise.catch(() => {
|