tosijs-ui 1.12.8 → 1.14.0
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 +894 -0
- package/bin/caddy-install.ts +17 -32
- package/bin/deploy-preview.ts +14 -7
- package/bin/make-icon-data.js +11 -0
- package/bin/release-notes.ts +15 -4
- package/bin/resolve-site-config.ts +77 -0
- package/bin/tunnel.ts +15 -7
- package/dist/babylon-3d.d.ts +14 -0
- package/dist/babylon-3d.js +52 -0
- package/dist/code-editor.d.ts +1 -0
- package/dist/code-editor.js +50 -2
- package/dist/codemirror.d.ts +2 -0
- package/dist/codemirror.js +43 -0
- package/dist/data-table.d.ts +32 -1
- package/dist/data-table.js +177 -2
- package/dist/doc-browser.d.ts +1 -0
- package/dist/doc-browser.js +101 -63
- package/dist/doc-system/doc-system-styles.js +14 -6
- package/dist/doc-system/doc-system.js +32 -0
- package/dist/doc-system/site/audit-guard.d.ts +22 -0
- package/dist/doc-system/site/audit-guard.js +69 -0
- package/dist/doc-system/site/audit-reach.d.ts +16 -0
- package/dist/doc-system/site/audit-reach.js +56 -0
- package/dist/doc-system/site/build-lock.d.ts +11 -2
- package/dist/doc-system/site/build-lock.js +12 -1
- package/dist/doc-system/site/build-stamp.d.ts +18 -0
- package/dist/doc-system/site/build-stamp.js +77 -0
- package/dist/doc-system/site/dev-auth.d.ts +15 -3
- package/dist/doc-system/site/dev-auth.js +132 -20
- package/dist/doc-system/site/dev-server.d.ts +13 -0
- package/dist/doc-system/site/dev-server.js +277 -33
- package/dist/doc-system/site/editable-sources.d.ts +14 -0
- package/dist/doc-system/site/editable-sources.js +86 -0
- package/dist/doc-system/site/epub.d.ts +1 -0
- package/dist/doc-system/site/epub.js +73 -1
- package/dist/doc-system/site/generate-site.d.ts +2 -0
- package/dist/doc-system/site/generate-site.js +10 -2
- package/dist/doc-system/site/host-preset.d.ts +13 -0
- package/dist/doc-system/site/host-preset.js +56 -0
- package/dist/doc-system/site/invite-page.d.ts +2 -2
- package/dist/doc-system/site/invite-page.js +10 -3
- package/dist/doc-system/site/orchestrator.d.ts +27 -0
- package/dist/doc-system/site/orchestrator.js +306 -9
- package/dist/doc-system/site/preflight.d.ts +26 -2
- package/dist/doc-system/site/preflight.js +97 -14
- package/dist/doc-system/site/site-config.d.ts +15 -2
- package/dist/filter-builder.js +35 -1
- package/dist/icon-data.js +6 -0
- package/dist/icon-types.d.ts +16 -1
- package/dist/iife.js +112 -93
- package/dist/iife.js.map +42 -42
- package/dist/index-iife.js +20 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +40 -4
- package/dist/live-example/code-transform.d.ts +3 -0
- package/dist/live-example/code-transform.js +58 -12
- package/dist/live-example/component.d.ts +20 -1
- package/dist/live-example/component.js +69 -7
- package/dist/live-example/execution.js +9 -2
- package/dist/live-example/index.d.ts +1 -1
- package/dist/live-example/index.js +1 -1
- package/dist/live-example/insert-examples.js +26 -0
- package/dist/live-example/styles.d.ts +3 -0
- package/dist/live-example/styles.js +14 -0
- package/dist/live-theme.d.ts +1 -1
- package/dist/localize.d.ts +8 -8
- package/dist/menu.d.ts +13 -1
- package/dist/pocket-bar.d.ts +13 -0
- package/dist/pocket-bar.js +58 -6
- package/dist/rich-text.d.ts +2 -1
- package/dist/router.d.ts +1 -1
- package/dist/segmented.d.ts +2 -1
- package/dist/segmented.js +1 -1
- package/dist/sizer.d.ts +1 -1
- package/dist/theme.d.ts +2 -2
- 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 +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,899 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0
|
|
4
|
+
|
|
5
|
+
### Quote style no longer decides whether a live example runs (#141)
|
|
6
|
+
|
|
7
|
+
The live-example import rewriter accepted `'single-quoted'` specifiers and rejected
|
|
8
|
+
`"double-quoted"` ones — the same import, the same `{ named }` form, differing only in quotes.
|
|
9
|
+
|
|
10
|
+
On its own that is a papercut. What made it serious is **Prettier**: it formats fenced code
|
|
11
|
+
inside markdown and normalises quotes to double, so on any project that formats its `.md`,
|
|
12
|
+
every live example was silently converted into a non-running one. The only symptom was a
|
|
13
|
+
build-time warning that reads as advisory.
|
|
14
|
+
|
|
15
|
+
Reported from tosijs-3d-ensemble, where the README's headline example — the one showing what a
|
|
16
|
+
game imports — had never run for the life of the repo. Because it never ran, nothing noticed it
|
|
17
|
+
also named a function the package has never exported. Two defects in the most-read code block
|
|
18
|
+
there, both invisible for the same reason. Found by the doc-test corpus, not by reading.
|
|
19
|
+
|
|
20
|
+
All three context forms (`{ named }`, `* as ns`, default) now accept either quote style, as
|
|
21
|
+
does the `.elements` accessor.
|
|
22
|
+
|
|
23
|
+
**And the error message named the wrong thing.** It listed the supported forms and the context
|
|
24
|
+
packages — so a failing `import { x } from "pkg"`, a listed package in a listed form, was
|
|
25
|
+
answered with a sentence that ruled out its own cause. The reporter spent ~40 minutes rewriting
|
|
26
|
+
a multiline import that was never the problem. The message now separates the two real causes: a
|
|
27
|
+
specifier the context does not carry, versus a clause shape the rewriter cannot handle.
|
|
28
|
+
|
|
29
|
+
### `<tosi-code>`'s own diff overlay is readable by default (#143)
|
|
30
|
+
|
|
31
|
+
`<tosi-diff>` resolves its surface as `--tosi-diff-bg || --background`. Inside `<tosi-code>`,
|
|
32
|
+
`--background` is still the page's white while `--text-color` is deliberately the light code
|
|
33
|
+
colour — so the diff `showDiff()` mounts rendered near-white text on white, and its unchanged
|
|
34
|
+
lines were invisible. The one place the component mounts a diff itself was the one place the
|
|
35
|
+
default did not work.
|
|
36
|
+
|
|
37
|
+
`--background` is now redefined for that subtree to follow `--code-bg`, so the nested diff
|
|
38
|
+
inherits the editor's palette. Deliberately **not** by setting `--tosi-diff-*` here: an inner
|
|
39
|
+
definition beats an inherited one, so that would have overridden the very custom properties
|
|
40
|
+
adopters are already using as an escape hatch — a silent break of the thing the issue was
|
|
41
|
+
about. A consumer's `--tosi-diff-bg` is still checked first and still wins.
|
|
42
|
+
|
|
43
|
+
### Live `ts` examples load a pinned TypeScript compiler
|
|
44
|
+
|
|
45
|
+
tjs-lang's `fromTS` lazy-loads the TypeScript compiler from `DEFAULT_TYPESCRIPT_URL` —
|
|
46
|
+
`https://esm.sh/typescript@5`, an **unpinned major range**. It was the one hop in the entire
|
|
47
|
+
live-example chain that was neither same-origin nor pinned: everything else routes through
|
|
48
|
+
`__TJS_LOCAL_BASE` or a `TJS_VERSION`-pinned CDN. Because it is fetched at runtime it appears
|
|
49
|
+
in no lockfile, so `bun audit` cannot see it and "which compiler ran in a reader's browser" had
|
|
50
|
+
no answer.
|
|
51
|
+
|
|
52
|
+
Now pinned, via the `typescriptUrl` option tjs-lang already exposes, to the **exact version
|
|
53
|
+
this repo type-checks with** — so a doc example is lowered by the same compiler that validates
|
|
54
|
+
the source it was extracted from. A drift test asserts the pin against the **installed**
|
|
55
|
+
compiler rather than the declared range, because `typescript` is a caret dependency here and a
|
|
56
|
+
range check would call `^5.9.3` satisfied while the pin said something else.
|
|
57
|
+
|
|
58
|
+
esm.sh stays as the host — tjs-lang measured it as the only CDN that reliably serves
|
|
59
|
+
`typescript` as ESM. The fix is the pin, not the host; a same-origin copy is the upstream ask.
|
|
60
|
+
|
|
61
|
+
Nobody was exposed: this repo's corpus contains no executable ` ```ts ` fences.
|
|
62
|
+
|
|
63
|
+
### `MenuAction.action` is optional, so a drop-only menu item is expressible
|
|
64
|
+
|
|
65
|
+
`acceptsDrop` + `dropAction` with no `action` — a target you can drag onto but not click — is
|
|
66
|
+
a shape the runtime supports and `popDropMenu`'s docs describe, but the type required `action`.
|
|
67
|
+
When the new typecheck lane compiled the tests for the first time, four literals were silenced
|
|
68
|
+
with `as unknown as MenuItem` rather than the type being corrected.
|
|
69
|
+
|
|
70
|
+
The casts are gone. Two tests now pin what the widened type admits: a drop-only item appears
|
|
71
|
+
**disabled** in a click menu and is removed only under `hideDisabled` — the documented
|
|
72
|
+
`hideDisabled` contract. (The first draft of that test asserted it was filtered out entirely,
|
|
73
|
+
and failed; the finding implied a behaviour the code does not have.)
|
|
74
|
+
|
|
75
|
+
### `buildSite` docs: `dist/` is reset only when the build owns it (#130)
|
|
76
|
+
|
|
77
|
+
Three shipped surfaces still promised an unconditional reset — the config table and the
|
|
78
|
+
gotchas list in `doc-site-system.md`, and the `prebuild` JSDoc, which reaches
|
|
79
|
+
`dist/doc-system/site/site-config.d.ts` and is what an adopter reads on editor hover.
|
|
80
|
+
|
|
81
|
+
`dist/` is cleared only when `emitLibrary` or `libraryTsconfig` means this build produces it.
|
|
82
|
+
A `libraryBuild` function owns the directory and may deliberately emit a subset, so it is never
|
|
83
|
+
cleared — **if you supply `libraryBuild`, cleaning `dist/` is yours.** The output dir is still
|
|
84
|
+
always reset, which is the part `prebuild` authors actually need to know.
|
|
85
|
+
|
|
86
|
+
### Security: the source editor can no longer write files the build executes (#128)
|
|
87
|
+
|
|
88
|
+
`editableSources` writes were contained only to the project root — which includes
|
|
89
|
+
`.git/hooks/*`, `bunfig.toml` (preload), `package.json` scripts and `bin/`, every one of which
|
|
90
|
+
runs on the developer's next ordinary command. With CSRF closed (#90/#121) there was no known
|
|
91
|
+
path to an unauthorised write; this removes the *consequence* of a future one.
|
|
92
|
+
|
|
93
|
+
`editableSourcePaths` / `mayEditSource` build an **allow-list derived from the doc corpus**,
|
|
94
|
+
not a deny-list of dangerous names: the endpoint exists to edit the source of a page you are
|
|
95
|
+
looking at, so the writable set is exactly what the extractor scraped. A deny-list needs
|
|
96
|
+
extending every time someone invents a new way to make a file execute and is wrong by default;
|
|
97
|
+
this is right by default. It **fails closed** — an unreadable corpus permits nothing.
|
|
98
|
+
|
|
99
|
+
**The first version of this was not enough, and the pre-release review caught it.** A doc
|
|
100
|
+
source can itself be an executed script: `bin/make-icon-data.js` carries a `/*#` block, so it
|
|
101
|
+
was a doc source, so it was writable — and `bin/dev.ts` spawns it on *every* build. The
|
|
102
|
+
original tests passed because the synthetic corpus omitted `bin/`, and the live check used
|
|
103
|
+
`.git/hooks/pre-commit`, which the corpus never contained. Both proved something true and
|
|
104
|
+
irrelevant. There is now an `isExecutedByBuild` deny layer (all of `bin/`, the spawned `-cli`
|
|
105
|
+
scripts, `generate-css`), and the tests run against the **shipped** corpus with an assertion
|
|
106
|
+
that the dangerous entry is really in it — otherwise the refusal test proves nothing.
|
|
107
|
+
|
|
108
|
+
The **read** half is gated by the same predicate. It was left open initially on the grounds
|
|
109
|
+
that the reported issue was a drive-by write; the review was right that the argument for
|
|
110
|
+
symmetry was stronger, since "view source" only ever reads a doc's own source.
|
|
111
|
+
|
|
112
|
+
### The dependency audit knows where a package sits (#56)
|
|
113
|
+
|
|
114
|
+
`runtimeReachable` / `classifyReach` walk the manifest graph and label an advisory's package
|
|
115
|
+
**runtime** or **build-only**. Reported from an app monorepo running `buildSite`: 18
|
|
116
|
+
high/critical advisories of which the runtime-reachable subset was a small fraction, so
|
|
117
|
+
blocking on all of them would have bricked local dev over risk they do not carry.
|
|
118
|
+
|
|
119
|
+
Conservative by construction — root `dependencies`, `optionalDependencies` **and
|
|
120
|
+
`peerDependencies`** are runtime seeds (a peer ends up in the consumer's app), and an
|
|
121
|
+
unresolvable package counts as reachable. A finding silently downgraded is the one outcome
|
|
122
|
+
worth engineering against.
|
|
123
|
+
|
|
124
|
+
`audit.blockOn: 'severity' | 'runtime'` defaults to `'severity'` — today's behaviour. Set
|
|
125
|
+
`'runtime'` and a build-only finding is reported but does not block. If the manifest walk fails
|
|
126
|
+
the filter is disabled entirely, because "we could not tell" must not read as "not reachable".
|
|
127
|
+
|
|
128
|
+
A build that passes **only because of an active gate** now says so:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
🟡 Build: PASSING ON A WAIVER — 1 finding(s) at or above high are suppressed
|
|
132
|
+
by an active gate. This is not a clean audit.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Only when the waiver is load-bearing: a gate suppressing something below the threshold would
|
|
136
|
+
have passed anyway, and flagging that would train people to ignore the marker.
|
|
137
|
+
|
|
138
|
+
### The doc-test runner stopped executing pages that only *mention* tests
|
|
139
|
+
|
|
140
|
+
`docsWithTests` matched ` ```test ` as a **substring anywhere in the document**, so a page
|
|
141
|
+
merely writing about the test tier qualified — and was rendered in a background iframe with its
|
|
142
|
+
examples executed. A failure in an example on a page with *no tests* therefore surfaced through
|
|
143
|
+
the doc-test tier, and only sometimes, depending on how quickly the TypeScript compiler loaded.
|
|
144
|
+
|
|
145
|
+
That produced a **false green**: a standalone doc-tests run reported "62 passed" against a build
|
|
146
|
+
the full suite failed on. Page selection now requires a fence at the start of a line, the same
|
|
147
|
+
rule the doc extractor applies to `/*#`.
|
|
148
|
+
|
|
149
|
+
The example it was hiding was ours: a ` ```ts ` fence in `doc-site-system.md` — added in the
|
|
150
|
+
#93 write-up — was **executable**, so the doc-test lane ran `Bun.spawn` and `process.on` in a
|
|
151
|
+
browser and threw `ReferenceError: process is not defined`. It is now ` ```typescript `, which
|
|
152
|
+
is display-only. One character, and it had been red on `main` from the moment it was pushed.
|
|
153
|
+
|
|
154
|
+
Related and still open (#138): an example that *compiles and then throws* is caught by nothing
|
|
155
|
+
on a page without a ` ```test ` block. The build-time check catches what will not compile, not
|
|
156
|
+
what compiles and fails.
|
|
157
|
+
|
|
158
|
+
### An existing `firebase.json` is checked against `outputDir` (#134)
|
|
159
|
+
|
|
160
|
+
`host: 'firebase'` scaffolds a config for a fresh project and keeps its hands off an existing
|
|
161
|
+
one — but checked nothing, so `buildSite` could write `docs/` while `firebase deploy` published
|
|
162
|
+
`.demo/`. Both commands succeed and the site serves whatever was there last. It now warns,
|
|
163
|
+
handling multi-site arrays, framework-aware hosting with no `public`, and `./docs` vs `docs`
|
|
164
|
+
spelling, and warns only when *every* declared target disagrees.
|
|
165
|
+
|
|
166
|
+
### tjs-lang 0.13.4 → 0.13.11, and the two pins can no longer drift (#135)
|
|
167
|
+
|
|
168
|
+
We were pinned to a version npm has **deprecated**, seven releases behind. The deprecation's
|
|
169
|
+
threat model does not reach live-example — it is about a server transpiling submitted source —
|
|
170
|
+
but sitting on a deprecated version with seven releases of fixes is its own problem.
|
|
171
|
+
|
|
172
|
+
`TJS_VERSION` (what the *published site* fetches from a CDN) and the exact devDependency (what
|
|
173
|
+
tests and local builds use) live in different files, so a test now asserts they agree. Drift
|
|
174
|
+
there is invisible exactly where it matters: every lane stays green while the deployed site
|
|
175
|
+
transpiles with a different version.
|
|
176
|
+
|
|
177
|
+
### `<tosi-example>` warns when executable fences collide (#139)
|
|
178
|
+
|
|
179
|
+
`js`/`tjs`/`ts` all write the same single-valued slot, so a second executable fence silently
|
|
180
|
+
overwrote the first — the earlier block vanished from the page, the example ran as the later
|
|
181
|
+
dialect, and nothing said so. Behaviour is unchanged (last one still wins); it now names the
|
|
182
|
+
file and which dialect survived. This is the seam a dialect selector needs, which is why it is
|
|
183
|
+
worth fixing before that feature rather than during it.
|
|
184
|
+
|
|
185
|
+
### `getSelectText` returned the empty string on every call
|
|
186
|
+
|
|
187
|
+
`<tosi-filter>` read `select.options[select.selectedIndex]`, and `selectedIndex` has never
|
|
188
|
+
existed on `TosiSelect` — it selects by value. So every filter chip read ` "needle"` with two
|
|
189
|
+
blanks where the field and condition belong, which looks like a styling bug. Found by adopting
|
|
190
|
+
tosijs 1.10.0, whose removal of `Component`'s `[key: string]: any` index signature made the
|
|
191
|
+
compiler able to see it for the first time.
|
|
192
|
+
|
|
193
|
+
### Markdown is no longer formatted, and `bun format` is green again
|
|
194
|
+
|
|
195
|
+
`*.md` is in `.prettierignore` by request from tosijs. `proseWrap` was already `preserve`, so
|
|
196
|
+
prettier's only remaining markdown behaviours were escaping literal characters
|
|
197
|
+
(`a * literal` → `a \* literal`), padding table cells, and rewriting `*` bullets — the first of
|
|
198
|
+
which edits a document's *content*, and markdown is the product here.
|
|
199
|
+
|
|
200
|
+
Separately, `bun format` had been exiting 1 on seven ESLint errors: CI gates `format-check`,
|
|
201
|
+
which runs prettier only, so the ESLint half of the documented pre-commit command was red
|
|
202
|
+
without failing anything. Among the fixes: two dead locals in the CSRF same-origin check
|
|
203
|
+
(checked rather than assumed — the logic was sound, only the initialisers were dead); a dead
|
|
204
|
+
`explanation` in `dev-server.ts` that was built and never used, left over from before
|
|
205
|
+
`invite-page.ts` took ownership of that copy in #114; and an
|
|
206
|
+
`AsyncFunction` construction failure that now rethrows with `{ cause }` so the engine's own
|
|
207
|
+
error — the one naming *which* parameter it choked on — is not discarded.
|
|
208
|
+
|
|
209
|
+
### `icons.*` was typed `SVGElement`, and composites are spans
|
|
210
|
+
|
|
211
|
+
**This is why the release is a minor.** `SVGIconMap` declared every icon as
|
|
212
|
+
`ElementCreator<SVGElement>`. A **composite** icon — any name containing `$`, the stacking
|
|
213
|
+
syntax `icon-composition.md` documents and the demos use throughout — returns an
|
|
214
|
+
`HTMLSpanElement`.
|
|
215
|
+
|
|
216
|
+
Measured rather than reasoned about:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
icons['tosiHat$tosi']() → SPAN, instanceof SVGElement === false
|
|
220
|
+
icons.lock() → svg, instanceof SVGElement === true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`resolveIcon` has always returned the honest `Element`; only the cast on the exported proxy
|
|
224
|
+
disagreed. It is now `ElementCreator<IconElement>`, and **`IconElement` is exported from
|
|
225
|
+
`icon-types.ts` for consumers holding an icon** — defined as `SVGElement | HTMLSpanElement`.
|
|
226
|
+
Both members carry `.style` and `.dataset`,
|
|
227
|
+
which is what callers actually reach for — widening all the way to `Element` would break
|
|
228
|
+
working code to no purpose.
|
|
229
|
+
|
|
230
|
+
**Why a minor rather than a patch.** Nothing about the runtime changed; a composite has always
|
|
231
|
+
been a span. But code that annotated an icon as `SVGElement` compiled before and does not now,
|
|
232
|
+
and "a patch never breaks you" is the contract. This breaks you out of a false belief, which is
|
|
233
|
+
still breaking.
|
|
234
|
+
|
|
235
|
+
**The lie was load-bearing.** Correcting it surfaced **14 sites in this repo** — `data-table`,
|
|
236
|
+
`notifications`, `rich-text`, `segmented` — that stored or passed an icon as `SVGElement`. Any
|
|
237
|
+
consumer doing the same has the same latent bug, which is the argument for fixing the type
|
|
238
|
+
rather than the tests. If your build newly complains, that is the bug being shown to you: the
|
|
239
|
+
fix is `IconElement`, or `as SVGElement` at a site you know is not a composite.
|
|
240
|
+
|
|
241
|
+
### The test suite is type-checked now (#73)
|
|
242
|
+
|
|
243
|
+
`tsconfig.json` excludes `*.test.ts` — right for emit, and it meant nothing ever compiled them.
|
|
244
|
+
**48 errors had accumulated unseen** (the issue counted 40 when it was filed; it grows).
|
|
245
|
+
`tsconfig.tests.json` plus a CI step brings it to zero and holds it there.
|
|
246
|
+
|
|
247
|
+
The icons defect above is what that gate found on its first run. Two test files carried
|
|
248
|
+
duplicate imports — `sessionCookie` and `nodePath` — which is invalid ESM that the runtime
|
|
249
|
+
tolerated and nothing checked. The other 46 were genuine test-side noise, fixed honestly
|
|
250
|
+
rather than with blanket `any`.
|
|
251
|
+
|
|
252
|
+
A type-level test asserts nothing if nothing compiles it — which was already the argument for
|
|
253
|
+
`typecheck-guards`, applied to everything else.
|
|
254
|
+
|
|
255
|
+
### The test lane itself, honestly
|
|
256
|
+
|
|
257
|
+
Three things the pre-release review found in the tests rather than in the code, recorded
|
|
258
|
+
because a suite that is quietly wrong is worse than one that is loudly red.
|
|
259
|
+
|
|
260
|
+
**Six tests were deleted and the suite stayed green.** Adding the #139 cases to
|
|
261
|
+
`insert-examples.test.ts`, `cat >` was used instead of `cat >>`, wiping two `describe` blocks
|
|
262
|
+
that covered still-shipping behaviour — grouping across a baked `<script>`, and fenced
|
|
263
|
+
execution modes. Nothing complained, because fewer tests all passed. That is the exact shape of
|
|
264
|
+
a coverage regression no gate catches: green is not a measurement of how much you asked. The
|
|
265
|
+
blocks are restored, and the #139 cases rewritten against the same stub creator the originals
|
|
266
|
+
used.
|
|
267
|
+
|
|
268
|
+
**`ENOENT reading "https://esm.sh/typescript@5"` during `bun test`** came from the same
|
|
269
|
+
rewrite: the real `liveExample` transpiled a `ts` fence, and the rejection leaked across test
|
|
270
|
+
files, printing under an unrelated file's header. Zero occurrences now — and worth noting that
|
|
271
|
+
the message named a URL while claiming `ENOENT`, which is why it read as noise for as long as
|
|
272
|
+
it did.
|
|
273
|
+
|
|
274
|
+
**`caddy-install.test.ts` was flaky about one run in ten.** Each of its tests cold-starts a
|
|
275
|
+
real `bun` to exercise the generated remote script, so its cost is interpreter startup — ~1.2s
|
|
276
|
+
each, with the file swinging 3.5s to 10.1s under load. Under bun's 5s default that put single
|
|
277
|
+
tests over the line whenever the machine was busy. They now carry a 20s budget that describes
|
|
278
|
+
the work; a test exceeding *that* is a real defect rather than machine load.
|
|
279
|
+
|
|
280
|
+
### Every bin answers `--help`, and refuses what it does not understand (#85)
|
|
281
|
+
|
|
282
|
+
`caddy-install`, `deploy-preview`, `tunnel` and `release-notes` each carried their own
|
|
283
|
+
`flag()`/`has()` pair. One shared `parseArgv` replaces all four.
|
|
284
|
+
|
|
285
|
+
**Refusing unknown flags is the load-bearing part.** `tosijs-caddy-install --status` — a flag it
|
|
286
|
+
never had — used to read as "no flags at all", and the bin went on to ssh into the configured
|
|
287
|
+
host. The same shape was still live in `tosijs-deploy`. A tool that contacts a machine must not
|
|
288
|
+
read an instruction it does not understand as consent to proceed.
|
|
289
|
+
|
|
290
|
+
`--help` comes with it, and is parsed **before** the site config is read, so it works from any
|
|
291
|
+
directory. That matters because no lane runs a shipped bin's argument handling —
|
|
292
|
+
`smoke-consumer.ts` checks shebangs and deliberately does not execute the bins that reach the
|
|
293
|
+
network — so `tosijs-release-notes --help` shipped unrecognised and died on a raw `ShellError`
|
|
294
|
+
outside a git repo.
|
|
295
|
+
|
|
296
|
+
Also: `deploy:index` now asks for `PREVIEW_HOST` like the bins, instead of the `PREVIEW_SSH`
|
|
297
|
+
they moved off.
|
|
298
|
+
|
|
299
|
+
### Magic-link codes are eight letters, no digits (#132)
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
🔗 code: JCYGCCVX (case-insensitive)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The code is typed by hand into a floating keyboard on a headset. Crockford base32 was already
|
|
306
|
+
chosen for typo-resistance and that was right as far as it went — but it contained **digits**,
|
|
307
|
+
and on a headset the letter↔number switch is a trip to a different keyboard page. A reported XR
|
|
308
|
+
session minted `A70MDCD` and `X9Q3Z53`: three digits each in seven characters, so three round
|
|
309
|
+
trips for a code being gaze-and-pinched one glyph at a time.
|
|
310
|
+
|
|
311
|
+
**The extra character is free; the mode switch is not.** Eight letters of a 22-letter alphabet
|
|
312
|
+
is **35.7 bits against the old seven-of-32's 35.0** — slightly _stronger_, while never leaving
|
|
313
|
+
the alphabetic keyboard.
|
|
314
|
+
|
|
315
|
+
`I`, `L`, `O` and `U` stay excluded. `I`/`L` are indistinguishable across case, which matters
|
|
316
|
+
_more_ now that there are no digits, because the code is case-insensitive so the reader cannot
|
|
317
|
+
use case to tell them apart. Redemption also maps back the digits someone might type for a
|
|
318
|
+
letter they misread (`5`→`S`, `2`→`Z`, `8`→`B`, `6`→`G`) — pure upside, since a digit can never
|
|
319
|
+
be part of a real code — and ignores hyphens and spaces.
|
|
320
|
+
|
|
321
|
+
Rate limiting is unchanged and still does the heavy lifting: redemption is serialized at ~10
|
|
322
|
+
attempts/second, so 22⁸ takes ~174 years to exhaust and a five-minute window is about 1 in 18
|
|
323
|
+
million.
|
|
324
|
+
|
|
325
|
+
`dev-auth.ts` itself went on arguing from **32⁷** — ~111 years, 1 in 11 million — for the whole
|
|
326
|
+
of the change that made the token 22⁸. The conclusion never stopped holding, which is precisely
|
|
327
|
+
why nothing surfaced it: a safety argument that survives its own premises being replaced is not
|
|
328
|
+
being read. The figures are corrected in place, with the superseded ones kept beside them, since
|
|
329
|
+
this is the one file where the numbers *are* the argument rather than a description of it.
|
|
330
|
+
|
|
331
|
+
The invite page's code box (shipped for #75) covers most of the rest of the ask: bookmark the
|
|
332
|
+
host once, then each session is eight letters into a text field. The `dev.tosijs.net/<code>`
|
|
333
|
+
short URL needs host-side infrastructure and is tracked in `TODO.md`.
|
|
334
|
+
|
|
335
|
+
### BREAKING (narrow): the root barrel no longer re-exports the doc system (#133)
|
|
336
|
+
|
|
337
|
+
`import { … } from 'tosijs-ui'` no longer gives you `code-editor`, `doc-browser`,
|
|
338
|
+
`doc-system/doc-system` or `live-example`. **Import them by subpath instead** — they were
|
|
339
|
+
already reachable that way, and nothing else changes:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { tosiCode } from 'tosijs-ui/code-editor'
|
|
343
|
+
import { createDocBrowser } from 'tosijs-ui/doc-browser'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Why.** `code-editor` pulls CodeMirror; `live-example` and `doc-system/doc-system` each pull
|
|
347
|
+
`tjs-lang` independently. This package has no `sideEffects` field — correctly, since
|
|
348
|
+
`elementCreator()` registers elements at import time and a blanket `sideEffects: false` shakes
|
|
349
|
+
a bare `import 'tosijs-ui'` down to zero registrations — so a bundler must treat every
|
|
350
|
+
`export *` target as side-effectful and cannot drop it. All four were therefore eager in any
|
|
351
|
+
app that imported _anything_ from the barrel.
|
|
352
|
+
|
|
353
|
+
Measured by snowfox-app moving a 15MB React bundle to the bun bundler, and reproduced here:
|
|
354
|
+
|
|
355
|
+
| | size |
|
|
356
|
+
| ----------------------- | ------------------------------------ |
|
|
357
|
+
| barrel as shipped | 1.68 MB |
|
|
358
|
+
| barrel without the four | 0.38 MB |
|
|
359
|
+
| real app bundle | 15.21 → 13.86 MB (**1.35 MB, 8.9%**) |
|
|
360
|
+
|
|
361
|
+
The doc cluster was **77% of the barrel**. For contrast the 3D, map and lottie components —
|
|
362
|
+
the ones you would expect to be the heavy tail — cost about 10kB, because they already
|
|
363
|
+
lazy-load properly.
|
|
364
|
+
|
|
365
|
+
The doc site and CDN `<script>` users are unaffected: `index-iife.ts` imports all four
|
|
366
|
+
explicitly, so the weight lands on the bundle built _for_ the doc site rather than on an app
|
|
367
|
+
that imports a button.
|
|
368
|
+
|
|
369
|
+
### New: `tosijs-ui/codemirror` — extend `<tosi-code>.editor` without a silent no-op (#131)
|
|
370
|
+
|
|
371
|
+
`<tosi-code>` exposes the raw `EditorView` and the 1.7 notes invite you to configure CodeMirror
|
|
372
|
+
through it. But CodeMirror 6 keys facets, `StateField`s and gutters by **object identity**, so
|
|
373
|
+
an extension built from a _second copy_ of `@codemirror/state` or `@codemirror/view` is not
|
|
374
|
+
mismatched — it is **silently ignored**. No error, no warning; the gutter never renders.
|
|
375
|
+
|
|
376
|
+
Reported with a real repro: a consumer's `@codemirror/view` resolved to `6.43.6` while ours
|
|
377
|
+
hoisted to a nested `6.43.10`, so their `gutter()` and `GutterMarker` came from a different
|
|
378
|
+
instance than the view. `@codemirror/state` happened to dedupe, which made it worse — enough
|
|
379
|
+
worked that the failure looked like a bug in their gutter code.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { gutter, GutterMarker, StateField, StateEffect } from 'tosijs-ui/codemirror'
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Only `@codemirror/state` and `@codemirror/view` are re-exported — the two needed to extend a
|
|
386
|
+
view, exactly as the report identified.
|
|
387
|
+
|
|
388
|
+
**Why not peer dependencies**, which the issue proposed and is the obvious fix: a required peer
|
|
389
|
+
would make every consumer install two CodeMirror packages including the overwhelming majority
|
|
390
|
+
who never render an editor (`elementCreator()` registers eagerly, so importing tosijs-ui is not
|
|
391
|
+
evidence of using it), and an _optional_ peer is worse than either — the component silently
|
|
392
|
+
does nothing until you discover you needed 12 packages. Re-exporting removes the failure by
|
|
393
|
+
construction rather than policing it: there is no version to match and no `overrides` to write,
|
|
394
|
+
because there is only ever one copy.
|
|
395
|
+
|
|
396
|
+
### `buildSite` no longer wipes `dist/` — it is your package output, not a site artifact (#130)
|
|
397
|
+
|
|
398
|
+
The prebuild ran `rm -rf dist/` on **every** `buildSite`, including from `devServer` and
|
|
399
|
+
including for projects that configure no library build at all. For the repos this API exists to
|
|
400
|
+
serve, `dist/` is the **published package output** — an input to `npm publish` — so the wipe
|
|
401
|
+
destroyed build products the current run does not rebuild.
|
|
402
|
+
|
|
403
|
+
Reported from tosijs with the full chain: two bundles (`module.debug.js`, `module.safe.js`,
|
|
404
|
+
exposed as the `./debug` and `./safe` subpath exports) are produced only under `--build`,
|
|
405
|
+
because they need a 53-file transpile too slow for the dev loop. `bun start` — or Playwright's
|
|
406
|
+
`webServer`, which runs it — wiped `dist/` and rebuilt five of seven. A publish from that tree
|
|
407
|
+
ships two subpaths that throw `ERR_MODULE_NOT_FOUND`, and it reached a commit once already.
|
|
408
|
+
Nothing local could catch it: size and smoke gates iterate the bundles the current run _built_,
|
|
409
|
+
so a bundle that was **deleted rather than skipped** is invisible to all of them.
|
|
410
|
+
|
|
411
|
+
The rule is now "clean only what you wholly generate":
|
|
412
|
+
|
|
413
|
+
| configuration | `dist/` cleaned? |
|
|
414
|
+
| -------------------------------------------------------------- | ---------------- |
|
|
415
|
+
| `emitLibrary` / `libraryTsconfig` (tsc emits the complete set) | yes |
|
|
416
|
+
| `libraryBuild` — a consumer function that may emit a subset | **no** |
|
|
417
|
+
| none configured — nothing here owns it | **no** |
|
|
418
|
+
|
|
419
|
+
The doc `outputDir` wipe is unchanged and stays: that directory is wholly generated, and the
|
|
420
|
+
build announces it. `dist/` is different.
|
|
421
|
+
|
|
422
|
+
Incidental: `emitLibrary` runs `tsc --incremental`, whose `.tsbuildinfo` lives in `dist/`.
|
|
423
|
+
Wiping it every run meant incremental compilation had never once done anything.
|
|
424
|
+
|
|
425
|
+
### `bun start` serves in ~3.6s instead of ~23s, and prints 13 lines instead of 37
|
|
426
|
+
|
|
427
|
+
**20.1 of those 23 seconds were the dependency audit**, running synchronously before the port
|
|
428
|
+
was bound — where it hit its own 20s timeout, gave up, and **failed open**. So the stall
|
|
429
|
+
bought nothing on that run: it was the cost of a check that did not happen. Measured against
|
|
430
|
+
the live registry, `bun audit` takes **79.5s**.
|
|
431
|
+
|
|
432
|
+
The code justified blocking on the grounds that "the audit is sub-second… a gate you wait for
|
|
433
|
+
cannot be raced". The premise was false, so the conclusion was buying a twenty-second stall on
|
|
434
|
+
every start. It now runs after the server is up and reports when it lands, prefixed loudly
|
|
435
|
+
enough that a late advisory cannot be mistaken for part of the startup banner — non-blocking
|
|
436
|
+
must not mean easy to miss. **Release builds still block**: `buildSite` audits synchronously,
|
|
437
|
+
and `--test` gates there too, which is where waiting is the right trade.
|
|
438
|
+
|
|
439
|
+
**Build output is 13 lines, down from 37.** Eighteen of them were `bun build` listing every
|
|
440
|
+
chunk and every sourcemap — the same size information the build already summarises, only
|
|
441
|
+
longer. Captured now, and replayed **in full** on failure; verified by deliberately breaking
|
|
442
|
+
the bundle entry, which still prints the errors, exits 1, and restores the last good build.
|
|
443
|
+
|
|
444
|
+
Spam is not free: it trains people — and agents — to stop reading build output, which is
|
|
445
|
+
exactly what lets a real failure through.
|
|
446
|
+
|
|
447
|
+
The ePub was the assumed culprit and is innocent: 0.34s of the 23. Measuring first is the only
|
|
448
|
+
reason the actual cause was found.
|
|
449
|
+
|
|
450
|
+
### `docs.json` is cache-busted, so a stale corpus stops surviving a deploy
|
|
451
|
+
|
|
452
|
+
Every generated URL went through the asset stamp — the stylesheet, the hydration bundle, the
|
|
453
|
+
IIFE — **except the one that matters most**. `docs.json` is the entire corpus: nav, routes and
|
|
454
|
+
every page's content, and it was emitted bare.
|
|
455
|
+
|
|
456
|
+
A static host sends no `Cache-Control` for it, so browsers apply **heuristic** caching — free
|
|
457
|
+
to invent a freshness lifetime when you decline to state one. The site then renders a previous
|
|
458
|
+
deploy's corpus against the current bundle, and nothing in the console says so. It presents as
|
|
459
|
+
pages that will not update, and the fix is a hard reload nobody thinks to try. It cost a
|
|
460
|
+
maintainer a long debugging session before it was traced to a Chrome cache entry.
|
|
461
|
+
|
|
462
|
+
The corpus gets **its own stamp**, hashed from `docs.json`'s own bytes rather than the project
|
|
463
|
+
version. The version is right for the bundles — it moves once per release, which is when they
|
|
464
|
+
change. The corpus does not work that way: it changes whenever anyone edits a doc, many times
|
|
465
|
+
within one version, and a preview host redeployed mid-version is the normal way this project
|
|
466
|
+
reads its own site. A version-keyed corpus URL would have been stale for exactly that workflow.
|
|
467
|
+
|
|
468
|
+
The dev server was already correct here — it sends `no-store, must-revalidate` on everything,
|
|
469
|
+
verified. This was only ever the built static site.
|
|
470
|
+
|
|
471
|
+
### `<tosi-table>` stops silently capping at 10,000 rows (#82)
|
|
472
|
+
|
|
473
|
+
`maxVisibleRows` defaulted to a flat `10000` and `slice`d everything past it **without a
|
|
474
|
+
word**. A 25,000-row table showed 10,000, and every count, filter and sort then ran on the
|
|
475
|
+
truncated set — self-consistent, and disagreeing with the data. The property was documented
|
|
476
|
+
nowhere, so the only way to find it was to lose rows and go looking.
|
|
477
|
+
|
|
478
|
+
The number was wrong because it was sized as a _rendering-cost_ limit. It is a **layout**
|
|
479
|
+
limit. A virtual `listBinding` renders only the visible window, so the UI cost is O(1) in row
|
|
480
|
+
count — 300,000 rows scroll like 300 — and the array size is irrelevant. What binds is the
|
|
481
|
+
spacer element's height against the maximum height a browser will lay out.
|
|
482
|
+
|
|
483
|
+
So the cap is now derived at runtime: `maxElementHeight / rowHeight`, which at the default
|
|
484
|
+
30px rows is on the order of half a million to a million rows. **Probed, not hardcoded**,
|
|
485
|
+
because the ceiling is engine- _and version_-specific — two Chromium measurements in this
|
|
486
|
+
project sit a factor of two apart (Chrome 151 clamps at 16777214px, the Playwright Chromium
|
|
487
|
+
at 33554428px). Any constant would be wrong for somebody, in the direction that loses rows.
|
|
488
|
+
|
|
489
|
+
And when a cap does bite, it now **says so in the console** with both numbers and how to
|
|
490
|
+
raise it. `rowHeight: 0` stays conservative on purpose: with no fixed row height there is no
|
|
491
|
+
virtualisation, every row is a real DOM node, and the cost genuinely is O(n).
|
|
492
|
+
|
|
493
|
+
Set `table.maxVisibleRows = n` to override, exactly as before.
|
|
494
|
+
|
|
495
|
+
### A large `rowHeight: 0` table now complains, loudly (#84)
|
|
496
|
+
|
|
497
|
+
`rowHeight: 0` turns virtualisation off — every row becomes a real DOM node, and sorting,
|
|
498
|
+
filtering and group toggles walk all of them on every render. It is documented for "smaller
|
|
499
|
+
tables, or tables with variable row-heights", and that is the only thing it is for.
|
|
500
|
+
|
|
501
|
+
Past **1,000 rows** the table now warns **on every render** until a `rowHeight` is set. That
|
|
502
|
+
is deliberate, and deliberately unlike every other notice in this codebase, which fire once:
|
|
503
|
+
those describe states a developer may not be able to change, whereas this is a one-line fix
|
|
504
|
+
that persists until made, and a single notice scrolls out of the console and is gone.
|
|
505
|
+
|
|
506
|
+
The originally suggested fix was to binary-search the scroll-anchor walk. That was declined:
|
|
507
|
+
it optimises a configuration nobody should be in, and makes a misconfiguration marginally
|
|
508
|
+
less bad instead of surfacing it. A table big enough for the cost to matter is big enough to
|
|
509
|
+
virtualise, and virtual tables are O(1) in row count.
|
|
510
|
+
|
|
511
|
+
### `<tosi-3d>` is deprecated — use `tosijs-3d`
|
|
512
|
+
|
|
513
|
+
Nothing breaks: it still works, still ships, and stays until a future major. But new work
|
|
514
|
+
should use **[`tosijs-3d`](https://www.npmjs.com/package/tosijs-3d)**.
|
|
515
|
+
|
|
516
|
+
It is a much better library, and maintaining both is duplicated effort spent against the
|
|
517
|
+
weaker one. `<tosi-3d>` is a thin Babylon wrapper that grew inside a general-purpose UI
|
|
518
|
+
library; `tosijs-3d` is a project actually about 3D and XR, with the declarative component
|
|
519
|
+
set and XR support that implies. Every hour spent here is an hour not spent there, and the
|
|
520
|
+
result would still be the lesser of the two.
|
|
521
|
+
|
|
522
|
+
`tosijs-3d` grew out of this component, so migration is mostly a rename.
|
|
523
|
+
|
|
524
|
+
The deprecation appears in three places, deliberately: the doc page (what people read), the
|
|
525
|
+
`@deprecated` tags on `B3d`/`b3d` (what editors surface), and a console notice printed **once
|
|
526
|
+
per page on first use** — not at import, since `elementCreator()` registers eagerly and a bare
|
|
527
|
+
`import 'tosijs-ui'` is not evidence anyone is using this; and not per instance, because a page
|
|
528
|
+
with several scenes would spam its own console, which is how a deprecation notice teaches
|
|
529
|
+
people to filter warnings rather than act on them.
|
|
530
|
+
|
|
531
|
+
### The machine-health guard now reports 13GB of orphans instead of skipping them (#93)
|
|
532
|
+
|
|
533
|
+
`isDevProcess` decided what was even a _candidate_ for the runaway check, and excluded
|
|
534
|
+
`build` — because we spawn the bundler ourselves and a build peaking mid-run must not look
|
|
535
|
+
like a runaway to its own parent. But the exemption did not say "our bundler". It said
|
|
536
|
+
**every `bun build` on the machine, forever, regardless of owner or age**, so an orphan whose
|
|
537
|
+
parent died a week ago was exactly as exempt as one that started 200ms ago.
|
|
538
|
+
|
|
539
|
+
Reported with measurements: **69 orphaned `bun build --watch` processes holding 13.09GB**, ~55
|
|
540
|
+
of them spawned inside a single 61-minute window, on a 128GB box running these very dev servers
|
|
541
|
+
and sitting at 116MB free. The guard looked, and skipped every one — while resting on the
|
|
542
|
+
premise that "nothing noticed the runaways because nothing ever looked".
|
|
543
|
+
|
|
544
|
+
`--watch` is the distinguishing signal, and it was right there in the argv: a watcher is a
|
|
545
|
+
server whoever started it. The mid-rebuild false positive the exemption existed for is already
|
|
546
|
+
handled, and handled _precisely_, by parentage — the assessor filters `process.pid` plus
|
|
547
|
+
`descendantsOf(process.pid)` before assessing anything. That is the exact test where a
|
|
548
|
+
subcommand name was a blunt proxy for it, so this costs no false positives. `bun run` with
|
|
549
|
+
`--watch` is caught too; the reported machine had four such servers up to 18 days old.
|
|
550
|
+
|
|
551
|
+
**Visible is not the same as caught**, and the first pass at this shipped the difference. Every
|
|
552
|
+
trigger in the assessor asks "is _one_ process too big?", and 194MB is nowhere near any of them —
|
|
553
|
+
so dropping the exemption walked the guard past all 69 orphans and returned `ok` for 13GB, while
|
|
554
|
+
the note above claimed they had been dealt with. The assessor now carries a rule that never
|
|
555
|
+
consults RSS: **a watcher whose parent is gone (`ppid` 1) is waste at any size**, reported with
|
|
556
|
+
the `kill` command. One 12MB orphan warns. It warns rather than refuses, because a build that
|
|
557
|
+
will not start because of someone's stray process is a guard people switch off.
|
|
558
|
+
|
|
559
|
+
"A bunch of similar names" was the other candidate signal and is deliberately _not_ used: three
|
|
560
|
+
identical `bun --watch bin/dev.ts` lines are usually three sibling projects each running their
|
|
561
|
+
own server. Identical argv is not duplicate work — the distinguishing fact is the cwd, which
|
|
562
|
+
`ps` does not report.
|
|
563
|
+
|
|
564
|
+
**And the other half, in `doc-site-system.md`:** "shell out so the OS reclaims the memory on
|
|
565
|
+
exit" is only true if the child _exits_. A `dev.ts` that spawns `bun build --watch`, itself run
|
|
566
|
+
under `bun --watch dev.ts`, leaks one bundler per keystroke — the previous child is never
|
|
567
|
+
signalled. The doc now carries the reaping idiom (kill the previous child; reap on
|
|
568
|
+
`SIGINT`/`SIGTERM`/`beforeExit`) beside the advice that produced the leak. This project's own
|
|
569
|
+
builds are one-shot and were never affected.
|
|
570
|
+
|
|
571
|
+
### `layout: "full-screen"` finally gets its scroll container (#119)
|
|
572
|
+
|
|
573
|
+
`.doc-content`'s `overflow` is an **inline** style set by the doc-browser, so the full-screen
|
|
574
|
+
layout's stylesheet `overflow: auto` was competing with an inline style and always lost. The
|
|
575
|
+
`height: 100%` landed and the scroller did not — a full-screen page taller than its box was
|
|
576
|
+
clipped and unscrollable. It sat one line below a `padding` that correctly went through a
|
|
577
|
+
variable, with a comment explaining exactly why it had to.
|
|
578
|
+
|
|
579
|
+
Both now go through `--doc-content-overflow` (default `hidden`), on the inline style **and**
|
|
580
|
+
the pre-hydration rule — they have to match, or an override would take effect only after
|
|
581
|
+
hydration and the page would visibly change shape.
|
|
582
|
+
|
|
583
|
+
The second consequence is the one worth knowing about: `overflow: hidden` on an ancestor
|
|
584
|
+
kills `position: sticky` in its subtree, so a page wanting a sticky viewport-sized window
|
|
585
|
+
inside the article previously needed `.doc-content { overflow: visible !important }`. An
|
|
586
|
+
`!important` in a consumer's stylesheet is this component failing to offer a seam.
|
|
587
|
+
|
|
588
|
+
### A page with tests says so, even when tests are off (#113)
|
|
589
|
+
|
|
590
|
+
Tests default to on for localhost and off everywhere else, and when they are off the widget is
|
|
591
|
+
hidden — so **a page with failing tests looked identical to a page with no tests**. There was
|
|
592
|
+
an override, `localStorage['tosijs-ui-tests-enabled']`, but you had to know the key, which
|
|
593
|
+
means you had to already suspect there was something to see.
|
|
594
|
+
|
|
595
|
+
That bit hardest down the path we recommend: `tosijs-tunnel` necessarily serves from a
|
|
596
|
+
non-localhost hostname, so the sanctioned way to view a dev site remotely is exactly where test
|
|
597
|
+
state went invisible. A maintainer read a clean tunnelled page as "the failure must be
|
|
598
|
+
localhost-specific" and lost the thread.
|
|
599
|
+
|
|
600
|
+
The app menu now offers a **Tests** toggle — but only on a page that has tests, where it reads
|
|
601
|
+
`Tests (N not run)` while they are off. `pageTestCount()` is exported from live-example for
|
|
602
|
+
this: how many examples carry tests, regardless of whether tests are enabled — the number that
|
|
603
|
+
lets a UI say a runner exists here without turning it on. A published page with no tests offers nothing, so the
|
|
604
|
+
default stays as quiet as it should be. The default itself was never the problem; the silence
|
|
605
|
+
was.
|
|
606
|
+
|
|
607
|
+
### `/version.json` stops restamping a site that did not change (#122)
|
|
608
|
+
|
|
609
|
+
`docs/` is committed here and in the sibling projects, and the stamp named
|
|
610
|
+
`git rev-parse HEAD` — so **every commit's `version.json` named its own parent, forever**, and
|
|
611
|
+
no build→commit cycle converged:
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
build at A → version.json says A
|
|
615
|
+
commit → B contains a version.json saying A
|
|
616
|
+
rebuild → version.json says B → dirty
|
|
617
|
+
commit → C contains a version.json saying B → …
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`build-stamp.ts` already argued against a wall-clock stamp for exactly this reason and missed
|
|
621
|
+
that the argument applies one level up: `HEAD` is not a clock, but in a repo that commits its
|
|
622
|
+
own output it moves on the same cadence. Small, and it cost something real — a false positive
|
|
623
|
+
on `git status` at the moment you are trying to confirm a release tree is clean, so it gets
|
|
624
|
+
checked, re-checked, and eventually ignored.
|
|
625
|
+
|
|
626
|
+
The fix is the reporter's: if nothing else in the output changed, leave the stamp alone. A new
|
|
627
|
+
`contentHash` field records what the rest of the output hashed to, so the next build can tell
|
|
628
|
+
"changed" from "unchanged" without keeping a copy of the old tree — though as it happens the
|
|
629
|
+
previous build is still on disk (the build moves it aside as `<outputDir>.last-good` rather
|
|
630
|
+
than deleting it, and drops it only on success), so the comparison costs no extra bookkeeping.
|
|
631
|
+
|
|
632
|
+
Verified end-to-end, which is the only way to check a convergence claim: build → commit →
|
|
633
|
+
rebuild now leaves a **clean tree**.
|
|
634
|
+
|
|
635
|
+
The stamp is also more honest for it. It now names the last build that actually changed the
|
|
636
|
+
site, rather than the last one that happened to run. **Deleting `version.json` still forces a
|
|
637
|
+
fresh stamp** — the escape hatch for re-identifying an unchanged build, at no extra flag.
|
|
638
|
+
|
|
639
|
+
### `describeHolder(null)` says nothing is building (#123)
|
|
640
|
+
|
|
641
|
+
`describeHolder(currentHolder(root))` is the natural spelling, and `currentHolder` returns
|
|
642
|
+
`null` whenever nothing holds the lock — the ordinary, healthy case. It rendered the alarm
|
|
643
|
+
template with the fields unfilled (`pid undefined … building in undefined`), reporting a scary
|
|
644
|
+
problem that did not exist, in a reader whose whole purpose is answering "who holds this?".
|
|
645
|
+
|
|
646
|
+
## 1.13.0
|
|
647
|
+
|
|
648
|
+
### tosijs `^1.9.1` — and a console with nothing in it
|
|
649
|
+
|
|
650
|
+
The peer floor moves to `^1.9.1`. **The binding reason is a type-surface change, not the console
|
|
651
|
+
cleanup** — our emitted declarations now reference `TosiProps` and `TosiStyleSheet` (renamed
|
|
652
|
+
upstream from `XinProps`/`XinStyleSheet`), so `dist/*.d.ts` _hard-requires_ a tosijs that exports
|
|
653
|
+
them. An adopter on an older tosijs gets type errors, not warnings. The floor also carries
|
|
654
|
+
tosijs's **agent-surface security fix**, and the deprecation cleanup below.
|
|
655
|
+
|
|
656
|
+
**Migration note if you are on tosijs 1.7.x:** this floor takes you through **tosijs 1.8.0**,
|
|
657
|
+
which its own notes describe as deviating from semver — it removed `data-ref` and `<xin-slot>`.
|
|
658
|
+
Read tosijs's 1.8.0 entry before upgrading; that step is not automatic.
|
|
659
|
+
|
|
660
|
+
Before 1.9.1, every page of every doc site logged warnings a consumer could not act on:
|
|
661
|
+
|
|
662
|
+
- `xinValue, tosiValue, xinPath…` — tosijs reading its own deprecated proxy property
|
|
663
|
+
([tosijs#31](https://github.com/tonioloewald/tosijs/issues/31)). Nothing in any consumer's
|
|
664
|
+
code caused it.
|
|
665
|
+
- `bindText` / `bindList` — deprecated in 1.8, then **withdrawn as a category error**
|
|
666
|
+
([tosijs#33](https://github.com/tonioloewald/tosijs/issues/33)). They are a pair: `bindList`
|
|
667
|
+
supplies the per-item template and `bindText` fills it, and `{ textContent: proxy }` was
|
|
668
|
+
never a replacement for the path form (measured: `textContent: '^.name'` inside a template
|
|
669
|
+
renders the literal string, `bindText: '^.name'` binds). `bindText`'s runtime warning
|
|
670
|
+
outlived its typings by one release and is gone in 1.9.1.
|
|
671
|
+
|
|
672
|
+
Verified on a from-scratch build rather than assumed: **the three deprecation warnings above are
|
|
673
|
+
gone.** An earlier draft of these notes claimed _zero console warnings_, and that was false — one warning remains and it is ours, not tosijs's. Instantiating `<tosi-tabs>` still logs an
|
|
674
|
+
on-event collision from our deprecated `onCloseTab` alias, which the elements factory reads as
|
|
675
|
+
event-handler sugar.
|
|
676
|
+
`onCloseTab` is a deprecated alias we keep for compatibility, so removing it is a breaking change
|
|
677
|
+
and waits for the next major. Stating it rather than claiming a clean console we do not have.
|
|
678
|
+
|
|
679
|
+
### Live examples: pick a side by clicking, and see what actually changed
|
|
680
|
+
|
|
681
|
+
Both diff views — the resolvable one and the read-only one behind `<tosi-code>`'s review
|
|
682
|
+
overlay — now let you **click a coloured line** to take that side, and mark **the words that
|
|
683
|
+
actually differ** more strongly than the line containing them. Dragging to select text does not
|
|
684
|
+
change anything; those lines are also what you copy from.
|
|
685
|
+
|
|
686
|
+
`<tosi-example>`'s widget bar used to stay **open after every action** — you clicked "maximize"
|
|
687
|
+
and it sat there covering the corner of the thing you had just maximised, until you remembered
|
|
688
|
+
to click the handle again. It now closes once you have actually done something, its controls are ordered by reach (maximize nearest the handle), and the handle is the
|
|
689
|
+
tosi owl on a status-coloured badge — orange running, green passed, red failed.
|
|
690
|
+
|
|
691
|
+
**Tapping that handle on a touch device no longer flashes the menu open and shut.** A touch
|
|
692
|
+
device has no hover, but the browser still fires `pointerenter`/`pointerleave` around a tap, so
|
|
693
|
+
the hover _peek_ opened the bar and then undid itself before the click pinned it — you tapped
|
|
694
|
+
again believing it had failed, and that second tap closed it for real.
|
|
695
|
+
|
|
696
|
+
### Source moved into the app menu
|
|
697
|
+
|
|
698
|
+
The floating `<> Source` chip that sat over the content is gone; Source is a submenu of the app
|
|
699
|
+
menu beside Language and Color Theme, and it disappears entirely on a page with no source file.
|
|
700
|
+
|
|
701
|
+
### Security: cross-site requests can no longer reach the dev server's write endpoints
|
|
702
|
+
|
|
703
|
+
`POST /__docstore/source` and `POST /__build` were gated on the **peer address alone** — no
|
|
704
|
+
credential — and the handlers parse JSON regardless of `Content-Type`. That made a cross-site
|
|
705
|
+
`fetch(…, {mode:'no-cors'})` a _simple_ request: no preflight, peer is `127.0.0.1`, gate passes.
|
|
706
|
+
Any page you happened to visit while `bun start` was running could write any file under the repo
|
|
707
|
+
root, `.git/hooks/*` included — code execution at your next git operation
|
|
708
|
+
([#90](https://github.com/tonioloewald/tosijs-ui/issues/90)).
|
|
709
|
+
|
|
710
|
+
All three state-changing endpoints — `/__docstore/source`, `/__build` and `/report` — now
|
|
711
|
+
require the request to be same-origin, via one shared predicate rather than a term repeated per
|
|
712
|
+
call site. It checks **both** `Sec-Fetch-Site` **and** `Origin`: the first is unforgeable but
|
|
713
|
+
absent on Safari < 16.4, Firefox < 90 and older WKWebViews, and those browsers send the second.
|
|
714
|
+
An earlier pass checked only the first and left exactly that population unprotected. Applied
|
|
715
|
+
**on the non-tunnel path only** — the tunnel is legitimately cross-site and
|
|
716
|
+
is gated by a session cookie instead. Non-browser callers (the CLI, `curl`) send no fetch
|
|
717
|
+
metadata and are unaffected; the attack this closes is specifically a _browser_ being induced to
|
|
718
|
+
make the request, and a browser always tells us.
|
|
719
|
+
|
|
720
|
+
The prose in `dev-auth.ts` had claimed `SameSite=Lax` gave "free CSRF protection". That was true
|
|
721
|
+
only of the tunnel path, which needs a cookie — the default loopback path needed nothing, so the
|
|
722
|
+
stated defence never covered its own primary case.
|
|
723
|
+
|
|
724
|
+
### New dev-server endpoint: `POST /__build` (loopback only)
|
|
725
|
+
|
|
726
|
+
Called out under its own heading because it is a new HTTP surface on a tool people run all day.
|
|
727
|
+
|
|
728
|
+
It exists so `bun run build` can ask a running dev server to build instead of refusing (below).
|
|
729
|
+
It is gated exactly like `/__devlink`: **loopback and NOT via the tunnel**. Verified against a
|
|
730
|
+
real server rather than reasoned about — an anonymous tunnel request gets `401` from the session
|
|
731
|
+
gate, and an **authenticated** one gets `404` from this gate, which is the case that matters. A
|
|
732
|
+
remote visitor triggering builds would be a denial of service at best, and runs the project's own
|
|
733
|
+
build code at worst. `GET` does nothing.
|
|
734
|
+
|
|
735
|
+
Nothing else about the build lock changed: two builders racing `rm -rf` on one output tree is
|
|
736
|
+
still refused, and a stale lock still gets the old message.
|
|
737
|
+
|
|
738
|
+
### ePub dates, and an `epub.modified` option
|
|
739
|
+
|
|
740
|
+
`dcterms:modified` is the only date an ePub reader can surface, and it is baked into a
|
|
741
|
+
committed, redistributed file — so it has to be deterministic _and_ coherent. The
|
|
742
|
+
version-derived encoding introduced in this release **wrapped** — twice. First `1.13.24` and
|
|
743
|
+
`1.14.0` produced the same instant; the repack then made `1.13.1-beta.1` byte-identical to
|
|
744
|
+
`1.13.0`, collided `1.0.1000` with `1.1.0`, and sent a CalVer `2026.9.3` to the year 5872. Each
|
|
745
|
+
attempt shipped a docblock asserting the ordering that execution disproved.
|
|
746
|
+
|
|
747
|
+
So the promise is narrowed to what is actually needed and can actually be held: **the same
|
|
748
|
+
version always yields the same instant, and different versions yield different ones.** It is no
|
|
749
|
+
longer ordered, because nothing reads these dates as a sequence — a reader sees one date on one
|
|
750
|
+
book — and a claim nobody needs, made three times and false three times, is worse than no claim.
|
|
751
|
+
The test sweep is generated rather than hand-picked, which is what would have caught the
|
|
752
|
+
adjacent-version collision.
|
|
753
|
+
|
|
754
|
+
New: **`epub.modified`** in the site config. The derived date is synthetic — it looks like a
|
|
755
|
+
date without being one — and until now an adopter had no way to publish the real one.
|
|
756
|
+
|
|
757
|
+
That option shipped broken for a few hours and is worth recording: `modified` was written
|
|
758
|
+
**above** a `...epubOpts` spread, and `epubOpts` _is_ `config.epub` — so the adopter's raw value
|
|
759
|
+
overwrote the sanitised one. `'2026-09-03'` reached the OPF unnormalised and `'not a date'`
|
|
760
|
+
reached it verbatim, and EPUB 3 requires exactly `CCYY-MM-DDThh:mm:ssZ`, so the obvious spelling
|
|
761
|
+
of the new option produced a file EPUBCheck rejects. The option assembly is now an exported
|
|
762
|
+
**`epubOptionsFor()`** with tests that drive **the call site**, not just the date function beside it
|
|
763
|
+
— the third time in this release that a green test asserted a pure function whose only consumer
|
|
764
|
+
threw the result away.
|
|
765
|
+
|
|
766
|
+
### The book this project ships carries a real date again
|
|
767
|
+
|
|
768
|
+
`docs/tosijs-ui.epub` is redistributed, and its OPF has no `dc:date` — so `dcterms:modified` is
|
|
769
|
+
the only date a reader ever sees. The version-derived scheme hashed 1.13.0 to **2012**, fourteen
|
|
770
|
+
years adrift, where v1.12.8's book carried a correct contemporary date. This repo now sets
|
|
771
|
+
`epub.modified` explicitly, which is exactly what that new option is for; adopters who care
|
|
772
|
+
about the date should do the same, and adopters who don't still get a deterministic one.
|
|
773
|
+
|
|
774
|
+
A related tidy: a docblock asserting the monotonicity this function does not have had survived
|
|
775
|
+
three rewrites by sitting forty lines above it in a different file, and was shipping in `dist/`.
|
|
776
|
+
The behaviour is described in one place now.
|
|
777
|
+
|
|
778
|
+
### One place decides the ePub date
|
|
779
|
+
|
|
780
|
+
`buildEpub` — the public export, and what `bun book` calls — now defaults and validates
|
|
781
|
+
`dcterms:modified` itself. It previously used `new Date()` with no validation, so the
|
|
782
|
+
determinism fix and the `epub.modified` handling lived at a single `buildSite` call site that
|
|
783
|
+
`bun book` never executed: that path still stamped a wall clock, re-dirtying the committed ePub,
|
|
784
|
+
and still passed a date-only string into an OPF that EPUBCheck rejects.
|
|
785
|
+
|
|
786
|
+
That is the fourth instance in this release of one shape: **a green test asserting a function
|
|
787
|
+
whose only consumer discards the result.** The date function was correct and tested; the
|
|
788
|
+
assembly threw its return value away. The predicate was correct and tested; two of three
|
|
789
|
+
endpoints used it. The touch guard was correct; the test retyped it. And the fix for the
|
|
790
|
+
_first_ three introduced the fourth, by testing a helper that one of two shipped paths called.
|
|
791
|
+
|
|
792
|
+
The answer is not another helper — it is a single choke point every path already goes through,
|
|
793
|
+
tested from the public entry point by asserting the OPF text.
|
|
794
|
+
|
|
795
|
+
### Tests that can actually fail
|
|
796
|
+
|
|
797
|
+
Two "regression tests" added earlier in this release were **tautologies**: each re-declared the
|
|
798
|
+
production predicate inside the test file and asserted on the copy, so both passed forever and
|
|
799
|
+
stayed green when the real code was broken. A mutation test in the pre-release review killed
|
|
800
|
+
zero of them. They now mount the real `<tosi-pocket-bar>` and import the exported delegation
|
|
801
|
+
predicate, and were re-mutated to confirm the opposite — deleting the touch guards and the
|
|
802
|
+
pin-clear turns two tests red.
|
|
803
|
+
|
|
804
|
+
A re-review then found the same shape one layer down: the delegated-build queue —
|
|
805
|
+
`buildNow`/`buildWaiters` — had **zero** coverage, and an unconditional infinite loop passed the
|
|
806
|
+
whole green suite. The loop is fixed (it started a build on every pass instead of once, spinning
|
|
807
|
+
the dev server into ~30 builds and a `process.exit(1)` that blamed the user's watcher config),
|
|
808
|
+
and the decision is now a pure exported function, `nextBuildStep()`, with tests for the
|
|
809
|
+
contended orderings. `POST /report` was also found never to have received the CSRF gate — the
|
|
810
|
+
term had been spliced into two call sites by hand and the third missed, which is why all three
|
|
811
|
+
now share one named predicate.
|
|
812
|
+
|
|
813
|
+
Recorded here rather than buried because it is the same failure this project's notes already
|
|
814
|
+
describe from 1.9.0: work asserted in prose that the code did not do. A test that retypes the
|
|
815
|
+
condition it is testing is that failure wearing a lab coat.
|
|
816
|
+
|
|
817
|
+
### What delegation guarantees (and what it refuses)
|
|
818
|
+
|
|
819
|
+
The delegation path above was hardened after a pre-release review found several ways it could
|
|
820
|
+
report a success nobody got. Worth stating, because "my build tool said it built" is a claim you
|
|
821
|
+
rely on:
|
|
822
|
+
|
|
823
|
+
- **It proves the build was yours.** `holder.port` is validated before it reaches a URL, the
|
|
824
|
+
request carries `?root=`, and the reply carries `pid`/`root`/`outputDir` which must match the
|
|
825
|
+
lock and the tree you asked for. A planted lock plus any listener answering `{"ok":true}`
|
|
826
|
+
previously printed "rebuilt this tree" and exited 0 over an untouched tree — stale locks from
|
|
827
|
+
other projects are ordinary on a dev machine, one of them usually on the shared default port.
|
|
828
|
+
- **It waits for the tree to settle**, not for whichever build finishes next — starting one
|
|
829
|
+
build and then parking, with a cap so a watcher that keeps firing degrades to a message
|
|
830
|
+
instead of a build storm. (The first attempt called `rebuild()` on **every pass**, and
|
|
831
|
+
`rebuild()` sets `pending` when a build is in flight — so the settled check could never pass
|
|
832
|
+
under contention and it re-armed forever, spinning the dev server into ~30 builds and a
|
|
833
|
+
`process.exit(1)` blaming the user's watcher config. It was correct when idle, which is why
|
|
834
|
+
it looked fine. A re-review caught it; the decision is now a pure function with tests for
|
|
835
|
+
the contended orderings, which had no coverage at all.) A reply used to be
|
|
836
|
+
released by an already-running build one line before a queued rebuild began wiping the output
|
|
837
|
+
directory — success reported while the tree was being emptied, right before the documented
|
|
838
|
+
`git add`.
|
|
839
|
+
- **It does not believe a reply it does not recognise.** A dev server predating this endpoint
|
|
840
|
+
answers the POST with the SPA shell; that now reports "restart your dev server" instead of
|
|
841
|
+
accusing your build of failing. Dev servers here live for days running the code they loaded,
|
|
842
|
+
so that is the normal upgrade path.
|
|
843
|
+
- **Its timeout is the server's real one.** The client promised ten minutes while `Bun.serve`
|
|
844
|
+
idles connections at 120s, so a _successful_ long build was reported as a failure. "Did not
|
|
845
|
+
answer in time — the build may still be running" is now distinguished from "unreachable".
|
|
846
|
+
|
|
847
|
+
`<tosi-pocket-bar>` also gained a public `close()`. The live-example bar was setting `open =
|
|
848
|
+
false` directly, which left the component's internal pin set — so it re-opened on hover, stuck
|
|
849
|
+
open over the content, and the next handle tap did nothing. On touch, where this release removes
|
|
850
|
+
the hover path, there was no way back at all.
|
|
851
|
+
|
|
852
|
+
### Builds stopped fighting the dev server
|
|
853
|
+
|
|
854
|
+
`bun run build` with a dev server running now **asks it to build** instead of refusing, and
|
|
855
|
+
`bun run test-browser` uses its **own port (8798)** instead of reclaiming 8787 by killing
|
|
856
|
+
whatever holds it. Both produced the same workflow — kill the server, build, forget to restart
|
|
857
|
+
it — and the second twice took a live tunnel offline, where the symptom is a page reporting
|
|
858
|
+
"offline" with no hint which half died.
|
|
859
|
+
|
|
860
|
+
The generated ePub is **reproducible** now. It differed on every build, so it landed in every
|
|
861
|
+
diff and made a clean tree impossible after building — two independent causes:
|
|
862
|
+
`dcterms:modified` defaulted to `new Date()`, and the zip stored each file's mtime, and those
|
|
863
|
+
files had just been written. Entry mtimes are normalised to a constant before zipping (zip has
|
|
864
|
+
no flag for it; normalising the inputs is the standard fix), and the date is anchored to the
|
|
865
|
+
last commit that touched `package.json` rather than to a clock or to HEAD. After
|
|
866
|
+
commit-then-rebuild the tree carries one changed file (`docs/version.json`, which records the
|
|
867
|
+
commit by design) where it used to carry 68 plus a binary.
|
|
868
|
+
|
|
869
|
+
### Icons
|
|
870
|
+
|
|
871
|
+
`shift` (⇧) — the standard shift-key outline, asked for by tosijs-3d and general enough to live
|
|
872
|
+
here rather than in one consumer. `tab` (⇥) — an arrow pointing at a vertical bar, built on
|
|
873
|
+
`arrowRight`'s proportions. And `fatArrowUp`/`fatArrowRight`/`fatArrowDown`/`fatArrowLeft`: one
|
|
874
|
+
drawing and four rotations via the same redirect mechanism the arrow and chevron families use.
|
|
875
|
+
|
|
876
|
+
The shift glyph is centred on 12,12 (apex y=4, base y=20; it was 3..20, half a pixel high) —
|
|
877
|
+
invisible head-on, and what makes each rotation land square instead of drifting off-centre.
|
|
878
|
+
|
|
879
|
+
With `delete`, `cornerDownLeft` and `command` already present, an on-screen keyboard now has
|
|
880
|
+
everything except caps-lock.
|
|
881
|
+
|
|
882
|
+
### Smaller things
|
|
883
|
+
|
|
884
|
+
- **`<tosi-pocket-bar>` handles are brandable**: `--tosi-pocket-handle-bg`,
|
|
885
|
+
`--tosi-pocket-handle-radius` and `--tosi-pocket-handle-size`, all defaulting to exactly their
|
|
886
|
+
previous values, so nothing changes for an existing consumer. A circle needs all three —
|
|
887
|
+
`border-radius: 50%` on a content-sized flex button is an ellipse.
|
|
888
|
+
- The live-example handle uses them for the owl badge, and its **controls were reordered** so
|
|
889
|
+
maximize sits nearest the handle and tests furthest.
|
|
890
|
+
- The dead `.view-source` CSS went with the button it styled, rather than leaving rules for an
|
|
891
|
+
element nothing creates.
|
|
892
|
+
- **`bindText` in `<tosi-table>` briefly moved to `textContent`, then reverted back to
|
|
893
|
+
`bindText`.** `data-table.ts:2179` holds a proxy, which is the only shape the replacement
|
|
894
|
+
accepts — but with the deprecation withdrawn upstream, keeping it would have left the file
|
|
895
|
+
using two idioms for the same job with nothing gained. Net change: zero lines.
|
|
896
|
+
|
|
3
897
|
## 1.12.8
|
|
4
898
|
|
|
5
899
|
### Diffs: pick a side by clicking it, and see what actually changed
|