@krantho/server 0.0.0-stage → 1.0.1
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 +21 -0
- package/dist/cdn/cloudfront.d.ts +9 -0
- package/dist/cdn/cloudfront.d.ts.map +1 -0
- package/dist/cdn/cloudfront.js +35 -0
- package/dist/cdn/cloudfront.js.map +1 -0
- package/dist/cdn/gcp.d.ts +15 -0
- package/dist/cdn/gcp.d.ts.map +1 -0
- package/dist/cdn/gcp.js +60 -0
- package/dist/cdn/gcp.js.map +1 -0
- package/dist/cdn/index.d.ts +9 -0
- package/dist/cdn/index.d.ts.map +1 -0
- package/dist/cdn/index.js +30 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/listener.d.ts +14 -0
- package/dist/cdn/listener.d.ts.map +1 -0
- package/dist/cdn/listener.js +53 -0
- package/dist/cdn/listener.js.map +1 -0
- package/dist/cdn/retry.d.ts +3 -0
- package/dist/cdn/retry.d.ts.map +1 -0
- package/dist/cdn/retry.js +17 -0
- package/dist/cdn/retry.js.map +1 -0
- package/dist/cdn/types.d.ts +12 -0
- package/dist/cdn/types.d.ts.map +1 -0
- package/dist/cdn/types.js +2 -0
- package/dist/cdn/types.js.map +1 -0
- package/dist/cdn/vercel.d.ts +9 -0
- package/dist/cdn/vercel.d.ts.map +1 -0
- package/dist/cdn/vercel.js +31 -0
- package/dist/cdn/vercel.js.map +1 -0
- package/dist/content-conflicts.d.ts +18 -0
- package/dist/content-conflicts.d.ts.map +1 -0
- package/dist/content-conflicts.js +22 -0
- package/dist/content-conflicts.js.map +1 -0
- package/dist/content-dates.d.ts +35 -0
- package/dist/content-dates.d.ts.map +1 -0
- package/dist/content-dates.js +85 -0
- package/dist/content-dates.js.map +1 -0
- package/dist/content-loader.d.ts +37 -0
- package/dist/content-loader.d.ts.map +1 -0
- package/dist/content-loader.js +223 -0
- package/dist/content-loader.js.map +1 -0
- package/dist/content-migration.d.ts +101 -0
- package/dist/content-migration.d.ts.map +1 -0
- package/dist/content-migration.js +291 -0
- package/dist/content-migration.js.map +1 -0
- package/dist/convert.d.ts +18 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +152 -0
- package/dist/convert.js.map +1 -0
- package/dist/file-store.d.ts +11 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +39 -0
- package/dist/file-store.js.map +1 -0
- package/dist/file-watcher.d.ts +19 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +99 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/handler.d.ts +61 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +197 -0
- package/dist/handler.js.map +1 -0
- package/dist/import.d.ts +25 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +71 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/indexnow.d.ts +38 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +89 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/lazy-handler.d.ts +7 -0
- package/dist/lazy-handler.d.ts.map +1 -0
- package/dist/lazy-handler.js +16 -0
- package/dist/lazy-handler.js.map +1 -0
- package/dist/llm-seed.d.ts +25 -0
- package/dist/llm-seed.d.ts.map +1 -0
- package/dist/llm-seed.js +42 -0
- package/dist/llm-seed.js.map +1 -0
- package/dist/lock-adapters.d.ts +21 -0
- package/dist/lock-adapters.d.ts.map +1 -0
- package/dist/lock-adapters.js +69 -0
- package/dist/lock-adapters.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +39 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mime-types.d.ts +3 -0
- package/dist/mime-types.d.ts.map +1 -0
- package/dist/mime-types.js +15 -0
- package/dist/mime-types.js.map +1 -0
- package/dist/page-cache-rules.d.ts +41 -0
- package/dist/page-cache-rules.d.ts.map +1 -0
- package/dist/page-cache-rules.js +94 -0
- package/dist/page-cache-rules.js.map +1 -0
- package/dist/page-cache-store.d.ts +11 -0
- package/dist/page-cache-store.d.ts.map +1 -0
- package/dist/page-cache-store.js +46 -0
- package/dist/page-cache-store.js.map +1 -0
- package/dist/page-request.d.ts +26 -0
- package/dist/page-request.d.ts.map +1 -0
- package/dist/page-request.js +29 -0
- package/dist/page-request.js.map +1 -0
- package/dist/persistence.d.ts +36 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +108 -0
- package/dist/persistence.js.map +1 -0
- package/dist/preview-token.d.ts +9 -0
- package/dist/preview-token.d.ts.map +1 -0
- package/dist/preview-token.js +14 -0
- package/dist/preview-token.js.map +1 -0
- package/dist/read-cache.d.ts +17 -0
- package/dist/read-cache.d.ts.map +1 -0
- package/dist/read-cache.js +33 -0
- package/dist/read-cache.js.map +1 -0
- package/dist/route-matcher.d.ts +21 -0
- package/dist/route-matcher.d.ts.map +1 -0
- package/dist/route-matcher.js +75 -0
- package/dist/route-matcher.js.map +1 -0
- package/dist/secret-vault.d.ts +20 -0
- package/dist/secret-vault.d.ts.map +1 -0
- package/dist/secret-vault.js +60 -0
- package/dist/secret-vault.js.map +1 -0
- package/dist/seo-config-io.d.ts +51 -0
- package/dist/seo-config-io.d.ts.map +1 -0
- package/dist/seo-config-io.js +151 -0
- package/dist/seo-config-io.js.map +1 -0
- package/dist/seo-seed.d.ts +12 -0
- package/dist/seo-seed.d.ts.map +1 -0
- package/dist/seo-seed.js +100 -0
- package/dist/seo-seed.js.map +1 -0
- package/dist/studio-gate.d.ts +25 -0
- package/dist/studio-gate.d.ts.map +1 -0
- package/dist/studio-gate.js +70 -0
- package/dist/studio-gate.js.map +1 -0
- package/dist/studio.d.ts +17 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +110 -0
- package/dist/studio.js.map +1 -0
- package/dist/webhook-sweep.d.ts +22 -0
- package/dist/webhook-sweep.d.ts.map +1 -0
- package/dist/webhook-sweep.js +33 -0
- package/dist/webhook-sweep.js.map +1 -0
- package/package.json +89 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Descoped
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CdnInvalidator } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* CloudFront purges through `CreateInvalidation` with `@aws-sdk/client-cloudfront`
|
|
4
|
+
* (an optional peer); credentials come from the SDK's default chain. A tag
|
|
5
|
+
* invalidates as a `#`-prefixed path. CloudFront only deletes, so the mode is
|
|
6
|
+
* ignored.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createCloudFrontInvalidator(distributionId: string): CdnInvalidator;
|
|
9
|
+
//# sourceMappingURL=cloudfront.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudfront.d.ts","sourceRoot":"","sources":["../../src/cdn/cloudfront.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAKhD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc,CA0BlF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { purgeWithRetry } from './retry.js';
|
|
3
|
+
/** CloudFront's API is global; requests are signed for us-east-1 */
|
|
4
|
+
const CLOUDFRONT_REGION = 'us-east-1';
|
|
5
|
+
/**
|
|
6
|
+
* CloudFront purges through `CreateInvalidation` with `@aws-sdk/client-cloudfront`
|
|
7
|
+
* (an optional peer); credentials come from the SDK's default chain. A tag
|
|
8
|
+
* invalidates as a `#`-prefixed path. CloudFront only deletes, so the mode is
|
|
9
|
+
* ignored.
|
|
10
|
+
*/
|
|
11
|
+
export function createCloudFrontInvalidator(distributionId) {
|
|
12
|
+
let client;
|
|
13
|
+
async function invalidate(items) {
|
|
14
|
+
if (items.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
// One reference per purge: a retry of the same batch is idempotent.
|
|
17
|
+
const callerReference = `krantho-${Date.now()}-${randomUUID()}`;
|
|
18
|
+
await purgeWithRetry('cloudfront', async () => {
|
|
19
|
+
const sdk = await import('@aws-sdk/client-cloudfront');
|
|
20
|
+
client ??= new sdk.CloudFrontClient({ region: CLOUDFRONT_REGION });
|
|
21
|
+
await client.send(new sdk.CreateInvalidationCommand({
|
|
22
|
+
DistributionId: distributionId,
|
|
23
|
+
InvalidationBatch: {
|
|
24
|
+
CallerReference: callerReference,
|
|
25
|
+
Paths: { Quantity: items.length, Items: items },
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
purgeTags: (tags) => invalidate(tags.map((tag) => `#${tag}`)),
|
|
32
|
+
purgePaths: (paths) => invalidate([...paths]),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=cloudfront.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudfront.js","sourceRoot":"","sources":["../../src/cdn/cloudfront.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAG3C,oEAAoE;AACpE,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAErC;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CAAC,cAAsB;IAChE,IAAI,MAAoC,CAAA;IAExC,KAAK,UAAU,UAAU,CAAC,KAAe;QACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC9B,oEAAoE;QACpE,MAAM,eAAe,GAAG,WAAW,IAAI,CAAC,GAAG,EAAE,IAAI,UAAU,EAAE,EAAE,CAAA;QAC/D,MAAM,cAAc,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE;YAC5C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAA;YACtD,MAAM,KAAK,IAAI,GAAG,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAA;YAClE,MAAM,MAAM,CAAC,IAAI,CACf,IAAI,GAAG,CAAC,yBAAyB,CAAC;gBAChC,cAAc,EAAE,cAAc;gBAC9B,iBAAiB,EAAE;oBACjB,eAAe,EAAE,eAAe;oBAChC,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE;iBAChD;aACF,CAAC,CACH,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAC7D,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;KAC9C,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CdnInvalidator } from './types.js';
|
|
2
|
+
export interface GcpInvalidatorOptions {
|
|
3
|
+
project: string;
|
|
4
|
+
urlMap: string;
|
|
5
|
+
/** A fixed bearer token; without one, tokens come from the metadata server */
|
|
6
|
+
accessToken?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Cloud CDN purges through the URL map's `invalidateCache`: tags in chunks
|
|
10
|
+
* to the `beta` endpoint, each path to `v1`. The bearer token is the
|
|
11
|
+
* configured one, or a metadata-server token reused until it expires. Cloud
|
|
12
|
+
* CDN only deletes, so the mode is ignored.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createGcpInvalidator(options: GcpInvalidatorOptions): CdnInvalidator;
|
|
15
|
+
//# sourceMappingURL=gcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcp.d.ts","sourceRoot":"","sources":["../../src/cdn/gcp.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAUhD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACjC;AAOD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CA2BnF"}
|
package/dist/cdn/gcp.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { purgeWithRetry } from './retry.js';
|
|
2
|
+
const COMPUTE_API = 'https://compute.googleapis.com/compute';
|
|
3
|
+
const METADATA_TOKEN_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token';
|
|
4
|
+
/** Cloud CDN accepts at most this many tags per invalidation request */
|
|
5
|
+
const TAGS_PER_REQUEST = 10;
|
|
6
|
+
/** Refresh a metadata token this long before it expires */
|
|
7
|
+
const TOKEN_EXPIRY_MARGIN_MS = 60_000;
|
|
8
|
+
/**
|
|
9
|
+
* Cloud CDN purges through the URL map's `invalidateCache`: tags in chunks
|
|
10
|
+
* to the `beta` endpoint, each path to `v1`. The bearer token is the
|
|
11
|
+
* configured one, or a metadata-server token reused until it expires. Cloud
|
|
12
|
+
* CDN only deletes, so the mode is ignored.
|
|
13
|
+
*/
|
|
14
|
+
export function createGcpInvalidator(options) {
|
|
15
|
+
const token = createTokenSource(options.accessToken);
|
|
16
|
+
const mapPath = `projects/${encodeURIComponent(options.project)}/global/urlMaps/${encodeURIComponent(options.urlMap)}/invalidateCache`;
|
|
17
|
+
function invalidate(version, body) {
|
|
18
|
+
return purgeWithRetry('gcp', async () => {
|
|
19
|
+
const response = await fetch(`${COMPUTE_API}/${version}/${mapPath}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { Authorization: `Bearer ${await token()}`, 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok)
|
|
25
|
+
throw new Error(`invalidateCache answered ${response.status}`);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
async purgeTags(tags) {
|
|
30
|
+
for (let i = 0; i < tags.length; i += TAGS_PER_REQUEST) {
|
|
31
|
+
await invalidate('beta', { cacheTags: tags.slice(i, i + TAGS_PER_REQUEST) });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async purgePaths(paths) {
|
|
35
|
+
for (const path of paths) {
|
|
36
|
+
await invalidate('v1', { path });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The configured token, or a metadata-server token cached until shortly before it expires */
|
|
42
|
+
function createTokenSource(accessToken) {
|
|
43
|
+
if (accessToken !== undefined)
|
|
44
|
+
return async () => accessToken;
|
|
45
|
+
let cached;
|
|
46
|
+
return async () => {
|
|
47
|
+
if (cached !== undefined && Date.now() < cached.expiresAt)
|
|
48
|
+
return cached.value;
|
|
49
|
+
const response = await fetch(METADATA_TOKEN_URL, { headers: { 'Metadata-Flavor': 'Google' } });
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw new Error(`metadata server answered ${response.status}`);
|
|
52
|
+
const body = (await response.json());
|
|
53
|
+
cached = {
|
|
54
|
+
value: body.access_token,
|
|
55
|
+
expiresAt: Date.now() + body.expires_in * 1000 - TOKEN_EXPIRY_MARGIN_MS,
|
|
56
|
+
};
|
|
57
|
+
return cached.value;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=gcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcp.js","sourceRoot":"","sources":["../../src/cdn/gcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAG3C,MAAM,WAAW,GAAG,wCAAwC,CAAA;AAC5D,MAAM,kBAAkB,GACtB,4FAA4F,CAAA;AAC9F,wEAAwE;AACxE,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAC3B,2DAA2D;AAC3D,MAAM,sBAAsB,GAAG,MAAM,CAAA;AAcrC;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACpD,MAAM,OAAO,GAAG,YAAY,kBAAkB,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAA;IAEtI,SAAS,UAAU,CAAC,OAAsB,EAAE,IAA6B;QACvE,OAAO,cAAc,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;YACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,WAAW,IAAI,OAAO,IAAI,OAAO,EAAE,EAAE;gBACnE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,KAAK,EAAE,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBACzF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAA;YACF,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;QAClF,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,IAAI;YAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,gBAAgB,EAAE,CAAC;gBACvD,MAAM,UAAU,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA;YAC9E,CAAC;QACH,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,KAAK;YACpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,UAAU,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,8FAA8F;AAC9F,SAAS,iBAAiB,CAAC,WAA+B;IACxD,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,KAAK,IAAI,EAAE,CAAC,WAAW,CAAA;IAC7D,IAAI,MAAwD,CAAA;IAC5D,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS;YAAE,OAAO,MAAM,CAAC,KAAK,CAAA;QAC9E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;QAC9F,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;QAChF,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAkB,CAAA;QACrD,MAAM,GAAG;YACP,KAAK,EAAE,IAAI,CAAC,YAAY;YACxB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,GAAG,sBAAsB;SACxE,CAAA;QACD,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CacheSection } from '@krantho/config';
|
|
2
|
+
import type { CdnInvalidator } from './types.js';
|
|
3
|
+
export type { CdnInvalidator } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* The CDN invalidator for the configured provider; null when no CDN is
|
|
6
|
+
* configured. Configuration validation guarantees each provider's settings.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createCdnInvalidator(cache: CacheSection): CdnInvalidator | null;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdn/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhD,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,cAAc,GAAG,IAAI,CAkB/E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createCloudFrontInvalidator } from './cloudfront.js';
|
|
2
|
+
import { createGcpInvalidator } from './gcp.js';
|
|
3
|
+
import { createVercelInvalidator } from './vercel.js';
|
|
4
|
+
/**
|
|
5
|
+
* The CDN invalidator for the configured provider; null when no CDN is
|
|
6
|
+
* configured. Configuration validation guarantees each provider's settings.
|
|
7
|
+
*/
|
|
8
|
+
export function createCdnInvalidator(cache) {
|
|
9
|
+
const { cdn } = cache;
|
|
10
|
+
switch (cdn.provider) {
|
|
11
|
+
case 'none':
|
|
12
|
+
return null;
|
|
13
|
+
case 'vercel':
|
|
14
|
+
return createVercelInvalidator();
|
|
15
|
+
case 'cloudfront':
|
|
16
|
+
return createCloudFrontInvalidator(required(cdn.cloudfront.distributionId, 'cache.cdn.cloudfront.distributionId'));
|
|
17
|
+
case 'gcp':
|
|
18
|
+
return createGcpInvalidator({
|
|
19
|
+
project: required(cdn.gcp.project, 'cache.cdn.gcp.project'),
|
|
20
|
+
urlMap: required(cdn.gcp.urlMap, 'cache.cdn.gcp.urlMap'),
|
|
21
|
+
accessToken: cdn.gcp.accessToken,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function required(value, name) {
|
|
26
|
+
if (value === undefined || value === '')
|
|
27
|
+
throw new Error(`${name} is required for this CDN provider`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cdn/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAE/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAIrD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAmB;IACtD,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAA;IACrB,QAAQ,GAAG,CAAC,QAAQ,EAAE,CAAC;QACrB,KAAK,MAAM;YACT,OAAO,IAAI,CAAA;QACb,KAAK,QAAQ;YACX,OAAO,uBAAuB,EAAE,CAAA;QAClC,KAAK,YAAY;YACf,OAAO,2BAA2B,CAChC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,cAAc,EAAE,qCAAqC,CAAC,CAC/E,CAAA;QACH,KAAK,KAAK;YACR,OAAO,oBAAoB,CAAC;gBAC1B,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,uBAAuB,CAAC;gBAC3D,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC;gBACxD,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW;aACjC,CAAC,CAAA;IACN,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB,EAAE,IAAY;IACvD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QACrC,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,oCAAoC,CAAC,CAAA;IAC9D,OAAO,KAAK,CAAA;AACd,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CacheSection } from '@krantho/config';
|
|
2
|
+
import type { ContentChangeListener } from '@krantho/core';
|
|
3
|
+
import type { CdnInvalidator } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* The change listener that purges a CDN in front of the site, or null when
|
|
6
|
+
* nothing needs purging: `pages: 'cdn'` purges by tag, `pages: 'next-isr'`
|
|
7
|
+
* purges every path. Next.js on Vercel needs no listener — `revalidateTag`
|
|
8
|
+
* already purges Vercel's edge.
|
|
9
|
+
*
|
|
10
|
+
* A purge runs in the background: the listener returns at once, hands the
|
|
11
|
+
* purge to `waitUntil` when the host gives one, and never fails the write.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cdnChangeListener(cache: CacheSection, invalidator: CdnInvalidator, waitUntil?: ((work: Promise<unknown>) => void) | undefined): ContentChangeListener | null;
|
|
14
|
+
//# sourceMappingURL=listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/cdn/listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAqB,qBAAqB,EAAoB,MAAM,eAAe,CAAA;AAE/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,WAAW,EAAE,cAAc,EAC3B,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,GACzD,qBAAqB,GAAG,IAAI,CAe9B"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { invalidationsFor } from '@krantho/core';
|
|
2
|
+
/**
|
|
3
|
+
* The change listener that purges a CDN in front of the site, or null when
|
|
4
|
+
* nothing needs purging: `pages: 'cdn'` purges by tag, `pages: 'next-isr'`
|
|
5
|
+
* purges every path. Next.js on Vercel needs no listener — `revalidateTag`
|
|
6
|
+
* already purges Vercel's edge.
|
|
7
|
+
*
|
|
8
|
+
* A purge runs in the background: the listener returns at once, hands the
|
|
9
|
+
* purge to `waitUntil` when the host gives one, and never fails the write.
|
|
10
|
+
*/
|
|
11
|
+
export function cdnChangeListener(cache, invalidator, waitUntil) {
|
|
12
|
+
const purge = purgeFor(cache, invalidator);
|
|
13
|
+
if (!purge)
|
|
14
|
+
return null;
|
|
15
|
+
return (change) => {
|
|
16
|
+
const invalidations = invalidationsFor(change);
|
|
17
|
+
if (invalidations.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
const work = Promise.resolve()
|
|
20
|
+
.then(() => purge(invalidations))
|
|
21
|
+
.catch((error) => console.error('[cdn] purge failed:', error));
|
|
22
|
+
try {
|
|
23
|
+
waitUntil?.(work);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('[cdn] waitUntil rejected the purge:', error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function purgeFor(cache, invalidator) {
|
|
31
|
+
if (cache.pages === 'cdn')
|
|
32
|
+
return (invalidations) => purgeTags(invalidator, invalidations);
|
|
33
|
+
if (cache.pages === 'next-isr' && cache.cdn.provider !== 'vercel') {
|
|
34
|
+
// Every live change purges every path: a published document also shows on
|
|
35
|
+
// list pages whose paths krantho cannot know, and a per-path purge would
|
|
36
|
+
// leave them stale until `s-maxage`. The CDN's origin is Next's warm ISR
|
|
37
|
+
// cache, so a full purge costs only cheap origin hits.
|
|
38
|
+
return (invalidations) => {
|
|
39
|
+
const mode = invalidations.some((i) => i.mode === 'expire') ? 'expire' : 'stale';
|
|
40
|
+
return invalidator.purgePaths(['/*'], mode);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** One tag purge per mode */
|
|
46
|
+
async function purgeTags(invalidator, invalidations) {
|
|
47
|
+
const byMode = new Map();
|
|
48
|
+
for (const { tags, mode } of invalidations) {
|
|
49
|
+
byMode.set(mode, [...(byMode.get(mode) ?? []), ...tags]);
|
|
50
|
+
}
|
|
51
|
+
await Promise.all([...byMode].map(([mode, tags]) => invalidator.purgeTags(tags, mode)));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.js","sourceRoot":"","sources":["../../src/cdn/listener.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAGhD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAmB,EACnB,WAA2B,EAC3B,SAA0D;IAE1D,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IAC1C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;QAC9C,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE;aAC3B,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;aAChC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC,CAAA;QACzE,IAAI,CAAC;YACH,SAAS,EAAE,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAID,SAAS,QAAQ,CAAC,KAAmB,EAAE,WAA2B;IAChE,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;QAAE,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;IAC1F,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClE,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,uDAAuD;QACvD,OAAO,CAAC,aAAa,EAAE,EAAE;YACvB,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAA;YAChF,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAA;QAC7C,CAAC,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,6BAA6B;AAC7B,KAAK,UAAU,SAAS,CACtB,WAA2B,EAC3B,aAA2C;IAE3C,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAA;IACpD,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,aAAa,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;IAC1D,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AACzF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/cdn/retry.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAYf"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runs one purge request, retries it once on failure, then logs — never throws */
|
|
2
|
+
export async function purgeWithRetry(provider, request) {
|
|
3
|
+
try {
|
|
4
|
+
await request();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
// Retried once below; the second failure is the one logged.
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
await request();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error(`[cdn] ${provider} purge failed after a retry:`, error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/cdn/retry.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAAgB,EAChB,OAA4B;IAE5B,IAAI,CAAC;QACH,MAAM,OAAO,EAAE,CAAA;QACf,OAAM;IACR,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,EAAE,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,SAAS,QAAQ,8BAA8B,EAAE,KAAK,CAAC,CAAA;IACvE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InvalidationMode } from '@krantho/core';
|
|
2
|
+
/**
|
|
3
|
+
* Purges cached pages from a CDN. A purge never throws: a failed request is
|
|
4
|
+
* retried once, then logged — a lost purge is bounded by the page's `s-maxage`.
|
|
5
|
+
*/
|
|
6
|
+
export interface CdnInvalidator {
|
|
7
|
+
/** Purge every cached response carrying any of the tags */
|
|
8
|
+
purgeTags(tags: readonly string[], mode: InvalidationMode): Promise<void>;
|
|
9
|
+
/** Purge cached responses by path (`/about`, `/about*`, `/*`) */
|
|
10
|
+
purgePaths(paths: readonly string[], mode: InvalidationMode): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cdn/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzE,iEAAiE;IACjE,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cdn/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CdnInvalidator } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vercel purges by tag through `@vercel/functions` (an optional peer). Both
|
|
4
|
+
* calls work inside a Vercel function and resolve as no-ops elsewhere.
|
|
5
|
+
* `expire` deletes, so the next request renders; `stale` serves the old page
|
|
6
|
+
* while it refreshes. Vercel has no path purge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createVercelInvalidator(): CdnInvalidator;
|
|
9
|
+
//# sourceMappingURL=vercel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../src/cdn/vercel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,cAAc,CAoBxD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { purgeWithRetry } from './retry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vercel purges by tag through `@vercel/functions` (an optional peer). Both
|
|
4
|
+
* calls work inside a Vercel function and resolve as no-ops elsewhere.
|
|
5
|
+
* `expire` deletes, so the next request renders; `stale` serves the old page
|
|
6
|
+
* while it refreshes. Vercel has no path purge.
|
|
7
|
+
*/
|
|
8
|
+
export function createVercelInvalidator() {
|
|
9
|
+
let warnedPaths = false;
|
|
10
|
+
return {
|
|
11
|
+
async purgeTags(tags, mode) {
|
|
12
|
+
if (tags.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
await purgeWithRetry('vercel', async () => {
|
|
15
|
+
const vercel = await import('@vercel/functions');
|
|
16
|
+
if (mode === 'expire') {
|
|
17
|
+
await vercel.dangerouslyDeleteByTag([...tags], { revalidationDeadlineSeconds: 0 });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await vercel.invalidateByTag([...tags]);
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
async purgePaths() {
|
|
24
|
+
if (warnedPaths)
|
|
25
|
+
return;
|
|
26
|
+
warnedPaths = true;
|
|
27
|
+
console.warn('[cdn] vercel purges by tag only; path purges are ignored');
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=vercel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel.js","sourceRoot":"","sources":["../../src/cdn/vercel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAG3C;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,WAAW,GAAG,KAAK,CAAA;IACvB,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI;YACxB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YAC7B,MAAM,cAAc,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;gBACxC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAA;gBAChD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACtB,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,2BAA2B,EAAE,CAAC,EAAE,CAAC,CAAA;oBAClF,OAAM;gBACR,CAAC;gBACD,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;YACzC,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,UAAU;YACd,IAAI,WAAW;gBAAE,OAAM;YACvB,WAAW,GAAG,IAAI,CAAA;YAClB,OAAO,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAA;QAC1E,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ContentConflict, IContentMigrationAdapter } from '@krantho/persistence';
|
|
2
|
+
/** A conflict as a deployment records it */
|
|
3
|
+
type NewConflict = Parameters<IContentMigrationAdapter['recordConflict']>[0];
|
|
4
|
+
/**
|
|
5
|
+
* The document a conflict is about. Without a locale (a new file in the default locale, with
|
|
6
|
+
* no document yet) it matches the document in any locale.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConflictDocument {
|
|
9
|
+
collection: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
locale: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** An imported document's open conflicts are settled: the repository's version now stands */
|
|
14
|
+
export declare function resolveConflictsOf(migrationAdapter: IContentMigrationAdapter, open: readonly ContentConflict[], document: ConflictDocument): Promise<void>;
|
|
15
|
+
/** Record a conflict unless the same one is still open from an earlier deployment */
|
|
16
|
+
export declare function recordNewConflict(migrationAdapter: IContentMigrationAdapter, open: readonly ContentConflict[], conflict: NewConflict): Promise<void>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=content-conflicts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-conflicts.d.ts","sourceRoot":"","sources":["../src/content-conflicts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAErF,4CAA4C;AAC5C,KAAK,WAAW,GAAG,UAAU,CAAC,wBAAwB,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAE5E;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAUD,6FAA6F;AAC7F,wBAAsB,kBAAkB,CACtC,gBAAgB,EAAE,wBAAwB,EAC1C,IAAI,EAAE,SAAS,eAAe,EAAE,EAChC,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,qFAAqF;AACrF,wBAAsB,iBAAiB,CACrC,gBAAgB,EAAE,wBAAwB,EAC1C,IAAI,EAAE,SAAS,eAAe,EAAE,EAChC,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function isConflictOf(conflict, document) {
|
|
2
|
+
return (conflict.collection === document.collection &&
|
|
3
|
+
conflict.slug === document.slug &&
|
|
4
|
+
(document.locale === undefined || conflict.locale === document.locale));
|
|
5
|
+
}
|
|
6
|
+
/** An imported document's open conflicts are settled: the repository's version now stands */
|
|
7
|
+
export async function resolveConflictsOf(migrationAdapter, open, document) {
|
|
8
|
+
for (const conflict of open) {
|
|
9
|
+
if (isConflictOf(conflict, document)) {
|
|
10
|
+
await migrationAdapter.resolveConflict(conflict.id, 'use_repo');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Record a conflict unless the same one is still open from an earlier deployment */
|
|
15
|
+
export async function recordNewConflict(migrationAdapter, open, conflict) {
|
|
16
|
+
const recorded = open.some((o) => isConflictOf(o, conflict) &&
|
|
17
|
+
o.repoChecksum === conflict.repoChecksum &&
|
|
18
|
+
o.dbChecksum === conflict.dbChecksum);
|
|
19
|
+
if (!recorded)
|
|
20
|
+
await migrationAdapter.recordConflict(conflict);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=content-conflicts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-conflicts.js","sourceRoot":"","sources":["../src/content-conflicts.ts"],"names":[],"mappings":"AAeA,SAAS,YAAY,CAAC,QAAyB,EAAE,QAA0B;IACzE,OAAO,CACL,QAAQ,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU;QAC3C,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;QAC/B,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC,CACvE,CAAA;AACH,CAAC;AAED,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,gBAA0C,EAC1C,IAAgC,EAChC,QAA0B;IAE1B,KAAK,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,IAAI,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACrC,MAAM,gBAAgB,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QACjE,CAAC;IACH,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,gBAA0C,EAC1C,IAAgC,EAChC,QAAqB;IAErB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAC;QACzB,CAAC,CAAC,YAAY,KAAK,QAAQ,CAAC,YAAY;QACxC,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU,CACvC,CAAA;IACD,IAAI,CAAC,QAAQ;QAAE,MAAM,gBAAgB,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAA;AAChE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The file in the content directory that holds each content file's last change */
|
|
2
|
+
export declare const CONTENT_DATES_FILE = "dates.json";
|
|
3
|
+
/**
|
|
4
|
+
* When each content file last changed, keyed by its path relative to the
|
|
5
|
+
* content directory with forward slashes (`docs/install.md`), as ISO 8601.
|
|
6
|
+
*/
|
|
7
|
+
export type ContentDates = Readonly<Record<string, string>>;
|
|
8
|
+
/** The key a content file has in {@link ContentDates} */
|
|
9
|
+
export declare function contentDateKey(contentDir: string, filePath: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Write `<contentDir>/dates.json`: each markdown file's last git commit date.
|
|
12
|
+
*
|
|
13
|
+
* Run at build time, where the repository's history is available; the server
|
|
14
|
+
* reads the file when it imports content, so a page that was never edited in
|
|
15
|
+
* Studio carries the date its file last changed, not the date of the
|
|
16
|
+
* import. Answers null and writes nothing when the history is missing or
|
|
17
|
+
* shallow (a CI checkout without `fetch-depth: 0`) — every file would then
|
|
18
|
+
* carry the latest commit's date. A file that is not committed yet is left out.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateContentDates(contentDir: string): Promise<ContentDates | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The content's version: when the last commit that touched the content
|
|
23
|
+
* directory was made, in seconds since the epoch. It grows with every content
|
|
24
|
+
* change, including a deleted file. Null without the full history.
|
|
25
|
+
*/
|
|
26
|
+
export declare function contentVersion(contentDir: string): Promise<number | null>;
|
|
27
|
+
/** The commit the content is built from (`git rev-parse HEAD`), or null outside a git checkout */
|
|
28
|
+
export declare function contentCommit(contentDir: string): Promise<string | null>;
|
|
29
|
+
/** Read `<contentDir>/dates.json`, or no dates when the build wrote none */
|
|
30
|
+
export declare function loadContentDates(contentDir: string): Promise<ContentDates>;
|
|
31
|
+
/** When a content file last changed, by its path; undefined dates the import now */
|
|
32
|
+
export type ContentDateOf = (filePath: string) => string | undefined;
|
|
33
|
+
/** Look up a content file's date in the dates the build wrote for `contentDir` */
|
|
34
|
+
export declare function contentDateLookup(contentDir: string): Promise<ContentDateOf>;
|
|
35
|
+
//# sourceMappingURL=content-dates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-dates.d.ts","sourceRoot":"","sources":["../src/content-dates.ts"],"names":[],"mappings":"AAOA,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,eAAe,CAAA;AAE9C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAE3D,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE3E;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAe3F;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAK/E;AAED,kGAAkG;AAClG,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAG9E;AAED,4EAA4E;AAC5E,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAYhF;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;AAEpE,kFAAkF;AAClF,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAGlF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
/** The file in the content directory that holds each content file's last change */
|
|
7
|
+
export const CONTENT_DATES_FILE = 'dates.json';
|
|
8
|
+
/** The key a content file has in {@link ContentDates} */
|
|
9
|
+
export function contentDateKey(contentDir, filePath) {
|
|
10
|
+
return relative(contentDir, filePath).split(sep).join('/');
|
|
11
|
+
}
|
|
12
|
+
async function git(cwd, args) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await run('git', args, { cwd });
|
|
15
|
+
return stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Every markdown file under `dir`, recursively */
|
|
22
|
+
async function markdownFiles(dir) {
|
|
23
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
24
|
+
return entries
|
|
25
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
26
|
+
.map((entry) => join(entry.parentPath, entry.name));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Write `<contentDir>/dates.json`: each markdown file's last git commit date.
|
|
30
|
+
*
|
|
31
|
+
* Run at build time, where the repository's history is available; the server
|
|
32
|
+
* reads the file when it imports content, so a page that was never edited in
|
|
33
|
+
* Studio carries the date its file last changed, not the date of the
|
|
34
|
+
* import. Answers null and writes nothing when the history is missing or
|
|
35
|
+
* shallow (a CI checkout without `fetch-depth: 0`) — every file would then
|
|
36
|
+
* carry the latest commit's date. A file that is not committed yet is left out.
|
|
37
|
+
*/
|
|
38
|
+
export async function generateContentDates(contentDir) {
|
|
39
|
+
const shallow = await git(contentDir, ['rev-parse', '--is-shallow-repository']);
|
|
40
|
+
if (shallow !== 'false')
|
|
41
|
+
return null;
|
|
42
|
+
const dates = {};
|
|
43
|
+
for (const file of await markdownFiles(contentDir)) {
|
|
44
|
+
const date = await git(contentDir, ['log', '-1', '--format=%cI', '--', file]);
|
|
45
|
+
if (date)
|
|
46
|
+
dates[contentDateKey(contentDir, file)] = new Date(date).toISOString();
|
|
47
|
+
}
|
|
48
|
+
await writeFile(join(contentDir, CONTENT_DATES_FILE), `${JSON.stringify(dates, null, 2)}\n`, 'utf-8');
|
|
49
|
+
return dates;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The content's version: when the last commit that touched the content
|
|
53
|
+
* directory was made, in seconds since the epoch. It grows with every content
|
|
54
|
+
* change, including a deleted file. Null without the full history.
|
|
55
|
+
*/
|
|
56
|
+
export async function contentVersion(contentDir) {
|
|
57
|
+
const shallow = await git(contentDir, ['rev-parse', '--is-shallow-repository']);
|
|
58
|
+
if (shallow !== 'false')
|
|
59
|
+
return null;
|
|
60
|
+
const seconds = Number(await git(contentDir, ['log', '-1', '--format=%ct', '--', '.']));
|
|
61
|
+
return Number.isInteger(seconds) && seconds > 0 ? seconds : null;
|
|
62
|
+
}
|
|
63
|
+
/** The commit the content is built from (`git rev-parse HEAD`), or null outside a git checkout */
|
|
64
|
+
export async function contentCommit(contentDir) {
|
|
65
|
+
const commit = await git(contentDir, ['rev-parse', 'HEAD']);
|
|
66
|
+
return commit && /^[0-9a-f]{40}$/.test(commit) ? commit : null;
|
|
67
|
+
}
|
|
68
|
+
/** Read `<contentDir>/dates.json`, or no dates when the build wrote none */
|
|
69
|
+
export async function loadContentDates(contentDir) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = JSON.parse(await readFile(join(contentDir, CONTENT_DATES_FILE), 'utf-8'));
|
|
72
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
73
|
+
return {};
|
|
74
|
+
return Object.fromEntries(Object.entries(raw).filter((entry) => typeof entry[1] === 'string'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Look up a content file's date in the dates the build wrote for `contentDir` */
|
|
81
|
+
export async function contentDateLookup(contentDir) {
|
|
82
|
+
const dates = await loadContentDates(contentDir);
|
|
83
|
+
return (filePath) => dates[contentDateKey(contentDir, filePath)];
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=content-dates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-dates.js","sourceRoot":"","sources":["../src/content-dates.ts"],"names":[],"mappings":"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"}
|