@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.
- package/CHANGELOG.md +364 -0
- package/dist/ui/attach.js +1 -1
- package/dist/ui/attach.ts +1 -1
- package/dist/ui/compositions.d.ts +6 -0
- package/dist/ui/compositions.js +69 -16
- package/dist/ui/compositions.ts +75 -17
- package/dist/ui/generators/dev.js +15 -11
- package/dist/ui/generators/dev.ts +13 -11
- package/dist/ui/generators/routes.js +78 -12
- package/dist/ui/generators/routes.ts +95 -14
- package/dist/ui/variant.d.ts +1 -3
- package/dist/ui/variant.ts +1 -4
- package/package.json +2 -2
package/dist/ui/compositions.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from '@prestack/thread/integrations/disk';
|
|
2
2
|
import { basename, dirname, join, relative } from 'node:path';
|
|
3
3
|
import { argv, stdout } from 'node:process';
|
|
4
|
+
import ts from 'typescript';
|
|
4
5
|
import { SHAPED } from '@prestack/thread/core/init';
|
|
5
6
|
import { exit, invoked } from '@prestack/thread/core/invoked';
|
|
6
7
|
import { quoted, valuesIn } from '@prestack/thread/core/rows';
|
|
7
|
-
import { named as packageOf } from '@prestack/thread/core/scope';
|
|
8
|
+
import { THING, named as packageOf } from '@prestack/thread/core/scope';
|
|
8
9
|
import { findTree, inTree } from '@prestack/thread/core/tree';
|
|
9
10
|
import { COMPILED } from '@prestack/thread/operations/capabilities';
|
|
10
11
|
import { compiled } from './generators/react.js';
|
|
@@ -156,7 +157,21 @@ export function settled(root, rows, checking, by, kept) {
|
|
|
156
157
|
return said;
|
|
157
158
|
}
|
|
158
159
|
const ORG = packageOf('ui').split('/')[0] ?? '';
|
|
159
|
-
const
|
|
160
|
+
const CORE = packageOf('core');
|
|
161
|
+
const FROM_CORE = `${COMPILED}/${THING}/`;
|
|
162
|
+
export function reachedIn(at, text) {
|
|
163
|
+
const file = ts.createSourceFile(at, text, ts.ScriptTarget.Latest, false, at.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
164
|
+
const found = [];
|
|
165
|
+
for (const one of file.statements) {
|
|
166
|
+
if (!ts.isImportDeclaration(one) && !ts.isExportDeclaration(one))
|
|
167
|
+
continue;
|
|
168
|
+
const said = one.moduleSpecifier;
|
|
169
|
+
if (said === undefined || !ts.isStringLiteral(said))
|
|
170
|
+
continue;
|
|
171
|
+
found.push({ from: said.text, typed: ts.isImportDeclaration(one) ? one.importClause?.isTypeOnly === true : one.isTypeOnly });
|
|
172
|
+
}
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
160
175
|
export const RUNTIME = ['wrappers.tsx', 'drag.ts', 'provider.tsx', 'islands/map.ts'];
|
|
161
176
|
const NODE = /^node:/;
|
|
162
177
|
function installs(root) {
|
|
@@ -182,6 +197,31 @@ export function emitting(root, entries, checking, kept) {
|
|
|
182
197
|
];
|
|
183
198
|
}
|
|
184
199
|
const seen = new Set();
|
|
200
|
+
const core = join(root, 'node_modules', CORE, 'dist');
|
|
201
|
+
const copied = (path) => {
|
|
202
|
+
const file = join(core, `${path}.ts`);
|
|
203
|
+
if (!existsSync(file))
|
|
204
|
+
return undefined;
|
|
205
|
+
const text = readFileSync(file, 'utf8');
|
|
206
|
+
if (reachedIn(path, text).length > 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
const where = `${FROM_CORE}${path}.ts`;
|
|
209
|
+
if (seen.has(where))
|
|
210
|
+
return where;
|
|
211
|
+
seen.add(where);
|
|
212
|
+
kept.add(where);
|
|
213
|
+
const to = join(root, where);
|
|
214
|
+
if (checking) {
|
|
215
|
+
if (!existsSync(to) || readFileSync(to, 'utf8') !== text) {
|
|
216
|
+
said.push(`${where} is not what this app emits from ${CORE}. An emitted file that differs from its source is a hand ` +
|
|
217
|
+
'edit wearing a generated name. Run `thread compositions` and commit what it writes.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
writeFileSync(to, text, 'utf8');
|
|
222
|
+
}
|
|
223
|
+
return where;
|
|
224
|
+
};
|
|
185
225
|
const walk = (at) => {
|
|
186
226
|
if (seen.has(at))
|
|
187
227
|
return;
|
|
@@ -194,19 +234,9 @@ export function emitting(root, entries, checking, kept) {
|
|
|
194
234
|
}
|
|
195
235
|
const text = readFileSync(file, 'utf8');
|
|
196
236
|
const where = `${COMPILED}/ui/${at}`;
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (!existsSync(to) || readFileSync(to, 'utf8') !== text) {
|
|
201
|
-
said.push(`${where} is not what this app emits from ${packageOf('ui')}. An emitted file that differs from ` +
|
|
202
|
-
'its source is a hand edit wearing a generated name. Run `thread compositions` and commit what it writes.');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
writeFileSync(to, text, 'utf8');
|
|
207
|
-
}
|
|
208
|
-
for (const one of text.matchAll(REACHED)) {
|
|
209
|
-
const named = one[2] ?? '';
|
|
237
|
+
let shipped = text;
|
|
238
|
+
for (const one of reachedIn(at, text)) {
|
|
239
|
+
const named = one.from;
|
|
210
240
|
if (named.startsWith('.')) {
|
|
211
241
|
const next = slashed(join(dirname(at), named));
|
|
212
242
|
if (next.startsWith('..')) {
|
|
@@ -216,7 +246,7 @@ export function emitting(root, entries, checking, kept) {
|
|
|
216
246
|
walk(next);
|
|
217
247
|
continue;
|
|
218
248
|
}
|
|
219
|
-
if (one
|
|
249
|
+
if (one.typed)
|
|
220
250
|
continue;
|
|
221
251
|
if (!named.startsWith(`${ORG}/`)) {
|
|
222
252
|
const held = packaging(named);
|
|
@@ -227,9 +257,27 @@ export function emitting(root, entries, checking, kept) {
|
|
|
227
257
|
`thread attach ${packageOf('ui')} runs.`);
|
|
228
258
|
continue;
|
|
229
259
|
}
|
|
260
|
+
const copy = named.startsWith(`${CORE}/`) ? copied(named.slice(CORE.length + 1)) : undefined;
|
|
261
|
+
if (copy !== undefined) {
|
|
262
|
+
const near = slashed(relative(dirname(where), copy));
|
|
263
|
+
const pointed = near.startsWith('.') ? near : `./${near}`;
|
|
264
|
+
shipped = shipped.split(`'${named}'`).join(`'${pointed}'`).split(`"${named}"`).join(`"${pointed}"`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
230
267
|
said.push(`${at} reaches \`${named}\`, so emitting it would leave this app importing the framework while it runs. ` +
|
|
231
268
|
'It reaches what is emitted beside it, or nothing of this framework at all.');
|
|
232
269
|
}
|
|
270
|
+
kept.add(where);
|
|
271
|
+
const to = join(root, where);
|
|
272
|
+
if (checking) {
|
|
273
|
+
if (!existsSync(to) || readFileSync(to, 'utf8') !== shipped) {
|
|
274
|
+
said.push(`${where} is not what this app emits from ${packageOf('ui')}. An emitted file that differs from ` +
|
|
275
|
+
'its source is a hand edit wearing a generated name. Run `thread compositions` and commit what it writes.');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
writeFileSync(to, shipped, 'utf8');
|
|
280
|
+
}
|
|
233
281
|
};
|
|
234
282
|
for (const one of entries)
|
|
235
283
|
walk(one);
|
|
@@ -266,6 +314,11 @@ export const SCREENED_BY = 'thread screens';
|
|
|
266
314
|
const SHIPS = `${COMPILED}/ui/`;
|
|
267
315
|
const idFor = (at) => `compiled-${at.slice(COMPILED.length + 1).replace(/\.[^./]+$/, '').split('/').join('-')}`;
|
|
268
316
|
function meaning(at, by) {
|
|
317
|
+
if (at.startsWith(FROM_CORE)) {
|
|
318
|
+
return ('`' + at.slice(FROM_CORE.length) + '` from ' + CORE + ', copied into this app beside the interface that imports it. ' +
|
|
319
|
+
'It imports nothing itself, so a page runs it with nothing of the framework installed. `' + by + '` writes it, so an ' +
|
|
320
|
+
'edit made here is refused.');
|
|
321
|
+
}
|
|
269
322
|
if (at.startsWith(SHIPS)) {
|
|
270
323
|
return ('`' + at.slice(SHIPS.length) + '` from ' + packageOf('ui') + ', copied into this app so a page runs it with nothing of ' +
|
|
271
324
|
'the framework installed. `' + by + '` writes it, so an edit made here is refused.');
|
package/dist/ui/compositions.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from '@prestack/thread/integrations/disk'
|
|
2
2
|
import { basename, dirname, join, relative } from 'node:path'
|
|
3
3
|
import { argv, stdout } from 'node:process'
|
|
4
|
+
import ts from 'typescript'
|
|
4
5
|
|
|
5
6
|
import { SHAPED } from '@prestack/thread/core/init'
|
|
6
7
|
import { exit, invoked } from '@prestack/thread/core/invoked'
|
|
7
8
|
import { quoted, valuesIn } from '@prestack/thread/core/rows'
|
|
8
9
|
import type { Threaded } from '@prestack/thread/core/thread'
|
|
9
|
-
import { named as packageOf } from '@prestack/thread/core/scope'
|
|
10
|
+
import { THING, named as packageOf } from '@prestack/thread/core/scope'
|
|
10
11
|
import type { Tree } from '@prestack/thread/core/tree'
|
|
11
12
|
import { findTree, inTree } from '@prestack/thread/core/tree'
|
|
12
13
|
import { COMPILED } from '@prestack/thread/operations/capabilities'
|
|
@@ -208,7 +209,26 @@ export function settled(
|
|
|
208
209
|
|
|
209
210
|
const ORG = packageOf('ui').split('/')[0] ?? ''
|
|
210
211
|
|
|
211
|
-
const
|
|
212
|
+
const CORE = packageOf('core')
|
|
213
|
+
|
|
214
|
+
const FROM_CORE = `${COMPILED}/${THING}/`
|
|
215
|
+
|
|
216
|
+
interface Reached {
|
|
217
|
+
readonly from: string
|
|
218
|
+
readonly typed: boolean
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function reachedIn(at: string, text: string): readonly Reached[] {
|
|
222
|
+
const file = ts.createSourceFile(at, text, ts.ScriptTarget.Latest, false, at.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
|
|
223
|
+
const found: Reached[] = []
|
|
224
|
+
for (const one of file.statements) {
|
|
225
|
+
if (!ts.isImportDeclaration(one) && !ts.isExportDeclaration(one)) continue
|
|
226
|
+
const said = one.moduleSpecifier
|
|
227
|
+
if (said === undefined || !ts.isStringLiteral(said)) continue
|
|
228
|
+
found.push({ from: said.text, typed: ts.isImportDeclaration(one) ? one.importClause?.isTypeOnly === true : one.isTypeOnly })
|
|
229
|
+
}
|
|
230
|
+
return found
|
|
231
|
+
}
|
|
212
232
|
|
|
213
233
|
export const RUNTIME: readonly string[] = ['wrappers.tsx', 'drag.ts', 'provider.tsx', 'islands/map.ts']
|
|
214
234
|
|
|
@@ -246,6 +266,29 @@ export function emitting(
|
|
|
246
266
|
]
|
|
247
267
|
}
|
|
248
268
|
const seen = new Set<string>()
|
|
269
|
+
const core = join(root, 'node_modules', CORE, 'dist')
|
|
270
|
+
const copied = (path: string): string | undefined => {
|
|
271
|
+
const file = join(core, `${path}.ts`)
|
|
272
|
+
if (!existsSync(file)) return undefined
|
|
273
|
+
const text = readFileSync(file, 'utf8')
|
|
274
|
+
if (reachedIn(path, text).length > 0) return undefined
|
|
275
|
+
const where = `${FROM_CORE}${path}.ts`
|
|
276
|
+
if (seen.has(where)) return where
|
|
277
|
+
seen.add(where)
|
|
278
|
+
kept.add(where)
|
|
279
|
+
const to = join(root, where)
|
|
280
|
+
if (checking) {
|
|
281
|
+
if (!existsSync(to) || readFileSync(to, 'utf8') !== text) {
|
|
282
|
+
said.push(
|
|
283
|
+
`${where} is not what this app emits from ${CORE}. An emitted file that differs from its source is a hand ` +
|
|
284
|
+
'edit wearing a generated name. Run `thread compositions` and commit what it writes.',
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
writeFileSync(to, text, 'utf8')
|
|
289
|
+
}
|
|
290
|
+
return where
|
|
291
|
+
}
|
|
249
292
|
const walk = (at: string): void => {
|
|
250
293
|
if (seen.has(at)) return
|
|
251
294
|
seen.add(at)
|
|
@@ -259,20 +302,9 @@ export function emitting(
|
|
|
259
302
|
}
|
|
260
303
|
const text = readFileSync(file, 'utf8')
|
|
261
304
|
const where = `${COMPILED}/ui/${at}`
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
if (!existsSync(to) || readFileSync(to, 'utf8') !== text) {
|
|
266
|
-
said.push(
|
|
267
|
-
`${where} is not what this app emits from ${packageOf('ui')}. An emitted file that differs from ` +
|
|
268
|
-
'its source is a hand edit wearing a generated name. Run `thread compositions` and commit what it writes.',
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
writeFileSync(to, text, 'utf8')
|
|
273
|
-
}
|
|
274
|
-
for (const one of text.matchAll(REACHED)) {
|
|
275
|
-
const named = one[2] ?? ''
|
|
305
|
+
let shipped = text
|
|
306
|
+
for (const one of reachedIn(at, text)) {
|
|
307
|
+
const named = one.from
|
|
276
308
|
if (named.startsWith('.')) {
|
|
277
309
|
const next = slashed(join(dirname(at), named))
|
|
278
310
|
if (next.startsWith('..')) {
|
|
@@ -282,7 +314,7 @@ export function emitting(
|
|
|
282
314
|
walk(next)
|
|
283
315
|
continue
|
|
284
316
|
}
|
|
285
|
-
if (one
|
|
317
|
+
if (one.typed) continue
|
|
286
318
|
if (!named.startsWith(`${ORG}/`)) {
|
|
287
319
|
const held = packaging(named)
|
|
288
320
|
if (NODE.test(named) || has.has(held)) continue
|
|
@@ -293,11 +325,30 @@ export function emitting(
|
|
|
293
325
|
)
|
|
294
326
|
continue
|
|
295
327
|
}
|
|
328
|
+
const copy = named.startsWith(`${CORE}/`) ? copied(named.slice(CORE.length + 1)) : undefined
|
|
329
|
+
if (copy !== undefined) {
|
|
330
|
+
const near = slashed(relative(dirname(where), copy))
|
|
331
|
+
const pointed = near.startsWith('.') ? near : `./${near}`
|
|
332
|
+
shipped = shipped.split(`'${named}'`).join(`'${pointed}'`).split(`"${named}"`).join(`"${pointed}"`)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
296
335
|
said.push(
|
|
297
336
|
`${at} reaches \`${named}\`, so emitting it would leave this app importing the framework while it runs. ` +
|
|
298
337
|
'It reaches what is emitted beside it, or nothing of this framework at all.',
|
|
299
338
|
)
|
|
300
339
|
}
|
|
340
|
+
kept.add(where)
|
|
341
|
+
const to = join(root, where)
|
|
342
|
+
if (checking) {
|
|
343
|
+
if (!existsSync(to) || readFileSync(to, 'utf8') !== shipped) {
|
|
344
|
+
said.push(
|
|
345
|
+
`${where} is not what this app emits from ${packageOf('ui')}. An emitted file that differs from ` +
|
|
346
|
+
'its source is a hand edit wearing a generated name. Run `thread compositions` and commit what it writes.',
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
writeFileSync(to, shipped, 'utf8')
|
|
351
|
+
}
|
|
301
352
|
}
|
|
302
353
|
for (const one of entries) walk(one)
|
|
303
354
|
return said
|
|
@@ -340,6 +391,13 @@ const SHIPS = `${COMPILED}/ui/`
|
|
|
340
391
|
const idFor = (at: string): string => `compiled-${at.slice(COMPILED.length + 1).replace(/\.[^./]+$/, '').split('/').join('-')}`
|
|
341
392
|
|
|
342
393
|
function meaning(at: string, by: string): string {
|
|
394
|
+
if (at.startsWith(FROM_CORE)) {
|
|
395
|
+
return (
|
|
396
|
+
'`' + at.slice(FROM_CORE.length) + '` from ' + CORE + ', copied into this app beside the interface that imports it. ' +
|
|
397
|
+
'It imports nothing itself, so a page runs it with nothing of the framework installed. `' + by + '` writes it, so an ' +
|
|
398
|
+
'edit made here is refused.'
|
|
399
|
+
)
|
|
400
|
+
}
|
|
343
401
|
if (at.startsWith(SHIPS)) {
|
|
344
402
|
return (
|
|
345
403
|
'`' + at.slice(SHIPS.length) + '` from ' + packageOf('ui') + ', copied into this app so a page runs it with nothing of ' +
|
|
@@ -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';
|
|
5
|
-
import { existsSync } from '@prestack/thread/integrations/disk';
|
|
6
|
-
import {
|
|
3
|
+
import { argv, stdout } from 'node:process';
|
|
4
|
+
import { batched, generators, settling } from '@prestack/thread/core/chain';
|
|
5
|
+
import { existsSync, readFileSync } from '@prestack/thread/integrations/disk';
|
|
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
|
};
|
|
@@ -23,8 +23,8 @@ async function run() {
|
|
|
23
23
|
const tree = findTree();
|
|
24
24
|
const shipping = argv.includes(SHIP);
|
|
25
25
|
const entry = shipping ? SHIPPED : ENTRY;
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
26
|
+
const found = programOf(SERVER, tree.at, existsSync, (path) => readFileSync(path, 'utf8'));
|
|
27
|
+
if (found === undefined) {
|
|
28
28
|
say('thread: ' + unrunnable(SERVER) + '\n');
|
|
29
29
|
return 1;
|
|
30
30
|
}
|
|
@@ -38,16 +38,20 @@ 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);
|
|
48
51
|
if (!shipping)
|
|
49
52
|
releases();
|
|
50
|
-
const
|
|
53
|
+
const [program = '', ...lead] = found;
|
|
54
|
+
const held = starts(program, [...lead, entry, ...extra], { at: tree.at, shows: true });
|
|
51
55
|
if (!held.there) {
|
|
52
56
|
say('thread: could not start ' + SERVER + ' ' + entry + ': ' + held.why + '\n');
|
|
53
57
|
return 1;
|
|
@@ -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'
|
|
6
|
-
import { existsSync } from '@prestack/thread/integrations/disk'
|
|
7
|
-
import {
|
|
5
|
+
import { batched, generators, settling } from '@prestack/thread/core/chain'
|
|
6
|
+
import { existsSync, readFileSync } from '@prestack/thread/integrations/disk'
|
|
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)
|
|
@@ -35,8 +35,8 @@ async function run(): Promise<number> {
|
|
|
35
35
|
const shipping = argv.includes(SHIP)
|
|
36
36
|
const entry = shipping ? SHIPPED : ENTRY
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
38
|
+
const found = programOf(SERVER, tree.at, existsSync, (path) => readFileSync(path, 'utf8'))
|
|
39
|
+
if (found === undefined) {
|
|
40
40
|
say('thread: ' + unrunnable(SERVER) + '\n')
|
|
41
41
|
return 1
|
|
42
42
|
}
|
|
@@ -53,16 +53,18 @@ 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')
|
|
63
64
|
const extra = argv.slice(2).filter((one) => one !== SHIP)
|
|
64
65
|
if (!shipping) releases()
|
|
65
|
-
const
|
|
66
|
+
const [program = '', ...lead] = found
|
|
67
|
+
const held = starts(program, [...lead, entry, ...extra], { at: tree.at, shows: true })
|
|
66
68
|
if (!held.there) {
|
|
67
69
|
say('thread: could not start ' + SERVER + ' ' + entry + ': ' + held.why + '\n')
|
|
68
70
|
return 1
|
|
@@ -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;
|