@prestack/thread-ui 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +366 -0
- package/dist/ui/attach.js +6 -7
- package/dist/ui/compositions.d.ts +0 -2
- package/dist/ui/compositions.js +2 -112
- package/dist/ui/generators/routes.js +1 -0
- package/dist/ui/variant.d.ts +1 -3
- 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 -555
- 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 -61
- package/dist/ui/generators/dev.ts +0 -74
- 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 -352
- 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,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())
|