@prestack/thread-ui 0.1.7 → 0.1.9

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.
@@ -333,27 +333,29 @@ const SHEET = 'compiled.ts'
333
333
 
334
334
  const EMITS = 'thread compositions'
335
335
 
336
+ export const SCREENED_BY = 'thread screens'
337
+
336
338
  const SHIPS = `${COMPILED}/ui/`
337
339
 
338
340
  const idFor = (at: string): string => `compiled-${at.slice(COMPILED.length + 1).replace(/\.[^./]+$/, '').split('/').join('-')}`
339
341
 
340
- function meaning(at: string): string {
342
+ function meaning(at: string, by: string): string {
341
343
  if (at.startsWith(SHIPS)) {
342
344
  return (
343
345
  '`' + at.slice(SHIPS.length) + '` from ' + packageOf('ui') + ', copied into this app so a page runs it with nothing of ' +
344
- 'the framework installed. `' + EMITS + '` writes it, so an edit made here is refused.'
346
+ 'the framework installed. `' + by + '` writes it, so an edit made here is refused.'
345
347
  )
346
348
  }
347
349
  return (
348
350
  '`' + at.slice(COMPILED.length + 1) + '` as React runs it, compiled from the file a person wrote: a variant worn as a ' +
349
- 'tag has become its class, and a named place the class its arrangement declares. `' + EMITS + '` writes it, so an ' +
351
+ 'tag has become its class, and a named place the class its arrangement declares. `' + by + '` writes it, so an ' +
350
352
  'edit made here is refused.'
351
353
  )
352
354
  }
353
355
 
354
356
  type Sheeted = { readonly text: string } | { readonly because: string }
355
357
 
356
- function sheeted(tree: Tree, written: readonly string[]): Sheeted {
358
+ function sheeted(tree: Tree, written: readonly string[], screened: ReadonlySet<string>): Sheeted {
357
359
  const at = relative(tree.at, inTree(tree, SHEET))
358
360
  const ui = inTree(tree, 'ui.ts')
359
361
  const held = (existsSync(ui) ? valuesIn(readFileSync(ui)) : {}) as { readonly GENERATORS?: readonly Threaded[] }
@@ -365,18 +367,19 @@ function sheeted(tree: Tree, written: readonly string[]): Sheeted {
365
367
  'Run `thread attach ' + packageOf('ui') + '`: that sheet is written by the package, never by hand.',
366
368
  }
367
369
  }
368
- const rows = [...written].sort().map((one) =>
369
- [
370
+ const rows = [...written].sort().map((one) => {
371
+ const by = screened.has(one) ? SCREENED_BY : EMITS
372
+ return [
370
373
  ' {',
371
374
  ` id: ${quoted(idFor(one))},`,
372
375
  ` at: ${quoted(one)},`,
373
- ` describes: ${quoted(meaning(one))},`,
376
+ ` describes: ${quoted(meaning(one, by))},`,
374
377
  ' version: 1,',
375
- ` by: ${quoted(EMITS)},`,
376
- ` runs: ${quoted(`${EMITS} --check`)},`,
378
+ ` by: ${quoted(by)},`,
379
+ ` runs: ${quoted(`${by} --check`)},`,
377
380
  ' },',
378
- ].join('\n'),
379
- )
381
+ ].join('\n')
382
+ })
380
383
  return {
381
384
  text: [
382
385
  `import type { Threaded } from '${SHAPED}'`,
@@ -395,8 +398,8 @@ function sheeted(tree: Tree, written: readonly string[]): Sheeted {
395
398
  }
396
399
  }
397
400
 
398
- function declaring(tree: Tree, written: readonly string[], checking: boolean): readonly string[] {
399
- const made = sheeted(tree, written)
401
+ function declaring(tree: Tree, written: readonly string[], checking: boolean, screened: ReadonlySet<string>): readonly string[] {
402
+ const made = sheeted(tree, written, screened)
400
403
  if ('because' in made) return [made.because]
401
404
  const at = inTree(tree, SHEET)
402
405
  if (!checking) {
@@ -405,7 +408,7 @@ function declaring(tree: Tree, written: readonly string[], checking: boolean): r
405
408
  }
406
409
  if (existsSync(at) && readFileSync(at, 'utf8') === made.text) return []
407
410
  return [
408
- relative(tree.at, at) + ' is not what this command writes from what it emits. A sheet that differs from the files it ' +
411
+ relative(tree.at, at) + ' is not what this command writes. A sheet that differs from the files it ' +
409
412
  'declares is a hand edit wearing a generated name. Run `' + EMITS + '` and commit what it writes.',
410
413
  ]
411
414
  }
@@ -537,7 +540,7 @@ function run(): number {
537
540
  ...emitting(tree.at, entries, checking, kept),
538
541
  ...settled(tree.at, rows, checking, EMITS, kept),
539
542
  ]
540
- found.push(...declaring(tree, [...kept].filter((one) => !screened.has(one)), checking))
543
+ found.push(...declaring(tree, [...kept], checking, screened))
541
544
  const loose = [...found, ...(found.length === 0 ? stale(tree.at, kept, checking) : [])]
542
545
  for (const one of loose) stdout.write(`thread: ${one}\n`)
543
546
 
@@ -1,9 +1,9 @@
1
1
  import { releases, starts } from '@prestack/thread/integrations/cli';
2
2
  import { basename } from 'node:path';
3
- import { argv, execPath, stdout } from 'node:process';
4
- import { generators, settling } from '@prestack/thread/core/chain';
3
+ import { argv, stdout } from 'node:process';
4
+ import { batched, generators, settling } from '@prestack/thread/core/chain';
5
5
  import { existsSync } from '@prestack/thread/integrations/disk';
6
- import { CLI, exit, invoked } from '@prestack/thread/core/invoked';
6
+ import { exit, invoked } from '@prestack/thread/core/invoked';
7
7
  import { named } from '@prestack/thread/core/scope';
8
8
  import { findTree } from '@prestack/thread/core/tree';
9
9
  import { programOf } from '@prestack/thread/integrations/target';
@@ -11,7 +11,7 @@ const SERVER = 'next';
11
11
  const SHIP = '--ship';
12
12
  const ENTRY = 'dev';
13
13
  const SHIPPED = 'build';
14
- const GATE = 'gate';
14
+ const GATE = 'thread gate';
15
15
  const say = (line) => {
16
16
  stdout.write(line);
17
17
  };
@@ -38,10 +38,13 @@ async function run() {
38
38
  return 1;
39
39
  }
40
40
  say('thread: ' + basename(tree.at) + ' settled in ' + String(settled.rounds) + ' round(s); running the gate\n');
41
- const gated = starts(execPath, [...CLI, GATE], { at: tree.at, shows: true });
42
- if (!gated.ok) {
41
+ const gated = batched(tree.at, [GATE]);
42
+ for (const one of gated)
43
+ if (one.said !== '')
44
+ say(one.said + '\n');
45
+ if (!gated.every((one) => one.ok)) {
43
46
  say('thread: the gate refused this tree, and nothing is served over a red gate. What it refused is printed above.\n');
44
- return gated.code === 0 ? 1 : gated.code;
47
+ return 1;
45
48
  }
46
49
  say('thread: the gate passed; running ' + SERVER + ' ' + entry + '\n');
47
50
  const extra = argv.slice(2).filter((one) => one !== SHIP);
@@ -1,10 +1,10 @@
1
1
  import { releases, starts } from '@prestack/thread/integrations/cli'
2
2
  import { basename } from 'node:path'
3
- import { argv, execPath, stdout } from 'node:process'
3
+ import { argv, stdout } from 'node:process'
4
4
 
5
- import { generators, settling } from '@prestack/thread/core/chain'
5
+ import { batched, generators, settling } from '@prestack/thread/core/chain'
6
6
  import { existsSync } from '@prestack/thread/integrations/disk'
7
- import { CLI, exit, invoked } from '@prestack/thread/core/invoked'
7
+ import { exit, invoked } from '@prestack/thread/core/invoked'
8
8
  import { named } from '@prestack/thread/core/scope'
9
9
  import { findTree } from '@prestack/thread/core/tree'
10
10
  import { programOf } from '@prestack/thread/integrations/target'
@@ -17,7 +17,7 @@ const ENTRY = 'dev'
17
17
 
18
18
  const SHIPPED = 'build'
19
19
 
20
- const GATE = 'gate'
20
+ const GATE = 'thread gate'
21
21
 
22
22
  const say = (line: string): void => {
23
23
  stdout.write(line)
@@ -53,10 +53,11 @@ async function run(): Promise<number> {
53
53
  }
54
54
 
55
55
  say('thread: ' + basename(tree.at) + ' settled in ' + String(settled.rounds) + ' round(s); running the gate\n')
56
- const gated = starts(execPath, [...CLI, GATE], { at: tree.at, shows: true })
57
- if (!gated.ok) {
56
+ const gated = batched(tree.at, [GATE])
57
+ for (const one of gated) if (one.said !== '') say(one.said + '\n')
58
+ if (!gated.every((one) => one.ok)) {
58
59
  say('thread: the gate refused this tree, and nothing is served over a red gate. What it refused is printed above.\n')
59
- return gated.code === 0 ? 1 : gated.code
60
+ return 1
60
61
  }
61
62
 
62
63
  say('thread: the gate passed; running ' + SERVER + ' ' + entry + '\n')
@@ -182,8 +182,7 @@ export function ungated(from) {
182
182
  checked = null;
183
183
  if (at === '' || !existsSync(at)) {
184
184
  checked =
185
- 'this tree has no passing gate, so nothing in it may compile. Run `thread gate` — or the ' +
186
- '`dev`, `build` and `start` scripts, which run it for you. A gate that has never run is not a ' +
185
+ 'this tree has no passing gate, so nothing in it may compile. Run `thread gate`. A gate that has never run is not a ' +
187
186
  'gate that passed, and the file you are compiling was never measured. Compiling ' +
188
187
  `\`${from}\`, it looked for a gate receipt from \`${dirname(resolve(from))}\` and from ` +
189
188
  `\`${resolve(cwd())}\` upwards${at === '' ? ', and neither reached a tree' : `, and found none at ${at}`}`;
@@ -226,8 +226,7 @@ export function ungated(from: string): string | null {
226
226
 
227
227
  if (at === '' || !existsSync(at)) {
228
228
  checked =
229
- 'this tree has no passing gate, so nothing in it may compile. Run `thread gate` — or the ' +
230
- '`dev`, `build` and `start` scripts, which run it for you. A gate that has never run is not a ' +
229
+ 'this tree has no passing gate, so nothing in it may compile. Run `thread gate`. A gate that has never run is not a ' +
231
230
  'gate that passed, and the file you are compiling was never measured. Compiling ' +
232
231
  `\`${from}\`, it looked for a gate receipt from \`${dirname(resolve(from))}\` and from ` +
233
232
  `\`${resolve(cwd())}\` upwards${at === '' ? ', and neither reached a tree' : `, and found none at ${at}`}`
@@ -1,12 +1,13 @@
1
1
  import { dirname, join, relative } from 'node:path';
2
2
  import { argv, stderr, stdout } from 'node:process';
3
3
  import ts from 'typescript';
4
- import { existsSync, readFileSync, realpathSync, writeFileSync } from '@prestack/thread/integrations/disk';
4
+ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from '@prestack/thread/integrations/disk';
5
5
  import { exit, invoked } from '@prestack/thread/core/invoked';
6
- import { valuesIn } from '@prestack/thread/core/rows';
6
+ import { listBounds, nestedBounds, rowsIn, valuesIn, withField, withRow, withoutField, withoutRow } from '@prestack/thread/core/rows';
7
7
  import { named as packageOf } from '@prestack/thread/core/scope';
8
8
  import { findTree, inTree } from '@prestack/thread/core/tree';
9
9
  import { PERMITS_AT } from '@prestack/thread/operations/access';
10
+ import { said } from '@prestack/thread/operations/attach';
10
11
  import { ACTOR_AT, COMPILED } from '@prestack/thread/operations/capabilities';
11
12
  import { compiledAt, nameOf as compositionOf } from '../compositions.js';
12
13
  import { PRESENTATIONS } from '../door.js';
@@ -21,6 +22,11 @@ const MAP = `app/_generated/map.${'ts'}`;
21
22
  const PROVIDER = `${COMPILED}/ui/provider.tsx`;
22
23
  const GRAMMAR = `${COMPILED}/ui/islands/map.ts`;
23
24
  const MOVES = 'navigateTo';
25
+ const PAGE_ROW = 'page';
26
+ const WRITER = 'thread routes';
27
+ const PAGE_SAID = 'The page a browser gets at the address of this screen: `/` when it is the first page this app declares, and its own ' +
28
+ 'name otherwise. `thread routes` writes the page and this row from where the screen sits, and moves or deletes both ' +
29
+ 'when that changes.';
24
30
  function served(at) {
25
31
  try {
26
32
  const held = JSON.parse(readFileSync(join(at, 'package.json'), 'utf8'));
@@ -316,6 +322,40 @@ function page(name, at, route) {
316
322
  '',
317
323
  ].join('\n');
318
324
  }
325
+ const pageShaped = (path) => path === pageAt(`/${path.split('/').slice(1, -1).join('/')}`);
326
+ function unpaged(one) {
327
+ if (one.where === undefined) {
328
+ return (`\`${one.id}\` is presented as a ${one.shown}, so it has no page, and its row still derives ${String(one.was)}. ` +
329
+ `Run \`${WRITER}\`, which takes that row out and deletes the page.`);
330
+ }
331
+ return (`\`${one.id}\` answers ${String(one.route)}, so its page is ${one.where}, and its row ` +
332
+ `${one.was === undefined ? 'derives no page' : `says ${one.was}`}. Run \`${WRITER}\`, which writes that row from ` +
333
+ 'where the screen now sits and puts the page there.');
334
+ }
335
+ function repaged(text, moved) {
336
+ let next = text;
337
+ for (const one of moved) {
338
+ const list = listBounds(next, 'declares');
339
+ const inside = list === undefined ? undefined : nestedBounds(next, list, one.id, 'derives');
340
+ if (inside !== undefined)
341
+ next = withoutRow(next, inside, PAGE_ROW);
342
+ const rows = listBounds(next, 'declares');
343
+ if (rows === undefined)
344
+ return undefined;
345
+ const within = nestedBounds(next, rows, one.id, 'derives');
346
+ if (one.where === undefined) {
347
+ if (within !== undefined && rowsIn(next, within).length === 0)
348
+ next = withoutField(next, rows, one.id, 'derives');
349
+ continue;
350
+ }
351
+ const row = said({ id: PAGE_ROW, at: one.where, describes: PAGE_SAID, version: 1, by: WRITER, runs: `${WRITER} --check` }, ' ');
352
+ const put = within === undefined ? withField(next, rows, one.id, 'derives', `[\n${row}\n ]`) : withRow(next, within, row);
353
+ if (put === undefined)
354
+ return undefined;
355
+ next = put;
356
+ }
357
+ return next;
358
+ }
319
359
  function gate(typed) {
320
360
  const said = typed ? ' as { of?: Record<string, string> }' : '';
321
361
  return [
@@ -532,6 +572,7 @@ async function run() {
532
572
  const shownOf = (id) => presentedIn(texts.get(id) ?? '');
533
573
  const entry = entryAmong(rows, shownOf);
534
574
  const made = [];
575
+ const moved = [];
535
576
  const routes = new Map();
536
577
  const screens = [];
537
578
  const reaches = reachable(at);
@@ -541,7 +582,7 @@ async function run() {
541
582
  if (typed !== undefined) {
542
583
  stderr.write(`routes: ${row.at} states \`route: '${typed}'\`, and a route is never typed: the first page an app declares ` +
543
584
  'answers `/`, every other page answers its own name, and a sheet has no route. Take `route:` out of the ' +
544
- 'file — `thread create screen edit` rewrites it without one when it is given the body.\n');
585
+ `file with \`thread edit ${row.at} --find <text> --replace <text>\`, which changes that one stretch of it.\n`);
545
586
  return 1;
546
587
  }
547
588
  const name = nameOf(row.at);
@@ -576,18 +617,19 @@ async function run() {
576
617
  entry: row.id === entry,
577
618
  shows: [...shows],
578
619
  });
579
- if (route === undefined)
620
+ const where = route === undefined ? undefined : pageAt(route);
621
+ const was = (row.derives ?? []).find((one) => one.id === PAGE_ROW)?.at;
622
+ if (was !== where)
623
+ moved.push({ id: row.id, shown: shownAs, route, was, where });
624
+ if (route === undefined || where === undefined)
580
625
  continue;
581
- const where = pageAt(route);
582
- const stated = (row.derives ?? []).find((one) => one.id === 'page')?.at;
583
- if (stated !== where) {
584
- stderr.write(`routes: \`${row.id}\` answers ${route}, so its page is ${where}, and its row ` +
585
- `${stated === undefined ? 'derives no page' : `says ${stated}`}. Run \`thread create screen edit\` on it — ` +
586
- 'the row is rewritten from where the screen now lives.\n');
587
- return 1;
588
- }
589
626
  made.push({ path: where, text: page(name, row.at, route) });
590
627
  }
628
+ if (moved.length > 0 && argv.includes('--check')) {
629
+ for (const one of moved)
630
+ stderr.write(`routes: ${unpaged(one)}\n`);
631
+ return 1;
632
+ }
591
633
  const { links, wrong } = linksOf(at, rows, compositions, routes, wordingFirst(keyProps(at)));
592
634
  if (wrong.length > 0) {
593
635
  for (const one of wrong)
@@ -611,6 +653,30 @@ async function run() {
611
653
  });
612
654
  const paged = map.screens.filter((one) => one.path !== undefined).length;
613
655
  const counted = `${String(map.screens.length)} screen(s), ${String(paged)} route(s), ${String(map.links.length)} link(s)`;
656
+ if (moved.length > 0) {
657
+ const declaredIn = relative(at, screensAt);
658
+ const rewritten = repaged(readFileSync(screensAt, 'utf8'), moved);
659
+ if (rewritten === undefined) {
660
+ stderr.write(`routes: ${declaredIn} could not be given the page row of each screen, so nothing was written. ` +
661
+ `\`thread read ${declaredIn}\` prints what it states now.\n`);
662
+ return 1;
663
+ }
664
+ writeFileSync(screensAt, rewritten, 'utf8');
665
+ const answered = new Set(made.map((one) => one.path));
666
+ const home = dirname(join(at, pageAt('/')));
667
+ for (const one of moved) {
668
+ stdout.write(one.where === undefined
669
+ ? `routes: \`${one.id}\` has no route, so its row no longer derives ${String(one.was)}\n`
670
+ : `routes: \`${one.id}\` answers ${String(one.route)}, so its row now derives ${one.where}\n`);
671
+ if (one.was === undefined || answered.has(one.was) || !pageShaped(one.was) || !existsSync(join(at, one.was)))
672
+ continue;
673
+ rmSync(join(at, one.was));
674
+ const folder = dirname(join(at, one.was));
675
+ if (folder !== home && readdirSync(folder).length === 0)
676
+ rmSync(folder, { recursive: true, force: true });
677
+ stdout.write(`routes: deleted ${one.was}, because no screen answers there any more\n`);
678
+ }
679
+ }
614
680
  const refused = landed(at, made);
615
681
  if (refused !== undefined)
616
682
  return refused;
@@ -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
 
@@ -423,6 +423,7 @@ async function node(root, file, name) {
423
423
  }
424
424
  return one;
425
425
  }
426
+ const STYLESHEET = 'app/_generated/tokens.css';
426
427
  const DECLARED = 'app/_generated/declared.ts';
427
428
  async function run() {
428
429
  const own = argv.includes('--own');
@@ -462,7 +463,7 @@ async function run() {
462
463
  '] as const',
463
464
  '',
464
465
  ].join('\n');
465
- const at = join(root, 'app/_generated/tokens.css');
466
+ const at = join(root, STYLESHEET);
466
467
  const made = sheet(tokens, guide, materials, variants, modes, themes, screens, compositions, elements, fonts, icons, animations);
467
468
  if (own) {
468
469
  stdout.write(`styles: this framework's own declarations build a stylesheet — ${String(guide.picks.length)} values, ` +
@@ -472,25 +473,29 @@ async function run() {
472
473
  return 0;
473
474
  }
474
475
  if (argv.includes('--check')) {
475
- let current = '';
476
- try {
477
- current = readFileSync(at, 'utf8');
478
- }
479
- catch {
480
- current = '';
481
- }
482
- if (current === made) {
483
- stdout.write('styles: app/_generated/tokens.css is the bytes the declarations produce\n');
476
+ const drifted = [
477
+ { named: STYLESHEET, text: made },
478
+ { named: DECLARED, text: wearingSource },
479
+ ].filter((one) => {
480
+ try {
481
+ return readFileSync(join(root, one.named), 'utf8') !== one.text;
482
+ }
483
+ catch {
484
+ return true;
485
+ }
486
+ });
487
+ if (drifted.length === 0) {
488
+ stdout.write(`styles: ${STYLESHEET} and ${DECLARED} are the bytes the declarations produce\n`);
484
489
  return 0;
485
490
  }
486
- stderr.write('styles: app/_generated/tokens.css is not what the declarations produce. A derived file that ' +
487
- 'differs from its generator is a hand edit wearing a generated name, and every reader downstream ' +
488
- 'believes the wrong one. Run `thread styles` and commit what it writes.\n');
491
+ stderr.write(`styles: ${drifted.map((one) => one.named).join(' and ')} ${drifted.length === 1 ? 'is' : 'are'} not what the declarations produce. ` +
492
+ 'A derived file that differs from its generator is a hand edit wearing a generated name, and every reader ' +
493
+ 'downstream believes the wrong one. Run `thread styles` and commit what it writes.\n');
489
494
  return 1;
490
495
  }
491
496
  writeFileSync(at, made, 'utf8');
492
497
  writeFileSync(join(root, DECLARED), wearingSource, 'utf8');
493
- stdout.write(`thread: wrote app/_generated/tokens.css — ${String(Object.keys(tokens).length)} tokens, ` +
498
+ stdout.write(`thread: wrote ${STYLESHEET} and ${DECLARED} — ${String(Object.keys(tokens).length)} tokens, ` +
494
499
  `${String(guide.picks.length)} values, ${String(Object.keys(materials).length)} materials, ` +
495
500
  `${String(Object.keys(variants).length)} variants, ${String(inside(fonts).length - 1)} font roles, ` +
496
501
  `${String(Object.keys(animations).length)} animations, ${String(modes.length)} modes, ` +
@@ -563,6 +563,8 @@ async function node(root: string, file: string, name: string): Promise<Node> {
563
563
  return one as Node
564
564
  }
565
565
 
566
+ const STYLESHEET = 'app/_generated/tokens.css'
567
+
566
568
  const DECLARED = 'app/_generated/declared.ts'
567
569
 
568
570
  async function run(): Promise<number> {
@@ -623,7 +625,7 @@ async function run(): Promise<number> {
623
625
  '',
624
626
  ].join('\n')
625
627
 
626
- const at = join(root, 'app/_generated/tokens.css')
628
+ const at = join(root, STYLESHEET)
627
629
  const made = sheet(tokens, guide, materials, variants, modes, themes, screens, compositions, elements, fonts, icons, animations)
628
630
 
629
631
  if (own) {
@@ -637,20 +639,24 @@ async function run(): Promise<number> {
637
639
  }
638
640
 
639
641
  if (argv.includes('--check')) {
640
- let current = ''
641
- try {
642
- current = readFileSync(at, 'utf8')
643
- } catch {
644
- current = ''
645
- }
646
- if (current === made) {
647
- stdout.write('styles: app/_generated/tokens.css is the bytes the declarations produce\n')
642
+ const drifted = [
643
+ { named: STYLESHEET, text: made },
644
+ { named: DECLARED, text: wearingSource },
645
+ ].filter((one) => {
646
+ try {
647
+ return readFileSync(join(root, one.named), 'utf8') !== one.text
648
+ } catch {
649
+ return true
650
+ }
651
+ })
652
+ if (drifted.length === 0) {
653
+ stdout.write(`styles: ${STYLESHEET} and ${DECLARED} are the bytes the declarations produce\n`)
648
654
  return 0
649
655
  }
650
656
  stderr.write(
651
- 'styles: app/_generated/tokens.css is not what the declarations produce. A derived file that ' +
652
- 'differs from its generator is a hand edit wearing a generated name, and every reader downstream ' +
653
- 'believes the wrong one. Run `thread styles` and commit what it writes.\n',
657
+ `styles: ${drifted.map((one) => one.named).join(' and ')} ${drifted.length === 1 ? 'is' : 'are'} not what the declarations produce. ` +
658
+ 'A derived file that differs from its generator is a hand edit wearing a generated name, and every reader ' +
659
+ 'downstream believes the wrong one. Run `thread styles` and commit what it writes.\n',
654
660
  )
655
661
  return 1
656
662
  }
@@ -659,7 +665,7 @@ async function run(): Promise<number> {
659
665
  writeFileSync(join(root, DECLARED), wearingSource, 'utf8')
660
666
 
661
667
  stdout.write(
662
- `thread: wrote app/_generated/tokens.css — ${String(Object.keys(tokens).length)} tokens, ` +
668
+ `thread: wrote ${STYLESHEET} and ${DECLARED} — ${String(Object.keys(tokens).length)} tokens, ` +
663
669
  `${String(guide.picks.length)} values, ${String(Object.keys(materials).length)} materials, ` +
664
670
  `${String(Object.keys(variants).length)} variants, ${String(inside(fonts).length - 1)} font roles, ` +
665
671
  `${String(Object.keys(animations).length)} animations, ${String(modes.length)} modes, ` +
@@ -4,7 +4,7 @@ import { argv, stdout } from 'node:process';
4
4
  import { exit, invoked } from '@prestack/thread/core/invoked';
5
5
  import { valuesIn } from '@prestack/thread/core/rows';
6
6
  import { findTree, inTree } from '@prestack/thread/core/tree';
7
- import { HOME, settled, undeclared } from './compositions.js';
7
+ import { HOME, SCREENED_BY, settled, undeclared } from './compositions.js';
8
8
  import { PRESENTATIONS } from './door.js';
9
9
  const here = 'thread/ui/screens.ts';
10
10
  const said = [
@@ -100,7 +100,7 @@ function run() {
100
100
  stdout.write(`thread: seeded ${one.path}\n`);
101
101
  }
102
102
  }
103
- const loose = [...undeclared(tree, rows), ...settled(tree.at, rows, checking, 'thread screens', new Set())];
103
+ const loose = [...undeclared(tree, rows), ...settled(tree.at, rows, checking, SCREENED_BY, new Set())];
104
104
  for (const one of loose)
105
105
  stdout.write(`thread: ${one}\n`);
106
106
  stdout.write(`thread: ${String(rows.length)} screen(s) declared, ${String(made.length)} seeded, ` +