## 経緯
[[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に書く