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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.23
|
|
4
|
+
|
|
5
|
+
**The two biggest sources of ecosystem-wide friction — runaway dev servers and haltija
|
|
6
|
+
conflicts — fixed in the version people actually run.** If you use `tosijs-ui/site`, update.
|
|
7
|
+
|
|
8
|
+
### Machine-safety guards (a leaking/forgotten dev server took a machine down twice)
|
|
9
|
+
|
|
10
|
+
- **`killStrayServer` no longer `kill -9`s processes merely _connected_ to its port.** It freed
|
|
11
|
+
its port with `lsof -ti:PORT | xargs kill -9`, and `lsof -i:PORT` matches sockets whose local
|
|
12
|
+
**or remote** port is PORT — so it SIGKILLed every _client_ of that port, not just the
|
|
13
|
+
listener: the browser reading your page, an editor's language server, anything with a
|
|
14
|
+
connection. It now signals **only the process listening** on the port, only after confirming
|
|
15
|
+
it is a JS runtime, SIGTERM first and SIGKILL only as a fallback.
|
|
16
|
+
|
|
17
|
+
- **Machine-health preflight** before each build and at dev-server launch (and on a 60s tick):
|
|
18
|
+
refuses to add load to a machine that is already dying — a runaway dev server, a VM stall.
|
|
19
|
+
Names the offending PIDs, sizes, ages and project dirs. **Local LLM runtimes (LM Studio,
|
|
20
|
+
ollama, llama.cpp, mlx_lm, vLLM, …) are exempt** — their memory shrinks the dev budget rather
|
|
21
|
+
than tripping the guard, and a big _non-dev_ process (Docker, a JVM, a database) only warns,
|
|
22
|
+
never fails. It never kills anything; it declines to _start_. `preflight: 'fail' | 'warn' |
|
|
23
|
+
false` in the site config, or `DEV_SKIP_PREFLIGHT=1`; auto-downgraded to a warning in CI.
|
|
24
|
+
|
|
25
|
+
- **`idleTimeoutHours` (default 8)** — ⚠️ **behavior change:** the dev server now exits after 8
|
|
26
|
+
idle hours (no request, no rebuild). A forgotten dev server is not inert — it is a days-old
|
|
27
|
+
process still running the code it loaded at launch (updating the package does nothing for it).
|
|
28
|
+
`idleTimeoutHours: 0` disables it.
|
|
29
|
+
|
|
30
|
+
- **RSS ceiling** (`memoryLimitMb`) sampled every 60s, and a **rebuild-loop detector** that
|
|
31
|
+
stops a build which writes a file it also watches instead of spawning a bundler forever.
|
|
32
|
+
|
|
33
|
+
### haltija
|
|
34
|
+
|
|
35
|
+
- Spawned as a **pinned range (`haltija@^1.4.0`)**, overridable with `HALTIJA_VERSION`, instead
|
|
36
|
+
of a floating `@latest` fetched fresh on every adopter's machine. The 1.4.0 floor is
|
|
37
|
+
deliberate: it routes `hj` by working directory (so a project's dev server drives its own
|
|
38
|
+
browser, not whichever was focused), never overwrites a newer machine-wide `hj`, and exits
|
|
39
|
+
non-zero on a failed command — which the doc-test lane now checks instead of racing to a
|
|
40
|
+
timeout. Teardown kills only its own process tree, never a haltija you were already running.
|
|
41
|
+
|
|
42
|
+
### Also fixed
|
|
43
|
+
|
|
44
|
+
- **Builds again against tosijs 1.6.9.** `<tosi-example>` detected hydration with a
|
|
45
|
+
`try { this.parts.js } catch` probe, which before tosijs 1.6.9 permanently poisoned the
|
|
46
|
+
`parts` proxy on the pre-hydration read it performed (it could blank the whole doc page), and
|
|
47
|
+
whose `private get hydrated()` collided with 1.6.9's new public `hydrated` (a `tsc` error, so
|
|
48
|
+
the package could not build against current tosijs). The probe is gone — it uses the base
|
|
49
|
+
class's `this.hydrated` now — and the tosijs floor moves to **`^1.6.9`**
|
|
50
|
+
([tosijs#13](https://github.com/tonioloewald/tosijs/issues/13)).
|
|
51
|
+
|
|
52
|
+
- **The doc-browser no longer double-suffixes the page `<title>`.** On path routing it re-derived
|
|
53
|
+
the title on hydration, ignoring `headTitle` and re-appending the project name to a title that
|
|
54
|
+
already contained it — so the home page flipped from its real title to `tosijs-ui — tosijs-ui`
|
|
55
|
+
the moment the bundle loaded. The static generator and the runtime now share **one**
|
|
56
|
+
`pageTitle` rule (case-insensitive, both-directions containment check; whitespace-only titles
|
|
57
|
+
and project names handled). Fixes [#6](https://github.com/tonioloewald/tosijs-ui/issues/6).
|
|
58
|
+
|
|
59
|
+
- **Two more per-rebuild build leaks moved to child processes** (the dev server lives for days):
|
|
60
|
+
the live-example **check** (`new AsyncFunction` per block + a `Bun.Transpiler` per `ts` block)
|
|
61
|
+
and the bundle **gzip** (native zlib). Both now run in a child whose memory the OS reclaims on
|
|
62
|
+
exit; the gzip stays zlib-in-a-child (not the `gzip` CLI) so the reported size doesn't drift.
|
|
63
|
+
|
|
64
|
+
## 1.6.22
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **The dev server no longer leaks memory on every rebuild — update as a priority if
|
|
69
|
+
you use `tosijs-ui/site`.** `buildSite()` called `Bun.build()` in-process, and Bun's
|
|
70
|
+
bundler never returns its native arena: RSS grows monotonically per call with no
|
|
71
|
+
plateau (40 sequential builds of one entry = **+367MB**, still climbing ~5MB/build at
|
|
72
|
+
the end), while the JS heap stays flat — so it is invisible to `Bun.gc()` and to any
|
|
73
|
+
heap profiler. `devServer()` calls it once per rebuild in a process that runs for
|
|
74
|
+
days, so it compounds: a ~2-day watch session reached **136GB RSS** and took the
|
|
75
|
+
machine down with it. Filed upstream as
|
|
76
|
+
[oven-sh/bun#34053](https://github.com/oven-sh/bun/issues/34053).
|
|
77
|
+
|
|
78
|
+
The bundle now builds in a **child process** (`bun build`), whose memory the OS
|
|
79
|
+
reclaims on exit — the same 15 bundles leave the parent **+0.5MB** instead of +192MB.
|
|
80
|
+
`buildEpub()` moved to a child too (happy-dom + `@resvg/resvg-js` are both native and
|
|
81
|
+
retaining, and it runs on every rebuild), with a 120s timeout kill so a hung or failed
|
|
82
|
+
ePub warns instead of wedging the rebuild. Measured on this repo's own dev server:
|
|
83
|
+
**baseline RSS 503MB → 150MB, and per-rebuild growth 26–59MB → ~2.7MB.**
|
|
84
|
+
|
|
85
|
+
Bundle output is byte-for-byte identical; this is purely where the work runs.
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **Dev-server memory watchdog.** `devServer()` now samples RSS after each rebuild and,
|
|
90
|
+
past a ceiling (`memoryLimitMb` in the site config, or `DEV_MEMORY_LIMIT_MB`; default
|
|
91
|
+
4096), prints the growth-per-rebuild and exits rather than let a leak thrash the
|
|
92
|
+
machine. It distinguishes real growth (a leak — report it) from a build whose baseline
|
|
93
|
+
simply exceeds the ceiling (raise the ceiling).
|
|
94
|
+
|
|
95
|
+
## 1.6.21
|
|
96
|
+
|
|
97
|
+
- Memory-routed nav-toggle fix; `buildSite` `.tjs` hooks (`libraryBuild`,
|
|
98
|
+
`generateCssPreload` — see BUILD-TJS-HOOK.md); tjs-lang 0.9.0.
|
|
99
|
+
|
|
3
100
|
## 1.6.20
|
|
4
101
|
|
|
5
102
|
### Fixed
|
package/dist/code-editor.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import { Component as WebComponent, ElementCreator
|
|
2
|
-
|
|
3
|
-
interface CodeEditorParts extends PartsMap {
|
|
4
|
-
host: HTMLDivElement;
|
|
5
|
-
}
|
|
6
|
-
export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
1
|
+
import { Component as WebComponent, ElementCreator } from 'tosijs';
|
|
2
|
+
export declare class CodeEditor extends WebComponent {
|
|
7
3
|
static preferredTagName: string;
|
|
8
4
|
private source;
|
|
9
|
-
private _handle;
|
|
10
|
-
private _loadPromise;
|
|
11
|
-
private _appliedMode;
|
|
12
|
-
private _appliedDisabled;
|
|
13
5
|
get value(): string;
|
|
14
6
|
set value(text: string);
|
|
15
7
|
private _original;
|
|
@@ -20,16 +12,16 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
20
12
|
showDiff(on: boolean): void;
|
|
21
13
|
static initAttributes: {
|
|
22
14
|
mode: string;
|
|
15
|
+
theme: string;
|
|
23
16
|
disabled: boolean;
|
|
24
17
|
};
|
|
25
18
|
role: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
content: () => HTMLDivElement[];
|
|
19
|
+
private _ace;
|
|
20
|
+
private _editor;
|
|
21
|
+
private _editorPromise;
|
|
22
|
+
options: any;
|
|
23
|
+
get ace(): any;
|
|
24
|
+
get editor(): any;
|
|
33
25
|
static shadowStyleSpec: {
|
|
34
26
|
':host': {
|
|
35
27
|
display: string;
|
|
@@ -37,27 +29,9 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
|
37
29
|
width: string;
|
|
38
30
|
height: string;
|
|
39
31
|
};
|
|
40
|
-
'[part="host"]': {
|
|
41
|
-
height: string;
|
|
42
|
-
};
|
|
43
|
-
'.cm-editor': {
|
|
44
|
-
height: string;
|
|
45
|
-
};
|
|
46
|
-
'.cm-scroller': {
|
|
47
|
-
outline: string;
|
|
48
|
-
fontFamily: string;
|
|
49
|
-
};
|
|
50
32
|
};
|
|
51
33
|
onResize(): void;
|
|
52
34
|
connectedCallback(): void;
|
|
53
|
-
private isTjsMode;
|
|
54
|
-
/**
|
|
55
|
-
* When in tjs mode, lazily upgrade the editor to tjs-lang's CodeMirror language +
|
|
56
|
-
* autocomplete. No-op (keeps TS highlighting) if not tjs, if tjs-lang isn't
|
|
57
|
-
* installed, or if the mode/handle changed before the async load resolved.
|
|
58
|
-
*/
|
|
59
|
-
private applyTjsExtension;
|
|
60
35
|
render(): void;
|
|
61
36
|
}
|
|
62
37
|
export declare const codeEditor: ElementCreator<CodeEditor>;
|
|
63
|
-
export {};
|
package/dist/code-editor.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*#
|
|
2
2
|
# code
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
An [ACE Editor](https://ace.c9.io/) wrapper.
|
|
5
5
|
|
|
6
6
|
Sometimes, it's nice to be able to just toss a code-editor in a web-page.
|
|
7
7
|
|
|
8
|
-
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the
|
|
9
|
-
|
|
8
|
+
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the language, and you can further configure
|
|
9
|
+
the ACE editor instance via its `options` property.
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
12
|
<tosi-code style="width: 100%; height: 100%" mode="css">
|
|
@@ -16,33 +16,51 @@ body {
|
|
|
16
16
|
</tosi-code>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The `<tosi-code>` element has an `editor` property that gives you its
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
CodeMirror is loaded lazily on first use — a page with no `<tosi-code>` bundles
|
|
24
|
-
none of it.
|
|
19
|
+
The `<tosi-code>` element has an `editor` property that gives you its ACE editor instance,
|
|
20
|
+
and an `ace` property that returns the `ace` module, giving you complete access to the
|
|
21
|
+
[Ace API](https://ace.c9.io/api/index.html).
|
|
25
22
|
*/
|
|
26
23
|
/*{ "parent": "Components" }*/
|
|
27
|
-
import { Component as WebComponent
|
|
24
|
+
import { Component as WebComponent } from 'tosijs';
|
|
25
|
+
import { scriptTag } from './via-tag';
|
|
28
26
|
import { tosiDiff } from './diff';
|
|
29
|
-
const
|
|
27
|
+
const ACE_BASE_URL = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.2/';
|
|
28
|
+
const DEFAULT_THEME = 'ace/theme/tomorrow';
|
|
29
|
+
const getAce = async () => {
|
|
30
|
+
const { ace } = await scriptTag(`${ACE_BASE_URL}ace.min.js`);
|
|
31
|
+
return ace;
|
|
32
|
+
};
|
|
33
|
+
const makeCodeEditor = async (codeElement, mode = 'html', options = {}, theme = DEFAULT_THEME) => {
|
|
34
|
+
const ace = await getAce();
|
|
35
|
+
ace.config.set('basePath', ACE_BASE_URL);
|
|
36
|
+
const editor = ace.edit(codeElement, {
|
|
37
|
+
mode: `ace/mode/${mode}`,
|
|
38
|
+
tabSize: 2,
|
|
39
|
+
useSoftTabs: true,
|
|
40
|
+
useWorker: false,
|
|
41
|
+
// Wrap wide lines instead of scrolling horizontally; indentedSoftWrap keeps
|
|
42
|
+
// wrapped continuations aligned under the line's indent.
|
|
43
|
+
wrap: true,
|
|
44
|
+
indentedSoftWrap: true,
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
47
|
+
editor.setTheme(theme);
|
|
48
|
+
return { ace, editor };
|
|
49
|
+
};
|
|
30
50
|
export class CodeEditor extends WebComponent {
|
|
31
51
|
static preferredTagName = 'tosi-code';
|
|
32
52
|
source = '';
|
|
33
|
-
_handle;
|
|
34
|
-
_loadPromise;
|
|
35
|
-
_appliedMode = '';
|
|
36
|
-
_appliedDisabled;
|
|
37
53
|
get value() {
|
|
38
|
-
return this.
|
|
54
|
+
return this.editor === undefined ? this.source : this.editor.getValue();
|
|
39
55
|
}
|
|
40
56
|
set value(text) {
|
|
41
|
-
if (this.
|
|
42
|
-
this.
|
|
57
|
+
if (this.editor === undefined) {
|
|
58
|
+
this.source = text;
|
|
43
59
|
}
|
|
44
60
|
else {
|
|
45
|
-
this.
|
|
61
|
+
this.editor.setValue(text);
|
|
62
|
+
this.editor.clearSelection();
|
|
63
|
+
this.editor.session.getUndoManager().reset();
|
|
46
64
|
}
|
|
47
65
|
}
|
|
48
66
|
// Baseline for `showDiff()` — the version to diff the current `value` against.
|
|
@@ -54,9 +72,9 @@ export class CodeEditor extends WebComponent {
|
|
|
54
72
|
set original(text) {
|
|
55
73
|
this._original = text;
|
|
56
74
|
}
|
|
57
|
-
// Diff overlay — built only on the editor's public surface
|
|
58
|
-
// and the tosi-diff component, never the underlying
|
|
59
|
-
// editor
|
|
75
|
+
// Diff overlay — deliberately built only on the editor's public surface
|
|
76
|
+
// (`value` + `original`) and the tosi-diff component, never the underlying
|
|
77
|
+
// editor's API, so it survives a future Ace → CodeMirror swap untouched.
|
|
60
78
|
diffOverlay;
|
|
61
79
|
get showingDiff() {
|
|
62
80
|
return this.diffOverlay !== undefined && !this.diffOverlay.hidden;
|
|
@@ -85,27 +103,20 @@ export class CodeEditor extends WebComponent {
|
|
|
85
103
|
}
|
|
86
104
|
static initAttributes = {
|
|
87
105
|
mode: 'javascript',
|
|
106
|
+
theme: DEFAULT_THEME,
|
|
88
107
|
disabled: false,
|
|
89
108
|
};
|
|
90
109
|
role = 'code editor';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
_ace;
|
|
111
|
+
_editor;
|
|
112
|
+
_editorPromise;
|
|
113
|
+
options = {};
|
|
114
|
+
get ace() {
|
|
115
|
+
return this._ace;
|
|
94
116
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this._handle?.undo();
|
|
98
|
-
}
|
|
99
|
-
redo() {
|
|
100
|
-
this._handle?.redo();
|
|
101
|
-
}
|
|
102
|
-
canUndo() {
|
|
103
|
-
return this._handle?.canUndo() ?? false;
|
|
104
|
-
}
|
|
105
|
-
canRedo() {
|
|
106
|
-
return this._handle?.canRedo() ?? false;
|
|
117
|
+
get editor() {
|
|
118
|
+
return this._editor;
|
|
107
119
|
}
|
|
108
|
-
content = () => [div({ part: 'host' })];
|
|
109
120
|
static shadowStyleSpec = {
|
|
110
121
|
':host': {
|
|
111
122
|
display: 'block',
|
|
@@ -113,71 +124,32 @@ export class CodeEditor extends WebComponent {
|
|
|
113
124
|
width: '100%',
|
|
114
125
|
height: '100%',
|
|
115
126
|
},
|
|
116
|
-
'[part="host"]': { height: '100%' },
|
|
117
|
-
'.cm-editor': { height: '100%' },
|
|
118
|
-
'.cm-scroller': {
|
|
119
|
-
outline: 'none',
|
|
120
|
-
fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
|
121
|
-
},
|
|
122
127
|
};
|
|
123
128
|
onResize() {
|
|
124
|
-
this.
|
|
129
|
+
if (this.editor !== undefined) {
|
|
130
|
+
this.editor.resize(true);
|
|
131
|
+
}
|
|
125
132
|
}
|
|
126
133
|
connectedCallback() {
|
|
127
134
|
super.connectedCallback();
|
|
128
135
|
if (this.source === '') {
|
|
129
136
|
this.value = this.textContent !== null ? this.textContent.trim() : '';
|
|
130
137
|
}
|
|
131
|
-
if (this.
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onChange: (value) => this.dispatchEvent(new CustomEvent('change', { detail: { value } })),
|
|
140
|
-
});
|
|
141
|
-
this._handle = handle;
|
|
142
|
-
this._appliedMode = this.mode;
|
|
143
|
-
this._appliedDisabled = this.disabled;
|
|
144
|
-
this.applyTjsExtension();
|
|
145
|
-
return handle;
|
|
138
|
+
if (this._editorPromise === undefined) {
|
|
139
|
+
this._editorPromise = makeCodeEditor(this, this.mode, this.options, this.theme);
|
|
140
|
+
this._editorPromise.then(({ ace, editor }) => {
|
|
141
|
+
this._ace = ace;
|
|
142
|
+
this._editor = editor;
|
|
143
|
+
editor.setValue(this.source, 1);
|
|
144
|
+
editor.clearSelection();
|
|
145
|
+
editor.session.getUndoManager().reset();
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
isTjsMode() {
|
|
150
|
-
return this.mode === 'tjs' || this.mode === 'ajs';
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* When in tjs mode, lazily upgrade the editor to tjs-lang's CodeMirror language +
|
|
154
|
-
* autocomplete. No-op (keeps TS highlighting) if not tjs, if tjs-lang isn't
|
|
155
|
-
* installed, or if the mode/handle changed before the async load resolved.
|
|
156
|
-
*/
|
|
157
|
-
applyTjsExtension() {
|
|
158
|
-
const handle = this._handle;
|
|
159
|
-
if (!handle || !this.isTjsMode())
|
|
160
|
-
return;
|
|
161
|
-
import('./code-editor-cm')
|
|
162
|
-
.then(({ loadTjsExtension }) => loadTjsExtension())
|
|
163
|
-
.then((ext) => {
|
|
164
|
-
if (ext && this._handle === handle && this.isTjsMode()) {
|
|
165
|
-
handle.setLanguageExtension(ext);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
149
|
render() {
|
|
170
150
|
super.render();
|
|
171
|
-
if (this.
|
|
172
|
-
|
|
173
|
-
this._handle.setReadOnly(this.disabled);
|
|
174
|
-
this._appliedDisabled = this.disabled;
|
|
175
|
-
}
|
|
176
|
-
if (this.mode !== this._appliedMode) {
|
|
177
|
-
this._handle.setMode(this.mode);
|
|
178
|
-
this._appliedMode = this.mode;
|
|
179
|
-
this.applyTjsExtension();
|
|
180
|
-
}
|
|
151
|
+
if (this._editorPromise !== undefined) {
|
|
152
|
+
this._editorPromise.then(({ editor }) => editor.setReadOnly(this.disabled));
|
|
181
153
|
}
|
|
182
154
|
}
|
|
183
155
|
}
|
package/dist/doc-browser.js
CHANGED
|
@@ -155,6 +155,7 @@ import { elements, vars, varDefault, bindings, touch, getListItem, debounce, tos
|
|
|
155
155
|
import { buildSlugMap, pathForSlug, filenameForPath } from './doc-system/routing';
|
|
156
156
|
import { buildNavTree } from './doc-system/nav-tree';
|
|
157
157
|
import { renderDocMarkdown } from './doc-system/render';
|
|
158
|
+
import { pageTitle } from './doc-system/doc-title';
|
|
158
159
|
import { LiveExample, testManager } from './live-example';
|
|
159
160
|
import { tosiSidenav, TosiSidenav } from './side-nav';
|
|
160
161
|
import { icons } from './icons';
|
|
@@ -613,9 +614,11 @@ export function createDocBrowser(options) {
|
|
|
613
614
|
LiveExample.insertExamples(docContent, context, doc.path || undefined);
|
|
614
615
|
scrollToHashExample();
|
|
615
616
|
if (routing === 'path') {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
617
|
+
// The SAME rule the static generator used for this page's <head> (doc-title.ts).
|
|
618
|
+
// This used to re-derive it — ignoring `headTitle` and re-suffixing a title that
|
|
619
|
+
// already ended in the project name — so the home page's title flipped to
|
|
620
|
+
// "tosijs-ui — tosijs-ui" the moment the bundle loaded (issue #6).
|
|
621
|
+
document.title = pageTitle(doc, projectName);
|
|
619
622
|
}
|
|
620
623
|
};
|
|
621
624
|
// Always resolve to the RAW doc from the original array — docs reached via the
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The fields of a Doc that bear on its title. Structural, so both callers fit. */
|
|
2
|
+
export interface TitledDoc {
|
|
3
|
+
title: string;
|
|
4
|
+
/** verbatim <title>, no project suffix — a per-doc override */
|
|
5
|
+
headTitle?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `headTitle` wins verbatim. Otherwise suffix the project name — but only when the two
|
|
9
|
+
* are genuinely distinct.
|
|
10
|
+
*
|
|
11
|
+
* "Distinct" means **neither string contains the other, compared case-insensitively.**
|
|
12
|
+
* A one-way, case-sensitive `title.includes(projectName)` check missed real cases: a
|
|
13
|
+
* project titled `Tosijs-UI` against a doc titled `tosijs-ui` (case), and a doc whose
|
|
14
|
+
* title is a *substring* of the project name (the containment runs the other way). Any
|
|
15
|
+
* of those, suffixed, reads as redundant — "tosijs-ui — tosijs-ui" being the one that
|
|
16
|
+
* shipped (issue #6). When one already contains the other, the doc's own title is the
|
|
17
|
+
* more specific label, so use it alone.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pageTitle(doc: TitledDoc, projectName?: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The ONE rule for a doc page's <title>.
|
|
3
|
+
|
|
4
|
+
It has to be one rule, because it is applied twice — once by the static generator into
|
|
5
|
+
the pre-rendered `<head>`, and once by the doc-browser when it hydrates. Two
|
|
6
|
+
independently-written copies drifted, and the title became the one thing on the page
|
|
7
|
+
that visibly MOVED on hydration:
|
|
8
|
+
|
|
9
|
+
JS off → "tosijs-ui — robust, dependency-free web components" (headTitle)
|
|
10
|
+
hydrated → "tosijs-ui — tosijs-ui" (!)
|
|
11
|
+
|
|
12
|
+
...because the runtime copy ignored `headTitle` and re-suffixed a title that already
|
|
13
|
+
ended in the project name. That is GitHub issue #6, and it was live on our own home
|
|
14
|
+
page while this release's whole thesis was "hydration is purely additive, nothing
|
|
15
|
+
moves".
|
|
16
|
+
|
|
17
|
+
Shared by `site/generate-site.ts` (build) and `doc-browser.ts` (runtime). Pure — no DOM,
|
|
18
|
+
no Bun — so both can import it.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* `headTitle` wins verbatim. Otherwise suffix the project name — but only when the two
|
|
22
|
+
* are genuinely distinct.
|
|
23
|
+
*
|
|
24
|
+
* "Distinct" means **neither string contains the other, compared case-insensitively.**
|
|
25
|
+
* A one-way, case-sensitive `title.includes(projectName)` check missed real cases: a
|
|
26
|
+
* project titled `Tosijs-UI` against a doc titled `tosijs-ui` (case), and a doc whose
|
|
27
|
+
* title is a *substring* of the project name (the containment runs the other way). Any
|
|
28
|
+
* of those, suffixed, reads as redundant — "tosijs-ui — tosijs-ui" being the one that
|
|
29
|
+
* shipped (issue #6). When one already contains the other, the doc's own title is the
|
|
30
|
+
* more specific label, so use it alone.
|
|
31
|
+
*/
|
|
32
|
+
export function pageTitle(doc, projectName) {
|
|
33
|
+
// A whitespace-only `headTitle` is not an override, it's a blank <title> — trim it
|
|
34
|
+
// and fall through. Trimming also cleans up an accidentally-padded one ("verbatim"
|
|
35
|
+
// means "no project suffix", not "keep your stray whitespace").
|
|
36
|
+
const headTitle = doc.headTitle?.trim();
|
|
37
|
+
if (headTitle)
|
|
38
|
+
return headTitle;
|
|
39
|
+
// Trim first: a whitespace-only name ('' or ' ') is "no project name", not a real
|
|
40
|
+
// one. Untrimmed, ' ' is truthy and slips through — and worse, `title.includes(' ')`
|
|
41
|
+
// is true for any multi-word title, so it would silently suppress a suffix that
|
|
42
|
+
// should appear. Trimming also strips a padded 'tosijs-ui ' down to the real name.
|
|
43
|
+
const project = projectName?.trim() ?? '';
|
|
44
|
+
if (!project)
|
|
45
|
+
return doc.title;
|
|
46
|
+
const t = doc.title.toLowerCase();
|
|
47
|
+
const p = project.toLowerCase();
|
|
48
|
+
if (t.includes(p) || p.includes(t))
|
|
49
|
+
return doc.title;
|
|
50
|
+
return `${doc.title} — ${project}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Live-example build check — the subprocess entry point for checkExamples().
|
|
3
|
+
|
|
4
|
+
Why a child process, when this is "just" compiling some strings:
|
|
5
|
+
|
|
6
|
+
checkExamples() compiles EVERY executable block in the corpus (83 of them here) with
|
|
7
|
+
`new AsyncFunction(js)` on EVERY dev rebuild, and — for `ts` blocks — runs them
|
|
8
|
+
through a `Bun.Transpiler`. Both retain memory the JS heap never shows you:
|
|
9
|
+
|
|
10
|
+
- JSC caches compiled code, KEYED BY SOURCE TEXT. That last part is the trap: replay
|
|
11
|
+
the same corpus and RSS plateaus (the cache dedups), so a leak test on an unchanged
|
|
12
|
+
corpus reports "flat" and lies. But a dev server only rebuilds BECAUSE A FILE
|
|
13
|
+
CHANGED — the source is fresh every time, so nothing dedups and it never plateaus.
|
|
14
|
+
Measured with fresh sources: +7.1MB over 40 rebuilds, still climbing.
|
|
15
|
+
- `Bun.Transpiler` strands ~40KB per construction (oven-sh/bun#34053).
|
|
16
|
+
|
|
17
|
+
In a process that lives for days across thousands of rebuilds, that is unbounded. The
|
|
18
|
+
child hands all of it back to the OS on exit. Same reasoning — and the same fix — as
|
|
19
|
+
the bundle step, the ePub step, and generate-css; see the Bun.build note in
|
|
20
|
+
orchestrator.ts.
|
|
21
|
+
|
|
22
|
+
bun check-examples-cli.ts <docs.json> [contextKey ...]
|
|
23
|
+
→ writes ExampleProblem[] as JSON to stdout; exit 0 whether or not problems were
|
|
24
|
+
found (a problem is data, not a crash — only a real failure exits non-zero).
|
|
25
|
+
|
|
26
|
+
Build-time only. Never import this from browser code.
|
|
27
|
+
*/
|
|
28
|
+
import { checkExamples } from './check-examples';
|
|
29
|
+
const docsJson = process.argv[2];
|
|
30
|
+
if (!docsJson) {
|
|
31
|
+
console.error('check-examples-cli: expected a docs.json path');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const contextKeys = process.argv.slice(3);
|
|
35
|
+
const corpus = JSON.parse(await Bun.file(docsJson).text());
|
|
36
|
+
const problems = await checkExamples(corpus, contextKeys.length ? { contextKeys } : {});
|
|
37
|
+
// stdout is the channel — the parent parses this. Anything else this process prints
|
|
38
|
+
// (warnings from the transform, say) goes to stderr so it can't corrupt the payload.
|
|
39
|
+
process.stdout.write(JSON.stringify(problems));
|
|
@@ -2,6 +2,32 @@ import type { SiteConfig } from './site-config';
|
|
|
2
2
|
declare global {
|
|
3
3
|
var Bun: any;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the idle-exit timeout to milliseconds (0 = disabled).
|
|
7
|
+
*
|
|
8
|
+
* Env wins over config, config over the default. An unparseable value falls back
|
|
9
|
+
* to the default rather than to 0: a typo'd `DEV_IDLE_TIMEOUT_HOURS=8h` must not
|
|
10
|
+
* silently turn the guard OFF — that is the exact failure it exists to prevent.
|
|
11
|
+
* Only an explicit non-positive number disables it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveIdleMs(configHours: number | undefined, envHours: string | undefined): number;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the RSS ceiling in MB (0 = disabled).
|
|
16
|
+
*
|
|
17
|
+
* Same rule as `resolveIdleMs`, and it was NOT being applied: the ceiling was read as
|
|
18
|
+
* `Number(env ?? config ?? 4096)`, which fails in both directions at once —
|
|
19
|
+
*
|
|
20
|
+
* DEV_MEMORY_LIMIT_MB='' → `??` passes '' through → Number('') === 0 → the ceiling
|
|
21
|
+
* is ZERO, so `rss >= limit` is true on the first sample
|
|
22
|
+
* and the dev server kills itself on every rebuild.
|
|
23
|
+
* DEV_MEMORY_LIMIT_MB=4gb → NaN → every `>=` comparison is false → the guard is
|
|
24
|
+
* silently OFF, on the machine of someone who was
|
|
25
|
+
* explicitly trying to configure it.
|
|
26
|
+
*
|
|
27
|
+
* An empty env var is *unset*, not zero. Garbage falls back to the default, never to
|
|
28
|
+
* off. Only an explicit non-positive number disables the ceiling.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveLimitMb(configMb: number | undefined, envMb: string | undefined): number;
|
|
5
31
|
export declare function devServer(config: SiteConfig, opts?: {
|
|
6
32
|
test?: boolean;
|
|
7
33
|
build?: () => unknown | Promise<unknown>;
|