@beltype/cli 0.1.5 → 0.1.6
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/README.md +1 -0
- package/dist/src/in-page.js +15 -0
- package/dist/src/run.js +7 -3
- package/dist/src/serve.js +59 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ curl -F index.html=@index.html -F paper.css=@paper.css -F fonts/a.woff2=@fonts/a
|
|
|
36
36
|
- `POST /print`, `multipart/form-data`. `index.html` is required; every other file keeps its form field name as its path relative to the document (`fonts/a.woff2`), plain segments with `/` separators, never absolute and never `..`. A field that is not a file is a flag, `<owner>.<name>` = value (`caster.writer=forme`, `cicero.…`); `beltype.check` is refused. A field `created` (an ISO 8601 instant) fixes the file's dates, else `SOURCE_DATE_EPOCH`, else the clock. The answer is `200 application/pdf` with `X-Beltype-Pages`, `X-Beltype-Words` and `X-Beltype-Warnings` (the count); `422` with `beltype print`'s sentence where the document is refused (a file it asked for and was not given, among them); `400` for a bad request; `413` over the upload limit; `504` over the time limit; `500` otherwise, after which the browser is restarted.
|
|
37
37
|
- `GET /health`: `{ "status": "up", "chromium": …, "prints": …, "queued": … }`, or `503` with `"status": "restarting"` while the browser starts again.
|
|
38
38
|
- `--host 127.0.0.1`, `--port 3000`, `--max-upload 50` (MB), `--timeout 60` (seconds per print), `--restart-after 100` (prints before the browser is restarted).
|
|
39
|
+
- A document whose text holds no `<script`, `<iframe`, `<object`, `<embed`, `<frame`, `<portal`, `javascript:` or `on…=` attribute is printed on one kept page, re-seated for each print as Caster's kept frame is (`document.open()` keeps the realm, so Caster's registry and the companions' modules stay warm); any other document gets a page of its own, because a script's timers, listeners and globals would outlive it on a kept `Window`. The log line of each print names the road (`road kept` or `road fresh`) and the time of every step. A print that fails or times out closes the kept page.
|
|
39
40
|
- One print runs at a time; the others wait their turn. After every start the server prints a built-in document twice under forme (the log shows both writes), so the first request is warm.
|
|
40
41
|
- It binds `127.0.0.1` unless `--host` says otherwise, and fetches nothing from a network: a document that asks for anything it was not posted is refused by name, as in `beltype print`. The built-in warm-up document ships one font, Source Sans 3 (SIL OFL), in `assets/warmup/`.
|
|
41
42
|
|
package/dist/src/in-page.js
CHANGED
|
@@ -70,3 +70,18 @@ export function chromiumSheetInPage(sheet) {
|
|
|
70
70
|
shadow.textContent = sheet.shadow;
|
|
71
71
|
document.querySelector('beltype-root')?.shadowRoot?.appendChild(shadow);
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Seat `html` as the page's document without a navigation: `document.open()` keeps the `Window`, so the realm Caster measures from, its modules and its capability registry stay (Caster's DESIGN.md S87, S89). The `adoptedStyleSheets` the last document left on the Document are emptied; `load` is awaited as a navigation's would be.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* The previous Cicero client is released first (`release()`, Cicero 0.1.6, SPEC XII.7): the `Window` and its one `FontFaceSet` outlive `document.open()`, and an unreleased client heard a face of the next document's family arrive as a late fact and ran beside the host's run, which was let go after one page (measured: the paper after a document of another family cut 1 page, not 4). A released client deletes its handle, so the next document's module scripts must load under a URL of their own, which `serve.ts` keys per re-seat.
|
|
78
|
+
*/
|
|
79
|
+
export async function reseatInPage(html) {
|
|
80
|
+
await window.__beltypeCicero?.release?.();
|
|
81
|
+
document.open();
|
|
82
|
+
document.adoptedStyleSheets = [];
|
|
83
|
+
const loaded = new Promise((resolve) => window.addEventListener('load', () => resolve(), { once: true }));
|
|
84
|
+
document.write(html);
|
|
85
|
+
document.close();
|
|
86
|
+
await loaded;
|
|
87
|
+
}
|
package/dist/src/run.js
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { ciceroEngine } from '@beltype/cicero/engine';
|
|
13
13
|
import { BuildError } from '@beltype/common/errors';
|
|
14
14
|
import { contentType } from '@beltype/common/mime';
|
|
15
|
-
import { cutInPage, printInPage, pageSizesInPage, chromiumSheetInPage } from './in-page.js';
|
|
15
|
+
import { cutInPage, printInPage, reseatInPage, pageSizesInPage, chromiumSheetInPage } from './in-page.js';
|
|
16
16
|
import { chromiumSheet } from './chromium.js';
|
|
17
17
|
import { screenshotPages } from './check.js';
|
|
18
18
|
/** `.invalid` is reserved (RFC 2606), so nothing on a network can ever answer it instead. */
|
|
@@ -93,9 +93,13 @@ export async function routeDocument(page, missing, o) {
|
|
|
93
93
|
*/
|
|
94
94
|
export async function printOnPage(page, missing, flags, o) {
|
|
95
95
|
const { progress } = o;
|
|
96
|
-
page.on('pageerror', (error) => console.warn(`page error: ${error.message}`));
|
|
97
96
|
progress?.step('load');
|
|
98
|
-
|
|
97
|
+
if (o.reseat !== undefined)
|
|
98
|
+
await page.evaluate(reseatInPage, o.reseat);
|
|
99
|
+
else {
|
|
100
|
+
page.on('pageerror', (error) => console.warn(`page error: ${error.message}`));
|
|
101
|
+
await page.goto(ORIGIN + o.docPath, { waitUntil: 'load' });
|
|
102
|
+
}
|
|
99
103
|
progress?.lap('load');
|
|
100
104
|
progress?.step('cut');
|
|
101
105
|
const cut = await page.evaluate(cutInPage, { cicero: flags.cicero, measures: o.measures === true });
|
package/dist/src/serve.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @remarks
|
|
5
5
|
* **One browser, one context, one holder page.** The context carries the companions' routes; the holder page opens the shared writer worker (Caster's `formeSharedWorkerUrl()`) and keeps it for as long as the page lives, so forme's WebAssembly stays warm across prints. A print is a new page in that context, routed to its own folder alone ({@link routeDocument}), run through {@link printOnPage} with `sharedWriter`, and closed.
|
|
6
6
|
*
|
|
7
|
+
* **One kept print page, for a document with no script of its own.** A print re-seats the kept page as Caster's kept frame does (`document.open()` keeps the `Window`, so the realm, Caster's registry and the companions' modules stay warm; *the printer runs in the realm it prints*, Caster's DESIGN.md S20, S87, S89): its routes are swapped for the print's folder, the served document is written in, and each module script the document names gets a query of its own. That page is shared only by a document whose text holds none of `<script`, `<iframe`, `<object`, `<embed`, `<frame`, `<portal`, `javascript:` or an `on…=` attribute ({@link keptRoad}); any other gets a new page, and the log line names the road. A print that fails or times out closes the kept page, and the next opens a new one.
|
|
8
|
+
*
|
|
7
9
|
* **One at a time, and kept alive.** A promise chain runs the prints in turn. The browser is closed and started again (routes, holder page, warm-up) after `restartAfter` prints and after a print that failed with a 500, and a disconnected browser is started again before the next print. The warm-up prints a built-in document twice under forme, so the first request is warm too.
|
|
8
10
|
*
|
|
9
11
|
* @packageDocumentation
|
|
@@ -24,11 +26,19 @@ const c = await import('/_beltype/caster/src/index.js');
|
|
|
24
26
|
window.sw = new SharedWorker(c.formeSharedWorkerUrl(), { type: 'module', name: 'forme' });
|
|
25
27
|
</script></body></html>`;
|
|
26
28
|
// Run from src/ or from dist/src/: the package root is the first folder up that holds `assets`.
|
|
27
|
-
const WARMUP_FONT = [1, 2].map((up) => fileURLToPath(new URL(`${'../'.repeat(up)}assets/warmup/SourceSans3-Variable.woff2`, import.meta.url))).find((f) => fs.existsSync(f));
|
|
29
|
+
export const WARMUP_FONT = [1, 2].map((up) => fileURLToPath(new URL(`${'../'.repeat(up)}assets/warmup/SourceSans3-Variable.woff2`, import.meta.url))).find((f) => fs.existsSync(f));
|
|
28
30
|
const WORDS = 'the quick brown fox jumps over a lazy dog while leaf litter decomposes along an altitudinal gradient and soil carbon settles slowly beneath the canopy of old beech and spruce'.split(' ');
|
|
29
31
|
const paragraph = (n) => Array.from({ length: 90 }, (_, i) => WORDS[(i * 7 + n * 3) % WORDS.length]).join(' ') + '.';
|
|
30
32
|
/** Plain paragraphs and one heading, in the one face the CLI ships for it (`assets/warmup/`, SIL OFL): neither Cicero nor Caster supplies a default face, and Caster refuses a document with none. */
|
|
31
|
-
const WARMUP = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Warm-up</title><style>@font-face { font-family: Warm; src: url(warm.woff2) format("woff2"); font-weight: 200 900 } @page { size: A4; margin: 20mm } body { font: 11pt/1.4 Warm }</style></head><body><h1>Warm-up</h1>${Array.from({ length: 16 }, (_, n) => `<p>${paragraph(n)}</p>`).join('')}</body></html>`;
|
|
33
|
+
export const WARMUP = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Warm-up</title><style>@font-face { font-family: Warm; src: url(warm.woff2) format("woff2"); font-weight: 200 900 } @page { size: A4; margin: 20mm } body { font: 11pt/1.4 Warm }</style></head><body><h1>Warm-up</h1>${Array.from({ length: 16 }, (_, n) => `<p>${paragraph(n)}</p>`).join('')}</body></html>`;
|
|
34
|
+
const DOC_PATH = '/index.html';
|
|
35
|
+
const lapper = (steps) => {
|
|
36
|
+
const began = {};
|
|
37
|
+
return { start: (name) => { began[name] = performance.now(); }, end: (name) => { steps[name] = performance.now() - (began[name] ?? performance.now()); } };
|
|
38
|
+
};
|
|
39
|
+
const stepsLine = ({ road, ms: steps }, printed) => `road ${road}; ${['page', 'load', 'cut', 'print', 'close'].map((n) => `${n} ${Math.round(steps[n] ?? 0)}`).join(', ')}; caster ${Object.entries(printed.caster?.ms ?? {}).map(([k, v]) => `${k} ${Math.round(Number(v))}`).join(', ')}`;
|
|
40
|
+
/** Whether a document may share the kept page: only one with no script of its own (the `Window` outlives `document.open()`, and so does a timer, a listener or a global a script left on it). The test is conservative; a false alarm costs only speed. */
|
|
41
|
+
export const keptRoad = (html) => !/<script|<iframe|<object|<embed|<frame|<portal|javascript:|\son[a-z]+\s*=/i.test(html);
|
|
32
42
|
class HttpError extends Error {
|
|
33
43
|
status;
|
|
34
44
|
constructor(status, message) { super(message); this.status = status; }
|
|
@@ -37,6 +47,8 @@ export async function serve(options) {
|
|
|
37
47
|
const log = options.log ?? ((line) => console.log(line));
|
|
38
48
|
const assets = documentAssets();
|
|
39
49
|
const missing = new Set();
|
|
50
|
+
let kept = null;
|
|
51
|
+
let reseats = 0;
|
|
40
52
|
let browser = null;
|
|
41
53
|
let context = null;
|
|
42
54
|
let version = 'unknown';
|
|
@@ -48,11 +60,25 @@ export async function serve(options) {
|
|
|
48
60
|
let tail = Promise.resolve();
|
|
49
61
|
const stopBrowser = async () => {
|
|
50
62
|
const old = browser;
|
|
51
|
-
browser = context = null;
|
|
63
|
+
browser = context = kept = null;
|
|
52
64
|
await old?.close().catch(() => { });
|
|
53
65
|
};
|
|
54
|
-
/**
|
|
55
|
-
const
|
|
66
|
+
/** The kept page, opened on a blank document at the document's own path so that relative references resolve as a print's would. */
|
|
67
|
+
const openKept = async () => {
|
|
68
|
+
const page = await context.newPage();
|
|
69
|
+
page.on('pageerror', (error) => console.warn(`page error: ${error.message}`));
|
|
70
|
+
await routeDocument(page, missing, { root: path.join(os.tmpdir(), 'beltype-serve-none'), docPath: DOC_PATH, served: servedHtml('<!doctype html><html><head></head><body></body></html>') });
|
|
71
|
+
await page.goto(ORIGIN + DOC_PATH, { waitUntil: 'load' });
|
|
72
|
+
return kept = page;
|
|
73
|
+
};
|
|
74
|
+
/** Closes `page`, and forgets it if it is the kept one: a page a failed or timed-out print used is never reused. */
|
|
75
|
+
const closePage = async (page) => {
|
|
76
|
+
if (kept === page)
|
|
77
|
+
kept = null;
|
|
78
|
+
await page.close().catch(() => { });
|
|
79
|
+
};
|
|
80
|
+
/** Run `html` and `files` through the print's road: re-seated on the kept page where the document carries no script of its own, else on a new page. The folder is gone after. */
|
|
81
|
+
const render = async (files, given, created, trace = { road: 'fresh', ms: {} }) => {
|
|
56
82
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'beltype-serve-'));
|
|
57
83
|
try {
|
|
58
84
|
for (const [name, bytes] of files) {
|
|
@@ -61,19 +87,34 @@ export async function serve(options) {
|
|
|
61
87
|
fs.writeFileSync(file, bytes);
|
|
62
88
|
}
|
|
63
89
|
const flags = { caster: given.caster, cicero: ciceroFlags.resolve(given.cicero), beltype: cliFlags.resolve(given.beltype) };
|
|
64
|
-
const
|
|
65
|
-
const served = servedHtml(
|
|
90
|
+
const author = Buffer.from(files.get('index.html')).toString('utf8');
|
|
91
|
+
const served = servedHtml(author);
|
|
92
|
+
const keep = keptRoad(author);
|
|
93
|
+
trace.road = keep ? 'kept' : 'fresh';
|
|
66
94
|
missing.clear();
|
|
67
|
-
const
|
|
95
|
+
const lapped = lapper(trace.ms);
|
|
96
|
+
lapped.start('page');
|
|
97
|
+
const page = keep ? (kept ?? await openKept()) : await context.newPage();
|
|
68
98
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
if (keep)
|
|
100
|
+
await page.unrouteAll({ behavior: 'wait' });
|
|
101
|
+
await routeDocument(page, missing, { root, docPath: DOC_PATH, served });
|
|
102
|
+
lapped.end('page');
|
|
103
|
+
const reseat = keep ? served.replace(/(<script type="module" src="[^"?]+)"/g, `$1?kept=${++reseats}"`) : undefined;
|
|
104
|
+
const printed = await printOnPage(page, missing, flags, { docPath: DOC_PATH, sharedWriter: true, progress: { step: lapped.start, lap: lapped.end }, ...(reseat !== undefined ? { reseat } : {}), ...(created !== undefined ? { created } : {}) });
|
|
105
|
+
lapped.end('print');
|
|
71
106
|
if (missing.size)
|
|
72
107
|
throw refusal(missing, null);
|
|
108
|
+
if (!keep) {
|
|
109
|
+
lapped.start('close');
|
|
110
|
+
await closePage(page);
|
|
111
|
+
lapped.end('close');
|
|
112
|
+
}
|
|
73
113
|
return printed;
|
|
74
114
|
}
|
|
75
|
-
|
|
76
|
-
await page
|
|
115
|
+
catch (error) {
|
|
116
|
+
await closePage(page);
|
|
117
|
+
throw error;
|
|
77
118
|
}
|
|
78
119
|
}
|
|
79
120
|
finally {
|
|
@@ -94,6 +135,7 @@ export async function serve(options) {
|
|
|
94
135
|
const holder = await context.newPage();
|
|
95
136
|
await holder.goto(ORIGIN + HOLDER_PATH, { waitUntil: 'load' });
|
|
96
137
|
await holder.waitForFunction(() => window.sw !== undefined);
|
|
138
|
+
await openKept();
|
|
97
139
|
const given = await parseFlagArgs(['caster.writer=forme']);
|
|
98
140
|
for (const n of [1, 2]) {
|
|
99
141
|
const printed = await render(new Map([['index.html', Buffer.from(WARMUP)], ['warm.woff2', fs.readFileSync(WARMUP_FONT)]]), given, undefined);
|
|
@@ -158,7 +200,8 @@ export async function serve(options) {
|
|
|
158
200
|
const id = prints + 1;
|
|
159
201
|
log(`beltype serve: print ${id} started`);
|
|
160
202
|
const t0 = performance.now();
|
|
161
|
-
const
|
|
203
|
+
const trace = { road: 'fresh', ms: {} };
|
|
204
|
+
const run = render(files, given, created, trace);
|
|
162
205
|
let timer;
|
|
163
206
|
const limit = new Promise((_, reject) => { timer = setTimeout(() => reject(new HttpError(504, `beltype serve: the print took over ${options.timeout / 1000} s.`)), options.timeout); });
|
|
164
207
|
try {
|
|
@@ -166,12 +209,15 @@ export async function serve(options) {
|
|
|
166
209
|
prints++;
|
|
167
210
|
sinceStart++;
|
|
168
211
|
log(`beltype serve: print ${id}: 200, ${printed.pages} pages, ${writer}, ${Math.round(performance.now() - t0)} ms (write ${printed.caster?.ms['emit'] ?? '?'} ms)`);
|
|
212
|
+
log(`beltype serve: print ${id}: ${stepsLine(trace, printed)}`);
|
|
169
213
|
return { bytes: printed.bytes, printed, writer };
|
|
170
214
|
}
|
|
171
215
|
catch (error) {
|
|
172
216
|
prints++;
|
|
173
217
|
sinceStart++;
|
|
174
218
|
run.catch(() => { });
|
|
219
|
+
if (error instanceof HttpError && kept)
|
|
220
|
+
await closePage(kept);
|
|
175
221
|
if (!(error instanceof BuildError) && !(error instanceof HttpError))
|
|
176
222
|
needsRestart = true;
|
|
177
223
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beltype/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "beltype, the command line: a finished HTML document printed to PDF by Cicero and Caster in Chromium's headless shell.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@beltype/caster": "0.1.6",
|
|
37
|
-
"@beltype/cicero": "0.1.
|
|
37
|
+
"@beltype/cicero": "0.1.6",
|
|
38
38
|
"@beltype/common": "0.1.2",
|
|
39
39
|
"mupdf": "1.28.1",
|
|
40
40
|
"playwright-core": "1.63.0"
|