@valbuild/server 0.146.4 → 0.148.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/browser/dist/valbuild-server-browser.cjs.d.ts +2 -0
  3. package/browser/dist/valbuild-server-browser.cjs.dev.js +146 -0
  4. package/browser/dist/valbuild-server-browser.cjs.js +7 -0
  5. package/browser/dist/valbuild-server-browser.cjs.prod.js +146 -0
  6. package/browser/dist/valbuild-server-browser.esm.js +142 -0
  7. package/browser/package.json +4 -0
  8. package/dist/ValRouterCore-42772c21.cjs.prod.js +10824 -0
  9. package/dist/ValRouterCore-7ca3dc68.esm.js +10784 -0
  10. package/dist/ValRouterCore-9bdaaf89.cjs.dev.js +10824 -0
  11. package/dist/declarations/src/ValOps.d.ts +44 -11
  12. package/dist/declarations/src/ValOpsFS.d.ts +1 -0
  13. package/dist/declarations/src/ValOpsHttp.d.ts +1 -0
  14. package/dist/declarations/src/ValOpsMemory.d.ts +13 -4
  15. package/dist/declarations/src/ValRouter.d.ts +1 -1
  16. package/dist/declarations/src/ValServer.d.ts +9 -3
  17. package/dist/declarations/src/browser/inTabFetch.d.ts +27 -0
  18. package/dist/declarations/src/browser/index.d.ts +9 -0
  19. package/dist/declarations/src/checkRemoteRef.d.ts +40 -2
  20. package/dist/declarations/src/createFixPatch.d.ts +25 -3
  21. package/dist/declarations/src/fixFiles.d.ts +42 -0
  22. package/dist/declarations/src/fixHandlers.d.ts +15 -1
  23. package/dist/declarations/src/getSettings.d.ts +1 -0
  24. package/dist/declarations/src/index.d.ts +1 -0
  25. package/dist/declarations/src/linkCheck/probeUrl.d.ts +40 -0
  26. package/dist/declarations/src/platform.d.ts +55 -0
  27. package/dist/declarations/src/remoteFileCheck.d.ts +116 -0
  28. package/dist/declarations/src/remoteHostCheck.d.ts +101 -0
  29. package/dist/declarations/src/studioValidation.d.ts +84 -0
  30. package/dist/declarations/src/uploadRemoteFile.d.ts +1 -1
  31. package/dist/declarations/src/videosetFixes.d.ts +3 -2
  32. package/dist/getJsonEntryAtCommit-2e36e805.cjs.prod.js +142 -0
  33. package/dist/getJsonEntryAtCommit-40b6d9c1.esm.js +135 -0
  34. package/dist/getJsonEntryAtCommit-64f67a50.cjs.dev.js +142 -0
  35. package/dist/jsonValuesModule-427d68c8.esm.js +273 -0
  36. package/dist/jsonValuesModule-8ae503a8.cjs.dev.js +288 -0
  37. package/dist/jsonValuesModule-d1bb8d8f.cjs.prod.js +288 -0
  38. package/dist/ops-0d05feaf.esm.js +353 -0
  39. package/dist/ops-b15239ac.cjs.prod.js +361 -0
  40. package/dist/ops-f37d18c6.cjs.dev.js +361 -0
  41. package/dist/publishKit-580f558a.cjs.prod.js +41 -0
  42. package/dist/publishKit-5865095c.cjs.dev.js +41 -0
  43. package/dist/publishKit-bc8df53d.esm.js +31 -0
  44. package/dist/valbuild-server.cjs.dev.js +9239 -18895
  45. package/dist/valbuild-server.cjs.prod.js +9239 -18895
  46. package/dist/valbuild-server.esm.js +9107 -18763
  47. package/package.json +17 -5
@@ -0,0 +1,116 @@
1
+ import { MediaSource, SerializedSchema, SourcePath, ValidationError } from "@valbuild/core";
2
+ import { type AskHostProject } from "./remoteHostCheck.js";
3
+ /**
4
+ * Does the host a remote ref names still serve it?
5
+ *
6
+ * `found` and `missing` are the host's answer. `unreachable` is no answer at
7
+ * all — a timeout, a refused connection, a 5xx — and says nothing about the
8
+ * file, only about the host.
9
+ */
10
+ export type RemoteFileAnswer = {
11
+ status: "found";
12
+ } | {
13
+ status: "missing";
14
+ httpStatus: number;
15
+ } | {
16
+ status: "unreachable";
17
+ reason: string;
18
+ };
19
+ export type HeadRemoteFile = (url: string) => Promise<RemoteFileAnswer>;
20
+ /**
21
+ * How the remote check reaches the network: what it asks a host about a file,
22
+ * and which project a platform host serves. Both default to the real thing;
23
+ * tests replace them.
24
+ */
25
+ export type RemoteNetwork = {
26
+ head?: HeadRemoteFile;
27
+ askHostProject?: AskHostProject;
28
+ };
29
+ /**
30
+ * A HEAD, falling back to the first byte of a GET for a host that does not
31
+ * answer HEAD (405, 501): either way no file is downloaded to learn that it is
32
+ * there.
33
+ */
34
+ export declare const headRemoteFile: HeadRemoteFile;
35
+ /** For tests: forget every answer. */
36
+ export declare function clearRemoteFileAnswers(): void;
37
+ /** A host that did not serve the refs of a module it was asked about. */
38
+ export type RemoteHostProblem = {
39
+ /** `https://remote.val.build`, as the ref names it. */
40
+ host: string;
41
+ status: "missing" | "unreachable";
42
+ /** What the host answered, for "Details": never the row's sentence. */
43
+ details: string;
44
+ /** Every ref on that host in the module, checked or vouched for. */
45
+ sourcePaths: SourcePath[];
46
+ };
47
+ export type RemoteFilesCheck = {
48
+ /** Wrong refs, one by one: each has its own row. */
49
+ errors: Record<SourcePath, ValidationError[]>;
50
+ /** Hosts that failed: one row each, however many refs they cover. */
51
+ hosts: RemoteHostProblem[];
52
+ };
53
+ export type RemoteFilesCheckInput = {
54
+ /** What `validateSources` set aside as remote: every remote media value. */
55
+ remoteFiles: Record<SourcePath, MediaSource>;
56
+ /** The serialized schema at a path, or undefined when it does not resolve. */
57
+ schemaAt: (sourcePath: SourcePath) => SerializedSchema | undefined;
58
+ /**
59
+ * Does this ref have its bytes in a pending patch? They go to the remote
60
+ * host at publish, so until then asking the host for it is asking a
61
+ * question whose answer is "not yet".
62
+ */
63
+ pendingBytes: (ref: string) => Promise<"present" | "missing" | "not-pending">;
64
+ /**
65
+ * Is this value's ref the one already published at this path? Only those
66
+ * may be vouched for by another ref on the same host; see `sampleByHost`.
67
+ */
68
+ isPublished: (sourcePath: SourcePath, ref: string) => boolean;
69
+ /**
70
+ * Ask one published ref per host and take its answer for the rest.
71
+ *
72
+ * Right for a managed project, whose refs are only ever written by the
73
+ * Studio and whose host changes for every ref at once. Wrong for a connected
74
+ * one, where a developer can edit any single ref by hand. See
75
+ * `docs/plans/studio-validate.md`, "Remote refs in the full check".
76
+ */
77
+ sampleByHost: boolean;
78
+ /**
79
+ * Ask only whether the host serves each ref. For the keys of a media
80
+ * directory: what is recorded about the file is the entry, not a value at
81
+ * the path, and the ref's hash was made from a schema the directory
82
+ * synthesizes for it, so there is no schema here to judge it by.
83
+ */
84
+ existenceOnly?: boolean;
85
+ head?: HeadRemoteFile;
86
+ /** How a platform ref's host is asked which project it serves. */
87
+ askHostProject?: AskHostProject;
88
+ };
89
+ /**
90
+ * The remote half of the Studio's full check, for one module's remote values.
91
+ *
92
+ * Four questions, cheapest first, and a ref stops at the first one it fails:
93
+ *
94
+ * 1. **Is it a ref at all?** `splitRemoteRef` parses it.
95
+ * 2. **Does the ref agree with what is recorded about it?** A ref carries a
96
+ * hash of the schema's options, the file's extension and its width, height
97
+ * and mime type. When the value or the schema changed and the ref did not,
98
+ * the ref is stale, and **Fix** (`image:check-remote`) re-reads the file and
99
+ * writes the ref it should have. Computed with the ref's OWN core version:
100
+ * the CLI also re-derives every ref after a Val upgrade, and listing every
101
+ * remote image in the project after each upgrade would bury the rows that
102
+ * matter. Nothing here downloads.
103
+ * 3. **Does its host still serve its project?** Only for a platform ref
104
+ * (`…/_val/file/…`) that is not on the project's own `valstart.dev`
105
+ * address: one `GET <host>/_val/project` per host. A host that dropped the
106
+ * project gets a **Fix** (`remote:rewrite-host`) that points the ref at
107
+ * `<public id>.valstart.dev`. See `remoteHostCheck.ts`.
108
+ * 4. **Does the host have it?** A HEAD.
109
+ *
110
+ * Neither host question is asked of a ref whose bytes are in a pending patch,
111
+ * which the host will not have until publish: there the question is whether
112
+ * the patch still has them.
113
+ */
114
+ export declare function checkRemoteFiles(input: RemoteFilesCheckInput): Promise<RemoteFilesCheck>;
115
+ /** `www.acme.no`, from `https://www.acme.no`: how a person names a host. */
116
+ export declare function hostName(host: string): string;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Does the host a platform ref names still serve the project the ref names?
3
+ *
4
+ * A ref served by the platform loader names the project's host at the time
5
+ * the file was added: `https://www.acme.no/_val/file/p/3f9a2c1/…`. When the
6
+ * project drops `www.acme.no`, the bytes are still there, at
7
+ * `3f9a2c1.valstart.dev`, but every ref that names the old domain points at a
8
+ * host that no longer serves them. This is the question that finds those refs,
9
+ * and `rewriteRefHost` is what the fix (`remote:rewrite-host`) does about it.
10
+ * See valbuild/home `docs/remote-files-on-platform.md`, "Rewriting refs after a
11
+ * domain is removed", and `docs/plans/studio-validate.md`, step 6.
12
+ *
13
+ * Asked of the host itself (`GET <host>/_val/project`), not of content: the
14
+ * host's answer covers DNS, the certificate and the loader's mapping in one,
15
+ * it needs no credential, and it works for a ref that names a project other
16
+ * than this one -- a project seeded from a template has refs naming the
17
+ * template's.
18
+ */
19
+ /** The domain every platform project is always served at, by its public id. */
20
+ export declare const PLATFORM_SITES_DOMAIN = "valstart.dev";
21
+ /**
22
+ * The host every platform ref of `publicProjectId` can always be pointed at:
23
+ * `<public id>.valstart.dev` is permanent (decision 4 of the remote-files
24
+ * plan), and the loader serves a project's files there whatever else it has.
25
+ */
26
+ export declare function fallbackRemoteHost(publicProjectId: string): string;
27
+ /**
28
+ * Only a platform ref is asked about. `remote.val.build` is not a platform
29
+ * host and is not going away, and a ref already on the project's own
30
+ * `valstart.dev` address is on the fallback, which always serves it.
31
+ */
32
+ export declare function needsHostCheck(remoteHost: string, projectId: string): boolean;
33
+ /** `ref` with its host part replaced, and everything else as it was. */
34
+ export declare function rewriteRefHost(ref: string, remoteHost: string): string | null;
35
+ /**
36
+ * - `serves`: the host answered for the project the ref names.
37
+ * - `other-project`: it answered, for another one -- the domain was moved to
38
+ * a different project, and decision 5 means it will not serve these files.
39
+ * - `no-site`: it answered, but serves no project at all (the loader's 404 for
40
+ * a domain nobody has mapped any more).
41
+ * - `unreachable`: no answer. A domain whose DNS was removed looks like this,
42
+ * and so does a host that is down for a minute, which is why the row says
43
+ * which it saw.
44
+ */
45
+ export type HostServes = {
46
+ status: "serves";
47
+ } | {
48
+ status: "other-project";
49
+ publicProjectId: string;
50
+ } | {
51
+ status: "no-site";
52
+ httpStatus: number;
53
+ } | {
54
+ status: "unreachable";
55
+ reason: string;
56
+ };
57
+ export type AskHostProject = (remoteHost: string) => Promise<{
58
+ status: "answered";
59
+ publicProjectId: string;
60
+ } | {
61
+ status: "no-site";
62
+ httpStatus: number;
63
+ } | {
64
+ status: "unreachable";
65
+ reason: string;
66
+ }>;
67
+ /** `GET <host>/_val/project`, answered by the platform loader. */
68
+ export declare const askHostProject: AskHostProject;
69
+ export declare function clearHostAnswers(): void;
70
+ export declare function hostServes(remoteHost: string, projectId: string, ask?: AskHostProject): Promise<HostServes>;
71
+ /**
72
+ * `ask`, asking each host once for as long as the returned function is kept:
73
+ * one check, not the life of the server. A check walks its refs in order, so
74
+ * without it every ref on a host that does not answer waits out its own
75
+ * timeout -- 150 images on a dead domain is 20 minutes. Kept per check rather
76
+ * than in {@link hostServes}'s cache because a host that did not answer has
77
+ * to be asked again by the next one.
78
+ */
79
+ export declare function askOncePerHost(ask?: AskHostProject): AskHostProject;
80
+ /** `www.acme.no`, from `https://www.acme.no/_val`: how a person names a host. */
81
+ export declare function hostNameOfRemoteHost(remoteHost: string): string;
82
+ /**
83
+ * Where a platform ref has to move to, when its host no longer serves the
84
+ * project it names; `undefined` when it does not have to (or is not a
85
+ * platform ref). `why` and `to` are for the caller's sentence: what can be
86
+ * done about it differs by caller (a Studio **Fix**, `val validate --fix`, or
87
+ * nothing yet).
88
+ */
89
+ export declare function movedRemoteHost(ref: string, ask?: AskHostProject): Promise<{
90
+ ref: string;
91
+ why: string;
92
+ to: string;
93
+ } | undefined>;
94
+ /**
95
+ * Where else a ref's file can be downloaded from: the same file on the
96
+ * project's own `valstart.dev` address, for a platform ref on any other host.
97
+ * `null` when there is nowhere else -- a `remote.val.build` ref, or one already
98
+ * on the fallback. See valbuild/home `docs/remote-files-on-platform.md`,
99
+ * "Tooling: the fallback".
100
+ */
101
+ export declare function fallbackRefOf(ref: string): string | null;
@@ -0,0 +1,84 @@
1
+ import { ModuleFilePath, SourcePath, ValidationError, ValidationFix } from "@valbuild/core";
2
+ import type { Patch } from "@valbuild/core/patch";
3
+ import { FixPatchRemainingError } from "./createFixPatch.js";
4
+ import type { FixFiles } from "./fixFiles.js";
5
+ import { type RemoteHostProblem, type RemoteNetwork } from "./remoteFileCheck.js";
6
+ import { type PatchAnalysis, type Schemas, type Sources, type ValOps } from "./ValOps.js";
7
+ /**
8
+ * The Studio's full check, one module at a time: what `/validate` and
9
+ * `/validate/fix` answer with. See `docs/plans/studio-validate.md`.
10
+ *
11
+ * Both take the sources the caller is looking at — base plus the patches it
12
+ * names — so an error is about the content on the editor's screen, and a fix
13
+ * is a patch on top of it.
14
+ */
15
+ export type StudioCheckInput = {
16
+ moduleFilePath: ModuleFilePath;
17
+ schemas: Schemas;
18
+ sources: Sources;
19
+ fileLastUpdatedByPatchId: PatchAnalysis["fileLastUpdatedByPatchId"];
20
+ /** How remote hosts are asked about refs. Tests replace it. */
21
+ network?: RemoteNetwork;
22
+ };
23
+ export type StudioModuleCheck = {
24
+ status: "ok";
25
+ errors: Record<SourcePath, ValidationError[]>;
26
+ /** Hosts that did not serve this module's remote files. */
27
+ remoteHosts: RemoteHostProblem[];
28
+ } | {
29
+ status: "unknown-module";
30
+ } | {
31
+ status: "invalid-source";
32
+ message: string;
33
+ };
34
+ /**
35
+ * Every error in one module: its sources against its schema, and its local
36
+ * files against their bytes.
37
+ *
38
+ * Not filtered the way the publish gate filters (`partitionValidationErrors`):
39
+ * the errors the gate hides because "the server repairs them on save" are the
40
+ * ones this check exists to show, since nothing repairs them in http mode.
41
+ *
42
+ * Remote files are checked too: a stale ref is an error with a fix, and a
43
+ * host that does not serve them is reported once, as a host, rather than as
44
+ * an error on every image it holds (`checkRemoteFiles`).
45
+ *
46
+ * Not yet here: the media directories' own entries, step 5 of the plan.
47
+ */
48
+ export declare function checkModuleForStudio(ops: ValOps, input: StudioCheckInput): Promise<StudioModuleCheck>;
49
+ export type StudioFixResult = {
50
+ status: "ok";
51
+ patch: Patch;
52
+ remainingErrors: FixPatchRemainingError[];
53
+ } | {
54
+ status: "unknown-module";
55
+ } | {
56
+ status: "not-in-studio";
57
+ } | {
58
+ status: "not-found";
59
+ };
60
+ /**
61
+ * The patch that fixes one error, built from the server's own reading of the
62
+ * module rather than from the error the caller saw.
63
+ *
64
+ * The caller names the error by where it is and which fix it wants; the error
65
+ * itself is found again here. That is what keeps a stale or hand-written
66
+ * request from steering `createFixPatch` (CLAUDE.md, "Adding a ValidationFix
67
+ * code"), and it is what lets an error reported at a FIELD be fixed at the
68
+ * media value it belongs to: `validateFiles` reports a wrong width at
69
+ * `…"hero"."width"`, while the fix rewrites `…"hero"`.
70
+ *
71
+ * Only {@link isStudioFix} codes are built. The others need a disk or a
72
+ * remote host this server may not have, and are the CLI's for now.
73
+ */
74
+ export declare function fixForStudio(ops: ValOps, input: StudioCheckInput & {
75
+ sourcePath: SourcePath;
76
+ fix: ValidationFix;
77
+ }): Promise<StudioFixResult>;
78
+ /**
79
+ * {@link FixFiles} over whatever store this server has: the disk in fs mode,
80
+ * the content service in http mode. A file a pending patch wrote is read from
81
+ * that patch, as `validateFiles` reads it, so a fix is made from the bytes
82
+ * the check was made from.
83
+ */
84
+ export declare function opsFixFiles(ops: ValOps, fileLastUpdatedByPatchId: PatchAnalysis["fileLastUpdatedByPatchId"]): FixFiles;
@@ -1,4 +1,4 @@
1
- export declare function uploadRemoteFile(contentHost: string, project: string, bucket: string, fileHash: string, fileExt: string, fileBuffer: Buffer, auth: {
1
+ export declare function uploadRemoteFile(contentHost: string, project: string, bucket: string, fileHash: string, fileExt: string, fileBuffer: Uint8Array, auth: {
2
2
  pat: string;
3
3
  } | {
4
4
  apiKey: string;
@@ -1,6 +1,7 @@
1
1
  import { type ModuleFilePath, type SerializedRecordSchema, type SerializedSchema, type SerializedVideoSchema, type Source, type SourcePath, type ValidationError } from "@valbuild/core";
2
2
  import { type Patch } from "@valbuild/core/patch";
3
3
  import type { FixPatchRemainingError } from "./createFixPatch.js";
4
+ import type { FixFiles } from "./fixFiles.js";
4
5
  import type { FixHandlerContext, FixHandlerResult, ModulePatch } from "./fixHandlers.js";
5
6
  import { type GalleryEntryKey } from "./galleryEntryKey.js";
6
7
  import type { Service } from "./Service.js";
@@ -129,8 +130,8 @@ export declare function videosetCheckAllFilesPatch({ projectRoot, apply, sourceP
129
130
  * from the file at the entry's KEY, and write only that, one op per field —
130
131
  * `alt` and anything already there are someone's, and stay.
131
132
  */
132
- export declare function videosetAddMetadataPatch({ projectRoot, sourcePath, validationError, moduleSource, moduleSchema, }: {
133
- projectRoot: string;
133
+ export declare function videosetAddMetadataPatch({ files, sourcePath, validationError, moduleSource, moduleSchema, }: {
134
+ files: FixFiles;
134
135
  sourcePath: SourcePath;
135
136
  validationError: ValidationError;
136
137
  moduleSource: Source | undefined;
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ var ts = require('typescript');
4
+ var ValRouterCore = require('./ValRouterCore-42772c21.cjs.prod.js');
5
+ var fp = require('@valbuild/core/fp');
6
+ var jsonValuesModule = require('./jsonValuesModule-d1bb8d8f.cjs.prod.js');
7
+ require('@valbuild/core');
8
+ require('@valbuild/shared/internal');
9
+ require('zod');
10
+ require('zod-validation-error');
11
+ require('@valbuild/core/patch');
12
+ require('image-size');
13
+
14
+ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
15
+
16
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
17
+
18
+ /**
19
+ * One `.jsonValues()` entry's content, as it was at a commit.
20
+ *
21
+ * The history pane renders a commit through the real field components, and a
22
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
23
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
24
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
25
+ * fetch from, so every entry stayed a marker and rendered as though the field
26
+ * were empty. Which is worse than an error: an empty value is a claim, and it
27
+ * was a false one.
28
+ *
29
+ * ## Why this needs the module's own `.val.ts`
30
+ *
31
+ * There is no derivable relationship between a record key and its entry file.
32
+ * From the example app:
33
+ *
34
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
35
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
36
+ *
37
+ * The key is a route and the path is whatever the author wrote. The only place
38
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
39
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
40
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
41
+ * its text - so the text is fetched from git at that commit and parsed here.
42
+ *
43
+ * Two round trips per entry, then: the module, and the entry. The module's
44
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
45
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
46
+ * knows how long a commit stays interesting.
47
+ */
48
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
49
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
50
+ if (fp.result.isErr(moduleGitPath)) {
51
+ return moduleGitPath;
52
+ }
53
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
54
+ if (fp.result.isErr(moduleRes)) {
55
+ return moduleRes;
56
+ }
57
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, ValRouterCore.bytesToUtf8(moduleRes.value), key);
58
+ if (fp.result.isErr(entryPath)) {
59
+ return entryPath;
60
+ }
61
+ // The entry path resolved above is PROJECT-relative, like the module path it
62
+ // came from; the git path needs the root in front of it, and the ops are what
63
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
64
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
65
+ // the same question and one implementation of it is the point.
66
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
67
+ if (fp.result.isErr(entryGitPath)) {
68
+ return entryGitPath;
69
+ }
70
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
71
+ if (fp.result.isErr(entryRes)) {
72
+ return entryRes;
73
+ }
74
+ try {
75
+ return fp.result.ok(JSON.parse(ValRouterCore.bytesToUtf8(entryRes.value)));
76
+ } catch (err) {
77
+ return fp.result.err({
78
+ kind: "file-unavailable",
79
+ gitPath: entryGitPath.value,
80
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
81
+ });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The `import()` path a key's entry is behind, from the module's text.
87
+ *
88
+ * Exported for its own test: this is the part with a real chance of being
89
+ * wrong, and it is pure.
90
+ */
91
+ function findEntryImportPath(moduleFilePath,
92
+ /**
93
+ * The same module, repository-relative, for error reporting only.
94
+ *
95
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
96
+ * other history helper reports, and what a reader can paste into a `git show`
97
+ * - so reporting the project-relative ModuleFilePath here named a file that
98
+ * does not exist at that path for any project not rooted at the repo root.
99
+ */
100
+ moduleGitPath, valTsSource, key) {
101
+ const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
102
+ let analysis;
103
+ try {
104
+ analysis = jsonValuesModule.analyzeValModule(sourceFile);
105
+ } catch (err) {
106
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
107
+ // the file we could not use, and adding a wire kind for "unparseable"
108
+ // would be a new case every reader has to handle to say the same thing.
109
+ return fp.result.err({
110
+ kind: "file-unavailable",
111
+ gitPath: moduleGitPath,
112
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
113
+ });
114
+ }
115
+ if (fp.result.isErr(analysis)) {
116
+ return fp.result.err({
117
+ kind: "file-unavailable",
118
+ gitPath: moduleGitPath,
119
+ message: "could not read the module at this commit"
120
+ });
121
+ }
122
+ const entries = jsonValuesModule.analyzeJsonValuesEntries(analysis.value.source);
123
+ const entry = entries.get(key);
124
+ if (entry === undefined) {
125
+ /*
126
+ * Reported, not treated as empty. A key with no entry at this commit means
127
+ * the entry was added later - so "there is nothing to show for it here" is
128
+ * the true answer, and rendering an empty field instead would say the
129
+ * author had left it blank.
130
+ */
131
+ return fp.result.err({
132
+ kind: "file-unavailable",
133
+ gitPath: moduleGitPath,
134
+ message: `'${key}' had no entry at this commit`
135
+ });
136
+ }
137
+ const moduleDir = ValRouterCore.posixPath.dirname(moduleFilePath);
138
+ return fp.result.ok(ValRouterCore.posixPath.join(moduleDir, entry.importPath));
139
+ }
140
+
141
+ exports.findEntryImportPath = findEntryImportPath;
142
+ exports.getJsonEntryAtCommit = getJsonEntryAtCommit;
@@ -0,0 +1,135 @@
1
+ import ts from 'typescript';
2
+ import { F as bytesToUtf8, G as posixPath } from './ValRouterCore-7ca3dc68.esm.js';
3
+ import { result } from '@valbuild/core/fp';
4
+ import { a as analyzeValModule, c as analyzeJsonValuesEntries } from './jsonValuesModule-427d68c8.esm.js';
5
+ import '@valbuild/core';
6
+ import '@valbuild/shared/internal';
7
+ import 'zod';
8
+ import 'zod-validation-error';
9
+ import '@valbuild/core/patch';
10
+ import 'image-size';
11
+
12
+ /**
13
+ * One `.jsonValues()` entry's content, as it was at a commit.
14
+ *
15
+ * The history pane renders a commit through the real field components, and a
16
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
17
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
18
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
19
+ * fetch from, so every entry stayed a marker and rendered as though the field
20
+ * were empty. Which is worse than an error: an empty value is a claim, and it
21
+ * was a false one.
22
+ *
23
+ * ## Why this needs the module's own `.val.ts`
24
+ *
25
+ * There is no derivable relationship between a record key and its entry file.
26
+ * From the example app:
27
+ *
28
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
29
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
30
+ *
31
+ * The key is a route and the path is whatever the author wrote. The only place
32
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
33
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
34
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
35
+ * its text - so the text is fetched from git at that commit and parsed here.
36
+ *
37
+ * Two round trips per entry, then: the module, and the entry. The module's
38
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
39
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
40
+ * knows how long a commit stays interesting.
41
+ */
42
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
43
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
44
+ if (result.isErr(moduleGitPath)) {
45
+ return moduleGitPath;
46
+ }
47
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
48
+ if (result.isErr(moduleRes)) {
49
+ return moduleRes;
50
+ }
51
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, bytesToUtf8(moduleRes.value), key);
52
+ if (result.isErr(entryPath)) {
53
+ return entryPath;
54
+ }
55
+ // The entry path resolved above is PROJECT-relative, like the module path it
56
+ // came from; the git path needs the root in front of it, and the ops are what
57
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
58
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
59
+ // the same question and one implementation of it is the point.
60
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
61
+ if (result.isErr(entryGitPath)) {
62
+ return entryGitPath;
63
+ }
64
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
65
+ if (result.isErr(entryRes)) {
66
+ return entryRes;
67
+ }
68
+ try {
69
+ return result.ok(JSON.parse(bytesToUtf8(entryRes.value)));
70
+ } catch (err) {
71
+ return result.err({
72
+ kind: "file-unavailable",
73
+ gitPath: entryGitPath.value,
74
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
75
+ });
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The `import()` path a key's entry is behind, from the module's text.
81
+ *
82
+ * Exported for its own test: this is the part with a real chance of being
83
+ * wrong, and it is pure.
84
+ */
85
+ function findEntryImportPath(moduleFilePath,
86
+ /**
87
+ * The same module, repository-relative, for error reporting only.
88
+ *
89
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
90
+ * other history helper reports, and what a reader can paste into a `git show`
91
+ * - so reporting the project-relative ModuleFilePath here named a file that
92
+ * does not exist at that path for any project not rooted at the repo root.
93
+ */
94
+ moduleGitPath, valTsSource, key) {
95
+ const sourceFile = ts.createSourceFile(moduleFilePath, valTsSource, ts.ScriptTarget.ES2015, true);
96
+ let analysis;
97
+ try {
98
+ analysis = analyzeValModule(sourceFile);
99
+ } catch (err) {
100
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
101
+ // the file we could not use, and adding a wire kind for "unparseable"
102
+ // would be a new case every reader has to handle to say the same thing.
103
+ return result.err({
104
+ kind: "file-unavailable",
105
+ gitPath: moduleGitPath,
106
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
107
+ });
108
+ }
109
+ if (result.isErr(analysis)) {
110
+ return result.err({
111
+ kind: "file-unavailable",
112
+ gitPath: moduleGitPath,
113
+ message: "could not read the module at this commit"
114
+ });
115
+ }
116
+ const entries = analyzeJsonValuesEntries(analysis.value.source);
117
+ const entry = entries.get(key);
118
+ if (entry === undefined) {
119
+ /*
120
+ * Reported, not treated as empty. A key with no entry at this commit means
121
+ * the entry was added later - so "there is nothing to show for it here" is
122
+ * the true answer, and rendering an empty field instead would say the
123
+ * author had left it blank.
124
+ */
125
+ return result.err({
126
+ kind: "file-unavailable",
127
+ gitPath: moduleGitPath,
128
+ message: `'${key}' had no entry at this commit`
129
+ });
130
+ }
131
+ const moduleDir = posixPath.dirname(moduleFilePath);
132
+ return result.ok(posixPath.join(moduleDir, entry.importPath));
133
+ }
134
+
135
+ export { findEntryImportPath, getJsonEntryAtCommit };