첫 번째 네이티브 플러그인

이 페이지

네이티브 플러그인은 EmDash가 Astro 사이트와 같은 프로세스로 임포트하는 npm 패키지입니다. 이 튜토리얼에서는 콘텐츠 저장을 로그로 기록하는 플러그인을 만들고, 사이트에 설치한 다음, astro.config.mjs에 등록합니다.

React 관리자 컴포넌트, Astro 렌더링 컴포넌트, 신뢰할 수 있는 페이지 조각처럼 플러그인에 인프로세스 기능이 필요하면 네이티브 형식을 사용하세요. 훅, 라우트, 스토리지, Block Kit으로 기능을 충분히 구현할 수 있다면 샌드박스 플러그인으로 시작하세요. 플러그인 형식 선택에서 각 형식을 비교합니다.

사전 준비

pnpm을 사용하고 개발 서버를 실행할 수 있는 EmDash 사이트에서 시작하세요.

명령에서는 사이트 디렉터리를 my-emdash-site라고 부르고, 그 옆에 plugin-activity를 만듭니다. my-emdash-site를 사이트의 디렉터리 이름으로 바꾸세요.

패키지 생성 및 등록

  1. 사이트 옆에 네이티브 패키지 디렉터리를 만듭니다.

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. 패키지 메타데이터와 빌드 스크립트를 추가합니다.

    {
        "name": "@example/plugin-activity",
        "version": "0.1.0",
        "type": "module",
        "main": "./dist/index.mjs",
        "exports": {
            ".": {
                "types": "./dist/index.d.mts",
                "import": "./dist/index.mjs"
            }
        },
        "files": ["dist"],
        "scripts": {
            "build": "tsdown src/index.ts --format esm --dts --clean",
            "dev": "tsdown src/index.ts --format esm --dts --watch",
            "typecheck": "tsc --noEmit"
        },
        "peerDependencies": {
            "emdash": "*"
        },
        "devDependencies": {
            "emdash": "*",
            "tsdown": "^0.20.0",
            "typescript": "^5.9.0"
        }
    }

    npm 패키지 이름은 @example/plugin-activity입니다. 아래 런타임 코드는 플러그인 API 라우트 URL에 맞도록 스코프가 없는 플러그인 ID plugin-activity를 사용합니다.

  3. TypeScript 구성을 추가합니다.

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "preserve",
            "moduleResolution": "bundler",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true,
            "outDir": "./dist",
            "rootDir": "./src"
        },
        "include": ["src/**/*"],
        "exclude": ["node_modules", "dist"]
    }
  4. 패키지 의존성을 설치합니다.

    pnpm install
  5. 콘텐츠 저장 훅이 있는 src/index.ts를 만듭니다.

    import { definePlugin } from "emdash";
    import type { PluginDescriptor } from "emdash";
    
    export interface ActivityPluginOptions {
        logUpdates?: boolean;
    }
    
    export function activityPlugin(
        options: ActivityPluginOptions = {},
    ): PluginDescriptor<ActivityPluginOptions> {
        return {
            id: "plugin-activity",
            version: "0.1.0",
            format: "native",
            entrypoint: "@example/plugin-activity",
            options,
        };
    }
    
    export function createPlugin(options: ActivityPluginOptions = {}) {
        return definePlugin({
            id: "plugin-activity",
            version: "0.1.0",
            capabilities: ["content:read"],
            hooks: {
                "content:afterSave": async (event, ctx) => {
                    if (!event.isNew && options.logUpdates === false) return;
    
                    ctx.log.info("Content saved", {
                        collection: event.collection,
                        contentId: event.content.id,
                        isNew: event.isNew,
                    });
                },
            },
        });
    }
    
    export default createPlugin;

    content:afterSave에는 content:read 기능이 필요합니다. 이 기능이 없으면 EmDash는 훅을 건너뜁니다.

  6. 패키지를 빌드합니다.

    pnpm build
  7. 로컬 패키지를 사이트에 설치합니다.

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. 디스크립터 팩토리를 EmDash 통합에 등록합니다.

    import { defineConfig } from "astro/config";
    import emdash from "emdash/astro";
    import { activityPlugin } from "@example/plugin-activity";
    
    export default defineConfig({
        integrations: [
            emdash({
                plugins: [activityPlugin({ logUpdates: true })],
            }),
        ],
    });

    네이티브 디스크립터는 sandboxed가 아니라 plugins에 넣어야 합니다. sandboxed 배열에 네이티브 디스크립터를 넣으면 EmDash가 거부합니다.

  9. 사이트를 시작하고 관리자 패널에서 항목을 저장합니다.

    pnpm dev

    서버 로그에는 컬렉션, 콘텐츠 ID, 항목이 새로 생성되었는지 여부와 함께 Content saved가 기록됩니다.

디스크립터와 런타임의 경계

패키지 내보내기에는 두 가지 역할이 있습니다. EmDash는 각각을 서로 다른 단계에서 사용합니다.

  • 디스크립터 팩토리인 activityPlugin()은 Astro가 구성을 평가하는 동안 실행됩니다. 직렬화할 수 있는 빌드 시점 메타데이터인 id, version, format, entrypoint, options를 반환합니다. React와 Astro 진입점도 이 디스크립터에 속합니다.
  • 이름 있는 내보내기 createPlugin()은 EmDash가 초기화될 때 실행됩니다. EmDash는 entrypoint에서 이를 임포트하고, 직렬화된 options를 전달하며, definePlugin()이 해석한 플러그인을 기대합니다.

이름 있는 내보내기 createPlugin은 필수입니다. 기본 내보내기는 패키지 사용자에게 유용할 수 있지만, EmDash의 네이티브 로더는 createPlugin을 이름으로 임포트합니다.

디스크립터와 definePlugin()에서 id와 version을 동일하게 유지하세요. 플러그인 ID는 plugin-activity처럼 스코프가 없는 케밥 케이스로 사용하고, npm 스코프는 패키지 이름과 entrypoint에 두세요. 그러면 ID를 API 라우트 URL의 단일 플러그인 세그먼트로 사용할 수 있습니다.

플러그인 식별과 버전 관리에 허용되는 ID와 버전 형식이 나와 있습니다.

런타임 동작은 definePlugin()에 작성합니다.

  • capabilities와 allowedHosts
  • storage
  • hooks와 routes
  • admin 설정, 페이지, 위젯, Portable Text 선언

디스크립터에는 Astro가 빌드 시점에 임포트하거나 노출해야 하는 정적 항목이 들어갑니다. 어떤 관리자 필드에 디스크립터와 런타임의 선언이 서로 일치해야 하는지는 각 주제별 가이드에서 확인하세요.

네이티브 라우트 핸들러

네이티브 라우트 핸들러는 하나의 RouteContext를 받습니다. 이는 검증된 입력과 요청 데이터를 일반 PluginContext와 결합한 것입니다.

routes: {
	status: {
		permission: "plugins:read",
		handler: async (ctx) => ({
			pluginId: ctx.plugin.id,
			callerId: ctx.user?.id ?? null,
		}),
	},
},

이에 해당하는 샌드박스 핸들러는 (routeCtx, ctx)를 두 개의 인수로 받습니다. 인증, 권한, 입력 스키마, 라우트 URL은 그 밖에는 공통 API 라우트 규약을 따릅니다.

네이티브 라우트가 request.body를 선언하면 definePluginRoute()로 감싸세요. 이 헬퍼는 본문 모드에서 ctx.input을 추론합니다. response: "raw"가 있는 네이티브 라우트는 pluginResponse()를 반환합니다. 두 헬퍼 모두 emdash에서 임포트하세요. 공통 API 라우트 가이드에 본문 모드, 제한, 응답 정책, 호환성 기본값이 나와 있습니다.

시크릿과 바인딩 읽기

네이티브 플러그인의 훅과 라우트 핸들러는 Astro 컨텍스트가 아니라 플러그인 컨텍스트를 받으므로 그 안에서는 Astro.locals를 사용할 수 없습니다. 시크릿과 플랫폼 바인딩은 런타임 환경에서 읽으세요.

배포 시크릿은 process.env에서 읽습니다. Node.js에서는 호스팅 플랫폼의 시크릿 관리자가 제공합니다. Cloudflare Workers에서는 wrangler secret put으로 설정한 시크릿이 Worker 시크릿에 설명된 대로 호환성 날짜가 2025-04-01 이후인 nodejs_compat 플래그를 통해 process.env에 전달됩니다.

사이트 관리자가 입력하는 자격 증명은 대신 생성된 설정 폼에서 secret 필드를 선언하세요. EmDash는 저장하기 전에 EMDASH_ENCRYPTION_KEY로 값을 암호화하므로, 관리자가 필드를 저장하기 전에 이 키를 설정하세요. 플러그인은 ctx.settings.get()으로 값을 읽습니다.

Cloudflare Workers에서 큐, R2 버킷, send_email 같은 바인딩은 cloudflare:workers의 env에 있는 객체입니다. Astro는 Node.js에서 astro.config.mjs를 로드하고, 이 파일은 디스크립터 팩토리를 위해 플러그인 패키지를 임포트합니다. Node.js는 cloudflare:workers를 로드할 수 없으므로 모듈 맨 위가 아니라 핸들러 안에서 임포트하세요. 다음 훅은 저장된 각 항목의 ID를 ACTIVITY_QUEUE로 바인딩된 큐에 보냅니다.

hooks: {
	"content:afterSave": async (event) => {
		const { env } = await import("cloudflare:workers");
		await env.ACTIVITY_QUEUE.send({ contentId: event.content.id });
	},
},

cloudflare:workers 모듈은 Workers 런타임에서만 해석되므로, 이를 임포트하는 플러그인은 Cloudflare 어댑터를 사용하는 사이트에서만 동작합니다.

이 임포트의 타입을 검사하려면 @cloudflare/workers-types를 패키지의 devDependencies와 tsconfig.json의 compilerOptions.types에 추가하세요. 그런 다음 플러그인이 읽는 각 바인딩을 Cloudflare.Env 인터페이스에 선언합니다. 다음 선언은 앞선 예시의 큐에 타입을 지정합니다.

declare namespace Cloudflare {
	interface Env {
		ACTIVITY_QUEUE: Queue;
	}
}

다른 표면 추가