@orion-studios/cms 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/analytics/react.d.ts +53 -0
- package/dist/analytics/react.js +195 -0
- package/dist/blocks/index.d.ts +222 -0
- package/dist/blocks/index.js +338 -0
- package/dist/chunk-HVJCF2IZ.js +76 -0
- package/dist/chunk-VPUODCNH.js +448 -0
- package/dist/chunk-WQDHEQDE.js +527 -0
- package/dist/content/index.d.ts +51 -0
- package/dist/content/index.js +8 -0
- package/dist/forms/index.d.ts +70 -0
- package/dist/forms/index.js +38 -0
- package/dist/forms/react.d.ts +45 -0
- package/dist/forms/react.js +8 -0
- package/dist/server/index.d.ts +403 -0
- package/dist/server/index.js +2280 -0
- package/dist/studio/index.d.ts +534 -0
- package/dist/studio/index.js +3824 -0
- package/dist/studio/styles.css +444 -0
- package/dist/submission-BKdBedOe.d.ts +61 -0
- package/dist/submission-CzrfXu17.d.ts +30 -0
- package/package.json +97 -0
- package/sql/bootstrap.sql +458 -0
- package/sql/migrations/0001_atomic_scheduled_publish.sql +68 -0
- package/sql/migrations/0002_rate_limits.sql +62 -0
- package/sql/migrations/0003_atomic_global_update.sql +46 -0
- package/sql/migrations/0004_analytics_visitor_tracking.sql +8 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// src/blocks/deriveEditor.ts
|
|
2
|
+
import { z as z2 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/blocks/fieldTypes.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var LINK_MARKER = "__orion:link__";
|
|
7
|
+
var MEDIA_MARKER = "__orion:media__";
|
|
8
|
+
var RICHTEXT_MARKER = "__orion:richtext__";
|
|
9
|
+
function link() {
|
|
10
|
+
return z.object({
|
|
11
|
+
label: z.string().default(""),
|
|
12
|
+
href: z.string().default(""),
|
|
13
|
+
variant: z.enum(["solid", "outline", "line"]).default("solid")
|
|
14
|
+
}).describe(LINK_MARKER);
|
|
15
|
+
}
|
|
16
|
+
function mediaRef() {
|
|
17
|
+
return z.object({
|
|
18
|
+
mediaId: z.string().nullable().default(null),
|
|
19
|
+
src: z.string().default(""),
|
|
20
|
+
alt: z.string().default(""),
|
|
21
|
+
caption: z.string().default("")
|
|
22
|
+
}).describe(MEDIA_MARKER);
|
|
23
|
+
}
|
|
24
|
+
function paragraphs() {
|
|
25
|
+
return z.array(z.string()).default([]).describe(RICHTEXT_MARKER);
|
|
26
|
+
}
|
|
27
|
+
var FILE_MARKER = "__orion:file__";
|
|
28
|
+
function fileRef() {
|
|
29
|
+
return z.object({
|
|
30
|
+
mediaId: z.string().nullable().default(null),
|
|
31
|
+
src: z.string().default(""),
|
|
32
|
+
filename: z.string().default("")
|
|
33
|
+
}).describe(FILE_MARKER);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/blocks/deriveEditor.ts
|
|
37
|
+
var TEXTAREA_KEY_HINTS = [
|
|
38
|
+
"answer",
|
|
39
|
+
"body",
|
|
40
|
+
"caption",
|
|
41
|
+
"description",
|
|
42
|
+
"intro",
|
|
43
|
+
"lead",
|
|
44
|
+
"message",
|
|
45
|
+
"note",
|
|
46
|
+
"quote",
|
|
47
|
+
"subtitle",
|
|
48
|
+
"text"
|
|
49
|
+
];
|
|
50
|
+
var ITEM_LABEL_KEY_PRIORITY = [
|
|
51
|
+
"title",
|
|
52
|
+
"label",
|
|
53
|
+
"caption",
|
|
54
|
+
"question",
|
|
55
|
+
"name",
|
|
56
|
+
"heading",
|
|
57
|
+
"value",
|
|
58
|
+
"alt",
|
|
59
|
+
"filename",
|
|
60
|
+
"description"
|
|
61
|
+
];
|
|
62
|
+
var labelForKey = (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
63
|
+
var unwrap = (schema) => {
|
|
64
|
+
let current = schema;
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
if (current instanceof z2.ZodDefault) {
|
|
67
|
+
current = current._def.innerType;
|
|
68
|
+
} else if (current instanceof z2.ZodOptional || current instanceof z2.ZodNullable) {
|
|
69
|
+
current = current.unwrap();
|
|
70
|
+
} else {
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var markerOf = (schema) => schema.description || unwrap(schema).description;
|
|
76
|
+
var defaultsOf = (schema) => {
|
|
77
|
+
const parsed = schema.safeParse(void 0);
|
|
78
|
+
if (parsed.success) return parsed.data;
|
|
79
|
+
const inner = unwrap(schema);
|
|
80
|
+
if (inner instanceof z2.ZodObject) {
|
|
81
|
+
const objectParsed = inner.safeParse({});
|
|
82
|
+
if (objectParsed.success) return objectParsed.data;
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
};
|
|
86
|
+
function deriveField(key, schema) {
|
|
87
|
+
const label = labelForKey(key);
|
|
88
|
+
const marker = markerOf(schema);
|
|
89
|
+
if (marker === LINK_MARKER) return { key, label, input: "link" };
|
|
90
|
+
if (marker === MEDIA_MARKER) return { key, label, input: "media" };
|
|
91
|
+
if (marker === FILE_MARKER) return { key, label, input: "file" };
|
|
92
|
+
if (marker === RICHTEXT_MARKER) return { key, label, input: "paragraphs" };
|
|
93
|
+
const inner = unwrap(schema);
|
|
94
|
+
if (inner instanceof z2.ZodString) {
|
|
95
|
+
const lower = key.toLowerCase();
|
|
96
|
+
const isLong = TEXTAREA_KEY_HINTS.some((hint) => lower === hint || lower.endsWith(hint));
|
|
97
|
+
return { key, label, input: isLong ? "textarea" : "text", inline: true };
|
|
98
|
+
}
|
|
99
|
+
if (inner instanceof z2.ZodNumber) return { key, label, input: "number" };
|
|
100
|
+
if (inner instanceof z2.ZodBoolean) return { key, label, input: "checkbox" };
|
|
101
|
+
if (inner instanceof z2.ZodEnum) {
|
|
102
|
+
const values = inner._def.values;
|
|
103
|
+
return {
|
|
104
|
+
key,
|
|
105
|
+
label,
|
|
106
|
+
input: "select",
|
|
107
|
+
options: values.map((value) => ({ label: labelForKey(value), value }))
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (inner instanceof z2.ZodArray) {
|
|
111
|
+
const element = unwrap(inner._def.type);
|
|
112
|
+
if (element instanceof z2.ZodString) {
|
|
113
|
+
return { key, label, input: "stringList" };
|
|
114
|
+
}
|
|
115
|
+
if (element instanceof z2.ZodObject) {
|
|
116
|
+
const itemFields = deriveFields(element);
|
|
117
|
+
const itemTemplate = defaultsOf(element) || {};
|
|
118
|
+
const itemLabelKey = ITEM_LABEL_KEY_PRIORITY.map((key2) => itemFields.find((field) => field.key === key2)).find(Boolean)?.key || itemFields.find((field) => field.input === "text" || field.input === "textarea")?.key || itemFields[0]?.key;
|
|
119
|
+
return { key, label, input: "itemList", itemFields, itemTemplate, itemLabelKey };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (inner instanceof z2.ZodObject) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function deriveFields(schema) {
|
|
129
|
+
const fields = [];
|
|
130
|
+
for (const [key, value] of Object.entries(schema.shape)) {
|
|
131
|
+
const fieldSchema = value;
|
|
132
|
+
const derived = deriveField(key, fieldSchema);
|
|
133
|
+
if (derived) {
|
|
134
|
+
fields.push(derived);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const inner = unwrap(fieldSchema);
|
|
138
|
+
if (inner instanceof z2.ZodObject && !markerOf(fieldSchema)) {
|
|
139
|
+
for (const nested of deriveFields(inner)) {
|
|
140
|
+
fields.push({
|
|
141
|
+
...nested,
|
|
142
|
+
key: `${key}.${nested.key}`,
|
|
143
|
+
label: `${labelForKey(key)} ${nested.label}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return fields;
|
|
149
|
+
}
|
|
150
|
+
function mergeEditorFields(derived, overrides) {
|
|
151
|
+
if (!overrides || overrides.length === 0) return derived;
|
|
152
|
+
const byKey = new Map(derived.map((field) => [field.key, field]));
|
|
153
|
+
const ordered = [];
|
|
154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
155
|
+
for (const override of overrides) {
|
|
156
|
+
const base = byKey.get(override.key);
|
|
157
|
+
const merged = {
|
|
158
|
+
key: override.key,
|
|
159
|
+
label: override.label || base?.label || labelForKey(override.key),
|
|
160
|
+
input: override.input || base?.input || "text",
|
|
161
|
+
...override.inline !== void 0 ? { inline: override.inline } : base?.inline !== void 0 ? { inline: base.inline } : {},
|
|
162
|
+
...override.options || base?.options ? { options: override.options || base?.options } : {},
|
|
163
|
+
...override.itemFields || base?.itemFields ? { itemFields: override.itemFields || base?.itemFields } : {},
|
|
164
|
+
...override.itemTemplate || base?.itemTemplate ? { itemTemplate: override.itemTemplate || base?.itemTemplate } : {},
|
|
165
|
+
...override.itemLabelKey || base?.itemLabelKey ? { itemLabelKey: override.itemLabelKey || base?.itemLabelKey } : {}
|
|
166
|
+
};
|
|
167
|
+
ordered.push(merged);
|
|
168
|
+
seen.add(override.key);
|
|
169
|
+
}
|
|
170
|
+
for (const field of derived) {
|
|
171
|
+
if (!seen.has(field.key)) ordered.push(field);
|
|
172
|
+
}
|
|
173
|
+
return ordered;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/blocks/globals.ts
|
|
177
|
+
function defineGlobal(args) {
|
|
178
|
+
const parsedDefaults = args.schema.safeParse({});
|
|
179
|
+
if (!parsedDefaults.success) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`defineGlobal("${args.key}"): every field needs a .default() or .optional().`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
key: args.key,
|
|
186
|
+
label: args.label || labelForKey(args.key),
|
|
187
|
+
description: args.description,
|
|
188
|
+
schema: args.schema,
|
|
189
|
+
editor: { fields: mergeEditorFields(deriveFields(args.schema), args.editor?.fields) },
|
|
190
|
+
defaultData: parsedDefaults.data
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createGlobalRegistry(globals) {
|
|
194
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const definition of globals) {
|
|
196
|
+
if (byKey.has(definition.key)) {
|
|
197
|
+
throw new Error(`Duplicate global registered: "${definition.key}"`);
|
|
198
|
+
}
|
|
199
|
+
byKey.set(definition.key, definition);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
list: () => [...byKey.values()],
|
|
203
|
+
get: (key) => byKey.get(key),
|
|
204
|
+
normalize: (key, data) => {
|
|
205
|
+
const definition = byKey.get(key);
|
|
206
|
+
if (!definition) return data ?? {};
|
|
207
|
+
const parsed = definition.schema.safeParse(data ?? {});
|
|
208
|
+
return parsed.success ? parsed.data : definition.defaultData;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/blocks/index.ts
|
|
214
|
+
function defineBlock(args) {
|
|
215
|
+
const parsedDefaults = args.schema.safeParse({});
|
|
216
|
+
if (!parsedDefaults.success) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`defineBlock("${args.type}"): every field needs a .default() or .optional() so the block can be created empty. ` + parsedDefaults.error.issues.map((issue) => issue.path.join(".")).join(", ")
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const derived = deriveFields(args.schema);
|
|
222
|
+
return {
|
|
223
|
+
type: args.type,
|
|
224
|
+
label: args.label || labelForKey(args.type),
|
|
225
|
+
description: args.description,
|
|
226
|
+
category: args.category,
|
|
227
|
+
schema: args.schema,
|
|
228
|
+
editor: { fields: mergeEditorFields(derived, args.editor?.fields) },
|
|
229
|
+
defaultData: parsedDefaults.data,
|
|
230
|
+
preview: args.preview
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
234
|
+
var newBlockId = () => `b_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
235
|
+
function createBlockRegistry(blocks) {
|
|
236
|
+
const byType = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const block of blocks) {
|
|
238
|
+
if (byType.has(block.type)) {
|
|
239
|
+
throw new Error(`Duplicate block type registered: "${block.type}"`);
|
|
240
|
+
}
|
|
241
|
+
byType.set(block.type, block);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
list: () => [...byType.values()],
|
|
245
|
+
get: (type) => byType.get(type),
|
|
246
|
+
has: (type) => byType.has(type),
|
|
247
|
+
palette: () => [...byType.values()].map((block) => ({
|
|
248
|
+
type: block.type,
|
|
249
|
+
label: block.label,
|
|
250
|
+
description: block.description,
|
|
251
|
+
category: block.category,
|
|
252
|
+
defaultData: block.defaultData
|
|
253
|
+
})),
|
|
254
|
+
validateLayout: (layout) => {
|
|
255
|
+
const issues = [];
|
|
256
|
+
const normalized = [];
|
|
257
|
+
if (!Array.isArray(layout)) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
layout: [],
|
|
261
|
+
issues: [{ blockId: "", blockType: "", path: "", message: "Layout must be an array of blocks." }]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
for (const [index, raw] of layout.entries()) {
|
|
265
|
+
if (!isRecord(raw) || typeof raw.type !== "string") {
|
|
266
|
+
issues.push({
|
|
267
|
+
blockId: "",
|
|
268
|
+
blockType: "",
|
|
269
|
+
path: `[${index}]`,
|
|
270
|
+
message: 'Block must be an object with a "type".'
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const definition = byType.get(raw.type);
|
|
275
|
+
const blockId = typeof raw.id === "string" && raw.id.length > 0 ? raw.id : newBlockId();
|
|
276
|
+
if (!definition) {
|
|
277
|
+
issues.push({
|
|
278
|
+
blockId,
|
|
279
|
+
blockType: raw.type,
|
|
280
|
+
path: `[${index}]`,
|
|
281
|
+
message: `Unknown block type "${raw.type}".`
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const parsed = definition.schema.safeParse(isRecord(raw.data) ? raw.data : {});
|
|
286
|
+
if (!parsed.success) {
|
|
287
|
+
for (const issue of parsed.error.issues) {
|
|
288
|
+
issues.push({
|
|
289
|
+
blockId,
|
|
290
|
+
blockType: raw.type,
|
|
291
|
+
path: issue.path.join("."),
|
|
292
|
+
message: issue.message
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
normalized.push({
|
|
298
|
+
id: blockId,
|
|
299
|
+
type: raw.type,
|
|
300
|
+
data: parsed.data,
|
|
301
|
+
...isRecord(raw.settings) ? { settings: raw.settings } : {}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return { ok: issues.length === 0, layout: normalized, issues };
|
|
305
|
+
},
|
|
306
|
+
createInstance: (type) => {
|
|
307
|
+
const definition = byType.get(type);
|
|
308
|
+
if (!definition) throw new Error(`Unknown block type "${type}".`);
|
|
309
|
+
return {
|
|
310
|
+
id: newBlockId(),
|
|
311
|
+
type,
|
|
312
|
+
data: JSON.parse(JSON.stringify(definition.defaultData))
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
var instanceCounter = 0;
|
|
318
|
+
function blockInstance(definition, data = {}, id) {
|
|
319
|
+
const parsed = definition.schema.parse(data);
|
|
320
|
+
instanceCounter += 1;
|
|
321
|
+
return {
|
|
322
|
+
id: id || `${definition.type}-${instanceCounter}`,
|
|
323
|
+
type: definition.type,
|
|
324
|
+
data: parsed
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
blockInstance,
|
|
329
|
+
createBlockRegistry,
|
|
330
|
+
createGlobalRegistry,
|
|
331
|
+
defineBlock,
|
|
332
|
+
defineGlobal,
|
|
333
|
+
fileRef,
|
|
334
|
+
labelForKey,
|
|
335
|
+
link,
|
|
336
|
+
mediaRef,
|
|
337
|
+
paragraphs
|
|
338
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/content/index.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
var CONTENT_CACHE_TAG = "orion-content";
|
|
4
|
+
var toPublicPage = (row) => ({
|
|
5
|
+
id: String(row.id),
|
|
6
|
+
slug: String(row.slug),
|
|
7
|
+
path: String(row.path),
|
|
8
|
+
title: String(row.title ?? ""),
|
|
9
|
+
seo: row.seo ?? {},
|
|
10
|
+
layout: row.published_layout ?? []
|
|
11
|
+
});
|
|
12
|
+
function createContentClient(options = {}) {
|
|
13
|
+
const supabaseUrl = options.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
14
|
+
const anonKey = options.anonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_PUBLISHABLE_KEY || "";
|
|
15
|
+
if (!supabaseUrl || !anonKey) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Orion CMS: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) must be set."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const client = createClient(supabaseUrl, anonKey, {
|
|
21
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
22
|
+
});
|
|
23
|
+
const selectPage = "id, slug, path, title, seo, published_layout";
|
|
24
|
+
return {
|
|
25
|
+
async getPageByPath(path) {
|
|
26
|
+
const { data } = await client.from("cms_pages").select(selectPage).eq("path", path).eq("status", "published").maybeSingle();
|
|
27
|
+
return data ? toPublicPage(data) : null;
|
|
28
|
+
},
|
|
29
|
+
async getPageBySlug(slug) {
|
|
30
|
+
const { data } = await client.from("cms_pages").select(selectPage).eq("slug", slug).eq("status", "published").maybeSingle();
|
|
31
|
+
return data ? toPublicPage(data) : null;
|
|
32
|
+
},
|
|
33
|
+
async listPublishedPaths() {
|
|
34
|
+
const { data } = await client.from("cms_pages").select("path").eq("status", "published").limit(1e3);
|
|
35
|
+
return (data || []).map((row) => row.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
36
|
+
},
|
|
37
|
+
async getGlobal(key) {
|
|
38
|
+
const { data } = await client.from("cms_globals").select("data").eq("key", key).maybeSingle();
|
|
39
|
+
return data?.data ?? {};
|
|
40
|
+
},
|
|
41
|
+
async getFormConfig(slug) {
|
|
42
|
+
const { data } = await client.from("cms_forms").select("slug, title, config, success_message").eq("slug", slug).maybeSingle();
|
|
43
|
+
if (!data) return null;
|
|
44
|
+
const config = { ...data.config ?? {} };
|
|
45
|
+
delete config.notify;
|
|
46
|
+
return {
|
|
47
|
+
slug: data.slug,
|
|
48
|
+
title: data.title ?? "",
|
|
49
|
+
config,
|
|
50
|
+
successMessage: data.success_message ?? ""
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
async getRedirect(path) {
|
|
54
|
+
const { data } = await client.from("cms_redirects").select("to_path, permanent").eq("from_path", path).maybeSingle();
|
|
55
|
+
if (!data) return null;
|
|
56
|
+
return { toPath: String(data.to_path), permanent: data.permanent !== false };
|
|
57
|
+
},
|
|
58
|
+
mediaUrl(storagePath, transform) {
|
|
59
|
+
if (storagePath.startsWith("data:")) return storagePath;
|
|
60
|
+
const base = `${supabaseUrl}/storage/v1`;
|
|
61
|
+
if (!transform || !transform.width && !transform.height) {
|
|
62
|
+
return `${base}/object/public/media/${storagePath}`;
|
|
63
|
+
}
|
|
64
|
+
const params = new URLSearchParams();
|
|
65
|
+
if (transform.width) params.set("width", String(transform.width));
|
|
66
|
+
if (transform.height) params.set("height", String(transform.height));
|
|
67
|
+
params.set("quality", String(transform.quality ?? 80));
|
|
68
|
+
return `${base}/render/image/public/media/${storagePath}?${params.toString()}`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
CONTENT_CACHE_TAG,
|
|
75
|
+
createContentClient
|
|
76
|
+
};
|