schema-components 2.0.1 → 2.1.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/README.md +98 -1
- package/dist/SchemaComponent-B__6-5-E.d.mts +277 -0
- package/dist/SchemaComponent-BxzzsHsK.mjs +668 -0
- package/dist/adapter-ktQaheWB.d.mts +213 -0
- package/dist/constructorTypes-BdCiMS6e.d.mts +30 -0
- package/dist/core/adapter.d.mts +3 -213
- package/dist/core/adapter.mjs +33 -25
- package/dist/core/constraintHint.d.mts +1 -1
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/contexts.d.mts +71 -0
- package/dist/core/contexts.mjs +1 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/{react → core}/fieldPath.d.mts +2 -2
- package/dist/{react → core}/fieldPath.mjs +3 -3
- package/dist/core/formats.d.mts +1 -1
- package/dist/core/guards.d.mts +2 -2
- package/dist/core/guards.mjs +2 -2
- package/dist/core/inferValue.d.mts +1 -1
- package/dist/core/limits.d.mts +1 -1
- package/dist/core/merge.d.mts +1 -1
- package/dist/core/normalise.d.mts +6 -6
- package/dist/core/normalise.mjs +1 -1
- package/dist/core/openapi30.d.mts +1 -1
- package/dist/core/openapi30.mjs +1 -1
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/renderField.d.mts +147 -0
- package/dist/core/renderField.mjs +81 -0
- package/dist/core/renderer.d.mts +2 -199
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +1 -981
- package/dist/core/types.d.mts +1 -1
- package/dist/core/unionMatch.d.mts +1 -1
- package/dist/core/uri.d.mts +2 -2
- package/dist/core/uri.mjs +2 -2
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +4 -4
- package/dist/core/walkBuilders.mjs +1 -1
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +3 -3
- package/dist/{errors-Dki7tji4.d.mts → errors-DbaI04x2.d.mts} +1 -1
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/html.d.mts +10 -8
- package/dist/html/renderToHtml.d.mts +5 -5
- package/dist/html/renderToHtml.mjs +45 -24
- package/dist/html/renderToHtmlStream.d.mts +5 -5
- package/dist/html/renderers.d.mts +1 -1
- package/dist/html/streamRenderers.d.mts +3 -3
- package/dist/{inferValue-PPXWJpbN.d.mts → inferValue-eAnh50EM.d.mts} +6 -6
- package/dist/lit/SchemaComponent.d.mts +125 -0
- package/dist/lit/SchemaComponent.mjs +2 -0
- package/dist/lit/SchemaField.d.mts +65 -0
- package/dist/lit/SchemaField.mjs +2 -0
- package/dist/lit/SchemaView.d.mts +14 -0
- package/dist/lit/SchemaView.mjs +2 -0
- package/dist/lit/constructorTypes.d.mts +2 -0
- package/dist/lit/constructorTypes.mjs +1 -0
- package/dist/lit/contexts.d.mts +78 -0
- package/dist/lit/contexts.mjs +238 -0
- package/dist/lit/defaultResolver.d.mts +33 -0
- package/dist/lit/defaultResolver.mjs +2 -0
- package/dist/lit/registry.d.mts +66 -0
- package/dist/lit/registry.mjs +2 -0
- package/dist/lit/renderers/baseElement.d.mts +131 -0
- package/dist/lit/renderers/baseElement.mjs +109 -0
- package/dist/lit/renderers/recordHelpers.d.mts +25 -0
- package/dist/lit/renderers/recordHelpers.mjs +55 -0
- package/dist/lit/renderers/scArray.d.mts +14 -0
- package/dist/lit/renderers/scArray.mjs +86 -0
- package/dist/lit/renderers/scBoolean.d.mts +15 -0
- package/dist/lit/renderers/scBoolean.mjs +47 -0
- package/dist/lit/renderers/scConditional.d.mts +23 -0
- package/dist/lit/renderers/scConditional.mjs +65 -0
- package/dist/lit/renderers/scDiscriminated.d.mts +23 -0
- package/dist/lit/renderers/scDiscriminated.mjs +138 -0
- package/dist/lit/renderers/scEnum.d.mts +16 -0
- package/dist/lit/renderers/scEnum.mjs +66 -0
- package/dist/lit/renderers/scFile.d.mts +15 -0
- package/dist/lit/renderers/scFile.mjs +53 -0
- package/dist/lit/renderers/scLiteralNullNever.d.mts +30 -0
- package/dist/lit/renderers/scLiteralNullNever.mjs +57 -0
- package/dist/lit/renderers/scNumber.d.mts +15 -0
- package/dist/lit/renderers/scNumber.mjs +64 -0
- package/dist/lit/renderers/scObject.d.mts +14 -0
- package/dist/lit/renderers/scObject.mjs +57 -0
- package/dist/lit/renderers/scRecord.d.mts +14 -0
- package/dist/lit/renderers/scRecord.mjs +112 -0
- package/dist/lit/renderers/scString.d.mts +19 -0
- package/dist/lit/renderers/scString.mjs +165 -0
- package/dist/lit/renderers/scTuple.d.mts +14 -0
- package/dist/lit/renderers/scTuple.mjs +58 -0
- package/dist/lit/renderers/scUnion.d.mts +14 -0
- package/dist/lit/renderers/scUnion.mjs +44 -0
- package/dist/lit/renderers/scUnknown.d.mts +15 -0
- package/dist/lit/renderers/scUnknown.mjs +45 -0
- package/dist/lit/ssr.d.mts +37 -0
- package/dist/lit/ssr.mjs +9565 -0
- package/dist/lit/types.d.mts +2 -0
- package/dist/lit/types.mjs +1 -0
- package/dist/lit/widget.d.mts +71 -0
- package/dist/lit/widget.mjs +87 -0
- package/dist/{normalise-DB-Xtjmn.mjs → normalise-BkePrJ4v.mjs} +6 -6
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/components.d.mts +5 -5
- package/dist/openapi/components.mjs +1 -1
- package/dist/openapi/parser.d.mts +2 -2
- package/dist/openapi/resolve.d.mts +1 -1
- package/dist/openapi/resolve.mjs +1 -1
- package/dist/preact/SchemaComponent.d.mts +3 -0
- package/dist/preact/SchemaComponent.mjs +26 -0
- package/dist/preact/SchemaErrorBoundary.d.mts +2 -0
- package/dist/preact/SchemaErrorBoundary.mjs +20 -0
- package/dist/preact/SchemaView.d.mts +2 -0
- package/dist/preact/SchemaView.mjs +22 -0
- package/dist/preact/headless.d.mts +2 -0
- package/dist/preact/headless.mjs +18 -0
- package/dist/react/SchemaComponent.d.mts +3 -270
- package/dist/react/SchemaComponent.mjs +48 -39
- package/dist/react/SchemaErrorBoundary.mjs +7 -4
- package/dist/react/SchemaView.d.mts +11 -10
- package/dist/react/SchemaView.mjs +32 -29
- package/dist/react/a11y.d.mts +2 -2
- package/dist/react/fieldShell.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +2 -2
- package/dist/{ref-DdsbekXX.d.mts → ref-DWrQG1Er.d.mts} +1 -1
- package/dist/renderer-ab9E52Bp.d.mts +245 -0
- package/dist/solid/SchemaComponent.d.mts +136 -0
- package/dist/solid/SchemaComponent.mjs +391 -0
- package/dist/solid/SchemaErrorBoundary.d.mts +38 -0
- package/dist/solid/SchemaErrorBoundary.mjs +57 -0
- package/dist/solid/SchemaField.d.mts +40 -0
- package/dist/solid/SchemaField.mjs +113 -0
- package/dist/solid/SchemaView.d.mts +54 -0
- package/dist/solid/SchemaView.mjs +168 -0
- package/dist/solid/a11y.d.mts +70 -0
- package/dist/solid/a11y.mjs +71 -0
- package/dist/solid/contexts.d.mts +37 -0
- package/dist/solid/contexts.mjs +66 -0
- package/dist/solid/headless.d.mts +10 -0
- package/dist/solid/headless.mjs +27 -0
- package/dist/solid/renderers.d.mts +79 -0
- package/dist/solid/renderers.mjs +840 -0
- package/dist/solid/types.d.mts +90 -0
- package/dist/solid/types.mjs +1 -0
- package/dist/solid/widget.d.mts +29 -0
- package/dist/solid/widget.mjs +35 -0
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/typeInference-Y8tNEQJk.d.mts +983 -0
- package/dist/types-BCy7K3nk.d.mts +125 -0
- package/package.json +71 -1
- package/src/svelte/SchemaComponent.svelte +427 -0
- package/src/svelte/SchemaErrorBoundary.svelte +66 -0
- package/src/svelte/SchemaField.svelte +216 -0
- package/src/svelte/SchemaProvider.svelte +46 -0
- package/src/svelte/SchemaView.svelte +244 -0
- package/src/svelte/a11y.ts +112 -0
- package/src/svelte/contexts.ts +79 -0
- package/src/svelte/dispatch.ts +267 -0
- package/src/svelte/headless.ts +73 -0
- package/src/svelte/headlessFns.ts +124 -0
- package/src/svelte/renderers/Array.svelte +98 -0
- package/src/svelte/renderers/Boolean.svelte +43 -0
- package/src/svelte/renderers/Conditional.svelte +67 -0
- package/src/svelte/renderers/DiscriminatedUnion.svelte +197 -0
- package/src/svelte/renderers/Enum.svelte +53 -0
- package/src/svelte/renderers/Fallback.svelte +24 -0
- package/src/svelte/renderers/File.svelte +46 -0
- package/src/svelte/renderers/Literal.svelte +29 -0
- package/src/svelte/renderers/Mount.svelte +24 -0
- package/src/svelte/renderers/Negation.svelte +35 -0
- package/src/svelte/renderers/Never.svelte +24 -0
- package/src/svelte/renderers/Null.svelte +19 -0
- package/src/svelte/renderers/Number.svelte +68 -0
- package/src/svelte/renderers/Object.svelte +74 -0
- package/src/svelte/renderers/Record.svelte +134 -0
- package/src/svelte/renderers/RecursionSentinel.svelte +27 -0
- package/src/svelte/renderers/String.svelte +152 -0
- package/src/svelte/renderers/Tuple.svelte +84 -0
- package/src/svelte/renderers/Union.svelte +49 -0
- package/src/svelte/renderers/Unknown.svelte +42 -0
- package/src/svelte/svelte-modules.d.ts +25 -0
- package/src/svelte/types.ts +238 -0
- package/src/svelte/widget.ts +62 -0
- /package/dist/{diagnostics-BTrm3O6J.d.mts → diagnostics-mftUZI7c.d.mts} +0 -0
- /package/dist/{limits-x4OiyJxh.d.mts → limits-Vv9hUbI_.d.mts} +0 -0
- /package/dist/{types-BrYbjC7_.d.mts → types-BBQaEPfE.d.mts} +0 -0
- /package/dist/{version-DL8U5RuA.d.mts → version-BEBx10ND.d.mts} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Svelte 5 error boundary for schema-components.
|
|
3
|
+
|
|
4
|
+
Uses Svelte 5's native `<svelte:boundary>` primitive (introduced
|
|
5
|
+
in Svelte 5) — the structural equivalent of the React class
|
|
6
|
+
component `componentDidCatch` / `getDerivedStateFromError`
|
|
7
|
+
pattern from `react/SchemaErrorBoundary.tsx`.
|
|
8
|
+
|
|
9
|
+
Catches synchronous render errors thrown by descendant
|
|
10
|
+
components (including `<SchemaComponent>` / `<SchemaView>` and
|
|
11
|
+
any custom theme adapter renderers). The boundary surfaces the
|
|
12
|
+
caught `Error` to the supplied `fallback` snippet alongside a
|
|
13
|
+
`reset` callback that clears the error state — Svelte's boundary
|
|
14
|
+
primitive owns the state internally; the snippet just receives
|
|
15
|
+
whichever helpers Svelte passes to it.
|
|
16
|
+
|
|
17
|
+
Mirrors the React boundary's documentation: this primitive does
|
|
18
|
+
NOT catch errors thrown from event handlers, async work, or
|
|
19
|
+
server-side rendering. Those must be handled at the host
|
|
20
|
+
application boundary.
|
|
21
|
+
-->
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import type { Snippet } from "svelte";
|
|
24
|
+
import { SchemaError } from "../core/errors.ts";
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Rendered while no error is in flight. */
|
|
28
|
+
children: Snippet;
|
|
29
|
+
/**
|
|
30
|
+
* Rendered when the boundary catches an error. Receives the
|
|
31
|
+
* thrown `Error` and a `reset` thunk that re-renders the
|
|
32
|
+
* children once the underlying problem is fixed (e.g. a
|
|
33
|
+
* corrected `schema` prop).
|
|
34
|
+
*/
|
|
35
|
+
fallback: Snippet<[Error, () => void]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { children, fallback }: Props = $props();
|
|
39
|
+
|
|
40
|
+
function logIfUnexpected(error: unknown): void {
|
|
41
|
+
// Non-SchemaError thrown values are forwarded to the
|
|
42
|
+
// console so the application has at least one signal in the
|
|
43
|
+
// logs. SchemaError instances are presumed handled by the
|
|
44
|
+
// `onError` callback on `<SchemaComponent>` / `<SchemaView>`.
|
|
45
|
+
if (!(error instanceof SchemaError)) {
|
|
46
|
+
// Diagnostic surface mirroring the React adapter — the
|
|
47
|
+
// boundary's contract is "log the unexpected, surface
|
|
48
|
+
// structured errors through `failed`".
|
|
49
|
+
console.error("[schema-components] Unhandled render error:", error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<svelte:boundary
|
|
55
|
+
onerror={(error) => {
|
|
56
|
+
logIfUnexpected(error);
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{@render children()}
|
|
60
|
+
{#snippet failed(error, reset)}
|
|
61
|
+
{@render fallback(
|
|
62
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
63
|
+
reset
|
|
64
|
+
)}
|
|
65
|
+
{/snippet}
|
|
66
|
+
</svelte:boundary>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Render a single field from a schema by dot-separated `path`.
|
|
3
|
+
Mirror of `react/SchemaComponent.tsx :: SchemaField`.
|
|
4
|
+
|
|
5
|
+
Walks the full schema tree, resolves the field at `path`, and
|
|
6
|
+
renders only that subtree. Useful for embedding individual
|
|
7
|
+
fields inside bespoke layouts when `<SchemaComponent>` would
|
|
8
|
+
render too much.
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts" generics="T = unknown, Ref extends string | undefined = undefined">
|
|
11
|
+
import { walk } from "../core/walker.ts";
|
|
12
|
+
import type { WalkOptions } from "../core/walkBuilders.ts";
|
|
13
|
+
import {
|
|
14
|
+
normaliseSchema,
|
|
15
|
+
type SchemaIoSide,
|
|
16
|
+
} from "../core/adapter.ts";
|
|
17
|
+
import {
|
|
18
|
+
SchemaNormalisationError,
|
|
19
|
+
SchemaFieldError,
|
|
20
|
+
} from "../core/errors.ts";
|
|
21
|
+
import type { SchemaMeta, WalkedField } from "../core/types.ts";
|
|
22
|
+
import {
|
|
23
|
+
resolvePath,
|
|
24
|
+
resolveValue,
|
|
25
|
+
setNestedValue,
|
|
26
|
+
} from "../core/fieldPath.ts";
|
|
27
|
+
import { resolverContext, widgetsContext } from "./contexts.ts";
|
|
28
|
+
import { renderFieldSvelte } from "./dispatch.ts";
|
|
29
|
+
import RecursionSentinel from "./renderers/RecursionSentinel.svelte";
|
|
30
|
+
import Fallback from "./renderers/Fallback.svelte";
|
|
31
|
+
import Mount from "./renderers/Mount.svelte";
|
|
32
|
+
import type {
|
|
33
|
+
SvelteRenderDescriptor,
|
|
34
|
+
SvelteRenderProps,
|
|
35
|
+
} from "./types.ts";
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
/** Dot-separated path (e.g. "address.city"). */
|
|
39
|
+
path: string;
|
|
40
|
+
/** The schema to extract the field from. */
|
|
41
|
+
schema: T;
|
|
42
|
+
/** OpenAPI ref string. */
|
|
43
|
+
ref?: Ref;
|
|
44
|
+
/** Direction (`"output"` / `"input"`) for codec / transform schemas. */
|
|
45
|
+
io?: SchemaIoSide;
|
|
46
|
+
/** Current value of the root schema. */
|
|
47
|
+
value?: unknown;
|
|
48
|
+
/** Called with the updated root value when this field changes. */
|
|
49
|
+
onChange?: (value: unknown) => void;
|
|
50
|
+
/** Override meta for this specific field. */
|
|
51
|
+
meta?: SchemaMeta;
|
|
52
|
+
/**
|
|
53
|
+
* Prefix used for every input `id` / label `for` in this
|
|
54
|
+
* subtree. Defaults to a sanitised, mount-stable counter.
|
|
55
|
+
*/
|
|
56
|
+
idPrefix?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
path,
|
|
61
|
+
schema,
|
|
62
|
+
ref,
|
|
63
|
+
io,
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
meta: fieldMeta,
|
|
67
|
+
idPrefix,
|
|
68
|
+
}: Props = $props();
|
|
69
|
+
|
|
70
|
+
const userResolver = resolverContext.consume();
|
|
71
|
+
const contextWidgets = widgetsContext.consume();
|
|
72
|
+
|
|
73
|
+
const fallbackPrefix = `sc-svelte-field-${String(nextFieldInstanceId())}`;
|
|
74
|
+
const rootPath = $derived(joinPath(idPrefix ?? fallbackPrefix, path));
|
|
75
|
+
|
|
76
|
+
interface NormalisedShape {
|
|
77
|
+
jsonSchema: Record<string, unknown>;
|
|
78
|
+
rootMeta: SchemaMeta | undefined;
|
|
79
|
+
rootDocument: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const normalisedResult = $derived<NormalisedShape>(
|
|
83
|
+
normaliseOrThrow(schema, ref, io)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const walkOptions = $derived<WalkOptions>({
|
|
87
|
+
...(fieldMeta !== undefined ? { componentMeta: fieldMeta } : {}),
|
|
88
|
+
...(normalisedResult.rootMeta !== undefined
|
|
89
|
+
? { rootMeta: normalisedResult.rootMeta }
|
|
90
|
+
: {}),
|
|
91
|
+
rootDocument: normalisedResult.rootDocument,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const fullTree = $derived(walk(normalisedResult.jsonSchema, walkOptions));
|
|
95
|
+
|
|
96
|
+
const fieldTree = $derived<WalkedField>(
|
|
97
|
+
resolvePathOrThrow(fullTree, path, schema)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const fieldValue = $derived(resolveValue(value, path));
|
|
101
|
+
|
|
102
|
+
function handleChange(nextFieldValue: unknown): void {
|
|
103
|
+
const newRootValue = setNestedValue(value, path, nextFieldValue);
|
|
104
|
+
onChange?.(newRootValue);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function makeRenderChild(
|
|
108
|
+
currentDepth: number,
|
|
109
|
+
parentPath: string
|
|
110
|
+
): SvelteRenderProps["renderChild"] {
|
|
111
|
+
return (
|
|
112
|
+
childTree: WalkedField,
|
|
113
|
+
childValue: unknown,
|
|
114
|
+
childOnChange: (v: unknown) => void,
|
|
115
|
+
pathSuffix?: string
|
|
116
|
+
): SvelteRenderDescriptor | null => {
|
|
117
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
118
|
+
return renderFieldSvelte(
|
|
119
|
+
childTree,
|
|
120
|
+
childValue,
|
|
121
|
+
childOnChange,
|
|
122
|
+
userResolver,
|
|
123
|
+
makeRenderChild(currentDepth + 1, childPath),
|
|
124
|
+
childPath,
|
|
125
|
+
undefined,
|
|
126
|
+
contextWidgets,
|
|
127
|
+
currentDepth + 1,
|
|
128
|
+
Fallback,
|
|
129
|
+
RecursionSentinel
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const renderChild = $derived(makeRenderChild(0, rootPath));
|
|
135
|
+
|
|
136
|
+
const rootDescriptor = $derived<SvelteRenderDescriptor | null>(
|
|
137
|
+
renderFieldSvelte(
|
|
138
|
+
fieldTree,
|
|
139
|
+
fieldValue,
|
|
140
|
+
handleChange,
|
|
141
|
+
userResolver,
|
|
142
|
+
renderChild,
|
|
143
|
+
rootPath,
|
|
144
|
+
undefined,
|
|
145
|
+
contextWidgets,
|
|
146
|
+
0,
|
|
147
|
+
Fallback,
|
|
148
|
+
RecursionSentinel
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
function joinPath(parent: string, suffix: string | undefined): string {
|
|
153
|
+
if (suffix === undefined || suffix.length === 0) return parent;
|
|
154
|
+
if (parent.length === 0) return suffix;
|
|
155
|
+
if (suffix.startsWith("[")) return `${parent}${suffix}`;
|
|
156
|
+
return `${parent}.${suffix}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normaliseOrThrow(
|
|
160
|
+
schemaInput: unknown,
|
|
161
|
+
refInput: string | undefined,
|
|
162
|
+
ioSide: SchemaIoSide | undefined
|
|
163
|
+
): NormalisedShape {
|
|
164
|
+
try {
|
|
165
|
+
const opts =
|
|
166
|
+
ioSide !== undefined ? { io: ioSide } : undefined;
|
|
167
|
+
const out = normaliseSchema(schemaInput, refInput, opts);
|
|
168
|
+
return out;
|
|
169
|
+
} catch (err: unknown) {
|
|
170
|
+
if (err instanceof SchemaNormalisationError) throw err;
|
|
171
|
+
throw new SchemaNormalisationError(
|
|
172
|
+
err instanceof Error
|
|
173
|
+
? err.message
|
|
174
|
+
: "Failed to normalise schema",
|
|
175
|
+
schemaInput,
|
|
176
|
+
"unknown"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolvePathOrThrow(
|
|
182
|
+
tree: WalkedField,
|
|
183
|
+
p: string,
|
|
184
|
+
schemaInput: unknown
|
|
185
|
+
): WalkedField {
|
|
186
|
+
const resolved = resolvePath(tree, p);
|
|
187
|
+
if (resolved === undefined) {
|
|
188
|
+
throw new SchemaFieldError(
|
|
189
|
+
`Field not found: ${p}`,
|
|
190
|
+
schemaInput,
|
|
191
|
+
p
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<script lang="ts" module>
|
|
199
|
+
let fieldInstanceCounter = 0;
|
|
200
|
+
/**
|
|
201
|
+
* Module-scoped counter feeding the default `idPrefix` for every
|
|
202
|
+
* `<SchemaField>` instance. Separate from the `<SchemaComponent>`
|
|
203
|
+
* and `<SchemaView>` counters so the three namespaces never
|
|
204
|
+
* overlap.
|
|
205
|
+
*
|
|
206
|
+
* @returns The next per-instance integer (1, 2, …).
|
|
207
|
+
*/
|
|
208
|
+
export function nextFieldInstanceId(): number {
|
|
209
|
+
fieldInstanceCounter += 1;
|
|
210
|
+
return fieldInstanceCounter;
|
|
211
|
+
}
|
|
212
|
+
</script>
|
|
213
|
+
|
|
214
|
+
{#if rootDescriptor !== null}
|
|
215
|
+
<Mount descriptor={rootDescriptor} />
|
|
216
|
+
{/if}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Provide a theme resolver and scoped widgets to every
|
|
3
|
+
`<SchemaComponent>` and `<SchemaView>` rendered inside the
|
|
4
|
+
subtree. Mirror of `react/SchemaComponent.tsx :: SchemaProvider`
|
|
5
|
+
for Svelte 5.
|
|
6
|
+
|
|
7
|
+
Sets the resolver and widget map via `setContext()`
|
|
8
|
+
(`./contexts.ts :: resolverContext`, `widgetsContext`) so any
|
|
9
|
+
descendant calling `resolverContext.consume()` or
|
|
10
|
+
`widgetsContext.consume()` receives the supplied values.
|
|
11
|
+
`<SchemaComponent>` calls those consume hooks on mount and will
|
|
12
|
+
therefore pick up the provided theme automatically.
|
|
13
|
+
-->
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import type { Snippet } from "svelte";
|
|
16
|
+
import { resolverContext, widgetsContext } from "./contexts.ts";
|
|
17
|
+
import type { SvelteComponentResolver, SvelteWidgetMap } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Theme resolver to install for the subtree. */
|
|
21
|
+
resolver: SvelteComponentResolver;
|
|
22
|
+
/** Scoped widgets — override per-instance and global widgets. */
|
|
23
|
+
widgets?: SvelteWidgetMap;
|
|
24
|
+
children: Snippet;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { resolver, widgets, children }: Props = $props();
|
|
28
|
+
|
|
29
|
+
// Svelte's `setContext` is one-shot per component mount; the
|
|
30
|
+
// provide port returns its `children` argument so the call site
|
|
31
|
+
// gets a familiar value-returning shape, but the actual context
|
|
32
|
+
// wiring happens as a side effect. Reactive updates to
|
|
33
|
+
// `resolver` / `widgets` after mount intentionally do not
|
|
34
|
+
// propagate — matches the React adapter's
|
|
35
|
+
// `<UserResolverContext.Provider value={resolver}>` semantics
|
|
36
|
+
// where re-rendering the provider with a new value would update
|
|
37
|
+
// consumers; here we contract the value as mount-fixed and
|
|
38
|
+
// recommend re-mounting the subtree when the theme changes.
|
|
39
|
+
|
|
40
|
+
/* svelte-ignore state_referenced_locally */
|
|
41
|
+
resolverContext.provide(resolver, undefined);
|
|
42
|
+
/* svelte-ignore state_referenced_locally */
|
|
43
|
+
widgetsContext.provide(widgets, undefined);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{@render children()}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Read-only Svelte 5 schema renderer — counterpart to
|
|
3
|
+
`<SchemaComponent>` for SSR / hydration / display-only use.
|
|
4
|
+
|
|
5
|
+
Mirror of `react/SchemaView.tsx`. Receives the same prop bag but
|
|
6
|
+
drops `onChange`, `validate`, `onValidationError`, and `onError`
|
|
7
|
+
— the read-only path has no interactive state and no validation
|
|
8
|
+
to dispatch. The theme adapter is passed via the `resolver` prop
|
|
9
|
+
rather than `<SchemaProvider>` so the component remains usable in
|
|
10
|
+
Svelte's SSR pass without depending on context propagation.
|
|
11
|
+
|
|
12
|
+
Internally, the dispatcher chain is identical to
|
|
13
|
+
`<SchemaComponent>` — `renderFieldSvelte` from `./dispatch.ts`
|
|
14
|
+
handles the depth cap, widget overrides, resolver dispatch, and
|
|
15
|
+
recursion sentinel. The only differences are:
|
|
16
|
+
|
|
17
|
+
- `readOnly` is forced to `true` on the merged meta.
|
|
18
|
+
- `onChange` is a noop (no event handlers fire; every renderer
|
|
19
|
+
observes `readOnly === true` and skips wiring DOM handlers).
|
|
20
|
+
- No global widget lookup — Svelte SSR mustn't read
|
|
21
|
+
module-level mutable state.
|
|
22
|
+
-->
|
|
23
|
+
<script lang="ts" generics="T = unknown, Ref extends string | undefined = undefined">
|
|
24
|
+
import { walk } from "../core/walker.ts";
|
|
25
|
+
import type { WalkOptions } from "../core/walkBuilders.ts";
|
|
26
|
+
import {
|
|
27
|
+
normaliseSchema,
|
|
28
|
+
type SchemaIoSide,
|
|
29
|
+
} from "../core/adapter.ts";
|
|
30
|
+
import { SchemaNormalisationError } from "../core/errors.ts";
|
|
31
|
+
import type { SchemaMeta, WalkedField } from "../core/types.ts";
|
|
32
|
+
import { toRecordOrUndefined } from "../core/guards.ts";
|
|
33
|
+
import type {
|
|
34
|
+
DiagnosticsOptions,
|
|
35
|
+
Diagnostic,
|
|
36
|
+
} from "../core/diagnostics.ts";
|
|
37
|
+
import type {
|
|
38
|
+
InferFields,
|
|
39
|
+
InferredValue,
|
|
40
|
+
} from "../core/inferValue.ts";
|
|
41
|
+
import { renderFieldSvelte } from "./dispatch.ts";
|
|
42
|
+
import RecursionSentinel from "./renderers/RecursionSentinel.svelte";
|
|
43
|
+
import Fallback from "./renderers/Fallback.svelte";
|
|
44
|
+
import Mount from "./renderers/Mount.svelte";
|
|
45
|
+
import type {
|
|
46
|
+
SvelteComponentResolver,
|
|
47
|
+
SvelteRenderDescriptor,
|
|
48
|
+
SvelteRenderProps,
|
|
49
|
+
SvelteWidgetMap,
|
|
50
|
+
} from "./types.ts";
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
schema: T;
|
|
54
|
+
ref?: Ref;
|
|
55
|
+
io?: SchemaIoSide;
|
|
56
|
+
value?: InferredValue<T, Ref, undefined, "output">;
|
|
57
|
+
fields?: InferFields<T, Ref>;
|
|
58
|
+
meta?: SchemaMeta;
|
|
59
|
+
description?: string;
|
|
60
|
+
/** Theme resolver — Svelte SSR has no context fallthrough, pass explicitly. */
|
|
61
|
+
resolver?: SvelteComponentResolver;
|
|
62
|
+
/** Instance-scoped widgets. */
|
|
63
|
+
widgets?: SvelteWidgetMap;
|
|
64
|
+
onDiagnostic?: (diagnostic: Diagnostic) => void;
|
|
65
|
+
strict?: boolean;
|
|
66
|
+
idPrefix?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
schema,
|
|
71
|
+
ref,
|
|
72
|
+
io,
|
|
73
|
+
value,
|
|
74
|
+
fields,
|
|
75
|
+
meta: componentMeta,
|
|
76
|
+
description,
|
|
77
|
+
resolver,
|
|
78
|
+
widgets,
|
|
79
|
+
onDiagnostic,
|
|
80
|
+
strict,
|
|
81
|
+
idPrefix,
|
|
82
|
+
}: Props = $props();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Per-mount fallback prefix — matches `<SchemaComponent>`'s
|
|
86
|
+
* default so a `<SchemaView>` and a sibling `<SchemaComponent>`
|
|
87
|
+
* on the same page can never collide.
|
|
88
|
+
*/
|
|
89
|
+
const fallbackPrefix = `sc-svelte-view-${String(nextViewInstanceId())}`;
|
|
90
|
+
const rootPath = $derived(idPrefix ?? fallbackPrefix);
|
|
91
|
+
|
|
92
|
+
const mergedMeta = $derived<SchemaMeta>({
|
|
93
|
+
...componentMeta,
|
|
94
|
+
readOnly: true,
|
|
95
|
+
...(description !== undefined ? { description } : {}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const diagnostics: DiagnosticsOptions | undefined = $derived(
|
|
99
|
+
onDiagnostic !== undefined || strict === true
|
|
100
|
+
? {
|
|
101
|
+
...(onDiagnostic !== undefined
|
|
102
|
+
? { diagnostics: onDiagnostic }
|
|
103
|
+
: {}),
|
|
104
|
+
...(strict !== undefined ? { strict } : {}),
|
|
105
|
+
}
|
|
106
|
+
: undefined
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
interface NormalisedShape {
|
|
110
|
+
jsonSchema: Record<string, unknown>;
|
|
111
|
+
rootMeta: SchemaMeta | undefined;
|
|
112
|
+
rootDocument: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalisedResult = $derived<NormalisedShape>(
|
|
116
|
+
normaliseOrThrow(schema, ref, io, diagnostics)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const fieldsRecord = $derived(toRecordOrUndefined(fields));
|
|
120
|
+
|
|
121
|
+
const walkOptions = $derived<WalkOptions>({
|
|
122
|
+
componentMeta: mergedMeta,
|
|
123
|
+
...(normalisedResult.rootMeta !== undefined
|
|
124
|
+
? { rootMeta: normalisedResult.rootMeta }
|
|
125
|
+
: {}),
|
|
126
|
+
...(fieldsRecord !== undefined
|
|
127
|
+
? { fieldOverrides: fieldsRecord }
|
|
128
|
+
: {}),
|
|
129
|
+
rootDocument: normalisedResult.rootDocument,
|
|
130
|
+
...(diagnostics !== undefined ? { diagnostics } : {}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const tree = $derived<WalkedField>(
|
|
134
|
+
walk(normalisedResult.jsonSchema, walkOptions)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
function readOnlyOnChange(_v: unknown): void {
|
|
138
|
+
/* intentional no-op — SchemaView is read-only. */
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function makeRenderChild(
|
|
142
|
+
currentDepth: number,
|
|
143
|
+
parentPath: string
|
|
144
|
+
): SvelteRenderProps["renderChild"] {
|
|
145
|
+
return (
|
|
146
|
+
childTree: WalkedField,
|
|
147
|
+
childValue: unknown,
|
|
148
|
+
_childOnChange: (v: unknown) => void,
|
|
149
|
+
pathSuffix?: string
|
|
150
|
+
): SvelteRenderDescriptor | null => {
|
|
151
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
152
|
+
return renderFieldSvelte(
|
|
153
|
+
childTree,
|
|
154
|
+
childValue,
|
|
155
|
+
readOnlyOnChange,
|
|
156
|
+
resolver,
|
|
157
|
+
makeRenderChild(currentDepth + 1, childPath),
|
|
158
|
+
childPath,
|
|
159
|
+
widgets,
|
|
160
|
+
undefined,
|
|
161
|
+
currentDepth + 1,
|
|
162
|
+
Fallback,
|
|
163
|
+
RecursionSentinel
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const renderChild = $derived(makeRenderChild(0, rootPath));
|
|
169
|
+
|
|
170
|
+
const rootDescriptor = $derived<SvelteRenderDescriptor | null>(
|
|
171
|
+
renderFieldSvelte(
|
|
172
|
+
tree,
|
|
173
|
+
value ?? tree.defaultValue,
|
|
174
|
+
readOnlyOnChange,
|
|
175
|
+
resolver,
|
|
176
|
+
renderChild,
|
|
177
|
+
rootPath,
|
|
178
|
+
widgets,
|
|
179
|
+
undefined,
|
|
180
|
+
0,
|
|
181
|
+
Fallback,
|
|
182
|
+
RecursionSentinel
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Path-suffix join, mirror of `SchemaComponent.svelte :: joinPath`
|
|
188
|
+
* and `react/SchemaComponent.tsx :: joinPath`.
|
|
189
|
+
*/
|
|
190
|
+
function joinPath(parent: string, suffix: string | undefined): string {
|
|
191
|
+
if (suffix === undefined || suffix.length === 0) return parent;
|
|
192
|
+
if (parent.length === 0) return suffix;
|
|
193
|
+
if (suffix.startsWith("[")) return `${parent}${suffix}`;
|
|
194
|
+
return `${parent}.${suffix}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normaliseOrThrow(
|
|
198
|
+
schemaInput: unknown,
|
|
199
|
+
refInput: string | undefined,
|
|
200
|
+
ioSide: SchemaIoSide | undefined,
|
|
201
|
+
diags: DiagnosticsOptions | undefined
|
|
202
|
+
): NormalisedShape {
|
|
203
|
+
try {
|
|
204
|
+
const opts =
|
|
205
|
+
diags !== undefined || ioSide !== undefined
|
|
206
|
+
? {
|
|
207
|
+
...(diags !== undefined ? { diagnostics: diags } : {}),
|
|
208
|
+
...(ioSide !== undefined ? { io: ioSide } : {}),
|
|
209
|
+
}
|
|
210
|
+
: undefined;
|
|
211
|
+
const out = normaliseSchema(schemaInput, refInput, opts);
|
|
212
|
+
return out;
|
|
213
|
+
} catch (err: unknown) {
|
|
214
|
+
if (err instanceof SchemaNormalisationError) throw err;
|
|
215
|
+
throw new SchemaNormalisationError(
|
|
216
|
+
err instanceof Error
|
|
217
|
+
? err.message
|
|
218
|
+
: "Failed to normalise schema",
|
|
219
|
+
schemaInput,
|
|
220
|
+
"unknown"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<script lang="ts" module>
|
|
227
|
+
let viewInstanceCounter = 0;
|
|
228
|
+
/**
|
|
229
|
+
* Module-scoped counter feeding the default `idPrefix` for every
|
|
230
|
+
* `<SchemaView>` instance. Separate from the `<SchemaComponent>`
|
|
231
|
+
* counter so the two namespaces never overlap; ids stay unique
|
|
232
|
+
* across all schema-driven Svelte components on a page.
|
|
233
|
+
*
|
|
234
|
+
* @returns The next per-instance integer (1, 2, …).
|
|
235
|
+
*/
|
|
236
|
+
export function nextViewInstanceId(): number {
|
|
237
|
+
viewInstanceCounter += 1;
|
|
238
|
+
return viewInstanceCounter;
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
{#if rootDescriptor !== null}
|
|
243
|
+
<Mount descriptor={rootDescriptor} />
|
|
244
|
+
{/if}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ARIA attribute helpers for Svelte 5 renderers.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `react/a11y.ts` returning the same shapes — plain
|
|
5
|
+
* `Record<string, string>` spreadable attribute bags and a
|
|
6
|
+
* {@link HintInfo} descriptor for the constraint-hint `<small>`
|
|
7
|
+
* element — so theme adapters compose around the same accessibility
|
|
8
|
+
* primitives the headless renderer emits.
|
|
9
|
+
*
|
|
10
|
+
* Keep this module aligned with `react/a11y.ts` and `html/a11y.ts` —
|
|
11
|
+
* all renderers must emit the same accessibility metadata for the
|
|
12
|
+
* same schema field. The constraint-hint text builder
|
|
13
|
+
* (`core/constraintHint.ts`) is the single source of truth for hint
|
|
14
|
+
* copy; this module wires it into Svelte-flavoured attribute bags
|
|
15
|
+
* and hint descriptors.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AllConstraints } from "../core/renderer.ts";
|
|
19
|
+
import type { WalkedField } from "../core/types.ts";
|
|
20
|
+
import { hintIdFor } from "../core/idPath.ts";
|
|
21
|
+
import { constraintHint as coreConstraintHint } from "../core/constraintHint.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the ARIA attribute bundle for a Svelte renderer.
|
|
25
|
+
*
|
|
26
|
+
* - `aria-required="true"` whenever the field is non-optional.
|
|
27
|
+
* - `aria-describedby=<hint-id>` whenever a constraint hint applies.
|
|
28
|
+
* - `aria-label=<description>` when a non-empty description is
|
|
29
|
+
* supplied via the `description` argument.
|
|
30
|
+
*
|
|
31
|
+
* Returns a plain `Record<string, string>` so callers can spread the
|
|
32
|
+
* result into any HTML element type without per-element TypeScript
|
|
33
|
+
* widening. Svelte's `{...attrs}` spread accepts the record directly.
|
|
34
|
+
*/
|
|
35
|
+
export function buildAriaAttrs(
|
|
36
|
+
tree: WalkedField,
|
|
37
|
+
description?: unknown,
|
|
38
|
+
inputId?: string,
|
|
39
|
+
constraints?: AllConstraints
|
|
40
|
+
): Record<string, string> {
|
|
41
|
+
const attrs: Record<string, string> = {};
|
|
42
|
+
if (tree.isOptional === false) {
|
|
43
|
+
attrs["aria-required"] = "true";
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
inputId !== undefined &&
|
|
47
|
+
constraints !== undefined &&
|
|
48
|
+
coreConstraintHint(constraints) !== undefined
|
|
49
|
+
) {
|
|
50
|
+
attrs["aria-describedby"] = hintIdFor(inputId);
|
|
51
|
+
}
|
|
52
|
+
if (typeof description === "string" && description.length > 0) {
|
|
53
|
+
attrs["aria-label"] = description;
|
|
54
|
+
}
|
|
55
|
+
return attrs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Description for a constraint hint emitted alongside an input.
|
|
60
|
+
*
|
|
61
|
+
* Returned by {@link buildHintInfo} when the field carries one or
|
|
62
|
+
* more constraint keywords worth announcing (min / max length,
|
|
63
|
+
* pattern without format, item count, …). Theme adapters render this
|
|
64
|
+
* as a `<small>` wired to the input via `aria-describedby`.
|
|
65
|
+
*/
|
|
66
|
+
export interface HintInfo {
|
|
67
|
+
/** DOM id matching {@link hintIdFor}(inputId) on the host input. */
|
|
68
|
+
readonly id: string;
|
|
69
|
+
/** Human-readable hint text. */
|
|
70
|
+
readonly hint: string;
|
|
71
|
+
/** Same value as {@link id}, exposed under the React-aligned name. */
|
|
72
|
+
readonly ariaDescribedBy: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build {@link HintInfo} for a field at `inputId` given its declared
|
|
77
|
+
* constraints. Returns `undefined` when no constraint message would
|
|
78
|
+
* be produced — the Svelte renderers then skip emitting the hint
|
|
79
|
+
* element entirely so consumers don't see an empty `<small>`.
|
|
80
|
+
*/
|
|
81
|
+
export function buildHintInfo(
|
|
82
|
+
inputId: string,
|
|
83
|
+
constraints: AllConstraints
|
|
84
|
+
): HintInfo | undefined {
|
|
85
|
+
const hint = coreConstraintHint(constraints);
|
|
86
|
+
if (hint === undefined) return undefined;
|
|
87
|
+
const id = hintIdFor(inputId);
|
|
88
|
+
return { id, hint, ariaDescribedBy: id };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Narrow `meta.description` (typed `unknown`) to a string value safe
|
|
93
|
+
* to pass into an `aria-label` attribute. Returns `undefined` for
|
|
94
|
+
* non-string or empty-string descriptions so Svelte drops the
|
|
95
|
+
* attribute rather than stringifying e.g. `{}` to
|
|
96
|
+
* `"[object Object]"`.
|
|
97
|
+
*/
|
|
98
|
+
export function ariaLabel(description: unknown): string | undefined {
|
|
99
|
+
if (typeof description !== "string") return undefined;
|
|
100
|
+
if (description.length === 0) return undefined;
|
|
101
|
+
return description;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* True when the supplied field is non-optional and therefore
|
|
106
|
+
* deserves a visual required indicator alongside its label. Exposed
|
|
107
|
+
* as a predicate (rather than a built element) so theme adapters can
|
|
108
|
+
* choose any presentation.
|
|
109
|
+
*/
|
|
110
|
+
export function isFieldRequired(tree: WalkedField): boolean {
|
|
111
|
+
return tree.isOptional === false;
|
|
112
|
+
}
|