@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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +38 -8
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +120 -492
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +144 -157
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +90 -166
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +112 -37
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -178
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -971
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. package/src/capture.d.mts +0 -1
package/src/client.tsx CHANGED
@@ -12,9 +12,16 @@ export type { ClientRoutes } from './run'
12
12
  * Hydrate the page in `#app` and take over its links. Resolves once hydrated, which on a page of lazy islands is when
13
13
  * one is first needed. `eager`: don't wait for that.
14
14
  */
15
- export function start(routes: ClientRoutes, options: { container?: Element; eager?: boolean } = {}): ReturnType<typeof run> {
15
+ export function start(
16
+ routes: ClientRoutes,
17
+ options: { container?: Element; eager?: boolean } = {},
18
+ ): ReturnType<typeof run> {
16
19
  return new Promise((settle, fail) => {
17
- const full = () => import('./run').then(m => m.start(routes, options)).then(settle).catch(e => (fail(e), Promise.reject(e)))
20
+ const full = () =>
21
+ import('./run')
22
+ .then((m) => m.start(routes, options))
23
+ .then(settle)
24
+ .catch((e) => (fail(e), Promise.reject(e)))
18
25
  options.eager ? void full() : boot(full)
19
26
  })
20
27
  }
@@ -0,0 +1,231 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { toJson } from '../shape.mjs'
3
+
4
+ const tagsOf = (t) => (typeof t === 'function' ? 'dynamic' : [...(t ?? [])])
5
+
6
+ /** The first string or array in a JSON Schema with no upper bound, as a path. */
7
+ function unbounded(s, path = 'input') {
8
+ if (!s || typeof s !== 'object') return
9
+ if (s.type === 'string' && s.maxLength === undefined && !s.enum && s.const === undefined)
10
+ return path
11
+ if (s.type === 'array' && s.maxItems === undefined) return path
12
+ if (s.items) {
13
+ const hit = unbounded(s.items, `${path}[]`)
14
+ if (hit) return hit
15
+ }
16
+ for (const [k, v] of Object.entries(s.properties ?? {})) {
17
+ const hit = unbounded(v, `${path}.${k}`)
18
+ if (hit) return hit
19
+ }
20
+ for (const v of [...(s.anyOf ?? []), ...(s.oneOf ?? [])]) {
21
+ const hit = unbounded(v, path)
22
+ if (hit) return hit
23
+ }
24
+ }
25
+
26
+ /** A one-way `.transform()` or `preprocess` in a Zod schema: parsed input it touched can't be sent on (`encode` throws). */
27
+ function oneWay(schema, seen = new Set()) {
28
+ if (!schema || typeof schema !== 'object' || seen.has(schema)) return false
29
+ seen.add(schema)
30
+ const def = schema._zod?.def
31
+ if (def?.type === 'transform') return true
32
+ return Object.values(def ?? schema).some((v) => oneWay(v, seen)) // arrays and `shape` hold schemas too
33
+ }
34
+
35
+ /** The first union in a JSON Schema with two object or array arms, as a path: typify has no faithful type for one. */
36
+ function plainUnion(s, path) {
37
+ if (!s || typeof s !== 'object') return
38
+ const arms = s.anyOf?.filter((x) => x.type !== 'null')
39
+ if (arms?.length > 1 && arms.some((x) => x.type === 'object' || x.type === 'array' || x.$ref))
40
+ return path
41
+ for (const [k, v] of Object.entries(s.properties ?? {})) {
42
+ const hit = plainUnion(v, `${path}.${k}`)
43
+ if (hit) return hit
44
+ }
45
+ for (const v of [
46
+ s.items,
47
+ ...(s.prefixItems ?? []),
48
+ ...(s.anyOf ?? []),
49
+ ...(s.oneOf ?? []),
50
+ ...Object.values(s.$defs ?? {}),
51
+ ]) {
52
+ const hit = plainUnion(v, path)
53
+ if (hit) return hit
54
+ }
55
+ }
56
+
57
+ const CODEC =
58
+ '`z.codec(z.iso.datetime(), z.date(), { decode, encode })` for a date, `z.codec(z.string(), z.bigint(), …)` for a bigint'
59
+
60
+ /**
61
+ * A function with `impl`, as a sidecar sees it (research 36 §3). The wire is the *input* side of every schema: TS sends
62
+ * parsed input encoded back, and parses what comes back, so codecs and transforms on an output are TS's business.
63
+ */
64
+ function implOf(f, report) {
65
+ const { impl, input, output, errors = {} } = f.def
66
+ const r101 = (why) => report({ code: 'R101', severity: 'error', why: `\`${f.$id}\`: ${why}` })
67
+ const wire = (schema, what) => {
68
+ const json = toJson(schema, 'input')
69
+ if (!json)
70
+ r101(`${what} has a type JSON can't carry, so no Rust type can be generated. Use ${CODEC}.`)
71
+ return json ?? {}
72
+ }
73
+ if (input) wire(input, '`input`')
74
+ if (input && oneWay(input))
75
+ r101(
76
+ "`input` has a one-way `.transform()`, so its parsed value can't be sent. Use `z.codec(…)`, or do it in Rust.",
77
+ )
78
+ if (!output)
79
+ r101(
80
+ 'a function with `impl` needs `output`: it\'s the contract for what comes back, and `z.null()` says "nothing".',
81
+ )
82
+ return {
83
+ kind: impl.kind,
84
+ target: impl.target,
85
+ service: impl.service,
86
+ ctx:
87
+ f.def.cache && f.def.cache.scope !== 'user'
88
+ ? 'shared'
89
+ : f.def.auth === 'user'
90
+ ? 'user'
91
+ : 'ctx',
92
+ output: output ? wire(output, '`output`') : {},
93
+ errors: Object.fromEntries(
94
+ Object.entries(errors).map(([code, schema]) => [code, wire(schema, `error \`${code}\``)]),
95
+ ),
96
+ }
97
+ }
98
+
99
+ /** What a sidecar is built from, per service: a binary whose hash differs was built from another contract (R103). */
100
+ function services(functions) {
101
+ const out = {}
102
+ for (const f of functions)
103
+ if (f.impl)
104
+ (out[f.impl.service] ??= {
105
+ kind: f.impl.kind,
106
+ hash: createHash('sha256'),
107
+ functions: [],
108
+ }).functions.push(f.id)
109
+ for (const f of functions)
110
+ if (f.impl) out[f.impl.service].hash.update(JSON.stringify([f.id, f.kind, f.input, f.impl]))
111
+ return Object.fromEntries(
112
+ Object.entries(out).map(([k, v]) => [
113
+ k,
114
+ { kind: v.kind, hash: v.hash.digest('hex').slice(0, 16), functions: v.functions },
115
+ ]),
116
+ )
117
+ }
118
+
119
+ /** Presets with no file for an addon or a `.wasm` to be read from. */
120
+ function channelDescription(f, bound) {
121
+ const part = (key) => (f.def[key] ? (toJson(f.def[key], 'input') ?? {}) : undefined)
122
+ const messages = (list) =>
123
+ Object.fromEntries(Object.entries(list ?? {}).map(([k, s]) => [k, toJson(s, 'input') ?? {}]))
124
+ const client = f.kind === 'shared' ? { value: part('value') } : messages(f.def.client)
125
+ bound(part(f.kind === 'shared' ? 'key' : 'params'), f.kind === 'shared' ? 'key' : 'params')
126
+ for (const [k, json] of Object.entries(client)) bound(json, `client.${k}`)
127
+ return {
128
+ id: f.$id,
129
+ name: f.$id.replace(/\..*$/, ''),
130
+ kind: f.kind,
131
+ input: part(f.kind === 'shared' ? 'key' : 'params') ?? null,
132
+ output: null,
133
+ client,
134
+ server: messages(f.def.server),
135
+ auth: 'authorize',
136
+ errors: ['R011', 'Q002'],
137
+ }
138
+ }
139
+
140
+ function describeFunction(f, rust, diagnostics) {
141
+ const schema = (key, side) => {
142
+ const s = f.def[key]
143
+ if (!s) return undefined
144
+ const json = toJson(s, side)
145
+ if (!json)
146
+ diagnostics.push({
147
+ code: 'C001',
148
+ severity: 'warning',
149
+ why: `\`${f.$id}\`'s \`${key}\` schema can't become JSON Schema: the contract says \`{}\`.`,
150
+ })
151
+ return json ?? {}
152
+ }
153
+ const errors = [
154
+ ...(f.def.input ? ['Q002'] : []),
155
+ ...(f.def.auth ? ['A002'] : []),
156
+ ...(f.kind === 'mutation' ? ['Q010'] : []),
157
+ ...Object.keys(f.def.errors ?? {}),
158
+ ]
159
+ const impl = f.def.impl && implOf(f, (d) => diagnostics.push(d))
160
+ const input = impl
161
+ ? f.def.input
162
+ ? (toJson(f.def.input, 'input') ?? {})
163
+ : null
164
+ : (schema('input', 'input') ?? null)
165
+ if (impl && rust.unions !== 'allow') {
166
+ const hit = [
167
+ ['input', input],
168
+ ['output', impl.output],
169
+ ...Object.entries(impl.errors).map(([k, v]) => [`error ${k}`, v]),
170
+ ]
171
+ .map(([name, s]) => plainUnion(s, name))
172
+ .find(Boolean)
173
+ if (hit)
174
+ diagnostics.push({
175
+ code: 'R102',
176
+ severity: 'error',
177
+ why: `\`${f.$id}\`'s \`${hit}\` is a plain \`z.union\` of objects: it has no tag, so Rust can't tell its arms apart reliably. Use \`z.discriminatedUnion\`, or set \`rust: { unions: 'allow' }\` in weft.config.ts.`,
178
+ })
179
+ }
180
+ const bound = (json, path) => {
181
+ const hit = unbounded(json, path)
182
+ if (hit)
183
+ diagnostics.push({
184
+ code: 'R005',
185
+ severity: 'warning',
186
+ why: `\`${f.$id}\`'s \`${hit}\` has no upper bound: a client decides how much the server holds. Add \`.max()\`.`,
187
+ })
188
+ }
189
+ if (f.kind === 'stream') bound(input)
190
+ if (f.kind === 'channel' || f.kind === 'shared') return channelDescription(f, bound)
191
+
192
+ return {
193
+ id: f.$id,
194
+ name: f.$id.replace(/\..*$/, ''),
195
+ kind: f.kind,
196
+ input,
197
+ output: impl
198
+ ? impl.output
199
+ : (schema(f.kind === 'stream' ? 'message' : 'output', 'output') ?? null),
200
+ ...(impl && { impl }),
201
+ ...(f.kind === 'query'
202
+ ? { tags: tagsOf(f.def.tags), cache: f.def.cache ?? null }
203
+ : f.kind === 'mutation'
204
+ ? { invalidates: tagsOf(f.def.invalidates) }
205
+ : {}),
206
+ auth: f.def.auth ?? 'public',
207
+ errors,
208
+ }
209
+ }
210
+
211
+ /** @param {Map<string, any>} fns @param {{ unions?: 'allow' }} [rust] the config's `rust` */
212
+ export function describe(fns, rust = {}) {
213
+ const diagnostics = []
214
+ const functions = [...fns.values()]
215
+ .sort((a, b) => a.$id.localeCompare(b.$id))
216
+ .map((f) => describeFunction(f, rust, diagnostics))
217
+ const rusts = services(functions)
218
+ for (const f of functions)
219
+ if (f.impl && f.impl.kind !== rusts[f.impl.service].kind)
220
+ diagnostics.push({
221
+ code: 'R109',
222
+ severity: 'error',
223
+ why: `\`${f.impl.service}\` is named by both \`remote()\` and \`native()\`: a crate is one or the other. Rename one.`,
224
+ })
225
+ return {
226
+ version: 1,
227
+ ...(Object.keys(rusts).length && { services: rusts }),
228
+ functions,
229
+ diagnostics,
230
+ }
231
+ }
package/src/contract.mjs CHANGED
@@ -7,152 +7,46 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
7
7
  import { join, resolve } from 'node:path'
8
8
  import { createServer } from 'vite'
9
9
  import weft from '@weft-js/core/vite'
10
- import { holdsSockets, literalSecrets, liveTarget, livePlan, refusal, resolveTarget, scanRequired } from './live-target.mjs'
10
+ import {
11
+ holdsSockets,
12
+ literalSecrets,
13
+ liveTarget,
14
+ livePlan,
15
+ refusal,
16
+ resolveTarget,
17
+ scanRequired,
18
+ } from './live-target.mjs'
11
19
  import { readMigrations } from './migrations.mjs'
12
- import { shapeTree, toJson } from './shape.mjs'
20
+ import { shapeTree } from './shape.mjs'
21
+ import { describe } from './contract/describe.mjs'
13
22
  import weftStart, { startDir } from './vite.mjs'
14
23
 
15
- const tagsOf = t => (typeof t === 'function' ? 'dynamic' : [...(t ?? [])])
24
+ export { describe }
16
25
 
17
- /** The first string or array in a JSON Schema with no upper bound, as a path. */
18
- function unbounded(s, path = 'input') {
19
- if (!s || typeof s !== 'object') return
20
- if (s.type === 'string' && s.maxLength === undefined && !s.enum && s.const === undefined) return path
21
- if (s.type === 'array' && s.maxItems === undefined) return path
22
- if (s.items) {
23
- const hit = unbounded(s.items, `${path}[]`)
24
- if (hit) return hit
25
- }
26
- for (const [k, v] of Object.entries(s.properties ?? {})) {
27
- const hit = unbounded(v, `${path}.${k}`)
28
- if (hit) return hit
29
- }
30
- for (const v of [...(s.anyOf ?? []), ...(s.oneOf ?? [])]) {
31
- const hit = unbounded(v, path)
32
- if (hit) return hit
33
- }
34
- }
35
-
36
- /** A one-way `.transform()` or `preprocess` in a Zod schema: parsed input it touched can't be sent on (`encode` throws). */
37
- function oneWay(schema, seen = new Set()) {
38
- if (!schema || typeof schema !== 'object' || seen.has(schema)) return false
39
- seen.add(schema)
40
- const def = schema._zod?.def
41
- if (def?.type === 'transform') return true
42
- return Object.values(def ?? schema).some(v => oneWay(v, seen)) // arrays and `shape` hold schemas too
43
- }
44
-
45
- /** The first union in a JSON Schema with two object or array arms, as a path: typify has no faithful type for one. */
46
- function plainUnion(s, path) {
47
- if (!s || typeof s !== 'object') return
48
- const arms = s.anyOf?.filter(x => x.type !== 'null')
49
- if (arms?.length > 1 && arms.some(x => x.type === 'object' || x.type === 'array' || x.$ref)) return path
50
- for (const [k, v] of Object.entries(s.properties ?? {})) {
51
- const hit = plainUnion(v, `${path}.${k}`)
52
- if (hit) return hit
53
- }
54
- for (const v of [s.items, ...(s.prefixItems ?? []), ...(s.anyOf ?? []), ...(s.oneOf ?? []), ...Object.values(s.$defs ?? {})]) {
55
- const hit = plainUnion(v, path)
56
- if (hit) return hit
57
- }
58
- }
59
-
60
- const CODEC = '`z.codec(z.iso.datetime(), z.date(), { decode, encode })` for a date, `z.codec(z.string(), z.bigint(), …)` for a bigint'
61
-
62
- /**
63
- * A function with `impl`, as a sidecar sees it (research 36 §3). The wire is the *input* side of every schema: TS sends
64
- * parsed input encoded back, and parses what comes back, so codecs and transforms on an output are TS's business.
65
- */
66
- function implOf(f, report) {
67
- const { impl, input, output, errors = {} } = f.def
68
- const r101 = why => report({ code: 'R101', severity: 'error', why: `\`${f.$id}\`: ${why}` })
69
- const wire = (schema, what) => {
70
- const json = toJson(schema, 'input')
71
- if (!json) r101(`${what} has a type JSON can't carry, so no Rust type can be generated. Use ${CODEC}.`)
72
- return json ?? {}
73
- }
74
- if (input) wire(input, '`input`')
75
- if (input && oneWay(input)) r101('`input` has a one-way `.transform()`, so its parsed value can\'t be sent. Use `z.codec(…)`, or do it in Rust.')
76
- if (!output) r101('a function with `impl` needs `output`: it\'s the contract for what comes back, and `z.null()` says "nothing".')
77
- return {
78
- kind: impl.kind, target: impl.target, service: impl.service,
79
- ctx: f.def.cache && f.def.cache.scope !== 'user' ? 'shared' : f.def.auth === 'user' ? 'user' : 'ctx',
80
- output: output ? wire(output, '`output`') : {},
81
- errors: Object.fromEntries(Object.entries(errors).map(([code, schema]) => [code, wire(schema, `error \`${code}\``)])),
82
- }
83
- }
84
-
85
- /** What a sidecar is built from, per service: a binary whose hash differs was built from another contract (R103). */
86
- function services(functions) {
87
- const out = {}
88
- for (const f of functions) if (f.impl) (out[f.impl.service] ??= { kind: f.impl.kind, hash: createHash('sha256'), functions: [] }).functions.push(f.id)
89
- for (const f of functions) if (f.impl) out[f.impl.service].hash.update(JSON.stringify([f.id, f.kind, f.input, f.impl]))
90
- return Object.fromEntries(Object.entries(out).map(([k, v]) => [k, { kind: v.kind, hash: v.hash.digest('hex').slice(0, 16), functions: v.functions }]))
91
- }
92
-
93
- /** Presets with no file for an addon or a `.wasm` to be read from. */
94
26
  const NO_FILES = /^(cloudflare|vercel[-_]edge|netlify[-_]edge|deno[-_]deploy|bunny|fastly)/
95
27
  /** Presets that run where they were built, so the addon from this machine is the right one. */
96
28
  const LOCAL = /^(node|bun|deno[-_]server)/
97
29
 
98
30
  /** R104 for a build: `native()` on a preset that has no way to run it. */
99
31
  export function nativeProblem({ service, preset, wasm }) {
100
- if (preset && NO_FILES.test(preset)) return `\`native('${service}')\` can't load on the \`${preset}\` preset: it has no file system, so neither an addon nor a \`.wasm\` file reaches it. Use \`remote('rust://${service}')\`, which is the same crate behind \`weft::router\``
101
- if (wasm === false && preset && !LOCAL.test(preset)) return `\`native('${service}')\` has no WASM, and an addon built here may not fit the \`${preset}\` preset's machines. Drop \`wasm: false\` from \`rust.services.${service}\`, or use \`remote()\``
102
- }
103
-
104
- /** @param {Map<string, any>} fns @param {{ unions?: 'allow' }} [rust] the config's `rust` */
105
- export function describe(fns, rust = {}) {
106
- const diagnostics = []
107
- const functions = [...fns.values()].sort((a, b) => a.$id.localeCompare(b.$id)).map(f => {
108
- const schema = (key, side) => {
109
- const s = f.def[key]
110
- if (!s) return undefined
111
- const json = toJson(s, side)
112
- if (!json) diagnostics.push({ code: 'C001', severity: 'warning', why: `\`${f.$id}\`'s \`${key}\` schema can't become JSON Schema: the contract says \`{}\`.` })
113
- return json ?? {}
114
- }
115
- const errors = [...(f.def.input ? ['Q002'] : []), ...(f.def.auth ? ['A002'] : []), ...(f.kind === 'mutation' ? ['Q010'] : []), ...Object.keys(f.def.errors ?? {})]
116
- const impl = f.def.impl && implOf(f, d => diagnostics.push(d))
117
- const input = impl ? (f.def.input ? toJson(f.def.input, 'input') ?? {} : null) : schema('input', 'input') ?? null
118
- if (impl && rust.unions !== 'allow') {
119
- const hit = [['input', input], ['output', impl.output], ...Object.entries(impl.errors).map(([k, v]) => [`error ${k}`, v])].map(([name, s]) => plainUnion(s, name)).find(Boolean)
120
- if (hit) diagnostics.push({ code: 'R102', severity: 'error', why: `\`${f.$id}\`'s \`${hit}\` is a plain \`z.union\` of objects: it has no tag, so Rust can't tell its arms apart reliably. Use \`z.discriminatedUnion\`, or set \`rust: { unions: 'allow' }\` in weft.config.ts.` })
121
- }
122
- const bound = (json, path) => {
123
- const hit = unbounded(json, path)
124
- if (hit) diagnostics.push({ code: 'R005', severity: 'warning', why: `\`${f.$id}\`'s \`${hit}\` has no upper bound: a client decides how much the server holds. Add \`.max()\`.` })
125
- }
126
- if (f.kind === 'stream') bound(input)
127
- if (f.kind === 'channel' || f.kind === 'shared') {
128
- const part = key => (f.def[key] ? toJson(f.def[key], 'input') ?? {} : undefined)
129
- const messages = list => Object.fromEntries(Object.entries(list ?? {}).map(([k, s]) => [k, toJson(s, 'input') ?? {}]))
130
- const client = f.kind === 'shared' ? { value: part('value') } : messages(f.def.client)
131
- bound(part(f.kind === 'shared' ? 'key' : 'params'), f.kind === 'shared' ? 'key' : 'params')
132
- for (const [k, json] of Object.entries(client)) bound(json, `client.${k}`)
133
- return { id: f.$id, name: f.$id.replace(/\..*$/, ''), kind: f.kind, input: part(f.kind === 'shared' ? 'key' : 'params') ?? null, output: null, client, server: messages(f.def.server), auth: 'authorize', errors: ['R011', 'Q002'] }
134
- }
135
- return {
136
- id: f.$id, name: f.$id.replace(/\..*$/, ''), kind: f.kind,
137
- input, output: impl ? impl.output : schema(f.kind === 'stream' ? 'message' : 'output', 'output') ?? null,
138
- ...(impl && { impl }),
139
- ...(f.kind === 'query' ? { tags: tagsOf(f.def.tags), cache: f.def.cache ?? null } : f.kind === 'mutation' ? { invalidates: tagsOf(f.def.invalidates) } : {}),
140
- auth: f.def.auth ?? 'public', errors,
141
- }
142
- })
143
- const rusts = services(functions)
144
- for (const f of functions) if (f.impl && f.impl.kind !== rusts[f.impl.service].kind) diagnostics.push({ code: 'R109', severity: 'error', why: `\`${f.impl.service}\` is named by both \`remote()\` and \`native()\`: a crate is one or the other. Rename one.` })
145
- return { version: 1, ...(Object.keys(rusts).length && { services: rusts }), functions, diagnostics }
32
+ if (preset && NO_FILES.test(preset))
33
+ return `\`native('${service}')\` can't load on the \`${preset}\` preset: it has no file system, so neither an addon nor a \`.wasm\` file reaches it. Use \`remote('rust://${service}')\`, which is the same crate behind \`weft::router\``
34
+ if (wasm === false && preset && !LOCAL.test(preset))
35
+ return `\`native('${service}')\` has no WASM, and an addon built here may not fit the \`${preset}\` preset's machines. Drop \`wasm: false\` from \`rust.services.${service}\`, or use \`remote()\``
146
36
  }
147
37
 
148
38
  /** Presets that run many instances: a process-local bus reaches a fraction of the clients (R002). */
149
- const MULTI = /^(vercel|netlify|aws[-_]|azure|cloudflare|firebase|deno[-_]deploy|digitalocean|render|koyeb|zeabur)/
39
+ const MULTI =
40
+ /^(vercel|netlify|aws[-_]|azure|cloudflare|firebase|deno[-_]deploy|digitalocean|render|koyeb|zeabur)/
150
41
 
151
- const usesLive = root => {
152
- const walk = dir => readdirSync(dir, { withFileTypes: true }).some(d => {
153
- const f = join(dir, d.name)
154
- return d.isDirectory() ? walk(f) : /\.[cm]?[jt]sx?$/.test(d.name) && /[\w)\]]\.live\(/.test(readFileSync(f, 'utf8'))
155
- })
42
+ const usesLive = (root) => {
43
+ const walk = (dir) =>
44
+ readdirSync(dir, { withFileTypes: true }).some((d) => {
45
+ const f = join(dir, d.name)
46
+ return d.isDirectory()
47
+ ? walk(f)
48
+ : /\.[cm]?[jt]sx?$/.test(d.name) && /[\w)\]]\.live\(/.test(readFileSync(f, 'utf8'))
49
+ })
156
50
  return existsSync(join(root, 'src')) && walk(join(root, 'src'))
157
51
  }
158
52
 
@@ -164,23 +58,45 @@ export const busProblem = ({ preset, bus, streams, root }) =>
164
58
  bus === 'durable-object' && preset !== 'cloudflare_durable'
165
59
  ? `\`durableObjectBus\` holds a socket open, and on the \`${preset}\` preset that socket dies with the request that opened it: use the \`cloudflare_durable\` preset`
166
60
  : bus === 'memory' && MULTI.test(preset ?? '') && (streams || usesLive(root))
167
- ? `the \`${preset}\` preset runs many instances, and the memory bus only reaches clients on its own: pokes and messages would be lost`
168
- : undefined
61
+ ? `the \`${preset}\` preset runs many instances, and the memory bus only reaches clients on its own: pokes and messages would be lost`
62
+ : undefined
169
63
 
170
64
  /** What this deploy gets from live data, with the config loaded. R018 is a table `changes` lists and no migration covers. */
171
65
  function planOf({ root, preset, realtime }) {
172
66
  const source = realtime?.changes
173
- const plan = livePlan({ preset, source: source && { name: source.name, external: source.external, tables: source.tables }, required: scanRequired(root) })
67
+ const plan = livePlan({
68
+ preset,
69
+ source: source && { name: source.name, external: source.external, tables: source.tables },
70
+ required: scanRequired(root),
71
+ })
174
72
  if (source?.dialect) {
175
- const sql = readMigrations(root).map(m => m.sql).join('\n')
176
- const missing = source.tables.filter(t => !new RegExp(`weft_changes_(${t.replace(/\W/g, '_')}_)?upd\\b[^;]*["\`\\s]${t}["\`\\s]`).test(sql))
177
- if (missing.length) plan.diagnostics.push({ code: 'R018', severity: 'warning', why: `no migration adds change triggers to ${missing.map(t => `\`${t}\``).join(', ')}: run \`weft db live\`. Until then the server falls back to polling.` })
73
+ const sql = readMigrations(root)
74
+ .map((m) => m.sql)
75
+ .join('\n')
76
+ const missing = source.tables.filter(
77
+ (t) =>
78
+ !new RegExp(
79
+ `weft_changes_(${t.replace(/\W/g, '_')}_)?upd\\b[^;]*["\`\\s]${t}["\`\\s]`,
80
+ ).test(sql),
81
+ )
82
+ if (missing.length)
83
+ plan.diagnostics.push({
84
+ code: 'R018',
85
+ severity: 'warning',
86
+ why: `no migration adds change triggers to ${missing.map((t) => `\`${t}\``).join(', ')}: run \`weft db live\`. Until then the server falls back to polling.`,
87
+ })
178
88
  }
179
89
  return plan
180
90
  }
181
91
 
182
- const verifiedFile = root => join(root, '.weft', 'verify.json')
183
- const readVerified = root => { try { return JSON.parse(readFileSync(verifiedFile(root), 'utf8')) } catch { return {} } }
92
+ const verifiedFile = (root) => join(root, '.weft', 'verify.json')
93
+ const readVerified = (root) => {
94
+ try {
95
+ return JSON.parse(readFileSync(verifiedFile(root), 'utf8'))
96
+ } catch {
97
+ return {}
98
+ }
99
+ }
184
100
 
185
101
  /**
186
102
  * Every query's `live.verify`, run. `strict` is a build: a target that isn't reachable from here fails it, where a
@@ -188,9 +104,16 @@ const readVerified = root => { try { return JSON.parse(readFileSync(verifiedFile
188
104
  * @returns {Promise<{ diagnostics: any[], verified: any[] }>}
189
105
  */
190
106
  async function verifyAll({ root, fns, realtime, server, live, strict }) {
191
- const diagnostics = [], verified = []
192
- for (const hit of literalSecrets(root)) diagnostics.push({ code: 'R020', severity: 'error', file: hit.file, why: `${hit.file}:${hit.line} has a connection string with a password in it: it would be committed. Read it from the environment: \`verify: { db: process.env.STAGING_DATABASE_URL }\`, or \`db: 'STAGING_DATABASE_URL'\` in a target.` })
193
- const queries = [...fns.values()].filter(f => f.kind === 'query' && f.def.live?.verify)
107
+ const diagnostics = [],
108
+ verified = []
109
+ for (const hit of literalSecrets(root))
110
+ diagnostics.push({
111
+ code: 'R020',
112
+ severity: 'error',
113
+ file: hit.file,
114
+ why: `${hit.file}:${hit.line} has a connection string with a password in it: it would be committed. Read it from the environment: \`verify: { db: process.env.STAGING_DATABASE_URL }\`, or \`db: 'STAGING_DATABASE_URL'\` in a target.`,
115
+ })
116
+ const queries = [...fns.values()].filter((f) => f.kind === 'query' && f.def.live?.verify)
194
117
  if (!queries.length) return { diagnostics, verified }
195
118
  const { roundTrip } = await server.ssrLoadModule(join(startDir, 'verify.ts'))
196
119
  const cache = readVerified(root)
@@ -202,7 +125,11 @@ async function verifyAll({ root, fns, realtime, server, live, strict }) {
202
125
  const options = typeof v === 'object' ? v : {}
203
126
  const target = resolveTarget(v, { targets: live?.targets, env })
204
127
  if (target.missing) {
205
- diagnostics.push({ code: 'R021', severity: strict ? 'error' : 'warning', why: `\`${name}\` can't be verified on this machine: ${target.missing}${strict ? ' A build needs it, because the query declares it.' : ' Skipped.'}` })
128
+ diagnostics.push({
129
+ code: 'R021',
130
+ severity: strict ? 'error' : 'warning',
131
+ why: `\`${name}\` can't be verified on this machine: ${target.missing}${strict ? ' A build needs it, because the query declares it.' : ' Skipped.'}`,
132
+ })
206
133
  continue
207
134
  }
208
135
  const stop = refusal(target, { targets: live?.targets })
@@ -210,19 +137,46 @@ async function verifyAll({ root, fns, realtime, server, live, strict }) {
210
137
  diagnostics.push({ code: 'R022', severity: 'error', why: `\`${name}\`: ${stop}.` })
211
138
  continue
212
139
  }
213
- const key = createHash('sha256').update(JSON.stringify([contract.find(c => c.id === f.$id), realtime?.changes?.tables, target.name, target.url ?? '', !!target.db])).digest('hex').slice(0, 16)
140
+ const key = createHash('sha256')
141
+ .update(
142
+ JSON.stringify([
143
+ contract.find((c) => c.id === f.$id),
144
+ realtime?.changes?.tables,
145
+ target.name,
146
+ target.url ?? '',
147
+ !!target.db,
148
+ ]),
149
+ )
150
+ .digest('hex')
151
+ .slice(0, 16)
214
152
  if (cache[f.$id]?.key === key && !env.WEFT_VERIFY_FRESH) {
215
153
  verified.push({ name, cached: true, ...cache[f.$id].result })
216
154
  continue
217
155
  }
218
- const result = await roundTrip({ fn: f, source: realtime?.changes, url: target.url, db: target.db, options, env })
156
+ const result = await roundTrip({
157
+ fn: f,
158
+ source: realtime?.changes,
159
+ url: target.url,
160
+ db: target.db,
161
+ options,
162
+ env,
163
+ })
219
164
  verified.push({ name, ...result })
220
165
  if (result.ok) cache[f.$id] = { key, result, at: new Date().toISOString() }
221
166
  else {
222
167
  delete cache[f.$id]
223
- diagnostics.push({ code: 'R019', severity: 'error', why: `\`${name}\` failed its live round trip at \`${result.broke.link}\`: ${result.broke.why}.` })
168
+ diagnostics.push({
169
+ code: 'R019',
170
+ severity: 'error',
171
+ why: `\`${name}\` failed its live round trip at \`${result.broke.link}\`: ${result.broke.why}.`,
172
+ })
224
173
  }
225
- if (result.ok && result.untested.length) diagnostics.push({ code: 'R023', severity: 'warning', why: `\`${name}\` passed, with ${result.untested.map(u => `\`${u.link}\``).join(', ')} untested: ${[...new Set(result.untested.map(u => u.why))].join(' ')}` })
174
+ if (result.ok && result.untested.length)
175
+ diagnostics.push({
176
+ code: 'R023',
177
+ severity: 'warning',
178
+ why: `\`${name}\` passed, with ${result.untested.map((u) => `\`${u.link}\``).join(', ')} untested: ${[...new Set(result.untested.map((u) => u.why))].join(' ')}`,
179
+ })
226
180
  }
227
181
  mkdirSync(join(root, '.weft'), { recursive: true })
228
182
  writeFileSync(verifiedFile(root), JSON.stringify(cache, null, 2) + '\n')
@@ -236,13 +190,23 @@ export const readLivePlan = ({ root, site, preset, live, verify = true }) => {
236
190
  const realtime = (await server.ssrLoadModule('virtual:weft/realtime')).default
237
191
  const plan = planOf({ root, preset, realtime })
238
192
  if (!verify) return { ...plan, verified: [] }
239
- const { diagnostics, verified } = await verifyAll({ root, fns, realtime, server, live, strict: false })
193
+ const { diagnostics, verified } = await verifyAll({
194
+ root,
195
+ fns,
196
+ realtime,
197
+ server,
198
+ live,
199
+ strict: false,
200
+ })
240
201
  return { ...plan, diagnostics: [...plan.diagnostics, ...diagnostics], verified }
241
202
  })
242
203
  }
243
204
 
244
205
  /** `src/realtime.server.ts` loaded, for `weft db live` and `weft db listen`. `fn` holds it open as long as it needs. */
245
- export const withRealtime = ({ root, site }, fn) => withFns({ root: resolve(root), site }, async (_, server) => fn((await server.ssrLoadModule('virtual:weft/realtime')).default ?? {}, server))
206
+ export const withRealtime = ({ root, site }, fn) =>
207
+ withFns({ root: resolve(root), site }, async (_, server) =>
208
+ fn((await server.ssrLoadModule('virtual:weft/realtime')).default ?? {}, server),
209
+ )
246
210
 
247
211
  /** Every query's shape tree (research 31 §3), by id. */
248
212
  export function shapeTable(fns) {
@@ -264,7 +228,10 @@ async function withFns({ root, site = {} }, fn) {
264
228
  let server
265
229
  try {
266
230
  server = await createServer({
267
- root, configFile: false, logLevel: 'silent', appType: 'custom',
231
+ root,
232
+ configFile: false,
233
+ logLevel: 'silent',
234
+ appType: 'custom',
268
235
  server: { middlewareMode: true, hmr: false, ws: false },
269
236
  plugins: [weft({ hydratable: true }), weftStart({ root, site })],
270
237
  })
@@ -279,7 +246,8 @@ async function withFns({ root, site = {} }, fn) {
279
246
  }
280
247
 
281
248
  /** The shape table at build: nothing is running yet, so the server modules load the way the contract's do. */
282
- export const readShapes = options => withFns({ ...options, root: resolve(options.root) }, shapeTable)
249
+ export const readShapes = (options) =>
250
+ withFns({ ...options, root: resolve(options.root) }, shapeTable)
283
251
 
284
252
  /** Load every server module and write `.weft/contract.json`. */
285
253
  export function writeContract({ root, site = {}, preset, rust, live, verify }) {
@@ -287,12 +255,29 @@ export function writeContract({ root, site = {}, preset, rust, live, verify }) {
287
255
  return withFns({ root, site }, async (fns, server) => {
288
256
  const { diagnostics, ...contract } = describe(fns, rust)
289
257
  const mod = await server.ssrLoadModule('virtual:weft/realtime')
290
- const socket = [...fns.values()].find(f => f.kind === 'channel' || f.kind === 'shared')
258
+ const socket = [...fns.values()].find((f) => f.kind === 'channel' || f.kind === 'shared')
291
259
  // a channel needs the socket itself: on a function host no bus helps, so R024 says where it can run instead of R002
292
260
  const unheld = socket && !holdsSockets(preset)
293
- if (unheld) diagnostics.push({ code: 'R024', severity: 'error', why: `\`${socket.$id.replace(/\..*$/, '')}\` is a ${socket.kind === 'shared' ? 'shared state' : 'channel'}, which needs a WebSocket, and ${liveTarget(preset).host} on \`${preset}\` can't hold one: the page loads, and the channel says \`unavailable\`. \`weft deploy hosts\` lists the hosts that can (Fly, Render).` })
294
- const bus = busProblem({ preset, bus: mod.default?.bus?.name ?? 'memory', streams: [...fns.values()].some(f => f.kind === 'stream' || (!unheld && (f.kind === 'channel' || f.kind === 'shared'))), root })
295
- if (bus) diagnostics.push({ code: 'R002', severity: 'error', why: `${bus}. Add \`@weft-js/realtime-redis\` or \`@weft-js/realtime-nats\` to \`plugins\`, or set \`postgresBus\` in \`src/realtime.server.ts\`.` })
261
+ if (unheld)
262
+ diagnostics.push({
263
+ code: 'R024',
264
+ severity: 'error',
265
+ why: `\`${socket.$id.replace(/\..*$/, '')}\` is a ${socket.kind === 'shared' ? 'shared state' : 'channel'}, which needs a WebSocket, and ${liveTarget(preset).host} on \`${preset}\` can't hold one: the page loads, and the channel says \`unavailable\`. \`weft deploy hosts\` lists the hosts that can (Fly, Render).`,
266
+ })
267
+ const bus = busProblem({
268
+ preset,
269
+ bus: mod.default?.bus?.name ?? 'memory',
270
+ streams: [...fns.values()].some(
271
+ (f) => f.kind === 'stream' || (!unheld && (f.kind === 'channel' || f.kind === 'shared')),
272
+ ),
273
+ root,
274
+ })
275
+ if (bus)
276
+ diagnostics.push({
277
+ code: 'R002',
278
+ severity: 'error',
279
+ why: `${bus}. Add \`@weft-js/realtime-redis\` or \`@weft-js/realtime-nats\` to \`plugins\`, or set \`postgresBus\` in \`src/realtime.server.ts\`.`,
280
+ })
296
281
  diagnostics.push(...planOf({ root, preset, realtime: mod.default }).diagnostics)
297
282
  let verified = []
298
283
  if (verify) {
@@ -301,7 +286,9 @@ export function writeContract({ root, site = {}, preset, rust, live, verify }) {
301
286
  verified = done.verified
302
287
  }
303
288
  for (const [service, s] of Object.entries(contract.services ?? {})) {
304
- const why = s.kind === 'native' && nativeProblem({ service, preset, wasm: rust?.services?.[service]?.wasm })
289
+ const why =
290
+ s.kind === 'native' &&
291
+ nativeProblem({ service, preset, wasm: rust?.services?.[service]?.wasm })
305
292
  if (why) diagnostics.push({ code: 'R104', severity: 'error', why })
306
293
  }
307
294
  mkdirSync(join(root, '.weft'), { recursive: true })