# 概要 [[Obsidian Sample Plugin]]を使って[[Obsidianプラグイン]]を作り始めるチュートリアル。 ## 利用技術 - [[TypeScript]] - [[Obsidian API]] > [!caution] > [[Obsidian API]]はearly alpha版のため破壊的変更に対応すること ## 確認環境 - [[Windows 11]] - [[Node.js]] v18.12.0 - [[Obsidian]] v1.3.7 # 開発準備 ## プロジェクト作成 [[Obsidian Sample Plugin]]をテンプレートしてリポジトリを作成する。 ![[Pasted image 20230702140701.png]] 作成したリポジトリをクローンする。 > [!caution] > クローンは[[Vault]]配下の`.obsidian/plugins/<your-plugin-name>`にしておくと動作確認が楽だが、[[Obsidian Sync]]でプラグインを同期している場合は、`node_modules`が同期されてしまうためオススメしない。 > [!caution] > 2022-10-26時点でのテンプレートは、[[Rollup]]でなく[[esbuild]]が使用されている。そのため、本稿も[[esbuild]]の前提で記載しなおしている。[[Rollup]]を使っていたときのテンプレートを利用しており[[esbuild]]に移行したい場合は、[[📜2022-09-11 Obsidianプラグイン開発のビルドツールをrollupからesbuildに移行する]] が参考になるはず。 ### [[manifest.json (Obsidian)|manifest.json]]の設定 以下の設定を変更する。 | プロパティ | 個人的な推奨値 | | ------------- | -------------------------------- | | id | リポジトリのslugと同じ | | name | プラグイン名 | | version | 0.0.0 | | minAppVersion | [[Obsidian API]]の最新バージョン | | description | プラグインの説明 | | isDesktopOnly | モバイル非対応なら`true` | ```json { "id": "obsidian-free-writing", "name": "Free Writing", "version": "0.0.0", "minAppVersion": "1.2.8", "description": "TODO", "author": "tadashi-aikawa", "isDesktopOnly": false } ``` > [!info] descriptionについて > [Publishにあたってのガイド](https://docs.obsidian.md/Plugins/Releasing/Submission+requirements+for+plugins#Keep+plugin+descriptions+short+and+simple) に従わないと、[[コミュニティプラグイン]]のプルリクエストを通過しない。特に以下の点に注意すること。 > > - [[📚Obsidian documentation Style guide]]に準拠する > - `"This is a plugin"`のような自明な説明をしない > - ピリオド(`.`)で終了する > - 絵文字や環境依存文字を割ける > - 固有名詞や商標、頭字語には正しく大文字を使用する ### [[package.json]]の設定 `name`, `version`, `description`を[[manifest.json (Obsidian)|manifest.json]]にあわせて設定。 ```json { "name": "obsidian-free-writing", "version": "0.0.0", "description": "TODO", } ``` ### versionsの設定 [[versions.json]]を一旦空にする。リリース時に自動で追加されれば良い。 ```json {} ``` ### [[Prettier]]のインストール > [!info] この設定は任意 [[Prettier]]派なので`.editorconfig`を削除する。 ```console $ rm .editorconfig ``` [[フォーマッター]]として[[Prettier]]をインストール。 ```console npm i -D prettier ``` [[.prettierrc]]を作成。 ```json { "endOfLine": "lf" } ``` ### [[tsconfig.json]]の設定 > [!info] この設定は任意 テンプレートのデフォルトでは[[strict]]が`false`になっているので`true`にする。そうしないとNull安全なコードが書けず、プルリクエストの差し戻しも増える。 [[Parameter Properties]]を使いたい場合はエラーになるので、[[strictPropertyInitialization]]を別途`false`にしておく。 ```diff { "compilerOptions": { + "strict": true, - "strictNullChecks": true, + "strictPropertyInitialization": false, }, } ``` ### [[ESLint]]の削除 > [!info] この設定は任意 [[ESLint]]は不要なので削除。 ```console npm uninstall @typescript-eslint/eslint-plugin @typescript-eslint/parser rm .eslint* ``` ### 依存パッケージインストール Cloneしたらpackageのインストール。 ```console $ npm i ``` ## ビルド `main.ts`からエントリポイントとなる`main.js`をビルドする。 ```console npm run dev ``` watchが有効になっているため変更待ちになるが、一旦強制終了する。`main.js`が生成されていればOK。 ## プラグインの反映 `main.js`と[[manifest.json (Obsidian)|manifest.json]]がプラグイン本体となるので、それを[[Obsidian]]で読み込み利用できるようにする。 ### プラグインの移動 [[Vaultルート]]を開き、`.obsidian/plugins`配下に同名のディレクトリを作成する。 ```console cd ${your vault root} cd .obsidian/plugins mkdir obsidian-free-writing ``` `obsidian-free-writing`ディレクトリ配下には、[[Obsidianプラグインに必要なファイル]]を格納する必要がある。先ほど生成した`main.js`をここにコピーする。 ```console cp main.js ${your vault root}/.obsidian/plugins/obsidian-free-writing/ cp manifest.json ${your vault root}/.obsidian/plugins/obsidian-free-writing/ ``` 上記 `obsidian-free-writing` のことを **以後は『プラグインディレクトリ』と呼ぶ**。 ### 動作確認 [[Obsidian]]を再起動して設定から今回作成するプラグインを有効にする。 ![[Pasted image 20230702150101.png]] `Free Writing`のコマンドが表示され、sample modalが表示されればOK。 ![[Pasted image 20230702150237.png]] ## ホットリロード > [!info] この設定は任意 通常は`main.js`などの成果物を更新しても、[[Obsidian]]を再起動しなければプラグインは再読み込みされない。これでは作業効率が下がるため、[[📘Obsidianプラグイン開発で自動リロードさせる]]ことを推奨する。 <div class="link-card"> <div class="link-card-header"> <img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" class="link-card-site-icon"/> <span class="link-card-site-name">minerva.mamansoft.net</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">📘Obsidianプラグイン開発で自動リロードさせる</p> </div> <div class="link-card-description"> Obsidianプラグインを開発しているとき、コードベースに変更があったら自動でObsidianをリロードする仕組みを整えました。 </div> </div> <img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/%F0%9F%93%98Articles/attachments/2021-09-20.jpg" class="link-card-image" /> </div> <a class="internal-link" data-href="📘Obsidianプラグイン開発で自動リロードさせる" ></a> </div> %%[[📘Obsidianプラグイン開発で自動リロードさせる]]%% 仕組みは上記の記事を参照してもらうとして、実際に行う作業は以下のとおり。 [[watchexec]]と[[Hot Reload (Obsidian)|Hot Reload]]をインストールしていなければインストールする。 プラグインディレクトリに空の `.hotreload` を作成する。 ```console touch ${your vault root}/.obsidian/plugins/obsidian-free-writing/.hotreload ``` リポジトリディレクトリ配下に[[Taskfile.yml]]を作成する。以下は一例。 ```yaml version: "3" tasks: default: - task: help help: silent: true cmds: - task -l build:dev: npm run dev build: npm run build watch: watchexec --no-vcs-ignore --exts "js,json,css" cp main.js styles.css manifest.json $USERPROFILE/work/minerva/.obsidian/plugins/obsidian-free-writing/ dev: desc: Build and copy files when they are updated. deps: - build:dev - watch ``` 起動する。 ```console task dev ``` 右上にリロードした旨のトーストが表示されればOK。 ![[Pasted image 20230702151117.png]] ## コミット前に[[TypeScript]]の型チェックをする > [!info] この設定は任意 [[esbuildはTypeScriptの型チェックをしない]]ので、[[📕huskyでコミット前に自動でTypeScriptの型チェックとテスト実行]]する仕組みを作った方がいい。 <div class="link-card"> <div class="link-card-header"> <img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" class="link-card-site-icon"/> <span class="link-card-site-name">minerva.mamansoft.net</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">📕huskyでコミット前に自動でTypeScriptの型チェックとテスト実行</p> </div> </div> <img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/minerva-image.webp" class="link-card-image" /> </div> <a class="internal-link" data-href="📕huskyでコミット前に自動でTypeScriptの型チェックとテスト実行" ></a> </div> %%[[📕huskyでコミット前に自動でTypeScriptの型チェックとテスト実行]]%% # ベースコード変更 > [!info] この設定は任意 デフォルトのコードはチュートリアルな側面もあるため、無駄な部分をそぎ落とす。既存コードを削除。 ```console rm main.ts main.js ``` `src/settings.ts`を作成。設定はこの中で完結させる。 ```ts import { App, PluginSettingTab, Setting } from "obsidian"; import type FreeWritingPlugin from "./main"; export interface Settings { hoge: string; } export const DEFAULT_SETTINGS: Settings = { hoge: "", }; export class FreeWritingSettingTab extends PluginSettingTab { plugin: FreeWritingPlugin; constructor(app: App, plugin: FreeWritingPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); new Setting(containerEl).setName("Hoge").addText((text) => text .setPlaceholder("ex: hoge.md") .setValue(this.plugin.settings.hoge) .onChange(async (value) => { this.plugin.settings.hoge = value; await this.plugin.saveSettings(); }) ); } } ``` `src/app-helper.ts`を作成。`App`のwrapperで使いやすい独自IFと処理を定義する。実際はもっとIFや[[メソッド (TypeScript)|メソッド]]がある。 ```ts import { App, Editor, MarkdownView, TFile } from "obsidian"; interface UnsafeAppInterface { commands: { commands: { [commandId: string]: any }; executeCommandById(commandId: string): boolean; }; } export class AppHelper { private unsafeApp: App & UnsafeAppInterface; constructor(app: App) { this.unsafeApp = app as any; } async loadFile(path: string): Promise<string> { return this.unsafeApp.vault.adapter.read(path); } getActiveFile(): TFile | null { // noinspection TailRecursionJS return this.unsafeApp.workspace.getActiveFile(); } getActiveMarkdownView(): MarkdownView | null { return this.unsafeApp.workspace.getActiveViewOfType(MarkdownView); } getActiveMarkdownEditor(): Editor | null { return this.getActiveMarkdownView()?.editor ?? null; } } ``` コマンドは `src/commands.ts` に集約する。 ```ts import { Command } from "obsidian"; import { Settings } from "./settings"; import { AppHelper } from "./app-helper"; export function createCommands( appHelper: AppHelper, settings: Settings ): Command[] { return [ { id: "main-command", name: "Main command", checkCallback: (checking: boolean) => { if (appHelper.getActiveFile() && appHelper.getActiveMarkdownView()) { if (!checking) { // TODO: } return true; } }, }, ]; } ``` 最後に`src/main.ts`を作成。 ```ts import { Plugin } from "obsidian"; import { DEFAULT_SETTINGS, Settings, FreeWritingSettingTab } from "./settings"; import { AppHelper } from "./app-helper"; import { createCommands } from "./commands"; export default class FreeWritingPlugin extends Plugin { settings: Settings; appHelper: AppHelper; async onload() { await this.loadSettings(); this.appHelper = new AppHelper(this.app); this.init(); createCommands(this.appHelper, this.settings).forEach((c) => this.addCommand(c) ); this.addSettingTab(new FreeWritingSettingTab(this.app, this)); } private init() { // UIなどの登録系はここ } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings() { await this.saveData(this.settings); this.init(); } } ``` エントリポイントが変わったので`esbuild.config.mjs`に変更が必要。 ```diff const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ["main.ts"], + entryPoints: ["src/main.ts"], ``` # テスト > [!info] この設定は任意 [[Obsidianプラグイン開発でテストコード]]を書く準備をする。その後に `src/hoge.test.ts` を作って動作することを確認する。 ```ts import { describe, expect, test } from "@jest/globals"; describe("from", () => { describe.each<{ a: number; b: number; expected: number; }>` a | b | expected ${1} | ${2} | ${3} `("test", ({ a, b, expected }) => { test(`test(${a}, ${b}, ${expected})`, () => { const actual = a + b; expect(actual).toBe(expected); }); }); }); ``` [[package.json]]に`scripts.test`を追加。 ```diff "scripts": { + "test": "jest", } ``` テストが実行されればOK。 ```console $ npm run test > [email protected] test > jest PASS src/hoge.test.ts from test √ test(1, 2, 3) (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.288 s Ran all test suites. ``` [[Taskfile.yml]]にも`test`を追加しておく。 ```yaml version: "3" tasks: default: - task: help help: silent: true cmds: - task -l build:dev: npm run dev build: npm run build test: npm test watch: watchexec --no-vcs-ignore --exts "js,json,css" cp main.js styles.css manifest.json $USERPROFILE/work/minerva/.obsidian/plugins/obsidian-embedded-code-title/ dev: desc: Build and copy files when they are updated. deps: - build:dev - watch ``` # UIライブラリの使用 > [!info] この設定は任意 [[React]]や[[Svelte]]を使ってUIを開発したい場合。 ### [[React]] 以下を参照。 <div class="link-card"> <div class="link-card-header"> <img src="https://obsidian.md/favicon.ico" class="link-card-site-icon"/> <span class="link-card-site-name">marcus.se.net</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">React | Obsidian Plugin Developer Docs</p> </div> <div class="link-card-description"> In this guide, you'll configure your plugin to use React. It assumes that you already have a plugin ... </div> </div> </div> <a href="https://marcus.se.net/obsidian-plugin-docs/getting-started/react"></a> </div> [[📜Obsidianプラグイン開発でReactを導入]] も参考に。 ### [[Svelte]] 以下を参照。 <div class="link-card"> <div class="link-card-header"> <img src="https://obsidian.md/favicon.ico" class="link-card-site-icon"/> <span class="link-card-site-name">marcus.se.net</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">Svelte | Obsidian Plugin Developer Docs</p> </div> <div class="link-card-description"> This guide explains how to configure your plugin to use Svelte, a light-weight alternative to tradit... </div> </div> </div> <a href="https://marcus.se.net/obsidian-plugin-docs/getting-started/svelte"></a> </div> [[📜2023-01-29 SilhouetteにSvelteを導入する]] も参考に。 # リリース ## リリースに必要な作業と内訳 リリースに必要な作業と、それをどこで実行するかは以下のとおり。 | 作業 | どこで | | ------------------------------- | ------------------- | | バージョンのタグ付け | `npm version` | | `manifest.json`のバージョン更新 | `versions-bump.mjs` | | リリースビルド | [[GitHub Actions]] | | 成果物のアップロード | [[GitHub Actions]] | ## 準備/確認 ### バージョンprefixの制御 > [!info] デフォルトで設定されているので、変更の必要はない。 バージョンにvのprefixをつけてはいけないので、[[.npmrc]]ファイルでそのように設定されている必要がある。 ``` tag-version-prefix="" ``` ### バージョンアップ用の[[JavaScript]]ファイル編集 > [!info] このscriptは任意。リリースのたびに手動設定でも良い。 [[クロスプラットフォーム]]環境でリリースできるようにするため、[[CLI]]のコマンドに極力依存しないよう[[JavaScript]]ファイルが用意されている。これを少し変更して以下のようにする。 `version-bump.mjs` ```js import { readFileSync, writeFileSync } from "fs"; const targetVersion = process.env.npm_package_version; function updateVersion(beta) { const manifestBeta = JSON.parse(readFileSync("manifest-beta.json", "utf8")); manifestBeta.version = targetVersion; writeFileSync("manifest-beta.json", JSON.stringify(manifestBeta, null, " ")); if (beta) { return; } const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); const { minAppVersion } = manifest; manifest.version = targetVersion; writeFileSync("manifest.json", JSON.stringify(manifest, null, " ")); // update versions.json with target version and minAppVersion from manifest.json let versions = JSON.parse(readFileSync("versions.json", "utf8")); versions[targetVersion] = minAppVersion; writeFileSync("versions.json", JSON.stringify(versions, null, " ")); } updateVersion(targetVersion.includes("beta")); ``` バージョンに`beta`の文字列を含む場合のみベータバージョンとして、それ以外の場合は正式バージョンとして各ファイルのバージョンインクリメントを行う。 > [!caution] > ベータリリース機能を使うには `manifest-beta.json` が存在するという前提が必要。ベータリリースが不要なら、もともとテンプレートで用意されていたコードをそのまま使えばよい。 ### バージョンアップのnpmコマンド追加 > [!info] このscriptは任意。リリースのたびに手動設定でも良い。 `npm version`コマンド実行時に併せて実行されるよう[[package.json]]に追加。 ```json "scripts": { "version": "node version-bump.mjs && git add manifest-beta.json manifest.json versions.json" } ``` > [!caution] > ベータリリース機能が不要なら `manifest-beta.json` はコマンドから削除する。 ### [[GitHub Actions]]の設定 タグがpushされたら自動で成果物をビルドし、[[GitHub]]でリリースするためのAction。 `.github/workflows/release.yaml`を作成する。 ```yaml name: "Release" on: push: tags: - "*" jobs: release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm install - run: npm run build - name: Create Release id: create_release uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: draft: true files: | main.js styles.css manifest.json manifest-beta.json ``` > [!caution] > リリース以外でもタグを使う場合は、`on.push.tags[]`の設定を変更する必要がある。 > [!caution] > ベータリリース機能が不要なら `manifest-beta.json` は `files` から削除する。 ### タスクの設定 > [!info] このscriptは任意。リリースのたびに手動実行でも良い。 リリースに必要な作業を1コマンドで実行できるよう[[Taskfile.yml]]を記載する。 ```yaml release: desc: | Build ∟ [Ex] task release VERSION=1.2.3 ∟ [Ex] task release VERSION=1.2.3-beta deps: - ci cmds: - npm version {{.VERSION}} - task: ci - git push --tags - git push preconditions: - sh: "[ {{.VERSION}} != '' ]" msg: "VERSION is required." ``` このコマンドはバージョンのインクリメントと、テスト/リリース可能なことの確認、および[[Git]]やタグの情報更新をしているだけ。`task release`コマンドを実行してもリリース物はアップロードされない。それは`git push`をトリガーに[[GitHub Actions]]で自動的に実施する。 > [!hint]- `task: ci`について > `task: ci`と関連コマンドの定義は以下の通り。リリース前に確認が不要なら必要ない。 > ```yaml > init: > desc: Install dependencies > cmds: > - npm install > build: npm run build > test: npm test > > ci: > desc: For CI > cmds: > - task: init > - task: build > - task: test > ``` ## リリースする リリースコマンドを実行するだけ。 ```console task release VERSION=0.1.0 ``` ## 公開 [[Obsidian Pluginをコミュニティプラグインとして公開]]する。 # 参考 - [[ObsidianプラグインやTemplaterでCORS制限を回避する方法]]