@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
@@ -0,0 +1,86 @@
1
+ import type { Body } from '../hub'
2
+ import { HANDOVER, openLine, type Line } from '../link'
3
+ import { FnError } from './api'
4
+
5
+ /** How the client reaches `/_weft/rt`: SSE, a shared socket, or a fake under test. */
6
+ export interface Link {
7
+ /** Resolves when the connection ends. */
8
+ open(
9
+ body: Body,
10
+ frame: (event: string, data: unknown) => void,
11
+ signal: AbortSignal,
12
+ alive: () => void,
13
+ ): Promise<void>
14
+ change(body: Body): Promise<void>
15
+ }
16
+
17
+ export function transports(handover: () => void) {
18
+ const post = (body: Body, signal?: AbortSignal) =>
19
+ fetch('/_weft/rt', {
20
+ method: 'POST',
21
+ headers: { 'content-type': 'application/json', accept: 'text/event-stream', 'x-weft': '1' },
22
+ body: JSON.stringify(body),
23
+ ...(signal && { signal }),
24
+ })
25
+
26
+ const http: Link = {
27
+ async open(body, heard, signal, alive) {
28
+ const res = await post(body, signal)
29
+ if (!res.ok) {
30
+ const { e } = await res.json().catch(() => ({ e: {} }))
31
+ throw new FnError(e, res.status)
32
+ }
33
+ const reader = res.body!.getReader()
34
+ const decoder = new TextDecoder()
35
+ let buf = ''
36
+ for (;;) {
37
+ const { done, value } = await reader.read()
38
+ if (done) return
39
+ alive()
40
+ buf += decoder.decode(value, { stream: true })
41
+ for (let i; (i = buf.indexOf('\n\n')) >= 0; buf = buf.slice(i + 2)) {
42
+ if (buf[0] === ':') continue
43
+ const line = buf.indexOf('\n')
44
+ heard(buf.slice(7, line), JSON.parse(buf.slice(line + 7, i)))
45
+ }
46
+ }
47
+ },
48
+ async change(body) {
49
+ const res = await post(body)
50
+ if (!res.ok) throw new FnError({}, res.status)
51
+ },
52
+ }
53
+
54
+ /** A line on the browser's one socket (link.ts): the same events, and `/_weft/rt`'s SSE is for a host with no sockets. */
55
+ let line: Line | undefined
56
+ const socket: Link = {
57
+ open(body, heard, signal, alive) {
58
+ return new Promise<void>((done, fail) => {
59
+ const l = (line = openLine(
60
+ 'r',
61
+ JSON.stringify(body),
62
+ (text) => {
63
+ alive()
64
+ const at = text.indexOf(':'),
65
+ event = text.slice(0, at)
66
+ if (event !== 'ping') heard(event, JSON.parse(text.slice(at + 1)))
67
+ },
68
+ (code, reason) => {
69
+ if (line === l) line = undefined
70
+ if (code === HANDOVER) handover()
71
+ if (code === 4000 && reason) fail(new FnError(JSON.parse(reason), 429))
72
+ else done()
73
+ },
74
+ ))
75
+ signal.addEventListener('abort', () => l.close())
76
+ })
77
+ },
78
+ change(body) {
79
+ if (!line) return Promise.reject(new FnError({}, 503))
80
+ line.send(JSON.stringify(body))
81
+ return Promise.resolve()
82
+ },
83
+ }
84
+
85
+ return { http, socket }
86
+ }
@@ -0,0 +1,52 @@
1
+ import type { Derived } from '@weft-js/core'
2
+ import type { Context, LiveOptions, LiveStatus, Skip, StandardSchemaV1 } from '../data'
3
+ import type { Tracked } from '../hub'
4
+
5
+ export type In<S> = S extends StandardSchemaV1<infer I, unknown> ? I : void
6
+ type Out<S> = S extends StandardSchemaV1<unknown, infer O> ? O : undefined
7
+
8
+ /** Where a stream stands; `done` is the end of a generator. */
9
+ export type StreamStatus = LiveStatus | 'done'
10
+
11
+ /** `stream()`: the input, the message schema and the generator. */
12
+ export interface StreamDef<S, M, A = undefined> {
13
+ input?: S
14
+ /** Checked in dev and tests (C002), and the contract's `message`. */
15
+ message?: StandardSchemaV1<unknown, M>
16
+ auth?: A
17
+ /** `lastEventId`: the last `tracked` id the client saw, on a resume. Stop when `signal` aborts. */
18
+ run: (args: {
19
+ input: Out<S>
20
+ ctx: Context
21
+ signal: AbortSignal
22
+ lastEventId?: string
23
+ }) => AsyncIterable<M | Tracked<M>>
24
+ $id?: string
25
+ }
26
+
27
+ /** `.live()` on a stream: what the value starts as, and how messages fold into it. */
28
+ export interface StreamOptions<M, T> extends LiveOptions {
29
+ /** Keep every message: `fold(acc, message)`. Its state lives in the cache, so a remount finds it. */
30
+ fold?: (acc: T, message: M) => T
31
+ initial?: T
32
+ /** Messages the server holds for a slow client. A fold defaults to 256; without one, the latest wins. */
33
+ buffer?: number
34
+ }
35
+
36
+ /** A stream as a page reads it. */
37
+ export interface Stream<T> {
38
+ /** The latest message, or the fold. */
39
+ readonly value: T
40
+ readonly status: Derived<StreamStatus>
41
+ readonly error: unknown
42
+ }
43
+
44
+ /** What `stream()` returns. */
45
+ export interface StreamFn<I, M> {
46
+ (input: Arg<I>): Stream<M | undefined>
47
+ <T>(input: Arg<I>, options: StreamOptions<M, T> & { initial: T }): Stream<T>
48
+ (input: Arg<I>, options: StreamOptions<M, M>): Stream<M | undefined>
49
+ readonly kind: 'stream'
50
+ readonly $id: string
51
+ }
52
+ type Arg<I> = I | (() => I | Skip)
package/src/live-fake.ts CHANGED
@@ -15,7 +15,17 @@ export async function change(body: Body) {
15
15
  }
16
16
 
17
17
  /** A connection to the real hub in this process. */
18
- export async function open(body: Body, frame: (event: string, data: unknown) => void, signal: AbortSignal, up: () => void, subs: Map<string, { fn: Fn; input: unknown }>, closing: (close: () => unknown) => void) {
18
+ export async function open(
19
+ body: Body,
20
+ frame: (event: string, data: unknown) => void,
21
+ signal: AbortSignal,
22
+ hooks: {
23
+ up: () => void
24
+ subs: Map<string, { fn: Fn; input: unknown }>
25
+ closing: (close: () => unknown) => void
26
+ },
27
+ ) {
28
+ const { up, subs, closing } = hooks
19
29
  const n = net()!
20
30
  const [{ hub, resetHub }, exec] = await Promise.all([import('./hub'), loadExec()])
21
31
  if (!n.online()) throw Object.assign(new Error('offline'), { name: 'OfflineError' })
@@ -25,15 +35,20 @@ export async function open(body: Body, frame: (event: string, data: unknown) =>
25
35
  const request = new Request('http://localhost/_weft/rt', { method: 'POST' })
26
36
  return new Promise<void>((done, fail) => {
27
37
  let conn: Parameters<typeof h.close>[0] | undefined
28
- const sink = { write: (e: string, d: string) => frame(e, JSON.parse(d)), backlog: () => 0, close: () => done() }
38
+ const sink = {
39
+ write: (e: string, d: string) => frame(e, JSON.parse(d)),
40
+ backlog: () => 0,
41
+ close: () => done(),
42
+ }
29
43
  n.rt = {
30
44
  push: (id, msg) => h.push(id, msg),
31
- poke: tags => h.poke(tags),
45
+ poke: (tags) => h.poke(tags),
32
46
  drop: () => conn && h.close(conn),
33
- subscriptions: () => [...subs.values()].map(s => ({ kind: s.fn.kind, id: s.fn.$id, input: s.input })),
47
+ subscriptions: () =>
48
+ [...subs.values()].map((s) => ({ kind: s.fn.kind, id: s.fn.$id, input: s.input })),
34
49
  }
35
50
  signal.addEventListener('abort', () => (conn ? h.close(conn) : done()))
36
- h.connect(request, fakeScope(exec, n, request), body, sink).then(c => {
51
+ h.connect(request, fakeScope(exec, n, request), body, sink).then((c) => {
37
52
  conn = c
38
53
  if (signal.aborted) h.close(c)
39
54
  up()
@@ -4,14 +4,21 @@
4
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs'
5
5
  import { join } from 'node:path'
6
6
 
7
- const LISTENER = 'run `weft db listen` on any always-on host (a VPS, Fly, Railway), publishing into a shared bus, and set `dbChanges(db, tables, { listener: \'external\' })`'
8
- const SERVER = 'deploy to a server preset (`node_server`, `bun`, `deno_server`, or a container on Fly, Railway or Render)'
7
+ const LISTENER =
8
+ "run `weft db listen` on any always-on host (a VPS, Fly, Railway), publishing into a shared bus, and set `dbChanges(db, tables, { listener: 'external' })`"
9
+ const SERVER =
10
+ 'deploy to a server preset (`node_server`, `bun`, `deno_server`, or a container on Fly, Railway or Render)'
9
11
  const POLLED = 'or poll the query yourself by reloading: `.live()` still updates on mutations'
10
12
 
11
13
  /** Presets that give each request its own short-lived process. */
12
14
  const FUNCTIONS = [
13
- [/^vercel/, 'Vercel Functions'], [/^netlify/, 'Netlify Functions'], [/^aws[-_]lambda/, 'AWS Lambda'], [/^azure/, 'Azure Functions'],
14
- [/^firebase/, 'Firebase Functions'], [/^cloudflare/, 'Cloudflare Workers'], [/^deno[-_]deploy/, 'Deno Deploy'],
15
+ [/^vercel/, 'Vercel Functions'],
16
+ [/^netlify/, 'Netlify Functions'],
17
+ [/^aws[-_]lambda/, 'AWS Lambda'],
18
+ [/^azure/, 'Azure Functions'],
19
+ [/^firebase/, 'Firebase Functions'],
20
+ [/^cloudflare/, 'Cloudflare Workers'],
21
+ [/^deno[-_]deploy/, 'Deno Deploy'],
15
22
  ]
16
23
 
17
24
  /**
@@ -20,35 +27,41 @@ const FUNCTIONS = [
20
27
  */
21
28
  export function liveTarget(preset = 'node') {
22
29
  const hit = FUNCTIONS.find(([re]) => re.test(preset))
23
- return hit ? { preset, held: false, host: hit[1] } : { preset, held: true, host: 'a long-running server' }
30
+ return hit
31
+ ? { preset, held: false, host: hit[1] }
32
+ : { preset, held: true, host: 'a long-running server' }
24
33
  }
25
34
 
26
35
  /** Whether a deploy can hold a browser's socket: a long-running server, a Durable Object, or Deno's upgraded request. */
27
- export const holdsSockets = (preset = 'node') => liveTarget(preset).held || /^(cloudflare[-_]durable|deno[-_]deploy)/.test(preset)
36
+ export const holdsSockets = (preset = 'node') =>
37
+ liveTarget(preset).held || /^(cloudflare[-_]durable|deno[-_]deploy)/.test(preset)
28
38
 
29
39
  const SKIP = new Set(['node_modules', '.weft', '.output', 'dist'])
30
40
 
31
41
  /** `live: { required: true }` on a query, and `.live(input, { required: true })` in a component, with where. */
32
42
  export function scanRequired(root) {
33
43
  const out = []
34
- const walk = dir => {
44
+ const walk = (dir) => {
35
45
  for (const d of readdirSync(dir, { withFileTypes: true })) {
36
46
  const f = join(dir, d.name)
37
- if (d.isDirectory()) { if (!SKIP.has(d.name)) walk(f) } else if (/\.[cm]?[jt]sx?$/.test(d.name) && !/\.test\./.test(d.name)) read(f)
47
+ if (d.isDirectory()) {
48
+ if (!SKIP.has(d.name)) walk(f)
49
+ } else if (/\.[cm]?[jt]sx?$/.test(d.name) && !/\.test\./.test(d.name)) read(f)
38
50
  }
39
51
  }
40
- const read = file => {
52
+ const read = (file) => {
41
53
  const text = readFileSync(file, 'utf8')
42
54
  if (!/required:\s*true/.test(text)) return
43
55
  for (const m of text.matchAll(/\blive:\s*\{[^}]*required:\s*true/g)) {
44
56
  const name = [...text.slice(0, m.index).matchAll(/\b(\w+)\s*=\s*query\s*[(<]/g)].at(-1)?.[1]
45
57
  if (name) out.push({ name, file: file.slice(root.length + 1) })
46
58
  }
47
- for (const m of text.matchAll(/\b(\w+)\.live\(([^;\n]*?)\brequired:\s*true/g)) out.push({ name: m[1], file: file.slice(root.length + 1) })
59
+ for (const m of text.matchAll(/\b(\w+)\.live\(([^;\n]*?)\brequired:\s*true/g))
60
+ out.push({ name: m[1], file: file.slice(root.length + 1) })
48
61
  }
49
62
  const src = join(root, 'src')
50
63
  if (existsSync(src)) walk(src)
51
- return [...new Map(out.map(r => [r.name, r])).values()]
64
+ return [...new Map(out.map((r) => [r.name, r])).values()]
52
65
  }
53
66
 
54
67
  /**
@@ -57,14 +70,20 @@ export function scanRequired(root) {
57
70
  */
58
71
  export function livePlan({ preset, source, required = [] }) {
59
72
  const target = liveTarget(preset)
60
- const names = required.map(r => `\`${r.name}\``).join(', ')
73
+ const names = required.map((r) => `\`${r.name}\``).join(', ')
61
74
  const diagnostics = []
62
75
  const options = target.held ? [] : [LISTENER, SERVER]
63
76
  let mode, says
64
77
  if (!source) {
65
78
  mode = 'app'
66
- says = 'Mutations update `.live()` queries. Writes from outside the app do not: nothing listens to the database.'
67
- if (required.length) diagnostics.push({ code: 'R013', severity: 'error', why: `${names} require${required.length > 1 ? '' : 's'} a live feed, and \`src/realtime.server.ts\` sets no \`changes\`. Add \`changes: dbChanges(db, [tables])\`, or drop \`required\`.` })
79
+ says =
80
+ 'Mutations update `.live()` queries. Writes from outside the app do not: nothing listens to the database.'
81
+ if (required.length)
82
+ diagnostics.push({
83
+ code: 'R013',
84
+ severity: 'error',
85
+ why: `${names} require${required.length > 1 ? '' : 's'} a live feed, and \`src/realtime.server.ts\` sets no \`changes\`. Add \`changes: dbChanges(db, [tables])\`, or drop \`required\`.`,
86
+ })
68
87
  } else if (source.external) {
69
88
  mode = 'external'
70
89
  says = `A separate listener reports writes to ${source.tables.join(', ')} through the bus. It is yours to keep running; \`.live()\` shows \`reconnecting\` when it goes quiet.`
@@ -74,40 +93,86 @@ export function livePlan({ preset, source, required = [] }) {
74
93
  } else {
75
94
  mode = 'off'
76
95
  says = `${target.host} ends a process with its request, so nothing can hold a connection to ${source.name}: mutations update \`.live()\`, and writes from outside the app do not. \`.live()\` shows \`degraded\`.`
77
- if (required.length) diagnostics.push({ code: 'R013', severity: 'error', why: `${names} require${required.length > 1 ? '' : 's'} a live feed, and \`${target.preset}\` can't hold one: ${target.host} ends a process with its request. Either ${LISTENER}, or ${SERVER}, or drop \`required\`.` })
78
- else diagnostics.push({ code: 'R014', severity: 'warning', why: `\`changes\` needs a process that stays up, and on \`${target.preset}\` none does: writes from outside the app won't reach \`.live()\`. Either ${LISTENER}, or ${SERVER}, ${POLLED}.` })
96
+ if (required.length)
97
+ diagnostics.push({
98
+ code: 'R013',
99
+ severity: 'error',
100
+ why: `${names} require${required.length > 1 ? '' : 's'} a live feed, and \`${target.preset}\` can't hold one: ${target.host} ends a process with its request. Either ${LISTENER}, or ${SERVER}, or drop \`required\`.`,
101
+ })
102
+ else
103
+ diagnostics.push({
104
+ code: 'R014',
105
+ severity: 'warning',
106
+ why: `\`changes\` needs a process that stays up, and on \`${target.preset}\` none does: writes from outside the app won't reach \`.live()\`. Either ${LISTENER}, or ${SERVER}, ${POLLED}.`,
107
+ })
108
+ }
109
+ return {
110
+ preset: target.preset,
111
+ held: target.held,
112
+ host: target.host,
113
+ mode,
114
+ says,
115
+ required: required.map((r) => r.name),
116
+ options,
117
+ diagnostics,
79
118
  }
80
- return { preset: target.preset, held: target.held, host: target.host, mode, says, required: required.map(r => r.name), options, diagnostics }
81
119
  }
82
120
 
83
121
  // ---- `verify` ---------------------------------------------------------------------------------------------------------
84
122
 
85
123
  const PRODUCTION = /(^|[.\-_])(prod|production|live|www|main|master)([.\-_]|$)/i
86
- const NOT_PRODUCTION = /(^|[.\-_])(staging|stage|stg|dev|develop|development|test|testing|qa|sandbox|preview|uat|demo)([.\-_]|$)|^localhost$|^127\.|^\[?::1\]?$/i
87
-
88
- const hostOf = url => { try { return new URL(url).hostname } catch { return url } }
89
- const dbNameOf = url => { try { return new URL(url).pathname.slice(1) } catch { return '' } }
124
+ const NOT_PRODUCTION =
125
+ /(^|[.\-_])(staging|stage|stg|dev|develop|development|test|testing|qa|sandbox|preview|uat|demo)([.\-_]|$)|^localhost$|^127\.|^\[?::1\]?$/i
126
+
127
+ const hostOf = (url) => {
128
+ try {
129
+ return new URL(url).hostname
130
+ } catch {
131
+ return url
132
+ }
133
+ }
134
+ const dbNameOf = (url) => {
135
+ try {
136
+ return new URL(url).pathname.slice(1)
137
+ } catch {
138
+ return ''
139
+ }
140
+ }
90
141
 
91
- const HOWTO = 'Set `WEFT_STAGING_URL` to a running staging app, `STAGING_DATABASE_URL` to its database, or both for the whole chain.'
142
+ const HOWTO =
143
+ 'Set `WEFT_STAGING_URL` to a running staging app, `STAGING_DATABASE_URL` to its database, or both for the whole chain.'
92
144
 
93
145
  /**
94
146
  * Where a `verify` points, from the query's declaration, `live.targets` in weft.config.ts and the environment.
95
147
  * @returns {{ name: string, url?: string, db?: string, marked: boolean } | { missing: string }}
96
148
  */
97
149
  export function resolveTarget(verify, { targets = {}, env = process.env } = {}) {
98
- if (typeof verify === 'object' && verify.target !== undefined) return resolveTarget(verify.target, { targets, env })
150
+ if (typeof verify === 'object' && verify.target !== undefined)
151
+ return resolveTarget(verify.target, { targets, env })
99
152
  if (verify === true) {
100
- const url = env.WEFT_STAGING_URL, db = env.STAGING_DATABASE_URL
101
- return url || db ? { name: 'staging', url, db, marked: false } : { missing: `\`verify: true\` found neither \`WEFT_STAGING_URL\` nor \`STAGING_DATABASE_URL\`. ${HOWTO}` }
153
+ const url = env.WEFT_STAGING_URL,
154
+ db = env.STAGING_DATABASE_URL
155
+ return url || db
156
+ ? { name: 'staging', url, db, marked: false }
157
+ : {
158
+ missing: `\`verify: true\` found neither \`WEFT_STAGING_URL\` nor \`STAGING_DATABASE_URL\`. ${HOWTO}`,
159
+ }
102
160
  }
103
161
  if (typeof verify === 'string') {
104
162
  const t = targets[verify]
105
- if (!t) return { missing: `\`verify: '${verify}'\` names no target. Add it to weft.config.ts: \`live: { targets: { ${verify}: { url: 'https://…', db: 'STAGING_DATABASE_URL', production: false } } }\`` }
163
+ if (!t)
164
+ return {
165
+ missing: `\`verify: '${verify}'\` names no target. Add it to weft.config.ts: \`live: { targets: { ${verify}: { url: 'https://…', db: 'STAGING_DATABASE_URL', production: false } } }\``,
166
+ }
106
167
  const db = t.db && (t.db.includes('://') ? t.db : env[t.db])
107
- if (t.db && !db) return { missing: `target \`${verify}\` reads its database from \`${t.db}\`, which isn't set on this machine.` }
168
+ if (t.db && !db)
169
+ return {
170
+ missing: `target \`${verify}\` reads its database from \`${t.db}\`, which isn't set on this machine.`,
171
+ }
108
172
  return { name: verify, url: t.url, db, marked: t.production === false }
109
173
  }
110
- if (!verify.url && !verify.db) return { missing: `\`verify\` got an empty target (is its env var set?). ${HOWTO}` }
174
+ if (!verify.url && !verify.db)
175
+ return { missing: `\`verify\` got an empty target (is its env var set?). ${HOWTO}` }
111
176
  return { name: 'inline', url: verify.url, db: verify.db, marked: false }
112
177
  }
113
178
 
@@ -115,12 +180,18 @@ export function resolveTarget(verify, { targets = {}, env = process.env } = {})
115
180
  export function refusal(target, { targets = {} } = {}) {
116
181
  const hosts = [target.url && hostOf(target.url), target.db && hostOf(target.db)].filter(Boolean)
117
182
  const names = [target.name, ...hosts, ...(target.db ? [dbNameOf(target.db)] : [])]
118
- const said = names.find(n => PRODUCTION.test(n))
183
+ const said = names.find((n) => PRODUCTION.test(n))
119
184
  if (said) return `\`${said}\` looks like production: \`verify\` writes rows, and won't go near it`
120
- if (target.name !== 'inline' && target.name !== 'staging' && targets[target.name]?.production !== false) return `target \`${target.name}\` isn't marked: add \`production: false\` to it in weft.config.ts`
185
+ if (
186
+ target.name !== 'inline' &&
187
+ target.name !== 'staging' &&
188
+ targets[target.name]?.production !== false
189
+ )
190
+ return `target \`${target.name}\` isn't marked: add \`production: false\` to it in weft.config.ts`
121
191
  if (target.marked) return
122
- const unsure = hosts.find(h => !NOT_PRODUCTION.test(h))
123
- if (unsure) return `nothing says \`${unsure}\` isn't production. Name it in weft.config.ts: \`live: { targets: { staging: { db: 'STAGING_DATABASE_URL', production: false } } }\`, and use \`verify: 'staging'\``
192
+ const unsure = hosts.find((h) => !NOT_PRODUCTION.test(h))
193
+ if (unsure)
194
+ return `nothing says \`${unsure}\` isn't production. Name it in weft.config.ts: \`live: { targets: { staging: { db: 'STAGING_DATABASE_URL', production: false } } }\`, and use \`verify: 'staging'\``
124
195
  }
125
196
 
126
197
  const PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`/:@]+:[^\s'"`/@]+@/i
@@ -128,20 +199,24 @@ const PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`/:@]+:[^\s'"`/@]+@/i
128
199
  /** R020: a connection string with a password, written into source. An env var's value never reaches this. */
129
200
  export function literalSecrets(root) {
130
201
  const out = []
131
- const look = file => {
202
+ const look = (file) => {
132
203
  const text = readFileSync(file, 'utf8')
133
204
  if (!/\b(verify|targets)\b/.test(text)) return
134
205
  text.split('\n').forEach((line, i) => {
135
- if (PASSWORD.test(line) && /\b(db|url|verify)\b/.test(line)) out.push({ file: file.slice(root.length + 1), line: i + 1 })
206
+ if (PASSWORD.test(line) && /\b(db|url|verify)\b/.test(line))
207
+ out.push({ file: file.slice(root.length + 1), line: i + 1 })
136
208
  })
137
209
  }
138
- const walk = dir => {
210
+ const walk = (dir) => {
139
211
  for (const d of readdirSync(dir, { withFileTypes: true })) {
140
212
  const f = join(dir, d.name)
141
- if (d.isDirectory()) { if (!SKIP.has(d.name)) walk(f) } else if (/\.[cm]?[jt]sx?$/.test(d.name)) look(f)
213
+ if (d.isDirectory()) {
214
+ if (!SKIP.has(d.name)) walk(f)
215
+ } else if (/\.[cm]?[jt]sx?$/.test(d.name)) look(f)
142
216
  }
143
217
  }
144
218
  if (existsSync(join(root, 'src'))) walk(join(root, 'src'))
145
- for (const f of ['weft.config.ts', 'weft.config.js', 'weft.config.mjs']) if (existsSync(join(root, f))) look(join(root, f))
219
+ for (const f of ['weft.config.ts', 'weft.config.js', 'weft.config.mjs'])
220
+ if (existsSync(join(root, f))) look(join(root, f))
146
221
  return out
147
222
  }