tosijs-ui 1.10.0 → 1.11.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 +543 -0
- package/bin/caddy-install.ts +9 -11
- package/bin/deploy-preview.ts +13 -11
- package/bin/release-notes.ts +43 -0
- package/bin/resolve-site-config.ts +84 -1
- package/bin/tunnel.ts +42 -12
- package/dist/code-editor-cm.d.ts +17 -8
- package/dist/crud.d.ts +155 -0
- package/dist/crud.js +822 -0
- package/dist/data-table.d.ts +37 -0
- package/dist/data-table.js +472 -19
- package/dist/doc-system/doc-system.d.ts +3 -0
- package/dist/doc-system/doc-system.js +68 -2
- package/dist/doc-system/release-notes.d.ts +34 -0
- package/dist/doc-system/release-notes.js +82 -0
- package/dist/doc-system/site/dev-auth.d.ts +101 -1
- package/dist/doc-system/site/dev-auth.js +240 -8
- package/dist/doc-system/site/dev-server.js +106 -6
- package/dist/doc-system/site/site-config.d.ts +9 -3
- package/dist/hash-state.d.ts +29 -0
- package/dist/hash-state.js +210 -0
- package/dist/iife.js +153 -150
- package/dist/iife.js.map +20 -12
- package/dist/index-iife.js +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/live-example/code-transform.js +1 -1
- package/dist/localize.d.ts +19 -1
- package/dist/localize.js +150 -1
- package/dist/router.js +9 -1
- package/dist/schema-form/fields.d.ts +187 -0
- package/dist/schema-form/fields.js +593 -0
- package/dist/schema-form/json-schema.d.ts +41 -0
- package/dist/schema-form/json-schema.js +25 -0
- package/dist/schema-form/plugins.d.ts +45 -0
- package/dist/schema-form/plugins.js +55 -0
- package/dist/schema-form/unenforced.d.ts +11 -0
- package/dist/schema-form/unenforced.js +104 -0
- package/dist/schema-form/validator.d.ts +58 -0
- package/dist/schema-form/validator.js +111 -0
- package/dist/schema-form.d.ts +148 -0
- package/dist/schema-form.js +1472 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +8 -2
- package/package.json +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,548 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
|
|
5
|
+
**Schema-driven editing.** Three new pieces that compose: `<tosi-schema-form>` renders a form
|
|
6
|
+
from a JSON Schema, `<tosi-crud>` puts search, list and edit over a store you supply, and
|
|
7
|
+
`hashState` keeps filter and selection in the URL so a filtered list with a record open is a
|
|
8
|
+
link you can send.
|
|
9
|
+
|
|
10
|
+
**The dev bridge is usable on a headset.** The edit-link token is now **seven characters** you
|
|
11
|
+
can read off one screen and type on another, instead of twenty-two of mixed-case base64url.
|
|
12
|
+
That change came from watching the feature fail in practice: people gave up and typed LAN IP
|
|
13
|
+
addresses instead, and a credential too painful to use is not protecting anything.
|
|
14
|
+
|
|
15
|
+
> **If you are on 1.10.0, this is a three-version jump.** 1.10.1 and 1.10.2 were tagged in git
|
|
16
|
+
> but **never published to npm** — `latest` has been 1.10.0 throughout. Nothing is lost: every
|
|
17
|
+
> change in those releases is in this one, and their notes are below where they always were.
|
|
18
|
+
> They are simply not installable, so `npm i tosijs-ui@1.10.2` will fail.
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
|
|
22
|
+
- **`tosijs-schema` floor is `^1.8.0`** (optional peer). 1.8.0 enforces `oneOf`,
|
|
23
|
+
`exclusiveMinimum` and `exclusiveMaximum` — which is what
|
|
24
|
+
[tosijs-schema#8](https://github.com/tonioloewald/tosijs-schema/issues/8), filed from this
|
|
25
|
+
work, asked for — and exports `unenforcedKeywords`. The form now asks the **registered
|
|
26
|
+
validator** which keywords it does not check, falling back to a local list only when a
|
|
27
|
+
validator cannot answer. Against 1.7.0 a `oneOf` field carried "oneOf is not validated";
|
|
28
|
+
against 1.8.0 it carries nothing and the value is genuinely checked. Verified with
|
|
29
|
+
`bun bin/verify-schema-dep.ts --version=1.8.0` (17/17).
|
|
30
|
+
|
|
31
|
+
- **Validation is supplied, not imported.** `setSchemaValidator({ validate, inferSchema, unenforcedKeywords })` —
|
|
32
|
+
one line, anywhere, before or after render. The CDN `<script>` build and any `tosijs-ui/site`
|
|
33
|
+
doc site register it themselves, so only an ESM consumer writes it.
|
|
34
|
+
|
|
35
|
+
The reason is worth knowing, because it is not a preference. A bare `import('tosijs-schema')`
|
|
36
|
+
in shipped code is either **resolved** by your bundler — which fails the build for anyone who
|
|
37
|
+
did not install it, including people using only `<tosi-table>` — or left **external**, which
|
|
38
|
+
cannot resolve in a browser and kills validation for everyone. Both were measured. There is
|
|
39
|
+
no third option, so the component asks for two functions instead of a package. The upside is
|
|
40
|
+
that they are just functions: an Ajv wrapper, a house validator or a test stub all work.
|
|
41
|
+
|
|
42
|
+
- **`tjs-lang` moves to `^0.13.1`** (from `^0.12.0`), and `TJS_VERSION` in `code-transform.ts`
|
|
43
|
+
moves in lockstep. 0.12.0 is **deprecated on npm**, and the deprecation names this release's
|
|
44
|
+
own combination — _"tosijs-schema >=1.5.0 breaks the battery atoms' output validation in these
|
|
45
|
+
versions. Upgrade to 0.13.1."_ Worse for adopters, `^0.12.0` does not admit 0.13.x, so anyone
|
|
46
|
+
on current tjs-lang got a hard `ERESOLVE`. The tjs CodeMirror extension is still bundled
|
|
47
|
+
rather than externalized — the constraint that silently no-ops highlighting if it breaks.
|
|
48
|
+
|
|
49
|
+
- **The edit-link token is 7 Crockford base32 characters**, and `linkTtlMinutes` defaults to
|
|
50
|
+
**5** (was 15). A link minted by an older server is not redeemable by a newer one; both are
|
|
51
|
+
in-memory and per-process, so this only matters across a restart mid-session.
|
|
52
|
+
|
|
53
|
+
### New
|
|
54
|
+
|
|
55
|
+
**`<tosi-schema-form>`** — a form from a JSON Schema. `value` is the state and `change` fires on
|
|
56
|
+
edit, the same contract as every other component here.
|
|
57
|
+
|
|
58
|
+
- Scalars, enums, `const`, and `format`-driven input types. Anything it cannot render is a
|
|
59
|
+
labelled placeholder saying so, never a silently omitted field — a field that vanishes is
|
|
60
|
+
indistinguishable from a schema that never mentioned it, which is how an editor loses data.
|
|
61
|
+
- **Nested objects** become `<details>` sections to any depth, with fully-qualified leaf paths
|
|
62
|
+
(`address.geo.lat`), so value sync and error keying work at depth with no special cases.
|
|
63
|
+
`required` is scoped to the object that declares it, which is what JSON Schema means: a
|
|
64
|
+
required `city` inside an optional `address` says _if you give an address, it needs a city_.
|
|
65
|
+
- **Arrays** with add, remove and reorder. Each element expands against `items`; scalar items
|
|
66
|
+
are a single field at the index (`tags.0`), object items expand to their properties
|
|
67
|
+
(`items.0.sku`).
|
|
68
|
+
- **Unions** (`anyOf`/`oneOf`), each rendered as what it actually is: `[X, null]` is just an
|
|
69
|
+
optional X, an all-`const` union is a `<select>`, an all-object union is a variant picker plus
|
|
70
|
+
the matching branch's fields. Anything else names the shapes it saw ("a union of string |
|
|
71
|
+
object"). Discriminators are derived from the property every branch pins to a different
|
|
72
|
+
`const`, or declared as `x-discriminator`.
|
|
73
|
+
- **Format plugins** — `registerFieldPlugin(format, plugin)` claims a schema `format`. The
|
|
74
|
+
plugin owns the control; the form keeps the label and the error slot. Plugins are dispatched
|
|
75
|
+
first and for every node kind, so `unsupported` is the fallback, not the ceiling. Register
|
|
76
|
+
whenever you like: styles are injected on registration and live forms rebuild.
|
|
77
|
+
- **Validation is optional.** Without a validator the form still renders and edits and simply
|
|
78
|
+
reports no errors, which is a smaller failure than refusing to render.
|
|
79
|
+
- **No schema? It infers one** from the value, via `inferSchema`. Read it back from `.schema`,
|
|
80
|
+
edit it, set it again — that round trip is the point.
|
|
81
|
+
|
|
82
|
+
**`<tosi-crud>`** — search, list, edit over a `CrudStore` adapter (`list` / `save?` / `delete?`,
|
|
83
|
+
promise-returning). There is **no transport in the component**: REST, a DocStore, IndexedDB, an
|
|
84
|
+
array in a closure and a mock all satisfy the same three methods. Omit `save` and the form is
|
|
85
|
+
read-only; omit `delete` and its button is not shown. `columnsFromSchema()` lets one description
|
|
86
|
+
of the shape drive both surfaces — strictly better than the table's own inference, which reads
|
|
87
|
+
`Object.keys(array[0])` and silently loses the column for a property the first row lacks. The
|
|
88
|
+
search term and selected id live in the URL hash, so typing **replaces** the history entry while
|
|
89
|
+
selecting a record **pushes** one: back leaves the record rather than un-typing your search a
|
|
90
|
+
letter at a time.
|
|
91
|
+
|
|
92
|
+
**`hashState({ namespace, mode })`** — key-value state in the page hash. Every key is namespaced
|
|
93
|
+
in the URL and bare in the API, and a write never touches a key it did not put there, so two
|
|
94
|
+
instances (or a hashState beside a hash router) share one URL without deleting each other. That
|
|
95
|
+
is the failure that made `createDocBrowser` grow a `'memory'` routing mode, so `mode: 'memory'`
|
|
96
|
+
is here from the start.
|
|
97
|
+
|
|
98
|
+
**Editable `<tosi-table>`** — set `editable` and cells become inputs, per-column overridable
|
|
99
|
+
either way; a column with its own `dataCell` is never made editable. A `schema` drives the
|
|
100
|
+
control type and validates edits, using the same model the form uses. A `change` event carries
|
|
101
|
+
`{ item, field, oldValue, newValue, error }` and commits on `change`, not `input`: an event per
|
|
102
|
+
keystroke would make `3` a legitimate intermediate state of typing `35`.
|
|
103
|
+
|
|
104
|
+
**Localization** — `localize(pattern, values)` fills `{name}` placeholders **after** translating,
|
|
105
|
+
so the key is the whole sentence (`'Add {item}'`, never `'Add ' + item`). A concatenated
|
|
106
|
+
sentence is untranslatable in principle: the translator sees a dangling fragment and cannot move
|
|
107
|
+
the placeholder to where their language puts it. An unknown placeholder stays visible as
|
|
108
|
+
`{name}` rather than blanked. `<tosi-schema-form>` localizes its own chrome and rebuilds on
|
|
109
|
+
locale change.
|
|
110
|
+
|
|
111
|
+
**The dev bridge.** `mintLinkToken()` / `normalizeLinkToken()`: Crockford base32 excludes `I`,
|
|
112
|
+
`L`, `O` and `U`, so the `0`/`O` and `1`/`l` mistypes that hurt most on a floating keyboard are
|
|
113
|
+
impossible rather than merely unlikely. `randomInt`, not `randomBytes % 32`. Redemption folds
|
|
114
|
+
case, accepts `I`/`L` as `1` and `O` as `0`, and ignores hyphens — normalising on **redemption**
|
|
115
|
+
rather than only when minting, because case-insensitivity that exists in the alphabet but not in
|
|
116
|
+
the comparison is a claim rather than a behaviour, and the failure it produces is a correct human
|
|
117
|
+
being told they typed it wrong. `--link` prints the code on its own line as well as in the URL.
|
|
118
|
+
|
|
119
|
+
The TTL is 5 minutes because the link is a **bearer token** for its lifetime — which the
|
|
120
|
+
`dev-auth` header comment now says plainly instead of describing the single-use rule it stopped
|
|
121
|
+
following in 1.10.0. About 35 bits is ample for what this actually is: an online-only guess
|
|
122
|
+
against a `Map` lookup, for a token redeemed seconds after minting, that mints only a write
|
|
123
|
+
session `mayWriteSource` still gates.
|
|
124
|
+
|
|
125
|
+
Guess-rate control: redemption runs **one at a time** and every attempt occupies at least 100ms,
|
|
126
|
+
so ten attempts a second against 32⁷ is ~111 years to exhaust and about 1 in 11 million inside a
|
|
127
|
+
five-minute window (measured — 20 concurrent attempts complete at 9.8/sec). After ten
|
|
128
|
+
consecutive failures the slot widens to a second. Not a lockout: the door never closes on a
|
|
129
|
+
human, it only gets slower to knock on. An earlier draft of this used escalating delays plus a
|
|
130
|
+
lockout, and replacing it was the right call — that version had two tunable constants, a counter
|
|
131
|
+
needing reset, and a lockout an attacker could trigger, which is a denial of service against the
|
|
132
|
+
developer on the one credential they need in order to work.
|
|
133
|
+
|
|
134
|
+
`tosijs-tunnel` prints the host **and where the address came from** before it acts —
|
|
135
|
+
`tosijs-deploy` is dry-run by default and shows its target, this one is not.
|
|
136
|
+
|
|
137
|
+
**Tooling.** `bin/verify-schema-dep.ts` runs the twelve requirements from tosijs-schema#6 plus
|
|
138
|
+
both directions of #7 against a candidate release and exits non-zero on any failure.
|
|
139
|
+
`PREVIEW_HOST` now also resolves from `~/local-secrets/tosijs-preview.env`, because the previous
|
|
140
|
+
advice — export it from your shell profile — is present for a human and **absent for every
|
|
141
|
+
tool**: non-interactive shells inherit no interactive profile, so an agent or CI step sees
|
|
142
|
+
nothing and reports a missing credential rather than a missing `export`. `readLocalSecret` parses
|
|
143
|
+
`KEY=value` only and never evaluates; a credentials file that needs a shell to read is one that
|
|
144
|
+
can run code. `bin/preview-host.test.ts` includes a guard that greps the committed config for a `user@host`
|
|
145
|
+
literal — and asserts the pattern would have caught the real one, so it cannot pass by being
|
|
146
|
+
wrong. `bin/smoke-consumer.ts` gained four things it could not previously see: resolution of the
|
|
147
|
+
new subpaths from an installed tarball, a `tsc --noEmit` over an installed consumer with
|
|
148
|
+
`skipLibCheck: false` and no optional peers, a bundle entry that actually imports the library,
|
|
149
|
+
and both vendored types pinned by assignability tests in both directions.
|
|
150
|
+
|
|
151
|
+
- **`<tosi-table>` gained `full-width-header`.** A table whose columns add up to less than its
|
|
152
|
+
container left a strip of blank space on the right for no reason; with the attribute the
|
|
153
|
+
leftover width goes to the last **unpinned** column. Right-pinned columns are skipped —
|
|
154
|
+
they sit against the right edge by definition, so stretching one would push the space back
|
|
155
|
+
into the middle. When the columns overflow, nothing changes: every column keeps its width
|
|
156
|
+
and the table scrolls, because the row width is `max(sum, 100%)` and `1fr` then resolves to
|
|
157
|
+
exactly the width it was given. Header and body share one `grid-template-columns`, so they
|
|
158
|
+
stretch together.
|
|
159
|
+
|
|
160
|
+
### Process
|
|
161
|
+
|
|
162
|
+
- **`bun run release-check` must run AFTER the release commit, and it is the last thing before
|
|
163
|
+
`git tag`.** The range is `<last tag>..HEAD`, so a bullet written _in_ the release commit is
|
|
164
|
+
not in the range you checked a moment earlier — 1.11.0 shipped a commit whose body claimed
|
|
165
|
+
the gate was green while it exited 1 on the state that commit created. The range is also
|
|
166
|
+
exclusive of the since-commit, so tagging turns the gate green without the bullet ever being
|
|
167
|
+
written: the annotation escapes in both directions. Now documented in `CLAUDE.md`.
|
|
168
|
+
- The `unenforced.ts` parity test is a **subset** check rather than equality. Upstream growing
|
|
169
|
+
the set of keywords it enforces is good news, and an equality assertion turned that into a
|
|
170
|
+
red build. Upstream _shrinking_ below our fallback is the dangerous direction — the fallback
|
|
171
|
+
would claim a keyword is checked when it is not — and that is still caught.
|
|
172
|
+
|
|
173
|
+
### Fixed after the pre-release review
|
|
174
|
+
|
|
175
|
+
The nine-lens review ran **three times** against this release. Everything in this section was
|
|
176
|
+
found by it and fixed before tagging.
|
|
177
|
+
|
|
178
|
+
- **Editable numeric cells silently truncated what you typed.** Typing `19.95` into a
|
|
179
|
+
`<tosi-table editable>` number cell committed **95**; `-4` committed **4** — and every
|
|
180
|
+
commit fired `change` with `error: null`, so nothing reported that a digit had gone. Two-way
|
|
181
|
+
`bindValue` wrote the model on every `input`, and `<input type=number>` sanitizes an
|
|
182
|
+
intermediate `"19."` to `""` under the HTML value-sanitization algorithm, so the binding
|
|
183
|
+
wrote `""` straight back into the focused input. The binding is one-way now and
|
|
184
|
+
`handleCellChange` is the sole model write — which also makes "commits on `change`, not on
|
|
185
|
+
`input`" true of the _model_ and not only of the event.
|
|
186
|
+
|
|
187
|
+
It was invisible to every lane because every test assigned `.value` programmatically or
|
|
188
|
+
typed with no delay, and neither ever lets a frame run between keystrokes. There are three
|
|
189
|
+
tests that genuinely **type**, with a per-character delay, across all three engines.
|
|
190
|
+
|
|
191
|
+
The nine-lens review ran twice against this release. Everything below was found by it and
|
|
192
|
+
fixed before tagging.
|
|
193
|
+
|
|
194
|
+
- **`<tosi-crud>` destroyed unsaved edits** — one frame after any keystroke, with no user
|
|
195
|
+
action, and `save()` then posted the unedited record.
|
|
196
|
+
- **`<tosi-crud>`'s table selection never followed the form**, so a `#?people.id=2` deep link
|
|
197
|
+
opened the record with nothing highlighted — the feature these notes headline, producing a
|
|
198
|
+
list that did not show where you were. **Back** also left the opened record on screen while
|
|
199
|
+
the URL said otherwise, so back-then-reload showed you two different things; and a
|
|
200
|
+
hash-driven selection change fired no `change` event. Saving a new record now writes its id
|
|
201
|
+
to the hash, which is both correct and what makes "absent id means deselect" safe.
|
|
202
|
+
- **`<tosi-crud>` rebuilt the entire table on every keystroke.** Identity-guarded; measured at
|
|
203
|
+
zero table renders per keystroke afterwards.
|
|
204
|
+
- **`<tosi-schema-form>` emitted duplicate `change` events** — a checkbox click produced
|
|
205
|
+
three. `<tosi-table>` fixed the identical hazard in this release; the sibling shipped
|
|
206
|
+
without it.
|
|
207
|
+
- **The "not validated" note skipped containers**, so `uniqueItems` on an array and `allOf` on
|
|
208
|
+
an object — exactly the keywords a validator is most likely to skip — carried nothing.
|
|
209
|
+
- **`columnsFromSchema` re-derived property types** and got `{anyOf:[{type:'boolean'},
|
|
210
|
+
{type:'null'}]}` wrong, and emitted only `title` as the column name — so a title-less schema
|
|
211
|
+
gave headers reading `firstName` beside a form showing "first name".
|
|
212
|
+
- **`<tosi-table>`'s column menu still concatenated localized words**, which the localize docs
|
|
213
|
+
in this same release condemn by name. All five languages checked rendered wrong:
|
|
214
|
+
`Sortieren Aufsteigend`, `숨기기 열`, `非表示 カラム`, `隐藏 列`, `Cacher Colonne`.
|
|
215
|
+
`localizePhrase(key, fragments)` asks for the whole sentence and joins the fragments only
|
|
216
|
+
when nobody has translated it — so an existing translation table keeps its behaviour exactly
|
|
217
|
+
and adding one row upgrades it. Nothing is orphaned.
|
|
218
|
+
- **Six `on<Event>` members were shadowed by the element factory**, which warns about it in
|
|
219
|
+
the console. Renamed to `handle<Event>` per the framework's own guidance.
|
|
220
|
+
- **The no-validator path had no coverage in any tier** — the default for every ESM adopter,
|
|
221
|
+
while every lane ran with a validator registered. It has its own test file now. Two real
|
|
222
|
+
defects were behind that gap: removing a validator left the warnings permanently
|
|
223
|
+
suppressed, and a single once-flag meant whichever component spoke first silenced the rest.
|
|
224
|
+
- **A form with no schema and no validator rendered an empty box**, warning about validation
|
|
225
|
+
when the problem was that it had nothing to render. It says so on screen now, and the
|
|
226
|
+
warning names the right problem.
|
|
227
|
+
- **`change.detail.oldValue` was `undefined` for every cell edit after the first.** The
|
|
228
|
+
baseline was deleted on each commit rather than rolled forward, and the delete ran _before_
|
|
229
|
+
the equality guard — so a no-op commit wiped it, and clearing a numeric cell as the second
|
|
230
|
+
edit fired no `change` at all and skipped the coercion write, leaving a schema-typed integer
|
|
231
|
+
holding a raw string.
|
|
232
|
+
- **The documented `setSchemaValidator` recipe was wrong**, and only for the people it was
|
|
233
|
+
written for. `{ validate, inferSchema }` omits `unenforcedKeywords`, so the form fell back
|
|
234
|
+
to a keyword list frozen at tosijs-schema 1.7.0 and labelled every `oneOf` and
|
|
235
|
+
`exclusiveMinimum` field "not validated" while 1.8.0 was checking it. Our own doc site was
|
|
236
|
+
correct because the iife passed all three — so the failure was visible _only_ to the ESM
|
|
237
|
+
adopters the docs instruct. Pass all three.
|
|
238
|
+
- **`crud.table` and `crud.form` threw before hydration** — the only two accessors on the
|
|
239
|
+
class without the guard the rest have — so the documented way to reach the composed parts
|
|
240
|
+
threw on the line the docs told you to write. They return `null` now. The snippet was also
|
|
241
|
+
in a display-only fence, so no lane ran it; it is executable and tested. And its second line
|
|
242
|
+
was wrong too: `crud.form.readOnly = true` was silently reverted on the next render.
|
|
243
|
+
- **`<tosi-crud>` wrote `history.replaceState` on every search keystroke.** WebKit throws past
|
|
244
|
+
~100 calls in 10s — a held key clearing a long term gets there — and the throw preceded the
|
|
245
|
+
debounce, so search silently stopped working with an uncaught error. The URL write moved
|
|
246
|
+
inside the debounce, where the store query already was.
|
|
247
|
+
- **An empty `PREVIEW_HOST` swallowed the whole resolution chain.** `??` only skips
|
|
248
|
+
null/undefined, so `PREVIEW_HOST=''` — what a CI `env:` renders for a missing secret —
|
|
249
|
+
counted as "set" and hid both the site config and `~/local-secrets`, the exact symptom that
|
|
250
|
+
rung was added to abolish.
|
|
251
|
+
- **Adding or moving an array item dropped focus.** Every row is rebuilt, so the button you
|
|
252
|
+
just clicked stopped existing: you could not press ↓ twice to move an item two places, or
|
|
253
|
+
add two rows from the keyboard. Focus is restored by role and index, skipping controls that
|
|
254
|
+
are disabled at the destination.
|
|
255
|
+
- **`<tosi-schema-form>` was quadratic in the keystroke path** — one root-scoped
|
|
256
|
+
`querySelector` per field, twice, per keystroke. Measured on the built component: 800 fields
|
|
257
|
+
cost **240 ms** per keystroke before and **0.64 ms** after. An index built with the DOM
|
|
258
|
+
replaces the scans.
|
|
259
|
+
- **The redemption gate had 21 tests and its wiring had none.** Reverting the dev server to a
|
|
260
|
+
bare `redeemLink()` call restored both the unbounded queue and the unthrottled guess rate
|
|
261
|
+
while leaving every lane green — a security control held in place by nothing but the diff.
|
|
262
|
+
The decision is an exported function now, like the others in that file, and is tested.
|
|
263
|
+
- **Notes read `root.uniqueItems is not validated`** on a field called Tags — upstream's
|
|
264
|
+
`unenforcedKeywords` answers with paths. The leading `root.` is stripped and deeper segments
|
|
265
|
+
kept, since for a container the keyword may be several levels down.
|
|
266
|
+
- **These notes themselves published the old preview-host precedence**, inverted against the
|
|
267
|
+
code and against another paragraph thirty lines earlier. Corrected, with a test asserting
|
|
268
|
+
all four places that state it agree — that sentence had been written three different ways.
|
|
269
|
+
- **A duplicate mid-file import** in `dev-auth.test.ts`, found only because nothing had ever
|
|
270
|
+
type-checked the test suite. (~150 accumulated type errors remain; that is tracked, not
|
|
271
|
+
fixed here.)
|
|
272
|
+
- **The two vendored-type anti-drift guards were never type-checked** — `tsconfig.json`
|
|
273
|
+
excludes `*.test.ts`, so both were inert and one was **red**: `json-schema.test.ts` asserted
|
|
274
|
+
our `JSONSchema` is assignable to theirs, which TypeScript cannot decide because both types
|
|
275
|
+
recurse through `additionalProperties`. The guard now asserts the direction it CAN decide —
|
|
276
|
+
theirs → ours, the assignment the code actually performs when storing an inferred schema —
|
|
277
|
+
and the reverse is exercised at runtime by validating an ours-typed schema through their
|
|
278
|
+
validator. `bun run typecheck-guards` (`tsconfig.guards.json`) runs in CI beside the
|
|
279
|
+
existing typecheck; mutation-verified that drifting the vendored type turns it red.
|
|
280
|
+
- **`<tosi-table>`'s four column-menu captions** now use `localizePhrase`, so the docs and the
|
|
281
|
+
docs' own worked example agree.
|
|
282
|
+
|
|
283
|
+
### Also fixed in the review passes
|
|
284
|
+
|
|
285
|
+
- `<tosi-schema-form>` let the control's native `change` escape to consumers, so a form edit
|
|
286
|
+
fired a duplicate event — the `input`-only handler's `stopPropagation()` never saw a
|
|
287
|
+
`change`. Three events per checkbox click became one.
|
|
288
|
+
- `localizePhrase` asked "did the output differ from the input?" rather than "does a row
|
|
289
|
+
exist", so a locale that deliberately maps a phrase to itself fell back to joined fragments
|
|
290
|
+
— the behaviour the row was added to replace. It also re-implemented annotation stripping
|
|
291
|
+
with a weaker regex than the module's own, which already disagreed about `tag\#42`.
|
|
292
|
+
- `--tosi-spacing-50` is not a variable — the scale is `-xs/-sm/-lg/-xl` — and the `var()`
|
|
293
|
+
fallback made the mistake invisible. The three genuinely-new customization points
|
|
294
|
+
(`--tosi-error`, `--tosi-border`, `--tosi-border-radius`) are documented on the schema-form
|
|
295
|
+
page rather than left accidental.
|
|
296
|
+
- `<tosi-table>`'s `fieldFor` was documented as "cached per render pass" and cached nothing,
|
|
297
|
+
while allocating a schema and running the validator's keyword walker per editable cell per
|
|
298
|
+
render.
|
|
299
|
+
- `SiteConfig.tunnel.linkTtlMinutes` said "Default 15" two lines above "Defaults to 5"; the
|
|
300
|
+
summary line is what an editor tooltip shows.
|
|
301
|
+
- `tosijs-tunnel --link` now prints the 7-character code as well as the URL, which the docs
|
|
302
|
+
already described and only the dev server's own banner did.
|
|
303
|
+
- The "No preview host" instructions promised a `700` credentials directory and gave a command
|
|
304
|
+
that neither created nor secured it — and failed outright on a machine that did not have it.
|
|
305
|
+
- `src/docs/utilities.md` was a five-line stub duplicating a section `helper-libraries.md`
|
|
306
|
+
already provides. Removed.
|
|
307
|
+
- `src/docs/migrating.md` has a **1.11.0 section** — the validator seam, the `tjs-lang` floor,
|
|
308
|
+
and the edit-token change. It exists for "releases you may be upgrading past" and had no
|
|
309
|
+
entry despite this release having a Breaking section.
|
|
310
|
+
|
|
311
|
+
- `<tosi-table>`'s `change` event reported `oldValue: undefined` whenever no `focus` event had
|
|
312
|
+
fired on the cell — an unfocused browser window, or any consumer who sets `.value` and
|
|
313
|
+
dispatches `change`, which is what an adopter's test does. The old value came from a
|
|
314
|
+
`WeakMap` filled on focus, which existed because two-way `bindValue` used to overwrite the
|
|
315
|
+
model as the user typed. The one-way binding shipped earlier in this release ended that, so
|
|
316
|
+
the old value is now read from the model immediately before the write — no focus event
|
|
317
|
+
required, and one mechanism instead of two. Caught by the haltija lane, which the other
|
|
318
|
+
three lanes do not substitute for.
|
|
319
|
+
|
|
320
|
+
- `crud.save()` threw away the record it sent when the store returned nothing. A 204-style
|
|
321
|
+
adapter satisfies `Promise<any>` by resolving `undefined`, which is a perfectly ordinary
|
|
322
|
+
thing for a REST store to do, and the row went blank. It now keeps what it sent when the
|
|
323
|
+
store has nothing to say.
|
|
324
|
+
- The `doc-system.pw.ts` nested-doc-system flake — the **fourth** instance this release of a
|
|
325
|
+
spec racing the page's own inline doc tests. The runner is now disabled for that whole
|
|
326
|
+
spec file rather than per test, because after four the fix plainly belonged at the file
|
|
327
|
+
level. 6/6 clean since.
|
|
328
|
+
|
|
329
|
+
- `<tosi-doc-system>` gave up on the first failed `docs.json` fetch, so a doc site rendered
|
|
330
|
+
nothing at all when one request lost — a real-user failure, not only the test flake that
|
|
331
|
+
surfaced it. It now retries three times with full-jitter backoff (250ms base, 4s cap) and
|
|
332
|
+
honours `Retry-After`. It retries **only 429, 5xx and network errors**: a 404 or a 403 is an
|
|
333
|
+
answer, and re-asking cannot change it — which is what stops a retry turning an overloaded
|
|
334
|
+
server into a self-inflicted DoS.
|
|
335
|
+
|
|
336
|
+
### Known issues
|
|
337
|
+
|
|
338
|
+
- **firefox intermittently reports `no WebAssembly compiler available`** under the full
|
|
339
|
+
parallel test lane, which stops the inline-WASM example loading at all. Filed as
|
|
340
|
+
[tjs-lang#36](https://github.com/tonioloewald/tjs-lang/issues/36): a `wasm {} fallback {}`
|
|
341
|
+
block should take its fallback branch when the engine declines to compile, which is exactly
|
|
342
|
+
what that branch is for. Affects the test lane, not shipped behaviour — the example
|
|
343
|
+
degrades to not rendering rather than misreporting, and the doc-test guard against _silent_
|
|
344
|
+
fallback is unaffected.
|
|
345
|
+
|
|
346
|
+
- **[#102](https://github.com/tonioloewald/tosijs-ui/issues/102)** — `<tosi-table>`'s header
|
|
347
|
+
and body columns can disagree when a programmatic `columns` assignment lands while a render
|
|
348
|
+
is already in flight. The reporting app changes column visibility on a page-size change,
|
|
349
|
+
which is what produces the timing. The host app arguably should not reassign columns
|
|
350
|
+
mid-render, but a torn header/body is ours either way. Targeted at **1.11.1**; the path
|
|
351
|
+
predates 1.11.0.
|
|
352
|
+
|
|
353
|
+
### Fixed
|
|
354
|
+
|
|
355
|
+
**Packaging — this one affects existing `<tosi-table>` users.** `<tosi-table>` no longer reaches
|
|
356
|
+
for a schema library at all: it is the component people use _without_ one, and it must never make
|
|
357
|
+
anyone install something in order to build.
|
|
358
|
+
|
|
359
|
+
`tosijs-schema` was imported by
|
|
360
|
+
shipped `dist/` and declared in **no dependency field**, so a consumer's build failed on a
|
|
361
|
+
package they were never told they needed. It is now a declared optional peer at `^1.7.0` — which
|
|
362
|
+
is what the design note said from the start — and nothing we ship imports a type from an optional
|
|
363
|
+
peer any more: `import type` is erased from emitted JS but **not** from emitted `.d.ts`, and
|
|
364
|
+
declaring a peer optional does not help because TypeScript has no notion of an optional peer.
|
|
365
|
+
A second instance had been shipping for a while: `dist/code-editor-cm.d.ts` imported a type from
|
|
366
|
+
`tjs-lang/editors/codemirror`, a declared optional peer that TS2307s anyway (reported upstream as
|
|
367
|
+
tjs-lang#28). Verified from a packed tarball in a clean project: `tsc` and `vite build` both exit
|
|
368
|
+
0 with the peers absent.
|
|
369
|
+
|
|
370
|
+
- The `./*` subpath wildcard mapped `tosijs-ui/schema-form/fields.js` to
|
|
371
|
+
`dist/schema-form/fields.js.js`, so the extension-ful spelling — what an adopter copying this
|
|
372
|
+
repo's own ESM import style would write — did not resolve at all. A second `./*.js` wildcard
|
|
373
|
+
fixes it, strictly additively.
|
|
374
|
+
|
|
375
|
+
**Localization was wrong in shipped data, and the keys were why.** `<tosi-table>`'s column-header
|
|
376
|
+
menu asked for bare words, and our own translation table shows the cost: `Right` came back in the
|
|
377
|
+
_correct_ sense rather than the direction in four of nine languages (sv `Rätt`, zh `正确的`, es
|
|
378
|
+
`Bien`, it `Giusto`), `Column` as `柱子` (a pillar), `Sort` as `种类` / `종류` (a kind of thing),
|
|
379
|
+
`Show` as `Espectáculo` / `Spettacolo` (the kind with a stage). Every one is a competent
|
|
380
|
+
translation of the word it was given — which is the point. The keys now carry sense annotations
|
|
381
|
+
(`Right#direction`, `Column#table`, `Sort#order`, `Show#reveal`, `Hide#conceal`, `Pin#fasten`,
|
|
382
|
+
`Ascending#sort-order`), and `demo/src/localized-strings.ts` has matching rows with the
|
|
383
|
+
wrong-sense entries corrected, verified in a browser across sv/es/zh. Annotating a key is
|
|
384
|
+
backward compatible, so this cannot orphan an existing table.
|
|
385
|
+
|
|
386
|
+
**`<tosi-crud>` destroyed unsaved edits.** Four specs now cover delete, its rejection path and
|
|
387
|
+
`createNew`→`save`; mutation testing confirms that restoring the defects turns two of them red.
|
|
388
|
+
|
|
389
|
+
`showSelected()` compared `form.value !== _selected`,
|
|
390
|
+
and `setByPath` is immutable, so after the first keystroke that was permanently true and every
|
|
391
|
+
render wrote the record as loaded back over the edit — one frame later, with no user action, and
|
|
392
|
+
`save()` then posted the unedited record. The fix keeps the load guard
|
|
393
|
+
(`_loaded !== _selected`) — one invariant, one place — and drops a form-change listener that set
|
|
394
|
+
`_selected = form.value`, which nothing needed: `value` and `save()` read `form.value` directly,
|
|
395
|
+
and `remove()` is better with the record as opened than with a half-edited copy of it. Mutation
|
|
396
|
+
testing settled that: the two were independently sufficient, so neither was pinned and either
|
|
397
|
+
could have been removed without a single test failing.
|
|
398
|
+
|
|
399
|
+
- `<tosi-crud>`'s destructive paths ran in no lane, and the gap hid two defects: `createNew()`
|
|
400
|
+
followed by `remove()` sent `store.delete({})`, and the Delete button raised an unhandled
|
|
401
|
+
rejection on a rejecting store.
|
|
402
|
+
- Grid navigation swallowed arrows, Home and End inside editable cells, so the caret could not be
|
|
403
|
+
moved and focus jumped away mid-edit; on an enum `<select>` the cell could not be changed by
|
|
404
|
+
keyboard at all. Escape and Tab stay with the grid.
|
|
405
|
+
- The cell's native `change` no longer escapes the cell. It bubbles, so a listener on the table
|
|
406
|
+
received both — theirs with a `detail`, the input's with none.
|
|
407
|
+
- The "this keyword is not validated" note rendered **only for unions**, so the honesty it exists
|
|
408
|
+
for covered exactly one keyword while the docs promised otherwise.
|
|
409
|
+
- `validate` is called with `strict: true`. Without it the validator samples: `maxProperties` is
|
|
410
|
+
skipped entirely and a bad element deep in a long array is stepped over.
|
|
411
|
+
- The optional-peer degrade was silent. It now warns once naming the package and the seam, and
|
|
412
|
+
`validationAvailable` lets a Save handler tell _"this conforms"_ from _"nobody checked"_.
|
|
413
|
+
- We read `x-discriminator` first. `discriminator` is an OpenAPI keyword that tosijs-schema's own
|
|
414
|
+
`agentContract` refuses, so a schema written to pass their check spells it `x-`.
|
|
415
|
+
- The schema→control layer existed twice and had drifted: a `const` property was readonly in the
|
|
416
|
+
form and a freely editable text box in the table.
|
|
417
|
+
- Variant detection scores branches on how many required keys are present, rather than demanding
|
|
418
|
+
every key — a half-filled variant used to match nothing and show an empty box.
|
|
419
|
+
- `readOnly` hides the add/remove/reorder controls instead of disabling them.
|
|
420
|
+
- The array controls carry stable classes (`.schema-move-up`, `.schema-move-down`,
|
|
421
|
+
`.schema-remove`) alongside their localized `title`/`aria-label`: a localized tooltip cannot
|
|
422
|
+
also be a selector.
|
|
423
|
+
- The hash router treats `?…` in the hash as a query, not part of the path. `#/invoices/42?q=x`
|
|
424
|
+
previously matched no route at all.
|
|
425
|
+
|
|
426
|
+
The intermittent Playwright failure was chased rather than re-run until green: `hash-state.pw.ts`
|
|
427
|
+
on WebKit, about one run in four, because the page's own inline doc test writes into the hash
|
|
428
|
+
asynchronously and could land after the spec cleared it. A doc page under test is also a page
|
|
429
|
+
running tests.
|
|
430
|
+
|
|
431
|
+
**The dev bridge's own guard was a denial of service.** Serialized redemption on an
|
|
432
|
+
unauthenticated path with no depth cap meant 50 junk requests delayed a legitimate link by 42
|
|
433
|
+
seconds, and a trickle outgrew the drain so the denial outlasted the attack. Depth is capped;
|
|
434
|
+
overflow is refused instantly with 503 and `Retry-After`. The clock is also read on **arrival**,
|
|
435
|
+
so a token valid when you clicked can no longer expire while queued.
|
|
436
|
+
|
|
437
|
+
**Preview host handling.** A project's own `preview.host` outranks
|
|
438
|
+
`~/local-secrets/tosijs-preview.env`; the machine-global file holds one host, and when it won,
|
|
439
|
+
running `tosijs-tunnel` in someone else's checkout pointed it at your box — and that bin does not
|
|
440
|
+
merely print a target. One `resolvePreviewHost()` now serves all three bins. The tunnel's link
|
|
441
|
+
line no longer claims "Single-use edit link (valid 15 min)", and `doc-site-system.md` no longer
|
|
442
|
+
shows `host:` in its example config. `UPSTREAM.md` claimed 0.12.0 was the latest tjs-lang; stale
|
|
443
|
+
by two minors.
|
|
444
|
+
|
|
445
|
+
### Housekeeping: we published a preview address, and rotated it
|
|
446
|
+
|
|
447
|
+
Our own `tosijs-site.config.ts` committed the preview host as an ssh target
|
|
448
|
+
(`user@address`) — an **address, not a credential**. Nothing an adopter installs is
|
|
449
|
+
affected, and this is not a security advisory for anyone but us. Recording it because the
|
|
450
|
+
practice it produced is worth adopting.
|
|
451
|
+
|
|
452
|
+
Verified rather than assumed, since "we're sure there were no keys in there" is exactly the
|
|
453
|
+
sort of claim that deserves checking:
|
|
454
|
+
|
|
455
|
+
- **no private key material anywhere in the repo's history** — no `BEGIN … PRIVATE KEY`
|
|
456
|
+
block in any commit on any ref, and no `.pem`, `.key`, `id_rsa` or `id_ed25519` file ever
|
|
457
|
+
added. `*.pem` is gitignored; the only tracked files under `tls/` are a README and the
|
|
458
|
+
cert-generation script.
|
|
459
|
+
- access to that box always required a key that was never published, so the exposure was an
|
|
460
|
+
address someone could try to connect to — not a way in.
|
|
461
|
+
|
|
462
|
+
It has been **rotated regardless**, because a value that has ever been committed to a public
|
|
463
|
+
repo is public: `git log -S` finds it in seconds, and rewriting history does not un-publish
|
|
464
|
+
anything.
|
|
465
|
+
|
|
466
|
+
The practice that came out of it, now in `tosijs-coding-practices` → `deployment.md`:
|
|
467
|
+
machine-local credentials live in `~/local-secrets/` — a `700` directory beside the repos,
|
|
468
|
+
never inside one, so committing them is structurally impossible rather than merely against
|
|
469
|
+
the rules. `tosijs-ui`'s bins resolve `--host=` > `PREVIEW_HOST` > `PREVIEW_SSH` > site config >
|
|
470
|
+
`~/local-secrets/tosijs-preview.env` — the machine-global file is **last**, so a project that
|
|
471
|
+
names its own host is never silently redirected at someone else's box — and a regression test
|
|
472
|
+
now greps our own
|
|
473
|
+
config for a `user@host` literal. The rule had been documented in `site-config.ts` the whole
|
|
474
|
+
time and violated in the same repo — which is why the guarantee is now a directory.
|
|
475
|
+
|
|
476
|
+
## 1.10.2
|
|
477
|
+
|
|
478
|
+
> **Never released.** This version was tagged but never published, and the tag has since been
|
|
479
|
+
> deleted. Everything below shipped in **1.11.0** instead — including the `killStrayServer`
|
|
480
|
+
> fix, which is why the withdrawal is safe rather than a dropped security patch. npm went
|
|
481
|
+
> straight from 1.10.0 to 1.11.0. Kept here because these changes are real and a reader
|
|
482
|
+
> tracing when something landed needs to find them. ([#101](https://github.com/tonioloewald/tosijs-ui/issues/101))
|
|
483
|
+
|
|
484
|
+
**The dev server no longer serves stale content after a rebuild.** This is the one that
|
|
485
|
+
presented as random flakiness across several sessions: a served `/docs.json` disagreeing with
|
|
486
|
+
the file on disk made the doc browser's route match fail, so live-example insertion silently
|
|
487
|
+
skipped — the page rendered, hydration reported success, custom elements were defined, and
|
|
488
|
+
**zero examples appeared, with no error anywhere**.
|
|
489
|
+
|
|
490
|
+
The cause was a cache key of `path + lastModified`, with a comment asserting that a rebuild
|
|
491
|
+
invalidates it naturally. It does not: `lastModified` is millisecond-granularity, and six
|
|
492
|
+
rapid writes to one file report the same value. A rebuild that rewrote a file inside one
|
|
493
|
+
millisecond kept serving the first compressed body until the process restarted — which is why
|
|
494
|
+
a restart "fixed" it. The cache is now emptied on every completed rebuild, and the key carries
|
|
495
|
+
the file size. (#50)
|
|
496
|
+
|
|
497
|
+
**`killStrayServer` no longer shoots an unrelated process.** Reclaiming the port checked that
|
|
498
|
+
the holder was a JS runtime (`bun`/`node`/`deno`) — but "a JS runtime" is not "our dev
|
|
499
|
+
server", and a colleague's dev server, a language server or a test runner would all pass. It
|
|
500
|
+
now requires the process to be working in **this project**, and skips with a message naming
|
|
501
|
+
the directory when it is not. If the working directory cannot be read it skips too: refusing
|
|
502
|
+
to start costs you ten seconds, shooting the wrong process costs you an afternoon. (#77)
|
|
503
|
+
|
|
504
|
+
**`bun run release-check` now checks the version against what actually changed** — it blocks a
|
|
505
|
+
patch that carries a `[break]` annotation or touches a security-relevant path, and warns on a
|
|
506
|
+
`[change]`. The nine-lens review triggers on the version _letter_, so it only ever fired when
|
|
507
|
+
the letter was already right; 1.9.9 was prepared, gated, tagged and pushed as a patch while
|
|
508
|
+
relocating a build artifact and loosening a security default, and was caught only because a
|
|
509
|
+
human asked for a review anyway. (#78, #79)
|
|
510
|
+
|
|
511
|
+
**Docs:** `bundleOutDir` is in the Bundle config table it belongs in, and `checkExamples` —
|
|
512
|
+
including `contextKeys`, which **extends** the default context rather than replacing it — is
|
|
513
|
+
documented where adopters actually look. (#80, #81)
|
|
514
|
+
|
|
515
|
+
## 1.10.1
|
|
516
|
+
|
|
517
|
+
> **Never released.** This version was tagged but never published, and the tag has since been
|
|
518
|
+
> deleted. Everything below shipped in **1.11.0** instead — including the `killStrayServer`
|
|
519
|
+
> fix, which is why the withdrawal is safe rather than a dropped security patch. npm went
|
|
520
|
+
> straight from 1.10.0 to 1.11.0. Kept here because these changes are real and a reader
|
|
521
|
+
> tracing when something landed needs to find them. ([#101](https://github.com/tonioloewald/tosijs-ui/issues/101))
|
|
522
|
+
|
|
523
|
+
Two fixes to 1.10.0's row grouping and scroll preservation, both reported by snowfox against
|
|
524
|
+
a real wide, grouped, pinned table.
|
|
525
|
+
|
|
526
|
+
**A pinned column that repeats a group value is no longer a hole.** A column that is both
|
|
527
|
+
`pinned` and listed in `nonRepeatingGroupedRowCells` used `visibility: hidden` on its repeated
|
|
528
|
+
cells — which suppresses _everything the cell paints_, including the opaque background
|
|
529
|
+
`.col-pinned` exists to provide. On every row but the first of its group, horizontally
|
|
530
|
+
scrolled cells showed through the sticky column **and were clickable there**: a visual hole
|
|
531
|
+
and a hit-testing hole from one rule. Repeated cells now hide their _content_ (transparent
|
|
532
|
+
text, `display: none` on element children, `user-select: none`) and keep painting their box.
|
|
533
|
+
(#83)
|
|
534
|
+
|
|
535
|
+
**`preserveScroll` now restores horizontal position too.** It restored `scrollTop` alone, and
|
|
536
|
+
the tables that most need it are the wide ones — normally read scrolled sideways, with pinned
|
|
537
|
+
columns as the identity and the interesting columns off to the right. Sorting or toggling a
|
|
538
|
+
group snapped back to column 0, so the row stayed put while appearing to say something
|
|
539
|
+
completely different. `scrollLeft` is captured independently of the row anchor, so it also
|
|
540
|
+
survives on a table that is scrolled sideways but still at the top — which used to return
|
|
541
|
+
early and lose the columns. (#86)
|
|
542
|
+
|
|
543
|
+
Group ids are memoized per render in a `WeakMap`; the map is replaced each render, so a
|
|
544
|
+
cached id can never outlive the values it came from.
|
|
545
|
+
|
|
3
546
|
## 1.10.0
|
|
4
547
|
|
|
5
548
|
Mostly a **silent-failure** release: several things that were quietly not working, or quietly
|
package/bin/caddy-install.ts
CHANGED
|
@@ -34,7 +34,7 @@ Three deliberate properties:
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { existsSync, readFileSync } from 'fs'
|
|
37
|
-
import { resolveSiteConfig } from './resolve-site-config'
|
|
37
|
+
import { resolvePreviewHost, resolveSiteConfig } from './resolve-site-config'
|
|
38
38
|
|
|
39
39
|
const siteConfig = await resolveSiteConfig()
|
|
40
40
|
|
|
@@ -69,21 +69,19 @@ if (unknown.length || has('help')) {
|
|
|
69
69
|
process.exit(bad ? 1 : 0)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const host =
|
|
73
|
-
flag('host') ??
|
|
74
|
-
process.env.PREVIEW_HOST ??
|
|
75
|
-
// Legacy alias. This repo's own `deploy:index` script still asks for PREVIEW_SSH,
|
|
76
|
-
// so someone who exported the only variable it ever mentioned would otherwise be
|
|
77
|
-
// told "No preview host" with nothing pointing at why.
|
|
78
|
-
process.env.PREVIEW_SSH ??
|
|
79
|
-
siteConfig.preview?.host
|
|
72
|
+
const host = resolvePreviewHost(flag('host'), siteConfig.preview?.host)
|
|
80
73
|
const go = has('go')
|
|
81
74
|
|
|
82
75
|
if (!host) {
|
|
83
76
|
console.error(
|
|
84
77
|
`\nNo preview host.\n\n` +
|
|
85
|
-
`
|
|
86
|
-
`
|
|
78
|
+
` Put it where machine-local credentials live — a 700 directory beside the\n` +
|
|
79
|
+
` repos, so it cannot be committed:\n` +
|
|
80
|
+
` mkdir -p ~/local-secrets && chmod 700 ~/local-secrets\n` +
|
|
81
|
+
` echo 'PREVIEW_HOST=user@example.com' >> ~/local-secrets/tosijs-preview.env\n` +
|
|
82
|
+
` chmod 600 ~/local-secrets/tosijs-preview.env\n` +
|
|
83
|
+
` echo '[ -f ~/local-secrets/tosijs-preview.env ] && . ~/local-secrets/tosijs-preview.env' >> ~/.zshenv\n` +
|
|
84
|
+
` # ~/.zshenv, not ~/.zshrc — scripts and agents get no interactive profile\n\n` +
|
|
87
85
|
` ...or pass it per-run:\n` +
|
|
88
86
|
` tosijs-caddy-install --host=user@example.com --go\n` +
|
|
89
87
|
` PREVIEW_HOST=user@example.com tosijs-caddy-install\n`
|
package/bin/deploy-preview.ts
CHANGED
|
@@ -28,7 +28,11 @@ Two deliberate safety properties:
|
|
|
28
28
|
|
|
29
29
|
import { $ } from 'bun'
|
|
30
30
|
import { existsSync } from 'fs'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
previewRootFor,
|
|
33
|
+
registerCaddyFragment,
|
|
34
|
+
resolvePreviewHost,
|
|
35
|
+
} from './resolve-site-config'
|
|
32
36
|
import {
|
|
33
37
|
resolveSiteConfig,
|
|
34
38
|
isSafeRemotePath,
|
|
@@ -51,14 +55,7 @@ const preview = siteConfig.preview
|
|
|
51
55
|
// Precedence: explicit flag > env > site config. The config is the "just works"
|
|
52
56
|
// default; the flag exists so you can push a one-off somewhere else without editing
|
|
53
57
|
// (and committing) a config file.
|
|
54
|
-
const host =
|
|
55
|
-
flag('host') ??
|
|
56
|
-
process.env.PREVIEW_HOST ??
|
|
57
|
-
// Legacy alias. This repo's own `deploy:index` script still asks for PREVIEW_SSH,
|
|
58
|
-
// so someone who exported the only variable it ever mentioned would otherwise be
|
|
59
|
-
// told "No preview host" with nothing pointing at why.
|
|
60
|
-
process.env.PREVIEW_SSH ??
|
|
61
|
-
preview?.host
|
|
58
|
+
const host = resolvePreviewHost(flag('host'), preview?.host)
|
|
62
59
|
const remotePath =
|
|
63
60
|
flag('path') ??
|
|
64
61
|
process.env.PREVIEW_PATH ??
|
|
@@ -70,8 +67,13 @@ const go = has('go')
|
|
|
70
67
|
if (!host) {
|
|
71
68
|
console.error(
|
|
72
69
|
`\nNo preview host.\n\n` +
|
|
73
|
-
`
|
|
74
|
-
`
|
|
70
|
+
` Put it where machine-local credentials live — a 700 directory beside the\n` +
|
|
71
|
+
` repos, so it cannot be committed:\n` +
|
|
72
|
+
` mkdir -p ~/local-secrets && chmod 700 ~/local-secrets\n` +
|
|
73
|
+
` echo 'PREVIEW_HOST=user@example.com' >> ~/local-secrets/tosijs-preview.env\n` +
|
|
74
|
+
` chmod 600 ~/local-secrets/tosijs-preview.env\n` +
|
|
75
|
+
` echo '[ -f ~/local-secrets/tosijs-preview.env ] && . ~/local-secrets/tosijs-preview.env' >> ~/.zshenv\n` +
|
|
76
|
+
` # ~/.zshenv, not ~/.zshrc — scripts and agents get no interactive profile\n\n` +
|
|
75
77
|
` ...or pass it per-run:\n` +
|
|
76
78
|
` tosijs-deploy --host=user@example.com [--path=/srv/preview/x] [--go]\n` +
|
|
77
79
|
` PREVIEW_HOST=user@example.com tosijs-deploy\n`
|