開発環境と型定義
プラグインは2つの書き方から選べます。どちらでも同じことができます(型補完を除く)。
| ビルドなし | ビルドあり | |
|---|---|---|
| 必要なもの | テキストエディタのみ | Node.js・バンドラ(webpack / Vite など) |
| API の呼び方 | window.ACMS 経由 | npm パッケージから import |
| 型補完 | なし | あり |
| React | ACMS.Library.React.createElement | JSX が使える |
| 管理画面のコンポーネント | 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 だけがバンドルに入ります)。
| 外部依存にするもの | 参照先 |
|---|---|
react | ACMS.Library.React |
react/jsx-runtime | ACMS.Library.ReactJsxRuntime |
react-dom | ACMS.Library.ReactDom |
react-dom/client | ACMS.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 しないでください。 内部のパスは予告なく変わります。