本指南將帶你從頭開始建置一個最小的沙箱外掛程式——一個記錄每次內容儲存並公開單個 API 路由的外掛程式。完成後,你將擁有一個透過已設定的沙箱執行器在隔離執行時期中執行的外掛程式。如果網站管理員選擇將其從 sandboxed: [] 移到 plugins: [],同樣的外掛程式程式碼也可以在處理程序內執行——例如在沒有沙箱執行器的平台上。
如果你還沒有決定是要沙箱外掛程式還是原生外掛程式,請先閱讀選擇外掛程式格式。
兩個檔案
每個沙箱外掛程式包含兩個部分:
- 描述符 — 一個描述外掛程式的小物件(id、版本、能力、儲存、執行時期入口的位置)。在建置時由
astro.config.mjs匯入。 - 沙箱入口 — 執行時期程式碼:掛鉤、路由、儲存存取。在請求時載入到沙箱執行時期中。
這兩個檔案位於同一個套件中,但在完全不同的環境中執行。描述符永遠看不到執行時期上下文;入口永遠看不到 astro.config.mjs。
my-plugin/
├── src/
│ ├── index.ts # 描述符 — 在建置時的 Vite 中執行
│ └── sandbox-entry.ts # 掛鉤、路由、儲存 — 在沙箱執行時期中執行
├── package.json
└── tsconfig.json
設定套件
-
建立一個新目錄並將其初始化為 TypeScript ES 模組套件。
{ "name": "@my-org/plugin-hello", "version": "0.1.0", "type": "module", "main": "dist/index.mjs", "exports": { ".": { "import": "./dist/index.mjs", "types": "./dist/index.d.mts" }, "./sandbox": "./dist/sandbox-entry.mjs" }, "files": ["dist"], "scripts": { "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean" }, "peerDependencies": { "emdash": "*" }, "devDependencies": { "emdash": "*", "tsdown": "^0.6.0", "typescript": "^5.5.0" } }"./sandbox"匯出是描述符的entrypoint將指向的內容。打包器將兩個檔案都建置到dist/中。 -
新增
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
撰寫描述符
描述符是一個回傳 PluginDescriptor 的工廠函式。它在建置時的 Vite 中執行,這意味著它必須是無副作用的,不能使用任何執行時期 API(fetch、資料庫、環境變數——這些都還不存在)。
import type { PluginDescriptor } from "emdash";
export function helloPlugin(): PluginDescriptor {
return {
id: "plugin-hello",
version: "0.1.0",
format: "standard",
entrypoint: "@my-org/plugin-hello/sandbox",
capabilities: [],
storage: {
events: { indexes: ["timestamp"] },
},
};
}
一些重要的細節:
format: "standard"是必需的。 沒有它,EmDash 會將套件視為原生外掛程式並尋找不同的結構。format欄位預設為"native"。entrypoint是一個模組說明符,而不是檔案路徑。使用你傳遞給import的相同字串——通常是"<package-name>/sandbox"。套件名稱可以有作用域(@my-org/plugin-hello);外掛程式id不能有。id是一個 URL 安全的 slug,而不是 npm 套件名稱。 它必須符合/^[a-z][a-z0-9_-]*$/— 以小寫字母開頭,然後是字母、數字、連字號或底線。id 既用作外掛程式路由 URL 中的單個路徑段(/_emdash/api/plugins/<id>/...),也用作外掛程式儲存索引產生的 SQL 識別符的一部分,因此@、/、前導數字和大寫字母都會在執行時期失敗。將像plugin-hello這樣的無作用域id與entrypoint中的有作用域 npm 套件名稱配對。- 能力、allowedHosts 和儲存位於描述符上。 沙箱入口不宣告它們——它只能使用描述符允許的內容。
- 不要在這裡放置執行時期邏輯。 不要頂層
await,不要模組級fetch,不要讀取檔案。描述符是元資料。
撰寫沙箱入口
執行時期端。此檔案在請求時載入到沙箱執行時期中,除了 ctx 提供的內容外無法存取任何東西。
import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";
interface ContentSaveEvent {
collection: string;
content: { id: string };
isNew: boolean;
}
export default definePlugin({
hooks: {
"content:afterSave": {
handler: async (event: ContentSaveEvent, ctx: PluginContext) => {
ctx.log.info("Content saved", {
collection: event.collection,
id: event.content.id,
});
await ctx.storage.events.put(`save-${Date.now()}`, {
timestamp: new Date().toISOString(),
collection: event.collection,
contentId: event.content.id,
});
},
},
},
routes: {
recent: {
handler: async (_routeCtx, ctx: PluginContext) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
});
值得了解的事項:
- 沙箱入口中的
definePlugin()只接受{ hooks, routes }。 沒有id,沒有version,沒有capabilities——這些來自描述符。如果你嘗試在這裡傳遞它們,EmDash 會在建置時拋出錯誤。 - 掛鉤處理器接受
(event, ctx)。 事件形狀取決於掛鉤名稱;參見掛鉤參考。 - 路由處理器接受
(routeCtx, ctx)— 兩個參數。routeCtx有{ input, request, requestMeta };ctx是你在掛鉤中獲得的相同PluginContext。路由可以在/_emdash/api/plugins/<plugin-id>/<route-name>存取。 ctx.storage.events有效是因為events在描述符上宣告了。 存取未宣告的集合會拋出錯誤。ctx.kv始終可用 — 一個具有get、set、delete和list(prefix)的每個外掛程式的鍵值儲存。
註冊外掛程式
在你網站的 astro.config.mjs 中,匯入描述符工廠並將其傳遞給 EmDash 整合。沙箱外掛程式放在 sandboxed: [] 中;處理程序內外掛程式放在 plugins: [] 中。標準格式外掛程式在兩者中都有效——從 sandboxed 開始。
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import { helloPlugin } from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [helloPlugin()],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner 是可插拔的部分。上面的範例使用來自 @emdash-cms/cloudflare 的 sandbox(),這是當今大多數網站使用的沙箱執行器。其他平台的執行器正在開發中。如果沒有設定執行器(或設定的執行器在當前平台上回報不可用),sandboxed: [] 外掛程式會在啟動時被跳過——要在處理程序內執行相同的外掛程式,請將其從 sandboxed: [] 移到 plugins: []。
建置和執行
在外掛程式目錄中:
pnpm build
在網站目錄中,連結或安裝外掛程式(pnpm add @my-org/plugin-hello 或工作區連結),然後啟動開發伺服器。下次你在管理面板中儲存一條內容時,你應該在日誌中看到 [hello] Content saved …,並且 GET /_emdash/api/plugins/plugin-hello/recent 應該回傳最後十個儲存事件。