@valbuild/core 0.121.0 → 0.125.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/CHANGELOG.md +110 -0
- package/dist/declarations/src/getSourcePathFromRoute.d.ts +12 -3
- package/dist/declarations/src/index.d.ts +13 -3
- package/dist/declarations/src/initVal.d.ts +2 -0
- package/dist/declarations/src/module.d.ts +18 -1
- package/dist/declarations/src/router.d.ts +45 -0
- package/dist/declarations/src/schema/files.d.ts +2 -5
- package/dist/declarations/src/schema/hasRemoteFileSchema.d.ts +21 -3
- package/dist/declarations/src/schema/images.d.ts +1 -6
- package/dist/declarations/src/schema/record.d.ts +79 -3
- package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
- package/dist/declarations/src/selector/index.d.ts +3 -2
- package/dist/declarations/src/source/external.d.ts +91 -0
- package/dist/declarations/src/source/externalRef.d.ts +46 -0
- package/dist/declarations/src/source/index.d.ts +3 -2
- package/dist/{index-5fe6fb98.esm.js → index-915dd675.esm.js} +644 -50
- package/dist/{index-61237991.cjs.prod.js → index-a3846321.cjs.prod.js} +649 -49
- package/dist/{index-9860179f.cjs.dev.js → index-daa122af.cjs.dev.js} +649 -49
- package/dist/valbuild-core.cjs.dev.js +7 -1
- package/dist/valbuild-core.cjs.prod.js +7 -1
- package/dist/valbuild-core.esm.js +1 -1
- package/package.json +1 -1
- package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
- package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
- package/patch/dist/valbuild-core-patch.esm.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,115 @@
|
|
|
1
1
|
# @valbuild/core
|
|
2
2
|
|
|
3
|
+
## 0.125.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#638](https://github.com/valbuild/val/pull/638) [`c390397`](https://github.com/valbuild/val/commit/c390397cb5e203eb1bedae3a6fab15726e850b90) Thanks [@freekh](https://github.com/freekh)! - Val now runs on TanStack Start.
|
|
8
|
+
|
|
9
|
+
`@valbuild/tanstack` is a new package with everything `@valbuild/next` has: Val
|
|
10
|
+
Studio at `/val`, the on-page overlay and canvas, draft mode, the client hooks,
|
|
11
|
+
server-side content reads, images, and the MCP tools.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @valbuild/tanstack
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// val.config.ts
|
|
19
|
+
import { initVal } from "@valbuild/tanstack";
|
|
20
|
+
const { s, c, config, tanstackRouter } = initVal({ project: "org/project" });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Routes are first class.** A Val module for a route is named after the route
|
|
24
|
+
file it sits beside — `src/routes/posts.$postId.tsx` is served content by
|
|
25
|
+
`src/routes/posts.$postId.val.ts` — and its keys are the URLs that route serves:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export default c.define(
|
|
29
|
+
"/src/routes/posts.$postId.val.ts",
|
|
30
|
+
s.router(tanstackRouter, s.object({ title: s.string() })),
|
|
31
|
+
{ "/posts/hello-world": { title: "Hello world" } },
|
|
32
|
+
);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`.` and `/` both separate segments, `$param` is a parameter, `$` is a splat, and
|
|
36
|
+
`index`, `route`, `(groups)` and `_pathless` layouts add no URL segment — the
|
|
37
|
+
same rules TanStack Router uses for the route file itself. Val validates every
|
|
38
|
+
key against that pattern, and Val Studio shows these modules as a sitemap under
|
|
39
|
+
**Pages**, where an editor can add a page.
|
|
40
|
+
|
|
41
|
+
Point the route generator away from your content files, in `vite.config.ts` and
|
|
42
|
+
`tsr.config.json`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
tanstackStart({ router: { routeFileIgnorePattern: "\\.val\\.[tj]sx?$" } });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See the [README](https://github.com/valbuild/val/blob/main/packages/tanstack/README.md)
|
|
49
|
+
for the full wiring, and `examples/tanstack` for a working app.
|
|
50
|
+
|
|
51
|
+
**Also fixed, for everyone:** `Internal.VERSION.core` was `null` in any ESM
|
|
52
|
+
bundle — it was read with `require("../package.json")` inside a `try`, so the
|
|
53
|
+
failure was silent. That version goes into every remote file ref and proxy mode
|
|
54
|
+
refuses to start without it. Every package now reads its version through a
|
|
55
|
+
static JSON import, which is inlined at build time.
|
|
56
|
+
|
|
57
|
+
Two smaller fixes that came out of running the whole toolchain against a
|
|
58
|
+
TanStack project:
|
|
59
|
+
|
|
60
|
+
- `val versions` reports `@valbuild/tanstack`, and `val debug` writes a snapshot
|
|
61
|
+
that names whichever framework package the captured project actually has.
|
|
62
|
+
- `@valbuild/eslint-plugin` reads a `tsconfig.json` that has comments in it. A
|
|
63
|
+
tsconfig is JSONC, and `JSON.parse` is not — so on any project whose tsconfig
|
|
64
|
+
carries a comment (the TanStack starter's does) the
|
|
65
|
+
`module-in-val-modules` rule threw `Expected double-quoted property name in
|
|
66
|
+
JSON` on the first file and took the whole lint run with it.
|
|
67
|
+
|
|
68
|
+
Three fixes found by driving the Studio against a real TanStack app:
|
|
69
|
+
|
|
70
|
+
- **A draft image now loads on the page.** The source the Studio pushes to the
|
|
71
|
+
host page carries `patch_id` for any file whose bytes are still in a patch, so
|
|
72
|
+
the page asks `/api/val/files/...?patch_id=` rather than a `/public` path that
|
|
73
|
+
nothing has written yet. This was silent — the image just did not appear — and
|
|
74
|
+
it affects any page that reads media through the hooks rather than on the
|
|
75
|
+
server, `@valbuild/next` included.
|
|
76
|
+
- **A splat route no longer logs an error on every render.** TanStack returns
|
|
77
|
+
both `_splat` and `*` for the same parameter; Val reported the second as a
|
|
78
|
+
parameter it could not place in the path.
|
|
79
|
+
- The TanStack example and starter render a 404 page instead of throwing
|
|
80
|
+
`notFound()` from a component, which escaped to the error boundary and logged
|
|
81
|
+
`Error in renderToReadableStream` (and, with no not-found component
|
|
82
|
+
configured, aborted the response) on every miss.
|
|
83
|
+
|
|
84
|
+
## 0.124.0
|
|
85
|
+
|
|
86
|
+
### Minor Changes
|
|
87
|
+
|
|
88
|
+
- [#622](https://github.com/valbuild/val/pull/622) [`aa89fc2`](https://github.com/valbuild/val/commit/aa89fc26de7028b3c5a1666b99afc03b39e92769) Thanks [@freekh](https://github.com/freekh)! - **Breaking:** `s.images()` and `s.files()` no longer take a `remote` option. Use
|
|
89
|
+
`.remote()`, the same way `s.image()` and `s.file()` already did.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// before
|
|
93
|
+
s.images({ directory: "/public/val/images", remote: true });
|
|
94
|
+
s.files({
|
|
95
|
+
directory: "/public/val/docs",
|
|
96
|
+
accept: "application/pdf",
|
|
97
|
+
remote: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// after
|
|
101
|
+
s.images({ directory: "/public/val/images" }).remote();
|
|
102
|
+
s.files({
|
|
103
|
+
directory: "/public/val/docs",
|
|
104
|
+
accept: "application/pdf",
|
|
105
|
+
}).remote();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Behaviour is unchanged — remote is still off unless asked for. This only makes
|
|
109
|
+
remote one thing spelled one way across all four media schemas, instead of an
|
|
110
|
+
option on the two collections and a method on the two fields. `remote: false`
|
|
111
|
+
has no replacement because it was the default; drop it.
|
|
112
|
+
|
|
3
113
|
## 0.121.0
|
|
4
114
|
|
|
5
115
|
### Minor Changes
|
|
@@ -2,14 +2,23 @@ import type { ModuleFilePath, SourcePath } from "./val/index.js";
|
|
|
2
2
|
import type { SerializedSchema } from "./schema/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Given a URL pathname (e.g. "/blogs/blog-1") and all serialized module schemas,
|
|
5
|
-
* finds the
|
|
6
|
-
* and the source path for that route's content.
|
|
5
|
+
* finds the page-router module that serves that route and returns the module
|
|
6
|
+
* file path and the source path for that route's content.
|
|
7
7
|
*
|
|
8
8
|
* External routers (e.g. external-url-router) are intentionally skipped.
|
|
9
|
-
* Returns null if no
|
|
9
|
+
* Returns null if no page-router module matches the pathname.
|
|
10
10
|
*/
|
|
11
11
|
export declare function getSourcePathFromRoute(pathname: string, schemas: Record<ModuleFilePath, SerializedSchema>): {
|
|
12
12
|
moduleFilePath: ModuleFilePath;
|
|
13
13
|
sourcePath: SourcePath;
|
|
14
14
|
route: string;
|
|
15
15
|
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a router id is one whose keys are routes of this site.
|
|
18
|
+
*
|
|
19
|
+
* The same question `getSourcePathFromRoute` asks, exported because the Studio
|
|
20
|
+
* asks it too — a page router gets the sitemap and the "Pages" menu, an
|
|
21
|
+
* external one gets a flat list of URLs — and the two must not disagree about
|
|
22
|
+
* which is which.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isPageRouter(routerId: string): boolean;
|
|
@@ -3,13 +3,14 @@ export { modules, type ValModules } from "./modules.js";
|
|
|
3
3
|
export { extractValModules, computeValModuleShas, type ExtractedValModules, type ValModuleShaEntry, type ValModuleShas, type ExtractedModuleError, } from "./extractValModules.js";
|
|
4
4
|
export type { InitVal, ValConfig, ValConstructor, ContentConstructor, } from "./initVal.js";
|
|
5
5
|
export { Schema, type SerializedSchema, type SelectorOfSchema } from "./schema/index.js";
|
|
6
|
-
export { hasRemoteFileSchema } from "./schema/hasRemoteFileSchema.js";
|
|
6
|
+
export { hasRemoteFileSchema, hasMediaSchema, } from "./schema/hasRemoteFileSchema.js";
|
|
7
7
|
export type { ImageMetadata, ImageEncodeOption, ImageEncodeOptions, } from "./schema/image.js";
|
|
8
8
|
export type { FileMetadata } from "./schema/file.js";
|
|
9
9
|
export type { ValModule, SerializedModule, InferValModuleType } from "./module.js";
|
|
10
10
|
export type { SourceObject, SourcePrimitive, Source } from "./source/index.js";
|
|
11
11
|
export type { FileSource } from "./source/media.js";
|
|
12
12
|
export type { JsonSource, JsonOf, JsonImportThunk } from "./source/json.js";
|
|
13
|
+
export type { ExternalRecordSrc, ExternalRecordWritableSrc, ExternalItemOf, ExternalLabelOf, ExternalReadonlyOf, } from "./source/external.js";
|
|
13
14
|
export type { RemoteRef } from "./source/remote.js";
|
|
14
15
|
export { DEFAULT_VAL_REMOTE_HOST } from "./schema/remote.js";
|
|
15
16
|
export type { RawString } from "./schema/string.js";
|
|
@@ -35,6 +36,8 @@ import { getMimeType, mimeTypeToFileExt, filenameToMimeType, mimeTypeMatchesAcce
|
|
|
35
36
|
import { type ImageMetadata } from "./schema/image.js";
|
|
36
37
|
import { type FileMetadata } from "./schema/file.js";
|
|
37
38
|
import { isJson, getJsonImport, resolveJsonValues } from "./source/json.js";
|
|
39
|
+
import { isExternal, hasInlineExternalEntries } from "./source/external.js";
|
|
40
|
+
import { createExternalFileRef, splitExternalFileRef, isExternalFileRef } from "./source/externalRef.js";
|
|
38
41
|
import { createRemoteRef } from "./source/remote.js";
|
|
39
42
|
import { getValidationBasis, getValidationHash } from "./remote/validationBasis.js";
|
|
40
43
|
import { getFileHash, hashToRemoteFileHash } from "./remote/fileHash.js";
|
|
@@ -66,14 +69,15 @@ export { deserializeSchema } from "./schema/deserialize.js";
|
|
|
66
69
|
export { type PreviewItem, type ItemPreviewInput, type RecordPreview, type ArrayPreview, type ReifiedPreview, type PreviewScope, previewScope, } from "./preview.js";
|
|
67
70
|
export { type InlineRender, type FieldRender, isInlineRender } from "./render.js";
|
|
68
71
|
export type { ValRouter, RouteValidationError } from "./router.js";
|
|
69
|
-
export {
|
|
72
|
+
export { parseNextJsRoutePattern, parseTanStackRoutePattern, tanStackSegmentsOfRoutePath, validateUrlAgainstPattern, } from "./router.js";
|
|
73
|
+
export { getSourcePathFromRoute, isPageRouter } from "./getSourcePathFromRoute.js";
|
|
70
74
|
export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module", "invalid-patch"];
|
|
71
75
|
export type FatalErrorType = (typeof FATAL_ERROR_TYPES)[number];
|
|
72
76
|
export declare const DEFAULT_CONTENT_HOST = "https://content.val.build";
|
|
73
77
|
export declare const DEFAULT_APP_HOST = "https://admin.val.build";
|
|
74
78
|
declare const Internal: {
|
|
75
79
|
VERSION: {
|
|
76
|
-
core: string
|
|
80
|
+
core: string;
|
|
77
81
|
};
|
|
78
82
|
mediaUrl: typeof mediaUrl;
|
|
79
83
|
resolveMedia: typeof resolveMedia;
|
|
@@ -90,6 +94,7 @@ declare const Internal: {
|
|
|
90
94
|
splitModuleFilePathAndModulePath: typeof splitModuleFilePathAndModulePath;
|
|
91
95
|
joinModuleFilePathAndModulePath: typeof joinModuleFilePathAndModulePath;
|
|
92
96
|
nextAppRouter: import("./router.js").ValRouter;
|
|
97
|
+
tanstackRouter: import("./router.js").ValRouter;
|
|
93
98
|
externalPageRouter: import("./router.js").ValRouter;
|
|
94
99
|
color: {
|
|
95
100
|
parseColor: typeof parseColor;
|
|
@@ -111,6 +116,11 @@ declare const Internal: {
|
|
|
111
116
|
isJson: typeof isJson;
|
|
112
117
|
getJsonImport: typeof getJsonImport;
|
|
113
118
|
resolveJsonValues: typeof resolveJsonValues;
|
|
119
|
+
isExternal: typeof isExternal;
|
|
120
|
+
hasInlineExternalEntries: typeof hasInlineExternalEntries;
|
|
121
|
+
createExternalFileRef: typeof createExternalFileRef;
|
|
122
|
+
splitExternalFileRef: typeof splitExternalFileRef;
|
|
123
|
+
isExternalFileRef: typeof isExternalFileRef;
|
|
114
124
|
createValPathOfItem: typeof createValPathOfItem;
|
|
115
125
|
getSHA256Hash: (bits: Uint8Array) => string;
|
|
116
126
|
initSchema: typeof initSchema;
|
|
@@ -2,9 +2,11 @@ import { define } from "./module.js";
|
|
|
2
2
|
import { InitSchema } from "./initSchema.js";
|
|
3
3
|
import { getValPath as getPath } from "./val/index.js";
|
|
4
4
|
import { json } from "./source/json.js";
|
|
5
|
+
import { external } from "./source/external.js";
|
|
5
6
|
export type ContentConstructor = {
|
|
6
7
|
define: typeof define;
|
|
7
8
|
json: typeof json;
|
|
9
|
+
external: typeof external;
|
|
8
10
|
};
|
|
9
11
|
export type ValConstructor = {
|
|
10
12
|
unstable_getPath: typeof getPath;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema, SelectorOfSchema, SerializedSchema } from "./schema/index.js";
|
|
2
2
|
import { GenericSelector, SelectorOf, SelectorSource } from "./selector/index.js";
|
|
3
3
|
import { Source } from "./source/index.js";
|
|
4
|
+
import { ExternalRecordSrc } from "./source/external.js";
|
|
4
5
|
import { ModuleFilePath, ModulePath, SourcePath } from "./val/index.js";
|
|
5
6
|
import { Json } from "./Json.js";
|
|
6
7
|
import { RawString } from "./schema/string.js";
|
|
@@ -15,8 +16,24 @@ export type ReplaceRawStringWithString<T extends SelectorSource> = SelectorSourc
|
|
|
15
16
|
} ? {
|
|
16
17
|
[key in keyof T]: ReplaceRawStringWithString<T[key]>;
|
|
17
18
|
} : T extends SelectorSource[] ? ReplaceRawStringWithString<T[number]>[] : T;
|
|
19
|
+
/**
|
|
20
|
+
* What may be WRITTEN as an external record's source, on top of the marker.
|
|
21
|
+
*
|
|
22
|
+
* Entries inline in the `.val.ts` are accepted on purpose — the same call
|
|
23
|
+
* `.jsonValues()` makes for the same reason: pasting content straight into the
|
|
24
|
+
* module is the natural first thing to write, and a type error there is a dead
|
|
25
|
+
* end, because the author cannot see what to write instead. Validation reports
|
|
26
|
+
* them as an `external:upload` fix, and they are readable at runtime meanwhile.
|
|
27
|
+
*
|
|
28
|
+
* Widened HERE rather than in the schema's own source type so that the MODULE
|
|
29
|
+
* stays typed on the marker alone. An adapter is bound to a module, and a module
|
|
30
|
+
* whose type were "marker or plain record" would be indistinguishable from an
|
|
31
|
+
* ordinary record — which would let `entry()` accept a module that is not
|
|
32
|
+
* external at all.
|
|
33
|
+
*/
|
|
34
|
+
type InlineEntriesFor<S> = S extends ExternalRecordSrc<infer Item, string, boolean> ? Record<string, Item> : never;
|
|
18
35
|
export declare function define<T extends Schema<SelectorSource>>(id: string, // TODO: `/${string}`
|
|
19
|
-
schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T>>;
|
|
36
|
+
schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>> | InlineEntriesFor<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T>>;
|
|
20
37
|
export declare function getSource<T extends Source>(valModule: ValModule<T>): T;
|
|
21
38
|
/**
|
|
22
39
|
* Whether `value` is a Val module, i.e. something {@link define} produced.
|
|
@@ -23,6 +23,51 @@ export declare const nextAppRouter: ValRouter;
|
|
|
23
23
|
* - /app/(..)(dashboard)/feed/page.val.ts -> ["feed"] (interception route)
|
|
24
24
|
*/
|
|
25
25
|
export declare function parseNextJsRoutePattern(moduleFilePath: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Parse a TanStack Router route pattern out of a Val module file path.
|
|
28
|
+
*
|
|
29
|
+
* TanStack Router's file conventions are not Next's, so this is a different
|
|
30
|
+
* parser rather than a flag on the one above. A Val module for a route lives
|
|
31
|
+
* beside the route file and is named the same way — `routes/posts.$postId.tsx`
|
|
32
|
+
* is served content by `routes/posts.$postId.val.ts` — so the pattern is the
|
|
33
|
+
* file path with the conventions applied:
|
|
34
|
+
*
|
|
35
|
+
* - `.` and `/` both separate segments (`posts.$postId.val.ts` and
|
|
36
|
+
* `posts/$postId.val.ts` are the same route)
|
|
37
|
+
* - `$param` is a dynamic segment, `$` on its own is a splat
|
|
38
|
+
* - `index` is the directory's own route and contributes no segment
|
|
39
|
+
* - `route` is a layout for the directory, likewise
|
|
40
|
+
* - `(group)` folders and `_pathless` layout segments are not in the URL
|
|
41
|
+
* - a trailing `_` on a segment opts out of nesting and is not in the URL
|
|
42
|
+
*
|
|
43
|
+
* The result is expressed in the SAME vocabulary the Next parser uses
|
|
44
|
+
* (`[param]`, `[...param]`), because everything downstream — validation, the
|
|
45
|
+
* sitemap, the key inputs in the Studio — is written against that vocabulary
|
|
46
|
+
* and there is no reason for two.
|
|
47
|
+
*
|
|
48
|
+
* - /src/routes/posts.$postId.val.ts -> ["posts", "[postId]"]
|
|
49
|
+
* - /routes/posts/$postId.val.ts -> ["posts", "[postId]"]
|
|
50
|
+
* - /src/routes/index.val.ts -> []
|
|
51
|
+
* - /src/routes/files.$.val.ts -> ["files", "[..._splat]"]
|
|
52
|
+
* - /src/routes/(app)/_layout.about.val.ts -> ["about"]
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseTanStackRoutePattern(moduleFilePath: string): string[];
|
|
55
|
+
/**
|
|
56
|
+
* The URL segments of a TanStack route path, given relative to `routes/`.
|
|
57
|
+
*
|
|
58
|
+
* Exported because the Studio derives the same pattern from the same file path
|
|
59
|
+
* and must not disagree with validation about what a route is.
|
|
60
|
+
*/
|
|
61
|
+
export declare function tanStackSegmentsOfRoutePath(routePath: string): string[];
|
|
62
|
+
/**
|
|
63
|
+
* The router for a TanStack Router (and TanStack Start) app.
|
|
64
|
+
*
|
|
65
|
+
* Like `nextAppRouter` this does not belong in core — it is here for the same
|
|
66
|
+
* reason that one is: the serialized schema carries only a router id, so the
|
|
67
|
+
* thing that turns an id back into a validator has to be somewhere every
|
|
68
|
+
* consumer can reach.
|
|
69
|
+
*/
|
|
70
|
+
export declare const tanstackRouter: ValRouter;
|
|
26
71
|
export interface ValRouter {
|
|
27
72
|
getRouterId(): string;
|
|
28
73
|
validate(moduleFilePath: ModuleFilePath, urlPaths: string[]): RouteValidationError[];
|
|
@@ -21,11 +21,6 @@ export type FilesOptions = {
|
|
|
21
21
|
* a directory with every other one.
|
|
22
22
|
*/
|
|
23
23
|
directory: "/public" | `/public/${string}`;
|
|
24
|
-
/**
|
|
25
|
-
* Whether remote files are allowed
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
remote?: boolean;
|
|
29
24
|
};
|
|
30
25
|
/**
|
|
31
26
|
* Metadata for a file entry in the files record
|
|
@@ -43,6 +38,8 @@ type FilesItemSrc = {
|
|
|
43
38
|
/**
|
|
44
39
|
* Define a collection of files.
|
|
45
40
|
*
|
|
41
|
+
* Remote is off by default: call `.remote()` on the result to allow remote files.
|
|
42
|
+
*
|
|
46
43
|
* @example
|
|
47
44
|
* ```typescript
|
|
48
45
|
* const schema = s.files({
|
|
@@ -5,9 +5,10 @@ import type { SerializedSchema } from "./index.js";
|
|
|
5
5
|
* The one answer to that question. There used to be two — `hasRemoteFileSchema`
|
|
6
6
|
* in the server, gating whether `/save` demands remote credentials, and
|
|
7
7
|
* `findRequiredRemoteFiles` in the Studio, gating the `/remote/settings` fetch —
|
|
8
|
-
* and they disagreed about `s.images({
|
|
9
|
-
* remote media record, the server's did not, because a media
|
|
10
|
-
* serializes as a `record` of metadata rather than as an image
|
|
8
|
+
* and they disagreed about `s.images({ ... }).remote()`: the Studio's version
|
|
9
|
+
* counted a remote media record, the server's did not, because a media
|
|
10
|
+
* collection serializes as a `record` of metadata rather than as an image
|
|
11
|
+
* schema.
|
|
11
12
|
*
|
|
12
13
|
* The server's answer was the wrong one, and silently so. With it false,
|
|
13
14
|
* `/save` runs `saveOrUploadFiles` in `skip-remote` mode, and that mode does not
|
|
@@ -32,3 +33,20 @@ import type { SerializedSchema } from "./index.js";
|
|
|
32
33
|
* instead would reintroduce exactly the silent skip described above.
|
|
33
34
|
*/
|
|
34
35
|
export declare function hasRemoteFileSchema(schema: SerializedSchema): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Does this schema hold any media at all — remote or not?
|
|
38
|
+
*
|
|
39
|
+
* The sibling of {@link hasRemoteFileSchema}, and the same walk minus the
|
|
40
|
+
* `remote` predicate. It answers the question an external record has to ask
|
|
41
|
+
* before it will accept a binding: an item schema containing media needs a
|
|
42
|
+
* `putFile`/`getFile` pair, and without one that is a schema error.
|
|
43
|
+
*
|
|
44
|
+
* It is a walk over the SERIALIZED schema for the reasons given above, and for
|
|
45
|
+
* one more that is specific to this question: the two cases most likely to be
|
|
46
|
+
* missed are invisible to a value type. A media collection (`s.images()` /
|
|
47
|
+
* `s.files()`) serializes as a record of metadata with no image schema inside
|
|
48
|
+
* it, and a richtext inline image lives in a constructor argument. A check
|
|
49
|
+
* written against the item TYPE would silently pass both — and the gallery case
|
|
50
|
+
* already has a data-loss incident attached to it (see above).
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasMediaSchema(schema: SerializedSchema): boolean;
|
|
@@ -35,11 +35,6 @@ export type ImagesOptions<Accept extends `image/${string}`> = {
|
|
|
35
35
|
* - s.record(s.string(), s.string()) for locale-based alt text
|
|
36
36
|
*/
|
|
37
37
|
alt?: AltSchema;
|
|
38
|
-
/**
|
|
39
|
-
* Whether remote images are allowed
|
|
40
|
-
* @default false
|
|
41
|
-
*/
|
|
42
|
-
remote?: boolean;
|
|
43
38
|
/**
|
|
44
39
|
* Re-encode uploads in the browser before they are uploaded.
|
|
45
40
|
*
|
|
@@ -79,7 +74,7 @@ type ImagesItemSrc = {
|
|
|
79
74
|
*
|
|
80
75
|
* `directory` is required — it decides where uploads land, so it is not something
|
|
81
76
|
* to be inferred. The rest defaults: any image type (`"image/*"`), nullable alt
|
|
82
|
-
* text, remote disabled.
|
|
77
|
+
* text, remote disabled. Call `.remote()` on the result to allow remote images.
|
|
83
78
|
*
|
|
84
79
|
* @example
|
|
85
80
|
* ```typescript
|
|
@@ -4,6 +4,7 @@ import { FieldRender } from "../render.js";
|
|
|
4
4
|
import { ValRouter } from "../router.js";
|
|
5
5
|
import { SelectorSource } from "../selector/index.js";
|
|
6
6
|
import { JsonOf, JsonSource } from "../source/json.js";
|
|
7
|
+
import { ExternalRecordSrc } from "../source/external.js";
|
|
7
8
|
import { ModuleFilePath, SourcePath } from "../val/index.js";
|
|
8
9
|
import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
|
|
9
10
|
import type { ImageEncodeOption } from "./image.js";
|
|
@@ -38,6 +39,12 @@ export type SerializedRecordSchema = {
|
|
|
38
39
|
encode?: ImageEncodeOption;
|
|
39
40
|
alt?: SerializedSchema;
|
|
40
41
|
jsonValues?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The label from `.external(label)`. Its presence is what marks the record as
|
|
44
|
+
* externally stored; the value names the binding a reader — or a model — can
|
|
45
|
+
* grep for. See `source/external.ts`.
|
|
46
|
+
*/
|
|
47
|
+
external?: string;
|
|
41
48
|
readonly?: boolean;
|
|
42
49
|
hidden?: boolean;
|
|
43
50
|
description?: string;
|
|
@@ -56,7 +63,13 @@ export type SerializedRecordSchema = {
|
|
|
56
63
|
* authoring.
|
|
57
64
|
*/
|
|
58
65
|
export type JsonValuesRecordSrc<T extends Schema<SelectorSource>, K extends Schema<string>> = Record<SelectorOfSchema<K>, JsonSource<JsonOf<SelectorOfSchema<T>>> | SelectorOfSchema<T>>;
|
|
59
|
-
export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Schema<string>, Src extends Record<SelectorOfSchema<K>, SelectorOfSchema<T>> | JsonValuesRecordSrc<T, K> |
|
|
66
|
+
export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Schema<string>, Src extends Record<SelectorOfSchema<K>, SelectorOfSchema<T>> | JsonValuesRecordSrc<T, K> | ExternalRecordSrc | null,
|
|
67
|
+
/**
|
|
68
|
+
* Whether `.readonly()` was called. It rides in the type — not only in
|
|
69
|
+
* `isReadonly` — because `.external()` freezes it into the source marker, and
|
|
70
|
+
* the adapter's write methods are required or forbidden on the strength of it.
|
|
71
|
+
*/
|
|
72
|
+
RO extends boolean = false> extends Schema<Src> {
|
|
60
73
|
private readonly item;
|
|
61
74
|
private readonly opt;
|
|
62
75
|
private readonly customValidateFunctions;
|
|
@@ -70,9 +83,31 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
|
|
|
70
83
|
private readonly isJsonValues;
|
|
71
84
|
private readonly previewInput;
|
|
72
85
|
private readonly renderInput;
|
|
86
|
+
/**
|
|
87
|
+
* Set by `.external(label)`; the entries live behind that adapter.
|
|
88
|
+
*
|
|
89
|
+
* LAST on purpose: every other `new RecordSchema(...,
|
|
90
|
+
this.externalLabel,
|
|
91
|
+
)` in the codebase
|
|
92
|
+
* passes its arguments positionally, so a new parameter anywhere else
|
|
93
|
+
* silently shifts `previewInput` and `renderInput` at each of those call
|
|
94
|
+
* sites.
|
|
95
|
+
*/
|
|
96
|
+
private readonly externalLabel;
|
|
73
97
|
constructor(item: T, opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], currentRouter?: ValRouter | null, keySchema?: Schema<string> | null, mediaOptions?: MediaOptions | undefined, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined,
|
|
74
98
|
/** When true, entry values are lazily loaded {@link JsonSource} thunks. */
|
|
75
|
-
isJsonValues?: boolean, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null
|
|
99
|
+
isJsonValues?: boolean, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null,
|
|
100
|
+
/**
|
|
101
|
+
* Set by `.external(label)`; the entries live behind that adapter.
|
|
102
|
+
*
|
|
103
|
+
* LAST on purpose: every other `new RecordSchema(...,
|
|
104
|
+
this.externalLabel,
|
|
105
|
+
)` in the codebase
|
|
106
|
+
* passes its arguments positionally, so a new parameter anywhere else
|
|
107
|
+
* silently shifts `previewInput` and `renderInput` at each of those call
|
|
108
|
+
* sites.
|
|
109
|
+
*/
|
|
110
|
+
externalLabel?: string | null);
|
|
76
111
|
describe(description: string | null): RecordSchema<T, K, Src>;
|
|
77
112
|
validate(validationFunction: (src: Src) => false | string): RecordSchema<T, K, Src>;
|
|
78
113
|
protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
|
|
@@ -84,7 +119,17 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
|
|
|
84
119
|
private validateMediaMimeType;
|
|
85
120
|
protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
|
|
86
121
|
nullable(): RecordSchema<T, K, Src | null>;
|
|
87
|
-
|
|
122
|
+
/**
|
|
123
|
+
* A record you can look at but not change.
|
|
124
|
+
*
|
|
125
|
+
* The flag rides in the TYPE as well as in the instance, so `.external()` can
|
|
126
|
+
* freeze it into the source marker and the adapter's write methods become
|
|
127
|
+
* required or forbidden on the strength of it. That is also why `.readonly()`
|
|
128
|
+
* must come BEFORE `.external()`: afterwards the value is a `Schema`, and
|
|
129
|
+
* there is no `.readonly()` left to call — which is the error the compiler
|
|
130
|
+
* gives, and a clearer one than a runtime throw.
|
|
131
|
+
*/
|
|
132
|
+
readonly<B extends boolean = true>(isReadonly?: B): RecordSchema<T, K, Src, B>;
|
|
88
133
|
hidden(isHidden?: boolean): RecordSchema<T, K, Src>;
|
|
89
134
|
router(router: ValRouter): RecordSchema<T, K, Src>;
|
|
90
135
|
remote(): RecordSchema<T, K, Src>;
|
|
@@ -103,6 +148,37 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
|
|
|
103
148
|
* substitution and content validation are all root-only.
|
|
104
149
|
*/
|
|
105
150
|
jsonValues(): RecordSchema<T, K, JsonValuesRecordSrc<T, K>>;
|
|
151
|
+
/**
|
|
152
|
+
* Store this record's entries behind an adapter — a database, an HTTP API, a
|
|
153
|
+
* bucket — instead of in the module.
|
|
154
|
+
*
|
|
155
|
+
* Available on every record-derived schema, which is `s.record()`,
|
|
156
|
+
* `s.router()`, `s.images()` and `s.files()` alike: a router is a
|
|
157
|
+
* `RecordSchema` with a `ValRouter`, a gallery one with media options, so all
|
|
158
|
+
* four get external storage from this one method.
|
|
159
|
+
*
|
|
160
|
+
* `label` does not address the binding — the registry is keyed by module, and
|
|
161
|
+
* so is the wire protocol. It exists so a reader of the schema can see what
|
|
162
|
+
* backs it, and it is checked against the binding that claims it, so the two
|
|
163
|
+
* cannot drift.
|
|
164
|
+
*
|
|
165
|
+
* Only supported on a module's ROOT record, exactly as `.jsonValues()` is: a
|
|
166
|
+
* nested external record would leave a hole in the middle of a module's
|
|
167
|
+
* source, and the Studio, patching and validation all assume a module's
|
|
168
|
+
* source is complete apart from its entries.
|
|
169
|
+
*/
|
|
170
|
+
external<L extends string>(label: L): RecordSchema<T, K, ExternalRecordSrc<SelectorOfSchema<T>, L, RO>, RO>;
|
|
171
|
+
/**
|
|
172
|
+
* An external record's source is either the `c.external()` marker or entries
|
|
173
|
+
* still written inline in the `.val.ts`.
|
|
174
|
+
*
|
|
175
|
+
* Inline entries are NOT a type error (see `ExternalRecordWritableSrc`), so
|
|
176
|
+
* this is where they surface: as an `external:upload` fix, per key, the way
|
|
177
|
+
* `.jsonValues()` reports an un-extracted entry. Reporting per key rather than
|
|
178
|
+
* once for the record is what lets the fix be applied to one entry, and what
|
|
179
|
+
* puts the error at a path the editor can navigate to.
|
|
180
|
+
*/
|
|
181
|
+
private validateExternal;
|
|
106
182
|
private getRouterValidations;
|
|
107
183
|
protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
|
|
108
184
|
protected executeSerialize(): SerializedRecordSchema;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry"];
|
|
1
|
+
export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry", "external:upload"];
|
|
2
2
|
export type ValidationFix = (typeof ValidationFix)[number];
|
|
@@ -12,11 +12,12 @@ import { MediaSource } from "../source/media.js";
|
|
|
12
12
|
import { AllRichTextOptions, RichTextSource } from "../source/richtext.js";
|
|
13
13
|
import { RichTextSelector } from "./richtext.js";
|
|
14
14
|
import { JsonSource } from "../source/json.js";
|
|
15
|
+
import { ExternalRecordSrc } from "../source/external.js";
|
|
15
16
|
import { SettingsSource } from "../source/settings.js";
|
|
16
|
-
export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
|
|
17
|
+
export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends ExternalRecordSrc ? GenericSelector<T> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
|
|
17
18
|
export type SelectorSource = SourcePrimitive | undefined | readonly SelectorSource[] | {
|
|
18
19
|
[key: string]: SelectorSource;
|
|
19
|
-
} | MediaSource | JsonSource | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
|
|
20
|
+
} | MediaSource | JsonSource | ExternalRecordSrc | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
|
|
20
21
|
/**
|
|
21
22
|
* @internal
|
|
22
23
|
*/
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Source, VAL_EXTENSION } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The string used as the `_type` discriminator of an {@link ExternalRecordSrc}.
|
|
4
|
+
*/
|
|
5
|
+
export declare const EXTERNAL_VAL_EXTENSION_TAG: "external";
|
|
6
|
+
/**
|
|
7
|
+
* Type-level-only slots. Declared here rather than reusing `PhantomType`
|
|
8
|
+
* (`source/index.ts`) because three separate facts have to ride along, and that
|
|
9
|
+
* helper carries one.
|
|
10
|
+
*
|
|
11
|
+
* None of them exist at runtime: `c.external()` produces `{ _type: "external" }`
|
|
12
|
+
* and nothing else.
|
|
13
|
+
*/
|
|
14
|
+
declare const ExternalItem: unique symbol;
|
|
15
|
+
declare const ExternalLabel: unique symbol;
|
|
16
|
+
declare const ExternalReadonly: unique symbol;
|
|
17
|
+
/**
|
|
18
|
+
* The source of a record whose entries live behind an adapter — a database, an
|
|
19
|
+
* HTTP API, a bucket — instead of in the module.
|
|
20
|
+
*
|
|
21
|
+
* This is what `c.external()` returns, and it is the counterpart of the
|
|
22
|
+
* `c.json(() => import(...))` marker a `.jsonValues()` entry carries: a value
|
|
23
|
+
* that says "the content is not here" without saying anything about what is.
|
|
24
|
+
* The schema says *which* adapter (`.external("posts")`); this says *that* the
|
|
25
|
+
* entries are elsewhere, which is what every source-walking consumer needs to
|
|
26
|
+
* know without holding a schema.
|
|
27
|
+
*
|
|
28
|
+
* Three phantom parameters, each earning its place:
|
|
29
|
+
*
|
|
30
|
+
* - `T` — the item type. Without it `c.define` would return a module whose type
|
|
31
|
+
* is "a marker", the item type would be gone, and an adapter could not be
|
|
32
|
+
* typed from the schema at all.
|
|
33
|
+
* - `L` — the label from `.external(label)`, so a binding can be checked against
|
|
34
|
+
* the schema that declared it.
|
|
35
|
+
* - `RO` — whether the schema said `.readonly()`, which decides whether the
|
|
36
|
+
* adapter must implement writes or is forbidden from doing so.
|
|
37
|
+
*
|
|
38
|
+
* The defaults are the WIDEST instantiation on purpose: the bare name appears in
|
|
39
|
+
* the `Source` and `SelectorSource` unions, and every narrower instantiation has
|
|
40
|
+
* to be assignable to it. The `external()` factory below defaults the other way,
|
|
41
|
+
* exactly as `JsonSource<T = unknown>` and `json<T = Json>()` do.
|
|
42
|
+
*/
|
|
43
|
+
export type ExternalRecordSrc<T = unknown, L extends string = string, RO extends boolean = boolean> = {
|
|
44
|
+
readonly [VAL_EXTENSION]: typeof EXTERNAL_VAL_EXTENSION_TAG;
|
|
45
|
+
} & {
|
|
46
|
+
readonly [ExternalItem]: T;
|
|
47
|
+
readonly [ExternalLabel]: L;
|
|
48
|
+
readonly [ExternalReadonly]: RO;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The marker written as a module's source when its record is `.external()`.
|
|
52
|
+
*
|
|
53
|
+
* Takes no arguments: everything it carries is inferred from the schema it is
|
|
54
|
+
* checked against in `c.define`. The assertion mirrors `json()` — the phantom
|
|
55
|
+
* slots are symbol-keyed and have no runtime representation, so there is nothing
|
|
56
|
+
* to construct for them.
|
|
57
|
+
*/
|
|
58
|
+
export declare function external<T = never, L extends string = string, RO extends boolean = false>(): ExternalRecordSrc<T, L, RO>;
|
|
59
|
+
export declare function isExternal(obj: unknown): obj is ExternalRecordSrc;
|
|
60
|
+
/**
|
|
61
|
+
* The item type an external record holds, read back off a module.
|
|
62
|
+
*
|
|
63
|
+
* `GenericSelector` is not imported here — that would be a cycle through
|
|
64
|
+
* `../selector` — so this is written against the phantom slot directly and the
|
|
65
|
+
* selector-shaped overloads live where the selector types do.
|
|
66
|
+
*/
|
|
67
|
+
export type ExternalItemOf<S> = S extends ExternalRecordSrc<infer I, string, boolean> ? I : never;
|
|
68
|
+
/** The label an external record was declared with. */
|
|
69
|
+
export type ExternalLabelOf<S> = S extends ExternalRecordSrc<unknown, infer L, boolean> ? L : never;
|
|
70
|
+
/** Whether the schema behind an external record said `.readonly()`. */
|
|
71
|
+
export type ExternalReadonlyOf<S> = S extends ExternalRecordSrc<unknown, string, infer RO> ? RO : never;
|
|
72
|
+
/**
|
|
73
|
+
* The source of an external record as it may be WRITTEN.
|
|
74
|
+
*
|
|
75
|
+
* Either the marker, or entries inline in the `.val.ts`. Inline entries are
|
|
76
|
+
* accepted by the type on purpose, for the same reason `.jsonValues()` accepts
|
|
77
|
+
* them (see `JsonValuesRecordSrc`): pasting content straight into the module is
|
|
78
|
+
* the natural first thing to write, and a type error there is a dead end — the
|
|
79
|
+
* author cannot see what to write instead. Validation reports the inline entries
|
|
80
|
+
* as an `external:upload` fix instead, so the mistake is repaired rather than
|
|
81
|
+
* blocking authoring, and they are readable at runtime meanwhile.
|
|
82
|
+
*/
|
|
83
|
+
export type ExternalRecordWritableSrc<Item, Key extends string = string, L extends string = string, RO extends boolean = boolean> = ExternalRecordSrc<Item, L, RO> | Record<Key, Item>;
|
|
84
|
+
/**
|
|
85
|
+
* Is this module source an external record that still has entries written
|
|
86
|
+
* inline? Used by validation to raise `external:upload`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function hasInlineExternalEntries(source: Source): source is {
|
|
89
|
+
[key: string]: Source;
|
|
90
|
+
};
|
|
91
|
+
export {};
|