@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 +29 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +20 -0
- package/dist/declarations/src/ValRouter.d.ts +7 -0
- package/dist/declarations/src/ValServer.d.ts +2 -0
- package/dist/valbuild-server.cjs.dev.js +166 -69
- package/dist/valbuild-server.cjs.prod.js +166 -69
- package/dist/valbuild-server.esm.js +166 -69
- package/package.json +3 -3
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.
|
|
14299
|
-
*
|
|
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
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
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:
|
|
14426
|
-
binaryFiles:
|
|
14427
|
-
binaryFilesUnread:
|
|
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.
|
|
14299
|
-
*
|
|
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
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
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:
|
|
14426
|
-
binaryFiles:
|
|
14427
|
-
binaryFilesUnread:
|
|
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.
|
|
14262
|
-
*
|
|
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
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
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:
|
|
14389
|
-
binaryFiles:
|
|
14390
|
-
binaryFilesUnread:
|
|
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.
|
|
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.
|
|
35
|
-
"@valbuild/ui": "0.138.
|
|
34
|
+
"@valbuild/shared": "0.138.4",
|
|
35
|
+
"@valbuild/ui": "0.138.5"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": "^20.19.0 || >=22"
|