@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.
Files changed (209) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/dist/ui/attach.js +6 -7
  3. package/dist/ui/compositions.d.ts +0 -8
  4. package/dist/ui/compositions.js +3 -166
  5. package/dist/ui/generators/routes.js +1 -0
  6. package/package.json +2 -2
  7. package/dist/ui/animations/anm_appear.ts +0 -13
  8. package/dist/ui/animations/anm_blur_in.ts +0 -19
  9. package/dist/ui/animations/anm_blur_out.ts +0 -18
  10. package/dist/ui/animations/anm_dismiss.ts +0 -18
  11. package/dist/ui/animations/anm_drop.ts +0 -19
  12. package/dist/ui/animations/anm_fall.ts +0 -18
  13. package/dist/ui/animations/anm_flash.ts +0 -18
  14. package/dist/ui/animations/anm_glow.ts +0 -16
  15. package/dist/ui/animations/anm_lift.ts +0 -16
  16. package/dist/ui/animations/anm_nudge.ts +0 -16
  17. package/dist/ui/animations/anm_pop_in.ts +0 -23
  18. package/dist/ui/animations/anm_pop_out.ts +0 -22
  19. package/dist/ui/animations/anm_press.ts +0 -12
  20. package/dist/ui/animations/anm_pulse.ts +0 -13
  21. package/dist/ui/animations/anm_rise.ts +0 -19
  22. package/dist/ui/animations/anm_shake.ts +0 -13
  23. package/dist/ui/animations/anm_slide_in_left.ts +0 -19
  24. package/dist/ui/animations/anm_slide_in_right.ts +0 -19
  25. package/dist/ui/animations/anm_slide_out_left.ts +0 -18
  26. package/dist/ui/animations/anm_slide_out_right.ts +0 -18
  27. package/dist/ui/animations/anm_spin.ts +0 -12
  28. package/dist/ui/animations/anm_swell.ts +0 -16
  29. package/dist/ui/animations/anm_wobble.ts +0 -24
  30. package/dist/ui/animations/anm_zoom_in.ts +0 -19
  31. package/dist/ui/animations/anm_zoom_out.ts +0 -18
  32. package/dist/ui/animations.ts +0 -308
  33. package/dist/ui/attach.ts +0 -553
  34. package/dist/ui/charts.tsx +0 -69
  35. package/dist/ui/checked.ts +0 -32
  36. package/dist/ui/collector.ts +0 -140
  37. package/dist/ui/compositions.ts +0 -613
  38. package/dist/ui/consent.ts +0 -55
  39. package/dist/ui/content.tsx +0 -27
  40. package/dist/ui/contrast.ts +0 -27
  41. package/dist/ui/controls.tsx +0 -25
  42. package/dist/ui/disclosure.tsx +0 -20
  43. package/dist/ui/dismiss-layer.ts +0 -87
  44. package/dist/ui/door.ts +0 -140
  45. package/dist/ui/drag.ts +0 -316
  46. package/dist/ui/elements/accordion.tsx +0 -47
  47. package/dist/ui/elements/accordion.works.ts +0 -66
  48. package/dist/ui/elements/aspect-ratio.tsx +0 -31
  49. package/dist/ui/elements/aspect-ratio.works.ts +0 -5
  50. package/dist/ui/elements/avatar.tsx +0 -34
  51. package/dist/ui/elements/avatar.works.ts +0 -16
  52. package/dist/ui/elements/badge.tsx +0 -33
  53. package/dist/ui/elements/badge.works.ts +0 -9
  54. package/dist/ui/elements/bars.tsx +0 -50
  55. package/dist/ui/elements/bars.works.ts +0 -29
  56. package/dist/ui/elements/box.tsx +0 -36
  57. package/dist/ui/elements/breadcrumb.tsx +0 -38
  58. package/dist/ui/elements/breadcrumb.works.ts +0 -24
  59. package/dist/ui/elements/button.tsx +0 -46
  60. package/dist/ui/elements/button.works.tsx +0 -52
  61. package/dist/ui/elements/column.tsx +0 -29
  62. package/dist/ui/elements/consent.tsx +0 -71
  63. package/dist/ui/elements/consent.works.tsx +0 -132
  64. package/dist/ui/elements/donut.tsx +0 -45
  65. package/dist/ui/elements/donut.works.ts +0 -42
  66. package/dist/ui/elements/each.tsx +0 -34
  67. package/dist/ui/elements/each.works.tsx +0 -82
  68. package/dist/ui/elements/empty.tsx +0 -33
  69. package/dist/ui/elements/field.tsx +0 -45
  70. package/dist/ui/elements/field.works.ts +0 -65
  71. package/dist/ui/elements/funnel.tsx +0 -50
  72. package/dist/ui/elements/funnel.works.ts +0 -37
  73. package/dist/ui/elements/gauge.tsx +0 -40
  74. package/dist/ui/elements/gauge.works.ts +0 -42
  75. package/dist/ui/elements/glyph.tsx +0 -30
  76. package/dist/ui/elements/glyph.works.tsx +0 -42
  77. package/dist/ui/elements/grid.tsx +0 -29
  78. package/dist/ui/elements/heatmap.tsx +0 -48
  79. package/dist/ui/elements/heatmap.works.ts +0 -24
  80. package/dist/ui/elements/hit-box.tsx +0 -47
  81. package/dist/ui/elements/hit-box.works.tsx +0 -576
  82. package/dist/ui/elements/image.tsx +0 -29
  83. package/dist/ui/elements/line-chart.tsx +0 -45
  84. package/dist/ui/elements/line-chart.works.ts +0 -41
  85. package/dist/ui/elements/link.tsx +0 -38
  86. package/dist/ui/elements/link.works.ts +0 -25
  87. package/dist/ui/elements/media.tsx +0 -57
  88. package/dist/ui/elements/menu.tsx +0 -51
  89. package/dist/ui/elements/menu.works.tsx +0 -154
  90. package/dist/ui/elements/money.tsx +0 -36
  91. package/dist/ui/elements/money.works.ts +0 -9
  92. package/dist/ui/elements/pagination.tsx +0 -39
  93. package/dist/ui/elements/pagination.works.ts +0 -20
  94. package/dist/ui/elements/progress.tsx +0 -34
  95. package/dist/ui/elements/progress.works.ts +0 -8
  96. package/dist/ui/elements/radio-group.tsx +0 -50
  97. package/dist/ui/elements/radio-group.works.ts +0 -33
  98. package/dist/ui/elements/row.tsx +0 -29
  99. package/dist/ui/elements/scatter.tsx +0 -42
  100. package/dist/ui/elements/scatter.works.ts +0 -38
  101. package/dist/ui/elements/select.tsx +0 -43
  102. package/dist/ui/elements/select.works.ts +0 -5
  103. package/dist/ui/elements/separator.tsx +0 -31
  104. package/dist/ui/elements/skeleton.tsx +0 -31
  105. package/dist/ui/elements/slider.tsx +0 -34
  106. package/dist/ui/elements/slider.works.ts +0 -5
  107. package/dist/ui/elements/sparkline.tsx +0 -37
  108. package/dist/ui/elements/sparkline.works.ts +0 -32
  109. package/dist/ui/elements/spinner.tsx +0 -33
  110. package/dist/ui/elements/stack.tsx +0 -29
  111. package/dist/ui/elements/stat.tsx +0 -36
  112. package/dist/ui/elements/stat.works.ts +0 -14
  113. package/dist/ui/elements/surface.tsx +0 -36
  114. package/dist/ui/elements/surface.works.ts +0 -9
  115. package/dist/ui/elements/table.tsx +0 -62
  116. package/dist/ui/elements/table.works.ts +0 -30
  117. package/dist/ui/elements/tabs.tsx +0 -47
  118. package/dist/ui/elements/tabs.works.ts +0 -84
  119. package/dist/ui/elements/text.tsx +0 -33
  120. package/dist/ui/elements/textarea.tsx +0 -38
  121. package/dist/ui/elements/textarea.works.ts +0 -5
  122. package/dist/ui/elements/time.tsx +0 -40
  123. package/dist/ui/elements/time.works.ts +0 -29
  124. package/dist/ui/elements/toggle.tsx +0 -34
  125. package/dist/ui/elements/toggle.works.ts +0 -5
  126. package/dist/ui/elements/tooltip.tsx +0 -40
  127. package/dist/ui/elements/tooltip.works.ts +0 -77
  128. package/dist/ui/elements/value.tsx +0 -38
  129. package/dist/ui/elements/value.works.ts +0 -9
  130. package/dist/ui/elements.ts +0 -127
  131. package/dist/ui/focus-scope.ts +0 -121
  132. package/dist/ui/fonts.ts +0 -355
  133. package/dist/ui/generators/dev.d.ts +0 -1
  134. package/dist/ui/generators/dev.js +0 -62
  135. package/dist/ui/generators/dev.ts +0 -75
  136. package/dist/ui/generators/elements.ts +0 -796
  137. package/dist/ui/generators/i18n.ts +0 -283
  138. package/dist/ui/generators/icons.ts +0 -207
  139. package/dist/ui/generators/react.ts +0 -267
  140. package/dist/ui/generators/routes.ts +0 -847
  141. package/dist/ui/generators/show.ts +0 -261
  142. package/dist/ui/generators/styles.ts +0 -677
  143. package/dist/ui/generators/variants.ts +0 -196
  144. package/dist/ui/i18n/catalogs/en.ts +0 -191
  145. package/dist/ui/i18n/format.ts +0 -221
  146. package/dist/ui/i18n/locales.ts +0 -33
  147. package/dist/ui/i18n/parse.ts +0 -315
  148. package/dist/ui/i18n/translator.ts +0 -31
  149. package/dist/ui/i18n/types.ts +0 -96
  150. package/dist/ui/icons.ts +0 -331
  151. package/dist/ui/islands/map.ts +0 -58
  152. package/dist/ui/localizations.ts +0 -75
  153. package/dist/ui/material.ts +0 -236
  154. package/dist/ui/materials/mat_bordered.ts +0 -13
  155. package/dist/ui/materials/mat_ceramic.ts +0 -13
  156. package/dist/ui/materials/mat_clay.ts +0 -13
  157. package/dist/ui/materials/mat_elevated.ts +0 -24
  158. package/dist/ui/materials/mat_flat.ts +0 -34
  159. package/dist/ui/materials/mat_glass.ts +0 -20
  160. package/dist/ui/materials/mat_lit.ts +0 -13
  161. package/dist/ui/materials/mat_moulded.ts +0 -11
  162. package/dist/ui/materials.ts +0 -197
  163. package/dist/ui/modes.ts +0 -44
  164. package/dist/ui/navigation.tsx +0 -29
  165. package/dist/ui/positioning.ts +0 -90
  166. package/dist/ui/presented.tsx +0 -119
  167. package/dist/ui/provider.tsx +0 -184
  168. package/dist/ui/render-loop.ts +0 -133
  169. package/dist/ui/roving-focus.ts +0 -161
  170. package/dist/ui/screens.ts +0 -141
  171. package/dist/ui/scroll-lock.ts +0 -28
  172. package/dist/ui/series.ts +0 -97
  173. package/dist/ui/signals.ts +0 -386
  174. package/dist/ui/styleguide.ts +0 -231
  175. package/dist/ui/styleguides.ts +0 -374
  176. package/dist/ui/themes.ts +0 -22
  177. package/dist/ui/token.ts +0 -78
  178. package/dist/ui/tokens/shadow.ts +0 -25
  179. package/dist/ui/tokens.ts +0 -234
  180. package/dist/ui/validators.ts +0 -232
  181. package/dist/ui/variant.ts +0 -349
  182. package/dist/ui/variants/var_avatar-round.ts +0 -14
  183. package/dist/ui/variants/var_bubble-me.ts +0 -14
  184. package/dist/ui/variants/var_bubble-them.ts +0 -13
  185. package/dist/ui/variants/var_button-border.ts +0 -76
  186. package/dist/ui/variants/var_button-cta.ts +0 -76
  187. package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
  188. package/dist/ui/variants/var_button-destructive.ts +0 -76
  189. package/dist/ui/variants/var_button-fill.ts +0 -76
  190. package/dist/ui/variants/var_button-ghost.ts +0 -72
  191. package/dist/ui/variants/var_button-glass.ts +0 -55
  192. package/dist/ui/variants/var_button-normal.ts +0 -76
  193. package/dist/ui/variants/var_button-quiet.ts +0 -74
  194. package/dist/ui/variants/var_card.ts +0 -17
  195. package/dist/ui/variants/var_chip.ts +0 -13
  196. package/dist/ui/variants/var_choice.ts +0 -15
  197. package/dist/ui/variants/var_framed.ts +0 -10
  198. package/dist/ui/variants/var_glyph-plain.ts +0 -11
  199. package/dist/ui/variants/var_input.ts +0 -43
  200. package/dist/ui/variants/var_link-inline.ts +0 -29
  201. package/dist/ui/variants/var_list-row.ts +0 -34
  202. package/dist/ui/variants/var_rule.ts +0 -10
  203. package/dist/ui/variants/var_skeleton-block.ts +0 -12
  204. package/dist/ui/variants/var_spinner-turning.ts +0 -12
  205. package/dist/ui/variants/var_stat-tile.ts +0 -13
  206. package/dist/ui/variants/var_tab-bar.ts +0 -15
  207. package/dist/ui/variants/var_tap-target.ts +0 -25
  208. package/dist/ui/variants.ts +0 -64
  209. package/dist/ui/wrappers.tsx +0 -429
@@ -1,283 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, writeFileSync } from '@prestack/thread/integrations/disk'
2
- import { join, relative } from 'node:path'
3
- import { argv, stderr, stdout } from 'node:process'
4
- import { pathToFileURL } from 'node:url'
5
-
6
- import { exit, invoked } from '@prestack/thread/core/invoked'
7
- import { named as packageOf } from '@prestack/thread/core/scope'
8
- import { findTree, inTree } from '@prestack/thread/core/tree'
9
-
10
- import { fileFor, languagesIn, sourceIn } from '../localizations.ts'
11
-
12
- const GENERATED = `app/_generated/wording.${'ts'}`
13
-
14
- const SKIP = new Set(['node_modules', '.next', '.git', 'dist', '_generated'])
15
-
16
- const CODE = /\.(tsx?|jsx?)$/
17
-
18
- export function walk(at: string, found: string[] = [], skip: ReadonlySet<string> = new Set()): readonly string[] {
19
- for (const entry of readdirSync(at, { withFileTypes: true })) {
20
- if (SKIP.has(entry.name)) continue
21
- const path = join(at, entry.name)
22
- if (skip.has(path)) continue
23
- if (entry.isDirectory()) walk(path, found, skip)
24
- else if (CODE.test(entry.name)) found.push(path)
25
- }
26
- return found
27
- }
28
-
29
- function leftOut(at: string): ReadonlySet<string> {
30
- try {
31
- const held = JSON.parse(readFileSync(join(at, 'tsconfig.json'), 'utf8')) as { exclude?: readonly string[] }
32
- return new Set((held.exclude ?? []).map((one) => join(at, one)))
33
- } catch {
34
-
35
- return new Set()
36
- }
37
- }
38
-
39
- const TYPED = /readonly\s+([A-Za-z][A-Za-z0-9]*)\??\s*:\s*MessageKey\b/g
40
-
41
- export function keyProps(at: string): readonly string[] {
42
- const found = new Set<string>()
43
- const home = join(at, 'node_modules', packageOf('ui'))
44
- if (!existsSync(home)) return []
45
- for (const file of walk(home)) {
46
- for (const one of readFileSync(file, 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
47
- }
48
- for (const file of walk(join(home, 'dist'))) {
49
- if (!file.endsWith('.d.ts')) continue
50
- for (const one of readFileSync(file, 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
51
- }
52
- return [...found].sort()
53
- }
54
-
55
- const named = (props: readonly string[]): RegExp =>
56
- new RegExp(`\\b(?:${props.join('|')})\\s*=\\s*(?:"([^"]+)"|'([^']+)')`, 'g')
57
-
58
- export function keysIn(text: string, props: readonly string[]): readonly string[] {
59
- if (props.length === 0) return []
60
- return [...text.matchAll(named(props))].map((one) => one[1] ?? one[2] ?? '')
61
- }
62
-
63
- async function shipped(at: string): Promise<Readonly<Record<string, string>>> {
64
- const held = (await import('../i18n/catalogs/en.ts')) as { en?: Readonly<Record<string, unknown>> }
65
- const made: Record<string, string> = {}
66
- for (const [key, entry] of Object.entries(held.en ?? {})) {
67
- made[key] = typeof entry === 'string' ? entry : String((entry as { text?: string }).text ?? '')
68
- }
69
- void at
70
- return made
71
- }
72
-
73
- export function readWording(at: string, language: string): Readonly<Record<string, string>> {
74
- const path = join(at, fileFor(language))
75
- if (!existsSync(path)) return {}
76
- try {
77
- const held = JSON.parse(readFileSync(path, 'utf8')) as Readonly<Record<string, unknown>>
78
- const made: Record<string, string> = {}
79
- for (const [key, value] of Object.entries(held)) made[key] = typeof value === 'string' ? value : ''
80
- return made
81
- } catch (wrong) {
82
- throw new Error(
83
- `${fileFor(language)} is there and will not parse, so what this app says in ${language} cannot ` +
84
- `be known. Nothing was written — a reader that cannot read is not a reader that found nothing.\n ${String(wrong)}`,
85
- )
86
- }
87
- }
88
-
89
- const asJson = (value: string): string => JSON.stringify(value)
90
-
91
- export function wordingFile(held: Readonly<Record<string, string>>): string {
92
- const keys = Object.keys(held).sort()
93
- return `{\n${keys.map((key) => ` ${asJson(key)}: ${asJson(held[key] ?? '')}`).join(',\n')}\n}\n`
94
- }
95
-
96
- function moduleFor(
97
- languages: readonly { readonly id: string; readonly name: string; readonly flag: string }[],
98
- source: string,
99
- used: readonly string[],
100
- wording: ReadonlyMap<string, Readonly<Record<string, string>>>,
101
- ): string {
102
- const catalogue = (language: string): string => {
103
- const held = wording.get(language) ?? {}
104
- const keys = Object.keys(held).sort()
105
- return [
106
- ` ${asJson(language)}: {`,
107
- ...keys.map((key) => ` ${asJson(key)}: ${asJson(held[key] ?? '')},`),
108
- ' },',
109
- ].join('\n')
110
- }
111
-
112
- return [
113
- 'export type Language =',
114
- ...languages.map((one) => ` | ${asJson(one.id)}`),
115
- '',
116
- `export const SOURCE: Language = ${asJson(source)}`,
117
- '',
118
- 'export const LANGUAGES: readonly { readonly id: Language; readonly name: string; readonly flag: string }[] = [',
119
- ...languages.map((one) => ` { id: ${asJson(one.id)}, name: ${asJson(one.name)}, flag: ${asJson(one.flag)} },`),
120
- ']',
121
- '',
122
- 'export type Used =',
123
- ...(used.length === 0 ? [' | never'] : used.map((one) => ` | ${asJson(one)}`)),
124
- '',
125
- 'const held = {',
126
- ...languages.map((one) => catalogue(one.id)),
127
- '} as const',
128
- '',
129
- "type Filled<S> = S extends '' ? never : S",
130
- '',
131
- 'type Answered<L extends Language> = {',
132
- ' readonly [K in Used]: K extends keyof (typeof held)[L] ? Filled<(typeof held)[L][K]> : never',
133
- '}',
134
- '',
135
- 'export const WORDING: { readonly [L in Language]: Answered<L> } = held',
136
- '',
137
- ].join('\n')
138
- }
139
-
140
- interface Node {
141
- readonly holds?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
142
- readonly declares?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
143
- }
144
-
145
- async function rows(at: string): Promise<NonNullable<Node['holds']>> {
146
- const path = inTree(findTree(), `localizations.${'ts'}`)
147
- if (!existsSync(path)) {
148
- stdout.write(`i18n: this tree has no ${relative(at, path)}, so it is in no language and there is no wording to write or check.\n`)
149
- exit(0)
150
- }
151
- const held = (await import(pathToFileURL(path).href)) as { LOCALIZATION?: Node }
152
- if (held.LOCALIZATION === undefined) {
153
- throw new Error(
154
- `${relative(at, path)} exports no \`LOCALIZATION\`, so nothing says what languages this app is ` +
155
- `in. Run \`thread attach ${packageOf('ui')}\` — the sheet is the package's to write, not yours.`,
156
- )
157
- }
158
- return [...(held.LOCALIZATION.holds ?? []), ...(held.LOCALIZATION.declares ?? [])]
159
- }
160
-
161
- async function run(): Promise<number> {
162
- const tree = findTree()
163
- const at = tree.at
164
- const held = (await rows(at)) ?? []
165
-
166
- const languages = languagesIn(held)
167
- const source = sourceIn(held)
168
-
169
- if (languages.length === 0) {
170
- stderr.write('i18n: no language is declared, so this app is in no language at all. Add one with `thread create language`.\n')
171
- return 1
172
- }
173
- if (source === undefined || !languages.some((one) => one.id === source)) {
174
- stderr.write(
175
- `i18n: the source row names \`${String(source)}\`, and that is not one of the declared languages ` +
176
- `(${languages.map((one) => one.id).join(', ')}). An app is written in one of the languages it is in.\n`,
177
- )
178
- return 1
179
- }
180
-
181
- const props = keyProps(at)
182
- if (props.length === 0) {
183
- stderr.write(
184
- `i18n: nothing in the installed \`${packageOf('ui')}\` declares a prop of type \`MessageKey\`, so no key could ` +
185
- 'be recognised in any file. That is a broken install, not an app with no wording.\n',
186
- )
187
- return 1
188
- }
189
-
190
- const used = new Set<string>()
191
- const mine = leftOut(at)
192
- for (const file of walk(at, [], mine)) for (const key of keysIn(readFileSync(file, 'utf8'), props)) used.add(key)
193
-
194
- const seeded = await shipped(at)
195
-
196
- const answered = new Set<string>()
197
- for (const language of languages) {
198
- for (const key of Object.keys(readWording(at, language.id))) answered.add(key)
199
- }
200
-
201
- const everything = [...new Set([...used, ...Object.keys(seeded), ...answered])].sort()
202
-
203
- const wording = new Map<string, Readonly<Record<string, string>>>()
204
- for (const language of languages) {
205
- const already = readWording(at, language.id)
206
- const made: Record<string, string> = {}
207
- for (const key of everything) made[key] = already[key] ?? (language.id === source ? (seeded[key] ?? '') : '')
208
- wording.set(language.id, made)
209
- }
210
-
211
- const files = [
212
- ...languages.map((one) => ({ path: fileFor(one.id), made: wordingFile(wording.get(one.id) ?? {}) })),
213
- { path: GENERATED, made: moduleFor(languages, source, [...used].sort(), wording) },
214
- ]
215
-
216
- const named = new Set(languages.map((one) => fileFor(one.id)))
217
- const stray: string[] = []
218
- try {
219
- for (const entry of readdirSync(join(at, 'i18n'), { withFileTypes: true })) {
220
- const path = `i18n/${entry.name}`
221
- if (entry.isDirectory() || named.has(path)) continue
222
- stray.push(path)
223
- }
224
- } catch { }
225
-
226
- const blank = languages.flatMap((one) =>
227
- everything.filter((key) => (wording.get(one.id) ?? {})[key] === '').map((key) => `${one.id}: ${key}`),
228
- )
229
-
230
- if (argv.includes('--check')) {
231
- const drifted = files.find((one) => {
232
- let current = ''
233
- try {
234
- current = readFileSync(join(at, one.path), 'utf8')
235
- } catch {
236
- current = ''
237
- }
238
- return current !== one.made
239
- })
240
- if (drifted !== undefined) {
241
- stderr.write(
242
- `i18n: ${drifted.path} is not what the declarations produce. Run \`thread i18n\` and fill in what it adds.\n`,
243
- )
244
- return 1
245
- }
246
- if (stray.length > 0) {
247
- stderr.write(
248
- `i18n: ${stray.join(', ')} — no declared language accounts for ${stray.length === 1 ? 'it' : 'them'}, ` +
249
- 'so it is not wording, it is a file somebody left there. Declare the language with ' +
250
- '`thread create language`, or delete the file.\n',
251
- )
252
- return 1
253
- }
254
- if (blank.length > 0) {
255
- stderr.write(
256
- `i18n: ${String(blank.length)} key(s) have no wording, and an untranslated key is an error rather ` +
257
- `than a word that comes out in the wrong language:\n${blank.map((one) => ` ${one}`).join('\n')}\n`,
258
- )
259
- return 1
260
- }
261
- stdout.write(`i18n: ${String(everything.length)} key(s) answered in ${String(languages.length)} language(s)\n`)
262
- return 0
263
- }
264
-
265
- for (const one of files) {
266
- writeFileSync(join(at, one.path), one.made, 'utf8')
267
- }
268
-
269
- stdout.write(
270
- `i18n: ${String(everything.length)} key(s) across ${String(languages.length)} language(s) — ` +
271
- `${String(used.size)} named by this interface, ${String(everything.length - used.size)} shipped with the framework\n`,
272
- )
273
- if (blank.length > 0) {
274
- stdout.write(
275
- `i18n: ${String(blank.length)} still to write — the gate refuses until they are filled:\n` +
276
- `${blank.slice(0, 20).map((one) => ` ${one}`).join('\n')}\n` +
277
- (blank.length > 20 ? ` … and ${String(blank.length - 20)} more\n` : ''),
278
- )
279
- }
280
- return 0
281
- }
282
-
283
- if (invoked(import.meta.url)) exit(await run())
@@ -1,207 +0,0 @@
1
- import { createRequire } from 'node:module'
2
-
3
- import { existsSync, readdirSync, readFileSync, writeFileSync } from '@prestack/thread/integrations/disk'
4
- import { valuesIn } from '@prestack/thread/core/rows'
5
- import { dirname, join, relative } from 'node:path'
6
- import { argv, stderr, stdout } from 'node:process'
7
-
8
- import { exit } from '@prestack/thread/core/invoked'
9
- import { findTree, inTree } from '@prestack/thread/core/tree'
10
- import { COMPILED } from '@prestack/thread/operations/capabilities'
11
-
12
- import { brandExport, drawnAs, setNamed, sizeRows, SET_ROW, type IconSet } from '../icons.ts'
13
-
14
- interface Row {
15
- readonly id: string
16
- readonly is?: string
17
- readonly sets?: Readonly<Record<string, string>>
18
- }
19
-
20
- interface Node {
21
- readonly holds?: readonly Row[]
22
- readonly declares?: readonly Row[]
23
- }
24
-
25
- function inside(node: Node): readonly Row[] {
26
- return [...(node.holds ?? []), ...(node.declares ?? [])]
27
- }
28
-
29
- const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
30
-
31
- const SYMBOLS = 'app/_generated/icons.ts'
32
-
33
- function module(
34
- set: IconSet,
35
- drawn: readonly { readonly id: string; readonly as: string }[],
36
- sizes: readonly { readonly id: string; readonly is: string }[],
37
- ): string {
38
- const names = [...new Set(drawn.map((one) => one.as))].sort(compare)
39
-
40
- return [
41
-
42
- "'use client'",
43
- '',
44
- `import { ${names.join(', ')} } from '${set.module}'`,
45
- `import { installIcons } from './${relative(dirname(SYMBOLS), `${COMPILED}/ui/icons.ts`)}'`,
46
- '',
47
- 'installIcons(',
48
- ` { name: '${set.name}', module: '${set.module}', size: ${String(set.size)}, viewBox: ${String(set.viewBox)}` +
49
- `${set.nativeStroke === undefined ? '' : `, nativeStroke: ${String(set.nativeStroke)}`} },`,
50
- ' {',
51
- ...drawn.map((one) => ` '${one.id}': ${one.as},`),
52
- ' },',
53
- ' {',
54
- ...sizes.map((one) => ` '${one.id}': '${one.is}',`),
55
- ' },',
56
- ')',
57
- '',
58
- 'export function ThreadIcons(): null {',
59
- ' return null',
60
- '}',
61
- '',
62
- ].join('\n')
63
- }
64
-
65
- function exported(root: string, module: string): ReadonlySet<string> | undefined {
66
- let entry: string
67
- try {
68
- entry = createRequire(join(root, 'package.json')).resolve(module)
69
- } catch {
70
- return undefined
71
- }
72
- const suffix = `.d.${'ts'}`
73
- const found = new Set<string>()
74
- const seen: string[] = []
75
- let dir = dirname(entry)
76
- for (let up = 0; up < 5; up += 1) {
77
- let names: readonly string[] = []
78
- try {
79
- names = readdirSync(dir).filter((each) => each.endsWith(suffix))
80
- } catch {
81
- names = []
82
- }
83
- for (const one of names) seen.push(join(dir, one))
84
- if (existsSync(join(dir, 'package.json')) && seen.length > 0) break
85
- const next = dirname(dir)
86
- if (next === dir) break
87
- dir = next
88
- }
89
- for (const one of seen) {
90
- let text = ''
91
- try {
92
- text = readFileSync(one, 'utf8')
93
- } catch {
94
- continue
95
- }
96
- for (const each of text.matchAll(/declare const (\w+)/g)) found.add(each[1] ?? '')
97
- for (const each of text.matchAll(/(\w+) as (\w+)/g)) found.add(each[2] ?? '')
98
- for (const each of text.matchAll(/export declare const (\w+)/g)) found.add(each[1] ?? '')
99
- }
100
- return found.size === 0 ? undefined : found
101
- }
102
-
103
- async function node(file: string, name: string): Promise<Node> {
104
- const at = inTree(findTree(), file)
105
- if (!existsSync(at)) {
106
- stdout.write(`icons: this tree has no ${findTree().name}/${file}, so it declares no icon and there is no module to write or check.\n`)
107
- exit(0)
108
- }
109
- const held = valuesIn(readFileSync(at)) as Record<string, unknown>
110
- const one = held[name]
111
- if (one === undefined) throw new Error(`_threads/${file} exports no \`${name}\`.`)
112
- return one as Node
113
- }
114
-
115
- async function run(): Promise<number> {
116
- const tree = findTree()
117
- const rows = inside(await node('icons.ts', 'ICON'))
118
-
119
- const picked = rows.find((row) => row.id === SET_ROW)
120
- if (picked?.is === undefined) {
121
- stderr.write(
122
- `thread: the icon sheet declares no \`${SET_ROW}\` row, so nothing says which library the app ` +
123
- 'draws from. One row, naming a registered library.\n',
124
- )
125
- return 1
126
- }
127
- const set = setNamed(picked.is)
128
-
129
- try {
130
- createRequire(join(tree.at, 'package.json')).resolve(set.module)
131
- } catch {
132
- stderr.write(
133
- `thread: the icon rows draw from \`${set.name}\`, which is the package \`${set.module}\`, and this ` +
134
- `app does not have it installed. Add it, or name a library the app already has.\n`,
135
- )
136
- return 1
137
- }
138
-
139
- const sizes = [...sizeRows(rows)].sort((one, two) => compare(one.id, two.id))
140
- const held = new Set(sizes.map((one) => one.id))
141
-
142
- if (sizes.length === 0) {
143
- stderr.write(
144
- 'thread: the icon sheet declares no size row, so nothing says how large a symbol is drawn. ' +
145
- 'One row per size, holding a length.\n',
146
- )
147
- return 1
148
- }
149
-
150
- const drawn = rows
151
- .filter((row) => row.id !== SET_ROW && !held.has(row.id))
152
- .map((row) => ({ id: row.id, as: row.sets?.[set.name] ?? drawnAs(set.name, row.id, row.id) }))
153
- .sort((one, two) => compare(one.id, two.id))
154
-
155
- const missing = drawn.find((one) => one.as === '')
156
- if (missing !== undefined) {
157
- stderr.write(`thread: \`${missing.id}\` names no symbol in \`${set.name}\`.\n`)
158
- return 1
159
- }
160
-
161
- const known = exported(tree.at, set.module)
162
- if (known === undefined) {
163
- stdout.write(`thread: the declarations of \`${set.module}\` could not be read from this app, so the names the icon rows give it are unverified.\n`)
164
- } else {
165
- const unknown = drawn.find((one) => !one.as.startsWith('si') && !known.has(one.as))
166
- if (unknown !== undefined) {
167
- stderr.write(
168
- `thread: \`${unknown.id}\` is drawn as \`${unknown.as}\` in \`${set.name}\`, and that library does not export it — its ` +
169
- `declarations list ${String(known.size)} names and none of them is that one. Check the library's own naming; the module ` +
170
- 'would fail to build with it.\n',
171
- )
172
- return 1
173
- }
174
- }
175
-
176
- const brand = drawn.filter((one) => one.as.startsWith('si')).map((one) => brandExport(one.id))
177
- if (brand.length > 0) stdout.write(`thread: ${String(brand.length)} brand icon(s) drawn from their own set\n`)
178
-
179
- const at = join(tree.at, SYMBOLS)
180
- const made = module(set, drawn, sizes)
181
-
182
- if (argv.includes('--check')) {
183
- let current = ''
184
- try {
185
- current = readFileSync(at, 'utf8')
186
- } catch {
187
- current = ''
188
- }
189
- if (current === made) {
190
- stdout.write('icons: app/_generated/icons.ts is what the rows produce\n')
191
- return 0
192
- }
193
- stderr.write(
194
- 'icons: app/_generated/icons.ts is not what the rows produce. A derived file that differs from ' +
195
- 'its generator is a hand edit wearing a generated name. Run `thread icons` and commit what it writes.\n',
196
- )
197
- return 1
198
- }
199
-
200
- writeFileSync(at, made, 'utf8')
201
- stdout.write(
202
- `thread: wrote app/_generated/icons.ts — ${String(drawn.length)} icons drawn from \`${set.name}\`\n`,
203
- )
204
- return 0
205
- }
206
-
207
- if (argv[1] !== undefined) exit(await run())