@prestack/thread-ui 0.1.9 → 0.1.11

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 (210) hide show
  1. package/CHANGELOG.md +366 -0
  2. package/dist/ui/attach.js +6 -7
  3. package/dist/ui/compositions.d.ts +0 -2
  4. package/dist/ui/compositions.js +2 -112
  5. package/dist/ui/generators/routes.js +1 -0
  6. package/dist/ui/variant.d.ts +1 -3
  7. package/package.json +2 -2
  8. package/dist/ui/animations/anm_appear.ts +0 -13
  9. package/dist/ui/animations/anm_blur_in.ts +0 -19
  10. package/dist/ui/animations/anm_blur_out.ts +0 -18
  11. package/dist/ui/animations/anm_dismiss.ts +0 -18
  12. package/dist/ui/animations/anm_drop.ts +0 -19
  13. package/dist/ui/animations/anm_fall.ts +0 -18
  14. package/dist/ui/animations/anm_flash.ts +0 -18
  15. package/dist/ui/animations/anm_glow.ts +0 -16
  16. package/dist/ui/animations/anm_lift.ts +0 -16
  17. package/dist/ui/animations/anm_nudge.ts +0 -16
  18. package/dist/ui/animations/anm_pop_in.ts +0 -23
  19. package/dist/ui/animations/anm_pop_out.ts +0 -22
  20. package/dist/ui/animations/anm_press.ts +0 -12
  21. package/dist/ui/animations/anm_pulse.ts +0 -13
  22. package/dist/ui/animations/anm_rise.ts +0 -19
  23. package/dist/ui/animations/anm_shake.ts +0 -13
  24. package/dist/ui/animations/anm_slide_in_left.ts +0 -19
  25. package/dist/ui/animations/anm_slide_in_right.ts +0 -19
  26. package/dist/ui/animations/anm_slide_out_left.ts +0 -18
  27. package/dist/ui/animations/anm_slide_out_right.ts +0 -18
  28. package/dist/ui/animations/anm_spin.ts +0 -12
  29. package/dist/ui/animations/anm_swell.ts +0 -16
  30. package/dist/ui/animations/anm_wobble.ts +0 -24
  31. package/dist/ui/animations/anm_zoom_in.ts +0 -19
  32. package/dist/ui/animations/anm_zoom_out.ts +0 -18
  33. package/dist/ui/animations.ts +0 -308
  34. package/dist/ui/attach.ts +0 -553
  35. package/dist/ui/charts.tsx +0 -69
  36. package/dist/ui/checked.ts +0 -32
  37. package/dist/ui/collector.ts +0 -140
  38. package/dist/ui/compositions.ts +0 -555
  39. package/dist/ui/consent.ts +0 -55
  40. package/dist/ui/content.tsx +0 -27
  41. package/dist/ui/contrast.ts +0 -27
  42. package/dist/ui/controls.tsx +0 -25
  43. package/dist/ui/disclosure.tsx +0 -20
  44. package/dist/ui/dismiss-layer.ts +0 -87
  45. package/dist/ui/door.ts +0 -140
  46. package/dist/ui/drag.ts +0 -316
  47. package/dist/ui/elements/accordion.tsx +0 -47
  48. package/dist/ui/elements/accordion.works.ts +0 -66
  49. package/dist/ui/elements/aspect-ratio.tsx +0 -31
  50. package/dist/ui/elements/aspect-ratio.works.ts +0 -5
  51. package/dist/ui/elements/avatar.tsx +0 -34
  52. package/dist/ui/elements/avatar.works.ts +0 -16
  53. package/dist/ui/elements/badge.tsx +0 -33
  54. package/dist/ui/elements/badge.works.ts +0 -9
  55. package/dist/ui/elements/bars.tsx +0 -50
  56. package/dist/ui/elements/bars.works.ts +0 -29
  57. package/dist/ui/elements/box.tsx +0 -36
  58. package/dist/ui/elements/breadcrumb.tsx +0 -38
  59. package/dist/ui/elements/breadcrumb.works.ts +0 -24
  60. package/dist/ui/elements/button.tsx +0 -46
  61. package/dist/ui/elements/button.works.tsx +0 -52
  62. package/dist/ui/elements/column.tsx +0 -29
  63. package/dist/ui/elements/consent.tsx +0 -71
  64. package/dist/ui/elements/consent.works.tsx +0 -132
  65. package/dist/ui/elements/donut.tsx +0 -45
  66. package/dist/ui/elements/donut.works.ts +0 -42
  67. package/dist/ui/elements/each.tsx +0 -34
  68. package/dist/ui/elements/each.works.tsx +0 -82
  69. package/dist/ui/elements/empty.tsx +0 -33
  70. package/dist/ui/elements/field.tsx +0 -45
  71. package/dist/ui/elements/field.works.ts +0 -65
  72. package/dist/ui/elements/funnel.tsx +0 -50
  73. package/dist/ui/elements/funnel.works.ts +0 -37
  74. package/dist/ui/elements/gauge.tsx +0 -40
  75. package/dist/ui/elements/gauge.works.ts +0 -42
  76. package/dist/ui/elements/glyph.tsx +0 -30
  77. package/dist/ui/elements/glyph.works.tsx +0 -42
  78. package/dist/ui/elements/grid.tsx +0 -29
  79. package/dist/ui/elements/heatmap.tsx +0 -48
  80. package/dist/ui/elements/heatmap.works.ts +0 -24
  81. package/dist/ui/elements/hit-box.tsx +0 -47
  82. package/dist/ui/elements/hit-box.works.tsx +0 -576
  83. package/dist/ui/elements/image.tsx +0 -29
  84. package/dist/ui/elements/line-chart.tsx +0 -45
  85. package/dist/ui/elements/line-chart.works.ts +0 -41
  86. package/dist/ui/elements/link.tsx +0 -38
  87. package/dist/ui/elements/link.works.ts +0 -25
  88. package/dist/ui/elements/media.tsx +0 -57
  89. package/dist/ui/elements/menu.tsx +0 -51
  90. package/dist/ui/elements/menu.works.tsx +0 -154
  91. package/dist/ui/elements/money.tsx +0 -36
  92. package/dist/ui/elements/money.works.ts +0 -9
  93. package/dist/ui/elements/pagination.tsx +0 -39
  94. package/dist/ui/elements/pagination.works.ts +0 -20
  95. package/dist/ui/elements/progress.tsx +0 -34
  96. package/dist/ui/elements/progress.works.ts +0 -8
  97. package/dist/ui/elements/radio-group.tsx +0 -50
  98. package/dist/ui/elements/radio-group.works.ts +0 -33
  99. package/dist/ui/elements/row.tsx +0 -29
  100. package/dist/ui/elements/scatter.tsx +0 -42
  101. package/dist/ui/elements/scatter.works.ts +0 -38
  102. package/dist/ui/elements/select.tsx +0 -43
  103. package/dist/ui/elements/select.works.ts +0 -5
  104. package/dist/ui/elements/separator.tsx +0 -31
  105. package/dist/ui/elements/skeleton.tsx +0 -31
  106. package/dist/ui/elements/slider.tsx +0 -34
  107. package/dist/ui/elements/slider.works.ts +0 -5
  108. package/dist/ui/elements/sparkline.tsx +0 -37
  109. package/dist/ui/elements/sparkline.works.ts +0 -32
  110. package/dist/ui/elements/spinner.tsx +0 -33
  111. package/dist/ui/elements/stack.tsx +0 -29
  112. package/dist/ui/elements/stat.tsx +0 -36
  113. package/dist/ui/elements/stat.works.ts +0 -14
  114. package/dist/ui/elements/surface.tsx +0 -36
  115. package/dist/ui/elements/surface.works.ts +0 -9
  116. package/dist/ui/elements/table.tsx +0 -62
  117. package/dist/ui/elements/table.works.ts +0 -30
  118. package/dist/ui/elements/tabs.tsx +0 -47
  119. package/dist/ui/elements/tabs.works.ts +0 -84
  120. package/dist/ui/elements/text.tsx +0 -33
  121. package/dist/ui/elements/textarea.tsx +0 -38
  122. package/dist/ui/elements/textarea.works.ts +0 -5
  123. package/dist/ui/elements/time.tsx +0 -40
  124. package/dist/ui/elements/time.works.ts +0 -29
  125. package/dist/ui/elements/toggle.tsx +0 -34
  126. package/dist/ui/elements/toggle.works.ts +0 -5
  127. package/dist/ui/elements/tooltip.tsx +0 -40
  128. package/dist/ui/elements/tooltip.works.ts +0 -77
  129. package/dist/ui/elements/value.tsx +0 -38
  130. package/dist/ui/elements/value.works.ts +0 -9
  131. package/dist/ui/elements.ts +0 -127
  132. package/dist/ui/focus-scope.ts +0 -121
  133. package/dist/ui/fonts.ts +0 -355
  134. package/dist/ui/generators/dev.d.ts +0 -1
  135. package/dist/ui/generators/dev.js +0 -61
  136. package/dist/ui/generators/dev.ts +0 -74
  137. package/dist/ui/generators/elements.ts +0 -796
  138. package/dist/ui/generators/i18n.ts +0 -283
  139. package/dist/ui/generators/icons.ts +0 -207
  140. package/dist/ui/generators/react.ts +0 -267
  141. package/dist/ui/generators/routes.ts +0 -847
  142. package/dist/ui/generators/show.ts +0 -261
  143. package/dist/ui/generators/styles.ts +0 -677
  144. package/dist/ui/generators/variants.ts +0 -196
  145. package/dist/ui/i18n/catalogs/en.ts +0 -191
  146. package/dist/ui/i18n/format.ts +0 -221
  147. package/dist/ui/i18n/locales.ts +0 -33
  148. package/dist/ui/i18n/parse.ts +0 -315
  149. package/dist/ui/i18n/translator.ts +0 -31
  150. package/dist/ui/i18n/types.ts +0 -96
  151. package/dist/ui/icons.ts +0 -331
  152. package/dist/ui/islands/map.ts +0 -58
  153. package/dist/ui/localizations.ts +0 -75
  154. package/dist/ui/material.ts +0 -236
  155. package/dist/ui/materials/mat_bordered.ts +0 -13
  156. package/dist/ui/materials/mat_ceramic.ts +0 -13
  157. package/dist/ui/materials/mat_clay.ts +0 -13
  158. package/dist/ui/materials/mat_elevated.ts +0 -24
  159. package/dist/ui/materials/mat_flat.ts +0 -34
  160. package/dist/ui/materials/mat_glass.ts +0 -20
  161. package/dist/ui/materials/mat_lit.ts +0 -13
  162. package/dist/ui/materials/mat_moulded.ts +0 -11
  163. package/dist/ui/materials.ts +0 -197
  164. package/dist/ui/modes.ts +0 -44
  165. package/dist/ui/navigation.tsx +0 -29
  166. package/dist/ui/positioning.ts +0 -90
  167. package/dist/ui/presented.tsx +0 -119
  168. package/dist/ui/provider.tsx +0 -184
  169. package/dist/ui/render-loop.ts +0 -133
  170. package/dist/ui/roving-focus.ts +0 -161
  171. package/dist/ui/screens.ts +0 -141
  172. package/dist/ui/scroll-lock.ts +0 -28
  173. package/dist/ui/series.ts +0 -97
  174. package/dist/ui/signals.ts +0 -386
  175. package/dist/ui/styleguide.ts +0 -231
  176. package/dist/ui/styleguides.ts +0 -374
  177. package/dist/ui/themes.ts +0 -22
  178. package/dist/ui/token.ts +0 -78
  179. package/dist/ui/tokens/shadow.ts +0 -25
  180. package/dist/ui/tokens.ts +0 -234
  181. package/dist/ui/validators.ts +0 -232
  182. package/dist/ui/variant.ts +0 -352
  183. package/dist/ui/variants/var_avatar-round.ts +0 -14
  184. package/dist/ui/variants/var_bubble-me.ts +0 -14
  185. package/dist/ui/variants/var_bubble-them.ts +0 -13
  186. package/dist/ui/variants/var_button-border.ts +0 -76
  187. package/dist/ui/variants/var_button-cta.ts +0 -76
  188. package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
  189. package/dist/ui/variants/var_button-destructive.ts +0 -76
  190. package/dist/ui/variants/var_button-fill.ts +0 -76
  191. package/dist/ui/variants/var_button-ghost.ts +0 -72
  192. package/dist/ui/variants/var_button-glass.ts +0 -55
  193. package/dist/ui/variants/var_button-normal.ts +0 -76
  194. package/dist/ui/variants/var_button-quiet.ts +0 -74
  195. package/dist/ui/variants/var_card.ts +0 -17
  196. package/dist/ui/variants/var_chip.ts +0 -13
  197. package/dist/ui/variants/var_choice.ts +0 -15
  198. package/dist/ui/variants/var_framed.ts +0 -10
  199. package/dist/ui/variants/var_glyph-plain.ts +0 -11
  200. package/dist/ui/variants/var_input.ts +0 -43
  201. package/dist/ui/variants/var_link-inline.ts +0 -29
  202. package/dist/ui/variants/var_list-row.ts +0 -34
  203. package/dist/ui/variants/var_rule.ts +0 -10
  204. package/dist/ui/variants/var_skeleton-block.ts +0 -12
  205. package/dist/ui/variants/var_spinner-turning.ts +0 -12
  206. package/dist/ui/variants/var_stat-tile.ts +0 -13
  207. package/dist/ui/variants/var_tab-bar.ts +0 -15
  208. package/dist/ui/variants/var_tap-target.ts +0 -25
  209. package/dist/ui/variants.ts +0 -64
  210. package/dist/ui/wrappers.tsx +0 -429
@@ -1,847 +0,0 @@
1
- import { dirname, join, relative } from 'node:path'
2
- import { argv, stderr, stdout } from 'node:process'
3
-
4
- import ts from 'typescript'
5
-
6
- import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from '@prestack/thread/integrations/disk'
7
- import { exit, invoked } from '@prestack/thread/core/invoked'
8
- import { listBounds, nestedBounds, rowsIn, valuesIn, withField, withRow, withoutField, withoutRow } from '@prestack/thread/core/rows'
9
- import { named as packageOf } from '@prestack/thread/core/scope'
10
- import { findTree, inTree } from '@prestack/thread/core/tree'
11
- import { PERMITS_AT } from '@prestack/thread/operations/access'
12
- import { said } from '@prestack/thread/operations/attach'
13
- import { ACTOR_AT, COMPILED } from '@prestack/thread/operations/capabilities'
14
-
15
- import { compiledAt, nameOf as compositionOf } from '../compositions.ts'
16
- import { PRESENTATIONS } from '../door.ts'
17
- import type { MapIsland } from '../islands/map.ts'
18
- import { PAGE, entryAmong, nameOf, pageAt, presentedIn, routeFor } from '../screens.ts'
19
-
20
- import { keyProps } from './i18n.ts'
21
-
22
- const LAYOUT = `app/layout.${'tsx'}`
23
-
24
- const WORDING = `app/_generated/wording.${'ts'}`
25
-
26
- const DECLARED = 'src/_generated/reaching.' + 'ts'
27
-
28
- const CLIENT = `app/_generated/dispatching.${'ts'}`
29
-
30
- const DOOR = `app/_generated/door.${'tsx'}`
31
-
32
- const MAP = `app/_generated/map.${'ts'}`
33
-
34
- const PROVIDER = `${COMPILED}/ui/provider.tsx`
35
-
36
- const GRAMMAR = `${COMPILED}/ui/islands/map.ts`
37
-
38
- const MOVES = 'navigateTo'
39
-
40
- const PAGE_ROW = 'page'
41
-
42
- const WRITER = 'thread routes'
43
-
44
- const PAGE_SAID =
45
- 'The page a browser gets at the address of this screen: `/` when it is the first page this app declares, and its own ' +
46
- 'name otherwise. `thread routes` writes the page and this row from where the screen sits, and moves or deletes both ' +
47
- 'when that changes.'
48
-
49
- function served(at: string): boolean {
50
- try {
51
- const held = JSON.parse(readFileSync(join(at, 'package.json'), 'utf8')) as {
52
- scripts?: Readonly<Record<string, string>>
53
- }
54
- return Object.values(held.scripts ?? {}).some((one) => /(?:^|[\s;&|])next(?:\s|$)/.test(one))
55
- } catch {
56
- return false
57
- }
58
- }
59
-
60
- const sheetAt = (at: string): Record<string, unknown> => {
61
- if (!existsSync(at)) return {}
62
- return valuesIn(readFileSync(at)) as Record<string, unknown>
63
- }
64
-
65
- function door(
66
- knowing: boolean,
67
- wording: boolean,
68
- dispatching: boolean,
69
- moving: boolean,
70
- presenting: readonly { readonly name: string; readonly at: string }[],
71
- ): string {
72
- const opening = presenting.length > 0
73
- const props =
74
- `asking={ASKING}${knowing ? ' actor={actor} permits={gate}' : ''}${dispatching ? ' dispatch={dispatchTo}' : ''}` +
75
- `${moving ? ' navigateTo={navigateTo}' : ''}${wording ? ' locale={SOURCE} wording={WORDING}' : ''}`
76
- return [
77
- "'use client'",
78
- '',
79
- ...(moving || knowing
80
- ? [
81
- `import { useCallback, ${moving && opening ? 'useState, ' : ''}type ReactNode } from 'react'`,
82
- ...(moving ? ["import { useRouter } from 'next/navigation'"] : []),
83
- ]
84
- : ["import type { ReactNode } from 'react'"]),
85
- '',
86
- `import { ThreadUi } from '${reaching(DOOR, PROVIDER)}'`,
87
- '',
88
- "import { ASKING } from './declared.ts'",
89
- ...(knowing ? [`import { permits } from '${reaching(DOOR, PERMITS_AT)}'`] : []),
90
- ...(dispatching ? ["import { dispatchTo } from './dispatching.ts'"] : []),
91
- ...(moving ? ["import { MAP } from './map.ts'"] : []),
92
- ...(wording ? ["import { SOURCE, WORDING } from './wording.ts'"] : []),
93
- ...(opening ? ['', ...presenting.map((one) => `import { ${one.name} } from '${reaching(DOOR, compiledAt(one.at))}'`)] : []),
94
- '',
95
- 'export function Door({',
96
- ...(knowing
97
- ? [' actor,']
98
- : []),
99
- ' children,',
100
- '}: {',
101
- ...(knowing
102
- ? [' readonly actor?: { readonly id: string | null; readonly roles: readonly string[]; readonly entitlements: readonly string[] }']
103
- : []),
104
- ' readonly children: ReactNode',
105
- '}) {',
106
- ...(knowing
107
- ? [
108
- ' const gate = useCallback(',
109
- ' (action: string): boolean => (actor === undefined ? false : permits(actor, action, null).allow),',
110
- ' [actor],',
111
- ' )',
112
- ]
113
- : []),
114
- ...(moving
115
- ? [
116
- ' const router = useRouter()',
117
- ...(opening ? [' const [presented, setPresented] = useState<string | undefined>(undefined)'] : []),
118
- ' const navigateTo = useCallback(',
119
- ' (screen: string): void => {',
120
- ...(opening
121
- ? [
122
- ' const found = MAP.screens.find((one) => one.id === screen)',
123
- ' if (found === undefined) {',
124
- ' throw new Error(',
125
- ' `thread: \\`${screen}\\` is not a screen this app has, so there is nothing to go to. ` +',
126
- " `The screens are ${MAP.screens.map((one) => one.id).join(', ')}.`,",
127
- ' )',
128
- ' }',
129
- ' if (found.path === undefined) {',
130
- ' setPresented(screen)',
131
- ' return',
132
- ' }',
133
- ' setPresented(undefined)',
134
- ' router.push(found.path)',
135
- ]
136
- : [
137
- ' const path = MAP.screens.find((one) => one.id === screen)?.path',
138
- ' if (path === undefined) {',
139
- ' throw new Error(',
140
- ' `thread: \\`${screen}\\` is not a screen this app has, so there is nothing to go to. ` +',
141
- " `The screens are ${MAP.screens.map((one) => one.id).join(', ')}.`,",
142
- ' )',
143
- ' }',
144
- ' router.push(path)',
145
- ]),
146
- ' },',
147
- ' [router],',
148
- ' )',
149
- ...(opening ? [' const dismiss = useCallback((): void => setPresented(undefined), [])'] : []),
150
- ]
151
- : []),
152
-
153
- ...(opening
154
- ? [
155
- ' return (',
156
- ` <ThreadUi ${props}>`,
157
- ' {children}',
158
- ...presenting.map((one) => ` <${one.name} open={presented === '${one.name}'} onDismiss={dismiss} />`),
159
- ' </ThreadUi>',
160
- ' )',
161
- ]
162
- : [` return <ThreadUi ${props}>{children}</ThreadUi>`]),
163
- '}',
164
- '',
165
- ].join('\n')
166
- }
167
-
168
- interface Row {
169
- readonly id: string
170
- readonly at: string
171
- readonly describes?: string
172
- readonly derives?: readonly { readonly id: string; readonly at: string }[]
173
- }
174
-
175
- const TYPED = /screen\(\s*'[^']*'\s*,\s*\{[^}]*route:\s*'([^']*)'/
176
-
177
- export function typedRoute(source: string): string | undefined {
178
- return TYPED.exec(source)?.[1]
179
- }
180
-
181
- const SHOWING = /screen\(\s*'[^']*'\s*,\s*\{[^}]*views:\s*\[([^\]]*)\]/
182
-
183
- export function viewsIn(source: string): readonly string[] {
184
- const held = SHOWING.exec(source)?.[1]
185
- if (held === undefined) return []
186
- return [...held.matchAll(QUOTED)].map(([, single, double]) => single ?? double ?? '').filter((one) => one !== '')
187
- }
188
-
189
- const VIEW = /^vw_[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/
190
-
191
- const OPERATION = /operation:\s*"([^"]+)"/g
192
-
193
- function reachable(at: string): readonly string[] {
194
- const held = join(at, CLIENT)
195
- if (!existsSync(held)) return []
196
- return [...readFileSync(held, 'utf8').matchAll(OPERATION)].map(([, one]) => one ?? '')
197
- }
198
-
199
- interface Move {
200
- readonly to?: string
201
- readonly through?: string
202
- }
203
-
204
- interface Holding {
205
- readonly moves: readonly Move[]
206
- readonly places: readonly string[]
207
- }
208
-
209
- function written(node: ts.Node | undefined): string | undefined {
210
- if (node === undefined) return undefined
211
- if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
212
- if (ts.isJsxExpression(node) || ts.isParenthesizedExpression(node)) return written(node.expression)
213
- return undefined
214
- }
215
-
216
- function keyed(name: ts.PropertyName): string | undefined {
217
- return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined
218
- }
219
-
220
- function attribute(held: ts.JsxAttributes, name: string): ts.JsxAttribute | undefined {
221
- for (const one of held.properties) {
222
- if (ts.isJsxAttribute(one) && ts.isIdentifier(one.name) && one.name.text === name) return one
223
- }
224
- return undefined
225
- }
226
-
227
- function property(held: ts.ObjectLiteralExpression, name: string): ts.PropertyAssignment | undefined {
228
- for (const one of held.properties) {
229
- if (ts.isPropertyAssignment(one) && keyed(one.name) === name) return one
230
- }
231
- return undefined
232
- }
233
-
234
- function moveOf(found: (name: string) => ts.Node | undefined, wording: readonly string[]): Move {
235
- const to = written(found(MOVES))
236
- let through: string | undefined
237
- for (const key of wording) {
238
- through = written(found(key))
239
- if (through !== undefined) break
240
- }
241
- return { ...(to === undefined ? {} : { to }), ...(through === undefined ? {} : { through }) }
242
- }
243
-
244
- export function holding(text: string, at: string, wording: readonly string[], placeable: ReadonlySet<string>): Holding {
245
- const file = ts.createSourceFile(at, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
246
- const moves: Move[] = []
247
- const places = new Set<string>()
248
-
249
- const walk = (node: ts.Node): void => {
250
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
251
- const tag = node.tagName
252
- if (ts.isIdentifier(tag) && placeable.has(tag.text)) places.add(tag.text)
253
- if (attribute(node.attributes, MOVES) !== undefined) {
254
- moves.push(moveOf((name) => attribute(node.attributes, name)?.initializer, wording))
255
- }
256
- }
257
- if (ts.isObjectLiteralExpression(node) && property(node, MOVES) !== undefined) {
258
- moves.push(moveOf((name) => property(node, name)?.initializer, wording))
259
- }
260
- ts.forEachChild(node, walk)
261
- }
262
- walk(file)
263
-
264
- return { moves, places: [...places].sort() }
265
- }
266
-
267
- function wordingFirst(props: readonly string[]): readonly string[] {
268
- const first = ['label', 'labelKey']
269
- return [...first.filter((one) => props.includes(one)), ...props.filter((one) => !first.includes(one))]
270
- }
271
-
272
- function linksOf(
273
- at: string,
274
- rows: readonly Row[],
275
- compositions: readonly Row[],
276
- routes: ReadonlyMap<string, string | undefined>,
277
- wording: readonly string[],
278
- ): { readonly links: MapIsland['links']; readonly wrong: readonly string[] } {
279
- const named = new Map<string, string>()
280
- for (const row of compositions) named.set(compositionOf(row.at), row.at)
281
- for (const row of rows) named.set(nameOf(row.at), row.at)
282
- const placeable = new Set(named.keys())
283
- const screens = [...routes.keys()].sort()
284
-
285
- const held = new Map<string, Holding>()
286
- const read = (path: string): Holding => {
287
- const kept = held.get(path)
288
- if (kept !== undefined) return kept
289
- const file = join(at, path)
290
- const made = existsSync(file) ? holding(readFileSync(file, 'utf8'), path, wording, placeable) : { moves: [], places: [] }
291
- held.set(path, made)
292
- return made
293
- }
294
-
295
- const links: MapIsland['links'][number][] = []
296
- const stated = new Set<string>()
297
- const wrong: string[] = []
298
-
299
- for (const row of rows) {
300
- const from = nameOf(row.at)
301
- const seen = new Set<string>()
302
- const gather = (path: string): void => {
303
- if (seen.has(path)) return
304
- seen.add(path)
305
- const one = read(path)
306
- for (const move of one.moves) {
307
- const through = move.through === undefined ? '' : ` through \`${move.through}\``
308
- if (move.to === undefined) {
309
- wrong.push(
310
- `${path} moves somebody with a \`${MOVES}\` that is not written out as a screen name, so the map cannot ` +
311
- `say where ${from} goes${through}. Name the screen — \`${MOVES}="${screens[0] ?? 'Home'}"\`.`,
312
- )
313
- continue
314
- }
315
- if (!routes.has(move.to)) {
316
- wrong.push(
317
- `${from} goes to \`${move.to}\`${through}, and no screen is called that. The screens are ` +
318
- `${screens.join(', ')} — declare it with \`thread create screen\`, or name one of those.`,
319
- )
320
- continue
321
- }
322
- const link = { from, to: move.to, ...(move.through === undefined ? {} : { labelKey: move.through }) }
323
- const key = JSON.stringify(link)
324
- if (stated.has(key)) continue
325
- stated.add(key)
326
- links.push(link)
327
- }
328
- for (const place of one.places) {
329
- const where = named.get(place)
330
- if (where !== undefined) gather(where)
331
- }
332
- }
333
- gather(row.at)
334
- }
335
-
336
- return { links, wrong }
337
- }
338
-
339
- const quoted = (value: string): string => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
340
-
341
- function mapModule(map: MapIsland): string {
342
- const screen = (one: MapIsland['screens'][number]): string =>
343
- ` { id: ${quoted(one.id)}, nameKey: ${quoted(one.nameKey)}, ` +
344
- `${one.path === undefined ? '' : `path: ${quoted(one.path)}, `}` +
345
- `entry: ${String(one.entry)}, shows: [${one.shows.map(quoted).join(', ')}] },`
346
- const link = (one: MapIsland['links'][number]): string =>
347
- ` { from: ${quoted(one.from)}, to: ${quoted(one.to)}${one.labelKey === undefined ? '' : `, labelKey: ${quoted(one.labelKey)}`} },`
348
- return [
349
- `import type { MapIsland } from '${reaching(MAP, GRAMMAR)}'`,
350
- '',
351
- 'export const MAP: MapIsland = {',
352
- ' specVersion: 1,',
353
- ' screens: [',
354
- ...map.screens.map(screen),
355
- ' ],',
356
- ' links: [',
357
- ...map.links.map(link),
358
- ' ],',
359
- '}',
360
- '',
361
- ].join('\n')
362
- }
363
-
364
- function shown(map: MapIsland): readonly string[] {
365
- return [
366
- ...map.screens.map((one) =>
367
- one.path === undefined
368
- ? ` ${one.id} has no route — a press opens it where somebody already is`
369
- : ` ${one.id} answers ${one.path}${one.entry ? ' — the entry, because it is the first page declared' : ''}`,
370
- ),
371
- ...map.links.map((one) => ` ${one.from} → ${one.to}${one.labelKey === undefined ? '' : `, through ${one.labelKey}`}`),
372
- ...map.screens
373
- .filter((one) => !one.entry && !map.links.some((link) => link.to === one.id))
374
- .map((one) =>
375
- one.path === undefined
376
- ? ` ${one.id} — no button goes there, and a screen with no route is reached no other way`
377
- : ` ${one.id} — no button goes there, so it is reached only by its address ${one.path}`,
378
- ),
379
- ]
380
- }
381
-
382
- function reaching(file: string, at: string): string {
383
- const up = '../'.repeat(file.split('/').length - 1)
384
- return `${up}${at.replace(/\.tsx?$/, '')}`
385
- }
386
-
387
- function page(name: string, at: string, route: string): string {
388
- const from = reaching(pageAt(route), compiledAt(at))
389
- return [
390
- `import { ${name} } from '${from}'`,
391
- '',
392
- 'export default function Page() {',
393
- ` return <${name} />`,
394
- '}',
395
- '',
396
- ].join('\n')
397
- }
398
-
399
- interface Repaged {
400
- readonly id: string
401
- readonly shown: string
402
- readonly route: string | undefined
403
- readonly was: string | undefined
404
- readonly where: string | undefined
405
- }
406
-
407
- const pageShaped = (path: string): boolean => path === pageAt(`/${path.split('/').slice(1, -1).join('/')}`)
408
-
409
- function unpaged(one: Repaged): string {
410
- if (one.where === undefined) {
411
- return (
412
- `\`${one.id}\` is presented as a ${one.shown}, so it has no page, and its row still derives ${String(one.was)}. ` +
413
- `Run \`${WRITER}\`, which takes that row out and deletes the page.`
414
- )
415
- }
416
- return (
417
- `\`${one.id}\` answers ${String(one.route)}, so its page is ${one.where}, and its row ` +
418
- `${one.was === undefined ? 'derives no page' : `says ${one.was}`}. Run \`${WRITER}\`, which writes that row from ` +
419
- 'where the screen now sits and puts the page there.'
420
- )
421
- }
422
-
423
- function repaged(text: string, moved: readonly Repaged[]): string | undefined {
424
- let next = text
425
- for (const one of moved) {
426
- const list = listBounds(next, 'declares')
427
- const inside = list === undefined ? undefined : nestedBounds(next, list, one.id, 'derives')
428
- if (inside !== undefined) next = withoutRow(next, inside, PAGE_ROW)
429
- const rows = listBounds(next, 'declares')
430
- if (rows === undefined) return undefined
431
- const within = nestedBounds(next, rows, one.id, 'derives')
432
- if (one.where === undefined) {
433
- if (within !== undefined && rowsIn(next, within).length === 0) next = withoutField(next, rows, one.id, 'derives')
434
- continue
435
- }
436
- const row = said({ id: PAGE_ROW, at: one.where, describes: PAGE_SAID, version: 1, by: WRITER, runs: `${WRITER} --check` }, ' ')
437
- const put = within === undefined ? withField(next, rows, one.id, 'derives', `[\n${row}\n ]`) : withRow(next, within, row)
438
- if (put === undefined) return undefined
439
- next = put
440
- }
441
- return next
442
- }
443
-
444
- function gate(typed: boolean): readonly string[] {
445
- const said = typed ? ' as { of?: Record<string, string> }' : ''
446
- return [
447
- "import { createHash } from 'node:crypto'",
448
- "import { existsSync, readFileSync, statSync } from 'node:fs'",
449
- "import { join } from 'node:path'",
450
- '',
451
- "const receipt = join(process.cwd(), '.thread', 'gate.json')",
452
- 'if (!existsSync(receipt)) {',
453
- ' throw new Error(',
454
- " 'thread: this tree has no passing gate, so nothing in it may compile. Run `thread gate` — or the ' +",
455
- " 'thread dev, which runs it before anything is served. A gate that has never run is not a gate that passed.',",
456
- ' )',
457
- '}',
458
- "const passed = Symbol.for('thread.gate')",
459
- "const stamp = String(statSync(receipt).mtimeMs) + ':' + String(statSync(receipt).size)",
460
- 'if (Reflect.get(globalThis, passed) !== stamp) {',
461
- ` const measured = JSON.parse(readFileSync(receipt, 'utf8'))${said}`,
462
- ' for (const [named, was] of Object.entries(measured.of ?? {})) {',
463
- ' const at = join(process.cwd(), named)',
464
- " const now = existsSync(at) ? createHash('sha256').update(readFileSync(at)).digest('hex').slice(0, 16) : 'gone'",
465
- ' if (now === was) continue',
466
- ' throw new Error(',
467
- " 'thread: ' + named + ' has changed since the gate last passed, so what this tree declares and what ' +",
468
- " 'it was measured as no longer agree. Run `thread gate`',",
469
- ' )',
470
- ' }',
471
- ' Reflect.set(globalThis, passed, stamp)',
472
- '}',
473
- '',
474
- ]
475
- }
476
-
477
- function layout(
478
- wording: boolean,
479
- icons: boolean,
480
- source: string,
481
- knowing: boolean,
482
- doored: boolean,
483
- ): string {
484
- return [
485
- ...gate(true),
486
- "import type { ReactNode } from 'react'",
487
- "import type {} from 'next'",
488
- ...(doored ? ["import { Door } from './_generated/door.tsx'"] : [`import { ThreadUi } from '${reaching(LAYOUT, PROVIDER)}'`]),
489
-
490
- ...(doored ? [] : ["import { ASKING } from './_generated/declared.ts'"]),
491
- ...(knowing
492
- ? ["import { headers } from 'next/headers'", `import { actor } from '../${ACTOR_AT}'`]
493
- : []),
494
-
495
- "import './_generated/tokens.css'",
496
- ...(icons ? ["import { ThreadIcons } from './_generated/icons.ts'"] : []),
497
-
498
- ...(wording && !doored ? ["import { SOURCE, WORDING } from './_generated/wording.ts'"] : []),
499
- '',
500
- knowing
501
- ? 'export default async function RootLayout({ children }: { children: ReactNode }) {'
502
- : 'export default function RootLayout({ children }: { children: ReactNode }) {',
503
-
504
- ...(knowing
505
- ? [" const who = await actor(new Request('http://localhost', { headers: await headers() }))"]
506
- : []),
507
- ' return (',
508
- ` <html lang="${source}">`,
509
- ' <body>',
510
- ...(icons ? [' <ThreadIcons />'] : []),
511
- doored
512
- ? ` <Door${knowing ? ' actor={who}' : ''}>{children}</Door>`
513
- : ` <ThreadUi asking={ASKING}${knowing ? ' actor={who}' : ''}${wording ? ' locale={SOURCE} wording={WORDING}' : ''}>{children}</ThreadUi>`,
514
- ' </body>',
515
- ' </html>',
516
- ' )',
517
- '}',
518
- '',
519
- ].join('\n')
520
- }
521
-
522
- const CONFIG = `next.config.${'js'}`
523
-
524
- const SHADOWS: readonly string[] = [`next.config.${'mjs'}`, `next.config.${'ts'}`, `next.config.${'mts'}`]
525
-
526
- function rootFor(at: string): string | undefined {
527
- let real: string
528
- try {
529
- real = realpathSync(join(at, 'node_modules', packageOf('ui')))
530
- } catch {
531
- return undefined
532
- }
533
- const here = realpathSync(at)
534
- if (real.startsWith(`${here}/`)) return undefined
535
-
536
- const one = here.split('/')
537
- const two = real.split('/')
538
- const shared: string[] = []
539
- for (let step = 0; step < Math.min(one.length, two.length); step += 1) {
540
- if (one[step] !== two[step]) break
541
- shared.push(one[step] ?? '')
542
- }
543
- const common = shared.join('/')
544
- return relative(here, common) || '.'
545
- }
546
-
547
- const DRIVER = /export\s+const\s+driver\s*:[^=\n]*=\s*\[([^\]]*)\]/
548
-
549
- const QUOTED = /'([^']+)'|"([^"]+)"/g
550
-
551
- const RELATIVE = /(?:^|[\s;{(])import\s+(?!type\s)(?:[\w*{][^'"]*from\s+)?['"](\.[^'"]+)['"]/g
552
-
553
- function externalFor(at: string): readonly string[] {
554
- const wiring = join(at, DECLARED)
555
- if (!existsSync(wiring)) return []
556
-
557
- const read = (path: string): string => {
558
- try {
559
- return readFileSync(path, 'utf8')
560
- } catch {
561
- return ''
562
- }
563
- }
564
-
565
- const found = new Set<string>()
566
- const wiredIn = read(wiring)
567
-
568
- const reachable = [...wiredIn.matchAll(RELATIVE)].map(([, one]) => {
569
- const path = join(dirname(wiring), (one ?? '').replace(/\.tsx?$/, '')) + '.ts'
570
- return [path, read(path)] as const
571
- })
572
-
573
- const STORE = /export\s+const\s+store\s*:/
574
-
575
- for (const [where, text] of [[wiring, wiredIn] as const, ...reachable]) {
576
- const held = DRIVER.exec(text)?.[1]
577
- if (held === undefined) {
578
- if (where === wiring || !STORE.test(text)) continue
579
- stderr.write(
580
- `routes: ${relative(at, where)} declares a store and does not say what must stay out of the bundle. ` +
581
- 'A store opens something, and a config written without that entry breaks where the driver is used rather ' +
582
- "than here. Re-run the command that writes it — if it is older than the `driver` export, that is what is " +
583
- 'missing.\n',
584
- )
585
- exit(1)
586
- }
587
- for (const [, single, double] of held.matchAll(QUOTED)) {
588
- const one = single ?? double
589
- if (one !== undefined && one !== '') found.add(one)
590
- }
591
- }
592
-
593
- return [...found].sort()
594
- }
595
-
596
- function config(root: string | undefined, external: readonly string[]): string {
597
- const turbopack = [
598
- ...(root === undefined ? [] : [` root: join(import.meta.dirname, ${JSON.stringify(root)}),`]),
599
- ]
600
-
601
- return [
602
- ...gate(false),
603
- 'export default {',
604
- ...(turbopack.length === 0 ? [] : [' turbopack: {', ...turbopack, ' },']),
605
-
606
- ...(external.length === 0
607
- ? []
608
- : [` serverExternalPackages: [${external.map((one) => JSON.stringify(one)).join(', ')}],`]),
609
- ' agentRules: false,',
610
- '}',
611
- '',
612
- ].join('\n')
613
- }
614
-
615
- interface Node {
616
- readonly declares?: readonly Row[]
617
- }
618
-
619
- function landed(at: string, made: readonly { readonly path: string; readonly text: string }[]): number | undefined {
620
- if (!argv.includes('--check')) {
621
- for (const one of made) writeFileSync(join(at, one.path), one.text, 'utf8')
622
- return undefined
623
- }
624
-
625
- for (const one of SHADOWS) {
626
- if (!existsSync(join(at, one))) continue
627
- stderr.write(
628
- `routes: ${one} sits beside ${CONFIG}, and Next loads ${CONFIG} because it is the first name it ` +
629
- `looks for. Nothing reads ${one} — an edit to it changes nothing and says nothing. Delete it.\n`,
630
- )
631
- return 1
632
- }
633
-
634
- for (const one of made) {
635
- let current = ''
636
- try {
637
- current = readFileSync(join(at, one.path), 'utf8')
638
- } catch {
639
- current = ''
640
- }
641
- if (current === one.text) continue
642
- stderr.write(
643
- `routes: ${one.path} is not what the declarations produce. A derived file that differs from its ` +
644
- 'generator is a hand edit wearing a generated name. Run `thread routes` and commit what it writes.\n',
645
- )
646
- return 1
647
- }
648
-
649
- return undefined
650
- }
651
-
652
- async function run(): Promise<number> {
653
- const tree = findTree()
654
- const at = tree.at
655
-
656
- const screensAt = inTree(tree, `screens.${'ts'}`)
657
- if (!existsSync(screensAt)) {
658
- if (!served(at)) {
659
- stdout.write(
660
- `routes: this tree has no ${relative(at, screensAt)} and its manifest runs no page server, so it declares ` +
661
- 'no screen and there is nothing to write or check.\n',
662
- )
663
- return 0
664
- }
665
-
666
- const only = [{ path: CONFIG, text: config(rootFor(at), externalFor(at)) }]
667
- const refused = landed(at, only)
668
- if (refused !== undefined) return refused
669
- stdout.write(
670
- `routes: ${CONFIG} ${argv.includes('--check') ? 'is what the declarations produce' : 'written'} — this tree ` +
671
- `declares no screen, and Next serves it all the same.\n`,
672
- )
673
- return 0
674
- }
675
- const sheet = valuesIn(readFileSync(screensAt)) as { SCREEN?: Node }
676
- if (sheet.SCREEN === undefined) {
677
- stderr.write('routes: _threads/screens.ts exports no `SCREEN`, so nothing declares a screen here.\n')
678
- return 1
679
- }
680
-
681
- const held = sheetAt(inTree(tree, `localizations.${'ts'}`)) as {
682
- LOCALIZATION?: { readonly holds?: readonly { readonly id: string; readonly is?: string }[] }
683
- }
684
- const source = held.LOCALIZATION?.holds?.find((one) => one.id === 'source')?.is ?? 'en'
685
-
686
- const placed = sheetAt(inTree(tree, `compositions.${'ts'}`)) as {
687
- COMPOSITION?: Node
688
- }
689
- const compositions = placed.COMPOSITION?.declares ?? []
690
-
691
- const rows = sheet.SCREEN.declares ?? []
692
- const texts = new Map<string, string>()
693
- for (const row of rows) {
694
- const file = join(at, row.at)
695
- if (!existsSync(file)) {
696
- stderr.write(`routes: \`${row.id}\` is declared at \`${row.at}\` and the file is not there. Run \`thread screens\`.\n`)
697
- return 1
698
- }
699
- texts.set(row.id, readFileSync(file, 'utf8'))
700
- }
701
- const shownOf = (id: string): string => presentedIn(texts.get(id) ?? '')
702
- const entry = entryAmong(rows, shownOf)
703
-
704
- const made: { readonly path: string; readonly text: string }[] = []
705
- const moved: Repaged[] = []
706
- const routes = new Map<string, string | undefined>()
707
- const screens: MapIsland['screens'][number][] = []
708
- const reaches = reachable(at)
709
-
710
- for (const row of rows) {
711
- const text = texts.get(row.id) ?? ''
712
- const typed = typedRoute(text)
713
- if (typed !== undefined) {
714
- stderr.write(
715
- `routes: ${row.at} states \`route: '${typed}'\`, and a route is never typed: the first page an app declares ` +
716
- 'answers `/`, every other page answers its own name, and a sheet has no route. Take `route:` out of the ' +
717
- `file with \`thread edit ${row.at} --find <text> --replace <text>\`, which changes that one stretch of it.\n`,
718
- )
719
- return 1
720
- }
721
- const name = nameOf(row.at)
722
- const shownAs = shownOf(row.id)
723
- if (!(PRESENTATIONS as readonly string[]).includes(shownAs)) {
724
- stderr.write(
725
- `routes: ${row.at} is presented as \`${shownAs}\`, and a presentation is not a free word: it is one of ` +
726
- `${PRESENTATIONS.join(', ')}, because each carries a settled behaviour for portalling, trapping focus and dismissing, ` +
727
- 'and one that is not among them would compile and behave as none of them. Change the `as:` in that file.\n',
728
- )
729
- return 1
730
- }
731
- const route = shownAs === PAGE ? routeFor(row.id, row.id === entry) : undefined
732
- routes.set(name, route)
733
- const shows = viewsIn(text)
734
- for (const one of shows) {
735
- if (!VIEW.test(one)) {
736
- stderr.write(
737
- `routes: ${row.at} shows \`${one}\`, and that is not the name of a view. A view is ` +
738
- '`vw_` and then lower case words joined by underscores. A screen names a declared read, ' +
739
- 'and never states what the read does.\n',
740
- )
741
- return 1
742
- }
743
- if (!reaches.includes(one)) {
744
- stderr.write(
745
- `routes: ${row.at} shows \`${one}\`, and this app reaches no operation by that name. It reaches ` +
746
- `${reaches.length === 0 ? 'nothing yet — \`thread capabilities\` writes that list, and the settle runs it before this' : reaches.join(', ')}. Declare the view with ` +
747
- '`thread create operation`, or name one of those.\n',
748
- )
749
- return 1
750
- }
751
- }
752
- screens.push({
753
- id: name,
754
- nameKey: `screen.${row.id}`,
755
- ...(route === undefined ? {} : { path: route }),
756
- entry: row.id === entry,
757
- shows: [...shows],
758
- })
759
- const where = route === undefined ? undefined : pageAt(route)
760
- const was = (row.derives ?? []).find((one) => one.id === PAGE_ROW)?.at
761
- if (was !== where) moved.push({ id: row.id, shown: shownAs, route, was, where })
762
- if (route === undefined || where === undefined) continue
763
- made.push({ path: where, text: page(name, row.at, route) })
764
- }
765
-
766
- if (moved.length > 0 && argv.includes('--check')) {
767
- for (const one of moved) stderr.write(`routes: ${unpaged(one)}\n`)
768
- return 1
769
- }
770
-
771
- const { links, wrong } = linksOf(at, rows, compositions, routes, wordingFirst(keyProps(at)))
772
- if (wrong.length > 0) {
773
- for (const one of wrong) stderr.write(`routes: ${one}\n`)
774
- return 1
775
- }
776
-
777
- const map: MapIsland = { specVersion: 1, screens, links }
778
- made.push({ path: MAP, text: mapModule(map) })
779
-
780
- made.push({ path: CONFIG, text: config(rootFor(at), externalFor(at)) })
781
-
782
- const dispatching = existsSync(join(at, CLIENT))
783
- const moving = rows.length > 0
784
- const doored = dispatching || moving
785
- const presenting = rows
786
- .filter((row) => routes.get(nameOf(row.at)) === undefined)
787
- .map((row) => ({ name: nameOf(row.at), at: row.at }))
788
-
789
- if (doored) made.push({ path: DOOR, text: door(existsSync(join(at, ACTOR_AT)), existsSync(join(at, WORDING)), dispatching, moving, presenting) })
790
-
791
- made.push({
792
- path: LAYOUT,
793
-
794
- text: layout(
795
- existsSync(join(at, WORDING)),
796
- existsSync(join(at, 'app/_generated/icons.ts')),
797
- source,
798
- existsSync(join(at, ACTOR_AT)),
799
- doored,
800
- ),
801
- })
802
-
803
- const paged = map.screens.filter((one) => one.path !== undefined).length
804
- const counted = `${String(map.screens.length)} screen(s), ${String(paged)} route(s), ${String(map.links.length)} link(s)`
805
-
806
- if (moved.length > 0) {
807
- const declaredIn = relative(at, screensAt)
808
- const rewritten = repaged(readFileSync(screensAt, 'utf8'), moved)
809
- if (rewritten === undefined) {
810
- stderr.write(
811
- `routes: ${declaredIn} could not be given the page row of each screen, so nothing was written. ` +
812
- `\`thread read ${declaredIn}\` prints what it states now.\n`,
813
- )
814
- return 1
815
- }
816
- writeFileSync(screensAt, rewritten, 'utf8')
817
- const answered = new Set(made.map((one) => one.path))
818
- const home = dirname(join(at, pageAt('/')))
819
- for (const one of moved) {
820
- stdout.write(
821
- one.where === undefined
822
- ? `routes: \`${one.id}\` has no route, so its row no longer derives ${String(one.was)}\n`
823
- : `routes: \`${one.id}\` answers ${String(one.route)}, so its row now derives ${one.where}\n`,
824
- )
825
- if (one.was === undefined || answered.has(one.was) || !pageShaped(one.was) || !existsSync(join(at, one.was))) continue
826
- rmSync(join(at, one.was))
827
- const folder = dirname(join(at, one.was))
828
- if (folder !== home && readdirSync(folder).length === 0) rmSync(folder, { recursive: true, force: true })
829
- stdout.write(`routes: deleted ${one.was}, because no screen answers there any more\n`)
830
- }
831
- }
832
-
833
- const refused = landed(at, made)
834
- if (refused !== undefined) return refused
835
-
836
- if (argv.includes('--check')) {
837
- stdout.write(`routes: ${String(made.length)} file(s) are what the screens produce — ${counted}\n`)
838
- for (const line of shown(map)) stdout.write(`${line}\n`)
839
- return 0
840
- }
841
- stdout.write(`routes: the map — ${counted}\n`)
842
- for (const line of shown(map)) stdout.write(`${line}\n`)
843
- stdout.write(`routes: wrote ${String(paged)} page(s), the map and the layout\n`)
844
- return 0
845
- }
846
-
847
- if (invoked(import.meta.url)) exit(await run())