@weft-js/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +108 -0
  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 +174 -188
  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 +68 -444
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +199 -207
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +228 -224
  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 +142 -0
  74. package/src/deploy/files.mjs +266 -0
  75. package/src/deploy/fit.mjs +166 -0
  76. package/src/deploy/hosts.mjs +126 -0
  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 +387 -0
  81. package/src/deploy/needs.mjs +149 -0
  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 -42
  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 +144 -116
  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 +194 -92
  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
@@ -11,7 +11,11 @@ const { Session } = require('./session.cjs')
11
11
  const { root, write, debounce = 25, settle = 400, rest = 120_000 } = workerData
12
12
  const session = new Session(ts, root)
13
13
  const pending = new Set()
14
- let timer = null, running = false, last = '', writes = null, resting = null
14
+ let timer = null,
15
+ running = false,
16
+ last = '',
17
+ writes = null,
18
+ resting = null
15
19
 
16
20
  function publish(list) {
17
21
  const text = JSON.stringify(list)
@@ -24,13 +28,28 @@ function publish(list) {
24
28
  return true
25
29
  }
26
30
 
27
- const applyEdits = (text, edits) => [...edits].sort((a, b) => b.start - a.start).reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text)
31
+ const applyEdits = (text, edits) =>
32
+ [...edits]
33
+ .sort((a, b) => b.start - a.start)
34
+ .reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text)
28
35
 
29
36
  /** Exported types go into the source once the file has been quiet, and only if it's still the text they were made from. */
30
37
  function scheduleWrites(findings) {
31
38
  clearTimeout(writes)
32
39
  if (!write) return
33
- const jobs = [...findings].flatMap(([file, list]) => { const edits = list.filter(f => f.safe && f.edits).flatMap(f => f.edits); return edits.length ? [{ file, edits, hash: session.engine.facts.get(file)?.hash, codes: list.map(f => f.code) }] : [] })
40
+ const jobs = [...findings].flatMap(([file, list]) => {
41
+ const edits = list.filter((f) => f.safe && f.edits).flatMap((f) => f.edits)
42
+ return edits.length
43
+ ? [
44
+ {
45
+ file,
46
+ edits,
47
+ hash: session.engine.facts.get(file)?.hash,
48
+ codes: list.map((f) => f.code),
49
+ },
50
+ ]
51
+ : []
52
+ })
34
53
  if (!jobs.length) return
35
54
  writes = setTimeout(() => {
36
55
  for (const job of jobs) {
@@ -51,18 +70,39 @@ function rested() {
51
70
 
52
71
  async function first() {
53
72
  running = true
54
- try { await report(performance.now()) } catch (e) { parentPort.postMessage({ type: 'failed', message: e.stack }) } finally { running = false }
73
+ try {
74
+ await report(performance.now())
75
+ } catch (e) {
76
+ parentPort.postMessage({ type: 'failed', message: e.stack })
77
+ } finally {
78
+ running = false
79
+ }
55
80
  await drain()
56
81
  }
57
82
 
58
83
  async function report(t, files) {
59
84
  const { findings, errors } = await session.analyze(files)
60
85
  const flat = []
61
- for (const [file, list] of [...findings, ...errors]) for (const f of list) flat.push({ file: relative(root, file), line: session.engine.texts.get(file).slice(0, f.start).split('\n').length, code: f.code, severity: f.severity ?? 'error', why: f.why, help: f.help })
86
+ for (const [file, list] of [...findings, ...errors])
87
+ for (const f of list)
88
+ flat.push({
89
+ file: relative(root, file),
90
+ line: session.engine.texts.get(file).slice(0, f.start).split('\n').length,
91
+ code: f.code,
92
+ severity: f.severity ?? 'error',
93
+ why: f.why,
94
+ help: f.help,
95
+ })
62
96
  scheduleWrites(findings)
63
97
  const changed = publish(flat)
64
98
  rested()
65
- parentPort.postMessage({ type: 'done', ms: performance.now() - t, at: Date.now(), changed, diagnostics: flat })
99
+ parentPort.postMessage({
100
+ type: 'done',
101
+ ms: performance.now() - t,
102
+ at: Date.now(),
103
+ changed,
104
+ diagnostics: flat,
105
+ })
66
106
  }
67
107
 
68
108
  async function drain() {
@@ -76,7 +116,10 @@ async function drain() {
76
116
  let changed = false
77
117
  for (const f of files) if (session.engine.set(f)) changed = true
78
118
  if (session.host) session.touch(files)
79
- if (!changed || !files.some(f => session.engine.relevant(f))) { parentPort.postMessage({ type: 'idle', ms: performance.now() - t }); continue }
119
+ if (!changed || !files.some((f) => session.engine.relevant(f))) {
120
+ parentPort.postMessage({ type: 'idle', ms: performance.now() - t })
121
+ continue
122
+ }
80
123
  await report(t, files)
81
124
  }
82
125
  } catch (e) {
@@ -86,8 +129,13 @@ async function drain() {
86
129
  }
87
130
  }
88
131
 
89
- parentPort.on('message', msg => {
90
- if (msg === 'close') { clearTimeout(timer); clearTimeout(writes); session.close(); process.exit(0) }
132
+ parentPort.on('message', (msg) => {
133
+ if (msg === 'close') {
134
+ clearTimeout(timer)
135
+ clearTimeout(writes)
136
+ session.close()
137
+ process.exit(0)
138
+ }
91
139
  pending.add(msg)
92
140
  clearTimeout(timer)
93
141
  timer = setTimeout(() => void drain(), debounce)
package/src/jsx-scan.mjs CHANGED
@@ -16,51 +16,53 @@ import ts from 'typescript'
16
16
  * @returns {{ imports: Import[], elements: Element[], functions: Map<string, string> }}
17
17
  */
18
18
  export function scanJsx(text, file = 'file.tsx') {
19
- const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.TSX)
19
+ const sf = ts.createSourceFile(
20
+ file,
21
+ text,
22
+ ts.ScriptTarget.Latest,
23
+ true,
24
+ file.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
25
+ )
20
26
  const imports = []
21
27
  const elements = []
22
28
  const functions = new Map()
23
29
  const stack = []
24
30
 
25
- const attrsOf = attributes =>
26
- attributes.properties.map(p => {
27
- if (ts.isJsxSpreadAttribute(p)) return { name: '...', kind: 'spread', expr: p.expression.getText(sf), start: p.getStart(sf), end: p.end }
28
- const name = p.name.getText(sf)
29
- const base = { name, start: p.getStart(sf), end: p.end }
30
- if (!p.initializer) return { ...base, kind: 'bool' }
31
- if (ts.isStringLiteral(p.initializer)) return { ...base, kind: 'string', value: p.initializer.text, valueStart: p.initializer.getStart(sf), valueEnd: p.initializer.end }
32
- const e = p.initializer.expression
33
- if (e && (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))) return { ...base, kind: 'string', value: e.text, valueStart: e.getStart(sf), valueEnd: e.end }
34
- return { ...base, kind: 'expr', expr: e ? e.getText(sf) : '', valueStart: e?.getStart(sf), valueEnd: e?.end }
35
- })
36
-
37
31
  const record = (open, node) => {
38
- const attrs = attrsOf(open.attributes)
32
+ const attrs = attrsOf(sf, open.attributes)
39
33
  const el = {
40
34
  name: open.tagName.getText(sf),
41
35
  attrs,
42
- attr: n => attrs.find(a => a.name === n),
36
+ attr: (n) => attrs.find((a) => a.name === n),
43
37
  ancestors: [...stack],
44
38
  start: node.getStart(sf),
45
39
  end: node.end,
46
40
  tagEnd: open.tagName.end,
47
- children: ts.isJsxElement(node) ? node.children.map(c => c.getText(sf)).join('') : '',
41
+ children: ts.isJsxElement(node) ? node.children.map((c) => c.getText(sf)).join('') : '',
48
42
  }
49
43
  elements.push(el)
50
44
  return el
51
45
  }
52
46
 
53
- const visit = node => {
47
+ const visit = (node) => {
54
48
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
55
49
  const source = node.moduleSpecifier.text
56
50
  const c = node.importClause
57
51
  const at = { source, start: node.getStart(sf), end: node.end }
58
52
  if (c?.name) imports.push({ local: c.name.text, ...at })
59
- if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const s of c.namedBindings.elements) imports.push({ local: s.name.text, imported: s.propertyName?.text ?? s.name.text, ...at })
53
+ if (c?.namedBindings && ts.isNamedImports(c.namedBindings))
54
+ for (const s of c.namedBindings.elements)
55
+ imports.push({ local: s.name.text, imported: s.propertyName?.text ?? s.name.text, ...at })
60
56
  if (!c) imports.push({ local: '', ...at })
61
57
  }
62
- if (ts.isFunctionDeclaration(node) && node.name && node.body) functions.set(node.name.text, node.body.getText(sf))
63
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
58
+ if (ts.isFunctionDeclaration(node) && node.name && node.body)
59
+ functions.set(node.name.text, node.body.getText(sf))
60
+ if (
61
+ ts.isVariableDeclaration(node) &&
62
+ ts.isIdentifier(node.name) &&
63
+ node.initializer &&
64
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
65
+ ) {
64
66
  functions.set(node.name.text, node.initializer.body.getText(sf))
65
67
  }
66
68
  if (ts.isJsxSelfClosingElement(node)) {
@@ -82,5 +84,45 @@ export function scanJsx(text, file = 'file.tsx') {
82
84
  /** The import an identifier in an expression (`{hero}`, `{hero.src}`) comes from, or undefined. */
83
85
  export function importOf(scan, expr) {
84
86
  const id = /^\s*([A-Za-z_$][\w$]*)/.exec(expr ?? '')?.[1]
85
- return id ? scan.imports.find(i => i.local === id) : undefined
87
+ return id ? scan.imports.find((i) => i.local === id) : undefined
88
+ }
89
+
90
+ function attrsOf(sf, attributes) {
91
+ return attributes.properties.map((p) => {
92
+ if (ts.isJsxSpreadAttribute(p))
93
+ return {
94
+ name: '...',
95
+ kind: 'spread',
96
+ expr: p.expression.getText(sf),
97
+ start: p.getStart(sf),
98
+ end: p.end,
99
+ }
100
+ const name = p.name.getText(sf)
101
+ const base = { name, start: p.getStart(sf), end: p.end }
102
+ if (!p.initializer) return { ...base, kind: 'bool' }
103
+ if (ts.isStringLiteral(p.initializer))
104
+ return {
105
+ ...base,
106
+ kind: 'string',
107
+ value: p.initializer.text,
108
+ valueStart: p.initializer.getStart(sf),
109
+ valueEnd: p.initializer.end,
110
+ }
111
+ const e = p.initializer.expression
112
+ if (e && (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)))
113
+ return {
114
+ ...base,
115
+ kind: 'string',
116
+ value: e.text,
117
+ valueStart: e.getStart(sf),
118
+ valueEnd: e.end,
119
+ }
120
+ return {
121
+ ...base,
122
+ kind: 'expr',
123
+ expr: e ? e.getText(sf) : '',
124
+ valueStart: e?.getStart(sf),
125
+ valueEnd: e?.end,
126
+ }
127
+ })
86
128
  }
@@ -0,0 +1,281 @@
1
+ function baseSchemas(z) {
2
+ const zodSchema = z.custom(
3
+ (v) => v != null && typeof v === 'object' && ('_zod' in v || '~standard' in v),
4
+ )
5
+ const globs = z.array(z.string())
6
+
7
+ const task = z.strictObject({
8
+ run: z.string().min(1),
9
+ description: z.string().optional(),
10
+ sideEffects: z.array(z.string()).optional(),
11
+ env: z.array(z.string()).optional(),
12
+ args: z.union([zodSchema, z.record(z.string(), z.unknown())]).optional(),
13
+ })
14
+
15
+ const pluginEntry = z.looseObject({
16
+ name: z.string().min(1),
17
+ version: z.string().optional(),
18
+ provides: z
19
+ .array(z.union([z.string(), z.strictObject({ slot: z.string(), key: z.string() })]))
20
+ .optional(),
21
+ })
22
+
23
+ const image = z.strictObject({
24
+ url: z.string().min(1),
25
+ width: z.int().positive().optional(),
26
+ height: z.int().positive().optional(),
27
+ alt: z.string().optional(),
28
+ })
29
+
30
+ const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, { error: 'must be #rrggbb' })
31
+ const colors = {
32
+ background: hex.optional(),
33
+ text: hex.optional(),
34
+ subtle: hex.optional(),
35
+ accent: hex.optional(),
36
+ }
37
+ const cardFont = z.strictObject({
38
+ family: z.string().regex(/^[\w -]+$/, { error: 'must be a font family name' }),
39
+ src: z.string().min(1),
40
+ weight: z.number().optional(),
41
+ })
42
+ const card = z.strictObject({
43
+ ...colors,
44
+ scheme: z.enum(['dark', 'light']).optional(),
45
+ dark: z.strictObject(colors).optional(),
46
+ light: z.strictObject(colors).optional(),
47
+ fonts: z.strictObject({ display: cardFont.optional(), body: cardFont.optional() }).optional(),
48
+ })
49
+
50
+ const site = z.strictObject({
51
+ headers: z.record(z.string(), z.record(z.string(), z.string())).optional(),
52
+ name: z.string().min(1).optional(),
53
+ url: z.url({ protocol: /^https?$/, error: 'must be an absolute http(s) URL' }).optional(),
54
+ title: z.string().includes('%s', { error: 'must contain `%s` (the page title)' }).optional(),
55
+ description: z.string().optional(),
56
+ locale: z
57
+ .string()
58
+ .regex(/^[a-z]{2,3}(_[A-Z]{2})?$/, { error: 'must look like `en_US`' })
59
+ .optional(),
60
+ inlineCss: z.boolean().optional(),
61
+ criticalCss: z.boolean().optional(),
62
+ criticalKeep: z.array(z.string()).optional(),
63
+ image: z.union([z.literal('auto'), z.literal(false), z.string().min(1), image]).optional(),
64
+ card: card.optional(),
65
+ head: z.string().optional(),
66
+ twitter: z
67
+ .strictObject({
68
+ card: z.enum(['summary', 'summary_large_image', 'app', 'player']).optional(),
69
+ site: z.string().optional(),
70
+ creator: z.string().optional(),
71
+ })
72
+ .optional(),
73
+ })
74
+
75
+ return { globs, task, pluginEntry, site }
76
+ }
77
+
78
+ function mainProperties(z, { globs, task, pluginEntry, site }) {
79
+ return {
80
+ scope: z
81
+ .strictObject({ write: globs.optional(), read: globs.optional(), forbid: globs.optional() })
82
+ .optional(),
83
+ tasks: z.record(z.string(), task).optional(),
84
+ environments: z.record(z.string(), z.looseObject({ url: z.string().optional() })).optional(),
85
+ test: z
86
+ .strictObject({
87
+ include: globs.optional(),
88
+ lanes: z.array(z.enum(['fast', 'browser', 'e2e'])).optional(),
89
+ gate: z
90
+ .strictObject({
91
+ mutationBudgetSec: z.number().positive().optional(),
92
+ maxMutants: z.number().int().positive().optional(),
93
+ })
94
+ .optional(),
95
+ })
96
+ .optional(),
97
+ build: z
98
+ .strictObject({
99
+ audit: z
100
+ .union([
101
+ z.boolean(),
102
+ z.strictObject({
103
+ routes: z.array(z.string()).optional(),
104
+ all: z.boolean().optional(),
105
+ max: z.number().int().positive().optional(),
106
+ indexing: z.enum(['auto', 'public', 'private']).optional(),
107
+ }),
108
+ ])
109
+ .optional(),
110
+ unsafeAllow: z
111
+ .record(
112
+ z.string().regex(/^(all|[A-Z]|[A-Z]+\d{3})$/, 'a code (R002), a family (L) or all'),
113
+ z.string().min(1, 'say why'),
114
+ )
115
+ .optional(),
116
+ })
117
+ .optional(),
118
+ check: z
119
+ .strictObject({
120
+ include: globs.optional(),
121
+ exclude: globs.optional(),
122
+ design: z.boolean().optional(),
123
+ routes: z.boolean().optional(),
124
+ props: z.union([z.boolean(), z.literal('manual')]).optional(),
125
+ })
126
+ .optional(),
127
+ plugins: z.array(pluginEntry).optional(),
128
+ slots: z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())])).optional(),
129
+ site: site.optional(),
130
+ runtime: z.enum(['node', 'bun', 'deno']).optional(),
131
+ hydration: z.enum(['lazy', 'partial', 'full']).optional(),
132
+ rust: z
133
+ .strictObject({
134
+ unions: z.literal('allow').optional(),
135
+ services: z
136
+ .record(
137
+ z.string(),
138
+ z.strictObject({
139
+ crate: z.string().optional(),
140
+ external: z.boolean().optional(),
141
+ wasm: z.literal(false).optional(),
142
+ }),
143
+ )
144
+ .optional(),
145
+ })
146
+ .optional(),
147
+ }
148
+ }
149
+
150
+ function deployFirst(z) {
151
+ return {
152
+ target: z.enum(['netlify', 'vercel', 'cloudflare', 'fly', 'render']).optional(),
153
+ netlify: z.strictObject({ site: z.string().min(1).optional() }).optional(),
154
+ vercel: z
155
+ .strictObject({
156
+ project: z
157
+ .string()
158
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
159
+ .optional(),
160
+ })
161
+ .optional(),
162
+ cloudflare: z
163
+ .strictObject({
164
+ name: z
165
+ .string()
166
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
167
+ .optional(),
168
+ })
169
+ .optional(),
170
+ }
171
+ }
172
+
173
+ function deployFly(z) {
174
+ return {
175
+ fly: z
176
+ .strictObject({
177
+ app: z
178
+ .string()
179
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
180
+ .optional(),
181
+ region: z
182
+ .string()
183
+ .regex(/^[a-z]{3}$/, 'a Fly region code, like `iad`')
184
+ .optional(),
185
+ memory: z
186
+ .string()
187
+ .regex(/^\d+(mb|gb)$/, 'like `512mb`')
188
+ .optional(),
189
+ volume: z
190
+ .strictObject({
191
+ name: z
192
+ .string()
193
+ .regex(/^[a-z0-9_]+$/, 'lowercase letters, digits and underscores')
194
+ .optional(),
195
+ path: z.string().startsWith('/').optional(),
196
+ sizeGb: z.number().int().positive().optional(),
197
+ })
198
+ .optional(),
199
+ })
200
+ .optional(),
201
+ }
202
+ }
203
+
204
+ function deployRest(z) {
205
+ return {
206
+ render: z
207
+ .strictObject({
208
+ service: z
209
+ .string()
210
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
211
+ .optional(),
212
+ plan: z.string().optional(),
213
+ region: z.string().optional(),
214
+ disk: z
215
+ .strictObject({
216
+ path: z.string().startsWith('/').optional(),
217
+ sizeGb: z.number().int().positive().optional(),
218
+ })
219
+ .optional(),
220
+ })
221
+ .optional(),
222
+ ci: z.union([z.literal('github'), z.literal(false)]).optional(),
223
+ branch: z.string().min(1).optional(),
224
+ cdn: z
225
+ .strictObject({
226
+ proxy: z.literal('cloudflare').optional(),
227
+ images: z
228
+ .strictObject({
229
+ provider: z.literal('cloudinary'),
230
+ cloud: z
231
+ .string()
232
+ .regex(/^[\w-]+$/, "the cloud's name, not its URL")
233
+ .optional(),
234
+ })
235
+ .optional(),
236
+ })
237
+ .optional(),
238
+ }
239
+ }
240
+
241
+ function deploySchema(z) {
242
+ return z.strictObject({ ...deployFirst(z), ...deployFly(z), ...deployRest(z) }).optional()
243
+ }
244
+
245
+ function remainingProperties(z) {
246
+ return {
247
+ /** Named targets for a query's `live: { verify: 'staging' }`. `db` is an env var's name. */
248
+ live: z
249
+ .strictObject({
250
+ targets: z
251
+ .record(
252
+ z.string(),
253
+ z.strictObject({
254
+ url: z.string().optional(),
255
+ db: z.string().optional(),
256
+ production: z.boolean().optional(),
257
+ }),
258
+ )
259
+ .optional(),
260
+ })
261
+ .optional(),
262
+ trust: z.enum(['enforce', 'warn']).optional(),
263
+ }
264
+ }
265
+
266
+ export function createSchemas(z) {
267
+ const base = baseSchemas(z)
268
+ const configSchema = z.strictObject({
269
+ ...mainProperties(z, base),
270
+ deploy: deploySchema(z),
271
+ ...remainingProperties(z),
272
+ })
273
+ const { pluginEntry } = base
274
+ const dataSchema = configSchema.extend({
275
+ $schema: z.string().optional(),
276
+ plugins: z
277
+ .array(z.union([z.string(), z.tuple([z.string(), z.unknown()]), pluginEntry]))
278
+ .optional(),
279
+ })
280
+ return { configSchema, dataSchema }
281
+ }