첫 번째 샌드박스 플러그인

이 페이지

이 가이드는 최소한의 샌드박스 플러그인을 처음부터 빌드합니다. 이 플러그인은 모든 콘텐츠 저장을 로깅하고 단일 API 라우트를 노출합니다. 구성된 샌드박스 러너가 제공하는 격리된 런타임에서 실행됩니다. 같은 코드는 사이트 운영자가 sandboxed: []에서 plugins: []로 이동할 때 인프로세스로도 실행됩니다(예: 샌드박스 러너가 없는 플랫폼에서).

샌드박스와 네이티브 중에서 아직 결정하지 못했다면 먼저 플러그인 형식 선택하기를 읽어보세요.

두 가지 구성 요소

샌드박스 플러그인은 다음으로 구성됩니다:

  1. emdash-plugin.jsonc — 수동으로 편집된 매니페스트: 신원, 신뢰 계약(capabilities, hosts, storage) 및 프로필 필드. 코드 없음.
  2. src/plugin.ts — 런타임: 훅과 라우트. emdash/plugin에서 타입만 임포트; emdash의 런타임 임포트 없음.

emdash-plugin build는 둘 다 읽고 사이트가 소비하는 dist/ 아티팩트를 생성합니다.

다음 예제는 완전한 플러그인의 파일 레이아웃을 보여줍니다:

my-plugin/
├── emdash-plugin.jsonc   # 신원 + 신뢰 계약 + 프로필
├── src/
│   └── plugin.ts         # 훅, 라우트 — 샌드박스 런타임에서 실행
├── package.json
└── tsconfig.json

패키지 설정

  1. 디렉터리와 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는 둘 다 생성합니다.

  2. 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.eventsevents가 여기에 선언되어 있기 때문에 런타임에 작동합니다. 선언되지 않은 컬렉션에 액세스하면 오류가 발생합니다.
  • version은 생략됩니다. 빌드는 package.json에서 읽으므로 단일 진실의 원천이 있습니다. 매니페스트 참조를 참조하세요.
  • 신뢰 계약은 동의입니다. 나중에 capabilities, allowedHosts 또는 storage를 변경하려면 버전 범프가 필요합니다 — 설치된 사이트는 이전 계약에 동의했습니다.

런타임 작성

src/plugin.tssatisfies 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를 추론하고(전체 표준 이벤트 타입으로) ctxPluginContext로 추론하므로 핸들러는 매개변수 주석이 필요하지 않습니다. "content:afterSav"와 같은 오타가 있는 훅 키는 컴파일 오류입니다.
  • 훅 핸들러는 (event, ctx)를 받습니다. 이벤트 형태는 훅 이름에 따라 다릅니다; 훅 가이드를 참조하세요.
  • 라우트 핸들러는 (routeCtx, ctx)를 받습니다 — 두 개의 인수. routeCtx{ input, request, requestMeta? }이고; ctx는 동일한 PluginContext입니다. 라우트는 /_emdash/api/plugins/<slug>/<route-name>에서 도달할 수 있습니다.
  • ctx.storage.eventsevents가 매니페스트에 선언되어 있기 때문에 작동합니다.
  • 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/cloudflaresandbox()를 사용합니다. 러너가 구성되지 않았거나(또는 구성된 러너가 현재 플랫폼에서 사용할 수 없는 경우) 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개의 저장 이벤트를 반환합니다.

다음에 읽을 내용