prolog-notebook 0.6.0 → 0.6.2
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 +58 -0
- package/bin/prolog-notebook.mjs +18 -5
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/build.js +91 -7
- package/src/notebook.css +37 -3
- package/src/notebook.js +14 -1
- package/src/render.js +21 -3
- package/src/serve.js +16 -3
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2] — 2026-08-30
|
|
4
|
+
|
|
5
|
+
From a field report: a built chapter opened in Chrome and Safari showed no controls and a Run
|
|
6
|
+
button that did nothing, while the same directory worked in VS Code's browser.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **A page that could not run looked exactly like one that could.** Opening `index.html` from
|
|
11
|
+
disk means `file://`, where browsers refuse ES modules, so none of the runtime loads. That
|
|
12
|
+
part is the browser doing its job; the page then lied about it twice.
|
|
13
|
+
|
|
14
|
+
The warning that exists for this sat **after the whole chapter** — line 540 of 551 — several
|
|
15
|
+
screens below the fold, which is the one place it could not do its job. It is now first in
|
|
16
|
+
the document and a fixed banner, revealed by a CSS `animation-delay` rather than a timer,
|
|
17
|
+
because script is precisely what is broken when it matters. `mount()` removes it long before
|
|
18
|
+
the delay elapses, so a page that boots never shows it and nothing jolts.
|
|
19
|
+
|
|
20
|
+
And **Run and Consult shipped enabled**, so the two controls a reader reaches for first were
|
|
21
|
+
full colour and inert — a screenshot of the failure was indistinguishable from success. They
|
|
22
|
+
now ship disabled and `mount()` enables them, which makes "this button works" and "something
|
|
23
|
+
is here to work it" one fact rather than two that can disagree. It also closes a real race on
|
|
24
|
+
a slow page.
|
|
25
|
+
|
|
26
|
+
- **Every prediction box had the same id.** The ordinal is counted correctly and was dropped on
|
|
27
|
+
the last hop into `renderPredict`, which took its default every time. The test asserted the
|
|
28
|
+
shape of the id, which three identical ones satisfy; it now asserts uniqueness.
|
|
29
|
+
|
|
30
|
+
- **The version picker had neither an id nor a name.** It only exists on a page that boots,
|
|
31
|
+
which is why the earlier console sweep never saw it. A served chapter's console is now empty.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `build` says that opening the directory from disk will not run, rather than mentioning HTTP
|
|
36
|
+
in passing. It is the point at which somebody is about to double-click the thing.
|
|
37
|
+
|
|
38
|
+
## [0.6.1] — 2026-08-30
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`view` served the notebook as it was when the server started.** It built the page once and
|
|
43
|
+
handed the map to the server, which held it for the life of the process — so an author who
|
|
44
|
+
edited their chapter and reloaded was shown the version the server had started with. A reload
|
|
45
|
+
is the gesture for *show me what I just did*, and answering it with the old page teaches an
|
|
46
|
+
author to doubt their own edit rather than the tool.
|
|
47
|
+
|
|
48
|
+
The request is now what reads the file, so there is no window in which the page and the file
|
|
49
|
+
can disagree, and nothing that can have missed a change. Compared by bytes rather than mtime:
|
|
50
|
+
an editor writing through a rename, a `git checkout` and two saves inside one millisecond are
|
|
51
|
+
all changes an mtime reports unreliably, and reparsing a chapter is milliseconds on a file the
|
|
52
|
+
author has open anyway. An unchanged file is read and not rebuilt.
|
|
53
|
+
|
|
54
|
+
A chapter that stops parsing keeps the last version that did, with the parser's own message
|
|
55
|
+
across the top of the page and once on stderr — not once per asset. Silently serving the
|
|
56
|
+
previous version would be the same bug wearing the fix's clothes, and blanking the page would
|
|
57
|
+
throw a chapter away over a half-typed fence. Fixing the file restores it with no restart.
|
|
58
|
+
|
|
59
|
+
`build` is unchanged: it takes the first answer and writes it.
|
|
60
|
+
|
|
3
61
|
## [0.6.0] — 2026-08-30
|
|
4
62
|
|
|
5
63
|
Taking the answers back out — at the terminal, and on the page. Plus the first documentation
|
package/bin/prolog-notebook.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { updateNotice } from '../src/update.js';
|
|
|
14
14
|
import { confirm, describeInstall, globalRoot, install, relaunch, upgradePlan } from '../src/upgrade.js';
|
|
15
15
|
import { clearedSource, exportSource } from '../src/export.js';
|
|
16
16
|
import { runNotebook, DEFAULT_LIMIT } from '../src/run.js';
|
|
17
|
-
import {
|
|
17
|
+
import { livePages } from '../src/build.js';
|
|
18
18
|
import { openInBrowser, serve } from '../src/serve.js';
|
|
19
19
|
|
|
20
20
|
// The engine is imported WHERE IT IS USED, never at the top. src/node.js pulls in
|
|
@@ -391,10 +391,17 @@ async function page(command, args) {
|
|
|
391
391
|
if (jump !== null) return jump;
|
|
392
392
|
|
|
393
393
|
const file = files[0];
|
|
394
|
+
// ASKED AGAIN ON EVERY REQUEST, and built again only when the bytes have moved
|
|
395
|
+
// (869erpuhk). `build` takes the first answer and writes it; `view` keeps the
|
|
396
|
+
// producer, so a reload shows the chapter as it is now rather than as it was
|
|
397
|
+
// when the server started.
|
|
398
|
+
const pages = livePages(() => readFileSync(file, 'utf8'), {
|
|
399
|
+
filename: basename(file),
|
|
400
|
+
onError: (e) => process.stderr.write(`${file}: ${e.message}\n`),
|
|
401
|
+
});
|
|
394
402
|
let built;
|
|
395
403
|
try {
|
|
396
|
-
|
|
397
|
-
built = buildFiles(parse(source), source, { filename: basename(file) });
|
|
404
|
+
built = pages();
|
|
398
405
|
} catch (e) {
|
|
399
406
|
process.stderr.write(`${file}: ${e.message}\n`);
|
|
400
407
|
return 1;
|
|
@@ -409,11 +416,17 @@ async function page(command, args) {
|
|
|
409
416
|
else copyFileSync(entry.copy, target);
|
|
410
417
|
}
|
|
411
418
|
process.stderr.write(`${out}: ${built.size} files\n`);
|
|
412
|
-
|
|
419
|
+
// SAID HERE BECAUSE THIS IS WHERE IT IS ACTED ON. The obvious next move is to
|
|
420
|
+
// double-click index.html, and that is the one thing that cannot work:
|
|
421
|
+
// browsers refuse ES modules over file:// and the engine cannot be fetched
|
|
422
|
+
// there either (869erqq1u). The page says so too, but by then somebody is
|
|
423
|
+
// already looking at a chapter whose buttons do nothing.
|
|
424
|
+
process.stderr.write(`Host ${out} over HTTP — opening ${join(out, 'index.html')} from disk`
|
|
425
|
+
+ ' will not run.\n');
|
|
413
426
|
return 0;
|
|
414
427
|
}
|
|
415
428
|
|
|
416
|
-
const server = await serve(
|
|
429
|
+
const server = await serve(pages, { port: options.port });
|
|
417
430
|
// THE URL IS THIS COMMAND'S OUTPUT. `view` writes no notebook and no data to
|
|
418
431
|
// stdout, so there is nothing for it to corrupt — and a URL on stderr is a URL
|
|
419
432
|
// a wrapper does not see, which is how somebody came to type localhost by hand
|
package/package.json
CHANGED
package/src/build-info.json
CHANGED
package/src/build.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// contain — generated text, or a path to copy — so that `build` can write it,
|
|
16
16
|
// `view` can serve it, and a test can read it, without any of the three
|
|
17
17
|
// disagreeing about what a page is.
|
|
18
|
+
import { parse } from './format.js';
|
|
18
19
|
import { renderNotebook } from './render.js';
|
|
19
20
|
|
|
20
21
|
/** The runtime a page needs. Copied side by side, so their relative imports hold. */
|
|
@@ -67,6 +68,82 @@ export function buildFiles(notebook, source, options = {}) {
|
|
|
67
68
|
return files;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The page, rebuilt whenever the notebook has changed underneath it (869erpuhk).
|
|
73
|
+
*
|
|
74
|
+
* `view` used to build once and serve that forever, so an author who edited their
|
|
75
|
+
* chapter and reloaded was shown the version the server had started with. A
|
|
76
|
+
* reload is the gesture for "show me what I just did"; answering it with the old
|
|
77
|
+
* page teaches an author to doubt their own edit.
|
|
78
|
+
*
|
|
79
|
+
* COMPARED BY BYTES, not by mtime. The point of a rebuild here is to be right
|
|
80
|
+
* rather than quick — an editor that writes through a rename, a `git checkout`, a
|
|
81
|
+
* clock that went backwards, two saves inside one millisecond, all of them are
|
|
82
|
+
* changes and none of them reliably move an mtime the way one would hope. Reading
|
|
83
|
+
* a chapter is microseconds and reparsing one is milliseconds, on a file the
|
|
84
|
+
* author has open anyway.
|
|
85
|
+
*
|
|
86
|
+
* A BROKEN FILE KEEPS THE LAST GOOD PAGE AND SAYS SO. Silently serving the
|
|
87
|
+
* previous version would be the same bug this exists to fix, so the page carries
|
|
88
|
+
* the parser's own message. Blanking it instead would throw away a chapter over a
|
|
89
|
+
* half-typed fence — an author saves mid-thought, and the useful thing on screen
|
|
90
|
+
* is the last version that worked, labelled.
|
|
91
|
+
*
|
|
92
|
+
* @param {() => string} read the notebook's bytes, now
|
|
93
|
+
* @param {{onError?: (e: Error) => void}} [options] and everything buildFiles takes
|
|
94
|
+
* @returns {() => Map<string, {text: string}|{copy: URL}>}
|
|
95
|
+
*/
|
|
96
|
+
export function livePages(read, { onError = () => {}, ...options } = {}) {
|
|
97
|
+
let last = null;
|
|
98
|
+
let failing = null;
|
|
99
|
+
return () => {
|
|
100
|
+
let source = null;
|
|
101
|
+
try {
|
|
102
|
+
source = read();
|
|
103
|
+
if (last && last.source === source) return last.files;
|
|
104
|
+
const files = buildFiles(parse(source), source, options);
|
|
105
|
+
last = { source, files };
|
|
106
|
+
failing = null;
|
|
107
|
+
return files;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Nothing good to fall back to: this is the first build, and the caller —
|
|
110
|
+
// the command — is the one that should report it and stop.
|
|
111
|
+
if (!last) throw e;
|
|
112
|
+
// Once per broken version, not once per request. A page fetches a dozen
|
|
113
|
+
// files, and a terminal repeating the same syntax error a dozen times is
|
|
114
|
+
// worse at communicating it than saying it once.
|
|
115
|
+
if (failing !== source) onError(e);
|
|
116
|
+
failing = source;
|
|
117
|
+
return withNotice(last.files, e.message);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The last good page, wearing the reason it is not the current one.
|
|
124
|
+
*
|
|
125
|
+
* A copy, so the good build is never mutated and recovering is simply serving it
|
|
126
|
+
* again.
|
|
127
|
+
*/
|
|
128
|
+
function withNotice(files, message) {
|
|
129
|
+
const index = files.get('index.html');
|
|
130
|
+
if (index?.text === undefined) return files;
|
|
131
|
+
const copy = new Map(files);
|
|
132
|
+
copy.set('index.html', { text: index.text.replace('<body>', `<body>\n${notice(message)}`) });
|
|
133
|
+
return copy;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inline styles, deliberately: this belongs to no chapter and must never depend
|
|
138
|
+
* on a stylesheet the broken file might itself have been changing.
|
|
139
|
+
*/
|
|
140
|
+
function notice(message) {
|
|
141
|
+
return '<div role="alert" style="position:sticky;top:0;z-index:99;padding:.7rem 1rem;'
|
|
142
|
+
+ 'background:#7a2618;color:#fff;font:500 .8rem/1.5 ui-monospace,Menlo,monospace">'
|
|
143
|
+
+ '<strong>This notebook does not currently parse.</strong> Showing the last version that'
|
|
144
|
+
+ ` did.<br>${escapeHtml(message)}</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
70
147
|
/**
|
|
71
148
|
* The chapter's own title, from its first H1 (format §2).
|
|
72
149
|
*
|
|
@@ -99,19 +176,26 @@ function page(notebook) {
|
|
|
99
176
|
</head>
|
|
100
177
|
<body>
|
|
101
178
|
<main>
|
|
102
|
-
${renderNotebook(notebook)}
|
|
103
179
|
<!--
|
|
104
|
-
|
|
105
|
-
|
|
180
|
+
FIRST IN THE DOCUMENT, AND IT COSTS A READER NOTHING (869erqq1u). mount()
|
|
181
|
+
removes this element, and the stylesheet holds it invisible for a second and a
|
|
182
|
+
half before revealing it — so a page that boots never shows it, and a page that
|
|
183
|
+
cannot boot says so where somebody staring at a dead button will see it. It
|
|
184
|
+
used to sit AFTER the chapter, several screens below the fold, which is the one
|
|
185
|
+
place it could not do its job.
|
|
186
|
+
|
|
187
|
+
The delay is CSS rather than a timer, for the obvious reason: script is exactly
|
|
188
|
+
what is broken when this matters.
|
|
106
189
|
-->
|
|
107
190
|
<div id="boot-warning">
|
|
108
191
|
<strong>This notebook is not running.</strong>
|
|
109
192
|
The page loaded but its JavaScript did not. The usual cause is opening
|
|
110
|
-
<code>index.html</code> straight from disk — browsers
|
|
111
|
-
<code>file://</code
|
|
112
|
-
<code>prolog-notebook view</code> on the notebook
|
|
113
|
-
The chapter is readable either way; only the buttons need this.
|
|
193
|
+
<code>index.html</code> straight from disk — browsers refuse ES modules over
|
|
194
|
+
<code>file://</code>, and the engine cannot be fetched there either. Serve this
|
|
195
|
+
directory over HTTP, or run <code>prolog-notebook view</code> on the notebook
|
|
196
|
+
itself. The chapter is readable either way; only the buttons need this.
|
|
114
197
|
</div>
|
|
198
|
+
${renderNotebook(notebook)}
|
|
115
199
|
</main>
|
|
116
200
|
<script type="module" src="app.js"></script>
|
|
117
201
|
</body>
|
package/src/notebook.css
CHANGED
|
@@ -502,13 +502,47 @@ button.primary:hover { background: #98380f; }
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
/* Shown until mount() runs; see notebook.js. Its presence means the page is inert. */
|
|
505
|
+
/* THE PAGE THAT CANNOT RUN HAS TO SAY SO, WHERE IT WILL BE READ (869erqq1u).
|
|
506
|
+
|
|
507
|
+
Two properties, and both are the point:
|
|
508
|
+
|
|
509
|
+
FIXED, so it takes no layout. A working page carries this element for the
|
|
510
|
+
fraction of a second before mount() removes it, and a banner in the flow would
|
|
511
|
+
push the whole chapter down and then let it snap back — a visible jolt on every
|
|
512
|
+
single load, to warn about something that is not happening.
|
|
513
|
+
|
|
514
|
+
REVEALED ON A DELAY, in CSS. mount() removes it long before the animation
|
|
515
|
+
fires, so a page that boots never shows it at all; a page that cannot boot
|
|
516
|
+
shows it a second and a half in. It cannot be a timer, because script is the
|
|
517
|
+
thing that is broken. `forwards` holds the final frame rather than snapping
|
|
518
|
+
back to invisible. */
|
|
505
519
|
#boot-warning {
|
|
506
|
-
|
|
520
|
+
position: fixed;
|
|
521
|
+
top: 0;
|
|
522
|
+
left: 0;
|
|
523
|
+
right: 0;
|
|
524
|
+
z-index: 100;
|
|
525
|
+
margin: 0;
|
|
507
526
|
padding: 1rem 1.2rem;
|
|
508
|
-
border: 2px solid var(--err);
|
|
509
|
-
border-radius: 6px;
|
|
527
|
+
border-bottom: 2px solid var(--err);
|
|
510
528
|
background: #fff5f3;
|
|
511
529
|
font-size: .95rem;
|
|
530
|
+
opacity: 0;
|
|
531
|
+
animation: boot-warning .25s ease 1.5s forwards;
|
|
532
|
+
}
|
|
533
|
+
@keyframes boot-warning { to { opacity: 1; } }
|
|
534
|
+
/* ROOM FOR IT, made on the same delay and only while it is there. A fixed banner
|
|
535
|
+
would otherwise sit over the chapter's title — the page is broken, but it
|
|
536
|
+
should not also look broken. `:has` is what keeps this honest: the moment
|
|
537
|
+
mount() removes the element the selector stops matching, so a page that boots
|
|
538
|
+
never reserves the space, and a browser too old for `:has` merely goes back to
|
|
539
|
+
the banner overlapping. */
|
|
540
|
+
body:has(#boot-warning) { animation: boot-warning-room .25s ease 1.5s forwards; }
|
|
541
|
+
@keyframes boot-warning-room { to { padding-top: 7rem; } }
|
|
542
|
+
/* Somebody who has asked not to be moved still gets the message — only the fade
|
|
543
|
+
goes. Without this the whole rule is skipped and the warning never appears. */
|
|
544
|
+
@media (prefers-reduced-motion: reduce) {
|
|
545
|
+
#boot-warning { animation-duration: 0s; }
|
|
512
546
|
}
|
|
513
547
|
#boot-warning strong { color: var(--err); }
|
|
514
548
|
#boot-warning pre {
|
package/src/notebook.js
CHANGED
|
@@ -595,7 +595,12 @@ export function offerDownload(root, options = {}) {
|
|
|
595
595
|
// their version IS the chapter, and a menu with one item is a control
|
|
596
596
|
// pretending to offer something.
|
|
597
597
|
unit.innerHTML = '<span class="state notebook-state"><span class="only"></span>'
|
|
598
|
-
|
|
598
|
+
// `name` rather than `id`, because a page may hold more than one notebook
|
|
599
|
+
// (v0.4) and two ids would collide where two names cannot — this select is in
|
|
600
|
+
// no form. Without either, the issues panel flags it, which is the same red
|
|
601
|
+
// console 869ernmxe was about; it only shows up on a page that BOOTS, so the
|
|
602
|
+
// earlier sweep never saw it.
|
|
603
|
+
+ '<span class="picker" hidden><select name="notebook-version" aria-label="which version to download">'
|
|
599
604
|
+ '<option value="mine">Your version</option>'
|
|
600
605
|
+ '<option value="published">As published</option>'
|
|
601
606
|
+ `</select>${icon('chevron')}</span></span>`
|
|
@@ -677,6 +682,10 @@ function mountProgram(cell, options, bus) {
|
|
|
677
682
|
const button = cell.querySelector('[data-act="consult"]') ?? cell.querySelector('button');
|
|
678
683
|
const resetBtn = cell.querySelector('[data-act="reset"]');
|
|
679
684
|
const status = cell.querySelector('.status');
|
|
685
|
+
// THE BUTTON IS LIVE BECAUSE THIS RAN, and not a moment before (869erqq1u).
|
|
686
|
+
// It ships disabled so that a page whose runtime never arrived looks like what
|
|
687
|
+
// it is, rather than offering a control nothing is listening to.
|
|
688
|
+
button.disabled = false;
|
|
680
689
|
|
|
681
690
|
/**
|
|
682
691
|
* The one cell whose state a re-consult does not undo (format §8).
|
|
@@ -1482,6 +1491,10 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1482
1491
|
}
|
|
1483
1492
|
|
|
1484
1493
|
decorateSaved();
|
|
1494
|
+
// Same as a program cell's Consult: enabled here, by the thing that makes it
|
|
1495
|
+
// do something, so the page cannot show a live-looking Run with no runtime
|
|
1496
|
+
// behind it (869erqq1u).
|
|
1497
|
+
runBtn.disabled = false;
|
|
1485
1498
|
refresh();
|
|
1486
1499
|
|
|
1487
1500
|
return {
|
package/src/render.js
CHANGED
|
@@ -100,7 +100,7 @@ export function renderContainer(cell, ordinal = 1) {
|
|
|
100
100
|
return `<div class="aside">\n${renderProse(joinHeadAndBody(cell))}\n</div>`;
|
|
101
101
|
|
|
102
102
|
case 'predict':
|
|
103
|
-
return renderPredict(cell);
|
|
103
|
+
return renderPredict(cell, ordinal);
|
|
104
104
|
|
|
105
105
|
case 'bullets':
|
|
106
106
|
return `<div class="bullets">\n${cell.title ? `<h2>${renderInline(cell.title)}</h2>\n` : ''}${renderProse(cell.body)}\n</div>`;
|
|
@@ -164,6 +164,24 @@ function splitReveal(body) {
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* EVERY BUTTON SHIPS DISABLED, AND THE RUNTIME TURNS IT ON (869erqq1u).
|
|
169
|
+
*
|
|
170
|
+
* A page whose JavaScript never arrives is a page where none of these can work —
|
|
171
|
+
* opened from disk, where browsers refuse ES modules; behind a CSP that blocks
|
|
172
|
+
* the script; on a connection that dropped it. What that page must not do is look
|
|
173
|
+
* exactly like a working one. Consult and Run used to ship enabled, so the two
|
|
174
|
+
* controls a reader reaches for first were full colour and did nothing, and a
|
|
175
|
+
* screenshot of the failure was indistinguishable from success.
|
|
176
|
+
*
|
|
177
|
+
* mount() enables them, which makes "this button works" and "something is here to
|
|
178
|
+
* work it" the same fact rather than two that can disagree. It also closes a real
|
|
179
|
+
* race on a slow page: they were clickable before anything was wired to them.
|
|
180
|
+
*
|
|
181
|
+
* `; next`, `all` and `stop` were already disabled for the ordinary reason — there
|
|
182
|
+
* is no query open yet — and now the whole bar tells one story.
|
|
183
|
+
*/
|
|
184
|
+
|
|
167
185
|
/**
|
|
168
186
|
* A program cell: the Prolog, and a button that loads it.
|
|
169
187
|
*
|
|
@@ -180,7 +198,7 @@ export function renderProgram(cell) {
|
|
|
180
198
|
return `<div class="cell program" data-cell="${escapeHtml(cell.id)}">
|
|
181
199
|
<div class="bar">program<span class="spacer"></span><span class="status"></span>
|
|
182
200
|
<button data-act="reset" disabled>reset</button>
|
|
183
|
-
<button class="primary" data-act="consult">Consult</button></div>
|
|
201
|
+
<button class="primary" data-act="consult" disabled>Consult</button></div>
|
|
184
202
|
<textarea id="src-${escapeHtml(cell.id)}" spellcheck="false">${escapeHtml(cell.source)}</textarea>
|
|
185
203
|
</div>`;
|
|
186
204
|
}
|
|
@@ -217,7 +235,7 @@ export function renderQuery(cell, options = {}) {
|
|
|
217
235
|
return `<div class="cell query" data-cell="${escapeHtml(cell.id)}"${hold}${auto}${wasStale}>
|
|
218
236
|
<div class="bar">query<span class="spacer"></span><span class="status"></span>
|
|
219
237
|
<button data-act="reset" disabled>reset</button>
|
|
220
|
-
<button class="primary" data-act="run">Run</button>
|
|
238
|
+
<button class="primary" data-act="run" disabled>Run</button>
|
|
221
239
|
<button data-act="next" disabled>; next</button>
|
|
222
240
|
<button data-act="all" disabled>all</button>
|
|
223
241
|
<button data-act="stop" disabled>stop</button></div>
|
package/src/serve.js
CHANGED
|
@@ -31,11 +31,24 @@ export function contentType(name) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Serve a built page.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* THE REQUEST IS WHAT READS THE FILE, when a producer is given rather than a map
|
|
35
|
+
* (869erpuhk). The first version of this held the map it was handed for the life
|
|
36
|
+
* of the process, so `view` served the notebook as it had been at start-up and a
|
|
37
|
+
* reload — the universal gesture for "show me what I just did" — confirmed the
|
|
38
|
+
* old version. An author doubts their edit before they doubt the tool.
|
|
39
|
+
*
|
|
40
|
+
* Asked per request rather than pushed by a watcher, because that is what makes
|
|
41
|
+
* the guarantee unconditional: there is no window in which the page and the file
|
|
42
|
+
* disagree, and nothing to have missed a change. A watcher (869edp5c8) can only
|
|
43
|
+
* ever save the reader a keystroke on top of this.
|
|
44
|
+
*
|
|
45
|
+
* @param {Map<string, {text: string}|{copy: URL}>|(() => Map)} pages what build
|
|
46
|
+
* produced, or something that produces it — called once per request
|
|
35
47
|
* @param {{port?: number, host?: string}} [options]
|
|
36
48
|
* @returns {Promise<{url: string, port: number, close: () => Promise<void>}>}
|
|
37
49
|
*/
|
|
38
|
-
export async function serve(
|
|
50
|
+
export async function serve(pages, { port = 8777, host = '127.0.0.1' } = {}) {
|
|
51
|
+
const files = typeof pages === 'function' ? pages : () => pages;
|
|
39
52
|
// ASK WHETHER ANYBODY IS THERE, on both stacks, before binding to one of them.
|
|
40
53
|
//
|
|
41
54
|
// An IPv6 wildcard listener — `python3 -m http.server --bind ::` — does not
|
|
@@ -49,7 +62,7 @@ export async function serve(files, { port = 8777, host = '127.0.0.1' } = {}) {
|
|
|
49
62
|
// Only GET, and only the names this process generated: the path never
|
|
50
63
|
// reaches the filesystem, so there is nothing for a `..` to escape into.
|
|
51
64
|
const name = decodeURIComponent(new URL(request.url, 'http://x').pathname).replace(/^\//, '');
|
|
52
|
-
const entry = files.get(name === '' ? 'index.html' : name);
|
|
65
|
+
const entry = files().get(name === '' ? 'index.html' : name);
|
|
53
66
|
if (request.method !== 'GET' || !entry) {
|
|
54
67
|
response.writeHead(404, { 'content-type': 'text/plain' }).end('not found\n');
|
|
55
68
|
return;
|
package/src/version.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export const NAME = 'Prolog Notebook';
|
|
11
11
|
|
|
12
12
|
/** Must equal package.json's `version` — test/run.test.mjs enforces it. */
|
|
13
|
-
export const VERSION = '0.6.
|
|
13
|
+
export const VERSION = '0.6.2';
|
|
14
14
|
|
|
15
15
|
/** The two facts a licence notice is actually made of. */
|
|
16
16
|
export const YEAR = '2026';
|