@weft-js/start 0.1.0 → 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 +51 -10
- 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 +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- 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 +97 -168
- 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 +113 -35
- 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 -174
- 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 -964
- 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,32 +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
|
|
|
35
|
+
/** Whether a deploy can hold a browser's socket: a long-running server, a Durable Object, or Deno's upgraded request. */
|
|
36
|
+
export const holdsSockets = (preset = 'node') =>
|
|
37
|
+
liveTarget(preset).held || /^(cloudflare[-_]durable|deno[-_]deploy)/.test(preset)
|
|
38
|
+
|
|
26
39
|
const SKIP = new Set(['node_modules', '.weft', '.output', 'dist'])
|
|
27
40
|
|
|
28
41
|
/** `live: { required: true }` on a query, and `.live(input, { required: true })` in a component, with where. */
|
|
29
42
|
export function scanRequired(root) {
|
|
30
43
|
const out = []
|
|
31
|
-
const walk = dir => {
|
|
44
|
+
const walk = (dir) => {
|
|
32
45
|
for (const d of readdirSync(dir, { withFileTypes: true })) {
|
|
33
46
|
const f = join(dir, d.name)
|
|
34
|
-
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)
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
|
-
const read = file => {
|
|
52
|
+
const read = (file) => {
|
|
38
53
|
const text = readFileSync(file, 'utf8')
|
|
39
54
|
if (!/required:\s*true/.test(text)) return
|
|
40
55
|
for (const m of text.matchAll(/\blive:\s*\{[^}]*required:\s*true/g)) {
|
|
41
56
|
const name = [...text.slice(0, m.index).matchAll(/\b(\w+)\s*=\s*query\s*[(<]/g)].at(-1)?.[1]
|
|
42
57
|
if (name) out.push({ name, file: file.slice(root.length + 1) })
|
|
43
58
|
}
|
|
44
|
-
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) })
|
|
45
61
|
}
|
|
46
62
|
const src = join(root, 'src')
|
|
47
63
|
if (existsSync(src)) walk(src)
|
|
48
|
-
return [...new Map(out.map(r => [r.name, r])).values()]
|
|
64
|
+
return [...new Map(out.map((r) => [r.name, r])).values()]
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/**
|
|
@@ -54,14 +70,20 @@ export function scanRequired(root) {
|
|
|
54
70
|
*/
|
|
55
71
|
export function livePlan({ preset, source, required = [] }) {
|
|
56
72
|
const target = liveTarget(preset)
|
|
57
|
-
const names = required.map(r => `\`${r.name}\``).join(', ')
|
|
73
|
+
const names = required.map((r) => `\`${r.name}\``).join(', ')
|
|
58
74
|
const diagnostics = []
|
|
59
75
|
const options = target.held ? [] : [LISTENER, SERVER]
|
|
60
76
|
let mode, says
|
|
61
77
|
if (!source) {
|
|
62
78
|
mode = 'app'
|
|
63
|
-
says =
|
|
64
|
-
|
|
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
|
+
})
|
|
65
87
|
} else if (source.external) {
|
|
66
88
|
mode = 'external'
|
|
67
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.`
|
|
@@ -71,40 +93,86 @@ export function livePlan({ preset, source, required = [] }) {
|
|
|
71
93
|
} else {
|
|
72
94
|
mode = 'off'
|
|
73
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\`.`
|
|
74
|
-
if (required.length)
|
|
75
|
-
|
|
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,
|
|
76
118
|
}
|
|
77
|
-
return { preset: target.preset, held: target.held, host: target.host, mode, says, required: required.map(r => r.name), options, diagnostics }
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
// ---- `verify` ---------------------------------------------------------------------------------------------------------
|
|
81
122
|
|
|
82
123
|
const PRODUCTION = /(^|[.\-_])(prod|production|live|www|main|master)([.\-_]|$)/i
|
|
83
|
-
const NOT_PRODUCTION =
|
|
124
|
+
const NOT_PRODUCTION =
|
|
125
|
+
/(^|[.\-_])(staging|stage|stg|dev|develop|development|test|testing|qa|sandbox|preview|uat|demo)([.\-_]|$)|^localhost$|^127\.|^\[?::1\]?$/i
|
|
84
126
|
|
|
85
|
-
const hostOf =
|
|
86
|
-
|
|
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
|
+
}
|
|
87
141
|
|
|
88
|
-
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.'
|
|
89
144
|
|
|
90
145
|
/**
|
|
91
146
|
* Where a `verify` points, from the query's declaration, `live.targets` in weft.config.ts and the environment.
|
|
92
147
|
* @returns {{ name: string, url?: string, db?: string, marked: boolean } | { missing: string }}
|
|
93
148
|
*/
|
|
94
149
|
export function resolveTarget(verify, { targets = {}, env = process.env } = {}) {
|
|
95
|
-
if (typeof verify === 'object' && verify.target !== undefined)
|
|
150
|
+
if (typeof verify === 'object' && verify.target !== undefined)
|
|
151
|
+
return resolveTarget(verify.target, { targets, env })
|
|
96
152
|
if (verify === true) {
|
|
97
|
-
const url = env.WEFT_STAGING_URL,
|
|
98
|
-
|
|
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
|
+
}
|
|
99
160
|
}
|
|
100
161
|
if (typeof verify === 'string') {
|
|
101
162
|
const t = targets[verify]
|
|
102
|
-
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
|
+
}
|
|
103
167
|
const db = t.db && (t.db.includes('://') ? t.db : env[t.db])
|
|
104
|
-
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
|
+
}
|
|
105
172
|
return { name: verify, url: t.url, db, marked: t.production === false }
|
|
106
173
|
}
|
|
107
|
-
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}` }
|
|
108
176
|
return { name: 'inline', url: verify.url, db: verify.db, marked: false }
|
|
109
177
|
}
|
|
110
178
|
|
|
@@ -112,12 +180,18 @@ export function resolveTarget(verify, { targets = {}, env = process.env } = {})
|
|
|
112
180
|
export function refusal(target, { targets = {} } = {}) {
|
|
113
181
|
const hosts = [target.url && hostOf(target.url), target.db && hostOf(target.db)].filter(Boolean)
|
|
114
182
|
const names = [target.name, ...hosts, ...(target.db ? [dbNameOf(target.db)] : [])]
|
|
115
|
-
const said = names.find(n => PRODUCTION.test(n))
|
|
183
|
+
const said = names.find((n) => PRODUCTION.test(n))
|
|
116
184
|
if (said) return `\`${said}\` looks like production: \`verify\` writes rows, and won't go near it`
|
|
117
|
-
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`
|
|
118
191
|
if (target.marked) return
|
|
119
|
-
const unsure = hosts.find(h => !NOT_PRODUCTION.test(h))
|
|
120
|
-
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'\``
|
|
121
195
|
}
|
|
122
196
|
|
|
123
197
|
const PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`/:@]+:[^\s'"`/@]+@/i
|
|
@@ -125,20 +199,24 @@ const PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`/:@]+:[^\s'"`/@]+@/i
|
|
|
125
199
|
/** R020: a connection string with a password, written into source. An env var's value never reaches this. */
|
|
126
200
|
export function literalSecrets(root) {
|
|
127
201
|
const out = []
|
|
128
|
-
const look = file => {
|
|
202
|
+
const look = (file) => {
|
|
129
203
|
const text = readFileSync(file, 'utf8')
|
|
130
204
|
if (!/\b(verify|targets)\b/.test(text)) return
|
|
131
205
|
text.split('\n').forEach((line, i) => {
|
|
132
|
-
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 })
|
|
133
208
|
})
|
|
134
209
|
}
|
|
135
|
-
const walk = dir => {
|
|
210
|
+
const walk = (dir) => {
|
|
136
211
|
for (const d of readdirSync(dir, { withFileTypes: true })) {
|
|
137
212
|
const f = join(dir, d.name)
|
|
138
|
-
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)
|
|
139
216
|
}
|
|
140
217
|
}
|
|
141
218
|
if (existsSync(join(root, 'src'))) walk(join(root, 'src'))
|
|
142
|
-
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))
|
|
143
221
|
return out
|
|
144
222
|
}
|