@uicast/core 0.0.1-beta.1 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -6
- package/README.md +120 -1
- package/dist/build-elements-by-key.d.ts +13 -0
- package/dist/build-elements-by-key.js +39 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +4 -0
- package/dist/create-component-definition.d.ts +26 -0
- package/dist/create-component-definition.js +74 -0
- package/dist/entry-error.d.ts +57 -0
- package/dist/entry-error.js +85 -0
- package/dist/entry-shape.d.ts +7 -0
- package/dist/entry-shape.js +88 -0
- package/dist/expr/evaluate.d.ts +5 -0
- package/dist/expr/evaluate.js +54 -0
- package/dist/expr/extract-deps.d.ts +4 -0
- package/dist/expr/extract-deps.js +30 -0
- package/dist/expr/plan-step-waves.d.ts +8 -0
- package/dist/expr/plan-step-waves.js +45 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +13 -0
- package/dist/json-schema.d.ts +58 -0
- package/dist/json-schema.js +73 -0
- package/dist/prompt/format.d.ts +5 -0
- package/dist/prompt/format.js +22 -0
- package/dist/prompt/get-common-instructions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-common-instructions-partial-prompt.js +16 -0
- package/dist/prompt/get-components-partial-prompt.d.ts +27 -0
- package/dist/prompt/get-components-partial-prompt.js +133 -0
- package/dist/prompt/get-edit-request-prompt.d.ts +23 -0
- package/dist/prompt/get-edit-request-prompt.js +15 -0
- package/dist/prompt/get-error-recovery-prompt.d.ts +35 -0
- package/dist/prompt/get-error-recovery-prompt.js +26 -0
- package/dist/prompt/get-expressions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-expressions-partial-prompt.js +17 -0
- package/dist/prompt/get-functions-partial-prompt.d.ts +22 -0
- package/dist/prompt/get-functions-partial-prompt.js +58 -0
- package/dist/prompt/get-scope-partial-prompt.d.ts +24 -0
- package/dist/prompt/get-scope-partial-prompt.js +30 -0
- package/dist/prompt/index.d.ts +7 -0
- package/dist/prompt/index.js +7 -0
- package/dist/prompt/json-schema-to-ts.d.ts +6 -0
- package/dist/prompt/json-schema-to-ts.js +146 -0
- package/dist/prompt/md/EXPRESSIONS.json +1 -0
- package/dist/prompt/md/INSTRUCTIONS.json +1 -0
- package/dist/prompt/shared-types.d.ts +5 -0
- package/dist/prompt/shared-types.js +82 -0
- package/dist/scope/create-proxy-scope.d.ts +55 -0
- package/dist/scope/create-proxy-scope.js +295 -0
- package/dist/scope/parse-scope.d.ts +2 -0
- package/dist/scope/parse-scope.js +11 -0
- package/dist/scope/parse-set-address.d.ts +16 -0
- package/dist/scope/parse-set-address.js +52 -0
- package/dist/stream-json-lines.d.ts +14 -0
- package/dist/stream-json-lines.js +59 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.js +19 -0
- package/dist/url-policy.d.ts +45 -0
- package/dist/url-policy.js +252 -0
- package/package.json +27 -18
- package/src/def/create-component-definition.ts +0 -21
- package/src/entry-error.ts +0 -104
- package/src/expr/analyze.ts +0 -190
- package/src/expr/ast-utils.ts +0 -28
- package/src/expr/evaluate.ts +0 -74
- package/src/expr/extract-deps.ts +0 -32
- package/src/expr/globals.ts +0 -118
- package/src/expr/safer-eval.ts +0 -233
- package/src/expr/validate.ts +0 -149
- package/src/index.ts +0 -28
- package/src/prompt/get-common-instructions-partial-prompt.ts +0 -20
- package/src/prompt/get-components-partial-prompt.ts +0 -153
- package/src/prompt/get-edit-request-prompt.ts +0 -37
- package/src/prompt/get-error-recovery-prompt.ts +0 -37
- package/src/prompt/get-expressions-partial-prompt.ts +0 -33
- package/src/prompt/get-functions-partial-prompt.ts +0 -62
- package/src/prompt/get-scope-partial-prompt.ts +0 -57
- package/src/prompt/index.ts +0 -32
- package/src/prompt/md/EXPRESSIONS.json +0 -1
- package/src/prompt/md/EXPRESSIONS.md +0 -29
- package/src/prompt/md/INSTRUCTIONS.json +0 -1
- package/src/prompt/md/INSTRUCTIONS.md +0 -172
- package/src/prompt-utils/json-schema-to-ts.ts +0 -225
- package/src/scope/create-proxy-scope.ts +0 -131
- package/src/scope/parse-scope.ts +0 -36
- package/src/stream/stream-json-lines.ts +0 -56
- package/src/types.ts +0 -124
- package/src/utils/utils.ts +0 -64
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# Overview
|
|
2
|
-
|
|
3
|
-
You are a UI generator that outputs JSONL (JSON Lines) where each line is a complete JSON object representing one element.
|
|
4
|
-
|
|
5
|
-
Match the ambition of the output to the request: a request for a page gets a complete, working page; a question gets a compact answer built from the fewest elements that fully answer it. Never respond with a bare fragment of what was asked, and never pad beyond it.
|
|
6
|
-
|
|
7
|
-
# Output Format
|
|
8
|
-
|
|
9
|
-
**Output ONLY raw JSONL — nothing else.** Do NOT include any reasoning, thinking, explanation, commentary, or natural language text before, between, or after the JSON lines. Every line of your output must be a valid JSON object. If you feel the need to plan or reason, do so silently — never emit non-JSON text.
|
|
10
|
-
|
|
11
|
-
Output one JSON object per line (JSONL). Each line is a self-contained element. Do NOT wrap output in a JSON array, code fences, or any other formatting — just raw JSONL lines.
|
|
12
|
-
|
|
13
|
-
Each line must be a valid JSON object with at minimum these fields:
|
|
14
|
-
|
|
15
|
-
- `"key"`: a unique string identifier for this element. Use **kebab-case** — lowercase words joined by hyphens (e.g. `"stats-row"`, `"line-chart"`, `"save-btn"`), never camelCase or snake_case.
|
|
16
|
-
- `"component"`: one of the registered component names listed below.
|
|
17
|
-
|
|
18
|
-
Optional fields: `"props"`, `"seed"`, `"hidden"`, `"callbacks"`, `"children"` (array of child element keys), and for lists: `"each"`, `"as"`, `"keyBy"`.
|
|
19
|
-
|
|
20
|
-
An element is a **list** (an iterating template) if and only if it has an `"each"` field; an element without `"each"` is a plain (non-list) element. There is no `"kind"` field — list-ness is determined structurally by the presence of `"each"`.
|
|
21
|
-
|
|
22
|
-
The **root** element is the single top-level element — the one element whose `key` is NOT listed in any other element's `children` array. There is no field marking it; the root is identified structurally as the element nothing else references. Every other element must be referenced as a child by exactly one parent.
|
|
23
|
-
|
|
24
|
-
Stream elements in order: emit the root element first, then its children depth-first. A parent element must always appear before any element it references in its `children` array.
|
|
25
|
-
|
|
26
|
-
# Rules
|
|
27
|
-
|
|
28
|
-
## 1. Structure
|
|
29
|
-
|
|
30
|
-
- The output is a flat sequence of element objects — never nested inside each other.
|
|
31
|
-
- Exactly one element must be the root: exactly one element's `key` must NOT appear in any other element's `children` array. Every other element must be referenced as a child exactly once.
|
|
32
|
-
- Parent elements reference children by `key` strings in their `children` array. Children are never inlined as objects — always referenced by key.
|
|
33
|
-
- The `children` array defines rendering order: children are rendered in the order they appear in the array.
|
|
34
|
-
- An element without `children` is a leaf node.
|
|
35
|
-
- Every `key` referenced in any `children` array must exist as an element in the output.
|
|
36
|
-
|
|
37
|
-
## 2. Props
|
|
38
|
-
|
|
39
|
-
- Props are provided via a `ValueSource` object: either `{ "literal": { ... } }` for static values or `{ "expr": "..." }` for dynamic JavaScript expressions.
|
|
40
|
-
- Use `literal` when all prop values are known at build time and never change. Use `expr` when any prop value depends on state.
|
|
41
|
-
- A `literal` ValueSource passes its value directly as the component's props object: `"props": { "literal": { "children": "Hello" } }`.
|
|
42
|
-
- A `literal` value is taken **verbatim** without any evaluation, and may be **any JSON value** — a string, number, boolean, `null`, or an arbitrarily nested object/array (e.g. `"props": { "literal": { "rows": [{ "id": 1, "tags": ["a", "b"] }] } }`). It is NOT parsed as a JavaScript expression, so `{ "literal": "scopes.x" }` renders the literal text `scopes.x`, not the value at that path — use `{ "expr": "scopes.x" }` for that.
|
|
43
|
-
- An `expr` ValueSource is a JavaScript expression that must evaluate to an object matching the component's props shape: `"props": { "expr": "({ value: scopes.root.count })" }`.
|
|
44
|
-
- When the expression IS an object literal, wrap it in parentheses to distinguish from a block statement: `"props": { "expr": "({ value: scopes.root.count, placeholder: \"Enter value\" })" }`.
|
|
45
|
-
- Props expressions must NEVER call async functions. Props are evaluated synchronously during render.
|
|
46
|
-
- **Expressions must NEVER produce side effects.** Expressions (in `props.expr`, `hidden`, `seed[].expr`, `callbacks[].expr`) are pure computations that return a value. The ONLY way to produce a side effect (writing state) is through the `"set"` field on `seed` and `callbacks` entries. The `"set"` field receives the return value of the expression and writes it to the specified scope path. Never assign to `scopes.*` or mutate any external state inside an expression itself.
|
|
47
|
-
- WRONG: `"props": { "expr": "(() => { scopes.root.sortedTasks = [...scopes.root.tasks].sort(...); return {}; })()" }` — this assigns to scope inside an expression.
|
|
48
|
-
- CORRECT: Express the derived value directly in `props.expr` — the runtime auto-subscribes to every `scopes.X.Y` read, so the prop recomputes whenever any input changes: `"props": { "expr": "({ data: [...scopes.root.tasks].sort((a, b) => a[scopes.root.sortKey] - b[scopes.root.sortKey]) })" }`. No separate state slot, no manual recompute.
|
|
49
|
-
- **Expressions must be stateless and deterministic.** An expression must compute its result purely from the `scopes` (and, in callbacks, `evt`) it reads — the same inputs MUST always produce the same output. Do NOT read non-deterministic or persistent state inside an expression: no `Math.random()`, no current-date/time reads, no counter that survives between evaluations. The runtime re-evaluates an expression every time a scope path it reads changes (and may run it more than once per change), so a non-deterministic expression yields inconsistent UI. If you need a random value or a timestamp, produce it once via a `"set"` in `seed`/`callbacks` and read the stored value from scope.
|
|
50
|
-
- Some components accept a special `children` prop (e.g., Card, Text, Badge, Button). When set via `literal` or `expr`, this renders as inline text/content. This is distinct from the `children` array on the element, which references child elements. Both can coexist: the `children` array renders child elements, and if the component also reads `props.children`, inline content is rendered too. Typically you use one or the other.
|
|
51
|
-
|
|
52
|
-
## 3. State & Seed
|
|
53
|
-
|
|
54
|
-
- State is initialized via `seed` on any element: an array of `ValueSourceAssignment` objects.
|
|
55
|
-
- Each seed entry is `{ "set": "scopes.<scope>.<path>", "literal": <value> }` or `{ "set": "scopes.<scope>.<path>", "expr": "<JavaScript expression>" }`.
|
|
56
|
-
- `literal` seeds are synchronous. `expr` seeds may call async functions and the element will suspend (show a loading placeholder) until all async seeds resolve.
|
|
57
|
-
- Seeds are evaluated exactly once when the element first mounts.
|
|
58
|
-
- **`seed` is for storing user-mutable state and one-shot data fetches — NOT for derived state.** Each `seed` entry runs once at mount and never recomputes. Use `seed` to:
|
|
59
|
-
- Initialize a value the user will later change (a form field, a selection, a search term, a pagination cursor).
|
|
60
|
-
- Fetch data from a host function on mount: `{ "set": "scopes.root.rows", "expr": "InvApi_getRows()" }`.
|
|
61
|
-
- **Do NOT use `seed` to store a value computed from other state.** A filtered list, a sorted list, a paginated slice, a sum, a formatted string — anything that should change when its inputs change — belongs inline in a reactive site (`props.expr`, `hidden`, or `each`), where it recomputes automatically. Putting derived state in `seed` produces a stale snapshot.
|
|
62
|
-
- WRONG: `"seed": [{ "set": "scopes.root.filteredRows", "expr": "scopes.root.rows.filter(r => r.name.includes(scopes.root.searchTerm))" }]` then `"each": "scopes.root.filteredRows"`. The filter runs once at mount; typing into the search input does nothing.
|
|
63
|
-
- CORRECT: Leave `filteredRows` out of state. Set `"each": "scopes.root.rows.filter(r => r.name.includes(scopes.root.searchTerm))"` directly on the list element. The runtime subscribes to both `rows` and `searchTerm`; the list updates as the user types.
|
|
64
|
-
- State paths are safe to assign deeply even if parent objects don't exist yet: `{ "set": "scopes.root.foo.bar.baz", "literal": 1 }` works because paths are backed by Proxy internally.
|
|
65
|
-
- However, expressions must NEVER read a scope path that hasn't been set yet. If `scopes.root.foo` is not initialized, no expression should reference it. Always initialize before reading.
|
|
66
|
-
- **CRITICAL: All `seed` expressions within a single element are evaluated BEFORE any values are written.** This means a later seed in the same element CANNOT read a value set by an earlier seed in the same array. If seed B depends on a value set by seed A, they must be in **different elements** — put seed A in a parent element and seed B in a child element. Child element seeds run after the parent element's seeds have fully completed.
|
|
67
|
-
- The root element should initialize all root-level state in its `seed` before any child references it.
|
|
68
|
-
|
|
69
|
-
## 4. Scopes
|
|
70
|
-
|
|
71
|
-
- All state lives under the `scopes` namespace.
|
|
72
|
-
- The root element's scope is `scopes.root`. All root-level state is accessed as `scopes.root.<key>`.
|
|
73
|
-
- List items generate their own scope named by the `as` property. If `as: "row"`, each item's scope is `scopes.row`.
|
|
74
|
-
- Inside a list item scope: `scopes.<as>.item` is the current item value (the element from the array), and `scopes.<as>.index` is the current zero-based index.
|
|
75
|
-
- Scope names must be globally unique across ALL lists in the entire output, regardless of nesting depth. For example, do not use `as: "item"` on two different lists.
|
|
76
|
-
- A parent scope can access all child item scopes via `childScopes`: `scopes.root.childScopes.row` returns the array of all `row` scopes (one per list item).
|
|
77
|
-
- For nested lists (lists within lists), chain `childScopes`: `scopes.root.childScopes.row.childScopes.nestedItem`.
|
|
78
|
-
- `childScopes` is useful for aggregation (e.g., summing a field across all rows).
|
|
79
|
-
|
|
80
|
-
## 5. Reactivity
|
|
81
|
-
|
|
82
|
-
- Reactivity is automatic. Any `scopes.X.Y` path read by an element's `props.expr`, `hidden`, or (for list elements) `each` is automatically subscribed — when any of those paths is written via a `"set"` from a `seed` entry or a `callbacks` entry, the element re-renders and its expressions are re-evaluated.
|
|
83
|
-
- You do NOT specify a `deps` array. The runtime extracts the read set from your expression text. Just write the expression as you'd naturally write JavaScript; the renderer figures out what state it depends on.
|
|
84
|
-
- Reactivity only applies to `props.expr` and `hidden` (and `each` on lists). `seed` are one-shot at mount; `callbacks` run on event. Neither subscribes.
|
|
85
|
-
- Write paths in `"set"` must match read paths exactly for the read side to wake. Subscribing is path-exact (no parent fanout): writing `"set": "scopes.root.rows.0.name"` does NOT wake a reader of `scopes.root.rows`. Replace the array wholesale (`"set": "scopes.root.rows"` with a new array literal) when you want list-level readers to re-render.
|
|
86
|
-
- **Therefore: derived state belongs inline in reactive sites, not in `seed`.** Because `props.expr`, `hidden`, and `each` auto-subscribe to every scope read, you can express filtered/sorted/paginated/aggregated values directly where they're consumed — no separate state slot, no manual recompute in callbacks. Reach for `seed`/`callbacks` writes only when a value must persist across re-renders (user input, selection, mutable form data) or comes from a host function.
|
|
87
|
-
|
|
88
|
-
## 6. Lists
|
|
89
|
-
|
|
90
|
-
- A list element is any element with an `each` field — its presence is what marks the element as a list (there is no `kind` field). A list requires `each` (the array to iterate, written as a bare expression) and `as` (string — the per-item scope name), and optionally `keyBy` (string key for stable identity).
|
|
91
|
-
- `each` is a **bare JavaScript expression string** (like `hidden`, not a `ValueSource`). Prefer the simplest form — a plain reference to a state array, e.g. `"scopes.root.rows"` or `"scopes.row.nestedItems"`. An inline array-returning expression also works, and is the right tool for a **derived** list: `"each": "scopes.root.rows.filter(r => r.active)"` (per §3/§5 a filtered/sorted view belongs inline here, not pre-computed into `seed`). The underlying state array must be initialized via `seed` (on this element or an ancestor) before the list renders; `each` yields `[]` if it would otherwise be undefined.
|
|
92
|
-
- Always provide `keyBy` when list items are objects with a unique identifier (e.g., `"keyBy": "id"`). This enables stable re-rendering on mutations (add/remove/reorder). Without `keyBy`, items are keyed by index.
|
|
93
|
-
- The list element's `component` is rendered once per item in the array — it wraps each individual item, not the whole list. For example, a list with `component: "TableRow"` renders one `<tr>` per item.
|
|
94
|
-
- The list element can have `props` that are evaluated per-item with the item scope available. E.g., `"props": { "expr": "{\"children\": scopes.row.item.name}" }`.
|
|
95
|
-
- List elements can have `children` which are also rendered per-item. Inside children, the item scope is available.
|
|
96
|
-
- A list element can never be the root — lists cannot be the top-level element. Always wrap a list in a container element (e.g., `TableBody` or `FlexCol`).
|
|
97
|
-
|
|
98
|
-
## 7. Callbacks
|
|
99
|
-
|
|
100
|
-
- Callbacks are declared as named event handlers on an element: `"callbacks": { "onClick": [...], "onChange": [...] }`.
|
|
101
|
-
- Only declare callback names that match the component's documented event handlers.
|
|
102
|
-
- Each callback is an array of `ConfirmableValueSourceAssignment` objects executed sequentially: `{ "set": "scopes.<scope>.<path>", "expr": "<JavaScript expression>" }` or `{ "set": "scopes.<scope>.<path>", "literal": <value> }`.
|
|
103
|
-
- The `evt` object is available in callback expressions and contains event-specific data. Check each component's event handler signature for available fields (e.g., `evt.value`, `evt.valueAsNumber` for Input's onChange).
|
|
104
|
-
- `currentValue` is available in any callback (or `seed`) step that has a `"set"`: it holds the current value at that step's `set` path — the value being replaced — so you can read-modify-write without re-reading the path. It is `undefined` when the path was never set (so a first-time `!currentValue` toggles to `true`).
|
|
105
|
-
- Callbacks CAN call async functions. Each step in the array is awaited before the next executes.
|
|
106
|
-
- Multiple assignments in one callback execute in order. Use this for chained updates (e.g., update a row value, then recompute a total).
|
|
107
|
-
- **Confirmation prompts**: Any callback action can include an optional `"confirm"` field with a string message. When present, a confirmation dialog is shown to the user before that action executes. If the user cancels, the current action AND all remaining actions in the callback array are skipped. Place `confirm` on the FIRST action in the callback array (typically the dangerous one, such as a delete call) so the user is prompted before anything happens. Do NOT create separate `ConfirmDialog` elements for confirmations — use the `confirm` field on callback actions instead.
|
|
108
|
-
- Example: `{ "set": "scopes.root._result", "expr": "UserApi_deleteUser({ params: { id: scopes.row.item.id } })", "confirm": "Are you sure you want to delete this user? This action cannot be undone." }`
|
|
109
|
-
- Common patterns:
|
|
110
|
-
- Toggle a flag: `{ "set": "scopes.root.open", "expr": "!currentValue" }`
|
|
111
|
-
- Increment a counter: `{ "set": "scopes.root.count", "expr": "currentValue + 1" }`
|
|
112
|
-
- Append to array: `{ "set": "scopes.root.rows", "expr": "[...currentValue, { id: scopes.root.nextId, a: 0 }]" }`
|
|
113
|
-
- Filter array: `{ "set": "scopes.root.rows", "expr": "currentValue.filter(r => r.id !== scopes.row.item.id)" }`
|
|
114
|
-
- Aggregate: `{ "set": "scopes.root.total", "expr": "scopes.root.childScopes.row.reduce((acc, r) => acc + r.item.value, 0)" }`
|
|
115
|
-
- Call a host function: `{ "set": "scopes.root.result", "expr": "UserApi_deleteUser({ params: { id: scopes.row.item.id } })" }`
|
|
116
|
-
- Dangerous delete with confirmation: `{ "set": "scopes.root.result", "expr": "UserApi_deleteUser({ params: { id: scopes.row.item.id } })", "confirm": "Are you sure you want to delete this user?" }`
|
|
117
|
-
|
|
118
|
-
## 8. Hidden (Conditional Visibility)
|
|
119
|
-
|
|
120
|
-
- Any element can have a `hidden` property: a **bare JavaScript expression string** — NOT a `ValueSource` (no `{ "expr": ... }` / `{ "literal": ... }` wrapper, and no `literal` form). A constant `hidden` is meaningless: constant-true would just mean "always hide" (omit the element instead), and constant-false is the same as having no `hidden` at all. This mirrors `each`, which is also a bare expression string.
|
|
121
|
-
- `{ "hidden": "scopes.root.activeTab !== 'settings'" }` hides the element when the expression evaluates to truthy.
|
|
122
|
-
- Hidden elements are not rendered but retain their state. When unhidden, they reappear with their state intact.
|
|
123
|
-
- `hidden` expressions must NOT call async functions. They are evaluated synchronously.
|
|
124
|
-
|
|
125
|
-
## 9. Async functions
|
|
126
|
-
|
|
127
|
-
- Host functions (e.g., `UserApi_getUsers()`) are available in `seed` expressions and `callbacks` expressions ONLY.
|
|
128
|
-
- Async calls are NEVER allowed in `props.expr` or `hidden` — these are evaluated synchronously during render.
|
|
129
|
-
- Host functions are called with a single object argument matching their documented input type. Functions with no input take no arguments.
|
|
130
|
-
- Correct: `UserApi_getUsers()` (no input)
|
|
131
|
-
- Correct: `UserApi_deleteUser({ params: { id: scopes.row.item.id } })` (with input)
|
|
132
|
-
- Correct: `UserApi_createUser({ body: { fullName: "Alice", email: "a@b.com" } })` (with body)
|
|
133
|
-
- Correct: `TaskApi_findTasks({ query: { search: scopes.root.searchTerm } })` (with query)
|
|
134
|
-
- WRONG: `UserApi_deleteUser(scopes.row.item.id)` — must wrap in the expected shape.
|
|
135
|
-
- Use `seed` with `expr` to fetch initial data on mount: `{ "set": "scopes.root.users", "expr": "UserApi_getUsers()" }`. The element will suspend until the data loads.
|
|
136
|
-
- Use `callbacks` to trigger mutations in response to user actions: `{ "set": "scopes.root.result", "expr": "UserApi_deleteUser({ params: { id: scopes.row.item.id } })" }`.
|
|
137
|
-
- After a mutation, you typically need to re-fetch or update the local state. Chain multiple assignments in the callback to achieve this: first mutate, then refresh.
|
|
138
|
-
|
|
139
|
-
## 10. Ordering
|
|
140
|
-
|
|
141
|
-
- Elements must be emitted so that a parent always appears before its children in the JSONL output.
|
|
142
|
-
- State that is read by expressions must be initialized (via `seed`) in an element that appears before or is the same element that reads it.
|
|
143
|
-
- The root element should initialize all root-level state in its `seed`.
|
|
144
|
-
- If multiple seeds are needed and some depend on others, place them in the correct element order — a later element's seeds can read state set by an earlier element's seeds.
|
|
145
|
-
|
|
146
|
-
## 11. Partial Replacement (Correcting Mistakes)
|
|
147
|
-
|
|
148
|
-
- If you realize a previously emitted element or subtree has a bug, you do NOT need to re-emit the entire tree from the root.
|
|
149
|
-
- Instead, re-emit an element using the **same `key`** as the element you want to fix. When a duplicate `key` appears, the old element and its old subtree are automatically replaced by the new one. This works no matter when the duplicate appears — later in the same output, or in a later turn appended to the same document (e.g. when the user requests changes).
|
|
150
|
-
- This also recovers from invalid expressions. If you notice an expression you already emitted breaks the expression rules (e.g. references `document`, `window`, or another unavailable global; calls a function that doesn't exist; or is syntactically malformed), re-emit that element with the corrected expression. The broken element shows an inline error until the corrected line arrives, then renders normally — the rest of the UI is unaffected.
|
|
151
|
-
- Correct a mistake with at most one re-emission, and only when you are sure the replacement fixes the problem. Do not re-emit the same key repeatedly as trial-and-error: repeated replacements make the stream longer and the UI flicker. Getting it right the first time is always better — treat correction-by-re-emission as a last resort. (This does not limit deliberate edits: a later change request may re-emit the same key again.)
|
|
152
|
-
- After the re-emitted element, emit its new children (and their descendants) as usual. To correct only the element itself (props, expressions), keep the `children` array identical — the existing children are reused without re-emitting them.
|
|
153
|
-
- The new element's `children` array defines the new subtree structure. Old children that ARE referenced by the new `children` array are kept as-is, with their own subtrees — reference them without re-emitting them. Any old children not referenced are discarded.
|
|
154
|
-
- State initialized by ancestor elements (above the replaced subtree) is preserved. Only the replaced subtree re-renders.
|
|
155
|
-
- `seed` on the re-emitted element does NOT re-run if it already ran successfully (state is preserved). If you need to re-initialize state, update it via a sibling element's `seed` or restructure accordingly. Exception: a `seed` that FAILED (bad expression, rejected function call) never ran, so the corrected element's `seed` runs normally.
|
|
156
|
-
- You can re-emit any element in the tree — not just leaf nodes. Re-emitting a parent lets you restructure its subtree: keep children by reference, add new ones, or drop old ones.
|
|
157
|
-
- Example — appending a new child to an existing parent (existing children kept by reference):
|
|
158
|
-
```
|
|
159
|
-
{"key":"root","component":"Container","children":["heading","text","quote"]}
|
|
160
|
-
{"key":"quote","component":"Text","props":{"literal":{"content":"..."}}}
|
|
161
|
-
```
|
|
162
|
-
- Example — fixing a chart element that had wrong props:
|
|
163
|
-
```
|
|
164
|
-
... (earlier elements already emitted) ...
|
|
165
|
-
{"key":"line-chart","component":"LineChart","props":{"expr":"({ data: scopes.root.chartData, xKey: 'date', yKeys: ['count'], height: 300 })"}}
|
|
166
|
-
```
|
|
167
|
-
- Example — replacing a parent with its entire subtree:
|
|
168
|
-
```
|
|
169
|
-
{"key":"table-card","component":"Card","props":{"literal":{"title":"Fixed Table"}},"children":["new-table"]}
|
|
170
|
-
{"key":"new-table","component":"Table","children":["new-thead","new-tbody"]}
|
|
171
|
-
... (emit new-thead, new-tbody, etc.) ...
|
|
172
|
-
```
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
type JSONSchemaType =
|
|
2
|
-
| "object"
|
|
3
|
-
| "array"
|
|
4
|
-
| "string"
|
|
5
|
-
| "number"
|
|
6
|
-
| "boolean"
|
|
7
|
-
| "null"
|
|
8
|
-
| "integer";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* JSON Schema shape this module reads — a recursive structural type covering
|
|
12
|
-
* the draft-07 / 2020-12 keywords a Standard-Schema `toJSONSchema()` can emit
|
|
13
|
-
* (primitives, composition, objects, arrays/tuples, `$ref` + `$defs`, and the
|
|
14
|
-
* refinement keywords that constrain a value without changing its TypeScript
|
|
15
|
-
* type). Every sub-schema slot is another `JSONSchema`.
|
|
16
|
-
*
|
|
17
|
-
* `JSONSchemaToTs` still takes `unknown` and narrows at runtime, because a
|
|
18
|
-
* schema value can also be a boolean (`true`/`false`) — and, in legacy
|
|
19
|
-
* draft-07, an `items` array — which this object type intentionally doesn't
|
|
20
|
-
* model. The function reads back through itself, so the recursion stays loose.
|
|
21
|
-
*/
|
|
22
|
-
export interface JSONSchema {
|
|
23
|
-
$schema?:
|
|
24
|
-
| "https://json-schema.org/draft/2020-12/schema"
|
|
25
|
-
| "http://json-schema.org/draft-07/schema#";
|
|
26
|
-
type?: JSONSchemaType | JSONSchemaType[];
|
|
27
|
-
format?: string;
|
|
28
|
-
pattern?: string;
|
|
29
|
-
$ref?: string;
|
|
30
|
-
items?: boolean | JSONSchema;
|
|
31
|
-
prefixItems?: JSONSchema[];
|
|
32
|
-
additionalItems?: boolean | JSONSchema;
|
|
33
|
-
enum?: unknown[];
|
|
34
|
-
minimum?: number;
|
|
35
|
-
maximum?: number;
|
|
36
|
-
exclusiveMinimum?: number;
|
|
37
|
-
exclusiveMaximum?: number;
|
|
38
|
-
minItems?: number;
|
|
39
|
-
maxItems?: number;
|
|
40
|
-
title?: string;
|
|
41
|
-
description?: string;
|
|
42
|
-
properties?: { [key: string]: JSONSchema };
|
|
43
|
-
required?: string[];
|
|
44
|
-
examples?: unknown[];
|
|
45
|
-
not?: JSONSchema;
|
|
46
|
-
// support both $defs and definitions
|
|
47
|
-
$defs?: { [key: string]: JSONSchema };
|
|
48
|
-
definitions?: { [key: string]: JSONSchema };
|
|
49
|
-
additionalProperties?: boolean | JSONSchema;
|
|
50
|
-
anyOf?: JSONSchema[];
|
|
51
|
-
oneOf?: JSONSchema[];
|
|
52
|
-
allOf?: JSONSchema[];
|
|
53
|
-
// older schema
|
|
54
|
-
const?: unknown;
|
|
55
|
-
example?: unknown;
|
|
56
|
-
// binary
|
|
57
|
-
contentEncoding?: string;
|
|
58
|
-
contentMediaType?: string;
|
|
59
|
-
minLength?: number;
|
|
60
|
-
maxLength?: number;
|
|
61
|
-
// explicit TypeScript-type override for code generation
|
|
62
|
-
"x-tsType"?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Convert a JSON Schema to a compact TypeScript type string for the prompt.
|
|
67
|
-
*
|
|
68
|
-
* Pass the whole schema document (with any `$defs` / `definitions`): local
|
|
69
|
-
* `$ref`s resolve against it, and a recursive schema terminates — the cycle's
|
|
70
|
-
* back-edge renders as `unknown` while everything above it stays fully typed.
|
|
71
|
-
*/
|
|
72
|
-
export function JSONSchemaToTs(jsonSchema: unknown): string {
|
|
73
|
-
return toTs(jsonSchema, jsonSchema, new Set());
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Recursive worker. `root` is the document the first call was handed (the
|
|
78
|
-
* resolution base for `$ref`); `seen` is the set of refs currently being
|
|
79
|
-
* expanded on this path, so a cycle short-circuits to `unknown` instead of
|
|
80
|
-
* recursing forever.
|
|
81
|
-
*/
|
|
82
|
-
function toTs(jsonSchema: unknown, root: unknown, seen: Set<string>): string {
|
|
83
|
-
if (jsonSchema === true) return "unknown";
|
|
84
|
-
if (jsonSchema === false) return "never";
|
|
85
|
-
if (jsonSchema === null || jsonSchema === undefined) return "unknown";
|
|
86
|
-
if (typeof jsonSchema !== "object") return "unknown";
|
|
87
|
-
const schema = jsonSchema as JSONSchema;
|
|
88
|
-
|
|
89
|
-
if (typeof schema.$ref === "string") {
|
|
90
|
-
if (seen.has(schema.$ref)) return "unknown"; // cycle back-edge
|
|
91
|
-
const target = resolveRef(schema.$ref, root);
|
|
92
|
-
if (target === undefined) return "unknown";
|
|
93
|
-
seen.add(schema.$ref);
|
|
94
|
-
const resolved = toTs(target, root, seen);
|
|
95
|
-
seen.delete(schema.$ref);
|
|
96
|
-
return resolved;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if ("const" in schema) return JSON.stringify(schema.const);
|
|
100
|
-
|
|
101
|
-
if (schema.enum) {
|
|
102
|
-
return schema.enum.map((v) => JSON.stringify(v)).join(" | ") || "never";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (schema.allOf) {
|
|
106
|
-
const parts = schema.allOf.map((s) => toTs(s, root, seen));
|
|
107
|
-
return parts.length ? `(${parts.join(" & ")})` : "unknown";
|
|
108
|
-
}
|
|
109
|
-
if (schema.anyOf) {
|
|
110
|
-
const parts = schema.anyOf.map((s) => toTs(s, root, seen));
|
|
111
|
-
return parts.length ? `(${parts.join(" | ")})` : "never";
|
|
112
|
-
}
|
|
113
|
-
if (schema.oneOf) {
|
|
114
|
-
const parts = schema.oneOf.map((s) => toTs(s, root, seen));
|
|
115
|
-
return parts.length ? `(${parts.join(" | ")})` : "never";
|
|
116
|
-
}
|
|
117
|
-
if (schema.not) return "unknown";
|
|
118
|
-
|
|
119
|
-
if (Array.isArray(schema.type)) {
|
|
120
|
-
const types = schema.type.map((t) =>
|
|
121
|
-
toTs({ ...schema, type: t }, root, seen),
|
|
122
|
-
);
|
|
123
|
-
return types.length ? `(${types.join(" | ")})` : "unknown";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const type = schema.type;
|
|
127
|
-
|
|
128
|
-
if (type === "string") return "string";
|
|
129
|
-
if (type === "number" || type === "integer") return "number";
|
|
130
|
-
if (type === "boolean") return "boolean";
|
|
131
|
-
if (type === "null") return "null";
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
type === "object" ||
|
|
135
|
-
schema.properties ||
|
|
136
|
-
schema.additionalProperties !== undefined
|
|
137
|
-
) {
|
|
138
|
-
const props = schema.properties || {};
|
|
139
|
-
const required = schema.required || [];
|
|
140
|
-
|
|
141
|
-
const propStrings = Object.entries(props).map(([key, value]) => {
|
|
142
|
-
const isRequired = required.includes(key);
|
|
143
|
-
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
|
|
144
|
-
? key
|
|
145
|
-
: JSON.stringify(key);
|
|
146
|
-
return `${safeName}${isRequired ? "" : "?"}: ${toTs(value, root, seen)}`;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
let additionalType: string | null = null;
|
|
150
|
-
if (schema.additionalProperties === true) {
|
|
151
|
-
additionalType = "unknown";
|
|
152
|
-
} else if (
|
|
153
|
-
schema.additionalProperties &&
|
|
154
|
-
typeof schema.additionalProperties === "object"
|
|
155
|
-
) {
|
|
156
|
-
additionalType = toTs(schema.additionalProperties, root, seen);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (propStrings.length === 0 && additionalType) {
|
|
160
|
-
return `{ [key: string]: ${additionalType} }`;
|
|
161
|
-
}
|
|
162
|
-
if (propStrings.length === 0 && !additionalType) {
|
|
163
|
-
return schema.additionalProperties === false
|
|
164
|
-
? "{}"
|
|
165
|
-
: "{ [key: string]: unknown }";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let result = `{ ${propStrings.join("; ")} }`;
|
|
169
|
-
if (additionalType) {
|
|
170
|
-
result = `(${result} & { [key: string]: ${additionalType} })`;
|
|
171
|
-
}
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (type === "array" || schema.items || schema.prefixItems) {
|
|
176
|
-
if (schema.prefixItems) {
|
|
177
|
-
const tupleTypes = schema.prefixItems.map((s) => toTs(s, root, seen));
|
|
178
|
-
if (schema.items === false) return `[${tupleTypes.join(", ")}]`;
|
|
179
|
-
const restType = schema.items ? toTs(schema.items, root, seen) : "unknown";
|
|
180
|
-
return `[${tupleTypes.join(", ")}, ...${restType}[]]`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (schema.items !== undefined && schema.items !== null) {
|
|
184
|
-
if (Array.isArray(schema.items)) {
|
|
185
|
-
const tupleTypes = schema.items.map((s) => toTs(s, root, seen));
|
|
186
|
-
if (schema.additionalItems === false) {
|
|
187
|
-
return `[${tupleTypes.join(", ")}]`;
|
|
188
|
-
}
|
|
189
|
-
const restType = schema.additionalItems
|
|
190
|
-
? toTs(schema.additionalItems, root, seen)
|
|
191
|
-
: "unknown";
|
|
192
|
-
return `[${tupleTypes.join(", ")}, ...${restType}[]]`;
|
|
193
|
-
}
|
|
194
|
-
return `${toTs(schema.items, root, seen)}[]`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return "unknown[]";
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (schema.properties) {
|
|
201
|
-
return toTs({ ...schema, type: "object" }, root, seen);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return "unknown";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Resolve a local JSON Pointer ref (`#/$defs/Foo`, `#/definitions/Bar`)
|
|
209
|
-
* against the document root. Returns the target sub-schema, or `undefined`
|
|
210
|
-
* for a non-local ref (`https://…`, bare `#`) or a path that doesn't exist —
|
|
211
|
-
* the caller renders those as `unknown`.
|
|
212
|
-
*/
|
|
213
|
-
function resolveRef(ref: string, root: unknown): unknown {
|
|
214
|
-
if (!ref.startsWith("#/")) return undefined;
|
|
215
|
-
const segments = ref
|
|
216
|
-
.slice(2)
|
|
217
|
-
.split("/")
|
|
218
|
-
.map((seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
219
|
-
let node: unknown = root;
|
|
220
|
-
for (const seg of segments) {
|
|
221
|
-
if (node === null || typeof node !== "object") return undefined;
|
|
222
|
-
node = (node as Record<string, unknown>)[seg];
|
|
223
|
-
}
|
|
224
|
-
return node;
|
|
225
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
type EventHandler<T = unknown> = (payload: T) => void;
|
|
2
|
-
|
|
3
|
-
interface ChangePayload<T = unknown> {
|
|
4
|
-
path: string;
|
|
5
|
-
value: T;
|
|
6
|
-
oldValue: T;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface Emitter {
|
|
10
|
-
on<T = unknown>(type: string, handler: EventHandler<T>): () => void;
|
|
11
|
-
off<T = unknown>(type: string, handler: EventHandler<T>): void;
|
|
12
|
-
emit<T = unknown>(type: string, payload: T): void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function createEmitter(): Emitter {
|
|
16
|
-
const events = new Map<string, Set<EventHandler<any>>>();
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
on(type, handler) {
|
|
20
|
-
const handlers = events.get(type);
|
|
21
|
-
if (handlers) handlers.add(handler);
|
|
22
|
-
else events.set(type, new Set([handler]));
|
|
23
|
-
return () => events.get(type)?.delete(handler);
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
off(type, handler) {
|
|
27
|
-
events.get(type)?.delete(handler);
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
emit(type, payload) {
|
|
31
|
-
events.get(type)?.forEach((fn) => {
|
|
32
|
-
fn(payload);
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type ReactiveProxy<T extends object = object> = T & {
|
|
39
|
-
$emitter: Emitter;
|
|
40
|
-
$set: (path: string, value: unknown, options?: { default?: boolean }) => void;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function createProxyScope<T extends object>(
|
|
44
|
-
target: T = {} as T,
|
|
45
|
-
): ReactiveProxy<T> {
|
|
46
|
-
const emitter = createEmitter();
|
|
47
|
-
const proxyCache = new WeakMap<object, object>();
|
|
48
|
-
|
|
49
|
-
function set(
|
|
50
|
-
path: string,
|
|
51
|
-
value: unknown,
|
|
52
|
-
options?: { default?: boolean },
|
|
53
|
-
): void {
|
|
54
|
-
const keys = path.split(".");
|
|
55
|
-
let current: any = proxy;
|
|
56
|
-
|
|
57
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
58
|
-
const key = keys[i];
|
|
59
|
-
if (current[key] === undefined || current[key] === null) {
|
|
60
|
-
current[key] = {};
|
|
61
|
-
}
|
|
62
|
-
current = current[key];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const lastKey = keys[keys.length - 1];
|
|
66
|
-
// `default: true` → init-if-absent (first-writer-wins); never clobber an
|
|
67
|
-
// existing value. Used by `seed` so two components seeding the same
|
|
68
|
-
// path don't overwrite each other, and user edits survive remounts.
|
|
69
|
-
if (options?.default && current[lastKey] !== undefined) return;
|
|
70
|
-
current[lastKey] = value;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function wrap<U>(obj: U, path: PropertyKey[] = []): U {
|
|
74
|
-
if (obj === null || typeof obj !== "object") return obj;
|
|
75
|
-
if (proxyCache.has(obj as object))
|
|
76
|
-
return proxyCache.get(obj as object) as U;
|
|
77
|
-
|
|
78
|
-
const proxy = new Proxy(obj as object, {
|
|
79
|
-
get(target, prop, receiver) {
|
|
80
|
-
if (path.length === 0) {
|
|
81
|
-
if (prop === "$emitter") return emitter;
|
|
82
|
-
if (prop === "$set") return set;
|
|
83
|
-
}
|
|
84
|
-
const value = Reflect.get(target, prop, receiver);
|
|
85
|
-
|
|
86
|
-
// Proxy invariant: for non-configurable, non-writable properties
|
|
87
|
-
// the trap MUST return the exact target value.
|
|
88
|
-
if (value !== null && typeof value === "object") {
|
|
89
|
-
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
90
|
-
if (desc && !desc.configurable && !desc.writable) {
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return wrap(value, path.concat(prop));
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
set(target, prop, value, receiver) {
|
|
99
|
-
const oldValue = (target as Record<PropertyKey, unknown>)[prop];
|
|
100
|
-
const result = Reflect.set(target, prop, value, receiver);
|
|
101
|
-
|
|
102
|
-
if (oldValue !== value) {
|
|
103
|
-
const fullPath = path.concat(prop).join(".");
|
|
104
|
-
emitter.emit<ChangePayload>(fullPath, {
|
|
105
|
-
path: fullPath,
|
|
106
|
-
value,
|
|
107
|
-
oldValue,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
},
|
|
113
|
-
}) as U;
|
|
114
|
-
|
|
115
|
-
proxyCache.set(obj as object, proxy as object);
|
|
116
|
-
return proxy;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const proxy = wrap(target) as ReactiveProxy<T>;
|
|
120
|
-
|
|
121
|
-
return proxy;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export {
|
|
125
|
-
createProxyScope,
|
|
126
|
-
createEmitter,
|
|
127
|
-
type Emitter,
|
|
128
|
-
type ChangePayload,
|
|
129
|
-
type EventHandler,
|
|
130
|
-
type ReactiveProxy,
|
|
131
|
-
};
|
package/src/scope/parse-scope.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Split a scope key into `[scopeName, leafPath]`.
|
|
3
|
-
*
|
|
4
|
-
* Strips the leading namespace token (default `"scopes"`) if present, then
|
|
5
|
-
* splits on the first dot: the head is the named scope, the tail is the path
|
|
6
|
-
* *within* that scope.
|
|
7
|
-
*
|
|
8
|
-
* parseScope("scopes.root.user.name") → ["root", "user.name"]
|
|
9
|
-
* parseScope("root.count") → ["root", "count"] (prefix optional)
|
|
10
|
-
*
|
|
11
|
-
* `prefix` (default `"scopes"`) is the leading namespace identifier the dotted
|
|
12
|
-
* key is written against; it builds the `scopesPrefix` (`prefix + "."`) that
|
|
13
|
-
* gets stripped. Pass a different value when keys are written against another
|
|
14
|
-
* root identifier.
|
|
15
|
-
*
|
|
16
|
-
* This is the scope-layer counterpart to `createProxyScope`: `extractDeps`
|
|
17
|
-
* produces `scopes.X.Y` read strings, and the renderer hands each to
|
|
18
|
-
* `parseScope(...)` to route an `$emitter.on(targetPath, …)` subscription.
|
|
19
|
-
*/
|
|
20
|
-
export const parseScope = (key: string, prefix = "scopes") => {
|
|
21
|
-
// Strip the "<prefix>." namespace token if present.
|
|
22
|
-
const scopesPrefix = `${prefix}.`;
|
|
23
|
-
const normalizedKey = key.startsWith(scopesPrefix)
|
|
24
|
-
? key.slice(scopesPrefix.length)
|
|
25
|
-
: key;
|
|
26
|
-
|
|
27
|
-
const dotIndex = normalizedKey.indexOf(".");
|
|
28
|
-
if (dotIndex === -1) {
|
|
29
|
-
throw new Error(`Invalid scope key: ${key}`);
|
|
30
|
-
} else {
|
|
31
|
-
return [
|
|
32
|
-
normalizedKey.slice(0, dotIndex),
|
|
33
|
-
normalizedKey.slice(dotIndex + 1),
|
|
34
|
-
] as [string, string];
|
|
35
|
-
}
|
|
36
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Yields every parseable JSON value from a byte or string stream, skipping
|
|
2
|
-
// blank/non-JSON lines (code fences, model prose, partial lines). Narrow the
|
|
3
|
-
// element type via <T>; shape-checking is the caller's concern.
|
|
4
|
-
export async function* streamJsonLines<T = unknown>(
|
|
5
|
-
source: ReadableStream<Uint8Array | string> | AsyncIterable<string | Uint8Array>,
|
|
6
|
-
): AsyncGenerator<T> {
|
|
7
|
-
const decoder = new TextDecoder();
|
|
8
|
-
let buffer = "";
|
|
9
|
-
|
|
10
|
-
for await (const chunk of toAsyncIterable(source)) {
|
|
11
|
-
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
12
|
-
|
|
13
|
-
let nl = buffer.indexOf("\n");
|
|
14
|
-
while (nl !== -1) {
|
|
15
|
-
yield* parseLine<T>(buffer.slice(0, nl));
|
|
16
|
-
buffer = buffer.slice(nl + 1);
|
|
17
|
-
nl = buffer.indexOf("\n");
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
yield* parseLine<T>(buffer + decoder.decode());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function* parseLine<T>(raw: string): Generator<T> {
|
|
25
|
-
const line = raw.trim();
|
|
26
|
-
if (!line) return;
|
|
27
|
-
try {
|
|
28
|
-
yield JSON.parse(line) as T;
|
|
29
|
-
} catch {
|
|
30
|
-
// non-JSON noise (fences, prose, partial) — drop it
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function toAsyncIterable(
|
|
35
|
-
source: ReadableStream<Uint8Array | string> | AsyncIterable<string | Uint8Array>,
|
|
36
|
-
): AsyncIterable<string | Uint8Array> {
|
|
37
|
-
if (Symbol.asyncIterator in source) {
|
|
38
|
-
return source as AsyncIterable<string | Uint8Array>;
|
|
39
|
-
}
|
|
40
|
-
// Browser ReadableStream isn't async-iterable — pull it through a reader.
|
|
41
|
-
const stream = source as ReadableStream<Uint8Array | string>;
|
|
42
|
-
return {
|
|
43
|
-
async *[Symbol.asyncIterator]() {
|
|
44
|
-
const reader = stream.getReader();
|
|
45
|
-
try {
|
|
46
|
-
for (;;) {
|
|
47
|
-
const { done, value } = await reader.read();
|
|
48
|
-
if (done) return;
|
|
49
|
-
if (value !== undefined) yield value;
|
|
50
|
-
}
|
|
51
|
-
} finally {
|
|
52
|
-
reader.releaseLock();
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|