@weft-js/start 0.1.1 → 0.1.2
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/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +38 -8
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +90 -166
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +112 -37
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -178
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -971
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- 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(
|
|
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 = {
|
|
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: () =>
|
|
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()
|
package/src/live-target.mjs
CHANGED
|
@@ -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 =
|
|
8
|
-
|
|
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'],
|
|
14
|
-
[/^
|
|
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
|
|
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') =>
|
|
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()) {
|
|
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))
|
|
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 =
|
|
67
|
-
|
|
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)
|
|
78
|
-
|
|
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 =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
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 =
|
|
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)
|
|
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,
|
|
101
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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 (
|
|
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)
|
|
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))
|
|
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()) {
|
|
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'])
|
|
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
|
}
|