@michaelthielemann/kestrel-delivery-static 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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ export { createStaticDeliveryPort, deliveryPortFor } from './server/port.js';
2
+ export { renderRoute } from './server/render-route.js';
3
+ export { isEnvTrue, planS3Deploy, resolveOutputTarget, resolveOutputCreds, isCompressedSidecar, isPermanentError, deployStaticOutput, recordRouteDiscovery, readRouteDiscovery, type OutputTarget, type OutputConfig, type DeployResult, type DeployDriver, type RouteDiscovery, type DeployOptions, } from './server/deploy-output.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EACL,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,2BAA2B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createStaticDeliveryPort, deliveryPortFor } from './server/port.js';
2
+ export { renderRoute } from './server/render-route.js';
3
+ export { isEnvTrue, planS3Deploy, resolveOutputTarget, resolveOutputCreds, isCompressedSidecar, isPermanentError, deployStaticOutput, recordRouteDiscovery, readRouteDiscovery, } from './server/deploy-output.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EACL,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GAOnB,MAAM,2BAA2B,CAAA"}
@@ -0,0 +1,139 @@
1
+ import type { StorageDriver } from '@michaelthielemann/kestrel-core';
2
+ /** A boolean env flag is on for any of the common truthy spellings (case-insensitive, trimmed).
3
+ * @public */
4
+ export declare function isEnvTrue(value: string | undefined): boolean;
5
+ /** @public */
6
+ export declare function planS3Deploy(opts: {
7
+ isStaticGenerate: boolean;
8
+ dryRun: boolean;
9
+ bucket: string;
10
+ accessKeyId: string;
11
+ secretAccessKey: string;
12
+ prefix?: string;
13
+ /**
14
+ * `output.auto` — the running server publishes to this bucket/prefix itself. Build-time route seeding is
15
+ * then disabled (a prerendered file would shadow the live route), so `.output/public` holds only what
16
+ * Nitro's crawler reached: shipping it would reconcile the server's live pages, sitemap and robots away.
17
+ */
18
+ autoPublish?: boolean;
19
+ /** Bucket the S3 media driver writes live uploads to (empty when media isn't on S3 — then irrelevant). */
20
+ mediaBucket?: string;
21
+ /** Key prefix of those live media objects, to keep the reconcile from deleting them. */
22
+ mediaPrefix?: string;
23
+ }): 'skip' | 'deploy';
24
+ /** @public */
25
+ export interface OutputTarget {
26
+ driver: 'local' | 's3';
27
+ /** Non-secret S3 settings; credentials are resolved separately by `resolveOutputCreds`. */
28
+ s3: {
29
+ bucket: string;
30
+ region: string;
31
+ endpoint: string;
32
+ prefix: string;
33
+ };
34
+ }
35
+ /** @public */
36
+ export type OutputConfig = {
37
+ driver?: string;
38
+ s3?: {
39
+ bucket?: string;
40
+ region?: string;
41
+ endpoint?: string;
42
+ prefix?: string;
43
+ };
44
+ };
45
+ /**
46
+ * Resolve where `nuxt generate`'s static output is shipped: a local directory (default) or an S3
47
+ * bucket. Precedence per setting: `KESTREL_OUTPUT_*` env → config → default (the documented Kestrel
48
+ * order, same as the runtime publisher). Pure, so the precedence is unit-testable; secrets are resolved
49
+ * by `resolveOutputCreds`, never here.
50
+ * @public
51
+ */
52
+ export declare function resolveOutputTarget(out?: OutputConfig, env?: Record<string, string | undefined>): OutputTarget;
53
+ /**
54
+ * S3 credentials for the output deploy: the output-specific `KESTREL_OUTPUT_S3_*`, falling back to the
55
+ * shared media `KESTREL_S3_*` so a single S3 account "just works" — identical to the runtime publisher's
56
+ * resolution. Required keys default to '' (not undefined) so `planS3Deploy` detects a missing credential.
57
+ * @public
58
+ */
59
+ export declare function resolveOutputCreds(env?: Record<string, string | undefined>): {
60
+ accessKeyId: string;
61
+ secretAccessKey: string;
62
+ sessionToken?: string;
63
+ };
64
+ /**
65
+ * A `*.gz`/`*.br` file is a pre-compressed sidecar (from `nitro.compressPublicAssets`) when its
66
+ * uncompressed base is also in the output. On a plain static S3 host these are dead weight — the host
67
+ * does no `Accept-Encoding` negotiation, so nobody fetches `index.html.gz`, and storing it *as*
68
+ * `index.html` with `Content-Encoding` would collide with the real file — so the deploy skips them.
69
+ * A standalone `*.gz` archive (no base file) is genuine content and is kept.
70
+ * @public
71
+ */
72
+ export declare function isCompressedSidecar(key: string, all: Set<string>): boolean;
73
+ /** @public */
74
+ export interface DeployResult {
75
+ pruned: number;
76
+ keys: string[];
77
+ }
78
+ /**
79
+ * What build-time route discovery enumerated for this generate, plus WHY that enumeration is known to be
80
+ * short of the site's pages (`incomplete` unset ⇒ it is complete). The runtime publisher's
81
+ * `{ routes, failed }` is the same contract: an enumeration that came up short must never be the
82
+ * authority for what gets deleted.
83
+ */
84
+ /** @public */
85
+ export interface RouteDiscovery {
86
+ routes: string[];
87
+ incomplete?: string;
88
+ }
89
+ /** The prerender and deploy modules are loaded separately and share no module scope — but they are handed
90
+ * the same Nuxt instance, so that object is what carries the signal from the one to the other.
91
+ * @public */
92
+ export declare function recordRouteDiscovery(nuxt: object, discovery: RouteDiscovery): void;
93
+ /** `undefined` ⇒ discovery never reported anything, which is not itself evidence of a degraded build.
94
+ * @public */
95
+ export declare function readRouteDiscovery(nuxt: object): RouteDiscovery | undefined;
96
+ /** The deploy needs `put` always, and `list`+`delete` for the reconcile; both are optional on the driver.
97
+ * @public */
98
+ export type DeployDriver = Pick<StorageDriver, 'put'> & Partial<Pick<StorageDriver, 'list' | 'delete'>>;
99
+ /** @public */
100
+ export interface DeployOptions {
101
+ dryRun?: boolean;
102
+ log?: (msg: string) => void;
103
+ /** Max operations in flight at once (default 8). Bounded so a large tree never opens thousands of sockets. */
104
+ concurrency?: number;
105
+ /** Total attempts per operation before giving up (default 3); retries ride out transient S3 errors. */
106
+ retries?: number;
107
+ /** Sleep between retries (exponential backoff); injectable so tests run without real delay. */
108
+ sleep?: (ms: number) => Promise<void>;
109
+ /**
110
+ * Why a build step REPORTED that this output tree is short of pages (routes that failed to prerender,
111
+ * route discovery that could not read the DB). Set ⇒ upload but never reconcile: pages missing from the
112
+ * input are indistinguishable from pages removed from the CMS, so the prune would delete live ones.
113
+ * Only a step's own failure report belongs here: the SHAPE of the output cannot tell a degraded build
114
+ * from a small site, so inferring it would strand the pages an editor deleted.
115
+ */
116
+ incomplete?: string;
117
+ }
118
+ /**
119
+ * A permanent client error — bad/forbidden credentials, missing bucket, denied access (400/401/403/404).
120
+ * These never become successful on a later attempt, so retrying only delays the inevitable loud failure.
121
+ * @public
122
+ */
123
+ export declare function isPermanentError(err: unknown): boolean;
124
+ /**
125
+ * Upload every file under `dir` to `driver`, keyed by its POSIX-relative path, with a content type
126
+ * inferred from the extension — the S3 equivalent of `aws s3 sync` (overwrites). Then reconciles: deletes
127
+ * remote objects under the prefix that this generate didn't produce (`--delete`), so pages removed from
128
+ * the CMS stop being served (output ≡ DB, no toggle). The reconcile is skipped when the driver can't
129
+ * list+delete and when the caller flagged the tree `incomplete` — a build step reported it is short of
130
+ * pages, so their absence says nothing about the CMS. Uploads still run then: shipping the pages that did
131
+ * render is safe, deleting on their absence is not.
132
+ * Uploads and deletes run with bounded concurrency and each is retried with
133
+ * backoff, so a transient 5xx/throttle/dropped-connection no longer aborts the deploy mid-way and
134
+ * half-updates the bucket; only an operation that fails every attempt rejects (true rollback isn't
135
+ * possible on S3). With `dryRun`, walks/lists and reports without writing.
136
+ * @public
137
+ */
138
+ export declare function deployStaticOutput(dir: string, driver: DeployDriver, opts?: DeployOptions): Promise<DeployResult>;
139
+ //# sourceMappingURL=deploy-output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-output.d.ts","sourceRoot":"","sources":["../../src/server/deploy-output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { setTimeout as sleepFor } from 'node:timers/promises';
4
+ import { resolveS3Settings, contentTypeFor, cacheControlFor } from '@michaelthielemann/kestrel-core';
5
+ /** A boolean env flag is on for any of the common truthy spellings (case-insensitive, trimmed).
6
+ * @public */
7
+ export function isEnvTrue(value) {
8
+ const t = value?.trim().toLowerCase();
9
+ return t === 'true' || t === '1' || t === 'yes' || t === 'on';
10
+ }
11
+ /**
12
+ * Decide what the deploy hook should do, kept pure so the trigger guard is unit-testable. Returns
13
+ * `'skip'` when this isn't a static `nuxt generate` — a plain `nuxt build` (or dev) must never deploy,
14
+ * even with `driver: s3` configured — and when `output.auto` hands publishing to the running server
15
+ * (see `autoPublish`). For a real (non-dry-run) static generate it throws when the
16
+ * bucket or `KESTREL_S3_*` credentials are missing, so a misconfigured deploy fails the generate
17
+ * loudly instead of warning and exiting 0 with a stale live bucket. A dry-run needs neither.
18
+ */
19
+ /** Strip leading/trailing slashes so prefixes compare on bare path segments. */
20
+ const normPrefix = (p) => p.replace(/^\/+|\/+$/g, '');
21
+ /**
22
+ * Do two S3 key prefixes share keyspace on a path-segment boundary — i.e. is one equal to, or nested
23
+ * under, the other? Catches both nesting directions: a prune of `a` reaches everything under `a/b`, and
24
+ * a prune of `a/b` reaches the `a/b/…` slice of media stored under `a`. A pure-string collision like
25
+ * `site` vs `siteother` is *not* an overlap. Empty prefixes never overlap (handled by their own guards).
26
+ */
27
+ function prefixesOverlap(a, b) {
28
+ const x = normPrefix(a);
29
+ const y = normPrefix(b);
30
+ if (!x || !y)
31
+ return false;
32
+ return x === y || x.startsWith(`${y}/`) || y.startsWith(`${x}/`);
33
+ }
34
+ /** @public */
35
+ export function planS3Deploy(opts) {
36
+ if (!opts.isStaticGenerate)
37
+ return 'skip';
38
+ if (opts.autoPublish)
39
+ return 'skip';
40
+ if (!opts.dryRun) {
41
+ const missing = [];
42
+ if (!opts.bucket)
43
+ missing.push('bucket (KESTREL_OUTPUT_S3_BUCKET)');
44
+ if (!opts.accessKeyId)
45
+ missing.push('KESTREL_OUTPUT_S3_ACCESS_KEY_ID / KESTREL_S3_ACCESS_KEY_ID');
46
+ if (!opts.secretAccessKey)
47
+ missing.push('KESTREL_OUTPUT_S3_SECRET_ACCESS_KEY / KESTREL_S3_SECRET_ACCESS_KEY');
48
+ if (missing.length) {
49
+ throw new Error(`[kestrel] output.driver=s3 but ${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} missing`
50
+ + ' — refusing to finish `nuxt generate` with a broken S3 deploy.');
51
+ }
52
+ // The deploy always reconciles (deletes every remote object under the prefix not in this generate —
53
+ // output ≡ DB). With an empty prefix that's the whole bucket root, which may hold the media bucket's
54
+ // objects too. Refuse rather than risk wiping it: an S3 deploy must target a dedicated prefix.
55
+ if (!opts.prefix) {
56
+ throw new Error('[kestrel] output.s3.prefix is empty — refusing to deploy (the always-on reconcile would delete the'
57
+ + ' whole bucket root, and a shared/media bucket could be wiped). Set KESTREL_OUTPUT_S3_PREFIX to a'
58
+ + ' prefix dedicated to the generated site.');
59
+ }
60
+ // Same bucket as the S3 media driver AND overlapping prefixes → the reconcile would delete live uploads.
61
+ if (opts.mediaBucket && opts.mediaBucket === opts.bucket
62
+ && opts.mediaPrefix && prefixesOverlap(opts.prefix, opts.mediaPrefix)) {
63
+ throw new Error(`[kestrel] the output reconcile would delete live media: output prefix "${opts.prefix}" overlaps the`
64
+ + ` S3 media prefix "${opts.mediaPrefix}" in the same bucket "${opts.bucket}". Give the generated`
65
+ + ' site its own bucket or a non-overlapping KESTREL_OUTPUT_S3_PREFIX.');
66
+ }
67
+ }
68
+ return 'deploy';
69
+ }
70
+ const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
71
+ /**
72
+ * Resolve where `nuxt generate`'s static output is shipped: a local directory (default) or an S3
73
+ * bucket. Precedence per setting: `KESTREL_OUTPUT_*` env → config → default (the documented Kestrel
74
+ * order, same as the runtime publisher). Pure, so the precedence is unit-testable; secrets are resolved
75
+ * by `resolveOutputCreds`, never here.
76
+ * @public
77
+ */
78
+ export function resolveOutputTarget(out = {}, env = {}) {
79
+ const driver = (clean(env.KESTREL_OUTPUT_DRIVER) ?? clean(out.driver))?.toLowerCase() === 's3' ? 's3' : 'local';
80
+ // Same non-secret resolver media uses, under the KESTREL_OUTPUT_S3_* prefix (own bucket, own creds).
81
+ return { driver, s3: resolveS3Settings(out.s3, env, 'KESTREL_OUTPUT_S3') };
82
+ }
83
+ /**
84
+ * S3 credentials for the output deploy: the output-specific `KESTREL_OUTPUT_S3_*`, falling back to the
85
+ * shared media `KESTREL_S3_*` so a single S3 account "just works" — identical to the runtime publisher's
86
+ * resolution. Required keys default to '' (not undefined) so `planS3Deploy` detects a missing credential.
87
+ * @public
88
+ */
89
+ export function resolveOutputCreds(env = {}) {
90
+ return {
91
+ accessKeyId: clean(env.KESTREL_OUTPUT_S3_ACCESS_KEY_ID) ?? clean(env.KESTREL_S3_ACCESS_KEY_ID) ?? '',
92
+ secretAccessKey: clean(env.KESTREL_OUTPUT_S3_SECRET_ACCESS_KEY) ?? clean(env.KESTREL_S3_SECRET_ACCESS_KEY) ?? '',
93
+ sessionToken: clean(env.KESTREL_OUTPUT_S3_SESSION_TOKEN) ?? clean(env.KESTREL_S3_SESSION_TOKEN),
94
+ };
95
+ }
96
+ /** Every file under `dir` as `{ abs, key }`, the key being its POSIX-relative path (stable order). */
97
+ function listFiles(dir) {
98
+ return readdirSync(dir, { recursive: true, withFileTypes: true })
99
+ .filter((e) => e.isFile())
100
+ .map((e) => {
101
+ const abs = join(e.parentPath, e.name);
102
+ return { abs, key: relative(dir, abs).split(sep).join('/') };
103
+ });
104
+ }
105
+ /**
106
+ * A `*.gz`/`*.br` file is a pre-compressed sidecar (from `nitro.compressPublicAssets`) when its
107
+ * uncompressed base is also in the output. On a plain static S3 host these are dead weight — the host
108
+ * does no `Accept-Encoding` negotiation, so nobody fetches `index.html.gz`, and storing it *as*
109
+ * `index.html` with `Content-Encoding` would collide with the real file — so the deploy skips them.
110
+ * A standalone `*.gz` archive (no base file) is genuine content and is kept.
111
+ * @public
112
+ */
113
+ export function isCompressedSidecar(key, all) {
114
+ const m = /\.(?:gz|br)$/.exec(key);
115
+ return m ? all.has(key.slice(0, m.index)) : false;
116
+ }
117
+ /** The prerender and deploy modules are loaded separately and share no module scope — but they are handed
118
+ * the same Nuxt instance, so that object is what carries the signal from the one to the other.
119
+ * @public */
120
+ export function recordRouteDiscovery(nuxt, discovery) {
121
+ nuxt._kestrelRouteDiscovery = discovery;
122
+ }
123
+ /** `undefined` ⇒ discovery never reported anything, which is not itself evidence of a degraded build.
124
+ * @public */
125
+ export function readRouteDiscovery(nuxt) {
126
+ return nuxt._kestrelRouteDiscovery;
127
+ }
128
+ async function runPool(items, concurrency, task) {
129
+ let next = 0;
130
+ const worker = async () => {
131
+ while (next < items.length) {
132
+ const i = next++;
133
+ await task(items[i]);
134
+ }
135
+ };
136
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
137
+ }
138
+ /** HTTP statuses the S3 driver surfaces in its error message (e.g. `S3 put failed (403) for x`). */
139
+ function statusFromError(err) {
140
+ const m = /\((\d{3})\)/.exec(err?.message ?? '');
141
+ return m ? Number(m[1]) : undefined;
142
+ }
143
+ /**
144
+ * A permanent client error — bad/forbidden credentials, missing bucket, denied access (400/401/403/404).
145
+ * These never become successful on a later attempt, so retrying only delays the inevitable loud failure.
146
+ * @public
147
+ */
148
+ export function isPermanentError(err) {
149
+ const s = statusFromError(err);
150
+ return s === 400 || s === 401 || s === 403 || s === 404;
151
+ }
152
+ /** Retry `fn` with exponential backoff up to `retries` total attempts, surfacing the last error. */
153
+ async function withRetry(fn, label, retries, sleep, log) {
154
+ for (let attempt = 1;; attempt++) {
155
+ try {
156
+ await fn();
157
+ return;
158
+ }
159
+ catch (err) {
160
+ // A permanent 4xx can't be ridden out — re-throw at once so the deploy fails loudly, not slowly.
161
+ if (isPermanentError(err) || attempt >= retries)
162
+ throw err;
163
+ log?.(`retry ${attempt}/${retries - 1} for ${label}: ${err.message}`);
164
+ await sleep(100 * 2 ** (attempt - 1));
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Upload every file under `dir` to `driver`, keyed by its POSIX-relative path, with a content type
170
+ * inferred from the extension — the S3 equivalent of `aws s3 sync` (overwrites). Then reconciles: deletes
171
+ * remote objects under the prefix that this generate didn't produce (`--delete`), so pages removed from
172
+ * the CMS stop being served (output ≡ DB, no toggle). The reconcile is skipped when the driver can't
173
+ * list+delete and when the caller flagged the tree `incomplete` — a build step reported it is short of
174
+ * pages, so their absence says nothing about the CMS. Uploads still run then: shipping the pages that did
175
+ * render is safe, deleting on their absence is not.
176
+ * Uploads and deletes run with bounded concurrency and each is retried with
177
+ * backoff, so a transient 5xx/throttle/dropped-connection no longer aborts the deploy mid-way and
178
+ * half-updates the bucket; only an operation that fails every attempt rejects (true rollback isn't
179
+ * possible on S3). With `dryRun`, walks/lists and reports without writing.
180
+ * @public
181
+ */
182
+ export async function deployStaticOutput(dir, driver, opts = {}) {
183
+ const concurrency = Math.max(1, opts.concurrency ?? 8);
184
+ const retries = Math.max(1, opts.retries ?? 3);
185
+ const sleep = opts.sleep ?? ((ms) => sleepFor(ms));
186
+ const all = listFiles(dir);
187
+ const allKeys = new Set(all.map((f) => f.key));
188
+ const files = all.filter((f) => !isCompressedSidecar(f.key, allKeys));
189
+ const skipped = all.length - files.length;
190
+ if (skipped)
191
+ opts.log?.(`skipping ${skipped} pre-compressed sidecar(s) (.gz/.br)`);
192
+ const keys = files.map((f) => f.key);
193
+ if (!opts.dryRun) {
194
+ // `files` already excludes pre-compressed sidecars (above), so anything ending `.gz`/`.br` here is a
195
+ // standalone archive shipped with no `Content-Encoding` — keep its own extension in the type lookup.
196
+ await runPool(files, concurrency, (f) => withRetry(() => driver.put(f.key, readFileSync(f.abs), contentTypeFor(f.key, false), { cacheControl: cacheControlFor(f.key) }), f.key, retries, sleep, opts.log));
197
+ }
198
+ opts.log?.(`${opts.dryRun ? 'Would deploy' : 'Deployed'} ${keys.length} file(s)`);
199
+ // Reconcile (output ≡ DB) — skipped when the driver lacks list/delete (e.g. a put-only target) and when
200
+ // a build step reported that this generate is short of pages.
201
+ let pruned = 0;
202
+ if (!driver.list || !driver.delete) {
203
+ opts.log?.('driver cannot list/delete — skipping reconcile');
204
+ }
205
+ else if (opts.incomplete) {
206
+ opts.log?.(`skipping reconcile — ${opts.incomplete}; the missing pages would read as stale and be deleted`);
207
+ }
208
+ else {
209
+ const keep = new Set(keys);
210
+ const stale = (await driver.list()).filter((k) => !keep.has(k));
211
+ if (!opts.dryRun) {
212
+ const del = driver.delete;
213
+ await runPool(stale, concurrency, (k) => withRetry(() => del(k), `delete ${k}`, retries, sleep, opts.log));
214
+ pruned = stale.length;
215
+ }
216
+ opts.log?.(`${opts.dryRun ? 'Would prune' : 'Pruned'} ${stale.length} stale object(s)`);
217
+ }
218
+ return { pruned, keys };
219
+ }
220
+ //# sourceMappingURL=deploy-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-output.js","sourceRoot":"","sources":["../../src/server/deploy-output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { DeliveryPort } from '@michaelthielemann/kestrel-contracts';
2
+ import type { StorageDriver } from '@michaelthielemann/kestrel-core';
3
+ /**
4
+ * The static delivery adapter's `DeliveryPort` (ADR-0013 §3.3): writes/prunes files through a
5
+ * `StorageDriver` (local dir or S3) — the same output the runtime publisher and the build-time deploy have
6
+ * always written to. `publishSnapshot`/`rebuildAll` take the snapshot's `html` as-is (already fully
7
+ * populated, media fixed by the producer at snapshot-creation time) and never re-render or touch the DB.
8
+ * @public
9
+ */
10
+ export declare function createStaticDeliveryPort(driver: StorageDriver): DeliveryPort;
11
+ /**
12
+ * The `delivery` config selection seam (`kestrel.delivery: 'static' | 'live'`, default `'static'` —
13
+ * see `resolveKestrel`): `'static'` returns the file-backed adapter above, `'live'` returns
14
+ * `delivery-live`'s adapter (serves `published_snapshots` at request time instead of writing files —
15
+ * see `createLiveDeliveryPort`'s own TSDoc). The runtime publisher/queue do not construct `DeliveryPort`s
16
+ * directly yet (they still write through `StorageDriver` in `publisher.ts`, and that keeps running under
17
+ * `'live'` too — see `KestrelConfig.delivery`), so nothing calls this seam in production yet.
18
+ * @public
19
+ */
20
+ export declare function deliveryPortFor(delivery: 'static' | 'live', driver: StorageDriver): DeliveryPort;
21
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/server/port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,sCAAsC,CAAA;AAC3F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAKpE;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,YAAY,CAqB5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,YAAY,CAGhG"}
@@ -0,0 +1,46 @@
1
+ import { contentTypeFor, cacheControlFor } from '@michaelthielemann/kestrel-core';
2
+ import { createLiveDeliveryPort } from '@michaelthielemann/kestrel-delivery-live';
3
+ import { htmlKeyForRoute } from '@michaelthielemann/kestrel-publishing';
4
+ /**
5
+ * The static delivery adapter's `DeliveryPort` (ADR-0013 §3.3): writes/prunes files through a
6
+ * `StorageDriver` (local dir or S3) — the same output the runtime publisher and the build-time deploy have
7
+ * always written to. `publishSnapshot`/`rebuildAll` take the snapshot's `html` as-is (already fully
8
+ * populated, media fixed by the producer at snapshot-creation time) and never re-render or touch the DB.
9
+ * @public
10
+ */
11
+ export function createStaticDeliveryPort(driver) {
12
+ async function writeSnapshot(snapshot) {
13
+ const key = htmlKeyForRoute(snapshot.route);
14
+ await driver.put(key, Buffer.from(snapshot.html, 'utf8'), contentTypeFor(key), { cacheControl: cacheControlFor(key) });
15
+ }
16
+ return {
17
+ async publishSnapshot(snapshot) {
18
+ await writeSnapshot(snapshot);
19
+ },
20
+ async removeRoutes(routes) {
21
+ for (const route of routes) {
22
+ await driver.delete(htmlKeyForRoute(route), { pruneEmptyDirs: true });
23
+ }
24
+ },
25
+ async rebuildAll(iter) {
26
+ for await (const snapshot of iter) {
27
+ await writeSnapshot(snapshot);
28
+ }
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * The `delivery` config selection seam (`kestrel.delivery: 'static' | 'live'`, default `'static'` —
34
+ * see `resolveKestrel`): `'static'` returns the file-backed adapter above, `'live'` returns
35
+ * `delivery-live`'s adapter (serves `published_snapshots` at request time instead of writing files —
36
+ * see `createLiveDeliveryPort`'s own TSDoc). The runtime publisher/queue do not construct `DeliveryPort`s
37
+ * directly yet (they still write through `StorageDriver` in `publisher.ts`, and that keeps running under
38
+ * `'live'` too — see `KestrelConfig.delivery`), so nothing calls this seam in production yet.
39
+ * @public
40
+ */
41
+ export function deliveryPortFor(delivery, driver) {
42
+ if (delivery === 'live')
43
+ return createLiveDeliveryPort();
44
+ return createStaticDeliveryPort(driver);
45
+ }
46
+ //# sourceMappingURL=port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/server/port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAA;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAqB;IAC5D,KAAK,UAAU,aAAa,CAAC,QAA2B;QACtD,MAAM,GAAG,GAAG,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACxH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,eAAe,CAAC,QAAQ;YAC5B,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAA;QAC/B,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,MAAM;YACvB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,IAAI,KAAK,EAAE,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;gBAClC,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,QAA2B,EAAE,MAAqB;IAChF,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,sBAAsB,EAAE,CAAA;IACxD,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAA;AACzC,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The static delivery adapter's per-route content lookup: reads the route's current published snapshot
3
+ * and hands back its HTML as a body/status pair, in the same shape the producer's live render uses —
4
+ * so the rest of the publisher (write-to-driver, prune, meta) does not need to know which one produced it.
5
+ * No live server round trip, no DB populate: a route with no current snapshot (never published, or
6
+ * retracted — see `retractSnapshot`) is a plain 404, not an error.
7
+ *
8
+ * An intentional seam, not dead code: `publisher.ts` still writes files by rendering live and calling
9
+ * `driver.put` directly rather than through this function, the same not-yet-wired state `deliveryPortFor`
10
+ * documents for the port it belongs to.
11
+ * @public
12
+ */
13
+ export declare function renderRoute(route: string): {
14
+ body: Buffer | null;
15
+ status: number;
16
+ };
17
+ //# sourceMappingURL=render-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-route.d.ts","sourceRoot":"","sources":["../../src/server/render-route.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAIlF"}
@@ -0,0 +1,20 @@
1
+ import { currentSnapshot, usePublishingDb } from '@michaelthielemann/kestrel-publishing';
2
+ /**
3
+ * The static delivery adapter's per-route content lookup: reads the route's current published snapshot
4
+ * and hands back its HTML as a body/status pair, in the same shape the producer's live render uses —
5
+ * so the rest of the publisher (write-to-driver, prune, meta) does not need to know which one produced it.
6
+ * No live server round trip, no DB populate: a route with no current snapshot (never published, or
7
+ * retracted — see `retractSnapshot`) is a plain 404, not an error.
8
+ *
9
+ * An intentional seam, not dead code: `publisher.ts` still writes files by rendering live and calling
10
+ * `driver.put` directly rather than through this function, the same not-yet-wired state `deliveryPortFor`
11
+ * documents for the port it belongs to.
12
+ * @public
13
+ */
14
+ export function renderRoute(route) {
15
+ const snapshot = currentSnapshot(usePublishingDb().db, route);
16
+ if (!snapshot)
17
+ return { body: null, status: 404 };
18
+ return { body: Buffer.from(snapshot.html, 'utf8'), status: 200 };
19
+ }
20
+ //# sourceMappingURL=render-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-route.js","sourceRoot":"","sources":["../../src/server/render-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAExF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,QAAQ,GAAG,eAAe,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;IAC7D,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;IACjD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;AAClE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@michaelthielemann/kestrel-delivery-static",
3
+ "version": "0.1.0",
4
+ "description": "The static delivery adapter: writes/prunes published snapshots through a StorageDriver (the build-time deploy target), plus the S3 static-output shipper.",
5
+ "license": "Apache-2.0",
6
+ "author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
7
+ "homepage": "https://github.com/MichaelThielemann/kestrel#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MichaelThielemann/kestrel.git",
11
+ "directory": "packages/kestrel-delivery-static"
12
+ },
13
+ "bugs": "https://github.com/MichaelThielemann/kestrel/issues",
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "provenance": true
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22.12.0"
33
+ },
34
+ "dependencies": {
35
+ "@michaelthielemann/kestrel-contracts": "0.1.0",
36
+ "@michaelthielemann/kestrel-publishing": "0.1.0",
37
+ "@michaelthielemann/kestrel-core": "0.1.0",
38
+ "@michaelthielemann/kestrel-delivery-live": "0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^26.2.0",
42
+ "typescript": "^6.0.3",
43
+ "vitest": "^4.1.10"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json",
47
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
48
+ "test": "vitest run --typecheck"
49
+ }
50
+ }