tosijs-ui 1.6.21 → 1.6.23
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 +97 -0
- package/dist/code-editor.d.ts +9 -35
- package/dist/code-editor.js +62 -90
- package/dist/doc-browser.js +6 -3
- package/dist/doc-system/doc-title.d.ts +19 -0
- package/dist/doc-system/doc-title.js +51 -0
- package/dist/doc-system/site/check-examples-cli.d.ts +1 -0
- package/dist/doc-system/site/check-examples-cli.js +39 -0
- package/dist/doc-system/site/dev-server.d.ts +26 -0
- package/dist/doc-system/site/dev-server.js +444 -19
- package/dist/doc-system/site/epub-cli.d.ts +1 -0
- package/dist/doc-system/site/epub-cli.js +22 -0
- package/dist/doc-system/site/epub.js +6 -0
- package/dist/doc-system/site/generate-site.js +4 -6
- package/dist/doc-system/site/make-llms-txt.js +0 -16
- package/dist/doc-system/site/orchestrator.js +176 -52
- package/dist/doc-system/site/preflight.d.ts +152 -0
- package/dist/doc-system/site/preflight.js +401 -0
- package/dist/doc-system/site/site-config.d.ts +34 -0
- package/dist/iife.js +109 -153
- package/dist/iife.js.map +9 -36
- package/dist/live-example/component.d.ts +0 -1
- package/dist/live-example/component.js +22 -200
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +2 -9
- package/package.json +3 -18
- package/tls/create-dev-certs.sh +9 -25
- package/dist/code-editor-cm.d.ts +0 -48
- package/dist/code-editor-cm.js +0 -219
|
@@ -12,32 +12,135 @@ the icon system here would put src/icon-data.ts into `bun --watch`'s graph and
|
|
|
12
12
|
cause an endless rebuild loop.
|
|
13
13
|
*/
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import { existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
16
|
+
import { tmpdir } from 'os';
|
|
17
|
+
import { $, spawn } from 'bun';
|
|
18
18
|
import { extractDocs } from './docs';
|
|
19
|
-
import {
|
|
19
|
+
import { preflight } from './preflight';
|
|
20
|
+
import { checkExamples, formatExampleProblems, } from './check-examples';
|
|
20
21
|
import { ensureSections } from './sections';
|
|
21
22
|
import { generateLlmsTxt } from './make-llms-txt';
|
|
22
23
|
import { generateSite } from './generate-site';
|
|
23
24
|
import { findOutputDirOverlap } from './output-guard';
|
|
24
|
-
|
|
25
|
+
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
26
|
+
// before interpolating into the require-shim detector below.
|
|
27
|
+
/** Give up on a hung ePub child rather than wedge the dev server's rebuild. */
|
|
28
|
+
const EPUB_TIMEOUT_MS = 120_000;
|
|
29
|
+
/**
|
|
30
|
+
* Run buildEpub() in a child process.
|
|
31
|
+
*
|
|
32
|
+
* buildEpub drives happy-dom (HTML→XHTML for every chapter) and @resvg/resvg-js
|
|
33
|
+
* (cover raster) — both native, both retaining — and it runs on EVERY dev rebuild,
|
|
34
|
+
* so in-process it strands memory in a watch process that lives for days. The child
|
|
35
|
+
* hands it all back on exit. If it hangs we kill it, and if it fails we warn: the
|
|
36
|
+
* ePub is a side artifact, so neither should block the page you're trying to look at.
|
|
37
|
+
*
|
|
38
|
+
* Only the data buildEpub reads is forwarded — SiteConfig also carries functions
|
|
39
|
+
* (llmsTxt, libraryBuild, prebuild) that don't survive JSON.
|
|
40
|
+
*/
|
|
41
|
+
async function buildEpubInChild(config, opts) {
|
|
42
|
+
// Resolve the sibling relative to THIS module so it works both in-repo (.ts)
|
|
43
|
+
// and when shipped (compiled .js) — same trick as generate-css below.
|
|
44
|
+
const cliTs = `${import.meta.dir}/epub-cli.ts`;
|
|
45
|
+
const cli = existsSync(cliTs) ? cliTs : `${import.meta.dir}/epub-cli.js`;
|
|
46
|
+
const payload = {
|
|
47
|
+
config: {
|
|
48
|
+
basePath: config.basePath,
|
|
49
|
+
baseUrl: config.baseUrl,
|
|
50
|
+
book: config.book,
|
|
51
|
+
docsJson: config.docsJson,
|
|
52
|
+
favicon: config.favicon,
|
|
53
|
+
lang: config.lang,
|
|
54
|
+
name: config.name,
|
|
55
|
+
outputDir: config.outputDir,
|
|
56
|
+
},
|
|
57
|
+
opts,
|
|
58
|
+
};
|
|
59
|
+
const payloadPath = path.join(tmpdir(), `tosijs-epub-${process.pid}.json`);
|
|
60
|
+
await Bun.write(payloadPath, JSON.stringify(payload));
|
|
61
|
+
const child = spawn(['bun', cli, payloadPath], {
|
|
62
|
+
stdout: 'inherit',
|
|
63
|
+
stderr: 'inherit',
|
|
64
|
+
});
|
|
65
|
+
const killer = setTimeout(() => {
|
|
66
|
+
console.warn(`⚠️ epub build exceeded ${EPUB_TIMEOUT_MS / 1000}s — killing it. The site is\n` +
|
|
67
|
+
` fine; the .epub may be stale.`);
|
|
68
|
+
child.kill();
|
|
69
|
+
}, EPUB_TIMEOUT_MS);
|
|
70
|
+
try {
|
|
71
|
+
const code = await child.exited;
|
|
72
|
+
if (code !== 0) {
|
|
73
|
+
console.warn(`⚠️ epub build failed (exit ${code}). The site is fine; the .epub may be stale.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(killer);
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(payloadPath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// already gone — fine
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
25
86
|
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
26
87
|
// before interpolating into the require-shim detector below.
|
|
27
88
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Run checkExamples() in a child process.
|
|
91
|
+
*
|
|
92
|
+
* It compiles every executable block in the corpus with `new AsyncFunction` on every
|
|
93
|
+
* rebuild — and JSC caches compiled code keyed by source text, so it retains, and
|
|
94
|
+
* (because a rebuild only happens when a file CHANGED) the source is fresh every time
|
|
95
|
+
* and nothing dedups. It also constructs a `Bun.Transpiler` per `ts` block (~40KB
|
|
96
|
+
* stranded per construction — oven-sh/bun#34053). Both accumulate in a watch process
|
|
97
|
+
* that lives for days; the child gives it all back on exit. Falls back to in-process
|
|
98
|
+
* if the child can't run — a build must not break because the leak-safe path is
|
|
99
|
+
* unavailable.
|
|
100
|
+
*/
|
|
101
|
+
async function checkExamplesInChild(docsJson) {
|
|
102
|
+
const cliTs = `${import.meta.dir}/check-examples-cli.ts`;
|
|
103
|
+
const cli = existsSync(cliTs)
|
|
104
|
+
? cliTs
|
|
105
|
+
: `${import.meta.dir}/check-examples-cli.js`;
|
|
32
106
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
107
|
+
const child = spawn(['bun', cli, docsJson], {
|
|
108
|
+
stdout: 'pipe',
|
|
109
|
+
stderr: 'pipe',
|
|
110
|
+
});
|
|
111
|
+
// Drain BOTH pipes while awaiting exit. An undrained pipe fills its buffer, the
|
|
112
|
+
// child blocks writing to it, and we deadlock waiting for an exit that can't come.
|
|
113
|
+
const [out, err, code] = await Promise.all([
|
|
114
|
+
new Response(child.stdout).text(),
|
|
115
|
+
new Response(child.stderr).text(),
|
|
116
|
+
child.exited,
|
|
117
|
+
]);
|
|
118
|
+
if (err.trim())
|
|
119
|
+
console.warn(err.trim());
|
|
120
|
+
if (code !== 0)
|
|
121
|
+
throw new Error(`check-examples exited ${code}`);
|
|
122
|
+
return JSON.parse(out);
|
|
35
123
|
}
|
|
36
|
-
catch {
|
|
37
|
-
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.warn(`⚠️ example check: could not run it in a child (${String(e)}) — ` +
|
|
126
|
+
`falling back to in-process.`);
|
|
127
|
+
const corpus = JSON.parse(await Bun.file(docsJson).text());
|
|
128
|
+
return checkExamples(corpus);
|
|
38
129
|
}
|
|
39
130
|
}
|
|
40
131
|
export async function buildSite(config) {
|
|
132
|
+
// Look at the machine before adding load to it — a build runs on every watch
|
|
133
|
+
// rebuild, so a dying box (a runaway dev server elsewhere, a VM stall) is caught
|
|
134
|
+
// here too, not just at dev-server launch. Returns false rather than exiting: this
|
|
135
|
+
// is a public export, and an adopter's `await buildSite(cfg); await deploy()` must
|
|
136
|
+
// not be killed from inside a health check they never asked for.
|
|
137
|
+
if (!(await preflight({
|
|
138
|
+
label: 'Build',
|
|
139
|
+
devLimitMb: config.memoryLimitMb,
|
|
140
|
+
mode: config.preflight,
|
|
141
|
+
}))) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
41
144
|
const PROJECT_ROOT = './';
|
|
42
145
|
const PUBLIC = path.resolve(PROJECT_ROOT, config.outputDir ?? 'docs');
|
|
43
146
|
const DIST = path.resolve(PROJECT_ROOT, 'dist');
|
|
@@ -86,8 +189,7 @@ export async function buildSite(config) {
|
|
|
86
189
|
// display-only `typescript`. Runs on the whole corpus, so it catches breakage
|
|
87
190
|
// on pages the browser test never navigates to.
|
|
88
191
|
if (config.checkExamples !== false) {
|
|
89
|
-
const
|
|
90
|
-
const problems = await checkExamples(corpus);
|
|
192
|
+
const problems = await checkExamplesInChild(DOCS_JSON);
|
|
91
193
|
if (problems.length) {
|
|
92
194
|
throw new Error(`doc-site build: ${problems.length} live example(s) failed to build:\n\n` +
|
|
93
195
|
formatExampleProblems(problems) +
|
|
@@ -142,36 +244,47 @@ export async function buildSite(config) {
|
|
|
142
244
|
// consumer supplies (e.g. via staticDirs or an absolute URL).
|
|
143
245
|
const scriptName = (config.scriptUrl ?? '/iife.js').replace(/^\//, '');
|
|
144
246
|
if (config.bundleEntry) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
247
|
+
// tjs-lang transpiles live examples; it's dynamically import()'d at
|
|
248
|
+
// runtime (falling back to CDN), so keep it out of the bundle.
|
|
249
|
+
const externals = [
|
|
250
|
+
'tjs-lang',
|
|
251
|
+
'tjs-lang/browser',
|
|
252
|
+
'tjs-lang/browser/from-ts',
|
|
253
|
+
...(config.bundleExternals ?? []),
|
|
254
|
+
];
|
|
255
|
+
// Bundle in a CHILD PROCESS, not via Bun.build().
|
|
256
|
+
//
|
|
257
|
+
// Bun.build() never gives back the bundler's native arena: measured at ~9MB
|
|
258
|
+
// of RSS per call and rising, monotonic, with no plateau (40 sequential
|
|
259
|
+
// builds of one real entry = +367MB, still climbing ~5MB/build at the end),
|
|
260
|
+
// while the JS heap stays flat — so it is invisible to Bun.gc() and to any
|
|
261
|
+
// heap profiler. devServer() calls this once per rebuild in a process that
|
|
262
|
+
// lives for DAYS, so it compounds: a ~2-day watch session reached 136GB RSS
|
|
263
|
+
// and took the machine down with it. Filed as oven-sh/bun#34053.
|
|
264
|
+
//
|
|
265
|
+
// The CLI does identical work in a child whose memory the OS reclaims on
|
|
266
|
+
// exit: the same 15 bundles leave the parent +0.5MB instead of +192MB.
|
|
267
|
+
// Keep these flags in sync with the Bun.build() options they replace.
|
|
268
|
+
const bundle = spawn([
|
|
269
|
+
'bun',
|
|
270
|
+
'build',
|
|
271
|
+
config.bundleEntry,
|
|
272
|
+
'--outdir',
|
|
273
|
+
DIST,
|
|
274
|
+
'--sourcemap=linked',
|
|
275
|
+
'--format=iife',
|
|
276
|
+
'--minify',
|
|
277
|
+
'--entry-naming',
|
|
278
|
+
scriptName,
|
|
279
|
+
...externals.flatMap((ext) => ['--external', ext]),
|
|
280
|
+
], { stdout: 'inherit', stderr: 'inherit' });
|
|
281
|
+
if ((await bundle.exited) !== 0) {
|
|
170
282
|
console.error('bundle build failed');
|
|
171
|
-
for (const message of result.logs)
|
|
172
|
-
console.error(message);
|
|
173
283
|
return false;
|
|
174
284
|
}
|
|
285
|
+
await $ `cp ${DIST}/${scriptName} ${PUBLIC}`.text();
|
|
286
|
+
const bundleFile = await Bun.file(`${DIST}/${scriptName}`).arrayBuffer();
|
|
287
|
+
const bundleJs = Buffer.from(bundleFile).toString('utf8');
|
|
175
288
|
// Warn only when an external actually compiled to a synchronous require()
|
|
176
289
|
// shim, which throws at module-eval ("Dynamic require of … is not
|
|
177
290
|
// supported"). That only happens for a *static* `import x from 'ext'`. A
|
|
@@ -180,26 +293,37 @@ export async function buildSite(config) {
|
|
|
180
293
|
// stay silent. The config alone can't tell the two apart; the emitted
|
|
181
294
|
// bundle can, so we inspect the actual output.
|
|
182
295
|
if (config.bundleExternals && config.bundleExternals.length > 0) {
|
|
183
|
-
const
|
|
184
|
-
.filter((o) => o.kind === 'entry-point' || o.path.endsWith('.js'))
|
|
185
|
-
.map((o) => o.text()))).join('\n');
|
|
186
|
-
const broken = config.bundleExternals.filter((ext) => new RegExp(`(?:__require|\\brequire)\\(\\s*["'\`]${escapeRegExp(ext)}["'\`]`).test(js));
|
|
296
|
+
const broken = config.bundleExternals.filter((ext) => new RegExp(`(?:__require|\\brequire)\\(\\s*["'\`]${escapeRegExp(ext)}["'\`]`).test(bundleJs));
|
|
187
297
|
if (broken.length > 0) {
|
|
188
298
|
console.warn(`⚠️ bundleExternals compiled to a synchronous require() shim that throws at runtime (${broken.join(', ')}). Reference these via a dynamic import() (kept async by the bundler)\n` +
|
|
189
299
|
` or an importmap, not a static import.`);
|
|
190
300
|
}
|
|
191
301
|
}
|
|
192
|
-
await $ `cp ${DIST}/${scriptName} ${PUBLIC}`.text();
|
|
193
|
-
const bundleFile = await Bun.file(`${DIST}/${scriptName}`).arrayBuffer();
|
|
194
302
|
// import.meta is illegal in a classic <script> — if it survived bundling
|
|
195
303
|
// (a branch the bundler couldn't eliminate) the IIFE will SyntaxError.
|
|
196
|
-
if (
|
|
304
|
+
if (bundleJs.includes('import.meta')) {
|
|
197
305
|
console.warn(`⚠️ ${scriptName} contains \`import.meta\`, which is a SyntaxError in a classic <script>.\n` +
|
|
198
306
|
` A dependency referenced it in a branch the bundler couldn't drop — mark that dep external\n` +
|
|
199
307
|
` (+ importmap) or choose a browser-only entry point.`);
|
|
200
308
|
}
|
|
201
|
-
|
|
202
|
-
|
|
309
|
+
// Gzip in a CHILD, not in-process. zlib's gzip is native, so it strands memory the
|
|
310
|
+
// JS heap never sees; this runs on every rebuild over a ~1MB bundle to print ONE
|
|
311
|
+
// size line, in a process that lives for days. Deliberately zlib-in-a-child rather
|
|
312
|
+
// than the `gzip` CLI: the two disagree by ~1.6%, and this number is tracked across
|
|
313
|
+
// releases, so moving the work must not move the measurement. Best-effort — a
|
|
314
|
+
// missing size line must never fail a build.
|
|
315
|
+
let gzipKb = '';
|
|
316
|
+
try {
|
|
317
|
+
const bytes = Number((await $ `bun -e ${`const {gzipSync}=require('zlib');const b=await Bun.file(${JSON.stringify(`${DIST}/${scriptName}`)}).arrayBuffer();process.stdout.write(String(gzipSync(Buffer.from(b)).length))`}`
|
|
318
|
+
.quiet()
|
|
319
|
+
.text()).trim());
|
|
320
|
+
if (bytes > 0)
|
|
321
|
+
gzipKb = ` (${(bytes / 1024).toFixed(1)}kb gzip)`;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// couldn't measure — report the raw size alone
|
|
325
|
+
}
|
|
326
|
+
console.log(`${scriptName}: ${(bundleFile.byteLength / 1024).toFixed(1)}kb${gzipKb}`);
|
|
203
327
|
}
|
|
204
328
|
else if (!/^(https?:)?\/\//.test(config.scriptUrl ?? '/iife.js')) {
|
|
205
329
|
// No custom bundleEntry (the normal case for a pure-docs / book site) and a
|
|
@@ -319,7 +443,7 @@ export async function buildSite(config) {
|
|
|
319
443
|
// the corpus and survives the `rm -rf <outputDir>` at the top of the NEXT build
|
|
320
444
|
// (otherwise a dev rebuild would silently drop it). Cheap (~0.4s).
|
|
321
445
|
if (config.epub) {
|
|
322
|
-
await
|
|
446
|
+
await buildEpubInChild(config, typeof config.epub === 'object' ? config.epub : {});
|
|
323
447
|
}
|
|
324
448
|
console.timeEnd('build');
|
|
325
449
|
return true;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export interface ProcInfo {
|
|
2
|
+
pid: number;
|
|
3
|
+
rssMb: number;
|
|
4
|
+
/** elapsed time as `ps` reports it, e.g. `01:23:45` or `2-04:11:07` */
|
|
5
|
+
etime: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Assessment {
|
|
9
|
+
level: 'ok' | 'warn' | 'fail';
|
|
10
|
+
/** processes worth naming, worst first */
|
|
11
|
+
offenders: ProcInfo[];
|
|
12
|
+
/** human-readable reason, empty when ok */
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse `ps -eo pid=,rss=,etime=,args=` output. Bytes are KB in `ps`; the command
|
|
17
|
+
* is everything after the third field, so it may contain spaces.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parsePs(output: string): ProcInfo[];
|
|
20
|
+
/** `ps` etime — `[[dd-]hh:]mm:ss` — as hours. */
|
|
21
|
+
export declare function etimeHours(etime: string): number;
|
|
22
|
+
/**
|
|
23
|
+
* How much RAM is actually available to *dev work* — the denominator every
|
|
24
|
+
* percentage below is taken against.
|
|
25
|
+
*
|
|
26
|
+
* This is the whole trick, and it is worth being explicit about why it is not
|
|
27
|
+
* `totalmem()`. **Percentages of physical RAM scale with the hardware; headroom does
|
|
28
|
+
* not.** Buy 96GB more memory and spend all of it on a resident model, and the slack
|
|
29
|
+
* available to a runaway build is exactly what it was before — but every
|
|
30
|
+
* percentage-of-RAM threshold has quadrupled, so the guard gets four times laxer on
|
|
31
|
+
* the machine where it needed to be just as strict. On a 128GB box with a 96GB model
|
|
32
|
+
* up, a 50%-of-RAM "catastrophic" bar is 64GB: double the headroom that exists, so it
|
|
33
|
+
* can never fire in time.
|
|
34
|
+
*
|
|
35
|
+
* Budget = RAM − (what exempt processes hold) − (`reservedMb`), floored at
|
|
36
|
+
* `minBudgetPct` of RAM so a machine that is genuinely fully committed still gets
|
|
37
|
+
* sane thresholds rather than absurd ones. On the 128GB/96GB box that yields a ~32GB
|
|
38
|
+
* budget — the same absolute thresholds that are right on a 32GB laptop, because the
|
|
39
|
+
* slack is the same. Unload the model and the budget floats back up on its own.
|
|
40
|
+
*
|
|
41
|
+
* `devLimitMb` is deliberately NOT a percentage: it survives a hardware change
|
|
42
|
+
* precisely because it is absolute. 4GB is too big for a dev server on any machine.
|
|
43
|
+
*/
|
|
44
|
+
export declare function memoryBudgetMb(procs: ProcInfo[], opts: {
|
|
45
|
+
totalRamMb: number;
|
|
46
|
+
reservedMb?: number;
|
|
47
|
+
minBudgetPct?: number;
|
|
48
|
+
}): number;
|
|
49
|
+
/**
|
|
50
|
+
* Judge the process table against physical RAM.
|
|
51
|
+
*
|
|
52
|
+
* THREE per-process triggers. Note what is deliberately NOT here: a rule that SUMS
|
|
53
|
+
* RSS across processes. It is the obvious idea and it is wrong on macOS — at the
|
|
54
|
+
* moment of the fatal crash, total RSS across 629 processes was ~225GB against 32GB
|
|
55
|
+
* of RAM, and the box was still limping. Compression and swap mean summed footprint
|
|
56
|
+
* routinely exceeds physical RAM on a perfectly healthy machine, so an aggregate-RSS
|
|
57
|
+
* rule would fire during ordinary work and get switched off within a week. A SINGLE
|
|
58
|
+
* process at half the machine is genuinely anomalous; the sum is not. (For a real
|
|
59
|
+
* machine-level signal, see `assessMemoryPressure` below — VM pressure, not RSS
|
|
60
|
+
* arithmetic.)
|
|
61
|
+
*
|
|
62
|
+
* Every percentage is taken against the **dev memory budget** (`memoryBudgetMb`), NOT
|
|
63
|
+
* against physical RAM — see that function for why. Exempt processes (a resident LLM,
|
|
64
|
+
* say) can never be offenders; they shrink the budget instead.
|
|
65
|
+
*
|
|
66
|
+
* - **Any process over `catastrophicPct` of the budget** (default 50%). Nothing
|
|
67
|
+
* healthy is half the available machine. Not limited to dev processes — if
|
|
68
|
+
* something else is eating the box, adding a build to it is still the wrong move.
|
|
69
|
+
* - **A dev process over `hugeDevPct` of the budget** (default 25%), *regardless of
|
|
70
|
+
* age*. The age gate below is a real blind spot: the runaway that took the machine
|
|
71
|
+
* down went 0→100GB in about twenty minutes, so it spent its entire climb through
|
|
72
|
+
* the 4–16GB band both too young to be "stale" and too small to be "catastrophic" —
|
|
73
|
+
* i.e. invisible for the whole ascent. No legitimate dev process is a quarter of
|
|
74
|
+
* the available machine, young or not.
|
|
75
|
+
* - **A long-lived dev process over `devLimitMb`** (default: the dev server's own RSS
|
|
76
|
+
* ceiling). Absolute, not a percentage — 4GB is too big for a dev server on any
|
|
77
|
+
* machine, and an absolute limit survives a hardware upgrade. Age-gated so a build
|
|
78
|
+
* legitimately peaking mid-run isn't flagged.
|
|
79
|
+
*
|
|
80
|
+
* `selfPid` is excluded throughout — this process's own growth is the memory
|
|
81
|
+
* watchdog's job, and it is bounded there.
|
|
82
|
+
*/
|
|
83
|
+
export declare function assessProcesses(procs: ProcInfo[], opts: {
|
|
84
|
+
totalRamMb: number;
|
|
85
|
+
selfPid?: number;
|
|
86
|
+
devLimitMb?: number;
|
|
87
|
+
catastrophicPct?: number;
|
|
88
|
+
hugeDevPct?: number;
|
|
89
|
+
minAgeHours?: number;
|
|
90
|
+
/** RAM to hold back for known-heavy non-dev work, on top of exempt processes */
|
|
91
|
+
reservedMb?: number;
|
|
92
|
+
}): Assessment;
|
|
93
|
+
export interface VmPressure {
|
|
94
|
+
freeMb: number;
|
|
95
|
+
compressorMb: number;
|
|
96
|
+
swapUsedMb: number;
|
|
97
|
+
}
|
|
98
|
+
/** Parse `vm_stat`. Page size is in its header ("page size of 16384 bytes"). */
|
|
99
|
+
export declare function parseVmStat(output: string): {
|
|
100
|
+
freeMb: number;
|
|
101
|
+
compressorMb: number;
|
|
102
|
+
};
|
|
103
|
+
/** Parse `sysctl vm.swapusage` → used MB. */
|
|
104
|
+
export declare function parseSwapUsage(output: string): number;
|
|
105
|
+
/**
|
|
106
|
+
* Judge VM pressure.
|
|
107
|
+
*
|
|
108
|
+
* Requires BOTH a swollen compressor AND almost no free memory. Either alone is
|
|
109
|
+
* normal: macOS keeps free memory low on purpose (it is caching, not idling), and a
|
|
110
|
+
* few GB of compressor on a busy machine is routine. It is the combination — the
|
|
111
|
+
* compressor has eaten a large slice of RAM *and* there is nothing left to hand out
|
|
112
|
+
* — that means the VM system is about to stall. Swap is reported for context and as
|
|
113
|
+
* a corroborating trigger, not on its own: a Mac can sit on GBs of swap indefinitely
|
|
114
|
+
* while perfectly healthy.
|
|
115
|
+
*/
|
|
116
|
+
export declare function assessMemoryPressure(vm: VmPressure, opts: {
|
|
117
|
+
totalRamMb: number;
|
|
118
|
+
compressorPct?: number;
|
|
119
|
+
freeFloorMb?: number;
|
|
120
|
+
swapFloorMb?: number;
|
|
121
|
+
}): {
|
|
122
|
+
level: 'ok' | 'fail';
|
|
123
|
+
reason: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Check the machine before adding load to it.
|
|
127
|
+
*
|
|
128
|
+
* Returns **true when it is safe to proceed**. It never calls `process.exit` — this is
|
|
129
|
+
* library code (`buildSite`/`devServer` are public exports of `tosijs-ui/site`), and an
|
|
130
|
+
* adopter's `await buildSite(cfg); await publishToS3()` must not be killed from inside
|
|
131
|
+
* a health check it did not ask for. The caller decides what a `false` means.
|
|
132
|
+
*
|
|
133
|
+
* Modes (`mode`, or `preflight` in SiteConfig):
|
|
134
|
+
* 'fail' — refuse (return false). Auto-downgraded to 'warn' in CI / non-TTY.
|
|
135
|
+
* 'warn' — print and proceed.
|
|
136
|
+
* 'off' — skip entirely. Also `DEV_SKIP_PREFLIGHT=1`.
|
|
137
|
+
*
|
|
138
|
+
* Best-effort by construction: `ps` is POSIX-only, and a health check that breaks the
|
|
139
|
+
* build when IT fails is worse than no health check at all.
|
|
140
|
+
*/
|
|
141
|
+
export type PreflightMode = 'off' | 'warn' | 'fail';
|
|
142
|
+
export declare function preflight(opts?: {
|
|
143
|
+
devLimitMb?: number;
|
|
144
|
+
label?: string;
|
|
145
|
+
mode?: PreflightMode | false;
|
|
146
|
+
/** injected for tests; defaults to a real `ps` snapshot */
|
|
147
|
+
procs?: ProcInfo[];
|
|
148
|
+
/** injected for tests; defaults to this machine's physical RAM */
|
|
149
|
+
totalRamMb?: number;
|
|
150
|
+
/** injected for tests; defaults to a real vm_stat/swapusage read (null off darwin) */
|
|
151
|
+
vm?: VmPressure | null;
|
|
152
|
+
}): Promise<boolean>;
|