@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.
- package/CHANGELOG.md +370 -0
- package/dist/ui/attach.js +1 -1
- package/dist/ui/attach.ts +1 -1
- package/dist/ui/compositions.d.ts +1 -0
- package/dist/ui/compositions.js +22 -18
- package/dist/ui/compositions.ts +18 -15
- package/dist/ui/generators/dev.js +10 -7
- package/dist/ui/generators/dev.ts +8 -7
- package/dist/ui/generators/react.js +1 -2
- package/dist/ui/generators/react.ts +1 -2
- package/dist/ui/generators/routes.js +78 -12
- package/dist/ui/generators/routes.ts +95 -14
- package/dist/ui/generators/styles.js +19 -14
- package/dist/ui/generators/styles.ts +19 -13
- package/dist/ui/screens.js +2 -2
- package/dist/ui/screens.ts +2 -2
- package/package.json +2 -2
package/dist/ui/compositions.ts
CHANGED
|
@@ -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. `' +
|
|
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. `' +
|
|
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(
|
|
376
|
-
` runs: ${quoted(`${
|
|
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
|
|
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]
|
|
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,
|
|
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 {
|
|
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 =
|
|
42
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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 =
|
|
57
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
if (
|
|
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,
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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(
|
|
487
|
-
'differs from its generator is a hand edit wearing a generated name, and every reader
|
|
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
|
|
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,
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
652
|
-
'differs from its generator is a hand edit wearing a generated name, and every reader
|
|
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
|
|
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, ` +
|
package/dist/ui/screens.js
CHANGED
|
@@ -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,
|
|
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, ` +
|