nanocodex-vite 0.6.1 → 0.6.5
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/chatgpt-egress.mjs +20 -5
- package/oauth-relay.d.mts +1 -1
- package/oauth-relay.mjs +2 -2
- package/package.json +3 -3
- package/plugin.mjs +11 -0
- package/scripts/build-js-package.sh +25 -3
- package/scripts/wasm-output-cache.mjs +254 -0
- package/scripts/wasm-output-cache.test.mjs +202 -0
package/chatgpt-egress.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import WebSocket, { WebSocketServer } from "ws";
|
|
|
7
7
|
const CHATGPT_HOST = "chatgpt.com";
|
|
8
8
|
const CHATGPT_PATH_PREFIX = "/backend-api/codex/";
|
|
9
9
|
const RELAY_HTTP_PATHS = new Map([
|
|
10
|
+
["codex-responses", "/backend-api/codex/responses"],
|
|
10
11
|
["codex-web-search", "/backend-api/codex/alpha/search"],
|
|
11
12
|
["codex-image-generation", "/backend-api/codex/images/generations"],
|
|
12
13
|
["codex-image-edit", "/backend-api/codex/images/edits"],
|
|
@@ -84,13 +85,18 @@ export async function startChatGptWorkerEgress(options = {}) {
|
|
|
84
85
|
|
|
85
86
|
async function proxyHttpRequest(request, response, fetchImpl, localPrefix, relayPrefix) {
|
|
86
87
|
const upstreamPath = allowedUpstreamPath(request.url, localPrefix, relayPrefix, false);
|
|
87
|
-
if (!safeLoopbackRequest(request) || upstreamPath === undefined
|
|
88
|
+
if (!safeLoopbackRequest(request) || upstreamPath === undefined
|
|
89
|
+
|| (request.url?.startsWith(`${relayPrefix}/http/codex-responses`)
|
|
90
|
+
&& (request.method !== "POST" || request.url !== `${relayPrefix}/http/codex-responses`))) {
|
|
88
91
|
response.writeHead(404, noStoreHeaders("text/plain; charset=utf-8"));
|
|
89
92
|
response.end("Not found\n");
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
95
|
try {
|
|
93
96
|
const method = request.method ?? "GET";
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
request.once("aborted", () => controller.abort());
|
|
99
|
+
response.once("close", () => { if (!response.writableFinished) controller.abort(); });
|
|
94
100
|
const upstream = await fetchImpl(`https://${CHATGPT_HOST}${upstreamPath}`, {
|
|
95
101
|
method,
|
|
96
102
|
headers: upstreamHttpHeaders(request),
|
|
@@ -99,17 +105,22 @@ async function proxyHttpRequest(request, response, fetchImpl, localPrefix, relay
|
|
|
99
105
|
: Readable.toWeb(request),
|
|
100
106
|
duplex: "half",
|
|
101
107
|
redirect: "manual",
|
|
108
|
+
signal: controller.signal,
|
|
102
109
|
});
|
|
103
110
|
const headers = new Headers(upstream.headers);
|
|
104
111
|
headers.delete("content-encoding");
|
|
105
112
|
headers.delete("content-length");
|
|
106
113
|
headers.set("cache-control", "no-store");
|
|
107
114
|
response.writeHead(upstream.status, upstream.statusText, Object.fromEntries(headers));
|
|
108
|
-
if (upstream.body) Readable.fromWeb(upstream.body).pipe(response);
|
|
115
|
+
if (upstream.body) Readable.fromWeb(upstream.body).once("error", (error) => response.destroy(error)).pipe(response);
|
|
109
116
|
else response.end();
|
|
110
|
-
} catch {
|
|
111
|
-
if (
|
|
112
|
-
response.
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (response.destroyed) return;
|
|
119
|
+
if (response.headersSent) response.destroy(error);
|
|
120
|
+
else {
|
|
121
|
+
response.writeHead(502, noStoreHeaders("text/plain; charset=utf-8"));
|
|
122
|
+
response.end("ChatGPT development egress failed\n");
|
|
123
|
+
}
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
|
|
@@ -214,6 +225,10 @@ function upstreamHttpHeaders(request) {
|
|
|
214
225
|
"x-oai-attestation",
|
|
215
226
|
"x-openai-fedramp",
|
|
216
227
|
"x-session-id",
|
|
228
|
+
"x-client-request-id",
|
|
229
|
+
"x-codex-turn-state",
|
|
230
|
+
"x-openai-internal-codex-responses-lite",
|
|
231
|
+
"x-responsesapi-include-timing-metrics",
|
|
217
232
|
]) {
|
|
218
233
|
const value = request.headers[name];
|
|
219
234
|
if (typeof value === "string") headers.set(name, value);
|
package/oauth-relay.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LocalOAuthProvider = "github" | "google" | "gmail" | "gdrive" | "slack" | "x";
|
|
1
|
+
export type LocalOAuthProvider = "github" | "google" | "gmail" | "gdrive" | "slack" | "x" | "spotify" | "soundcloud";
|
|
2
2
|
export type LocalOAuthFlow = "connect" | "managed";
|
|
3
3
|
export type LocalConnectorFlow = LocalOAuthFlow;
|
|
4
4
|
export type LocalConnectorAuthorization = Readonly<{
|
package/oauth-relay.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const PROVIDERS = new Set(["github", "google", "gmail", "gdrive", "slack", "x"]);
|
|
1
|
+
const PROVIDERS = new Set(["github", "google", "gmail", "gdrive", "slack", "x", "spotify", "soundcloud"]);
|
|
2
2
|
const FLOWS = new Set(["connect", "managed"]);
|
|
3
3
|
const BASE64_URL = /^[A-Za-z0-9_-]+$/;
|
|
4
4
|
const INSTANCE_HOST = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.nanocodex\.localhost$/;
|
|
@@ -9,7 +9,7 @@ const MAX_CLOCK_SKEW_SECONDS = 30;
|
|
|
9
9
|
const MAX_INNER_STATE_LENGTH = 512;
|
|
10
10
|
const MAX_ENVELOPE_LENGTH = 2_048;
|
|
11
11
|
const CONNECTION_ID = /^[A-Za-z0-9_-]{43}$/;
|
|
12
|
-
const CONNECTOR_CALLBACK = /^\/v1\/connectors\/(github|google|gmail|gdrive|slack|x)\/callback$/;
|
|
12
|
+
const CONNECTOR_CALLBACK = /^\/v1\/connectors\/(github|google|gmail|gdrive|slack|x|spotify|soundcloud)\/callback$/;
|
|
13
13
|
const MCP_CALLBACK = /^\/v1\/mcp-connections\/([A-Za-z0-9_-]{43})\/callback$/;
|
|
14
14
|
const SAFE_QUERY = ["code", "error", "error_description"];
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanocodex-vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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.5",
|
|
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.5",
|
|
43
43
|
"vite": ">=6.1.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
package/plugin.mjs
CHANGED
|
@@ -339,6 +339,9 @@ async function localOAuthBindings() {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
export function oauthBindingsFromEnvironment(environment) {
|
|
342
|
+
const spotifyClientId = firstEnvironmentValue(environment, [
|
|
343
|
+
"NANOCODEX_SPOTIFY_OAUTH_CLIENT_ID", "SPOTIFY_OAUTH_CLIENT_ID", "SPOTIFY_CLIENT_ID",
|
|
344
|
+
]);
|
|
342
345
|
return {
|
|
343
346
|
...oauthCredentialPair(environment, {
|
|
344
347
|
label: "GitHub",
|
|
@@ -361,6 +364,14 @@ export function oauthBindingsFromEnvironment(environment) {
|
|
|
361
364
|
targetId: "SLACK_OAUTH_CLIENT_ID",
|
|
362
365
|
targetSecret: "SLACK_OAUTH_CLIENT_SECRET",
|
|
363
366
|
}),
|
|
367
|
+
...(spotifyClientId ? { SPOTIFY_OAUTH_CLIENT_ID: spotifyClientId } : {}),
|
|
368
|
+
...oauthCredentialPair(environment, {
|
|
369
|
+
label: "SoundCloud",
|
|
370
|
+
ids: ["NANOCODEX_SOUNDCLOUD_OAUTH_CLIENT_ID", "SOUNDCLOUD_OAUTH_CLIENT_ID", "SOUNDCLOUD_CLIENT_ID"],
|
|
371
|
+
secrets: ["NANOCODEX_SOUNDCLOUD_OAUTH_CLIENT_SECRET", "SOUNDCLOUD_OAUTH_CLIENT_SECRET", "SOUNDCLOUD_CLIENT_SECRET"],
|
|
372
|
+
targetId: "SOUNDCLOUD_OAUTH_CLIENT_ID",
|
|
373
|
+
targetSecret: "SOUNDCLOUD_OAUTH_CLIENT_SECRET",
|
|
374
|
+
}),
|
|
364
375
|
...oauthCredentialPair(environment, {
|
|
365
376
|
label: "X",
|
|
366
377
|
ids: ["NANOCODEX_X_OAUTH_CLIENT_ID", "X_OAUTH_CLIENT_ID", "X_CLIENT_ID"],
|
|
@@ -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
|
+
}
|