@blinkk/root-cms 2.5.7 → 2.5.9
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/dist/app.js +8 -2
- package/dist/{chunk-7LT3ZBCG.js → chunk-5N2KLMQH.js} +15 -0
- package/dist/{chunk-A5DEI2QQ.js → chunk-AFL5KJ4E.js} +1 -1
- package/dist/{chunk-LAHFBC64.js → chunk-SW3UAKP5.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{client-TgoRyFWJ.d.ts → client-BUnrUs3a.d.ts} +85 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/functions.js +2 -2
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +317 -9
- package/dist/ui/ui.css +1 -1
- package/dist/ui/ui.js +152 -152
- package/dist/ui/ui.js.LEGAL.txt +2 -0
- package/package.json +3 -3
package/dist/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getServerVersion
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SW3UAKP5.js";
|
|
4
4
|
import {
|
|
5
5
|
getCollectionSchema,
|
|
6
6
|
getProjectSchemas
|
|
@@ -122,7 +122,13 @@ async function renderApp(req, res, options) {
|
|
|
122
122
|
experiments: cmsConfig.experiments,
|
|
123
123
|
preview: {
|
|
124
124
|
channel: cmsConfig.preview?.channel ?? false
|
|
125
|
-
}
|
|
125
|
+
},
|
|
126
|
+
checks: (cmsConfig.checks || []).map((c) => ({
|
|
127
|
+
id: c.id,
|
|
128
|
+
label: c.label,
|
|
129
|
+
description: c.description,
|
|
130
|
+
collections: c.collections
|
|
131
|
+
}))
|
|
126
132
|
};
|
|
127
133
|
const projectName = cmsConfig.name || cmsConfig.id || "";
|
|
128
134
|
const title = getCmsTitle(projectName, cmsConfig.minimalBranding);
|
|
@@ -1238,6 +1238,13 @@ ${errorMessages}`);
|
|
|
1238
1238
|
await batch.commit();
|
|
1239
1239
|
}
|
|
1240
1240
|
console.log(`published ${publishedDocs.length} docs!`);
|
|
1241
|
+
for (const doc of publishedDocs) {
|
|
1242
|
+
const scheduledBy = doc.data?.sys?.scheduledBy || "system";
|
|
1243
|
+
await this.logAction("doc.scheduled_publish", {
|
|
1244
|
+
by: scheduledBy,
|
|
1245
|
+
metadata: { docId: doc.id }
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1241
1248
|
return publishedDocs;
|
|
1242
1249
|
}
|
|
1243
1250
|
/**
|
|
@@ -1271,6 +1278,14 @@ ${errorMessages}`);
|
|
|
1271
1278
|
batch,
|
|
1272
1279
|
releaseId: release.id
|
|
1273
1280
|
});
|
|
1281
|
+
await this.logAction("release.scheduled_publish", {
|
|
1282
|
+
by: publishedBy,
|
|
1283
|
+
metadata: {
|
|
1284
|
+
releaseId: release.id,
|
|
1285
|
+
docIds: release.docIds || [],
|
|
1286
|
+
dataSourceIds: release.dataSourceIds || []
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1274
1289
|
}
|
|
1275
1290
|
}
|
|
1276
1291
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@blinkk/root-cms",
|
|
4
|
-
version: "2.5.
|
|
4
|
+
version: "2.5.9",
|
|
5
5
|
author: "s@blinkk.com",
|
|
6
6
|
license: "MIT",
|
|
7
7
|
engines: {
|
|
@@ -169,7 +169,7 @@ var package_default = {
|
|
|
169
169
|
yjs: "13.6.27"
|
|
170
170
|
},
|
|
171
171
|
peerDependencies: {
|
|
172
|
-
"@blinkk/root": "2.5.
|
|
172
|
+
"@blinkk/root": "2.5.9",
|
|
173
173
|
"firebase-admin": ">=11",
|
|
174
174
|
"firebase-functions": ">=4",
|
|
175
175
|
preact: ">=10",
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plugin, Request
|
|
1
|
+
import { RootConfig, Plugin, Request } from '@blinkk/root';
|
|
2
2
|
import { App } from 'firebase-admin/app';
|
|
3
3
|
import { Firestore, WriteBatch, Timestamp, Query } from 'firebase-admin/firestore';
|
|
4
4
|
import { C as Collection } from './schema-Bht24XmU.js';
|
|
@@ -8,6 +8,65 @@ import { C as Collection } from './schema-Bht24XmU.js';
|
|
|
8
8
|
*/
|
|
9
9
|
type RootAiModel = 'gemini-3-pro-preview' | 'gemini-3-flash-preview' | 'gemini-2.5-flash' | 'gemini-2.0-pro' | string;
|
|
10
10
|
|
|
11
|
+
/** The result status of a check. */
|
|
12
|
+
type CheckStatus = 'success' | 'warning' | 'error';
|
|
13
|
+
/** Result returned by a check function after execution. */
|
|
14
|
+
interface CheckResult {
|
|
15
|
+
/** Whether the check succeeded, warned, or failed. */
|
|
16
|
+
status: CheckStatus;
|
|
17
|
+
/** A message describing the result. Supports markdown. */
|
|
18
|
+
message: string;
|
|
19
|
+
/** Optional metadata to include with the result. */
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
/** Context passed to a check function during execution. */
|
|
23
|
+
interface CheckContext {
|
|
24
|
+
/** The Root.js config. */
|
|
25
|
+
rootConfig: RootConfig;
|
|
26
|
+
/** The Root CMS client for accessing the database. */
|
|
27
|
+
cmsClient: RootCMSClient;
|
|
28
|
+
/** The document ID, e.g. `Pages/index`. */
|
|
29
|
+
docId: string;
|
|
30
|
+
/** The collection ID, e.g. `Pages`. */
|
|
31
|
+
collectionId: string;
|
|
32
|
+
/** The slug, e.g. `index`. */
|
|
33
|
+
slug: string;
|
|
34
|
+
/** The collection schema. */
|
|
35
|
+
collectionSchema: Collection | null;
|
|
36
|
+
}
|
|
37
|
+
/** Configuration for defining a CMS check. */
|
|
38
|
+
interface CMSCheck {
|
|
39
|
+
/** Unique ID for the check. */
|
|
40
|
+
id: string;
|
|
41
|
+
/** Human-readable label displayed in the UI. */
|
|
42
|
+
label: string;
|
|
43
|
+
/** Optional description explaining what the check does. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional list of collection IDs to restrict this check to. When set, the
|
|
47
|
+
* check is only shown and runnable for documents belonging to one of the
|
|
48
|
+
* listed collections. When omitted, the check applies to all collections.
|
|
49
|
+
*/
|
|
50
|
+
collections?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Function that runs the check on the server-side and returns a result.
|
|
53
|
+
*/
|
|
54
|
+
run: (ctx: CheckContext) => Promise<CheckResult>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TranslationsCheckOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Optional list of collection IDs to restrict this check to. When set, the
|
|
60
|
+
* check is only shown and runnable for documents in these collections.
|
|
61
|
+
*/
|
|
62
|
+
collections?: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A first-party CMS check that verifies all translatable strings in a document
|
|
66
|
+
* have translations for each of the document's enabled locales.
|
|
67
|
+
*/
|
|
68
|
+
declare function translationsCheck(options?: TranslationsCheckOptions): CMSCheck;
|
|
69
|
+
|
|
11
70
|
/**
|
|
12
71
|
* Built-in sidebar tools that can be toggled on/off in the CMS UI.
|
|
13
72
|
*/
|
|
@@ -168,6 +227,30 @@ type CMSPluginOptions = {
|
|
|
168
227
|
*/
|
|
169
228
|
channel: boolean | 'to-preview' | 'from-preview';
|
|
170
229
|
};
|
|
230
|
+
/**
|
|
231
|
+
* Checks that can be run on-demand from the CMS UI to validate document
|
|
232
|
+
* content. Each check defines a server-side function that returns a
|
|
233
|
+
* success/warning/error result with a markdown message.
|
|
234
|
+
*
|
|
235
|
+
* Example:
|
|
236
|
+
* ```ts
|
|
237
|
+
* cmsPlugin({
|
|
238
|
+
* checks: [
|
|
239
|
+
* {
|
|
240
|
+
* id: 'my-check',
|
|
241
|
+
* label: 'My Check',
|
|
242
|
+
* run: async (ctx) => {
|
|
243
|
+
* return {status: 'success', message: 'All good!'};
|
|
244
|
+
* },
|
|
245
|
+
* },
|
|
246
|
+
* ],
|
|
247
|
+
* });
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* NOTE: The checks feature is considered a "beta" feature, its interface
|
|
251
|
+
* may change from version to version as we add new features.
|
|
252
|
+
*/
|
|
253
|
+
checks?: CMSCheck[];
|
|
171
254
|
};
|
|
172
255
|
type CMSPlugin = Plugin & {
|
|
173
256
|
name: 'root-cms';
|
|
@@ -1021,4 +1104,4 @@ declare class BatchResponse {
|
|
|
1021
1104
|
private getTranslationsMap;
|
|
1022
1105
|
}
|
|
1023
1106
|
|
|
1024
|
-
export { type CMSPluginOptions as $, type Action as A, type BatchRequestOptions as B, BatchRequest as C, type DocMode as D, BatchResponse as E, type Locale as F, type GetDocOptions as G, type HttpMethod as H, type SourceString as I, type TranslatedString as J, type TranslationsDocMode as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocHashMap as M, type TranslationsLocaleDocEntry as N, type MultiLocaleTranslationsMap as O, type SingleLocaleTranslationsMap as P, TranslationsManager as Q, RootCMSClient as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsDbPath as V, buildTranslationsLocaleDocDbPath as W, type CMSBuiltInSidebarTool as X, type CMSUser as Y, type CMSAIConfig as Z, type CMSSidebarTool as _, type LocaleTranslations as a, type CMSPlugin as a0, cmsPlugin as a1, type Doc as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type UpdateDraftOptions as g, type ListDocsOptions as h, type GetCountOptions as i, type Translation as j, type Release as k, type ListActionsOptions as l, isRichTextData as m, getCmsPlugin as n, marshalData as o, normalizeData as p, type ArrayObject as q, marshalArray as r, unmarshalArray as s, toArrayObject as t, unmarshalData as u, translationsForLocale as v, parseDocId as w, type BatchRequestQuery as x, type BatchRequestQueryOptions as y, type TranslationsDoc as z };
|
|
1107
|
+
export { type CMSPluginOptions as $, type Action as A, type BatchRequestOptions as B, BatchRequest as C, type DocMode as D, BatchResponse as E, type Locale as F, type GetDocOptions as G, type HttpMethod as H, type SourceString as I, type TranslatedString as J, type TranslationsDocMode as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocHashMap as M, type TranslationsLocaleDocEntry as N, type MultiLocaleTranslationsMap as O, type SingleLocaleTranslationsMap as P, TranslationsManager as Q, RootCMSClient as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsDbPath as V, buildTranslationsLocaleDocDbPath as W, type CMSBuiltInSidebarTool as X, type CMSUser as Y, type CMSAIConfig as Z, type CMSSidebarTool as _, type LocaleTranslations as a, type CMSPlugin as a0, cmsPlugin as a1, type CMSCheck as a2, type CheckResult as a3, type CheckContext as a4, type CheckStatus as a5, translationsCheck as a6, type TranslationsCheckOptions as a7, type Doc as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type UpdateDraftOptions as g, type ListDocsOptions as h, type GetCountOptions as i, type Translation as j, type Release as k, type ListActionsOptions as l, isRichTextData as m, getCmsPlugin as n, marshalData as o, normalizeData as p, type ArrayObject as q, marshalArray as r, unmarshalArray as s, toArrayObject as t, unmarshalData as u, translationsForLocale as v, parseDocId as w, type BatchRequestQuery as x, type BatchRequestQueryOptions as y, type TranslationsDoc as z };
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '@blinkk/root';
|
|
2
2
|
import 'firebase-admin/app';
|
|
3
3
|
import 'firebase-admin/firestore';
|
|
4
|
-
export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, k as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, j as Translation, z as TranslationsDoc, T as TranslationsMap, g as UpdateDraftOptions, U as UserRole, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-
|
|
4
|
+
export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, k as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, j as Translation, z as TranslationsDoc, T as TranslationsMap, g as UpdateDraftOptions, U as UserRole, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-BUnrUs3a.js';
|
|
5
5
|
import './schema-Bht24XmU.js';
|
package/dist/client.js
CHANGED
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RootCMSClient, D as DocMode, L as LoadTranslationsOptions, T as TranslationsMap, a as LocaleTranslations } from './client-
|
|
2
|
-
export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, F as Locale, O as MultiLocaleTranslationsMap, k as Release, f as SaveDraftOptions, S as SetDocOptions, P as SingleLocaleTranslationsMap, I as SourceString, J as TranslatedString, j as Translation, z as TranslationsDoc, K as TranslationsDocMode, N as TranslationsLocaleDocEntry, M as TranslationsLocaleDocHashMap, Q as TranslationsManager, g as UpdateDraftOptions, U as UserRole, V as buildTranslationsDbPath, W as buildTranslationsLocaleDocDbPath, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-
|
|
1
|
+
import { R as RootCMSClient, D as DocMode, L as LoadTranslationsOptions, T as TranslationsMap, a as LocaleTranslations } from './client-BUnrUs3a.js';
|
|
2
|
+
export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, F as Locale, O as MultiLocaleTranslationsMap, k as Release, f as SaveDraftOptions, S as SetDocOptions, P as SingleLocaleTranslationsMap, I as SourceString, J as TranslatedString, j as Translation, z as TranslationsDoc, K as TranslationsDocMode, N as TranslationsLocaleDocEntry, M as TranslationsLocaleDocHashMap, Q as TranslationsManager, g as UpdateDraftOptions, U as UserRole, V as buildTranslationsDbPath, W as buildTranslationsLocaleDocDbPath, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-BUnrUs3a.js';
|
|
3
3
|
import { Request, RootConfig, Response, RouteParams, GetStaticProps, GetStaticPaths } from '@blinkk/root';
|
|
4
4
|
import { Query } from 'firebase-admin/firestore';
|
|
5
5
|
export { s as schema } from './schema-Bht24XmU.js';
|
package/dist/core.js
CHANGED
package/dist/functions.js
CHANGED
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '@blinkk/root';
|
|
2
2
|
import 'firebase-admin/app';
|
|
3
3
|
import 'firebase-admin/firestore';
|
|
4
|
-
export { Z as CMSAIConfig, X as CMSBuiltInSidebarTool, a0 as CMSPlugin, $ as CMSPluginOptions, _ as CMSSidebarTool, Y as CMSUser, a1 as cmsPlugin } from './client-
|
|
4
|
+
export { Z as CMSAIConfig, X as CMSBuiltInSidebarTool, a2 as CMSCheck, a0 as CMSPlugin, $ as CMSPluginOptions, _ as CMSSidebarTool, Y as CMSUser, a4 as CheckContext, a3 as CheckResult, a5 as CheckStatus, a7 as TranslationsCheckOptions, a1 as cmsPlugin, a6 as translationsCheck } from './client-BUnrUs3a.js';
|
|
5
5
|
import './schema-Bht24XmU.js';
|
package/dist/plugin.js
CHANGED
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
} from "./chunk-T5UK2H24.js";
|
|
5
5
|
import {
|
|
6
6
|
getServerVersion
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-SW3UAKP5.js";
|
|
8
8
|
import {
|
|
9
9
|
runCronJobs
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-AFL5KJ4E.js";
|
|
11
11
|
import {
|
|
12
12
|
RootCMSClient,
|
|
13
13
|
parseDocId,
|
|
14
14
|
unmarshalData
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-5N2KLMQH.js";
|
|
16
16
|
import "./chunk-MLKGABMK.js";
|
|
17
17
|
|
|
18
18
|
// core/plugin.ts
|
|
@@ -463,6 +463,57 @@ function api(server, options) {
|
|
|
463
463
|
res.status(500).json({ success: false, error: err.message || "UNKNOWN" });
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
|
+
server.use("/cms/api/check.test", async (req, res) => {
|
|
467
|
+
if (req.method !== "POST" || !String(req.get("content-type")).startsWith("application/json")) {
|
|
468
|
+
res.status(400).json({ success: false, error: "BAD_REQUEST" });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!req.user?.email) {
|
|
472
|
+
res.status(401).json({ success: false, error: "UNAUTHORIZED" });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const reqBody = req.body || {};
|
|
476
|
+
const checkId = String(reqBody.check || "");
|
|
477
|
+
const docId = String(reqBody.docId || "");
|
|
478
|
+
if (!checkId || !docId) {
|
|
479
|
+
res.status(400).json({ success: false, error: "MISSING_REQUIRED_FIELD" });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const checks = options.checks || [];
|
|
483
|
+
const check = checks.find((c) => c.id === checkId);
|
|
484
|
+
if (!check) {
|
|
485
|
+
res.status(404).json({ success: false, error: "CHECK_NOT_FOUND" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const { collection: collectionId, slug } = parseDocId(docId);
|
|
489
|
+
if (check.collections && !check.collections.includes(collectionId)) {
|
|
490
|
+
res.status(400).json({
|
|
491
|
+
success: false,
|
|
492
|
+
error: "CHECK_NOT_APPLICABLE"
|
|
493
|
+
});
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const cmsClient = new RootCMSClient(req.rootConfig);
|
|
497
|
+
let collectionSchema = null;
|
|
498
|
+
try {
|
|
499
|
+
collectionSchema = await getCollectionSchema(req, collectionId);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const result = await check.run({
|
|
504
|
+
rootConfig: req.rootConfig,
|
|
505
|
+
cmsClient,
|
|
506
|
+
docId,
|
|
507
|
+
collectionId,
|
|
508
|
+
slug,
|
|
509
|
+
collectionSchema
|
|
510
|
+
});
|
|
511
|
+
res.status(200).json({ success: true, data: result });
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.error(err.stack || err);
|
|
514
|
+
res.status(500).json({ success: false, error: err.message || "UNKNOWN" });
|
|
515
|
+
}
|
|
516
|
+
});
|
|
466
517
|
}
|
|
467
518
|
|
|
468
519
|
// core/sse.ts
|
|
@@ -498,12 +549,29 @@ function sse(server) {
|
|
|
498
549
|
{ serverVersion: getServerVersion() }
|
|
499
550
|
);
|
|
500
551
|
res.write(connectedMessage);
|
|
501
|
-
|
|
552
|
+
const heartbeatInterval = setInterval(() => {
|
|
553
|
+
try {
|
|
554
|
+
res.write(":heartbeat\n\n");
|
|
555
|
+
} catch {
|
|
556
|
+
clearInterval(heartbeatInterval);
|
|
557
|
+
}
|
|
558
|
+
}, 3e4);
|
|
559
|
+
const maxConnectionTimer = setTimeout(() => {
|
|
560
|
+
clearInterval(heartbeatInterval);
|
|
561
|
+
try {
|
|
562
|
+
res.write("retry: 1000\n\n");
|
|
563
|
+
res.end();
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
502
566
|
sseClients.delete(res);
|
|
503
|
-
});
|
|
504
|
-
|
|
567
|
+
}, 9 * 6e4);
|
|
568
|
+
const cleanup = () => {
|
|
569
|
+
clearInterval(heartbeatInterval);
|
|
570
|
+
clearTimeout(maxConnectionTimer);
|
|
505
571
|
sseClients.delete(res);
|
|
506
|
-
}
|
|
572
|
+
};
|
|
573
|
+
req.on("close", cleanup);
|
|
574
|
+
req.on("aborted", cleanup);
|
|
507
575
|
});
|
|
508
576
|
return { sseBroadcast };
|
|
509
577
|
}
|
|
@@ -516,6 +584,232 @@ function formatMessage(event, data) {
|
|
|
516
584
|
return message;
|
|
517
585
|
}
|
|
518
586
|
|
|
587
|
+
// core/checks-translations.ts
|
|
588
|
+
function extractTranslatableStrings(fields, data, types) {
|
|
589
|
+
const strings = /* @__PURE__ */ new Set();
|
|
590
|
+
function addString(text) {
|
|
591
|
+
const normalized = (text || "").trim();
|
|
592
|
+
if (normalized) {
|
|
593
|
+
strings.add(normalized);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function walkFields(fieldDefs, fieldData) {
|
|
597
|
+
for (const field of fieldDefs) {
|
|
598
|
+
if (!field.id) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const value = fieldData[field.id];
|
|
602
|
+
if (!value) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const metadataKey = `@${field.id}`;
|
|
606
|
+
const metadata = fieldData[metadataKey];
|
|
607
|
+
if (metadata?.disableTranslations) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
walkField(field, value);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function walkField(field, value) {
|
|
614
|
+
if (!value) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (field.type === "string" || field.type === "select") {
|
|
618
|
+
if (field.translate) {
|
|
619
|
+
addString(value);
|
|
620
|
+
}
|
|
621
|
+
} else if (field.type === "multiselect") {
|
|
622
|
+
if (field.translate && Array.isArray(value)) {
|
|
623
|
+
for (const item of value) {
|
|
624
|
+
addString(item);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (field.type === "image") {
|
|
628
|
+
if (field.translate && value.alt && field.alt !== false) {
|
|
629
|
+
addString(value.alt);
|
|
630
|
+
}
|
|
631
|
+
} else if (field.type === "richtext") {
|
|
632
|
+
if (field.translate) {
|
|
633
|
+
extractRichTextStrings(strings, value);
|
|
634
|
+
}
|
|
635
|
+
} else if (field.type === "object") {
|
|
636
|
+
walkFields(field.fields || [], value);
|
|
637
|
+
} else if (field.type === "array") {
|
|
638
|
+
const arrayKeys = value._array || [];
|
|
639
|
+
for (const arrayKey of arrayKeys) {
|
|
640
|
+
if (value[arrayKey]) {
|
|
641
|
+
walkField(field.of, value[arrayKey]);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} else if (field.type === "oneof") {
|
|
645
|
+
const fieldTypes = field.types || [];
|
|
646
|
+
let fieldValueType;
|
|
647
|
+
if (Array.isArray(fieldTypes) && typeof fieldTypes[0] === "string") {
|
|
648
|
+
if (fieldTypes.includes(value._type)) {
|
|
649
|
+
fieldValueType = types[value._type];
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
fieldValueType = fieldTypes.find(
|
|
653
|
+
(item) => item.name === value._type
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (fieldValueType) {
|
|
657
|
+
walkFields(fieldValueType.fields || [], value);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
walkFields(fields, data);
|
|
662
|
+
return strings;
|
|
663
|
+
}
|
|
664
|
+
function extractRichTextStrings(strings, data) {
|
|
665
|
+
if (!data || !data.root || !data.root.children) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
for (const block of data.root.children) {
|
|
669
|
+
extractRichTextBlock(strings, block);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function extractRichTextBlock(strings, block) {
|
|
673
|
+
if (!block) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (block.type === "paragraph" || block.type === "heading") {
|
|
677
|
+
const text = extractRichTextNodeText(block);
|
|
678
|
+
if (text.trim()) {
|
|
679
|
+
strings.add(text.trim());
|
|
680
|
+
}
|
|
681
|
+
} else if (block.type === "list") {
|
|
682
|
+
for (const item of block.children || []) {
|
|
683
|
+
extractRichTextBlock(strings, item);
|
|
684
|
+
}
|
|
685
|
+
} else if (block.type === "listitem") {
|
|
686
|
+
const text = extractRichTextNodeText(block);
|
|
687
|
+
if (text.trim()) {
|
|
688
|
+
strings.add(text.trim());
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function extractRichTextNodeText(node) {
|
|
693
|
+
if (!node) {
|
|
694
|
+
return "";
|
|
695
|
+
}
|
|
696
|
+
if (node.type === "text") {
|
|
697
|
+
return node.text || "";
|
|
698
|
+
}
|
|
699
|
+
if (node.children) {
|
|
700
|
+
return node.children.map(extractRichTextNodeText).join("");
|
|
701
|
+
}
|
|
702
|
+
return "";
|
|
703
|
+
}
|
|
704
|
+
function translationsCheck(options) {
|
|
705
|
+
return {
|
|
706
|
+
id: "root-cms/translations",
|
|
707
|
+
label: "Missing Translations",
|
|
708
|
+
description: "Checks that all translatable strings have translations for each enabled locale.",
|
|
709
|
+
collections: options?.collections,
|
|
710
|
+
run: async (ctx) => {
|
|
711
|
+
const { cmsClient, collectionId, slug, collectionSchema } = ctx;
|
|
712
|
+
const rawDoc = await cmsClient.getRawDoc(collectionId, slug, {
|
|
713
|
+
mode: "draft"
|
|
714
|
+
});
|
|
715
|
+
if (!rawDoc) {
|
|
716
|
+
return {
|
|
717
|
+
status: "error",
|
|
718
|
+
message: "Document not found."
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (!collectionSchema) {
|
|
722
|
+
return {
|
|
723
|
+
status: "warning",
|
|
724
|
+
message: "Could not load collection schema. Unable to check translations."
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const fields = collectionSchema.fields || [];
|
|
728
|
+
const schemaTypes = collectionSchema.types || {};
|
|
729
|
+
const docFields = rawDoc.fields || {};
|
|
730
|
+
const sourceStrings = extractTranslatableStrings(
|
|
731
|
+
fields,
|
|
732
|
+
docFields,
|
|
733
|
+
schemaTypes
|
|
734
|
+
);
|
|
735
|
+
if (sourceStrings.size === 0) {
|
|
736
|
+
return {
|
|
737
|
+
status: "success",
|
|
738
|
+
message: "No translatable strings found in this document."
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const locales = (rawDoc.sys?.locales || ["en"]).filter(
|
|
742
|
+
(l) => l !== "en"
|
|
743
|
+
);
|
|
744
|
+
if (locales.length === 0) {
|
|
745
|
+
return {
|
|
746
|
+
status: "success",
|
|
747
|
+
message: `Found **${sourceStrings.size}** translatable string(s), but no non-default locales are enabled.`
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const docId = `${collectionId}/${slug}`;
|
|
751
|
+
const translationsMap = await cmsClient.loadTranslations({
|
|
752
|
+
tags: [docId]
|
|
753
|
+
});
|
|
754
|
+
const sourceToTranslations = /* @__PURE__ */ new Map();
|
|
755
|
+
for (const [, translation] of Object.entries(translationsMap)) {
|
|
756
|
+
const source = (translation.source || "").trim();
|
|
757
|
+
if (source) {
|
|
758
|
+
const localeMap = {};
|
|
759
|
+
for (const [key, value] of Object.entries(translation)) {
|
|
760
|
+
if (key !== "source" && key !== "tags" && typeof value === "string") {
|
|
761
|
+
localeMap[key] = value;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
sourceToTranslations.set(source, localeMap);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const missingByLocale = {};
|
|
768
|
+
let totalMissing = 0;
|
|
769
|
+
for (const source of sourceStrings) {
|
|
770
|
+
const translations = sourceToTranslations.get(source);
|
|
771
|
+
for (const locale of locales) {
|
|
772
|
+
if (!translations || !translations[locale]) {
|
|
773
|
+
if (!missingByLocale[locale]) {
|
|
774
|
+
missingByLocale[locale] = [];
|
|
775
|
+
}
|
|
776
|
+
missingByLocale[locale].push(source);
|
|
777
|
+
totalMissing++;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (totalMissing === 0) {
|
|
782
|
+
return {
|
|
783
|
+
status: "success",
|
|
784
|
+
message: `All **${sourceStrings.size}** translatable string(s) have translations for ${locales.length} locale(s).`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const lines = [];
|
|
788
|
+
lines.push(
|
|
789
|
+
`Found **${totalMissing}** missing translation(s) across **${Object.keys(missingByLocale).length}** locale(s):
|
|
790
|
+
`
|
|
791
|
+
);
|
|
792
|
+
for (const [locale, missing] of Object.entries(missingByLocale)) {
|
|
793
|
+
lines.push(`**${locale}** \u2014 ${missing.length} missing:`);
|
|
794
|
+
const shown = missing.slice(0, 5);
|
|
795
|
+
for (const str of shown) {
|
|
796
|
+
const truncated = str.length > 80 ? str.substring(0, 80) + "..." : str;
|
|
797
|
+
lines.push(`- \`${truncated}\``);
|
|
798
|
+
}
|
|
799
|
+
if (missing.length > 5) {
|
|
800
|
+
lines.push(`- ...and ${missing.length - 5} more`);
|
|
801
|
+
}
|
|
802
|
+
lines.push("");
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
status: totalMissing > 0 ? "warning" : "success",
|
|
806
|
+
message: lines.join("\n"),
|
|
807
|
+
metadata: { totalMissing, missingByLocale }
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
519
813
|
// core/plugin.ts
|
|
520
814
|
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
521
815
|
async function writeCollectionSchemasToJson(rootConfig) {
|
|
@@ -765,6 +1059,19 @@ function cmsPlugin(options) {
|
|
|
765
1059
|
*/
|
|
766
1060
|
configureServer: async (server, serverOptions) => {
|
|
767
1061
|
server.use(bodyParser.json());
|
|
1062
|
+
server.use(
|
|
1063
|
+
(err, req, res, next) => {
|
|
1064
|
+
if (err.type === "entity.too.large" || err.status === 413) {
|
|
1065
|
+
res.status(413).json({ error: "Payload too large" });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (err.type === "entity.parse.failed" || err.status === 400) {
|
|
1069
|
+
res.status(400).json({ error: "Bad request" });
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
next(err);
|
|
1073
|
+
}
|
|
1074
|
+
);
|
|
768
1075
|
async function getRenderer(req) {
|
|
769
1076
|
if (serverOptions.type === "dev") {
|
|
770
1077
|
const appFilePath = path2.resolve(__dirname, "./app.js");
|
|
@@ -854,7 +1161,7 @@ function cmsPlugin(options) {
|
|
|
854
1161
|
if (sseData.sseBroadcast) {
|
|
855
1162
|
sseBroadcast = sseData.sseBroadcast;
|
|
856
1163
|
}
|
|
857
|
-
api(server, { getRenderer });
|
|
1164
|
+
api(server, { getRenderer, checks: options.checks });
|
|
858
1165
|
server.use("/cms", async (req, res) => {
|
|
859
1166
|
try {
|
|
860
1167
|
if (!req.user) {
|
|
@@ -911,5 +1218,6 @@ function fileExists(filepath) {
|
|
|
911
1218
|
return fs2.access(filepath).then(() => true).catch(() => false);
|
|
912
1219
|
}
|
|
913
1220
|
export {
|
|
914
|
-
cmsPlugin
|
|
1221
|
+
cmsPlugin,
|
|
1222
|
+
translationsCheck
|
|
915
1223
|
};
|