@sproutboat/assets 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @sproutboat/assets
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c45098e: Adopt broker, JSON utilities, assets, wrapper, source validation, both transports and the prelude from sproutboat-cli (moved verbatim). First publishable versions; consumed via `file:` links until the npm publish flow lands.
package/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # `@sproutboat/assets`
2
+
3
+ Single source of truth for static assets: manifests, key resolution, and
4
+ the sprout-first routing rules shared by the edge asset server and the CLI.
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@sproutboat/assets",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "scripts": {
9
+ "test": "bun test ./"
10
+ }
11
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,121 @@
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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // @sproutboat/assets: static asset manifests, key resolution, sprout-first
2
+ // routing helpers shared by the edge asset server and the CLI.
3
+ //
4
+ // Moved verbatim from sproutboat-cli/src/assets.ts.
5
+ export * from "./assets";