@valbuild/server 0.138.3 → 0.138.5

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.138.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`c532afc`](https://github.com/valbuild/val/commit/c532afc1818ee0249ad227a7da029c3e8d06baa8)]:
8
+ - @valbuild/ui@0.138.5
9
+
10
+ ## 0.138.4
11
+
12
+ ### Patch Changes
13
+
14
+ - [#771](https://github.com/valbuild/val/pull/771) [`63bff05`](https://github.com/valbuild/val/commit/63bff0589b6df42efaa63ec606688d20a9670439) Thanks [@freekh](https://github.com/freekh)! - Changes that are already published no longer briefly vanish from the Studio, or show up twice, right after a publish in app mode.
15
+
16
+ A build the Studio makes in the browser now records which publish it was made for, so Val knows exactly which changes the running site already has. Before this, Val worked that out from whichever build was live, and was wrong for a moment while a publish was going live. A reload in that moment could hide changes that were not on the site yet, or apply ones that were a second time. Projects set up before this release need no changes.
17
+
18
+ - [#770](https://github.com/valbuild/val/pull/770) [`0a885b1`](https://github.com/valbuild/val/commit/0a885b1f0304517b167b959958464066194732d0) Thanks [@freekh](https://github.com/freekh)! - Publishing a Git-connected app-mode project now puts your changes on the site in seconds, instead of waiting for CI.
19
+
20
+ - The Studio builds the site in the browser, the same way it does for a managed project. Val pushes the commit to your repository as before. When nothing but content has changed since the build the site is serving, the browser's build goes live right away. If a developer has pushed code in between, the site waits for CI's build, so their code is never taken off the site.
21
+ - If the browser cannot build, the publish still goes through and CI delivers it. This happens when the page is not cross-origin isolated, or when the build keeps failing. Your changes stay visible in the Studio until the site serves a build that has them.
22
+ - Content that would break a page is caught before anything is pushed to your repository.
23
+
24
+ - [#771](https://github.com/valbuild/val/pull/771) [`58d13a2`](https://github.com/valbuild/val/commit/58d13a2a534421b341b1bf71d02f0a2290fc0d61) Thanks [@freekh](https://github.com/freekh)! - A project created on val.build with a GitHub repository can publish right away. Before, the first publish was refused with "this deployment was not built from one" until the repository's first CI build had finished.
25
+
26
+ While that first build runs, the site is served the template's build. When you publish from it, Val now checks that its content files are exactly the same as the files in your repository. If they are, the publish goes ahead and is pushed to your repository straight away. Your changes stay visible in the Studio until the site serves CI's build of them. If the files differ, the publish is refused, as before.
27
+
28
+ - Updated dependencies [[`63bff05`](https://github.com/valbuild/val/commit/63bff0589b6df42efaa63ec606688d20a9670439), [`0a885b1`](https://github.com/valbuild/val/commit/0a885b1f0304517b167b959958464066194732d0), [`8b1e47e`](https://github.com/valbuild/val/commit/8b1e47e50f9556d8469358af93ba32a084e2e131), [`58d13a2`](https://github.com/valbuild/val/commit/58d13a2a534421b341b1bf71d02f0a2290fc0d61), [`4268beb`](https://github.com/valbuild/val/commit/4268beb4beec53cb52584f59f9bbd5ffdcc9095d), [`e79451f`](https://github.com/valbuild/val/commit/e79451faab37f46e2039d681a19eea06d537dc44)]:
29
+ - @valbuild/ui@0.138.4
30
+ - @valbuild/shared@0.138.4
31
+
3
32
  ## 0.138.3
4
33
 
5
34
  ### Patch Changes
@@ -72,8 +72,22 @@ export declare class ValOpsHttp extends ValOps {
72
72
  * file read for one with "no GitHub repo".
73
73
  */
74
74
  private readonly projectSource;
75
+ /** See `publishJob` on {@link ValApiOptions}; sent only with no commit. */
76
+ private readonly publishJob;
75
77
  /** Did the host hand over the running build's source? See `projectSource`. */
76
78
  embedsSource(): boolean;
79
+ /**
80
+ * The Val source this build embeds, as git blob shas by path from the
81
+ * project root: every module and `val.modules.*`. `null` when it embeds no
82
+ * source.
83
+ *
84
+ * For a build of no commit -- the template's, which a project `/new` made
85
+ * with a repository is served until CI's first build -- so the content
86
+ * service can compare it with the branch file by file, having no commit to
87
+ * compare from. A git blob sha is `sha1("blob <bytes>\0" + content)`, which is
88
+ * what makes it comparable with the branch's tree without reading a file.
89
+ */
90
+ embeddedValSourceShas(): Promise<Record<string, string> | null>;
77
91
  /**
78
92
  * What the content service last said this project expects of its publisher.
79
93
  *
@@ -126,6 +140,8 @@ export declare class ValOpsHttp extends ValOps {
126
140
  root?: string;
127
141
  /** See `projectSource` on {@link ValApiOptions}. */
128
142
  projectSource?: Record<string, string>;
143
+ /** See `publishJob` on {@link ValApiOptions}. */
144
+ publishJob?: string;
129
145
  });
130
146
  /**
131
147
  * A deployment that cannot mirror a project which expects to be mirrored.
@@ -250,6 +266,10 @@ export declare class ValOpsHttp extends ValOps {
250
266
  modules: PreparedCommit["moduleVersions"];
251
267
  /** Connected: the git commit this deployment was built from. */
252
268
  gitCommit?: string;
269
+ /** Connected: the tab can build this job (the deployment embeds its source). */
270
+ tabBuilds?: true;
271
+ /** Connected, a build of no commit: its Val source. See `embeddedValSourceShas`. */
272
+ deploymentFiles?: Record<string, string>;
253
273
  }): Promise<{
254
274
  status: number;
255
275
  body: string;
@@ -140,6 +140,13 @@ type ValServerOverrides = Partial<{
140
140
  * itself embeds its source in the build and hands it over here.
141
141
  */
142
142
  projectSource?: Record<string, string>;
143
+ /**
144
+ * The publish job the RUNNING build was made for, when a Studio built it in
145
+ * a tab. Such a build has no commit to name its place in the content
146
+ * service's chain with, so it names its job, and the service places it by
147
+ * that rather than at whichever build is live.
148
+ */
149
+ publishJob?: string;
143
150
  /**
144
151
  * The base url of Val.
145
152
  *
@@ -141,6 +141,8 @@ export type ValServerConfig = ValServerOptions & ({
141
141
  root?: string;
142
142
  /** See `projectSource` on {@link ValApiOptions}. */
143
143
  projectSource?: Record<string, string>;
144
+ /** See `publishJob` on {@link ValApiOptions}. */
145
+ publishJob?: string;
144
146
  config: ValConfig;
145
147
  }
146
148
  /**
@@ -8638,11 +8638,43 @@ class ValOpsHttp extends ValOps {
8638
8638
  * file read for one with "no GitHub repo".
8639
8639
  */
8640
8640
  projectSource;
8641
+ /** See `publishJob` on {@link ValApiOptions}; sent only with no commit. */
8642
+ publishJob;
8641
8643
 
8642
8644
  /** Did the host hand over the running build's source? See `projectSource`. */
8643
8645
  embedsSource() {
8644
8646
  return this.projectSource !== null;
8645
8647
  }
8648
+
8649
+ /**
8650
+ * The Val source this build embeds, as git blob shas by path from the
8651
+ * project root: every module and `val.modules.*`. `null` when it embeds no
8652
+ * source.
8653
+ *
8654
+ * For a build of no commit -- the template's, which a project `/new` made
8655
+ * with a repository is served until CI's first build -- so the content
8656
+ * service can compare it with the branch file by file, having no commit to
8657
+ * compare from. A git blob sha is `sha1("blob <bytes>\0" + content)`, which is
8658
+ * what makes it comparable with the branch's tree without reading a file.
8659
+ */
8660
+ async embeddedValSourceShas() {
8661
+ if (this.projectSource === null) return null;
8662
+ const encoder = new TextEncoder();
8663
+ const shas = {};
8664
+ for (const [path, content] of Object.entries(this.projectSource)) {
8665
+ const base = path.slice(path.lastIndexOf("/") + 1);
8666
+ const isValSource = path.endsWith(".val.ts") || path.endsWith(".val.js") || path.endsWith(".val.json") || base === "val.modules.ts" || base === "val.modules.js";
8667
+ if (!isValSource) continue;
8668
+ const body = encoder.encode(content);
8669
+ const header = encoder.encode(`blob ${body.byteLength}\0`);
8670
+ const bytes = new Uint8Array(header.byteLength + body.byteLength);
8671
+ bytes.set(header, 0);
8672
+ bytes.set(body, header.byteLength);
8673
+ const digest = await crypto.subtle.digest("SHA-1", bytes);
8674
+ shas[path.replace(/^\/+/, "")] = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, "0")).join("");
8675
+ }
8676
+ return shas;
8677
+ }
8646
8678
  /**
8647
8679
  * What the content service last said this project expects of its publisher.
8648
8680
  *
@@ -8691,6 +8723,7 @@ class ValOpsHttp extends ValOps {
8691
8723
  };
8692
8724
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
8693
8725
  this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
8726
+ this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
8694
8727
  this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
8695
8728
  }
8696
8729
  /**
@@ -8720,6 +8753,15 @@ class ValOpsHttp extends ValOps {
8720
8753
  if (this.git !== null) {
8721
8754
  return null;
8722
8755
  }
8756
+ /*
8757
+ * Content runs this project's publishes as jobs, and its prepare decides
8758
+ * what the push goes on top of -- including for a site that was never
8759
+ * built from a commit (a project `/new` made, before its first CI build).
8760
+ * Refusing here would refuse what content can answer.
8761
+ */
8762
+ if (this.publishesAsJobs()) {
8763
+ return null;
8764
+ }
8723
8765
  if (((_this$projectExpectat = this.projectExpectation) === null || _this$projectExpectat === void 0 ? void 0 : _this$projectExpectat.sourceMode) !== "connected") {
8724
8766
  return null;
8725
8767
  }
@@ -9210,6 +9252,8 @@ class ValOpsHttp extends ValOps {
9210
9252
  ...(this.git ? {
9211
9253
  branch: this.git.branch,
9212
9254
  commitSha: this.git.commit
9255
+ } : this.publishJob !== null ? {
9256
+ job: this.publishJob
9213
9257
  } : {})
9214
9258
  }),
9215
9259
  headers: {
@@ -9391,6 +9435,9 @@ class ValOpsHttp extends ValOps {
9391
9435
  if (this.git) {
9392
9436
  params.push(["branch", this.git.branch]);
9393
9437
  params.push(["commit", this.git.commit]);
9438
+ } else if (this.publishJob !== null) {
9439
+ // A build a tab made has no commit, and says which job it was made for.
9440
+ params.push(["job", this.publishJob]);
9394
9441
  }
9395
9442
  if (filters.patchIds) {
9396
9443
  for (const patchId of filters.patchIds) {
@@ -12128,6 +12175,9 @@ async function initHandlerOptions(route, opts, config) {
12128
12175
  ...(opts.projectSource !== undefined ? {
12129
12176
  projectSource: opts.projectSource
12130
12177
  } : {}),
12178
+ ...(opts.publishJob !== undefined ? {
12179
+ publishJob: opts.publishJob
12180
+ } : {}),
12131
12181
  project: maybeValProject,
12132
12182
  valEnableRedirectUrl,
12133
12183
  valDisableRedirectUrl,
@@ -12197,6 +12247,9 @@ function createValOps(valModules, options) {
12197
12247
  config: options.config,
12198
12248
  ...(options.projectSource !== undefined ? {
12199
12249
  projectSource: options.projectSource
12250
+ } : {}),
12251
+ ...(options.publishJob !== undefined ? {
12252
+ publishJob: options.publishJob
12200
12253
  } : {})
12201
12254
  });
12202
12255
  }
@@ -14295,8 +14348,12 @@ const ValServer = (valModules, options, callbacks) => {
14295
14348
  /*
14296
14349
  * CONNECTED: the job's sources are what `/save` would commit -- its
14297
14350
  * own changes, applied as `/save` applies them -- and content pushes
14298
- * them at the seal. Nothing is built here: CI builds after the push,
14299
- * so the answer carries the job and no sources.
14351
+ * them at the seal. The tab ALSO builds the job, as a managed one is
14352
+ * built (docs/app-mode.md, "Since changed: connected builds in the
14353
+ * tab"): from this deployment's embedded source, the commits since it
14354
+ * and the job's changes. A deployment that embeds no source answers
14355
+ * `buildable: false`, and the tab reports "no build": CI builds the
14356
+ * push either way.
14300
14357
  */
14301
14358
  if (serverOps.sourceMode() === "connected") {
14302
14359
  var _options$config$files;
@@ -14342,90 +14399,54 @@ const ValServer = (valModules, options, callbacks) => {
14342
14399
  withJob: prepared,
14343
14400
  jobPatchIds: patchIds
14344
14401
  });
14402
+ const build = serverOps.embedsSource() ? await buildSourcesOf(serverOps, patchIds, commit) : null;
14403
+ if (build !== null && "status" in build) return build;
14404
+ /*
14405
+ * A build of no commit says what Val source it holds instead, so
14406
+ * the content service can compare it with the branch -- the
14407
+ * template's build a `/new` project is served before CI's first.
14408
+ */
14409
+ const deploymentFiles = commit === undefined ? await serverOps.embeddedValSourceShas() : null;
14345
14410
  const sent = await serverOps.prepareJob(jobId, {
14346
14411
  tab,
14347
14412
  filesDirectory: ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val",
14348
14413
  ...split.archive,
14349
14414
  ...(commit !== undefined ? {
14350
14415
  gitCommit: commit
14416
+ } : {}),
14417
+ ...(deploymentFiles !== null ? {
14418
+ deploymentFiles
14419
+ } : {}),
14420
+ // Said, so content waits for this tab's build. A server that does
14421
+ // not say it gets CI's: what Val servers before this one get.
14422
+ ...(build !== null ? {
14423
+ tabBuilds: true
14351
14424
  } : {})
14352
14425
  });
14353
14426
  return jobPrepareAnswer(sent, {
14354
- sourceFiles: {},
14355
- binaryFiles: {},
14356
- binaryFilesUnread: [],
14357
- branch: serverOps.projectBranch()
14427
+ sourceFiles: (build === null || build === void 0 ? void 0 : build.sourceFiles) ?? {},
14428
+ binaryFiles: (build === null || build === void 0 ? void 0 : build.binaryFiles) ?? {},
14429
+ binaryFilesUnread: (build === null || build === void 0 ? void 0 : build.binaryFilesUnread) ?? [],
14430
+ branch: serverOps.projectBranch(),
14431
+ buildable: build !== null
14358
14432
  });
14359
14433
  }
14360
- const chain = await serverOps.fetchPatches({
14361
- patchIds: undefined,
14362
- excludePatchOps: false
14363
- });
14364
- if (chain.error) {
14365
- return {
14366
- status: 500,
14367
- json: {
14368
- message: chain.error.message
14369
- }
14370
- };
14371
- }
14372
- /*
14373
- * The job's content, in two steps from this build's embedded source.
14374
- *
14375
- * The commits since this build, as though pending, are the branch's
14376
- * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
14377
- * out of its own changes (docs/app-mode.md, "Publishing is a queued
14378
- * job"), and were that ever not so, the seal would restart the job
14379
- * rather than publish it. Chain order, both times.
14380
- */
14381
- const job = new Set(patchIds);
14382
- const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
14383
- ...patch,
14384
- appliedAt: null
14385
- }));
14386
- const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
14387
- const known = new Set(chain.patches.map(patch => patch.patchId));
14388
- const gone = patchIds.filter(id => !known.has(id));
14389
- if (gone.length > 0) {
14390
- return {
14391
- status: 409,
14392
- json: {
14393
- message: `This publish's changes are no longer all there (${gone.length} discarded).`
14394
- }
14395
- };
14396
- }
14397
- const prepareOf = patches => serverOps.prepare({
14398
- ...serverOps.analyzePatches(patches, chain.commits, commit),
14399
- ...chain,
14400
- patches
14401
- });
14402
- const chainOnly = await prepareOf(committed);
14403
- const withJob = await prepareOf([...committed, ...pending]);
14404
- if (chainOnly.hasErrors || withJob.hasErrors) {
14405
- return {
14406
- status: 500,
14407
- json: {
14408
- message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
14409
- }
14410
- };
14411
- }
14412
- const split = splitJobPrepare({
14413
- chainOnly,
14414
- withJob,
14415
- jobPatchIds: patchIds
14416
- });
14417
- // Read now, while the job's changes still hold their files.
14418
- const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
14434
+ const built = await buildSourcesOf(serverOps, patchIds, commit);
14435
+ if ("status" in built) return built;
14436
+ const {
14437
+ split
14438
+ } = built;
14419
14439
  const sent = await serverOps.prepareJob(jobId, {
14420
14440
  tab,
14421
14441
  filesDirectory: ((_options$config$files2 = options.config.files) === null || _options$config$files2 === void 0 ? void 0 : _options$config$files2.directory) || "/public/val",
14422
14442
  ...split.archive
14423
14443
  });
14424
14444
  return jobPrepareAnswer(sent, {
14425
- sourceFiles: split.buildSourceFiles,
14426
- binaryFiles: binaries.files,
14427
- binaryFilesUnread: binaries.unread,
14428
- branch: serverOps.projectBranch()
14445
+ sourceFiles: built.sourceFiles,
14446
+ binaryFiles: built.binaryFiles,
14447
+ binaryFilesUnread: built.binaryFilesUnread,
14448
+ branch: serverOps.projectBranch(),
14449
+ buildable: true
14429
14450
  });
14430
14451
  }
14431
14452
  },
@@ -16536,6 +16557,82 @@ function getIsRemoteRequired(schemas) {
16536
16557
  return false;
16537
16558
  }
16538
16559
 
16560
+ /**
16561
+ * What a tab builds a job from: this deployment's embedded source, the
16562
+ * commits since it (as though pending: together they are the branch's head),
16563
+ * and the job's own pending changes on top. Managed and connected alike.
16564
+ *
16565
+ * `split.archive` is the managed job's archive; a connected job's archive is
16566
+ * the push's, made from its own changes alone, by the caller.
16567
+ */
16568
+ async function buildSourcesOf(serverOps, patchIds, commit) {
16569
+ const chain = await serverOps.fetchPatches({
16570
+ patchIds: undefined,
16571
+ excludePatchOps: false
16572
+ });
16573
+ if (chain.error) {
16574
+ return {
16575
+ status: 500,
16576
+ json: {
16577
+ message: chain.error.message
16578
+ }
16579
+ };
16580
+ }
16581
+ /*
16582
+ * The job's content, in two steps from this build's embedded source.
16583
+ *
16584
+ * The commits since this build, as though pending, are the branch's
16585
+ * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
16586
+ * out of its own changes (docs/app-mode.md, "Publishing is a queued
16587
+ * job"), and were that ever not so, the seal would restart the job
16588
+ * rather than publish it. Chain order, both times.
16589
+ */
16590
+ const job = new Set(patchIds);
16591
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
16592
+ ...patch,
16593
+ appliedAt: null
16594
+ }));
16595
+ const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
16596
+ const known = new Set(chain.patches.map(patch => patch.patchId));
16597
+ const gone = patchIds.filter(id => !known.has(id));
16598
+ if (gone.length > 0) {
16599
+ return {
16600
+ status: 409,
16601
+ json: {
16602
+ message: `This publish's changes are no longer all there (${gone.length} discarded).`
16603
+ }
16604
+ };
16605
+ }
16606
+ const prepareOf = patches => serverOps.prepare({
16607
+ ...serverOps.analyzePatches(patches, chain.commits, commit),
16608
+ ...chain,
16609
+ patches
16610
+ });
16611
+ const chainOnly = await prepareOf(committed);
16612
+ const withJob = await prepareOf([...committed, ...pending]);
16613
+ if (chainOnly.hasErrors || withJob.hasErrors) {
16614
+ return {
16615
+ status: 500,
16616
+ json: {
16617
+ message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
16618
+ }
16619
+ };
16620
+ }
16621
+ const split = splitJobPrepare({
16622
+ chainOnly,
16623
+ withJob,
16624
+ jobPatchIds: patchIds
16625
+ });
16626
+ // Read now, while the job's changes still hold their files.
16627
+ const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
16628
+ return {
16629
+ split,
16630
+ sourceFiles: split.buildSourceFiles,
16631
+ binaryFiles: binaries.files,
16632
+ binaryFilesUnread: binaries.unread
16633
+ };
16634
+ }
16635
+
16539
16636
  /**
16540
16637
  * `/publish-job-prepare`'s answer from content's: the job as content has it
16541
16638
  * now, and what the tab builds from. Content's refusal is passed on -- 409
@@ -8638,11 +8638,43 @@ class ValOpsHttp extends ValOps {
8638
8638
  * file read for one with "no GitHub repo".
8639
8639
  */
8640
8640
  projectSource;
8641
+ /** See `publishJob` on {@link ValApiOptions}; sent only with no commit. */
8642
+ publishJob;
8641
8643
 
8642
8644
  /** Did the host hand over the running build's source? See `projectSource`. */
8643
8645
  embedsSource() {
8644
8646
  return this.projectSource !== null;
8645
8647
  }
8648
+
8649
+ /**
8650
+ * The Val source this build embeds, as git blob shas by path from the
8651
+ * project root: every module and `val.modules.*`. `null` when it embeds no
8652
+ * source.
8653
+ *
8654
+ * For a build of no commit -- the template's, which a project `/new` made
8655
+ * with a repository is served until CI's first build -- so the content
8656
+ * service can compare it with the branch file by file, having no commit to
8657
+ * compare from. A git blob sha is `sha1("blob <bytes>\0" + content)`, which is
8658
+ * what makes it comparable with the branch's tree without reading a file.
8659
+ */
8660
+ async embeddedValSourceShas() {
8661
+ if (this.projectSource === null) return null;
8662
+ const encoder = new TextEncoder();
8663
+ const shas = {};
8664
+ for (const [path, content] of Object.entries(this.projectSource)) {
8665
+ const base = path.slice(path.lastIndexOf("/") + 1);
8666
+ const isValSource = path.endsWith(".val.ts") || path.endsWith(".val.js") || path.endsWith(".val.json") || base === "val.modules.ts" || base === "val.modules.js";
8667
+ if (!isValSource) continue;
8668
+ const body = encoder.encode(content);
8669
+ const header = encoder.encode(`blob ${body.byteLength}\0`);
8670
+ const bytes = new Uint8Array(header.byteLength + body.byteLength);
8671
+ bytes.set(header, 0);
8672
+ bytes.set(body, header.byteLength);
8673
+ const digest = await crypto.subtle.digest("SHA-1", bytes);
8674
+ shas[path.replace(/^\/+/, "")] = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, "0")).join("");
8675
+ }
8676
+ return shas;
8677
+ }
8646
8678
  /**
8647
8679
  * What the content service last said this project expects of its publisher.
8648
8680
  *
@@ -8691,6 +8723,7 @@ class ValOpsHttp extends ValOps {
8691
8723
  };
8692
8724
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
8693
8725
  this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
8726
+ this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
8694
8727
  this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
8695
8728
  }
8696
8729
  /**
@@ -8720,6 +8753,15 @@ class ValOpsHttp extends ValOps {
8720
8753
  if (this.git !== null) {
8721
8754
  return null;
8722
8755
  }
8756
+ /*
8757
+ * Content runs this project's publishes as jobs, and its prepare decides
8758
+ * what the push goes on top of -- including for a site that was never
8759
+ * built from a commit (a project `/new` made, before its first CI build).
8760
+ * Refusing here would refuse what content can answer.
8761
+ */
8762
+ if (this.publishesAsJobs()) {
8763
+ return null;
8764
+ }
8723
8765
  if (((_this$projectExpectat = this.projectExpectation) === null || _this$projectExpectat === void 0 ? void 0 : _this$projectExpectat.sourceMode) !== "connected") {
8724
8766
  return null;
8725
8767
  }
@@ -9210,6 +9252,8 @@ class ValOpsHttp extends ValOps {
9210
9252
  ...(this.git ? {
9211
9253
  branch: this.git.branch,
9212
9254
  commitSha: this.git.commit
9255
+ } : this.publishJob !== null ? {
9256
+ job: this.publishJob
9213
9257
  } : {})
9214
9258
  }),
9215
9259
  headers: {
@@ -9391,6 +9435,9 @@ class ValOpsHttp extends ValOps {
9391
9435
  if (this.git) {
9392
9436
  params.push(["branch", this.git.branch]);
9393
9437
  params.push(["commit", this.git.commit]);
9438
+ } else if (this.publishJob !== null) {
9439
+ // A build a tab made has no commit, and says which job it was made for.
9440
+ params.push(["job", this.publishJob]);
9394
9441
  }
9395
9442
  if (filters.patchIds) {
9396
9443
  for (const patchId of filters.patchIds) {
@@ -12128,6 +12175,9 @@ async function initHandlerOptions(route, opts, config) {
12128
12175
  ...(opts.projectSource !== undefined ? {
12129
12176
  projectSource: opts.projectSource
12130
12177
  } : {}),
12178
+ ...(opts.publishJob !== undefined ? {
12179
+ publishJob: opts.publishJob
12180
+ } : {}),
12131
12181
  project: maybeValProject,
12132
12182
  valEnableRedirectUrl,
12133
12183
  valDisableRedirectUrl,
@@ -12197,6 +12247,9 @@ function createValOps(valModules, options) {
12197
12247
  config: options.config,
12198
12248
  ...(options.projectSource !== undefined ? {
12199
12249
  projectSource: options.projectSource
12250
+ } : {}),
12251
+ ...(options.publishJob !== undefined ? {
12252
+ publishJob: options.publishJob
12200
12253
  } : {})
12201
12254
  });
12202
12255
  }
@@ -14295,8 +14348,12 @@ const ValServer = (valModules, options, callbacks) => {
14295
14348
  /*
14296
14349
  * CONNECTED: the job's sources are what `/save` would commit -- its
14297
14350
  * own changes, applied as `/save` applies them -- and content pushes
14298
- * them at the seal. Nothing is built here: CI builds after the push,
14299
- * so the answer carries the job and no sources.
14351
+ * them at the seal. The tab ALSO builds the job, as a managed one is
14352
+ * built (docs/app-mode.md, "Since changed: connected builds in the
14353
+ * tab"): from this deployment's embedded source, the commits since it
14354
+ * and the job's changes. A deployment that embeds no source answers
14355
+ * `buildable: false`, and the tab reports "no build": CI builds the
14356
+ * push either way.
14300
14357
  */
14301
14358
  if (serverOps.sourceMode() === "connected") {
14302
14359
  var _options$config$files;
@@ -14342,90 +14399,54 @@ const ValServer = (valModules, options, callbacks) => {
14342
14399
  withJob: prepared,
14343
14400
  jobPatchIds: patchIds
14344
14401
  });
14402
+ const build = serverOps.embedsSource() ? await buildSourcesOf(serverOps, patchIds, commit) : null;
14403
+ if (build !== null && "status" in build) return build;
14404
+ /*
14405
+ * A build of no commit says what Val source it holds instead, so
14406
+ * the content service can compare it with the branch -- the
14407
+ * template's build a `/new` project is served before CI's first.
14408
+ */
14409
+ const deploymentFiles = commit === undefined ? await serverOps.embeddedValSourceShas() : null;
14345
14410
  const sent = await serverOps.prepareJob(jobId, {
14346
14411
  tab,
14347
14412
  filesDirectory: ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val",
14348
14413
  ...split.archive,
14349
14414
  ...(commit !== undefined ? {
14350
14415
  gitCommit: commit
14416
+ } : {}),
14417
+ ...(deploymentFiles !== null ? {
14418
+ deploymentFiles
14419
+ } : {}),
14420
+ // Said, so content waits for this tab's build. A server that does
14421
+ // not say it gets CI's: what Val servers before this one get.
14422
+ ...(build !== null ? {
14423
+ tabBuilds: true
14351
14424
  } : {})
14352
14425
  });
14353
14426
  return jobPrepareAnswer(sent, {
14354
- sourceFiles: {},
14355
- binaryFiles: {},
14356
- binaryFilesUnread: [],
14357
- branch: serverOps.projectBranch()
14427
+ sourceFiles: (build === null || build === void 0 ? void 0 : build.sourceFiles) ?? {},
14428
+ binaryFiles: (build === null || build === void 0 ? void 0 : build.binaryFiles) ?? {},
14429
+ binaryFilesUnread: (build === null || build === void 0 ? void 0 : build.binaryFilesUnread) ?? [],
14430
+ branch: serverOps.projectBranch(),
14431
+ buildable: build !== null
14358
14432
  });
14359
14433
  }
14360
- const chain = await serverOps.fetchPatches({
14361
- patchIds: undefined,
14362
- excludePatchOps: false
14363
- });
14364
- if (chain.error) {
14365
- return {
14366
- status: 500,
14367
- json: {
14368
- message: chain.error.message
14369
- }
14370
- };
14371
- }
14372
- /*
14373
- * The job's content, in two steps from this build's embedded source.
14374
- *
14375
- * The commits since this build, as though pending, are the branch's
14376
- * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
14377
- * out of its own changes (docs/app-mode.md, "Publishing is a queued
14378
- * job"), and were that ever not so, the seal would restart the job
14379
- * rather than publish it. Chain order, both times.
14380
- */
14381
- const job = new Set(patchIds);
14382
- const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
14383
- ...patch,
14384
- appliedAt: null
14385
- }));
14386
- const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
14387
- const known = new Set(chain.patches.map(patch => patch.patchId));
14388
- const gone = patchIds.filter(id => !known.has(id));
14389
- if (gone.length > 0) {
14390
- return {
14391
- status: 409,
14392
- json: {
14393
- message: `This publish's changes are no longer all there (${gone.length} discarded).`
14394
- }
14395
- };
14396
- }
14397
- const prepareOf = patches => serverOps.prepare({
14398
- ...serverOps.analyzePatches(patches, chain.commits, commit),
14399
- ...chain,
14400
- patches
14401
- });
14402
- const chainOnly = await prepareOf(committed);
14403
- const withJob = await prepareOf([...committed, ...pending]);
14404
- if (chainOnly.hasErrors || withJob.hasErrors) {
14405
- return {
14406
- status: 500,
14407
- json: {
14408
- message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
14409
- }
14410
- };
14411
- }
14412
- const split = splitJobPrepare({
14413
- chainOnly,
14414
- withJob,
14415
- jobPatchIds: patchIds
14416
- });
14417
- // Read now, while the job's changes still hold their files.
14418
- const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
14434
+ const built = await buildSourcesOf(serverOps, patchIds, commit);
14435
+ if ("status" in built) return built;
14436
+ const {
14437
+ split
14438
+ } = built;
14419
14439
  const sent = await serverOps.prepareJob(jobId, {
14420
14440
  tab,
14421
14441
  filesDirectory: ((_options$config$files2 = options.config.files) === null || _options$config$files2 === void 0 ? void 0 : _options$config$files2.directory) || "/public/val",
14422
14442
  ...split.archive
14423
14443
  });
14424
14444
  return jobPrepareAnswer(sent, {
14425
- sourceFiles: split.buildSourceFiles,
14426
- binaryFiles: binaries.files,
14427
- binaryFilesUnread: binaries.unread,
14428
- branch: serverOps.projectBranch()
14445
+ sourceFiles: built.sourceFiles,
14446
+ binaryFiles: built.binaryFiles,
14447
+ binaryFilesUnread: built.binaryFilesUnread,
14448
+ branch: serverOps.projectBranch(),
14449
+ buildable: true
14429
14450
  });
14430
14451
  }
14431
14452
  },
@@ -16536,6 +16557,82 @@ function getIsRemoteRequired(schemas) {
16536
16557
  return false;
16537
16558
  }
16538
16559
 
16560
+ /**
16561
+ * What a tab builds a job from: this deployment's embedded source, the
16562
+ * commits since it (as though pending: together they are the branch's head),
16563
+ * and the job's own pending changes on top. Managed and connected alike.
16564
+ *
16565
+ * `split.archive` is the managed job's archive; a connected job's archive is
16566
+ * the push's, made from its own changes alone, by the caller.
16567
+ */
16568
+ async function buildSourcesOf(serverOps, patchIds, commit) {
16569
+ const chain = await serverOps.fetchPatches({
16570
+ patchIds: undefined,
16571
+ excludePatchOps: false
16572
+ });
16573
+ if (chain.error) {
16574
+ return {
16575
+ status: 500,
16576
+ json: {
16577
+ message: chain.error.message
16578
+ }
16579
+ };
16580
+ }
16581
+ /*
16582
+ * The job's content, in two steps from this build's embedded source.
16583
+ *
16584
+ * The commits since this build, as though pending, are the branch's
16585
+ * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
16586
+ * out of its own changes (docs/app-mode.md, "Publishing is a queued
16587
+ * job"), and were that ever not so, the seal would restart the job
16588
+ * rather than publish it. Chain order, both times.
16589
+ */
16590
+ const job = new Set(patchIds);
16591
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
16592
+ ...patch,
16593
+ appliedAt: null
16594
+ }));
16595
+ const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
16596
+ const known = new Set(chain.patches.map(patch => patch.patchId));
16597
+ const gone = patchIds.filter(id => !known.has(id));
16598
+ if (gone.length > 0) {
16599
+ return {
16600
+ status: 409,
16601
+ json: {
16602
+ message: `This publish's changes are no longer all there (${gone.length} discarded).`
16603
+ }
16604
+ };
16605
+ }
16606
+ const prepareOf = patches => serverOps.prepare({
16607
+ ...serverOps.analyzePatches(patches, chain.commits, commit),
16608
+ ...chain,
16609
+ patches
16610
+ });
16611
+ const chainOnly = await prepareOf(committed);
16612
+ const withJob = await prepareOf([...committed, ...pending]);
16613
+ if (chainOnly.hasErrors || withJob.hasErrors) {
16614
+ return {
16615
+ status: 500,
16616
+ json: {
16617
+ message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
16618
+ }
16619
+ };
16620
+ }
16621
+ const split = splitJobPrepare({
16622
+ chainOnly,
16623
+ withJob,
16624
+ jobPatchIds: patchIds
16625
+ });
16626
+ // Read now, while the job's changes still hold their files.
16627
+ const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
16628
+ return {
16629
+ split,
16630
+ sourceFiles: split.buildSourceFiles,
16631
+ binaryFiles: binaries.files,
16632
+ binaryFilesUnread: binaries.unread
16633
+ };
16634
+ }
16635
+
16539
16636
  /**
16540
16637
  * `/publish-job-prepare`'s answer from content's: the job as content has it
16541
16638
  * now, and what the tab builds from. Content's refusal is passed on -- 409
@@ -8601,11 +8601,43 @@ class ValOpsHttp extends ValOps {
8601
8601
  * file read for one with "no GitHub repo".
8602
8602
  */
8603
8603
  projectSource;
8604
+ /** See `publishJob` on {@link ValApiOptions}; sent only with no commit. */
8605
+ publishJob;
8604
8606
 
8605
8607
  /** Did the host hand over the running build's source? See `projectSource`. */
8606
8608
  embedsSource() {
8607
8609
  return this.projectSource !== null;
8608
8610
  }
8611
+
8612
+ /**
8613
+ * The Val source this build embeds, as git blob shas by path from the
8614
+ * project root: every module and `val.modules.*`. `null` when it embeds no
8615
+ * source.
8616
+ *
8617
+ * For a build of no commit -- the template's, which a project `/new` made
8618
+ * with a repository is served until CI's first build -- so the content
8619
+ * service can compare it with the branch file by file, having no commit to
8620
+ * compare from. A git blob sha is `sha1("blob <bytes>\0" + content)`, which is
8621
+ * what makes it comparable with the branch's tree without reading a file.
8622
+ */
8623
+ async embeddedValSourceShas() {
8624
+ if (this.projectSource === null) return null;
8625
+ const encoder = new TextEncoder();
8626
+ const shas = {};
8627
+ for (const [path, content] of Object.entries(this.projectSource)) {
8628
+ const base = path.slice(path.lastIndexOf("/") + 1);
8629
+ const isValSource = path.endsWith(".val.ts") || path.endsWith(".val.js") || path.endsWith(".val.json") || base === "val.modules.ts" || base === "val.modules.js";
8630
+ if (!isValSource) continue;
8631
+ const body = encoder.encode(content);
8632
+ const header = encoder.encode(`blob ${body.byteLength}\0`);
8633
+ const bytes = new Uint8Array(header.byteLength + body.byteLength);
8634
+ bytes.set(header, 0);
8635
+ bytes.set(body, header.byteLength);
8636
+ const digest = await crypto.subtle.digest("SHA-1", bytes);
8637
+ shas[path.replace(/^\/+/, "")] = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, "0")).join("");
8638
+ }
8639
+ return shas;
8640
+ }
8609
8641
  /**
8610
8642
  * What the content service last said this project expects of its publisher.
8611
8643
  *
@@ -8654,6 +8686,7 @@ class ValOpsHttp extends ValOps {
8654
8686
  };
8655
8687
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
8656
8688
  this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
8689
+ this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
8657
8690
  this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
8658
8691
  }
8659
8692
  /**
@@ -8683,6 +8716,15 @@ class ValOpsHttp extends ValOps {
8683
8716
  if (this.git !== null) {
8684
8717
  return null;
8685
8718
  }
8719
+ /*
8720
+ * Content runs this project's publishes as jobs, and its prepare decides
8721
+ * what the push goes on top of -- including for a site that was never
8722
+ * built from a commit (a project `/new` made, before its first CI build).
8723
+ * Refusing here would refuse what content can answer.
8724
+ */
8725
+ if (this.publishesAsJobs()) {
8726
+ return null;
8727
+ }
8686
8728
  if (((_this$projectExpectat = this.projectExpectation) === null || _this$projectExpectat === void 0 ? void 0 : _this$projectExpectat.sourceMode) !== "connected") {
8687
8729
  return null;
8688
8730
  }
@@ -9173,6 +9215,8 @@ class ValOpsHttp extends ValOps {
9173
9215
  ...(this.git ? {
9174
9216
  branch: this.git.branch,
9175
9217
  commitSha: this.git.commit
9218
+ } : this.publishJob !== null ? {
9219
+ job: this.publishJob
9176
9220
  } : {})
9177
9221
  }),
9178
9222
  headers: {
@@ -9354,6 +9398,9 @@ class ValOpsHttp extends ValOps {
9354
9398
  if (this.git) {
9355
9399
  params.push(["branch", this.git.branch]);
9356
9400
  params.push(["commit", this.git.commit]);
9401
+ } else if (this.publishJob !== null) {
9402
+ // A build a tab made has no commit, and says which job it was made for.
9403
+ params.push(["job", this.publishJob]);
9357
9404
  }
9358
9405
  if (filters.patchIds) {
9359
9406
  for (const patchId of filters.patchIds) {
@@ -12091,6 +12138,9 @@ async function initHandlerOptions(route, opts, config) {
12091
12138
  ...(opts.projectSource !== undefined ? {
12092
12139
  projectSource: opts.projectSource
12093
12140
  } : {}),
12141
+ ...(opts.publishJob !== undefined ? {
12142
+ publishJob: opts.publishJob
12143
+ } : {}),
12094
12144
  project: maybeValProject,
12095
12145
  valEnableRedirectUrl,
12096
12146
  valDisableRedirectUrl,
@@ -12160,6 +12210,9 @@ function createValOps(valModules, options) {
12160
12210
  config: options.config,
12161
12211
  ...(options.projectSource !== undefined ? {
12162
12212
  projectSource: options.projectSource
12213
+ } : {}),
12214
+ ...(options.publishJob !== undefined ? {
12215
+ publishJob: options.publishJob
12163
12216
  } : {})
12164
12217
  });
12165
12218
  }
@@ -14258,8 +14311,12 @@ const ValServer = (valModules, options, callbacks) => {
14258
14311
  /*
14259
14312
  * CONNECTED: the job's sources are what `/save` would commit -- its
14260
14313
  * own changes, applied as `/save` applies them -- and content pushes
14261
- * them at the seal. Nothing is built here: CI builds after the push,
14262
- * so the answer carries the job and no sources.
14314
+ * them at the seal. The tab ALSO builds the job, as a managed one is
14315
+ * built (docs/app-mode.md, "Since changed: connected builds in the
14316
+ * tab"): from this deployment's embedded source, the commits since it
14317
+ * and the job's changes. A deployment that embeds no source answers
14318
+ * `buildable: false`, and the tab reports "no build": CI builds the
14319
+ * push either way.
14263
14320
  */
14264
14321
  if (serverOps.sourceMode() === "connected") {
14265
14322
  var _options$config$files;
@@ -14305,90 +14362,54 @@ const ValServer = (valModules, options, callbacks) => {
14305
14362
  withJob: prepared,
14306
14363
  jobPatchIds: patchIds
14307
14364
  });
14365
+ const build = serverOps.embedsSource() ? await buildSourcesOf(serverOps, patchIds, commit) : null;
14366
+ if (build !== null && "status" in build) return build;
14367
+ /*
14368
+ * A build of no commit says what Val source it holds instead, so
14369
+ * the content service can compare it with the branch -- the
14370
+ * template's build a `/new` project is served before CI's first.
14371
+ */
14372
+ const deploymentFiles = commit === undefined ? await serverOps.embeddedValSourceShas() : null;
14308
14373
  const sent = await serverOps.prepareJob(jobId, {
14309
14374
  tab,
14310
14375
  filesDirectory: ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val",
14311
14376
  ...split.archive,
14312
14377
  ...(commit !== undefined ? {
14313
14378
  gitCommit: commit
14379
+ } : {}),
14380
+ ...(deploymentFiles !== null ? {
14381
+ deploymentFiles
14382
+ } : {}),
14383
+ // Said, so content waits for this tab's build. A server that does
14384
+ // not say it gets CI's: what Val servers before this one get.
14385
+ ...(build !== null ? {
14386
+ tabBuilds: true
14314
14387
  } : {})
14315
14388
  });
14316
14389
  return jobPrepareAnswer(sent, {
14317
- sourceFiles: {},
14318
- binaryFiles: {},
14319
- binaryFilesUnread: [],
14320
- branch: serverOps.projectBranch()
14390
+ sourceFiles: (build === null || build === void 0 ? void 0 : build.sourceFiles) ?? {},
14391
+ binaryFiles: (build === null || build === void 0 ? void 0 : build.binaryFiles) ?? {},
14392
+ binaryFilesUnread: (build === null || build === void 0 ? void 0 : build.binaryFilesUnread) ?? [],
14393
+ branch: serverOps.projectBranch(),
14394
+ buildable: build !== null
14321
14395
  });
14322
14396
  }
14323
- const chain = await serverOps.fetchPatches({
14324
- patchIds: undefined,
14325
- excludePatchOps: false
14326
- });
14327
- if (chain.error) {
14328
- return {
14329
- status: 500,
14330
- json: {
14331
- message: chain.error.message
14332
- }
14333
- };
14334
- }
14335
- /*
14336
- * The job's content, in two steps from this build's embedded source.
14337
- *
14338
- * The commits since this build, as though pending, are the branch's
14339
- * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
14340
- * out of its own changes (docs/app-mode.md, "Publishing is a queued
14341
- * job"), and were that ever not so, the seal would restart the job
14342
- * rather than publish it. Chain order, both times.
14343
- */
14344
- const job = new Set(patchIds);
14345
- const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
14346
- ...patch,
14347
- appliedAt: null
14348
- }));
14349
- const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
14350
- const known = new Set(chain.patches.map(patch => patch.patchId));
14351
- const gone = patchIds.filter(id => !known.has(id));
14352
- if (gone.length > 0) {
14353
- return {
14354
- status: 409,
14355
- json: {
14356
- message: `This publish's changes are no longer all there (${gone.length} discarded).`
14357
- }
14358
- };
14359
- }
14360
- const prepareOf = patches => serverOps.prepare({
14361
- ...serverOps.analyzePatches(patches, chain.commits, commit),
14362
- ...chain,
14363
- patches
14364
- });
14365
- const chainOnly = await prepareOf(committed);
14366
- const withJob = await prepareOf([...committed, ...pending]);
14367
- if (chainOnly.hasErrors || withJob.hasErrors) {
14368
- return {
14369
- status: 500,
14370
- json: {
14371
- message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
14372
- }
14373
- };
14374
- }
14375
- const split = splitJobPrepare({
14376
- chainOnly,
14377
- withJob,
14378
- jobPatchIds: patchIds
14379
- });
14380
- // Read now, while the job's changes still hold their files.
14381
- const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
14397
+ const built = await buildSourcesOf(serverOps, patchIds, commit);
14398
+ if ("status" in built) return built;
14399
+ const {
14400
+ split
14401
+ } = built;
14382
14402
  const sent = await serverOps.prepareJob(jobId, {
14383
14403
  tab,
14384
14404
  filesDirectory: ((_options$config$files2 = options.config.files) === null || _options$config$files2 === void 0 ? void 0 : _options$config$files2.directory) || "/public/val",
14385
14405
  ...split.archive
14386
14406
  });
14387
14407
  return jobPrepareAnswer(sent, {
14388
- sourceFiles: split.buildSourceFiles,
14389
- binaryFiles: binaries.files,
14390
- binaryFilesUnread: binaries.unread,
14391
- branch: serverOps.projectBranch()
14408
+ sourceFiles: built.sourceFiles,
14409
+ binaryFiles: built.binaryFiles,
14410
+ binaryFilesUnread: built.binaryFilesUnread,
14411
+ branch: serverOps.projectBranch(),
14412
+ buildable: true
14392
14413
  });
14393
14414
  }
14394
14415
  },
@@ -16499,6 +16520,82 @@ function getIsRemoteRequired(schemas) {
16499
16520
  return false;
16500
16521
  }
16501
16522
 
16523
+ /**
16524
+ * What a tab builds a job from: this deployment's embedded source, the
16525
+ * commits since it (as though pending: together they are the branch's head),
16526
+ * and the job's own pending changes on top. Managed and connected alike.
16527
+ *
16528
+ * `split.archive` is the managed job's archive; a connected job's archive is
16529
+ * the push's, made from its own changes alone, by the caller.
16530
+ */
16531
+ async function buildSourcesOf(serverOps, patchIds, commit) {
16532
+ const chain = await serverOps.fetchPatches({
16533
+ patchIds: undefined,
16534
+ excludePatchOps: false
16535
+ });
16536
+ if (chain.error) {
16537
+ return {
16538
+ status: 500,
16539
+ json: {
16540
+ message: chain.error.message
16541
+ }
16542
+ };
16543
+ }
16544
+ /*
16545
+ * The job's content, in two steps from this build's embedded source.
16546
+ *
16547
+ * The commits since this build, as though pending, are the branch's
16548
+ * head. The job's pending changes on top of them are its content: whatever was applied since the job started came
16549
+ * out of its own changes (docs/app-mode.md, "Publishing is a queued
16550
+ * job"), and were that ever not so, the seal would restart the job
16551
+ * rather than publish it. Chain order, both times.
16552
+ */
16553
+ const job = new Set(patchIds);
16554
+ const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
16555
+ ...patch,
16556
+ appliedAt: null
16557
+ }));
16558
+ const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
16559
+ const known = new Set(chain.patches.map(patch => patch.patchId));
16560
+ const gone = patchIds.filter(id => !known.has(id));
16561
+ if (gone.length > 0) {
16562
+ return {
16563
+ status: 409,
16564
+ json: {
16565
+ message: `This publish's changes are no longer all there (${gone.length} discarded).`
16566
+ }
16567
+ };
16568
+ }
16569
+ const prepareOf = patches => serverOps.prepare({
16570
+ ...serverOps.analyzePatches(patches, chain.commits, commit),
16571
+ ...chain,
16572
+ patches
16573
+ });
16574
+ const chainOnly = await prepareOf(committed);
16575
+ const withJob = await prepareOf([...committed, ...pending]);
16576
+ if (chainOnly.hasErrors || withJob.hasErrors) {
16577
+ return {
16578
+ status: 500,
16579
+ json: {
16580
+ message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
16581
+ }
16582
+ };
16583
+ }
16584
+ const split = splitJobPrepare({
16585
+ chainOnly,
16586
+ withJob,
16587
+ jobPatchIds: patchIds
16588
+ });
16589
+ // Read now, while the job's changes still hold their files.
16590
+ const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
16591
+ return {
16592
+ split,
16593
+ sourceFiles: split.buildSourceFiles,
16594
+ binaryFiles: binaries.files,
16595
+ binaryFilesUnread: binaries.unread
16596
+ };
16597
+ }
16598
+
16502
16599
  /**
16503
16600
  * `/publish-job-prepare`'s answer from content's: the job as content has it
16504
16601
  * now, and what the tab builds from. Content's refusal is passed on -- 409
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.138.3",
19
+ "version": "0.138.5",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0",
@@ -31,8 +31,8 @@
31
31
  "zod": "^4.4.3",
32
32
  "zod-validation-error": "^5.0.0",
33
33
  "@valbuild/core": "0.138.2",
34
- "@valbuild/shared": "0.138.2",
35
- "@valbuild/ui": "0.138.3"
34
+ "@valbuild/shared": "0.138.4",
35
+ "@valbuild/ui": "0.138.5"
36
36
  },
37
37
  "engines": {
38
38
  "node": "^20.19.0 || >=22"