rollupを用いて、ブックマークレットをTypeScriptで書く環境を構築する手順を示します。
ブックマークレットをTypeScriptで書きたい
私は、日々のウェブブラウジングを快適にするためのブックマークレットを自作しています。
ブックマークレットの魅力は、なんといっても1行でさくっと書けるお手軽さにあります。
しかしながら、ある程度の規模を超えたブックマークレットを素のJavaScriptで、それも1行の中に書いていくのは、なかなかつらいものです。
そこで、ブックマークレットをTypeScriptで書きたいと思いました。
ブックマークレットをTypeScriptで書くには
ブックマークレットをTypeScriptで書くには、おおむね次のようなことをおこなう必要があります。
- TypeScriptをJavaScriptにトランスパイル
- ファイルが分かれている場合は1ファイルにバンドル
- グローバル空間の汚染を避けるために、スクリプト全体をIIFEでラップ
- 複数行に渡る場合は1行に圧縮
- ファイルの先頭に
javascript:
を付与
これには様々な手法がありますが、今回はモジュールバンドラーのrollupを採用しました。
特徴
この記事で紹介する手法には、次のような特徴があります。
これらの特徴によって、快適なDX(開発者体験)がもたらされることでしょう。
ファイル分割
適切にファイルを分割することで、大規模なブックマークレットでもメンテナビリティを高く保つことができます。
また、ブックマークレット間で共通する処理を切り出して共用することもできます。
たとえば、クリップボードにコピーする処理をよく使うのであればcopy.ts
のようなファイルに関数を定義しておき、それを各所でインポートするとよいでしょう。
Jestによるテスト実行
テストを書くことで、ブックマークレットの品質を担保することができます。
もちろん、テストコードの中でDOMを扱うこともできます。
ウォッチモード
開発時に使用するウォッチモードではソースコードを保存する度にビルドが走り、ブックマークレットが更新されます。
ディレクトリ構成
この記事で紹介する手法では、次のようなディレクトリ構成になります。
.
├── .eslintrc.js
├── .gitignore
├── dist
│ └── foo.min.js
├── jest.config.ts
├── package-lock.json
├── package.json
├── rollup.config.ts
├── src
│ └── foo
│ └── main.ts
└── tsconfig.json
-
.eslintrc.js
:ESLintの設定ファイル -
.gitignore
:Gitの除外設定ファイル -
dist
:ブックマークレットが出力されるディレクトリ-
dist/**/*.min.js
:ブックマークレット
-
-
jest.config.ts
:Jestの設定ファイル -
package-lock.json
:npmのパッケージ情報のロックファイル -
package.json
:npmのパッケージ情報ファイル -
rollup.config.ts
:rollupの設定ファイル -
src
:ブックマークレットのソースコードを配置するディレクトリ-
src/**/main.ts
:ブックマークレットの本体にあたるエントリーファイル
-
-
tsconfig.json
:TypeScriptの設定ファイル
このようにファイルパスのルールを定めることで、新たにブックマークレットを追加する場合はsrc
ディレクトリ以下に任意のディレクトリを切り、最低限main.ts
の1ファイルさえつくればいい状態になります。
使用環境
記事執筆時点で使用している環境について、記載しておきます。
開発環境
ブックマークレットの開発には、次のような環境を使っています。
- OS:macOS Monterey 12.6
- JSランタイム:Node.js v16.11.1
- パッケージマネージャー:npm v8.0.0
実行するコマンドやインストールするパッケージは、使用するNode.jsのバージョンやパッケージマネージャーに応じてよしなに読み替えてください。
実行環境
ブックマークレットの実行には、次のような環境を使っています。
- OS:macOS Monterey 12.6
- ブラウザ:Google Chrome v108.0.5359.94
環境構築
ブックマークレットを開発するための環境を構築します。
パッケージをインストール
開発に必要なパッケージをひととおりインストールします。
npm i -D \
@munierujp/eslint-config-typescript \
@rollup/plugin-terser \
@rollup/plugin-typescript \
@tsconfig/node16 \
@types/glob \
@types/node@^16.11 \
eslint \
eslint-plugin-jest \
glob \
jest \
jest-environment-jsdom \
rimraf \
rollup \
rollup-plugin-bookmarklet \
ts-jest \
typescript
ただし、使用するパッケージやバージョンは状況に応じて適切なものを選んでください。
-
@tsconfig/node16
:使用するNode.jsのバージョンに対応したパッケージを使うこと1 -
@types/glob
:使用するglobのバージョンに対応したバージョンを使うこと2 -
@types/node
:使用するNode.jsのバージョンに対応したバージョンを使うこと2
package.json
のscripts
を定義
package.json
に、次のようなscriptsを定義します。
{
"scripts": {
"prebuild": "npm run clean",
"build": "rollup --config rollup.config.ts --configPlugin typescript",
"clean": "rimraf dist",
"predev": "npm run clean",
"dev": "rollup --config rollup.config.ts --configPlugin typescript --watch",
"lint": "eslint '**/*.{js,ts}'",
"test": "jest --passWithNoTests"
}
}
それぞれの役割は以下の通りです。
-
build
:ソースコードをビルドしてブックマークレットを生成 -
clean
:ビルド結果を削除 -
dev
:開発用のウォッチモードを起動 -
lint
:ESLintによる静的解析を実行 -
test
:Jestによるテストを実行
tsconfig.json
を作成
TypeScriptの設定ファイルとして、tsconfig.json
をつくります。
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"lib": [
// see https://www.npmjs.com/package/@tsconfig/node16
"es2021",
"dom"
]
}
}
@tsconfig/node16/tsconfig.json
を継承していますが、DOMを扱うためcompilerOptions.lib
はdom
を含めたものを再定義しています。
.eslintrc.js
を作成
ESLintの設定ファイルとして、.eslintrc.js
をつくります。
module.exports = {
ignorePatterns: [
'dist/**/*.js'
],
extends: [
'@munierujp/eslint-config-typescript',
'plugin:jest/recommended'
],
parserOptions: {
project: './tsconfig.json'
}
}
ignorePatterns
にdist/**/*.js
を指定することで、生成されたブックマークレットには適用されないようにしています。
jest.config.ts
を作成
Jestの設定ファイルとして、jest.config.ts
をつくります。
import type { Config } from 'jest'
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.ts'
]
}
export default config
TypeScriptを扱うためpreset
にはts-jest
を、DOMを扱うためtestEnvironment
にはjsdom
をそれぞれ指定しています。
rollup.config.ts
を作成
rollupの設定ファイルとして、rollup.config.ts
をつくります。
import terser from '@rollup/plugin-terser'
import typescript from '@rollup/plugin-typescript'
import glob from 'glob'
import type { RollupOptions } from 'rollup'
import bookmarklet from 'rollup-plugin-bookmarklet'
const entryPaths = glob.sync('src/**/main.ts')
const configs: RollupOptions[] = entryPaths.map(entryPath => ({
input: entryPath,
output: {
file: entryPath.replace(/^src\//, 'dist/').replace(/\/(.+)\/main\.ts$/, '/$1.min.js'),
format: 'iife'
},
plugins: [
typescript(),
terser(),
bookmarklet()
]
}))
export default configs
ブックマークレットのサンプル
ブックマークレットを開発するための環境が整ったので、いくつかの例を示します。
Hello World
はじめにもっともシンプルな例として、Hello World
というアラートを表示するブックマークレットをつくります。
まずはsrc
ディレクトリ以下にhello
ディレクトリを切り、その中にエントリーファイルとしてmain.ts
をつくります。
alert('Hello World')
npm run build
またはnpm run dev
でソースコードをビルドすると、dist
ディレクトリにブックマークレットが生成されます。
javascript:!function(){"use strict";alert("Hello World")}();
タイトルをコピー
次にもう少し複雑な例として、ページのタイトルをコピーするブックマークレットをつくります。
まずはsrc
ディレクトリ以下にcopy-title
ディレクトリを切り、その中にエントリーファイルとしてmain.ts
をつくります。
import { copy } from './copy'
copy(document.title).catch(error => {
throw error
})
また、他にも必要なファイルをひととおりつくります。
export const copy = async (text: string): Promise<void> => {
await navigator.clipboard.writeText(text)
}
npm run build
またはnpm run dev
でソースコードをビルドすると、dist
ディレクトリにブックマークレットが生成されます。
javascript:!function(){"use strict";(async t=>{await navigator.clipboard.writeText(t)})(document.title).catch((t=>{throw t}))}();
サンプルリポジトリ
この記事で紹介している手法は、こちらのリポジトリで実際に採用しているものです。
よければ参考にしてみてください。
次回予告
次回は、ユーザースクリプトをTypeScriptで書く方法について紹介します。