@valbuild/server 0.144.1 → 0.144.3

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,30 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.144.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`99fcac5`](https://github.com/valbuild/val/commit/99fcac527f2d5ba37c4b132ef08189b955d69e51), [`0e09e9c`](https://github.com/valbuild/val/commit/0e09e9cdac4918bdfa1c01a92a8968a516f8c0c6)]:
8
+ - @valbuild/ui@0.144.3
9
+
10
+ ## 0.144.2
11
+
12
+ ### Patch Changes
13
+
14
+ - [#832](https://github.com/valbuild/val/pull/832) [`ab0ea20`](https://github.com/valbuild/val/commit/ab0ea2046e3872c4955f34b363881f76d48d1b1e) Thanks [@freekh](https://github.com/freekh)! - A site served by Val's platform can now be given its content separately from its build. When the platform hands a TanStack app a content version (`VAL_CONTENT`) that differs from what the build bundles, Val renders that content, in the Studio and in draft mode too. It reads it the way it already reads a proposal's saved content.
15
+
16
+ Nothing changes for an app that is not given one, and nothing hands one over yet. This is the reading side of publishing content without rebuilding the site.
17
+
18
+ A version too large for one environment variable can name others under `refs` (`"env://VAL_CONTENT_1"`), each holding one module's Source or one file's text. Only `refs` is resolved, never a string inside your content, and only variables named `VAL_CONTENT_<n>` are read.
19
+
20
+ At a proposal's address, a proposal's saved content is shown over the content version the site serves, so modules the proposal did not change show what the site shows rather than what the build bundles.
21
+
22
+ When Val's platform puts a publish live without building the site, because it changed only content, the Studio shows it being checked and going live instead of "Building".
23
+
24
+ - Updated dependencies [[`ab0ea20`](https://github.com/valbuild/val/commit/ab0ea2046e3872c4955f34b363881f76d48d1b1e), [`44a5636`](https://github.com/valbuild/val/commit/44a5636955e005a40f2e6c8544e5cf4378fd0fdf)]:
25
+ - @valbuild/shared@0.144.2
26
+ - @valbuild/ui@0.144.2
27
+
3
28
  ## 0.144.1
4
29
 
5
30
  ### Patch Changes
@@ -51,6 +51,14 @@ export type ValOpsOptions = {
51
51
  * the next save's prepare -- starts from it, and the SHAs describe it.
52
52
  */
53
53
  snapshotSources?: Sources;
54
+ /**
55
+ * Source the SITE serves over what the modules evaluate to, beneath
56
+ * {@link snapshotSources}: a content version (`ValContentVersion` in
57
+ * `./proposal`), the content published since the build. Applied the same
58
+ * way, and what a proposal's saves are compared against
59
+ * ({@link ValOps.siteSourcesUnderSnapshot}) -- the site, not the bundle.
60
+ */
61
+ siteSnapshotSources?: Sources;
54
62
  /**
55
63
  * The `.jsonValues()` entries the same snapshot saved, by module and key:
56
64
  * their content is in their own files, not in {@link snapshotSources}, which
@@ -97,6 +105,13 @@ export declare abstract class ValOps {
97
105
  * {@link siteSourcesUnderSnapshot}, and only where there is a snapshot.
98
106
  */
99
107
  private siteSourcesBeforeSnapshot;
108
+ /**
109
+ * The same modules as the bundle evaluated them, imports and all: where a
110
+ * `.jsonValues()` entry the snapshot did not save is read from. Not
111
+ * {@link siteSourcesBeforeSnapshot}, whose markers may have come as JSON
112
+ * from a content version, with no import behind them.
113
+ */
114
+ private bundleSourcesBeforeSnapshot;
100
115
  /**
101
116
  * What a save has told us each `.jsonValues()` entry now holds.
102
117
  *
@@ -1,4 +1,4 @@
1
- import { type ValProposal } from "./proposal.js";
1
+ import { type ValContentVersion, type ValProposal } from "./proposal.js";
2
2
  import { type PatchId, type ModuleFilePath, ValModules } from "@valbuild/core";
3
3
  import type { Patch as PatchT, ParentRef as ParentRefT } from "@valbuild/core/patch";
4
4
  import { type AuthorId, type BaseSha, BinaryFileType, type CommitSha, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, OrderedPatchesMetadata, OrderedPatches, SourcesSha, type PublishRefusal } from "./ValOps.js";
@@ -81,6 +81,8 @@ export declare class ValOpsHttp extends ValOps {
81
81
  private readonly publishBuild;
82
82
  /** See `proposal` on {@link ValApiOptions}; `null` on the site. */
83
83
  private readonly proposal;
84
+ /** See `contentVersion` on {@link ValApiOptions}; `null` at a proposal's address. */
85
+ private readonly contentVersion;
84
86
  /** Did the host hand over the running build's source? See `projectSource`. */
85
87
  embedsSource(): boolean;
86
88
  /**
@@ -153,6 +155,8 @@ export declare class ValOpsHttp extends ValOps {
153
155
  publishBuild?: string;
154
156
  /** See `proposal` on {@link ValApiOptions}. */
155
157
  proposal?: ValProposal;
158
+ /** See `contentVersion` on {@link ValApiOptions}. */
159
+ contentVersion?: ValContentVersion;
156
160
  });
157
161
  /**
158
162
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -8,7 +8,7 @@ type Versions = {
8
8
  };
9
9
  };
10
10
  import type { ValPatchStore } from "./ValOpsMemory.js";
11
- import type { ValProposal } from "./proposal.js";
11
+ import type { ValContentVersion, ValProposal } from "./proposal.js";
12
12
  import type { CommitContext, CommitResult } from "./ValServer.js";
13
13
  export type ValApiOptions = ValServerOverrides & ValConfig & Versions;
14
14
  type ValServerOverrides = Partial<{
@@ -163,6 +163,14 @@ type ValServerOverrides = Partial<{
163
163
  * and the snapshot that replaces the bundle's Source for what it saved.
164
164
  */
165
165
  proposal?: ValProposal;
166
+ /**
167
+ * The content version the site serves, when a platform says it differs from
168
+ * the one this build bundles (`VAL_CONTENT`; read it with `contentFromEnv`).
169
+ * See {@link ValContentVersion}: its Source replaces the bundle's for what
170
+ * it holds, and its publish is this server's position. Ignored at a
171
+ * proposal's address.
172
+ */
173
+ contentVersion?: ValContentVersion;
166
174
  /**
167
175
  * The base url of Val.
168
176
  *
@@ -1,4 +1,4 @@
1
- import type { ValProposal } from "./proposal.js";
1
+ import type { ValContentVersion, ValProposal } from "./proposal.js";
2
2
  import { ValModules, PatchId, ModuleFilePath, ValConfig } from "@valbuild/core";
3
3
  import { Api, ServerOf, type PatchGroupT } from "@valbuild/shared/internal";
4
4
  import { z } from "zod";
@@ -148,6 +148,8 @@ export type ValServerConfig = ValServerOptions & ({
148
148
  publishBuild?: string;
149
149
  /** See `proposal` on {@link ValApiOptions}. */
150
150
  proposal?: ValProposal;
151
+ /** See `contentVersion` on {@link ValApiOptions}. */
152
+ contentVersion?: ValContentVersion;
151
153
  config: ValConfig;
152
154
  }
153
155
  /**
@@ -2,8 +2,8 @@ export { createService, Service } from "./Service.js";
2
2
  export { defineExternal, ok, err, isExternalResult, EXTERNAL_RESULT, } from "./externalRecords.js";
3
3
  export type { AdapterFor, BoundExternalRecord, ExternalAuthor, ExternalBuilder, ExternalCtx, ExternalDefinition, ExternalFile, ExternalIssue, ExternalKeyPage, ExternalRecords, ExternalResult, ExternalSearchHit, ExternalSearchPage, ExternalSort, ItemOfModule, ReadonlyRecordHasNoWrites, Returns, } from "./externalRecords.js";
4
4
  export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
5
- export { proposalFromEnv, proposalSnapshot, proposalSnapshotForReaders, } from "./proposal.js";
6
- export type { ValProposal } from "./proposal.js";
5
+ export { contentFromEnv, proposalFromEnv, layeredSnapshot, proposalSnapshot, proposalSnapshotForReaders, } from "./proposal.js";
6
+ export type { ValContentVersion, ValProposal } from "./proposal.js";
7
7
  export { initHandlerOptions, createValOps } from "./valServerConfig.js";
8
8
  export { resolveRemoteFileAuth } from "./valServerConfig.js";
9
9
  export { APP_MODE_REQUIRES_REMOTE_FILES } from "./valServerConfig.js";
@@ -65,6 +65,59 @@ export declare function proposalFromEnv(env: {
65
65
  VAL_BRANCH?: string | undefined;
66
66
  VAL_OVERLAY?: string | undefined;
67
67
  }): ValProposal | undefined;
68
+ /**
69
+ * A content version: the site's content, served by a build that bundles an
70
+ * older one. valbuild/home `docs/hosted-publishing.md`, step 2.
71
+ *
72
+ * The platform hands over only what differs from the bundle -- the modules
73
+ * whose Source changed and the Val files whose text did -- the way it hands a
74
+ * proposal's address its saved content, and it is read the same way: the
75
+ * snapshot source provider, over the bundle. What differs is the position: a
76
+ * version names the publish that made it (`job`, or its `commit`), and that is
77
+ * where this server says it sits, never at the build it runs in.
78
+ */
79
+ export type ValContentVersion = {
80
+ /** The version's id: the hash of its manifest, as the platform stores it. */
81
+ id: string;
82
+ /** The publish job that made it, when a job did. */
83
+ job: string | null;
84
+ /** Its place in the chain: the commit that publish minted. */
85
+ commit: string | null;
86
+ /** Source, by module file path, for the modules that differ from the bundle. */
87
+ modules: Record<string, unknown>;
88
+ /** Val file text, by path, for the files that differ from the bundle. */
89
+ files: Record<string, string>;
90
+ };
91
+ /**
92
+ * The content version a platform hands over in its runtime env
93
+ * (`VAL_CONTENT`), or `undefined` when it hands over none.
94
+ *
95
+ * Throws on one it cannot read rather than serving the bundle as though it
96
+ * were the version: the site would show content older than what was
97
+ * published, and nothing would say so.
98
+ *
99
+ * A version too large for one variable names others under `refs`: a module's
100
+ * Source (JSON) or a file's text, each in a variable of its own --
101
+ * `{ "refs": { "modules": { "/a.val.ts": "env://VAL_CONTENT_1" } } }`. Flat:
102
+ * a referenced variable holds the value itself and is never read for further
103
+ * references. Only `refs` is resolved, never a string inside Source, and only
104
+ * `VAL_CONTENT_<n>` is ever read: Source is text editors write, and a
105
+ * reference resolved in there would read any variable the isolate holds,
106
+ * `VAL_SECRET` included, into a page. The scheme leaves room for another
107
+ * place to read from; one this does not know is refused, not skipped.
108
+ */
109
+ export declare function contentFromEnv(env: Record<string, string | undefined>): ValContentVersion | undefined;
110
+ /**
111
+ * What a server or reader renders over the bundle: a proposal's saves over the
112
+ * content version the site serves, either alone, or nothing.
113
+ *
114
+ * Layered rather than chosen. At a proposal's address the platform hands both
115
+ * when the base the proposal was opened from serves content its build does not
116
+ * bundle (a content-only publish), and the proposal saves only the modules it
117
+ * changed: choosing the proposal alone would show every other module as the
118
+ * build bundles it -- older than the site it was opened from.
119
+ */
120
+ export declare function layeredSnapshot(proposal: Pick<ValProposal, "modules" | "files"> | undefined, contentVersion: Pick<ValContentVersion, "modules" | "files"> | undefined): Pick<ValProposal, "modules" | "files"> | undefined;
68
121
  /**
69
122
  * The proposal's saved Source, as Source: every module whose value is JSON.
70
123
  * One place, so the server and the readers that render without it agree on
@@ -3636,6 +3636,13 @@ class ValOps {
3636
3636
  * {@link siteSourcesUnderSnapshot}, and only where there is a snapshot.
3637
3637
  */
3638
3638
  siteSourcesBeforeSnapshot = null;
3639
+ /**
3640
+ * The same modules as the bundle evaluated them, imports and all: where a
3641
+ * `.jsonValues()` entry the snapshot did not save is read from. Not
3642
+ * {@link siteSourcesBeforeSnapshot}, whose markers may have come as JSON
3643
+ * from a content version, with no import behind them.
3644
+ */
3645
+ bundleSourcesBeforeSnapshot = null;
3639
3646
  /**
3640
3647
  * What a save has told us each `.jsonValues()` entry now holds.
3641
3648
  *
@@ -3694,7 +3701,7 @@ class ValOps {
3694
3701
  // #region initTree
3695
3702
  async initSources() {
3696
3703
  if (this.baseSha === null || this.sourcesSha === null || this.configSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
3697
- var _this$options;
3704
+ var _this$options, _this$options2;
3698
3705
  const extracted = await core.extractValModules(this.valModules);
3699
3706
  const moduleErrors = extracted.moduleErrors.concat(findNestedJsonValuesModuleErrors(extracted.schemas));
3700
3707
  this.sources = extracted.sources;
@@ -3712,16 +3719,45 @@ class ValOps {
3712
3719
  * being served, and a base SHA that still described the bundle would
3713
3720
  * say the snapshot's content was the bundle's.
3714
3721
  */
3715
- if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.snapshotSources) {
3722
+ const siteLayer = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.siteSnapshotSources;
3723
+ const snapshotLayer = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.snapshotSources;
3724
+ /*
3725
+ * The bundle's own Source for every module either layer replaces: a
3726
+ * replaced `.jsonValues()` module's markers came as JSON, and an entry
3727
+ * neither layer saved is read through the bundle's marker, which still
3728
+ * has its `import()`.
3729
+ */
3730
+ if (siteLayer || snapshotLayer) {
3731
+ const bundle = {};
3732
+ for (const moduleFilePath of Object.keys({
3733
+ ...siteLayer,
3734
+ ...snapshotLayer
3735
+ })) {
3736
+ const path = moduleFilePath;
3737
+ const bundled = extracted.sources[path];
3738
+ if (bundled !== undefined) {
3739
+ bundle[path] = bundled;
3740
+ }
3741
+ }
3742
+ this.bundleSourcesBeforeSnapshot = bundle;
3743
+ }
3744
+ // What a proposal is compared against: the site, only where there is a proposal.
3745
+ if (snapshotLayer) {
3716
3746
  const site = {};
3717
- for (const moduleFilePath of Object.keys(this.options.snapshotSources)) {
3718
- const source = extracted.sources[moduleFilePath];
3747
+ for (const moduleFilePath of Object.keys(snapshotLayer)) {
3748
+ const path = moduleFilePath;
3749
+ const source = (siteLayer === null || siteLayer === void 0 ? void 0 : siteLayer[path]) ?? extracted.sources[path];
3719
3750
  if (source !== undefined) {
3720
- site[moduleFilePath] = source;
3751
+ site[path] = source;
3721
3752
  }
3722
3753
  }
3723
3754
  this.siteSourcesBeforeSnapshot = site;
3724
- this.promoteCommittedSources(this.options.snapshotSources);
3755
+ }
3756
+ if (siteLayer || snapshotLayer) {
3757
+ this.promoteCommittedSources({
3758
+ ...siteLayer,
3759
+ ...snapshotLayer
3760
+ });
3725
3761
  }
3726
3762
  return {
3727
3763
  baseSha: this.baseSha,
@@ -3751,8 +3787,8 @@ class ValOps {
3751
3787
  * both, so it is not sent. `null` anywhere that is not a proposal.
3752
3788
  */
3753
3789
  async siteSourcesUnderSnapshot() {
3754
- var _this$options2;
3755
- if (!((_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.snapshotSources)) return null;
3790
+ var _this$options3;
3791
+ if (!((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.snapshotSources)) return null;
3756
3792
  await this.initSources();
3757
3793
  return this.siteSourcesBeforeSnapshot;
3758
3794
  }
@@ -4059,7 +4095,7 @@ class ValOps {
4059
4095
  }
4060
4096
  }
4061
4097
  const loaded = await Promise.all([...requestedKeys, ...sourceKeys].map(async entryKey => {
4062
- var _this$adoptedJsonEntr, _this$siteSourcesBefo;
4098
+ var _this$adoptedJsonEntr, _this$bundleSourcesBe;
4063
4099
  const marker = record[entryKey];
4064
4100
  if (marker !== undefined && !core.Internal.isJson(marker)) {
4065
4101
  // Not a jsonValues entry — return the inlined value as-is (defensive).
@@ -4106,7 +4142,7 @@ class ValOps {
4106
4142
  * entry the proposal did not save is the build's, behind the marker
4107
4143
  * the module evaluated to.
4108
4144
  */
4109
- const siteMarker = (_this$siteSourcesBefo = this.siteSourcesBeforeSnapshot) === null || _this$siteSourcesBefo === void 0 || (_this$siteSourcesBefo = _this$siteSourcesBefo[moduleFilePath]) === null || _this$siteSourcesBefo === void 0 ? void 0 : _this$siteSourcesBefo[entryKey];
4145
+ const siteMarker = (_this$bundleSourcesBe = this.bundleSourcesBeforeSnapshot) === null || _this$bundleSourcesBe === void 0 || (_this$bundleSourcesBe = _this$bundleSourcesBe[moduleFilePath]) === null || _this$bundleSourcesBe === void 0 ? void 0 : _this$bundleSourcesBe[entryKey];
4110
4146
  const thunk = core.Internal.getJsonImport(marker) ?? (core.Internal.isJson(siteMarker) ? core.Internal.getJsonImport(siteMarker) : undefined);
4111
4147
  if (!thunk) {
4112
4148
  return {
@@ -5365,9 +5401,9 @@ class ValOps {
5365
5401
  // https://github.com/microsoft/TypeScript/issues/36174
5366
5402
  let sourceFileText = null;
5367
5403
  if (tsChanged) {
5368
- var _this$options3;
5404
+ var _this$options4;
5369
5405
  sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
5370
- if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.formatter) {
5406
+ if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
5371
5407
  try {
5372
5408
  sourceFileText = await this.options.formatter(sourceFileText, path);
5373
5409
  } catch (err) {
@@ -5379,13 +5415,13 @@ class ValOps {
5379
5415
  }
5380
5416
  const extraFiles = {};
5381
5417
  for (const [jsonPath, content] of Array.from(jsonEntryContents)) {
5382
- var _this$options4;
5418
+ var _this$options5;
5383
5419
  if (content === null) {
5384
5420
  extraFiles[jsonPath] = null;
5385
5421
  continue;
5386
5422
  }
5387
5423
  let jsonText = JSON.stringify(content, null, 2);
5388
- if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
5424
+ if ((_this$options5 = this.options) !== null && _this$options5 !== void 0 && _this$options5.formatter) {
5389
5425
  try {
5390
5426
  jsonText = await this.options.formatter(jsonText, jsonPath);
5391
5427
  } catch (err) {
@@ -9004,6 +9040,182 @@ function proposalFromEnv(env) {
9004
9040
  };
9005
9041
  }
9006
9042
 
9043
+ /**
9044
+ * A content version: the site's content, served by a build that bundles an
9045
+ * older one. valbuild/home `docs/hosted-publishing.md`, step 2.
9046
+ *
9047
+ * The platform hands over only what differs from the bundle -- the modules
9048
+ * whose Source changed and the Val files whose text did -- the way it hands a
9049
+ * proposal's address its saved content, and it is read the same way: the
9050
+ * snapshot source provider, over the bundle. What differs is the position: a
9051
+ * version names the publish that made it (`job`, or its `commit`), and that is
9052
+ * where this server says it sits, never at the build it runs in.
9053
+ */
9054
+
9055
+ /**
9056
+ * The content version a platform hands over in its runtime env
9057
+ * (`VAL_CONTENT`), or `undefined` when it hands over none.
9058
+ *
9059
+ * Throws on one it cannot read rather than serving the bundle as though it
9060
+ * were the version: the site would show content older than what was
9061
+ * published, and nothing would say so.
9062
+ *
9063
+ * A version too large for one variable names others under `refs`: a module's
9064
+ * Source (JSON) or a file's text, each in a variable of its own --
9065
+ * `{ "refs": { "modules": { "/a.val.ts": "env://VAL_CONTENT_1" } } }`. Flat:
9066
+ * a referenced variable holds the value itself and is never read for further
9067
+ * references. Only `refs` is resolved, never a string inside Source, and only
9068
+ * `VAL_CONTENT_<n>` is ever read: Source is text editors write, and a
9069
+ * reference resolved in there would read any variable the isolate holds,
9070
+ * `VAL_SECRET` included, into a page. The scheme leaves room for another
9071
+ * place to read from; one this does not know is refused, not skipped.
9072
+ */
9073
+ function contentFromEnv(env) {
9074
+ if (!env.VAL_CONTENT) {
9075
+ return undefined;
9076
+ }
9077
+ let parsed;
9078
+ try {
9079
+ parsed = JSON.parse(env.VAL_CONTENT);
9080
+ } catch (err) {
9081
+ throw new Error(`Val: VAL_CONTENT is not JSON (${err instanceof Error ? err.message : String(err)})`, {
9082
+ cause: err
9083
+ });
9084
+ }
9085
+ if (!isObject$1(parsed) || Array.isArray(parsed)) {
9086
+ throw new Error("Val: VAL_CONTENT is not an object");
9087
+ }
9088
+ const id = parsed["id"];
9089
+ if (typeof id !== "string" || id === "") {
9090
+ throw new Error("Val: VAL_CONTENT has no id");
9091
+ }
9092
+ const job = parsed["job"] ?? null;
9093
+ const commit = parsed["commit"] ?? null;
9094
+ if (job !== null && typeof job !== "string") {
9095
+ throw new Error(`Val: content version ${id}: job must be a string or null`);
9096
+ }
9097
+ if (commit !== null && typeof commit !== "string") {
9098
+ throw new Error(`Val: content version ${id}: commit must be a string or null`);
9099
+ }
9100
+ const modulesIn = parsed["modules"] ?? {};
9101
+ const filesIn = parsed["files"] ?? {};
9102
+ if (!isObject$1(modulesIn) || Array.isArray(modulesIn) || !isObject$1(filesIn) || Array.isArray(filesIn)) {
9103
+ throw new Error(`Val: content version ${id}: modules and files must be objects`);
9104
+ }
9105
+ const files = {};
9106
+ for (const [path, text] of Object.entries(filesIn)) {
9107
+ if (typeof text === "string") {
9108
+ files[path] = text;
9109
+ }
9110
+ }
9111
+ const modules = {
9112
+ ...modulesIn
9113
+ };
9114
+ const refs = parsed["refs"] ?? {};
9115
+ if (!isObject$1(refs) || Array.isArray(refs)) {
9116
+ throw new Error(`Val: content version ${id}: refs must be an object`);
9117
+ }
9118
+ const read = (kind, path, ref) => {
9119
+ const where = `Val: content version ${id}: refs.${kind}["${path}"]`;
9120
+ if (typeof ref !== "string") {
9121
+ throw new Error(`${where} must be a string`);
9122
+ }
9123
+ if (!ref.startsWith(ENV_REF)) {
9124
+ throw new Error(`${where} is ${ref}: only ${ENV_REF}VAL_CONTENT_<n> is read`);
9125
+ }
9126
+ const name = ref.slice(ENV_REF.length);
9127
+ if (!CONTENT_VARIABLE.test(name)) {
9128
+ throw new Error(`${where} names ${name}: only VAL_CONTENT_<n> is read`);
9129
+ }
9130
+ const value = env[name];
9131
+ if (value === undefined) {
9132
+ throw new Error(`${where} names ${name}, which is not set`);
9133
+ }
9134
+ return {
9135
+ where,
9136
+ name,
9137
+ value
9138
+ };
9139
+ };
9140
+ const refModules = refs["modules"] ?? {};
9141
+ const refFiles = refs["files"] ?? {};
9142
+ if (!isObject$1(refModules) || Array.isArray(refModules) || !isObject$1(refFiles) || Array.isArray(refFiles)) {
9143
+ throw new Error(`Val: content version ${id}: refs.modules and refs.files must be objects`);
9144
+ }
9145
+ for (const [path, ref] of Object.entries(refModules)) {
9146
+ const {
9147
+ where,
9148
+ name,
9149
+ value
9150
+ } = read("modules", path, ref);
9151
+ if (Object.prototype.hasOwnProperty.call(modules, path)) {
9152
+ throw new Error(`${where}: ${path} is also in modules`);
9153
+ }
9154
+ try {
9155
+ modules[path] = JSON.parse(value);
9156
+ } catch (err) {
9157
+ throw new Error(`${where}: ${name} is not JSON (${err instanceof Error ? err.message : String(err)})`, {
9158
+ cause: err
9159
+ });
9160
+ }
9161
+ }
9162
+ for (const [path, ref] of Object.entries(refFiles)) {
9163
+ const {
9164
+ where,
9165
+ value
9166
+ } = read("files", path, ref);
9167
+ // The handed object, not `files`: an inline value that is not text was
9168
+ // dropped from `files`, and is still a second naming of the path.
9169
+ if (Object.prototype.hasOwnProperty.call(filesIn, path)) {
9170
+ throw new Error(`${where}: ${path} is also in files`);
9171
+ }
9172
+ files[path] = value;
9173
+ }
9174
+ if (job === null && commit === null) {
9175
+ // Placed nowhere, Val would place it at the build it runs in -- the one
9176
+ // position a version must never take: its own changes would be replayed
9177
+ // over a snapshot that already holds them.
9178
+ throw new Error(`Val: content version ${id} must name a job or a commit, or it would be placed at the build it runs in`);
9179
+ }
9180
+ return {
9181
+ id,
9182
+ job,
9183
+ commit,
9184
+ modules,
9185
+ files
9186
+ };
9187
+ }
9188
+
9189
+ /** The one scheme a content version's `refs` may use, for now. */
9190
+ const ENV_REF = "env://";
9191
+ /** The only variables a reference may name. */
9192
+ const CONTENT_VARIABLE = /^VAL_CONTENT_[0-9]+$/;
9193
+
9194
+ /**
9195
+ * What a server or reader renders over the bundle: a proposal's saves over the
9196
+ * content version the site serves, either alone, or nothing.
9197
+ *
9198
+ * Layered rather than chosen. At a proposal's address the platform hands both
9199
+ * when the base the proposal was opened from serves content its build does not
9200
+ * bundle (a content-only publish), and the proposal saves only the modules it
9201
+ * changed: choosing the proposal alone would show every other module as the
9202
+ * build bundles it -- older than the site it was opened from.
9203
+ */
9204
+ function layeredSnapshot(proposal, contentVersion) {
9205
+ if (proposal === undefined) return contentVersion;
9206
+ if (contentVersion === undefined) return proposal;
9207
+ return {
9208
+ modules: {
9209
+ ...contentVersion.modules,
9210
+ ...proposal.modules
9211
+ },
9212
+ files: {
9213
+ ...contentVersion.files,
9214
+ ...proposal.files
9215
+ }
9216
+ };
9217
+ }
9218
+
9007
9219
  /**
9008
9220
  * The proposal's saved Source, as Source: every module whose value is JSON.
9009
9221
  * One place, so the server and the readers that render without it agree on
@@ -9498,6 +9710,8 @@ class ValOpsHttp extends ValOps {
9498
9710
  publishBuild;
9499
9711
  /** See `proposal` on {@link ValApiOptions}; `null` on the site. */
9500
9712
  proposal;
9713
+ /** See `contentVersion` on {@link ValApiOptions}; `null` at a proposal's address. */
9714
+ contentVersion;
9501
9715
 
9502
9716
  /** Did the host hand over the running build's source? See `projectSource`. */
9503
9717
  embedsSource() {
@@ -9570,10 +9784,22 @@ class ValOpsHttp extends ValOps {
9570
9784
  * getSettings / uploadRemoteFile / getPresignedAuthNonce.
9571
9785
  */
9572
9786
  auth, valModules, options) {
9573
- super(valModules, options !== null && options !== void 0 && options.proposal ? {
9787
+ /*
9788
+ * What replaces the bundle's Source: the content version the site serves,
9789
+ * and a proposal's saves over it at its address. Two layers rather than
9790
+ * one, because the version is the SITE -- what a proposal is compared
9791
+ * against -- and the proposal is what is being proposed.
9792
+ */
9793
+ const snapshot = layeredSnapshot(options === null || options === void 0 ? void 0 : options.proposal, options === null || options === void 0 ? void 0 : options.contentVersion);
9794
+ super(valModules, options && snapshot ? {
9574
9795
  ...options,
9575
- snapshotSources: proposalSnapshot(options.proposal),
9576
- snapshotJsonEntries: proposalJsonEntries(options.proposal, options.projectSource)
9796
+ ...(options.proposal !== undefined ? {
9797
+ snapshotSources: proposalSnapshot(options.proposal)
9798
+ } : {}),
9799
+ ...(options.contentVersion !== undefined ? {
9800
+ siteSnapshotSources: proposalSnapshot(options.contentVersion)
9801
+ } : {}),
9802
+ snapshotJsonEntries: proposalJsonEntries(snapshot, options.projectSource)
9577
9803
  } : options);
9578
9804
  this.contentUrl = contentUrl;
9579
9805
  this.project = project;
@@ -9585,16 +9811,25 @@ class ValOpsHttp extends ValOps {
9585
9811
  };
9586
9812
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
9587
9813
  this.proposal = (options === null || options === void 0 ? void 0 : options.proposal) ?? null;
9814
+ this.contentVersion = this.proposal === null ? (options === null || options === void 0 ? void 0 : options.contentVersion) ?? null : null;
9588
9815
  /*
9589
9816
  * In a proposal, the text its saves wrote over the text the build was made
9590
9817
  * from: the next save patches the last one's `.val.ts`, not the base's.
9818
+ * The same for a content version: the next publish patches what the last
9819
+ * one published, not what the build bundles.
9591
9820
  */
9592
- this.projectSource = this.proposal !== null ? {
9821
+ this.projectSource = snapshot !== undefined ? {
9593
9822
  ...((options === null || options === void 0 ? void 0 : options.projectSource) ?? {}),
9594
- ...this.proposal.files
9823
+ ...snapshot.files
9595
9824
  } : (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
9596
9825
  this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
9597
- this.publishBuild = (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null;
9826
+ /*
9827
+ * Not with a content version: the content service places a build by the
9828
+ * build first (`getApplicablePatchesAndCommits`), and this build bundles an
9829
+ * older version than it serves -- placed there, the version's own changes
9830
+ * would be replayed over a snapshot that already holds them.
9831
+ */
9832
+ this.publishBuild = this.contentVersion === null ? (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null : null;
9598
9833
  this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
9599
9834
  }
9600
9835
  /**
@@ -10268,6 +10503,22 @@ class ValOpsHttp extends ValOps {
10268
10503
  } : {})
10269
10504
  };
10270
10505
  }
10506
+ /*
10507
+ * A content version sits where the publish that made it does: its job,
10508
+ * which content places exactly, or its commit, once nothing else says.
10509
+ */
10510
+ if (this.contentVersion !== null) {
10511
+ if (this.contentVersion.job !== null) {
10512
+ return {
10513
+ job: this.contentVersion.job
10514
+ };
10515
+ }
10516
+ if (this.contentVersion.commit !== null) {
10517
+ return {
10518
+ commit: this.contentVersion.commit
10519
+ };
10520
+ }
10521
+ }
10271
10522
  if (this.git) {
10272
10523
  return {
10273
10524
  branch: this.git.branch,
@@ -13528,6 +13779,9 @@ async function initHandlerOptions(route, opts, config) {
13528
13779
  ...(opts.proposal !== undefined ? {
13529
13780
  proposal: opts.proposal
13530
13781
  } : {}),
13782
+ ...(opts.contentVersion !== undefined ? {
13783
+ contentVersion: opts.contentVersion
13784
+ } : {}),
13531
13785
  project: maybeValProject,
13532
13786
  valEnableRedirectUrl,
13533
13787
  valDisableRedirectUrl,
@@ -13606,6 +13860,9 @@ function createValOps(valModules, options) {
13606
13860
  } : {}),
13607
13861
  ...(options.proposal !== undefined ? {
13608
13862
  proposal: options.proposal
13863
+ } : {}),
13864
+ ...(options.contentVersion !== undefined ? {
13865
+ contentVersion: options.contentVersion
13609
13866
  } : {})
13610
13867
  });
13611
13868
  }
@@ -24102,6 +24359,7 @@ exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
24102
24359
  exports.checkRemoteRef = checkRemoteRef;
24103
24360
  exports.classifyJsonValuesOp = classifyJsonValuesOp;
24104
24361
  exports.compareWithCapturedReport = compareWithCapturedReport;
24362
+ exports.contentFromEnv = contentFromEnv;
24105
24363
  exports.createDefaultValFSHost = createDefaultValFSHost;
24106
24364
  exports.createFixPatch = createFixPatch;
24107
24365
  exports.createJsonEntryPathMap = createJsonEntryPathMap;
@@ -24157,6 +24415,7 @@ exports.handleVideosetMetadata = handleVideosetMetadata;
24157
24415
  exports.handleVideosetUploadRemote = handleVideosetUploadRemote;
24158
24416
  exports.initHandlerOptions = initHandlerOptions;
24159
24417
  exports.isExternalResult = isExternalResult;
24418
+ exports.layeredSnapshot = layeredSnapshot;
24160
24419
  exports.loadValModules = loadValModules;
24161
24420
  exports.ok = ok;
24162
24421
  exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;