@weft-js/cli 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 (171) hide show
  1. package/README.md +38 -14
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. package/ts-plugin/proxy.js +144 -0
@@ -0,0 +1,280 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { join, relative } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { diagnostic, locate } from '../diag.mjs'
6
+ import { start } from '../start.mjs'
7
+
8
+ const posix = (p) => p.split('\\').join('/')
9
+
10
+ /**
11
+ * R101, R102 from the contract, and R103, R109 from `cargo check`, for an app with a sidecar: an agent gets the same
12
+ * answer here as from `weft build`, with the function's id on it.
13
+ */
14
+ export async function rustChecks(root, loaded) {
15
+ let sidecar
16
+ try {
17
+ sidecar = await start(root, 'sidecar')
18
+ } catch {
19
+ return []
20
+ }
21
+ if (!sidecar.usesRemote(root)) return []
22
+ const rust = loaded.config?.rust
23
+ const held = process.env.WEFT_BUILD
24
+ process.env.WEFT_BUILD = '1'
25
+ try {
26
+ const { writeContract } = await start(root, 'contract')
27
+ const preset = { node: 'node', bun: 'bun', deno: 'deno_server' }[
28
+ loaded.config?.runtime ?? 'node'
29
+ ]
30
+ const { contract, diagnostics } = await writeContract({
31
+ root,
32
+ site: loaded.config?.site,
33
+ rust,
34
+ preset,
35
+ })
36
+ const out = diagnostics
37
+ .filter((d) => d.code.startsWith('R1'))
38
+ .map((d) => diagnostic({ ...d, file: '.weft/contract.json' }))
39
+ if (out.some((d) => d.severity === 'error')) return out
40
+ for (const service of Object.keys(contract.services ?? {})) {
41
+ if (rust?.services?.[service]?.external) continue
42
+ const dir = sidecar.crateDir(root, rust, service)
43
+ if (!existsSync(dir)) {
44
+ out.push(
45
+ diagnostic({
46
+ code: 'R109',
47
+ file: '.weft/contract.json',
48
+ why: `\`rust://${service}\` has no crate at \`${posix(relative(root, dir))}\`.`,
49
+ }),
50
+ )
51
+ continue
52
+ }
53
+ const checked = await sidecar.cargo('check', { root, dir, service, contract })
54
+ out.push(
55
+ ...checked.diagnostics.map(({ rendered, ...d }) => diagnostic({ ...d, help: rendered })),
56
+ )
57
+ out.push(...untouchedQueries(root, dir, contract, service))
58
+ if (contract.services[service].kind !== 'native' || !checked.ok) continue
59
+ const wasm = await sidecar.checkWasm({ root, dir, service, contract, preset, rust })
60
+ out.push(...wasm.diagnostics.map((d) => diagnostic(d)))
61
+ }
62
+ return out
63
+ } catch (e) {
64
+ return [
65
+ diagnostic({
66
+ code: 'R109',
67
+ severity: 'warning',
68
+ file: '.weft/contract.json',
69
+ why: `the contract couldn't be written, so the sidecars weren't checked: ${e.message.split('\n')[0]}`,
70
+ }),
71
+ ]
72
+ } finally {
73
+ if (held === undefined) delete process.env.WEFT_BUILD
74
+ else process.env.WEFT_BUILD = held
75
+ }
76
+ }
77
+
78
+ /** R105 before anything runs: a cached query with tags whose Rust never mentions `touch`, or hands `ctx` to anything. */
79
+ function untouchedQueries(root, dir, contract, service) {
80
+ const sources = readdirSync(join(dir, 'src'), { recursive: true, withFileTypes: true })
81
+ .filter((f) => f.isFile() && f.name.endsWith('.rs'))
82
+ .map((f) => join(f.parentPath, f.name))
83
+ const out = []
84
+ for (const f of contract.functions) {
85
+ if (f.impl?.service !== service || !f.cache || !(f.tags === 'dynamic' || f.tags?.length))
86
+ continue
87
+ const method = f.name.replace(/[A-Z]/g, (c, i) => `${i ? '_' : ''}${c.toLowerCase()}`)
88
+ for (const file of sources) {
89
+ const text = readFileSync(file, 'utf8')
90
+ const at = text.search(new RegExp(`\\bfn\\s+${method}\\s*\\(`))
91
+ if (at < 0) continue
92
+ const open = text.indexOf('{', at)
93
+ let depth = 0,
94
+ end = open
95
+ for (; end < text.length; end++) {
96
+ if (text[end] === '{') depth++
97
+ if (text[end] === '}' && --depth === 0) break
98
+ }
99
+ const body = text.slice(open, end)
100
+ const line = text.slice(0, at).split('\n').length
101
+ if (!/\.touch\(|\(\s*&?ctx\b|,\s*&?ctx\b/.test(body))
102
+ out.push(
103
+ diagnostic({
104
+ code: 'R105',
105
+ severity: 'warning',
106
+ file: posix(relative(root, file)),
107
+ span: [line, 1, line, 1],
108
+ why: `\`${f.id}\` is cached under tags and reports no table: nothing can tell whether they cover what it reads. Call \`ctx.touch("table")\`, or drop \`tags\` if it reads none.`,
109
+ }),
110
+ )
111
+ break
112
+ }
113
+ }
114
+ return out
115
+ }
116
+
117
+ /** R013, R014 and R018: what the deploy target does to live data. Only an app with `changes` or `required` loads anything. */
118
+ export async function liveChecks(root, loaded, preset) {
119
+ const file = join(root, 'src/realtime.server.ts')
120
+ let target
121
+ try {
122
+ target = await start(root, 'live-target')
123
+ } catch {
124
+ return []
125
+ }
126
+ const required = target.scanRequired(root)
127
+ if (
128
+ !required.length &&
129
+ !target.literalSecrets(root).length &&
130
+ !(existsSync(file) && /\bchanges\s*[:,}]/.test(readFileSync(file, 'utf8')))
131
+ )
132
+ return []
133
+ const held = process.env.WEFT_BUILD
134
+ process.env.WEFT_BUILD = '1'
135
+ try {
136
+ const { readLivePlan } = await start(root, 'contract')
137
+ const plan = await readLivePlan({
138
+ root,
139
+ site: loaded.config?.site,
140
+ live: loaded.config?.live,
141
+ preset:
142
+ preset ??
143
+ { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node'],
144
+ })
145
+ return plan.diagnostics.map((d) =>
146
+ diagnostic({ ...d, file: d.file ?? required[0]?.file ?? 'src/realtime.server.ts' }),
147
+ )
148
+ } catch (e) {
149
+ return [
150
+ diagnostic({
151
+ code: 'R014',
152
+ severity: 'warning',
153
+ file: 'src/realtime.server.ts',
154
+ why: `the deploy target couldn't be checked: ${e.message.split('\n')[0]}`,
155
+ }),
156
+ ]
157
+ } finally {
158
+ if (held === undefined) delete process.env.WEFT_BUILD
159
+ else process.env.WEFT_BUILD = held
160
+ }
161
+ }
162
+
163
+ /** R001 for a server module's `channel`, and R004 for a socket handler of the app's own (research 32 §1). */
164
+ export async function socketChecks(root) {
165
+ const src = join(root, 'src')
166
+ if (!existsSync(src)) return []
167
+ const out = []
168
+ let withIds
169
+ for (const f of readdirSync(src, { recursive: true, withFileTypes: true })) {
170
+ if (!f.isFile() || !/\.[cm]?[jt]sx?$/.test(f.name) || f.name.endsWith('.d.ts')) continue
171
+ const abs = join(f.parentPath, f.name),
172
+ rel = relative(root, abs).replaceAll('\\', '/'),
173
+ text = readFileSync(abs, 'utf8')
174
+ const hand = /from\s+['"]crossws|\bdefineWebSocketHandler\b/.exec(text)
175
+ if (hand)
176
+ out.push(
177
+ diagnostic({
178
+ code: 'R004',
179
+ file: rel,
180
+ ...locate(text, hand[0]),
181
+ why: 'A WebSocket handler of its own skips the Origin check, `authorize`, the limits and resume. Use `channel`.',
182
+ }),
183
+ )
184
+ if (
185
+ !/\b(channel|sharedState)\s*[(<]/.test(text) ||
186
+ !/^src\/server\/|\.server\.[cm]?[jt]sx?$/.test(rel)
187
+ )
188
+ continue
189
+ if (withIds === undefined) {
190
+ try {
191
+ withIds = (
192
+ await import(
193
+ pathToFileURL(createRequire(join(root, 'package.json')).resolve('@weft-js/start/vite'))
194
+ .href
195
+ )
196
+ ).withIds
197
+ } catch {
198
+ withIds = null
199
+ }
200
+ }
201
+ try {
202
+ withIds?.(text, abs, rel)
203
+ } catch (e) {
204
+ if (e.code !== 'R001') continue
205
+ const line = +/:(\d+) /.exec(e.message)?.[1]
206
+ out.push(
207
+ diagnostic({
208
+ code: 'R001',
209
+ file: rel,
210
+ span: [line, 1, line, 1],
211
+ why: e.message.split('\n')[0].replace(/^R001: \S+ /, ''),
212
+ }),
213
+ )
214
+ }
215
+ }
216
+ return out
217
+ }
218
+
219
+ /** weft owns the connection: these bypass reconnects, resume and the fake transport (research 30 §8). */
220
+ const SOCKET_PACKAGES = [
221
+ 'ws',
222
+ 'socket.io-client',
223
+ 'reconnecting-websocket',
224
+ '@microsoft/fetch-event-source',
225
+ ]
226
+
227
+ /** P002 (`package.json` scripts that aren't `weft …`), P120 (weft plugins wired straight into Vite), R007 (socket libraries). */
228
+ export function hygiene(root) {
229
+ const out = []
230
+ const pkgFile = join(root, 'package.json')
231
+ if (existsSync(pkgFile)) {
232
+ const text = readFileSync(pkgFile, 'utf8')
233
+ let scripts = {},
234
+ deps = {}
235
+ try {
236
+ const pkg = JSON.parse(text)
237
+ scripts = pkg.scripts ?? {}
238
+ deps = pkg.dependencies ?? {}
239
+ } catch {}
240
+ for (const name of SOCKET_PACKAGES) {
241
+ if (deps[name])
242
+ out.push(
243
+ diagnostic({
244
+ code: 'R007',
245
+ file: 'package.json',
246
+ ...locate(text, `"${name}"`),
247
+ why: `\`${name}\` is a dependency: its connection has no resume, no cleanup and no fake in @weft-js/core/test. Use \`stream\`, \`.live()\` or \`channel\`.`,
248
+ }),
249
+ )
250
+ }
251
+ for (const [name, cmd] of Object.entries(scripts)) {
252
+ if (!/^weft(\s|$)/.test(cmd.trim())) {
253
+ out.push(
254
+ diagnostic({
255
+ code: 'P002',
256
+ file: 'package.json',
257
+ ...locate(text, `"${name}"`),
258
+ why: `Script \`${name}\` runs \`${cmd}\`, not \`weft …\`: agents can't see what it does or what it touches.`,
259
+ }),
260
+ )
261
+ }
262
+ }
263
+ }
264
+ for (const name of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']) {
265
+ const file = join(root, name)
266
+ if (!existsSync(file)) continue
267
+ const text = readFileSync(file, 'utf8')
268
+ for (const m of text.matchAll(/from\s+['"](@weft-js\/(?:plugin-[\w-]+|text))/g)) {
269
+ out.push(
270
+ diagnostic({
271
+ code: 'P120',
272
+ file: name,
273
+ ...locate(text, m[1]),
274
+ why: `\`${m[1]}\` is registered in ${name}, which bypasses weft.config.ts.`,
275
+ }),
276
+ )
277
+ }
278
+ }
279
+ return out
280
+ }