이 가이드는 네이티브 플러그인을 처음부터 구축하는 과정을 안내합니다. 네이티브 플러그인은 Astro 사이트와 동일한 프로세스에서 실행됩니다——샌드박스 경계가 없고, 런타임에 대한 전체 액세스, 그리고 샌드박스가 제공할 수 없는 기능(React 관리 페이지, Portable Text 컴포넌트, 페이지 프래그먼트)에 대한 액세스가 가능합니다.
네이티브 플러그인과 샌드박스 플러그인 중 무엇을 사용할지 아직 결정하지 못했다면, 먼저 플러그인 형식 선택하기를 읽어보세요. 네이티브 경로는 샌드박스가 정말로 할 수 없는 작업이 필요한 경우에 적합합니다.
두 부분, 하나 또는 두 개의 파일에
샌드박스 플러그인과 마찬가지로, 네이티브 플러그인은 두 부분으로 구성됩니다:
- 디스크립터 팩토리 —
format: "native"및 관리 관련 진입점이 있는PluginDescriptor를 반환합니다. 빌드 시astro.config.mjs에 의해 가져옵니다. createPlugin(options)함수 — 런타임 측면.definePlugin({ id, version, capabilities, hooks, routes, admin })결과를 반환합니다.
샌드박스 플러그인과 달리, 두 부분 모두 동일한 파일에 있을 수 있습니다. 다른 환경에서 실행되지 않기 때문입니다——전체 플러그인이 프로세스 내에서 실행됩니다. 패키지의 "." 내보내기는 디스크립터 팩토리와 createPlugin(또는 default) 함수를 모두 내보내는 파일을 가리킵니다:
my-native-plugin/
├── src/
│ ├── index.ts # 디스크립터 팩토리 + createPlugin
│ ├── admin.tsx # React 관리 컴포넌트 (선택 사항)
│ └── astro/ # PT 블록 렌더링을 위한 Astro 컴포넌트 (선택 사항)
│ └── index.ts
├── package.json
└── tsconfig.json
패키지 설정
{
"name": "@my-org/plugin-analytics",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./admin": {
"types": "./dist/admin.d.ts",
"import": "./dist/admin.js"
}
},
"files": ["dist"],
"peerDependencies": {
"emdash": "*",
"react": "^18.0.0"
}
}
호스트 사이트가 실제 버전을 제공하고 중복을 방지하도록 emdash와 react를 피어 종속성으로 유지하세요.
디스크립터 및 런타임 작성
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface AnalyticsOptions {
enabled?: boolean;
maxEvents?: number;
}
export function analyticsPlugin(options: AnalyticsOptions = {}): PluginDescriptor {
return {
id: "analytics",
version: "0.1.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
options,
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin(options: AnalyticsOptions = {}) {
const maxEvents = options.maxEvents ?? 100;
return definePlugin({
id: "analytics",
version: "0.1.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: {
events: { indexes: ["type", "createdAt"] },
},
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "추적 ID" },
enabled: { type: "boolean", label: "활성화됨", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "대시보드", icon: "chart" }],
widgets: [{ id: "events-today", title: "오늘의 이벤트", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("분석 플러그인이 설치되었습니다", { maxEvents });
},
"content:afterSave": async (event, ctx) => {
const enabled = await ctx.kv.get<boolean>("settings:enabled");
if (enabled === false) return;
await ctx.storage.events.put(`evt_${Date.now()}`, {
type: "content:save",
contentId: event.content.id,
createdAt: new Date().toISOString(),
});
},
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({
createdAt: { gte: today },
});
return { today: count };
},
},
},
});
}
export default createPlugin;
알아야 할 몇 가지 세부 사항:
format: "native"이 필수입니다. 기본값도"native"이지만, 모든 디스크립터에 명시적으로 표시하면 작업 중인 형식을 쉽게 파악할 수 있습니다.entrypoint는 패키지의 메인 내보내기입니다. EmDash는 런타임에 이를 가져오고 기본 내보내기를 호출하여 해결된 플러그인을 구성합니다.options는 디스크립터에서createPlugin으로 흐릅니다. 사용자가 플러그인을 등록할 때 전달하는 모든 것(analyticsPlugin({ enabled: false }))은 디스크립터에 보존되고createPlugin으로 전달됩니다. 샌드박스 플러그인에는 이 표면이 없습니다——대신 KV에서 설정을 읽습니다.id,version,capabilities가 두 번 나타납니다. 한 번은 디스크립터에, 한 번은definePlugin()에. 일치해야 합니다. 디스크립터의 복사본은 빌드 시astro.config.mjs가 보는 것이고,definePlugin()의 복사본은 요청 시 실행되는 것입니다.- 네이티브 라우트 핸들러는 단일 인수를 받습니다 —
(ctx: RouteContext). 여기서ctx.input,ctx.request,ctx.requestMeta는 일반PluginContext속성과 병합됩니다. 이는 표준 형식의 두 인수 형태와 반대입니다. 전체 표면은 API 라우트를 참조하세요(다른 모든 것은 동일합니다).
플러그인 id 규칙
id 필드는 /^[a-z][a-z0-9_-]*$/와 일치해야 합니다——소문자로 시작한 다음 문자, 숫자, 하이픈 또는 밑줄이 옵니다. id는 플러그인 라우트 URL의 단일 경로 세그먼트로 사용되며 플러그인 스토리지 인덱스에 대해 생성된 SQL 식별자의 일부로 사용되므로, 해당 패턴 외부의 모든 것은 런타임에 실패합니다.
// 유효
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// 무효
"@my-org/plugin-forms"; // 런타임에 스코프 형식이 허용되지 않음
"MyPlugin"; // 대문자 없음
"42-plugin"; // 숫자로 시작할 수 없음
"my.plugin"; // 점 없음
범위가 없는 id를 entrypoint의 범위가 있는 npm 패키지 이름과 쌍을 이룹니다——패키지 이름과 플러그인 id는 별개의 개념입니다.
버전 형식
시맨틱 버전 관리를 사용합니다:
version: "1.0.0"; // 유효
version: "1.2.3-beta"; // 유효 (프리릴리스)
version: "1.0"; // 무효 (패치 누락)
플러그인 등록
사이트의 astro.config.mjs에서 디스크립터 팩토리를 가져와 plugins: [] 배열에 전달합니다——네이티브 플러그인은 항상 프로세스 내에서 실행되며, sandboxed: []에서는 실행되지 않습니다:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { analyticsPlugin } from "@my-org/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [
analyticsPlugin({ enabled: true, maxEvents: 500 }),
],
}),
],
});
설정 UI
네이티브 플러그인은 자동 생성된 설정 양식에 admin.settingsSchema를 사용할 수 있으며, 이것이 가장 간단한 방법입니다:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API 키" },
enabled: { type: "boolean", label: "활성화됨", default: true },
maxItems: { type: "number", label: "최대 항목 수", min: 1, max: 1000, default: 100 },
},
},
필드 유형: string, number, boolean, select, secret, url, email. 각각은 label, description, default와 함께 min/max/options와 같은 유형별 추가 항목을 허용합니다. 설정은 샌드박스 플러그인이 사용하는 것과 동일한 플러그인별 KV 스토어에 저장됩니다——어디서나 ctx.kv.get<T>("settings:<key>")로 읽습니다.
settingsSchema가 제공하는 것보다 더 풍부한 설정 UI를 위해서는 사용자 정의 React 페이지를 제공하세요——React 관리 페이지 및 위젯을 참조하세요.
완전한 예제: 감사 로그 플러그인
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
interface AuditEntry {
timestamp: string;
action: "create" | "update" | "delete";
collection: string;
resourceId: string;
userId?: string;
}
export function auditLogPlugin(): PluginDescriptor {
return {
id: "audit-log",
version: "0.1.0",
format: "native",
entrypoint: "@emdash-cms/plugin-audit-log",
};
}
export function createPlugin() {
return definePlugin({
id: "audit-log",
version: "0.1.0",
storage: {
entries: {
indexes: [
"timestamp",
"action",
"collection",
["collection", "timestamp"],
["action", "timestamp"],
],
},
},
admin: {
settingsSchema: {
retentionDays: {
type: "number",
label: "보존 기간(일)",
description: "항목을 보관할 일 수. 0 = 영구.",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "감사 기록", icon: "history" }],
widgets: [{ id: "recent-activity", title: "최근 활동", size: "half" }],
},
hooks: {
"content:afterSave": {
priority: 200,
handler: async (event, ctx) => {
const entry: AuditEntry = {
timestamp: new Date().toISOString(),
action: event.isNew ? "create" : "update",
collection: event.collection,
resourceId: event.content.id as string,
};
await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, entry);
},
},
"content:afterDelete": {
priority: 200,
handler: async (event, ctx) => {
await ctx.storage.entries.put(`${Date.now()}-${event.id}`, {
timestamp: new Date().toISOString(),
action: "delete",
collection: event.collection,
resourceId: event.id,
});
},
},
},
routes: {
recent: {
handler: async (ctx) => {
const result = await ctx.storage.entries.query({
orderBy: { timestamp: "desc" },
limit: 10,
});
return {
entries: result.items.map((item) => ({
id: item.id,
...(item.data as AuditEntry),
})),
};
},
},
},
});
}
export default createPlugin;
테스트
플러그인이 등록된 최소한의 Astro 사이트를 생성하여 네이티브 플러그인을 테스트합니다:
- EmDash가 설치된 테스트 사이트를 생성합니다.
astro.config.mjs에 플러그인을 등록하고 로컬 소스 경로에서 직접 가져옵니다.- 개발 서버를 실행하고 콘텐츠를 생성, 업데이트 또는 삭제하여 훅을 트리거합니다.
- 콘솔에서
ctx.log출력을 확인하고 API 라우트를 통해 스토리지를 확인합니다.
단위 테스트의 경우 PluginContext 인터페이스를 모의하고 훅 핸들러를 직접 호출합니다.
다음 단계
- React 관리 페이지 및 위젯 — 관리 패널용 사용자 정의 React UI를 제공합니다.
- Portable Text 렌더링 컴포넌트 — 플러그인 정의 블록 유형을 렌더링하는 Astro 컴포넌트를 제공합니다.
- 페이지 프래그먼트 — 스크립트, 스타일시트 또는 HTML을 공개 페이지에 주입합니다.
- 네이티브 플러그인 배포 — npm 패키징 및 버전 관리.