@prestack/thread-ui 0.1.10 → 0.1.12
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.
- package/CHANGELOG.md +375 -0
- package/dist/ui/attach.js +6 -7
- package/dist/ui/compositions.d.ts +0 -8
- package/dist/ui/compositions.js +3 -166
- package/dist/ui/generators/routes.js +1 -0
- package/package.json +2 -2
- package/dist/ui/animations/anm_appear.ts +0 -13
- package/dist/ui/animations/anm_blur_in.ts +0 -19
- package/dist/ui/animations/anm_blur_out.ts +0 -18
- package/dist/ui/animations/anm_dismiss.ts +0 -18
- package/dist/ui/animations/anm_drop.ts +0 -19
- package/dist/ui/animations/anm_fall.ts +0 -18
- package/dist/ui/animations/anm_flash.ts +0 -18
- package/dist/ui/animations/anm_glow.ts +0 -16
- package/dist/ui/animations/anm_lift.ts +0 -16
- package/dist/ui/animations/anm_nudge.ts +0 -16
- package/dist/ui/animations/anm_pop_in.ts +0 -23
- package/dist/ui/animations/anm_pop_out.ts +0 -22
- package/dist/ui/animations/anm_press.ts +0 -12
- package/dist/ui/animations/anm_pulse.ts +0 -13
- package/dist/ui/animations/anm_rise.ts +0 -19
- package/dist/ui/animations/anm_shake.ts +0 -13
- package/dist/ui/animations/anm_slide_in_left.ts +0 -19
- package/dist/ui/animations/anm_slide_in_right.ts +0 -19
- package/dist/ui/animations/anm_slide_out_left.ts +0 -18
- package/dist/ui/animations/anm_slide_out_right.ts +0 -18
- package/dist/ui/animations/anm_spin.ts +0 -12
- package/dist/ui/animations/anm_swell.ts +0 -16
- package/dist/ui/animations/anm_wobble.ts +0 -24
- package/dist/ui/animations/anm_zoom_in.ts +0 -19
- package/dist/ui/animations/anm_zoom_out.ts +0 -18
- package/dist/ui/animations.ts +0 -308
- package/dist/ui/attach.ts +0 -553
- package/dist/ui/charts.tsx +0 -69
- package/dist/ui/checked.ts +0 -32
- package/dist/ui/collector.ts +0 -140
- package/dist/ui/compositions.ts +0 -613
- package/dist/ui/consent.ts +0 -55
- package/dist/ui/content.tsx +0 -27
- package/dist/ui/contrast.ts +0 -27
- package/dist/ui/controls.tsx +0 -25
- package/dist/ui/disclosure.tsx +0 -20
- package/dist/ui/dismiss-layer.ts +0 -87
- package/dist/ui/door.ts +0 -140
- package/dist/ui/drag.ts +0 -316
- package/dist/ui/elements/accordion.tsx +0 -47
- package/dist/ui/elements/accordion.works.ts +0 -66
- package/dist/ui/elements/aspect-ratio.tsx +0 -31
- package/dist/ui/elements/aspect-ratio.works.ts +0 -5
- package/dist/ui/elements/avatar.tsx +0 -34
- package/dist/ui/elements/avatar.works.ts +0 -16
- package/dist/ui/elements/badge.tsx +0 -33
- package/dist/ui/elements/badge.works.ts +0 -9
- package/dist/ui/elements/bars.tsx +0 -50
- package/dist/ui/elements/bars.works.ts +0 -29
- package/dist/ui/elements/box.tsx +0 -36
- package/dist/ui/elements/breadcrumb.tsx +0 -38
- package/dist/ui/elements/breadcrumb.works.ts +0 -24
- package/dist/ui/elements/button.tsx +0 -46
- package/dist/ui/elements/button.works.tsx +0 -52
- package/dist/ui/elements/column.tsx +0 -29
- package/dist/ui/elements/consent.tsx +0 -71
- package/dist/ui/elements/consent.works.tsx +0 -132
- package/dist/ui/elements/donut.tsx +0 -45
- package/dist/ui/elements/donut.works.ts +0 -42
- package/dist/ui/elements/each.tsx +0 -34
- package/dist/ui/elements/each.works.tsx +0 -82
- package/dist/ui/elements/empty.tsx +0 -33
- package/dist/ui/elements/field.tsx +0 -45
- package/dist/ui/elements/field.works.ts +0 -65
- package/dist/ui/elements/funnel.tsx +0 -50
- package/dist/ui/elements/funnel.works.ts +0 -37
- package/dist/ui/elements/gauge.tsx +0 -40
- package/dist/ui/elements/gauge.works.ts +0 -42
- package/dist/ui/elements/glyph.tsx +0 -30
- package/dist/ui/elements/glyph.works.tsx +0 -42
- package/dist/ui/elements/grid.tsx +0 -29
- package/dist/ui/elements/heatmap.tsx +0 -48
- package/dist/ui/elements/heatmap.works.ts +0 -24
- package/dist/ui/elements/hit-box.tsx +0 -47
- package/dist/ui/elements/hit-box.works.tsx +0 -576
- package/dist/ui/elements/image.tsx +0 -29
- package/dist/ui/elements/line-chart.tsx +0 -45
- package/dist/ui/elements/line-chart.works.ts +0 -41
- package/dist/ui/elements/link.tsx +0 -38
- package/dist/ui/elements/link.works.ts +0 -25
- package/dist/ui/elements/media.tsx +0 -57
- package/dist/ui/elements/menu.tsx +0 -51
- package/dist/ui/elements/menu.works.tsx +0 -154
- package/dist/ui/elements/money.tsx +0 -36
- package/dist/ui/elements/money.works.ts +0 -9
- package/dist/ui/elements/pagination.tsx +0 -39
- package/dist/ui/elements/pagination.works.ts +0 -20
- package/dist/ui/elements/progress.tsx +0 -34
- package/dist/ui/elements/progress.works.ts +0 -8
- package/dist/ui/elements/radio-group.tsx +0 -50
- package/dist/ui/elements/radio-group.works.ts +0 -33
- package/dist/ui/elements/row.tsx +0 -29
- package/dist/ui/elements/scatter.tsx +0 -42
- package/dist/ui/elements/scatter.works.ts +0 -38
- package/dist/ui/elements/select.tsx +0 -43
- package/dist/ui/elements/select.works.ts +0 -5
- package/dist/ui/elements/separator.tsx +0 -31
- package/dist/ui/elements/skeleton.tsx +0 -31
- package/dist/ui/elements/slider.tsx +0 -34
- package/dist/ui/elements/slider.works.ts +0 -5
- package/dist/ui/elements/sparkline.tsx +0 -37
- package/dist/ui/elements/sparkline.works.ts +0 -32
- package/dist/ui/elements/spinner.tsx +0 -33
- package/dist/ui/elements/stack.tsx +0 -29
- package/dist/ui/elements/stat.tsx +0 -36
- package/dist/ui/elements/stat.works.ts +0 -14
- package/dist/ui/elements/surface.tsx +0 -36
- package/dist/ui/elements/surface.works.ts +0 -9
- package/dist/ui/elements/table.tsx +0 -62
- package/dist/ui/elements/table.works.ts +0 -30
- package/dist/ui/elements/tabs.tsx +0 -47
- package/dist/ui/elements/tabs.works.ts +0 -84
- package/dist/ui/elements/text.tsx +0 -33
- package/dist/ui/elements/textarea.tsx +0 -38
- package/dist/ui/elements/textarea.works.ts +0 -5
- package/dist/ui/elements/time.tsx +0 -40
- package/dist/ui/elements/time.works.ts +0 -29
- package/dist/ui/elements/toggle.tsx +0 -34
- package/dist/ui/elements/toggle.works.ts +0 -5
- package/dist/ui/elements/tooltip.tsx +0 -40
- package/dist/ui/elements/tooltip.works.ts +0 -77
- package/dist/ui/elements/value.tsx +0 -38
- package/dist/ui/elements/value.works.ts +0 -9
- package/dist/ui/elements.ts +0 -127
- package/dist/ui/focus-scope.ts +0 -121
- package/dist/ui/fonts.ts +0 -355
- package/dist/ui/generators/dev.d.ts +0 -1
- package/dist/ui/generators/dev.js +0 -62
- package/dist/ui/generators/dev.ts +0 -75
- package/dist/ui/generators/elements.ts +0 -796
- package/dist/ui/generators/i18n.ts +0 -283
- package/dist/ui/generators/icons.ts +0 -207
- package/dist/ui/generators/react.ts +0 -267
- package/dist/ui/generators/routes.ts +0 -847
- package/dist/ui/generators/show.ts +0 -261
- package/dist/ui/generators/styles.ts +0 -677
- package/dist/ui/generators/variants.ts +0 -196
- package/dist/ui/i18n/catalogs/en.ts +0 -191
- package/dist/ui/i18n/format.ts +0 -221
- package/dist/ui/i18n/locales.ts +0 -33
- package/dist/ui/i18n/parse.ts +0 -315
- package/dist/ui/i18n/translator.ts +0 -31
- package/dist/ui/i18n/types.ts +0 -96
- package/dist/ui/icons.ts +0 -331
- package/dist/ui/islands/map.ts +0 -58
- package/dist/ui/localizations.ts +0 -75
- package/dist/ui/material.ts +0 -236
- package/dist/ui/materials/mat_bordered.ts +0 -13
- package/dist/ui/materials/mat_ceramic.ts +0 -13
- package/dist/ui/materials/mat_clay.ts +0 -13
- package/dist/ui/materials/mat_elevated.ts +0 -24
- package/dist/ui/materials/mat_flat.ts +0 -34
- package/dist/ui/materials/mat_glass.ts +0 -20
- package/dist/ui/materials/mat_lit.ts +0 -13
- package/dist/ui/materials/mat_moulded.ts +0 -11
- package/dist/ui/materials.ts +0 -197
- package/dist/ui/modes.ts +0 -44
- package/dist/ui/navigation.tsx +0 -29
- package/dist/ui/positioning.ts +0 -90
- package/dist/ui/presented.tsx +0 -119
- package/dist/ui/provider.tsx +0 -184
- package/dist/ui/render-loop.ts +0 -133
- package/dist/ui/roving-focus.ts +0 -161
- package/dist/ui/screens.ts +0 -141
- package/dist/ui/scroll-lock.ts +0 -28
- package/dist/ui/series.ts +0 -97
- package/dist/ui/signals.ts +0 -386
- package/dist/ui/styleguide.ts +0 -231
- package/dist/ui/styleguides.ts +0 -374
- package/dist/ui/themes.ts +0 -22
- package/dist/ui/token.ts +0 -78
- package/dist/ui/tokens/shadow.ts +0 -25
- package/dist/ui/tokens.ts +0 -234
- package/dist/ui/validators.ts +0 -232
- package/dist/ui/variant.ts +0 -349
- package/dist/ui/variants/var_avatar-round.ts +0 -14
- package/dist/ui/variants/var_bubble-me.ts +0 -14
- package/dist/ui/variants/var_bubble-them.ts +0 -13
- package/dist/ui/variants/var_button-border.ts +0 -76
- package/dist/ui/variants/var_button-cta.ts +0 -76
- package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
- package/dist/ui/variants/var_button-destructive.ts +0 -76
- package/dist/ui/variants/var_button-fill.ts +0 -76
- package/dist/ui/variants/var_button-ghost.ts +0 -72
- package/dist/ui/variants/var_button-glass.ts +0 -55
- package/dist/ui/variants/var_button-normal.ts +0 -76
- package/dist/ui/variants/var_button-quiet.ts +0 -74
- package/dist/ui/variants/var_card.ts +0 -17
- package/dist/ui/variants/var_chip.ts +0 -13
- package/dist/ui/variants/var_choice.ts +0 -15
- package/dist/ui/variants/var_framed.ts +0 -10
- package/dist/ui/variants/var_glyph-plain.ts +0 -11
- package/dist/ui/variants/var_input.ts +0 -43
- package/dist/ui/variants/var_link-inline.ts +0 -29
- package/dist/ui/variants/var_list-row.ts +0 -34
- package/dist/ui/variants/var_rule.ts +0 -10
- package/dist/ui/variants/var_skeleton-block.ts +0 -12
- package/dist/ui/variants/var_spinner-turning.ts +0 -12
- package/dist/ui/variants/var_stat-tile.ts +0 -13
- package/dist/ui/variants/var_tab-bar.ts +0 -15
- package/dist/ui/variants/var_tap-target.ts +0 -25
- package/dist/ui/variants.ts +0 -64
- 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())
|