@valbuild/shared 0.148.6 → 0.149.7
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 +17 -0
- package/client/dist/valbuild-shared-client.cjs.dev.js +2 -1
- package/client/dist/valbuild-shared-client.cjs.prod.js +2 -1
- package/client/dist/valbuild-shared-client.esm.js +2 -1
- package/dist/acquireTypes-50960f2c.esm.js +1801 -0
- package/dist/acquireTypes-de10005d.cjs.dev.js +1815 -0
- package/dist/acquireTypes-e9467a2a.cjs.prod.js +1815 -0
- package/dist/createForOfIteratorHelper-3db5d737.esm.js +121 -0
- package/dist/createForOfIteratorHelper-90e6a8b6.cjs.prod.js +129 -0
- package/dist/createForOfIteratorHelper-d86bb352.cjs.dev.js +129 -0
- package/dist/declarations/src/internal/ApiRoutes.d.ts +3 -3
- package/dist/declarations/src/internal/index.d.ts +1 -0
- package/dist/declarations/src/internal/selfCheck.d.ts +51 -0
- package/dist/declarations/src/publish/buildProposal.d.ts +106 -0
- package/dist/declarations/src/publish/builder.d.ts +22 -0
- package/dist/declarations/src/publish/callJson.d.ts +21 -0
- package/dist/declarations/src/publish/index.d.ts +23 -0
- package/dist/declarations/src/publish/jobClient.d.ts +96 -0
- package/dist/declarations/src/publish/loadedLayer.d.ts +50 -0
- package/dist/declarations/src/publish/proposalProject.d.ts +2 -0
- package/dist/declarations/src/publish/publishClient.d.ts +95 -0
- package/dist/declarations/src/publish/runStudioDeploy.d.ts +244 -0
- package/dist/declarations/src/publish/runStudioJob.d.ts +102 -0
- package/dist/declarations/src/publish/runStudioPublish.d.ts +80 -0
- package/dist/declarations/src/typecheck/acquireTypes.d.ts +109 -0
- package/dist/declarations/src/typecheck/adeptSources.d.ts +9 -0
- package/dist/declarations/src/typecheck/index.d.ts +20 -0
- package/dist/declarations/src/typecheck/linkedFileSystem.d.ts +53 -0
- package/dist/declarations/src/typecheck/lockedVersions.d.ts +14 -0
- package/dist/declarations/src/typecheck/pnpmLayout.d.ts +95 -0
- package/dist/declarations/src/typecheck/pnpmLockfile.d.ts +57 -0
- package/dist/declarations/src/typecheck/proposalTypecheck.d.ts +76 -0
- package/dist/declarations/src/typecheck/testing/index.d.ts +6 -0
- package/dist/declarations/src/typecheck/testing/nodeModulesSources.d.ts +5 -0
- package/dist/declarations/src/typecheck/typecheckProject.d.ts +89 -0
- package/dist/declarations/src/typecheck/typescriptCompiler.d.ts +52 -0
- package/dist/{getSitemapTree-40d76bf8.cjs.prod.js → getSitemapTree-0fa0569c.cjs.dev.js} +5 -131
- package/dist/{getSitemapTree-cd006a36.esm.js → getSitemapTree-5afbc3ac.esm.js} +2 -121
- package/dist/{getSitemapTree-a7f2cc6d.cjs.dev.js → getSitemapTree-667bcb3c.cjs.prod.js} +5 -131
- package/dist/proposalProject-28550146.cjs.prod.js +18 -0
- package/dist/proposalProject-a3d3ad76.esm.js +16 -0
- package/dist/proposalProject-cdc53711.cjs.dev.js +18 -0
- package/dist/protocol-1c13de0d.cjs.prod.js +433 -0
- package/dist/protocol-3b9ef786.esm.js +413 -0
- package/dist/protocol-d8a6efda.cjs.dev.js +433 -0
- package/dist/slicedToArray-62e428df.cjs.dev.js +309 -0
- package/dist/slicedToArray-6cd80315.esm.js +299 -0
- package/dist/slicedToArray-af50e684.cjs.prod.js +309 -0
- package/internal/dist/valbuild-shared-internal.cjs.dev.js +419 -888
- package/internal/dist/valbuild-shared-internal.cjs.prod.js +419 -888
- package/internal/dist/valbuild-shared-internal.esm.js +236 -707
- package/package.json +28 -5
- package/publish/dist/valbuild-shared-publish.cjs.d.ts +2 -0
- package/publish/dist/valbuild-shared-publish.cjs.dev.js +2466 -0
- package/publish/dist/valbuild-shared-publish.cjs.js +7 -0
- package/publish/dist/valbuild-shared-publish.cjs.prod.js +2466 -0
- package/publish/dist/valbuild-shared-publish.esm.js +2428 -0
- package/publish/package.json +4 -0
- package/typecheck/dist/valbuild-shared-typecheck.cjs.d.ts +2 -0
- package/typecheck/dist/valbuild-shared-typecheck.cjs.dev.js +1173 -0
- package/typecheck/dist/valbuild-shared-typecheck.cjs.js +7 -0
- package/typecheck/dist/valbuild-shared-typecheck.cjs.prod.js +1173 -0
- package/typecheck/dist/valbuild-shared-typecheck.esm.js +1152 -0
- package/typecheck/package.json +4 -0
- package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.d.ts +2 -0
- package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.dev.js +202 -0
- package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.js +7 -0
- package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.prod.js +202 -0
- package/typecheck/testing/dist/valbuild-shared-typecheck-testing.esm.js +177 -0
- package/typecheck/testing/package.json +4 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type BuildTargetResponse, type JobStepBody, type PressResponse, type PublishRequestStatus, type PublishTabJob } from "../internal/index.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Publishing as a queued job, as a Studio tab speaks it.
|
|
5
|
+
*
|
|
6
|
+
* valbuild/home's docs/app-mode.md, "Publishing is a queued job", is the
|
|
7
|
+
* design: a press is a REQUEST that returns at once; the tab that pressed runs
|
|
8
|
+
* the job it is given -- prepare, build, upload -- and content runs verify and
|
|
9
|
+
* the seal. Everything goes through this deployment: the job routes through
|
|
10
|
+
* the publish proxy, and the prepare through this server's own route, which is
|
|
11
|
+
* the one place a job's sources can be computed.
|
|
12
|
+
*/
|
|
13
|
+
export type StudioJobClient = {
|
|
14
|
+
/** A press of Publish. Idempotent on `requestId`. */
|
|
15
|
+
press(requestId: string, tab: string): Promise<PressResponse>;
|
|
16
|
+
/**
|
|
17
|
+
* Publish in a proposal: press the merge of `proposal`. Idempotent on
|
|
18
|
+
* `requestId`, as `press` is; the merge checks run first and may refuse it.
|
|
19
|
+
* From the press on it is a request like any other.
|
|
20
|
+
*/
|
|
21
|
+
pressMerge(proposal: string, requestId: string, tab: string,
|
|
22
|
+
/**
|
|
23
|
+
* The checks the person chose to publish over, having been told what
|
|
24
|
+
* failed: `types`, an upgrade's new type errors (Publish anyway on home's
|
|
25
|
+
* upgrade page). Content refuses it over anything else.
|
|
26
|
+
*/
|
|
27
|
+
anyway?: "types"[]): Promise<PressResponse>;
|
|
28
|
+
/** Try again: resume a paused queue and press anew. */
|
|
29
|
+
tryAgain(requestId: string, tab: string): Promise<PressResponse>;
|
|
30
|
+
/** Where a press is. */
|
|
31
|
+
requestStatus(requestId: string): Promise<PublishRequestStatus>;
|
|
32
|
+
/** A free tab asking for queued work. */
|
|
33
|
+
next(tab: string): Promise<PublishTabJob | null>;
|
|
34
|
+
/** The job's prepare, by this server: its sources, and content's archive of them. */
|
|
35
|
+
prepare(job: PublishTabJob, tab: string): Promise<PreparedJob>;
|
|
36
|
+
/** The tab reports a step it ran. */
|
|
37
|
+
step(jobId: string, body: JobStepBody): Promise<PublishTabJob | null>;
|
|
38
|
+
/** The tab is still building. `false`: it no longer holds the job. */
|
|
39
|
+
renew(jobId: string, tab: string): Promise<boolean>;
|
|
40
|
+
cancel(jobId: string): Promise<boolean>;
|
|
41
|
+
/** Discard a failed job's changes; answers what another job still holds. */
|
|
42
|
+
discard(jobId: string): Promise<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Connected: the newest CI run the branch's workflow reported -- where
|
|
45
|
+
* "View run" goes for a build that failed. `null` when none was reported.
|
|
46
|
+
*/
|
|
47
|
+
newestCiRun(): Promise<{
|
|
48
|
+
status: "failed" | "succeeded";
|
|
49
|
+
url: string | null;
|
|
50
|
+
} | null>;
|
|
51
|
+
};
|
|
52
|
+
/** What the tab builds a job from: `/api/val/publish-job-prepare`'s answer. */
|
|
53
|
+
export type PreparedJob = {
|
|
54
|
+
job: PublishTabJob | null;
|
|
55
|
+
sourceFiles: Record<string, string | null>;
|
|
56
|
+
binaryFiles: Record<string, string>;
|
|
57
|
+
binaryFilesUnread: string[];
|
|
58
|
+
branch: string | null;
|
|
59
|
+
/** Can this tab build the job from these sources? See the route's `buildable`. */
|
|
60
|
+
buildable: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* What to build against, when it is not the site's: a merge of a proposal
|
|
63
|
+
* that changed its dependencies builds on the proposal's own framework and
|
|
64
|
+
* layer (valbuild/home `docs/adept.md`, step 6). Absent, the deploy asks
|
|
65
|
+
* the site's, as it always did.
|
|
66
|
+
*/
|
|
67
|
+
target?: BuildTargetResponse;
|
|
68
|
+
};
|
|
69
|
+
export declare function createStudioJobClient(options: ({
|
|
70
|
+
api: string;
|
|
71
|
+
} | {
|
|
72
|
+
/** Content's publish API itself: see `PublishApiAt`. */
|
|
73
|
+
publishApi: string;
|
|
74
|
+
/** The project's proposals on content, `<content>/v1/<org>/<project>/proposals`. */
|
|
75
|
+
proposalsApi: string;
|
|
76
|
+
}) & {
|
|
77
|
+
fetchImpl?: typeof fetch;
|
|
78
|
+
}): StudioJobClient;
|
|
79
|
+
/** A job's change that merges a proposal: `merge:<name>` (content's `mergeChange`). */
|
|
80
|
+
export declare function isMergeChange(change: string): boolean;
|
|
81
|
+
/** `/publish-jobs/:id/merge-prepare`'s answer: the merge's files, from content. */
|
|
82
|
+
export declare const MergePrepareAnswer: z.ZodObject<{
|
|
83
|
+
job: z.ZodNullable<z.ZodObject<{
|
|
84
|
+
id: z.ZodString;
|
|
85
|
+
step: z.ZodNullable<z.ZodEnum<{
|
|
86
|
+
prepare: "prepare";
|
|
87
|
+
build: "build";
|
|
88
|
+
upload: "upload";
|
|
89
|
+
}>>;
|
|
90
|
+
base: z.ZodNullable<z.ZodString>;
|
|
91
|
+
patches: z.ZodArray<z.ZodString>;
|
|
92
|
+
contentOnly: z.ZodOptional<z.ZodLiteral<true>>;
|
|
93
|
+
}, z.core.$strip>>;
|
|
94
|
+
sourceFiles: z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>;
|
|
95
|
+
target: z.ZodOptional<z.ZodUnknown>;
|
|
96
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Has the site moved onto other dependencies since this page loaded?
|
|
3
|
+
*
|
|
4
|
+
* A managed project's Studio is served by the site's own live build, so the
|
|
5
|
+
* Studio on screen -- its builder, its Val -- is the version that build was
|
|
6
|
+
* made with. When the site is updated (Settings → Updates, in this tab or
|
|
7
|
+
* another), every Studio opened before that is the OLD version, and a publish
|
|
8
|
+
* from one would build the site with the old builder, or, if it read its build
|
|
9
|
+
* target before the update went live, put the old dependencies back.
|
|
10
|
+
*
|
|
11
|
+
* So the page remembers which dependency layer the live build had when it
|
|
12
|
+
* loaded, and a publish compares that with the live build's now. They differ
|
|
13
|
+
* only when somebody moved the site's dependencies in between; the answer is
|
|
14
|
+
* to reload, which loses nothing -- unpublished changes are the server's.
|
|
15
|
+
*
|
|
16
|
+
* Read from `/__api/head`, which the platform answers on every site without a
|
|
17
|
+
* credential and which names the layer (`projectVendorRev`). Anything that
|
|
18
|
+
* cannot answer -- not on the platform, the loader unreachable, an older
|
|
19
|
+
* loader -- reads as "cannot tell", and "cannot tell" never blocks a publish.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The layer, or `undefined` for "cannot tell". `null` is never READ -- a head
|
|
23
|
+
* that names no layer is "cannot tell" too, see `readLiveLayer` -- but is what
|
|
24
|
+
* a build target says for a project with none, which is why it is comparable.
|
|
25
|
+
*/
|
|
26
|
+
export type LayerReading = string | null | undefined;
|
|
27
|
+
export declare function readLiveLayer(fetchImpl?: typeof fetch): Promise<LayerReading>;
|
|
28
|
+
/**
|
|
29
|
+
* Stale only on a positive answer from both readings. A page that could not
|
|
30
|
+
* tell what it loaded on, or cannot tell now, publishes as before.
|
|
31
|
+
*/
|
|
32
|
+
export declare function layerMoved(loaded: LayerReading, now: LayerReading): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Take the reading this page is compared against. Once per page: the first
|
|
35
|
+
* call wins, because a later one would record the layer an update just moved
|
|
36
|
+
* to and make a stale page look current.
|
|
37
|
+
*/
|
|
38
|
+
export declare function rememberLoadedLayer(fetchImpl?: typeof fetch): void;
|
|
39
|
+
/** What {@link rememberLoadedLayer} read, or `undefined` if it never ran. */
|
|
40
|
+
export declare function loadedLayer(): Promise<LayerReading>;
|
|
41
|
+
/** Has the live build's layer moved since this page loaded? */
|
|
42
|
+
export declare function siteMovedSinceLoad(fetchImpl?: typeof fetch): Promise<boolean>;
|
|
43
|
+
/** The code a deploy refuses with when this page is out of date. */
|
|
44
|
+
export declare const STUDIO_OUT_OF_DATE = "STUDIO_OUT_OF_DATE";
|
|
45
|
+
export declare const STUDIO_OUT_OF_DATE_MESSAGE: string;
|
|
46
|
+
export declare function markStudioOutOfDate(): void;
|
|
47
|
+
export declare function studioOutOfDateRefusals(): number;
|
|
48
|
+
export declare function subscribeStudioOutOfDate(listener: () => void): () => void;
|
|
49
|
+
/** For tests: forget everything this module has learnt. */
|
|
50
|
+
export declare function resetLoadedLayerForTests(): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio's side of the content service's publish API.
|
|
3
|
+
*
|
|
4
|
+
* Same conversation `val publish` has -- declare, upload, confirm, verify,
|
|
5
|
+
* promote -- and deliberately the same PARSERS: `parseDeclare` and the rest
|
|
6
|
+
* come from `@valbuild/shared/internal`, where they live precisely so that two
|
|
7
|
+
* publishers cannot come to disagree about what content said.
|
|
8
|
+
*
|
|
9
|
+
* ## Two things differ from the CLI's client, and both are the same fact
|
|
10
|
+
*
|
|
11
|
+
* A browser holds a session cookie for the app's origin and no credential
|
|
12
|
+
* content would accept -- content's publish API takes a project token and
|
|
13
|
+
* refuses everything else. So:
|
|
14
|
+
*
|
|
15
|
+
* - **Every call goes to this origin**, at `/api/val/publish-api`, and the
|
|
16
|
+
* deployment swaps the credential on the way (see `ValOpsHttp.publishApi`).
|
|
17
|
+
* Nothing here has a token, and nothing here should ever be given one.
|
|
18
|
+
* - **The uploads do not.** A slot is a presigned URL for object storage, and
|
|
19
|
+
* those bytes go straight there rather than through the isolate -- which is
|
|
20
|
+
* the whole reason the API hands out slots instead of accepting the artifacts
|
|
21
|
+
* itself. `credentials: "omit"` on those, because a presigned URL carries its
|
|
22
|
+
* own authorisation and a cookie sent to a storage host is a cookie leaked to
|
|
23
|
+
* one.
|
|
24
|
+
*/
|
|
25
|
+
import { ArtifactsResponse, BuildTargetResponse, DeclareBody, DeclareResponse, PromoteResponse, StatusResponse, UploadSlot, VerifyResponse } from "../internal/index.js";
|
|
26
|
+
export { StudioPublishError, callJson, isTransientPublishError, } from "./callJson.js";
|
|
27
|
+
/** An artifact the live build has and this publish re-declares by hash. */
|
|
28
|
+
export type CarriedArtifact = {
|
|
29
|
+
key: string;
|
|
30
|
+
sha256: string;
|
|
31
|
+
bytes: number;
|
|
32
|
+
};
|
|
33
|
+
/** See {@link StudioPublishClient.publicFiles}. */
|
|
34
|
+
export type LivePublicFiles = {
|
|
35
|
+
carried: CarriedArtifact[];
|
|
36
|
+
} | {
|
|
37
|
+
paths: string[];
|
|
38
|
+
} | null;
|
|
39
|
+
/** Object storage refused the bytes, or could not be reached. */
|
|
40
|
+
export declare class StudioUploadError extends Error {
|
|
41
|
+
readonly statusCode: number;
|
|
42
|
+
readonly key: string;
|
|
43
|
+
constructor(statusCode: number, key: string, message: string);
|
|
44
|
+
}
|
|
45
|
+
export type StudioPublishClient = {
|
|
46
|
+
/**
|
|
47
|
+
* What to build against, and what to build -- the two reads a publish makes
|
|
48
|
+
* before it has anything to declare.
|
|
49
|
+
*
|
|
50
|
+
* On this type rather than beside it because they go through the same proxy
|
|
51
|
+
* with the same credential, and because a caller holding one of them and not
|
|
52
|
+
* the other can do nothing at all.
|
|
53
|
+
*/
|
|
54
|
+
buildTarget(): Promise<BuildTargetResponse>;
|
|
55
|
+
/** `null` when the project has never published. */
|
|
56
|
+
projectSource(): Promise<Record<string, string> | null>;
|
|
57
|
+
/**
|
|
58
|
+
* What the live build serves from `public/`: by hash when content published
|
|
59
|
+
* it, by path when only the loader knows, `null` when neither can say. See
|
|
60
|
+
* `publicFiles` and `publicPaths` on `GET /v1/project-source`, which this
|
|
61
|
+
* reads with {@link projectSource} in one request.
|
|
62
|
+
*/
|
|
63
|
+
publicFiles(): Promise<LivePublicFiles>;
|
|
64
|
+
declare(body: DeclareBody): Promise<DeclareResponse>;
|
|
65
|
+
confirmArtifacts(publishId: string): Promise<ArtifactsResponse>;
|
|
66
|
+
verify(publishId: string): Promise<VerifyResponse>;
|
|
67
|
+
promote(publishId: string): Promise<PromoteResponse>;
|
|
68
|
+
status(publishId: string): Promise<StatusResponse>;
|
|
69
|
+
/** The artifact's bytes, straight to object storage. */
|
|
70
|
+
upload(slot: UploadSlot, body: string): Promise<void>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A slot's permission to write has run out.
|
|
74
|
+
*
|
|
75
|
+
* Its own shape because it is not a failure: the answer is to declare again for
|
|
76
|
+
* fresh slots, and a publish that treated it as one would give up an upload
|
|
77
|
+
* short of the finish.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isExpiredSlot(error: unknown): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Where content's publish API is reached: through Val's proxy on this origin
|
|
82
|
+
* (`api`, the Studio), or directly (`publishApi`, `<content>/v1`: home's
|
|
83
|
+
* upgrade page, whose `fetchImpl` adds the project token).
|
|
84
|
+
*/
|
|
85
|
+
export type PublishApiAt = {
|
|
86
|
+
/** Val's API root on this origin, e.g. `/api/val`. */
|
|
87
|
+
api: string;
|
|
88
|
+
} | {
|
|
89
|
+
/** Content's publish API itself, e.g. `https://content.val.build/v1`. */
|
|
90
|
+
publishApi: string;
|
|
91
|
+
};
|
|
92
|
+
export declare function publishApiOf(at: PublishApiAt): string;
|
|
93
|
+
export declare function createStudioPublishClient(options: PublishApiAt & {
|
|
94
|
+
fetchImpl?: typeof fetch;
|
|
95
|
+
}): StudioPublishClient;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A build, from the Studio, handed to content.
|
|
3
|
+
*
|
|
4
|
+
* Two callers. A publish JOB's tab builds the job's content and stops once
|
|
5
|
+
* its artifacts are confirmed (`until: "confirmed"`): content verifies and
|
|
6
|
+
* seals it, and mints the commit then (valbuild/home, docs/app-mode.md,
|
|
7
|
+
* "Publishing is a queued job"). An UPDATE builds the live content against a
|
|
8
|
+
* new dependency layer and takes it live itself. Either way what is here is
|
|
9
|
+
* the part a managed project has nobody else to do -- read what to build and
|
|
10
|
+
* what to build it from, build it, and hand the result to content.
|
|
11
|
+
*
|
|
12
|
+
* ## Everything it cannot do itself is a parameter
|
|
13
|
+
*
|
|
14
|
+
* There are four, and each is something this package must not contain:
|
|
15
|
+
*
|
|
16
|
+
* - **`client`** reaches content through this origin's proxy. A browser holds
|
|
17
|
+
* a session cookie and no project token; the deployment swaps the credential.
|
|
18
|
+
* - **`loadBuilder`** is the dynamic import, replaceable because nothing that
|
|
19
|
+
* imports it for real runs under jest — `@rolldown/browser` is ESM-only and
|
|
20
|
+
* this repository's jest is CommonJS.
|
|
21
|
+
* - **`generateRouteTree`** is TanStack's generator plus babel, which is 2.6 MB
|
|
22
|
+
* and is not on npm. See {@link StudioDeployOptions.generateRouteTree}.
|
|
23
|
+
* - **`commit`** is the commit an update is wired at, which only the caller has.
|
|
24
|
+
*
|
|
25
|
+
* So this module is testable without any of them, which is the point: the
|
|
26
|
+
* sequence is where the mistakes are, and the sequence is what is left.
|
|
27
|
+
*
|
|
28
|
+
* ## It does not throw
|
|
29
|
+
*
|
|
30
|
+
* Same rule as `runStudioPublish`, and for the same reason: every failure here
|
|
31
|
+
* means one thing to an editor — nothing is live, and here is why — so a caller
|
|
32
|
+
* that had to distinguish eight rejections would write eight identical
|
|
33
|
+
* branches. The `problems` from content survive because they are the only part
|
|
34
|
+
* a person can act on.
|
|
35
|
+
*/
|
|
36
|
+
import type { BuildTarget } from "@valbuild/tanstack-build";
|
|
37
|
+
import { PublishProblem } from "../internal/index.js";
|
|
38
|
+
import { CarriedArtifact, StudioPublishClient } from "./publishClient.js";
|
|
39
|
+
import { PublishPhase } from "./runStudioPublish.js";
|
|
40
|
+
import type { StudioBuilder } from "./builder.js";
|
|
41
|
+
import { LayerReading } from "./loadedLayer.js";
|
|
42
|
+
/**
|
|
43
|
+
* The binary files a commit wrote, as `/save` answered them.
|
|
44
|
+
*
|
|
45
|
+
* `unread` is files the server could not read back before committing. The
|
|
46
|
+
* build refuses over them: the site would link a file it does not serve.
|
|
47
|
+
*/
|
|
48
|
+
export type CommittedBinaryFiles = {
|
|
49
|
+
files: Record<string, string>;
|
|
50
|
+
unread: string[];
|
|
51
|
+
};
|
|
52
|
+
export type DeployPhase =
|
|
53
|
+
/**
|
|
54
|
+
* A publish job waiting its turn: queued behind the one before it. Never
|
|
55
|
+
* reported by the deploy itself -- it is how the Studio shows a press whose
|
|
56
|
+
* job has not started (see `publishProgress`).
|
|
57
|
+
*/
|
|
58
|
+
{
|
|
59
|
+
kind: "queued";
|
|
60
|
+
}
|
|
61
|
+
/** Waiting for the bundler. Only ever seen when the preload has not landed. */
|
|
62
|
+
| {
|
|
63
|
+
kind: "getting-ready";
|
|
64
|
+
} | {
|
|
65
|
+
kind: "reading";
|
|
66
|
+
} | {
|
|
67
|
+
kind: "building";
|
|
68
|
+
} | PublishPhase;
|
|
69
|
+
export type StudioDeployResult = {
|
|
70
|
+
status: "live";
|
|
71
|
+
url: string | null;
|
|
72
|
+
} | {
|
|
73
|
+
status: "already-live";
|
|
74
|
+
url: string | null;
|
|
75
|
+
}
|
|
76
|
+
/** A publish job's build, uploaded and confirmed: content does the rest. */
|
|
77
|
+
| {
|
|
78
|
+
status: "uploaded";
|
|
79
|
+
publishId: string;
|
|
80
|
+
} | {
|
|
81
|
+
status: "failed";
|
|
82
|
+
message: string;
|
|
83
|
+
problems: PublishProblem[];
|
|
84
|
+
};
|
|
85
|
+
export interface StudioDeployOptions {
|
|
86
|
+
client: StudioPublishClient;
|
|
87
|
+
/**
|
|
88
|
+
* For a publish job's tab: stop once the artifacts are confirmed, and
|
|
89
|
+
* answer with the publish id. Content runs verify and the seal itself, and
|
|
90
|
+
* `commit` is `null` -- a job's build is wired at no commit, because its
|
|
91
|
+
* commit is minted at the seal (valbuild/home, docs/app-mode.md: "A
|
|
92
|
+
* platform build bakes nothing and asks content").
|
|
93
|
+
*/
|
|
94
|
+
until?: "confirmed";
|
|
95
|
+
/**
|
|
96
|
+
* The commit `/save` just made, or `null` for a publish from no commit.
|
|
97
|
+
*
|
|
98
|
+
* The BRANCH is deliberately not here, because the caller has no honest
|
|
99
|
+
* source for one: `/stat` does not carry it and neither does `/save`, and a
|
|
100
|
+
* branch the browser invented is a branch content would be asked to record a
|
|
101
|
+
* build against on the browser's say-so. It is read out of the project's own
|
|
102
|
+
* `val.server.ts` instead — see {@link branchOf} — which is where the last
|
|
103
|
+
* build put it and which does not change between publishes.
|
|
104
|
+
*
|
|
105
|
+
* The commit is baked in before the build, and that is not bookkeeping: Val
|
|
106
|
+
* reads content from the content service AT A COMMIT, so a build wired at
|
|
107
|
+
* the previous one renders and edits a different version of its files than
|
|
108
|
+
* the code it is running.
|
|
109
|
+
*/
|
|
110
|
+
commit: string | null;
|
|
111
|
+
/**
|
|
112
|
+
* The publish job this build is for, baked in beside the commit so the
|
|
113
|
+
* build can say where it sits in the content service's chain -- it has no
|
|
114
|
+
* commit to say it with (`BUILT_FOR_JOB` in `@valbuild/tanstack-build`).
|
|
115
|
+
* Left out by a build that is not a job's (an update), which keeps the job
|
|
116
|
+
* of the source it was made from: an update changes no content.
|
|
117
|
+
*/
|
|
118
|
+
job?: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* The source files to build, by path, over the project's stored source --
|
|
121
|
+
* for a publish job, every file of the job's content, as the server's
|
|
122
|
+
* `/publish-job-prepare` rendered it -- or `null` when there are none to add
|
|
123
|
+
* (an update, which changes no content).
|
|
124
|
+
*
|
|
125
|
+
* Laid over the stored source before anything else, because the stored
|
|
126
|
+
* source is what the LAST build was made from: without this the build
|
|
127
|
+
* publishes the site as it was before the change. `null` values are files
|
|
128
|
+
* the change deleted. Paths may carry a leading `/` (Val's module paths do)
|
|
129
|
+
* and are matched without it.
|
|
130
|
+
*/
|
|
131
|
+
committedFiles?: Record<string, string | null> | null;
|
|
132
|
+
/**
|
|
133
|
+
* The binary files that commit wrote -- an image uploaded in this save --
|
|
134
|
+
* base64 by path, or `null` when there are none to add.
|
|
135
|
+
*
|
|
136
|
+
* Nothing else has them: the stored source is text, and the live build
|
|
137
|
+
* predates the upload. A file under `public/` is published as one; anything
|
|
138
|
+
* else goes to the build as an asset, where an import can find it.
|
|
139
|
+
*/
|
|
140
|
+
committedBinaryFiles?: CommittedBinaryFiles | null;
|
|
141
|
+
/**
|
|
142
|
+
* The branch the commit is on, as the server had it from the content
|
|
143
|
+
* service, or `null` when it did not say.
|
|
144
|
+
*
|
|
145
|
+
* Preferred over the branch baked into the last build's `val.server.ts`,
|
|
146
|
+
* which is the fallback: a project cloned from a template seed was wired at
|
|
147
|
+
* no commit and no branch, and a branchless publish is refused by a loader
|
|
148
|
+
* whose pointer follows one -- which admin configures for every project.
|
|
149
|
+
*/
|
|
150
|
+
branch?: string | null;
|
|
151
|
+
/**
|
|
152
|
+
* The live site's compiled stylesheet, used when this build produced none.
|
|
153
|
+
*
|
|
154
|
+
* A build in the browser cannot run Tailwind `@plugin`s, and the stored
|
|
155
|
+
* source may not carry the stylesheets at all. A Studio save changes content
|
|
156
|
+
* and uploaded files, never a stylesheet or a component, so the live CSS is
|
|
157
|
+
* still the right CSS; without this every Studio publish shipped unstyled.
|
|
158
|
+
*/
|
|
159
|
+
liveStylesheet?: () => Promise<string>;
|
|
160
|
+
/**
|
|
161
|
+
* One of the live site's public files, base64, by its URL path
|
|
162
|
+
* (`favicon.ico` for `/favicon.ico`).
|
|
163
|
+
*
|
|
164
|
+
* Only asked for when content cannot name the live build's public files by
|
|
165
|
+
* hash and the loader can name them by path. Absent, that case is refused
|
|
166
|
+
* like the one where nobody can say.
|
|
167
|
+
*/
|
|
168
|
+
fetchPublicFile?: (path: string) => Promise<string>;
|
|
169
|
+
/**
|
|
170
|
+
* What to build against, when it is not what the live build was built
|
|
171
|
+
* against.
|
|
172
|
+
*
|
|
173
|
+
* Absent for every publish but one: an update, which moves the project onto
|
|
174
|
+
* its template's current dependency layer. The live build's target names the
|
|
175
|
+
* layer it is leaving, so the update's own answer is used instead -- and the
|
|
176
|
+
* publish then names the layer it carries, which is what moves the site.
|
|
177
|
+
*/
|
|
178
|
+
target?: BuildTarget;
|
|
179
|
+
/**
|
|
180
|
+
* The dependency layer the live build had when this page loaded (see
|
|
181
|
+
* `loadedLayer.ts`), or `undefined` when it cannot say.
|
|
182
|
+
*
|
|
183
|
+
* A publish whose build target names a different one is refused: the site
|
|
184
|
+
* was updated since, this Studio is the old version, and building with it
|
|
185
|
+
* would use the old builder. Not asked of an update, whose whole point is a
|
|
186
|
+
* different layer.
|
|
187
|
+
*/
|
|
188
|
+
loadedLayer?: LayerReading;
|
|
189
|
+
/**
|
|
190
|
+
* The live build's layer NOW. Asked once more after the build, just before
|
|
191
|
+
* the declare: an update that went live while this page was building would
|
|
192
|
+
* pass the check against the target read at the start, and this publish
|
|
193
|
+
* would then put the old dependencies back. Absent, only that first check
|
|
194
|
+
* is made. What closes the remaining window -- declare to promote -- is the
|
|
195
|
+
* seal in publish-jobs.
|
|
196
|
+
*/
|
|
197
|
+
liveLayer?: () => Promise<LayerReading>;
|
|
198
|
+
loadBuilder: () => Promise<StudioBuilder>;
|
|
199
|
+
/**
|
|
200
|
+
* Generates `src/routeTree.gen.ts` for a file-based project.
|
|
201
|
+
*
|
|
202
|
+
* A capability, like `routeSplitter` and `loadCssModule` on `BuildInput` and
|
|
203
|
+
* for the same reason: it is TanStack's generator over babel, it is 2.6 MB
|
|
204
|
+
* built for a browser, and it is not published. A deployment that has one
|
|
205
|
+
* supplies it.
|
|
206
|
+
*
|
|
207
|
+
* Absent, a file-based project is REFUSED by name. It would otherwise fail
|
|
208
|
+
* inside rolldown with `UNRESOLVED_ENTRY`, because a file-based project's
|
|
209
|
+
* entry IS the generated tree — a message about a module specifier, for a
|
|
210
|
+
* missing capability, several layers from anything anyone can act on.
|
|
211
|
+
*/
|
|
212
|
+
generateRouteTree?: (files: Record<string, string>) => Promise<Record<string, string>>;
|
|
213
|
+
onPhase: (phase: DeployPhase) => void;
|
|
214
|
+
}
|
|
215
|
+
export declare function runStudioDeploy(options: StudioDeployOptions): Promise<StudioDeployResult>;
|
|
216
|
+
/**
|
|
217
|
+
* The stored source with a commit's files laid over it.
|
|
218
|
+
*
|
|
219
|
+
* A new record rather than an edit of the one read, which the caller may still
|
|
220
|
+
* hold. Val names module files with a leading `/`; the stored record does
|
|
221
|
+
* not, so the slash is dropped -- two spellings of one path would build the
|
|
222
|
+
* old file and publish the new one beside it.
|
|
223
|
+
*/
|
|
224
|
+
export declare function withCommittedFiles(source: Record<string, string>, committed: Record<string, string | null> | null): Record<string, string>;
|
|
225
|
+
/**
|
|
226
|
+
* A commit's binary files, as the build takes them: `public/...` served as is,
|
|
227
|
+
* everything else an asset an import can reach. Keys lose the leading `/`
|
|
228
|
+
* Val's paths carry, as in {@link withCommittedFiles}.
|
|
229
|
+
*/
|
|
230
|
+
export declare function splitBinaryFiles(files: Record<string, string>): {
|
|
231
|
+
publicFiles: Record<string, string>;
|
|
232
|
+
assets: Record<string, string>;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* The live build's public files this build keeps, re-declared by hash.
|
|
236
|
+
*
|
|
237
|
+
* All of them, except the ones this build produced itself (a file the commit
|
|
238
|
+
* replaced) and the ones the commit deleted. Content holds the bytes already,
|
|
239
|
+
* so this uploads nothing -- and if it has lost one, it asks for it, and
|
|
240
|
+
* `runStudioPublish` refuses rather than publishing a hole.
|
|
241
|
+
*/
|
|
242
|
+
export declare function carriedInto(built: ReadonlyArray<{
|
|
243
|
+
key: string;
|
|
244
|
+
}>, carried: ReadonlyArray<CarriedArtifact>, committed: Record<string, string | null> | null): CarriedArtifact[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { PublishTabJob } from "../internal/index.js";
|
|
2
|
+
import type { DeployPhase, StudioDeployResult } from "./runStudioDeploy.js";
|
|
3
|
+
import type { PreparedJob, StudioJobClient } from "./jobClient.js";
|
|
4
|
+
/**
|
|
5
|
+
* A publish job, as the tab that holds it runs it: prepare, build, upload --
|
|
6
|
+
* and then it is content's (valbuild/home, docs/app-mode.md, "Publishing is a
|
|
7
|
+
* queued job"). Content runs verify and the seal, and "Live" arrives on the
|
|
8
|
+
* websocket, so a tab that closes after its upload costs nothing.
|
|
9
|
+
*
|
|
10
|
+
* The tab holds the job by a lease, renewed on a timer while it works. A
|
|
11
|
+
* renewal answered `false` means the job is no longer this tab's -- its lease
|
|
12
|
+
* lapsed and its requests went back to the queue, or it was cancelled -- and
|
|
13
|
+
* the tab stops: another tab, or this one's next job, will build it.
|
|
14
|
+
*
|
|
15
|
+
* Every step is reported by name, so a report retried after a lost answer
|
|
16
|
+
* does nothing twice, and a failure is reported as the step's: content counts
|
|
17
|
+
* the attempts and, after the last, fails the job with Try again.
|
|
18
|
+
*/
|
|
19
|
+
export type JobPhase = {
|
|
20
|
+
kind: "preparing";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "deploying";
|
|
23
|
+
phase: DeployPhase;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "handing-off";
|
|
26
|
+
};
|
|
27
|
+
export type StudioJobResult =
|
|
28
|
+
/**
|
|
29
|
+
* The tab's part is done, and content has the job. `built`: this tab built
|
|
30
|
+
* and uploaded it. `false` is a job whose build is CI's -- a connected
|
|
31
|
+
* project's, where the tab's part is the prepare alone -- or, with
|
|
32
|
+
* `contentOnly`, a job nobody builds: its content goes live on the build
|
|
33
|
+
* already live.
|
|
34
|
+
*/
|
|
35
|
+
{
|
|
36
|
+
status: "handed-off";
|
|
37
|
+
jobId: string;
|
|
38
|
+
built: boolean;
|
|
39
|
+
contentOnly?: true;
|
|
40
|
+
}
|
|
41
|
+
/** The job is no longer this tab's. Nothing to report. */
|
|
42
|
+
| {
|
|
43
|
+
status: "lost";
|
|
44
|
+
jobId: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A managed job that needs a build, prepared by a tab that cannot run the
|
|
48
|
+
* builder (`buildsElsewhere`). Not reported: its lease lapses, it goes back
|
|
49
|
+
* to the queue, and a builder tab -- opened with one more tap -- takes it.
|
|
50
|
+
*/
|
|
51
|
+
| {
|
|
52
|
+
status: "needs-builder";
|
|
53
|
+
jobId: string;
|
|
54
|
+
}
|
|
55
|
+
/** A step failed, and was reported as failed. */
|
|
56
|
+
| {
|
|
57
|
+
status: "failed";
|
|
58
|
+
jobId: string;
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
/** How often the tab renews its lease. Content's job lease is 30 s. */
|
|
62
|
+
export declare const RENEW_EVERY_MS = 10000;
|
|
63
|
+
export declare function runStudioJob(options: {
|
|
64
|
+
client: StudioJobClient;
|
|
65
|
+
job: PublishTabJob;
|
|
66
|
+
tab: string;
|
|
67
|
+
/**
|
|
68
|
+
* Build the job from its prepared sources, stopping once the artifacts are
|
|
69
|
+
* confirmed: `runStudioDeploy` with `until: "confirmed"` and no commit.
|
|
70
|
+
*/
|
|
71
|
+
deploy: (prepared: PreparedJob, onPhase: (phase: DeployPhase) => void) => Promise<StudioDeployResult>;
|
|
72
|
+
onPhase: (phase: JobPhase) => void;
|
|
73
|
+
renewEveryMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Can this tab build at all? Connected only: a tab that cannot (no
|
|
76
|
+
* cross-origin isolation) reports "no build", and CI builds the push. A
|
|
77
|
+
* managed job never reaches a tab that cannot -- it is handed to one that can.
|
|
78
|
+
*/
|
|
79
|
+
canBuild?: () => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Managed, and a tab that cannot build: an iPhone, or the overlay, running
|
|
82
|
+
* its own publish so that one of content alone needs no tab. A job that
|
|
83
|
+
* needs a build after all is given back (`needs-builder`) rather than said
|
|
84
|
+
* to need none -- content refuses that for a managed job, which has no CI.
|
|
85
|
+
*/
|
|
86
|
+
buildsElsewhere?: boolean;
|
|
87
|
+
}): Promise<StudioJobResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Run a job until this tab's part of it is over: handed off, lost, or failed
|
|
90
|
+
* for good.
|
|
91
|
+
*
|
|
92
|
+
* A failed step is counted by content, which keeps the job at that step for
|
|
93
|
+
* the tab to run again -- until its last attempt, when it fails the job.
|
|
94
|
+
* Whether it is still this tab's is the renewal's answer, so a failure is
|
|
95
|
+
* followed by one, and a `true` runs it again.
|
|
96
|
+
*/
|
|
97
|
+
export declare function runJobToEnd(options: {
|
|
98
|
+
client: Pick<StudioJobClient, "renew">;
|
|
99
|
+
job: PublishTabJob;
|
|
100
|
+
tab: string;
|
|
101
|
+
run: () => Promise<StudioJobResult>;
|
|
102
|
+
}): Promise<StudioJobResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishing what the tab just built.
|
|
3
|
+
*
|
|
4
|
+
* The same conversation `val publish` has, with the same parsers, and the same
|
|
5
|
+
* shape of state machine -- declare, upload what content does not already hold,
|
|
6
|
+
* confirm, verify, promote. It is written out again here rather than shared
|
|
7
|
+
* with the CLI's `runPublish` because the two differ in every input and in what
|
|
8
|
+
* they do when something goes wrong: the CLI reads artifacts off a disk it
|
|
9
|
+
* controls, takes a token, prints lines and sets an exit code; this holds them
|
|
10
|
+
* in memory, has no credential, and has to leave an EDITOR with something they
|
|
11
|
+
* can act on.
|
|
12
|
+
*
|
|
13
|
+
* What is shared is what must not drift: the wire shapes and their parsers
|
|
14
|
+
* (`@valbuild/shared/internal`) and the artifact key namespace
|
|
15
|
+
* (`@valbuild/tanstack-build`). Those are the parts where a second, slightly
|
|
16
|
+
* different copy is how two publishers come to disagree with one service.
|
|
17
|
+
*
|
|
18
|
+
* ## No progress reporting beyond phases, on purpose
|
|
19
|
+
*
|
|
20
|
+
* Byte-level upload progress would need `XMLHttpRequest` per artifact, and a
|
|
21
|
+
* publish is dozens of small ones rather than one big one -- so what a person
|
|
22
|
+
* sees from counting artifacts is the same and the code is a quarter the size.
|
|
23
|
+
*
|
|
24
|
+
* ## Why it never throws
|
|
25
|
+
*
|
|
26
|
+
* Every exit is a value. A publish that failed has to say WHICH step and leave
|
|
27
|
+
* the commit intact; a thrown error at an await boundary in a React handler is
|
|
28
|
+
* an unhandled rejection and a spinner that never stops.
|
|
29
|
+
*/
|
|
30
|
+
import type { PublishArtifact } from "@valbuild/tanstack-build";
|
|
31
|
+
import { type DeclareBody, type PublishProblem } from "../internal/index.js";
|
|
32
|
+
import { StudioPublishClient } from "./publishClient.js";
|
|
33
|
+
/** Which step the publish is on, for the one line an editor is shown. */
|
|
34
|
+
export type PublishPhase = {
|
|
35
|
+
kind: "declaring";
|
|
36
|
+
} | {
|
|
37
|
+
kind: "uploading";
|
|
38
|
+
done: number;
|
|
39
|
+
total: number;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "confirming";
|
|
42
|
+
} | {
|
|
43
|
+
kind: "verifying";
|
|
44
|
+
} | {
|
|
45
|
+
kind: "promoting";
|
|
46
|
+
};
|
|
47
|
+
export type StudioPublishResult = {
|
|
48
|
+
status: "live";
|
|
49
|
+
publishId: string;
|
|
50
|
+
url: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** Content had already taken this exact build. Not a failure. */
|
|
53
|
+
| {
|
|
54
|
+
status: "already-live";
|
|
55
|
+
publishId: string;
|
|
56
|
+
url: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Stopped after the artifacts were confirmed, as `until: "confirmed"` asks:
|
|
60
|
+
* a publish JOB's tab does no more than that, and content runs verify and
|
|
61
|
+
* the seal (valbuild/home, docs/app-mode.md, "Publishing is a queued job").
|
|
62
|
+
*/
|
|
63
|
+
| {
|
|
64
|
+
status: "uploaded";
|
|
65
|
+
publishId: string;
|
|
66
|
+
} | {
|
|
67
|
+
status: "failed";
|
|
68
|
+
/** `null` when it failed before content had accepted anything. */
|
|
69
|
+
publishId: string | null;
|
|
70
|
+
message: string;
|
|
71
|
+
problems: PublishProblem[];
|
|
72
|
+
};
|
|
73
|
+
export declare function runStudioPublish(args: {
|
|
74
|
+
client: StudioPublishClient;
|
|
75
|
+
artifacts: PublishArtifact[];
|
|
76
|
+
declare: DeclareBody;
|
|
77
|
+
onPhase: (phase: PublishPhase) => void;
|
|
78
|
+
/** Stop once the artifacts are confirmed: for a publish job's tab. */
|
|
79
|
+
until?: "confirmed";
|
|
80
|
+
}): Promise<StudioPublishResult>;
|