@valbuild/next 0.99.1 → 0.101.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 +232 -0
- package/client/dist/valbuild-next-client.cjs.dev.js +162 -2
- package/client/dist/valbuild-next-client.cjs.prod.js +162 -2
- package/client/dist/valbuild-next-client.esm.js +162 -2
- package/dist/declarations/src/client/initValClient.d.ts +15 -2
- package/dist/declarations/src/external_exempt_from_val_quickjs.d.ts +10 -0
- package/dist/declarations/src/rsc/initValRsc.d.ts +61 -2
- package/dist/{routeFromVal-8855a5cc.esm.js → routeFromVal-263eec96.esm.js} +41 -1
- package/dist/{routeFromVal-13c832b2.cjs.prod.js → routeFromVal-8ac2f984.cjs.prod.js} +43 -0
- package/dist/{routeFromVal-9b610e77.cjs.dev.js → routeFromVal-b48a454a.cjs.dev.js} +43 -0
- package/package.json +7 -7
- package/rsc/dist/valbuild-next-rsc.cjs.dev.js +398 -74
- package/rsc/dist/valbuild-next-rsc.cjs.prod.js +398 -74
- package/rsc/dist/valbuild-next-rsc.esm.js +398 -74
package/README.md
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
- [Image](#image)
|
|
56
56
|
- [keyOf](#keyof)
|
|
57
57
|
- [Route](#route)
|
|
58
|
+
- [Color](#color)
|
|
59
|
+
- [Date](#date)
|
|
60
|
+
- [DateTime](#datetime)
|
|
58
61
|
|
|
59
62
|
## Installation
|
|
60
63
|
|
|
@@ -886,6 +889,235 @@ export default c.define("/components/link.val.ts", linkSchema, {
|
|
|
886
889
|
});
|
|
887
890
|
```
|
|
888
891
|
|
|
892
|
+
## Color
|
|
893
|
+
|
|
894
|
+
The `color` schema represents a color, stored as a CSS color string. Because the value is a plain CSS string, it can be used directly in a `style` attribute or set as a CSS custom property - there is nothing to convert in your components.
|
|
895
|
+
|
|
896
|
+
### Color Schema
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
s.color(); // <- Schema<string>
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Output format
|
|
903
|
+
|
|
904
|
+
The `format` option decides which CSS notation the color is stored in. It defaults to `"hsl"`.
|
|
905
|
+
|
|
906
|
+
```ts
|
|
907
|
+
s.color(); // hsl(217.22 91.22% 59.8%)
|
|
908
|
+
s.color({ format: "hex" }); // #3b82f6
|
|
909
|
+
s.color({ format: "rgb" }); // rgb(59 130 246)
|
|
910
|
+
s.color({ format: "oklch" }); // oklch(0.6231 0.188 259.81)
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
The Val editor writes the value back in this format, so an editor who pastes `#3b82f6` into a field declared as `s.color()` gets `hsl(217.22 91.22% 59.8%)` stored.
|
|
914
|
+
|
|
915
|
+
### Transparency
|
|
916
|
+
|
|
917
|
+
Colors are fully opaque unless you opt into an alpha channel with `alpha: true`. A color with an alpha channel is a validation error in a field that does not allow it, so you can be sure an opaque color stays opaque.
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
s.color({ format: "hsl", alpha: true }); // hsl(217.22 91.22% 59.8% / 0.5)
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
With `alpha: true` the editor also gets an alpha slider next to the color picker.
|
|
924
|
+
|
|
925
|
+
### Validation
|
|
926
|
+
|
|
927
|
+
Validation is lenient about the syntax and strict about the format:
|
|
928
|
+
|
|
929
|
+
- both the modern and the legacy notation of the declared format are accepted, so `hsl(0 100% 50%)` and `hsl(0, 100%, 50%)` are both valid `hsl` values, and `#f00` is a valid `hex` value
|
|
930
|
+
- a color written in another format is an error which tells you the equivalent value in the right notation:
|
|
931
|
+
`Expected a color in the 'hsl' format (e.g. 'hsl(217.22 91.22% 59.8%)'), got '#ff0000'. Did you mean 'hsl(0 100% 50%)'?`
|
|
932
|
+
- named colors (`red`), `lab()`, `color()` and `color-mix()` are not supported
|
|
933
|
+
|
|
934
|
+
Colors can be nullable and can use `.describe()` and `.validate()` like any other schema:
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
s.color().nullable().describe("Optional highlight color");
|
|
938
|
+
|
|
939
|
+
s.color({ format: "hex" }).validate((color) => {
|
|
940
|
+
if (color === "#000000") {
|
|
941
|
+
return "Pure black is too harsh - pick a dark grey instead";
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
});
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Initializing color content
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
import { s, c, type t } from "../val.config";
|
|
951
|
+
|
|
952
|
+
export const schema = s.object({
|
|
953
|
+
brand: s.color().describe("Primary brand color"),
|
|
954
|
+
background: s.color({ format: "hex" }).describe("Page background"),
|
|
955
|
+
overlay: s.color({ format: "hsl", alpha: true }).describe("Overlay tint"),
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
export type Theme = t.inferSchema<typeof schema>;
|
|
959
|
+
export default c.define("/content/theme.val.ts", schema, {
|
|
960
|
+
brand: "hsl(217.22 91.22% 59.8%)",
|
|
961
|
+
background: "#0b1020",
|
|
962
|
+
overlay: "hsl(217.22 91.22% 59.8% / 0.15)",
|
|
963
|
+
});
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Using colors
|
|
967
|
+
|
|
968
|
+
The value is a string, so use it wherever CSS expects a color:
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
import { fetchVal } from "../val/rsc";
|
|
972
|
+
import themeVal from "../content/theme.val";
|
|
973
|
+
|
|
974
|
+
export default async function Hero() {
|
|
975
|
+
const theme = await fetchVal(themeVal);
|
|
976
|
+
return (
|
|
977
|
+
<section style={{ background: theme.background, color: theme.brand }}>
|
|
978
|
+
<h1 style={{ borderBottom: `2px solid ${theme.brand}` }}>Hello</h1>
|
|
979
|
+
</section>
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
To hand a color to a stylesheet instead, set it as a CSS custom property:
|
|
985
|
+
|
|
986
|
+
```tsx
|
|
987
|
+
<div style={{ "--brand": theme.brand } as React.CSSProperties}>
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**NOTE**: colors are not steganographically tagged, since the value ends up in CSS where the invisible characters would break the declaration. Colors therefore do not participate in click-then-edit visual editing (the same is true of dates) - edit them from the studio instead.
|
|
991
|
+
|
|
992
|
+
## Date
|
|
993
|
+
|
|
994
|
+
The `date` schema represents a calendar day, with no time and no timezone. It is stored as a `YYYY-MM-DD` string.
|
|
995
|
+
|
|
996
|
+
### Date Schema
|
|
997
|
+
|
|
998
|
+
```ts
|
|
999
|
+
s.date(); // <- Schema<string>
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Date bounds
|
|
1003
|
+
|
|
1004
|
+
Use `.from()` and `.to()` to constrain which days are valid. Both bounds are inclusive.
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
s.date().from("1900-01-01"); // this day or later
|
|
1008
|
+
s.date().to("2024-01-01"); // this day or earlier
|
|
1009
|
+
s.date().from("1900-01-01").to("2024-01-01"); // within the range
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
These are methods, not options - `s.date({ from: "1900-01-01" })` does not type check.
|
|
1013
|
+
|
|
1014
|
+
Bounds are compared as strings, which is exactly right for `YYYY-MM-DD` (it sorts chronologically) and wrong for anything else, so write bounds in that format.
|
|
1015
|
+
|
|
1016
|
+
**NOTE**: the schema checks the bounds, but not the shape of the string: a value like `"the 3rd of May"` is stored and validated without complaint. The editor always writes `YYYY-MM-DD`, so this only bites hand-written content. Use `.validate()` if you want it enforced:
|
|
1017
|
+
|
|
1018
|
+
```ts
|
|
1019
|
+
s.date().validate((day) =>
|
|
1020
|
+
/^\d{4}-\d{2}-\d{2}$/.test(day) ? false : "Must be a YYYY-MM-DD date",
|
|
1021
|
+
);
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Editing dates
|
|
1025
|
+
|
|
1026
|
+
Editors get a calendar. `from` and `to` limit which days can be picked, and a value already outside the bounds is shown clamped to the nearest one.
|
|
1027
|
+
|
|
1028
|
+
### Initializing date content
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
import { s, c } from "../val.config";
|
|
1032
|
+
|
|
1033
|
+
export const schema = s.object({
|
|
1034
|
+
birthdate: s
|
|
1035
|
+
.date()
|
|
1036
|
+
.from("1900-01-01")
|
|
1037
|
+
.to("2024-01-01")
|
|
1038
|
+
.nullable()
|
|
1039
|
+
.describe("Author's birthdate"),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
export default c.define("/content/author.val.ts", schema, {
|
|
1043
|
+
birthdate: "1981-12-30",
|
|
1044
|
+
});
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Using dates
|
|
1048
|
+
|
|
1049
|
+
The value is a plain string, so it can be compared and sorted as one:
|
|
1050
|
+
|
|
1051
|
+
```tsx
|
|
1052
|
+
const authors = [...allAuthors].sort((a, b) =>
|
|
1053
|
+
(a.birthdate ?? "").localeCompare(b.birthdate ?? ""),
|
|
1054
|
+
);
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
To format it, hand it to `Date` or a date library. Note that `new Date("2024-05-03")` parses as UTC midnight, so formatting it in a local timezone west of UTC shows the previous day - format from the parts, or use a library that treats the value as a plain day:
|
|
1058
|
+
|
|
1059
|
+
```tsx
|
|
1060
|
+
const [year, month, day] = author.birthdate.split("-").map(Number);
|
|
1061
|
+
const label = new Date(year, month - 1, day).toLocaleDateString();
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## DateTime
|
|
1065
|
+
|
|
1066
|
+
The `dateTime` schema represents an instant in time. It is stored as an ISO 8601 string in UTC, for example `2023-04-12T09:30:00.000Z`.
|
|
1067
|
+
|
|
1068
|
+
### DateTime Schema
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
s.datetime(); // <- Schema<string>
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
The factory is spelled `datetime`, all lowercase. The schema type it serializes to is `dateTime` - that name shows up in validation output and in the editor, but you never write it yourself.
|
|
1075
|
+
|
|
1076
|
+
### DateTime bounds
|
|
1077
|
+
|
|
1078
|
+
As with `date`, use the inclusive `.from()` and `.to()` methods. They accept any ISO 8601 datetime that `Date.parse` understands, and are compared as instants rather than as strings, so bounds and values may be written in different notations:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
s.datetime().from("2020-01-01T00:00:00Z");
|
|
1082
|
+
s.datetime().from("2020-01-01T00:00:00Z").to("2030-12-31T23:59:59Z");
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
Unlike `date`, the value itself is checked: a string that `Date.parse` cannot read is a validation error.
|
|
1086
|
+
|
|
1087
|
+
> Value 'yesterday' is not a valid ISO 8601 datetime
|
|
1088
|
+
|
|
1089
|
+
### Editing datetimes
|
|
1090
|
+
|
|
1091
|
+
The editor shows a calendar, a time input (down to seconds) and a timezone picker. The picker starts on the browser's timezone and remembers the last choice, so an editor in one place can enter a time as it will be experienced somewhere else. Whichever zone is chosen, the value is converted and stored as UTC - the timezone is a property of the editor, never of the content.
|
|
1092
|
+
|
|
1093
|
+
### Initializing datetime content
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
import { s, c } from "../val.config";
|
|
1097
|
+
|
|
1098
|
+
export const schema = s.object({
|
|
1099
|
+
joinedAt: s.datetime().nullable().describe("When the author joined"),
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
export default c.define("/content/author.val.ts", schema, {
|
|
1103
|
+
joinedAt: "2023-04-12T09:30:00.000Z",
|
|
1104
|
+
});
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Using datetimes
|
|
1108
|
+
|
|
1109
|
+
Since the value is an ISO 8601 UTC string, `Date` parses it directly:
|
|
1110
|
+
|
|
1111
|
+
```tsx
|
|
1112
|
+
<time dateTime={author.joinedAt}>
|
|
1113
|
+
{new Date(author.joinedAt).toLocaleString()}
|
|
1114
|
+
</time>
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
Rendering a UTC instant in the visitor's local timezone means server and client can format it differently. In Next.js that shows up as a hydration mismatch, so format inside a client component (or pass a fixed `timeZone` to `toLocaleString`) when the exact time matters.
|
|
1118
|
+
|
|
1119
|
+
**NOTE**: neither `date` nor `dateTime` values are steganographically tagged, so they do not participate in click-then-edit visual editing - edit them from the studio instead. Both support `.nullable()`, `.describe()`, `.validate()`, `.readonly()` and `.hidden()` like any other schema.
|
|
1120
|
+
|
|
889
1121
|
# Custom validation
|
|
890
1122
|
|
|
891
1123
|
All schema can use the `validate` method to show custom validation errors to editors.
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
require('client-only');
|
|
6
|
+
var routeFromVal = require('../../dist/routeFromVal-b48a454a.cjs.dev.js');
|
|
6
7
|
var core = require('@valbuild/core');
|
|
7
8
|
var stega = require('@valbuild/react/stega');
|
|
8
9
|
var React = require('react');
|
|
9
10
|
var ValOverlayContext = require('../../dist/ValOverlayContext-c6f27a6d.cjs.dev.js');
|
|
10
|
-
var routeFromVal = require('../../dist/routeFromVal-9b610e77.cjs.dev.js');
|
|
11
11
|
require('../../dist/createForOfIteratorHelper-0445603c.cjs.dev.js');
|
|
12
12
|
require('../../dist/unsupportedIterableToArray-c8ab77c9.cjs.dev.js');
|
|
13
13
|
require('../../dist/objectSpread2-58024783.cjs.dev.js');
|
|
@@ -58,6 +58,133 @@ function useValStega(selector) {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The module's source as the overlay currently sees it — i.e. WITH the editor's
|
|
64
|
+
* unpublished changes — or undefined when there is no draft view to be had
|
|
65
|
+
* (production, draft mode off, or the overlay has not pushed this module yet).
|
|
66
|
+
*
|
|
67
|
+
* This is what lets the single-entry readers show drafts. They otherwise resolve
|
|
68
|
+
* an entry through its local import thunk, which is the content that was bundled:
|
|
69
|
+
* correct in production, and stale the moment anyone edits in the Studio.
|
|
70
|
+
*/
|
|
71
|
+
function useDraftModuleSource(moduleFilePath) {
|
|
72
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
73
|
+
var store = valOverlayContext.store;
|
|
74
|
+
var moduleIds = React__default["default"].useMemo(function () {
|
|
75
|
+
return moduleFilePath ? [moduleFilePath] : [];
|
|
76
|
+
}, [moduleFilePath]);
|
|
77
|
+
var moduleMap = React__default["default"].useSyncExternalStore(store ? store.subscribe(moduleIds) : function () {
|
|
78
|
+
return function () {};
|
|
79
|
+
}, store ? store.getSnapshot(moduleIds) : function () {
|
|
80
|
+
return undefined;
|
|
81
|
+
}, store ? store.getServerSnapshot(moduleIds) : function () {
|
|
82
|
+
return undefined;
|
|
83
|
+
});
|
|
84
|
+
if (!valOverlayContext.draftMode || !moduleFilePath) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return moduleMap === null || moduleMap === void 0 ? void 0 : moduleMap[moduleFilePath];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What the draft view says about one `.jsonValues()` entry.
|
|
92
|
+
*
|
|
93
|
+
* Mirrors the server-side rule in `fetchValKey`/`fetchValRoute`: a draft view
|
|
94
|
+
* that HAS an answer wins, including the answer "this entry is gone", and the
|
|
95
|
+
* bundled content is used only when there is no draft view.
|
|
96
|
+
*/
|
|
97
|
+
function draftJsonEntry(draftSource, key) {
|
|
98
|
+
if (draftSource === undefined || draftSource === null || routeFromVal._typeof(draftSource) !== "object" || Array.isArray(draftSource)) {
|
|
99
|
+
return {
|
|
100
|
+
status: "unavailable"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
var entry = draftSource[key];
|
|
104
|
+
if (entry === undefined) {
|
|
105
|
+
// The module IS in the draft view and this key is not: it was deleted.
|
|
106
|
+
return {
|
|
107
|
+
status: "absent"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (core.Internal.isJson(entry)) {
|
|
111
|
+
// An un-loaded marker: the Studio has not fetched this entry's content, so
|
|
112
|
+
// the draft view cannot answer. (The engine asks for entries that pending
|
|
113
|
+
// patches touch, so this resolves itself for anything actually edited.)
|
|
114
|
+
return {
|
|
115
|
+
status: "unavailable"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
status: "content",
|
|
120
|
+
content: entry
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The (loosened) content type a single `.jsonValues()` entry resolves to.
|
|
125
|
+
|
|
126
|
+
// Module-level cache of in-flight/resolved entry loads, so `React.use` gets a
|
|
127
|
+
// stable promise across renders (keyed by module path + entry key).
|
|
128
|
+
var jsonEntryPromiseCache = new Map();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Client counterpart to `fetchValKey`: resolves a SINGLE `.jsonValues()` entry
|
|
132
|
+
* by key, loading only that entry's backing `*.val.json` (one dynamic import).
|
|
133
|
+
* Suspends (via `React.use`) until the entry loads, so it must be rendered
|
|
134
|
+
* inside a `<Suspense>` boundary.
|
|
135
|
+
*
|
|
136
|
+
* In draft mode it renders the editor's unpublished content, taken from the
|
|
137
|
+
* overlay; in production — and whenever there is no draft view — it resolves the
|
|
138
|
+
* entry's lazy import thunk from the local module.
|
|
139
|
+
*/
|
|
140
|
+
function useValKeyStega(selector, key) {
|
|
141
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
142
|
+
var moduleFilePath = selector && core.Internal.getValPath(selector);
|
|
143
|
+
var draftSource = useDraftModuleSource(moduleFilePath || undefined);
|
|
144
|
+
var draft = draftJsonEntry(draftSource, key);
|
|
145
|
+
if (draft.status === "absent") {
|
|
146
|
+
// Deleted in the draft state. Falling back to the bundled entry here would
|
|
147
|
+
// render content the editor has just removed.
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
var content = draft.status === "content" ? draft.content : undefined;
|
|
151
|
+
if (content === undefined) {
|
|
152
|
+
content = readCommittedJsonEntry(selector, key);
|
|
153
|
+
}
|
|
154
|
+
return stega.stegaEncode(content, {
|
|
155
|
+
disabled: !valOverlayContext.draftMode,
|
|
156
|
+
root: routeFromVal.getJsonEntryStegaRoot(selector, key)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The entry's content as bundled: its lazy import thunk, resolved through
|
|
162
|
+
* `React.use` so the caller suspends until it lands.
|
|
163
|
+
*
|
|
164
|
+
* Deliberately not named `use*`: it is called conditionally, which is fine
|
|
165
|
+
* because `React.use` is not a hook, but a `use*` name would read like one.
|
|
166
|
+
*/
|
|
167
|
+
function readCommittedJsonEntry(selector, key) {
|
|
168
|
+
var _Internal$getValPath;
|
|
169
|
+
var source = selector && core.Internal.getSource(selector);
|
|
170
|
+
var marker = source && routeFromVal._typeof(source) === "object" ? source[key] : undefined;
|
|
171
|
+
if (!core.Internal.isJson(marker)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
var thunk = core.Internal.getJsonImport(marker);
|
|
175
|
+
if (!thunk) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
var cacheKey = "".concat((_Internal$getValPath = core.Internal.getValPath(selector)) !== null && _Internal$getValPath !== void 0 ? _Internal$getValPath : "", " ").concat(key);
|
|
179
|
+
var promise = jsonEntryPromiseCache.get(cacheKey);
|
|
180
|
+
if (!promise) {
|
|
181
|
+
promise = thunk().then(function (mod) {
|
|
182
|
+
return mod["default"];
|
|
183
|
+
});
|
|
184
|
+
jsonEntryPromiseCache.set(cacheKey, promise);
|
|
185
|
+
}
|
|
186
|
+
return React__default["default"].use(promise);
|
|
187
|
+
}
|
|
61
188
|
function resolveParams(params) {
|
|
62
189
|
if (!params) {
|
|
63
190
|
return null;
|
|
@@ -76,13 +203,45 @@ function resolveParams(params) {
|
|
|
76
203
|
return params;
|
|
77
204
|
}
|
|
78
205
|
function useValRouteStega(selector, params) {
|
|
206
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
207
|
+
// Both called unconditionally to keep hook order stable. For a `.jsonValues()`
|
|
208
|
+
// router `val` is unused (we resolve a single entry below instead); for any
|
|
209
|
+
// other router `draftSource` is.
|
|
79
210
|
var val = useValStega(selector);
|
|
211
|
+
var draftSource = useDraftModuleSource(selector && core.Internal.getValPath(selector) || undefined);
|
|
80
212
|
var resolvedParams = resolveParams(params);
|
|
81
213
|
// Careful: null means there was an error - undefined means no params
|
|
82
214
|
if (resolvedParams === null) {
|
|
83
215
|
return null;
|
|
84
216
|
}
|
|
85
|
-
var
|
|
217
|
+
var path = selector && core.Internal.getValPath(selector);
|
|
218
|
+
var schema = selector && core.Internal.getSchema(selector);
|
|
219
|
+
// `.jsonValues()` router: map params → the entry key and load ONLY that
|
|
220
|
+
// entry's backing `*.val.json` (one dynamic import), like `useValKey`.
|
|
221
|
+
if (routeFromVal.isJsonValuesRecordSchema(schema)) {
|
|
222
|
+
var source = selector && core.Internal.getSource(selector);
|
|
223
|
+
var url = routeFromVal.getValRouteUrlFromVal(resolvedParams || {}, "useValRoute", path, schema, source);
|
|
224
|
+
if (!url) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
var draft = draftJsonEntry(draftSource, url);
|
|
228
|
+
if (draft.status === "absent") {
|
|
229
|
+
// The draft state says this route is gone — see useValKeyStega.
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
var content = draft.status === "content" ? draft.content : undefined;
|
|
233
|
+
if (content === undefined) {
|
|
234
|
+
content = readCommittedJsonEntry(selector, url);
|
|
235
|
+
}
|
|
236
|
+
if (content === undefined) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return stega.stegaEncode(content, {
|
|
240
|
+
disabled: !valOverlayContext.draftMode,
|
|
241
|
+
root: routeFromVal.getJsonEntryStegaRoot(selector, url)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
var route = routeFromVal.initValRouteFromVal(resolvedParams || {}, "useValRoute", path, schema, val);
|
|
86
245
|
return route;
|
|
87
246
|
}
|
|
88
247
|
function useValRouteUrl(selector, params) {
|
|
@@ -100,6 +259,7 @@ function useValRouteUrl(selector, params) {
|
|
|
100
259
|
function initValClient(config) {
|
|
101
260
|
return {
|
|
102
261
|
useValStega: useValStega,
|
|
262
|
+
useValKeyStega: useValKeyStega,
|
|
103
263
|
useValRouteStega: useValRouteStega,
|
|
104
264
|
useValRouteUrl: useValRouteUrl
|
|
105
265
|
};
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
require('client-only');
|
|
6
|
+
var routeFromVal = require('../../dist/routeFromVal-8ac2f984.cjs.prod.js');
|
|
6
7
|
var core = require('@valbuild/core');
|
|
7
8
|
var stega = require('@valbuild/react/stega');
|
|
8
9
|
var React = require('react');
|
|
9
10
|
var ValOverlayContext = require('../../dist/ValOverlayContext-f7f45bc7.cjs.prod.js');
|
|
10
|
-
var routeFromVal = require('../../dist/routeFromVal-13c832b2.cjs.prod.js');
|
|
11
11
|
require('../../dist/createForOfIteratorHelper-d4afcad8.cjs.prod.js');
|
|
12
12
|
require('../../dist/unsupportedIterableToArray-0d2087a2.cjs.prod.js');
|
|
13
13
|
require('../../dist/objectSpread2-13f847a9.cjs.prod.js');
|
|
@@ -58,6 +58,133 @@ function useValStega(selector) {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The module's source as the overlay currently sees it — i.e. WITH the editor's
|
|
64
|
+
* unpublished changes — or undefined when there is no draft view to be had
|
|
65
|
+
* (production, draft mode off, or the overlay has not pushed this module yet).
|
|
66
|
+
*
|
|
67
|
+
* This is what lets the single-entry readers show drafts. They otherwise resolve
|
|
68
|
+
* an entry through its local import thunk, which is the content that was bundled:
|
|
69
|
+
* correct in production, and stale the moment anyone edits in the Studio.
|
|
70
|
+
*/
|
|
71
|
+
function useDraftModuleSource(moduleFilePath) {
|
|
72
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
73
|
+
var store = valOverlayContext.store;
|
|
74
|
+
var moduleIds = React__default["default"].useMemo(function () {
|
|
75
|
+
return moduleFilePath ? [moduleFilePath] : [];
|
|
76
|
+
}, [moduleFilePath]);
|
|
77
|
+
var moduleMap = React__default["default"].useSyncExternalStore(store ? store.subscribe(moduleIds) : function () {
|
|
78
|
+
return function () {};
|
|
79
|
+
}, store ? store.getSnapshot(moduleIds) : function () {
|
|
80
|
+
return undefined;
|
|
81
|
+
}, store ? store.getServerSnapshot(moduleIds) : function () {
|
|
82
|
+
return undefined;
|
|
83
|
+
});
|
|
84
|
+
if (!valOverlayContext.draftMode || !moduleFilePath) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return moduleMap === null || moduleMap === void 0 ? void 0 : moduleMap[moduleFilePath];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What the draft view says about one `.jsonValues()` entry.
|
|
92
|
+
*
|
|
93
|
+
* Mirrors the server-side rule in `fetchValKey`/`fetchValRoute`: a draft view
|
|
94
|
+
* that HAS an answer wins, including the answer "this entry is gone", and the
|
|
95
|
+
* bundled content is used only when there is no draft view.
|
|
96
|
+
*/
|
|
97
|
+
function draftJsonEntry(draftSource, key) {
|
|
98
|
+
if (draftSource === undefined || draftSource === null || routeFromVal._typeof(draftSource) !== "object" || Array.isArray(draftSource)) {
|
|
99
|
+
return {
|
|
100
|
+
status: "unavailable"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
var entry = draftSource[key];
|
|
104
|
+
if (entry === undefined) {
|
|
105
|
+
// The module IS in the draft view and this key is not: it was deleted.
|
|
106
|
+
return {
|
|
107
|
+
status: "absent"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (core.Internal.isJson(entry)) {
|
|
111
|
+
// An un-loaded marker: the Studio has not fetched this entry's content, so
|
|
112
|
+
// the draft view cannot answer. (The engine asks for entries that pending
|
|
113
|
+
// patches touch, so this resolves itself for anything actually edited.)
|
|
114
|
+
return {
|
|
115
|
+
status: "unavailable"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
status: "content",
|
|
120
|
+
content: entry
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The (loosened) content type a single `.jsonValues()` entry resolves to.
|
|
125
|
+
|
|
126
|
+
// Module-level cache of in-flight/resolved entry loads, so `React.use` gets a
|
|
127
|
+
// stable promise across renders (keyed by module path + entry key).
|
|
128
|
+
var jsonEntryPromiseCache = new Map();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Client counterpart to `fetchValKey`: resolves a SINGLE `.jsonValues()` entry
|
|
132
|
+
* by key, loading only that entry's backing `*.val.json` (one dynamic import).
|
|
133
|
+
* Suspends (via `React.use`) until the entry loads, so it must be rendered
|
|
134
|
+
* inside a `<Suspense>` boundary.
|
|
135
|
+
*
|
|
136
|
+
* In draft mode it renders the editor's unpublished content, taken from the
|
|
137
|
+
* overlay; in production — and whenever there is no draft view — it resolves the
|
|
138
|
+
* entry's lazy import thunk from the local module.
|
|
139
|
+
*/
|
|
140
|
+
function useValKeyStega(selector, key) {
|
|
141
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
142
|
+
var moduleFilePath = selector && core.Internal.getValPath(selector);
|
|
143
|
+
var draftSource = useDraftModuleSource(moduleFilePath || undefined);
|
|
144
|
+
var draft = draftJsonEntry(draftSource, key);
|
|
145
|
+
if (draft.status === "absent") {
|
|
146
|
+
// Deleted in the draft state. Falling back to the bundled entry here would
|
|
147
|
+
// render content the editor has just removed.
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
var content = draft.status === "content" ? draft.content : undefined;
|
|
151
|
+
if (content === undefined) {
|
|
152
|
+
content = readCommittedJsonEntry(selector, key);
|
|
153
|
+
}
|
|
154
|
+
return stega.stegaEncode(content, {
|
|
155
|
+
disabled: !valOverlayContext.draftMode,
|
|
156
|
+
root: routeFromVal.getJsonEntryStegaRoot(selector, key)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The entry's content as bundled: its lazy import thunk, resolved through
|
|
162
|
+
* `React.use` so the caller suspends until it lands.
|
|
163
|
+
*
|
|
164
|
+
* Deliberately not named `use*`: it is called conditionally, which is fine
|
|
165
|
+
* because `React.use` is not a hook, but a `use*` name would read like one.
|
|
166
|
+
*/
|
|
167
|
+
function readCommittedJsonEntry(selector, key) {
|
|
168
|
+
var _Internal$getValPath;
|
|
169
|
+
var source = selector && core.Internal.getSource(selector);
|
|
170
|
+
var marker = source && routeFromVal._typeof(source) === "object" ? source[key] : undefined;
|
|
171
|
+
if (!core.Internal.isJson(marker)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
var thunk = core.Internal.getJsonImport(marker);
|
|
175
|
+
if (!thunk) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
var cacheKey = "".concat((_Internal$getValPath = core.Internal.getValPath(selector)) !== null && _Internal$getValPath !== void 0 ? _Internal$getValPath : "", " ").concat(key);
|
|
179
|
+
var promise = jsonEntryPromiseCache.get(cacheKey);
|
|
180
|
+
if (!promise) {
|
|
181
|
+
promise = thunk().then(function (mod) {
|
|
182
|
+
return mod["default"];
|
|
183
|
+
});
|
|
184
|
+
jsonEntryPromiseCache.set(cacheKey, promise);
|
|
185
|
+
}
|
|
186
|
+
return React__default["default"].use(promise);
|
|
187
|
+
}
|
|
61
188
|
function resolveParams(params) {
|
|
62
189
|
if (!params) {
|
|
63
190
|
return null;
|
|
@@ -76,13 +203,45 @@ function resolveParams(params) {
|
|
|
76
203
|
return params;
|
|
77
204
|
}
|
|
78
205
|
function useValRouteStega(selector, params) {
|
|
206
|
+
var valOverlayContext = ValOverlayContext.useValOverlayContext();
|
|
207
|
+
// Both called unconditionally to keep hook order stable. For a `.jsonValues()`
|
|
208
|
+
// router `val` is unused (we resolve a single entry below instead); for any
|
|
209
|
+
// other router `draftSource` is.
|
|
79
210
|
var val = useValStega(selector);
|
|
211
|
+
var draftSource = useDraftModuleSource(selector && core.Internal.getValPath(selector) || undefined);
|
|
80
212
|
var resolvedParams = resolveParams(params);
|
|
81
213
|
// Careful: null means there was an error - undefined means no params
|
|
82
214
|
if (resolvedParams === null) {
|
|
83
215
|
return null;
|
|
84
216
|
}
|
|
85
|
-
var
|
|
217
|
+
var path = selector && core.Internal.getValPath(selector);
|
|
218
|
+
var schema = selector && core.Internal.getSchema(selector);
|
|
219
|
+
// `.jsonValues()` router: map params → the entry key and load ONLY that
|
|
220
|
+
// entry's backing `*.val.json` (one dynamic import), like `useValKey`.
|
|
221
|
+
if (routeFromVal.isJsonValuesRecordSchema(schema)) {
|
|
222
|
+
var source = selector && core.Internal.getSource(selector);
|
|
223
|
+
var url = routeFromVal.getValRouteUrlFromVal(resolvedParams || {}, "useValRoute", path, schema, source);
|
|
224
|
+
if (!url) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
var draft = draftJsonEntry(draftSource, url);
|
|
228
|
+
if (draft.status === "absent") {
|
|
229
|
+
// The draft state says this route is gone — see useValKeyStega.
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
var content = draft.status === "content" ? draft.content : undefined;
|
|
233
|
+
if (content === undefined) {
|
|
234
|
+
content = readCommittedJsonEntry(selector, url);
|
|
235
|
+
}
|
|
236
|
+
if (content === undefined) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return stega.stegaEncode(content, {
|
|
240
|
+
disabled: !valOverlayContext.draftMode,
|
|
241
|
+
root: routeFromVal.getJsonEntryStegaRoot(selector, url)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
var route = routeFromVal.initValRouteFromVal(resolvedParams || {}, "useValRoute", path, schema, val);
|
|
86
245
|
return route;
|
|
87
246
|
}
|
|
88
247
|
function useValRouteUrl(selector, params) {
|
|
@@ -100,6 +259,7 @@ function useValRouteUrl(selector, params) {
|
|
|
100
259
|
function initValClient(config) {
|
|
101
260
|
return {
|
|
102
261
|
useValStega: useValStega,
|
|
262
|
+
useValKeyStega: useValKeyStega,
|
|
103
263
|
useValRouteStega: useValRouteStega,
|
|
104
264
|
useValRouteUrl: useValRouteUrl
|
|
105
265
|
};
|