@blinkk/root-cms 2.4.8 → 2.4.10

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/plugin.js CHANGED
@@ -1,2315 +1,52 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __esm = (fn, res) => function __init() {
8
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
- };
10
- var __commonJS = (cb, mod) => function __require() {
11
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
- };
13
- var __export = (target, all) => {
14
- for (var name in all)
15
- __defProp(target, name, { get: all[name], enumerable: true });
16
- };
17
- var __copyProps = (to, from, except, desc) => {
18
- if (from && typeof from === "object" || typeof from === "function") {
19
- for (let key of __getOwnPropNames(from))
20
- if (!__hasOwnProp.call(to, key) && key !== except)
21
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
- }
23
- return to;
24
- };
25
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
- // If the importer is in node compatibility mode or this is not an ESM
27
- // file that has been converted to a CommonJS file using a Babel-
28
- // compatible transform (i.e. "__esModule" has not been set), then set
29
- // "default" to the CommonJS "module.exports" for node compatibility.
30
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
- mod
32
- ));
33
-
34
- // shared/ai/prompts/edit.txt
35
- var require_edit = __commonJS({
36
- "shared/ai/prompts/edit.txt"(exports, module) {
37
- module.exports = 'Role: You are a highly accurate AI assistant specializing in converting unstructured documents into JSON files compatible with Root CMS. Root CMS is a web-based CMS for building websites and landing pages, storing its content as JSON files in a Firebase Firestore database.\n\nObjective: To edit and/or generate highly accurate JSON files compatible with Root CMS, based on user input (screenshots, chat messages, etc.) and, when editing JSON files, the original JSON file you are editing. The resulting content should contain all of the marketing content provided by the user input (and none of the annotations or superfluous content). The JSON files should be perfectly compatible with Root CMS.\n\nInput Requirements & Handling:\n\n- You will receive unstructured documents and you must convert them into JSON files compatible with Root CMS by either editing the existing JSON files or generating new ones from scratch. The JSON files you produce will typically represent a single section of a page of the website, such as a hero section, a feature section, or a footer.\n\n- When editing an existing JSON file, it will be annotated with a key `_type` that indicates the type of section it is, such as `Hero`, `Feature`, or `Footer`. You must ensure that the resulting JSON file is compatible with Root CMS and contains all of the marketing content provided by the user input. You will also be given a TypeScript interface in `root-cms.d.ts` that describes the structure of the JSON file you are editing. You must ensure that the resulting JSON file is compatible with this interface.\n\n- The input you receive will be broken into three parts. (1) `root-cms-d.ts` (the TypeScript interface), (2) the existing JSON file you are editing (or an empty JSON file when generating new content), and (3) the user input (screenshots, chat messages, instructions, etc.). You must ensure that the resulting JSON file is compatible with the structure defined by the TypeScript interface and contains all of the marketing content provided by the user input.\n\n- When you provide your response, if the user instructs you to only edit one part of the original JSON, make sure to leave the rest of the JSON file untouched. You must return the entire JSON document after applying changes. Never omit, rename, reorder or invent top-level keys. Copy all unmodified fields exactly as they appear in the input.\n\nRoot CMS JSON File Structure:\n\n- Root CMS JSON files are standard JSON files described by the TypeScript interface in `root-cms.d.ts`. However, there is one critical syntax rule that you must understand and always follow regarding array values. Array values are expressed as hash maps. The order of the resulting data when Root CMS unmarshals the data is provided by a special key called `_array`. For example, to represent an array [1, 2, 3] in the CMS, the JSON data may look like this. You may generate your own UIDs for the keys and the items within `_array`.\n\n```json\n{\n "_array": ["2HGwoV", "LjFHWk", "OLakGA", "UaGHJh"],\n "2HGwoV": 1,\n "LjFHWk": 2,\n "OLakGA": 3,\n "UaGHJh": 4\n}\n```\n\nWhen creating arrays in your JSON output, you must _never_ forget to add an `_array` key. If you forget to add an `_array` key, the output will NOT work with Root CMS. When adding new array items, you can create your own array keys.\n\n2. Images and file fields have system-provided metadata, that looks like this example:\n\n```\n{\n "src": "https://storage.googleapis.com/rootjs-dev.appspot.com/www/uploads/2cad5d65bb7690bad5dcd4a041cf2fef46361678.svg",\n "filename": "600x400.svg",\n "uploadedAt": "1754023634564",\n "alt": "",\n "width": "600",\n "uploadedBy": "user@example.com",\n "height": "400"\n}\n```\n\n3. Rich Text fields are stored as Editor.js documents, which look like this. You may generate your own UIDs for the `id` fields.\n\n```\n{\n "version": "2.28.2",\n "time": 1754024041637,\n "blocks": [\n {\n "type": "paragraph",\n "data": {\n "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <a href=\\"https://example.com\\">This accepts HTML</a>."\n },\n "id": "_8PEf3kBx2"\n },\n {\n "data": {\n "style": "unordered",\n "items": [\n {\n "content": "This is list item 1.",\n "items": []\n },\n {\n "items": [],\n "content": "This is list item 2."\n }\n ]\n },\n "id": "G5zMDP1ytM",\n "type": "unorderedList"\n }\n ]\n}\n```\n\n4. Root CMS supports "oneOf" fields, where values can be "one of" another schema. These are typically used as array values or object values, where the user can from one of other schema types to structure the data value. For example, each module in a page\'s module list is "one of" the modules available in the library. One of values have a system-provided `_type` key that describes the type of the value. For example, this is a hero module, whose type is "TemplateHeadline", one of the available templates from the module library:\n\n```\n{\n "eyebrow": "Lorem ipsum",\n "_type": "TemplateHeadline",\n "body": {\n "time": 1754023554170,\n "version": "2.28.2",\n "blocks": [\n {\n "type": "paragraph",\n "data": {\n "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."\n },\n "id": "IVmxEVsxmt"\n },\n {\n "id": "aMiHiywkQD",\n "type": "paragraph",\n "data": {\n "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."\n }\n }\n ]\n },\n "title": "Welcome to our website"\n}\n```\n\nWhen adding new items to arrays that use `oneOf` fields, try your best to figure out which content type would best satisfy the user\'s request. You must never _invent_ new content types, for example, if "TemplateFoo" doesn\'t exist in `root-cms.d.ts`, you must never add a value with `{"_type": "TemplateFoo"}`. If none of the available content types within `root-cms.d.ts` satisfy the user\'s request, avoid creating the new array item and inform the user that there is no existing template compatible.\n\nAdditionally, when adding new items to existing JSON data, you must carefully check the structure of those items as defined by `root-cms.d.ts`. Avoid inventing new fields for content types, use only those defined by the `root-cms.d.ts`. For example, if you are adding a button to a module, carefully check the button structure defined in `root-cms.d.ts` and only populate fields that are defined there.\n\nFinally, when you provide your response, it MUST be structured in the following way for handling by the chat client.\n\n{\n "data": <The JSON data generated by you via the instructions above>,\n "message": <A brief message that describes what you did.>\n}\n\nYou must always ONLY reply in this exact format. The brief message that describes what you did should be written succinctly, tersely, in a friendly but not cute tone, and be no more than a sentence or two.\n';
38
- }
39
- });
40
-
41
- // shared/ai/prompts/altText.txt
42
- var require_altText = __commonJS({
43
- "shared/ai/prompts/altText.txt"(exports, module) {
44
- module.exports = "Create a descriptive and concise alt text for the attached image.\n\n- The alt text should be a brief but comprehensive description of the image, including key subjects, the setting, and any relevant details or actions.\n- The alt text should not exceed 125 characters.\n- Only provide one generation, and include only that data in the response. No surrounding text, clarifications, etc. Just the alt text.\n";
45
- }
46
- });
47
-
48
- // core/ai.ts
49
- var ai_exports = {};
50
- __export(ai_exports, {
51
- Chat: () => Chat,
52
- ChatClient: () => ChatClient,
53
- generateImage: () => generateImage,
54
- summarizeDiff: () => summarizeDiff
55
- });
56
- import crypto from "crypto";
57
- import fs from "fs";
58
- import path from "path";
59
- import { vertexAI } from "@genkit-ai/google-genai";
60
- import { Timestamp } from "firebase-admin/firestore";
61
- import { genkit } from "genkit";
62
- import { logger } from "genkit/logging";
63
- async function summarizeDiff(cmsClient, options) {
64
- const cmsPluginOptions = cmsClient.cmsPlugin.getConfig();
65
- const firebaseConfig = cmsPluginOptions.firebaseConfig;
66
- const model = (typeof cmsPluginOptions.experiments?.ai === "object" ? cmsPluginOptions.experiments.ai.model : void 0) || DEFAULT_MODEL;
67
- const ai = genkit({
68
- plugins: [
69
- vertexAI({
70
- projectId: firebaseConfig.projectId,
71
- location: firebaseConfig.location || "us-central1"
72
- })
73
- ]
74
- });
75
- const beforeJson = JSON.stringify(options.before ?? null, null, 2);
76
- const afterJson = JSON.stringify(options.after ?? null, null, 2);
77
- const systemPrompt = [
78
- "You are an assistant that summarizes changes made to CMS documents stored as JSON.",
79
- "Provide a concise description of the most important updates using short bullet points.",
80
- 'If there are no meaningful differences, respond with "No significant changes."',
81
- `Focus on just the content changes, ignore insignificant changes to richtext blocks and structure, such as updates to the richtext block's "timestamp" and "version" fields.`
82
- ].join("\n");
83
- const diffPrompt = [
84
- "Previous version JSON:",
85
- "```json",
86
- beforeJson,
87
- "```",
88
- "",
89
- "Updated version JSON:",
90
- "```json",
91
- afterJson,
92
- "```",
93
- "",
94
- "Summarize the differences between the two payloads."
95
- ].join("\n");
96
- const res = await ai.generate({
97
- model: vertexAI.model(model),
98
- messages: [
99
- {
100
- role: "system",
101
- content: [{ text: systemPrompt }]
102
- }
103
- ],
104
- prompt: [{ text: diffPrompt }]
105
- });
106
- return res.text?.trim() || "";
107
- }
108
- async function generateImage(cmsClient, options) {
109
- const cmsPluginOptions = cmsClient.cmsPlugin.getConfig();
110
- const firebaseConfig = cmsPluginOptions.firebaseConfig;
111
- const model = options.model || DEFAULT_IMAGEGEN_MODEL;
112
- const ai = genkit({
113
- plugins: [
114
- vertexAI({
115
- projectId: firebaseConfig.projectId,
116
- location: firebaseConfig.location || "us-central1"
117
- })
118
- ]
119
- });
120
- const res = await ai.generate({
121
- model: vertexAI.model(model),
122
- prompt: [
123
- {
124
- text: options.prompt
125
- }
126
- ],
127
- config: {
128
- imageConfig: {
129
- aspectRatio: options.aspectRatio
130
- }
131
- },
132
- output: {
133
- format: "media"
134
- }
135
- });
136
- if (!res.media) {
137
- throw new Error("No image generated");
138
- }
139
- return res.media.url;
140
- }
141
- function cleanModelName(model) {
142
- return LEGACY_MODEL_RENAME[model] || model;
143
- }
144
- var DEFAULT_MODEL, DEFAULT_IMAGEGEN_MODEL, LEGACY_MODEL_RENAME, Chat, ChatClient;
145
- var init_ai = __esm({
146
- "core/ai.ts"() {
147
- "use strict";
148
- logger.setLogLevel("warn");
149
- DEFAULT_MODEL = "gemini-3-flash-preview";
150
- DEFAULT_IMAGEGEN_MODEL = "gemini-2.5-flash-image";
151
- LEGACY_MODEL_RENAME = {
152
- "vertexai/gemini-2.5-flash": "gemini-2.5-flash",
153
- "vertexai/gemini-2.0-pro": "gemini-2.0-pro"
154
- };
155
- Chat = class {
156
- constructor(chatClient, id, options) {
157
- this.chatClient = chatClient;
158
- this.cmsClient = chatClient.cmsClient;
159
- this.cmsPluginOptions = this.cmsClient.cmsPlugin.getConfig();
160
- this.id = id;
161
- this.history = options?.history ?? [];
162
- this.model = cleanModelName(
163
- options?.model || (typeof this.cmsPluginOptions.experiments?.ai === "object" ? this.cmsPluginOptions.experiments.ai.model : void 0) || DEFAULT_MODEL
164
- );
165
- const firebaseConfig = this.cmsPluginOptions.firebaseConfig;
166
- this.ai = genkit({
167
- plugins: [
168
- vertexAI({
169
- projectId: firebaseConfig.projectId,
170
- location: firebaseConfig.location || "us-central1"
171
- })
172
- ]
173
- });
174
- }
175
- /** Builds the messages for the AI request. */
176
- async buildMessages(options) {
177
- const messages = this.history;
178
- const hasSystemPrompt = messages.some(
179
- (msg) => msg.role === "system" && msg.content.length > 0
180
- );
181
- if (!hasSystemPrompt) {
182
- messages.push({
183
- role: "system",
184
- content: [{ text: await this.buildSystemPrompt(options) }]
185
- });
186
- }
187
- if (options.mode === "edit") {
188
- messages.push({
189
- role: "user",
190
- content: [
191
- {
192
- text: [
193
- "The JSON you must edit is:",
194
- "",
195
- JSON.stringify(options.editData || {}, null, 2)
196
- ].join("")
197
- }
198
- ]
199
- });
200
- }
201
- return messages;
202
- }
203
- /** Builds the request sent to the AI based on the `ChatMode`. */
204
- async buildGenerateRequest(prompt, options) {
205
- if (options.mode === "edit") {
206
- return {
207
- messages: await this.buildMessages(options),
208
- model: this.model,
209
- prompt
210
- };
211
- }
212
- return {
213
- messages: await this.buildMessages(options),
214
- model: this.model,
215
- prompt
216
- };
217
- }
218
- /** Sends the request to the AI and stores the history in the session and the database. */
219
- async sendPrompt(prompt, options = {}) {
220
- const chatRequest = await this.buildGenerateRequest(prompt, options);
221
- const res = await this.ai.generate({
222
- model: vertexAI.model(chatRequest.model),
223
- messages: chatRequest.messages,
224
- prompt: Array.isArray(prompt) ? prompt.flat() : prompt,
225
- // NOTE(stevenle): the global location is required for
226
- // `gemini-3-flash-preview`, it may be possible to remove this in the
227
- // future.
228
- config: { location: "global" }
229
- });
230
- this.history = res.messages;
231
- await this.dbDoc().update({
232
- history: this.history,
233
- modifiedAt: Timestamp.now()
234
- });
235
- if (options.mode === "edit") {
236
- const aiResponse = res.output;
237
- if (aiResponse?.data && !aiResponse.message) {
238
- aiResponse.message = "";
239
- }
240
- return aiResponse;
241
- }
242
- return { message: res.text, data: null };
243
- }
244
- dbDoc() {
245
- return this.chatClient.dbCollection().doc(this.id);
246
- }
247
- /**
248
- * Builds the system prompt sent to the AI, based on the `ChatMode` and
249
- * supplied `SendPromptOptions`. `SendPromptOptions` may contain data or
250
- * references to information needed to construct the prompt.
251
- */
252
- async buildSystemPrompt(options) {
253
- const serializedRootConfig = JSON.stringify(
254
- this.cmsClient.rootConfig,
255
- null,
256
- 2
257
- );
258
- if (options.mode === "edit") {
259
- const rootDir = process.cwd();
260
- const rootCmsDefsPath = path.resolve(rootDir, "root-cms.d.ts");
261
- const rootCmsDefs = fs.existsSync(rootCmsDefsPath) ? fs.readFileSync(rootCmsDefsPath, {
262
- encoding: "utf8"
263
- }) : null;
264
- const text = [(await Promise.resolve().then(() => __toESM(require_edit(), 1))).default];
265
- if (rootCmsDefs) {
266
- text.push(
267
- "Here is the `root-cms.d.ts` file for this project:",
268
- "```",
269
- rootCmsDefs,
270
- "```"
271
- );
272
- }
273
- return text.join("\n");
274
- }
275
- if (options.mode === "altText") {
276
- return (await Promise.resolve().then(() => __toESM(require_altText(), 1))).default;
277
- }
278
- const systemText = [
279
- `You are an assistant for a headless CMS called Root CMS which is used on a website called ${this.cmsPluginOptions.name || this.cmsPluginOptions.id}. Your job is to answer questions about the docs in the system, and if requested, help suggest changes to the JSON data in the docs. If you don't know the answer, just say that you don't know, don't try to make up an answer. Be friendly and playful with your messaging.`,
280
- "",
281
- "Here is the root.config.ts file for the site:",
282
- "```",
283
- serializedRootConfig,
284
- "```",
285
- "",
286
- "Here are the docs that exist in the system:"
287
- ];
288
- const pages = await this.cmsClient.listDocs("Pages", { mode: "draft" });
289
- pages.docs.forEach((doc) => {
290
- systemText.push(JSON.stringify(doc));
291
- });
292
- return systemText.join("\n");
293
- }
294
- };
295
- ChatClient = class {
296
- constructor(cmsClient, user) {
297
- this.cmsClient = cmsClient;
298
- this.user = user;
299
- }
300
- async createChat() {
301
- const chatId = crypto.randomUUID();
302
- const docRef = this.dbCollection().doc(chatId);
303
- const chat = new Chat(this, chatId);
304
- await docRef.set({
305
- id: chatId,
306
- createdBy: this.user,
307
- createdAt: Timestamp.now(),
308
- modifiedAt: Timestamp.now(),
309
- model: chat.model
310
- });
311
- return chat;
312
- }
313
- async getOrCreateChat(chatId) {
314
- return chatId ? this.getChat(chatId) : this.createChat();
315
- }
316
- async getChat(chatId) {
317
- const docRef = this.dbCollection().doc(chatId);
318
- const chatDoc = await docRef.get();
319
- if (!chatDoc.exists) {
320
- throw new Error(`${chatId} does not exist`);
321
- }
322
- const chatData = chatDoc.data() || {};
323
- return new Chat(this, chatId, {
324
- history: chatData.history,
325
- model: chatData.model
326
- });
327
- }
328
- async listChats(options) {
329
- const limit = options?.limit || 20;
330
- const query = this.dbCollection().where("createdBy", "==", this.user).limit(limit).orderBy("createdAt", "desc");
331
- const res = await query.get();
332
- return res.docs.map((doc) => doc.data());
333
- }
334
- dbCollection() {
335
- return this.cmsClient.db.collection(
336
- `Projects/${this.cmsClient.projectId}/Experiments/ai/Chat`
337
- );
338
- }
339
- };
340
- }
341
- });
342
-
343
- // cli/generate-types.ts
344
- var generate_types_exports = {};
345
- __export(generate_types_exports, {
346
- generateSchemaDts: () => generateSchemaDts,
347
- generateTypes: () => generateTypes
348
- });
349
- import { promises as fs3 } from "fs";
350
- import path4 from "path";
351
- import { fileURLToPath } from "url";
352
- import { loadRootConfig, viteSsrLoadModule } from "@blinkk/root/node";
353
- import * as dom from "dts-dom";
354
- async function generateTypes() {
355
- const rootDir = process.cwd();
356
- const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
357
- const modulePath = path4.resolve(__dirname, "./project.js");
358
- const project = await viteSsrLoadModule(
359
- rootConfig,
360
- modulePath
361
- );
362
- const schemas = await project.getProjectSchemas();
363
- const outputPath = path4.resolve(rootDir, "root-cms.d.ts");
364
- await generateSchemaDts(outputPath, schemas);
365
- console.log("saved root-cms.d.ts!");
366
- }
367
- async function generateSchemaDts(outputPath, schemas) {
368
- const dtsFormatter = new DtsFormatter();
369
- for (const fileId in schemas) {
370
- const schema = schemas[fileId];
371
- dtsFormatter.addSchemaFile(fileId, schema);
372
- }
373
- const output = dtsFormatter.toString();
374
- await fs3.writeFile(outputPath, output, "utf-8");
375
- }
376
- function fieldProperty(field, options) {
377
- const prop = dom.create.property(
378
- field.id,
379
- fieldType(field, options),
380
- dom.DeclarationFlags.Optional
381
- );
382
- const jsdoc = [];
383
- if (field.label) {
384
- if (field.help) {
385
- jsdoc.push(`${field.label}. ${field.help}`);
386
- } else {
387
- jsdoc.push(field.label);
388
- }
389
- }
390
- if (field.deprecated) {
391
- jsdoc.push("@deprecated");
392
- }
393
- if (jsdoc.length > 0) {
394
- prop.jsDocComment = jsdoc.join("\n");
395
- }
396
- return prop;
397
- }
398
- function fieldType(field, options) {
399
- if (field.type === "array") {
400
- return dom.type.array(fieldType(field.of, options));
401
- }
402
- if (field.type === "boolean") {
403
- return dom.type.boolean;
404
- }
405
- if (field.type === "date") {
406
- return dom.type.string;
407
- }
408
- if (field.type === "datetime") {
409
- return dom.type.number;
410
- }
411
- if (field.type === "file") {
412
- const fileType = dom.create.namedTypeReference("RootCMSFile");
413
- return fileType;
414
- }
415
- if (field.type === "image") {
416
- const imageType = dom.create.namedTypeReference("RootCMSImage");
417
- return imageType;
418
- }
419
- if (field.type === "multiselect") {
420
- return dom.type.array(dom.type.string);
421
- }
422
- if (field.type === "oneof") {
423
- const oneOf = dom.create.namedTypeReference("RootCMSOneOf");
424
- if (field.types && Array.isArray(field.types)) {
425
- const unionTypes = [];
426
- field.types.forEach((schema) => {
427
- let typeName;
428
- if (typeof schema === "string") {
429
- typeName = schema;
430
- return;
431
- } else {
432
- typeName = schema.name;
433
- }
434
- if (!typeName) {
435
- return;
436
- }
437
- const cleanName = alphanumeric(typeName);
438
- const oneOfTypeId = `${cleanName}Fields`;
439
- if (typeof schema === "object" && !options.oneOfTypes[oneOfTypeId]) {
440
- const oneOfTypeInterface = dom.create.interface(
441
- oneOfTypeId,
442
- dom.DeclarationFlags.Export
443
- );
444
- if (schema.description) {
445
- oneOfTypeInterface.jsDocComment = schema.description;
446
- }
447
- const oneOfTypeFields = schema.fields || [];
448
- oneOfTypeFields.forEach((f) => {
449
- oneOfTypeInterface.members.push(fieldProperty(f, options));
450
- });
451
- options.oneOfTypes[oneOfTypeId] = oneOfTypeInterface;
452
- }
453
- const oneOfOption = dom.create.namedTypeReference("RootCMSOneOfOption");
454
- oneOfOption.typeArguments = [
455
- dom.type.stringLiteral(typeName),
456
- dom.create.namedTypeReference(oneOfTypeId)
457
- ];
458
- unionTypes.push(oneOfOption);
459
- });
460
- if (unionTypes.length > 0) {
461
- oneOf.typeArguments = [dom.create.union(unionTypes)];
462
- }
463
- }
464
- return oneOf;
465
- }
466
- if (field.type === "reference") {
467
- const referenceType = dom.create.namedTypeReference("RootCMSReference");
468
- return referenceType;
469
- }
470
- if (field.type === "references") {
471
- const referenceType = dom.create.namedTypeReference("RootCMSReference");
472
- return dom.type.array(referenceType);
473
- }
474
- if (field.type === "richtext") {
475
- const richtextType = dom.create.namedTypeReference("RootCMSRichText");
476
- return richtextType;
477
- }
478
- if (field.type === "select") {
479
- return dom.type.string;
480
- }
481
- if (field.type === "string") {
482
- return dom.type.string;
483
- }
484
- if (field.type === "object") {
485
- const subproperties = (field.fields || []).map(
486
- (f) => fieldProperty(f, options)
487
- );
488
- return dom.create.objectType(subproperties);
489
- }
490
- return dom.type.unknown;
491
- }
492
- function alphanumeric(input) {
493
- return input.replace(/[^a-zA-Z0-9]/g, "");
494
- }
495
- var __dirname, TEMPLATE, DtsFormatter;
496
- var init_generate_types = __esm({
497
- "cli/generate-types.ts"() {
498
- "use strict";
499
- __dirname = path4.dirname(fileURLToPath(import.meta.url));
500
- TEMPLATE = `/* eslint-disable */
501
- /** Root.js CMS types. This file is autogenerated. */
502
-
503
- export interface RootCMSFile {
504
- src: string;
505
- width?: number;
506
- height?: number;
507
- alt?: string;
508
- }
509
-
510
- export type RootCMSImage = RootCMSFile;
511
-
512
- export type RootCMSOneOf<T = any> = T;
513
-
514
- export type RootCMSOneOfOption<T, Base> = Base & {_type: T};
515
-
516
- export interface RootCMSRichTextBlock {
517
- type: string;
518
- data: any;
519
- }
520
-
521
- export interface RootCMSRichText {
522
- blocks: RootCMSRichTextBlock[];
523
- }
524
-
525
- export interface RootCMSReference {
526
- /** The id of the doc, e.g. "Pages/foo-bar". */
527
- id: string;
528
- /** The collection id of the doc, e.g. "Pages". */
529
- collection: string;
530
- /** The slug of the doc, e.g. "foo-bar". */
531
- slug: string;
532
- }
533
-
534
- export interface RootCMSDoc<Fields extends {}> {
535
- /** The id of the doc, e.g. "Pages/foo-bar". */
536
- id: string;
537
- /** The collection id of the doc, e.g. "Pages". */
538
- collection: string;
539
- /** The slug of the doc, e.g. "foo-bar". */
540
- slug: string;
541
- /** System-level metadata. */
542
- sys: {
543
- createdAt: number;
544
- createdBy: string;
545
- modifiedAt: number;
546
- modifiedBy: string;
547
- firstPublishedAt?: number;
548
- firstPublishedBy?: string;
549
- publishedAt?: number;
550
- publishedBy?: string;
551
- locales?: string[];
552
- };
553
- /** User-entered field values from the CMS. */
554
- fields?: Fields;
555
- }`;
556
- DtsFormatter = class {
557
- constructor() {
558
- /**
559
- * Field types defined in either `.schema.ts` files or used by `oneOf()`
560
- * fields.
561
- */
562
- this.fieldsTypes = {};
563
- /**
564
- * For `collections/*.schema.ts` files, output a corresponding "RootCMSDoc"
565
- * type. For example, `collections/BlogPosts.schema.ts` would output a type
566
- * called `BlogPostsDoc`.
567
- */
568
- this.docTypes = {};
569
- }
570
- /** Adds the types for a `.schema.ts` file to the `.d.ts` file. */
571
- addSchemaFile(fileId, schema) {
572
- const jsdoc = `Generated from \`${fileId}\`.`;
573
- const typeId = alphanumeric(path4.parse(fileId).name.split(".")[0]);
574
- const oneOfTypes = {};
575
- const fieldsTypeId = `${typeId}Fields`;
576
- const fieldsType = dom.create.interface(
577
- fieldsTypeId,
578
- dom.DeclarationFlags.Export
579
- );
580
- fieldsType.jsDocComment = jsdoc;
581
- for (const field of schema.fields) {
582
- if (!field.id) {
583
- continue;
584
- }
585
- fieldsType.members.push(fieldProperty(field, { typeId, oneOfTypes }));
586
- }
587
- for (const oneOfTypeId in oneOfTypes) {
588
- if (!this.fieldsTypes[fieldsTypeId]) {
589
- this.fieldsTypes[oneOfTypeId] = oneOfTypes[oneOfTypeId];
590
- }
591
- }
592
- this.fieldsTypes[fieldsTypeId] = fieldsType;
593
- if (fileId.startsWith("/collections/")) {
594
- const docTypeId = `${typeId}Doc`;
595
- const baseType = dom.create.namedTypeReference("RootCMSDoc");
596
- baseType.typeArguments.push(dom.create.namedTypeReference(fieldsTypeId));
597
- const docType = dom.create.alias(
598
- docTypeId,
599
- baseType,
600
- dom.DeclarationFlags.Export
601
- );
602
- docType.jsDocComment = jsdoc;
603
- this.docTypes[docTypeId] = docType;
604
- }
605
- }
606
- /** Generates the `.d.ts` output as a string. */
607
- toString() {
608
- const results = [TEMPLATE];
609
- const sortedFieldsTypes = Object.keys(this.fieldsTypes).sort();
610
- for (const fieldsTypeId of sortedFieldsTypes) {
611
- const fieldsType = this.fieldsTypes[fieldsTypeId];
612
- results.push(this.typeToString(fieldsType));
613
- const docTypeId = this.fieldsTypeToDocType(fieldsTypeId);
614
- const docType = this.docTypes[docTypeId];
615
- if (docType) {
616
- results.push(this.typeToString(docType));
617
- }
618
- }
619
- const output = results.join("\n\n").replaceAll("/** ", "/** ").replaceAll(" */", " */").replace(/\r\n|\r|\n/g, "\n") + "\n";
620
- return output;
621
- }
622
- /**
623
- * Converts "<Name>Fields" to "<Name>Doc".
624
- */
625
- fieldsTypeToDocType(fieldsTypeId) {
626
- if (!fieldsTypeId.endsWith("Fields")) {
627
- throw new Error(`"${fieldsTypeId}" should be suffixed with "Fields"`);
628
- }
629
- const name = fieldsTypeId.slice(0, -6);
630
- return `${name}Doc`;
631
- }
632
- typeToString(type2) {
633
- return this.reformatOutput(dom.emit(type2, { singleLineJsDocComments: true }));
634
- }
635
- /**
636
- * Formats the output to Google style conventions, e.g. 2-space indents and
637
- * single quote strings.
638
- */
639
- reformatOutput(input) {
640
- const lines = input.trim().split("\n");
641
- const results = [];
642
- for (const line of lines) {
643
- const convertedLine = line.replace(/ {4}/g, " ").replaceAll('"', "'");
644
- results.push(convertedLine);
645
- }
646
- return results.join("\n");
647
- }
648
- };
649
- }
650
- });
651
-
652
- // core/plugin.ts
653
- import { promises as fs4 } from "fs";
654
- import path5 from "path";
655
- import { fileURLToPath as fileURLToPath2 } from "url";
656
- import { viteSsrLoadModule as viteSsrLoadModule2 } from "@blinkk/root/node";
657
- import bodyParser from "body-parser";
658
- import {
659
- applicationDefault,
660
- getApp,
661
- initializeApp
662
- } from "firebase-admin/app";
663
- import { getAuth } from "firebase-admin/auth";
664
- import { getFirestore } from "firebase-admin/firestore";
665
- import * as jsonwebtoken from "jsonwebtoken";
666
- import sirv from "sirv";
667
-
668
- // shared/sse.ts
669
- var SSEEvent = {
670
- /**
671
- * First connection event.
672
- */
673
- CONNECTED: "connected",
674
- /**
675
- * Changes to any .schema.ts file.
676
- */
677
- SCHEMA_CHANGED: "schemaChanged"
678
- };
679
-
680
- // core/api.ts
681
- init_ai();
682
- import { promises as fs2 } from "fs";
683
- import path3 from "path";
684
- import { multipartMiddleware } from "@blinkk/root/middleware";
685
-
686
- // core/client.ts
687
- import crypto2 from "crypto";
688
- import {
689
- FieldValue as FieldValue2,
690
- Timestamp as Timestamp3
691
- } from "firebase-admin/firestore";
692
-
693
- // core/translations-manager.ts
694
- import {
695
- FieldValue,
696
- Timestamp as Timestamp2
697
- } from "firebase-admin/firestore";
698
-
699
- // shared/strings.ts
700
- import fnv from "fnv-plus";
701
- function normalizeStr(str) {
702
- const lines = str.trim().split("\n").map((line) => removeTrailingWhitespace(line));
703
- return lines.join("\n");
704
- }
705
- function removeTrailingWhitespace(str) {
706
- return str.trimEnd().replace(/&nbsp;$/, "");
707
- }
708
- function hashStr(str) {
709
- if (!str || typeof str !== "string") {
710
- throw new Error("input string is invalid");
711
- }
712
- return fnv.fast1a52hex(normalizeStr(str));
713
- }
714
-
715
- // core/translations-manager.ts
716
- var TRANSLATIONS_DB_PATH_FORMAT = "/Projects/{project}/TranslationsManager/{mode}/Translations";
717
- var TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT = `${TRANSLATIONS_DB_PATH_FORMAT}/{id}:{locale}`;
718
- var TranslationsManager = class {
719
- constructor(cmsClient) {
720
- this.cmsClient = cmsClient;
721
- }
722
- /**
723
- * Saves draft translations for a translations doc id.
724
- *
725
- * Example:
726
- * ```
727
- * const strings = {
728
- * 'one': {es: 'uno', fr: 'un'},
729
- * 'two': {es: 'dos', fr: 'deux'},
730
- * };
731
- * await tm.saveTranslations('Pages/index', strings);
732
- * ```
733
- */
734
- async saveTranslations(id, strings, options) {
735
- const mode = "draft";
736
- const localesSet = /* @__PURE__ */ new Set();
737
- Object.values(strings).forEach((entry) => {
738
- Object.keys(entry).forEach((locale) => {
739
- if (locale !== "source") {
740
- localesSet.add(locale);
741
- }
742
- });
743
- });
744
- const batch = this.cmsClient.db.batch();
745
- const locales = Array.from(localesSet);
746
- locales.forEach((locale) => {
747
- const updates = {
748
- id,
749
- locale,
750
- sys: {
751
- modifiedAt: Timestamp2.now(),
752
- modifiedBy: options?.modifiedBy || "root-cms-client"
753
- },
754
- strings: {}
755
- };
756
- if (options?.tags && options.tags.length > 0) {
757
- updates.tags = FieldValue.arrayUnion(...options.tags);
758
- }
759
- let numUpdates = 0;
760
- const hashMap = this.toLocaleDocHashMap(strings, locale);
761
- Object.entries(hashMap).forEach(([hash, translations]) => {
762
- Object.entries(translations).forEach(([locale2, translation]) => {
763
- if (translation) {
764
- updates.strings[hash] ??= {};
765
- updates.strings[hash][locale2] = translation;
766
- numUpdates += 1;
767
- }
768
- });
769
- });
770
- if (numUpdates > 0) {
771
- const localeDocPath = buildTranslationsLocaleDocDbPath({
772
- project: this.cmsClient.projectId,
773
- mode,
774
- id,
775
- locale
776
- });
777
- const localeDocRef = this.cmsClient.db.doc(localeDocPath);
778
- batch.set(localeDocRef, updates, { merge: true });
779
- }
780
- });
781
- await batch.commit();
782
- }
783
- /**
784
- * Publishes a translations doc.
785
- */
786
- async publishTranslations(id, options) {
787
- const db = this.cmsClient.db;
788
- const project = this.cmsClient.projectId;
789
- const draftPath = buildTranslationsDbPath({ project, mode: "draft" });
790
- const query = db.collection(draftPath).where("id", "==", id);
791
- const res = await query.get();
792
- if (res.size === 0) {
793
- console.warn(`no translations to publish for ${id}`);
794
- return;
795
- }
796
- const batch = options?.batch || db.batch();
797
- res.docs.forEach((doc) => {
798
- const translationsLocaleDoc = doc.data();
799
- const sys = {
800
- ...translationsLocaleDoc.sys,
801
- publishedAt: Timestamp2.now(),
802
- publishedBy: options?.publishedBy || "root-cms-client"
803
- };
804
- batch.update(doc.ref, { sys });
805
- const publishedDocPath = buildTranslationsLocaleDocDbPath({
806
- project,
807
- mode: "published",
808
- id: translationsLocaleDoc.id,
809
- locale: translationsLocaleDoc.locale
810
- });
811
- const publishedDocRef = db.doc(publishedDocPath);
812
- batch.set(publishedDocRef, { ...translationsLocaleDoc, sys });
813
- });
814
- const shouldCommitBatch = !options?.batch;
815
- if (shouldCommitBatch) {
816
- await batch.commit();
817
- }
818
- }
819
- /**
820
- * Fetches translations from one or more translations docs in the translations
821
- * manager.
822
- *
823
- * Example:
824
- * ```
825
- * await tm.loadTranslations();
826
- * // =>
827
- * // {
828
- * // "one": {"es": "uno", "fr": "un"},
829
- * // "two": {"es": "dos", "fr": "deux"}
830
- * // }
831
- * ```
832
- *
833
- * To load a specific set of translations docs by id:
834
- * ```
835
- * const translationsToLoad = ['Global/strings', 'Global/header', 'Global/footer', 'Pages/index'];
836
- * await tm.loadTranslations({ids: translationsToLoad});
837
- * // =>
838
- * // {
839
- * // "one": {"es": "uno", "fr": "un"},
840
- * // "two": {"es": "dos", "fr": "deux"}
841
- * // }
842
- * ```
843
- *
844
- * To load a subset of locales (more performant):
845
- * ```
846
- * await tm.loadTranslations({locales: ['es']});
847
- * // =>
848
- * // {
849
- * // "one": {"es": "uno"},
850
- * // "two": {"es": "dos"}
851
- * // }
852
- * ```
853
- */
854
- async loadTranslations(options) {
855
- const mode = options?.mode || "published";
856
- const dbPath = buildTranslationsDbPath({
857
- project: this.cmsClient.projectId,
858
- mode
859
- });
860
- let query = this.cmsClient.db.collection(dbPath);
861
- if (options?.ids && options.ids.length > 0) {
862
- query = query.where("id", "in", options.ids);
863
- }
864
- if (options?.tags && options.tags.length > 0) {
865
- query = query.where("tags", "array-contains", options.tags);
866
- }
867
- if (options?.locales && options.locales.length > 0) {
868
- query = query.where("locale", "in", options.locales);
869
- }
870
- const results = await query.get();
871
- const strings = {};
872
- results.forEach((result) => {
873
- const localeDoc = result.data();
874
- Object.values(localeDoc.strings || {}).forEach((item) => {
875
- strings[item.source] ??= { source: item.source };
876
- strings[item.source][localeDoc.locale] = item.translation;
877
- });
878
- });
879
- return strings;
880
- }
881
- /**
882
- * Fetches translations for a given locale, with optional fallbacks.
883
- * The return value is a map of source string to translated string.
884
- *
885
- * Example:
886
- * ```
887
- * await translationsDoc.loadTranslationsForLocale('es');
888
- * // =>
889
- * // {
890
- * // "one": "uno",
891
- * // "two": "dos",
892
- * // }
893
- * ```
894
- */
895
- async loadTranslationsForLocale(locale, options) {
896
- const localeSet = /* @__PURE__ */ new Set([
897
- locale,
898
- ...options?.fallbackLocales || []
899
- ]);
900
- const fallbackLocales = Array.from(localeSet);
901
- const multiLocaleStrings = await this.loadTranslations({
902
- mode: options?.mode,
903
- locales: fallbackLocales
904
- });
905
- return this.toSingleLocaleMap(multiLocaleStrings, fallbackLocales);
906
- }
907
- /**
908
- * Converts a multi-locale translations map to a flat single-locale map,
909
- * with optional support for fallback locales.
910
- *
911
- * ```
912
- * const multiLocaleStrings = {
913
- * 'one': {es: 'uno', fr: 'un'},
914
- * 'two': {es: 'dos', fr: 'deux'}
915
- * };
916
- * translationsDoc.toSingleLocaleMap(multiLocaleStrings, ['es']);
917
- * // =>
918
- * // {
919
- * // "one": "uno",
920
- * // "two": "dos",
921
- * // }
922
- * ```
923
- */
924
- toSingleLocaleMap(multiLocaleStrings, fallbackLocales) {
925
- const singleLocaleStrings = {};
926
- Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
927
- let translation = source;
928
- for (const locale of fallbackLocales) {
929
- if (translations[locale]) {
930
- translation = translations[locale];
931
- break;
932
- }
933
- }
934
- singleLocaleStrings[source] = translation;
935
- });
936
- return singleLocaleStrings;
937
- }
938
- /**
939
- * Converts a multi-locale translations map to a single-locale hashed version,
940
- * used for storage in in the DB.
941
- *
942
- * ```
943
- * const multiLocaleStrings = {
944
- * 'one': {es: 'uno', fr: 'un'},
945
- * 'two': {es: 'dos', fr: 'deux'}
946
- * };
947
- * translationsDoc.toLocaleDocHashMap(multiLocaleStrings, 'es');
948
- * // =>
949
- * // {
950
- * // "<hash1>": {"source": "one", "translation": "uno"},
951
- * // "<hash2>": {"source": "two", "translation": "dos"},
952
- * // }
953
- * ```
954
- *
955
- * One reason for using hashes is because the DB has limits on the number of
956
- * chars that can be used as the "key" in a object map.
957
- */
958
- toLocaleDocHashMap(multiLocaleStrings, locale) {
959
- const hashMap = {};
960
- Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
961
- const translation = translations[locale];
962
- if (translation) {
963
- const hash = hashStr(source);
964
- hashMap[hash] = { source, translation };
965
- }
966
- });
967
- return hashMap;
968
- }
969
- /**
970
- * Import translations from the v1 system to the TranslationsManager.
971
- */
972
- async importTranslationsFromV1() {
973
- const projectId = this.cmsClient.projectId;
974
- const db = this.cmsClient.db;
975
- const dbPath = `Projects/${projectId}/Translations`;
976
- const query = db.collection(dbPath);
977
- const querySnapshot = await query.get();
978
- if (querySnapshot.size === 0) {
979
- return;
980
- }
981
- console.log(
982
- "[root cms] importing v1 Translations to v2 TranslationsManager"
983
- );
984
- const translationsDocs = {};
985
- querySnapshot.forEach((doc) => {
986
- const translation = doc.data();
987
- const source = this.cmsClient.normalizeString(translation.source);
988
- delete translation.source;
989
- const tags = translation.tags || [];
990
- delete translation.tags;
991
- for (const tag of tags) {
992
- if (tag.includes("/")) {
993
- const translationsId = tag;
994
- translationsDocs[translationsId] ??= {
995
- id: translationsId,
996
- tags,
997
- strings: {}
998
- };
999
- translationsDocs[translationsId].strings[source] = translation;
1000
- }
1001
- }
1002
- });
1003
- if (Object.keys(translationsDocs).length === 0) {
1004
- console.log("[root cms] no v1 translations to save");
1005
- return;
1006
- }
1007
- for (const docId in translationsDocs) {
1008
- const [collection, slug] = docId.split("/");
1009
- if (collection && slug) {
1010
- const doc = await this.cmsClient.getDoc(collection, slug, {
1011
- mode: "draft"
1012
- });
1013
- const linkedSheet = doc?.sys?.l10nSheet;
1014
- if (linkedSheet) {
1015
- translationsDocs[docId].sys.linkedSheet = linkedSheet;
1016
- }
1017
- }
1018
- }
1019
- Object.entries(translationsDocs).forEach(([translationsId, data]) => {
1020
- const len = Object.keys(data.strings).length;
1021
- console.log(`[root cms] saving ${len} string(s) to ${translationsId}...`);
1022
- this.saveTranslations(translationsId, data.strings, {
1023
- tags: data.tags || [translationsId]
1024
- });
1025
- });
1026
- }
1027
- };
1028
- function buildTranslationsDbPath(options) {
1029
- return TRANSLATIONS_DB_PATH_FORMAT.replace(
1030
- "{project}",
1031
- options.project
1032
- ).replace("{mode}", options.mode);
1033
- }
1034
- function buildTranslationsLocaleDocDbPath(options) {
1035
- return TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT.replace(
1036
- "{project}",
1037
- options.project
1038
- ).replace("{mode}", options.mode).replace("{id}", options.id.replaceAll("/", "--")).replace("{locale}", options.locale);
1039
- }
1040
-
1041
- // core/client.ts
1042
- var RootCMSClient = class {
1043
- constructor(rootConfig) {
1044
- this.rootConfig = rootConfig;
1045
- this.cmsPlugin = getCmsPlugin(this.rootConfig);
1046
- const cmsPluginOptions = this.cmsPlugin.getConfig();
1047
- this.projectId = cmsPluginOptions.id || "default";
1048
- this.app = this.cmsPlugin.getFirebaseApp();
1049
- this.db = this.cmsPlugin.getFirestore();
1050
- }
1051
- /**
1052
- * Retrieves doc data from Root.js CMS.
1053
- */
1054
- async getDoc(collectionId, slug, options) {
1055
- const rawData = await this.getRawDoc(collectionId, slug, options);
1056
- if (rawData) {
1057
- return unmarshalData(rawData);
1058
- }
1059
- return null;
1060
- }
1061
- /**
1062
- * Retrieves raw doc data as stored in the database. Only use this if you know
1063
- * what you are doing.
1064
- */
1065
- async getRawDoc(collectionId, slug, options) {
1066
- const mode = options.mode;
1067
- const modeCollection = mode === "draft" ? "Drafts" : "Published";
1068
- slug = slug.replaceAll("/", "--");
1069
- const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
1070
- const docRef = this.db.doc(dbPath);
1071
- const doc = await docRef.get();
1072
- if (doc.exists) {
1073
- return doc.data();
1074
- }
1075
- return null;
1076
- }
1077
- /**
1078
- * Firestore path for a collection.
1079
- */
1080
- dbCollectionDocsPath(collectionId, options) {
1081
- let modeCollection = "";
1082
- if (options.mode === "draft") {
1083
- modeCollection = "Drafts";
1084
- } else if (options.mode === "published") {
1085
- modeCollection = "Published";
1086
- } else {
1087
- throw new Error(`unknown mode: ${options.mode}`);
1088
- }
1089
- return `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
1090
- }
1091
- /**
1092
- * Firestore path for a content doc.
1093
- */
1094
- dbDocPath(collectionId, slug, options) {
1095
- const collectionDocsPath = this.dbCollectionDocsPath(collectionId, options);
1096
- const normalizedSlug = slug.replaceAll("/", "--");
1097
- return `${collectionDocsPath}/${normalizedSlug}`;
1098
- }
1099
- /**
1100
- * Firestore doc ref for a content doc.
1101
- */
1102
- dbDocRef(collectionId, slug, options) {
1103
- const docPath = this.dbDocPath(collectionId, slug, options);
1104
- return this.db.doc(docPath);
1105
- }
1106
- /**
1107
- * Saves draft data to a doc.
1108
- *
1109
- * Note: this saves data to the "fields" attr of the draft doc. If you need to
1110
- * modify the sys-level attributes of the doc, use `setRawDoc()`.
1111
- */
1112
- async saveDraftData(docId, fieldsData, options) {
1113
- const { collection, slug } = parseDocId(docId);
1114
- const draftDoc = await this.getRawDoc(collection, slug, { mode: "draft" }) || {};
1115
- const draftSys = draftDoc.sys || {};
1116
- const modifiedBy = options?.modifiedBy || "root-cms-client";
1117
- const fields = marshalData(fieldsData || {});
1118
- const data = {
1119
- id: docId,
1120
- collection,
1121
- slug,
1122
- sys: {
1123
- ...draftSys,
1124
- createdAt: draftSys.createdAt ?? Timestamp3.now(),
1125
- createdBy: draftSys.createdBy ?? modifiedBy,
1126
- modifiedAt: Timestamp3.now(),
1127
- modifiedBy,
1128
- locales: options?.locales ?? draftSys.locales ?? ["en"]
1129
- },
1130
- fields
1131
- };
1132
- await this.setRawDoc(collection, slug, data, { mode: "draft" });
1133
- }
1134
- /**
1135
- * Prefer `saveDraftData('Pages/foo', data)`. Only use this if you know what
1136
- * you're doing.
1137
- */
1138
- async setRawDoc(collectionId, slug, data, options) {
1139
- const mode = options.mode;
1140
- const modeCollection = mode === "draft" ? "Drafts" : "Published";
1141
- const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
1142
- const docRef = this.db.doc(dbPath);
1143
- await docRef.set(data);
1144
- }
1145
- /**
1146
- * Lists docs from a Root.js CMS collection.
1147
- */
1148
- async listDocs(collectionId, options) {
1149
- const mode = options.mode;
1150
- const modeCollection = mode === "draft" ? "Drafts" : "Published";
1151
- const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
1152
- let query = this.db.collection(dbPath);
1153
- if (options.limit) {
1154
- query = query.limit(options.limit);
1155
- }
1156
- if (options.offset) {
1157
- query = query.offset(options.offset);
1158
- }
1159
- if (options.orderBy) {
1160
- query = query.orderBy(options.orderBy, options.orderByDirection);
1161
- }
1162
- if (options.query) {
1163
- query = options.query(query);
1164
- }
1165
- const results = await query.get();
1166
- const docs = [];
1167
- results.forEach((result) => {
1168
- if (options.raw) {
1169
- const rawDoc = result.data();
1170
- docs.push(rawDoc);
1171
- } else {
1172
- const doc = unmarshalData(result.data());
1173
- docs.push(doc);
1174
- }
1175
- });
1176
- return { docs };
1177
- }
1178
- /**
1179
- * Returns the number of docs in a Root.js CMS collection.
1180
- */
1181
- async getDocsCount(collectionId, options) {
1182
- const mode = options.mode;
1183
- const modeCollection = mode === "draft" ? "Drafts" : "Published";
1184
- const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
1185
- let query = this.db.collection(dbPath);
1186
- if (options.query) {
1187
- query = options.query(query);
1188
- }
1189
- const results = await query.count().get();
1190
- const count = results.data().count;
1191
- return count;
1192
- }
1193
- /**
1194
- * Batch publishes a set of docs by id.
1195
- */
1196
- async publishDocs(docIds, options) {
1197
- const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
1198
- const publishedBy = options?.publishedBy || "root-cms-client";
1199
- const docRefs = docIds.map((docId) => {
1200
- const [collection, slug] = docId.split("/");
1201
- if (!collection || !slug) {
1202
- throw new Error(`invalid doc id: ${docId}`);
1203
- }
1204
- const docRef = this.db.doc(
1205
- `${projectCollectionsPath}/${collection}/Drafts/${slug}`
1206
- );
1207
- return docRef;
1208
- });
1209
- const docSnapshots = await this.db.getAll(...docRefs);
1210
- const docs = docSnapshots.map((snapshot) => snapshot.data()).filter((d) => !!d);
1211
- if (docs.length === 0) {
1212
- console.log("no docs to publish");
1213
- return [];
1214
- }
1215
- for (const doc of docs) {
1216
- if (this.testPublishingLocked(doc)) {
1217
- throw new Error(`publishing is locked for doc: ${doc.id}`);
1218
- }
1219
- }
1220
- let batchCount = 0;
1221
- const batch = options?.batch || this.db.batch();
1222
- const versionTags = ["published"];
1223
- if (options?.releaseId) {
1224
- versionTags.push(`release:${options.releaseId}`);
1225
- }
1226
- const publishedDocs = [];
1227
- for (const doc of docs) {
1228
- const { id, collection, slug, sys, fields } = doc;
1229
- const draftRef = this.db.doc(
1230
- `${projectCollectionsPath}/${collection}/Drafts/${slug}`
1231
- );
1232
- const scheduledRef = this.db.doc(
1233
- `${projectCollectionsPath}/${collection}/Scheduled/${slug}`
1234
- );
1235
- const publishedRef = this.db.doc(
1236
- `${projectCollectionsPath}/${collection}/Published/${slug}`
1237
- );
1238
- const firstPublishedAt = sys.firstPublishedAt ?? FieldValue2.serverTimestamp();
1239
- const firstPublishedBy = sys.firstPublishedBy ?? publishedBy;
1240
- batch.set(publishedRef, {
1241
- id,
1242
- collection,
1243
- slug,
1244
- fields: fields || {},
1245
- sys: {
1246
- ...sys,
1247
- firstPublishedAt,
1248
- firstPublishedBy,
1249
- publishedAt: FieldValue2.serverTimestamp(),
1250
- publishedBy
1251
- }
1252
- });
1253
- batchCount += 1;
1254
- const versionRef = this.db.doc(
1255
- `${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}`
1256
- );
1257
- const versionData = {
1258
- id,
1259
- collection,
1260
- slug,
1261
- fields: fields || {},
1262
- sys: {
1263
- ...sys,
1264
- firstPublishedAt,
1265
- firstPublishedBy,
1266
- publishedAt: FieldValue2.serverTimestamp(),
1267
- publishedBy
1268
- }
1269
- };
1270
- if (versionTags.length) {
1271
- versionData.tags = versionTags;
1272
- }
1273
- batch.set(versionRef, versionData);
1274
- batchCount += 1;
1275
- batch.delete(scheduledRef);
1276
- batchCount += 1;
1277
- batch.update(draftRef, {
1278
- "sys.scheduledAt": FieldValue2.delete(),
1279
- "sys.scheduledBy": FieldValue2.delete(),
1280
- "sys.firstPublishedAt": firstPublishedAt,
1281
- "sys.firstPublishedBy": firstPublishedBy,
1282
- "sys.publishedAt": FieldValue2.serverTimestamp(),
1283
- "sys.publishedBy": publishedBy
1284
- });
1285
- batchCount += 1;
1286
- publishedDocs.push(doc);
1287
- if (batchCount >= 400) {
1288
- await batch.commit();
1289
- batchCount = 0;
1290
- }
1291
- }
1292
- if (batchCount > 0) {
1293
- await batch.commit();
1294
- }
1295
- console.log(`published ${publishedDocs.length} docs!`);
1296
- return publishedDocs;
1297
- }
1298
- /**
1299
- * Publishes scheduled docs.
1300
- */
1301
- async publishScheduledDocs() {
1302
- const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
1303
- const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
1304
- const snapshot = await this.db.collectionGroup("Scheduled").get();
1305
- const docs = snapshot.docs.filter((d) => {
1306
- if (!d.ref.path.startsWith(projectCollectionsPath)) {
1307
- return false;
1308
- }
1309
- const data = d.data() || {};
1310
- const scheduledAt = data.sys?.scheduledAt;
1311
- return scheduledAt && scheduledAt.toMillis && scheduledAt.toMillis() <= now;
1312
- }).map((d) => {
1313
- const dbPath = d.ref.path;
1314
- const segments = dbPath.split("/");
1315
- const slug = segments.at(-1);
1316
- const collection = segments.at(-3);
1317
- const id = `${collection}/${slug}`;
1318
- return {
1319
- data: d.data(),
1320
- id,
1321
- collection,
1322
- slug
1323
- };
1324
- });
1325
- if (docs.length === 0) {
1326
- console.log("no docs to schedule");
1327
- return [];
1328
- }
1329
- let batchCount = 0;
1330
- const batch = this.db.batch();
1331
- const versionTags = ["published"];
1332
- const publishedDocs = [];
1333
- for (const doc of docs) {
1334
- const { id, collection, slug, data } = doc;
1335
- const draftRef = this.db.doc(
1336
- `${projectCollectionsPath}/${collection}/Drafts/${slug}`
1337
- );
1338
- const scheduledRef = this.db.doc(
1339
- `${projectCollectionsPath}/${collection}/Scheduled/${slug}`
1340
- );
1341
- const publishedRef = this.db.doc(
1342
- `${projectCollectionsPath}/${collection}/Published/${slug}`
1343
- );
1344
- const { scheduledAt, scheduledBy, ...sys } = data.sys || {};
1345
- const firstPublishedAt = sys.firstPublishedAt ?? scheduledAt;
1346
- const firstPublishedBy = sys.firstPublishedBy ?? (scheduledBy || "root-cms-client");
1347
- batch.set(publishedRef, {
1348
- id,
1349
- collection,
1350
- slug,
1351
- fields: data.fields || {},
1352
- sys: {
1353
- ...sys,
1354
- firstPublishedAt,
1355
- firstPublishedBy,
1356
- publishedAt: FieldValue2.serverTimestamp(),
1357
- publishedBy: scheduledBy || ""
1358
- }
1359
- });
1360
- batchCount += 1;
1361
- const versionRef = this.db.doc(
1362
- `${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}`
1363
- );
1364
- const versionData = {
1365
- id,
1366
- collection,
1367
- slug,
1368
- fields: data.fields || {},
1369
- sys: {
1370
- ...sys,
1371
- firstPublishedAt,
1372
- firstPublishedBy,
1373
- publishedAt: FieldValue2.serverTimestamp(),
1374
- publishedBy: scheduledBy || ""
1375
- },
1376
- tags: versionTags
1377
- };
1378
- batch.set(versionRef, versionData);
1379
- batchCount += 1;
1380
- batch.delete(scheduledRef);
1381
- batchCount += 1;
1382
- batch.update(draftRef, {
1383
- "sys.scheduledAt": FieldValue2.delete(),
1384
- "sys.scheduledBy": FieldValue2.delete(),
1385
- "sys.firstPublishedAt": firstPublishedAt,
1386
- "sys.firstPublishedBy": firstPublishedBy,
1387
- "sys.publishedAt": FieldValue2.serverTimestamp(),
1388
- "sys.publishedBy": scheduledBy || "root-cms-client"
1389
- });
1390
- batchCount += 1;
1391
- publishedDocs.push(doc);
1392
- if (batchCount >= 400) {
1393
- await batch.commit();
1394
- batchCount = 0;
1395
- continue;
1396
- }
1397
- }
1398
- if (batchCount > 0) {
1399
- await batch.commit();
1400
- }
1401
- console.log(`published ${publishedDocs.length} docs!`);
1402
- return publishedDocs;
1403
- }
1404
- /**
1405
- * Publishes docs in scheduled releases.
1406
- */
1407
- async publishScheduledReleases() {
1408
- const releasesPath = `Projects/${this.projectId}/Releases`;
1409
- const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
1410
- const query = this.db.collection(releasesPath).where("scheduledAt", "<=", Timestamp3.fromMillis(now));
1411
- const querySnapshot = await query.get();
1412
- for (const snapshot of querySnapshot.docs) {
1413
- const release = snapshot.data();
1414
- const batch = this.db.batch();
1415
- const publishedBy = release.scheduledBy || "root-cms-client";
1416
- batch.update(snapshot.ref, {
1417
- publishedAt: Timestamp3.now(),
1418
- publishedBy,
1419
- scheduledAt: FieldValue2.delete(),
1420
- scheduledBy: FieldValue2.delete()
1421
- });
1422
- const dataSourceIds = release.dataSourceIds || [];
1423
- if (dataSourceIds.length > 0) {
1424
- await this.publishDataSources(dataSourceIds, {
1425
- publishedBy,
1426
- batch,
1427
- commitBatch: false
1428
- });
1429
- }
1430
- await this.publishDocs(release.docIds || [], {
1431
- publishedBy,
1432
- batch,
1433
- releaseId: release.id
1434
- });
1435
- }
1436
- }
1437
- /**
1438
- * Checks if a doc is currently "locked" for publishing.
1439
- */
1440
- testPublishingLocked(doc) {
1441
- if (doc.sys?.publishingLocked) {
1442
- if (doc.sys.publishingLocked.until) {
1443
- const now = Timestamp3.now().toMillis();
1444
- const until = doc.sys.publishingLocked.until.toMillis();
1445
- return now < until;
1446
- }
1447
- return true;
1448
- }
1449
- return false;
1450
- }
1451
- /**
1452
- * Returns a `TranslationsManager` object for managing translations.
1453
- *
1454
- * To get translations:
1455
- * ```
1456
- * await tm.loadTranslations({
1457
- * ids: ['Global/strings', 'Pages/index'],
1458
- * locales: ['es'],
1459
- * });
1460
- * ```
1461
- *
1462
- * NOTE: The `TranslationsManager` is a v2 feature that will eventually
1463
- * replace the v1 translations system.
1464
- */
1465
- getTranslationsManager() {
1466
- const cmsPluginOptions = this.cmsPlugin.getConfig();
1467
- if (cmsPluginOptions.experiments?.v2TranslationsManager) {
1468
- throw new Error(
1469
- "`v2TranslationsManager` is not enabled. update root.config.ts and add: `{experiments: {v2TranslationsManager: true}}`"
1470
- );
1471
- }
1472
- return new TranslationsManager(this);
1473
- }
1474
- /**
1475
- * Loads translations saved in the translations collection, optionally
1476
- * filtered by tag.
1477
- *
1478
- * Returns a map like:
1479
- * ```
1480
- * {
1481
- * "<hash>": {"source": "Hello", "es": "Hola", "fr": "Bonjour"},
1482
- * }
1483
- * ```
1484
- */
1485
- async loadTranslations(options) {
1486
- const dbPath = `Projects/${this.projectId}/Translations`;
1487
- let query = this.db.collection(dbPath);
1488
- if (options?.tags) {
1489
- query = query.where("tags", "array-contains-any", options.tags);
1490
- }
1491
- const querySnapshot = await query.get();
1492
- const translationsMap = {};
1493
- querySnapshot.forEach((doc) => {
1494
- const hash = doc.id;
1495
- translationsMap[hash] = doc.data();
1496
- });
1497
- return translationsMap;
1498
- }
1499
- /**
1500
- * Saves a map of translations, e.g.:
1501
- * ```
1502
- * await client.saveTranslations({
1503
- * "Hello": {"es": "Hola", "fr": "Bonjour"},
1504
- * });
1505
- * ```
1506
- */
1507
- async saveTranslations(translations, tags) {
1508
- const translationsPath = `Projects/${this.projectId}/Translations`;
1509
- const batch = this.db.batch();
1510
- let batchCount = 0;
1511
- Object.entries(translations).forEach(([source, sourceTranslations]) => {
1512
- const hash = this.getTranslationKey(source);
1513
- const translationRef = this.db.doc(`${translationsPath}/${hash}`);
1514
- const data = {
1515
- ...sourceTranslations,
1516
- source: this.normalizeString(source)
1517
- };
1518
- if (tags) {
1519
- data.tags = FieldValue2.arrayUnion(...tags);
1520
- }
1521
- batch.set(translationRef, data, { merge: true });
1522
- batchCount += 1;
1523
- });
1524
- if (batchCount > 500) {
1525
- throw new Error("up to 500 translations can be saved at a time.");
1526
- }
1527
- await batch.commit();
1528
- }
1529
- /**
1530
- * Returns the "key" used for a translation as stored in the db. Translations
1531
- * are stored under `Projects/<project id>/Translations/<sha1 hash>`.
1532
- */
1533
- getTranslationKey(source) {
1534
- const sha1 = crypto2.createHash("sha1").update(this.normalizeString(source)).digest("hex");
1535
- return sha1;
1536
- }
1537
- /**
1538
- * Cleans a string that's used for translations. Performs the following:
1539
- * - Removes any leading/trailing whitespace
1540
- * - Removes spaces at the end of any line
1541
- */
1542
- normalizeString(str) {
1543
- const lines = String(str).trim().split("\n").map((line) => line.trimEnd());
1544
- return lines.join("\n");
1545
- }
1546
- /**
1547
- * Loads translations for a particular locale.
1548
- *
1549
- * Returns a map like:
1550
- * ```
1551
- * {
1552
- * "Hello": "Bonjour",
1553
- * }
1554
- * ```
1555
- */
1556
- async loadTranslationsForLocale(locale, options) {
1557
- const translationsMap = await this.loadTranslations(options);
1558
- return translationsForLocale(translationsMap, locale);
1559
- }
1560
- /**
1561
- * Firestore path for a translations file.
1562
- */
1563
- dbTranslationsPath(translationsId, options) {
1564
- const mode = options.mode;
1565
- if (!(mode === "draft" || mode === "published")) {
1566
- throw new Error(`invalid mode: ${mode}`);
1567
- }
1568
- const slug = translationsId.replaceAll("/", "--");
1569
- const dbPath = `Projects/${this.projectId}/TranslationsManager/${mode}/Translations/${slug}`;
1570
- return dbPath;
1571
- }
1572
- /**
1573
- * Firestore doc ref for a translations file.
1574
- */
1575
- dbTranslationsRef(translationsId, options) {
1576
- const dbPath = this.dbTranslationsPath(translationsId, options);
1577
- return this.db.doc(dbPath);
1578
- }
1579
- /**
1580
- * Returns a data source configuration object.
1581
- */
1582
- async getDataSource(dataSourceId) {
1583
- const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}`;
1584
- const docRef = this.db.doc(dbPath);
1585
- const doc = await docRef.get();
1586
- if (doc.exists) {
1587
- return doc.data();
1588
- }
1589
- return null;
1590
- }
1591
- /**
1592
- * Syncs a data source to draft state.
1593
- */
1594
- async syncDataSource(dataSourceId, options) {
1595
- const dataSource = await this.getDataSource(dataSourceId);
1596
- if (!dataSource) {
1597
- throw new Error(`data source not found: ${dataSourceId}`);
1598
- }
1599
- const data = await this.fetchData(dataSource);
1600
- const dataSourceDocRef = this.db.doc(
1601
- `Projects/${this.projectId}/DataSources/${dataSourceId}`
1602
- );
1603
- const dataDocRef = this.db.doc(
1604
- `Projects/${this.projectId}/DataSources/${dataSourceId}/Data/draft`
1605
- );
1606
- const syncedBy = options?.syncedBy || "root-cms-client";
1607
- const updatedDataSource = {
1608
- ...dataSource,
1609
- syncedAt: Timestamp3.now(),
1610
- syncedBy
1611
- };
1612
- const batch = this.db.batch();
1613
- batch.set(dataDocRef, {
1614
- dataSource: updatedDataSource,
1615
- data
1616
- });
1617
- batch.update(dataSourceDocRef, {
1618
- syncedAt: Timestamp3.now(),
1619
- syncedBy
1620
- });
1621
- await batch.commit();
1622
- console.log(`synced data source: ${dataSourceId}`);
1623
- console.log(`synced by: ${syncedBy}`);
1624
- }
1625
- async publishDataSource(dataSourceId, options) {
1626
- const dataSource = await this.getDataSource(dataSourceId);
1627
- if (!dataSource) {
1628
- throw new Error(`data source not found: ${dataSourceId}`);
1629
- }
1630
- const dataSourceDocRef = this.db.doc(
1631
- `Projects/${this.projectId}/DataSources/${dataSourceId}`
1632
- );
1633
- const dataDocRefDraft = this.db.doc(
1634
- `Projects/${this.projectId}/DataSources/${dataSourceId}/draft`
1635
- );
1636
- const dataDocRefPublished = this.db.doc(
1637
- `Projects/${this.projectId}/DataSources/${dataSourceId}/published`
1638
- );
1639
- const dataRes = await this.getFromDataSource(dataSourceId, { mode: "draft" });
1640
- const publishedBy = options?.publishedBy || "root-cms-client";
1641
- const updatedDataSource = {
1642
- ...dataSource,
1643
- publishedAt: Timestamp3.now(),
1644
- publishedBy
1645
- };
1646
- const batch = this.db.batch();
1647
- batch.set(dataDocRefPublished, {
1648
- dataSource: updatedDataSource,
1649
- data: dataRes?.data || null,
1650
- ...dataRes?.headers ? { headers: dataRes.headers } : {}
1651
- });
1652
- batch.update(dataDocRefDraft, {
1653
- dataSource: updatedDataSource
1654
- });
1655
- batch.update(dataSourceDocRef, {
1656
- publishedAt: Timestamp3.now(),
1657
- publishedBy
1658
- });
1659
- await batch.commit();
1660
- console.log(`published data ${dataSourceId}`);
1661
- console.log(`published by: ${publishedBy}`);
1662
- }
1663
- async publishDataSources(dataSourceIds, options) {
1664
- const publishedBy = options?.publishedBy || "root-cms-client";
1665
- const batch = options?.batch || this.db.batch();
1666
- for (const id of dataSourceIds) {
1667
- const dataSource = await this.getDataSource(id);
1668
- if (!dataSource) {
1669
- throw new Error(`data source not found: ${id}`);
1670
- }
1671
- const dataSourceDocRef = this.db.doc(
1672
- `Projects/${this.projectId}/DataSources/${id}`
1673
- );
1674
- const dataDocRefDraft = this.db.doc(
1675
- `Projects/${this.projectId}/DataSources/${id}/draft`
1676
- );
1677
- const dataDocRefPublished = this.db.doc(
1678
- `Projects/${this.projectId}/DataSources/${id}/published`
1679
- );
1680
- const dataRes = await this.getFromDataSource(id, { mode: "draft" });
1681
- const updatedDataSource = {
1682
- ...dataSource,
1683
- publishedAt: FieldValue2.serverTimestamp(),
1684
- publishedBy
1685
- };
1686
- batch.set(dataDocRefPublished, {
1687
- dataSource: updatedDataSource,
1688
- data: dataRes?.data || null,
1689
- ...dataRes?.headers ? { headers: dataRes.headers } : {}
1690
- });
1691
- batch.update(dataDocRefDraft, { dataSource: updatedDataSource });
1692
- batch.update(dataSourceDocRef, {
1693
- publishedAt: FieldValue2.serverTimestamp(),
1694
- publishedBy
1695
- });
1696
- }
1697
- if (!options?.batch || options?.commitBatch) {
1698
- await batch.commit();
1699
- }
1700
- }
1701
- async fetchData(dataSource) {
1702
- if (dataSource.type === "http") {
1703
- return await this.fetchHttpData(dataSource);
1704
- }
1705
- throw new Error(`unsupported data source: ${dataSource.type}`);
1706
- }
1707
- async fetchHttpData(dataSource) {
1708
- const url = dataSource.url || "";
1709
- if (!url.startsWith("https://")) {
1710
- throw new Error(`url not supported: ${url}`);
1711
- }
1712
- const res = await fetch(url, {
1713
- method: dataSource.httpOptions?.method || "GET",
1714
- headers: dataSource.httpOptions?.headers || [],
1715
- body: dataSource.httpOptions?.body || void 0
1716
- });
1717
- if (res.status !== 200) {
1718
- const err = await res.text();
1719
- throw new Error(`req failed: ${err}`);
1720
- }
1721
- const contentType = String(res.headers.get("content-type"));
1722
- if (contentType.includes("application/json")) {
1723
- return await res.json();
1724
- }
1725
- return res.text();
1726
- }
1727
- /**
1728
- * Fetches data from a data source.
1729
- */
1730
- async getFromDataSource(dataSourceId, options) {
1731
- const mode = options?.mode || "published";
1732
- if (!(mode === "draft" || mode === "published")) {
1733
- throw new Error(`invalid mode: ${mode}`);
1734
- }
1735
- if (!dataSourceId || dataSourceId.includes("/")) {
1736
- throw new Error(`invalid data source id: ${dataSourceId}`);
1737
- }
1738
- const docRef = this.dbDataSourceDataRef(dataSourceId, { mode });
1739
- const doc = await docRef.get();
1740
- if (doc.exists) {
1741
- return doc.data();
1742
- }
1743
- return null;
1744
- }
1745
- /**
1746
- * Firestore path for a datasource data.
1747
- */
1748
- dbDataSourceDataPath(dataSourceId, options) {
1749
- if (!dataSourceId || dataSourceId.includes("/")) {
1750
- throw new Error(`invalid data source id: ${dataSourceId}`);
1751
- }
1752
- const mode = options.mode;
1753
- if (!(mode === "draft" || mode === "published")) {
1754
- throw new Error(`invalid mode: ${mode}`);
1755
- }
1756
- const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}/Data/${mode}`;
1757
- return dbPath;
1758
- }
1759
- /**
1760
- * Firestore doc ref for a datasource data.
1761
- */
1762
- dbDataSourceDataRef(dataSourceId, options) {
1763
- const dbPath = this.dbDataSourceDataPath(dataSourceId, options);
1764
- return this.db.doc(dbPath);
1765
- }
1766
- /**
1767
- * Gets the user's role from the project's ACL.
1768
- */
1769
- async getUserRole(email) {
1770
- if (!email) {
1771
- return null;
1772
- }
1773
- const docRef = this.db.doc(`Projects/${this.projectId}`);
1774
- const snapshot = await docRef.get();
1775
- const data = snapshot.data() || {};
1776
- const acl = data.roles || {};
1777
- if (email in acl) {
1778
- return acl[email];
1779
- }
1780
- if (!email.includes("@")) {
1781
- console.warn(`invalid email: ${email}`);
1782
- return null;
1783
- }
1784
- const domain = email.split("@").at(-1);
1785
- if (domain && `*@${domain}` in acl) {
1786
- return acl[`*@${domain}`];
1787
- }
1788
- return null;
1789
- }
1790
- /**
1791
- * Verifies user exists in the ACL list.
1792
- */
1793
- async userExistsInAcl(email) {
1794
- if (!email) {
1795
- return false;
1796
- }
1797
- const docRef = this.db.doc(`Projects/${this.projectId}`);
1798
- const snapshot = await docRef.get();
1799
- const data = snapshot.data() || {};
1800
- const acl = data.roles || {};
1801
- if (email in acl) {
1802
- return true;
1803
- }
1804
- if (!email.includes("@")) {
1805
- console.warn(`invalid email: ${email}`);
1806
- return false;
1807
- }
1808
- const domain = email.split("@").at(-1);
1809
- if (domain && `*@${domain}` in acl) {
1810
- return true;
1811
- }
1812
- return false;
1813
- }
1814
- async logAction(action, options) {
1815
- if (!action) {
1816
- throw new Error('missing required: "action"');
1817
- }
1818
- const data = {
1819
- action,
1820
- timestamp: Timestamp3.now(),
1821
- by: options?.by || "system",
1822
- metadata: options?.metadata || {}
1823
- };
1824
- if (options?.links) {
1825
- data.links = options.links;
1826
- }
1827
- const colRef = this.db.collection(`Projects/${this.projectId}/ActionLogs`);
1828
- await colRef.add(data);
1829
- const metaStr = options?.metadata ? stringifyObj(options.metadata) : "";
1830
- console.log(`[${data.timestamp.toMillis()}] action: ${action} ${metaStr}`);
1831
- const cmsPluginConfig = this.cmsPlugin.getConfig();
1832
- if (cmsPluginConfig.onAction) {
1833
- try {
1834
- cmsPluginConfig.onAction(data);
1835
- } catch (err) {
1836
- console.error(err);
1837
- }
1838
- }
1839
- }
1840
- /**
1841
- * Creates a batch request that is capable of fetching one or more docs,
1842
- * corresponding translations, and dataSources.
1843
- */
1844
- createBatchRequest(options) {
1845
- return new BatchRequest(this, options);
1846
- }
1847
- };
1848
- function isRichTextData(data) {
1849
- return Boolean(
1850
- isObject(data) && Array.isArray(data.blocks) && data.time && data.version
1851
- );
1852
- }
1853
- function getCmsPlugin(rootConfig) {
1854
- const plugins = rootConfig.plugins || [];
1855
- const plugin = plugins.find((plugin2) => plugin2.name === "root-cms");
1856
- if (!plugin) {
1857
- throw new Error("could not find root-cms plugin config in root.config.ts");
1858
- }
1859
- return plugin;
1860
- }
1861
- function marshalData(data) {
1862
- if (isRichTextData(data)) {
1863
- return data;
1864
- }
1865
- const result = {};
1866
- for (const key in data) {
1867
- const val = data[key];
1868
- if (isObject(val)) {
1869
- result[key] = marshalData(val);
1870
- } else if (Array.isArray(val)) {
1871
- if (val.length > 0 && val.some((item) => isObject(item))) {
1872
- const items = val.map((item) => {
1873
- if (isObject(item)) {
1874
- return marshalData(item);
1875
- }
1876
- return item;
1877
- });
1878
- result[key] = toArrayObject(items);
1879
- } else {
1880
- result[key] = val;
1881
- }
1882
- } else {
1883
- result[key] = val;
1884
- }
1885
- }
1886
- return result;
1887
- }
1888
- function unmarshalData(data) {
1889
- const result = {};
1890
- for (const key in data) {
1891
- const val = data[key];
1892
- if (isObject(val)) {
1893
- if (val.toMillis) {
1894
- result[key] = val.toMillis();
1895
- } else if (Object.hasOwn(val, "_array") && Array.isArray(val._array)) {
1896
- const arr = val._array.map((arrayKey) => {
1897
- return {
1898
- ...unmarshalData(val[arrayKey] || {}),
1899
- _arrayKey: arrayKey
1900
- };
1901
- });
1902
- result[key] = arr;
1903
- } else {
1904
- result[key] = unmarshalData(val);
1905
- }
1906
- } else {
1907
- result[key] = val;
1908
- }
1909
- }
1910
- return result;
1911
- }
1912
- function toArrayObject(arr) {
1913
- if (!Array.isArray(arr)) {
1914
- return arr;
1915
- }
1916
- const arrObject = { _array: [] };
1917
- for (const item of arr) {
1918
- const key = randString(6);
1919
- arrObject[key] = item;
1920
- arrObject._array.push(key);
1921
- }
1922
- return arrObject;
1923
- }
1924
- function isObject(data) {
1925
- return typeof data === "object" && !Array.isArray(data) && data !== null;
1926
- }
1927
- function randString(len) {
1928
- const result = [];
1929
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1930
- for (let i = 0; i < len; i++) {
1931
- const rand = Math.floor(Math.random() * chars.length);
1932
- result.push(chars.charAt(rand));
1933
- }
1934
- return result.join("");
1935
- }
1936
- function translationsForLocale(translationsMap, locale) {
1937
- const localeTranslations = {};
1938
- Object.values(translationsMap).forEach((string) => {
1939
- const source = string.source;
1940
- const translation = string[locale] || string.en || string.source;
1941
- localeTranslations[source] = translation;
1942
- });
1943
- return localeTranslations;
1944
- }
1945
- function stringifyObj(obj) {
1946
- function format(obj2) {
1947
- if (obj2 === null) {
1948
- return "null";
1949
- }
1950
- if (typeof obj2 === "undefined") {
1951
- return "undefined";
1952
- }
1953
- if (typeof obj2 === "string") {
1954
- return `"${obj2.replaceAll('"', '\\"')}"`;
1955
- }
1956
- if (typeof obj2 !== "object") {
1957
- return String(obj2);
1958
- }
1959
- if (Array.isArray(obj2)) {
1960
- return `[${obj2.map(format).join(", ")}]`;
1961
- }
1962
- const entries = Object.entries(obj2).map(([key, value]) => {
1963
- return `${key}: ${format(value)}`;
1964
- });
1965
- return `{${entries.join(", ")}}`;
1966
- }
1967
- return format(obj);
1968
- }
1969
- function parseDocId(docId) {
1970
- const sepIndex = docId.indexOf("/");
1971
- if (sepIndex <= 0) {
1972
- throw new Error(`invalid doc id: ${docId}`);
1973
- }
1974
- const collection = docId.slice(0, sepIndex);
1975
- const slug = docId.slice(sepIndex + 1).replaceAll("/", "--");
1976
- if (!collection || !slug) {
1977
- throw new Error(`invalid doc id: ${docId}`);
1978
- }
1979
- return { collection, slug };
1980
- }
1981
- var BatchRequest = class {
1982
- constructor(cmsClient, options) {
1983
- this.docIds = [];
1984
- this.dataSourceIds = [];
1985
- this.queries = [];
1986
- this.translationsIds = [];
1987
- this.cmsClient = cmsClient;
1988
- this.db = cmsClient.db;
1989
- this.options = options;
1990
- }
1991
- /**
1992
- * Adds a doc to the batch request.
1993
- */
1994
- addDoc(docId) {
1995
- this.docIds.push(docId);
1996
- }
1997
- /**
1998
- * Adds a data source to the batch request.
1999
- */
2000
- addDataSource(dataSourceId) {
2001
- this.dataSourceIds.push(dataSourceId);
2002
- }
2003
- /**
2004
- * Adds a collection-based query to the batch request.
2005
- */
2006
- addQuery(queryId, collectionId, queryOptions) {
2007
- this.queries.push({
2008
- queryId,
2009
- collectionId,
2010
- queryOptions
2011
- });
2012
- }
2013
- /**
2014
- * Adds a translation file to the request.
2015
- */
2016
- addTranslations(translationsId) {
2017
- this.translationsIds.push(translationsId);
2018
- }
2019
- /**
2020
- * Fetches data from the DB.
2021
- */
2022
- async fetch() {
2023
- const res = new BatchResponse();
2024
- const promises = [
2025
- this.fetchDocs(res),
2026
- this.fetchQueries(res),
2027
- this.fetchDataSources(res)
2028
- ];
2029
- if (!this.options.translate && this.translationsIds.length > 0) {
2030
- promises.push(this.fetchTranslations(res));
2031
- }
2032
- await Promise.all(promises);
2033
- if (this.translationsIds.length > 0) {
2034
- await this.fetchTranslations(res);
2035
- }
2036
- return res;
2037
- }
2038
- async fetchDocs(res) {
2039
- if (this.docIds.length === 0) {
2040
- return;
2041
- }
2042
- const docRefs = this.docIds.map((docId) => {
2043
- const [collectionId, slug] = docId.split("/");
2044
- return this.cmsClient.dbDocRef(collectionId, slug, {
2045
- mode: this.options.mode
2046
- });
2047
- });
2048
- const docs = await this.db.getAll(...docRefs);
2049
- this.docIds.forEach((docId, i) => {
2050
- const doc = docs[i];
2051
- if (!doc.exists) {
2052
- console.warn(`doc "${docId}" does not exist`);
2053
- return;
2054
- }
2055
- const docData = unmarshalData(doc.data());
2056
- res.docs[docId] = docData;
2057
- if (this.options.translate) {
2058
- this.addTranslations(docId);
2059
- }
2060
- });
2061
- }
2062
- async fetchQueries(res) {
2063
- if (this.queries.length === 0) {
2064
- return;
2065
- }
2066
- const mode = this.options.mode;
2067
- const handleQuery = async (queryItem) => {
2068
- const docsPath = this.cmsClient.dbCollectionDocsPath(
2069
- queryItem.collectionId,
2070
- { mode }
2071
- );
2072
- const queryOptions = queryItem.queryOptions || {};
2073
- let query = this.db.collection(docsPath);
2074
- if (queryOptions.limit) {
2075
- query = query.limit(queryOptions.limit);
2076
- }
2077
- if (queryOptions.offset) {
2078
- query = query.offset(queryOptions.offset);
2079
- }
2080
- if (queryOptions.orderBy) {
2081
- query = query.orderBy(
2082
- queryOptions.orderBy,
2083
- queryOptions.orderByDirection
2084
- );
2085
- }
2086
- if (queryOptions.query) {
2087
- query = queryOptions.query(query);
2088
- }
2089
- const results = await query.get();
2090
- const docs = [];
2091
- results.forEach((result) => {
2092
- const doc = unmarshalData(result.data());
2093
- docs.push(doc);
2094
- });
2095
- res.queries[queryItem.queryId] = docs;
2096
- };
2097
- await Promise.all(this.queries.map((queryItem) => handleQuery(queryItem)));
2098
- }
2099
- async fetchDataSources(res) {
2100
- if (this.dataSourceIds.length === 0) {
2101
- return;
2102
- }
2103
- const docRefs = this.dataSourceIds.map((dataSourceId) => {
2104
- return this.cmsClient.dbDataSourceDataRef(dataSourceId, {
2105
- mode: this.options.mode
2106
- });
2107
- });
2108
- const docs = await this.db.getAll(...docRefs);
2109
- this.dataSourceIds.forEach((dataSourceId, i) => {
2110
- const doc = docs[i];
2111
- if (!doc.exists) {
2112
- console.warn(`"data source "${dataSourceId}" does not exist`);
2113
- return;
2114
- }
2115
- res.dataSources[dataSourceId] = doc.data();
2116
- });
2117
- }
2118
- async fetchTranslations(res) {
2119
- if (this.translationsIds.length === 0) {
2120
- return;
2121
- }
2122
- const docRefs = this.translationsIds.map((translationsId) => {
2123
- return this.cmsClient.dbTranslationsRef(translationsId, {
2124
- mode: this.options.mode
2125
- });
2126
- });
2127
- const docs = await this.db.getAll(...docRefs);
2128
- this.translationsIds.forEach((translationsId, i) => {
2129
- const doc = docs[i];
2130
- if (!doc.exists) {
2131
- return;
2132
- }
2133
- res.translations[translationsId] = doc.data();
2134
- });
2135
- }
2136
- };
2137
- var BatchResponse = class {
2138
- constructor() {
2139
- this.docs = {};
2140
- this.queries = {};
2141
- this.dataSources = {};
2142
- this.translations = {};
2143
- }
2144
- /**
2145
- * Returns a map of translations for a given locale or locale fallbacks.
2146
- *
2147
- * The input is either a single locale (e.g. "de") or an array of locales
2148
- * representing the fallback tree, e.g. ["en-CA", "en-GB", "en"].
2149
- *
2150
- * TODO(stevenle): support the locale fallback tree.
2151
- *
2152
- * The returned value is a flat map of source string to translated string,
2153
- * e.g.:
2154
- * {"<source>": "<translation>"}
2155
- */
2156
- getTranslations(locale) {
2157
- const translationsMap = this.getTranslationsMap();
2158
- const translations = translationsForLocale(translationsMap, locale);
2159
- return translations;
2160
- }
2161
- /**
2162
- * Merges the strings from all translations files retrieved in the request.
2163
- * The returned value is a map of string to translations, e.g.:
2164
- *
2165
- * {"<hash>": {"source": "<source>", "<locale>": "<translation>"}}
2166
- */
2167
- getTranslationsMap() {
2168
- const translationsDocs = Object.values(this.translations).reverse();
2169
- const translationsMap = {};
2170
- for (const translationsDoc of translationsDocs) {
2171
- const strings = translationsDoc.strings || {};
2172
- for (const hash in strings) {
2173
- const translations = strings[hash];
2174
- translationsMap[hash] ??= { source: translations.source };
2175
- for (const locale in translations) {
2176
- if (locale !== "source" && translations[locale]) {
2177
- translationsMap[hash][locale] = translations[locale];
2178
- }
2179
- }
2180
- }
2181
- }
2182
- return translationsMap;
2183
- }
2184
- };
1
+ import {
2
+ ChatClient,
3
+ summarizeDiff
4
+ } from "./chunk-ZOEPOKGE.js";
5
+ import {
6
+ getServerVersion
7
+ } from "./chunk-Y65VGJLE.js";
8
+ import {
9
+ runCronJobs
10
+ } from "./chunk-SI44FG3H.js";
11
+ import {
12
+ RootCMSClient,
13
+ parseDocId,
14
+ unmarshalData
15
+ } from "./chunk-62EVNFXB.js";
16
+ import "./chunk-MLKGABMK.js";
2185
17
 
2186
- // core/versions.ts
18
+ // core/plugin.ts
19
+ import { promises as fs2 } from "fs";
2187
20
  import path2 from "path";
2188
- import { Timestamp as Timestamp4 } from "firebase-admin/firestore";
2189
- import glob from "tiny-glob";
2190
- var DOCUMENT_SAVE_OFFSET = 5 * 60 * 1e3;
2191
- var VersionsService = class {
2192
- constructor(rootConfig) {
2193
- this.rootConfig = rootConfig;
2194
- const cmsPlugin2 = getCmsPlugin(rootConfig);
2195
- const cmsPluginOptions = cmsPlugin2.getConfig();
2196
- const projectId = cmsPluginOptions.id || "default";
2197
- this.projectId = projectId;
2198
- this.db = cmsPlugin2.getFirestore();
2199
- }
2200
- /**
2201
- * Saves a version of all documents that have been edited since the last run.
2202
- */
2203
- async saveVersions() {
2204
- const lastRun = await this.getLastRun();
2205
- const lastRunWithOffset = lastRun === 0 ? lastRun : lastRun - DOCUMENT_SAVE_OFFSET;
2206
- const changedDocs = await this.getDocsModifiedAfter(lastRunWithOffset);
2207
- const now = Timestamp4.now().toMillis();
2208
- const versions = changedDocs.filter((doc) => {
2209
- const modifiedAt = doc.sys.modifiedAt.toMillis();
2210
- return modifiedAt <= now - DOCUMENT_SAVE_OFFSET;
2211
- });
2212
- if (versions.length > 0) {
2213
- this.saveVersionsToFirestore(versions);
2214
- }
2215
- this.saveLastRun(now);
2216
- }
2217
- async saveVersionsToFirestore(versions) {
2218
- const batch = this.db.batch();
2219
- versions.forEach((version) => {
2220
- if (!version.collection || !version.slug || !version.sys?.modifiedAt) {
2221
- return;
2222
- }
2223
- const modifiedAtMillis = version.sys.modifiedAt.toMillis();
2224
- const versionPath = `Projects/${this.projectId}/Collections/${version.collection}/Drafts/${version.slug}/Versions/${modifiedAtMillis}`;
2225
- console.log(versionPath);
2226
- const versionRef = this.db.doc(versionPath);
2227
- batch.set(versionRef, version);
2228
- });
2229
- await batch.commit();
2230
- console.log(`versions: saved ${versions.length} versions`);
2231
- }
2232
- /**
2233
- * Returns the last time (in millis) saveVersions() was run, or 0 if has
2234
- * never been run.
2235
- */
2236
- async getLastRun() {
2237
- const projectDocRef = this.db.collection("Projects").doc(this.projectId);
2238
- const projectDoc = await projectDocRef.get();
2239
- if (projectDoc.exists) {
2240
- const data = projectDoc.data() || {};
2241
- const ts = data.versionsLastRun;
2242
- if (ts) {
2243
- return ts.toMillis();
2244
- }
2245
- }
2246
- return 0;
2247
- }
2248
- /**
2249
- * Saves {versionLastRun: <timestamp>} to the Projects/<projectId> doc.
2250
- */
2251
- async saveLastRun(millis) {
2252
- const ts = Timestamp4.fromMillis(millis);
2253
- const projectDocRef = this.db.collection("Projects").doc(this.projectId);
2254
- await projectDocRef.set({ versionsLastRun: ts }, { merge: true });
2255
- }
21
+ import { fileURLToPath } from "url";
22
+ import { viteSsrLoadModule } from "@blinkk/root/node";
23
+ import bodyParser from "body-parser";
24
+ import {
25
+ applicationDefault,
26
+ getApp,
27
+ initializeApp
28
+ } from "firebase-admin/app";
29
+ import { getAuth } from "firebase-admin/auth";
30
+ import { getFirestore } from "firebase-admin/firestore";
31
+ import * as jsonwebtoken from "jsonwebtoken";
32
+ import sirv from "sirv";
33
+
34
+ // shared/sse.ts
35
+ var SSEEvent = {
2256
36
  /**
2257
- * Returns a list of all docs that were edited after a certain time.
37
+ * First connection event.
2258
38
  */
2259
- async getDocsModifiedAfter(millis) {
2260
- const ts = Timestamp4.fromMillis(millis);
2261
- const results = [];
2262
- const collectionIds = await this.listCollections();
2263
- for (const collectionId of collectionIds) {
2264
- const collectionPath = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
2265
- const query = this.db.collection(collectionPath).where("sys.modifiedAt", ">=", ts);
2266
- const querySnapshot = await query.get();
2267
- querySnapshot.forEach((doc) => {
2268
- results.push(doc.data());
2269
- });
2270
- }
2271
- return results;
2272
- }
39
+ CONNECTED: "connected",
2273
40
  /**
2274
- * Returns a list of collection ids for the Root project.
41
+ * Changes to any .schema.ts file.
2275
42
  */
2276
- async listCollections() {
2277
- const collectionIds = [];
2278
- const collectionFileNames = await glob("*.schema.ts", {
2279
- cwd: path2.join(this.rootConfig.rootDir, "collections")
2280
- });
2281
- collectionFileNames.forEach((filename) => {
2282
- collectionIds.push(filename.slice(0, -10));
2283
- });
2284
- return collectionIds;
2285
- }
43
+ SCHEMA_CHANGED: "schemaChanged"
2286
44
  };
2287
45
 
2288
- // core/cron.ts
2289
- async function runCronJobs(rootConfig) {
2290
- await Promise.all([
2291
- runCronJob("publishScheduledDocs", runPublishScheduledDocs, rootConfig),
2292
- runCronJob("saveVersions", runSaveVersions, rootConfig)
2293
- ]);
2294
- }
2295
- async function runCronJob(name, fn, ...args) {
2296
- try {
2297
- await fn(...args);
2298
- } catch (err) {
2299
- console.log(`cron failed: ${name}`);
2300
- console.error(String(err.stack || err));
2301
- throw err;
2302
- }
2303
- }
2304
- async function runPublishScheduledDocs(rootConfig) {
2305
- const cmsClient = new RootCMSClient(rootConfig);
2306
- await cmsClient.publishScheduledDocs();
2307
- await cmsClient.publishScheduledReleases();
2308
- }
2309
- async function runSaveVersions(rootConfig) {
2310
- const service = new VersionsService(rootConfig);
2311
- await service.saveVersions();
2312
- }
46
+ // core/api.ts
47
+ import { promises as fs } from "fs";
48
+ import path from "path";
49
+ import { multipartMiddleware } from "@blinkk/root/middleware";
2313
50
 
2314
51
  // core/csv.ts
2315
52
  import { parse as csvParse } from "csv-parse/sync";
@@ -2379,13 +116,13 @@ function api(server, options) {
2379
116
  return await app.getCollection(collectionId);
2380
117
  }
2381
118
  try {
2382
- const schemaPath = path3.join(
119
+ const schemaPath = path.join(
2383
120
  req.rootConfig.rootDir,
2384
121
  "dist",
2385
122
  "collections",
2386
123
  `${collectionId}.schema.json`
2387
124
  );
2388
- const contents = await fs2.readFile(schemaPath, "utf8");
125
+ const contents = await fs.readFile(schemaPath, "utf8");
2389
126
  return JSON.parse(contents);
2390
127
  } catch (err) {
2391
128
  if (err && err.code === "ENOENT") {
@@ -2636,8 +373,8 @@ function api(server, options) {
2636
373
  }
2637
374
  try {
2638
375
  const cmsClient = new RootCMSClient(req.rootConfig);
2639
- const { generateImage: generateImage2 } = await Promise.resolve().then(() => (init_ai(), ai_exports));
2640
- const imageUrl = await generateImage2(cmsClient, {
376
+ const { generateImage } = await import("./ai-DRQJXU4N.js");
377
+ const imageUrl = await generateImage(cmsClient, {
2641
378
  prompt,
2642
379
  aspectRatio
2643
380
  });
@@ -2650,196 +387,6 @@ function api(server, options) {
2650
387
  );
2651
388
  }
2652
389
 
2653
- // package.json
2654
- var package_default = {
2655
- name: "@blinkk/root-cms",
2656
- version: "2.4.8",
2657
- author: "s@blinkk.com",
2658
- license: "MIT",
2659
- engines: {
2660
- node: ">=16.0.0"
2661
- },
2662
- repository: {
2663
- type: "git",
2664
- url: "git+https://github.com/blinkk/rootjs.git",
2665
- directory: "packages/root-cms"
2666
- },
2667
- files: [
2668
- "dist/**/*"
2669
- ],
2670
- bin: {
2671
- "root-cms": "./bin/root-cms.js"
2672
- },
2673
- type: "module",
2674
- module: "./dist/index.js",
2675
- types: "./dist/index.d.ts",
2676
- exports: {
2677
- ".": {
2678
- types: "./dist/core.d.ts",
2679
- import: "./dist/core.js"
2680
- },
2681
- "./client": {
2682
- types: "./dist/client.d.ts",
2683
- import: "./dist/client.js"
2684
- },
2685
- "./core": {
2686
- types: "./dist/core.d.ts",
2687
- import: "./dist/core.js"
2688
- },
2689
- "./functions": {
2690
- types: "./dist/functions.d.ts",
2691
- import: "./dist/functions.js"
2692
- },
2693
- "./plugin": {
2694
- types: "./dist/plugin.d.ts",
2695
- import: "./dist/plugin.js"
2696
- },
2697
- "./project": {
2698
- types: "./dist/project.d.ts",
2699
- import: "./dist/project.js"
2700
- },
2701
- "./richtext": {
2702
- types: "./dist/richtext.d.ts",
2703
- import: "./dist/richtext.js"
2704
- }
2705
- },
2706
- scripts: {
2707
- build: 'rm -rf dist && concurrently -n "core,signin,ui" npm:build:core npm:build:signin npm:build:ui',
2708
- "build:core": "tsup-node --config=./core/tsup.config.ts",
2709
- "//": "NOTE: esbuild is used here because tsup doesn't currently support aliases.",
2710
- "build:ui": "esbuild ui/ui.tsx --bundle --minify --alias:react=@preact/compat --alias:react-dom=@preact/compat --tsconfig=ui/tsconfig.json --outdir=dist/ui --legal-comments=external",
2711
- "build:signin": "esbuild signin/signin.tsx --bundle --minify --tsconfig=signin/tsconfig.json --outdir=dist/ui --legal-comments=external",
2712
- dev: 'rm -rf dist && concurrently -k -n "core,ui" npm:dev:core npm:dev:signin npm:dev:ui',
2713
- "dev:core": "pnpm build:core --watch",
2714
- "dev:signin": "pnpm build:signin --watch",
2715
- "dev:ui": "pnpm build:ui --watch",
2716
- test: "pnpm build && firebase emulators:exec 'vitest run --exclude=**/*.visual.test.tsx'",
2717
- "test:visual": "vitest run --config=vitest.config.visual.ts",
2718
- "test:watch": "pnpm build && firebase emulators:exec 'vitest'"
2719
- },
2720
- dependencies: {
2721
- "@ag-grid-community/client-side-row-model": "32.3.9",
2722
- "@ag-grid-community/core": "32.3.9",
2723
- "@ag-grid-community/react": "32.3.9",
2724
- "@ag-grid-community/styles": "32.3.9",
2725
- "@genkit-ai/ai": "1.26.0",
2726
- "@genkit-ai/core": "1.26.0",
2727
- "@genkit-ai/google-genai": "1.26.0",
2728
- "@google-cloud/firestore": "7.11.3",
2729
- "@hello-pangea/dnd": "18.0.1",
2730
- "@types/cli-progress": "3.11.6",
2731
- "body-parser": "1.20.2",
2732
- "cli-progress": "3.12.0",
2733
- commander: "11.0.0",
2734
- "csv-parse": "5.5.2",
2735
- "csv-stringify": "6.4.4",
2736
- "date-fns": "4.1.0",
2737
- "date-fns-tz": "3.2.0",
2738
- diff: "8.0.2",
2739
- "dts-dom": "3.7.0",
2740
- "fnv-plus": "1.3.1",
2741
- genkit: "1.26.0",
2742
- jsonwebtoken: "9.0.2",
2743
- kleur: "4.1.5",
2744
- sirv: "2.0.3",
2745
- "tiny-glob": "0.2.9",
2746
- zod: "3.23.8"
2747
- },
2748
- "//": "NOTE(stevenle): due to compat issues with mantine and preact, mantine is pinned to v4.2.12",
2749
- devDependencies: {
2750
- "@babel/core": "7.17.9",
2751
- "@blinkk/root": "workspace:*",
2752
- "@editorjs/editorjs": "2.30.8",
2753
- "@editorjs/header": "2.8.8",
2754
- "@editorjs/image": "2.10.2",
2755
- "@editorjs/list": "2.0.6",
2756
- "@editorjs/nested-list": "1.4.3",
2757
- "@editorjs/raw": "2.5.1",
2758
- "@editorjs/table": "2.4.4",
2759
- "@editorjs/underline": "1.2.1",
2760
- "@emotion/react": "11.10.5",
2761
- "@firebase/app-compat": "0.5.2",
2762
- "@firebase/app-types": "0.9.3",
2763
- "@firebase/rules-unit-testing": "5.0.0",
2764
- "@lexical/code": "0.33.1",
2765
- "@lexical/html": "0.33.1",
2766
- "@lexical/link": "0.33.1",
2767
- "@lexical/list": "0.33.1",
2768
- "@lexical/markdown": "0.33.1",
2769
- "@lexical/react": "0.33.1",
2770
- "@lexical/rich-text": "0.33.1",
2771
- "@lexical/selection": "0.33.1",
2772
- "@lexical/table": "0.33.1",
2773
- "@lexical/utils": "0.33.1",
2774
- "@mantine/core": "4.2.12",
2775
- "@mantine/hooks": "4.2.12",
2776
- "@mantine/modals": "4.2.12",
2777
- "@mantine/notifications": "4.2.12",
2778
- "@mantine/spotlight": "4.2.12",
2779
- "@preact/compat": "18.3.1",
2780
- "@tabler/icons-preact": "3.35.0",
2781
- "@testing-library/preact": "3.2.4",
2782
- "@testing-library/user-event": "14.6.1",
2783
- "@types/body-parser": "1.19.3",
2784
- "@types/fnv-plus": "1.3.2",
2785
- "@types/gapi": "0.0.47",
2786
- "@types/gapi.client.drive-v3": "0.0.4",
2787
- "@types/gapi.client.sheets-v4": "0.0.4",
2788
- "@types/google.accounts": "0.0.14",
2789
- "@types/jsonwebtoken": "9.0.1",
2790
- "@types/node": "24.3.1",
2791
- "@vitest/browser": "4.0.10",
2792
- "@vitest/browser-playwright": "4.0.10",
2793
- concurrently: "7.6.0",
2794
- esbuild: "0.25.9",
2795
- firebase: "12.2.1",
2796
- "firebase-admin": "13.5.0",
2797
- "firebase-functions": "6.4.0",
2798
- "firebase-tools": "14.15.2",
2799
- "highlight.js": "11.6.0",
2800
- jsdom: "27.2.0",
2801
- "json-diff-kit": "1.0.29",
2802
- lexical: "0.33.1",
2803
- marked: "9.1.1",
2804
- "mdast-util-from-markdown": "2.0.1",
2805
- "mdast-util-gfm": "3.0.0",
2806
- "micromark-extension-gfm": "3.0.0",
2807
- playwright: "1.56.1",
2808
- preact: "10.27.1",
2809
- "preact-render-to-string": "6.6.1",
2810
- "preact-router": "4.1.2",
2811
- react: "npm:@preact/compat@18.3.1",
2812
- "react-dom": "npm:@preact/compat@18.3.1",
2813
- "react-json-view-compare": "2.0.2",
2814
- tsup: "8.5.0",
2815
- typescript: "5.9.2",
2816
- vite: "7.1.4",
2817
- vitest: "4.0.10",
2818
- yjs: "13.6.27"
2819
- },
2820
- peerDependencies: {
2821
- "@blinkk/root": "2.4.8",
2822
- "firebase-admin": ">=11",
2823
- "firebase-functions": ">=4",
2824
- preact: ">=10",
2825
- "preact-render-to-string": ">=5"
2826
- },
2827
- peerDependenciesMeta: {
2828
- "firebase-functions": {
2829
- optional: true
2830
- }
2831
- }
2832
- };
2833
-
2834
- // core/server-version.ts
2835
- var SERVER_STARTUP_TS = String(Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3));
2836
- function getServerVersion() {
2837
- if (process.env.NODE_ENV === "development") {
2838
- return SERVER_STARTUP_TS;
2839
- }
2840
- return package_default?.version || "root-3.0.0";
2841
- }
2842
-
2843
390
  // core/sse.ts
2844
391
  function sse(server) {
2845
392
  const sseClients = /* @__PURE__ */ new Set();
@@ -2854,6 +401,10 @@ function sse(server) {
2854
401
  });
2855
402
  };
2856
403
  server.use("/cms/api/sse.connect", async (req, res) => {
404
+ if (!req.user) {
405
+ res.status(401).json({ success: false, error: "NOT_AUTHORIZED" });
406
+ return;
407
+ }
2857
408
  res.writeHead(200, {
2858
409
  "Content-Type": "text/event-stream",
2859
410
  "Cache-Control": "no-cache",
@@ -2888,29 +439,29 @@ function formatMessage(event, data) {
2888
439
  }
2889
440
 
2890
441
  // core/plugin.ts
2891
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
442
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
2892
443
  async function writeCollectionSchemasToJson(rootConfig) {
2893
- const modulePath = path5.resolve(__dirname2, "./project.js");
2894
- const project = await viteSsrLoadModule2(
444
+ const modulePath = path2.resolve(__dirname, "./project.js");
445
+ const project = await viteSsrLoadModule(
2895
446
  rootConfig,
2896
447
  modulePath
2897
448
  );
2898
- const outDir = path5.join(rootConfig.rootDir, "dist", "collections");
2899
- await fs4.mkdir(outDir, { recursive: true });
449
+ const outDir = path2.join(rootConfig.rootDir, "dist", "collections");
450
+ await fs2.mkdir(outDir, { recursive: true });
2900
451
  const fileIds = Object.keys(project.SCHEMA_MODULES);
2901
452
  for (const fileId of fileIds) {
2902
453
  if (!fileId.startsWith("/collections/")) {
2903
454
  continue;
2904
455
  }
2905
- const collectionId = path5.basename(fileId).split(".")[0];
456
+ const collectionId = path2.basename(fileId).split(".")[0];
2906
457
  const schema = await project.getCollectionSchema(collectionId);
2907
458
  if (!schema) {
2908
459
  console.warn(`collection is missing a default export: ${fileId}`);
2909
460
  continue;
2910
461
  }
2911
- const jsonPath = path5.join(outDir, `${collectionId}.schema.json`);
462
+ const jsonPath = path2.join(outDir, `${collectionId}.schema.json`);
2912
463
  const data = JSON.stringify(schema, null, 2);
2913
- await fs4.writeFile(jsonPath, data, "utf-8");
464
+ await fs2.writeFile(jsonPath, data, "utf-8");
2914
465
  }
2915
466
  }
2916
467
  var SESSION_COOKIE_AUTH = "root-cms-auth";
@@ -3128,7 +679,7 @@ function cmsPlugin(options) {
3128
679
  */
3129
680
  ssrInput: () => {
3130
681
  return {
3131
- cms: path5.resolve(__dirname2, "./app.js")
682
+ cms: path2.resolve(__dirname, "./app.js")
3132
683
  };
3133
684
  },
3134
685
  /**
@@ -3138,13 +689,13 @@ function cmsPlugin(options) {
3138
689
  server.use(bodyParser.json());
3139
690
  async function getRenderer(req) {
3140
691
  if (serverOptions.type === "dev") {
3141
- const appFilePath = path5.resolve(__dirname2, "./app.js");
692
+ const appFilePath = path2.resolve(__dirname, "./app.js");
3142
693
  const app3 = await req.viteServer.ssrLoadModule(
3143
694
  appFilePath
3144
695
  );
3145
696
  return app3;
3146
697
  }
3147
- const appImportPath = path5.resolve(
698
+ const appImportPath = path2.resolve(
3148
699
  req.rootConfig.rootDir,
3149
700
  "dist/server/cms.js"
3150
701
  );
@@ -3216,7 +767,7 @@ function cmsPlugin(options) {
3216
767
  }
3217
768
  next();
3218
769
  });
3219
- const staticDir = path5.resolve(__dirname2, "ui");
770
+ const staticDir = path2.resolve(__dirname, "ui");
3220
771
  server.use(
3221
772
  "/cms/static",
3222
773
  sirv(staticDir, { dev: process.env.NODE_ENV === "development" })
@@ -3263,13 +814,13 @@ function cmsPlugin(options) {
3263
814
  if (process.env.NODE_ENV === "development" && options.watch !== false) {
3264
815
  plugin.onFileChange = (eventName, filepath) => {
3265
816
  if (filepath.endsWith(".schema.ts")) {
3266
- Promise.resolve().then(() => (init_generate_types(), generate_types_exports)).then((generateTypesModule) => {
3267
- const generateTypes2 = generateTypesModule.generateTypes;
3268
- generateTypes2();
817
+ import("./generate-types-TQBCE2SG.js").then((generateTypesModule) => {
818
+ const generateTypes = generateTypesModule.generateTypes;
819
+ generateTypes();
3269
820
  });
3270
821
  if (sseBroadcast) {
3271
822
  const eventData = {
3272
- file: path5.basename(filepath)
823
+ file: path2.basename(filepath)
3273
824
  };
3274
825
  sseBroadcast(SSEEvent.SCHEMA_CHANGED, eventData);
3275
826
  }
@@ -3279,7 +830,7 @@ function cmsPlugin(options) {
3279
830
  return plugin;
3280
831
  }
3281
832
  function fileExists(filepath) {
3282
- return fs4.access(filepath).then(() => true).catch(() => false);
833
+ return fs2.access(filepath).then(() => true).catch(() => false);
3283
834
  }
3284
835
  export {
3285
836
  cmsPlugin