nanocodex-vite 0.6.0

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/plugin.mjs ADDED
@@ -0,0 +1,525 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { access, readFile, realpath, stat } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseEnv } from "node:util";
8
+
9
+ import { nanocodexTools } from "./tools.mjs";
10
+ import { chatGptSubscription } from "./chatgpt-subscription.mjs";
11
+ import { defaultCodexAuthFile, readCodexSubscription } from "./codex-auth-file.mjs";
12
+ import { startChatGptWorkerEgress } from "./chatgpt-egress.mjs";
13
+ import { startLocalOAuthRelay } from "./oauth-relay-server.mjs";
14
+
15
+ const LOCAL_SPONSORED_CHATGPT_USER_ID = "00000000-0000-4000-8000-000000000001";
16
+
17
+ const buildScript = fileURLToPath(new URL("./scripts/build-js-package.sh", import.meta.url));
18
+ const repositoryRoot = fileURLToPath(new URL("../../", import.meta.url));
19
+ const rustBuildFiles = new Set([
20
+ buildScript,
21
+ fileURLToPath(new URL("./scripts/wasm-memory-views.mjs", import.meta.url)),
22
+ resolve(repositoryRoot, "Cargo.lock"),
23
+ resolve(repositoryRoot, "Cargo.toml"),
24
+ resolve(repositoryRoot, ".cargo/config.toml"),
25
+ resolve(repositoryRoot, "js/nanocodex/Cargo.toml"),
26
+ ]);
27
+ const rustBuildDirectories = [
28
+ resolve(repositoryRoot, "crates"),
29
+ resolve(repositoryRoot, "js/nanocodex/src"),
30
+ ];
31
+ const rustBuildWatchRoots = [...rustBuildFiles, ...rustBuildDirectories];
32
+ const packageManifest = fileURLToPath(new URL("./package.json", import.meta.url));
33
+ const sourcePackageManifest = fileURLToPath(
34
+ new URL("../../js/nanocodex-vite/package.json", import.meta.url),
35
+ );
36
+ const browserPackage = new URL(import.meta.resolve("nanocodex/browser"));
37
+ const generatedPackage = [
38
+ new URL("../pkg-web/nanocodex.js", browserPackage),
39
+ new URL(import.meta.resolve("nanocodex/wasm")),
40
+ ];
41
+ const packageBuilds = new Map();
42
+
43
+ export function createNanocodexVitePlugin(options, integration) {
44
+ const tools = nanocodexTools();
45
+ const devApplications = normalizeDevApplications(options.devApplications);
46
+ const chatGpt = options.chatGpt ?? {};
47
+ const credentialBrokerWorker = integration.target === "cloudflare"
48
+ && typeof chatGpt.credentialBrokerWorker === "string"
49
+ ? chatGpt.credentialBrokerWorker
50
+ : undefined;
51
+ const direct = integration.target === "vite" && chatGpt !== false
52
+ ? chatGptSubscription(chatGpt)
53
+ : undefined;
54
+ const buildJsPackage = integration.buildJsPackage ?? ensureJsPackage;
55
+ const loadOAuthBindings = integration.loadOAuthBindings ?? localOAuthBindings;
56
+ const startOAuthRelay = integration.startOAuthRelay ?? startLocalOAuthRelay;
57
+ const rebuildJsPackage = integration.rebuildJsPackage ?? runJsPackageBuild;
58
+ let buildPromise;
59
+ let workerAuth;
60
+ let egress;
61
+ let oauthRelay;
62
+ let devApplicationServers = [];
63
+ let devApplicationCleanup;
64
+ let cleanupPromise;
65
+
66
+ const cleanup = () => cleanupPromise ??= (async () => {
67
+ try {
68
+ await egress?.close();
69
+ } finally {
70
+ egress = undefined;
71
+ workerAuth = undefined;
72
+ integration.setDevBindings?.(undefined);
73
+ }
74
+ })();
75
+
76
+ const cleanupOAuthRelay = async () => {
77
+ const active = oauthRelay;
78
+ oauthRelay = undefined;
79
+ await active?.close();
80
+ };
81
+
82
+ const cleanupDevApplications = () => devApplicationCleanup ??= (async () => {
83
+ const active = devApplicationServers;
84
+ devApplicationServers = [];
85
+ await Promise.all(active.map(({ server }) => server.close()));
86
+ })();
87
+
88
+ return {
89
+ name: "nanocodex",
90
+ enforce: "pre",
91
+ resolveId: tools.resolveId,
92
+ async config(config, environment) {
93
+ await (buildPromise ??= buildJsPackage(environment.command === "build"));
94
+ if (options.oauthRelay === true && environment.command === "serve") {
95
+ oauthRelay ??= await startOAuthRelay();
96
+ } else {
97
+ await cleanupOAuthRelay();
98
+ }
99
+ const nestedWorker = workerPlugins(config.worker?.plugins);
100
+ if (
101
+ integration.target !== "cloudflare"
102
+ || environment.command !== "serve"
103
+ ) {
104
+ if (integration.target === "cloudflare") await cleanup();
105
+ integration.setDevBindings?.(undefined);
106
+ return nanocodexConfig(nestedWorker, false);
107
+ }
108
+
109
+ await cleanup();
110
+ cleanupPromise = undefined;
111
+ let oauthBindings = {};
112
+ if (options.oauthRelay === true && credentialBrokerWorker) {
113
+ try {
114
+ oauthBindings = await loadOAuthBindings();
115
+ } catch (error) {
116
+ await cleanupOAuthRelay();
117
+ throw new Error(`Nanocodex local OAuth setup failed: ${errorMessage(error)}.`);
118
+ }
119
+ }
120
+ if (chatGpt === false) {
121
+ integration.setDevBindings(Object.freeze(oauthBindings));
122
+ return nanocodexConfig(nestedWorker, false);
123
+ }
124
+ try {
125
+ const configuredAuthFile = chatGpt.authFile === undefined
126
+ ? defaultCodexAuthFile()
127
+ : chatGpt.authFile;
128
+ const authFile = configuredAuthFile instanceof URL
129
+ ? fileURLToPath(configuredAuthFile)
130
+ : configuredAuthFile;
131
+ workerAuth = await readCodexSubscription(authFile);
132
+ egress = await startChatGptWorkerEgress();
133
+ if (credentialBrokerWorker) {
134
+ integration.setDevBindings(Object.freeze({
135
+ ...oauthBindings,
136
+ ALLOW_INSECURE_LOOPBACK_RELAY: "true",
137
+ CODEX_RELAY_URL: egress.relayUrl,
138
+ NANOCODEX_LOCAL_SPONSORED_TRIAL_RESET: "true",
139
+ NANOCODEX_SPONSORED_CHATGPT_USER_ID: LOCAL_SPONSORED_CHATGPT_USER_ID,
140
+ LOCAL_CHATGPT_BOOTSTRAP: JSON.stringify({
141
+ access_token: workerAuth.accessToken,
142
+ account_id: workerAuth.accountId,
143
+ expires_at: workerAuth.expiresAt,
144
+ fedramp: workerAuth.fedramp,
145
+ }),
146
+ }));
147
+ } else {
148
+ integration.setDevBindings(Object.freeze({
149
+ ...oauthBindings,
150
+ ENVIRONMENT: "development",
151
+ NANOCODEX_DEV_CHATGPT_ACCESS_TOKEN: workerAuth.accessToken,
152
+ NANOCODEX_DEV_CHATGPT_ACCOUNT_ID: workerAuth.accountId,
153
+ NANOCODEX_DEV_CHATGPT_FEDRAMP: String(workerAuth.fedramp),
154
+ NANOCODEX_DEV_CHATGPT_EXPIRES_AT: String(workerAuth.expiresAt),
155
+ NANOCODEX_DEV_CHATGPT_EGRESS_URL: egress.url,
156
+ NANOCODEX_DEV_CHATGPT_SESSION_ID: randomBytes(32).toString("base64url"),
157
+ }));
158
+ }
159
+ } catch (error) {
160
+ await Promise.all([cleanup(), cleanupOAuthRelay()]);
161
+ throw new Error(
162
+ `Nanocodex local ChatGPT setup failed: ${errorMessage(error)}. Run \`codex login\` and retry.`,
163
+ );
164
+ }
165
+ return nanocodexConfig(nestedWorker, Boolean(credentialBrokerWorker));
166
+ },
167
+ async configureServer(vite) {
168
+ vite.httpServer?.once("close", () => {
169
+ void Promise.all([cleanup(), cleanupOAuthRelay(), cleanupDevApplications()]);
170
+ });
171
+ if (devApplications.length > 0) {
172
+ const createViteServer = integration.createViteServer
173
+ ?? (await import("vite")).createServer;
174
+ try {
175
+ devApplicationServers = await createDevApplicationServers(
176
+ vite,
177
+ devApplications,
178
+ createViteServer,
179
+ );
180
+ } catch (error) {
181
+ await cleanupDevApplications();
182
+ throw error;
183
+ }
184
+ vite.middlewares.use(devApplicationMiddleware(devApplicationServers));
185
+ }
186
+ if (await isSourceCheckout()) {
187
+ watchRustBuildInputs(vite, rebuildJsPackage);
188
+ }
189
+ if (integration.target === "vite") {
190
+ await direct?.configureServer(vite);
191
+ return;
192
+ }
193
+ if (!workerAuth) return;
194
+ vite.config.logger.info(
195
+ `[nanocodex] local ChatGPT subscription ready through ${credentialBrokerWorker ?? "the application Worker"} (expires ${new Date(workerAuth.expiresAt).toISOString()})`,
196
+ );
197
+ },
198
+ async closeBundle() {
199
+ await Promise.all([cleanup(), cleanupOAuthRelay(), cleanupDevApplications()]);
200
+ },
201
+ };
202
+ }
203
+
204
+ function nanocodexConfig(nestedWorker, localSponsoredTrialReset) {
205
+ return {
206
+ define: {
207
+ __NANOCODEX_LOCAL_SPONSORED_TRIAL_RESET__: JSON.stringify(localSponsoredTrialReset),
208
+ },
209
+ worker: { plugins: nestedWorker },
210
+ };
211
+ }
212
+
213
+ function normalizeDevApplications(configured) {
214
+ if (configured === undefined) return [];
215
+ if (!Array.isArray(configured)) throw new TypeError("Nanocodex devApplications must be an array");
216
+ const paths = new Set();
217
+ const applications = configured.map((application, index) => {
218
+ if (application === null || typeof application !== "object") {
219
+ throw new TypeError(`Nanocodex devApplications[${index}] must be an object`);
220
+ }
221
+ const path = normalizeDevApplicationPath(application.path, index);
222
+ if (paths.has(path)) throw new TypeError(`Nanocodex devApplications contains duplicate path ${path}`);
223
+ paths.add(path);
224
+ const root = application.root;
225
+ if (typeof root !== "string" && !(root instanceof URL)) {
226
+ throw new TypeError(`Nanocodex devApplications[${index}].root must be a path or file URL`);
227
+ }
228
+ if (typeof root === "string" && root.length === 0) {
229
+ throw new TypeError(`Nanocodex devApplications[${index}].root must not be empty`);
230
+ }
231
+ if (root instanceof URL && root.protocol !== "file:") {
232
+ throw new TypeError(`Nanocodex devApplications[${index}].root must be a file URL`);
233
+ }
234
+ const headers = normalizeDevApplicationHeaders(application.headers, index);
235
+ return Object.freeze({ path, root, headers });
236
+ });
237
+ return Object.freeze(applications.sort((left, right) => right.path.length - left.path.length));
238
+ }
239
+
240
+ function normalizeDevApplicationHeaders(configured, index) {
241
+ if (configured === undefined) return Object.freeze({});
242
+ if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
243
+ throw new TypeError(`Nanocodex devApplications[${index}].headers must be an object`);
244
+ }
245
+ try {
246
+ return Object.freeze(Object.fromEntries(new Headers(configured)));
247
+ } catch (error) {
248
+ throw new TypeError(
249
+ `Nanocodex devApplications[${index}].headers contains an invalid header: ${errorMessage(error)}`,
250
+ );
251
+ }
252
+ }
253
+
254
+ function normalizeDevApplicationPath(configured, index) {
255
+ if (typeof configured !== "string") {
256
+ throw new TypeError(`Nanocodex devApplications[${index}].path must be a URL path`);
257
+ }
258
+ const path = configured.endsWith("/") ? configured.slice(0, -1) : configured;
259
+ if (path === "" || path === "/" || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
260
+ throw new TypeError(`Nanocodex devApplications[${index}].path must be a non-root URL path`);
261
+ }
262
+ for (const encoded of path.slice(1).split("/")) {
263
+ let segment;
264
+ try {
265
+ segment = decodeURIComponent(encoded);
266
+ } catch {
267
+ throw new TypeError(`Nanocodex devApplications[${index}].path must have valid URL encoding`);
268
+ }
269
+ if (segment === "" || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\")) {
270
+ throw new TypeError(`Nanocodex devApplications[${index}].path must have safe URL segments`);
271
+ }
272
+ }
273
+ return path;
274
+ }
275
+
276
+ async function createDevApplicationServers(vite, applications, createViteServer) {
277
+ const mounted = [];
278
+ try {
279
+ for (const application of applications) {
280
+ const root = application.root instanceof URL
281
+ ? fileURLToPath(application.root)
282
+ : resolve(vite.config.root, application.root);
283
+ const server = await createViteServer({
284
+ root,
285
+ base: `${application.path}/`,
286
+ appType: "spa",
287
+ clearScreen: false,
288
+ server: {
289
+ middlewareMode: true,
290
+ hmr: vite.httpServer ? { server: vite.httpServer } : false,
291
+ },
292
+ });
293
+ mounted.push({ ...application, server });
294
+ }
295
+ return mounted;
296
+ } catch (error) {
297
+ await Promise.all(mounted.map(({ server }) => server.close()));
298
+ throw error;
299
+ }
300
+ }
301
+
302
+ function devApplicationMiddleware(applications) {
303
+ return (request, response, next) => {
304
+ const method = request.method ?? "GET";
305
+ if (method !== "GET" && method !== "HEAD") return next();
306
+ let pathname;
307
+ try {
308
+ pathname = new URL(request.url ?? "/", "http://nanocodex.invalid").pathname;
309
+ } catch {
310
+ return next();
311
+ }
312
+ const application = applications.find(({ path }) => pathname === path || pathname.startsWith(`${path}/`));
313
+ if (!application) return next();
314
+ for (const [name, value] of Object.entries(application.headers)) {
315
+ response.setHeader(name, value);
316
+ }
317
+
318
+ const originalUrl = request.url;
319
+ let restored = false;
320
+ const restore = () => {
321
+ if (restored) return;
322
+ restored = true;
323
+ request.url = originalUrl;
324
+ response.off("finish", restore);
325
+ response.off("close", restore);
326
+ };
327
+ response.once("finish", restore);
328
+ response.once("close", restore);
329
+ application.server.middlewares(request, response, (error) => {
330
+ restore();
331
+ next(error);
332
+ });
333
+ };
334
+ }
335
+
336
+ async function localOAuthBindings() {
337
+ const environment = { ...await mainCheckoutEnvironment(), ...process.env };
338
+ return oauthBindingsFromEnvironment(environment);
339
+ }
340
+
341
+ export function oauthBindingsFromEnvironment(environment) {
342
+ return {
343
+ ...oauthCredentialPair(environment, {
344
+ label: "GitHub",
345
+ ids: ["NANOCODEX_GITHUB_OAUTH_CLIENT_ID", "GITHUB_OAUTH_CLIENT_ID", "GH_CLIENT_ID"],
346
+ secrets: ["NANOCODEX_GITHUB_OAUTH_CLIENT_SECRET", "GITHUB_OAUTH_CLIENT_SECRET", "GH_CLIENT_SECRETS"],
347
+ targetId: "GITHUB_OAUTH_CLIENT_ID",
348
+ targetSecret: "GITHUB_OAUTH_CLIENT_SECRET",
349
+ }),
350
+ ...oauthCredentialPair(environment, {
351
+ label: "Google",
352
+ ids: ["NANOCODEX_GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_CLIENT_ID"],
353
+ secrets: ["NANOCODEX_GOOGLE_OAUTH_CLIENT_SECRET", "GOOGLE_OAUTH_CLIENT_SECRET", "GOOGLE_CLIENT_SECRET"],
354
+ targetId: "GOOGLE_OAUTH_CLIENT_ID",
355
+ targetSecret: "GOOGLE_OAUTH_CLIENT_SECRET",
356
+ }),
357
+ ...oauthCredentialPair(environment, {
358
+ label: "Slack",
359
+ ids: ["NANOCODEX_SLACK_OAUTH_CLIENT_ID", "SLACK_OAUTH_CLIENT_ID", "SLACK_CLIENT_ID"],
360
+ secrets: ["NANOCODEX_SLACK_OAUTH_CLIENT_SECRET", "SLACK_OAUTH_CLIENT_SECRET", "SLACK_CLIENT_SECRET"],
361
+ targetId: "SLACK_OAUTH_CLIENT_ID",
362
+ targetSecret: "SLACK_OAUTH_CLIENT_SECRET",
363
+ }),
364
+ ...oauthCredentialPair(environment, {
365
+ label: "X",
366
+ ids: ["NANOCODEX_X_OAUTH_CLIENT_ID", "X_OAUTH_CLIENT_ID", "X_CLIENT_ID"],
367
+ secrets: ["NANOCODEX_X_OAUTH_CLIENT_SECRET", "X_OAUTH_CLIENT_SECRET", "X_CLIENT_SECRET"],
368
+ targetId: "X_OAUTH_CLIENT_ID",
369
+ targetSecret: "X_OAUTH_CLIENT_SECRET",
370
+ }),
371
+ };
372
+ }
373
+
374
+ async function mainCheckoutEnvironment() {
375
+ try {
376
+ const metadataPath = resolve(repositoryRoot, ".git");
377
+ const metadata = await stat(metadataPath);
378
+ let environmentPath = resolve(repositoryRoot, ".env");
379
+ if (metadata.isFile()) {
380
+ const pointer = (await readFile(metadataPath, "utf8")).trim();
381
+ const match = /^gitdir:\s*(.+)$/.exec(pointer);
382
+ if (!match) throw new Error(`${metadataPath} does not identify a Git directory`);
383
+ const gitDirectory = resolve(dirname(metadataPath), match[1]);
384
+ const commonDirectory = resolve(
385
+ gitDirectory,
386
+ (await readFile(resolve(gitDirectory, "commondir"), "utf8")).trim(),
387
+ );
388
+ environmentPath = resolve(dirname(commonDirectory), ".env");
389
+ }
390
+ return parseEnv(await readFile(environmentPath, "utf8"));
391
+ } catch (error) {
392
+ if (error?.code === "ENOENT") return {};
393
+ throw error;
394
+ }
395
+ }
396
+
397
+ function oauthCredentialPair(environment, names) {
398
+ const id = firstEnvironmentValue(environment, names.ids);
399
+ const secret = firstEnvironmentValue(environment, names.secrets);
400
+ if (Boolean(id) !== Boolean(secret)) {
401
+ throw new Error(`local ${names.label} OAuth client ID and secret must be configured together`);
402
+ }
403
+ return id && secret
404
+ ? { [names.targetId]: id, [names.targetSecret]: secret }
405
+ : {};
406
+ }
407
+
408
+ function firstEnvironmentValue(environment, names) {
409
+ for (const name of names) {
410
+ const value = environment[name]?.trim();
411
+ if (value) return value;
412
+ }
413
+ return undefined;
414
+ }
415
+
416
+ async function ensureJsPackage(release = false) {
417
+ const mode = release ? "release" : "development";
418
+ if (!packageBuilds.has(mode)) packageBuilds.set(mode, (async () => {
419
+ if (process.env.CI && await generatedPackageIsPresent()) return;
420
+ if (!await isSourceCheckout()) {
421
+ await Promise.all(generatedPackage.map((artifact) => access(artifact, constants.R_OK)));
422
+ return;
423
+ }
424
+ await runJsPackageBuild(release);
425
+ })());
426
+ return packageBuilds.get(mode);
427
+ }
428
+
429
+ async function runJsPackageBuild(release = false) {
430
+ await access(buildScript, constants.X_OK);
431
+ await new Promise((resolve, reject) => {
432
+ const child = spawn(buildScript, release ? ["--release"] : [], {
433
+ cwd: repositoryRoot,
434
+ stdio: "inherit",
435
+ });
436
+ child.once("error", reject);
437
+ child.once("exit", (code, signal) => {
438
+ if (code === 0) resolve();
439
+ else reject(new Error(
440
+ `Nanocodex WASM generation failed${signal ? ` (${signal})` : ` with exit code ${code}`}`,
441
+ ));
442
+ });
443
+ });
444
+ }
445
+
446
+ function watchRustBuildInputs(vite, rebuild) {
447
+ vite.watcher.add(rustBuildWatchRoots);
448
+ let debounce;
449
+ let queued = false;
450
+ let running;
451
+
452
+ const run = () => {
453
+ queued = true;
454
+ running ??= (async () => {
455
+ while (queued) {
456
+ queued = false;
457
+ vite.config.logger.info("[nanocodex] Rust/WASM input changed; rebuilding bindings...");
458
+ await rebuild();
459
+ vite.ws.send({ type: "full-reload" });
460
+ vite.config.logger.info("[nanocodex] Rust/WASM bindings rebuilt");
461
+ }
462
+ })().catch((error) => {
463
+ vite.config.logger.error(`[nanocodex] Rust/WASM rebuild failed: ${errorMessage(error)}`);
464
+ }).finally(() => {
465
+ running = undefined;
466
+ if (queued) run();
467
+ });
468
+ };
469
+ const changed = (path) => {
470
+ if (!isRustBuildInput(path)) return;
471
+ clearTimeout(debounce);
472
+ debounce = setTimeout(run, 75);
473
+ };
474
+ const close = () => {
475
+ clearTimeout(debounce);
476
+ for (const event of ["add", "change", "unlink"]) vite.watcher.off(event, changed);
477
+ };
478
+ for (const event of ["add", "change", "unlink"]) vite.watcher.on(event, changed);
479
+ vite.httpServer?.once("close", close);
480
+ }
481
+
482
+ function isRustBuildInput(path) {
483
+ if (rustBuildFiles.has(path)) return true;
484
+ return rustBuildDirectories.some((directory) => {
485
+ const nested = relative(directory, path);
486
+ return nested !== "" && !nested.startsWith(`..${sep}`) && nested !== ".." && !isAbsolute(nested);
487
+ });
488
+ }
489
+
490
+ async function generatedPackageIsPresent() {
491
+ try {
492
+ await Promise.all(generatedPackage.map((artifact) => access(artifact, constants.R_OK)));
493
+ return true;
494
+ } catch (error) {
495
+ if (error?.code === "ENOENT") return false;
496
+ throw error;
497
+ }
498
+ }
499
+
500
+ async function isSourceCheckout() {
501
+ try {
502
+ const [loaded, source] = await Promise.all([
503
+ realpath(packageManifest),
504
+ realpath(sourcePackageManifest),
505
+ ]);
506
+ return loaded === source;
507
+ } catch (error) {
508
+ if (error?.code === "ENOENT") return false;
509
+ throw error;
510
+ }
511
+ }
512
+
513
+ function workerPlugins(existing) {
514
+ return () => {
515
+ const configured = typeof existing === "function" ? existing() : [];
516
+ const plugins = (configured ?? []).flat(Infinity).filter(Boolean);
517
+ return plugins.some((plugin) => plugin?.name === "nanocodex-tools")
518
+ ? plugins
519
+ : [nanocodexTools(), ...plugins];
520
+ };
521
+ }
522
+
523
+ function errorMessage(error) {
524
+ return error instanceof Error ? error.message : String(error);
525
+ }
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
5
+ script_path="$repository_root/js/nanocodex-vite/scripts/build-js-package.sh"
6
+ cd "$repository_root"
7
+
8
+ wasm_target=wasm32-unknown-unknown
9
+ target_dir="${CARGO_TARGET_DIR:-$repository_root/target}"
10
+ if [[ "$target_dir" != /* ]]; then
11
+ target_dir="$repository_root/$target_dir"
12
+ fi
13
+
14
+ build_mode=development
15
+ cargo_profile=debug
16
+ if [[ "$#" -gt 0 ]]; then
17
+ if [[ "$#" -ne 1 || "$1" != "--release" ]]; then
18
+ echo "usage: $0 [--release]" >&2
19
+ exit 1
20
+ fi
21
+ build_mode=release
22
+ cargo_profile=wasm
23
+ fi
24
+
25
+ mkdir -p "$target_dir"
26
+ lock_file="$repository_root/.nanocodex-js-package.lock"
27
+ lock_timeout_seconds="${NANOCODEX_WASM_LOCK_TIMEOUT_SECONDS:-600}"
28
+ generated_dir=""
29
+
30
+ if [[ ! "$lock_timeout_seconds" =~ ^[0-9]+$ ]]; then
31
+ echo "NANOCODEX_WASM_LOCK_TIMEOUT_SECONDS must be a non-negative integer" >&2
32
+ exit 1
33
+ fi
34
+
35
+ if [[ "${NANOCODEX_WASM_LOCK_HELD:-}" != "$repository_root" ]]; then
36
+ if command -v flock >/dev/null 2>&1; then
37
+ if flock -E 75 -w "$lock_timeout_seconds" "$lock_file" \
38
+ env NANOCODEX_WASM_LOCK_HELD="$repository_root" "$script_path" "$@"; then
39
+ exit 0
40
+ else
41
+ lock_status=$?
42
+ fi
43
+ elif command -v lockf >/dev/null 2>&1; then
44
+ if lockf -k -t "$lock_timeout_seconds" "$lock_file" \
45
+ env NANOCODEX_WASM_LOCK_HELD="$repository_root" "$script_path" "$@"; then
46
+ exit 0
47
+ else
48
+ lock_status=$?
49
+ fi
50
+ else
51
+ echo "the Nanocodex WASM build requires flock or lockf for checkout-local serialization" >&2
52
+ exit 1
53
+ fi
54
+ if [[ "$lock_status" -eq 75 ]]; then
55
+ echo "timed out waiting for the checkout-local Nanocodex WASM build lock" >&2
56
+ fi
57
+ exit "$lock_status"
58
+ fi
59
+
60
+ cleanup() {
61
+ if [[ -n "$generated_dir" && -d "$generated_dir" ]]; then
62
+ rm -rf "$generated_dir"
63
+ fi
64
+ }
65
+
66
+ trap cleanup EXIT
67
+
68
+ if [[ "$build_mode" == release ]]; then
69
+ cargo build --locked -p nanocodex-wasm --target "$wasm_target" --profile wasm
70
+ else
71
+ cargo build --locked -p nanocodex-wasm --target "$wasm_target"
72
+ fi
73
+ wasm_artifact="$target_dir/$wasm_target/$cargo_profile/nanocodex_wasm.wasm"
74
+ stamp_path="js/nanocodex/pkg-web/.nanocodex-bindgen-stamp"
75
+ binaryen=""
76
+ if [[ "$build_mode" == release ]]; then
77
+ binaryen="$repository_root/js/nanocodex/node_modules/.bin/wasm-opt"
78
+ if [[ ! -x "$binaryen" ]]; then
79
+ echo "missing Binaryen dependency for the Nanocodex release WASM build" >&2
80
+ exit 1
81
+ fi
82
+ fi
83
+ fingerprint="$({
84
+ wasm-bindgen --version
85
+ printf 'build-mode=%s\n' "$build_mode"
86
+ printf 'worker-bundler-v1-simd\n'
87
+ cksum < js/nanocodex-vite/scripts/wasm-memory-views.mjs
88
+ if [[ "$build_mode" == release ]]; then
89
+ "$binaryen" --version
90
+ fi
91
+ cksum < "$wasm_artifact"
92
+ })"
93
+ if [[ -f "$stamp_path" ]] \
94
+ && [[ -f js/nanocodex/pkg-web/nanocodex_bg.wasm ]] \
95
+ && [[ -f js/nanocodex/pkg-web/nanocodex_bg.js ]] \
96
+ && [[ -f js/nanocodex/pkg-web/nanocodex_worker.js ]] \
97
+ && [[ -f js/nanocodex/pkg-node/nanocodex.js ]] \
98
+ && [[ -f js/nanocodex/pkg-node/nanocodex.d.ts ]] \
99
+ && [[ -f js/nanocodex/pkg-node/package.json ]] \
100
+ && [[ "$(<"$stamp_path")" == "$fingerprint" ]] \
101
+ && node js/nanocodex/scripts/write-wasm-attestation.mjs --check-cache "$wasm_artifact" 2>/dev/null; then
102
+ node js/nanocodex/scripts/write-wasm-attestation.mjs "$wasm_artifact"
103
+ echo "wasm-bindgen outputs are current"
104
+ exit 0
105
+ fi
106
+
107
+ generated_dir="$(mktemp -d)"
108
+ worker_bindings="$generated_dir/worker"
109
+ mkdir "$worker_bindings"
110
+ wasm-bindgen "$wasm_artifact" \
111
+ --target nodejs \
112
+ --out-dir js/nanocodex/pkg-node \
113
+ --out-name nanocodex
114
+ wasm-bindgen "$wasm_artifact" \
115
+ --target web \
116
+ --out-dir js/nanocodex/pkg-web \
117
+ --out-name nanocodex
118
+ wasm-bindgen "$wasm_artifact" \
119
+ --target bundler \
120
+ --out-dir "$worker_bindings" \
121
+ --out-name nanocodex
122
+ cmp "$worker_bindings/nanocodex_bg.wasm" js/nanocodex/pkg-web/nanocodex_bg.wasm
123
+ cp "$worker_bindings/nanocodex_bg.js" js/nanocodex/pkg-web/nanocodex_bg.js
124
+ cp "$worker_bindings/nanocodex.js" js/nanocodex/pkg-web/nanocodex_worker.js
125
+ node js/nanocodex-vite/scripts/wasm-memory-views.mjs \
126
+ js/nanocodex/pkg-web/nanocodex.js \
127
+ js/nanocodex/pkg-web/nanocodex_bg.js \
128
+ js/nanocodex/pkg-node/nanocodex.js
129
+ generated_wasm="js/nanocodex/pkg-web/nanocodex_bg.wasm"
130
+ if [[ "$build_mode" == release ]]; then
131
+ optimized_wasm="$generated_dir/nanocodex.wasm"
132
+ "$binaryen" -Oz \
133
+ --enable-bulk-memory \
134
+ --enable-bulk-memory-opt \
135
+ --enable-nontrapping-float-to-int \
136
+ --enable-simd \
137
+ --strip-debug \
138
+ --strip-producers \
139
+ --strip-toolchain-annotations \
140
+ "$generated_wasm" \
141
+ -o "$optimized_wasm"
142
+ mv "$optimized_wasm" "$generated_wasm"
143
+ fi
144
+ node js/nanocodex/scripts/deduplicate-wasm.mjs
145
+ node js/nanocodex/scripts/write-package-types.mjs
146
+ printf '%s\n' "$fingerprint" > "$stamp_path"
147
+ node js/nanocodex/scripts/write-wasm-attestation.mjs "$wasm_artifact"