Skip to content

Commit 8e10161

Browse files
committed
Merge branch 'main' into whole-beds-bet
2 parents 45630c7 + 3c01a91 commit 8e10161

48 files changed

Lines changed: 1380 additions & 444 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
3636

3737
Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/` and `packages/json-render-ui/src/.generated/` - are **generated, not committed** (`.generated` is gitignored). Each owning package builds its own with `pnpm run build:css`, and three things guarantee the file is on disk before anything imports it: the root `postinstall` runs `turbo run build:css`, the Turbo `typecheck` task depends on both `build:css` tasks, and each package's `build` script chains `build:css` first. A new generated-under-`src` artifact follows the same shape - its own build script, declared `outputs` in `turbo.json`, and a `typecheck` dependency - rather than being checked in, since a minified single-line blob conflicts on every concurrent edit.
3838

39+
**`starter/`** is the top-level, self-contained template for creating a new devframe integration (Vanilla TS, Vite client, playgrounds, tests). It uses real versions in its `package.json` (no catalogs, no `workspace:*`) so it's copy-paste ready for users. Pnpm links its `devframe`/`@devframes/*` dependencies to the local workspace copies during development. When `bumpp -r` bumps the repo versions, `bumpp.config.ts` runs `scripts/sync-starter-version.ts` to update the starter's dependencies to match.
40+
3941
`pnpm knip` finds unused files, dependencies, and exports across every workspace (config in `knip.jsonc`). It runs against source directly - no prior build needed. Most workspaces need no configuration; `knip.jsonc` only carries per-workspace overrides for cases knip's defaults can't infer on their own: a package's non-`index.ts` `exports` subpaths (knip's package.json→`dist`→`src` source mapping needs a workspace `tsconfig.json` `outDir`, which conflicts with this repo's cross-workspace `src/*.ts` imports, so multi-entry packages list their `exports`-mapped entry files explicitly instead - keep that list in sync with each `tsdown.config.ts`), config files knip's plugins don't discover in a nested location (a Next.js app rooted below the workspace root, `storybook-solidjs-vite` not matching the Storybook plugin trigger), and dependencies referenced dynamically outside its static import graph (icon collections consumed by UnoCSS at build time, plugin packages loaded via a runtime `import()` string). Prefer fixing the underlying gap or a scoped `ignoreDependencies`/`entry` override over a blanket `ignore`.
4042

4143
## Conventions

‎bump.config.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from 'bumpp'
2+
import { syncStarterVersion } from './scripts/sync-starter-version.ts'
3+
4+
export default defineConfig({
5+
// `starter/` pins real `devframe`/`@devframes/*` versions (it's a
6+
// copy-paste-ready template, not a workspace member consuming
7+
// `catalog:`/`workspace:*`), so `bumpp -r` can't reach it on its own -
8+
// sync it here, before the version-bump commit is made.
9+
execute: operation => syncStarterVersion(operation.state.newVersion),
10+
})

‎docs/content/6.errors/DF0073.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: 'DF0073: JSON-Render Spec Does Not Match Its Schema'
3+
description: 'JSON-render view "{id}" does not match its configured schema: {issues}'
4+
---
5+
6+
## Message
7+
8+
> JSON-render view "`{id}`" does not match its configured schema: `{issues}`
9+
10+
## Cause
11+
12+
The spec failed the optional Standard Schema supplied when the JSON-render view was created. The same schema guards the initial spec and every update.
13+
14+
## Fix
15+
16+
Match the authored spec to the configured schema before creating or updating the view.
17+
18+
## Source
19+
20+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `validateSpec()` throws this before shared state changes.

‎docs/content/6.errors/DF0074.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: 'DF0074: JSON-Render Schema Is Asynchronous'
3+
description: 'JSON-render view "{id}" uses an asynchronous Standard Schema.'
4+
---
5+
6+
## Message
7+
8+
> JSON-render view "`{id}`" uses an asynchronous Standard Schema.
9+
10+
## Cause
11+
12+
JSON-render view creation and updates are synchronous, while the configured Standard Schema returned a promise.
13+
14+
## Fix
15+
16+
Use a synchronous Standard Schema for JSON-render specs.
17+
18+
## Source
19+
20+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `validateSpec()` rejects promise-returning validators.

‎docs/content/6.errors/index.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ Emitted by `devframe` — framework-neutral host / shared-state / auth surface.
4141
| [DF0031](/errors/DF0031) | error | Write to Closed Stream |
4242
| [DF0032](/errors/DF0032) | error | Streaming Channel Already Registered |
4343
| [DF0033](/errors/DF0033) | warn | Dev RPC Bridge Failed to Start |
44+
| [DF0073](/errors/DF0073) | error | JSON-Render Spec Does Not Match Its Schema |
45+
| [DF0074](/errors/DF0074) | error | JSON-Render Schema Is Asynchronous |

‎packages/json-render/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@json-render/core": "catalog:deps",
49+
"@standard-schema/spec": "catalog:deps",
4950
"zod": "catalog:deps"
5051
},
5152
"devDependencies": {

‎packages/json-render/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export {
3535
} from './prop-schemas'
3636

3737
// ── Devframes-facing type names ──────────────────────────────────────────
38-
export type { DevframeJsonRenderSpec, JsonRenderView } from './types'
38+
export type { CatalogUIElement, DevframeJsonRenderSpec, JsonRenderView } from './types'
3939
// ── View index (frontend view discovery) ─────────────────────────────────
4040
export { JSON_RENDER_INDEX_KEY } from './view-index'
4141

‎packages/json-render/src/node/create-view.ts‎

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { StandardSchemaV1 } from '@standard-schema/spec'
12
import type { DevframeNodeContext, DevframeScopedNodeContext } from 'devframe'
23
import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state'
34
import type { DevframeJsonRenderSpec, JsonRenderStatePatch, JsonRenderView } from '../types'
@@ -8,15 +9,17 @@ import { JSON_RENDER_INDEX_KEY } from '../view-index'
89
import { diagnostics } from './diagnostics'
910

1011
/** Options for {@link createJsonRenderView}. */
11-
export interface CreateJsonRenderViewOptions {
12+
export interface CreateJsonRenderViewOptions<SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec> {
1213
/**
1314
* Stable, author-supplied id, unique within the view's scope. Forms the
1415
* shared-state key `devframe:json-render:<scope>:<id>` and never changes
1516
* across updates, so a client keeps its subscription across reconnects.
1617
*/
1718
id: string
1819
/** The initial spec. */
19-
spec: DevframeJsonRenderSpec
20+
spec: SpecType
21+
/** A replacement Standard Schema validator, or `false` to disable validation. */
22+
schema?: StandardSchemaV1 | false
2023
/**
2124
* Override the scope segment of the view's stable id. Defaults to the
2225
* context's namespace when created from a scoped context, otherwise
@@ -98,9 +101,43 @@ function validateElementProps(id: string, spec: DevframeJsonRenderSpec): void {
98101
}
99102
}
100103

104+
function formatStandardSchemaIssues(issues: readonly StandardSchemaV1.Issue[]): string {
105+
return issues
106+
.map((issue) => {
107+
const path = issue.path
108+
?.map(segment => typeof segment === 'object' ? String(segment.key) : String(segment))
109+
.join('.')
110+
return `${path || '(root)'}: ${issue.message}`
111+
})
112+
.join('; ')
113+
}
114+
115+
function isPromise<Result>(value: Result | Promise<Result>): value is Promise<Result> {
116+
return typeof (value as Promise<Result>).then === 'function'
117+
}
118+
119+
function validateSpec(
120+
id: string,
121+
spec: DevframeJsonRenderSpec,
122+
schema: StandardSchemaV1 | false | undefined,
123+
): void {
124+
if (schema === false)
125+
return
126+
if (!schema) {
127+
validateElementProps(id, spec)
128+
return
129+
}
130+
131+
const result = schema['~standard'].validate(spec)
132+
if (isPromise(result))
133+
throw diagnostics.DF0074({ id })
134+
if (result.issues)
135+
throw diagnostics.DF0073({ id, issues: formatStandardSchemaIssues(result.issues) })
136+
}
137+
101138
// Ensure the spec always carries a `state` object so JSON-Pointer patches
102139
// into `/state/...` have a container to target.
103-
function normalizeSpec(spec: DevframeJsonRenderSpec): DevframeJsonRenderSpec {
140+
function normalizeSpec<SpecType extends DevframeJsonRenderSpec>(spec: SpecType): SpecType {
104141
return spec.state ? spec : { ...spec, state: {} }
105142
}
106143

@@ -119,10 +156,10 @@ function normalizeSpec(spec: DevframeJsonRenderSpec): DevframeJsonRenderSpec {
119156
* view.dispose()
120157
* ```
121158
*/
122-
export function createJsonRenderView(
159+
export function createJsonRenderView<SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec>(
123160
ctx: AnyContext,
124-
options: CreateJsonRenderViewOptions,
125-
): JsonRenderView {
161+
options: CreateJsonRenderViewOptions<SpecType>,
162+
): JsonRenderView<SpecType> {
126163
const scoped = isScoped(ctx)
127164
const baseCtx = scoped ? ctx.base : ctx
128165
const scope = options.scope ?? (scoped ? ctx.namespace : 'global')
@@ -135,10 +172,10 @@ export function createJsonRenderView(
135172
throw diagnostics.DF0039({ id, scope })
136173

137174
const initial = normalizeSpec(options.spec)
138-
validateElementProps(id, initial)
175+
validateSpec(id, initial, options.schema)
139176
assertJsonSerializable(id, initial)
140177

141-
const state: SharedState<DevframeJsonRenderSpec> = createSharedState({
178+
const state: SharedState<SpecType> = createSharedState({
142179
initialValue: initial,
143180
enablePatches: true,
144181
})
@@ -165,11 +202,11 @@ export function createJsonRenderView(
165202
id,
166203
title,
167204
ref: { stateKey },
168-
value: () => state.value() as DevframeJsonRenderSpec,
205+
value: () => state.value() as SpecType,
169206
update(spec) {
170207
assertLive()
171208
const next = normalizeSpec(spec)
172-
validateElementProps(id, next)
209+
validateSpec(id, next, options.schema)
173210
assertJsonSerializable(id, next)
174211
state.mutate(() => next)
175212
},

‎packages/json-render/src/node/diagnostics.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { defineDiagnostics } from 'devframe/utils/nostics'
22

3-
// `@devframes/json-render` protocol/runtime diagnostics. These share the
4-
// `DF` prefix and live in the devframe core range (next free after the
5-
// current highest `DF00xx`, DF0037). Browser-only render failures keep
6-
// `console.*` in the UI package.
3+
// `@devframes/json-render` protocol/runtime diagnostics share the `DF`
4+
// prefix and use the next globally available core codes. Browser-only render
5+
// failures keep `console.*` in the UI package.
76
export const diagnostics = defineDiagnostics({
87
docsBase: 'https://devfra.me/errors',
98
codes: {
@@ -27,5 +26,15 @@ export const diagnostics = defineDiagnostics({
2726
`JSON-render view "${p.id}" spec is not JSON-serializable: ${p.reason}`,
2827
fix: 'Specs and state travel as strict JSON — remove functions, symbols, class instances, Map/Set, or circular references.',
2928
},
29+
DF0073: {
30+
why: (p: { id: string, issues: string }) =>
31+
`JSON-render view "${p.id}" does not match its configured schema: ${p.issues}`,
32+
fix: 'Match the authored spec to the Standard Schema passed to `createJsonRenderView`.',
33+
},
34+
DF0074: {
35+
why: (p: { id: string }) =>
36+
`JSON-render view "${p.id}" uses an asynchronous Standard Schema.`,
37+
fix: 'Use a synchronous Standard Schema so initial creation and updates remain synchronous.',
38+
},
3039
},
3140
})

‎packages/json-render/src/types.ts‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1-
import type { Spec } from '@json-render/core'
1+
import type {
2+
Catalog,
3+
InferCatalogComponents,
4+
InferComponentProps,
5+
Spec,
6+
UIElement,
7+
} from '@json-render/core'
28
import type { JsonRenderViewStateRef } from './view-ref'
39

410
/**
511
* A Devframes JSON-render spec **is** an `@json-render/core` `Spec`: a flat
612
* `root` key, an `elements` map, and optional initial `state`. This alias is
713
* the Devframes-facing name; it does not add or remove fields.
814
*/
9-
export type DevframeJsonRenderSpec = Spec
15+
export type DevframeJsonRenderSpec<Element extends UIElement = UIElement> = Omit<Spec, 'elements'> & {
16+
elements: Record<string, Element>
17+
}
18+
19+
/** Derive a discriminated element union from every component in a catalog. */
20+
export type CatalogUIElement<CatalogType extends Catalog> = {
21+
[ComponentName in keyof InferCatalogComponents<CatalogType> & string]: UIElement<
22+
ComponentName,
23+
InferComponentProps<CatalogType, ComponentName>
24+
>
25+
}[keyof InferCatalogComponents<CatalogType> & string]
1026

1127
/**
1228
* A single JSON-Pointer patch to a view's `state` model. `path` is an
@@ -27,23 +43,23 @@ export interface JsonRenderStatePatch {
2743
* serializable {@link JsonRenderViewStateRef} that a hub dock (or any client
2844
* transport) uses to locate it.
2945
*/
30-
export interface JsonRenderView {
46+
export interface JsonRenderView<SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec> {
3147
/** Author-supplied stable id, unique within the view's scope. */
3248
readonly id: string
3349
/** Human-facing label published in the view index (defaults to `id`). */
3450
readonly title: string
3551
/** The serializable reference clients subscribe through. */
3652
readonly ref: JsonRenderViewStateRef
3753
/** Replace the entire spec (a structural change replaces the whole spec). */
38-
update: (spec: DevframeJsonRenderSpec) => void
54+
update: (spec: SpecType) => void
3955
/**
4056
* Apply JSON-Pointer patches to the view's `state`. Travels as a
4157
* shared-state patch (not a whole-spec snapshot), so only the changed
4258
* paths cross the wire.
4359
*/
4460
patchState: (patches: JsonRenderStatePatch[]) => void
4561
/** Read the current spec (immutable). */
46-
value: () => DevframeJsonRenderSpec
62+
value: () => SpecType
4763
/** Unregister the shared state and its listeners. */
4864
dispose: () => void
4965
}

0 commit comments

Comments
 (0)