@weft-js/cli 0.1.1 → 0.1.3

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 +534 -54
  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
@@ -11,21 +11,35 @@ const USER_TYPED = /\b(?:location\.|URLSearchParams|useSearchParams|searchParams
11
11
  /** warp components that take `ui`. Anything else would write `ui="[object Object]"` on its element. */
12
12
  const TAKES_UI = new Set(['Button', 'Checkbox', 'Switch', 'Radio', 'Toggle', 'Chip', 'Card'])
13
13
 
14
- const rowsOf = sf => {
14
+ const rowsOf = (sf) => {
15
15
  const rows = new Set()
16
- const visit = n => {
17
- if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.initializer && ts.isCallExpression(n.initializer) && n.initializer.expression.getText?.(sf) === 'interaction') rows.add(n.name.text)
16
+ const visit = (n) => {
17
+ if (
18
+ ts.isVariableDeclaration(n) &&
19
+ ts.isIdentifier(n.name) &&
20
+ n.initializer &&
21
+ ts.isCallExpression(n.initializer) &&
22
+ n.initializer.expression.getText?.(sf) === 'interaction'
23
+ )
24
+ rows.add(n.name.text)
18
25
  ts.forEachChild(n, visit)
19
26
  }
20
27
  visit(sf)
21
28
  return rows
22
29
  }
23
30
 
24
- const warpNames = sf => {
31
+ const warpNames = (sf) => {
25
32
  const names = new Map()
26
33
  for (const n of sf.statements) {
27
- if (!ts.isImportDeclaration(n) || !n.moduleSpecifier.text.startsWith('@weft-js/warp') || !n.importClause?.namedBindings || !ts.isNamedImports(n.importClause.namedBindings)) continue
28
- for (const s of n.importClause.namedBindings.elements) names.set(s.name.text, (s.propertyName ?? s.name).text)
34
+ if (
35
+ !ts.isImportDeclaration(n) ||
36
+ !n.moduleSpecifier.text.startsWith('@weft-js/warp') ||
37
+ !n.importClause?.namedBindings ||
38
+ !ts.isNamedImports(n.importClause.namedBindings)
39
+ )
40
+ continue
41
+ for (const s of n.importClause.namedBindings.elements)
42
+ names.set(s.name.text, (s.propertyName ?? s.name).text)
29
43
  }
30
44
  return names
31
45
  }
@@ -34,41 +48,111 @@ export function shareChecks(root, { files }) {
34
48
  const out = []
35
49
  let sharesIn
36
50
  for (const { rel, text } of files) {
37
- const usesRow = text.includes('@weft-js/core/interaction')
38
- if (!usesRow && !/\bui=\{/.test(text)) continue
39
- const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
40
- const pos = positions(text)
41
- const report = (code, n, why) => out.push(diagnostic({ code, file: rel, span: pos.span(n.getStart(sf), n.end), offset: [n.getStart(sf), n.end], why }))
42
- const rows = rowsOf(sf), warp = warpNames(sf)
43
- const visit = n => {
44
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'interaction' && usesRow) {
45
- const share = n.arguments[0] && ts.isObjectLiteralExpression(n.arguments[0]) ? n.arguments[0].properties.find(p => p.name?.getText(sf) === 'share') : undefined
46
- if (share) {
47
- sharesIn ??= rel
48
- const value = share.getText(sf)
49
- if (UNSTABLE.test(value)) report('U001', share, 'A `share` key that changes on every load never meets another tab or client.')
50
- else if (USER_TYPED.test(value)) report('U002', share, 'This `share` key comes from what the user typed or the URL, so `realtime({ share })` is authorising a string they chose.')
51
- }
52
- }
53
- if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
54
- const l = n.left
55
- const target = ts.isPropertyAccessExpression(l) && l.name.text === 'value' ? l.expression : l
56
- if (ts.isPropertyAccessExpression(target) && target.name.text === 'remote' && ts.isIdentifier(target.expression) && rows.has(target.expression.text))
57
- report('U004', n, '`remote` is what others are doing: only the network writes it.')
58
- }
59
- if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {
60
- const tag = n.tagName.getText(sf), ui = n.attributes.properties.find(a => ts.isJsxAttribute(a) && a.name.getText(sf) === 'ui')
61
- if (ui && warp.has(tag) && !TAKES_UI.has(warp.get(tag))) report('U005', ui, `\`${warp.get(tag)}\` doesn't take \`ui\` yet, so this writes an attribute and draws nothing.`)
62
- }
63
- ts.forEachChild(n, visit)
64
- }
65
- visit(sf)
51
+ sharesIn = checkFile(rel, text, out, sharesIn)
66
52
  }
67
53
  if (sharesIn && existsSync(join(root, 'src/routes'))) {
68
- const file = ['src/realtime.server.ts', 'src/realtime.server.js'].map(f => join(root, f)).find(existsSync)
54
+ const file = ['src/realtime.server.ts', 'src/realtime.server.js']
55
+ .map((f) => join(root, f))
56
+ .find(existsSync)
69
57
  if (!file || !/\bshare\s*:/.test(readFileSync(file, 'utf8'))) {
70
- out.push(diagnostic({ code: 'U003', file: sharesIn, span: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }, offset: [0, 0], why: 'This app shares UI state and no `realtime({ share })` says who may: tabs work, and the socket refuses every key.' }))
58
+ out.push(
59
+ diagnostic({
60
+ code: 'U003',
61
+ file: sharesIn,
62
+ span: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
63
+ offset: [0, 0],
64
+ why: 'This app shares UI state and no `realtime({ share })` says who may: tabs work, and the socket refuses every key.',
65
+ }),
66
+ )
71
67
  }
72
68
  }
73
69
  return out
74
70
  }
71
+
72
+ function checkFile(rel, text, out, sharesIn) {
73
+ const usesRow = text.includes('@weft-js/core/interaction')
74
+ if (!usesRow && !/\bui=\{/.test(text)) return sharesIn
75
+ const sf = ts.createSourceFile(
76
+ rel,
77
+ text,
78
+ ts.ScriptTarget.Latest,
79
+ true,
80
+ /\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
81
+ )
82
+ const pos = positions(text)
83
+ const report = (code, n, why) =>
84
+ out.push(
85
+ diagnostic({
86
+ code,
87
+ file: rel,
88
+ span: pos.span(n.getStart(sf), n.end),
89
+ offset: [n.getStart(sf), n.end],
90
+ why,
91
+ }),
92
+ )
93
+ const rows = rowsOf(sf),
94
+ warp = warpNames(sf)
95
+ const markShare = () => {
96
+ sharesIn ??= rel
97
+ }
98
+ const visit = makeVisitor({ sf, usesRow, rows, warp, report, markShare })
99
+ visit(sf)
100
+ return sharesIn
101
+ }
102
+
103
+ function makeVisitor({ sf, usesRow, rows, warp, report, markShare }) {
104
+ const visit = (n) => {
105
+ if (
106
+ ts.isCallExpression(n) &&
107
+ ts.isIdentifier(n.expression) &&
108
+ n.expression.text === 'interaction' &&
109
+ usesRow
110
+ ) {
111
+ const share =
112
+ n.arguments[0] && ts.isObjectLiteralExpression(n.arguments[0])
113
+ ? n.arguments[0].properties.find((p) => p.name?.getText(sf) === 'share')
114
+ : undefined
115
+ if (share) {
116
+ markShare()
117
+ const value = share.getText(sf)
118
+ if (UNSTABLE.test(value))
119
+ report(
120
+ 'U001',
121
+ share,
122
+ 'A `share` key that changes on every load never meets another tab or client.',
123
+ )
124
+ else if (USER_TYPED.test(value))
125
+ report(
126
+ 'U002',
127
+ share,
128
+ 'This `share` key comes from what the user typed or the URL, so `realtime({ share })` is authorising a string they chose.',
129
+ )
130
+ }
131
+ }
132
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
133
+ const l = n.left
134
+ const target = ts.isPropertyAccessExpression(l) && l.name.text === 'value' ? l.expression : l
135
+ if (
136
+ ts.isPropertyAccessExpression(target) &&
137
+ target.name.text === 'remote' &&
138
+ ts.isIdentifier(target.expression) &&
139
+ rows.has(target.expression.text)
140
+ )
141
+ report('U004', n, '`remote` is what others are doing: only the network writes it.')
142
+ }
143
+ if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {
144
+ const tag = n.tagName.getText(sf),
145
+ ui = n.attributes.properties.find(
146
+ (a) => ts.isJsxAttribute(a) && a.name.getText(sf) === 'ui',
147
+ )
148
+ if (ui && warp.has(tag) && !TAKES_UI.has(warp.get(tag)))
149
+ report(
150
+ 'U005',
151
+ ui,
152
+ `\`${warp.get(tag)}\` doesn't take \`ui\` yet, so this writes an attribute and draws nothing.`,
153
+ )
154
+ }
155
+ ts.forEachChild(n, visit)
156
+ }
157
+ return visit
158
+ }
package/src/site.mjs CHANGED
@@ -7,13 +7,40 @@ const absolute = /^(https?:)?\/\//
7
7
  export function checkSite(site, report) {
8
8
  const image = typeof site.image === 'object' ? site.image.url : site.image
9
9
  if (image && !site.url && !absolute.test(image)) {
10
- const what = image === 'auto' ? 'The generated card (`image: \'auto\'`) is served from a relative URL' : `\`site.image\` (\`${image}\`) is relative`
11
- report('H001', `${what} and \`site.url\` is not set, so og:image (and canonical, og:url) can't be absolute. Crawlers don't resolve relative URLs.`, 'image')
10
+ const what =
11
+ image === 'auto'
12
+ ? "The generated card (`image: 'auto'`) is served from a relative URL"
13
+ : `\`site.image\` (\`${image}\`) is relative`
14
+ report(
15
+ 'H001',
16
+ `${what} and \`site.url\` is not set, so og:image (and canonical, og:url) can't be absolute. Crawlers don't resolve relative URLs.`,
17
+ 'image',
18
+ )
12
19
  }
13
- if (!site.description?.trim()) report('H003', 'Pages have no description: `site.description` is not set and no route overrides it yet.', 'site')
20
+ if (!site.description?.trim())
21
+ report(
22
+ 'H003',
23
+ 'Pages have no description: `site.description` is not set and no route overrides it yet.',
24
+ 'site',
25
+ )
14
26
  // The default page title is `name`; a template's fixed part is in every other title.
15
27
  const fixed = site.title ? site.title.replace('%s', '').length : 0
16
- if (site.name && site.name.length > 60) report('H004', `The default title \`${site.name}\` is ${site.name.length} characters (over 60).`, 'name')
17
- if (fixed > 40) report('H004', `The title template's fixed part is ${fixed} characters, leaving under 20 of 60 for the page title.`, 'title')
18
- if (site.description && site.description.length > 160) report('H004', `\`site.description\` is ${site.description.length} characters (over 160).`, 'description')
28
+ if (site.name && site.name.length > 60)
29
+ report(
30
+ 'H004',
31
+ `The default title \`${site.name}\` is ${site.name.length} characters (over 60).`,
32
+ 'name',
33
+ )
34
+ if (fixed > 40)
35
+ report(
36
+ 'H004',
37
+ `The title template's fixed part is ${fixed} characters, leaving under 20 of 60 for the page title.`,
38
+ 'title',
39
+ )
40
+ if (site.description && site.description.length > 160)
41
+ report(
42
+ 'H004',
43
+ `\`site.description\` is ${site.description.length} characters (over 160).`,
44
+ 'description',
45
+ )
19
46
  }
package/src/slots.mjs CHANGED
@@ -6,21 +6,92 @@
6
6
  */
7
7
  export const slotTable = [
8
8
  { id: 'font.build', version: 1, cardinality: 'one', sides: ['build'], default: null, phase: '2' },
9
- { id: 'media.build', version: 1, cardinality: 'one', sides: ['build'], default: null, phase: '2' },
10
- { id: 'media.encode', version: 1, cardinality: 'keyed', sides: ['build', 'server'], default: null, phase: '3c' },
11
- { id: 'media.server', version: 1, cardinality: 'one', sides: ['server'], default: null, phase: '3' },
12
- { id: 'text.measure', version: 1, cardinality: 'one', sides: ['client', 'worker'], default: null, phase: '2 (Surfaces)' },
13
- { id: 'surface.renderer', version: 1, cardinality: 'keyed', sides: ['client'], default: ['canvas2d', 'webgl2'], phase: '2 (Surfaces)' },
14
- { id: 'realtime.bus', version: 1, cardinality: 'one', sides: ['server'], default: 'memory', phase: '3' },
15
- { id: 'db.adapter', version: 1, cardinality: 'keyed', sides: ['server'], default: ['drizzle'], phase: '3c' },
16
- { id: 'server.extension', version: 1, cardinality: 'keyed', sides: ['server'], default: null, phase: '6c' },
17
- { id: 'pay.rail', version: 1, cardinality: 'keyed', sides: ['server', 'client', 'native'], default: null, phase: '6c' },
9
+ {
10
+ id: 'media.build',
11
+ version: 1,
12
+ cardinality: 'one',
13
+ sides: ['build'],
14
+ default: null,
15
+ phase: '2',
16
+ },
17
+ {
18
+ id: 'media.encode',
19
+ version: 1,
20
+ cardinality: 'keyed',
21
+ sides: ['build', 'server'],
22
+ default: null,
23
+ phase: '3c',
24
+ },
25
+ {
26
+ id: 'media.server',
27
+ version: 1,
28
+ cardinality: 'one',
29
+ sides: ['server'],
30
+ default: null,
31
+ phase: '3',
32
+ },
33
+ {
34
+ id: 'text.measure',
35
+ version: 1,
36
+ cardinality: 'one',
37
+ sides: ['client', 'worker'],
38
+ default: null,
39
+ phase: '2 (Surfaces)',
40
+ },
41
+ {
42
+ id: 'surface.renderer',
43
+ version: 1,
44
+ cardinality: 'keyed',
45
+ sides: ['client'],
46
+ default: ['canvas2d', 'webgl2'],
47
+ phase: '2 (Surfaces)',
48
+ },
49
+ {
50
+ id: 'realtime.bus',
51
+ version: 1,
52
+ cardinality: 'one',
53
+ sides: ['server'],
54
+ default: 'memory',
55
+ phase: '3',
56
+ },
57
+ {
58
+ id: 'db.adapter',
59
+ version: 1,
60
+ cardinality: 'keyed',
61
+ sides: ['server'],
62
+ default: ['drizzle'],
63
+ phase: '3c',
64
+ },
65
+ {
66
+ id: 'server.extension',
67
+ version: 1,
68
+ cardinality: 'keyed',
69
+ sides: ['server'],
70
+ default: null,
71
+ phase: '6c',
72
+ },
73
+ {
74
+ id: 'pay.rail',
75
+ version: 1,
76
+ cardinality: 'keyed',
77
+ sides: ['server', 'client', 'native'],
78
+ default: null,
79
+ phase: '6c',
80
+ },
18
81
  ]
19
82
 
20
83
  export function slotDef(id) {
21
- const def = slotTable.find(s => s.id === id)
84
+ const def = slotTable.find((s) => s.id === id)
22
85
  if (def) return def
23
- if (/^device\.[a-z][\w-]*$/.test(id)) return { id, version: 1, cardinality: 'keyed', sides: ['native', 'client'], default: null, phase: '6b' }
86
+ if (/^device\.[a-z][\w-]*$/.test(id))
87
+ return {
88
+ id,
89
+ version: 1,
90
+ cardinality: 'keyed',
91
+ sides: ['native', 'client'],
92
+ default: null,
93
+ phase: '6b',
94
+ }
24
95
  return undefined
25
96
  }
26
97
 
@@ -32,7 +103,7 @@ export function slotDef(id) {
32
103
  export function bindSlots(config, report) {
33
104
  const plugins = config.plugins ?? []
34
105
  const chosen = config.slots ?? {}
35
- const names = new Set(plugins.map(p => p.name))
106
+ const names = new Set(plugins.map((p) => p.name))
36
107
 
37
108
  /** slot id → key ('' for `one`) → provider names */
38
109
  const offers = new Map()
@@ -40,7 +111,11 @@ export function bindSlots(config, report) {
40
111
  for (const item of p.provides ?? []) {
41
112
  const { slot, key = '' } = typeof item === 'string' ? { slot: item } : item
42
113
  if (!slotDef(slot) && !slot.startsWith(`${p.name}.`)) {
43
- report('P008', `\`${p.name}\` provides \`${slot}\`, which is not a slot weft defines.`, p.name)
114
+ report(
115
+ 'P008',
116
+ `\`${p.name}\` provides \`${slot}\`, which is not a slot weft defines.`,
117
+ p.name,
118
+ )
44
119
  continue
45
120
  }
46
121
  const byKey = offers.get(slot) ?? new Map()
@@ -57,13 +132,15 @@ export function bindSlots(config, report) {
57
132
  const picks = typeof pick === 'string' ? { '': pick } : pick
58
133
  for (const [key, name] of Object.entries(picks)) {
59
134
  if (name.startsWith('weft:default')) continue
60
- if (!names.has(name)) report('P009', `\`slots['${id}']\` picks \`${name}\`, which is not in \`plugins\`.`, id)
61
- else if (!(offers.get(id)?.get(key) ?? []).includes(name)) report('P009', `\`slots['${id}']\` picks \`${name}\`, which does not provide it.`, id)
135
+ if (!names.has(name))
136
+ report('P009', `\`slots['${id}']\` picks \`${name}\`, which is not in \`plugins\`.`, id)
137
+ else if (!(offers.get(id)?.get(key) ?? []).includes(name))
138
+ report('P009', `\`slots['${id}']\` picks \`${name}\`, which does not provide it.`, id)
62
139
  }
63
140
  }
64
141
 
65
142
  /** @type {Set<string>} */
66
- const known = new Set([...slotTable.map(s => s.id), ...offers.keys()])
143
+ const known = new Set([...slotTable.map((s) => s.id), ...offers.keys()])
67
144
  const ids = [...known].sort()
68
145
  const out = {}
69
146
  for (const id of ids) {
@@ -77,7 +154,11 @@ export function bindSlots(config, report) {
77
154
  if (providers.length === 1) return providers[0]
78
155
  if (providers.length > 1) {
79
156
  const where = key ? `\`${id}\` key \`${key}\`` : `\`${id}\``
80
- report('P100', `${where} has ${providers.length} providers: ${providers.join(', ')}. Array order never decides.`, providers[0])
157
+ report(
158
+ 'P100',
159
+ `${where} has ${providers.length} providers: ${providers.join(', ')}. Array order never decides.`,
160
+ providers[0],
161
+ )
81
162
  return null
82
163
  }
83
164
  return fallback
@@ -86,9 +167,13 @@ export function bindSlots(config, report) {
86
167
  out[id] = resolve('', typeof def.default === 'string' ? `weft:default:${def.default}` : null)
87
168
  } else {
88
169
  /** @type {Set<string>} */
89
- const keys = new Set([...(Array.isArray(def.default) ? def.default : []), ...[...byKey.keys()].filter(Boolean)])
170
+ const keys = new Set([
171
+ ...(Array.isArray(def.default) ? def.default : []),
172
+ ...[...byKey.keys()].filter(Boolean),
173
+ ])
90
174
  const block = {}
91
- for (const key of [...keys].sort()) block[key] = resolve(key, def.default?.includes(key) ? 'weft:default' : null)
175
+ for (const key of [...keys].sort())
176
+ block[key] = resolve(key, def.default?.includes(key) ? 'weft:default' : null)
92
177
  out[id] = block
93
178
  }
94
179
  }
@@ -0,0 +1,116 @@
1
+ import { audit, builtPublic } from '../audit.mjs'
2
+ import { writeMap } from '../map.mjs'
3
+ import { diagnostic } from '../diag.mjs'
4
+ import { loadConfig } from '../load-config.mjs'
5
+ import { host, presetFor } from '../deploy/hosts.mjs'
6
+
7
+ export async function finishBuild({
8
+ root,
9
+ config,
10
+ preset,
11
+ rust,
12
+ refuse,
13
+ running,
14
+ shipSidecars,
15
+ output,
16
+ warned,
17
+ unsafe,
18
+ forced,
19
+ loaded,
20
+ t,
21
+ writeContract,
22
+ }) {
23
+ const contract = await writeContract({
24
+ root,
25
+ site: config.site,
26
+ preset,
27
+ rust,
28
+ live: (await loadConfig(root)).config?.live,
29
+ verify: true,
30
+ })
31
+ for (const refused of contract.diagnostics.filter((d) => d.severity === 'error')) refuse(refused)
32
+ if (running) shipSidecars(root)
33
+ await writeMap(root)
34
+ // Last, the pages as built, in a clean Chrome at a phone's width and a desktop's (audit.mjs): an error fails the build.
35
+ const settings = (await loadConfig(root)).config?.build?.audit ?? true
36
+ const audited =
37
+ settings === false
38
+ ? {
39
+ diagnostics: [
40
+ diagnostic({
41
+ code: 'L000',
42
+ severity: 'warning',
43
+ file: 'weft.config.ts',
44
+ why: '`build.audit` is false: the built pages were not audited.',
45
+ }),
46
+ ],
47
+ }
48
+ : await audit(root, {
49
+ dir: builtPublic(root),
50
+ ...(typeof settings === 'object' && settings),
51
+ })
52
+ const pages = audited.diagnostics.map((d) =>
53
+ d.severity === 'error' && unsafe.allows(d) ? unsafe.let(d) : d,
54
+ )
55
+ const kept = [
56
+ ...contract.diagnostics.filter((d) => !(d.severity === 'error' && unsafe.allows(d))),
57
+ ...warned,
58
+ ].map((d) => diagnostic({ file: '.weft/contract.json', ...d }))
59
+ const diagnostics = [
60
+ ...kept,
61
+ ...pages,
62
+ ...forced.map((d) => diagnostic({ file: '.weft/contract.json', ...d })),
63
+ ]
64
+ return {
65
+ ok: !diagnostics.some((d) => d.severity === 'error'),
66
+ preset,
67
+ output,
68
+ contract: contract.path,
69
+ functions: contract.functions,
70
+ notes: loaded.contributions.flatMap((c) => c.manifest.notes ?? []),
71
+ audited: audited.pages?.length ?? 0,
72
+ diagnostics,
73
+ ms: Math.round(performance.now() - t),
74
+ }
75
+ }
76
+
77
+ export async function startBuildSidecars({
78
+ start,
79
+ root,
80
+ config,
81
+ preset,
82
+ rust,
83
+ refuse,
84
+ writeContract,
85
+ }) {
86
+ const { buildSidecars, shipSidecars, startSidecars, usesRemote } = await start(root, 'sidecar')
87
+ const contracted = () => writeContract({ root, site: config.site, preset, rust })
88
+ let warned = []
89
+ // Sidecars first: prerendering a share card runs its route's `load`, which may read one.
90
+ const running = usesRemote(root)
91
+ ? await contracted().then(async (c) => {
92
+ const stop = c.diagnostics.find((d) => d.severity === 'error')
93
+ if (stop) refuse(stop)
94
+ const built = await buildSidecars({ root, contract: c.contract, rust, preset })
95
+ const failed = built.diagnostics.find((d) => d.severity === 'error')
96
+ if (failed) refuse(failed)
97
+ warned = built.diagnostics
98
+ return startSidecars({ root, built: true, log: () => {} })
99
+ })
100
+ : undefined
101
+ return { running, warned, shipSidecars }
102
+ }
103
+
104
+ export async function buildPreset(root, { preset, runtime, hostRuntime, PRESETS }) {
105
+ const chosen = runtime ?? (await loadConfig(root)).config?.runtime ?? hostRuntime()
106
+ // `deploy.target` picks the preset when nothing else does: a build for Fly is the runtime's server, for Netlify `netlify`
107
+ const target = !preset && !runtime && (await loadConfig(root)).config?.deploy?.target
108
+ if (target) preset = presetFor(host(target), chosen)
109
+ if (!preset && !PRESETS[chosen])
110
+ throw Object.assign(
111
+ new Error(`unknown runtime "${chosen}": use ${Object.keys(PRESETS).join(', ')}`),
112
+ { code: 'E_USAGE' },
113
+ )
114
+ preset ??= PRESETS[chosen]
115
+ return preset
116
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { diagnostic, locate, positions } from '../diag.mjs'
4
+
5
+ /**
6
+ * Route findings as diagnostics, pointing at the `meta` export, or at where one goes. H011's fix is that export, as an
7
+ * edit an agent can apply: not safe, since the words are a start and not the page's best.
8
+ */
9
+ export function routeDiagnostics(root, found) {
10
+ const text = (f) => {
11
+ try {
12
+ return readFileSync(join(root, f), 'utf8')
13
+ } catch {
14
+ return ''
15
+ }
16
+ }
17
+ return found.map(({ code, severity, file, route, why, help }) => {
18
+ const source = text(file)
19
+ const at =
20
+ code === 'H011' &&
21
+ help?.startsWith('export const') &&
22
+ !/\bexport\s+(?:const|function|async function)\s+meta\b/.test(source)
23
+ ? source.indexOf('export default')
24
+ : -1
25
+ const place = positions(source)
26
+ const fix = at >= 0 && {
27
+ fix: {
28
+ safe: false,
29
+ description: 'Add `meta`',
30
+ edits: [{ file, span: place.span(at, at), offset: [at, at], text: `${help}\n\n` }],
31
+ },
32
+ }
33
+ return {
34
+ ...diagnostic({ code, severity, file, why, help, ...locate(source, 'meta'), ...fix }),
35
+ route,
36
+ }
37
+ })
38
+ }