@weft-js/start 0.1.1 → 0.1.3

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.
Files changed (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
package/src/mux.ts CHANGED
@@ -11,11 +11,19 @@ import { attach, type Attached, type Peer } from './socket'
11
11
  const LINES = 64
12
12
  const HIGH = 64 * 1024
13
13
 
14
- interface Physical { send(text: string): void; close(code: number, reason?: string): void; backlog(): number }
14
+ interface Physical {
15
+ send(text: string): void
16
+ close(code: number, reason?: string): void
17
+ backlog(): number
18
+ }
15
19
  type Open = { kind: 'c'; a: Promise<Attached> } | { kind: 'r'; c: Promise<Conn | undefined> }
16
20
 
17
21
  const parsed = (text: string): unknown => {
18
- try { return JSON.parse(text) } catch { return undefined }
22
+ try {
23
+ return JSON.parse(text)
24
+ } catch {
25
+ return undefined
26
+ }
19
27
  }
20
28
 
21
29
  const counts = new WeakMap<Hub, Map<string, number>>()
@@ -25,27 +33,154 @@ const byUser = (h: Hub) => {
25
33
  return m
26
34
  }
27
35
 
28
- export interface Muxed { message(text: string): void; close(): void }
36
+ interface Socket {
37
+ peer: Physical
38
+ request: Request
39
+ scope: Scope
40
+ h: Hub
41
+ lines: Map<string, Open>
42
+ }
43
+
44
+ function openLines(
45
+ { peer, request, scope, h, lines }: Socket,
46
+ end: (id: string, code: number, reason?: string) => void,
47
+ ) {
48
+ function openC(id: string) {
49
+ const p: Peer = {
50
+ send: (t) => {
51
+ if (lines.has(id)) peer.send(`f:${id}:${t}`)
52
+ },
53
+ close: (code, reason) => end(id, code, reason),
54
+ backlog: () => peer.backlog(),
55
+ }
56
+ lines.set(id, {
57
+ kind: 'c',
58
+ a: attach(p, request, scope, h, true).catch(() => {
59
+ end(id, 1011)
60
+ return { message() {}, close() {} } as unknown as Attached
61
+ }),
62
+ })
63
+ }
64
+
65
+ function openR(id: string, hello: string) {
66
+ const body = parsed(hello)
67
+ if (!validBody(body) || body.k)
68
+ return peer.send(
69
+ `x:${id}:4000:${JSON.stringify({ code: 'Q002', message: 'a realtime line opened with a body of the wrong shape' })}`,
70
+ )
71
+ const sink: Sink = {
72
+ write: (event, data) => {
73
+ if (lines.has(id)) peer.send(`r:${id}:${event}:${data}`)
74
+ },
75
+ ping: () => {
76
+ if (lines.has(id)) peer.send(`r:${id}:ping:`)
77
+ },
78
+ backlog: () => peer.backlog(),
79
+ close: () => end(id, 1000),
80
+ }
81
+ const c = h.connect(request, scope, body, sink, true).catch((e) => {
82
+ const out =
83
+ e instanceof StartError
84
+ ? { code: e.code, message: e.message }
85
+ : { message: 'Realtime failed.' }
86
+ end(id, 4000, JSON.stringify(out))
87
+ return undefined
88
+ })
89
+ lines.set(id, { kind: 'r', c })
90
+ }
91
+
92
+ return { openC, openR }
93
+ }
94
+
95
+ function messages(
96
+ { peer, request, scope, h, lines }: Socket,
97
+ {
98
+ allowed,
99
+ end,
100
+ shut,
101
+ openC,
102
+ openR,
103
+ }: {
104
+ allowed: () => boolean
105
+ end: (id: string, code: number, reason?: string) => void
106
+ shut: (id: string, line: Open) => void
107
+ openC: (id: string) => void
108
+ openR: (id: string, hello: string) => void
109
+ },
110
+ ) {
111
+ return (text: string) => {
112
+ if (text === 'p') return peer.send('p')
113
+ if (text.length > h.limits.body + 64) return peer.close(1009, 'too big')
114
+ if (!allowed()) {
115
+ scope.options.onDiagnostic?.({
116
+ code: 'R008',
117
+ severity: 'error',
118
+ message: `R008: one browser sent over ${h.limits.perSecond * 8} messages a second`,
119
+ })
120
+ return peer.close(1008, 'rate limit')
121
+ }
122
+ const at = text.indexOf(':', 2)
123
+ const id = text.slice(2, at),
124
+ rest = text.slice(at + 1)
125
+ const line = lines.get(id)
126
+ if (text[0] === 'f') {
127
+ if (line?.kind === 'c') void line.a.then((a) => a.message(rest))
128
+ else if (line) {
129
+ let body: Body | undefined
130
+ try {
131
+ body = JSON.parse(rest)
132
+ } catch {}
133
+ if (validBody(body))
134
+ void line.c
135
+ .then((c) => c && h.change(c, request, scope.options, body))
136
+ .catch(() => end(id, 1011))
137
+ }
138
+ } else if (text[0] === 'x') {
139
+ if (line) shut(id, line)
140
+ } else if (text[0] === 'o') {
141
+ if (line) return
142
+ if (lines.size >= LINES) return peer.send(`x:${id}:1008:too many lines`)
143
+ if (rest[0] === 'c') openC(id)
144
+ else if (rest[0] === 'r') openR(id, rest.slice(2))
145
+ }
146
+ }
147
+ }
148
+
149
+ export interface Muxed {
150
+ message(text: string): void
151
+ close(): void
152
+ }
29
153
 
30
- export async function muxed(peer: Physical, request: Request, scope: Scope, h: Hub = hub()): Promise<Muxed> {
154
+ export async function muxed(
155
+ peer: Physical,
156
+ request: Request,
157
+ scope: Scope,
158
+ h: Hub = hub(),
159
+ ): Promise<Muxed> {
31
160
  const ctx = await scope.ctx()
32
161
  const user = (ctx as { user?: { id?: string } }).user?.id
33
162
  const users = byUser(h)
34
163
  if (user !== undefined) {
35
164
  if ((users.get(user) ?? 0) >= h.limits.connections) {
36
165
  peer.close(1008, 'too many sockets')
37
- scope.options.onDiagnostic?.({ code: 'R009', severity: 'error', message: `R009: user \`${user}\` already has ${h.limits.connections} sockets` })
166
+ scope.options.onDiagnostic?.({
167
+ code: 'R009',
168
+ severity: 'error',
169
+ message: `R009: user \`${user}\` already has ${h.limits.connections} sockets`,
170
+ })
38
171
  return { message() {}, close() {} }
39
172
  }
40
173
  users.set(user, (users.get(user) ?? 0) + 1)
41
174
  }
42
175
  const lines = new Map<string, Open>()
176
+ const socket = { peer, request, scope, h, lines }
43
177
  const bucket = { tokens: h.limits.burst * 8, at: Date.now() }
44
178
  let closed = false
45
179
 
46
180
  /** Every tab has the per-socket bucket; this is what stops them adding up without end. */
47
181
  const allowed = () => {
48
- const now = Date.now(), rate = h.limits.perSecond * 8
182
+ const now = Date.now(),
183
+ rate = h.limits.perSecond * 8
49
184
  bucket.tokens = Math.min(h.limits.burst * 8, bucket.tokens + ((now - bucket.at) * rate) / 1000)
50
185
  bucket.at = now
51
186
  return bucket.tokens-- >= 1
@@ -55,81 +190,43 @@ export async function muxed(peer: Physical, request: Request, scope: Scope, h: H
55
190
  peer.send(`x:${id}:${code}:${reason}`)
56
191
  }
57
192
 
58
- function openC(id: string) {
59
- const p: Peer = { send: t => { if (lines.has(id)) peer.send(`f:${id}:${t}`) }, close: (code, reason) => end(id, code, reason), backlog: () => peer.backlog() }
60
- lines.set(id, { kind: 'c', a: attach(p, request, scope, h, true).catch(() => {
61
- end(id, 1011)
62
- return { message() {}, close() {} } as unknown as Attached
63
- }) })
64
- }
65
-
66
- function openR(id: string, hello: string) {
67
- const body = parsed(hello)
68
- if (!validBody(body) || body.k) return peer.send(`x:${id}:4000:${JSON.stringify({ code: 'Q002', message: 'a realtime line opened with a body of the wrong shape' })}`)
69
- const sink: Sink = {
70
- write: (event, data) => { if (lines.has(id)) peer.send(`r:${id}:${event}:${data}`) },
71
- ping: () => { if (lines.has(id)) peer.send(`r:${id}:ping:`) },
72
- backlog: () => peer.backlog(),
73
- close: () => end(id, 1000),
74
- }
75
- const c = h.connect(request, scope, body, sink, true).catch(e => {
76
- const out = e instanceof StartError ? { code: e.code, message: e.message } : { message: 'Realtime failed.' }
77
- end(id, 4000, JSON.stringify(out))
78
- return undefined
79
- })
80
- lines.set(id, { kind: 'r', c })
81
- }
193
+ const { openC, openR } = openLines(socket, end)
82
194
 
83
195
  function shut(id: string, line: Open) {
84
196
  lines.delete(id)
85
- if (line.kind === 'c') void line.a.then(a => a.close())
86
- else void line.c.then(c => c && h.close(c))
197
+ if (line.kind === 'c') void line.a.then((a) => a.close())
198
+ else void line.c.then((c) => c && h.close(c))
87
199
  }
88
200
 
89
201
  const drain = setInterval(() => {
90
202
  if (peer.backlog() >= HIGH) return
91
- for (const l of lines.values()) if (l.kind === 'r') void l.c.then(c => c && h.drain(c))
203
+ for (const l of lines.values()) if (l.kind === 'r') void l.c.then((c) => c && h.drain(c))
92
204
  }, 100)
93
205
  ;(drain as { unref?(): void }).unref?.()
94
206
 
95
- peer.send(`h:${await sessionOf(ctx)}`)
207
+ const release = () => {
208
+ clearInterval(drain)
209
+ if (user === undefined) return
210
+ const n = (users.get(user) ?? 1) - 1
211
+ if (n) users.set(user, n)
212
+ else users.delete(user)
213
+ }
214
+ try {
215
+ peer.send(`h:${await sessionOf(ctx)}`)
216
+ } catch (e) {
217
+ release() // the caller never gets a close() for a socket that failed to open
218
+ throw e
219
+ }
220
+ const message = messages(socket, { allowed, end, shut, openC, openR })
96
221
  return {
97
222
  message(text) {
98
- if (text === 'p') return peer.send('p')
99
- if (text.length > h.limits.body + 64) return peer.close(1009, 'too big')
100
- if (!allowed()) {
101
- scope.options.onDiagnostic?.({ code: 'R008', severity: 'error', message: `R008: one browser sent over ${h.limits.perSecond * 8} messages a second` })
102
- return peer.close(1008, 'rate limit')
103
- }
104
- const at = text.indexOf(':', 2)
105
- const id = text.slice(2, at), rest = text.slice(at + 1)
106
- const line = lines.get(id)
107
- if (text[0] === 'f') {
108
- if (line?.kind === 'c') void line.a.then(a => a.message(rest))
109
- else if (line) {
110
- let body: Body | undefined
111
- try { body = JSON.parse(rest) } catch {}
112
- if (validBody(body)) void line.c.then(c => c && h.change(c, request, scope.options, body)).catch(() => end(id, 1011))
113
- }
114
- } else if (text[0] === 'x') {
115
- if (line) shut(id, line)
116
- } else if (text[0] === 'o') {
117
- if (line) return
118
- if (lines.size >= LINES) return peer.send(`x:${id}:1008:too many lines`)
119
- if (rest[0] === 'c') openC(id)
120
- else if (rest[0] === 'r') openR(id, rest.slice(2))
121
- }
223
+ return message(text)
122
224
  },
123
225
  close() {
124
226
  if (closed) return
125
227
  closed = true
126
- clearInterval(drain)
127
228
  for (const [id, line] of Array.from(lines)) shut(id, line)
128
- if (user !== undefined) {
129
- const n = (users.get(user) ?? 1) - 1
130
- if (n) users.set(user, n)
131
- else users.delete(user)
132
- }
229
+ release()
133
230
  },
134
231
  }
135
232
  }
@@ -24,7 +24,11 @@ const family = (name: string | undefined) => (name ? `'${name}',Inter` : 'Inter'
24
24
  /** The title without the template the site wraps around it: the card shows the site's name on its own. */
25
25
  function heading(title: string, template: string | undefined) {
26
26
  const [before = '', after = ''] = template?.split('%s') ?? []
27
- return title.startsWith(before) && title.endsWith(after) && title.length > before.length + after.length ? title.slice(before.length, title.length - after.length) : title
27
+ return title.startsWith(before) &&
28
+ title.endsWith(after) &&
29
+ title.length > before.length + after.length
30
+ ? title.slice(before.length, title.length - after.length)
31
+ : title
28
32
  }
29
33
 
30
34
  /** Everything the card draws, worked out once: a card renders one time, to a string. */
@@ -36,8 +40,13 @@ function look({ title, description, site, path }: DefaultProps) {
36
40
  const host = site.url?.replace(/^https?:\/\//, '').replace(/\/+$/, '')
37
41
  const shown = heading(title, site.title)
38
42
  return {
39
- c, description, title: shown, name: site.name ?? '', size: cardTitleSize(shown),
40
- display: family(card.fonts?.display?.family), body: family(card.fonts?.body?.family),
43
+ c,
44
+ description,
45
+ title: shown,
46
+ name: site.name ?? '',
47
+ size: cardTitleSize(shown),
48
+ display: family(card.fonts?.display?.family),
49
+ body: family(card.fonts?.body?.family),
41
50
  address: host && host + (path && path !== '/' ? path.replace(/[?#].*/, '') : ''),
42
51
  }
43
52
  }
@@ -45,16 +54,38 @@ function look({ title, description, site, path }: DefaultProps) {
45
54
  export function DefaultCard(props: DefaultProps) {
46
55
  const { c, title, description, name, size, display, body, address } = untrack(() => look(props))
47
56
  return (
48
- <div style={`display:flex;flex-direction:column;width:100%;height:100%;padding:68px 76px 60px;background-color:${c.background};color:${c.text};font-family:${body}`}>
49
- <div style={`display:flex;align-items:center;font-family:${display};font-size:32px;font-weight:600;letter-spacing:-0.3px`}>
50
- <div style={`width:22px;height:22px;border-radius:5px;background-color:${c.accent};margin-right:16px`} />
57
+ <div
58
+ style={`display:flex;flex-direction:column;width:100%;height:100%;padding:68px 76px 60px;background-color:${c.background};color:${c.text};font-family:${body}`}
59
+ >
60
+ <div
61
+ style={`display:flex;align-items:center;font-family:${display};font-size:32px;font-weight:600;letter-spacing:-0.3px`}
62
+ >
63
+ <div
64
+ style={`width:22px;height:22px;border-radius:5px;background-color:${c.accent};margin-right:16px`}
65
+ />
51
66
  <span>{name}</span>
52
67
  </div>
53
68
  <div style="display:flex;flex-direction:column;justify-content:center;flex:1;min-height:0;padding:24px 0">
54
- <div style={`display:block;font-family:${display};font-size:${size}px;font-weight:700;line-height:1.08;letter-spacing:-1.5px;overflow-wrap:anywhere;line-clamp:3`}>{title}</div>
55
- {description ? <div style={`display:block;margin-top:26px;font-size:30px;line-height:1.38;color:${c.subtle};line-clamp:3`}>{description}</div> : null}
69
+ <div
70
+ style={`display:block;font-family:${display};font-size:${size}px;font-weight:700;line-height:1.08;letter-spacing:-1.5px;overflow-wrap:anywhere;line-clamp:3`}
71
+ >
72
+ {title}
73
+ </div>
74
+ {description ? (
75
+ <div
76
+ style={`display:block;margin-top:26px;font-size:30px;line-height:1.38;color:${c.subtle};line-clamp:3`}
77
+ >
78
+ {description}
79
+ </div>
80
+ ) : null}
56
81
  </div>
57
- {address ? <div style={`display:flex;align-items:center;font-size:24px;color:${c.subtle};border-top:1px solid ${c.subtle}44;padding-top:22px`}>{address}</div> : null}
82
+ {address ? (
83
+ <div
84
+ style={`display:flex;align-items:center;font-size:24px;color:${c.subtle};border-top:1px solid ${c.subtle}44;padding-top:22px`}
85
+ >
86
+ {address}
87
+ </div>
88
+ ) : null}
58
89
  </div>
59
90
  )
60
91
  }
package/src/og/fonts.mjs CHANGED
@@ -26,9 +26,15 @@ export async function woffToSfnt(woff) {
26
26
  const tables = []
27
27
  for (let i = 0; i < numTables; i++) {
28
28
  const at = 44 + i * 20
29
- const offset = v.getUint32(at + 4), compLength = v.getUint32(at + 8), origLength = v.getUint32(at + 12)
29
+ const offset = v.getUint32(at + 4),
30
+ compLength = v.getUint32(at + 8),
31
+ origLength = v.getUint32(at + 12)
30
32
  const raw = woff.subarray(offset, offset + compLength)
31
- tables.push({ tag: v.getUint32(at), checksum: v.getUint32(at + 16), data: compLength < origLength ? await inflate(raw) : raw })
33
+ tables.push({
34
+ tag: v.getUint32(at),
35
+ checksum: v.getUint32(at + 16),
36
+ data: compLength < origLength ? await inflate(raw) : raw,
37
+ })
32
38
  }
33
39
  let size = 12 + 16 * numTables
34
40
  for (const t of tables) size += (t.data.length + 3) & ~3
@@ -55,10 +61,12 @@ export async function woffToSfnt(woff) {
55
61
 
56
62
  /** `{ name, data, weight?, style? }[]` in any format → the same with bytes every Takumi build reads. */
57
63
  export async function normalizeFonts(fonts) {
58
- return Promise.all(fonts.map(async f => {
59
- const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data)
60
- const format = fontFormat(data)
61
- if (!format) throw new Error(`weft/og: font "${f.name}" isn't TTF, OTF, WOFF or WOFF2`)
62
- return { ...f, data: format === 'woff' ? await woffToSfnt(data) : data }
63
- }))
64
+ return Promise.all(
65
+ fonts.map(async (f) => {
66
+ const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data)
67
+ const format = fontFormat(data)
68
+ if (!format) throw new Error(`weft/og: font "${f.name}" isn't TTF, OTF, WOFF or WOFF2`)
69
+ return { ...f, data: format === 'woff' ? await woffToSfnt(data) : data }
70
+ }),
71
+ )
64
72
  }
package/src/og/images.mjs CHANGED
@@ -8,7 +8,10 @@ export function imageCache(maxBytes = 32 << 20) {
8
8
  return {
9
9
  get(src) {
10
10
  const hit = map.get(src)
11
- if (hit) { map.delete(src); map.set(src, hit) }
11
+ if (hit) {
12
+ map.delete(src)
13
+ map.set(src, hit)
14
+ }
12
15
  return hit
13
16
  },
14
17
  set(src, data) {
@@ -29,26 +32,44 @@ export function imageCache(maxBytes = 32 << 20) {
29
32
  * renders data from `load`, so this is the SSRF guard); the default allows any https URL.
30
33
  * Returns the images Takumi needs, and the srcs that failed.
31
34
  */
32
- export async function loadImages(srcs, { cache, timeoutMs = 1500, maxBytes = 4 << 20, allow = u => u.protocol === 'https:', fetch = globalThis.fetch } = {}) {
33
- const images = [], failed = []
34
- await Promise.all([...new Set(srcs)].map(async src => {
35
- if (src.startsWith('data:')) return // Takumi decodes data URIs itself
36
- const hit = cache?.get(src)
37
- if (hit) return void images.push({ src, data: hit })
38
- try {
39
- const url = new URL(src)
40
- if (!allow(url)) throw new Error('not allowed')
41
- const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' })
42
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
43
- if (!res.headers.get('content-type')?.startsWith('image/')) throw new Error('not an image')
44
- if (Number(res.headers.get('content-length') ?? 0) > maxBytes) throw new Error('too large')
45
- const data = new Uint8Array(await res.arrayBuffer())
46
- if (data.length > maxBytes) throw new Error('too large')
47
- cache?.set(src, data)
48
- images.push({ src, data })
49
- } catch (e) {
50
- failed.push({ src, reason: e?.name === 'TimeoutError' ? `timed out after ${timeoutMs} ms` : String(e?.message ?? e) })
51
- }
52
- }))
35
+ export async function loadImages(
36
+ srcs,
37
+ {
38
+ cache,
39
+ timeoutMs = 1500,
40
+ maxBytes = 4 << 20,
41
+ allow = (u) => u.protocol === 'https:',
42
+ fetch = globalThis.fetch,
43
+ } = {},
44
+ ) {
45
+ const images = [],
46
+ failed = []
47
+ await Promise.all(
48
+ [...new Set(srcs)].map(async (src) => {
49
+ if (src.startsWith('data:')) return // Takumi decodes data URIs itself
50
+ const hit = cache?.get(src)
51
+ if (hit) return void images.push({ src, data: hit })
52
+ try {
53
+ const url = new URL(src)
54
+ if (!allow(url)) throw new Error('not allowed')
55
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' })
56
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
57
+ if (!res.headers.get('content-type')?.startsWith('image/')) throw new Error('not an image')
58
+ if (Number(res.headers.get('content-length') ?? 0) > maxBytes) throw new Error('too large')
59
+ const data = new Uint8Array(await res.arrayBuffer())
60
+ if (data.length > maxBytes) throw new Error('too large')
61
+ cache?.set(src, data)
62
+ images.push({ src, data })
63
+ } catch (e) {
64
+ failed.push({
65
+ src,
66
+ reason:
67
+ e?.name === 'TimeoutError'
68
+ ? `timed out after ${timeoutMs} ms`
69
+ : String(e?.message ?? e),
70
+ })
71
+ }
72
+ }),
73
+ )
53
74
  return { images, failed }
54
75
  }