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