@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.
Files changed (279) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/ui/animations/anm_appear.ts +13 -0
  3. package/dist/ui/animations/anm_blur_in.ts +19 -0
  4. package/dist/ui/animations/anm_blur_out.ts +18 -0
  5. package/dist/ui/animations/anm_dismiss.ts +18 -0
  6. package/dist/ui/animations/anm_drop.ts +19 -0
  7. package/dist/ui/animations/anm_fall.ts +18 -0
  8. package/dist/ui/animations/anm_flash.ts +18 -0
  9. package/dist/ui/animations/anm_glow.ts +16 -0
  10. package/dist/ui/animations/anm_lift.ts +16 -0
  11. package/dist/ui/animations/anm_nudge.ts +16 -0
  12. package/dist/ui/animations/anm_pop_in.ts +23 -0
  13. package/dist/ui/animations/anm_pop_out.ts +22 -0
  14. package/dist/ui/animations/anm_press.ts +12 -0
  15. package/dist/ui/animations/anm_pulse.ts +13 -0
  16. package/dist/ui/animations/anm_rise.ts +19 -0
  17. package/dist/ui/animations/anm_shake.ts +13 -0
  18. package/dist/ui/animations/anm_slide_in_left.ts +19 -0
  19. package/dist/ui/animations/anm_slide_in_right.ts +19 -0
  20. package/dist/ui/animations/anm_slide_out_left.ts +18 -0
  21. package/dist/ui/animations/anm_slide_out_right.ts +18 -0
  22. package/dist/ui/animations/anm_spin.ts +12 -0
  23. package/dist/ui/animations/anm_swell.ts +16 -0
  24. package/dist/ui/animations/anm_wobble.ts +24 -0
  25. package/dist/ui/animations/anm_zoom_in.ts +19 -0
  26. package/dist/ui/animations/anm_zoom_out.ts +18 -0
  27. package/dist/ui/animations.js +2 -3
  28. package/dist/ui/animations.ts +316 -0
  29. package/dist/ui/attach.js +13 -10
  30. package/dist/ui/attach.ts +607 -0
  31. package/dist/ui/charts.tsx +74 -0
  32. package/dist/ui/checked.d.ts +2 -2
  33. package/dist/ui/checked.ts +32 -0
  34. package/dist/ui/collector.ts +140 -0
  35. package/dist/ui/compositions.d.ts +11 -2
  36. package/dist/ui/compositions.js +283 -59
  37. package/dist/ui/compositions.ts +633 -0
  38. package/dist/ui/consent.ts +55 -0
  39. package/dist/ui/content.tsx +39 -0
  40. package/dist/ui/contrast.ts +27 -0
  41. package/dist/ui/controls.tsx +30 -0
  42. package/dist/ui/disclosure.tsx +22 -0
  43. package/dist/ui/dismiss-layer.ts +87 -0
  44. package/dist/ui/door.ts +140 -0
  45. package/dist/ui/drag.ts +316 -0
  46. package/dist/ui/elements/accordion.js +2 -2
  47. package/dist/ui/elements/accordion.tsx +47 -0
  48. package/dist/ui/elements/accordion.works.ts +66 -0
  49. package/dist/ui/elements/aspect-ratio.js +1 -1
  50. package/dist/ui/elements/aspect-ratio.tsx +30 -0
  51. package/dist/ui/elements/aspect-ratio.works.ts +5 -0
  52. package/dist/ui/elements/avatar.js +1 -1
  53. package/dist/ui/elements/avatar.tsx +33 -0
  54. package/dist/ui/elements/avatar.works.ts +16 -0
  55. package/dist/ui/elements/badge.js +1 -1
  56. package/dist/ui/elements/badge.tsx +32 -0
  57. package/dist/ui/elements/badge.works.ts +9 -0
  58. package/dist/ui/elements/bars.js +2 -2
  59. package/dist/ui/elements/bars.tsx +49 -0
  60. package/dist/ui/elements/bars.works.ts +29 -0
  61. package/dist/ui/elements/box.js +3 -2
  62. package/dist/ui/elements/box.tsx +35 -0
  63. package/dist/ui/elements/breadcrumb.js +2 -2
  64. package/dist/ui/elements/breadcrumb.tsx +37 -0
  65. package/dist/ui/elements/breadcrumb.works.ts +24 -0
  66. package/dist/ui/elements/button.js +1 -1
  67. package/dist/ui/elements/button.tsx +45 -0
  68. package/dist/ui/elements/button.works.tsx +52 -0
  69. package/dist/ui/elements/column.js +1 -1
  70. package/dist/ui/elements/column.tsx +28 -0
  71. package/dist/ui/elements/consent.js +2 -2
  72. package/dist/ui/elements/consent.tsx +70 -0
  73. package/dist/ui/elements/consent.works.tsx +132 -0
  74. package/dist/ui/elements/donut.js +2 -2
  75. package/dist/ui/elements/donut.tsx +44 -0
  76. package/dist/ui/elements/donut.works.ts +42 -0
  77. package/dist/ui/elements/each.js +1 -1
  78. package/dist/ui/elements/each.tsx +33 -0
  79. package/dist/ui/elements/each.works.tsx +82 -0
  80. package/dist/ui/elements/empty.js +1 -1
  81. package/dist/ui/elements/empty.tsx +32 -0
  82. package/dist/ui/elements/field.js +1 -1
  83. package/dist/ui/elements/field.tsx +44 -0
  84. package/dist/ui/elements/field.works.ts +65 -0
  85. package/dist/ui/elements/funnel.js +2 -2
  86. package/dist/ui/elements/funnel.tsx +49 -0
  87. package/dist/ui/elements/funnel.works.ts +37 -0
  88. package/dist/ui/elements/gauge.js +1 -1
  89. package/dist/ui/elements/gauge.tsx +39 -0
  90. package/dist/ui/elements/gauge.works.ts +42 -0
  91. package/dist/ui/elements/glyph.js +1 -1
  92. package/dist/ui/elements/glyph.tsx +29 -0
  93. package/dist/ui/elements/glyph.works.tsx +42 -0
  94. package/dist/ui/elements/grid.js +1 -1
  95. package/dist/ui/elements/grid.tsx +28 -0
  96. package/dist/ui/elements/heatmap.js +2 -2
  97. package/dist/ui/elements/heatmap.tsx +47 -0
  98. package/dist/ui/elements/heatmap.works.ts +24 -0
  99. package/dist/ui/elements/hit-box.js +1 -1
  100. package/dist/ui/elements/hit-box.tsx +46 -0
  101. package/dist/ui/elements/hit-box.works.tsx +576 -0
  102. package/dist/ui/elements/image.js +1 -1
  103. package/dist/ui/elements/image.tsx +28 -0
  104. package/dist/ui/elements/line-chart.js +2 -2
  105. package/dist/ui/elements/line-chart.tsx +44 -0
  106. package/dist/ui/elements/line-chart.works.ts +41 -0
  107. package/dist/ui/elements/link.js +1 -1
  108. package/dist/ui/elements/link.tsx +37 -0
  109. package/dist/ui/elements/link.works.ts +25 -0
  110. package/dist/ui/elements/media.js +1 -1
  111. package/dist/ui/elements/media.tsx +56 -0
  112. package/dist/ui/elements/menu.js +2 -2
  113. package/dist/ui/elements/menu.tsx +50 -0
  114. package/dist/ui/elements/menu.works.tsx +153 -0
  115. package/dist/ui/elements/money.js +1 -1
  116. package/dist/ui/elements/money.tsx +28 -0
  117. package/dist/ui/elements/money.works.ts +9 -0
  118. package/dist/ui/elements/pagination.js +2 -2
  119. package/dist/ui/elements/pagination.tsx +38 -0
  120. package/dist/ui/elements/pagination.works.ts +20 -0
  121. package/dist/ui/elements/progress.js +1 -1
  122. package/dist/ui/elements/progress.tsx +33 -0
  123. package/dist/ui/elements/progress.works.ts +8 -0
  124. package/dist/ui/elements/radio-group.js +2 -2
  125. package/dist/ui/elements/radio-group.tsx +49 -0
  126. package/dist/ui/elements/radio-group.works.ts +33 -0
  127. package/dist/ui/elements/row.js +1 -1
  128. package/dist/ui/elements/row.tsx +28 -0
  129. package/dist/ui/elements/scatter.js +1 -1
  130. package/dist/ui/elements/scatter.tsx +41 -0
  131. package/dist/ui/elements/scatter.works.ts +38 -0
  132. package/dist/ui/elements/select.js +2 -2
  133. package/dist/ui/elements/select.tsx +42 -0
  134. package/dist/ui/elements/select.works.ts +5 -0
  135. package/dist/ui/elements/separator.js +1 -1
  136. package/dist/ui/elements/separator.tsx +30 -0
  137. package/dist/ui/elements/skeleton.js +1 -1
  138. package/dist/ui/elements/skeleton.tsx +30 -0
  139. package/dist/ui/elements/slider.js +1 -1
  140. package/dist/ui/elements/slider.tsx +33 -0
  141. package/dist/ui/elements/slider.works.ts +5 -0
  142. package/dist/ui/elements/sparkline.js +1 -1
  143. package/dist/ui/elements/sparkline.tsx +36 -0
  144. package/dist/ui/elements/sparkline.works.ts +32 -0
  145. package/dist/ui/elements/spinner.js +1 -1
  146. package/dist/ui/elements/spinner.tsx +32 -0
  147. package/dist/ui/elements/stack.js +1 -1
  148. package/dist/ui/elements/stack.tsx +28 -0
  149. package/dist/ui/elements/stat.js +1 -1
  150. package/dist/ui/elements/stat.tsx +35 -0
  151. package/dist/ui/elements/stat.works.ts +14 -0
  152. package/dist/ui/elements/surface.js +1 -1
  153. package/dist/ui/elements/surface.tsx +35 -0
  154. package/dist/ui/elements/surface.works.ts +9 -0
  155. package/dist/ui/elements/table.js +2 -2
  156. package/dist/ui/elements/table.tsx +61 -0
  157. package/dist/ui/elements/table.works.ts +30 -0
  158. package/dist/ui/elements/tabs.js +2 -2
  159. package/dist/ui/elements/tabs.tsx +47 -0
  160. package/dist/ui/elements/tabs.works.ts +84 -0
  161. package/dist/ui/elements/text.js +1 -1
  162. package/dist/ui/elements/text.tsx +32 -0
  163. package/dist/ui/elements/textarea.js +1 -1
  164. package/dist/ui/elements/textarea.tsx +37 -0
  165. package/dist/ui/elements/textarea.works.ts +5 -0
  166. package/dist/ui/elements/time.js +1 -1
  167. package/dist/ui/elements/time.tsx +34 -0
  168. package/dist/ui/elements/time.works.ts +25 -0
  169. package/dist/ui/elements/toggle.js +1 -1
  170. package/dist/ui/elements/toggle.tsx +33 -0
  171. package/dist/ui/elements/toggle.works.ts +5 -0
  172. package/dist/ui/elements/tooltip.js +1 -1
  173. package/dist/ui/elements/tooltip.tsx +39 -0
  174. package/dist/ui/elements/tooltip.works.ts +77 -0
  175. package/dist/ui/elements/value.js +1 -1
  176. package/dist/ui/elements/value.tsx +30 -0
  177. package/dist/ui/elements/value.works.ts +9 -0
  178. package/dist/ui/elements.ts +127 -0
  179. package/dist/ui/focus-scope.ts +121 -0
  180. package/dist/ui/fonts.ts +355 -0
  181. package/dist/ui/generators/dev.js +2 -1
  182. package/dist/ui/generators/dev.ts +80 -0
  183. package/dist/ui/generators/elements.d.ts +1 -11
  184. package/dist/ui/generators/elements.js +44 -14
  185. package/dist/ui/generators/elements.ts +794 -0
  186. package/dist/ui/generators/i18n.js +2 -3
  187. package/dist/ui/generators/i18n.ts +300 -0
  188. package/dist/ui/generators/icons.js +3 -12
  189. package/dist/ui/generators/icons.ts +214 -0
  190. package/dist/ui/generators/react.js +13 -5
  191. package/dist/ui/generators/react.ts +269 -0
  192. package/dist/ui/generators/routes.d.ts +1 -0
  193. package/dist/ui/generators/routes.js +99 -83
  194. package/dist/ui/generators/routes.ts +743 -0
  195. package/dist/ui/generators/show.js +4 -12
  196. package/dist/ui/generators/show.ts +260 -0
  197. package/dist/ui/generators/styles.js +3 -4
  198. package/dist/ui/generators/styles.ts +669 -0
  199. package/dist/ui/generators/variants.js +2 -3
  200. package/dist/ui/generators/variants.ts +204 -0
  201. package/dist/ui/i18n/catalogs/en.ts +191 -0
  202. package/dist/ui/i18n/format.ts +221 -0
  203. package/dist/ui/i18n/locales.ts +33 -0
  204. package/dist/ui/i18n/parse.ts +315 -0
  205. package/dist/ui/i18n/translator.ts +31 -0
  206. package/dist/ui/i18n/types.ts +96 -0
  207. package/dist/ui/icons.ts +331 -0
  208. package/dist/ui/islands/map.ts +58 -0
  209. package/dist/ui/localizations.ts +75 -0
  210. package/dist/ui/material.ts +236 -0
  211. package/dist/ui/materials/mat_bordered.ts +13 -0
  212. package/dist/ui/materials/mat_ceramic.ts +13 -0
  213. package/dist/ui/materials/mat_clay.ts +13 -0
  214. package/dist/ui/materials/mat_elevated.ts +24 -0
  215. package/dist/ui/materials/mat_flat.ts +34 -0
  216. package/dist/ui/materials/mat_glass.ts +20 -0
  217. package/dist/ui/materials/mat_lit.ts +13 -0
  218. package/dist/ui/materials/mat_moulded.ts +11 -0
  219. package/dist/ui/materials.js +2 -3
  220. package/dist/ui/materials.ts +205 -0
  221. package/dist/ui/modes.ts +44 -0
  222. package/dist/ui/navigation.tsx +32 -0
  223. package/dist/ui/positioning.ts +90 -0
  224. package/dist/ui/presented.tsx +119 -0
  225. package/dist/ui/provider.d.ts +2 -0
  226. package/dist/ui/provider.js +7 -1
  227. package/dist/ui/provider.tsx +182 -0
  228. package/dist/ui/render-loop.ts +133 -0
  229. package/dist/ui/roving-focus.ts +161 -0
  230. package/dist/ui/screens.js +22 -22
  231. package/dist/ui/screens.ts +141 -0
  232. package/dist/ui/scroll-lock.ts +28 -0
  233. package/dist/ui/series.ts +97 -0
  234. package/dist/ui/signals.ts +386 -0
  235. package/dist/ui/styleguide.ts +231 -0
  236. package/dist/ui/styleguides.ts +374 -0
  237. package/dist/ui/themes.ts +22 -0
  238. package/dist/ui/token.js +1 -1
  239. package/dist/ui/token.ts +78 -0
  240. package/dist/ui/tokens/shadow.ts +25 -0
  241. package/dist/ui/tokens.js +1 -2
  242. package/dist/ui/tokens.ts +234 -0
  243. package/dist/ui/validators.ts +232 -0
  244. package/dist/ui/variant.d.ts +1 -0
  245. package/dist/ui/variant.js +3 -0
  246. package/dist/ui/variant.ts +352 -0
  247. package/dist/ui/variants/var_avatar-round.ts +14 -0
  248. package/dist/ui/variants/var_bubble-me.ts +14 -0
  249. package/dist/ui/variants/var_bubble-them.ts +13 -0
  250. package/dist/ui/variants/var_button-border.ts +76 -0
  251. package/dist/ui/variants/var_button-cta.ts +76 -0
  252. package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
  253. package/dist/ui/variants/var_button-destructive.ts +76 -0
  254. package/dist/ui/variants/var_button-fill.ts +76 -0
  255. package/dist/ui/variants/var_button-ghost.ts +72 -0
  256. package/dist/ui/variants/var_button-glass.ts +55 -0
  257. package/dist/ui/variants/var_button-normal.ts +76 -0
  258. package/dist/ui/variants/var_button-quiet.ts +74 -0
  259. package/dist/ui/variants/var_card.ts +17 -0
  260. package/dist/ui/variants/var_chip.ts +13 -0
  261. package/dist/ui/variants/var_choice.ts +15 -0
  262. package/dist/ui/variants/var_framed.ts +10 -0
  263. package/dist/ui/variants/var_glyph-plain.ts +11 -0
  264. package/dist/ui/variants/var_input.ts +43 -0
  265. package/dist/ui/variants/var_link-inline.ts +29 -0
  266. package/dist/ui/variants/var_list-row.ts +34 -0
  267. package/dist/ui/variants/var_rule.ts +10 -0
  268. package/dist/ui/variants/var_skeleton-block.ts +12 -0
  269. package/dist/ui/variants/var_spinner-turning.ts +12 -0
  270. package/dist/ui/variants/var_stat-tile.ts +13 -0
  271. package/dist/ui/variants/var_tab-bar.ts +15 -0
  272. package/dist/ui/variants/var_tap-target.ts +25 -0
  273. package/dist/ui/variants.ts +64 -0
  274. package/dist/ui/wrappers.d.ts +4 -1
  275. package/dist/ui/wrappers.js +60 -20
  276. package/dist/ui/wrappers.tsx +426 -0
  277. package/package.json +8 -2
  278. package/dist/ui/makers/message.d.ts +0 -1
  279. package/dist/ui/makers/message.js +0 -230
@@ -0,0 +1,669 @@
1
+ import { existsSync, readFileSync, writeFileSync } from '@prestack/thread/core/files'
2
+ import { dirname, join, resolve } from 'node:path'
3
+ import { argv, stderr, stdout } from 'node:process'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+
6
+ import { exit } 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 { keyframeName, shorthandOf, stopsOf, type Animation } from '../animations.ts'
11
+ import { classOfRole, familyFor, fontStack, fontVariable, setNamed, settingsOf, type FontRole } from '../fonts.ts'
12
+ import { sizeNamed, sizeRows, SIZE_PREFIX } from '../icons.ts'
13
+ import { compose, composed, keyframesOf, type Material } from '../material.ts'
14
+ import { checkInteractions, knowSignals } from '../signals.ts'
15
+ import { chosen, shifted, themedBy, type StyleGuide } from '../styleguide.ts'
16
+ import { naming, nameOf, sheeted, type Holds, type Token } from '../token.ts'
17
+ import { classOf, classOfComposition, classOfElement, classOfScreen, complete, statesOf, structural, under, undeclaredStates, unreacted, boundHeld, type Variant } from '../variant.ts'
18
+
19
+ interface Row {
20
+ readonly id: string
21
+ readonly at?: string
22
+ readonly token?: string
23
+ readonly is?: string
24
+ readonly asks?: Readonly<Record<string, string | number>>
25
+ readonly wears?: readonly string[]
26
+ readonly sets?: Readonly<Record<string, string>>
27
+ readonly places?: Readonly<Record<string, Readonly<Record<string, string>>>>
28
+ readonly modes?: Readonly<Record<string, string | Readonly<Record<string, string>>>>
29
+ readonly themes?: Readonly<Record<string, { readonly is?: string; readonly modes?: Readonly<Record<string, string>> }>>
30
+ readonly plays?: { readonly in?: string; readonly out?: string }
31
+ readonly measures?: readonly string[]
32
+ }
33
+
34
+ interface Node {
35
+ readonly id?: string
36
+ readonly at?: string
37
+ readonly declares?: readonly Row[]
38
+ readonly holds?: readonly Row[]
39
+ }
40
+
41
+ function inside(node: Node): readonly Row[] {
42
+ return [...(node.holds ?? []), ...(node.declares ?? [])]
43
+ }
44
+
45
+ const LAYERS = ['token', 'font', 'animation', 'material', 'element', 'variant', 'composition', 'screen'] as const
46
+
47
+ const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
48
+
49
+ const exported = (id: string): string => id.replace(/-/g, '_').toUpperCase()
50
+
51
+ function dashed(property: string): string {
52
+ return property.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
53
+ }
54
+
55
+ function block(selector: string, made: Readonly<Record<string, Holds>>, indent: string): readonly string[] {
56
+ const properties = Object.keys(made).sort(compare)
57
+ if (properties.length === 0) return []
58
+ const lines = [`${indent}${selector} {`]
59
+ for (const property of properties) lines.push(`${indent} ${dashed(property)}: ${made[property] ?? ''};`)
60
+ lines.push(`${indent}}`, '')
61
+ return lines
62
+ }
63
+
64
+ function sheet(
65
+ tokens: Readonly<Record<string, Token>>,
66
+ guide: StyleGuide,
67
+ materials: Readonly<Record<string, Material>>,
68
+ variants: Readonly<Record<string, Variant>>,
69
+ modes: readonly { readonly id: string; readonly is: string }[],
70
+ themes: readonly { readonly id: string }[],
71
+ screens: readonly Row[],
72
+ compositions: readonly Row[],
73
+ elements: readonly Row[],
74
+ fonts: Node,
75
+ icons: Node,
76
+ animations: Readonly<Record<string, Animation>>,
77
+ ): string {
78
+ const wrong = checkInteractions()[0]
79
+ if (wrong !== undefined) throw new Error(`${wrong.path}: ${wrong.message}`)
80
+
81
+ const sizes = sizeRows(inside(icons))
82
+ if (sizes.length === 0) {
83
+ throw new Error(
84
+ 'The icons sheet declares no size row, so nothing says how large a symbol is drawn. The style ' +
85
+ 'guide holds no icon size — one row per size in `icons.ts`, holding a length.',
86
+ )
87
+ }
88
+
89
+ const values = { ...chosen(guide, tokens), ...Object.fromEntries(sizes.map((one) => [one.id, sheeted(one.id, one.is)])) }
90
+ const named = new Set(Object.keys(values))
91
+ const themeIds = new Set(themes.map((theme) => theme.id))
92
+
93
+ const kinds: Record<string, string> = {}
94
+ for (const pick of guide.picks) kinds[pick.id] = pick.token
95
+ for (const size of sizes) kinds[size.id] = 'icon'
96
+
97
+ const played = (made: Readonly<Record<string, Holds>>): Readonly<Record<string, Holds>> => {
98
+ const out: Record<string, Holds> = {}
99
+ for (const [id, value] of Object.entries(made)) {
100
+ if (kinds[id] !== 'animation') {
101
+ out[id] = value
102
+ continue
103
+ }
104
+ const animation = animations[value]
105
+ if (animation === undefined) {
106
+ const has = Object.keys(animations).sort().map((one) => `\`${one}\``)
107
+ throw new Error(
108
+ `\`${id}\` names the animation \`${value}\`, and no row declares one by that name. ` +
109
+ (has.length === 0
110
+ ? 'The app declares no animations.'
111
+ : `The declared animations are ${has.join(', ')}.`),
112
+ )
113
+ }
114
+ out[id] = sheeted(value, shorthandOf(animation, named))
115
+ }
116
+ return out
117
+ }
118
+
119
+ const variables = (
120
+ made: Readonly<Record<string, Holds>>,
121
+ selector: string,
122
+ indent: string,
123
+ ): readonly string[] => {
124
+ const names = Object.keys(made).sort(compare)
125
+ if (names.length === 0) return []
126
+ const held = [`${indent}${selector} {`]
127
+ for (const id of names) held.push(`${indent} ${nameOf(id)}: ${made[id] ?? ''};`)
128
+ held.push(`${indent}}`, '')
129
+ return held
130
+ }
131
+
132
+ const lines: string[] = [
133
+ '/* GENERATED by thread — do not edit. Change the declarations and regenerate. */',
134
+ '',
135
+ `@layer ${LAYERS.join(', ')};`,
136
+ '',
137
+ '@layer token {',
138
+ ...variables(played(values), ':root', ' '),
139
+ ]
140
+
141
+ for (const mode of modes) {
142
+ const held = variables(played(shifted(guide, tokens, mode.id, undefined, themeIds)), ':root', ' ')
143
+ if (held.length === 0) continue
144
+ lines.push(` @media ${mode.is} {`, ...held, ' }', '')
145
+ }
146
+
147
+ for (const theme of themes) {
148
+ lines.push(...variables(played(shifted(guide, tokens, undefined, theme.id, themeIds)), themedBy(theme.id), ' '))
149
+ for (const mode of modes) {
150
+ const held = variables(played(shifted(guide, tokens, mode.id, theme.id, themeIds)), themedBy(theme.id), ' ')
151
+ if (held.length === 0) continue
152
+ lines.push(` @media ${mode.is} {`, ...held, ' }', '')
153
+ }
154
+ }
155
+
156
+ const asked = modes.map((mode) => mode.id).join(' ')
157
+ lines.push(
158
+ ...(asked === '' ? [] : [' :root {', ` color-scheme: ${asked};`, ' }', '']),
159
+ ' body {',
160
+ ' background: var(--colour-page);',
161
+ ' color: var(--colour-ink);',
162
+ ' }',
163
+ '',
164
+ )
165
+
166
+ lines.push('}', '')
167
+
168
+ const type = typography(fonts)
169
+ lines.push('@layer font {', ...type.lines, '}', '')
170
+
171
+ lines.push('@layer animation {')
172
+ for (const id of Object.keys(animations).sort(compare)) {
173
+ const animation = animations[id]
174
+ if (animation === undefined) continue
175
+ lines.push(` @keyframes ${keyframeName(id)} {`)
176
+ for (const stop of stopsOf(animation, named, tokens, kinds)) lines.push(...block(stop.stop, stop.made, ' '))
177
+ lines.push(' }', '')
178
+ }
179
+ lines.push('}', '')
180
+
181
+ const moving = new Map<
182
+ string,
183
+ {
184
+ readonly by: string
185
+ readonly stops: readonly { readonly stop: string; readonly made: Readonly<Record<string, Holds>> }[]
186
+ }
187
+ >()
188
+ for (const id of Object.keys(materials).sort(compare)) {
189
+ const material = materials[id]
190
+ if (material === undefined) continue
191
+ for (const one of keyframesOf(material, named, tokens, kinds)) {
192
+ const already = moving.get(one.id)
193
+ if (already !== undefined) {
194
+ throw new Error(
195
+ `\`${id}\` and \`${already.by}\` both declare keyframes called \`${one.id}\`, and a ` +
196
+ 'stylesheet holds one rule per name, so whichever came last would silently win.',
197
+ )
198
+ }
199
+ moving.set(one.id, { by: id, stops: one.stops })
200
+ }
201
+ }
202
+
203
+ lines.push('@layer material {')
204
+ for (const name of [...moving.keys()].sort(compare)) {
205
+ lines.push(` @keyframes ${name} {`)
206
+ for (const stop of moving.get(name)?.stops ?? []) lines.push(...block(stop.stop, stop.made, ' '))
207
+ lines.push(' }', '')
208
+ }
209
+ for (const id of Object.keys(materials).sort(compare)) {
210
+ const material = materials[id]
211
+ if (material === undefined) continue
212
+ const gap = undeclaredStates(material)[0]
213
+ if (gap !== undefined) throw new Error(`\`${id}\`: ${gap}`)
214
+ compose(material, named, tokens, kinds)
215
+ under(material, named, tokens, kinds)
216
+ }
217
+ lines.push('}', '')
218
+
219
+ lines.push('@layer variant {')
220
+ for (const id of Object.keys(variants).sort(compare)) {
221
+ const variant = variants[id]
222
+ if (variant === undefined) continue
223
+ const whole = statesOf(variant, materials)
224
+ const first = undeclaredStates(whole)[0]
225
+ if (first !== undefined) throw new Error(`\`${id}\`: ${first}`)
226
+ const unclaimed = unreacted(variant, materials)[0]
227
+ if (unclaimed !== undefined) throw new Error(unclaimed)
228
+
229
+ let typed: Readonly<Record<string, Holds>> = {}
230
+ if (variant.type !== undefined) {
231
+ const role = type.roles.get(variant.type)
232
+ if (role === undefined) {
233
+ const has = [...type.roles.keys()].sort(compare).map((one) => `\`${one}\``)
234
+ throw new Error(
235
+ `\`${id}\` sets its wording in the \`${variant.type}\` role, and the fonts sheet declares ` +
236
+ `no role by that name. The declared roles are ${has.join(', ')}.`,
237
+ )
238
+ }
239
+ typed = role
240
+ }
241
+
242
+ const made = { ...typed, ...complete(variant, materials, named, tokens, kinds) }
243
+ lines.push(...block(`.${classOf(id)}`, made, ' '))
244
+ lines.push(...playing(`.${classOf(id)}`, variant.plays, named, id))
245
+ lines.push(...symbols(id, variant.icon, named, made['display'] !== undefined))
246
+ for (const state of under(whole, named, tokens, kinds)) {
247
+ lines.push(...block(`.${classOf(id)}${state.marks}`, state.made, ' '))
248
+ }
249
+ }
250
+ lines.push('}', '')
251
+
252
+ for (const held of [
253
+
254
+ { layer: 'element', named: classOfElement, rows: elements },
255
+ { layer: 'composition', named: classOfComposition, rows: compositions },
256
+ { layer: 'screen', named: classOfScreen, rows: screens },
257
+ ]) {
258
+ lines.push(`@layer ${held.layer} {`)
259
+ for (const row of [...held.rows].sort((one, two) => compare(one.id, two.id))) {
260
+ const selector = `.${held.named(row.id)}`
261
+ lines.push(...block(selector, resolved(row.sets ?? {}, guide, row.id, row.measures), ' '))
262
+ lines.push(...playing(selector, row.plays, named, row.id))
263
+ for (const [id, made] of Object.entries(row.places ?? {})) {
264
+ if (!PLACE.test(id)) {
265
+ throw new Error(
266
+ `\`${row.id}\` gives a place the name "${id}", and a place is named in the markup as \`at="${id}"\` ` +
267
+ 'and becomes part of a class, so it has to be lowercase words joined by dashes. Anything else ' +
268
+ 'produces a class the child can never match, and nothing would look wrong until you saw the screen.',
269
+ )
270
+ }
271
+
272
+ const looks = Object.fromEntries(Object.entries(made).filter(([key]) => !SAID.has(key)))
273
+ lines.push(...block(`${selector}-${id}`, resolved(looks, guide, `${row.id} place \`${id}\``, row.measures), ' '))
274
+ }
275
+ for (const [id, made] of Object.entries(row.modes ?? {})) {
276
+ if (typeof made === 'string') continue
277
+ const query = modes.find((one) => one.id === id)?.is
278
+ if (query === undefined) {
279
+ throw new Error(
280
+ `\`${row.id}\` arranges itself under "${id}", and no mode by that name is declared, so nothing ` +
281
+ 'can tell when it applies. A breakpoint is a mode — declare it beside dark and light, with the ' +
282
+ 'width as its query.',
283
+ )
284
+ }
285
+ lines.push(` @media ${query} {`, ...block(selector, resolved(made, guide, row.id), ' '), ' }', '')
286
+ }
287
+ }
288
+ lines.push('}', '')
289
+ }
290
+
291
+ return lines.join('\n')
292
+ }
293
+
294
+ const LAID = 'layout-flex'
295
+
296
+ const CENTRED = 'align-centre'
297
+
298
+ function symbols(id: string, size: string | undefined, named: ReadonlySet<string>, laid: boolean): readonly string[] {
299
+ if (size === undefined) return []
300
+ const row = sizeNamed(size)
301
+ if (!named.has(row)) {
302
+ const has = [...named].filter((one) => one.startsWith(SIZE_PREFIX)).sort(compare).map((one) => `\`${one}\``)
303
+ throw new Error(
304
+ `\`${id}\` draws the symbol inside it at \`${row}\`, and the icons sheet declares no size by ` +
305
+ `that name. The declared sizes are ${has.join(', ')}.`,
306
+ )
307
+ }
308
+ const drawn = naming(named, row, id, 'the size its symbol is drawn at')
309
+
310
+ const centred =
311
+ laid || !named.has(LAID) || !named.has(CENTRED)
312
+ ? []
313
+ : block(
314
+ `.${classOf(id)}`,
315
+ {
316
+ display: naming(named, LAID, id, 'how what it holds is laid out'),
317
+ alignItems: naming(named, CENTRED, id, 'where its symbol sits across that'),
318
+ },
319
+ ' ',
320
+ )
321
+
322
+ return [
323
+ ...centred,
324
+ ...block(`.${classOf(id)} > svg`, { width: drawn, height: drawn, minWidth: drawn, minHeight: drawn }, ' '),
325
+ ]
326
+ }
327
+
328
+ function typography(node: Node): {
329
+ readonly lines: readonly string[]
330
+ readonly roles: ReadonlyMap<string, Readonly<Record<string, Holds>>>
331
+ } {
332
+ const rows = inside(node)
333
+ const roles = new Map<string, Readonly<Record<string, Holds>>>()
334
+
335
+ const picked = rows.find((row) => row.id === 'family-set')
336
+ if (picked?.is === undefined) {
337
+ throw new Error(
338
+ 'The fonts sheet declares no `family-set` row, so nothing says which set of families the app ' +
339
+ 'draws from. One row, naming a registered set — every face is drawn from it.',
340
+ )
341
+ }
342
+ const set = setNamed(picked.is)
343
+
344
+ const faces: Record<string, string> = {}
345
+ const classes: string[] = []
346
+
347
+ for (const row of rows) {
348
+ if (row.id === 'family-set') continue
349
+ if (row.sets === undefined) {
350
+ throw new Error(
351
+ `\`${row.id}\` is declared a font role and states nothing about how it is set. A role names a ` +
352
+ 'face and states its size, weight, leading and tracking.',
353
+ )
354
+ }
355
+ const role = row.sets as unknown as FontRole
356
+ const family = familyFor(set, role.face, row.id)
357
+ faces[role.face] = fontStack(family)
358
+
359
+ const made = settingsOf(row.id, role)
360
+ roles.set(row.id, made)
361
+
362
+ classes.push(...block(`.${classOfRole(row.id)}`, made, ' '))
363
+
364
+ }
365
+
366
+ const held = [' :root {']
367
+ for (const face of Object.keys(faces).sort(compare)) held.push(` ${fontVariable(face)}: ${faces[face] ?? ''};`)
368
+ held.push(' }', '')
369
+
370
+ return { lines: [...held, ...classes], roles }
371
+ }
372
+
373
+ function playing(
374
+ selector: string,
375
+ plays: { readonly in?: string; readonly out?: string } | undefined,
376
+ named: ReadonlySet<string>,
377
+ by: string,
378
+ ): readonly string[] {
379
+ const held: string[] = []
380
+ for (const [moment, row, on] of [
381
+ ['in', plays?.in, `${selector}[data-arriving]`],
382
+ ['out', plays?.out, `${selector}[data-leaving]`],
383
+ ] as const) {
384
+ if (row === undefined) continue
385
+ if (!named.has(row)) {
386
+ throw new Error(
387
+ `\`${by}\` plays \`${row}\` on the way ${moment}, and the style guide holds no row by that name. ` +
388
+ 'What a thing plays is a style guide value like any other — name a row, and the row names the animation.',
389
+ )
390
+ }
391
+ held.push(...block(on, { animation: naming(named, row, by, `what it plays on the way ${moment}`) }, ' '))
392
+ }
393
+ return held
394
+ }
395
+
396
+ const PLACE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
397
+
398
+ const SAID = structural
399
+
400
+ const RAW = /\d\s*(px|rem|em|ch|vh|vw)\b|\d%|#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(/
401
+
402
+ function resolved(
403
+ sets: Readonly<Record<string, string>>,
404
+ guide: StyleGuide,
405
+ by: string,
406
+ measures: readonly string[] = [],
407
+ ): Readonly<Record<string, Holds>> {
408
+ const made: Record<string, Holds> = {}
409
+ const bound = boundHeld()
410
+ for (const [property, whole] of Object.entries(sets)) {
411
+
412
+ const words = whole.trim().split(/\s+/)
413
+ const held: string[] = []
414
+ for (const value of words) {
415
+ if (measures.includes(value)) {
416
+ held.push(`var(--${value})`)
417
+ continue
418
+ }
419
+ if (RAW.test(value)) {
420
+ throw new Error(
421
+ `\`${by}\` sets ${property} to "${value}", and that is a value written where a name belongs. ` +
422
+ 'An arrangement names a style guide row and the row holds the value — so the whole app moves ' +
423
+ 'when the row moves. Add a row for it, or name one that exists.',
424
+ )
425
+ }
426
+ const row = guide.picks.find((one) => one.id === value)
427
+ if (row === undefined) {
428
+ throw new Error(
429
+ `\`${by}\` sets ${property} to "${value}", and that names neither a style guide row nor one ` +
430
+ "of this element's measures. A name that is neither is written into the stylesheet as-is " +
431
+ 'and thrown away by the browser. Declare it in `measures`, or name a row.',
432
+ )
433
+ }
434
+
435
+ const kinds = bound[property]
436
+ if (kinds !== undefined && !kinds.includes(String(row.token))) {
437
+ throw new Error(
438
+ `\`${by}\` sets ${property} to \`${value}\`, which is a \`${String(row.token)}\` row. ` +
439
+ `${property} takes ${kinds.map((one) => `\`${one}\``).join(' or ')}. A row of the wrong kind ` +
440
+ 'is written into the stylesheet and dropped by the browser without a word.',
441
+ )
442
+ }
443
+
444
+ held.push(`var(--${value})`)
445
+ }
446
+ made[property] = sheeted(whole, held.join(' '))
447
+ }
448
+ return made
449
+ }
450
+
451
+ async function fileFor<T>(root: string, at: string, id: string, what: string): Promise<T> {
452
+ const module = (await import(pathToFileURL(resolve(root, at)).href)) as Record<string, unknown>
453
+ const one = module[exported(id)]
454
+ if (one === undefined) {
455
+ throw new Error(
456
+ `${what} \`${id}\` is declared at ${at}, and that file exports no \`${exported(id)}\`. ` +
457
+ 'A row names a file, and the file is the thing.',
458
+ )
459
+ }
460
+ return one as T
461
+ }
462
+
463
+ async function loaded<T>(root: string, node: Node, what: string): Promise<Readonly<Record<string, T>>> {
464
+ const found: Record<string, T> = {}
465
+ for (const row of inside(node)) {
466
+ if (row.at === undefined) {
467
+ throw new Error(`${what} \`${row.id}\` names no file, so there is nothing to read it from.`)
468
+ }
469
+ found[row.id] = await fileFor<T>(root, row.at, row.id, what)
470
+ }
471
+ return found
472
+ }
473
+
474
+ async function tokensIn(root: string, node: Node): Promise<Readonly<Record<string, Token>>> {
475
+ const found: Record<string, Token> = {}
476
+ for (const row of inside(node)) {
477
+ found[row.id] =
478
+ row.at === undefined || row.at === node.at
479
+ ? { id: row.id }
480
+ : await fileFor<Token>(root, row.at, row.id, 'token')
481
+ }
482
+ return found
483
+ }
484
+
485
+ function guideIn(node: Node): StyleGuide {
486
+ return {
487
+ id: node.id ?? 'styleguide',
488
+ picks: inside(node).map((row) => {
489
+ if (row.token === undefined) {
490
+ throw new Error(
491
+ `\`${row.id}\` names no token. A style guide row is one value OF a token, so it has to ` +
492
+ 'say which token it is one of.',
493
+ )
494
+ }
495
+ return {
496
+ id: row.id,
497
+
498
+ token: row.token as StyleGuide['picks'][number]['token'],
499
+ ...(row.is === undefined ? {} : { is: row.is }),
500
+ ...(row.asks === undefined ? {} : { asks: row.asks }),
501
+ ...(row.modes === undefined
502
+ ? {}
503
+ : {
504
+ modes: Object.fromEntries(
505
+ Object.entries(row.modes).filter((one): one is [string, string] => typeof one[1] === 'string'),
506
+ ),
507
+ }),
508
+
509
+ ...(row.themes === undefined ? {} : { themes: row.themes }),
510
+ }
511
+ }),
512
+ }
513
+ }
514
+
515
+ function pascal(id: string): string {
516
+ return id
517
+ .split('-')
518
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
519
+ .join('')
520
+ }
521
+
522
+ function unwearable(variants: Node, elements: Node): readonly string[] {
523
+ const declared = new Set((elements.declares ?? []).map((row) => pascal(row.id)))
524
+ const missing: string[] = []
525
+ for (const row of variants.declares ?? []) {
526
+ for (const element of row.wears ?? []) {
527
+ if (declared.has(element)) continue
528
+ missing.push(
529
+ `\`${row.id}\` says \`${element}\` accepts it, and no element by that name is declared, ` +
530
+ 'so nothing would ever wear it.',
531
+ )
532
+ }
533
+ }
534
+ return missing
535
+ }
536
+
537
+ function modesIn(node: Node): readonly { readonly id: string; readonly is: string }[] {
538
+ return inside(node).map((row) => {
539
+ if (row.is === undefined) {
540
+ throw new Error(
541
+ `\`${row.id}\` is declared a mode and states no query, so nothing could tell when it applies. ` +
542
+ 'A mode is the question a device asks — give it the media query that answers it.',
543
+ )
544
+ }
545
+ return { id: row.id, is: row.is }
546
+ })
547
+ }
548
+
549
+ const OWN_AT = '../..'
550
+
551
+ async function node(root: string, file: string, name: string): Promise<Node> {
552
+ const own = argv.includes('--own')
553
+ const at = own ? resolve(fileURLToPath(import.meta.url), OWN_AT, file) : inTree(findTree(), file)
554
+ if (!own && !existsSync(at)) {
555
+ stdout.write(`styles: this tree has no ${findTree().name}/${file}, so it declares nothing to style and there is no stylesheet to write or check.\n`)
556
+ exit(0)
557
+ }
558
+ const module = (await import(pathToFileURL(at).href)) as Record<string, unknown>
559
+ const one = module[own ? `${name}S` : name]
560
+ if (one === undefined) {
561
+ throw new Error(
562
+ own ? `this package's own ${file} exports no \`${name}S\`.` : `_threads/${file} exports no \`${name}\`.`,
563
+ )
564
+ }
565
+ return one as Node
566
+ }
567
+
568
+ async function run(): Promise<number> {
569
+ const own = argv.includes('--own')
570
+ const root = own ? resolve(fileURLToPath(import.meta.url), '../../../../..') : findTree().at
571
+
572
+ knowSignals(inside(await node(root, 'signals.ts', 'SIGNAL')).map((one) => one.id))
573
+
574
+ const tokens = await tokensIn(root, await node(root, 'tokens.ts', 'TOKEN'))
575
+ const guide = guideIn(await node(root, 'styleguides.ts', 'STYLEGUIDE'))
576
+ const materials = await loaded<Material>(root, await node(root, 'materials.ts', 'MATERIAL'), 'material')
577
+ const declaring = await node(root, 'variants.ts', 'VARIANT')
578
+ const variants = await loaded<Variant>(root, declaring, 'variant')
579
+
580
+ const unworn = unwearable(declaring, await node(root, 'elements.ts', 'ELEMENT'))[0]
581
+ if (unworn !== undefined) throw new Error(unworn)
582
+
583
+ const fonts = await node(root, 'fonts.ts', 'FONT')
584
+ const icons = await node(root, `icons.${'ts'}`, 'ICON')
585
+ const animations = await loaded<Animation>(root, await node(root, 'animations.ts', 'ANIMATION'), 'animation')
586
+
587
+ const modes = modesIn(await node(root, 'modes.ts', 'MODE'))
588
+ const themes = inside(await node(root, 'themes.ts', 'THEME'))
589
+ const screens = (await node(root, 'screens.ts', 'SCREEN')).declares ?? []
590
+ const compositions = (await node(root, 'compositions.ts', 'COMPOSITION')).declares ?? []
591
+
592
+ const elements = ((await node(root, 'elements.ts', 'ELEMENT')).declares ?? []).filter(
593
+ (one) => one.sets !== undefined || one.places !== undefined,
594
+ )
595
+
596
+ const camelOf = (id: string): string => id.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase())
597
+
598
+ const worn = ((declaring.declares ?? []) as readonly { readonly id?: string; readonly wears?: readonly string[] }[])
599
+ .filter((one) => typeof one.id === 'string' && (one.wears ?? []).length > 0)
600
+ .map((one) => ` readonly ${camelOf(String(one.id))}: ${(one.wears ?? []).map((e) => `'${e}'`).join(' | ')}`)
601
+
602
+ const asking = (((await node(root, 'signals.ts', 'SIGNAL')).holds ?? []) as readonly {
603
+ readonly id?: string
604
+ readonly selector?: string
605
+ readonly when?: string
606
+ }[]).filter((one) => typeof one.id === 'string')
607
+
608
+ const wearingSource = [
609
+ '/* GENERATED by thread — do not edit. It is derived from what this app declares. */',
610
+ '',
611
+ `declare module '${packageOf('ui')}/checked' {`,
612
+ ...(worn.length === 0 ? [' interface Wearing {}'] : [' interface Wearing {', ...worn, ' }']),
613
+ '}',
614
+ '',
615
+ 'export const ASKING = [',
616
+ ...asking.map(
617
+ (one) =>
618
+ ` { id: '${String(one.id)}', selector: '${one.selector ?? `[data-${String(one.id)}]`}'${typeof one.when === 'string' ? `, when: '${one.when}'` : ''} },`,
619
+ ),
620
+ '] as const',
621
+ '',
622
+ ].join('\n')
623
+
624
+ const at = join(root, 'app/_generated/tokens.css')
625
+ const made = sheet(tokens, guide, materials, variants, modes, themes, screens, compositions, elements, fonts, icons, animations)
626
+
627
+ if (own) {
628
+ stdout.write(
629
+ `styles: this framework's own declarations build a stylesheet — ${String(guide.picks.length)} values, ` +
630
+ `${String(Object.keys(materials).length)} materials, ${String(Object.keys(variants).length)} variants, ` +
631
+ `${String(Object.keys(animations).length)} animations, every one naming rows that exist. ` +
632
+ 'Nothing is written: a stylesheet belongs to an app, and building it is the check.\n',
633
+ )
634
+ return 0
635
+ }
636
+
637
+ if (argv.includes('--check')) {
638
+ let current = ''
639
+ try {
640
+ current = readFileSync(at, 'utf8')
641
+ } catch {
642
+ current = ''
643
+ }
644
+ if (current === made) {
645
+ stdout.write('styles: app/_generated/tokens.css is the bytes the declarations produce\n')
646
+ return 0
647
+ }
648
+ stderr.write(
649
+ 'styles: app/_generated/tokens.css is not what the declarations produce. A derived file that ' +
650
+ 'differs from its generator is a hand edit wearing a generated name, and every reader downstream ' +
651
+ 'believes the wrong one. Run `thread styles` and commit what it writes.\n',
652
+ )
653
+ return 1
654
+ }
655
+
656
+ writeFileSync(at, made, 'utf8')
657
+ writeFileSync(join(root, 'app/_generated/declared.ts'), wearingSource, 'utf8')
658
+
659
+ stdout.write(
660
+ `thread: wrote app/_generated/tokens.css — ${String(Object.keys(tokens).length)} tokens, ` +
661
+ `${String(guide.picks.length)} values, ${String(Object.keys(materials).length)} materials, ` +
662
+ `${String(Object.keys(variants).length)} variants, ${String(inside(fonts).length - 1)} font roles, ` +
663
+ `${String(Object.keys(animations).length)} animations, ${String(modes.length)} modes, ` +
664
+ `${String(themes.length)} themes, ${String(compositions.length)} compositions, ${String(screens.length)} screens\n`,
665
+ )
666
+ return 0
667
+ }
668
+
669
+ if (argv[1] !== undefined) exit(await run())
@@ -6,8 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
- import { basename, dirname, join, resolve } from 'node:path';
9
+ import { existsSync, readFileSync, writeFileSync } from "@prestack/thread/core/files";
10
+ import { basename, join, resolve } from 'node:path';
11
11
  import { argv, stdout } from 'node:process';
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
13
  import { exit, invoked } from "@prestack/thread/core/invoked";
@@ -117,7 +117,6 @@ async function run() {
117
117
  }
118
118
  else {
119
119
  for (const seed of made) {
120
- mkdirSync(dirname(join(root, seed.path)), { recursive: true });
121
120
  writeFileSync(join(root, seed.path), seed.content, 'utf8');
122
121
  stdout.write(`thread: seeded ${seed.path}\n`);
123
122
  }