@valbuild/server 0.136.1 → 0.136.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,34 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.136.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#726](https://github.com/valbuild/val/pull/726) [`3b19534`](https://github.com/valbuild/val/commit/3b19534bb7aa16452a104906d9594083c3bc3e89) Thanks [@freekh](https://github.com/freekh)! - A Studio publish of a project with no repository now ships every edit that was saved, and keeps the site styled.
8
+
9
+ - A host that embeds the project's source can hand it to Val as `projectSource` (`initValServer(..., { http: { projectSource } })`). A publish then patches that text, not text fetched from the content service at a commit, which a project with no repository does not have. The build platform's wiring passes the build's own source, so `.val.ts` can be rendered for a project made on `/new`. Before, its first save produced no files, and the build that followed carried none of the save's edits.
10
+ - New route `GET /api/val/built-source`: the `.val.ts` text of every module changed since the running build, with every commit since it applied. The Studio builds from it, so an edit whose own publish failed, and a Finish publishing, are no longer left out of the next build.
11
+ - A Studio build that compiles no stylesheet (a browser cannot run Tailwind `@plugin`s) keeps the live site's. A Studio save never changes a stylesheet or a component, so the live CSS is still the right one.
12
+
13
+ - Updated dependencies [[`38d24f7`](https://github.com/valbuild/val/commit/38d24f723639ee109d1007854507c4571bc82575), [`3b19534`](https://github.com/valbuild/val/commit/3b19534bb7aa16452a104906d9594083c3bc3e89), [`fa28164`](https://github.com/valbuild/val/commit/fa28164dfd1e05ff53eccf375334741d857fd2ef)]:
14
+ - @valbuild/ui@0.136.3
15
+ - @valbuild/shared@0.136.3
16
+
17
+ ## 0.136.2
18
+
19
+ ### Patch Changes
20
+
21
+ - [#720](https://github.com/valbuild/val/pull/720) [`f76cd56`](https://github.com/valbuild/val/commit/f76cd56b9f1c1bcc0a608a7a4de24b1e776adccb) Thanks [@freekh](https://github.com/freekh)! - A managed project published from the Studio keeps its images, and a project created from a template can publish from the Studio at all.
22
+
23
+ - A publish from the Studio carries over every file under `public/` that the site already serves, so the favicon and images no longer disappear after the first Studio publish. When content published the live build, it already holds those files and nothing is uploaded again. When it didn't (a project cloned from a template), the Studio reads them from the site it runs on and builds with them.
24
+ - An image uploaded in the Studio is now part of the build that publishes it. For a managed project, `/save` returns the files the commit wrote (`binaryFiles`), the same way it already returns the `.val.ts` text.
25
+ - `/save` also returns the project's branch for a managed project, and the Studio's build uses it. A project cloned from a template was wired at no commit and no branch, so its first Studio publish went out branchless and the platform refused it.
26
+ - When neither content nor the platform can say which public files the live site serves, the Studio refuses to publish and says so, rather than publishing a site without them.
27
+
28
+ - Updated dependencies [[`f76cd56`](https://github.com/valbuild/val/commit/f76cd56b9f1c1bcc0a608a7a4de24b1e776adccb)]:
29
+ - @valbuild/ui@0.136.2
30
+ - @valbuild/shared@0.136.2
31
+
3
32
  ## 0.136.1
4
33
 
5
34
  ### Patch Changes
@@ -433,6 +433,18 @@ export declare abstract class ValOps {
433
433
  * an older service.
434
434
  */
435
435
  sourceMode(): "managed" | "connected" | null;
436
+ /**
437
+ * The branch the content service keeps this project's commits on, or `null`
438
+ * where there is no such service, or it has not said yet.
439
+ *
440
+ * For the Studio's in-tab build of a managed project, which has to name the
441
+ * branch its build was made at. Its other source is the `val.server.ts` the
442
+ * last build was wired with -- and a project cloned from a template seed has
443
+ * one wired at NO commit and no branch, so its first publish went out
444
+ * branchless and the loader refused it for a project whose pointer follows a
445
+ * branch. This is the project's own word for it.
446
+ */
447
+ projectBranch(): string | null;
436
448
  /**
437
449
  * Forward one call to the content service's publish API, as this project.
438
450
  *
@@ -52,6 +52,19 @@ export declare class ValOpsHttp extends ValOps {
52
52
  * initialiser runs before the constructor body has assigned it.
53
53
  */
54
54
  protected readonly mirrorsSourceFiles: boolean;
55
+ /**
56
+ * The running build's own source, when the host embedded it.
57
+ *
58
+ * Preferred over the content service for the `.val.ts` text a publish
59
+ * patches, for two reasons. It is exactly the text this build was made from,
60
+ * which is what the patches in its chain were written against. And it is the
61
+ * only copy there is for a project with no repository: the content service
62
+ * keeps a managed project's content as Source, not as files, and answers a
63
+ * file read for one with "no GitHub repo".
64
+ */
65
+ private readonly projectSource;
66
+ /** Did the host hand over the running build's source? See `projectSource`. */
67
+ embedsSource(): boolean;
55
68
  /**
56
69
  * What the content service last said this project expects of its publisher.
57
70
  *
@@ -102,6 +115,8 @@ export declare class ValOpsHttp extends ValOps {
102
115
  * the root would be /apps/my-app
103
116
  */
104
117
  root?: string;
118
+ /** See `projectSource` on {@link ValApiOptions}. */
119
+ projectSource?: Record<string, string>;
105
120
  });
106
121
  /**
107
122
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -141,6 +156,8 @@ export declare class ValOpsHttp extends ValOps {
141
156
  * has just arrived.
142
157
  */
143
158
  sourceMode(): "managed" | "connected" | null;
159
+ /** Remembered with {@link sourceMode}, from the same response. */
160
+ projectBranch(): string | null;
144
161
  /**
145
162
  * The short-lived publish token, and when it stops being usable.
146
163
  *
@@ -128,6 +128,18 @@ type ValServerOverrides = Partial<{
128
128
  * @example "main"
129
129
  */
130
130
  gitBranch?: string;
131
+ /**
132
+ * The project's own source files, by path with no leading slash, as the
133
+ * RUNNING build was made from them.
134
+ *
135
+ * Where a publish reads the `.val.ts` text it patches. Without it that text
136
+ * is fetched from the content service at {@link gitCommit}, which only a
137
+ * project with a repository has -- so a project whose content service is the
138
+ * store of record could not produce `.val.ts` at all, and a build made after
139
+ * a save carried none of the save's edits. A host that builds the project
140
+ * itself embeds its source in the build and hands it over here.
141
+ */
142
+ projectSource?: Record<string, string>;
131
143
  /**
132
144
  * The base url of Val.
133
145
  *
@@ -133,6 +133,8 @@ export type ValServerConfig = ValServerOptions & ({
133
133
  branch: string;
134
134
  };
135
135
  root?: string;
136
+ /** See `projectSource` on {@link ValApiOptions}. */
137
+ projectSource?: Record<string, string>;
136
138
  config: ValConfig;
137
139
  }
138
140
  /**
@@ -4595,6 +4595,21 @@ class ValOps {
4595
4595
  return null;
4596
4596
  }
4597
4597
 
4598
+ /**
4599
+ * The branch the content service keeps this project's commits on, or `null`
4600
+ * where there is no such service, or it has not said yet.
4601
+ *
4602
+ * For the Studio's in-tab build of a managed project, which has to name the
4603
+ * branch its build was made at. Its other source is the `val.server.ts` the
4604
+ * last build was wired with -- and a project cloned from a template seed has
4605
+ * one wired at NO commit and no branch, so its first publish went out
4606
+ * branchless and the loader refused it for a project whose pointer follows a
4607
+ * branch. This is the project's own word for it.
4608
+ */
4609
+ projectBranch() {
4610
+ return null;
4611
+ }
4612
+
4598
4613
  /**
4599
4614
  * Forward one call to the content service's publish API, as this project.
4600
4615
  *
@@ -7519,6 +7534,42 @@ class FSOpsHost {
7519
7534
  }
7520
7535
  }
7521
7536
 
7537
+ /**
7538
+ * The bytes of the local binary files a commit is about to write, base64.
7539
+ *
7540
+ * For the Studio's in-tab build of a managed project, which publishes the site:
7541
+ * an image uploaded in this save is in no earlier build, so unless the build
7542
+ * is given it the published page links a file that 404s. Remote files are left
7543
+ * out -- they are served from their own URL and never were part of a build.
7544
+ *
7545
+ * A file that cannot be read is NAMED rather than dropped, and does not stop
7546
+ * the save: the commit is what makes an edit durable, and the build is the
7547
+ * step that can refuse.
7548
+ *
7549
+ * Read BEFORE the commit, because after it the files are no longer the
7550
+ * patch's to read. Generic over the patch id so it asks for exactly the one
7551
+ * method it uses, and a test can hand it a plain object.
7552
+ */
7553
+ async function readCommittedBinaryFiles(ops, descriptors) {
7554
+ const files = {};
7555
+ const unread = [];
7556
+ await Promise.all(Object.entries(descriptors).map(async ([filePath, {
7557
+ patchId,
7558
+ remote
7559
+ }]) => {
7560
+ if (remote) return;
7561
+ const bytes = await ops.getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote).catch(error => {
7562
+ console.error("Could not read a committed file", filePath, error);
7563
+ return null;
7564
+ });
7565
+ if (bytes === null) unread.push(filePath);else files[filePath] = bytes.toString("base64");
7566
+ }));
7567
+ return {
7568
+ files,
7569
+ unread: unread.sort()
7570
+ };
7571
+ }
7572
+
7522
7573
  /**
7523
7574
  * Which unpublished changes a failed save has to throw away to make progress.
7524
7575
  *
@@ -7897,6 +7948,22 @@ class ValOpsHttp extends ValOps {
7897
7948
  * initialiser runs before the constructor body has assigned it.
7898
7949
  */
7899
7950
  mirrorsSourceFiles;
7951
+ /**
7952
+ * The running build's own source, when the host embedded it.
7953
+ *
7954
+ * Preferred over the content service for the `.val.ts` text a publish
7955
+ * patches, for two reasons. It is exactly the text this build was made from,
7956
+ * which is what the patches in its chain were written against. And it is the
7957
+ * only copy there is for a project with no repository: the content service
7958
+ * keeps a managed project's content as Source, not as files, and answers a
7959
+ * file read for one with "no GitHub repo".
7960
+ */
7961
+ projectSource;
7962
+
7963
+ /** Did the host hand over the running build's source? See `projectSource`. */
7964
+ embedsSource() {
7965
+ return this.projectSource !== null;
7966
+ }
7900
7967
  /**
7901
7968
  * What the content service last said this project expects of its publisher.
7902
7969
  *
@@ -7944,7 +8011,8 @@ class ValOpsHttp extends ValOps {
7944
8011
  Authorization: `Bearer ${auth.apiKey}`
7945
8012
  };
7946
8013
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
7947
- this.mirrorsSourceFiles = git !== null;
8014
+ this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
8015
+ this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
7948
8016
  }
7949
8017
  /**
7950
8018
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -8001,6 +8069,12 @@ class ValOpsHttp extends ValOps {
8001
8069
  return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
8002
8070
  }
8003
8071
 
8072
+ /** Remembered with {@link sourceMode}, from the same response. */
8073
+ projectBranch() {
8074
+ var _this$projectExpectat3;
8075
+ return ((_this$projectExpectat3 = this.projectExpectation) === null || _this$projectExpectat3 === void 0 ? void 0 : _this$projectExpectat3.branch) ?? null;
8076
+ }
8077
+
8004
8078
  /**
8005
8079
  * The short-lived publish token, and when it stops being usable.
8006
8080
  *
@@ -9124,6 +9198,19 @@ class ValOpsHttp extends ValOps {
9124
9198
  });
9125
9199
  }
9126
9200
  async getSourceFile(path) {
9201
+ if (this.projectSource !== null) {
9202
+ const text = this.projectSource[path.replace(/^\/+/, "")];
9203
+ if (text === undefined) {
9204
+ return {
9205
+ error: {
9206
+ message: `Cannot read the source of ${path}: it is not in the source this build was made from.`
9207
+ }
9208
+ };
9209
+ }
9210
+ return {
9211
+ data: text
9212
+ };
9213
+ }
9127
9214
  /*
9128
9215
  * There is no file to read without a repository to read it from.
9129
9216
  *
@@ -11253,6 +11340,9 @@ async function initHandlerOptions(route, opts, config) {
11253
11340
  }
11254
11341
  } : {}),
11255
11342
  root: opts.root,
11343
+ ...(opts.projectSource !== undefined ? {
11344
+ projectSource: opts.projectSource
11345
+ } : {}),
11256
11346
  project: maybeValProject,
11257
11347
  valEnableRedirectUrl,
11258
11348
  valDisableRedirectUrl,
@@ -11319,7 +11409,10 @@ function createValOps(valModules, options) {
11319
11409
  }, valModules, {
11320
11410
  formatter: options.formatter,
11321
11411
  root: options.root,
11322
- config: options.config
11412
+ config: options.config,
11413
+ ...(options.projectSource !== undefined ? {
11414
+ projectSource: options.projectSource
11415
+ } : {})
11323
11416
  });
11324
11417
  }
11325
11418
  if (options.mode === "memory") {
@@ -13312,6 +13405,71 @@ const ValServer = (valModules, options, callbacks) => {
13312
13405
  return res;
13313
13406
  }
13314
13407
  },
13408
+ "/built-source": {
13409
+ GET: async req => {
13410
+ const auth = getAuth(req.cookies);
13411
+ if (auth.error) {
13412
+ return {
13413
+ status: 401,
13414
+ json: {
13415
+ message: auth.error
13416
+ }
13417
+ };
13418
+ }
13419
+ if (!(serverOps instanceof ValOpsHttp) || !serverOps.embedsSource()) {
13420
+ return {
13421
+ status: 409,
13422
+ json: {
13423
+ message: "This server has no source of its own to render commits into."
13424
+ }
13425
+ };
13426
+ }
13427
+ const chain = await serverOps.fetchPatches({
13428
+ patchIds: undefined,
13429
+ excludePatchOps: false
13430
+ });
13431
+ if (chain.error) {
13432
+ return {
13433
+ status: 500,
13434
+ json: {
13435
+ message: chain.error.message
13436
+ }
13437
+ };
13438
+ }
13439
+ /*
13440
+ * The patches committed SINCE this build, as though pending.
13441
+ *
13442
+ * `applicable/patches` answers with the ones applied at a commit after
13443
+ * the one this build was made from, which is exactly what the text
13444
+ * embedded in it is missing. `analyzePatches` skips applied patches --
13445
+ * for a save they are already in the file it reads -- so they are
13446
+ * handed over unapplied: here the file predates them.
13447
+ */
13448
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
13449
+ ...patch,
13450
+ appliedAt: null
13451
+ }));
13452
+ const prepared = await serverOps.prepare({
13453
+ ...serverOps.analyzePatches(committed, chain.commits, commit),
13454
+ ...chain,
13455
+ patches: committed
13456
+ });
13457
+ if (prepared.hasErrors) {
13458
+ return {
13459
+ status: 500,
13460
+ json: {
13461
+ message: "The commits since this build could not be rendered into source: " + JSON.stringify(prepared.sourceFilePatchErrors).slice(0, 500)
13462
+ }
13463
+ };
13464
+ }
13465
+ return {
13466
+ status: 200,
13467
+ json: {
13468
+ files: prepared.patchedSourceFiles
13469
+ }
13470
+ };
13471
+ }
13472
+ },
13315
13473
  "/profiles": {
13316
13474
  GET: async req => {
13317
13475
  const cookies = req.cookies;
@@ -13757,6 +13915,12 @@ const ValServer = (valModules, options, callbacks) => {
13757
13915
  }
13758
13916
  }
13759
13917
  const message = body.message || "Val CMS update (" + Object.keys(analysis.patchesByModule).length + " files changed)";
13918
+ /*
13919
+ * Before the commit, because after it the files are no longer the
13920
+ * patch's to read. See `binaryFiles` on the route.
13921
+ */
13922
+ const managedBranch = serverOps.sourceMode() === "managed" ? serverOps.projectBranch() : null;
13923
+ const committedBinaries = serverOps.sourceMode() === "managed" ? await readCommittedBinaryFiles(serverOps, preparedCommit.patchedBinaryFilesDescriptors) : null;
13760
13924
  const commitToGit = () => {
13761
13925
  var _options$config$files;
13762
13926
  return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val", undefined,
@@ -13823,6 +13987,15 @@ const ValServer = (valModules, options, callbacks) => {
13823
13987
  */
13824
13988
  ...(serverOps.sourceMode() === "managed" ? {
13825
13989
  sourceFiles: preparedCommit.patchedSourceFiles
13990
+ } : {}),
13991
+ ...(managedBranch !== null ? {
13992
+ branch: managedBranch
13993
+ } : {}),
13994
+ ...(committedBinaries !== null ? {
13995
+ binaryFiles: committedBinaries.files,
13996
+ ...(committedBinaries.unread.length > 0 ? {
13997
+ binaryFilesUnread: committedBinaries.unread
13998
+ } : {})
13826
13999
  } : {})
13827
14000
  }
13828
14001
  };
@@ -4595,6 +4595,21 @@ class ValOps {
4595
4595
  return null;
4596
4596
  }
4597
4597
 
4598
+ /**
4599
+ * The branch the content service keeps this project's commits on, or `null`
4600
+ * where there is no such service, or it has not said yet.
4601
+ *
4602
+ * For the Studio's in-tab build of a managed project, which has to name the
4603
+ * branch its build was made at. Its other source is the `val.server.ts` the
4604
+ * last build was wired with -- and a project cloned from a template seed has
4605
+ * one wired at NO commit and no branch, so its first publish went out
4606
+ * branchless and the loader refused it for a project whose pointer follows a
4607
+ * branch. This is the project's own word for it.
4608
+ */
4609
+ projectBranch() {
4610
+ return null;
4611
+ }
4612
+
4598
4613
  /**
4599
4614
  * Forward one call to the content service's publish API, as this project.
4600
4615
  *
@@ -7519,6 +7534,42 @@ class FSOpsHost {
7519
7534
  }
7520
7535
  }
7521
7536
 
7537
+ /**
7538
+ * The bytes of the local binary files a commit is about to write, base64.
7539
+ *
7540
+ * For the Studio's in-tab build of a managed project, which publishes the site:
7541
+ * an image uploaded in this save is in no earlier build, so unless the build
7542
+ * is given it the published page links a file that 404s. Remote files are left
7543
+ * out -- they are served from their own URL and never were part of a build.
7544
+ *
7545
+ * A file that cannot be read is NAMED rather than dropped, and does not stop
7546
+ * the save: the commit is what makes an edit durable, and the build is the
7547
+ * step that can refuse.
7548
+ *
7549
+ * Read BEFORE the commit, because after it the files are no longer the
7550
+ * patch's to read. Generic over the patch id so it asks for exactly the one
7551
+ * method it uses, and a test can hand it a plain object.
7552
+ */
7553
+ async function readCommittedBinaryFiles(ops, descriptors) {
7554
+ const files = {};
7555
+ const unread = [];
7556
+ await Promise.all(Object.entries(descriptors).map(async ([filePath, {
7557
+ patchId,
7558
+ remote
7559
+ }]) => {
7560
+ if (remote) return;
7561
+ const bytes = await ops.getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote).catch(error => {
7562
+ console.error("Could not read a committed file", filePath, error);
7563
+ return null;
7564
+ });
7565
+ if (bytes === null) unread.push(filePath);else files[filePath] = bytes.toString("base64");
7566
+ }));
7567
+ return {
7568
+ files,
7569
+ unread: unread.sort()
7570
+ };
7571
+ }
7572
+
7522
7573
  /**
7523
7574
  * Which unpublished changes a failed save has to throw away to make progress.
7524
7575
  *
@@ -7897,6 +7948,22 @@ class ValOpsHttp extends ValOps {
7897
7948
  * initialiser runs before the constructor body has assigned it.
7898
7949
  */
7899
7950
  mirrorsSourceFiles;
7951
+ /**
7952
+ * The running build's own source, when the host embedded it.
7953
+ *
7954
+ * Preferred over the content service for the `.val.ts` text a publish
7955
+ * patches, for two reasons. It is exactly the text this build was made from,
7956
+ * which is what the patches in its chain were written against. And it is the
7957
+ * only copy there is for a project with no repository: the content service
7958
+ * keeps a managed project's content as Source, not as files, and answers a
7959
+ * file read for one with "no GitHub repo".
7960
+ */
7961
+ projectSource;
7962
+
7963
+ /** Did the host hand over the running build's source? See `projectSource`. */
7964
+ embedsSource() {
7965
+ return this.projectSource !== null;
7966
+ }
7900
7967
  /**
7901
7968
  * What the content service last said this project expects of its publisher.
7902
7969
  *
@@ -7944,7 +8011,8 @@ class ValOpsHttp extends ValOps {
7944
8011
  Authorization: `Bearer ${auth.apiKey}`
7945
8012
  };
7946
8013
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
7947
- this.mirrorsSourceFiles = git !== null;
8014
+ this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
8015
+ this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
7948
8016
  }
7949
8017
  /**
7950
8018
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -8001,6 +8069,12 @@ class ValOpsHttp extends ValOps {
8001
8069
  return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
8002
8070
  }
8003
8071
 
8072
+ /** Remembered with {@link sourceMode}, from the same response. */
8073
+ projectBranch() {
8074
+ var _this$projectExpectat3;
8075
+ return ((_this$projectExpectat3 = this.projectExpectation) === null || _this$projectExpectat3 === void 0 ? void 0 : _this$projectExpectat3.branch) ?? null;
8076
+ }
8077
+
8004
8078
  /**
8005
8079
  * The short-lived publish token, and when it stops being usable.
8006
8080
  *
@@ -9124,6 +9198,19 @@ class ValOpsHttp extends ValOps {
9124
9198
  });
9125
9199
  }
9126
9200
  async getSourceFile(path) {
9201
+ if (this.projectSource !== null) {
9202
+ const text = this.projectSource[path.replace(/^\/+/, "")];
9203
+ if (text === undefined) {
9204
+ return {
9205
+ error: {
9206
+ message: `Cannot read the source of ${path}: it is not in the source this build was made from.`
9207
+ }
9208
+ };
9209
+ }
9210
+ return {
9211
+ data: text
9212
+ };
9213
+ }
9127
9214
  /*
9128
9215
  * There is no file to read without a repository to read it from.
9129
9216
  *
@@ -11253,6 +11340,9 @@ async function initHandlerOptions(route, opts, config) {
11253
11340
  }
11254
11341
  } : {}),
11255
11342
  root: opts.root,
11343
+ ...(opts.projectSource !== undefined ? {
11344
+ projectSource: opts.projectSource
11345
+ } : {}),
11256
11346
  project: maybeValProject,
11257
11347
  valEnableRedirectUrl,
11258
11348
  valDisableRedirectUrl,
@@ -11319,7 +11409,10 @@ function createValOps(valModules, options) {
11319
11409
  }, valModules, {
11320
11410
  formatter: options.formatter,
11321
11411
  root: options.root,
11322
- config: options.config
11412
+ config: options.config,
11413
+ ...(options.projectSource !== undefined ? {
11414
+ projectSource: options.projectSource
11415
+ } : {})
11323
11416
  });
11324
11417
  }
11325
11418
  if (options.mode === "memory") {
@@ -13312,6 +13405,71 @@ const ValServer = (valModules, options, callbacks) => {
13312
13405
  return res;
13313
13406
  }
13314
13407
  },
13408
+ "/built-source": {
13409
+ GET: async req => {
13410
+ const auth = getAuth(req.cookies);
13411
+ if (auth.error) {
13412
+ return {
13413
+ status: 401,
13414
+ json: {
13415
+ message: auth.error
13416
+ }
13417
+ };
13418
+ }
13419
+ if (!(serverOps instanceof ValOpsHttp) || !serverOps.embedsSource()) {
13420
+ return {
13421
+ status: 409,
13422
+ json: {
13423
+ message: "This server has no source of its own to render commits into."
13424
+ }
13425
+ };
13426
+ }
13427
+ const chain = await serverOps.fetchPatches({
13428
+ patchIds: undefined,
13429
+ excludePatchOps: false
13430
+ });
13431
+ if (chain.error) {
13432
+ return {
13433
+ status: 500,
13434
+ json: {
13435
+ message: chain.error.message
13436
+ }
13437
+ };
13438
+ }
13439
+ /*
13440
+ * The patches committed SINCE this build, as though pending.
13441
+ *
13442
+ * `applicable/patches` answers with the ones applied at a commit after
13443
+ * the one this build was made from, which is exactly what the text
13444
+ * embedded in it is missing. `analyzePatches` skips applied patches --
13445
+ * for a save they are already in the file it reads -- so they are
13446
+ * handed over unapplied: here the file predates them.
13447
+ */
13448
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
13449
+ ...patch,
13450
+ appliedAt: null
13451
+ }));
13452
+ const prepared = await serverOps.prepare({
13453
+ ...serverOps.analyzePatches(committed, chain.commits, commit),
13454
+ ...chain,
13455
+ patches: committed
13456
+ });
13457
+ if (prepared.hasErrors) {
13458
+ return {
13459
+ status: 500,
13460
+ json: {
13461
+ message: "The commits since this build could not be rendered into source: " + JSON.stringify(prepared.sourceFilePatchErrors).slice(0, 500)
13462
+ }
13463
+ };
13464
+ }
13465
+ return {
13466
+ status: 200,
13467
+ json: {
13468
+ files: prepared.patchedSourceFiles
13469
+ }
13470
+ };
13471
+ }
13472
+ },
13315
13473
  "/profiles": {
13316
13474
  GET: async req => {
13317
13475
  const cookies = req.cookies;
@@ -13757,6 +13915,12 @@ const ValServer = (valModules, options, callbacks) => {
13757
13915
  }
13758
13916
  }
13759
13917
  const message = body.message || "Val CMS update (" + Object.keys(analysis.patchesByModule).length + " files changed)";
13918
+ /*
13919
+ * Before the commit, because after it the files are no longer the
13920
+ * patch's to read. See `binaryFiles` on the route.
13921
+ */
13922
+ const managedBranch = serverOps.sourceMode() === "managed" ? serverOps.projectBranch() : null;
13923
+ const committedBinaries = serverOps.sourceMode() === "managed" ? await readCommittedBinaryFiles(serverOps, preparedCommit.patchedBinaryFilesDescriptors) : null;
13760
13924
  const commitToGit = () => {
13761
13925
  var _options$config$files;
13762
13926
  return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val", undefined,
@@ -13823,6 +13987,15 @@ const ValServer = (valModules, options, callbacks) => {
13823
13987
  */
13824
13988
  ...(serverOps.sourceMode() === "managed" ? {
13825
13989
  sourceFiles: preparedCommit.patchedSourceFiles
13990
+ } : {}),
13991
+ ...(managedBranch !== null ? {
13992
+ branch: managedBranch
13993
+ } : {}),
13994
+ ...(committedBinaries !== null ? {
13995
+ binaryFiles: committedBinaries.files,
13996
+ ...(committedBinaries.unread.length > 0 ? {
13997
+ binaryFilesUnread: committedBinaries.unread
13998
+ } : {})
13826
13999
  } : {})
13827
14000
  }
13828
14001
  };
@@ -4561,6 +4561,21 @@ class ValOps {
4561
4561
  return null;
4562
4562
  }
4563
4563
 
4564
+ /**
4565
+ * The branch the content service keeps this project's commits on, or `null`
4566
+ * where there is no such service, or it has not said yet.
4567
+ *
4568
+ * For the Studio's in-tab build of a managed project, which has to name the
4569
+ * branch its build was made at. Its other source is the `val.server.ts` the
4570
+ * last build was wired with -- and a project cloned from a template seed has
4571
+ * one wired at NO commit and no branch, so its first publish went out
4572
+ * branchless and the loader refused it for a project whose pointer follows a
4573
+ * branch. This is the project's own word for it.
4574
+ */
4575
+ projectBranch() {
4576
+ return null;
4577
+ }
4578
+
4564
4579
  /**
4565
4580
  * Forward one call to the content service's publish API, as this project.
4566
4581
  *
@@ -7485,6 +7500,42 @@ class FSOpsHost {
7485
7500
  }
7486
7501
  }
7487
7502
 
7503
+ /**
7504
+ * The bytes of the local binary files a commit is about to write, base64.
7505
+ *
7506
+ * For the Studio's in-tab build of a managed project, which publishes the site:
7507
+ * an image uploaded in this save is in no earlier build, so unless the build
7508
+ * is given it the published page links a file that 404s. Remote files are left
7509
+ * out -- they are served from their own URL and never were part of a build.
7510
+ *
7511
+ * A file that cannot be read is NAMED rather than dropped, and does not stop
7512
+ * the save: the commit is what makes an edit durable, and the build is the
7513
+ * step that can refuse.
7514
+ *
7515
+ * Read BEFORE the commit, because after it the files are no longer the
7516
+ * patch's to read. Generic over the patch id so it asks for exactly the one
7517
+ * method it uses, and a test can hand it a plain object.
7518
+ */
7519
+ async function readCommittedBinaryFiles(ops, descriptors) {
7520
+ const files = {};
7521
+ const unread = [];
7522
+ await Promise.all(Object.entries(descriptors).map(async ([filePath, {
7523
+ patchId,
7524
+ remote
7525
+ }]) => {
7526
+ if (remote) return;
7527
+ const bytes = await ops.getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote).catch(error => {
7528
+ console.error("Could not read a committed file", filePath, error);
7529
+ return null;
7530
+ });
7531
+ if (bytes === null) unread.push(filePath);else files[filePath] = bytes.toString("base64");
7532
+ }));
7533
+ return {
7534
+ files,
7535
+ unread: unread.sort()
7536
+ };
7537
+ }
7538
+
7488
7539
  /**
7489
7540
  * Which unpublished changes a failed save has to throw away to make progress.
7490
7541
  *
@@ -7863,6 +7914,22 @@ class ValOpsHttp extends ValOps {
7863
7914
  * initialiser runs before the constructor body has assigned it.
7864
7915
  */
7865
7916
  mirrorsSourceFiles;
7917
+ /**
7918
+ * The running build's own source, when the host embedded it.
7919
+ *
7920
+ * Preferred over the content service for the `.val.ts` text a publish
7921
+ * patches, for two reasons. It is exactly the text this build was made from,
7922
+ * which is what the patches in its chain were written against. And it is the
7923
+ * only copy there is for a project with no repository: the content service
7924
+ * keeps a managed project's content as Source, not as files, and answers a
7925
+ * file read for one with "no GitHub repo".
7926
+ */
7927
+ projectSource;
7928
+
7929
+ /** Did the host hand over the running build's source? See `projectSource`. */
7930
+ embedsSource() {
7931
+ return this.projectSource !== null;
7932
+ }
7866
7933
  /**
7867
7934
  * What the content service last said this project expects of its publisher.
7868
7935
  *
@@ -7910,7 +7977,8 @@ class ValOpsHttp extends ValOps {
7910
7977
  Authorization: `Bearer ${auth.apiKey}`
7911
7978
  };
7912
7979
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
7913
- this.mirrorsSourceFiles = git !== null;
7980
+ this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
7981
+ this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
7914
7982
  }
7915
7983
  /**
7916
7984
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -7967,6 +8035,12 @@ class ValOpsHttp extends ValOps {
7967
8035
  return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
7968
8036
  }
7969
8037
 
8038
+ /** Remembered with {@link sourceMode}, from the same response. */
8039
+ projectBranch() {
8040
+ var _this$projectExpectat3;
8041
+ return ((_this$projectExpectat3 = this.projectExpectation) === null || _this$projectExpectat3 === void 0 ? void 0 : _this$projectExpectat3.branch) ?? null;
8042
+ }
8043
+
7970
8044
  /**
7971
8045
  * The short-lived publish token, and when it stops being usable.
7972
8046
  *
@@ -9090,6 +9164,19 @@ class ValOpsHttp extends ValOps {
9090
9164
  });
9091
9165
  }
9092
9166
  async getSourceFile(path) {
9167
+ if (this.projectSource !== null) {
9168
+ const text = this.projectSource[path.replace(/^\/+/, "")];
9169
+ if (text === undefined) {
9170
+ return {
9171
+ error: {
9172
+ message: `Cannot read the source of ${path}: it is not in the source this build was made from.`
9173
+ }
9174
+ };
9175
+ }
9176
+ return {
9177
+ data: text
9178
+ };
9179
+ }
9093
9180
  /*
9094
9181
  * There is no file to read without a repository to read it from.
9095
9182
  *
@@ -11219,6 +11306,9 @@ async function initHandlerOptions(route, opts, config) {
11219
11306
  }
11220
11307
  } : {}),
11221
11308
  root: opts.root,
11309
+ ...(opts.projectSource !== undefined ? {
11310
+ projectSource: opts.projectSource
11311
+ } : {}),
11222
11312
  project: maybeValProject,
11223
11313
  valEnableRedirectUrl,
11224
11314
  valDisableRedirectUrl,
@@ -11285,7 +11375,10 @@ function createValOps(valModules, options) {
11285
11375
  }, valModules, {
11286
11376
  formatter: options.formatter,
11287
11377
  root: options.root,
11288
- config: options.config
11378
+ config: options.config,
11379
+ ...(options.projectSource !== undefined ? {
11380
+ projectSource: options.projectSource
11381
+ } : {})
11289
11382
  });
11290
11383
  }
11291
11384
  if (options.mode === "memory") {
@@ -13278,6 +13371,71 @@ const ValServer = (valModules, options, callbacks) => {
13278
13371
  return res;
13279
13372
  }
13280
13373
  },
13374
+ "/built-source": {
13375
+ GET: async req => {
13376
+ const auth = getAuth(req.cookies);
13377
+ if (auth.error) {
13378
+ return {
13379
+ status: 401,
13380
+ json: {
13381
+ message: auth.error
13382
+ }
13383
+ };
13384
+ }
13385
+ if (!(serverOps instanceof ValOpsHttp) || !serverOps.embedsSource()) {
13386
+ return {
13387
+ status: 409,
13388
+ json: {
13389
+ message: "This server has no source of its own to render commits into."
13390
+ }
13391
+ };
13392
+ }
13393
+ const chain = await serverOps.fetchPatches({
13394
+ patchIds: undefined,
13395
+ excludePatchOps: false
13396
+ });
13397
+ if (chain.error) {
13398
+ return {
13399
+ status: 500,
13400
+ json: {
13401
+ message: chain.error.message
13402
+ }
13403
+ };
13404
+ }
13405
+ /*
13406
+ * The patches committed SINCE this build, as though pending.
13407
+ *
13408
+ * `applicable/patches` answers with the ones applied at a commit after
13409
+ * the one this build was made from, which is exactly what the text
13410
+ * embedded in it is missing. `analyzePatches` skips applied patches --
13411
+ * for a save they are already in the file it reads -- so they are
13412
+ * handed over unapplied: here the file predates them.
13413
+ */
13414
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
13415
+ ...patch,
13416
+ appliedAt: null
13417
+ }));
13418
+ const prepared = await serverOps.prepare({
13419
+ ...serverOps.analyzePatches(committed, chain.commits, commit),
13420
+ ...chain,
13421
+ patches: committed
13422
+ });
13423
+ if (prepared.hasErrors) {
13424
+ return {
13425
+ status: 500,
13426
+ json: {
13427
+ message: "The commits since this build could not be rendered into source: " + JSON.stringify(prepared.sourceFilePatchErrors).slice(0, 500)
13428
+ }
13429
+ };
13430
+ }
13431
+ return {
13432
+ status: 200,
13433
+ json: {
13434
+ files: prepared.patchedSourceFiles
13435
+ }
13436
+ };
13437
+ }
13438
+ },
13281
13439
  "/profiles": {
13282
13440
  GET: async req => {
13283
13441
  const cookies = req.cookies;
@@ -13723,6 +13881,12 @@ const ValServer = (valModules, options, callbacks) => {
13723
13881
  }
13724
13882
  }
13725
13883
  const message = body.message || "Val CMS update (" + Object.keys(analysis.patchesByModule).length + " files changed)";
13884
+ /*
13885
+ * Before the commit, because after it the files are no longer the
13886
+ * patch's to read. See `binaryFiles` on the route.
13887
+ */
13888
+ const managedBranch = serverOps.sourceMode() === "managed" ? serverOps.projectBranch() : null;
13889
+ const committedBinaries = serverOps.sourceMode() === "managed" ? await readCommittedBinaryFiles(serverOps, preparedCommit.patchedBinaryFilesDescriptors) : null;
13726
13890
  const commitToGit = () => {
13727
13891
  var _options$config$files;
13728
13892
  return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val", undefined,
@@ -13789,6 +13953,15 @@ const ValServer = (valModules, options, callbacks) => {
13789
13953
  */
13790
13954
  ...(serverOps.sourceMode() === "managed" ? {
13791
13955
  sourceFiles: preparedCommit.patchedSourceFiles
13956
+ } : {}),
13957
+ ...(managedBranch !== null ? {
13958
+ branch: managedBranch
13959
+ } : {}),
13960
+ ...(committedBinaries !== null ? {
13961
+ binaryFiles: committedBinaries.files,
13962
+ ...(committedBinaries.unread.length > 0 ? {
13963
+ binaryFilesUnread: committedBinaries.unread
13964
+ } : {})
13792
13965
  } : {})
13793
13966
  }
13794
13967
  };
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "types": "dist/valbuild-server.cjs.d.ts",
19
- "version": "0.136.1",
19
+ "version": "0.136.3",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0",
@@ -30,9 +30,9 @@
30
30
  "typescript": "^6.0.3",
31
31
  "zod": "^4.4.3",
32
32
  "zod-validation-error": "^5.0.0",
33
- "@valbuild/core": "0.136.1",
34
- "@valbuild/shared": "0.136.1",
35
- "@valbuild/ui": "0.136.1"
33
+ "@valbuild/shared": "0.136.3",
34
+ "@valbuild/ui": "0.136.3",
35
+ "@valbuild/core": "0.136.1"
36
36
  },
37
37
  "engines": {
38
38
  "node": "^20.19.0 || >=22"