@weft-js/start 0.0.0-stage → 0.1.1

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 (75) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +41 -2
  3. package/package.json +107 -4
  4. package/src/app.mjs +93 -0
  5. package/src/auth/better-auth.ts +58 -0
  6. package/src/auth/drizzle.ts +17 -0
  7. package/src/auth.ts +47 -0
  8. package/src/boot.ts +44 -0
  9. package/src/builtin.tsx +19 -0
  10. package/src/bus/postgres.ts +91 -0
  11. package/src/bus.ts +48 -0
  12. package/src/capture.d.mts +1 -0
  13. package/src/capture.mjs +15 -0
  14. package/src/changes-sql.d.mts +14 -0
  15. package/src/changes-sql.mjs +99 -0
  16. package/src/changes.ts +253 -0
  17. package/src/channel-fake.ts +55 -0
  18. package/src/channel.ts +622 -0
  19. package/src/client.tsx +20 -0
  20. package/src/contract.mjs +312 -0
  21. package/src/data.ts +929 -0
  22. package/src/db/bun-sqlite.d.ts +10 -0
  23. package/src/db/changes-mysql.ts +86 -0
  24. package/src/db/changes-postgres.ts +159 -0
  25. package/src/db/changes-sqlite.ts +116 -0
  26. package/src/db/core.ts +63 -0
  27. package/src/db/index.ts +27 -0
  28. package/src/db/mysql.ts +67 -0
  29. package/src/db/postgres.ts +65 -0
  30. package/src/db/sqlite.ts +139 -0
  31. package/src/derive.d.mts +16 -0
  32. package/src/derive.mjs +140 -0
  33. package/src/dev-runtime.mjs +42 -0
  34. package/src/exec.ts +519 -0
  35. package/src/extension.ts +38 -0
  36. package/src/first-paint.ts +57 -0
  37. package/src/form.tsx +134 -0
  38. package/src/gate.css +6 -0
  39. package/src/hub.ts +822 -0
  40. package/src/index.ts +106 -0
  41. package/src/inline.mjs +2 -0
  42. package/src/inspect.mjs +280 -0
  43. package/src/link.ts +461 -0
  44. package/src/list-drizzle.ts +50 -0
  45. package/src/list.ts +224 -0
  46. package/src/listen.ts +28 -0
  47. package/src/live-fake.ts +42 -0
  48. package/src/live-target.mjs +147 -0
  49. package/src/live.ts +499 -0
  50. package/src/media.ts +43 -0
  51. package/src/migrations.mjs +19 -0
  52. package/src/mux-session.ts +6 -0
  53. package/src/mux.ts +135 -0
  54. package/src/og/DefaultCard.tsx +60 -0
  55. package/src/og/fonts.mjs +64 -0
  56. package/src/og/images.mjs +54 -0
  57. package/src/og/index.mjs +146 -0
  58. package/src/og/native.mjs +29 -0
  59. package/src/range.mjs +19 -0
  60. package/src/router.ts +149 -0
  61. package/src/routes.mjs +142 -0
  62. package/src/run.tsx +202 -0
  63. package/src/rust.ts +333 -0
  64. package/src/server.tsx +1021 -0
  65. package/src/shape.d.mts +3 -0
  66. package/src/shape.mjs +73 -0
  67. package/src/share-server.ts +208 -0
  68. package/src/share.ts +503 -0
  69. package/src/sheets.mjs +75 -0
  70. package/src/sidecar.mjs +503 -0
  71. package/src/socket.ts +389 -0
  72. package/src/stream.ts +13 -0
  73. package/src/verify.ts +251 -0
  74. package/src/view.tsx +47 -0
  75. package/src/vite.mjs +899 -0
package/src/form.tsx ADDED
@@ -0,0 +1,134 @@
1
+ // `<Form of={mutation}>` (research 29 §5). The markup is a real `<form method="post">` with the mutation's id in a
2
+ // hidden field, so it works before and without JS: the server runs the mutation and answers 303, or the same page
3
+ // with the errors (server.tsx). Once hydrated, a submit calls the mutation in place, with `optimistic` and
4
+ // single-flight as in any call. Validation is the server's in both cases: client stubs have no schema.
5
+ //
6
+ // Reachable only from routes that import it, so an app without forms doesn't pay.
7
+
8
+ import { derived, flush, state, untrack, type Derived } from '@weft-js/core'
9
+ import { currentScope, decodeForm, fieldsOf, fnOf, StartError, SUBMIT, type FnError, type Mutation } from './data'
10
+ import type { FormFailure } from './exec'
11
+
12
+ /** What a failed form's page gives back for a field: what was typed. */
13
+ export type FormValue = string | string[] | undefined
14
+
15
+ /** A mutation input's field names: `title`, and `a.b` one level down. */
16
+ type Field<I> = I extends object ? { [K in keyof I & string]: NonNullable<I[K]> extends object ? K | `${K}.${keyof NonNullable<I[K]> & string}` : K }[keyof I & string] : string
17
+
18
+ /** The state and fields `<Form>` gives its children. */
19
+ export interface FormApi<I = unknown, O = unknown> {
20
+ /** This form's submit is in flight (`mutation.pending` is every caller's). */
21
+ readonly pending: Derived<boolean>
22
+ /** Errors by field, from the schema or `invalid()`. */
23
+ readonly errors: Derived<Record<string, string>>
24
+ /** The failure that names no field. */
25
+ readonly message: Derived<string | undefined>
26
+ /** The last successful result. */
27
+ readonly result: Derived<O | undefined>
28
+ /** One field's error (its reader re-runs only when that field's error changes). */
29
+ error(name: Field<I>): string | undefined
30
+ /** What was typed, when a no-JS submit came back with errors. */
31
+ value(name: Field<I>): FormValue
32
+ /** Spread on the input: `name`, the value to restore, and the invalid state. */
33
+ field(name: Field<I>): { name: string; value?: FormValue; 'aria-invalid'?: true; 'aria-describedby'?: string }
34
+ /** The field's error text, in a live region that's in the page before it has anything to say. */
35
+ Error(props: { name: Field<I>; class?: string }): unknown
36
+ }
37
+
38
+ /** `<Form>`: the mutation, and what to do with its outcome. */
39
+ export interface FormProps<I, O> {
40
+ of: Mutation<I, O>
41
+ /** Tells two forms for one mutation apart on the no-JS path. */
42
+ id?: string
43
+ /** Empty the fields after a success (default true). A no-JS submit always comes back to a fresh page. */
44
+ reset?: boolean
45
+ onSuccess?: (result: O) => void
46
+ onError?: (error: unknown) => void
47
+ children?: unknown
48
+ [attr: string]: unknown
49
+ }
50
+
51
+ let page: FormFailure | null | undefined
52
+
53
+ /** The failure this form came back with: the request's on the server, `#weft-form` in the hydrating page. */
54
+ function failure(id: string, key?: string): FormFailure | undefined {
55
+ const scope = currentScope()
56
+ let f: FormFailure | undefined | null = scope?.form
57
+ if (!scope && typeof document === 'object') {
58
+ if (page === undefined) {
59
+ const el = document.getElementById('weft-form')
60
+ page = el && JSON.parse(el.textContent)
61
+ el?.remove()
62
+ }
63
+ f = page
64
+ }
65
+ if (f?.id !== id || f.key !== key) return undefined
66
+ if (!scope) page = null // once: a later visit to this page starts clean
67
+ return f
68
+ }
69
+
70
+ const at = (values: Record<string, unknown> | undefined, name: string) => name.split('.').reduce<unknown>((v, k) => (v as Record<string, unknown> | undefined)?.[k], values) as FormValue
71
+
72
+ /** A form that posts to a mutation, and works before any code has loaded. */
73
+ export function Form<I, O>({ of, id, reset, onSuccess, onError, children, ...rest }: FormProps<I, O>) {
74
+ const m = untrack(() => of) as Mutation<I, O> & { [SUBMIT]: (input: unknown, form: true | FormData) => Promise<O> }
75
+ const key = untrack(() => id)
76
+ const fnId = fnOf(m)?.$id
77
+ if (fnOf(m)?.kind !== 'mutation') throw new StartError('F001', '`<Form of>` needs a mutation', '`<Form of={addTodo}>`, with `addTodo` a `mutation()` export of a server module')
78
+ const seed = failure(fnId, key)
79
+ const errors = state<Record<string, string>>(seed?.errors ?? {})
80
+ const message = state(seed?.message)
81
+ const result = state<O | undefined>(undefined)
82
+ const calls = state(0)
83
+ const errorId = (name: string) => `${key ?? fnId}-${name}-error`
84
+
85
+ const form: FormApi<I, O> = {
86
+ pending: derived(() => calls.value > 0),
87
+ errors,
88
+ message,
89
+ result,
90
+ error: name => errors.get(name),
91
+ value: name => at(seed?.values, name),
92
+ field(name) {
93
+ const value = at(seed?.values, name)
94
+ const invalid = !!errors.get(name)
95
+ return { name, ...(value !== undefined && { value }), ...(invalid && { 'aria-invalid': true as const, 'aria-describedby': errorId(name) }) }
96
+ },
97
+ Error: props => <p id={errorId(props.name)} class={props.class} role="alert">{errors.get(props.name)}</p>,
98
+ }
99
+
100
+ async function submit(e: SubmitEvent) {
101
+ e.preventDefault()
102
+ if (calls.value) return
103
+ const el = e.target as HTMLFormElement
104
+ const data = new FormData(el, e.submitter)
105
+ const files = [...data.values()].some(v => typeof v !== 'string' && v.size)
106
+ calls.value++
107
+ try {
108
+ const v = await m[SUBMIT](decodeForm(data), files ? data : true)
109
+ errors.value = {}
110
+ message.value = undefined
111
+ result.value = v
112
+ if (untrack(() => reset) !== false) el.reset()
113
+ untrack(() => onSuccess)?.(v)
114
+ } catch (err) {
115
+ const issues = (err as FnError).issues
116
+ const f = fieldsOf(issues)
117
+ errors.value = f.errors
118
+ message.value = issues ? f.message : (err as Error).message
119
+ flush()
120
+ el.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus()
121
+ untrack(() => onError)?.(err)
122
+ } finally {
123
+ calls.value--
124
+ }
125
+ }
126
+
127
+ return (
128
+ <form method="post" {...rest} aria-busy={form.pending.value || undefined} onSubmit={e => void submit(e)}>
129
+ <input type="hidden" name="$fn" value={fnId} />
130
+ {key !== undefined && <input type="hidden" name="$form" value={key} />}
131
+ {typeof children === 'function' ? (children as (api: FormApi<I, O>) => unknown)(form) : children}
132
+ </form>
133
+ )
134
+ }
package/src/gate.css ADDED
@@ -0,0 +1,6 @@
1
+ /* The late half of the style gate: `firstPaint` hides `data-weft="styled"` in the page, and this shows it. */
2
+ [data-weft="styled"] { visibility: visible; animation: none; }
3
+ @media (prefers-reduced-motion: no-preference) {
4
+ [data-weft="styled"] { animation: weft-reveal 160ms ease-out; }
5
+ }
6
+ @keyframes weft-reveal { from { opacity: 0; } }