이 가이드는 네이티브 플러그인을 처음부터 구축하는 과정을 안내합니다. 네이티브 플러그인은 React 관리 페이지, Portable Text 컴포넌트 및 페이지 프래그먼트를 포함하여 런타임에 대한 전체 액세스 권한을 가지고 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 # Descriptor factory + createPlugin
│ ├── admin.tsx # React admin components (optional)
│ └── astro/ # Astro components for PT block rendering (optional)
│ └── index.ts
├── package.json
└── tsconfig.json
패키지 설정
다음 package.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를 피어 종속성으로 유지하십시오.
디스크립터와 런타임 작성
다음 src/index.ts는 디스크립터 팩토리와 createPlugin 런타임을 하나의 파일로 정의합니다:
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: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("Analytics plugin installed", { 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 식별자의 일부로 사용되므로 해당 패턴 외부의 모든 것은 런타임에 실패합니다. 다음 값은 허용되는 ID를 보여줍니다:
// Valid
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// Invalid
"@my-org/plugin-forms"; // scoped form not allowed at runtime
"MyPlugin"; // no uppercase
"42-plugin"; // can't start with a digit
"my.plugin"; // no dots
범위가 없는 id를 entrypoint의 범위가 있는 npm 패키지 이름과 페어링하십시오 — 패키지 이름과 플러그인 ID는 별도의 관심사입니다.
버전 형식
시맨틱 버전을 사용하십시오. 다음 값은 허용되는 버전 문자열을 보여줍니다:
version: "1.0.0"; // valid
version: "1.2.3-beta"; // valid (prerelease)
version: "1.0"; // invalid (missing patch)
플러그인 등록
사이트의 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 Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
maxItems: { type: "number", label: "Max items", 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: "Retention (days)",
description: "Days to keep entries. 0 = forever.",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "Audit History", icon: "history" }],
widgets: [{ id: "recent-activity", title: "Recent Activity", 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 패키징 및 버전 관리.