@junejs/server 0.2.0 → 0.2.1
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/dist/{adapter-DcZ-W1-7.d.ts → adapter-D0UnYBuN.d.ts} +3 -1
- package/dist/adapter.d.ts +2 -2
- package/dist/adapter.js +55 -11
- package/dist/assets.d.ts +23 -0
- package/dist/assets.js +123 -0
- package/dist/build.d.ts +1 -1
- package/dist/build.js +8 -2
- package/dist/{config-freeze-CyXvNqi2.d.ts → config-freeze-r5p-vTWv.d.ts} +1 -1
- package/dist/config-freeze.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +9 -3
- package/src/adapter.ts +75 -11
- package/src/assets.ts +200 -0
- package/src/build.ts +11 -3
|
@@ -29,6 +29,7 @@ type AdapterEmitContext = {
|
|
|
29
29
|
plan: ResourcePlan;
|
|
30
30
|
defaultName: string;
|
|
31
31
|
publicFiles?: string[];
|
|
32
|
+
binaryImports?: string[];
|
|
32
33
|
};
|
|
33
34
|
interface JuneAdapter {
|
|
34
35
|
readonly name: string;
|
|
@@ -45,6 +46,7 @@ interface JuneAdapter {
|
|
|
45
46
|
emit(ctx: AdapterEmitContext): Promise<void>;
|
|
46
47
|
}
|
|
47
48
|
declare function hasDurableBinding(text: string, file: string, binding: string, className: string): boolean;
|
|
49
|
+
declare function hasLoaderRule(text: string, file: string, ext: string): boolean | undefined;
|
|
48
50
|
declare function workers(opts?: {
|
|
49
51
|
name?: string;
|
|
50
52
|
domain?: string;
|
|
@@ -59,4 +61,4 @@ declare function deno(opts?: {
|
|
|
59
61
|
}): JuneAdapter;
|
|
60
62
|
declare function staticSite(): JuneAdapter;
|
|
61
63
|
//#endregion
|
|
62
|
-
export { ResourcePlan as a,
|
|
64
|
+
export { ResourcePlan as a, hasLoaderRule as c, workers as d, JuneAdapter as i, staticSite as l, AdapterEmitContext as n, deno as o, AdapterEntry as r, hasDurableBinding as s, AdapterCapabilities as t, vercel as u };
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ResourcePlan, c as
|
|
2
|
-
export { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, hasDurableBinding, staticSite, vercel, workers };
|
|
1
|
+
import { a as ResourcePlan, c as hasLoaderRule, d as workers, i as JuneAdapter, l as staticSite, n as AdapterEmitContext, o as deno, r as AdapterEntry, s as hasDurableBinding, t as AdapterCapabilities, u as vercel } from "./adapter-D0UnYBuN.js";
|
|
2
|
+
export { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, hasDurableBinding, hasLoaderRule, staticSite, vercel, workers };
|
package/dist/adapter.js
CHANGED
|
@@ -5,15 +5,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
//#region src/adapter.ts
|
|
6
6
|
function hasDurableBinding(text, file, binding, className) {
|
|
7
7
|
if (file.endsWith(".toml")) {
|
|
8
|
-
const lines = text.split("\n")
|
|
9
|
-
let inString = false;
|
|
10
|
-
for (let i = 0; i < line.length; i++) {
|
|
11
|
-
const ch = line[i];
|
|
12
|
-
if (ch === "\"" && line[i - 1] !== "\\") inString = !inString;
|
|
13
|
-
else if (ch === "#" && !inString) return line.slice(0, i);
|
|
14
|
-
}
|
|
15
|
-
return line;
|
|
16
|
-
});
|
|
8
|
+
const lines = stripTomlComments(text).split("\n");
|
|
17
9
|
let inBindings = false;
|
|
18
10
|
let name;
|
|
19
11
|
let cls;
|
|
@@ -38,6 +30,39 @@ function hasDurableBinding(text, file, binding, className) {
|
|
|
38
30
|
return text.includes(className);
|
|
39
31
|
}
|
|
40
32
|
}
|
|
33
|
+
function hasLoaderRule(text, file, ext) {
|
|
34
|
+
const names = (glob) => new RegExp(`\\.(${ext}|\\{[^}]*\\b${ext}\\b[^}]*\\})$`, "i").test(glob);
|
|
35
|
+
if (file.endsWith(".toml")) {
|
|
36
|
+
const body = stripTomlComments(text);
|
|
37
|
+
let inRules = false;
|
|
38
|
+
for (const line of body.split("\n")) {
|
|
39
|
+
const t = line.trim();
|
|
40
|
+
if (t.startsWith("[")) {
|
|
41
|
+
inRules = /^\[\[\s*rules\s*\]\]$/.test(t);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const globs = inRules ? t.match(/^globs\s*=\s*\[(.*)\]/) : null;
|
|
45
|
+
if (globs && [...globs[1].matchAll(/"([^"]*)"/g)].some((m) => names(m[1]))) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return (JSON.parse(stripJsonc(text)).rules ?? []).some((r) => (r.globs ?? []).some(names));
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function stripTomlComments(text) {
|
|
56
|
+
return text.split("\n").map((line) => {
|
|
57
|
+
let inString = false;
|
|
58
|
+
for (let i = 0; i < line.length; i++) {
|
|
59
|
+
const ch = line[i];
|
|
60
|
+
if (ch === "\"" && line[i - 1] !== "\\") inString = !inString;
|
|
61
|
+
else if (ch === "#" && !inString) return line.slice(0, i);
|
|
62
|
+
}
|
|
63
|
+
return line;
|
|
64
|
+
}).join("\n");
|
|
65
|
+
}
|
|
41
66
|
function workers(opts) {
|
|
42
67
|
return {
|
|
43
68
|
name: "workers",
|
|
@@ -61,7 +86,8 @@ function workers(opts) {
|
|
|
61
86
|
wrap: (pipelineVar) => `export default withAssets(${pipelineVar}, { link: ${JSON.stringify(linkHeader)} });`
|
|
62
87
|
};
|
|
63
88
|
},
|
|
64
|
-
async emit({ appRoot, outDir, hasAssets, config, plan, defaultName }) {
|
|
89
|
+
async emit({ appRoot, outDir, hasAssets, config, plan, defaultName, binaryImports }) {
|
|
90
|
+
const dataExts = (binaryImports ?? []).filter((e) => e !== "wasm");
|
|
65
91
|
const own = ["wrangler.toml", "wrangler.jsonc"].map((f) => join(appRoot, f)).find(existsSync);
|
|
66
92
|
if (own) {
|
|
67
93
|
if (plan.agent) {
|
|
@@ -71,6 +97,19 @@ function workers(opts) {
|
|
|
71
97
|
console.warn(`[june build] durable agent built, but ${basename(own)} doesn't bind it — add:\n${snippet}`);
|
|
72
98
|
}
|
|
73
99
|
}
|
|
100
|
+
const text = dataExts.length ? await readFile(own, "utf8") : "";
|
|
101
|
+
const found = dataExts.map((ext) => [ext, hasLoaderRule(text, own, ext)]);
|
|
102
|
+
const unloaded = found.filter(([, has]) => has === false).map(([ext]) => ext);
|
|
103
|
+
const unknown = found.filter(([, has]) => has === void 0).map(([ext]) => ext);
|
|
104
|
+
if (unknown.length) {
|
|
105
|
+
const globs = unknown.map((e) => `"**/*.${e}"`).join(", ");
|
|
106
|
+
console.warn(`[june build] the worker imports ${unknown.map((e) => `.${e}`).join(", ")} files, and ${basename(own)} couldn't be parsed to check for a rule that loads them — make sure it has one, or the deploy fails with "No loader is configured":\n"rules": [{ "type": "Data", "globs": [${globs}], "fallthrough": true }]`);
|
|
107
|
+
}
|
|
108
|
+
if (unloaded.length) {
|
|
109
|
+
const globs = unloaded.map((e) => `"**/*.${e}"`).join(", ");
|
|
110
|
+
const snippet = own.endsWith(".toml") ? `[[rules]]\ntype = "Data"\nglobs = [${globs}]\nfallthrough = true` : `"rules": [{ "type": "Data", "globs": [${globs}], "fallthrough": true }]`;
|
|
111
|
+
console.warn(`[june build] the worker imports ${unloaded.map((e) => `.${e}`).join(", ")} files, but ${basename(own)} has no rule to load them (the deploy will fail with "No loader is configured") — add:\n${snippet}`);
|
|
112
|
+
}
|
|
74
113
|
return;
|
|
75
114
|
}
|
|
76
115
|
const deployCfg = config.deploy;
|
|
@@ -99,6 +138,11 @@ function workers(opts) {
|
|
|
99
138
|
new_sqlite_classes: [plan.agent.className]
|
|
100
139
|
}]
|
|
101
140
|
} : {},
|
|
141
|
+
...dataExts.length ? { rules: [{
|
|
142
|
+
type: "Data",
|
|
143
|
+
globs: dataExts.map((e) => `**/*.${e}`),
|
|
144
|
+
fallthrough: true
|
|
145
|
+
}] } : {},
|
|
102
146
|
...opts?.domain ?? deployCfg?.domain ? { routes: [{
|
|
103
147
|
pattern: opts?.domain ?? deployCfg?.domain,
|
|
104
148
|
custom_domain: true
|
|
@@ -265,4 +309,4 @@ function staticSite() {
|
|
|
265
309
|
};
|
|
266
310
|
}
|
|
267
311
|
//#endregion
|
|
268
|
-
export { deno, hasDurableBinding, staticSite, vercel, workers };
|
|
312
|
+
export { deno, hasDurableBinding, hasLoaderRule, staticSite, vercel, workers };
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/assets.d.ts
|
|
2
|
+
export type AssetRef = {
|
|
3
|
+
name: string;
|
|
4
|
+
sha256: string;
|
|
5
|
+
size: number;
|
|
6
|
+
urls: string[];
|
|
7
|
+
};
|
|
8
|
+
export type AssetCacheOptions = {
|
|
9
|
+
dir: string;
|
|
10
|
+
offline?: boolean;
|
|
11
|
+
urls?: (ref: AssetRef) => string[];
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
};
|
|
14
|
+
export interface AssetCache {
|
|
15
|
+
(ref: AssetRef): Promise<Uint8Array>;
|
|
16
|
+
cached(ref: AssetRef): Promise<boolean>;
|
|
17
|
+
egress(refs: AssetRef[]): Promise<{
|
|
18
|
+
host: string;
|
|
19
|
+
sends: "nothing";
|
|
20
|
+
}[]>;
|
|
21
|
+
}
|
|
22
|
+
export declare function assetCache(opts: AssetCacheOptions): AssetCache;
|
|
23
|
+
//#endregion
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
//#region src/assets.ts
|
|
4
|
+
const STALE_TEMP_MS = 36e5;
|
|
5
|
+
const TEMP_PREFIX = ".june-tmp-";
|
|
6
|
+
function assetCache(opts) {
|
|
7
|
+
const fetcher = opts.fetch ?? fetch;
|
|
8
|
+
const urlsFor = opts.urls ?? ((ref) => ref.urls);
|
|
9
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
10
|
+
let swept;
|
|
11
|
+
const pathOf = (ref) => join(opts.dir, safeName(ref.name));
|
|
12
|
+
async function readMatching(ref) {
|
|
13
|
+
const path = pathOf(ref);
|
|
14
|
+
let info;
|
|
15
|
+
try {
|
|
16
|
+
info = await stat(path);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code === "ENOENT") return void 0;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
if (!info.isFile()) throw new Error(`${path} is in the way of ${ref.name}: it isn't a file`);
|
|
22
|
+
if (info.size !== ref.size) return void 0;
|
|
23
|
+
const bytes = new Uint8Array(await readFile(path));
|
|
24
|
+
return await sha256(bytes) === ref.sha256 ? bytes : void 0;
|
|
25
|
+
}
|
|
26
|
+
async function get(ref) {
|
|
27
|
+
pathOf(ref);
|
|
28
|
+
const hit = await readMatching(ref);
|
|
29
|
+
if (hit) return hit;
|
|
30
|
+
if (opts.offline) {
|
|
31
|
+
const state = await stat(pathOf(ref)).then(() => true, () => false) ? `in ${opts.dir} but doesn't match` : `not in ${opts.dir}`;
|
|
32
|
+
throw new Error(`${ref.name} is ${state}, and this cache is offline: copy the file with sha256 ${ref.sha256} (${ref.size} bytes) to ${pathOf(ref)} — it is at ${ref.urls.join(" or ")}`);
|
|
33
|
+
}
|
|
34
|
+
await (swept ??= sweepTemps(opts.dir).catch(() => {}));
|
|
35
|
+
const bytes = await download(ref, urlsFor(ref), fetcher);
|
|
36
|
+
await mkdir(opts.dir, { recursive: true });
|
|
37
|
+
const temp = join(opts.dir, `${TEMP_PREFIX}${crypto.randomUUID()}-${safeName(ref.name)}`);
|
|
38
|
+
try {
|
|
39
|
+
await writeFile(temp, bytes);
|
|
40
|
+
await rename(temp, pathOf(ref));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
await rm(temp, { force: true }).catch(() => {});
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
const cache = ((ref) => {
|
|
48
|
+
const key = `${ref.name}\0${ref.sha256}\0${ref.size}`;
|
|
49
|
+
const running = inflight.get(key);
|
|
50
|
+
if (running) return running;
|
|
51
|
+
const p = get(ref).finally(() => inflight.delete(key));
|
|
52
|
+
inflight.set(key, p);
|
|
53
|
+
return p;
|
|
54
|
+
});
|
|
55
|
+
cache.cached = async (ref) => await readMatching(ref) !== void 0;
|
|
56
|
+
cache.egress = async (refs) => {
|
|
57
|
+
if (opts.offline) return [];
|
|
58
|
+
const missing = (await Promise.all(refs.map(async (r) => await cache.cached(r) ? [] : urlsFor(r)))).flat();
|
|
59
|
+
return [...new Set(missing.map((u) => new URL(u).host))].map((host) => ({
|
|
60
|
+
host,
|
|
61
|
+
sends: "nothing"
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
return cache;
|
|
65
|
+
}
|
|
66
|
+
async function download(ref, urls, fetcher) {
|
|
67
|
+
const failures = [];
|
|
68
|
+
for (const url of urls) try {
|
|
69
|
+
const res = await fetcher(url);
|
|
70
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
71
|
+
const declared = Number(res.headers.get("content-length") ?? NaN);
|
|
72
|
+
if (declared > ref.size) throw new Error(`${declared} bytes declared, expected ${ref.size}`);
|
|
73
|
+
const bytes = await readCapped(res, ref.size);
|
|
74
|
+
const digest = await sha256(bytes);
|
|
75
|
+
if (bytes.length !== ref.size || digest !== ref.sha256) throw new Error(`got ${bytes.length} bytes with sha256 ${digest}, expected ${ref.size} with ${ref.sha256}`);
|
|
76
|
+
return bytes;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
failures.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`${ref.name} could not be fetched (${failures.join("; ") || "no URLs"}). To run without the network, copy the file into the cache directory and pass offline: true`);
|
|
81
|
+
}
|
|
82
|
+
async function readCapped(res, size) {
|
|
83
|
+
if (!res.body) return new Uint8Array(await res.arrayBuffer());
|
|
84
|
+
const out = new Uint8Array(size + 1);
|
|
85
|
+
let length = 0;
|
|
86
|
+
const reader = res.body.getReader();
|
|
87
|
+
for (;;) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done) break;
|
|
90
|
+
if (length + value.length >= out.length) {
|
|
91
|
+
out.set(value.subarray(0, out.length - length), length);
|
|
92
|
+
length = out.length;
|
|
93
|
+
await reader.cancel();
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
out.set(value, length);
|
|
97
|
+
length += value.length;
|
|
98
|
+
}
|
|
99
|
+
return out.subarray(0, length);
|
|
100
|
+
}
|
|
101
|
+
async function sha256(bytes) {
|
|
102
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
103
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
104
|
+
}
|
|
105
|
+
function safeName(name) {
|
|
106
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name) || name.startsWith(TEMP_PREFIX)) throw new Error(`unsafe asset name: ${JSON.stringify(name)}`);
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
async function sweepTemps(dir) {
|
|
110
|
+
let names;
|
|
111
|
+
try {
|
|
112
|
+
names = await readdir(dir);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
await Promise.all(names.filter((n) => n.startsWith(TEMP_PREFIX)).map(async (n) => {
|
|
118
|
+
const s = await stat(join(dir, n)).catch(() => void 0);
|
|
119
|
+
if (s && now - s.mtimeMs > STALE_TEMP_MS) await rm(join(dir, n), { force: true });
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { assetCache };
|
package/dist/build.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as scanAppRoutes, o as scanRoutes, t as RouteEntry } from "./route-scan-BMbBzPIB.js";
|
|
2
2
|
import { t as generateContent } from "./content-freeze-CyiW2fRK.js";
|
|
3
|
-
import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-
|
|
3
|
+
import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-r5p-vTWv.js";
|
|
4
4
|
import { n as buildManifest } from "./manifest-CwTT1zVs.js";
|
|
5
5
|
//#region src/build.d.ts
|
|
6
6
|
export type BuildResult = {
|
package/dist/build.js
CHANGED
|
@@ -243,6 +243,7 @@ ${doClass}`;
|
|
|
243
243
|
const entryFile = join(genDir, "worker-entry.tsx");
|
|
244
244
|
await writeFile(entryFile, entry);
|
|
245
245
|
const { rolldown } = await import("rolldown");
|
|
246
|
+
const binaryImports = /* @__PURE__ */ new Set();
|
|
246
247
|
const bundle = await rolldown({
|
|
247
248
|
input: entryFile,
|
|
248
249
|
cwd: appRoot,
|
|
@@ -255,7 +256,11 @@ ${doClass}`;
|
|
|
255
256
|
external: (id) => {
|
|
256
257
|
if (isBunSpecifier(id)) return true;
|
|
257
258
|
if (id.startsWith("cloudflare:")) return true;
|
|
258
|
-
|
|
259
|
+
const binary = /\.(wasm|ttf|otf|woff2?|png|jpe?g|avif|webp)$/i.exec(id);
|
|
260
|
+
if (binary) {
|
|
261
|
+
binaryImports.add(binary[1].toLowerCase());
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
259
264
|
return [...adapter.buildExternal ?? [], ...options.external ?? frozen.buildExternal].some((e) => id === e || id.startsWith(`${e}/`));
|
|
260
265
|
},
|
|
261
266
|
resolve: { conditionNames: adapter.conditions }
|
|
@@ -421,7 +426,8 @@ ${doClass}`;
|
|
|
421
426
|
config: fullConfig,
|
|
422
427
|
plan,
|
|
423
428
|
defaultName,
|
|
424
|
-
publicFiles
|
|
429
|
+
publicFiles,
|
|
430
|
+
binaryImports: [...binaryImports].sort()
|
|
425
431
|
});
|
|
426
432
|
if (plan.db) console.log(` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`);
|
|
427
433
|
if (plan.agent) console.log(` ↳ durable agent mounted (${plan.agent.className} → env.${plan.agent.binding}; chat at ${frozen.agent.runtime.chat.path})`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as JuneAdapter } from "./adapter-
|
|
1
|
+
import { i as JuneAdapter } from "./adapter-D0UnYBuN.js";
|
|
2
2
|
import { n as WorkerManifest } from "./worker-2qbQjj2c.js";
|
|
3
3
|
import { JuneConfig } from "@junejs/core/config";
|
|
4
4
|
import { DocumentConfig } from "@junejs/core/document";
|
package/dist/config-freeze.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-
|
|
1
|
+
import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-r5p-vTWv.js";
|
|
2
2
|
export { freezeConfig, normalizeBase, resolveDeployAdapter, workerName };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ResourcePlan,
|
|
1
|
+
import { a as ResourcePlan, d as workers, i as JuneAdapter, l as staticSite, n as AdapterEmitContext, o as deno, r as AdapterEntry, t as AdapterCapabilities, u as vercel } from "./adapter-D0UnYBuN.js";
|
|
2
2
|
import { AGENT_MODULE_FILE, AgentDirScan, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir } from "./agent-compile.js";
|
|
3
3
|
import { CreateAppOptions, JuneApp, createApp } from "./app.js";
|
|
4
4
|
import { R2Bucket, localBlob, r2 } from "./blob.js";
|
|
@@ -6,7 +6,7 @@ import { o as scanRoutes } from "./route-scan-BMbBzPIB.js";
|
|
|
6
6
|
import { t as generateContent } from "./content-freeze-CyiW2fRK.js";
|
|
7
7
|
import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-BHUjBO0G.js";
|
|
8
8
|
import { n as WorkerManifest, r as createWorker } from "./worker-2qbQjj2c.js";
|
|
9
|
-
import { t as freezeConfig } from "./config-freeze-
|
|
9
|
+
import { t as freezeConfig } from "./config-freeze-r5p-vTWv.js";
|
|
10
10
|
import { n as buildManifest } from "./manifest-CwTT1zVs.js";
|
|
11
11
|
import { BuildResult, juneBuild } from "./build.js";
|
|
12
12
|
import { loadJuneConfig } from "./config-loader.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,6 +60,12 @@
|
|
|
60
60
|
"types": "./dist/blob.d.ts",
|
|
61
61
|
"default": "./dist/blob.js"
|
|
62
62
|
},
|
|
63
|
+
"./assets": {
|
|
64
|
+
"source": "./src/assets.ts",
|
|
65
|
+
"bun": "./src/assets.ts",
|
|
66
|
+
"types": "./dist/assets.d.ts",
|
|
67
|
+
"default": "./dist/assets.js"
|
|
68
|
+
},
|
|
63
69
|
"./resources": {
|
|
64
70
|
"source": "./src/resources.ts",
|
|
65
71
|
"bun": "./src/resources.ts",
|
|
@@ -132,7 +138,7 @@
|
|
|
132
138
|
"build": "tsdown"
|
|
133
139
|
},
|
|
134
140
|
"dependencies": {
|
|
135
|
-
"@junejs/db": "^0.2.
|
|
141
|
+
"@junejs/db": "^0.2.1",
|
|
136
142
|
"@momiji-rs/sparkdown": "^0.0.4",
|
|
137
143
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
138
144
|
"lightningcss": "^1.27.0",
|
|
@@ -140,7 +146,7 @@
|
|
|
140
146
|
"rolldown": "^1.1.0"
|
|
141
147
|
},
|
|
142
148
|
"peerDependencies": {
|
|
143
|
-
"@junejs/core": "^0.2.
|
|
149
|
+
"@junejs/core": "^0.2.1",
|
|
144
150
|
"react": "^19.2.0",
|
|
145
151
|
"react-dom": "^19.2.0",
|
|
146
152
|
"react-server-dom-webpack": "19.3.0",
|
package/src/adapter.ts
CHANGED
|
@@ -62,6 +62,9 @@ export type AdapterEmitContext = {
|
|
|
62
62
|
// copied into outDir/assets. Adapters that build a separate static tier (vercel)
|
|
63
63
|
// place these there; whole-assets targets (workers/static/deno) ignore it.
|
|
64
64
|
publicFiles?: string[];
|
|
65
|
+
// Extensions (lowercase, no dot: "wasm", "otf") of the binary files the worker bundle
|
|
66
|
+
// imports and leaves external for the platform's bundler to load.
|
|
67
|
+
binaryImports?: string[];
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
export interface JuneAdapter {
|
|
@@ -97,16 +100,7 @@ export interface JuneAdapter {
|
|
|
97
100
|
// (advisory) warning, never a suppressed one. Pure — unit-tested directly.
|
|
98
101
|
export function hasDurableBinding(text: string, file: string, binding: string, className: string): boolean {
|
|
99
102
|
if (file.endsWith(".toml")) {
|
|
100
|
-
|
|
101
|
-
const lines = text.split("\n").map((line) => {
|
|
102
|
-
let inString = false;
|
|
103
|
-
for (let i = 0; i < line.length; i++) {
|
|
104
|
-
const ch = line[i];
|
|
105
|
-
if (ch === '"' && line[i - 1] !== "\\") inString = !inString;
|
|
106
|
-
else if (ch === "#" && !inString) return line.slice(0, i);
|
|
107
|
-
}
|
|
108
|
-
return line;
|
|
109
|
-
});
|
|
103
|
+
const lines = stripTomlComments(text).split("\n");
|
|
110
104
|
let inBindings = false;
|
|
111
105
|
let name: string | undefined;
|
|
112
106
|
let cls: string | undefined;
|
|
@@ -133,6 +127,52 @@ export function hasDurableBinding(text: string, file: string, binding: string, c
|
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
129
|
|
|
130
|
+
// Does an app-owned wrangler config give `.ext` files a loader? Any rule with a glob ending in
|
|
131
|
+
// the extension (`**/*.otf`, `**/*.{otf,ttf}`) counts, whatever its type. The warning this
|
|
132
|
+
// feeds is advisory, so a glob it can't read may draw a spurious warning, never suppress one;
|
|
133
|
+
// comments are stripped first, so a commented-out rule doesn't count. undefined: the JSONC
|
|
134
|
+
// couldn't be parsed, so the answer is unknown. Pure — unit-tested.
|
|
135
|
+
export function hasLoaderRule(text: string, file: string, ext: string): boolean | undefined {
|
|
136
|
+
const names = (glob: string) => new RegExp(`\\.(${ext}|\\{[^}]*\\b${ext}\\b[^}]*\\})$`, "i").test(glob);
|
|
137
|
+
if (file.endsWith(".toml")) {
|
|
138
|
+
// The globs = [...] arrays of [[rules]] tables, with '#' comments removed.
|
|
139
|
+
const body = stripTomlComments(text);
|
|
140
|
+
let inRules = false;
|
|
141
|
+
for (const line of body.split("\n")) {
|
|
142
|
+
const t = line.trim();
|
|
143
|
+
if (t.startsWith("[")) {
|
|
144
|
+
inRules = /^\[\[\s*rules\s*\]\]$/.test(t);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const globs = inRules ? t.match(/^globs\s*=\s*\[(.*)\]/) : null;
|
|
148
|
+
if (globs && [...globs[1]!.matchAll(/"([^"]*)"/g)].some((m) => names(m[1]!))) return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(stripJsonc(text)) as { rules?: { globs?: string[] }[] };
|
|
154
|
+
return (parsed.rules ?? []).some((r) => (r.globs ?? []).some(names));
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined; // unparsable — the caller warns that it couldn't check, never crashes the build
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// '#' to end of line, unless inside a double-quoted string.
|
|
161
|
+
function stripTomlComments(text: string): string {
|
|
162
|
+
return text
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((line) => {
|
|
165
|
+
let inString = false;
|
|
166
|
+
for (let i = 0; i < line.length; i++) {
|
|
167
|
+
const ch = line[i];
|
|
168
|
+
if (ch === '"' && line[i - 1] !== "\\") inString = !inString;
|
|
169
|
+
else if (ch === "#" && !inString) return line.slice(0, i);
|
|
170
|
+
}
|
|
171
|
+
return line;
|
|
172
|
+
})
|
|
173
|
+
.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
136
176
|
// The built-in default. Reproduces exactly what build.ts emitted inline:
|
|
137
177
|
// withAssets(pipeline) as the entry, and a wrangler.jsonc with the assets
|
|
138
178
|
// binding (run_worker_first) + optional custom domain.
|
|
@@ -154,7 +194,13 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
154
194
|
};
|
|
155
195
|
},
|
|
156
196
|
|
|
157
|
-
async emit({ appRoot, outDir, hasAssets, config, plan, defaultName }) {
|
|
197
|
+
async emit({ appRoot, outDir, hasAssets, config, plan, defaultName, binaryImports }) {
|
|
198
|
+
// Wrangler loads .wasm by default (CompiledWasm) and has no loader for the other
|
|
199
|
+
// binaries build.ts leaves external, so a font package's .otf failed the deploy with
|
|
200
|
+
// "No loader is configured" (#379). Each one the bundle imports gets a Data rule: the
|
|
201
|
+
// module is the file's bytes, which is what a font or image package's workerd entry
|
|
202
|
+
// expects.
|
|
203
|
+
const dataExts = (binaryImports ?? []).filter((e) => e !== "wasm");
|
|
158
204
|
// An app that manages its own wrangler config wins — don't overwrite it.
|
|
159
205
|
// But a durable agent NEEDS its DO binding: warn when the app-owned config
|
|
160
206
|
// doesn't BIND the generated class under the name createWorker reads
|
|
@@ -173,6 +219,21 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
173
219
|
console.warn(`[june build] durable agent built, but ${basename(own)} doesn't bind it — add:\n${snippet}`);
|
|
174
220
|
}
|
|
175
221
|
}
|
|
222
|
+
const text = dataExts.length ? await readFile(own, "utf8") : "";
|
|
223
|
+
const found = dataExts.map((ext) => [ext, hasLoaderRule(text, own, ext)] as const);
|
|
224
|
+
const unloaded = found.filter(([, has]) => has === false).map(([ext]) => ext);
|
|
225
|
+
const unknown = found.filter(([, has]) => has === undefined).map(([ext]) => ext);
|
|
226
|
+
if (unknown.length) {
|
|
227
|
+
const globs = unknown.map((e) => `"**/*.${e}"`).join(", ");
|
|
228
|
+
console.warn(`[june build] the worker imports ${unknown.map((e) => `.${e}`).join(", ")} files, and ${basename(own)} couldn't be parsed to check for a rule that loads them — make sure it has one, or the deploy fails with "No loader is configured":\n"rules": [{ "type": "Data", "globs": [${globs}], "fallthrough": true }]`);
|
|
229
|
+
}
|
|
230
|
+
if (unloaded.length) {
|
|
231
|
+
const globs = unloaded.map((e) => `"**/*.${e}"`).join(", ");
|
|
232
|
+
const snippet = own.endsWith(".toml")
|
|
233
|
+
? `[[rules]]\ntype = "Data"\nglobs = [${globs}]\nfallthrough = true`
|
|
234
|
+
: `"rules": [{ "type": "Data", "globs": [${globs}], "fallthrough": true }]`;
|
|
235
|
+
console.warn(`[june build] the worker imports ${unloaded.map((e) => `.${e}`).join(", ")} files, but ${basename(own)} has no rule to load them (the deploy will fail with "No loader is configured") — add:\n${snippet}`);
|
|
236
|
+
}
|
|
176
237
|
return;
|
|
177
238
|
}
|
|
178
239
|
const deployCfg = config.deploy;
|
|
@@ -214,6 +275,9 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
214
275
|
migrations: [{ tag: "v1", new_sqlite_classes: [plan.agent.className] }],
|
|
215
276
|
}
|
|
216
277
|
: {}),
|
|
278
|
+
// Binary files the bundle imports that wrangler has no default loader for.
|
|
279
|
+
// fallthrough: without it wrangler drops its own default Data rule (**/*.bin).
|
|
280
|
+
...(dataExts.length ? { rules: [{ type: "Data", globs: dataExts.map((e) => `**/*.${e}`), fallthrough: true }] } : {}),
|
|
217
281
|
// config deploy.domain → a Workers custom domain; without it the
|
|
218
282
|
// regenerated file would silently drop a hand-attached domain.
|
|
219
283
|
...(opts?.domain ?? deployCfg?.domain
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// assetCache — a loader for the large files an `unbundled` entry leaves out of a build (#383):
|
|
2
|
+
// the PDF engine, a CJK font. Given a directory the app owns (a data volume), it serves each
|
|
3
|
+
// file from there and fetches it the first time, so a single binary doesn't carry 40 MiB it
|
|
4
|
+
// may never use:
|
|
5
|
+
//
|
|
6
|
+
// import { assetCache } from "@junejs/server/assets";
|
|
7
|
+
// const load = assetCache({ dir: "/data/june-assets" });
|
|
8
|
+
// typstWasmRenderer({ load, fonts: [notoSansTC({ load })] });
|
|
9
|
+
//
|
|
10
|
+
// What it guarantees, so it can sit under a production app:
|
|
11
|
+
// • a file is returned only when its size and SHA-256 match the ref — on disk or downloaded;
|
|
12
|
+
// a stale or corrupt file is replaced, never used (the packages check again regardless);
|
|
13
|
+
// • a download stops at the ref's size, so a wrong URL can't fill the disk;
|
|
14
|
+
// • the file lands by rename from a temp file in the same directory, so a crash leaves no
|
|
15
|
+
// half-written file under the real name;
|
|
16
|
+
// • one download per file per cache, however many renders ask at once (share one `load`;
|
|
17
|
+
// two caches over the same dir each download, and the rename keeps that safe);
|
|
18
|
+
// • `offline: true` never touches the network: for hosts without egress, the files are
|
|
19
|
+
// copied in beforehand (`dir/<name>`, e.g. dir/typst.wasm), and a missing one is an error
|
|
20
|
+
// that says so.
|
|
21
|
+
//
|
|
22
|
+
// The request is a fixed URL: no document data leaves. `egress(refs)` says which hosts a
|
|
23
|
+
// call would still contact, for a setup check.
|
|
24
|
+
//
|
|
25
|
+
// AssetRef is the shape @junejs/pdf-typst-wasm defines; it is repeated here, so the server
|
|
26
|
+
// depends on no PDF package.
|
|
27
|
+
|
|
28
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
export type AssetRef = { name: string; sha256: string; size: number; urls: string[] };
|
|
32
|
+
|
|
33
|
+
export type AssetCacheOptions = {
|
|
34
|
+
// Where the files live: one per ref, at dir/<name>.
|
|
35
|
+
dir: string;
|
|
36
|
+
// Never download: serve what is in `dir`, and fail naming the file to copy in when it's
|
|
37
|
+
// missing or doesn't match.
|
|
38
|
+
offline?: boolean;
|
|
39
|
+
// Where to fetch a ref from, in order (default: ref.urls) — a mirror the app controls.
|
|
40
|
+
urls?: (ref: AssetRef) => string[];
|
|
41
|
+
// How to fetch (default: global fetch).
|
|
42
|
+
fetch?: typeof fetch;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface AssetCache {
|
|
46
|
+
// The file's bytes, from `dir` or downloaded into it; always matching the ref.
|
|
47
|
+
(ref: AssetRef): Promise<Uint8Array>;
|
|
48
|
+
// Whether `dir` already holds a file matching the ref.
|
|
49
|
+
cached(ref: AssetRef): Promise<boolean>;
|
|
50
|
+
// The hosts a call for these refs would contact: [] when all are cached, or when offline.
|
|
51
|
+
egress(refs: AssetRef[]): Promise<{ host: string; sends: "nothing" }[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A temp file older than this is a crashed download; younger ones may be another process's.
|
|
55
|
+
const STALE_TEMP_MS = 60 * 60 * 1000;
|
|
56
|
+
// Temp files carry this prefix, and no asset may: the sweep removes nothing else.
|
|
57
|
+
const TEMP_PREFIX = ".june-tmp-";
|
|
58
|
+
|
|
59
|
+
export function assetCache(opts: AssetCacheOptions): AssetCache {
|
|
60
|
+
const fetcher = opts.fetch ?? fetch;
|
|
61
|
+
const urlsFor = opts.urls ?? ((ref: AssetRef) => ref.urls);
|
|
62
|
+
const inflight = new Map<string, Promise<Uint8Array>>();
|
|
63
|
+
let swept: Promise<void> | undefined;
|
|
64
|
+
|
|
65
|
+
const pathOf = (ref: AssetRef) => join(opts.dir, safeName(ref.name));
|
|
66
|
+
|
|
67
|
+
// The file at dir/<name> when it matches the ref; undefined when it's missing or doesn't.
|
|
68
|
+
// Sized first, so a large wrong file is never read; a read error other than "missing" (a
|
|
69
|
+
// directory in its place, no permission) is the caller's problem, not a cache miss.
|
|
70
|
+
async function readMatching(ref: AssetRef): Promise<Uint8Array | undefined> {
|
|
71
|
+
const path = pathOf(ref);
|
|
72
|
+
let info: Awaited<ReturnType<typeof stat>>;
|
|
73
|
+
try {
|
|
74
|
+
info = await stat(path);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if ((err as { code?: string }).code === "ENOENT") return undefined;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
if (!info.isFile()) throw new Error(`${path} is in the way of ${ref.name}: it isn't a file`);
|
|
80
|
+
if (info.size !== ref.size) return undefined;
|
|
81
|
+
const bytes = new Uint8Array(await readFile(path));
|
|
82
|
+
return (await sha256(bytes)) === ref.sha256 ? bytes : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function get(ref: AssetRef): Promise<Uint8Array> {
|
|
86
|
+
pathOf(ref); // an unsafe name fails here, before any read or fetch
|
|
87
|
+
const hit = await readMatching(ref);
|
|
88
|
+
if (hit) return hit;
|
|
89
|
+
if (opts.offline) {
|
|
90
|
+
const there = await stat(pathOf(ref)).then(() => true, () => false);
|
|
91
|
+
const state = there ? `in ${opts.dir} but doesn't match` : `not in ${opts.dir}`;
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${ref.name} is ${state}, and this cache is offline: ` +
|
|
94
|
+
`copy the file with sha256 ${ref.sha256} (${ref.size} bytes) to ${pathOf(ref)} — it is at ${ref.urls.join(" or ")}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
// Housekeeping, best effort: a temp it can't remove must not fail this download or any later.
|
|
98
|
+
await (swept ??= sweepTemps(opts.dir).catch(() => {}));
|
|
99
|
+
const bytes = await download(ref, urlsFor(ref), fetcher);
|
|
100
|
+
await mkdir(opts.dir, { recursive: true });
|
|
101
|
+
const temp = join(opts.dir, `${TEMP_PREFIX}${crypto.randomUUID()}-${safeName(ref.name)}`);
|
|
102
|
+
try {
|
|
103
|
+
await writeFile(temp, bytes);
|
|
104
|
+
await rename(temp, pathOf(ref)); // atomic on one filesystem: never a half-written file
|
|
105
|
+
} catch (err) {
|
|
106
|
+
await rm(temp, { force: true }).catch(() => {}); // don't leave it for the next sweep
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
return bytes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Keyed by the ref's whole identity (name, content, size): only identical refs share a
|
|
113
|
+
// download, so each call gets bytes that match its own ref.
|
|
114
|
+
const cache = ((ref: AssetRef) => {
|
|
115
|
+
const key = `${ref.name}\0${ref.sha256}\0${ref.size}`;
|
|
116
|
+
const running = inflight.get(key);
|
|
117
|
+
if (running) return running;
|
|
118
|
+
const p = get(ref).finally(() => inflight.delete(key));
|
|
119
|
+
inflight.set(key, p);
|
|
120
|
+
return p;
|
|
121
|
+
}) as AssetCache;
|
|
122
|
+
cache.cached = async (ref) => (await readMatching(ref)) !== undefined;
|
|
123
|
+
cache.egress = async (refs) => {
|
|
124
|
+
if (opts.offline) return [];
|
|
125
|
+
const missing = (await Promise.all(refs.map(async (r) => ((await cache.cached(r)) ? [] : urlsFor(r))))).flat();
|
|
126
|
+
return [...new Set(missing.map((u) => new URL(u).host))].map((host) => ({ host, sends: "nothing" as const }));
|
|
127
|
+
};
|
|
128
|
+
return cache;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Each URL in turn; the first whose bytes match wins. The error lists every attempt.
|
|
132
|
+
async function download(ref: AssetRef, urls: string[], fetcher: typeof fetch): Promise<Uint8Array> {
|
|
133
|
+
const failures: string[] = [];
|
|
134
|
+
for (const url of urls) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetcher(url);
|
|
137
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
138
|
+
const declared = Number(res.headers.get("content-length") ?? NaN);
|
|
139
|
+
if (declared > ref.size) throw new Error(`${declared} bytes declared, expected ${ref.size}`);
|
|
140
|
+
const bytes = await readCapped(res, ref.size);
|
|
141
|
+
const digest = await sha256(bytes);
|
|
142
|
+
if (bytes.length !== ref.size || digest !== ref.sha256) throw new Error(`got ${bytes.length} bytes with sha256 ${digest}, expected ${ref.size} with ${ref.sha256}`);
|
|
143
|
+
return bytes;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
failures.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`${ref.name} could not be fetched (${failures.join("; ") || "no URLs"}). To run without the network, copy the file into the cache directory and pass offline: true`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The body, cut off one byte past the expected size: enough to know it's wrong, never more.
|
|
152
|
+
async function readCapped(res: Response, size: number): Promise<Uint8Array> {
|
|
153
|
+
if (!res.body) return new Uint8Array(await res.arrayBuffer());
|
|
154
|
+
const out = new Uint8Array(size + 1);
|
|
155
|
+
let length = 0;
|
|
156
|
+
const reader = res.body.getReader();
|
|
157
|
+
for (;;) {
|
|
158
|
+
const { done, value } = await reader.read();
|
|
159
|
+
if (done) break;
|
|
160
|
+
if (length + value.length >= out.length) {
|
|
161
|
+
out.set(value.subarray(0, out.length - length), length);
|
|
162
|
+
length = out.length;
|
|
163
|
+
await reader.cancel();
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
out.set(value, length);
|
|
167
|
+
length += value.length;
|
|
168
|
+
}
|
|
169
|
+
return out.subarray(0, length);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function sha256(bytes: Uint8Array): Promise<string> {
|
|
173
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes as Uint8Array<ArrayBuffer>);
|
|
174
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A ref's name becomes a file name: one path segment, nothing that climbs out of `dir`.
|
|
178
|
+
function safeName(name: string): string {
|
|
179
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name) || name.startsWith(TEMP_PREFIX)) throw new Error(`unsafe asset name: ${JSON.stringify(name)}`);
|
|
180
|
+
return name;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Temp files a crashed download left behind; ones still young may belong to another process.
|
|
184
|
+
async function sweepTemps(dir: string): Promise<void> {
|
|
185
|
+
let names: string[];
|
|
186
|
+
try {
|
|
187
|
+
names = await readdir(dir);
|
|
188
|
+
} catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
await Promise.all(
|
|
193
|
+
names
|
|
194
|
+
.filter((n) => n.startsWith(TEMP_PREFIX))
|
|
195
|
+
.map(async (n) => {
|
|
196
|
+
const s = await stat(join(dir, n)).catch(() => undefined);
|
|
197
|
+
if (s && now - s.mtimeMs > STALE_TEMP_MS) await rm(join(dir, n), { force: true });
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -460,6 +460,9 @@ ${doClass}`;
|
|
|
460
460
|
|
|
461
461
|
// ---- bundle (Rolldown; self-contained ESM for workerd) -------------------
|
|
462
462
|
const { rolldown } = await import("rolldown");
|
|
463
|
+
// Extensions of the binary files the graph imports and leaves to the platform's bundler
|
|
464
|
+
// (below): the adapter needs them to give each a loader (workers() → wrangler rules, #379).
|
|
465
|
+
const binaryImports = new Set<string>();
|
|
463
466
|
const bundle = await rolldown({
|
|
464
467
|
input: entryFile,
|
|
465
468
|
cwd: appRoot,
|
|
@@ -483,8 +486,13 @@ ${doClass}`;
|
|
|
483
486
|
if (isBunSpecifier(id)) return true;
|
|
484
487
|
// workerd runtime modules (the durable-agent entry imports cloudflare:workers).
|
|
485
488
|
if (id.startsWith("cloudflare:")) return true;
|
|
486
|
-
// Binary assets stay external —
|
|
487
|
-
|
|
489
|
+
// Binary assets stay external — the platform's bundler loads them (wrangler: a
|
|
490
|
+
// CompiledWasm or Data rule, which workers() emits for what is recorded here).
|
|
491
|
+
const binary = /\.(wasm|ttf|otf|woff2?|png|jpe?g|avif|webp)$/i.exec(id);
|
|
492
|
+
if (binary) {
|
|
493
|
+
binaryImports.add(binary[1]!.toLowerCase());
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
488
496
|
// Merge: adapter.buildExternal (target-specific, e.g. workers-og for the
|
|
489
497
|
// Workers adapter) + config build.external (user additions). User config
|
|
490
498
|
// wins additions but can never REMOVE the adapter's own required externals.
|
|
@@ -754,7 +762,7 @@ ${doClass}`;
|
|
|
754
762
|
// and the class the generated entry exports.
|
|
755
763
|
...(agentModule ? { agent: { binding: "AGENT", className: "JuneAgentDO" } } : {}),
|
|
756
764
|
};
|
|
757
|
-
await adapter.emit({ appRoot, outDir, hasAssets, linkHeader, config: fullConfig, plan, defaultName, publicFiles });
|
|
765
|
+
await adapter.emit({ appRoot, outDir, hasAssets, linkHeader, config: fullConfig, plan, defaultName, publicFiles, binaryImports: [...binaryImports].sort() });
|
|
758
766
|
if (plan.db) {
|
|
759
767
|
console.log(
|
|
760
768
|
` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`,
|