@weft-js/start 0.0.0-stage → 0.1.0

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 +82 -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 +616 -0
  19. package/src/client.tsx +20 -0
  20. package/src/contract.mjs +308 -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 +456 -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 +144 -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 +198 -0
  63. package/src/rust.ts +333 -0
  64. package/src/server.tsx +1014 -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
@@ -0,0 +1,308 @@
1
+ // `.weft/contract.json` (PLAN §4.1, research 28 §4): every query and mutation as JSON Schema, with tags, cache and auth
2
+ // scope. `weft build` writes it; the OpenAPI export, the MCP server and Rust codegen read it. The server modules are
3
+ // loaded through Vite, never run: creating a database client connects to nothing.
4
+
5
+ import { createHash } from 'node:crypto'
6
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { join, resolve } from 'node:path'
8
+ import { createServer } from 'vite'
9
+ import weft from '@weft-js/core/vite'
10
+ import { literalSecrets, livePlan, refusal, resolveTarget, scanRequired } from './live-target.mjs'
11
+ import { readMigrations } from './migrations.mjs'
12
+ import { shapeTree, toJson } from './shape.mjs'
13
+ import weftStart, { startDir } from './vite.mjs'
14
+
15
+ const tagsOf = t => (typeof t === 'function' ? 'dynamic' : [...(t ?? [])])
16
+
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
+ const NO_FILES = /^(cloudflare|vercel[-_]edge|netlify[-_]edge|deno[-_]deploy|bunny|fastly)/
95
+ /** Presets that run where they were built, so the addon from this machine is the right one. */
96
+ const LOCAL = /^(node|bun|deno[-_]server)/
97
+
98
+ /** R104 for a build: `native()` on a preset that has no way to run it. */
99
+ 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 }
146
+ }
147
+
148
+ /** 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)/
150
+
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) && /\.live\(/.test(readFileSync(f, 'utf8'))
155
+ })
156
+ return existsSync(join(root, 'src')) && walk(join(root, 'src'))
157
+ }
158
+
159
+ /**
160
+ * R002 for one build: `bus` is the configured bus's name.
161
+ * @returns {string | undefined} the message when this build would drop messages
162
+ */
163
+ export const busProblem = ({ preset, bus, streams, root }) =>
164
+ bus === 'durable-object' && preset !== 'cloudflare_durable'
165
+ ? `\`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
+ : 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
169
+
170
+ /** What this deploy gets from live data, with the config loaded. R018 is a table `changes` lists and no migration covers. */
171
+ function planOf({ root, preset, realtime }) {
172
+ const source = realtime?.changes
173
+ const plan = livePlan({ preset, source: source && { name: source.name, external: source.external, tables: source.tables }, required: scanRequired(root) })
174
+ 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.` })
178
+ }
179
+ return plan
180
+ }
181
+
182
+ const verifiedFile = root => join(root, '.weft', 'verify.json')
183
+ const readVerified = root => { try { return JSON.parse(readFileSync(verifiedFile(root), 'utf8')) } catch { return {} } }
184
+
185
+ /**
186
+ * Every query's `live.verify`, run. `strict` is a build: a target that isn't reachable from here fails it, where a
187
+ * check warns and goes on. A pass is kept by the query's contract entry and target, so an unchanged query runs once.
188
+ * @returns {Promise<{ diagnostics: any[], verified: any[] }>}
189
+ */
190
+ 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)
194
+ if (!queries.length) return { diagnostics, verified }
195
+ const { roundTrip } = await server.ssrLoadModule(join(startDir, 'verify.ts'))
196
+ const cache = readVerified(root)
197
+ const contract = describe(fns).functions
198
+ const { env } = process
199
+ for (const f of queries) {
200
+ const name = f.$id.replace(/\..*$/, '')
201
+ const v = f.def.live.verify
202
+ const options = typeof v === 'object' ? v : {}
203
+ const target = resolveTarget(v, { targets: live?.targets, env })
204
+ 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.'}` })
206
+ continue
207
+ }
208
+ const stop = refusal(target, { targets: live?.targets })
209
+ if (stop) {
210
+ diagnostics.push({ code: 'R022', severity: 'error', why: `\`${name}\`: ${stop}.` })
211
+ continue
212
+ }
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)
214
+ if (cache[f.$id]?.key === key && !env.WEFT_VERIFY_FRESH) {
215
+ verified.push({ name, cached: true, ...cache[f.$id].result })
216
+ continue
217
+ }
218
+ const result = await roundTrip({ fn: f, source: realtime?.changes, url: target.url, db: target.db, options, env })
219
+ verified.push({ name, ...result })
220
+ if (result.ok) cache[f.$id] = { key, result, at: new Date().toISOString() }
221
+ else {
222
+ 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}.` })
224
+ }
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(' ')}` })
226
+ }
227
+ mkdirSync(join(root, '.weft'), { recursive: true })
228
+ writeFileSync(verifiedFile(root), JSON.stringify(cache, null, 2) + '\n')
229
+ return { diagnostics, verified }
230
+ }
231
+
232
+ /** The deploy's live plan on its own, for `weft check` and `weft doctor`: no contract is written. */
233
+ export const readLivePlan = ({ root, site, preset, live, verify = true }) => {
234
+ root = resolve(root)
235
+ return withFns({ root, site }, async (fns, server) => {
236
+ const realtime = (await server.ssrLoadModule('virtual:weft/realtime')).default
237
+ const plan = planOf({ root, preset, realtime })
238
+ if (!verify) return { ...plan, verified: [] }
239
+ const { diagnostics, verified } = await verifyAll({ root, fns, realtime, server, live, strict: false })
240
+ return { ...plan, diagnostics: [...plan.diagnostics, ...diagnostics], verified }
241
+ })
242
+ }
243
+
244
+ /** `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))
246
+
247
+ /** Every query's shape tree (research 31 §3), by id. */
248
+ export function shapeTable(fns) {
249
+ const table = {}
250
+ for (const f of fns.values()) {
251
+ const tree = f.kind === 'query' ? shapeTree(f.def) : undefined
252
+ if (tree !== undefined) table[f.$id] = tree
253
+ }
254
+ return table
255
+ }
256
+
257
+ /**
258
+ * `fn(fns, server)` with every server module loaded through a private Vite server. A dev server sets `NODE_ENV` to
259
+ * development when nothing had: that's put back after, or `weft build`, which reads the live plan first, would build
260
+ * with `import.meta.env.DEV` on and ship every dev-only diagnostic.
261
+ */
262
+ async function withFns({ root, site = {} }, fn) {
263
+ const env = process.env.NODE_ENV
264
+ let server
265
+ try {
266
+ server = await createServer({
267
+ root, configFile: false, logLevel: 'silent', appType: 'custom',
268
+ server: { middlewareMode: true, hmr: false, ws: false },
269
+ plugins: [weft({ hydratable: true }), weftStart({ root, site })],
270
+ })
271
+ await server.ssrLoadModule('virtual:weft/fns')
272
+ const { fns } = await server.ssrLoadModule(join(startDir, 'data.ts'))
273
+ return await fn(fns, server)
274
+ } finally {
275
+ await server?.close()
276
+ if (env === undefined) delete process.env.NODE_ENV
277
+ else process.env.NODE_ENV = env
278
+ }
279
+ }
280
+
281
+ /** 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)
283
+
284
+ /** Load every server module and write `.weft/contract.json`. */
285
+ export function writeContract({ root, site = {}, preset, rust, live, verify }) {
286
+ root = resolve(root)
287
+ return withFns({ root, site }, async (fns, server) => {
288
+ const { diagnostics, ...contract } = describe(fns, rust)
289
+ const mod = await server.ssrLoadModule('virtual:weft/realtime')
290
+ const bus = busProblem({ preset, bus: mod.default?.bus?.name ?? 'memory', streams: [...fns.values()].some(f => f.kind === 'stream' || f.kind === 'channel' || f.kind === 'shared'), root })
291
+ 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\`.` })
292
+ diagnostics.push(...planOf({ root, preset, realtime: mod.default }).diagnostics)
293
+ let verified = []
294
+ if (verify) {
295
+ const done = await verifyAll({ root, fns, realtime: mod.default, server, live, strict: true })
296
+ diagnostics.push(...done.diagnostics)
297
+ verified = done.verified
298
+ }
299
+ for (const [service, s] of Object.entries(contract.services ?? {})) {
300
+ const why = s.kind === 'native' && nativeProblem({ service, preset, wasm: rust?.services?.[service]?.wasm })
301
+ if (why) diagnostics.push({ code: 'R104', severity: 'error', why })
302
+ }
303
+ mkdirSync(join(root, '.weft'), { recursive: true })
304
+ const path = join(root, '.weft', 'contract.json')
305
+ writeFileSync(path, JSON.stringify(contract, null, 2) + '\n')
306
+ return { path, functions: contract.functions.length, diagnostics, contract, verified }
307
+ })
308
+ }