이 가이드는 최소한의 샌드박스 플러그인을 처음부터 빌드합니다. 이 플러그인은 모든 콘텐츠 저장을 로깅하고 단일 API 라우트를 노출합니다. 구성된 샌드박스 러너가 제공하는 격리된 런타임에서 실행됩니다. 같은 코드는 사이트 운영자가 sandboxed: []에서 plugins: []로 이동할 때 인프로세스로도 실행됩니다(예: 샌드박스 러너가 없는 플랫폼에서).
샌드박스와 네이티브 중에서 아직 결정하지 못했다면 먼저 플러그인 형식 선택하기를 읽어보세요.
두 가지 구성 요소
샌드박스 플러그인은 다음으로 구성됩니다:
emdash-plugin.jsonc— 수동으로 편집된 매니페스트: 신원, 신뢰 계약(capabilities, hosts, storage) 및 프로필 필드. 코드 없음.src/plugin.ts— 런타임: 훅과 라우트.emdash/plugin에서 타입만 임포트;emdash의 런타임 임포트 없음.
emdash-plugin build는 둘 다 읽고 사이트가 소비하는 dist/ 아티팩트를 생성합니다.
다음 예제는 완전한 플러그인의 파일 레이아웃을 보여줍니다:
my-plugin/
├── emdash-plugin.jsonc # 신원 + 신뢰 계약 + 프로필
├── src/
│ └── plugin.ts # 훅, 라우트 — 샌드박스 런타임에서 실행
├── package.json
└── tsconfig.json
패키지 설정
-
디렉터리와
package.json을 생성합니다. 빌드는emdash-plugin build입니다; 작성할tsdown호출이 없습니다.{ "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/plugin.mjs" }, "files": ["dist", "emdash-plugin.jsonc"], "scripts": { "build": "emdash-plugin build", "dev": "emdash-plugin dev" }, "peerDependencies": { "emdash": ">=0.13.0" }, "devDependencies": { "@emdash-cms/plugin-cli": "0.2.0", "emdash": ">=0.13.0", "typescript": "^5.9.0" } }"."는 사이트가 임포트하는 생성된 디스크립터입니다;"./sandbox"는 빌드된 런타임 파일입니다.emdash-plugin build는 둘 다 생성합니다. -
tsconfig.json을 추가합니다:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipLibCheck": true, "types": [] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
매니페스트 작성
emdash-plugin.jsonc는 플러그인의 신원(slug), 신뢰 계약(capabilities, allowedHosts, storage), 프로필 필드 및 퍼블리셔 핀을 포함합니다.
다음 예제는 hello 플러그인의 완전한 매니페스트를 보여줍니다:
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "plugin-hello",
"publisher": "did:plc:abc123def456", // your Atmosphere account DID
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"capabilities": [],
"allowedHosts": [],
"storage": { "events": { "indexes": ["timestamp"] } }
}
이 매니페스트에 대한 참고 사항:
slug는 URL 안전 ID이며 npm 패키지 이름이 아닙니다./^[a-z][a-z0-9_-]*$/, 최대 64자. 플러그인 라우트 URL(/_emdash/api/plugins/<slug>/...)의 단일 경로 세그먼트이며 스토리지 인덱스용으로 생성된 SQL 식별자의 일부이므로@,/, 선행 숫자 및 대문자는 모두 실패합니다. 스코프가 없는slug(plugin-hello)를 스코프가 있는 npm 패키지 이름과 쌍을 이룹니다.storage는 컬렉션을 미리 선언합니다.ctx.storage.events는events가 여기에 선언되어 있기 때문에 런타임에 작동합니다. 선언되지 않은 컬렉션에 액세스하면 오류가 발생합니다.version은 생략됩니다. 빌드는package.json에서 읽으므로 단일 진실의 원천이 있습니다. 매니페스트 참조를 참조하세요.- 신뢰 계약은 동의입니다. 나중에
capabilities,allowedHosts또는storage를 변경하려면 버전 범프가 필요합니다 — 설치된 사이트는 이전 계약에 동의했습니다.
런타임 작성
src/plugin.ts는 satisfies SandboxedPlugin으로 주석이 달린 간단한 객체를 기본 내보내기합니다. emdash/plugin은 타입만 제공하므로 샌드박스 플러그인은 emdash에 대한 런타임 종속성이 없습니다.
다음 예제는 모든 콘텐츠 저장을 플러그인 스토리지에 로깅하고 마지막 10개의 저장을 반환하는 recent 라우트를 노출합니다:
import type { SandboxedPlugin } from "emdash/plugin";
export default {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
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) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
} satisfies SandboxedPlugin;
런타임 파일에 대한 참고 사항:
satisfies SandboxedPlugin은 모든 것을 타입 지정합니다. 훅 이름에서event를 추론하고(전체 표준 이벤트 타입으로)ctx를PluginContext로 추론하므로 핸들러는 매개변수 주석이 필요하지 않습니다."content:afterSav"와 같은 오타가 있는 훅 키는 컴파일 오류입니다.- 훅 핸들러는
(event, ctx)를 받습니다. 이벤트 형태는 훅 이름에 따라 다릅니다; 훅 가이드를 참조하세요. - 라우트 핸들러는
(routeCtx, ctx)를 받습니다 — 두 개의 인수.routeCtx는{ input, request, requestMeta? }이고;ctx는 동일한PluginContext입니다. 라우트는/_emdash/api/plugins/<slug>/<route-name>에서 도달할 수 있습니다. ctx.storage.events는events가 매니페스트에 선언되어 있기 때문에 작동합니다.ctx.kv는 항상 사용 가능합니다 —get,set,delete,list(prefix)를 가진 플러그인별 키-값 저장소.
플러그인 등록
사이트의 astro.config.mjs에서 플러그인의 기본 내보내기를 가져와서 전달합니다. 샌드박스 플러그인은 sandboxed: []에 들어가고; 인프로세스 플러그인은 plugins: []에 들어갑니다. 샌드박스 플러그인은 둘 다에서 작동합니다. 아래 예제는 sandboxed:를 사용합니다:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import hello from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [hello],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner는 교체 가능한 부분입니다. 예제는 오늘날 대부분의 사이트가 사용하는 러너인 @emdash-cms/cloudflare의 sandbox()를 사용합니다. 러너가 구성되지 않았거나(또는 구성된 러너가 현재 플랫폼에서 사용할 수 없는 경우) sandboxed: [] 플러그인은 시작 시 건너뜁니다 — 플러그인을 plugins: []로 이동하여 인프로세스로 실행합니다.
빌드 및 실행
플러그인 디렉터리에서:
emdash-plugin validate # 먼저 매니페스트 스키마 검사
emdash-plugin build # dist/ 생성
편집 루프의 경우 emdash-plugin dev를 실행합니다(저장 시 재빌드하고 빌드 실패 시 마지막 양호한 dist/를 유지합니다). 사이트에서 플러그인을 설치하거나 링크하고(pnpm add file:../plugin-hello 또는 워크스페이스 링크) 개발 서버를 시작합니다. 관리자에서 콘텐츠를 저장하면 로그에서 Content saved …를 볼 수 있습니다; GET /_emdash/api/plugins/plugin-hello/recent는 마지막 10개의 저장 이벤트를 반환합니다.
다음에 읽을 내용
- 매니페스트 — 모든 필드, 신뢰 계약, 퍼블리셔 피닝
emdash-pluginCLI —build,dev,bundle- 훅 — 전체 이벤트 세트
- API 라우트 — 입력 검증, 공개 라우트, 오류
- 스토리지 및 KV — 쿼리 옵션, 인덱스, 배치 작업
- Capabilities 및 보안 — 콘텐츠 액세스, 네트워크, 호스트 허용 목록
- 번들링 및 퍼블리싱 — 마켓플레이스로 배송