nanocodex-vite 0.6.4 → 0.6.6
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanocodex-vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Vite, Cloudflare, WASM, ChatGPT development, and OAuth relay integration for Nanocodex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@cloudflare/vite-plugin": "1.54.2",
|
|
34
34
|
"@types/node": "^22.20.1",
|
|
35
|
-
"nanocodex": "0.6.
|
|
35
|
+
"nanocodex": "0.6.6",
|
|
36
36
|
"typescript": "5.9.3",
|
|
37
37
|
"vite": "8.2.1",
|
|
38
38
|
"wrangler": "4.127.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@cloudflare/vite-plugin": ">=1.54.2",
|
|
42
|
-
"nanocodex": "^0.6.
|
|
42
|
+
"nanocodex": "^0.6.6",
|
|
43
43
|
"vite": ">=6.1.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
@@ -65,10 +65,22 @@ cleanup() {
|
|
|
65
65
|
|
|
66
66
|
trap cleanup EXIT
|
|
67
67
|
|
|
68
|
+
cache_helper="js/nanocodex-vite/scripts/wasm-output-cache.mjs"
|
|
69
|
+
if node "$cache_helper" check "$build_mode"; then
|
|
70
|
+
node js/nanocodex/scripts/write-wasm-attestation.mjs .ci-wasm-cache/source.wasm
|
|
71
|
+
echo "WASM outputs are current; skipped Cargo and binding generation"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ "$(wasm-bindgen --version)" != "wasm-bindgen 0.2.126" ]]; then
|
|
76
|
+
echo "Nanocodex WASM requires wasm-bindgen 0.2.126" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
68
80
|
if [[ "$build_mode" == release ]]; then
|
|
69
|
-
cargo build --locked -p nanocodex-wasm --target "$wasm_target" --profile wasm
|
|
81
|
+
cargo +1.97 build --locked -p nanocodex-wasm --target "$wasm_target" --profile wasm
|
|
70
82
|
else
|
|
71
|
-
cargo build --locked -p nanocodex-wasm --target "$wasm_target"
|
|
83
|
+
cargo +1.97 build --locked -p nanocodex-wasm --target "$wasm_target"
|
|
72
84
|
fi
|
|
73
85
|
wasm_artifact="$target_dir/$wasm_target/$cargo_profile/nanocodex_wasm.wasm"
|
|
74
86
|
stamp_path="js/nanocodex/pkg-web/.nanocodex-bindgen-stamp"
|
|
@@ -84,7 +96,15 @@ fingerprint="$({
|
|
|
84
96
|
wasm-bindgen --version
|
|
85
97
|
printf 'build-mode=%s\n' "$build_mode"
|
|
86
98
|
printf 'worker-bundler-v1-simd\n'
|
|
87
|
-
|
|
99
|
+
# A source-cache miss must not bless bindings made by older generation policy.
|
|
100
|
+
for generator in "$script_path" "$cache_helper" \
|
|
101
|
+
js/nanocodex-vite/scripts/wasm-memory-views.mjs \
|
|
102
|
+
js/nanocodex/scripts/deduplicate-wasm.mjs \
|
|
103
|
+
js/nanocodex/scripts/write-package-types.mjs \
|
|
104
|
+
js/nanocodex/scripts/write-wasm-attestation.mjs \
|
|
105
|
+
js/nanocodex/scripts/check-managed-wasm.mjs; do
|
|
106
|
+
cksum < "$generator"
|
|
107
|
+
done
|
|
88
108
|
if [[ "$build_mode" == release ]]; then
|
|
89
109
|
"$binaryen" --version
|
|
90
110
|
fi
|
|
@@ -100,6 +120,7 @@ if [[ -f "$stamp_path" ]] \
|
|
|
100
120
|
&& [[ "$(<"$stamp_path")" == "$fingerprint" ]] \
|
|
101
121
|
&& node js/nanocodex/scripts/write-wasm-attestation.mjs --check-cache "$wasm_artifact" 2>/dev/null; then
|
|
102
122
|
node js/nanocodex/scripts/write-wasm-attestation.mjs "$wasm_artifact"
|
|
123
|
+
node "$cache_helper" save "$build_mode" "$wasm_artifact"
|
|
103
124
|
echo "wasm-bindgen outputs are current"
|
|
104
125
|
exit 0
|
|
105
126
|
fi
|
|
@@ -145,3 +166,4 @@ node js/nanocodex/scripts/deduplicate-wasm.mjs
|
|
|
145
166
|
node js/nanocodex/scripts/write-package-types.mjs
|
|
146
167
|
printf '%s\n' "$fingerprint" > "$stamp_path"
|
|
147
168
|
node js/nanocodex/scripts/write-wasm-attestation.mjs "$wasm_artifact"
|
|
169
|
+
node "$cache_helper" save "$build_mode" "$wasm_artifact"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Uses Node built-ins and Python 3.11+ tomllib; no Rust or pnpm setup needed.
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, relative, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { assertCachedManagedWasmAttestation, hashManagedWasmArtifacts } from "../../nanocodex/scripts/check-managed-wasm.mjs";
|
|
9
|
+
|
|
10
|
+
export const rustToolchain = "1.97";
|
|
11
|
+
const root = fileURLToPath(new URL("../../../", import.meta.url));
|
|
12
|
+
const metadataPath = ".ci-wasm-cache/outputs.json";
|
|
13
|
+
const rawPath = ".ci-wasm-cache/source.wasm";
|
|
14
|
+
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
const excluded = new Set([".git", "target", "node_modules", "pkg-web", "pkg-node"]);
|
|
16
|
+
|
|
17
|
+
async function walk(directory, skipStandaloneTests = false) {
|
|
18
|
+
const result = [];
|
|
19
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
20
|
+
if (excluded.has(entry.name) || (skipStandaloneTests && ["tests", "benches"].includes(entry.name))) continue;
|
|
21
|
+
const path = resolve(directory, entry.name);
|
|
22
|
+
if (entry.isDirectory()) result.push(...await walk(path));
|
|
23
|
+
else if (entry.isFile() || entry.isSymbolicLink()) result.push(path);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Retain optional dependencies and unknown cfgs, excluding only target tables
|
|
29
|
+
// proven inapplicable to wasm32. Host build/proc-macro dependencies stay broad.
|
|
30
|
+
// Registry/git dependencies are pinned by Cargo.lock. No Cargo metadata call.
|
|
31
|
+
function dependencyDirectories(repository) {
|
|
32
|
+
// Parse the entire closure in one Python process. tomllib handles all valid
|
|
33
|
+
// Cargo TOML key/table/string layouts instead of approximating that grammar.
|
|
34
|
+
return JSON.parse(execFileSync("python3", ["-c", String.raw`
|
|
35
|
+
import json, pathlib, re, sys, tomllib
|
|
36
|
+
root = pathlib.Path(sys.argv[1]).resolve()
|
|
37
|
+
def manifest(directory):
|
|
38
|
+
with (directory / "Cargo.toml").open("rb") as file:
|
|
39
|
+
return tomllib.load(file)
|
|
40
|
+
workspace = manifest(root).get("workspace", {}).get("dependencies", {})
|
|
41
|
+
|
|
42
|
+
def wasm_cfg(platform):
|
|
43
|
+
# Three-valued evaluation: unsupported predicates/syntax remain included.
|
|
44
|
+
# Cargo target predicates do not depend on optional dependency activation.
|
|
45
|
+
expression = re.fullmatch(r"cfg\s*\((.*)\)", platform.strip(), re.S)
|
|
46
|
+
if expression is None:
|
|
47
|
+
return None if re.match(r"cfg\b", platform.strip()) else platform == "wasm32-unknown-unknown"
|
|
48
|
+
text = expression[1].strip()
|
|
49
|
+
tokens = []
|
|
50
|
+
while text:
|
|
51
|
+
match = re.match(r'\s*([A-Za-z_][A-Za-z_0-9]*|"(?:[^"\\]|\\.)*"|[(),=])', text)
|
|
52
|
+
if not match:
|
|
53
|
+
return None
|
|
54
|
+
tokens.append(match[1])
|
|
55
|
+
text = text[match.end():].strip()
|
|
56
|
+
position = 0
|
|
57
|
+
def take():
|
|
58
|
+
nonlocal position
|
|
59
|
+
token = tokens[position]
|
|
60
|
+
position += 1
|
|
61
|
+
return token
|
|
62
|
+
def peek():
|
|
63
|
+
return tokens[position] if position < len(tokens) else None
|
|
64
|
+
def parse():
|
|
65
|
+
name = take()
|
|
66
|
+
if not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', name):
|
|
67
|
+
raise ValueError("unknown cfg syntax")
|
|
68
|
+
if peek() == "=":
|
|
69
|
+
take()
|
|
70
|
+
value = json.loads(take())
|
|
71
|
+
known = {"target_arch": "wasm32", "target_os": "unknown", "target_family": "wasm",
|
|
72
|
+
"target_env": "", "target_vendor": "unknown", "target_pointer_width": "32",
|
|
73
|
+
"target_endian": "little"}
|
|
74
|
+
return known[name] == value if name in known else None
|
|
75
|
+
if peek() == "(":
|
|
76
|
+
take()
|
|
77
|
+
values = []
|
|
78
|
+
while peek() != ")":
|
|
79
|
+
values.append(parse())
|
|
80
|
+
if peek() != ",":
|
|
81
|
+
break
|
|
82
|
+
take()
|
|
83
|
+
if take() != ")":
|
|
84
|
+
raise ValueError("unterminated cfg")
|
|
85
|
+
if name == "all":
|
|
86
|
+
return False if False in values else (None if None in values else True)
|
|
87
|
+
if name == "any":
|
|
88
|
+
return True if True in values else (None if None in values else False)
|
|
89
|
+
if name == "not" and len(values) == 1:
|
|
90
|
+
return None if values[0] is None else not values[0]
|
|
91
|
+
return None
|
|
92
|
+
return False if name in ("unix", "windows") else None
|
|
93
|
+
try:
|
|
94
|
+
value = parse()
|
|
95
|
+
return value if position == len(tokens) else None
|
|
96
|
+
except (IndexError, ValueError, TypeError):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
visited = set()
|
|
100
|
+
inputs = {}
|
|
101
|
+
def visit(directory, target=True):
|
|
102
|
+
directory = directory.resolve()
|
|
103
|
+
if not directory.is_relative_to(root):
|
|
104
|
+
raise ValueError("local Rust dependencies must remain inside repository")
|
|
105
|
+
data = manifest(directory)
|
|
106
|
+
# A proc macro and its dependency tree compile for the build host.
|
|
107
|
+
target = target and not data.get("lib", {}).get("proc-macro", False)
|
|
108
|
+
if (directory, target) in visited:
|
|
109
|
+
return
|
|
110
|
+
visited.add((directory, target))
|
|
111
|
+
build_script = (directory / "build.rs").exists() or bool(data.get("package", {}).get("build", False))
|
|
112
|
+
entry_points = [data.get("lib", {}), *data.get("bin", [])]
|
|
113
|
+
# Explicit production entry points can live in normally test-only folders.
|
|
114
|
+
keep_tests = build_script or any(pathlib.PurePosixPath(entry.get("path", "")).parts[:1] in [("tests",), ("benches",)] for entry in entry_points)
|
|
115
|
+
inputs[str(directory)] = {"directory": str(directory), "buildScript": build_script, "skipStandaloneTests": not keep_tests}
|
|
116
|
+
tables = [(None, data), *data.get("target", {}).items()]
|
|
117
|
+
for platform, table in tables:
|
|
118
|
+
for kind in ("dependencies", "build-dependencies"):
|
|
119
|
+
# Target-specific build dependencies are selected for the host.
|
|
120
|
+
dependency_target = target and kind != "build-dependencies"
|
|
121
|
+
if dependency_target and platform is not None and wasm_cfg(platform) is False:
|
|
122
|
+
continue
|
|
123
|
+
for name, dependency in table.get(kind, {}).items():
|
|
124
|
+
if not isinstance(dependency, dict):
|
|
125
|
+
continue
|
|
126
|
+
base = directory
|
|
127
|
+
if dependency.get("workspace"):
|
|
128
|
+
dependency = workspace[name]
|
|
129
|
+
base = root
|
|
130
|
+
if isinstance(dependency, dict) and "path" in dependency:
|
|
131
|
+
visit(base / dependency["path"], dependency_target)
|
|
132
|
+
visit(root / "js/nanocodex")
|
|
133
|
+
print(json.dumps([inputs[path] for path in sorted(inputs)]))
|
|
134
|
+
`, repository], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function fingerprint(repository = root, mode = "release", environment = process.env) {
|
|
138
|
+
try { return await fingerprintInputs(repository, mode, environment); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
// A new unsupported manifest/include cannot accidentally reuse stale output.
|
|
141
|
+
// A unique key still permits the ordinary build and its cache-save step.
|
|
142
|
+
console.error(`WASM cache reuse disabled: ${error.stderr?.toString().trim().split("\n").at(-1) || error.message.split("\n")[0]}`);
|
|
143
|
+
return sha(randomUUID());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Release identity must be deterministic: callers receive resolution errors.
|
|
148
|
+
// Only fingerprint(), the output-cache API, converts errors into non-reuse.
|
|
149
|
+
export async function fingerprintInputs(repository = root, mode = "release", environment = process.env) {
|
|
150
|
+
repository = await realpath(repository);
|
|
151
|
+
assert.ok(["release", "development"].includes(mode));
|
|
152
|
+
const files = new Set();
|
|
153
|
+
const omittedTestDirectories = new Set();
|
|
154
|
+
for (const { directory, buildScript, skipStandaloneTests } of dependencyDirectories(repository)) {
|
|
155
|
+
files.add(resolve(directory, "Cargo.toml"));
|
|
156
|
+
if (skipStandaloneTests) for (const name of ["tests", "benches"]) omittedTestDirectories.add(resolve(directory, name));
|
|
157
|
+
if (directory === resolve(repository, "js/nanocodex") && !buildScript) {
|
|
158
|
+
for (const path of await walk(resolve(directory, "src"))) files.add(path);
|
|
159
|
+
for (const name of ["build.rs", "README.md"]) {
|
|
160
|
+
try { await readFile(resolve(directory, name)); files.add(resolve(directory, name)); }
|
|
161
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
for (const path of await walk(directory, skipStandaloneTests)) files.add(path);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const name of ["Cargo.toml", "Cargo.lock", "js/nanocodex-vite/scripts/build-js-package.sh",
|
|
168
|
+
"js/nanocodex-vite/scripts/wasm-output-cache.mjs", "js/nanocodex-vite/scripts/wasm-memory-views.mjs",
|
|
169
|
+
"js/nanocodex/scripts/deduplicate-wasm.mjs", "js/nanocodex/scripts/write-package-types.mjs",
|
|
170
|
+
"js/nanocodex/scripts/write-wasm-attestation.mjs", "js/nanocodex/scripts/check-managed-wasm.mjs"]) files.add(resolve(repository, name));
|
|
171
|
+
for (const name of [".cargo", "rust-toolchain", "rust-toolchain.toml"]) {
|
|
172
|
+
try {
|
|
173
|
+
if (name === ".cargo") for (const path of await walk(resolve(repository, name))) files.add(path);
|
|
174
|
+
else { await readFile(resolve(repository, name)); files.add(resolve(repository, name)); }
|
|
175
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
176
|
+
}
|
|
177
|
+
// Rust literal include/#[path] references can leave their crate directory.
|
|
178
|
+
// Follow those recursively while retaining checkout-relative content keys.
|
|
179
|
+
for (const path of files) {
|
|
180
|
+
if (!path.endsWith(".rs")) continue;
|
|
181
|
+
const source = await readFile(path, "utf8");
|
|
182
|
+
for (const match of source.matchAll(/(?:include(?:_str|_bytes)?!\s*\(\s*|#\[path\s*=\s*)(?:r(#{0,8}))?"([^"\n]+)"/g)) {
|
|
183
|
+
const included = resolve(dirname(path), match[2]);
|
|
184
|
+
assert.ok(!relative(repository, included).startsWith(".."), "Rust include must remain inside repository");
|
|
185
|
+
await readFile(included);
|
|
186
|
+
files.add(included);
|
|
187
|
+
// A production Rust module in tests/ can itself use ordinary mod children.
|
|
188
|
+
// Retain that subtree rather than approximating Rust module resolution.
|
|
189
|
+
if (included.endsWith(".rs")) for (const directory of omittedTestDirectories) {
|
|
190
|
+
if (!relative(directory, included).startsWith("..")) {
|
|
191
|
+
for (const path of await walk(directory)) files.add(path);
|
|
192
|
+
omittedTestDirectories.delete(directory);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const pkg = JSON.parse(await readFile(resolve(repository, "js/nanocodex/package.json"), "utf8"));
|
|
198
|
+
const buildEnvironment = Object.fromEntries(Object.entries(environment)
|
|
199
|
+
.filter(([name]) => /^(RUSTFLAGS|CARGO_ENCODED_RUSTFLAGS|RUSTC|RUSTC_WRAPPER|RUSTC_WORKSPACE_WRAPPER|CARGO_INCREMENTAL|CARGO_PROFILE_.*|CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_.*)$/.test(name))
|
|
200
|
+
// cargo +1.97 overrides RUSTUP_TOOLCHAIN. Dev/test overrides do not affect
|
|
201
|
+
// --profile wasm, and rust-toolchain setup commonly introduces them.
|
|
202
|
+
.filter(([name, value]) => name !== "CARGO_INCREMENTAL" || value !== (mode === "release" ? "0" : "1"))
|
|
203
|
+
.filter(([name]) => !name.startsWith("CARGO_PROFILE_") || (mode === "release"
|
|
204
|
+
? /^CARGO_PROFILE_(WASM|RELEASE)_/.test(name)
|
|
205
|
+
: /^CARGO_PROFILE_DEV_/.test(name))).sort());
|
|
206
|
+
const hash = createHash("sha256");
|
|
207
|
+
hash.update(JSON.stringify({ schema: 1, mode, rustToolchain, bindgen: "0.2.126", binaryen: pkg.devDependencies.binaryen, buildEnvironment }));
|
|
208
|
+
for (const path of [...files].sort()) hash.update(JSON.stringify([relative(repository, path), sha(await readFile(path))]));
|
|
209
|
+
return hash.digest("hex");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function outputs(repository) {
|
|
213
|
+
const web = pathToFileURL(`${resolve(repository, "js/nanocodex/pkg-web")}/`);
|
|
214
|
+
const artifacts = await hashManagedWasmArtifacts(web);
|
|
215
|
+
const node = {};
|
|
216
|
+
for (const name of ["nanocodex.js", "nanocodex.d.ts", "package.json"]) node[name] = sha(await readFile(resolve(repository, "js/nanocodex/pkg-node", name)));
|
|
217
|
+
return { artifacts, node, sourceWasmSha256: sha(await readFile(resolve(repository, rawPath))) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function check(repository = root, mode = "release") {
|
|
221
|
+
const retained = JSON.parse(await readFile(resolve(repository, metadataPath), "utf8"));
|
|
222
|
+
assert.equal(retained.schema, 1);
|
|
223
|
+
assert.equal(retained.fingerprint, await fingerprint(repository, mode));
|
|
224
|
+
const current = await outputs(repository);
|
|
225
|
+
assert.deepEqual(retained.outputs, current);
|
|
226
|
+
assertCachedManagedWasmAttestation(JSON.parse(await readFile(resolve(repository, "js/nanocodex/pkg-web/nanocodex-build.json"), "utf8")), current);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function atomicWrite(path, bytes) {
|
|
230
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
231
|
+
try { await writeFile(temporary, bytes, { flag: "wx" }); await rename(temporary, path); }
|
|
232
|
+
finally { await rm(temporary, { force: true }); }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function save(repository = root, mode = "release", source) {
|
|
236
|
+
// Remove pre-release cache locations so older local builds cannot publish raw WASM.
|
|
237
|
+
for (const name of [".nanocodex-source.wasm", ".nanocodex-output-cache.json"]) {
|
|
238
|
+
await rm(resolve(repository, "js/nanocodex/pkg-web", name), { force: true });
|
|
239
|
+
}
|
|
240
|
+
await mkdir(resolve(repository, ".ci-wasm-cache"), { recursive: true });
|
|
241
|
+
await writeFile(resolve(repository, ".ci-wasm-cache/.gitignore"), "*\n");
|
|
242
|
+
await atomicWrite(resolve(repository, rawPath), await readFile(source));
|
|
243
|
+
await atomicWrite(resolve(repository, metadataPath), `${JSON.stringify({ schema: 1, fingerprint: await fingerprint(repository, mode), outputs: await outputs(repository) })}\n`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
247
|
+
const [command, mode = "release", source] = process.argv.slice(2);
|
|
248
|
+
if (command === "key") console.log(await fingerprint(root, mode));
|
|
249
|
+
else if (command === "check") {
|
|
250
|
+
try { await check(root, mode); console.log("WASM output cache verified"); }
|
|
251
|
+
catch (error) { console.error(`WASM output cache miss: ${error.message}`); process.exitCode = 1; }
|
|
252
|
+
} else if (command === "save" && source) await save(root, mode, resolve(source));
|
|
253
|
+
else throw new Error("usage: wasm-output-cache.mjs key|check [release|development], or save <mode> <raw-wasm>");
|
|
254
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { check, fingerprint, fingerprintInputs, save } from "./wasm-output-cache.mjs";
|
|
8
|
+
|
|
9
|
+
const repository = new URL("../../../", import.meta.url);
|
|
10
|
+
test("content key, attestation integrity, and pre-Cargo reuse", async () => {
|
|
11
|
+
const root = await mkdtemp(resolve(tmpdir(), "nanocodex-wasm-cache-"));
|
|
12
|
+
const put = async (path, value) => {
|
|
13
|
+
await mkdir(resolve(root, path, ".."), { recursive: true });
|
|
14
|
+
await writeFile(resolve(root, path), value);
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const scripts = ["js/nanocodex-vite/scripts/build-js-package.sh", "js/nanocodex-vite/scripts/wasm-output-cache.mjs", "js/nanocodex-vite/scripts/wasm-memory-views.mjs", "js/nanocodex/scripts/deduplicate-wasm.mjs", "js/nanocodex/scripts/write-package-types.mjs", "js/nanocodex/scripts/write-wasm-attestation.mjs", "js/nanocodex/scripts/check-managed-wasm.mjs"];
|
|
18
|
+
for (const path of scripts) await put(path, await readFile(new URL(path, repository)));
|
|
19
|
+
await put("Cargo.toml", '[workspace.dependencies]\ncore = { path = "crates/core" }\n');
|
|
20
|
+
await put("Cargo.lock", "locked");
|
|
21
|
+
await put("js/nanocodex/Cargo.toml", '[dependencies]\ncore.workspace = true\n');
|
|
22
|
+
await put("js/nanocodex/src/lib.rs", "wasm source");
|
|
23
|
+
await put("crates/core/Cargo.toml", '[package]\nname = "core"\n');
|
|
24
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
25
|
+
await put("js/nanocodex/package.json", '{"devDependencies":{"binaryen":"132.0.0"}}');
|
|
26
|
+
await put(".cargo/config.toml", "# config");
|
|
27
|
+
const key = await fingerprint(root);
|
|
28
|
+
assert.equal(await fingerprintInputs(root), key);
|
|
29
|
+
assert.equal(await fingerprint(root, "release", { ...process.env, RUSTUP_TOOLCHAIN: "1.97", CARGO_PROFILE_DEV_DEBUG: "0", CARGO_PROFILE_TEST_DEBUG: "0", CARGO_INCREMENTAL: "0" }), key);
|
|
30
|
+
const manifestPath = "js/nanocodex/Cargo.toml";
|
|
31
|
+
const oldManifest = await readFile(resolve(root, manifestPath));
|
|
32
|
+
await put(manifestPath, '[dependencies.core]\nworkspace = true\n');
|
|
33
|
+
const dottedKey = await fingerprint(root);
|
|
34
|
+
await put("crates/core/src/lib.rs", "changed dotted dependency");
|
|
35
|
+
assert.notEqual(await fingerprint(root), dottedKey);
|
|
36
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
37
|
+
await put(manifestPath, oldManifest);
|
|
38
|
+
await put("shared/prompt.md", "embedded prompt");
|
|
39
|
+
await put("js/nanocodex/src/lib.rs", 'const PROMPT: &str = include_str!("../../../shared/prompt.md");');
|
|
40
|
+
const includeKey = await fingerprint(root);
|
|
41
|
+
await put("shared/prompt.md", "changed embedded prompt");
|
|
42
|
+
assert.notEqual(await fingerprint(root), includeKey);
|
|
43
|
+
await put("js/nanocodex/src/lib.rs", "wasm source");
|
|
44
|
+
await put(manifestPath, '[dependencies."core"]\nworkspace = true\n');
|
|
45
|
+
assert.equal(await fingerprint(root), await fingerprint(root), "quoted dependency table is supported");
|
|
46
|
+
await put(manifestPath, '[dependencies]\n core = { workspace = true }\n');
|
|
47
|
+
const indentedKey = await fingerprint(root);
|
|
48
|
+
await put("crates/core/src/lib.rs", "changed indented dependency");
|
|
49
|
+
assert.notEqual(await fingerprint(root), indentedKey);
|
|
50
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
51
|
+
await put(manifestPath, '[invalid TOML');
|
|
52
|
+
await assert.rejects(fingerprintInputs(root), "release identity must surface resolution failures");
|
|
53
|
+
assert.notEqual(await fingerprint(root), await fingerprint(root), "invalid manifest disables reuse");
|
|
54
|
+
await put(manifestPath, oldManifest);
|
|
55
|
+
await put("js/nanocodex/cloudflare/worker.mjs", "unrelated worker change");
|
|
56
|
+
assert.equal(await fingerprint(root), key);
|
|
57
|
+
for (const path of ["crates/core/src/lib.rs", "Cargo.lock", ".cargo/config.toml", "js/nanocodex-vite/scripts/wasm-memory-views.mjs"]) {
|
|
58
|
+
const before = await readFile(resolve(root, path));
|
|
59
|
+
await put(path, `${before}\nchanged`);
|
|
60
|
+
assert.notEqual(await fingerprint(root), key, path);
|
|
61
|
+
await put(path, before);
|
|
62
|
+
}
|
|
63
|
+
assert.notEqual(await fingerprint(root, "development"), key);
|
|
64
|
+
assert.notEqual(await fingerprint(root, "release", { RUSTFLAGS: "-C opt-level=1" }), key);
|
|
65
|
+
for (const dir of ["pkg-web", "pkg-node"]) {
|
|
66
|
+
for (const name of ["nanocodex.js", "nanocodex.d.ts", "package.json", ...(dir === "pkg-web" ? ["nanocodex_bg.js", "nanocodex_bg.wasm", "nanocodex_worker.js"] : [])]) await put(`js/nanocodex/${dir}/${name}`, name);
|
|
67
|
+
}
|
|
68
|
+
await put("raw.wasm", "raw WASM bytes");
|
|
69
|
+
const git = (...args) => execFileSync("git", args, { cwd: root, stdio: "pipe" });
|
|
70
|
+
git("init"); git("add", "."); git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "fixture");
|
|
71
|
+
execFileSync(process.execPath, [resolve(root, "js/nanocodex/scripts/write-wasm-attestation.mjs"), resolve(root, "raw.wasm")]);
|
|
72
|
+
await save(root, "release", resolve(root, "raw.wasm"));
|
|
73
|
+
await check(root);
|
|
74
|
+
// The lock is already held by the fixture; Cargo and wasm-bindgen must not run.
|
|
75
|
+
const result = execFileSync("bash", [resolve(root, scripts[0]), "--release"], {
|
|
76
|
+
cwd: root, encoding: "utf8", env: { ...process.env, NANOCODEX_WASM_LOCK_HELD: root },
|
|
77
|
+
});
|
|
78
|
+
assert.match(result, /skipped Cargo and binding generation/);
|
|
79
|
+
for (const path of ["js/nanocodex/pkg-node/nanocodex.js", "js/nanocodex/pkg-web/nanocodex_bg.wasm", ".ci-wasm-cache/source.wasm"]) {
|
|
80
|
+
const before = await readFile(resolve(root, path));
|
|
81
|
+
await put(path, "corrupted");
|
|
82
|
+
await assert.rejects(check(root));
|
|
83
|
+
await put(path, before);
|
|
84
|
+
await rm(resolve(root, path));
|
|
85
|
+
await assert.rejects(check(root));
|
|
86
|
+
await put(path, before);
|
|
87
|
+
}
|
|
88
|
+
await assert.rejects(check(root, "development"));
|
|
89
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
test("WASM closure skips only proven target mismatches and preserves host dependencies", async () => {
|
|
94
|
+
const fixture = await inputFixture();
|
|
95
|
+
const { root, put } = fixture;
|
|
96
|
+
try {
|
|
97
|
+
const cases = [
|
|
98
|
+
['cfg(not(target_family = "wasm"))', false],
|
|
99
|
+
['cfg(unix)', false],
|
|
100
|
+
['cfg (unix)', false],
|
|
101
|
+
['cfg (not(unknown_flag))', true],
|
|
102
|
+
['cfg(windows)', false],
|
|
103
|
+
['x86_64-unknown-linux-gnu', false],
|
|
104
|
+
['wasm32-unknown-unknown', true],
|
|
105
|
+
['cfg(all(target_arch = "wasm32", target_os = "unknown"))', true],
|
|
106
|
+
['cfg(any(unix, target_family = "wasm"))', true],
|
|
107
|
+
['cfg(not(any(unix, windows)))', true],
|
|
108
|
+
['cfg(all(unknown_flag, unix))', false],
|
|
109
|
+
['cfg(any(unknown_flag, unix))', true],
|
|
110
|
+
['cfg(not(unknown_flag))', true],
|
|
111
|
+
['cfg(target_feature = "atomics")', true],
|
|
112
|
+
['cfg(feature = "optional-feature")', true],
|
|
113
|
+
['cfg(future_predicate(target_arch = "wasm32"))', true],
|
|
114
|
+
];
|
|
115
|
+
for (const [platform, included] of cases) {
|
|
116
|
+
await put("js/nanocodex/Cargo.toml", `[target.'${platform}'.dependencies]\ncore = { path = "../../crates/core", optional = true }\n`);
|
|
117
|
+
const before = await fingerprintInputs(root);
|
|
118
|
+
await put("crates/core/src/lib.rs", `changed for ${platform}`);
|
|
119
|
+
assert.equal((await fingerprintInputs(root)) !== before, included, platform);
|
|
120
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
121
|
+
}
|
|
122
|
+
await put("js/nanocodex/Cargo.toml", '[dependencies]\ncore = { path = "../../crates/core", optional = true }\n');
|
|
123
|
+
const optional = await fingerprintInputs(root);
|
|
124
|
+
await put("crates/core/src/lib.rs", "optional dependency still included");
|
|
125
|
+
assert.notEqual(await fingerprintInputs(root), optional, "do not infer optional feature activation");
|
|
126
|
+
|
|
127
|
+
for (const kind of ["build-dependency", "proc-macro"]) {
|
|
128
|
+
await put("js/nanocodex/Cargo.toml", kind === "build-dependency"
|
|
129
|
+
? `[target.'cfg(unix)'.build-dependencies]\ncore = { path = "../../crates/core" }\n`
|
|
130
|
+
: '[dependencies]\ncore = { path = "../../crates/core" }\n');
|
|
131
|
+
await put("crates/core/Cargo.toml", `[package]\nname = "core"\n${kind === "proc-macro" ? "[lib]\nproc-macro = true\n" : ""}[target.'cfg(unix)'.dependencies]\nhost = { path = "../host" }\n`);
|
|
132
|
+
await put("crates/host/Cargo.toml", '[package]\nname = "host"\n');
|
|
133
|
+
await put("crates/host/src/lib.rs", "host dependency source");
|
|
134
|
+
const before = await fingerprintInputs(root);
|
|
135
|
+
await put("crates/host/src/lib.rs", "host dependency changed");
|
|
136
|
+
assert.notEqual(await fingerprintInputs(root), before, kind);
|
|
137
|
+
}
|
|
138
|
+
} finally { await fixture.close(); }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("standalone test directories are omitted unless builds or production references need them", async () => {
|
|
142
|
+
const fixture = await inputFixture();
|
|
143
|
+
const { root, put } = fixture;
|
|
144
|
+
try {
|
|
145
|
+
const beforeTests = await fingerprintInputs(root);
|
|
146
|
+
await put("crates/core/tests/standalone.rs", "integration test");
|
|
147
|
+
await put("crates/core/benches/standalone.rs", "benchmark");
|
|
148
|
+
assert.equal(await fingerprintInputs(root), beforeTests);
|
|
149
|
+
await put("crates/core/tests/standalone.rs", "changed integration test");
|
|
150
|
+
assert.equal(await fingerprintInputs(root), beforeTests);
|
|
151
|
+
|
|
152
|
+
await put("crates/core/tests/prompt.txt", "embedded asset");
|
|
153
|
+
await put("crates/core/src/lib.rs", 'const PROMPT: &str = include_str!("../tests/prompt.txt");');
|
|
154
|
+
const beforeAsset = await fingerprintInputs(root);
|
|
155
|
+
await put("crates/core/tests/prompt.txt", "changed embedded asset");
|
|
156
|
+
assert.notEqual(await fingerprintInputs(root), beforeAsset, "literal includes retain excluded-directory assets");
|
|
157
|
+
|
|
158
|
+
await put("crates/core/src/lib.rs", '#[path = "../tests/production.rs"] mod production;');
|
|
159
|
+
await put("crates/core/tests/production.rs", "mod sibling;");
|
|
160
|
+
await put("crates/core/tests/sibling.rs", "production child module");
|
|
161
|
+
const beforeModule = await fingerprintInputs(root);
|
|
162
|
+
await put("crates/core/tests/sibling.rs", "changed production child module");
|
|
163
|
+
assert.notEqual(await fingerprintInputs(root), beforeModule, "referenced Rust modules retain their ordinary mod children");
|
|
164
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
165
|
+
|
|
166
|
+
await put("crates/core/src/tests/production.rs", "nested source remains conservative");
|
|
167
|
+
const beforeNested = await fingerprintInputs(root);
|
|
168
|
+
await put("crates/core/src/tests/production.rs", "changed nested source");
|
|
169
|
+
assert.notEqual(await fingerprintInputs(root), beforeNested);
|
|
170
|
+
|
|
171
|
+
for (const build of ["build.rs", "custom-build.rs"]) {
|
|
172
|
+
await put("crates/core/Cargo.toml", `[package]\nname = "core"\n${build === "build.rs" ? "" : 'build = "custom-build.rs"\n'}`);
|
|
173
|
+
await put(`crates/core/${build}`, "fn main() {}");
|
|
174
|
+
const before = await fingerprintInputs(root);
|
|
175
|
+
await put("crates/core/tests/standalone.rs", `build input for ${build}`);
|
|
176
|
+
assert.notEqual(await fingerprintInputs(root), before, "build scripts may read test assets");
|
|
177
|
+
await rm(resolve(root, `crates/core/${build}`));
|
|
178
|
+
}
|
|
179
|
+
await put("crates/core/Cargo.toml", '[package]\nname = "core"\n[lib]\npath = "tests/library.rs"\n');
|
|
180
|
+
await put("crates/core/tests/library.rs", "production library");
|
|
181
|
+
const explicitTarget = await fingerprintInputs(root);
|
|
182
|
+
await put("crates/core/tests/library.rs", "changed production library");
|
|
183
|
+
assert.notEqual(await fingerprintInputs(root), explicitTarget, "explicit production entry points remain covered");
|
|
184
|
+
} finally { await fixture.close(); }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
async function inputFixture() {
|
|
188
|
+
const root = await mkdtemp(resolve(tmpdir(), "nanocodex-wasm-inputs-"));
|
|
189
|
+
const put = async (path, value) => {
|
|
190
|
+
await mkdir(resolve(root, path, ".."), { recursive: true });
|
|
191
|
+
await writeFile(resolve(root, path), value);
|
|
192
|
+
};
|
|
193
|
+
for (const path of ["js/nanocodex-vite/scripts/build-js-package.sh", "js/nanocodex-vite/scripts/wasm-output-cache.mjs", "js/nanocodex-vite/scripts/wasm-memory-views.mjs", "js/nanocodex/scripts/deduplicate-wasm.mjs", "js/nanocodex/scripts/write-package-types.mjs", "js/nanocodex/scripts/write-wasm-attestation.mjs", "js/nanocodex/scripts/check-managed-wasm.mjs"]) await put(path, "// build policy");
|
|
194
|
+
await put("Cargo.toml", '[workspace]\n');
|
|
195
|
+
await put("Cargo.lock", "locked");
|
|
196
|
+
await put("js/nanocodex/Cargo.toml", '[dependencies]\ncore = { path = "../../crates/core" }\n');
|
|
197
|
+
await put("js/nanocodex/package.json", '{"devDependencies":{"binaryen":"132.0.0"}}');
|
|
198
|
+
await put("js/nanocodex/src/lib.rs", "wasm source");
|
|
199
|
+
await put("crates/core/Cargo.toml", '[package]\nname = "core"\n');
|
|
200
|
+
await put("crates/core/src/lib.rs", "core source");
|
|
201
|
+
return { root, put, close: () => rm(root, { recursive: true, force: true }) };
|
|
202
|
+
}
|