@constructive-io/site-deploy 0.1.0

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/esm/deploy.js ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * The deploy pipeline.
3
+ *
4
+ * Everything below the manifest is server-owned: writing the release row fires
5
+ * the trigger that commits the manifest into the site's own merkle store, so
6
+ * the commit id this returns *is* the release — history, time travel and
7
+ * rollback are then pointer moves, not client bookkeeping.
8
+ *
9
+ * Ordering is the contract: bytes land before the manifest names them, and the
10
+ * manifest lands before anything points at it. A failure anywhere leaves the
11
+ * live site exactly as it was.
12
+ */
13
+ import { diffCas, errorMessage, throwIfAborted, uploadMissing } from './cas-upload';
14
+ import { buildCreateReleaseMutation, buildProvisionPreviewMutation, buildPublishMutation, buildReleaseQuery, buildSetPreviewMutation, buildUpdateReleaseMutation, deployNames, } from './documents';
15
+ import { buildManifest, hashFile, manifestsEqual } from './manifest';
16
+ import { DeployError } from './types';
17
+ import { walkDirectory } from './walk';
18
+ /** Preview ref names become hostname labels, so they must be one. */
19
+ const PREVIEW_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
20
+ /**
21
+ * Deploy a build as an immutable release.
22
+ *
23
+ * Hashes every file, uploads only the bytes the server does not already have,
24
+ * writes one manifest — and optionally publishes it or points a preview ref at
25
+ * it.
26
+ */
27
+ export async function deploySite(options) {
28
+ const { api, storage = options.api, siteId, databaseId, bucketKey, scope = 'database', publish = false, preview, previewApex, concurrency = 8, batchSize = 100, retries = 2, skipIfUnchanged = false, dryRun = false, signal, onProgress, } = options;
29
+ const names = { ...deployNames(scope), ...options.names };
30
+ if (preview !== undefined && !PREVIEW_NAME_PATTERN.test(preview)) {
31
+ throw new DeployError('INVALID_PREVIEW_NAME', `Preview name must be a lowercase DNS label: "${preview}"`);
32
+ }
33
+ // 1. Hash the build. Content addressing means this is also the diff key.
34
+ const files = [];
35
+ let index = 0;
36
+ for await (const file of iterateSource(options.source, options.ignore)) {
37
+ throwIfAborted(signal);
38
+ if (options.ignore?.(file.path))
39
+ continue;
40
+ const hashed = hashFile(file, options.contentTypes);
41
+ files.push(hashed);
42
+ onProgress?.({
43
+ type: 'hashed',
44
+ path: hashed.path,
45
+ hash: hashed.hash,
46
+ size: hashed.size,
47
+ index: index++,
48
+ });
49
+ }
50
+ const manifest = buildManifest(files);
51
+ // 2. The release row is UNIQUE per site, so the first deploy creates it and
52
+ // every later one patches it — the caller should not have to know which.
53
+ const existing = await fetchExistingRelease(api, names, siteId);
54
+ if (skipIfUnchanged &&
55
+ existing?.commitId &&
56
+ existing.storeId &&
57
+ existing.manifest &&
58
+ manifestsEqual(existing.manifest, manifest)) {
59
+ onProgress?.({ type: 'unchanged', commitId: existing.commitId });
60
+ return {
61
+ commitId: existing.commitId,
62
+ storeId: existing.storeId,
63
+ releaseId: existing.id,
64
+ manifest,
65
+ files: manifest.file_count,
66
+ uploaded: 0,
67
+ skipped: manifest.file_count,
68
+ bytesUploaded: 0,
69
+ published: false,
70
+ previewUrl: null,
71
+ unchanged: true,
72
+ };
73
+ }
74
+ // 3. Register the bytes and learn which ones are missing.
75
+ const uploadOptions = {
76
+ storage,
77
+ mutation: names.bulkUploadMutation,
78
+ bucketKey,
79
+ batchSize,
80
+ concurrency,
81
+ retries,
82
+ putObject: options.putObject,
83
+ signal,
84
+ };
85
+ const diff = await diffCas(files, uploadOptions);
86
+ onProgress?.({
87
+ type: 'diffed',
88
+ files: manifest.file_count,
89
+ toUpload: diff.missing.length,
90
+ skipped: diff.deduplicated.length,
91
+ bytesToUpload: diff.missing.reduce((sum, entry) => sum + entry.file.size, 0),
92
+ });
93
+ if (dryRun) {
94
+ return {
95
+ commitId: existing?.commitId ?? '',
96
+ storeId: existing?.storeId ?? '',
97
+ releaseId: existing?.id ?? '',
98
+ manifest,
99
+ files: manifest.file_count,
100
+ uploaded: 0,
101
+ skipped: diff.deduplicated.length,
102
+ bytesUploaded: 0,
103
+ published: false,
104
+ previewUrl: null,
105
+ unchanged: false,
106
+ };
107
+ }
108
+ // 4. Upload. Any failure here throws, so the manifest is never written over
109
+ // bytes that are not there.
110
+ let done = 0;
111
+ const uploadResult = await uploadMissing(diff, {
112
+ ...uploadOptions,
113
+ onUploaded: (file) => {
114
+ done += 1;
115
+ onProgress?.({
116
+ type: 'uploaded',
117
+ path: file.path,
118
+ key: file.key,
119
+ size: file.size,
120
+ done,
121
+ total: diff.missing.length,
122
+ });
123
+ },
124
+ });
125
+ // 5. Write the manifest — this is the commit.
126
+ const release = existing
127
+ ? await updateRelease(api, names, existing.id, manifest)
128
+ : await createRelease(api, names, siteId, databaseId, manifest);
129
+ onProgress?.({
130
+ type: 'manifest',
131
+ commitId: release.commitId,
132
+ storeId: release.storeId,
133
+ created: !existing,
134
+ });
135
+ const result = {
136
+ commitId: release.commitId,
137
+ storeId: release.storeId,
138
+ releaseId: release.id,
139
+ manifest,
140
+ files: manifest.file_count,
141
+ uploaded: uploadResult.uploaded,
142
+ skipped: uploadResult.skipped,
143
+ bytesUploaded: uploadResult.bytesUploaded,
144
+ published: false,
145
+ previewUrl: null,
146
+ unchanged: false,
147
+ };
148
+ // 6. Point things at the commit. Separate steps: a failure here leaves a
149
+ // complete, retryable release rather than a half-published site.
150
+ if (preview !== undefined) {
151
+ result.previewUrl = await pointPreview(api, names, {
152
+ siteId,
153
+ name: preview,
154
+ commitId: release.commitId,
155
+ apex: previewApex,
156
+ });
157
+ onProgress?.({
158
+ type: 'preview',
159
+ name: preview,
160
+ commitId: release.commitId,
161
+ url: result.previewUrl,
162
+ });
163
+ }
164
+ if (publish) {
165
+ await publishCommit(api, names, siteId, release.commitId);
166
+ result.published = true;
167
+ onProgress?.({ type: 'published', commitId: release.commitId });
168
+ }
169
+ return result;
170
+ }
171
+ /** Move `site.activeCommitId` — the go-live and the rollback are one thing. */
172
+ export async function publishCommit(api, names, siteId, commitId) {
173
+ try {
174
+ await api(buildPublishMutation(names), {
175
+ input: { id: siteId, [names.sitePatchField]: { activeCommitId: commitId } },
176
+ });
177
+ }
178
+ catch (err) {
179
+ throw new DeployError('PUBLISH_FAILED', `Release ${commitId} was written but publishing it failed: ${errorMessage(err)}`, err);
180
+ }
181
+ }
182
+ async function* iterateSource(source, ignore) {
183
+ if (typeof source === 'string') {
184
+ yield* walkDirectory(source, ignore);
185
+ return;
186
+ }
187
+ if (Symbol.asyncIterator in source) {
188
+ yield* source;
189
+ return;
190
+ }
191
+ yield* source;
192
+ }
193
+ async function fetchExistingRelease(api, names, siteId) {
194
+ const data = await api(buildReleaseQuery(names), { siteId });
195
+ const connection = data[names.releasesQuery];
196
+ return connection?.nodes?.[0] ?? null;
197
+ }
198
+ async function createRelease(api, names, siteId, databaseId, manifest) {
199
+ if (!databaseId) {
200
+ throw new DeployError('MISSING_DATABASE_ID', `Site ${siteId} has no release yet, so databaseId is required to create one`);
201
+ }
202
+ const data = await api(buildCreateReleaseMutation(names), {
203
+ input: {
204
+ [names.releaseInputField]: { siteId, databaseId, manifest },
205
+ },
206
+ });
207
+ return readWrittenRelease(data, names);
208
+ }
209
+ async function updateRelease(api, names, releaseId, manifest) {
210
+ const data = await api(buildUpdateReleaseMutation(names), {
211
+ input: {
212
+ id: releaseId,
213
+ [names.releasePatchField]: { manifest },
214
+ },
215
+ });
216
+ return readWrittenRelease(data, names);
217
+ }
218
+ /**
219
+ * Read back the versioning the trigger stamped on the row.
220
+ *
221
+ * A release without a commit is not a release: it cannot be published, rolled
222
+ * back to, or previewed, so a missing id is a hard failure rather than a
223
+ * result with empty strings in it.
224
+ */
225
+ function readWrittenRelease(data, names) {
226
+ const mutation = (data[names.createRelease] ?? data[names.updateRelease]);
227
+ const row = mutation?.[names.releasePayloadField];
228
+ if (!row?.id || !row.commitId || !row.storeId) {
229
+ throw new DeployError('RELEASE_NOT_VERSIONED', 'Release row came back without a commitId/storeId — the manifest was not committed');
230
+ }
231
+ return { id: row.id, commitId: row.commitId, storeId: row.storeId };
232
+ }
233
+ async function pointPreview(api, names, args) {
234
+ try {
235
+ // With an apex, provision so the hostname and its route exist (idempotent
236
+ // and it moves the ref too); without one, just move the ref.
237
+ if (args.apex) {
238
+ const data = await api(buildProvisionPreviewMutation(names), {
239
+ input: {
240
+ siteId: args.siteId,
241
+ name: args.name,
242
+ commitId: args.commitId,
243
+ apex: args.apex,
244
+ },
245
+ });
246
+ const payload = data[names.provisionPreview];
247
+ const hostname = payload?.result?.domain?.hostname;
248
+ return hostname ? `https://${hostname}` : null;
249
+ }
250
+ await api(buildSetPreviewMutation(names), {
251
+ input: {
252
+ targetSiteId: args.siteId,
253
+ targetName: args.name,
254
+ targetCommitId: args.commitId,
255
+ },
256
+ });
257
+ return null;
258
+ }
259
+ catch (err) {
260
+ throw new DeployError('PREVIEW_FAILED', `Release ${args.commitId} was written but preview "${args.name}" could not be pointed at it: ${errorMessage(err)}`, err);
261
+ }
262
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * GraphQL documents and generated-name derivation.
3
+ *
4
+ * Plain strings, no graphql dependency: the same documents work through the
5
+ * generated SDK, urql, Apollo or a bare fetch. Every field name is generated
6
+ * per scope — unprefixed at the tenant (`database`) scope, `platform`-prefixed
7
+ * on the self-hosted lane — so the names are derived in one place here rather
8
+ * than spelled out at each call site.
9
+ */
10
+ /** Derive the deploy surface's field names for a scope. */
11
+ export function deployNames(scope = 'database') {
12
+ const Prefix = scope === 'platform' ? 'Platform' : '';
13
+ const prefix = scope === 'platform' ? 'platform' : '';
14
+ const lower = (name) => (prefix ? `${prefix}${name}` : lowerFirst(name));
15
+ return {
16
+ bulkUploadMutation: `upload${Prefix}Files`,
17
+ releasesQuery: `${lower('SiteReleases')}`,
18
+ createRelease: `create${Prefix}SiteRelease`,
19
+ releaseInputField: lower('SiteRelease'),
20
+ updateRelease: `update${Prefix}SiteRelease`,
21
+ releasePatchField: `${lower('SiteRelease')}Patch`,
22
+ releasePayloadField: lower('SiteRelease'),
23
+ updateSite: `update${Prefix}Site`,
24
+ sitePatchField: `${lower('Site')}Patch`,
25
+ sitePayloadField: lower('Site'),
26
+ // Preview procedures are exposed unprefixed; a plane that generates them
27
+ // per scope can override these two.
28
+ provisionPreview: 'provisionSitePreview',
29
+ setPreview: 'setSitePreview',
30
+ };
31
+ }
32
+ function lowerFirst(value) {
33
+ return value.charAt(0).toLowerCase() + value.slice(1);
34
+ }
35
+ /** Bulk presigned-upload mutation for one storage plane. */
36
+ export function buildBulkUploadMutation(mutation) {
37
+ return `
38
+ mutation BulkUpload($input: ${inputTypeName(mutation)}!) {
39
+ ${mutation}(input: $input) {
40
+ files {
41
+ uploadUrl
42
+ fileId
43
+ key
44
+ deduplicated
45
+ }
46
+ }
47
+ }
48
+ `;
49
+ }
50
+ /** `uploadFiles` → `UploadFileBulkInput`, `uploadPlatformFiles` → … */
51
+ function inputTypeName(mutation) {
52
+ const capitalized = mutation.charAt(0).toUpperCase() + mutation.slice(1);
53
+ return `${capitalized.replace(/s$/, '')}BulkInput`;
54
+ }
55
+ /** The site's existing release row, if it has one (the row is UNIQUE per site). */
56
+ export function buildReleaseQuery(names) {
57
+ return `
58
+ query SiteRelease($siteId: UUID!) {
59
+ ${names.releasesQuery}(where: { siteId: { equalTo: $siteId } }, first: 1) {
60
+ nodes {
61
+ id
62
+ commitId
63
+ storeId
64
+ manifest
65
+ }
66
+ }
67
+ }
68
+ `;
69
+ }
70
+ export function buildCreateReleaseMutation(names) {
71
+ return `
72
+ mutation CreateSiteRelease($input: ${typeName(names.createRelease)}Input!) {
73
+ ${names.createRelease}(input: $input) {
74
+ ${names.releasePayloadField} {
75
+ id
76
+ commitId
77
+ storeId
78
+ }
79
+ }
80
+ }
81
+ `;
82
+ }
83
+ export function buildUpdateReleaseMutation(names) {
84
+ return `
85
+ mutation UpdateSiteRelease($input: ${typeName(names.updateRelease)}Input!) {
86
+ ${names.updateRelease}(input: $input) {
87
+ ${names.releasePayloadField} {
88
+ id
89
+ commitId
90
+ storeId
91
+ }
92
+ }
93
+ }
94
+ `;
95
+ }
96
+ /** Publishing and rollback are the same mutation: move the pointer. */
97
+ export function buildPublishMutation(names) {
98
+ return `
99
+ mutation PublishSiteRelease($input: ${typeName(names.updateSite)}Input!) {
100
+ ${names.updateSite}(input: $input) {
101
+ ${names.sitePayloadField} {
102
+ id
103
+ activeCommitId
104
+ }
105
+ }
106
+ }
107
+ `;
108
+ }
109
+ /** Claims (or reuses) the preview hostname and points the ref at a commit. */
110
+ export function buildProvisionPreviewMutation(names) {
111
+ return `
112
+ mutation ProvisionSitePreview($input: ${typeName(names.provisionPreview)}Input!) {
113
+ ${names.provisionPreview}(input: $input) {
114
+ result {
115
+ id
116
+ previewRef
117
+ domain {
118
+ hostname
119
+ }
120
+ }
121
+ }
122
+ }
123
+ `;
124
+ }
125
+ /** Moves an existing ref without touching routing. */
126
+ export function buildSetPreviewMutation(names) {
127
+ return `
128
+ mutation SetSitePreview($input: ${typeName(names.setPreview)}Input!) {
129
+ ${names.setPreview}(input: $input) {
130
+ result
131
+ }
132
+ }
133
+ `;
134
+ }
135
+ function typeName(field) {
136
+ return field.charAt(0).toUpperCase() + field.slice(1);
137
+ }
package/esm/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@constructive-io/site-deploy` — deploy a static build as an immutable
3
+ * Constructive release.
4
+ *
5
+ * ```ts
6
+ * const result = await deploySite({
7
+ * api,
8
+ * siteId,
9
+ * databaseId,
10
+ * bucketKey: 'site-docs',
11
+ * source: './dist',
12
+ * publish: true,
13
+ * });
14
+ * ```
15
+ *
16
+ * The server owns versioning: writing the manifest commits it into the site's
17
+ * merkle store, so `result.commitId` is the release id you publish, roll back
18
+ * to, or point a preview ref at.
19
+ */
20
+ export { contentTypeFor, DEFAULT_CONTENT_TYPE } from './content-type';
21
+ export { deploySite, publishCommit } from './deploy';
22
+ export { deployNames } from './documents';
23
+ export { buildManifest, CAS_KEY_PATTERN, CAS_PREFIX, casKey, hashBytes, manifestsEqual, normalizePath } from './manifest';
24
+ export { DeployError } from './types';
25
+ export { walkDirectory } from './walk';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Manifest construction: the site-specific half of a deploy.
3
+ *
4
+ * The bytes of a build are content-addressed and say nothing about how the
5
+ * site is served; the manifest is what turns a bag of hashes into a site, by
6
+ * mapping each logical path to the hash, content type and size the edge
7
+ * serves for it.
8
+ */
9
+ import { sha256 } from '@decryption/hashes/sha2';
10
+ import { bytesToHex } from '@decryption/hashes/utils';
11
+ import { contentTypeFor } from './content-type';
12
+ import { DeployError } from './types';
13
+ /** Prefix every file's bytes are stored under, in the site's own bucket. */
14
+ export const CAS_PREFIX = 'cas/sha256/';
15
+ /** The one legal shape of a CAS key. */
16
+ export const CAS_KEY_PATTERN = /^cas\/sha256\/[0-9a-f]{64}$/;
17
+ /** The content-addressed bucket key holding the bytes with this hash. */
18
+ export function casKey(hash) {
19
+ return `${CAS_PREFIX}${hash}`;
20
+ }
21
+ /** SHA-256 of some bytes, hex — the CAS address and the served ETag. */
22
+ export function hashBytes(bytes) {
23
+ return bytesToHex(sha256(bytes));
24
+ }
25
+ /**
26
+ * Normalize a logical path to the form the gateway looks up.
27
+ *
28
+ * Windows separators become `/` and a leading `./` or `/` is dropped, so the
29
+ * same build deploys identically from any OS. Anything that would escape the
30
+ * site root or collide with the CAS namespace is a hard error — a silently
31
+ * dropped file is a silently broken site.
32
+ */
33
+ export function normalizePath(path) {
34
+ const normalized = path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
35
+ if (!normalized) {
36
+ throw new DeployError('INVALID_PATH', `Empty logical path (from "${path}")`);
37
+ }
38
+ if (normalized.split('/').some((segment) => segment === '..' || segment === '.')) {
39
+ throw new DeployError('INVALID_PATH', `Logical path must not contain "." or ".." segments: "${path}"`);
40
+ }
41
+ if (normalized.startsWith(CAS_PREFIX)) {
42
+ throw new DeployError('INVALID_PATH', `Logical path must not start with "${CAS_PREFIX}" (that is the byte namespace): "${path}"`);
43
+ }
44
+ return normalized;
45
+ }
46
+ /** Hash one file and resolve its content type. */
47
+ export function hashFile(file, contentTypes) {
48
+ const path = normalizePath(file.path);
49
+ const hash = hashBytes(file.bytes);
50
+ return {
51
+ path,
52
+ key: casKey(hash),
53
+ hash,
54
+ contentType: file.contentType ?? contentTypeFor(path, contentTypes),
55
+ size: file.bytes.byteLength,
56
+ bytes: file.bytes,
57
+ };
58
+ }
59
+ /**
60
+ * Build the release manifest from hashed files.
61
+ *
62
+ * `file_count`/`total_bytes` are derived here rather than trusted from a
63
+ * caller, so they can never disagree with `files`.
64
+ */
65
+ export function buildManifest(files) {
66
+ const entries = {};
67
+ let totalBytes = 0;
68
+ let count = 0;
69
+ for (const file of files) {
70
+ if (entries[file.path]) {
71
+ throw new DeployError('INVALID_PATH', `Duplicate logical path: "${file.path}"`);
72
+ }
73
+ entries[file.path] = {
74
+ hash: file.hash,
75
+ content_type: file.contentType,
76
+ size: file.size,
77
+ };
78
+ totalBytes += file.size;
79
+ count += 1;
80
+ }
81
+ if (count === 0) {
82
+ throw new DeployError('EMPTY_SOURCE', 'Refusing to release an empty manifest');
83
+ }
84
+ return { files: entries, file_count: count, total_bytes: totalBytes };
85
+ }
86
+ /** Whether two manifests would serve byte-identical sites. */
87
+ export function manifestsEqual(a, b) {
88
+ const aPaths = Object.keys(a.files);
89
+ const bPaths = Object.keys(b.files);
90
+ if (aPaths.length !== bPaths.length)
91
+ return false;
92
+ return aPaths.every((path) => {
93
+ const left = a.files[path];
94
+ const right = b.files[path];
95
+ return (right !== undefined &&
96
+ left.hash === right.hash &&
97
+ left.content_type === right.content_type &&
98
+ left.size === right.size);
99
+ });
100
+ }
package/esm/types.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Types for the site deploy pipeline.
3
+ *
4
+ * A deploy is bytes plus one manifest: every file's bytes live in the site's
5
+ * bucket at `cas/sha256/<hash>`, and one `siteRelease` row maps logical paths
6
+ * to those hashes. Writing that row commits it into the site's own merkle
7
+ * store, so the returned commit id *is* the release id.
8
+ */
9
+ /**
10
+ * A deploy failure. Never swallowed and never partially reported: if this
11
+ * throws before the manifest write, production is untouched.
12
+ */
13
+ export class DeployError extends Error {
14
+ code;
15
+ cause;
16
+ constructor(code, message, cause) {
17
+ super(message);
18
+ this.name = 'DeployError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ }
package/esm/walk.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Reading a build off disk.
3
+ *
4
+ * Node-only and loaded lazily, so the package stays usable in a browser (a UI
5
+ * that already holds the built files in memory passes them directly instead of
6
+ * a directory path).
7
+ */
8
+ import { DeployError } from './types';
9
+ /**
10
+ * Yield every file under `dir` with its POSIX path relative to `dir`.
11
+ *
12
+ * Dotfiles are included: a static build legitimately ships `.well-known/…`,
13
+ * and dropping files by a rule the caller cannot see is how a deploy silently
14
+ * loses a route. Use `ignore` to exclude paths explicitly.
15
+ */
16
+ export async function* walkDirectory(dir, ignore) {
17
+ const { readdir, readFile } = await loadFs();
18
+ const { join, posix } = await loadPath();
19
+ async function* walk(absolute, prefix) {
20
+ const entries = await readdir(absolute, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const logical = prefix ? posix.join(prefix, entry.name) : entry.name;
23
+ if (ignore?.(logical))
24
+ continue;
25
+ const child = join(absolute, entry.name);
26
+ if (entry.isDirectory()) {
27
+ yield* walk(child, logical);
28
+ continue;
29
+ }
30
+ // Symlinks to files are followed by readFile; anything that is neither a
31
+ // file nor a directory (socket, fifo) has no meaning in a static site.
32
+ if (!entry.isFile() && !entry.isSymbolicLink())
33
+ continue;
34
+ yield { path: logical, bytes: new Uint8Array(await readFile(child)) };
35
+ }
36
+ }
37
+ yield* walk(dir, '');
38
+ }
39
+ async function loadFs() {
40
+ try {
41
+ return await import('fs/promises');
42
+ }
43
+ catch (err) {
44
+ throw new DeployError('INVALID_PATH', 'Deploying from a directory path requires Node; pass the files directly instead', err);
45
+ }
46
+ }
47
+ async function loadPath() {
48
+ return import('path');
49
+ }
package/index.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@constructive-io/site-deploy` — deploy a static build as an immutable
3
+ * Constructive release.
4
+ *
5
+ * ```ts
6
+ * const result = await deploySite({
7
+ * api,
8
+ * siteId,
9
+ * databaseId,
10
+ * bucketKey: 'site-docs',
11
+ * source: './dist',
12
+ * publish: true,
13
+ * });
14
+ * ```
15
+ *
16
+ * The server owns versioning: writing the manifest commits it into the site's
17
+ * merkle store, so `result.commitId` is the release id you publish, roll back
18
+ * to, or point a preview ref at.
19
+ */
20
+ export { contentTypeFor, DEFAULT_CONTENT_TYPE } from './content-type';
21
+ export { deploySite, publishCommit } from './deploy';
22
+ export { deployNames } from './documents';
23
+ export type { HashedFile } from './manifest';
24
+ export { buildManifest, CAS_KEY_PATTERN, CAS_PREFIX, casKey, hashBytes, manifestsEqual, normalizePath } from './manifest';
25
+ export type { DeployErrorCode, DeployFile, DeployProgress, DeploySiteOptions, DeploySiteResult, DeploySource, GraphQLExecutor, ManifestEntry, PutObject, ReleaseManifest, SiteDeployNames, } from './types';
26
+ export { DeployError } from './types';
27
+ export { walkDirectory } from './walk';
package/index.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * `@constructive-io/site-deploy` — deploy a static build as an immutable
4
+ * Constructive release.
5
+ *
6
+ * ```ts
7
+ * const result = await deploySite({
8
+ * api,
9
+ * siteId,
10
+ * databaseId,
11
+ * bucketKey: 'site-docs',
12
+ * source: './dist',
13
+ * publish: true,
14
+ * });
15
+ * ```
16
+ *
17
+ * The server owns versioning: writing the manifest commits it into the site's
18
+ * merkle store, so `result.commitId` is the release id you publish, roll back
19
+ * to, or point a preview ref at.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.walkDirectory = exports.DeployError = exports.normalizePath = exports.manifestsEqual = exports.hashBytes = exports.casKey = exports.CAS_PREFIX = exports.CAS_KEY_PATTERN = exports.buildManifest = exports.deployNames = exports.publishCommit = exports.deploySite = exports.DEFAULT_CONTENT_TYPE = exports.contentTypeFor = void 0;
23
+ var content_type_1 = require("./content-type");
24
+ Object.defineProperty(exports, "contentTypeFor", { enumerable: true, get: function () { return content_type_1.contentTypeFor; } });
25
+ Object.defineProperty(exports, "DEFAULT_CONTENT_TYPE", { enumerable: true, get: function () { return content_type_1.DEFAULT_CONTENT_TYPE; } });
26
+ var deploy_1 = require("./deploy");
27
+ Object.defineProperty(exports, "deploySite", { enumerable: true, get: function () { return deploy_1.deploySite; } });
28
+ Object.defineProperty(exports, "publishCommit", { enumerable: true, get: function () { return deploy_1.publishCommit; } });
29
+ var documents_1 = require("./documents");
30
+ Object.defineProperty(exports, "deployNames", { enumerable: true, get: function () { return documents_1.deployNames; } });
31
+ var manifest_1 = require("./manifest");
32
+ Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return manifest_1.buildManifest; } });
33
+ Object.defineProperty(exports, "CAS_KEY_PATTERN", { enumerable: true, get: function () { return manifest_1.CAS_KEY_PATTERN; } });
34
+ Object.defineProperty(exports, "CAS_PREFIX", { enumerable: true, get: function () { return manifest_1.CAS_PREFIX; } });
35
+ Object.defineProperty(exports, "casKey", { enumerable: true, get: function () { return manifest_1.casKey; } });
36
+ Object.defineProperty(exports, "hashBytes", { enumerable: true, get: function () { return manifest_1.hashBytes; } });
37
+ Object.defineProperty(exports, "manifestsEqual", { enumerable: true, get: function () { return manifest_1.manifestsEqual; } });
38
+ Object.defineProperty(exports, "normalizePath", { enumerable: true, get: function () { return manifest_1.normalizePath; } });
39
+ var types_1 = require("./types");
40
+ Object.defineProperty(exports, "DeployError", { enumerable: true, get: function () { return types_1.DeployError; } });
41
+ var walk_1 = require("./walk");
42
+ Object.defineProperty(exports, "walkDirectory", { enumerable: true, get: function () { return walk_1.walkDirectory; } });