@azlib/cms 0.2.0 → 0.3.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 +64 -0
- package/dist/index.cjs +199 -27
- package/dist/index.d.cts +312 -194
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +312 -194
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +199 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A framework-agnostic, modular Content Management System (CMS) runtime inspired b
|
|
|
12
12
|
- 🗂️ **Hierarchical Taxonomies**: Nested category trees, flat tags, and custom taxonomy binding.
|
|
13
13
|
- 🖼️ **Media & Asset Management**: MIME validation, file sanitization, metadata, and asset queries.
|
|
14
14
|
- 🔒 **RBAC & Capability Matrix**: Built-in Administrator, Editor, Author, Contributor, and Subscriber roles with content ownership checks.
|
|
15
|
+
- 🔌 **Extensible Plugin System**: Package and distribute reusable plugins (`definePlugin`) with schema extensions, custom API routes, lifecycle hooks, and filter pipelines.
|
|
15
16
|
- 🌐 **Universal Web Standard Router**: `Request` -> `Response` HTTP router ready for Next.js App Router, Remix, Vite, Astro, Express, or Cloudflare Workers.
|
|
16
17
|
- 🚀 **Type-Safe Headless Client SDK**: Query content in-process or over HTTP with zero boilerplate (`createCmsClient`).
|
|
17
18
|
|
|
@@ -109,6 +110,69 @@ export async function POST(request: Request) {
|
|
|
109
110
|
|
|
110
111
|
---
|
|
111
112
|
|
|
113
|
+
## Plugin System
|
|
114
|
+
|
|
115
|
+
Create modular, decoupled plugins that extend collections, inject custom fields, register custom Web Standard routes, and hook into lifecycle pipelines:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { definePlugin, fields } from "@azlib/cms";
|
|
119
|
+
|
|
120
|
+
export const seoPlugin = definePlugin<{ defaultTitleSuffix?: string }>((options = {}) => ({
|
|
121
|
+
name: "seo-plugin",
|
|
122
|
+
version: "1.0.0",
|
|
123
|
+
|
|
124
|
+
// 1. Inject custom fields into existing collections
|
|
125
|
+
extendCollections: {
|
|
126
|
+
posts: [
|
|
127
|
+
fields.text({ name: "metaTitle", label: "Meta Title" }),
|
|
128
|
+
fields.text({ name: "metaDescription", label: "Meta Description" }),
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 2. Programmatic setup for hooks and routes
|
|
133
|
+
setup({ hooks, registerRoute, engine }) {
|
|
134
|
+
// Intercept content before saving
|
|
135
|
+
hooks.addFilter("cms.before_create_input", (input: any) => {
|
|
136
|
+
if (input.title && options.defaultTitleSuffix && !input.data?.metaTitle) {
|
|
137
|
+
input.data = {
|
|
138
|
+
...input.data,
|
|
139
|
+
metaTitle: `${input.title} | ${options.defaultTitleSuffix}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return input;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Expose custom Web Standard API route
|
|
146
|
+
registerRoute("GET", "/api/seo/sitemap", async () => {
|
|
147
|
+
const posts = await engine.collection("posts").find({ status: "published" });
|
|
148
|
+
const urls = posts.items.map((p) => `https://example.com/posts/${p.slug}`);
|
|
149
|
+
return new Response(JSON.stringify({ urls }), {
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// 3. Lifecycle callbacks
|
|
156
|
+
async onInit(engine) {
|
|
157
|
+
console.log("[SEO Plugin] Initialized");
|
|
158
|
+
},
|
|
159
|
+
}));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Register plugins declaratively in `defineConfig` or dynamically via `cms.use()`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Declarative registration
|
|
166
|
+
export default defineConfig({
|
|
167
|
+
plugins: [seoPlugin({ defaultTitleSuffix: "Alex's Journal" })],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Or dynamic registration
|
|
171
|
+
cms.use(seoPlugin());
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
112
176
|
## License
|
|
113
177
|
|
|
114
178
|
MIT © Google / azlib
|
package/dist/index.cjs
CHANGED
|
@@ -230,6 +230,7 @@ function normalizeConfig(config) {
|
|
|
230
230
|
},
|
|
231
231
|
collections,
|
|
232
232
|
taxonomies: config.taxonomies ?? [],
|
|
233
|
+
plugins: config.plugins ?? [],
|
|
233
234
|
admin: {
|
|
234
235
|
route: config.admin?.route ?? "/admin",
|
|
235
236
|
enableRegistration: config.admin?.enableRegistration ?? false,
|
|
@@ -1246,6 +1247,9 @@ var CMSEngine = class {
|
|
|
1246
1247
|
revisions;
|
|
1247
1248
|
lifecycle;
|
|
1248
1249
|
collections = /* @__PURE__ */ new Map();
|
|
1250
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1251
|
+
customRoutes = /* @__PURE__ */ new Map();
|
|
1252
|
+
pendingPluginSetups = [];
|
|
1249
1253
|
constructor(config = {}, storage, hooks) {
|
|
1250
1254
|
this.config = normalizeConfig(config);
|
|
1251
1255
|
this.hooks = hooks ?? new HooksManager();
|
|
@@ -1258,21 +1262,114 @@ var CMSEngine = class {
|
|
|
1258
1262
|
this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
|
|
1259
1263
|
for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
|
|
1260
1264
|
if (this.config.taxonomies) for (const tax of this.config.taxonomies) this.taxonomies.registerTaxonomy(tax);
|
|
1265
|
+
if (this.config.plugins) for (const plugin of this.config.plugins) this.use(plugin);
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Register a plugin with the CMS engine.
|
|
1269
|
+
*/
|
|
1270
|
+
use(plugin) {
|
|
1271
|
+
if (this.plugins.has(plugin.name)) throw new Error(`[CMSEngine] Plugin '${plugin.name}' is already registered.`);
|
|
1272
|
+
this.plugins.set(plugin.name, plugin);
|
|
1273
|
+
if (plugin.collections) for (const coll of plugin.collections) this.registerCollection(coll);
|
|
1274
|
+
if (plugin.taxonomies) for (const tax of plugin.taxonomies) this.taxonomies.registerTaxonomy(tax);
|
|
1275
|
+
if (plugin.extendCollections) for (const [slug, fields] of Object.entries(plugin.extendCollections)) this.extendCollection(slug, fields);
|
|
1276
|
+
if (plugin.setup) {
|
|
1277
|
+
const context = {
|
|
1278
|
+
engine: this,
|
|
1279
|
+
hooks: this.hooks,
|
|
1280
|
+
storage: this.storage,
|
|
1281
|
+
options: this.options,
|
|
1282
|
+
taxonomies: this.taxonomies,
|
|
1283
|
+
media: this.media,
|
|
1284
|
+
rbac: this.rbac,
|
|
1285
|
+
revisions: this.revisions,
|
|
1286
|
+
lifecycle: this.lifecycle,
|
|
1287
|
+
registerCollection: (c) => this.registerCollection(c),
|
|
1288
|
+
extendCollection: (s, f) => this.extendCollection(s, f),
|
|
1289
|
+
registerTaxonomy: (t) => this.taxonomies.registerTaxonomy(t),
|
|
1290
|
+
registerRoute: (m, p, h) => this.registerRoute(m, p, h)
|
|
1291
|
+
};
|
|
1292
|
+
const setupResult = plugin.setup(context);
|
|
1293
|
+
if (setupResult instanceof Promise) this.pendingPluginSetups.push(setupResult);
|
|
1294
|
+
}
|
|
1295
|
+
return this;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Get all registered plugins.
|
|
1299
|
+
*/
|
|
1300
|
+
getPlugins() {
|
|
1301
|
+
return Array.from(this.plugins.values());
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Get a registered plugin by name.
|
|
1305
|
+
*/
|
|
1306
|
+
getPlugin(name) {
|
|
1307
|
+
return this.plugins.get(name);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Check if a plugin is registered.
|
|
1311
|
+
*/
|
|
1312
|
+
hasPlugin(name) {
|
|
1313
|
+
return this.plugins.has(name);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Register a collection dynamically.
|
|
1317
|
+
*/
|
|
1318
|
+
registerCollection(coll) {
|
|
1319
|
+
this.collections.set(coll.slug, coll);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Inject additional field definitions into an existing collection.
|
|
1323
|
+
*/
|
|
1324
|
+
extendCollection(slug, newFields) {
|
|
1325
|
+
const existing = this.collections.get(slug);
|
|
1326
|
+
if (!existing) throw new Error(`[CMSEngine] Cannot extend non-existent collection '${slug}'.`);
|
|
1327
|
+
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
1328
|
+
const addedFields = newFields.filter((f) => !existingFieldNames.has(f.name));
|
|
1329
|
+
this.collections.set(slug, {
|
|
1330
|
+
...existing,
|
|
1331
|
+
fields: [...existing.fields, ...addedFields]
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Register a custom Web Standard HTTP route on the engine.
|
|
1336
|
+
*/
|
|
1337
|
+
registerRoute(method, path, handler) {
|
|
1338
|
+
const normalizedMethod = method.toUpperCase();
|
|
1339
|
+
let methodMap = this.customRoutes.get(normalizedMethod);
|
|
1340
|
+
if (!methodMap) {
|
|
1341
|
+
methodMap = /* @__PURE__ */ new Map();
|
|
1342
|
+
this.customRoutes.set(normalizedMethod, methodMap);
|
|
1343
|
+
}
|
|
1344
|
+
methodMap.set(path, handler);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Get all custom routes registered across all HTTP methods.
|
|
1348
|
+
*/
|
|
1349
|
+
getCustomRoutes() {
|
|
1350
|
+
return this.customRoutes;
|
|
1261
1351
|
}
|
|
1262
1352
|
/**
|
|
1263
1353
|
* Initialize storage and trigger bootstrap hooks.
|
|
1264
1354
|
*/
|
|
1265
1355
|
async init() {
|
|
1266
1356
|
await this.storage.init();
|
|
1357
|
+
if (this.pendingPluginSetups.length > 0) {
|
|
1358
|
+
await Promise.all(this.pendingPluginSetups);
|
|
1359
|
+
this.pendingPluginSetups = [];
|
|
1360
|
+
}
|
|
1267
1361
|
if (this.config.site) {
|
|
1268
1362
|
if (!await this.options.has("site_name")) await this.options.set("site_name", this.config.site.name);
|
|
1269
1363
|
}
|
|
1364
|
+
for (const plugin of this.plugins.values()) if (plugin.onInit) await plugin.onInit(this);
|
|
1365
|
+
await this.hooks.doAction("cms.plugins_initialized", this);
|
|
1270
1366
|
await this.hooks.doAction("cms.init", this);
|
|
1271
1367
|
}
|
|
1272
1368
|
/**
|
|
1273
|
-
* Close storage connections.
|
|
1369
|
+
* Close storage connections and run plugin teardown hooks.
|
|
1274
1370
|
*/
|
|
1275
1371
|
async close() {
|
|
1372
|
+
for (const plugin of this.plugins.values()) if (plugin.onClose) await plugin.onClose(this);
|
|
1276
1373
|
await this.storage.close();
|
|
1277
1374
|
await this.hooks.doAction("cms.close", this);
|
|
1278
1375
|
}
|
|
@@ -1298,38 +1395,39 @@ var CMSEngine = class {
|
|
|
1298
1395
|
return {
|
|
1299
1396
|
config: collConfig,
|
|
1300
1397
|
async create(input, authorId = null) {
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
if (
|
|
1304
|
-
if (
|
|
1398
|
+
const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
|
|
1399
|
+
const inputData = { ...filteredInput.data || {} };
|
|
1400
|
+
if (filteredInput.title !== void 0) inputData.title = filteredInput.title;
|
|
1401
|
+
if (filteredInput.slug !== void 0) inputData.slug = filteredInput.slug;
|
|
1402
|
+
if (filteredInput.status !== void 0) inputData.status = filteredInput.status;
|
|
1305
1403
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
|
|
1306
1404
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
|
|
1307
|
-
const title =
|
|
1308
|
-
const finalSlug = await resolveUniqueSlug(
|
|
1405
|
+
const title = filteredInput.title ?? normalizedData.title ?? "";
|
|
1406
|
+
const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
|
|
1309
1407
|
return await self.storage.getContentBySlug(slug, s) !== null;
|
|
1310
1408
|
});
|
|
1311
|
-
const status =
|
|
1409
|
+
const status = filteredInput.status ?? (collConfig.draftable ? "draft" : "published");
|
|
1312
1410
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1313
1411
|
const item = await self.storage.createContent({
|
|
1314
1412
|
collection: slug,
|
|
1315
1413
|
slug: finalSlug,
|
|
1316
1414
|
title,
|
|
1317
1415
|
status,
|
|
1318
|
-
parentId: collConfig.hierarchical ?
|
|
1416
|
+
parentId: collConfig.hierarchical ? filteredInput.parentId ?? null : null,
|
|
1319
1417
|
authorId,
|
|
1320
1418
|
publishedAt: status === "published" ? nowIso : null,
|
|
1321
|
-
scheduledAt: status === "scheduled" ?
|
|
1419
|
+
scheduledAt: status === "scheduled" ? filteredInput.scheduledAt ?? null : null,
|
|
1322
1420
|
data: normalizedData,
|
|
1323
|
-
terms:
|
|
1421
|
+
terms: filteredInput.terms
|
|
1324
1422
|
});
|
|
1325
|
-
if (
|
|
1326
|
-
const allTermIds = Object.values(
|
|
1423
|
+
if (filteredInput.terms) {
|
|
1424
|
+
const allTermIds = Object.values(filteredInput.terms).flat();
|
|
1327
1425
|
if (allTermIds.length > 0) await self.taxonomies.assignTerms(item.id, allTermIds);
|
|
1328
1426
|
}
|
|
1329
1427
|
if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
|
|
1330
1428
|
await self.hooks.doAction("cms.content_created", item);
|
|
1331
1429
|
await self.hooks.doAction(`cms.${slug}_created`, item);
|
|
1332
|
-
return item;
|
|
1430
|
+
return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
|
|
1333
1431
|
},
|
|
1334
1432
|
async findById(id) {
|
|
1335
1433
|
return self.storage.getContent(slug, id);
|
|
@@ -1338,51 +1436,58 @@ var CMSEngine = class {
|
|
|
1338
1436
|
return self.storage.getContentBySlug(slug, contentSlug);
|
|
1339
1437
|
},
|
|
1340
1438
|
async find(options = {}) {
|
|
1341
|
-
|
|
1439
|
+
const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
|
|
1440
|
+
return self.storage.findContent(slug, filteredOptions);
|
|
1342
1441
|
},
|
|
1343
1442
|
async update(id, input, authorId = null, revisionNote) {
|
|
1344
1443
|
const existing = await self.storage.getContent(slug, id);
|
|
1345
1444
|
if (!existing) return null;
|
|
1445
|
+
const filteredInput = await self.hooks.applyFilters("cms.before_update_input", input, {
|
|
1446
|
+
collection: slug,
|
|
1447
|
+
id,
|
|
1448
|
+
existing
|
|
1449
|
+
});
|
|
1346
1450
|
const mergedData = {
|
|
1347
1451
|
...existing.data,
|
|
1348
|
-
...
|
|
1452
|
+
...filteredInput.data || {}
|
|
1349
1453
|
};
|
|
1350
|
-
if (
|
|
1454
|
+
if (filteredInput.title !== void 0) mergedData.title = filteredInput.title;
|
|
1351
1455
|
else if (existing.title !== void 0 && mergedData.title === void 0) mergedData.title = existing.title;
|
|
1352
|
-
if (
|
|
1456
|
+
if (filteredInput.slug !== void 0) mergedData.slug = filteredInput.slug;
|
|
1353
1457
|
else if (existing.slug !== void 0 && mergedData.slug === void 0) mergedData.slug = existing.slug;
|
|
1354
|
-
if (
|
|
1458
|
+
if (filteredInput.status !== void 0) mergedData.status = filteredInput.status;
|
|
1355
1459
|
else if (existing.status !== void 0 && mergedData.status === void 0) mergedData.status = existing.status;
|
|
1356
1460
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, mergedData);
|
|
1357
1461
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed updating '${slug}': ${JSON.stringify(errors)}`);
|
|
1358
1462
|
let updatedSlug = existing.slug;
|
|
1359
|
-
if (
|
|
1463
|
+
if (filteredInput.slug && filteredInput.slug !== existing.slug) updatedSlug = await resolveUniqueSlug(filteredInput.slug, async (s) => {
|
|
1360
1464
|
const check = await self.storage.getContentBySlug(slug, s);
|
|
1361
1465
|
return check !== null && check.id !== id;
|
|
1362
1466
|
}, id);
|
|
1363
|
-
const nextStatus =
|
|
1467
|
+
const nextStatus = filteredInput.status ?? existing.status;
|
|
1364
1468
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1365
1469
|
let publishedAt = existing.publishedAt;
|
|
1366
1470
|
if (nextStatus === "published" && !publishedAt) publishedAt = nowIso;
|
|
1367
1471
|
const updated = await self.storage.updateContent(slug, id, {
|
|
1368
|
-
title:
|
|
1472
|
+
title: filteredInput.title !== void 0 ? filteredInput.title : existing.title,
|
|
1369
1473
|
slug: updatedSlug,
|
|
1370
1474
|
status: nextStatus,
|
|
1371
|
-
parentId: collConfig.hierarchical ?
|
|
1372
|
-
scheduledAt:
|
|
1475
|
+
parentId: collConfig.hierarchical ? filteredInput.parentId !== void 0 ? filteredInput.parentId : existing.parentId : null,
|
|
1476
|
+
scheduledAt: filteredInput.scheduledAt !== void 0 ? filteredInput.scheduledAt : existing.scheduledAt,
|
|
1373
1477
|
publishedAt,
|
|
1374
1478
|
authorId: authorId ?? existing.authorId,
|
|
1375
1479
|
data: normalizedData,
|
|
1376
|
-
terms:
|
|
1480
|
+
terms: filteredInput.terms !== void 0 ? filteredInput.terms : existing.terms
|
|
1377
1481
|
});
|
|
1378
1482
|
if (updated) {
|
|
1379
|
-
if (
|
|
1380
|
-
const allTermIds = Object.values(
|
|
1483
|
+
if (filteredInput.terms) {
|
|
1484
|
+
const allTermIds = Object.values(filteredInput.terms).flat();
|
|
1381
1485
|
await self.taxonomies.assignTerms(updated.id, allTermIds);
|
|
1382
1486
|
}
|
|
1383
1487
|
if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
|
|
1384
1488
|
await self.hooks.doAction("cms.content_updated", updated);
|
|
1385
1489
|
await self.hooks.doAction(`cms.${slug}_updated`, updated);
|
|
1490
|
+
return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
|
|
1386
1491
|
}
|
|
1387
1492
|
return updated;
|
|
1388
1493
|
},
|
|
@@ -1439,7 +1544,33 @@ function createCMSEngine(config, storage, hooks) {
|
|
|
1439
1544
|
return new CMSEngine(config, storage, hooks);
|
|
1440
1545
|
}
|
|
1441
1546
|
//#endregion
|
|
1547
|
+
//#region src/core/plugin.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* Type-safe helper for authoring reusable CMS plugins and plugin factories.
|
|
1550
|
+
*/
|
|
1551
|
+
function definePlugin(factory) {
|
|
1552
|
+
return factory;
|
|
1553
|
+
}
|
|
1554
|
+
//#endregion
|
|
1442
1555
|
//#region src/api/router.ts
|
|
1556
|
+
function compilePath(path) {
|
|
1557
|
+
const keys = [];
|
|
1558
|
+
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1559
|
+
keys.push(key);
|
|
1560
|
+
return "([^/]+)";
|
|
1561
|
+
});
|
|
1562
|
+
return {
|
|
1563
|
+
regex: new RegExp(`^${pattern}$`),
|
|
1564
|
+
keys
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function matchRoute(entry, pathname) {
|
|
1568
|
+
const match = pathname.match(entry.regex);
|
|
1569
|
+
if (!match) return null;
|
|
1570
|
+
const params = {};
|
|
1571
|
+
for (let i = 0; i < entry.keys.length; i++) params[entry.keys[i]] = decodeURIComponent(match[i + 1]);
|
|
1572
|
+
return params;
|
|
1573
|
+
}
|
|
1443
1574
|
function jsonResponse(data, status = 200, headers = {}) {
|
|
1444
1575
|
return new Response(JSON.stringify(data), {
|
|
1445
1576
|
status,
|
|
@@ -1454,10 +1585,25 @@ function jsonResponse(data, status = 200, headers = {}) {
|
|
|
1454
1585
|
}
|
|
1455
1586
|
var CMSRouter = class {
|
|
1456
1587
|
engine;
|
|
1588
|
+
localRoutes = [];
|
|
1457
1589
|
constructor(engine) {
|
|
1458
1590
|
this.engine = engine;
|
|
1459
1591
|
}
|
|
1460
1592
|
/**
|
|
1593
|
+
* Register a custom Web Standard route on the router.
|
|
1594
|
+
*/
|
|
1595
|
+
registerRoute(method, path, handler) {
|
|
1596
|
+
const { regex, keys } = compilePath(path);
|
|
1597
|
+
this.localRoutes.push({
|
|
1598
|
+
method: method.toUpperCase(),
|
|
1599
|
+
path,
|
|
1600
|
+
regex,
|
|
1601
|
+
keys,
|
|
1602
|
+
handler
|
|
1603
|
+
});
|
|
1604
|
+
return this;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1461
1607
|
* Universal Web Standards request handler.
|
|
1462
1608
|
*/
|
|
1463
1609
|
async handle(request) {
|
|
@@ -1485,6 +1631,8 @@ var CMSRouter = class {
|
|
|
1485
1631
|
if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request);
|
|
1486
1632
|
if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request);
|
|
1487
1633
|
if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request);
|
|
1634
|
+
const customResponse = await this.handleCustomRoute(pathname, method, url, request);
|
|
1635
|
+
if (customResponse) return customResponse;
|
|
1488
1636
|
return jsonResponse({
|
|
1489
1637
|
error: "Endpoint not found",
|
|
1490
1638
|
path: pathname
|
|
@@ -1493,6 +1641,29 @@ var CMSRouter = class {
|
|
|
1493
1641
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
1494
1642
|
}
|
|
1495
1643
|
}
|
|
1644
|
+
async handleCustomRoute(pathname, method, url, request) {
|
|
1645
|
+
for (const route of this.localRoutes) if (route.method === method) {
|
|
1646
|
+
const params = matchRoute(route, pathname);
|
|
1647
|
+
if (params !== null) {
|
|
1648
|
+
const context = {
|
|
1649
|
+
params,
|
|
1650
|
+
url,
|
|
1651
|
+
engine: this.engine
|
|
1652
|
+
};
|
|
1653
|
+
return await route.handler(request, context);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const engineRoutes = this.engine.getCustomRoutes().get(method);
|
|
1657
|
+
if (engineRoutes) for (const [pathPattern, handler] of engineRoutes) {
|
|
1658
|
+
const params = matchRoute(compilePath(pathPattern), pathname);
|
|
1659
|
+
if (params !== null) return await handler(request, {
|
|
1660
|
+
params,
|
|
1661
|
+
url,
|
|
1662
|
+
engine: this.engine
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1496
1667
|
async handleContent(pathname, method, url, request) {
|
|
1497
1668
|
const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
|
|
1498
1669
|
const collectionSlug = parts[0];
|
|
@@ -1796,6 +1967,7 @@ exports.createCMSRouter = createCMSRouter;
|
|
|
1796
1967
|
exports.createCmsClient = createCmsClient;
|
|
1797
1968
|
exports.defaultHooks = defaultHooks;
|
|
1798
1969
|
exports.defineConfig = defineConfig;
|
|
1970
|
+
exports.definePlugin = definePlugin;
|
|
1799
1971
|
exports.fields = fields;
|
|
1800
1972
|
exports.normalizeConfig = normalizeConfig;
|
|
1801
1973
|
exports.resolveUniqueSlug = resolveUniqueSlug;
|