@valbuild/shared 0.122.0 → 0.123.2

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 CHANGED
@@ -1,5 +1,136 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.123.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#629](https://github.com/valbuild/val/pull/629) [`04b6d4c`](https://github.com/valbuild/val/commit/04b6d4cbbecc131bbaf3c20633af9dad8c857310) Thanks [@freekh](https://github.com/freekh)! - Stop shipping zod to every visitor of a Val site (~113 KB)
8
+
9
+ `@valbuild/next`'s client code needed five things from
10
+ `@valbuild/shared/internal`, three of which are string constants like
11
+ `VAL_THEME_SESSION_STORAGE_KEY`. But preconstruct publishes each entrypoint as
12
+ a single bundled module, so importing a string constant pulled in the whole
13
+ entrypoint — including `ApiRoutes.ts` and its zod schemas. Measured on
14
+ val.build, that was 113 KB of zod in the initial JS of every page, for
15
+ visitors who never open the Studio.
16
+
17
+ Two changes:
18
+
19
+ - New `@valbuild/shared/client` entrypoint carrying the parts that are safe in
20
+ a browser bundle: the session-storage keys, the canvas protocol, and the
21
+ route-pattern helpers. Nothing reachable from it may import zod, and
22
+ `noZodInClientEntrypoint.test.ts` walks the source graph to enforce that —
23
+ one careless re-export would silently put the 113 KB back.
24
+ - `ValNextProvider` now imports `createValClient` on first use rather than at
25
+ module scope. It genuinely needs zod (it `safeParse`s every request and
26
+ response), but its only caller is the draft-mode poll, which returns early
27
+ unless the overlay is mounted. A visitor without the Val Enable cookie never
28
+ triggers the import.
29
+
30
+ No API change. `@valbuild/shared/internal` still exports everything it did.
31
+
32
+ ## 0.123.0
33
+
34
+ ### Minor Changes
35
+
36
+ - [#613](https://github.com/valbuild/val/pull/613) [`c5e7dfd`](https://github.com/valbuild/val/commit/c5e7dfd12aa1371195be642e1c2fb72b6f3e3ce2) Thanks [@freekh](https://github.com/freekh)! - Val's MCP endpoint moves to its own package, and can now upload images
37
+
38
+ Everything that serves Val's content tools over MCP — the tool registry, the
39
+ write path behind it, the request guards and the access-token verification —
40
+ now lives in **`@valbuild/mcp`** instead of being split between
41
+ `@valbuild/server` and `@valbuild/next`.
42
+
43
+ **Nothing changes for an app that already mounts it.** `initValMcp` is still
44
+ exported from `@valbuild/next/server` and behaves exactly as before; it is now a
45
+ ten-line binding over `@valbuild/mcp`, supplying the Next version that
46
+ `initHandlerOptions` asks for. A host that is not Next can call
47
+ `initValMcp` from `@valbuild/mcp` directly.
48
+
49
+ If you built your own host on `createValTools`, import it from `@valbuild/mcp`
50
+ rather than `@valbuild/server`; the tool types moved with it.
51
+
52
+ ## Image uploads
53
+
54
+ An agent can now add an image, with a new `upload_image` tool. It takes a path
55
+ to a file on the machine your app runs on, or the image inline as base64, and
56
+ puts it in an `s.image()` field or an `s.images()` gallery.
57
+
58
+ Uploads are converted **only where the Studio would convert them**: `encode` is
59
+ off unless the schema asks for it (`s.image({ encode: { type: "webp" } })`), and
60
+ when it does, which images are converted, how far they are scaled and when the
61
+ original wins are the same decisions the browser makes — the same code, now
62
+ shared. An upload to a schema without `encode` is stored exactly as it arrived,
63
+ whatever its size.
64
+
65
+ One thing the tool does that the Studio does not: it refuses an image the
66
+ schema's `accept` does not cover, checked on the bytes that would actually be
67
+ stored. The Studio does not need to — its file picker carries `accept` — and
68
+ validation reports a mismatch as server-repairable, so nothing downstream would
69
+ stop it. An agent has no picker. Note the ordering:
70
+ `s.image({ accept: "image/webp", encode: { type: "webp" } })` still takes a PNG,
71
+ because the conversion happens first and it is the result that is checked.
72
+
73
+ It is the one tool you construct yourself, because it needs an image library
74
+ and `sharp` ships a compiled binary per platform. Val does not put one in every
75
+ project that installs it, so you decide:
76
+
77
+ ```sh
78
+ npm install sharp
79
+ ```
80
+
81
+ ```ts
82
+ import sharp from "sharp";
83
+ import { createValImageTools } from "@valbuild/mcp";
84
+ import { sharpImageProcessor } from "@valbuild/mcp/sharp";
85
+
86
+ const { valMcpAuthorize, valMcpTools } = initValMcp(valModules, config, {
87
+ extraTools: createValImageTools(sharpImageProcessor(sharp)),
88
+ });
89
+ ```
90
+
91
+ Leave `extraTools` out and everything else works as before — the agent can read,
92
+ validate and edit content, it just cannot add an image. `sharp` is passed in
93
+ rather than imported, so you can supply another encoder: `ValImageProcessor` is
94
+ two functions, `read` and `encode`.
95
+
96
+ Remotely stored images work too — `s.image().remote()` and
97
+ `s.images({ remote: true })` — and they need nothing extra from the MCP client.
98
+ Adding one does not upload anything to Val's content host: the bytes go into the
99
+ patch store like any other unpublished change, and the push to
100
+ `remote.val.build` happens when you publish, exactly as it does for an image
101
+ added through the Studio. All the tool has to do first is ask the project which
102
+ bucket to name in the ref, and the credential for that is the one your app
103
+ already has — its API key when it has one, and in local development the
104
+ `val login` token in your project, the same one `val validate --fix` uses. If
105
+ you have not logged in, it says so and writes nothing.
106
+
107
+ ## `npm create @valbuild` asks
108
+
109
+ The starter template now ships the MCP endpoint, and `npm create @valbuild`
110
+ asks whether you want it — and, if you do, whether agents should be able to
111
+ upload images, saying that this adds `sharp`. Both default to yes, and both can
112
+ be answered up front for a scripted setup:
113
+
114
+ ```sh
115
+ pnpm create @valbuild my-app --mcp --no-image-uploads
116
+ ```
117
+
118
+ - [#620](https://github.com/valbuild/val/pull/620) [`53f670c`](https://github.com/valbuild/val/commit/53f670c0cf2d7a03a6d068c78b7874ce77652c2a) Thanks [@freekh](https://github.com/freekh)! - MCP: `search_content` — full-text search across the project's content, unpublished changes included.
119
+
120
+ The same index the Studio's search box uses, moved into `@valbuild/shared` so both run it, and built fresh on every call. That was the part that looked expensive and is not: indexing `valbuild/web`, a real production site of 20 modules and 206 KB of source, takes 162 ms, and it scales linearly from there — the 10 s default deadline is not reached until roughly 13 MB of content. Loading the modules costs more than indexing them, and every tool call already pays that.
121
+
122
+ That ratio between building the index and querying it — 162 ms against 0.2 ms — is why `queries` is a list. Everything expensive happens before the first query runs, so up to 20 of them are answered from a single pass, separately, so a caller can tell which of its guesses found the thing. A bare string still works as one query.
123
+
124
+ It returns source paths, so `get_source` reads what it finds. The rest of the arguments narrow or bound the work:
125
+
126
+ - `include` / `exclude` — module file path globs, e.g. `["/content/blogs/**"]`. `exclude` is applied after `include`.
127
+ - `limit` — per query, defaulting to 100. A model filters a long list more cheaply than it asks again.
128
+ - `timeoutMs` — stop indexing and answer with what has been indexed so far. The result then carries `timedOut`, the paths of the modules that were not reached, and a hint pointing at `include`.
129
+
130
+ Every answer says what it actually searched (`searched: { modules, of }`), so each query's `total` can be read as a count over those modules rather than over the project. Modules you excluded are not reported as omissions — an omission always means the deadline, never your filter.
131
+
132
+ `performSearch` now counts all the matches rather than the page it returns, which the Studio's result count gets too: FlexSearch stops as soon as it has the ids it was asked for, so a total taken from a page-sized search was only ever the page size again.
133
+
3
134
  ## 0.122.0
4
135
 
5
136
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export * from "../../dist/declarations/src/client/index.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsYnVpbGQtc2hhcmVkLWNsaWVudC5janMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL2Rpc3QvZGVjbGFyYXRpb25zL3NyYy9jbGllbnQvaW5kZXguZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var getSitemapTree = require('../../dist/getSitemapTree-a0c9a0e3.cjs.dev.js');
6
+ require('@valbuild/core');
7
+
8
+
9
+
10
+ exports.VAL_AI_SESSION_STORAGE_KEY = getSitemapTree.VAL_AI_SESSION_STORAGE_KEY;
11
+ exports.VAL_CANVAS_MESSAGE = getSitemapTree.VAL_CANVAS_MESSAGE;
12
+ exports.VAL_CANVAS_PARAM = getSitemapTree.VAL_CANVAS_PARAM;
13
+ exports.VAL_CONFIG_SESSION_STORAGE_KEY = getSitemapTree.VAL_CONFIG_SESSION_STORAGE_KEY;
14
+ exports.VAL_THEME_SESSION_STORAGE_KEY = getSitemapTree.VAL_THEME_SESSION_STORAGE_KEY;
15
+ exports.getNextAppRouterSitemapTree = getSitemapTree.getNextAppRouterSitemapTree;
16
+ exports.getNextAppRouterSourceFolder = getSitemapTree.getNextAppRouterSourceFolder;
17
+ exports.getPatternFromModuleFilePath = getSitemapTree.getPatternFromModuleFilePath;
18
+ exports.isValCanvasFrame = getSitemapTree.isValCanvasFrame;
19
+ exports.isValCanvasPageMessage = getSitemapTree.isValCanvasPageMessage;
20
+ exports.isValCanvasStudioMessage = getSitemapTree.isValCanvasStudioMessage;
21
+ exports.parseRoutePattern = getSitemapTree.parseRoutePattern;
22
+ exports.withValCanvasParam = getSitemapTree.withValCanvasParam;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === "production") {
4
+ module.exports = require("./valbuild-shared-client.cjs.prod.js");
5
+ } else {
6
+ module.exports = require("./valbuild-shared-client.cjs.dev.js");
7
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var getSitemapTree = require('../../dist/getSitemapTree-77b1203a.cjs.prod.js');
6
+ require('@valbuild/core');
7
+
8
+
9
+
10
+ exports.VAL_AI_SESSION_STORAGE_KEY = getSitemapTree.VAL_AI_SESSION_STORAGE_KEY;
11
+ exports.VAL_CANVAS_MESSAGE = getSitemapTree.VAL_CANVAS_MESSAGE;
12
+ exports.VAL_CANVAS_PARAM = getSitemapTree.VAL_CANVAS_PARAM;
13
+ exports.VAL_CONFIG_SESSION_STORAGE_KEY = getSitemapTree.VAL_CONFIG_SESSION_STORAGE_KEY;
14
+ exports.VAL_THEME_SESSION_STORAGE_KEY = getSitemapTree.VAL_THEME_SESSION_STORAGE_KEY;
15
+ exports.getNextAppRouterSitemapTree = getSitemapTree.getNextAppRouterSitemapTree;
16
+ exports.getNextAppRouterSourceFolder = getSitemapTree.getNextAppRouterSourceFolder;
17
+ exports.getPatternFromModuleFilePath = getSitemapTree.getPatternFromModuleFilePath;
18
+ exports.isValCanvasFrame = getSitemapTree.isValCanvasFrame;
19
+ exports.isValCanvasPageMessage = getSitemapTree.isValCanvasPageMessage;
20
+ exports.isValCanvasStudioMessage = getSitemapTree.isValCanvasStudioMessage;
21
+ exports.parseRoutePattern = getSitemapTree.parseRoutePattern;
22
+ exports.withValCanvasParam = getSitemapTree.withValCanvasParam;
@@ -0,0 +1,2 @@
1
+ export { b as VAL_AI_SESSION_STORAGE_KEY, d as VAL_CANVAS_MESSAGE, c as VAL_CANVAS_PARAM, V as VAL_CONFIG_SESSION_STORAGE_KEY, a as VAL_THEME_SESSION_STORAGE_KEY, h as getNextAppRouterSitemapTree, g as getNextAppRouterSourceFolder, j as getPatternFromModuleFilePath, f as isValCanvasFrame, i as isValCanvasPageMessage, e as isValCanvasStudioMessage, p as parseRoutePattern, w as withValCanvasParam } from '../../dist/getSitemapTree-d3e98a2e.esm.js';
2
+ import '@valbuild/core';
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "dist/valbuild-shared-client.cjs.js",
3
+ "module": "dist/valbuild-shared-client.esm.js"
4
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The part of `@valbuild/shared/internal` that is safe to put in a browser
3
+ * bundle.
4
+ *
5
+ * `@valbuild/shared/internal` is a single preconstruct entrypoint, so
6
+ * preconstruct publishes it as ONE module: importing a string constant from it
7
+ * pulls in everything else, and everything else includes `ApiRoutes.ts`, whose
8
+ * zod schemas cost ~113 KB minified. `@valbuild/next`'s client code needs five
9
+ * things from that entrypoint, three of which are string constants, so every
10
+ * production visitor to a Val site was paying for zod to read
11
+ * `VAL_THEME_SESSION_STORAGE_KEY`.
12
+ *
13
+ * Nothing re-exported here may import zod, directly or transitively. The
14
+ * `noZodInClientEntrypoint.test.ts` beside this file is what enforces that -
15
+ * a single new re-export can silently put 113 KB back.
16
+ */
17
+ export * from "../internal/sessionStorage.js";
18
+ export * from "../internal/valCanvasProtocol.js";
19
+ export * from "../internal/parseRoutePattern.js";
20
+ export * from "../internal/getNextAppRouterSourceFolder.js";
21
+ export * from "../internal/getSitemapTree.js";
@@ -25,3 +25,8 @@ export * from "./aiTools/aiImageToolPatches.js";
25
25
  export * from "./aiTools/aiSourceToolPatches.js";
26
26
  export * from "./validation/partitionValidationErrors.js";
27
27
  export * from "./validation/blockingValidationErrors.js";
28
+ export * from "./media/encodeImageDecisions.js";
29
+ export * from "./search/searchIndex.js";
30
+ export * from "./search/traverseSchemaSource.js";
31
+ export * from "./search/getFilenameFromRef.js";
32
+ export * from "./search/sourcePath.js";
@@ -0,0 +1,89 @@
1
+ import { type ImageEncodeOption } from "@valbuild/core";
2
+ /**
3
+ * Every decision `encode` makes, with none of the pixels.
4
+ *
5
+ * The re-encode itself happens in two places that share nothing: the Studio
6
+ * runs it on a `<canvas>` before the upload leaves the browser
7
+ * (`packages/ui/spa/utils/encodeImage.ts`), and the MCP image tool runs it on
8
+ * `sharp` in the server process (`@valbuild/mcp`). What must NOT differ between
9
+ * them is what `s.image({ encode })` means — which images are converted, how
10
+ * far they are scaled, and when the original wins anyway. So the decisions live
11
+ * here, once, and each side supplies only its own encoder.
12
+ *
13
+ * See `architecture/media.md`.
14
+ */
15
+ export type EncodeSettings = {
16
+ type: "webp";
17
+ quality: number;
18
+ maxWidth: number;
19
+ maxHeight: number;
20
+ };
21
+ export declare const ENCODE_DEFAULTS: Omit<EncodeSettings, "type">;
22
+ export declare const ENCODE_MIME_TYPE_OF: Record<EncodeSettings["type"], string>;
23
+ /**
24
+ * Formats an encoder cannot round-trip without losing something: SVG is vector,
25
+ * an animated GIF keeps only its first frame, and AVIF is already smaller than
26
+ * anything we would produce.
27
+ */
28
+ export declare const NEVER_ENCODED: string[];
29
+ /**
30
+ * The schema's own option first, then the gallery it references, then off.
31
+ *
32
+ * Mirrors how `ImageField` resolves `accept` and `directory`. A gallery-backed
33
+ * field (`s.image(galleryVal)`) serializes with EMPTY options, so without the
34
+ * gallery fallback it could never inherit what the gallery asked for — and
35
+ * `s.image(galleryVal, { encode: false })` is how such a field opts back out.
36
+ *
37
+ * Nonsense numbers fall back to the defaults rather than through. `fitWithin`
38
+ * reads a bound of 0 (or NaN, or a negative) as "everything already fits", so
39
+ * an unusable `maxWidth` would otherwise SILENTLY disable the downscale it was
40
+ * asking for, and a `quality` outside 0–1 is ignored by `canvas.toBlob` just as
41
+ * quietly. Falling back is deliberately not throwing: a schema is evaluated by
42
+ * the CLI's `node:vm` sandbox and by the Next server, so a typo'd quality would
43
+ * take a whole project down over an upload-time optimisation.
44
+ */
45
+ export declare function resolveEncodeSettings(fieldOption: ImageEncodeOption | undefined, galleryOption: ImageEncodeOption | undefined): EncodeSettings | null;
46
+ /**
47
+ * Dimensions scaled to fit inside the box, or null when the image already does.
48
+ *
49
+ * Null is the signal for "no downscale was needed", which the size comparison
50
+ * in {@link chooseEncoded} turns on.
51
+ */
52
+ export declare function fitWithin(width: number, height: number, maxWidth: number, maxHeight: number): {
53
+ width: number;
54
+ height: number;
55
+ } | null;
56
+ /**
57
+ * Whether a schema that declares `accept` would take this mime type.
58
+ *
59
+ * The same rules as `ImageSchema.executeValidate`, which validates the STORED
60
+ * mimeType against `accept` — so converting to a type `accept` forbids would
61
+ * upload a file the schema reports as invalid the moment it lands. Delegated to
62
+ * `@valbuild/core` rather than re-spelled, because a second reading of one
63
+ * `accept` string is how a converted image passes here and fails there.
64
+ */
65
+ export declare function isMimeTypeAccepted(mimeType: string, accept: string | undefined): boolean;
66
+ /** Formats left alone: the lossy list, plus the target type when it already fits. */
67
+ export declare function isSkippedSource(sourceMimeType: string, targetMimeType: string, needsDownscale: boolean): boolean;
68
+ /**
69
+ * Whether to upload what the encoder produced, or the file that was picked.
70
+ *
71
+ * Two ways the encoded bytes lose. They are not actually the type we asked for
72
+ * — `canvas.toBlob` answers an unsupported type with a PNG rather than with
73
+ * null, so "not null" is not the same question as "is a webp". Or they are
74
+ * simply bigger, which happens for small and flat images: measured, an 8x8
75
+ * solid PNG of 74 bytes becomes a 548 byte webp. A downscale overrides that,
76
+ * since the original is then the wrong size whatever it weighs.
77
+ */
78
+ export declare function chooseEncoded(input: {
79
+ originalSize: number;
80
+ encodedSize: number | null;
81
+ encodedType: string | null;
82
+ targetMimeType: string;
83
+ needsDownscale: boolean;
84
+ }): boolean;
85
+ /**
86
+ * `photo.png` -> `photo.webp`. The extension is re-derived downstream too, but
87
+ * the name should not lie.
88
+ */
89
+ export declare function withExtension(filename: string, ext: string): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Extract a human-readable filename from a file ref (local path or remote URL).
3
+ * Handles both remote refs (via splitRemoteRef) and plain `/public/...` paths.
4
+ */
5
+ export declare function getFilenameFromRef(ref: string): string;
6
+ /**
7
+ * Parse a file ref into its constituent parts: a clean path, filename, and
8
+ * folder (the path relative to `/public`).
9
+ *
10
+ * The `folder` strips the standard `/public` prefix so it shows a
11
+ * concise location like `/images` instead of `/public/images`.
12
+ */
13
+ export declare function getRefParts(ref: string): {
14
+ cleanPath: string;
15
+ filename: string;
16
+ folder: string;
17
+ };
@@ -0,0 +1,76 @@
1
+ import { Json, ModuleFilePath, SerializedSchema, SourcePath } from "@valbuild/core";
2
+ import { Index } from "flexsearch";
3
+ /**
4
+ * The search index and the labels to render for its hits.
5
+ *
6
+ * Kept as a plain module rather than living inside a worker so it can be tested:
7
+ * a worker entry runs `self.onmessage` on import, which no test environment here
8
+ * provides.
9
+ *
10
+ * In `@valbuild/shared` rather than the Studio because there are now two
11
+ * realms searching the same content: the Studio's `SearchStore`, which keeps an
12
+ * index alive in a worker and re-indexes a module at a time, and the MCP
13
+ * `search_content` tool, which builds one per call and throws it away. What
14
+ * counts as a document, and what a hit is labelled, must not differ between
15
+ * them — an agent and an editor searching the same project should find the same
16
+ * things.
17
+ */
18
+ export type SearchIndex = {
19
+ index: Index;
20
+ pathToLabel: Map<string, string>;
21
+ /**
22
+ * Which document ids belong to which module, so one module can be re-indexed
23
+ * without touching the others.
24
+ *
25
+ * The ids are the `SourcePath`s themselves — `index.add(path, …)` below — so
26
+ * this could be recovered by scanning `pathToLabel` for a module-file-path
27
+ * prefix. It is kept explicitly anyway: the scan would be O(every document in
28
+ * the project) per module re-indexed, which is the whole-project cost that
29
+ * re-indexing one module exists to avoid.
30
+ */
31
+ docsByModule: Map<ModuleFilePath, Set<string>>;
32
+ };
33
+ /** An index with nothing in it. Fill it with {@link indexModule}. */
34
+ export declare function createSearchIndex(): SearchIndex;
35
+ /**
36
+ * Index one module, replacing whatever was previously indexed for it.
37
+ *
38
+ * Remove-then-add rather than `update`, because the set of paths in a module is
39
+ * not stable: deleting an array item or a record entry means documents that must
40
+ * disappear, and `update` only revises ids it is given.
41
+ *
42
+ * `.jsonValues()` entries are indexed only once their content is loaded — an
43
+ * un-loaded entry is an opaque marker that {@link traverseSchemaSource} skips, so
44
+ * the index is naturally PARTIAL and grows as batches land. That is why the search
45
+ * UI loads the entries on the first query and re-indexes (debounced) as they
46
+ * arrive, rather than pretending an empty result set is an answer.
47
+ */
48
+ export declare function indexModule(searchIndex: SearchIndex, moduleFilePath: ModuleFilePath, source: Json, schema: SerializedSchema): void;
49
+ /**
50
+ * Drop everything indexed for one module.
51
+ *
52
+ * Also used for a module that has gone away entirely — otherwise its documents
53
+ * stay searchable and a hit navigates to a path that no longer exists.
54
+ */
55
+ export declare function removeModule(searchIndex: SearchIndex, moduleFilePath: ModuleFilePath): void;
56
+ /**
57
+ * Index every module, from scratch.
58
+ *
59
+ * Now a loop over {@link indexModule} rather than its own walk, so the full
60
+ * build and the incremental one cannot drift in what they consider a document.
61
+ */
62
+ export declare function buildSearchIndex(modules: Record<ModuleFilePath, {
63
+ source: Json;
64
+ schema: SerializedSchema;
65
+ }>): SearchIndex;
66
+ export type SearchResults = {
67
+ results: Array<{
68
+ path: SourcePath;
69
+ label: string;
70
+ }>;
71
+ /** Matches for the query, not just on this page. */
72
+ total: number;
73
+ /** `total` hit {@link MAX_COUNTED_RESULTS}, so it means "at least this many". */
74
+ totalIsLowerBound: boolean;
75
+ };
76
+ export declare function performSearch(searchIndex: SearchIndex | null, query: string, limit?: number, offset?: number): SearchResults;
@@ -0,0 +1,30 @@
1
+ import { ModuleFilePath, ModulePath, SourcePath } from "@valbuild/core";
2
+ /**
3
+ * The one way to build a child's source path.
4
+ *
5
+ * There were five hand-rolled versions of this, three of them subtly different
6
+ * and two of them wrong, and the wrong ones were wrong in a way that hid: a
7
+ * module path segment has to be JSON-quoted if it is a string and bare if it is
8
+ * an array index, and both broken copies appended the raw key at the module
9
+ * root. An unquoted key parses fine as long as it contains no `.` — so every
10
+ * object field and every ordinary record key still resolved, and only a key with
11
+ * a dot in it broke. That is exactly what a media gallery is keyed by, so
12
+ * clicking a file in the Media panel navigated to
13
+ * `?p=/public/val/red-8x8_bfbd0.png`, which `splitModulePath` reads as the two
14
+ * segments `/public/val/red-8x8_bfbd0` and `png`, and the studio said the module
15
+ * was not found.
16
+ *
17
+ * So: one function, and never a template string. `Internal.createValPathOfItem`
18
+ * is the rule itself — `JSON.stringify` quotes a string and leaves a number
19
+ * bare, which is precisely the module-path grammar — and everything here defers
20
+ * to it rather than restating it.
21
+ */
22
+ export declare function sourcePathOfChild(parent: SourcePath | ModuleFilePath, key: string | number): SourcePath;
23
+ /**
24
+ * The same thing, for callers holding the module file path and module path apart.
25
+ *
26
+ * `ModulePath` is empty at the module root, which is the case both broken copies
27
+ * got wrong: there is no separator in the path yet, so the key is the first
28
+ * segment and still has to be quoted.
29
+ */
30
+ export declare function concatModulePath(moduleFilePath: ModuleFilePath, modulePath: ModulePath, key: string | number): SourcePath;
@@ -0,0 +1,20 @@
1
+ import { Source, SerializedSchema, SourcePath } from "@valbuild/core";
2
+ /**
3
+ * Traverses a schema and source pair, calling a callback for each leaf node.
4
+ * Handles all schema types including primitives, objects, arrays, records, unions, richtext, files, images, etc.
5
+ *
6
+ * @param source - The JSON source data
7
+ * @param schema - The serialized schema
8
+ * @param path - The current source path
9
+ * @param callback - Function called for each traversable node with { source, schema, path }
10
+ */
11
+ export declare function traverseSchemaSource(source: Source, schema: SerializedSchema, path: SourcePath, callback: (opts: {
12
+ source: Source;
13
+ schema: SerializedSchema;
14
+ path: SourcePath;
15
+ }) => void): void;
16
+ /**
17
+ * Flattens richtext source to a plain string, extracting only text content.
18
+ * Used for indexing richtext content in search.
19
+ */
20
+ export declare function flattenRichText(source: Source): string;