@prestack/thread-ui 0.1.8 → 0.1.10

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.
@@ -3,12 +3,13 @@ import { argv, stderr, stdout } from 'node:process'
3
3
 
4
4
  import ts from 'typescript'
5
5
 
6
- import { existsSync, readFileSync, realpathSync, writeFileSync } from '@prestack/thread/integrations/disk'
6
+ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from '@prestack/thread/integrations/disk'
7
7
  import { exit, invoked } from '@prestack/thread/core/invoked'
8
- import { valuesIn } from '@prestack/thread/core/rows'
8
+ import { listBounds, nestedBounds, rowsIn, valuesIn, withField, withRow, withoutField, withoutRow } from '@prestack/thread/core/rows'
9
9
  import { named as packageOf } from '@prestack/thread/core/scope'
10
10
  import { findTree, inTree } from '@prestack/thread/core/tree'
11
11
  import { PERMITS_AT } from '@prestack/thread/operations/access'
12
+ import { said } from '@prestack/thread/operations/attach'
12
13
  import { ACTOR_AT, COMPILED } from '@prestack/thread/operations/capabilities'
13
14
 
14
15
  import { compiledAt, nameOf as compositionOf } from '../compositions.ts'
@@ -36,6 +37,15 @@ const GRAMMAR = `${COMPILED}/ui/islands/map.ts`
36
37
 
37
38
  const MOVES = 'navigateTo'
38
39
 
40
+ const PAGE_ROW = 'page'
41
+
42
+ const WRITER = 'thread routes'
43
+
44
+ const PAGE_SAID =
45
+ 'The page a browser gets at the address of this screen: `/` when it is the first page this app declares, and its own ' +
46
+ 'name otherwise. `thread routes` writes the page and this row from where the screen sits, and moves or deletes both ' +
47
+ 'when that changes.'
48
+
39
49
  function served(at: string): boolean {
40
50
  try {
41
51
  const held = JSON.parse(readFileSync(join(at, 'package.json'), 'utf8')) as {
@@ -386,6 +396,51 @@ function page(name: string, at: string, route: string): string {
386
396
  ].join('\n')
387
397
  }
388
398
 
399
+ interface Repaged {
400
+ readonly id: string
401
+ readonly shown: string
402
+ readonly route: string | undefined
403
+ readonly was: string | undefined
404
+ readonly where: string | undefined
405
+ }
406
+
407
+ const pageShaped = (path: string): boolean => path === pageAt(`/${path.split('/').slice(1, -1).join('/')}`)
408
+
409
+ function unpaged(one: Repaged): string {
410
+ if (one.where === undefined) {
411
+ return (
412
+ `\`${one.id}\` is presented as a ${one.shown}, so it has no page, and its row still derives ${String(one.was)}. ` +
413
+ `Run \`${WRITER}\`, which takes that row out and deletes the page.`
414
+ )
415
+ }
416
+ return (
417
+ `\`${one.id}\` answers ${String(one.route)}, so its page is ${one.where}, and its row ` +
418
+ `${one.was === undefined ? 'derives no page' : `says ${one.was}`}. Run \`${WRITER}\`, which writes that row from ` +
419
+ 'where the screen now sits and puts the page there.'
420
+ )
421
+ }
422
+
423
+ function repaged(text: string, moved: readonly Repaged[]): string | undefined {
424
+ let next = text
425
+ for (const one of moved) {
426
+ const list = listBounds(next, 'declares')
427
+ const inside = list === undefined ? undefined : nestedBounds(next, list, one.id, 'derives')
428
+ if (inside !== undefined) next = withoutRow(next, inside, PAGE_ROW)
429
+ const rows = listBounds(next, 'declares')
430
+ if (rows === undefined) return undefined
431
+ const within = nestedBounds(next, rows, one.id, 'derives')
432
+ if (one.where === undefined) {
433
+ if (within !== undefined && rowsIn(next, within).length === 0) next = withoutField(next, rows, one.id, 'derives')
434
+ continue
435
+ }
436
+ const row = said({ id: PAGE_ROW, at: one.where, describes: PAGE_SAID, version: 1, by: WRITER, runs: `${WRITER} --check` }, ' ')
437
+ const put = within === undefined ? withField(next, rows, one.id, 'derives', `[\n${row}\n ]`) : withRow(next, within, row)
438
+ if (put === undefined) return undefined
439
+ next = put
440
+ }
441
+ return next
442
+ }
443
+
389
444
  function gate(typed: boolean): readonly string[] {
390
445
  const said = typed ? ' as { of?: Record<string, string> }' : ''
391
446
  return [
@@ -647,6 +702,7 @@ async function run(): Promise<number> {
647
702
  const entry = entryAmong(rows, shownOf)
648
703
 
649
704
  const made: { readonly path: string; readonly text: string }[] = []
705
+ const moved: Repaged[] = []
650
706
  const routes = new Map<string, string | undefined>()
651
707
  const screens: MapIsland['screens'][number][] = []
652
708
  const reaches = reachable(at)
@@ -658,7 +714,7 @@ async function run(): Promise<number> {
658
714
  stderr.write(
659
715
  `routes: ${row.at} states \`route: '${typed}'\`, and a route is never typed: the first page an app declares ` +
660
716
  'answers `/`, every other page answers its own name, and a sheet has no route. Take `route:` out of the ' +
661
- 'file — `thread create screen edit` rewrites it without one when it is given the body.\n',
717
+ `file with \`thread edit ${row.at} --find <text> --replace <text>\`, which changes that one stretch of it.\n`,
662
718
  )
663
719
  return 1
664
720
  }
@@ -700,20 +756,18 @@ async function run(): Promise<number> {
700
756
  entry: row.id === entry,
701
757
  shows: [...shows],
702
758
  })
703
- if (route === undefined) continue
704
- const where = pageAt(route)
705
- const stated = (row.derives ?? []).find((one) => one.id === 'page')?.at
706
- if (stated !== where) {
707
- stderr.write(
708
- `routes: \`${row.id}\` answers ${route}, so its page is ${where}, and its row ` +
709
- `${stated === undefined ? 'derives no page' : `says ${stated}`}. Run \`thread create screen edit\` on it — ` +
710
- 'the row is rewritten from where the screen now lives.\n',
711
- )
712
- return 1
713
- }
759
+ const where = route === undefined ? undefined : pageAt(route)
760
+ const was = (row.derives ?? []).find((one) => one.id === PAGE_ROW)?.at
761
+ if (was !== where) moved.push({ id: row.id, shown: shownAs, route, was, where })
762
+ if (route === undefined || where === undefined) continue
714
763
  made.push({ path: where, text: page(name, row.at, route) })
715
764
  }
716
765
 
766
+ if (moved.length > 0 && argv.includes('--check')) {
767
+ for (const one of moved) stderr.write(`routes: ${unpaged(one)}\n`)
768
+ return 1
769
+ }
770
+
717
771
  const { links, wrong } = linksOf(at, rows, compositions, routes, wordingFirst(keyProps(at)))
718
772
  if (wrong.length > 0) {
719
773
  for (const one of wrong) stderr.write(`routes: ${one}\n`)
@@ -749,6 +803,33 @@ async function run(): Promise<number> {
749
803
  const paged = map.screens.filter((one) => one.path !== undefined).length
750
804
  const counted = `${String(map.screens.length)} screen(s), ${String(paged)} route(s), ${String(map.links.length)} link(s)`
751
805
 
806
+ if (moved.length > 0) {
807
+ const declaredIn = relative(at, screensAt)
808
+ const rewritten = repaged(readFileSync(screensAt, 'utf8'), moved)
809
+ if (rewritten === undefined) {
810
+ stderr.write(
811
+ `routes: ${declaredIn} could not be given the page row of each screen, so nothing was written. ` +
812
+ `\`thread read ${declaredIn}\` prints what it states now.\n`,
813
+ )
814
+ return 1
815
+ }
816
+ writeFileSync(screensAt, rewritten, 'utf8')
817
+ const answered = new Set(made.map((one) => one.path))
818
+ const home = dirname(join(at, pageAt('/')))
819
+ for (const one of moved) {
820
+ stdout.write(
821
+ one.where === undefined
822
+ ? `routes: \`${one.id}\` has no route, so its row no longer derives ${String(one.was)}\n`
823
+ : `routes: \`${one.id}\` answers ${String(one.route)}, so its row now derives ${one.where}\n`,
824
+ )
825
+ if (one.was === undefined || answered.has(one.was) || !pageShaped(one.was) || !existsSync(join(at, one.was))) continue
826
+ rmSync(join(at, one.was))
827
+ const folder = dirname(join(at, one.was))
828
+ if (folder !== home && readdirSync(folder).length === 0) rmSync(folder, { recursive: true, force: true })
829
+ stdout.write(`routes: deleted ${one.was}, because no screen answers there any more\n`)
830
+ }
831
+ }
832
+
752
833
  const refused = landed(at, made)
753
834
  if (refused !== undefined) return refused
754
835
 
@@ -1,5 +1,4 @@
1
1
  import { FREE, type Material, type Setting } from './material.ts';
2
- import type { MATERIALS } from './materials.ts';
3
2
  import type { Holds, Token, Wearing } from './token.ts';
4
3
  import { holds } from './signals.ts';
5
4
  interface Stateful {
@@ -10,7 +9,6 @@ interface Stateful {
10
9
  }[];
11
10
  }
12
11
  type SignalId = (typeof holds)[number]['id'] | (string & {});
13
- type MaterialId = (typeof MATERIALS)['declares'][number]['id'] | (string & {});
14
12
  declare const BOUND: {
15
13
  readonly background: readonly ['colour', 'gradient', 'texture', 'noise', 'img'];
16
14
  readonly borderColor: readonly ['colour'];
@@ -69,7 +67,7 @@ type VariantSetting = {
69
67
  export interface Variant {
70
68
  readonly id: string;
71
69
  readonly version?: number;
72
- readonly material?: MaterialId;
70
+ readonly material?: string;
73
71
  readonly type?: string;
74
72
  readonly icon?: string;
75
73
  readonly sets: readonly VariantSetting[];
@@ -1,5 +1,4 @@
1
1
  import { BOUND as MATERIAL_BOUND, compose, composed, FREE, type Material, type Setting } from './material.ts'
2
- import type { MATERIALS } from './materials.ts'
3
2
  import type { Holds, Token, Wearing } from './token.ts'
4
3
  import { coexist, holds, isSignal, selectorOf, signalNames } from './signals.ts'
5
4
 
@@ -14,8 +13,6 @@ interface Stateful {
14
13
 
15
14
  type SignalId = (typeof holds)[number]['id'] | (string & {})
16
15
 
17
- type MaterialId = (typeof MATERIALS)['declares'][number]['id'] | (string & {})
18
-
19
16
  const BOUND = {
20
17
  background: ['colour', 'gradient', 'texture', 'noise', 'img'],
21
18
  borderColor: ['colour'],
@@ -87,7 +84,7 @@ export interface Variant {
87
84
 
88
85
  readonly version?: number
89
86
 
90
- readonly material?: MaterialId
87
+ readonly material?: string
91
88
 
92
89
  readonly type?: string
93
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prestack/thread-ui",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -17,6 +17,6 @@
17
17
  "react-dom": "^19.0.0",
18
18
  "typescript": "^6.0.0",
19
19
  "zod": "^4.5.4",
20
- "@prestack/thread": "^0.1.19"
20
+ "@prestack/thread": "^0.1.21"
21
21
  }
22
22
  }