tosijs-ui 1.6.21 → 1.7.0-beta.1
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 +132 -0
- package/bin/make-icon-data.js +0 -0
- package/dist/code-editor-cm.js +41 -17
- package/dist/code-editor.d.ts +32 -2
- package/dist/code-editor.js +192 -25
- package/dist/doc-system/site/bundle-guard.d.ts +50 -0
- package/dist/doc-system/site/bundle-guard.js +111 -0
- package/dist/doc-system/site/check-examples.js +11 -3
- package/dist/doc-system/site/dev-server.js +97 -7
- 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 +34 -9
- package/dist/doc-system/site/make-llms-txt.js +1 -1
- package/dist/doc-system/site/orchestrator.js +139 -53
- package/dist/doc-system/site/site-config.d.ts +10 -0
- package/dist/iife.js +78 -71
- package/dist/iife.js.map +8 -8
- package/dist/live-example/code-transform.d.ts +39 -0
- package/dist/live-example/code-transform.js +305 -4
- package/dist/live-example/component.d.ts +10 -0
- package/dist/live-example/component.js +105 -21
- package/dist/live-example/execution.d.ts +16 -0
- package/dist/live-example/execution.js +29 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +5 -6
|
@@ -12,31 +12,80 @@ 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';
|
|
15
|
+
import { existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
16
|
+
import { tmpdir } from 'os';
|
|
16
17
|
import { gzipSync } from 'zlib';
|
|
17
|
-
import {
|
|
18
|
+
import { $, spawn } from 'bun';
|
|
18
19
|
import { extractDocs } from './docs';
|
|
19
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
|
-
import {
|
|
25
|
+
import { tjsEditorExternal, tjsEditorLeakedAsExternal, classicScriptSyntaxErrorInChild, } from './bundle-guard';
|
|
25
26
|
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
26
27
|
// before interpolating into the require-shim detector below.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
/** Give up on a hung ePub child rather than wedge the dev server's rebuild. */
|
|
29
|
+
const EPUB_TIMEOUT_MS = 120_000;
|
|
30
|
+
/**
|
|
31
|
+
* Run buildEpub() in a child process.
|
|
32
|
+
*
|
|
33
|
+
* buildEpub drives happy-dom (HTML→XHTML for every chapter) and @resvg/resvg-js
|
|
34
|
+
* (cover raster) — both native, both retaining — and it runs on EVERY dev rebuild,
|
|
35
|
+
* so in-process it strands memory in a watch process that lives for days. The child
|
|
36
|
+
* hands it all back on exit. If it hangs we kill it, and if it fails we warn: the
|
|
37
|
+
* ePub is a side artifact, so neither should block the page you're trying to look at.
|
|
38
|
+
*
|
|
39
|
+
* Only the data buildEpub reads is forwarded — SiteConfig also carries functions
|
|
40
|
+
* (llmsTxt, libraryBuild, prebuild) that don't survive JSON.
|
|
41
|
+
*/
|
|
42
|
+
async function buildEpubInChild(config, opts) {
|
|
43
|
+
// Resolve the sibling relative to THIS module so it works both in-repo (.ts)
|
|
44
|
+
// and when shipped (compiled .js) — same trick as generate-css below.
|
|
45
|
+
const cliTs = `${import.meta.dir}/epub-cli.ts`;
|
|
46
|
+
const cli = existsSync(cliTs) ? cliTs : `${import.meta.dir}/epub-cli.js`;
|
|
47
|
+
const payload = {
|
|
48
|
+
config: {
|
|
49
|
+
basePath: config.basePath,
|
|
50
|
+
baseUrl: config.baseUrl,
|
|
51
|
+
book: config.book,
|
|
52
|
+
docsJson: config.docsJson,
|
|
53
|
+
favicon: config.favicon,
|
|
54
|
+
lang: config.lang,
|
|
55
|
+
name: config.name,
|
|
56
|
+
outputDir: config.outputDir,
|
|
57
|
+
},
|
|
58
|
+
opts,
|
|
59
|
+
};
|
|
60
|
+
const payloadPath = path.join(tmpdir(), `tosijs-epub-${process.pid}.json`);
|
|
61
|
+
await Bun.write(payloadPath, JSON.stringify(payload));
|
|
62
|
+
const child = spawn(['bun', cli, payloadPath], {
|
|
63
|
+
stdout: 'inherit',
|
|
64
|
+
stderr: 'inherit',
|
|
65
|
+
});
|
|
66
|
+
const killer = setTimeout(() => {
|
|
67
|
+
console.warn(`⚠️ epub build exceeded ${EPUB_TIMEOUT_MS / 1000}s — killing it. The site is\n` + ` fine; the .epub may be stale.`);
|
|
68
|
+
child.kill();
|
|
69
|
+
}, EPUB_TIMEOUT_MS);
|
|
32
70
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
35
75
|
}
|
|
36
|
-
|
|
37
|
-
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(killer);
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(payloadPath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// already gone — fine
|
|
83
|
+
}
|
|
38
84
|
}
|
|
39
85
|
}
|
|
86
|
+
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
87
|
+
// before interpolating into the require-shim detector below.
|
|
88
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
89
|
export async function buildSite(config) {
|
|
41
90
|
const PROJECT_ROOT = './';
|
|
42
91
|
const PUBLIC = path.resolve(PROJECT_ROOT, config.outputDir ?? 'docs');
|
|
@@ -97,7 +146,8 @@ export async function buildSite(config) {
|
|
|
97
146
|
}
|
|
98
147
|
}
|
|
99
148
|
// Copy static-asset dirs into the web root.
|
|
100
|
-
const staticDirs = config.staticDirs ??
|
|
149
|
+
const staticDirs = config.staticDirs ??
|
|
150
|
+
(existsSync('demo/static') ? ['demo/static'] : ['static']);
|
|
101
151
|
for (const dir of staticDirs) {
|
|
102
152
|
if (existsSync(dir))
|
|
103
153
|
await $ `cp -R ${dir}/. ${PUBLIC}`.text();
|
|
@@ -142,36 +192,58 @@ export async function buildSite(config) {
|
|
|
142
192
|
// consumer supplies (e.g. via staticDirs or an absolute URL).
|
|
143
193
|
const scriptName = (config.scriptUrl ?? '/iife.js').replace(/^\//, '');
|
|
144
194
|
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
|
-
|
|
195
|
+
// tjs-lang's TRANSPILER (browser bundles) is dynamically import()'d at
|
|
196
|
+
// runtime (same-origin `/tjs/` copy, else CDN), so keep it out of the bundle.
|
|
197
|
+
//
|
|
198
|
+
// `tjs-lang/editors/codemirror` (the CodeMirror language + autocomplete) is
|
|
199
|
+
// different: it MUST share the editor's CodeMirror instance, so it has to be
|
|
200
|
+
// bundled IN — a separately loaded copy carries its own `@codemirror/state`
|
|
201
|
+
// and silently no-ops. So we do NOT externalize it *when tjs-lang is
|
|
202
|
+
// installed*. But tjs-lang is an OPTIONAL peer; if it's absent, bundling would
|
|
203
|
+
// fail to resolve, so we externalize it in that case (the runtime import then
|
|
204
|
+
// no-ops to plain TS highlighting — graceful degradation).
|
|
205
|
+
//
|
|
206
|
+
// NB: bare `'tjs-lang'` must NOT appear here. Externals are PREFIX matches, so
|
|
207
|
+
// it would silently externalize tjs-lang/editors/codemirror along with it.
|
|
208
|
+
const externals = [
|
|
209
|
+
'tjs-lang/browser',
|
|
210
|
+
'tjs-lang/browser/from-ts',
|
|
211
|
+
...tjsEditorExternal(PROJECT_ROOT),
|
|
212
|
+
...(config.bundleExternals ?? []),
|
|
213
|
+
];
|
|
214
|
+
// Bundle in a CHILD PROCESS, not via Bun.build().
|
|
215
|
+
//
|
|
216
|
+
// Bun.build() never gives back the bundler's native arena: measured at ~9MB
|
|
217
|
+
// of RSS per call and rising, monotonic, with no plateau (40 sequential
|
|
218
|
+
// builds of one real entry = +367MB, still climbing ~5MB/build at the end),
|
|
219
|
+
// while the JS heap stays flat — so it is invisible to Bun.gc() and to any
|
|
220
|
+
// heap profiler. devServer() calls this once per rebuild in a process that
|
|
221
|
+
// lives for DAYS, so it compounds: a ~2-day watch session reached 136GB RSS
|
|
222
|
+
// and took the machine down with it. Filed as oven-sh/bun#34053.
|
|
223
|
+
//
|
|
224
|
+
// The CLI does identical work in a child whose memory the OS reclaims on
|
|
225
|
+
// exit: the same 15 bundles leave the parent +0.5MB instead of +192MB.
|
|
226
|
+
// Keep these flags in sync with the Bun.build() options they replace.
|
|
227
|
+
const bundle = spawn([
|
|
228
|
+
'bun',
|
|
229
|
+
'build',
|
|
230
|
+
config.bundleEntry,
|
|
231
|
+
'--outdir',
|
|
232
|
+
DIST,
|
|
233
|
+
'--sourcemap=linked',
|
|
234
|
+
'--format=iife',
|
|
235
|
+
'--minify',
|
|
236
|
+
'--entry-naming',
|
|
237
|
+
scriptName,
|
|
238
|
+
...externals.flatMap((ext) => ['--external', ext]),
|
|
239
|
+
], { stdout: 'inherit', stderr: 'inherit' });
|
|
240
|
+
if ((await bundle.exited) !== 0) {
|
|
170
241
|
console.error('bundle build failed');
|
|
171
|
-
for (const message of result.logs)
|
|
172
|
-
console.error(message);
|
|
173
242
|
return false;
|
|
174
243
|
}
|
|
244
|
+
await $ `cp ${DIST}/${scriptName} ${PUBLIC}`.text();
|
|
245
|
+
const bundleFile = await Bun.file(`${DIST}/${scriptName}`).arrayBuffer();
|
|
246
|
+
const bundleJs = Buffer.from(bundleFile).toString('utf8');
|
|
175
247
|
// Warn only when an external actually compiled to a synchronous require()
|
|
176
248
|
// shim, which throws at module-eval ("Dynamic require of … is not
|
|
177
249
|
// supported"). That only happens for a *static* `import x from 'ext'`. A
|
|
@@ -180,23 +252,37 @@ export async function buildSite(config) {
|
|
|
180
252
|
// stay silent. The config alone can't tell the two apart; the emitted
|
|
181
253
|
// bundle can, so we inspect the actual output.
|
|
182
254
|
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));
|
|
255
|
+
const broken = config.bundleExternals.filter((ext) => new RegExp(`(?:__require|\\brequire)\\(\\s*["'\`]${escapeRegExp(ext)}["'\`]`).test(bundleJs));
|
|
187
256
|
if (broken.length > 0) {
|
|
188
257
|
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
258
|
` or an importmap, not a static import.`);
|
|
190
259
|
}
|
|
191
260
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
261
|
+
// The IIFE must parse as a classic <script>. `import.meta` is the usual way it
|
|
262
|
+
// doesn't (a branch the bundler couldn't eliminate) — and it's a SyntaxError, so
|
|
263
|
+
// the whole bundle fails to evaluate and the page never hydrates. Compile it
|
|
264
|
+
// (without running it) rather than grepping: the substring also occurs inside
|
|
265
|
+
// string literals — acorn's error messages contain it — which made the old grep
|
|
266
|
+
// fire on every build while the bundle was fine.
|
|
267
|
+
const syntaxError = await classicScriptSyntaxErrorInChild(`${DIST}/${scriptName}`);
|
|
268
|
+
if (syntaxError) {
|
|
269
|
+
console.error(`⚠️ ${scriptName} does not parse as a classic <script>: ${syntaxError}\n` +
|
|
270
|
+
` The page will not hydrate. If a dependency pulled in \`import.meta\`, mark it\n` +
|
|
271
|
+
` external (+ importmap) or choose a browser-only entry point.`);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
// tjs-lang's CodeMirror extension MUST be bundled, not externalized — a separate
|
|
275
|
+
// copy carries its own @codemirror/state and silently no-ops (tjs highlighting and
|
|
276
|
+
// autocomplete just stop working, with no error anywhere). If it was externalized,
|
|
277
|
+
// the bundler leaves its specifier behind. Failing the build is the only way this
|
|
278
|
+
// gets noticed; every test lane stays green when it regresses.
|
|
279
|
+
if (tjsEditorExternal(PROJECT_ROOT).length === 0 &&
|
|
280
|
+
tjsEditorLeakedAsExternal(bundleJs)) {
|
|
281
|
+
console.error(`⚠️ ${scriptName} externalized tjs-lang's CodeMirror extension instead of bundling it.\n` +
|
|
282
|
+
` It must share the editor's single CodeMirror instance; a separately loaded copy\n` +
|
|
283
|
+
` silently no-ops. Check the bundle's \`external\` list — entries are PREFIX matches,\n` +
|
|
284
|
+
` so a bare 'tjs-lang' externalizes tjs-lang/editors/codemirror along with it.`);
|
|
285
|
+
return false;
|
|
200
286
|
}
|
|
201
287
|
const bundleGzip = gzipSync(Buffer.from(bundleFile));
|
|
202
288
|
console.log(`${scriptName}: ${(bundleFile.byteLength / 1024).toFixed(1)}kb (${(bundleGzip.length / 1024).toFixed(1)}kb gzip)`);
|
|
@@ -319,7 +405,7 @@ export async function buildSite(config) {
|
|
|
319
405
|
// the corpus and survives the `rm -rf <outputDir>` at the top of the NEXT build
|
|
320
406
|
// (otherwise a dev rebuild would silently drop it). Cheap (~0.4s).
|
|
321
407
|
if (config.epub) {
|
|
322
|
-
await
|
|
408
|
+
await buildEpubInChild(config, typeof config.epub === 'object' ? config.epub : {});
|
|
323
409
|
}
|
|
324
410
|
console.timeEnd('build');
|
|
325
411
|
return true;
|
|
@@ -186,6 +186,16 @@ export interface SiteConfig {
|
|
|
186
186
|
port?: number;
|
|
187
187
|
/** extra dev-server watch paths (added to docPaths + bundleEntry dir). */
|
|
188
188
|
watchPaths?: string[];
|
|
189
|
+
/**
|
|
190
|
+
* RSS ceiling (MB) for the dev server, default 4096. A watch process lives for
|
|
191
|
+
* days across thousands of rebuilds, so anything the build strands per rebuild
|
|
192
|
+
* compounds until the machine swaps itself to death. Past this, the server
|
|
193
|
+
* prints the growth-per-rebuild and exits rather than take the machine with it.
|
|
194
|
+
* Overridden by the DEV_MEMORY_LIMIT_MB env var. Raise it if a genuinely large
|
|
195
|
+
* build needs the headroom — but sustained growth per rebuild is a leak, not a
|
|
196
|
+
* ceiling that's too low.
|
|
197
|
+
*/
|
|
198
|
+
memoryLimitMb?: number;
|
|
189
199
|
/**
|
|
190
200
|
* Enable the dev-server source read/write endpoints (`/__docstore/source`) that
|
|
191
201
|
* back in-browser "edit page source". Local dev only — the dev server runs on
|