sproutboat 0.8.0 → 0.10.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.
@@ -0,0 +1,44 @@
1
+ /** Build one platform package executable. npm's tiny root launcher resolves it. */
2
+ import { chmod, copyFile, mkdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+
5
+ const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
6
+ const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
7
+ if (!platform || !arch) throw new Error(`unsupported release host ${process.platform}/${process.arch}`);
8
+ const packageDir = resolve(import.meta.dir, "..", "platform-packages", `${platform}-${arch}`);
9
+ const out = resolve(packageDir, "bin", "sproutboat");
10
+ await mkdir(resolve(out, ".."), { recursive: true });
11
+ // SAFETY: package.json is this release's manifest and npm requires its version.
12
+ const version = ((await Bun.file(resolve(import.meta.dir, "..", "package.json")).json()) as { version: string })
13
+ .version;
14
+ // SAFETY: every checked-in platform package manifest owns a required string version.
15
+ const platformVersion = ((await Bun.file(resolve(packageDir, "package.json")).json()) as { version: string }).version;
16
+ if (platformVersion !== version)
17
+ throw new Error(`platform package version ${platformVersion} does not match root ${version}`);
18
+ const child = Bun.spawn(
19
+ [
20
+ process.execPath,
21
+ "build",
22
+ "--compile",
23
+ "--target=bun",
24
+ `--define:process.env.SPROUTBOAT_CLI_VERSION=${JSON.stringify(version)}`,
25
+ "--outfile",
26
+ out,
27
+ "src/main.ts",
28
+ ],
29
+ {
30
+ cwd: resolve(import.meta.dir, ".."),
31
+ stdout: "inherit",
32
+ stderr: "inherit",
33
+ },
34
+ );
35
+ if ((await child.exited) !== 0) process.exit(1);
36
+ let esbuild: string;
37
+ try {
38
+ esbuild = Bun.resolveSync(`@esbuild/${platform}-${arch}/bin/esbuild`, import.meta.dir);
39
+ } catch {
40
+ throw new Error("matching esbuild binary is missing; run bun install before building a platform package");
41
+ }
42
+ const packagedEsbuild = resolve(packageDir, "bin", "esbuild");
43
+ await copyFile(esbuild, packagedEsbuild);
44
+ await chmod(packagedEsbuild, 0o755);
package/src/assets.ts CHANGED
@@ -1,121 +1,4 @@
1
- /**
2
- * Static-asset manifest. `sproutboat build` copies the project's `assets`
3
- * directory next to the artifact and writes `assets.json` (this manifest); the
4
- * edge serves matching files directly (assets-first, like Cloudflare), and the
5
- * broker's `assets.get` op backs `env.<ASSETS>.fetch(request)` for the paths
6
- * the sprout chooses to serve itself.
7
- */
8
- import { createHash } from "node:crypto";
9
- import { readdirSync, readFileSync } from "node:fs";
10
- import { join, posix } from "node:path";
11
-
12
- export type AssetEntry = { hash: string; size: number; type: string };
13
- /** `/absolute/posix/path` -> entry. Serialised into `assets.json`. */
14
- export type AssetFiles = { [path: string]: AssetEntry };
15
- export type AssetManifest = {
16
- /** Fallback for requests that match no file, applied by the broker. */
17
- notFound: "none" | "single-page-application" | "404-page";
18
- /** `true` = every path to the sprout first; string[] = selective (leading `!` negates). */
19
- runSproutFirst: boolean | string[];
20
- files: AssetFiles;
21
- };
22
-
23
- const TYPES = new Map<string, string>([
24
- ["html", "text/html; charset=utf-8"],
25
- ["css", "text/css; charset=utf-8"],
26
- ["js", "text/javascript; charset=utf-8"],
27
- ["mjs", "text/javascript; charset=utf-8"],
28
- ["json", "application/json; charset=utf-8"],
29
- ["map", "application/json; charset=utf-8"],
30
- ["txt", "text/plain; charset=utf-8"],
31
- ["xml", "application/xml; charset=utf-8"],
32
- ["svg", "image/svg+xml"],
33
- ["png", "image/png"],
34
- ["jpg", "image/jpeg"],
35
- ["jpeg", "image/jpeg"],
36
- ["gif", "image/gif"],
37
- ["webp", "image/webp"],
38
- ["avif", "image/avif"],
39
- ["ico", "image/x-icon"],
40
- ["woff2", "font/woff2"],
41
- ["woff", "font/woff"],
42
- ["ttf", "font/ttf"],
43
- ["wasm", "application/wasm"],
44
- ["webmanifest", "application/manifest+json"],
45
- ]);
46
-
47
- export function contentType(name: string): string {
48
- const dot = name.lastIndexOf(".");
49
- return (dot >= 0 ? TYPES.get(name.slice(dot + 1).toLowerCase()) : undefined) ?? "application/octet-stream";
50
- }
51
-
52
- /**
53
- * Resolve a request path to a manifest key the way a static host does:
54
- * - an exact hit wins;
55
- * - a directory path (`/docs/`) tries `/docs/index.html`;
56
- * - an extensionless path (`/docs`) tries `/docs.html`, then `/docs/index.html`.
57
- * Returns the matched key, or `null`. This only picks which file to serve — no
58
- * canonical redirects, and the caller still owns not-found handling. Mirrors
59
- * Cloudflare's `html_handling: "auto-trailing-slash"` minus the 3xx responses.
60
- */
61
- export function resolveAssetKey(path: string, has: (key: string) => boolean): string | null {
62
- if (!path.startsWith("/")) path = `/${path}`;
63
- if (path.endsWith("/")) {
64
- const index = `${path}index.html`;
65
- return has(index) ? index : null;
66
- }
67
- if (has(path)) return path;
68
- const base = path.slice(path.lastIndexOf("/") + 1);
69
- if (!base.includes(".")) {
70
- if (has(`${path}.html`)) return `${path}.html`;
71
- if (has(`${path}/index.html`)) return `${path}/index.html`;
72
- }
73
- return null;
74
- }
75
-
76
- /** Walk `dir` recursively, returning `{ "/path": {hash,size,type} }`. */
77
- export function walkAssets(dir: string) {
78
- const out: AssetFiles = {};
79
- const recurse = (abs: string, rel: string): void => {
80
- for (const ent of readdirSync(abs, { withFileTypes: true })) {
81
- if (ent.name.startsWith(".")) continue;
82
- const childAbs = join(abs, ent.name);
83
- const childRel = posix.join(rel, ent.name);
84
- if (ent.isDirectory()) {
85
- recurse(childAbs, childRel);
86
- continue;
87
- }
88
- if (!ent.isFile()) continue;
89
- const body = readFileSync(childAbs);
90
- out[`/${childRel}`] = {
91
- hash: createHash("sha256").update(body).digest("hex"),
92
- size: body.byteLength,
93
- type: contentType(ent.name),
94
- };
95
- }
96
- };
97
- recurse(dir, "");
98
- return out;
99
- }
100
-
101
- /** Does `pathname` hit the sprout before assets, given a `runSproutFirst` spec? */
102
- export function isSproutFirst(spec: boolean | string[], pathname: string): boolean {
103
- if (!Array.isArray(spec)) return spec;
104
- let matched = false;
105
- for (const raw of spec) {
106
- const negate = raw.startsWith("!");
107
- const pattern = negate ? raw.slice(1) : raw;
108
- if (globMatch(pattern, pathname)) matched = !negate;
109
- }
110
- return matched;
111
- }
112
-
113
- /** `*` matches within a segment, `**` across segments. Anchored both ends. */
114
- function globMatch(pattern: string, path: string): boolean {
115
- const rx = pattern
116
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
117
- .replace(/\*\*/g, " ")
118
- .replace(/\*/g, "[^/]*")
119
- .replace(/ /g, ".*");
120
- return new RegExp(`^${rx}$`).test(path);
121
- }
1
+ // Re-exported from @sproutboat/assets (moved verbatim). This shim keeps
2
+ // every `./assets` importer and the `sproutboat/runtime/assets` export path
3
+ // working; the implementation lives in the shared package.
4
+ export * from "@sproutboat/assets";