@abide/abide 0.48.0 → 0.49.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/AGENTS.md +15 -7
- package/CHANGELOG.md +39 -0
- package/README.md +9 -3
- package/package.json +3 -1
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -3
- package/src/bundleApp.ts +2 -3
- package/src/compile.ts +2 -4
- package/src/lib/bundle/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- package/src/lib/server/rpc/parseArgs.ts +15 -1
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- package/src/lib/server/runtime/finalizeResponse.ts +11 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -2
- package/src/lib/shared/buildArtifact.ts +17 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- package/src/lib/shared/canonicalJson.ts +24 -1
- package/src/lib/shared/createChannelLog.ts +24 -0
- package/src/lib/shared/exitOnBuildFailure.ts +4 -3
- package/src/lib/shared/parseEnv.ts +17 -6
- package/src/lib/shared/serializeEnv.ts +18 -6
- package/src/lib/shared/snippet.ts +11 -6
- package/src/lib/shared/url.ts +5 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/bindListenEvent.ts +5 -6
- package/src/lib/ui/compile/compileSSR.ts +1 -1
- package/src/lib/ui/compile/compileShadow.ts +121 -25
- package/src/lib/ui/compile/composeProps.ts +3 -2
- package/src/lib/ui/compile/desugarSignals.ts +1 -1
- package/src/lib/ui/compile/generateBuild.ts +23 -9
- package/src/lib/ui/compile/generateSSR.ts +49 -37
- package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
- package/src/lib/ui/compile/lowerDocAccess.ts +39 -1
- package/src/lib/ui/compile/parseTemplate.ts +3 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +5 -18
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- package/src/lib/ui/dom/awaitBlock.ts +67 -29
- package/src/lib/ui/dom/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/each.ts +15 -8
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- package/src/lib/ui/dom/mergeProps.ts +1 -1
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/mutateDocArray.ts +38 -0
- package/src/lib/ui/dom/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/tryBlock.ts +10 -11
- package/src/lib/ui/dom/withScope.ts +7 -0
- package/src/lib/ui/history.ts +14 -7
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/props.ts +17 -0
- package/src/lib/ui/renderChain.ts +7 -3
- package/src/lib/ui/router.ts +23 -17
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- package/src/lib/ui/runtime/applyPatchToTree.ts +16 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +22 -7
- package/src/lib/ui/runtime/createDoc.ts +28 -12
- package/src/lib/ui/runtime/flushEffects.ts +23 -1
- package/src/lib/ui/runtime/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- package/src/lib/ui/runtime/types/UiProps.ts +6 -5
- package/src/lib/ui/runtime/withoutHydration.ts +22 -0
- package/src/lib/ui/state.ts +9 -3
package/AGENTS.md
CHANGED
|
@@ -189,7 +189,7 @@ cells read/written through `.value`.
|
|
|
189
189
|
| `state.share(key, value)` / `state.shared(key)` | members of `state` | Put a named value on the ambient scope / read the closest ancestor's |
|
|
190
190
|
| `watch(source, handler)` | `@abide/abide/ui/watch` | The single reaction primitive (client-only, stripped from SSR). Sources: a bare thunk `watch(() => …)` (auto-tracked effect), a state cell, a cell array, a socket/stream (`handler(frame)` per frame with reconnect replay), an rpc (`watch(fn, args?, handler)` — runs the smart read, `handler(value)` on each change). Returns a scope-tied disposer |
|
|
191
191
|
| `html(str)` / `` html`…` `` | `@abide/abide/ui/html` | Brands trusted raw HTML so `{expr}` inserts nodes instead of escaped text; plain `{value}` always escapes |
|
|
192
|
-
| `props()` |
|
|
192
|
+
| `props()` | `@abide/abide/ui/props` | The prop reader, resolved by import binding (alias-safe) like `state`: `const { name = fallback, ...rest } = props()`; a page/layout's declared props are additive with its route-param shape. `children` is an ordinary declared prop (`const { children } = props<{ children: Snippet }>()`), not ambient |
|
|
193
193
|
|
|
194
194
|
Bindings and directives (the attribute kinds `readAttributes` parses):
|
|
195
195
|
|
|
@@ -221,9 +221,15 @@ error):
|
|
|
221
221
|
| `{#snippet name(args)}…{/snippet}` | Declares a reusable builder, called as an interpolation: `{name(args)}`; a snippet value passes through props like any other value |
|
|
222
222
|
|
|
223
223
|
Components are capitalised tags (`<Panel prop={x}>…</Panel>`); nested content
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
becomes the component's `children` prop — an ordinary declared prop of type
|
|
225
|
+
`Snippet`, read with `const { children } = props<{ children: Snippet }>()`
|
|
226
|
+
and called as `{children()}` — the single fill point (`{#if children}
|
|
227
|
+
{children()}{:else}…{/if}` for a fallback; there are no named slots and no
|
|
228
|
+
`<slot>` element). Slotted content (`<Panel>…</Panel>`) and an explicit
|
|
229
|
+
`children={aSnippet}` attribute set the same prop — slotted content rides in
|
|
230
|
+
as the trailing prop layer, so it wins over an explicit `children` attribute
|
|
231
|
+
on the same tag (`mergeProps`, last layer wins per key). A layout's
|
|
232
|
+
`{children()}` is the route outlet.
|
|
227
233
|
|
|
228
234
|
`<script>` and `<style>` are **not component-root-only**: either may sit
|
|
229
235
|
inside a control-flow branch, scoped to that branch. A nested `<script>`
|
|
@@ -233,8 +239,9 @@ there — imports live in the leading `<script>`). A **root** `<style>` is
|
|
|
233
239
|
component-scoped; a nested `<style>` scopes to its sibling subtree only.
|
|
234
240
|
|
|
235
241
|
Removed forms throw migration errors at parse time: the `<slot>` element (use
|
|
236
|
-
`
|
|
237
|
-
|
|
242
|
+
a declared `children: Snippet` prop, called `{children()}`), `<template
|
|
243
|
+
name>` snippets (use `{#snippet}`), and all `<template
|
|
244
|
+
if/each/await/switch/…>` control flow (use `{#…}` blocks).
|
|
238
245
|
|
|
239
246
|
## Server surface — `abide/server/*`
|
|
240
247
|
|
|
@@ -325,7 +332,7 @@ Probes report, never act — reading one opens no fetch and no stream.
|
|
|
325
332
|
|
|
326
333
|
### Templating — `@documentation templating`
|
|
327
334
|
|
|
328
|
-
- `@abide/abide/shared/snippet` — `snippet(payload)`: brands a snippet payload so a `{expr}` interpolation mounts it (client: a DOM builder; server: the rendered string); the compiler wraps `{#snippet}` bodies in this. Also exports the `Snippet<
|
|
335
|
+
- `@abide/abide/shared/snippet` — `snippet(payload)`: brands a snippet payload so a `{expr}` interpolation mounts it (client: a DOM builder; server: the rendered string); the compiler wraps `{#snippet}` bodies in this. Also exports the `Snippet<Args>` type — a callable `(...args: Args) => SnippetValue`, generic over its call arguments (`children` is `Snippet`, invoked `children()`; a row snippet is `Snippet<[Item]>`, invoked `row(item)`) — plus `SnippetValue` (the internal payload brand) and `snippetPayload(value)` (a branded value's payload, or undefined for plain values).
|
|
329
336
|
|
|
330
337
|
### Shared plumbing — `@documentation plumbing`
|
|
331
338
|
|
|
@@ -337,6 +344,7 @@ Probes report, never act — reading one opens no fetch and no stream.
|
|
|
337
344
|
|
|
338
345
|
- `@abide/abide/ui/state` — the `state` primitive: `state(initial, transform?)` writable cell, `state.computed(fn)` read-only derived, `state.linked(fn, transform?)` writable-reseeded, `state.share(key, value)` / `state.shared(key)` ambient context. In `.abide` files the compiler lowers reads/writes to plain variable syntax; in `.ts` the cell is read/written through `.value`.
|
|
339
346
|
- `@abide/abide/ui/watch` — `watch(source, handler)`: the single reaction primitive over a thunk, cell, cell array, socket/stream, or rpc (see the grammar table). Client-only; the compiler strips author calls from SSR, and the `socket.watch` / `fn.watch` instance sugar is SSR-inert.
|
|
347
|
+
- `@abide/abide/ui/props` — `props<T>()`: the prop reader, resolved by import binding (alias-safe) like `state`; a required import — there is no ambient `props()`. Destructure declared props off it (`const { name, ...rest } = props<T>()`); a page/layout's declared `T` is additive with its auto-typed route-param shape. `children` is an ordinary declared prop, not ambient: `const { children } = props<{ children: Snippet }>()` (`Snippet` from `@abide/abide/shared/snippet`).
|
|
340
348
|
|
|
341
349
|
### Templating — `@documentation templating`
|
|
342
350
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.49.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dfb4f13: props import required + additive route-shape type; drop ambient children ([`0dbe248`](https://github.com/briancray/abide/commit/0dbe248fc959f3ca8168fb83e6f30619a17eff3b))
|
|
8
|
+
- dfb4f13: slotted content rides the children prop key as a Snippet ([`0de19e5`](https://github.com/briancray/abide/commit/0de19e594dabb6ddd92b08d5e473f13296f17247))
|
|
9
|
+
- dfb4f13: publish abide/ui/props module for the props reader ([`197c1a9`](https://github.com/briancray/abide/commit/197c1a900c0ea81b40d1cbb4a6bec1e021b9e932))
|
|
10
|
+
- dfb4f13: type element attach node param from the element tag ([`444ef35`](https://github.com/briancray/abide/commit/444ef356372f8fc39a0b5786bca2f70dcf750c53))
|
|
11
|
+
- 951e90b: `props` is now a required import (`@abide/abide/ui/props`), resolved by import binding like `state` — a bare ambient `props()` no longer works, and every component that reads props must `import { props } from '@abide/abide/ui/props'`.
|
|
12
|
+
|
|
13
|
+
`children` is now an ordinary declared prop instead of an ambient callable: read it with `const { children } = props<{ children: Snippet }>()` (`Snippet` from `@abide/abide/shared/snippet`). Slotted content (`<Panel>…</Panel>`) and an explicit `children={aSnippet}` attribute now set the same `children` prop key (slotted content wins if both are present). `props<T>()` stays additive — a page/layout still gets its route-param shape auto-typed, and declaring `children` (or any other prop) only adds to it.
|
|
14
|
+
|
|
15
|
+
The `Snippet` type's generic changed from its internal payload to its call arguments: `Snippet<Args extends unknown[] = []>` is now `(...args: Args) => SnippetValue` — `children` is `Snippet` (invoked `children()`), a row/label snippet is `Snippet<[Item]>` (invoked `row(item)`). The former payload-generic form (`Snippet<Payload>` describing the value flowing through) is gone; the internal payload is the newly exported `SnippetValue`.
|
|
16
|
+
|
|
17
|
+
**Behavior change:** an unguarded `{children()}` on a component mounted without children now throws at render, where it previously rendered nothing. Guard with `{#if children}{children()}{:else}…{/if}`, or declare `children: Snippet` as a required (non-optional) prop so `abide check` catches missing children at the call site.
|
|
18
|
+
|
|
19
|
+
Migration: add `import { props } from '@abide/abide/ui/props'` to every component that calls `props()`; for any component that reads `children`, add `import type { Snippet } from '@abide/abide/shared/snippet'` and declare it — `const { children } = props<{ children: Snippet }>()` (or `children?: Snippet` if optional).
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 4a6bf93: mount a reactive/loop-bound component tag by its value, not the cell ([`32a7cb8`](https://github.com/briancray/abide/commit/32a7cb8bdef0800d76b418eefbe550b0a939cd73))
|
|
24
|
+
- dfb4f13: correct UiProps.children to the ordinary prop-thunk shape ([`33a0de7`](https://github.com/briancray/abide/commit/33a0de72dc2fe6ce4a0e025e4158ab192e47f1f0))
|
|
25
|
+
- c7d25e4: type state<T>(undefined) and binding-annotated state() as T | undefined ([`3c40d99`](https://github.com/briancray/abide/commit/3c40d9981617968330a00bc2f034456b80365419))
|
|
26
|
+
- c7d25e4: await a snippet whose body fills a {children()} slot ([`4c2744b`](https://github.com/briancray/abide/commit/4c2744bc1aa42728a80adb3426a77c02c79b30d4))
|
|
27
|
+
- dfb4f13: repair pre-existing gate failures blocking release ([`4c55a4a`](https://github.com/briancray/abide/commit/4c55a4a1cbcb0b2afc842f7cc7134aed590006ce))
|
|
28
|
+
- dfb4f13: layout child sentinel + restProps skip move to the children key ([`500d1d8`](https://github.com/briancray/abide/commit/500d1d8d696147602eb1ce34706cbbbf724ceb0b))
|
|
29
|
+
- dfb4f13: fix stale props-declaration comment in compileShadow ([`532e079`](https://github.com/briancray/abide/commit/532e0796c54aa77c7b155145ae2d16193b7de4e5))
|
|
30
|
+
- c7d25e4: expand a thrown Bun.build AggregateError into per-file diagnostics ([`5643d95`](https://github.com/briancray/abide/commit/5643d95691dd9211bc5f3edb5a2ae77aa3989f63))
|
|
31
|
+
- dfb4f13: children lowers as a destructured prop, not $children ([`60bbbeb`](https://github.com/briancray/abide/commit/60bbbeb15cae41d34e5f5b4f07590bf0a5a1b795))
|
|
32
|
+
- dfb4f13: resolve 35 correctness/ergonomics findings from full-tree audit ([`73a72ac`](https://github.com/briancray/abide/commit/73a72ac1946f901f76d790b38fa3ebbf29b661fe))
|
|
33
|
+
- dfb4f13: extract buildArtifact (build-or-die primitive) ([`938e312`](https://github.com/briancray/abide/commit/938e3129f03cb6787624703422cd9472fbdcb83b))
|
|
34
|
+
- dfb4f13: format spreadProps + renderChain to canonical prettier ([`97b1565`](https://github.com/briancray/abide/commit/97b1565670eb0977b8079425f8e92becd6deb68e))
|
|
35
|
+
- dfb4f13: thunk-wrap layout children sentinel so {#if children} presence reads truthy ([`a884de1`](https://github.com/briancray/abide/commit/a884de1bbb0fcffaea91268cf49aa1d1b24a3d7a))
|
|
36
|
+
- 4a6bf93: drop an await settle whose anchor was detached from the tree ([`ad3d00c`](https://github.com/briancray/abide/commit/ad3d00c36ecfd935721164e7ebff7a7b3fffca4e))
|
|
37
|
+
- dfb4f13: resolve props by import binding, not ambient special-case ([`b624893`](https://github.com/briancray/abide/commit/b624893745aa0e1fdb407047b15f29ac0ceac3ec))
|
|
38
|
+
- dfb4f13: shadow declares props under the author's local alias ([`b821a5b`](https://github.com/briancray/abide/commit/b821a5bfef17233b2aa24502ea397eede62329a0))
|
|
39
|
+
- dfb4f13: extract withoutHydration (5 sites -> 1 helper) ([`c36be09`](https://github.com/briancray/abide/commit/c36be09d64846f76216bfcc7932cdbc026631f04))
|
|
40
|
+
- dfb4f13: redefine Snippet<Args> as a callable; add SnippetValue ([`ecc861f`](https://github.com/briancray/abide/commit/ecc861f589d5149a299b92ba6d08fd362644bcd1))
|
|
41
|
+
|
|
3
42
|
## 0.48.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -130,11 +130,16 @@ component you read and write state as plain variables. The same file renders on
|
|
|
130
130
|
the server — blocking awaits inline, streaming awaits out of order, cached
|
|
131
131
|
reads seeded warm — and hydrates in the browser.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Nested content becomes the component's `children` prop — an ordinary declared
|
|
134
|
+
prop of type `Snippet` — and renders wherever the component calls
|
|
135
|
+
`{children()}`:
|
|
134
136
|
|
|
135
137
|
```html
|
|
136
138
|
<script>
|
|
137
|
-
|
|
139
|
+
import { props } from '@abide/abide/ui/props'
|
|
140
|
+
import type { Snippet } from '@abide/abide/shared/snippet'
|
|
141
|
+
|
|
142
|
+
const { title, children } = props<{ title: string; children?: Snippet }>()
|
|
138
143
|
</script>
|
|
139
144
|
|
|
140
145
|
<section>
|
|
@@ -160,9 +165,10 @@ template grammar:
|
|
|
160
165
|
import { state } from '@abide/abide/ui/state'
|
|
161
166
|
import { watch } from '@abide/abide/ui/watch'
|
|
162
167
|
import { html } from '@abide/abide/ui/html'
|
|
168
|
+
import { props } from '@abide/abide/ui/props'
|
|
163
169
|
import Panel from '$ui/components/Panel.abide'
|
|
164
170
|
|
|
165
|
-
const { room = 'lobby' } = props()
|
|
171
|
+
const { room = 'lobby' } = props<{ room?: string }>()
|
|
166
172
|
|
|
167
173
|
let draft = state('')
|
|
168
174
|
let author = state('anon')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"./shared/createSubscriber": "./src/lib/shared/createSubscriber.ts",
|
|
78
78
|
"./ui/html": "./src/lib/ui/html.ts",
|
|
79
79
|
"./ui/state": "./src/lib/ui/state.ts",
|
|
80
|
+
"./ui/props": "./src/lib/ui/props.ts",
|
|
80
81
|
"./ui/effect": "./src/lib/ui/effect.ts",
|
|
81
82
|
"./ui/currentScope": "./src/lib/ui/scope.ts",
|
|
82
83
|
"./ui/enterRenderScope": "./src/lib/ui/enterScope.ts",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"./ui/dom/restProps": "./src/lib/ui/dom/restProps.ts",
|
|
90
91
|
"./ui/dom/spreadAttrs": "./src/lib/ui/dom/spreadAttrs.ts",
|
|
91
92
|
"./ui/dom/readCall": "./src/lib/ui/dom/readCall.ts",
|
|
93
|
+
"./ui/dom/mutateDocArray": "./src/lib/ui/dom/mutateDocArray.ts",
|
|
92
94
|
"./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
|
|
93
95
|
"./ui/dom/text": "./src/lib/ui/dom/text.ts",
|
|
94
96
|
"./ui/dom/appendText": "./src/lib/ui/dom/appendText.ts",
|
package/src/buildCli.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { dirname, join } from 'node:path'
|
|
|
2
2
|
import { build } from './build.ts'
|
|
3
3
|
import { compile } from './compile.ts'
|
|
4
4
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
5
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
5
6
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
6
7
|
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
7
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
8
8
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
9
9
|
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
10
10
|
import type { CompileTarget } from './lib/shared/types/CompileTarget.ts'
|
|
@@ -56,14 +56,13 @@ export async function buildCli({
|
|
|
56
56
|
stdout. We don't `bun build --compile` here because the discovery output is
|
|
57
57
|
throwaway; a plain JS bundle runs faster. Additive — does not clear dist.
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
await buildArtifact({
|
|
60
60
|
entrypoints: [DISCOVERY_ENTRY],
|
|
61
61
|
target: 'bun',
|
|
62
62
|
outdir: distDir,
|
|
63
63
|
naming: '_discovery.js',
|
|
64
64
|
plugins,
|
|
65
65
|
})
|
|
66
|
-
exitOnBuildFailure(discoveryResult)
|
|
67
66
|
|
|
68
67
|
const proc = Bun.spawn({
|
|
69
68
|
cmd: ['bun', discoveryOut],
|
|
@@ -94,13 +93,12 @@ export async function buildCli({
|
|
|
94
93
|
async function buildTargetPair(platformTarget: CompileTarget, cliOut: string): Promise<string> {
|
|
95
94
|
const serverOut = join(dirname(cliOut), `server${exeSuffix(platformTarget)}`)
|
|
96
95
|
await compile({ cwd, target: platformTarget, outfile: serverOut, buildClient: false })
|
|
97
|
-
|
|
96
|
+
await buildArtifact({
|
|
98
97
|
entrypoints: [CLI_ENTRY],
|
|
99
98
|
target: 'bun',
|
|
100
99
|
compile: { target: platformTarget, outfile: cliOut },
|
|
101
100
|
plugins,
|
|
102
101
|
})
|
|
103
|
-
exitOnBuildFailure(result)
|
|
104
102
|
abideLog.success(`compiled cli + server: ${cliOut}`)
|
|
105
103
|
return cliOut
|
|
106
104
|
}
|
package/src/buildDisconnected.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clientBuildPlugins } from './clientBuildPlugins.ts'
|
|
2
2
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
4
4
|
|
|
5
5
|
const ENTRY = new URL('./bundleDisconnectedEntry.ts', import.meta.url).pathname
|
|
6
6
|
const CSS_ENTRY = new URL('./lib/bundle/disconnected.css', import.meta.url).pathname
|
|
@@ -47,7 +47,7 @@ export async function buildDisconnected({
|
|
|
47
47
|
the entry (which TS can't type), so it compiles to a standalone artifact we
|
|
48
48
|
inline alongside the JS — the component carries no <style> of its own.
|
|
49
49
|
*/
|
|
50
|
-
const result = await
|
|
50
|
+
const result = await buildArtifact({
|
|
51
51
|
entrypoints: [ENTRY, CSS_ENTRY],
|
|
52
52
|
target: 'browser',
|
|
53
53
|
minify: true,
|
|
@@ -55,7 +55,6 @@ export async function buildDisconnected({
|
|
|
55
55
|
define: { __ABIDE_DEV__: 'false' },
|
|
56
56
|
plugins,
|
|
57
57
|
})
|
|
58
|
-
exitOnBuildFailure(result)
|
|
59
58
|
|
|
60
59
|
// Collect the JS bundle + extracted CSS from the in-memory artifacts.
|
|
61
60
|
let js = ''
|
package/src/bundleApp.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { serverBinaryFilename } from './lib/bundle/serverBinaryFilename.ts'
|
|
|
8
8
|
import { signMacApp } from './lib/bundle/signMacApp.ts'
|
|
9
9
|
import { webviewLibName } from './lib/bundle/webviewLibName.ts'
|
|
10
10
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
11
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
11
12
|
import { bundleLayout } from './lib/shared/bundleLayout.ts'
|
|
12
13
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
13
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
14
14
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
15
15
|
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
16
16
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
@@ -82,7 +82,7 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
82
82
|
// 3. Launcher binary — named after the program so CFBundleExecutable matches.
|
|
83
83
|
const launcherSuffix = target.includes('windows') ? '.exe' : ''
|
|
84
84
|
const launcherPath = `${binDir}/${programName}${launcherSuffix}`
|
|
85
|
-
|
|
85
|
+
await buildArtifact({
|
|
86
86
|
entrypoints: [APP_ENTRY],
|
|
87
87
|
target: 'bun',
|
|
88
88
|
compile: { target, outfile: launcherPath },
|
|
@@ -96,7 +96,6 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
96
96
|
*/
|
|
97
97
|
define: { __ABIDE_WORKER_ENTRY__: JSON.stringify(WORKER_ENTRY) },
|
|
98
98
|
})
|
|
99
|
-
exitOnBuildFailure(launcherResult)
|
|
100
99
|
|
|
101
100
|
// 4. Webview lib — built from the vendored source if needed, then copied
|
|
102
101
|
// beside the binaries (or into Frameworks on macOS) so the bundle is self-contained.
|
package/src/compile.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { build } from './build.ts'
|
|
2
2
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
3
|
+
import { buildArtifact } from './lib/shared/buildArtifact.ts'
|
|
3
4
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
4
5
|
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
5
|
-
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
6
6
|
import type { CompileTarget } from './lib/shared/types/CompileTarget.ts'
|
|
7
7
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
8
8
|
|
|
@@ -40,7 +40,7 @@ export async function compile({
|
|
|
40
40
|
|
|
41
41
|
const outPath = outfile ?? `${cwd}/dist/app${exeSuffix(target)}`
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
await buildArtifact({
|
|
44
44
|
entrypoints: [SERVER_ENTRY],
|
|
45
45
|
target: 'bun',
|
|
46
46
|
format: 'esm',
|
|
@@ -56,8 +56,6 @@ export async function compile({
|
|
|
56
56
|
plugins: serverBuildPlugins({ cwd, embedAssets: true }),
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
exitOnBuildFailure(result)
|
|
60
|
-
|
|
61
59
|
abideLog.success(`compiled standalone binary: ${outPath} (target: ${target})`)
|
|
62
60
|
return outPath
|
|
63
61
|
}
|
|
@@ -16,9 +16,19 @@ export function installDownloads(libPath: string, webviewHandle: Pointer | null)
|
|
|
16
16
|
if (process.platform !== 'darwin') {
|
|
17
17
|
return
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/* abide_install_downloads exists only in abide's own webview lib. A vanilla
|
|
20
|
+
ABIDE_WEBVIEW_LIB on darwin lacks it, so dlopen throws — degrade to a no-op
|
|
21
|
+
(downloads simply unhandled) instead of crashing the launcher, matching the
|
|
22
|
+
optional-symbol FFI binders. */
|
|
23
|
+
let symbols: { abide_install_downloads: (handle: Pointer | null) => void }
|
|
24
|
+
let close: () => void
|
|
25
|
+
try {
|
|
26
|
+
;({ symbols, close } = dlopen(libPath, {
|
|
27
|
+
abide_install_downloads: { args: [FFIType.ptr], returns: FFIType.void },
|
|
28
|
+
}))
|
|
29
|
+
} catch {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
22
32
|
symbols.abide_install_downloads(webviewHandle)
|
|
23
33
|
close()
|
|
24
34
|
}
|
|
@@ -30,9 +30,19 @@ export function installMacMenu(
|
|
|
30
30
|
if (process.platform !== 'darwin') {
|
|
31
31
|
return
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/* The abide_install_app_menu symbol is compiled only into abide's own webview lib.
|
|
34
|
+
A user who points ABIDE_WEBVIEW_LIB at a vanilla webview.dylib is on darwin but
|
|
35
|
+
lacks the symbol, so dlopen throws — degrade to a no-op (native menu simply absent)
|
|
36
|
+
rather than crashing the launcher, matching bindConnectedFlag/bindRequestNavigate. */
|
|
37
|
+
let symbols: { abide_install_app_menu: (handle: Pointer | null, config: Uint8Array) => void }
|
|
38
|
+
let close: () => void
|
|
39
|
+
try {
|
|
40
|
+
;({ symbols, close } = dlopen(libPath, {
|
|
41
|
+
abide_install_app_menu: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
42
|
+
}))
|
|
43
|
+
} catch {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
36
46
|
const config = JSON.stringify({ appName, fileMenu, menu })
|
|
37
47
|
symbols.abide_install_app_menu(webviewHandle, new TextEncoder().encode(`${config}\0`))
|
|
38
48
|
close()
|
|
@@ -74,8 +74,36 @@ export async function parseArgvForRpc(
|
|
|
74
74
|
}
|
|
75
75
|
const name = token.name
|
|
76
76
|
const propType = properties[name]?.type
|
|
77
|
+
/* Coerce one string token to a schema scalar type (or throw). Shared by scalar
|
|
78
|
+
props and array ELEMENTS so `--tags 1 --tags 2` on `z.array(z.number())` yields
|
|
79
|
+
numbers, not strings the server then rejects. */
|
|
80
|
+
const coerceScalar = (raw: string, type: string | undefined): unknown => {
|
|
81
|
+
if (type === 'number' || type === 'integer') {
|
|
82
|
+
// Reject a blank value explicitly — `Number('')` / `Number(' ')` is 0,
|
|
83
|
+
// not NaN, so the NaN guard alone would silently coerce it to zero.
|
|
84
|
+
const n = raw.trim() === '' ? Number.NaN : Number(raw)
|
|
85
|
+
if (Number.isNaN(n)) {
|
|
86
|
+
throw new Error(`--${name} expects a number, got ${raw}`)
|
|
87
|
+
}
|
|
88
|
+
return n
|
|
89
|
+
}
|
|
90
|
+
if (type === 'boolean') {
|
|
91
|
+
const lowered = raw.trim().toLowerCase()
|
|
92
|
+
if (lowered === 'true' || lowered === '1') {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
if (lowered === 'false' || lowered === '0') {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`--${name} expects true or false, got ${raw}`)
|
|
99
|
+
}
|
|
100
|
+
return raw
|
|
101
|
+
}
|
|
77
102
|
if (propType === 'boolean') {
|
|
78
|
-
|
|
103
|
+
/* Bare `--flag` / `--no-flag` toggles; inline `--flag=false` honours the RHS
|
|
104
|
+
instead of always resolving to true. */
|
|
105
|
+
args[name] =
|
|
106
|
+
token.value !== undefined ? coerceScalar(token.value, 'boolean') : !token.negated
|
|
79
107
|
continue
|
|
80
108
|
}
|
|
81
109
|
if (token.missingValue || token.value === undefined) {
|
|
@@ -83,18 +111,17 @@ export async function parseArgvForRpc(
|
|
|
83
111
|
}
|
|
84
112
|
const value = token.value
|
|
85
113
|
if (propType === 'number' || propType === 'integer') {
|
|
86
|
-
|
|
87
|
-
// not NaN, so the NaN guard alone would silently coerce it to zero.
|
|
88
|
-
const n = value.trim() === '' ? Number.NaN : Number(value)
|
|
89
|
-
if (Number.isNaN(n)) {
|
|
90
|
-
throw new Error(`--${name} expects a number, got ${value}`)
|
|
91
|
-
}
|
|
92
|
-
args[name] = n
|
|
114
|
+
args[name] = coerceScalar(value, propType)
|
|
93
115
|
continue
|
|
94
116
|
}
|
|
95
117
|
if (propType === 'array') {
|
|
118
|
+
/* Coerce each repeated element per the schema's `items.type` — a string-only
|
|
119
|
+
array arg silently fails server-side Zod validation for numeric/boolean items. */
|
|
120
|
+
const itemType = (properties[name] as { items?: { type?: string } } | undefined)?.items
|
|
121
|
+
?.type
|
|
122
|
+
const element = coerceScalar(value, itemType)
|
|
96
123
|
const existing = args[name]
|
|
97
|
-
args[name] = Array.isArray(existing) ? [...existing,
|
|
124
|
+
args[name] = Array.isArray(existing) ? [...existing, element] : [element]
|
|
98
125
|
continue
|
|
99
126
|
}
|
|
100
127
|
args[name] = value
|
|
@@ -64,9 +64,11 @@ export function* tokenizeArgvFlags(
|
|
|
64
64
|
: undefined
|
|
65
65
|
const negated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
|
|
66
66
|
const name = negated ? (negatedName as string) : literalName
|
|
67
|
-
// Boolean props and inline `--name=value` consume no following token.
|
|
67
|
+
// Boolean props and inline `--name=value` consume no following token. Pass any
|
|
68
|
+
// inline value through (`--flag=false`) so the parser can honour the RHS instead
|
|
69
|
+
// of the boolean always resolving to true.
|
|
68
70
|
if (properties[name]?.type === 'boolean') {
|
|
69
|
-
yield { name, negated }
|
|
71
|
+
yield { name, negated, value: inlineValue }
|
|
70
72
|
continue
|
|
71
73
|
}
|
|
72
74
|
if (inlineValue !== undefined) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// node:fs existsSync — cheap sync presence check, mirrors createPublicAssetServer
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve, sep } from 'node:path'
|
|
3
4
|
import { Glob } from 'bun'
|
|
4
5
|
import { mimeForExtension } from '../server/runtime/mimeForExtension.ts'
|
|
5
6
|
import type { Assets } from '../server/runtime/types/Assets.ts'
|
|
@@ -81,8 +82,10 @@ export function createMcpResourceServer({
|
|
|
81
82
|
return undefined
|
|
82
83
|
}
|
|
83
84
|
const relativePath = uri.slice(URI_PREFIX.length)
|
|
84
|
-
// reject `..` traversal
|
|
85
|
-
|
|
85
|
+
// reject `..` traversal on EITHER separator — a backslash-joined `..\` escapes
|
|
86
|
+
// resourcesDir on Windows (its OS layer treats `\` as a separator regardless of
|
|
87
|
+
// how the JS string was built), so a `/`-only split lets it through.
|
|
88
|
+
if (relativePath.split(/[\\/]/).includes('..')) {
|
|
86
89
|
return undefined
|
|
87
90
|
}
|
|
88
91
|
if (mcpResources) {
|
|
@@ -92,6 +95,14 @@ export function createMcpResourceServer({
|
|
|
92
95
|
}
|
|
93
96
|
return contentsFor(relativePath, Bun.gunzipSync(compressed))
|
|
94
97
|
}
|
|
98
|
+
// Belt-and-braces for the on-disk branch: resolve the target and confirm it
|
|
99
|
+
// stays inside resourcesDir, catching any traversal the segment check missed
|
|
100
|
+
// (absolute paths, platform separator quirks).
|
|
101
|
+
const root = resolve(resourcesDir)
|
|
102
|
+
const target = resolve(root, relativePath)
|
|
103
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
95
106
|
const file = Bun.file(`${resourcesDir}/${relativePath}`)
|
|
96
107
|
if (!(await file.exists())) {
|
|
97
108
|
return undefined
|
|
@@ -4,6 +4,12 @@ import { messageFromError } from '../shared/messageFromError.ts'
|
|
|
4
4
|
import { responseErrorText } from '../shared/responseErrorText.ts'
|
|
5
5
|
import { streamResponse } from '../shared/streamResponse.ts'
|
|
6
6
|
|
|
7
|
+
/* MCP `tools/call` is request/response, not a streaming transport, so a genuinely live
|
|
8
|
+
stream must be bounded: return what arrived within these limits (with a truncation note)
|
|
9
|
+
rather than buffering forever and never answering. */
|
|
10
|
+
const MAX_STREAM_FRAMES = 1000
|
|
11
|
+
const STREAM_TOOL_TIMEOUT_MS = 30_000
|
|
12
|
+
|
|
7
13
|
// Frames a value as MCP text content — strings verbatim, everything else as JSON.
|
|
8
14
|
// An undefined body (204 / empty) becomes '' — `JSON.stringify(undefined)` is the
|
|
9
15
|
// value `undefined`, not a string, which would make an invalid text content block.
|
|
@@ -38,26 +44,48 @@ export async function toolResultFromResponse(response: Response): Promise<Record
|
|
|
38
44
|
|
|
39
45
|
if (isStreamingResponse(response)) {
|
|
40
46
|
const frames: unknown[] = []
|
|
47
|
+
// Set when WE stop the stream (cap/timeout), so the iterator's resulting end/throw
|
|
48
|
+
// reads as truncation, not a stream error.
|
|
49
|
+
let truncated: string | undefined
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
truncated = `stream truncated after ${STREAM_TOOL_TIMEOUT_MS}ms — MCP tools/call is request/response, not a live stream`
|
|
52
|
+
void response.body?.cancel()
|
|
53
|
+
}, STREAM_TOOL_TIMEOUT_MS)
|
|
41
54
|
try {
|
|
42
55
|
for await (const frame of streamResponse(response)) {
|
|
43
56
|
frames.push(frame)
|
|
57
|
+
if (frames.length >= MAX_STREAM_FRAMES) {
|
|
58
|
+
truncated = `stream truncated at ${MAX_STREAM_FRAMES} frames`
|
|
59
|
+
void response.body?.cancel()
|
|
60
|
+
break
|
|
61
|
+
}
|
|
44
62
|
}
|
|
45
63
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
/* A real mid-stream error (we didn't cancel) surfaces as isError; a throw caused
|
|
65
|
+
by our own cancel falls through to the truncated result below. */
|
|
66
|
+
if (truncated === undefined) {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{ type: 'text', text: frames.map(asText).join('\n') },
|
|
71
|
+
{ type: 'text', text: `stream error: ${messageFromError(error)}` },
|
|
72
|
+
],
|
|
73
|
+
structuredContent: { frames },
|
|
74
|
+
isError: true,
|
|
75
|
+
}
|
|
56
76
|
}
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer)
|
|
79
|
+
}
|
|
80
|
+
const content: { type: 'text'; text: string }[] = [
|
|
81
|
+
{ type: 'text', text: frames.map(asText).join('\n') },
|
|
82
|
+
]
|
|
83
|
+
if (truncated !== undefined) {
|
|
84
|
+
content.push({ type: 'text', text: truncated })
|
|
57
85
|
}
|
|
58
86
|
return {
|
|
59
|
-
content
|
|
60
|
-
structuredContent: { frames },
|
|
87
|
+
content,
|
|
88
|
+
structuredContent: truncated === undefined ? { frames } : { frames, truncated },
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -137,7 +137,21 @@ export async function parseArgs(
|
|
|
137
137
|
needs the schema threaded in here (or a coercing schema adapter at the rpc).
|
|
138
138
|
*/
|
|
139
139
|
const bodyObject = (body ?? {}) as Record<string, unknown>
|
|
140
|
-
|
|
140
|
+
/* Collect the query into an object, arraying repeated keys (`?tag=a&tag=b` → `['a','b']`)
|
|
141
|
+
rather than letting `Object.fromEntries` silently keep only the last — mirrors
|
|
142
|
+
splitFormData, so a repeated field reaches the schema as an array, not a dropped value. */
|
|
143
|
+
const queryObject: Record<string, unknown> = {}
|
|
144
|
+
for (const [key, value] of url.searchParams) {
|
|
145
|
+
const existing = queryObject[key]
|
|
146
|
+
if (!(key in queryObject)) {
|
|
147
|
+
queryObject[key] = value
|
|
148
|
+
} else if (Array.isArray(existing)) {
|
|
149
|
+
existing.push(value)
|
|
150
|
+
} else {
|
|
151
|
+
queryObject[key] = [existing, value]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const merged = { ...queryObject, ...bodyObject }
|
|
141
155
|
if (Object.keys(merged).length === 0) {
|
|
142
156
|
return undefined
|
|
143
157
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { abideLog } from '../../shared/abideLog.ts'
|
|
1
2
|
import { error } from '../error.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -39,7 +40,17 @@ export function runWithRpcTimeout(
|
|
|
39
40
|
(late) => {
|
|
40
41
|
void late.body?.cancel()
|
|
41
42
|
},
|
|
42
|
-
() => {
|
|
43
|
+
(rejection) => {
|
|
44
|
+
/* The handler outlived its timeout and then rejected. A cooperative
|
|
45
|
+
abort (it honoured the composed signal) is expected and stays quiet;
|
|
46
|
+
any other error is a genuine handler bug and would otherwise leave
|
|
47
|
+
zero server-side trace behind the client's 504 — log it. */
|
|
48
|
+
const aborted =
|
|
49
|
+
(rejection as { name?: string } | undefined)?.name === 'AbortError'
|
|
50
|
+
if (!aborted) {
|
|
51
|
+
abideLog.error(rejection)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
43
54
|
)
|
|
44
55
|
}
|
|
45
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
2
2
|
import { gzipResponse } from './gzipResponse.ts'
|
|
3
|
+
import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
|
|
3
4
|
import type { RequestStore } from './types/RequestStore.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -24,7 +25,16 @@ export function finalizeResponse(
|
|
|
24
25
|
exemptIdleTimeout: () => void,
|
|
25
26
|
): Response {
|
|
26
27
|
const kind = responseBodyKind(response)
|
|
27
|
-
|
|
28
|
+
/* Close-monitoring (defer the closing record + cache-stats snapshot until the body
|
|
29
|
+
actually drains) must also cover progressively-streamed SSR HTML — a live
|
|
30
|
+
ReadableStream marked by STREAMED_HTML_HEADER that classifies as 'compressible' (it
|
|
31
|
+
still gzips), NOT as 'streaming'. Read the marker before gzipResponse strips it.
|
|
32
|
+
Without this, a page with a slow `{#await}` logs ~1ms elapsed and near-empty cache
|
|
33
|
+
stats at header time instead of the true drain-time values. */
|
|
34
|
+
const streamedHtml = response.headers.has(STREAMED_HTML_HEADER)
|
|
35
|
+
store.responseStreaming = kind === 'streaming' || streamedHtml
|
|
36
|
+
/* Only true frame protocols (SSE/JSONL, socket tail) opt out of the idle-timeout cap;
|
|
37
|
+
streamed HTML pages deliberately rely on it (see STREAMED_HTML_HEADER). */
|
|
28
38
|
if (kind === 'streaming') {
|
|
29
39
|
exemptIdleTimeout()
|
|
30
40
|
}
|
|
@@ -63,8 +63,16 @@ export async function snapshotEntryFromCache(
|
|
|
63
63
|
}
|
|
64
64
|
/* Read a CLONE, not the original: a reader that captured this same `entry.promise`
|
|
65
65
|
before the replacement below still holds `response` and may `.clone()` it — reading
|
|
66
|
-
the original here would lock its body and throw "Body already used" for that reader.
|
|
67
|
-
|
|
66
|
+
the original here would lock its body and throw "Body already used" for that reader.
|
|
67
|
+
The body stream can still error mid-read (reset/truncated upstream) after headers
|
|
68
|
+
resolved — degrade that to undefined (matching the fetch-rejection path) so one flaky
|
|
69
|
+
entry doesn't reject the whole snapshot batch and 500 / abort the page stream. */
|
|
70
|
+
let body: string
|
|
71
|
+
try {
|
|
72
|
+
body = await response.clone().text()
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
68
76
|
entry.promise = Promise.resolve(
|
|
69
77
|
new Response(body, {
|
|
70
78
|
status: response.status,
|