## 経緯 [[Templater]]は大変便利だが、scriptをいくつも作っていると毎回同じような処理を書くことがある。[[Script User Functions]]はそれを補うための良い仕組みだが、以下の点でメンテナンスがしにくいという欠点がある。 - [[Vault]]内に存在し、バージョン管理されていない - [[JavaScript]]なので型が存在しない - ファイル数が増えてくると億劫 以下のように使えるスクリプトが欲しい。 - バージョン管理されている - [[TypeScript]]で安全に開発できる - [[JavaScript]]ファイル1つで利用できる ## [[Script User Functions]]の挙動とIF `test.js`を作成し ```js async function insert(text) { const editor = app.workspace.activeLeaf.view.editor; editor.replaceRange(text, CodeMirror.Pos(-1)); } async function insert2(text) { const editor = app.workspace.activeLeaf.view.editor; editor.replaceRange(text, CodeMirror.Pos(-1)); editor.replaceRange(text, CodeMirror.Pos(-1)); } module.exports = () => ({ insert, insert2, }); ``` 以下のような[[Templaterスクリプト]]を作成したら動いた。 ```js <%* const {insert, insert2} = tp.user.test() insert("Mimizou") insert2("Tatsuwo") %> ``` ## リポジトリ作成 [[🧊Obsidian Tempura]]というリポジトリ名で。[[Ubuntu]]環境に[[Bun]]でつくる。 ```console mkdir obsidian-templater-use cd $_ bun init -y ``` appのインターフェースは少なくとも[[Obsidian API]]に含まれると想定できるので、これを使ってみる。 ```console bun add -D obsidian ``` ## [[TypeScript]]ファイルをつくる コードを書く。 ```ts import { App, Editor, Vault } from "obsidian"; type UVault = Vault & { config: { spellcheckDictionary?: string[]; useMarkdownLinks?: false; newLinkFormat?: "shortest" | "relative" | "absolute"; }; }; type UApp = App & { isMobile: boolean; vault: UVault; }; declare let app: UApp; const getActiveEditor = (): Editor | null => app.workspace.activeEditor?.editor ?? null; /** /* textをカーソル位置に挿入する */ async function insert(text: string): Promise<void> { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } export default () => ({ insert, }); ``` ## [[TypeScript]]ファイルを[[JavaScript]]ファイルに変換する 普通にビルドすると[[ES modules]]っぽいので[[Templater]]では使えなさそう。Issues検索してもヒットしなかったし。`--format`オプションが[[Bun]]にあるので指定してみる。 ```console $ bun build . --format cjs error: Formats besides 'esm' are not implemented ``` 今後予定はしているが、現在は未実装ぽい。[[esbuild]]を試してみる。 ```console $ bun add -D esbuild $ bun esbuild . "use strict"; const getActiveEditor = () => app.workspace.activeEditor?.editor ?? null; async function insert(text) { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } export default () => ({ insert }); ``` 良さそうな気がする。ただ、これだと `export default` を認識できない。`module.export`にしなければいけない。 [[esbuild]]のオプションを色々試してみたが、うまくいかなかったので[[sed]]で無理やりやることにした。 ```console $ bun esbuild . | sed 's/export default/module.exports =/g' "use strict"; const getActiveEditor = () => app.workspace.activeEditor?.editor ?? null; async function insert(text) { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } module.exports = () => ({ insert }); ``` これは動作したので `index.js` として作成する。 ```console bun esbuild . | sed 's/export default/module.exports =/g' > index.js ``` ## 型定義ファイルをつくる 型定義も欲しい気がするので `d.ts` ファイルも作成したい。 #2023/10/21 時点では[[📝bun buildで型定義を生成できない]]し、[[esbuild]]は型に関するサポートはしていないため、[[tsc]]を使う。 ```console npx tsc --declaration --noEmit false --emitDeclarationOnly ``` > [!hint] > [[Bun]]は[[tsconfig.json]]を利用しするが、[[tsc]]のトランスパイル機能は利用しないため [[noEmit]] オプションが `true` に設定されている。ただ、[[emitDeclarationOnly]]は[[noEmit]]オプションが`true`だと利用できないため `--noEmit false` でCLIから上書きする必要がある。 `index.d.ts`が作成される。 ```ts /** /* textをカーソル位置に挿入する */ declare function insert(text: string): Promise<void>; declare const _default: () => { insert: typeof insert; }; export default _default; ``` ## タスク化する [[package.json]]と[[tsconfig.json]]にそれぞれ設定を追加する。 `package.json` ```json "scripts": { "build": "mkdir -p dist && bun esbuild . | sed 's/export default/module.exports =/g' > dist/index.js", "build:type": "tsc --declaration --noEmit false --emitDeclarationOnly --outDir dist --tsBuildInfoFile .tsbuildinfo", "build:all": "bun run build && bun run build:type" }, ``` `tsconfig.json` ```json { "exclude": ["node_modules", "dist"] } ``` `bun build:all`で`dist`配下に`index.js`と`index.d.ts`が作成されるようになった。 > [!hint] > 他にも `.tsbuildinfo` が生成される。これは[[tsc]]の[[インクリメンタルコンパイル (TypeScript)|インクリメンタルコンパイル]]に使われる。正直なくても良いが、無効化するのが面倒そうだったので生成場所だけ変更するために `--tsBuildInfoFile .tsbuildinfo` オプションを指定した。 ## [[TypeScript]]ファイルを分割する `index.ts`だけでなく複数のファイルに分割する。エントリポイントは`index.ts`。 `index.ts`に直接 `module.exports` と書いても問題なかった。 `index.ts` ```ts import { getActiveEditor } from "./helper"; /** * Insert text at the cursor position */ async function insert(text: string): Promise<void> { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } module.exports = () => ({ insert, }); ``` このままだと警告が出るので `package.json`で[[CommonJS]]をデフォルトにする。[[ES modules]]にする必要はないと思うので。 ```diff { - "type": "module" + "type": "commonjs" } ``` `index.ts`から利用している[[TypeScript]]ファイル。 `helper.ts` ```ts import { UApp, UEditor } from "./types"; declare let app: UApp; export const getActiveEditor = (): UEditor | null => app.workspace.activeEditor?.editor ?? null; ``` `types.ts` ```ts import { App, Editor, Vault } from "obsidian"; export type UEditor = Editor; export type UVault = Vault & { config: { spellcheckDictionary?: string[]; useMarkdownLinks?: false; newLinkFormat?: "shortest" | "relative" | "absolute"; }; }; export type UApp = App & { isMobile: boolean; vault: UVault; }; ``` 普通にビルドすると、ファイルの数だけ[[JavaScript]]ファイルができてしまうので、[[esbuild]]の`--bundle`オプションを使う。 ```console $ bun esbuild --bundle index.ts --platform=neutral --format=cjs "use strict"; // helper.ts var getActiveEditor = () => app.workspace.activeEditor?.editor ?? null; // index.ts async function insert(text) { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } module.exports = () => ({ insert }); ``` ただ、今度は型定義ファイルがうまく作れなくなった... がコード分割して `index.ts` から無駄な実装が減ったから、それを見ればいい気がした。 というわけで `package.json` の `scripts` は。 ```json "scripts": { "build": "mkdir -p dist && esbuild --bundle index.ts --platform=neutral --format=cjs > dist/index.js", "release": "git tag ${VERSION} && git push origin ${VERSION}" }, ``` ## ドキュメントの生成 [[TypeDoc]]を使う。 ```console bun add -D typedoc ``` `export`を明示的につけないとドキュメントとして公開されないので ```diff /** * Insert text at the cursor position */ - async function insert(text: string): Promise<void> { + export async function insert(text: string): Promise<void> { ``` あとは以下のコマンドで`docs`が生成される。 ```console bunx typedoc index.ts ``` と思ったが `export` をつけることによって `index.ts` のビルド結果が変わってしまった... マジか...。他の方法を考える。 [[typedoc-plugin-not-exported]]を使えばいけるとか... ```console bun add -D typedoc-plugin-not-exported bunx typedoc index.ts --plugin typedoc-plugin-not-exported ``` ダメだった... [[typedoc-plugin-missing-exports]]の方がよさそう ```console bun add -D typedoc-plugin-missing-exports bunx typedoc index.ts --plugin typedoc-plugin-missing-exports ``` これもダメだった。。[[CommonJS]]だとキツイのかもしれない...。 諦めて、公開IFのファイルを外だししてみることにした。つまり `index.ts` を `functions.ts` とに分離する。 `functions.ts` ```ts import { getActiveEditor } from "./helper"; /** * Insert text at the cursor position */ export async function insert(text: string): Promise<void> { const editor = getActiveEditor(); if (!editor) { return; } editor.replaceRange(text, editor.getCursor()); } ``` `index.ts` ```ts import { insert } from "./functions"; module.exports = () => ({ insert, }); ``` そして、ドキュメントビルドコマンドのエントリポイントを`index.ts`から`functions.ts`にする。 ```console typedoc functions.ts ``` これなら`functions.ts`は[[ES modules]]準拠の書き方で問題ないし、成果物ビルドのエントリポイントは`index.ts`だから、[[Templater]]での認識もできる。`import`は[[esbuild]]のバンドラが解決するので気にしなくていい。 ## [[GitHub Actions]]で自動化 1つの[[ジョブ (GitHub Actions)|ジョブ]]で、成果物とドキュメントのデプロイをやってしまう。 ```yaml name: "Release" on: push: tags: - "*" jobs: release: runs-on: ubuntu-latest permissions: contents: write pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v3 - uses: oven-sh/setup-bun@v1 with: bun-version: 1.0.7 - run: bun install - run: bun run build - run: bun run build:docs - uses: actions/configure-pages@v3 - uses: actions/upload-pages-artifact@v2 with: path: 'docs' - uses: softprops/action-gh-release@v1 id: create_release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: draft: true files: | dist/useObsidian.js - uses: actions/deploy-pages@v2 id: deployment ``` `package.json`のリリースコマンドを実行。 ```json "scripts": { "release": "git tag ${VERSION} && git push origin ${VERSION}" }, ``` ```console VERSION=20231022.4 bun release ``` タグがpushされると[[GitHub Actions]]が動く。 - [実行結果](https://github.com/tadashi-aikawa/obsidian-templater-use/actions/runs/6602286838) - [ドキュメント](https://tadashi-aikawa.github.io/obsidian-templater-use/) - [リリースノートと成果物](https://github.com/tadashi-aikawa/obsidian-templater-use/releases/tag/20231022.4) 今はまだ `insert` しかできないけど。 ## [[Templater]]で利用する `useObsidian.js`をクリックしてダウンロード。 ![[Pasted image 20231022155721.png]] [[Templater]]の設定で`Script files folder location`に指定されたディレクトリ配下に`useObsidian.js`を入れる。以下のように `tp.user.useObsidian` が認識さればOK。 ![[Pasted image 20231022155819.png|frame]] あとは[[Templaterスクリプト]]で以下のようにして使うだけ。 ```js <%* const { insert } = tp.user.useObsidian() insert("🦉Mimizou") %> ``` コマンドを実行して`🦉Mimizou`が挿入されればOK。今後コマンドが増えれば以下のように追加できる。 ```js <%* const { insert, newFunc } = tp.user.useObsidian() insert("🦉Mimizou") newFunc() %> ``` 利用できる関数一覧と定義は [[📚Obsidian Tempuraのドキュメント]] を参照すれば分かるようになっていくはず。 ## [[esbuild]]のファイル化 CLIコマンドだと長いのでファイルにする。 `esbuild.config.mjs` ```js import * as esbuild from "esbuild"; await esbuild.build({ entryPoints: ["src/index.ts"], platform: "neutral", format: "cjs", bundle: true, outfile: "dist/useObsidian.js", // Bundled in Obsidian external: ["obsidian"], }); ``` 以下でビルド。 ```console bun esbuild.config.mjs ``` ## obsidianをrequireしてしまう `bun build`のエラーは `external: ["obsidian"]` で回避したが、bundleされたjavascriptファイルでrequireをしており、それがエラーになる。 ```error plugin:templater-obsidian:61 Templater Error: Failed to load user script at "_Privates/Templater/Scripts/useObsidian.js". Cannot find module 'obsidian' Require stack: ``` 問題の部分。 ```js // src/helper.ts var import_obsidian = require("obsidian"); ``` ## TODO - [x] 1つの[[JavaScript]]ファイルで複数の関数を利用できることの確認 - [x] [[TypeScript]]から[[JavaScript]]ファイルにbundleできるリポジトリ作成 - [x] [[Bun]]でつくる - [x] insert1をつくる - [-] bun build - [x] [[esbuild]]を試す - [x] [[Templater]]で使えるjsファイルをつくる - [x] 型定義ファイルを作る - [x] insert2をつくる - [x] タスクをつくる - [x] READMEをつくる - [x] 上記リポジトリを[[GitHub]]で管理 - [x] Insertを真面目につくる - [x] [[GitHub Actions]]でリリースできるようにする - [x] ファイル分割 - [x] 型定義ファイルは作成しない - [x] [[TypeDoc]]からドキュメントをつくる - [x] [[GitHub Actions]]でアップロードする - [[📜GitHub ActionsからGitHub Pagesにデプロイしてみる]] - [x] function に export をつけるとビルド成果物が読み込めなくなる問題 - exportを外せば動く - ただ [[TypeDoc]] のドキュメントができなくなる - プラグインで回避しようとしたけどキツそう - [[TypeDoc]]に表示するならexportは必要 - [-] `export.func = func` - ダメ. [[TypeDoc]]が受け付けない... - [-] functionの前にexportをつけてしまう - ダメ. [[TypeDoc]]が受け付けない - [x] `functions.ts`に分離 - [x] 利用方法をREADMEに書く