@prestack/thread-ui 0.0.1 → 0.1.0

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