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で書く方法について紹介します。