@prestack/thread-ui 0.0.1 → 0.0.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 (269) hide show
  1. package/dist/ui/animations/anm_appear.ts +13 -0
  2. package/dist/ui/animations/anm_blur_in.ts +19 -0
  3. package/dist/ui/animations/anm_blur_out.ts +18 -0
  4. package/dist/ui/animations/anm_dismiss.ts +18 -0
  5. package/dist/ui/animations/anm_drop.ts +19 -0
  6. package/dist/ui/animations/anm_fall.ts +18 -0
  7. package/dist/ui/animations/anm_flash.ts +18 -0
  8. package/dist/ui/animations/anm_glow.ts +16 -0
  9. package/dist/ui/animations/anm_lift.ts +16 -0
  10. package/dist/ui/animations/anm_nudge.ts +16 -0
  11. package/dist/ui/animations/anm_pop_in.ts +23 -0
  12. package/dist/ui/animations/anm_pop_out.ts +22 -0
  13. package/dist/ui/animations/anm_press.ts +12 -0
  14. package/dist/ui/animations/anm_pulse.ts +13 -0
  15. package/dist/ui/animations/anm_rise.ts +19 -0
  16. package/dist/ui/animations/anm_shake.ts +13 -0
  17. package/dist/ui/animations/anm_slide_in_left.ts +19 -0
  18. package/dist/ui/animations/anm_slide_in_right.ts +19 -0
  19. package/dist/ui/animations/anm_slide_out_left.ts +18 -0
  20. package/dist/ui/animations/anm_slide_out_right.ts +18 -0
  21. package/dist/ui/animations/anm_spin.ts +12 -0
  22. package/dist/ui/animations/anm_swell.ts +16 -0
  23. package/dist/ui/animations/anm_wobble.ts +24 -0
  24. package/dist/ui/animations/anm_zoom_in.ts +19 -0
  25. package/dist/ui/animations/anm_zoom_out.ts +18 -0
  26. package/dist/ui/animations.ts +317 -0
  27. package/dist/ui/attach.js +13 -4
  28. package/dist/ui/attach.ts +613 -0
  29. package/dist/ui/charts.tsx +74 -0
  30. package/dist/ui/checked.d.ts +2 -2
  31. package/dist/ui/checked.ts +32 -0
  32. package/dist/ui/collector.ts +140 -0
  33. package/dist/ui/compositions.d.ts +10 -1
  34. package/dist/ui/compositions.js +262 -23
  35. package/dist/ui/compositions.ts +640 -0
  36. package/dist/ui/consent.ts +55 -0
  37. package/dist/ui/content.tsx +39 -0
  38. package/dist/ui/contrast.ts +27 -0
  39. package/dist/ui/controls.tsx +30 -0
  40. package/dist/ui/disclosure.tsx +22 -0
  41. package/dist/ui/dismiss-layer.ts +87 -0
  42. package/dist/ui/door.ts +140 -0
  43. package/dist/ui/drag.ts +316 -0
  44. package/dist/ui/elements/accordion.js +2 -2
  45. package/dist/ui/elements/accordion.tsx +47 -0
  46. package/dist/ui/elements/accordion.works.ts +66 -0
  47. package/dist/ui/elements/aspect-ratio.js +1 -1
  48. package/dist/ui/elements/aspect-ratio.tsx +30 -0
  49. package/dist/ui/elements/aspect-ratio.works.ts +5 -0
  50. package/dist/ui/elements/avatar.js +1 -1
  51. package/dist/ui/elements/avatar.tsx +33 -0
  52. package/dist/ui/elements/avatar.works.ts +16 -0
  53. package/dist/ui/elements/badge.js +1 -1
  54. package/dist/ui/elements/badge.tsx +32 -0
  55. package/dist/ui/elements/badge.works.ts +9 -0
  56. package/dist/ui/elements/bars.js +2 -2
  57. package/dist/ui/elements/bars.tsx +49 -0
  58. package/dist/ui/elements/bars.works.ts +29 -0
  59. package/dist/ui/elements/box.js +3 -2
  60. package/dist/ui/elements/box.tsx +35 -0
  61. package/dist/ui/elements/breadcrumb.js +2 -2
  62. package/dist/ui/elements/breadcrumb.tsx +37 -0
  63. package/dist/ui/elements/breadcrumb.works.ts +24 -0
  64. package/dist/ui/elements/button.js +1 -1
  65. package/dist/ui/elements/button.tsx +45 -0
  66. package/dist/ui/elements/button.works.tsx +52 -0
  67. package/dist/ui/elements/column.js +1 -1
  68. package/dist/ui/elements/column.tsx +28 -0
  69. package/dist/ui/elements/consent.js +2 -2
  70. package/dist/ui/elements/consent.tsx +70 -0
  71. package/dist/ui/elements/consent.works.tsx +132 -0
  72. package/dist/ui/elements/donut.js +2 -2
  73. package/dist/ui/elements/donut.tsx +44 -0
  74. package/dist/ui/elements/donut.works.ts +42 -0
  75. package/dist/ui/elements/each.js +1 -1
  76. package/dist/ui/elements/each.tsx +33 -0
  77. package/dist/ui/elements/each.works.tsx +82 -0
  78. package/dist/ui/elements/empty.js +1 -1
  79. package/dist/ui/elements/empty.tsx +32 -0
  80. package/dist/ui/elements/field.js +1 -1
  81. package/dist/ui/elements/field.tsx +44 -0
  82. package/dist/ui/elements/field.works.ts +65 -0
  83. package/dist/ui/elements/funnel.js +2 -2
  84. package/dist/ui/elements/funnel.tsx +49 -0
  85. package/dist/ui/elements/funnel.works.ts +37 -0
  86. package/dist/ui/elements/gauge.js +1 -1
  87. package/dist/ui/elements/gauge.tsx +39 -0
  88. package/dist/ui/elements/gauge.works.ts +42 -0
  89. package/dist/ui/elements/glyph.js +1 -1
  90. package/dist/ui/elements/glyph.tsx +29 -0
  91. package/dist/ui/elements/glyph.works.tsx +42 -0
  92. package/dist/ui/elements/grid.js +1 -1
  93. package/dist/ui/elements/grid.tsx +28 -0
  94. package/dist/ui/elements/heatmap.js +2 -2
  95. package/dist/ui/elements/heatmap.tsx +47 -0
  96. package/dist/ui/elements/heatmap.works.ts +24 -0
  97. package/dist/ui/elements/hit-box.js +1 -1
  98. package/dist/ui/elements/hit-box.tsx +46 -0
  99. package/dist/ui/elements/hit-box.works.tsx +576 -0
  100. package/dist/ui/elements/image.js +1 -1
  101. package/dist/ui/elements/image.tsx +28 -0
  102. package/dist/ui/elements/line-chart.js +2 -2
  103. package/dist/ui/elements/line-chart.tsx +44 -0
  104. package/dist/ui/elements/line-chart.works.ts +41 -0
  105. package/dist/ui/elements/link.js +1 -1
  106. package/dist/ui/elements/link.tsx +37 -0
  107. package/dist/ui/elements/link.works.ts +25 -0
  108. package/dist/ui/elements/media.js +1 -1
  109. package/dist/ui/elements/media.tsx +56 -0
  110. package/dist/ui/elements/menu.js +2 -2
  111. package/dist/ui/elements/menu.tsx +50 -0
  112. package/dist/ui/elements/menu.works.tsx +153 -0
  113. package/dist/ui/elements/money.js +1 -1
  114. package/dist/ui/elements/money.tsx +28 -0
  115. package/dist/ui/elements/money.works.ts +9 -0
  116. package/dist/ui/elements/pagination.js +2 -2
  117. package/dist/ui/elements/pagination.tsx +38 -0
  118. package/dist/ui/elements/pagination.works.ts +20 -0
  119. package/dist/ui/elements/progress.js +1 -1
  120. package/dist/ui/elements/progress.tsx +33 -0
  121. package/dist/ui/elements/progress.works.ts +8 -0
  122. package/dist/ui/elements/radio-group.js +2 -2
  123. package/dist/ui/elements/radio-group.tsx +49 -0
  124. package/dist/ui/elements/radio-group.works.ts +33 -0
  125. package/dist/ui/elements/row.js +1 -1
  126. package/dist/ui/elements/row.tsx +28 -0
  127. package/dist/ui/elements/scatter.js +1 -1
  128. package/dist/ui/elements/scatter.tsx +41 -0
  129. package/dist/ui/elements/scatter.works.ts +38 -0
  130. package/dist/ui/elements/select.js +2 -2
  131. package/dist/ui/elements/select.tsx +42 -0
  132. package/dist/ui/elements/select.works.ts +5 -0
  133. package/dist/ui/elements/separator.js +1 -1
  134. package/dist/ui/elements/separator.tsx +30 -0
  135. package/dist/ui/elements/skeleton.js +1 -1
  136. package/dist/ui/elements/skeleton.tsx +30 -0
  137. package/dist/ui/elements/slider.js +1 -1
  138. package/dist/ui/elements/slider.tsx +33 -0
  139. package/dist/ui/elements/slider.works.ts +5 -0
  140. package/dist/ui/elements/sparkline.js +1 -1
  141. package/dist/ui/elements/sparkline.tsx +36 -0
  142. package/dist/ui/elements/sparkline.works.ts +32 -0
  143. package/dist/ui/elements/spinner.js +1 -1
  144. package/dist/ui/elements/spinner.tsx +32 -0
  145. package/dist/ui/elements/stack.js +1 -1
  146. package/dist/ui/elements/stack.tsx +28 -0
  147. package/dist/ui/elements/stat.js +1 -1
  148. package/dist/ui/elements/stat.tsx +35 -0
  149. package/dist/ui/elements/stat.works.ts +14 -0
  150. package/dist/ui/elements/surface.js +1 -1
  151. package/dist/ui/elements/surface.tsx +35 -0
  152. package/dist/ui/elements/surface.works.ts +9 -0
  153. package/dist/ui/elements/table.js +2 -2
  154. package/dist/ui/elements/table.tsx +61 -0
  155. package/dist/ui/elements/table.works.ts +30 -0
  156. package/dist/ui/elements/tabs.js +2 -2
  157. package/dist/ui/elements/tabs.tsx +47 -0
  158. package/dist/ui/elements/tabs.works.ts +84 -0
  159. package/dist/ui/elements/text.js +1 -1
  160. package/dist/ui/elements/text.tsx +32 -0
  161. package/dist/ui/elements/textarea.js +1 -1
  162. package/dist/ui/elements/textarea.tsx +37 -0
  163. package/dist/ui/elements/textarea.works.ts +5 -0
  164. package/dist/ui/elements/time.js +1 -1
  165. package/dist/ui/elements/time.tsx +34 -0
  166. package/dist/ui/elements/time.works.ts +25 -0
  167. package/dist/ui/elements/toggle.js +1 -1
  168. package/dist/ui/elements/toggle.tsx +33 -0
  169. package/dist/ui/elements/toggle.works.ts +5 -0
  170. package/dist/ui/elements/tooltip.js +1 -1
  171. package/dist/ui/elements/tooltip.tsx +39 -0
  172. package/dist/ui/elements/tooltip.works.ts +77 -0
  173. package/dist/ui/elements/value.js +1 -1
  174. package/dist/ui/elements/value.tsx +30 -0
  175. package/dist/ui/elements/value.works.ts +9 -0
  176. package/dist/ui/elements.ts +127 -0
  177. package/dist/ui/focus-scope.ts +121 -0
  178. package/dist/ui/fonts.ts +355 -0
  179. package/dist/ui/generators/dev.ts +79 -0
  180. package/dist/ui/generators/elements.d.ts +1 -11
  181. package/dist/ui/generators/elements.js +43 -13
  182. package/dist/ui/generators/elements.ts +794 -0
  183. package/dist/ui/generators/i18n.ts +301 -0
  184. package/dist/ui/generators/icons.ts +214 -0
  185. package/dist/ui/generators/react.js +10 -3
  186. package/dist/ui/generators/react.ts +268 -0
  187. package/dist/ui/generators/routes.d.ts +1 -0
  188. package/dist/ui/generators/routes.js +89 -69
  189. package/dist/ui/generators/routes.ts +739 -0
  190. package/dist/ui/generators/show.ts +259 -0
  191. package/dist/ui/generators/styles.js +1 -1
  192. package/dist/ui/generators/styles.ts +670 -0
  193. package/dist/ui/generators/variants.ts +205 -0
  194. package/dist/ui/i18n/catalogs/en.ts +191 -0
  195. package/dist/ui/i18n/format.ts +221 -0
  196. package/dist/ui/i18n/locales.ts +33 -0
  197. package/dist/ui/i18n/parse.ts +315 -0
  198. package/dist/ui/i18n/translator.ts +31 -0
  199. package/dist/ui/i18n/types.ts +96 -0
  200. package/dist/ui/icons.ts +331 -0
  201. package/dist/ui/islands/map.ts +58 -0
  202. package/dist/ui/localizations.ts +75 -0
  203. package/dist/ui/makers/message.ts +292 -0
  204. package/dist/ui/material.ts +236 -0
  205. package/dist/ui/materials/mat_bordered.ts +13 -0
  206. package/dist/ui/materials/mat_ceramic.ts +13 -0
  207. package/dist/ui/materials/mat_clay.ts +13 -0
  208. package/dist/ui/materials/mat_elevated.ts +24 -0
  209. package/dist/ui/materials/mat_flat.ts +34 -0
  210. package/dist/ui/materials/mat_glass.ts +20 -0
  211. package/dist/ui/materials/mat_lit.ts +13 -0
  212. package/dist/ui/materials/mat_moulded.ts +11 -0
  213. package/dist/ui/materials.ts +206 -0
  214. package/dist/ui/modes.ts +44 -0
  215. package/dist/ui/navigation.tsx +32 -0
  216. package/dist/ui/positioning.ts +90 -0
  217. package/dist/ui/presented.tsx +119 -0
  218. package/dist/ui/provider.d.ts +2 -0
  219. package/dist/ui/provider.js +7 -1
  220. package/dist/ui/provider.tsx +182 -0
  221. package/dist/ui/render-loop.ts +133 -0
  222. package/dist/ui/roving-focus.ts +161 -0
  223. package/dist/ui/screens.js +17 -8
  224. package/dist/ui/screens.ts +142 -0
  225. package/dist/ui/scroll-lock.ts +28 -0
  226. package/dist/ui/series.ts +97 -0
  227. package/dist/ui/signals.ts +386 -0
  228. package/dist/ui/styleguide.ts +231 -0
  229. package/dist/ui/styleguides.ts +374 -0
  230. package/dist/ui/themes.ts +22 -0
  231. package/dist/ui/token.js +1 -1
  232. package/dist/ui/token.ts +78 -0
  233. package/dist/ui/tokens/shadow.ts +25 -0
  234. package/dist/ui/tokens.ts +235 -0
  235. package/dist/ui/validators.ts +232 -0
  236. package/dist/ui/variant.d.ts +1 -0
  237. package/dist/ui/variant.js +3 -0
  238. package/dist/ui/variant.ts +352 -0
  239. package/dist/ui/variants/var_avatar-round.ts +14 -0
  240. package/dist/ui/variants/var_bubble-me.ts +14 -0
  241. package/dist/ui/variants/var_bubble-them.ts +13 -0
  242. package/dist/ui/variants/var_button-border.ts +76 -0
  243. package/dist/ui/variants/var_button-cta.ts +76 -0
  244. package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
  245. package/dist/ui/variants/var_button-destructive.ts +76 -0
  246. package/dist/ui/variants/var_button-fill.ts +76 -0
  247. package/dist/ui/variants/var_button-ghost.ts +72 -0
  248. package/dist/ui/variants/var_button-glass.ts +55 -0
  249. package/dist/ui/variants/var_button-normal.ts +76 -0
  250. package/dist/ui/variants/var_button-quiet.ts +74 -0
  251. package/dist/ui/variants/var_card.ts +17 -0
  252. package/dist/ui/variants/var_chip.ts +13 -0
  253. package/dist/ui/variants/var_choice.ts +15 -0
  254. package/dist/ui/variants/var_framed.ts +10 -0
  255. package/dist/ui/variants/var_glyph-plain.ts +11 -0
  256. package/dist/ui/variants/var_input.ts +43 -0
  257. package/dist/ui/variants/var_link-inline.ts +29 -0
  258. package/dist/ui/variants/var_list-row.ts +34 -0
  259. package/dist/ui/variants/var_rule.ts +10 -0
  260. package/dist/ui/variants/var_skeleton-block.ts +12 -0
  261. package/dist/ui/variants/var_spinner-turning.ts +12 -0
  262. package/dist/ui/variants/var_stat-tile.ts +13 -0
  263. package/dist/ui/variants/var_tab-bar.ts +15 -0
  264. package/dist/ui/variants/var_tap-target.ts +25 -0
  265. package/dist/ui/variants.ts +64 -0
  266. package/dist/ui/wrappers.d.ts +4 -1
  267. package/dist/ui/wrappers.js +60 -20
  268. package/dist/ui/wrappers.tsx +426 -0
  269. package/package.json +8 -2
@@ -0,0 +1,268 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readFileSync, statSync } from 'node:fs'
3
+ import { cwd } from 'node:process'
4
+ import { dirname, join, resolve } from 'node:path'
5
+
6
+ import ts from 'typescript'
7
+
8
+ import { idOf } from '../checked.ts'
9
+ import { classOf } from '../variant.ts'
10
+
11
+ const OWNED = /\.(t|j)sx$/
12
+
13
+ const VARIANT_MEMBER = /^[a-z]/
14
+
15
+ interface Edit {
16
+ readonly from: number
17
+ readonly to: number
18
+ readonly text: string
19
+ }
20
+
21
+ interface Arranging {
22
+ readonly from: number
23
+ readonly to: number
24
+ readonly own: string
25
+ }
26
+
27
+ const ARRANGERS = new Set(['composition', 'screen'])
28
+
29
+ const HOST = /^[a-z]/
30
+
31
+ const MAKES = 'element'
32
+
33
+ const ONE_WORD = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
34
+
35
+ function prefixed(called: string): string {
36
+ return called === 'screen' ? 's-' : 'c-'
37
+ }
38
+
39
+ function quoted(node: ts.Node): string | undefined {
40
+ return ts.isStringLiteral(node) ? node.text : undefined
41
+ }
42
+
43
+ function joined(held: readonly string[]): string {
44
+ return held.filter((one) => one !== '').join(' ')
45
+ }
46
+
47
+ function arranging(held: readonly Arranging[], at: number): string | undefined {
48
+ let found: Arranging | undefined
49
+ for (const one of held) {
50
+ if (at < one.from || at >= one.to) continue
51
+ if (found === undefined || one.from > found.from) found = one
52
+ }
53
+ return found?.own
54
+ }
55
+
56
+ function within(held: readonly { readonly from: number; readonly to: number }[], at: number): boolean {
57
+ return held.some((one) => at >= one.from && at < one.to)
58
+ }
59
+
60
+ function tagOf(node: ts.JsxOpeningElement | ts.JsxSelfClosingElement | ts.JsxClosingElement): ts.JsxTagNameExpression {
61
+ return node.tagName
62
+ }
63
+
64
+ function attributes(node: ts.JsxOpeningElement | ts.JsxSelfClosingElement): ts.JsxAttributes {
65
+ return node.attributes
66
+ }
67
+
68
+ interface Held {
69
+ readonly at?: ts.JsxAttribute
70
+ readonly className?: ts.JsxAttribute
71
+ }
72
+
73
+ function named(held: ts.JsxAttributes): Held {
74
+ let at: ts.JsxAttribute | undefined
75
+ let className: ts.JsxAttribute | undefined
76
+ for (const one of held.properties) {
77
+ if (!ts.isJsxAttribute(one) || !ts.isIdentifier(one.name)) continue
78
+ if (one.name.text === 'at') at = one
79
+ if (one.name.text === 'className') className = one
80
+ }
81
+ return {
82
+ ...(at === undefined ? {} : { at }),
83
+ ...(className === undefined ? {} : { className }),
84
+ }
85
+ }
86
+
87
+ function wearing(held: ts.JsxAttribute | undefined, added: string, text: string): string {
88
+ if (held?.initializer === undefined) return `className="${added}"`
89
+ const value = held.initializer
90
+ if (ts.isStringLiteral(value)) return `className="${joined([value.text, added])}"`
91
+ const inner = text.slice(value.getStart() + 1, value.getEnd() - 1)
92
+ return `className={\`\${${inner}} ${added}\`}`
93
+ }
94
+
95
+ export function compiled(text: string, at: string): { readonly text?: string; readonly because?: string } {
96
+ const file = ts.createSourceFile(at, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
97
+
98
+ const arranges: Arranging[] = []
99
+ const edits: Edit[] = []
100
+ let because: string | undefined
101
+ const writing: { readonly from: number; readonly to: number }[] = []
102
+
103
+ const calls = (node: ts.Node): void => {
104
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === MAKES) {
105
+ writing.push({ from: node.getStart(), to: node.getEnd() })
106
+ }
107
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ARRANGERS.has(node.expression.text)) {
108
+ const first = node.arguments[0]
109
+ const said = first === undefined ? undefined : quoted(first)
110
+ if (first !== undefined && said !== undefined) {
111
+ if (!ONE_WORD.test(said)) {
112
+ because =
113
+ `${at} names a ${node.expression.text} "${said}", and that name becomes the class everything inside ` +
114
+ 'it is arranged against. This one is not a single word, so it would become two classes and neither ' +
115
+ 'would be the one the stylesheet writes'
116
+ }
117
+ const own = `${prefixed(node.expression.text)}${idOf(said)}`
118
+ arranges.push({ from: node.getStart(), to: node.getEnd(), own })
119
+ edits.push({ from: first.getStart(), to: first.getEnd(), text: `'${own}'` })
120
+ }
121
+ }
122
+ ts.forEachChild(node, calls)
123
+ }
124
+ calls(file)
125
+
126
+ const tags = (node: ts.Node): void => {
127
+ if (ts.isJsxClosingElement(node)) {
128
+ const tag = tagOf(node)
129
+ if (ts.isPropertyAccessExpression(tag) && VARIANT_MEMBER.test(tag.name.text)) {
130
+ edits.push({ from: tag.getStart(), to: tag.getEnd(), text: tag.expression.getText() })
131
+ }
132
+ }
133
+
134
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
135
+ const tag = tagOf(node)
136
+ const held = named(attributes(node))
137
+ const worn: string[] = []
138
+
139
+ if (ts.isIdentifier(tag) && HOST.test(tag.text) && !within(writing, node.getStart())) {
140
+ because =
141
+ `${at} writes <${tag.text}>, which is a host element: it wears no declared style, sits in no ` +
142
+ 'catalogue, and the style guide, a rebrand and every overlay read past it. Declare the element ' +
143
+ 'and wear its look, or use the declared groupings where a host element would have gone'
144
+ }
145
+
146
+ if (held.className !== undefined && !within(writing, node.getStart())) {
147
+ because =
148
+ `${at} writes a \`className\` by hand, which is CSS written in place: it names no style guide ` +
149
+ 'row, wears no declared variant, and the style guide, a rebrand and every overlay read past ' +
150
+ 'it. Declare a variant and wear it — `Box.card` — or name a place with `at`, which the build ' +
151
+ 'turns into the class the arrangement already declares'
152
+ }
153
+
154
+ if (ts.isPropertyAccessExpression(tag) && VARIANT_MEMBER.test(tag.name.text)) {
155
+ worn.push(classOf(idOf(tag.name.text)))
156
+ edits.push({ from: tag.getStart(), to: tag.getEnd(), text: tag.expression.getText() })
157
+ }
158
+
159
+ if (held.at !== undefined) {
160
+ const place = held.at.initializer === undefined ? undefined : quoted(held.at.initializer)
161
+ if (place === undefined) {
162
+ because = `${at} gives a place that is not written out where it is used. A place is a class the build joins, so it has to be a plain word here`
163
+ } else {
164
+ const own = arranging(arranges, node.getStart())
165
+ if (own === undefined) {
166
+ because =
167
+ `${at} names the place "${place}" outside any composition or screen, and a place is declared by ` +
168
+ 'whatever arranges it. Nothing arranges this one, so the name points at nothing'
169
+ } else {
170
+ worn.push(`${own}-${place}`)
171
+ }
172
+ }
173
+ edits.push({ from: held.at.getFullStart(), to: held.at.getEnd(), text: '' })
174
+ }
175
+
176
+ if (worn.length > 0) {
177
+ const wrote = wearing(held.className, joined(worn), text)
178
+ if (held.className === undefined) edits.push({ from: tag.getEnd(), to: tag.getEnd(), text: ` ${wrote}` })
179
+ else edits.push({ from: held.className.getStart(), to: held.className.getEnd(), text: wrote })
180
+ }
181
+ }
182
+
183
+ ts.forEachChild(node, tags)
184
+ }
185
+ tags(file)
186
+
187
+ if (because !== undefined) return { because }
188
+ if (edits.length === 0) return { text }
189
+
190
+ let made = text
191
+ for (const one of [...edits].sort((a, b) => b.from - a.from)) {
192
+ made = made.slice(0, one.from) + one.text + made.slice(one.to)
193
+ }
194
+ return { text: made }
195
+ }
196
+
197
+ interface Loading {
198
+ readonly onLoad: (
199
+ opts: { filter: RegExp },
200
+ run: (args: { path: string }) => { contents?: string; loader?: string; errors?: readonly { text: string }[] } | undefined,
201
+ ) => void
202
+ }
203
+
204
+ let checked: string | null | undefined
205
+ let against: string | undefined
206
+
207
+ function treeAt(from: string): string | undefined {
208
+ for (const start of [dirname(resolve(from)), resolve(cwd())]) {
209
+ let root = start
210
+ while (root !== dirname(root)) {
211
+ if (existsSync(join(root, '.thread', 'gate.json')) || existsSync(join(root, '_threads'))) return root
212
+ root = dirname(root)
213
+ }
214
+ }
215
+ return undefined
216
+ }
217
+
218
+ export function ungated(from: string): string | null {
219
+ const root = treeAt(from)
220
+ const at = root === undefined ? '' : join(root, '.thread', 'gate.json')
221
+
222
+ const stamp = at !== '' && existsSync(at) ? `${String(statSync(at).mtimeMs)}:${String(statSync(at).size)}` : `none:${from}`
223
+ if (checked !== undefined && against === stamp) return checked
224
+ against = stamp
225
+ checked = null
226
+
227
+ if (at === '' || !existsSync(at)) {
228
+ checked =
229
+ 'this tree has no passing gate, so nothing in it may compile. Run `thread gate` — or the ' +
230
+ '`dev`, `build` and `start` scripts, which run it for you. A gate that has never run is not a ' +
231
+ 'gate that passed, and the file you are compiling was never measured. Compiling ' +
232
+ `\`${from}\`, it looked for a gate receipt from \`${dirname(resolve(from))}\` and from ` +
233
+ `\`${resolve(cwd())}\` upwards${at === '' ? ', and neither reached a tree' : `, and found none at ${at}`}`
234
+ return checked
235
+ }
236
+ const held = JSON.parse(readFileSync(at, 'utf8')) as { of?: Record<string, string> }
237
+ for (const [named, was] of Object.entries(held.of ?? {})) {
238
+ const now = existsSync(join(dirname(dirname(at)), named))
239
+ ? createHash('sha256').update(readFileSync(join(dirname(dirname(at)), named))).digest('hex').slice(0, 16)
240
+ : 'gone'
241
+ if (now === was) continue
242
+ checked = `${named} has changed since the gate last passed, so what this tree declares and what it was measured as no longer agree. Run \`thread gate\``
243
+ return checked
244
+ }
245
+ return checked
246
+ }
247
+
248
+ export default function loader(this: { readonly resourcePath?: string } | void, source: string): string {
249
+ const at = typeof this?.resourcePath === 'string' ? this.resourcePath : ''
250
+ const skipped = at === '' ? null : ungated(at)
251
+ if (skipped !== null) throw new Error(`thread: ${skipped}.`)
252
+ const made = compiled(source, at)
253
+
254
+ if (made.because !== undefined) throw new Error(made.because)
255
+ return made.text ?? source
256
+ }
257
+
258
+ export const plugin: { readonly name: string; readonly setup: (build: Loading) => void } = {
259
+ name: 'thread-react',
260
+ setup(build) {
261
+ build.onLoad({ filter: OWNED }, (args) => {
262
+ const text = readFileSync(args.path, 'utf8')
263
+ const made = compiled(text, args.path)
264
+ if (made.because !== undefined) return { errors: [{ text: made.because }] }
265
+ return { contents: made.text ?? text, loader: 'tsx' }
266
+ })
267
+ },
268
+ }
@@ -1,4 +1,5 @@
1
1
  export declare function typedRoute(source: string): string | undefined;
2
+ export declare function viewsIn(source: string): readonly string[];
2
3
  interface Move {
3
4
  readonly to?: string;
4
5
  readonly through?: string;
@@ -14,7 +14,8 @@ import ts from 'typescript';
14
14
  import { exit, invoked } from "@prestack/thread/core/invoked";
15
15
  import { named as packageOf } from "@prestack/thread/core/scope";
16
16
  import { findTree, inTree } from "@prestack/thread/core/tree";
17
- import { nameOf as compositionOf } from "../compositions.js";
17
+ import { PERMITS_AT } from "@prestack/thread/operations/access";
18
+ import { COMPILED, compiledAt, nameOf as compositionOf } from "../compositions.js";
18
19
  import { PRESENTATIONS } from "../door.js";
19
20
  import { PAGE, entryAmong, nameOf, pageAt, presentedIn, routeFor } from "../screens.js";
20
21
  import { keyProps } from "./i18n.js";
@@ -24,29 +25,33 @@ const DECLARED = `src/_generated/seams.${'ts'}`;
24
25
  const CLIENT = `app/_generated/dispatching.${'ts'}`;
25
26
  const DOOR = `app/_generated/door.${'tsx'}`;
26
27
  const MAP = `app/_generated/map.${'ts'}`;
27
- const PROVIDER = `${packageOf('ui')}/provider`;
28
- const GRAMMAR = `${packageOf('ui')}/islands/map`;
28
+ const PROVIDER = `${COMPILED}/ui/provider.tsx`;
29
+ const GRAMMAR = `${COMPILED}/ui/islands/map.ts`;
29
30
  const MOVES = 'navigateTo';
30
31
  function door(knowing, wording, dispatching, moving, presenting) {
31
32
  const opening = presenting.length > 0;
32
- const props = `asking={ASKING}${knowing ? ' actor={actor}' : ''}${dispatching ? ' dispatch={dispatchTo}' : ''}` +
33
+ const props = `asking={ASKING}${knowing ? ' actor={actor} permits={gate}' : ''}${dispatching ? ' dispatch={dispatchTo}' : ''}` +
33
34
  `${moving ? ' navigateTo={navigateTo}' : ''}${wording ? ' locale={SOURCE} wording={WORDING}' : ''}`;
34
35
  return [
35
36
  "'use client'",
36
37
  '',
37
38
  '/* GENERATED by thread — do not edit. It is derived from what this app declares. */',
38
39
  '',
39
- ...(moving
40
- ? [`import { useCallback, ${opening ? 'useState, ' : ''}type ReactNode } from 'react'`, "import { useRouter } from 'next/navigation'"]
40
+ ...(moving || knowing
41
+ ? [
42
+ `import { useCallback, ${moving && opening ? 'useState, ' : ''}type ReactNode } from 'react'`,
43
+ ...(moving ? ["import { useRouter } from 'next/navigation'"] : []),
44
+ ]
41
45
  : ["import type { ReactNode } from 'react'"]),
42
46
  '',
43
- `import { ThreadUi } from '${PROVIDER}'`,
47
+ `import { ThreadUi } from '${reaching(DOOR, PROVIDER)}'`,
44
48
  '',
45
49
  "import { ASKING } from './declared.ts'",
50
+ ...(knowing ? [`import { permits } from '${reaching(DOOR, PERMITS_AT)}'`] : []),
46
51
  ...(dispatching ? ["import { dispatchTo } from './dispatching.ts'"] : []),
47
52
  ...(moving ? ["import { MAP } from './map.ts'"] : []),
48
53
  ...(wording ? ["import { SOURCE, WORDING } from './wording.ts'"] : []),
49
- ...(opening ? ['', ...presenting.map((one) => `import { ${one.name} } from '${reaching(DOOR, one.at)}'`)] : []),
54
+ ...(opening ? ['', ...presenting.map((one) => `import { ${one.name} } from '${reaching(DOOR, compiledAt(one.at))}'`)] : []),
50
55
  '',
51
56
  'export function Door({',
52
57
  ...(knowing
@@ -59,6 +64,14 @@ function door(knowing, wording, dispatching, moving, presenting) {
59
64
  : []),
60
65
  ' readonly children: ReactNode',
61
66
  '}) {',
67
+ ...(knowing
68
+ ? [
69
+ ' const gate = useCallback(',
70
+ ' (action: string): boolean => (actor === undefined ? false : permits(actor, action, null).allow),',
71
+ ' [actor],',
72
+ ' )',
73
+ ]
74
+ : []),
62
75
  ...(moving
63
76
  ? [
64
77
  ' const router = useRouter()',
@@ -115,6 +128,21 @@ const TYPED = /screen\(\s*'[^']*'\s*,\s*\{[^}]*route:\s*'([^']*)'/;
115
128
  export function typedRoute(source) {
116
129
  return TYPED.exec(source)?.[1];
117
130
  }
131
+ const SHOWING = /screen\(\s*'[^']*'\s*,\s*\{[^}]*views:\s*\[([^\]]*)\]/;
132
+ export function viewsIn(source) {
133
+ const held = SHOWING.exec(source)?.[1];
134
+ if (held === undefined)
135
+ return [];
136
+ return [...held.matchAll(QUOTED)].map(([, single, double]) => single ?? double ?? '').filter((one) => one !== '');
137
+ }
138
+ const VIEW = /^vw_[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
139
+ const OPERATION = /operation:\s*"([^"]+)"/g;
140
+ function reachable(at) {
141
+ const held = join(at, CLIENT);
142
+ if (!existsSync(held))
143
+ return [];
144
+ return [...readFileSync(held, 'utf8').matchAll(OPERATION)].map(([, one]) => one ?? '');
145
+ }
118
146
  function written(node) {
119
147
  if (node === undefined)
120
148
  return undefined;
@@ -243,7 +271,7 @@ function mapModule(map) {
243
271
  return [
244
272
  '/* GENERATED by thread — do not edit. It is derived from the screens, and from every navigateTo they and their compositions hold. */',
245
273
  '',
246
- `import type { MapIsland } from '${GRAMMAR}'`,
274
+ `import type { MapIsland } from '${reaching(MAP, GRAMMAR)}'`,
247
275
  '',
248
276
  'export const MAP: MapIsland = {',
249
277
  ' specVersion: 1,',
@@ -275,7 +303,7 @@ function reaching(file, at) {
275
303
  return `${up}${at.replace(/\.tsx?$/, '')}`;
276
304
  }
277
305
  function page(name, at, route) {
278
- const from = reaching(pageAt(route), at);
306
+ const from = reaching(pageAt(route), compiledAt(at));
279
307
  return [
280
308
  '/* GENERATED by thread — do not edit. It follows from the screen\'s name and its place among the pages. */',
281
309
  '',
@@ -287,32 +315,47 @@ function page(name, at, route) {
287
315
  '',
288
316
  ].join('\n');
289
317
  }
290
- function guarding(checker) {
291
- if (checker === undefined) {
292
- return [
293
- "throw new Error('thread: the compiler that checks this tree cannot be found, so nothing here " +
294
- "can be compiled. Reinstall the framework packages. A check that cannot run is not a check " +
295
- "that passed.')",
296
- '',
297
- ];
298
- }
318
+ function gate(typed) {
319
+ const said = typed ? ' as { of?: Record<string, string> }' : '';
299
320
  return [
300
- "import { ungated } from './react.ts'",
321
+ "import { createHash } from 'node:crypto'",
322
+ "import { existsSync, readFileSync, statSync } from 'node:fs'",
323
+ "import { join } from 'node:path'",
301
324
  '',
302
- "const why = ungated(`${process.cwd()}/app/layout.tsx`)",
303
- 'if (why !== null) throw new Error(`thread: ${why}`)',
325
+ "const receipt = join(process.cwd(), '.thread', 'gate.json')",
326
+ 'if (!existsSync(receipt)) {',
327
+ ' throw new Error(',
328
+ " 'thread: this tree has no passing gate, so nothing in it may compile. Run `thread gate` — or the ' +",
329
+ " 'dev, build and start scripts, which run it for you. A gate that has never run is not a gate that passed.',",
330
+ ' )',
331
+ '}',
332
+ "const passed = Symbol.for('thread.gate')",
333
+ "const stamp = String(statSync(receipt).mtimeMs) + ':' + String(statSync(receipt).size)",
334
+ 'if (Reflect.get(globalThis, passed) !== stamp) {',
335
+ ` const measured = JSON.parse(readFileSync(receipt, 'utf8'))${said}`,
336
+ ' for (const [named, was] of Object.entries(measured.of ?? {})) {',
337
+ ' const at = join(process.cwd(), named)',
338
+ " const now = existsSync(at) ? createHash('sha256').update(readFileSync(at)).digest('hex').slice(0, 16) : 'gone'",
339
+ ' if (now === was) continue',
340
+ ' throw new Error(',
341
+ " 'thread: ' + named + ' has changed since the gate last passed, so what this tree declares and what ' +",
342
+ " 'it was measured as no longer agree. Run `thread gate`',",
343
+ ' )',
344
+ ' }',
345
+ ' Reflect.set(globalThis, passed, stamp)',
346
+ '}',
304
347
  '',
305
348
  ];
306
349
  }
307
- function layout(wording, icons, source, checker, knowing, doored) {
350
+ function layout(wording, icons, source, knowing, doored) {
308
351
  return [
309
352
  '/* GENERATED by thread — do not edit. It is derived from what the other generators produce. */',
310
353
  '',
311
354
  '/// <reference types="next/types/global" />',
312
355
  '',
313
- ...guarding(checker),
356
+ ...gate(true),
314
357
  "import type { ReactNode } from 'react'",
315
- ...(doored ? ["import { Door } from './_generated/door.tsx'"] : [`import { ThreadUi } from '${PROVIDER}'`]),
358
+ ...(doored ? ["import { Door } from './_generated/door.tsx'"] : [`import { ThreadUi } from '${reaching(LAYOUT, PROVIDER)}'`]),
316
359
  ...(doored ? [] : ["import { ASKING } from './_generated/declared.ts'"]),
317
360
  ...(knowing
318
361
  ? ["import { headers } from 'next/headers'", `import { actor } from '../${DECLARED}'`]
@@ -365,14 +408,6 @@ function rootFor(at) {
365
408
  const common = shared.join('/');
366
409
  return relative(here, common) || '.';
367
410
  }
368
- function loaderFor(at) {
369
- try {
370
- return realpathSync(join(at, 'node_modules', packageOf('ui'), 'dist', 'generators', 'react.js'));
371
- }
372
- catch {
373
- return undefined;
374
- }
375
- }
376
411
  const DRIVER = /export\s+const\s+driver\s*:[^=\n]*=\s*\[([^\]]*)\]/;
377
412
  const QUOTED = /'([^']+)'|"([^"]+)"/g;
378
413
  const RELATIVE = /(?:^|[\s;{(])import\s+(?!type\s)(?:[\w*{][^'"]*from\s+)?['"](\.[^'"]+)['"]/g;
@@ -414,45 +449,14 @@ function externalFor(at) {
414
449
  }
415
450
  return [...found].sort();
416
451
  }
417
- function builtAs(loader) {
418
- try {
419
- const held = JSON.parse(readFileSync(join(dirname(loader), '..', '..', 'package.json'), 'utf8'));
420
- return held.version ?? 'unknown';
421
- }
422
- catch {
423
- return 'unknown';
424
- }
425
- }
426
- function config(root, loader, external) {
452
+ function config(root, external) {
427
453
  const turbopack = [
428
454
  ...(root === undefined ? [] : [` root: join(import.meta.dirname, ${JSON.stringify(root)}),`]),
429
- ...(loader === undefined
430
- ? []
431
- : [
432
- ' rules: {',
433
- ` '**/*.tsx': { loaders: [{ loader: ${JSON.stringify(loader)}, options: ${JSON.stringify({ built: builtAs(loader) })} }] },`,
434
- ' },',
435
- ]),
436
455
  ];
437
- const gate = loader === undefined
438
- ? [
439
- "throw new Error('thread: the compiler that checks this tree cannot be found, so nothing here " +
440
- "can be compiled. Reinstall the framework packages. A check that cannot run is not a check " +
441
- "that passed.')",
442
- '',
443
- ]
444
- : [
445
- `import { ungated } from ${JSON.stringify(loader)}`,
446
- '',
447
- 'const why = ungated(import.meta.filename)',
448
- 'if (why !== null) throw new Error(`thread: ${why}`)',
449
- '',
450
- ];
451
456
  return [
452
457
  '/* GENERATED by thread — do not edit. It is derived from where the packages resolve. */',
453
458
  '',
454
- ...(root === undefined ? [] : ["import { join } from 'node:path'", '']),
455
- ...gate,
459
+ ...gate(false),
456
460
  'export default {',
457
461
  ...(turbopack.length === 0 ? [] : [' turbopack: {', ...turbopack, ' },']),
458
462
  ...(external.length === 0
@@ -495,6 +499,7 @@ async function run() {
495
499
  const made = [];
496
500
  const routes = new Map();
497
501
  const screens = [];
502
+ const reaches = reachable(at);
498
503
  for (const row of rows) {
499
504
  const text = texts.get(row.id) ?? '';
500
505
  const typed = typedRoute(text);
@@ -514,12 +519,27 @@ async function run() {
514
519
  }
515
520
  const route = shownAs === PAGE ? routeFor(row.id, row.id === entry) : undefined;
516
521
  routes.set(name, route);
522
+ const shows = viewsIn(text);
523
+ for (const one of shows) {
524
+ if (!VIEW.test(one)) {
525
+ stderr.write(`routes: ${row.at} shows \`${one}\`, and that is not the name of a view. A view is ` +
526
+ '`vw_` and then lower case words joined by underscores. A screen names a declared read, ' +
527
+ 'and never states what the read does.\n');
528
+ return 1;
529
+ }
530
+ if (!reaches.includes(one)) {
531
+ stderr.write(`routes: ${row.at} shows \`${one}\`, and this app reaches no operation by that name. It reaches ` +
532
+ `${reaches.length === 0 ? 'nothing yet — \`thread capabilities\` writes that list, and the settle runs it before this' : reaches.join(', ')}. Declare the view with ` +
533
+ '`thread create operation`, or name one of those.\n');
534
+ return 1;
535
+ }
536
+ }
517
537
  screens.push({
518
538
  id: name,
519
539
  nameKey: `screen.${row.id}`,
520
540
  ...(route === undefined ? {} : { path: route }),
521
541
  entry: row.id === entry,
522
- shows: [],
542
+ shows: [...shows],
523
543
  });
524
544
  if (route === undefined)
525
545
  continue;
@@ -541,7 +561,7 @@ async function run() {
541
561
  }
542
562
  const map = { specVersion: 1, screens, links };
543
563
  made.push({ path: MAP, text: mapModule(map) });
544
- made.push({ path: CONFIG, text: config(rootFor(at), loaderFor(at), externalFor(at)) });
564
+ made.push({ path: CONFIG, text: config(rootFor(at), externalFor(at)) });
545
565
  const dispatching = existsSync(join(at, CLIENT));
546
566
  const moving = rows.length > 0;
547
567
  const doored = dispatching || moving;
@@ -552,7 +572,7 @@ async function run() {
552
572
  made.push({ path: DOOR, text: door(existsSync(join(at, DECLARED)), existsSync(join(at, WORDING)), dispatching, moving, presenting) });
553
573
  made.push({
554
574
  path: LAYOUT,
555
- text: layout(existsSync(join(at, WORDING)), existsSync(join(at, 'app/_generated/icons.ts')), source, loaderFor(at), existsSync(join(at, DECLARED)), doored),
575
+ text: layout(existsSync(join(at, WORDING)), existsSync(join(at, 'app/_generated/icons.ts')), source, existsSync(join(at, DECLARED)), doored),
556
576
  });
557
577
  const paged = map.screens.filter((one) => one.path !== undefined).length;
558
578
  const counted = `${String(map.screens.length)} screen(s), ${String(paged)} route(s), ${String(map.links.length)} link(s)`;