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/README.md +48 -0
- package/chatgpt-egress.mjs +298 -0
- package/chatgpt-subscription.mjs +451 -0
- package/cloudflare-plugin.mjs +80 -0
- package/cloudflare.d.mts +21 -0
- package/cloudflare.mjs +32 -0
- package/codex-auth-file.mjs +158 -0
- package/index.d.mts +36 -0
- package/index.mjs +6 -0
- package/oauth-relay-server.mjs +125 -0
- package/oauth-relay.d.mts +104 -0
- package/oauth-relay.mjs +313 -0
- package/package.json +65 -0
- package/plugin.mjs +525 -0
- package/scripts/build-js-package.sh +147 -0
- package/scripts/wasm-memory-views.mjs +39 -0
- package/tools.d.mts +8 -0
- package/tools.mjs +38 -0
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"
|