開発環境と型定義


プラグインは2つの書き方から選べます。どちらでも同じことができます(型補完を除く)。

ビルドなしビルドあり
必要なものテキストエディタのみNode.js・バンドラ(webpack / Vite など)
API の呼び方window.ACMS 経由npm パッケージから import
型補完なしあり
ReactACMS.Library.React.createElementJSX が使える
管理画面のコンポーネントACMS.Library.components 経由で使える@ablogcms/components から import(実行時は ACMS.Library.components を参照させる)
向いているケース小〜中規模の拡張画面を作り込む拡張・チーム開発

実験的機能

この機能は、Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: パッケージのサブパスと型定義は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: テーマ向けの従来の機能(組み込み JS、ACMS.Library 経由の呼び出し)は、パッケージの変更の影響を受けません。
  • 公開 API の線引き: npm パッケージ に掲載しているものを公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

ビルドなしで書く

JS ファイルを1つ作り、<script type="module"> として読み込みます。

// /extension/acms/assets/my-plugin.js
ACMS.plugins.register('my-plugin', {
  setup(ctx) {
    ctx.events.on('media.upload.before', (event) => event);
  },
});
\Acms\Services\Facades\Asset::script('my-plugin', '/extension/acms/assets/my-plugin.js', [
    'position' => 'body_end',
    'attrs' => ['type' => 'module'],
]);

React が必要な場面(値を受け取る UI の差し込みなど)でも、ACMS.Library.React.createElement を使えばビルドは不要です。

ctx.ui.fill('preview.actions', function MyButton() {
  return ACMS.Library.React.createElement('button', { type: 'button', className: 'acms-admin-btn' }, '送信');
});

ACMS.Library.React は管理画面のスクリプト(admin.js)が読み込まれた後に使えます。トップレベルで参照する場合は、ACMS.adminReady(() => { ... }) で包んでください(アセット API 参照)。

ビルドありで書く

インストール

npm install @ablogcms/types@3.2.37

パッケージのバージョンは、利用している a-blog cms と同じバージョンを指定してください(上の例は Ver. 3.2.37 の場合)。パッケージは CMS 本体と同じバージョン番号で公開しており、Ver. 3.2.29 以降のものを npm から入手できます。

用途に応じて追加します。

使いたいものパッケージ
ACMS グローバルの型@ablogcms/types
イベントの型@ablogcms/events
プラグイン API の型・withFillProps@ablogcms/plugins
管理画面の UI コンポーネント@ablogcms/components
ブロックエディターの型・Tiptap@ablogcms/block-editor
ユニットエディターの型@ablogcms/unit-editor

一覧は npm パッケージ を参照してください。

型定義を有効にする

window.ACMS の型を有効にするには、tsconfig.json に追加します。

{
  "compilerOptions": {
    "types": ["@ablogcms/types/acms"]
  }
}

使う機能の型は、対応するパッケージを import した分だけ厳密になります。 型だけを読み込むため、import type {} from と書きます(バンドルには含まれません)。

import type {} from '@ablogcms/types/acms';
import type {} from '@ablogcms/events/acms'; // ACMS.events の型が付く
import type {} from '@ablogcms/url/acms'; // ACMS.Library.acmsLink の型が付く

ACMS.plugins.register('my-plugin', {
  setup(ctx) {
    ctx.events.on('media.upload.before', (payload) => {
      console.log(payload.files);
    });
  },
});

イベントの型を自分で宣言する

イベントが受け取る値の型は、AcmsEventMap に宣言したものだけが付きます。a-blog cms 本体のイベントの型は npm パッケージには含まれないため、使うイベントは自分のプロジェクトで宣言します。独自のイベントを発火する場合も同じ方法で宣言します。

import type { AcmsGateEvent, AcmsNotifyEvent } from '@ablogcms/events';

declare module '@ablogcms/events' {
  interface AcmsEventMap {
    'media.upload.before': AcmsGateEvent<{ files: File[] }>;
    'my-plugin.sync.success': AcmsNotifyEvent<{ count: number }>;
  }
}

宣言しなくてもイベントは使えます(値の型が緩くなるだけです)。

JSX で書く

import { withFillProps } from '@ablogcms/plugins';

type AltFillProps = {
  item: { media_title: string };
  setAlt: (value: string) => void;
};

ACMS.plugins.register('my-plugin', {
  setup(ctx) {
    ctx.ui.fill(
      'media.edit-modal.field.alt',
      withFillProps<AltFillProps>(({ item, setAlt }) => (
        <button type="button" className="acms-admin-btn" onClick={() => setAlt(item.media_title)}>
          タイトルを入れる
        </button>
      ))
    );
  },
});

React を同梱しないでください

React をプラグインのバンドルに含めると動作しません

管理画面が読み込んでいる React と、プラグインがバンドルした React は別のものになります。React は同じページに2つ存在すると正しく動作しません(画面が真っ白になる、状態が更新されないなどの症状が出ます)。

React に依存するパッケージは react を peerDependencies として要求します。バンドラの設定で React を外部依存にし、管理画面が公開している ACMS.Library を参照させてください。 react/jsx-runtime も外部依存にします(外し忘れると、react/jsx-runtime だけがバンドルに入ります)。

外部依存にするもの参照先
reactACMS.Library.React
react/jsx-runtimeACMS.Library.ReactJsxRuntime
react-domACMS.Library.ReactDom
react-dom/clientACMS.Library.ReactDomClient
// webpack.config.js
module.exports = {
  mode: 'production',
  entry: './src/plugin.tsx',
  output: { filename: 'plugin.js' },
  resolve: { extensions: ['.tsx', '.ts', '.js'] },
  module: {
    rules: [{ test: /\.tsx?$/, use: 'babel-loader' }],
  },
  externals: {
    react: 'var ACMS.Library.React',
    'react/jsx-runtime': 'var ACMS.Library.ReactJsxRuntime',
    'react-dom': 'var ACMS.Library.ReactDom',
    'react-dom/client': 'var ACMS.Library.ReactDomClient',
  },
};
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    lib: { entry: 'src/plugin.tsx', name: 'MyPlugin', formats: ['iife'], fileName: () => 'plugin.js' },
    rollupOptions: {
      external: ['react', 'react/jsx-runtime', 'react-dom', 'react-dom/client'],
      output: {
        globals: {
          react: 'ACMS.Library.React',
          'react/jsx-runtime': 'ACMS.Library.ReactJsxRuntime',
          'react-dom': 'ACMS.Library.ReactDom',
          'react-dom/client': 'ACMS.Library.ReactDomClient',
        },
      },
    },
  },
});

@ablogcms/components を import する場合も、バンドルに含めず外部依存にします。サブパス名は ACMS.Library.components のキーと同じです(button-v2 のようにハイフンを含む名前は components['button-v2'] と書きます)。

// webpack.config.js の externals に追加する
'@ablogcms/components/popover': 'var ACMS.Library.components.popover',
'@ablogcms/components/button-v2': "var ACMS.Library.components['button-v2']",

ACMS.Library.components で使えないサブパスは、dataview / date-picker / time-picker / syntax-highlight / rich-select / select / device-mode / icon-picker の8つです。

出力形式は IIFE にしてください。 ESM で出力すると import ... from "react" が残り、ブラウザが解決できません。IIFE で出力したファイルを、<script type="module"> として読み込みます。配布用のファイルは production ビルドで作ってください(開発用の react/jsx-dev-runtime は共有していません)。

Tiptap を参照する

ブロックエディターの拡張を書く場合、Tiptap と ProseMirror は @ablogcms/block-editor/tiptap/* から参照します。

import type { Tiptap } from '@ablogcms/block-editor/tiptap';
import type { Node } from '@ablogcms/block-editor/tiptap/pm/model';

型としてのみ import してください

値として import すると、Tiptap がプラグインのバンドルに取り込まれ、ブロックエディター本体とは別のインスタンスになります(エディターが動かなくなります)。

実際に使う値は、registerBlockDefinition / registerExtension に渡した関数の引数として受け取ります。

ctx.blockEditor.registerExtension('callout', ({ core }) =>
  core.Node.create({ name: 'callout', group: 'block', content: 'paragraph+' })
);

その他の注意

  • ESM のみを配布しています。 require() では読み込めません。バンドラ、または ESM に対応した Node.js で利用してください。
  • 公開しているサブパス以外を import しないでください。 内部のパスは予告なく変わります。

関連ページ