@valbuild/server 0.146.4 → 0.147.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 (37) hide show
  1. package/CHANGELOG.md +21 -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-3fd3d444.cjs.dev.js +9897 -0
  9. package/dist/ValRouterCore-7ffcc08c.cjs.prod.js +9897 -0
  10. package/dist/ValRouterCore-f5004f8f.esm.js +9866 -0
  11. package/dist/declarations/src/ValOps.d.ts +13 -1
  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 +9 -0
  15. package/dist/declarations/src/ValRouter.d.ts +1 -1
  16. package/dist/declarations/src/ValServer.d.ts +9 -2
  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/fixHandlers.d.ts +1 -1
  20. package/dist/declarations/src/linkCheck/probeUrl.d.ts +40 -0
  21. package/dist/declarations/src/platform.d.ts +39 -0
  22. package/dist/getJsonEntryAtCommit-1e08d724.esm.js +136 -0
  23. package/dist/getJsonEntryAtCommit-df65d861.cjs.prod.js +143 -0
  24. package/dist/getJsonEntryAtCommit-e96beb9e.cjs.dev.js +143 -0
  25. package/dist/jsonValuesModule-427d68c8.esm.js +273 -0
  26. package/dist/jsonValuesModule-8ae503a8.cjs.dev.js +288 -0
  27. package/dist/jsonValuesModule-d1bb8d8f.cjs.prod.js +288 -0
  28. package/dist/ops-0d05feaf.esm.js +353 -0
  29. package/dist/ops-b15239ac.cjs.prod.js +361 -0
  30. package/dist/ops-f37d18c6.cjs.dev.js +361 -0
  31. package/dist/publishKit-580f558a.cjs.prod.js +41 -0
  32. package/dist/publishKit-5865095c.cjs.dev.js +41 -0
  33. package/dist/publishKit-bc8df53d.esm.js +31 -0
  34. package/dist/valbuild-server.cjs.dev.js +7902 -18215
  35. package/dist/valbuild-server.cjs.prod.js +7902 -18215
  36. package/dist/valbuild-server.esm.js +7758 -18070
  37. package/package.json +18 -5
@@ -1,10 +1,11 @@
1
+ import { Buffer } from "buffer";
1
2
  import { MediaSource, FileMetadata, FileSource, ImageMetadata, ModuleFilePath, PatchId, Schema, SelectorSource, SerializedSchema, Source, SourcePath, ValConfig, ValModules, ValidationError } from "@valbuild/core";
2
3
  import { result } from "@valbuild/core/fp";
3
4
  import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
4
5
  import type { HistoryError } from "./history/HistoryError.js";
5
6
  import type { PlanRefusalErrorT } from "@valbuild/shared/internal";
6
7
  import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
7
- import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
8
+ import type { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
8
9
  import { ParentPatchId } from "@valbuild/core";
9
10
  import type { ReifiedPreview } from "@valbuild/core";
10
11
  import { ValCommit, ValDeployment } from "@valbuild/shared/internal";
@@ -598,6 +599,17 @@ export declare abstract class ValOps {
598
599
  * the ones that create patches and publish.
599
600
  */
600
601
  abstract readonly requiresAuth: boolean;
602
+ /**
603
+ * Which implementation this is, for the routes that differ by mode.
604
+ *
605
+ * A tag rather than `instanceof`, because `instanceof ValOpsFS` needs the
606
+ * class itself, and importing the class imports `fs`: `ValServer` asking
607
+ * which mode it is in was what put the filesystem and the content-service
608
+ * client in the module graph of a server that runs in a browser tab (the
609
+ * demo, `@valbuild/server/browser`). `isFsOps` and friends in `opsKind.ts`
610
+ * narrow on it with type-only imports.
611
+ */
612
+ abstract readonly kind: "fs" | "http" | "memory";
601
613
  abstract onInit(baseSha: BaseSha, schemaSha: SchemaSha): Promise<void>;
602
614
  abstract fetchPatches<ExcludePatchOps extends boolean>(filters: {
603
615
  patchIds?: PatchId[];
@@ -12,6 +12,7 @@ export declare class ValOpsFS extends ValOps {
12
12
  private readonly host;
13
13
  /** The developer's own working tree. See {@link ValOps.patchesAreLocal}. */
14
14
  readonly patchesAreLocal = true;
15
+ readonly kind = "fs";
15
16
  /**
16
17
  * The developer's own machine, where there is no credential to require and
17
18
  * nothing to protect it from. See {@link ValOps.requiresAuth}.
@@ -43,6 +43,7 @@ export declare class ValOpsHttp extends ValOps {
43
43
  private readonly root;
44
44
  /** Val's content service owns the store. See {@link ValOps.patchesAreLocal}. */
45
45
  readonly patchesAreLocal = false;
46
+ readonly kind = "http";
46
47
  /** See {@link ValOps.requiresAuth}. */
47
48
  readonly requiresAuth = true;
48
49
  /**
@@ -121,6 +121,14 @@ export type ValOpsMemoryOptions = ValOpsOptions & {
121
121
  * startup.
122
122
  */
123
123
  unsafelyAllowUnauthenticated?: boolean;
124
+ /**
125
+ * Whether to warn that the server is unauthenticated. On by default, because
126
+ * a deployed server serving without authentication is a server anyone can
127
+ * write to. Off only where nobody else CAN reach it: a server running in a
128
+ * browser tab (`@valbuild/server/browser`), where the warning would be
129
+ * printed into a visitor's console about a port that does not exist.
130
+ */
131
+ warnUnauthenticated?: boolean;
124
132
  /**
125
133
  * Val's content host, for pushing remote files at publish.
126
134
  *
@@ -168,6 +176,7 @@ export declare class ValOpsMemory extends ValOps {
168
176
  * in. Where the two differ is not something a route asks about.
169
177
  */
170
178
  readonly patchesAreLocal = true;
179
+ readonly kind = "memory";
171
180
  /**
172
181
  * Required, unless the host explicitly takes the boundary itself.
173
182
  *
@@ -1,5 +1,5 @@
1
1
  import { ValConfig, ValModules } from "@valbuild/core";
2
- import { ValServerGenericResult } from "@valbuild/shared/internal";
2
+ import type { ValServerGenericResult } from "@valbuild/shared/internal";
3
3
  import { ValServer, ValServerCallbacks } from "./ValServer.js";
4
4
  type Versions = {
5
5
  versions?: {
@@ -1,9 +1,11 @@
1
+ import { Buffer } from "buffer";
1
2
  import type { ValContentVersion, ValProposal } from "./proposal.js";
2
3
  import { ValModules, ValConfig } from "@valbuild/core";
3
4
  import { Api, ServerOf } from "@valbuild/shared/internal";
4
5
  import { z } from "zod";
5
6
  import { AuthorId, CommitSha, type GenericErrorMessage, type PreparedCommit } from "./ValOps.js";
6
- import { type ValPatchStore } from "./ValOpsMemory.js";
7
+ import type { ValPatchStore } from "./ValOpsMemory.js";
8
+ import type { ValServerPlatform } from "./platform.js";
7
9
  export type ValServerOptions = {
8
10
  route: string;
9
11
  valEnableRedirectUrl?: string;
@@ -169,7 +171,12 @@ export type ValServerConfig = ValServerOptions & ({
169
171
  config: ValConfig;
170
172
  });
171
173
  export type ValServer = ServerOf<Api>;
172
- export declare const ValServer: (valModules: ValModules, options: ValServerConfig, callbacks: ValServerCallbacks) => ServerOf<Api>;
174
+ export declare const ValServer: (valModules: ValModules, options: ValServerConfig, callbacks: ValServerCallbacks,
175
+ /**
176
+ * The Node-only parts, passed in. `createValServer` passes `nodePlatform`;
177
+ * the demo passes the browser one. See {@link ValServerPlatform}.
178
+ */
179
+ platform: ValServerPlatform) => ServerOf<Api>;
173
180
  export type ValServerCallbacks = {
174
181
  isEnabled: () => Promise<boolean>;
175
182
  onEnable: (success: boolean) => Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { type ValConfig, type ValModules } from "@valbuild/core";
2
+ import type { ValPatchStore } from "../ValOpsMemory.js";
3
+ /** A `fetch`, as far as the Studio's client calls one. */
4
+ export type InTabFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
5
+ /**
6
+ * Val's API, served in this tab: a `fetch` that never touches the network.
7
+ *
8
+ * The whole server -- the router's request parsing, `ValServer`'s routes,
9
+ * memory mode's patch chain -- runs here, exactly as it runs on a server.
10
+ * What is removed is the hop between them, and nothing else, which is why the
11
+ * demo built on it cannot drift from the product: there is no second
12
+ * implementation to drift. See `docs/plans/demo-mode.md`.
13
+ *
14
+ * Memory mode with no source files, because nothing here publishes (only a
15
+ * publish reads `.val.ts` text), and without authentication, because the only
16
+ * caller is the page this runs in.
17
+ */
18
+ export declare function createInTabValFetch(valModules: ValModules, options: {
19
+ config: ValConfig;
20
+ /**
21
+ * Where edits live. Memory by default, which is what the demo wants: the
22
+ * edits last as long as the tab's page does, and a reload starts over.
23
+ */
24
+ patchStore?: ValPatchStore;
25
+ /** The path the Studio's client calls. */
26
+ route?: string;
27
+ }): InTabFetch;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@valbuild/server/browser`: Val's server, for a browser tab.
3
+ *
4
+ * Nothing reachable from here may import a Node builtin --
5
+ * `noNodeFromBrowser.test.ts` walks the graph and fails if one appears. The
6
+ * Node-only parts of the server reach it through `ValServerPlatform`, and this
7
+ * entry passes the browser's.
8
+ */
9
+ export { createInTabValFetch, type InTabFetch } from "./inTabFetch.js";
@@ -247,6 +247,6 @@ export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Pr
247
247
  */
248
248
  export declare function handleViewCheckModule(ctx: FixHandlerContext): Promise<FixHandlerResult>;
249
249
  export declare function handleExternalUpload(): Promise<FixHandlerResult>;
250
- export declare const currentFixHandlers: Record<Exclude<ValidationFix, "keyof:check-keys" | "image:check-gallery" | "file:check-gallery" | "video:check-gallery" | "router:check-route" | "locale:check-locale" | "record:fill-keys">, FixHandler>;
250
+ export declare const currentFixHandlers: Record<Exclude<ValidationFix, "keyof:check-keys" | "image:check-gallery" | "file:check-gallery" | "video:check-gallery" | "router:check-route" | "locale:check-locale" | "router:check-locale-param" | "record:fill-keys">, FixHandler>;
251
251
  export declare const fixHandlers: Record<string, FixHandler>;
252
252
  export declare function createDefaultValFSHost(): IValFSHost;
@@ -0,0 +1,40 @@
1
+ import https from "node:https";
2
+ import { type LookupFunction } from "node:net";
3
+ /**
4
+ * Opening one external URL and reporting what answered.
5
+ *
6
+ * Mirrors `ExternalUrlProbeResult` in the Studio - the two are checked against
7
+ * each other by the API route's zod schema, which is the only place they meet.
8
+ */
9
+ export type ProbeResult = {
10
+ kind: "answered";
11
+ code: number;
12
+ finalUrl: string;
13
+ ms: number;
14
+ } | {
15
+ kind: "unreachable";
16
+ message: string;
17
+ } | {
18
+ kind: "timeout";
19
+ ms: number;
20
+ } | {
21
+ kind: "skipped";
22
+ message: string;
23
+ };
24
+ export type ProbeOptions = {
25
+ /** The whole check, redirects included. */
26
+ timeoutMs?: number;
27
+ /** How many redirects to follow before calling it a loop. */
28
+ maxRedirects?: number;
29
+ /**
30
+ * The transport, injected so the redirect and status logic can be tested
31
+ * without a network. Production passes nothing and gets `node:https`.
32
+ */
33
+ request?: typeof https.request;
34
+ /** Injected for the same reason, and never to relax the address guard. */
35
+ now?: () => number;
36
+ };
37
+ export declare const DEFAULT_TIMEOUT_MS = 5000;
38
+ export declare const DEFAULT_MAX_REDIRECTS = 5;
39
+ export declare function guardedLookup(lookup: LookupFunction): LookupFunction;
40
+ export declare function probeUrl(rawUrl: string, options?: ProbeOptions): Promise<ProbeResult>;
@@ -0,0 +1,39 @@
1
+ import type { ValModules } from "@valbuild/core";
2
+ import type { ValOpsFS } from "./ValOpsFS.js";
3
+ import type { ValOpsHttp } from "./ValOpsHttp.js";
4
+ import type { ValOpsMemory } from "./ValOpsMemory.js";
5
+ import type { ValServerConfig } from "./ValServer.js";
6
+ import type { ResolveRemoteFileAuthResult } from "./valServerConfig.js";
7
+ import type { JwtResult } from "./jwt.js";
8
+ import type { ProbeResult } from "./linkCheck/probeUrl.js";
9
+ /**
10
+ * What `ValServer` needs from the machine it runs on.
11
+ *
12
+ * Everything else in `ValServer` is request handling, and runs anywhere. These
13
+ * are the parts that reach for Node -- a filesystem, a socket, a keyed hash --
14
+ * and they are passed in rather than imported so that the same server can run
15
+ * in a browser tab: the demo (`@valbuild/server/browser`) serves a memory-mode
16
+ * server to the Studio with no network in between.
17
+ *
18
+ * `nodePlatform` is the real one, and `createValServer` passes it. A function
19
+ * here is whatever an implementation can honestly answer: the browser one has
20
+ * no filesystem to read a token from, so it answers as a server without one
21
+ * does.
22
+ */
23
+ export type ValServerPlatform = {
24
+ /** Builds the data layer the config asks for. See `createValOps`. */
25
+ createOps: (valModules: ValModules, options: ValServerConfig) => ValOpsFS | ValOpsHttp | ValOpsMemory;
26
+ /** Whether an external URL answers. See `linkCheck/probeUrl`. */
27
+ probeUrl: (url: string) => Promise<ProbeResult>;
28
+ /** The session cookie's signature. See `jwt.ts`. */
29
+ jwt: {
30
+ verifyJwt: (token: string, secretKey: string) => JwtResult;
31
+ decodeJwtWithoutVerifying: (token: string) => JwtResult;
32
+ encodeJwt: (payload: object, sessionKey: string) => string;
33
+ getExpire: () => number;
34
+ };
35
+ /** The developer's `val login` token. See `readValLoginToken`. */
36
+ readValLoginToken: (options: ValServerConfig) => Promise<string | null>;
37
+ /** Which credential talks to the content host. See `resolveRemoteFileAuth`. */
38
+ resolveRemoteFileAuth: (options: ValServerConfig) => Promise<ResolveRemoteFileAuthResult>;
39
+ };
@@ -0,0 +1,136 @@
1
+ import ts from 'typescript';
2
+ import { x as posixPath } from './ValRouterCore-f5004f8f.esm.js';
3
+ import { result } from '@valbuild/core/fp';
4
+ import { a as analyzeValModule, c as analyzeJsonValuesEntries } from './jsonValuesModule-427d68c8.esm.js';
5
+ import 'buffer';
6
+ import '@valbuild/core';
7
+ import '@valbuild/shared/internal';
8
+ import 'zod';
9
+ import 'zod-validation-error';
10
+ import '@valbuild/core/patch';
11
+ import 'image-size';
12
+
13
+ /**
14
+ * One `.jsonValues()` entry's content, as it was at a commit.
15
+ *
16
+ * The history pane renders a commit through the real field components, and a
17
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
18
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
19
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
20
+ * fetch from, so every entry stayed a marker and rendered as though the field
21
+ * were empty. Which is worse than an error: an empty value is a claim, and it
22
+ * was a false one.
23
+ *
24
+ * ## Why this needs the module's own `.val.ts`
25
+ *
26
+ * There is no derivable relationship between a record key and its entry file.
27
+ * From the example app:
28
+ *
29
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
30
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
31
+ *
32
+ * The key is a route and the path is whatever the author wrote. The only place
33
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
34
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
35
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
36
+ * its text - so the text is fetched from git at that commit and parsed here.
37
+ *
38
+ * Two round trips per entry, then: the module, and the entry. The module's
39
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
40
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
41
+ * knows how long a commit stays interesting.
42
+ */
43
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
44
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
45
+ if (result.isErr(moduleGitPath)) {
46
+ return moduleGitPath;
47
+ }
48
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
49
+ if (result.isErr(moduleRes)) {
50
+ return moduleRes;
51
+ }
52
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
53
+ if (result.isErr(entryPath)) {
54
+ return entryPath;
55
+ }
56
+ // The entry path resolved above is PROJECT-relative, like the module path it
57
+ // came from; the git path needs the root in front of it, and the ops are what
58
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
59
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
60
+ // the same question and one implementation of it is the point.
61
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
62
+ if (result.isErr(entryGitPath)) {
63
+ return entryGitPath;
64
+ }
65
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
66
+ if (result.isErr(entryRes)) {
67
+ return entryRes;
68
+ }
69
+ try {
70
+ return result.ok(JSON.parse(entryRes.value.toString("utf-8")));
71
+ } catch (err) {
72
+ return result.err({
73
+ kind: "file-unavailable",
74
+ gitPath: entryGitPath.value,
75
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
76
+ });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The `import()` path a key's entry is behind, from the module's text.
82
+ *
83
+ * Exported for its own test: this is the part with a real chance of being
84
+ * wrong, and it is pure.
85
+ */
86
+ function findEntryImportPath(moduleFilePath,
87
+ /**
88
+ * The same module, repository-relative, for error reporting only.
89
+ *
90
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
91
+ * other history helper reports, and what a reader can paste into a `git show`
92
+ * - so reporting the project-relative ModuleFilePath here named a file that
93
+ * does not exist at that path for any project not rooted at the repo root.
94
+ */
95
+ moduleGitPath, valTsSource, key) {
96
+ const sourceFile = ts.createSourceFile(moduleFilePath, valTsSource, ts.ScriptTarget.ES2015, true);
97
+ let analysis;
98
+ try {
99
+ analysis = analyzeValModule(sourceFile);
100
+ } catch (err) {
101
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
102
+ // the file we could not use, and adding a wire kind for "unparseable"
103
+ // would be a new case every reader has to handle to say the same thing.
104
+ return result.err({
105
+ kind: "file-unavailable",
106
+ gitPath: moduleGitPath,
107
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
108
+ });
109
+ }
110
+ if (result.isErr(analysis)) {
111
+ return result.err({
112
+ kind: "file-unavailable",
113
+ gitPath: moduleGitPath,
114
+ message: "could not read the module at this commit"
115
+ });
116
+ }
117
+ const entries = analyzeJsonValuesEntries(analysis.value.source);
118
+ const entry = entries.get(key);
119
+ if (entry === undefined) {
120
+ /*
121
+ * Reported, not treated as empty. A key with no entry at this commit means
122
+ * the entry was added later - so "there is nothing to show for it here" is
123
+ * the true answer, and rendering an empty field instead would say the
124
+ * author had left it blank.
125
+ */
126
+ return result.err({
127
+ kind: "file-unavailable",
128
+ gitPath: moduleGitPath,
129
+ message: `'${key}' had no entry at this commit`
130
+ });
131
+ }
132
+ const moduleDir = posixPath.dirname(moduleFilePath);
133
+ return result.ok(posixPath.join(moduleDir, entry.importPath));
134
+ }
135
+
136
+ export { findEntryImportPath, getJsonEntryAtCommit };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ var ts = require('typescript');
4
+ var ValRouterCore = require('./ValRouterCore-7ffcc08c.cjs.prod.js');
5
+ var fp = require('@valbuild/core/fp');
6
+ var jsonValuesModule = require('./jsonValuesModule-d1bb8d8f.cjs.prod.js');
7
+ require('buffer');
8
+ require('@valbuild/core');
9
+ require('@valbuild/shared/internal');
10
+ require('zod');
11
+ require('zod-validation-error');
12
+ require('@valbuild/core/patch');
13
+ require('image-size');
14
+
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
16
+
17
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
18
+
19
+ /**
20
+ * One `.jsonValues()` entry's content, as it was at a commit.
21
+ *
22
+ * The history pane renders a commit through the real field components, and a
23
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
24
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
25
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
26
+ * fetch from, so every entry stayed a marker and rendered as though the field
27
+ * were empty. Which is worse than an error: an empty value is a claim, and it
28
+ * was a false one.
29
+ *
30
+ * ## Why this needs the module's own `.val.ts`
31
+ *
32
+ * There is no derivable relationship between a record key and its entry file.
33
+ * From the example app:
34
+ *
35
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
36
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
37
+ *
38
+ * The key is a route and the path is whatever the author wrote. The only place
39
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
40
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
41
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
42
+ * its text - so the text is fetched from git at that commit and parsed here.
43
+ *
44
+ * Two round trips per entry, then: the module, and the entry. The module's
45
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
46
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
47
+ * knows how long a commit stays interesting.
48
+ */
49
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
50
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
51
+ if (fp.result.isErr(moduleGitPath)) {
52
+ return moduleGitPath;
53
+ }
54
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
55
+ if (fp.result.isErr(moduleRes)) {
56
+ return moduleRes;
57
+ }
58
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
59
+ if (fp.result.isErr(entryPath)) {
60
+ return entryPath;
61
+ }
62
+ // The entry path resolved above is PROJECT-relative, like the module path it
63
+ // came from; the git path needs the root in front of it, and the ops are what
64
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
65
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
66
+ // the same question and one implementation of it is the point.
67
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
68
+ if (fp.result.isErr(entryGitPath)) {
69
+ return entryGitPath;
70
+ }
71
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
72
+ if (fp.result.isErr(entryRes)) {
73
+ return entryRes;
74
+ }
75
+ try {
76
+ return fp.result.ok(JSON.parse(entryRes.value.toString("utf-8")));
77
+ } catch (err) {
78
+ return fp.result.err({
79
+ kind: "file-unavailable",
80
+ gitPath: entryGitPath.value,
81
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
82
+ });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The `import()` path a key's entry is behind, from the module's text.
88
+ *
89
+ * Exported for its own test: this is the part with a real chance of being
90
+ * wrong, and it is pure.
91
+ */
92
+ function findEntryImportPath(moduleFilePath,
93
+ /**
94
+ * The same module, repository-relative, for error reporting only.
95
+ *
96
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
97
+ * other history helper reports, and what a reader can paste into a `git show`
98
+ * - so reporting the project-relative ModuleFilePath here named a file that
99
+ * does not exist at that path for any project not rooted at the repo root.
100
+ */
101
+ moduleGitPath, valTsSource, key) {
102
+ const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
103
+ let analysis;
104
+ try {
105
+ analysis = jsonValuesModule.analyzeValModule(sourceFile);
106
+ } catch (err) {
107
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
108
+ // the file we could not use, and adding a wire kind for "unparseable"
109
+ // would be a new case every reader has to handle to say the same thing.
110
+ return fp.result.err({
111
+ kind: "file-unavailable",
112
+ gitPath: moduleGitPath,
113
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
114
+ });
115
+ }
116
+ if (fp.result.isErr(analysis)) {
117
+ return fp.result.err({
118
+ kind: "file-unavailable",
119
+ gitPath: moduleGitPath,
120
+ message: "could not read the module at this commit"
121
+ });
122
+ }
123
+ const entries = jsonValuesModule.analyzeJsonValuesEntries(analysis.value.source);
124
+ const entry = entries.get(key);
125
+ if (entry === undefined) {
126
+ /*
127
+ * Reported, not treated as empty. A key with no entry at this commit means
128
+ * the entry was added later - so "there is nothing to show for it here" is
129
+ * the true answer, and rendering an empty field instead would say the
130
+ * author had left it blank.
131
+ */
132
+ return fp.result.err({
133
+ kind: "file-unavailable",
134
+ gitPath: moduleGitPath,
135
+ message: `'${key}' had no entry at this commit`
136
+ });
137
+ }
138
+ const moduleDir = ValRouterCore.posixPath.dirname(moduleFilePath);
139
+ return fp.result.ok(ValRouterCore.posixPath.join(moduleDir, entry.importPath));
140
+ }
141
+
142
+ exports.findEntryImportPath = findEntryImportPath;
143
+ exports.getJsonEntryAtCommit = getJsonEntryAtCommit;
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ var ts = require('typescript');
4
+ var ValRouterCore = require('./ValRouterCore-3fd3d444.cjs.dev.js');
5
+ var fp = require('@valbuild/core/fp');
6
+ var jsonValuesModule = require('./jsonValuesModule-8ae503a8.cjs.dev.js');
7
+ require('buffer');
8
+ require('@valbuild/core');
9
+ require('@valbuild/shared/internal');
10
+ require('zod');
11
+ require('zod-validation-error');
12
+ require('@valbuild/core/patch');
13
+ require('image-size');
14
+
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
16
+
17
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
18
+
19
+ /**
20
+ * One `.jsonValues()` entry's content, as it was at a commit.
21
+ *
22
+ * The history pane renders a commit through the real field components, and a
23
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
24
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
25
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
26
+ * fetch from, so every entry stayed a marker and rendered as though the field
27
+ * were empty. Which is worse than an error: an empty value is a claim, and it
28
+ * was a false one.
29
+ *
30
+ * ## Why this needs the module's own `.val.ts`
31
+ *
32
+ * There is no derivable relationship between a record key and its entry file.
33
+ * From the example app:
34
+ *
35
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
36
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
37
+ *
38
+ * The key is a route and the path is whatever the author wrote. The only place
39
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
40
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
41
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
42
+ * its text - so the text is fetched from git at that commit and parsed here.
43
+ *
44
+ * Two round trips per entry, then: the module, and the entry. The module's
45
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
46
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
47
+ * knows how long a commit stays interesting.
48
+ */
49
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
50
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
51
+ if (fp.result.isErr(moduleGitPath)) {
52
+ return moduleGitPath;
53
+ }
54
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
55
+ if (fp.result.isErr(moduleRes)) {
56
+ return moduleRes;
57
+ }
58
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
59
+ if (fp.result.isErr(entryPath)) {
60
+ return entryPath;
61
+ }
62
+ // The entry path resolved above is PROJECT-relative, like the module path it
63
+ // came from; the git path needs the root in front of it, and the ops are what
64
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
65
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
66
+ // the same question and one implementation of it is the point.
67
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
68
+ if (fp.result.isErr(entryGitPath)) {
69
+ return entryGitPath;
70
+ }
71
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
72
+ if (fp.result.isErr(entryRes)) {
73
+ return entryRes;
74
+ }
75
+ try {
76
+ return fp.result.ok(JSON.parse(entryRes.value.toString("utf-8")));
77
+ } catch (err) {
78
+ return fp.result.err({
79
+ kind: "file-unavailable",
80
+ gitPath: entryGitPath.value,
81
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
82
+ });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The `import()` path a key's entry is behind, from the module's text.
88
+ *
89
+ * Exported for its own test: this is the part with a real chance of being
90
+ * wrong, and it is pure.
91
+ */
92
+ function findEntryImportPath(moduleFilePath,
93
+ /**
94
+ * The same module, repository-relative, for error reporting only.
95
+ *
96
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
97
+ * other history helper reports, and what a reader can paste into a `git show`
98
+ * - so reporting the project-relative ModuleFilePath here named a file that
99
+ * does not exist at that path for any project not rooted at the repo root.
100
+ */
101
+ moduleGitPath, valTsSource, key) {
102
+ const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
103
+ let analysis;
104
+ try {
105
+ analysis = jsonValuesModule.analyzeValModule(sourceFile);
106
+ } catch (err) {
107
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
108
+ // the file we could not use, and adding a wire kind for "unparseable"
109
+ // would be a new case every reader has to handle to say the same thing.
110
+ return fp.result.err({
111
+ kind: "file-unavailable",
112
+ gitPath: moduleGitPath,
113
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
114
+ });
115
+ }
116
+ if (fp.result.isErr(analysis)) {
117
+ return fp.result.err({
118
+ kind: "file-unavailable",
119
+ gitPath: moduleGitPath,
120
+ message: "could not read the module at this commit"
121
+ });
122
+ }
123
+ const entries = jsonValuesModule.analyzeJsonValuesEntries(analysis.value.source);
124
+ const entry = entries.get(key);
125
+ if (entry === undefined) {
126
+ /*
127
+ * Reported, not treated as empty. A key with no entry at this commit means
128
+ * the entry was added later - so "there is nothing to show for it here" is
129
+ * the true answer, and rendering an empty field instead would say the
130
+ * author had left it blank.
131
+ */
132
+ return fp.result.err({
133
+ kind: "file-unavailable",
134
+ gitPath: moduleGitPath,
135
+ message: `'${key}' had no entry at this commit`
136
+ });
137
+ }
138
+ const moduleDir = ValRouterCore.posixPath.dirname(moduleFilePath);
139
+ return fp.result.ok(ValRouterCore.posixPath.join(moduleDir, entry.importPath));
140
+ }
141
+
142
+ exports.findEntryImportPath = findEntryImportPath;
143
+ exports.getJsonEntryAtCommit = getJsonEntryAtCommit;