ここは読み飛ばしてもOKです
どうも限界派遣SESの自称社内ニートです。
ある程度暇になると、普段自分が本当に仕事をしているのか?と不安になるため社内ニートな気がしてきています。
ひとりアドカレ7日目は前から作りたかったVSCodeの拡張機能を作ることにしました。
VSCodeの拡張機能を作る方法
今回は以下の記事を参考にしています。
ざっくりテンプレートの作成
上記の方法でYeoman
と VS Code Extension Generator
のパッケージを追加してyo code
コマンドを実行して対話形式でテンプレートを作っていきます。
$ npm install -g yo generator-code
$ yo code
設定に関しては以下の2点のみは固定で、あとは任意の設定をしてください。(typescriptじゃなくても良いけどjavascriptにする理由も見つからないので。)
What type of extension do you want to create?
に対してNew Extension (TypeScript)
を指定。
Which bundler to use?
に対してwebpack
を指定。
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? my_extension
? What's the identifier of your extension? my-extension
? What's the description of your extension?
? Initialize a git repository? Yes
? Which bundler to use? webpack
? Which package manager to use? npm
これで一度デバッグ起動するとHello World
コマンドが追加されていることがわかります。
とりあえずWebViewを表示する
Web画面を表示するだけであれば、WebViewを利用することでHTMLのデータを表示することが出来ます。
テンプレートで生成されたextension.ts
を以下のように書き換える事でWebViewを表示できます。
しかし、標準的な手法ではHTMLは文字列で扱わなくてはならず、色々と扱いにくいです。
import * as vscode from 'vscode';
import * as childProcess from 'child_process';
export function activate(context: vscode.ExtensionContext) {
// Destroyコマンドを登録
const disposable = vscode.commands.registerCommand('my-extension.destroy', () => {
// WebViewを開く
const panel = vscode.window.createWebviewPanel(
'destroy',
'Destroy',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
// WebViewに表示するHTMLを生成
panel.webview.html = getWebviewContent();
// WebViewからの呼び出すコマンドを定義
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'destroy':
// consoleのコマンドを実行する
const command = "echo 'バルス'";
// コマンドを実行
childProcess.exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
if (stdout) {
console.log(`stdout: ${stdout}`);
return;
}
});
return;
}
},
undefined,
context.subscriptions
);
});
context.subscriptions.push(disposable);
}
// WebViewに表示するHTMLを生成(これが文字列なのでちょっとメンドイ)
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<h1>Destroy</h1>
<!-- このボタンからコマンドを呼び出す。 -->
<button onclick="destroy()">Destroy</button>
<script>
const vscode = acquireVsCodeApi();
function destroy() {
// PostMessageで対象のコマンドを指定して呼び出しを行う
vscode.postMessage({
command: 'destroy'
});
}
</script>
</body>
</html>`;
}
export function deactivate() { }
コマンド名を変更しているのでpackage.json
のcommands
の部分を変更しておきましょう。
"contributes": {
"commands": [
{
- "command": "my-extension.helloWorld",
- "title": "Hello World"
+ "command": "my-extension.destroy",
+ "title": "Destroy"
}
]
}
Destroy
コマンドを実行した際以下のように新しいタブが追加され、Destroyボタンが押せるようになっています。
これでsudo rm -rf --no-preserve-root /
1を実行してあげればdestroy完了です。
上記のようにWebViewを実装する場合、HTMLは文字列として扱わなければならず、本当に扱いにくいです。
CSSファイルもこの文字列の中に実装しなくてはならず実用的とはいえません。
そのため、ReactとTailwindcssを扱えるようにしていきましょう。
ReactとTailwindcssを使えるようにする
Reactを使えるようにする方法は以下を参考にしています。
まずは必要なパッケージをインストールしていきます。
npm install react react-dom tailwindcss postcss style-loader postcss-loader css-loader autoprefixer
PostCSSの設定
普段Next.jsなどでTailwindcssを設定する際はnpx create-next-app@latest
コマンドを実行することで、自動的にインストールされるため気にしたことがなかったですが、そもそもTailwindcssはPostCSSのプラグインだそうです。
postcss.config.js
を作成して以下を記述してください。
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Tailwindcssの設定
Tailwindcssを初期化していきましょう。
npx tailwindcss init
コマンドを実行するとtailwind.config.js
ファイルが生成されるため、以下のように書き換えます。
purge
を指定することで未使用のCSSを削除し軽量化をしてくれるそうです。
/** @type {import('tailwindcss').Config} */
module.exports = {
// Tailwind CSSを適用するファイルのパスを指定
purge: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'media',
content: [],
theme: {
extend: {},
},
plugins: [],
};
そしてsrc
配下にindex.css
を作成し以下を記述してください。
@tailwind base;
@tailwind components;
@tailwind utilities;
警告が出ていますが、VSCodeの拡張機能Tailwind CSS IntelliSense
をインストールすると警告が抑制できます。
Reactの設定
tsconfig.js
にreact
を利用する設定を追加します。
{
"compilerOptions": {
+ "jsx": "react",
"module": "Node16",
"target": "ES2022",
"lib": [
"ES2022",
+ "DOM"
],
"sourceMap": true,
"rootDir": "src",
"strict": true
}
}
次にsrc
配下にindex.tsx
を作成します。これはWebViewのエントリーポイントとしての役割を持ちます。
import { createRoot } from "react-dom/client";
import { App } from "./App";
import React from "react";
import "./index.css"; // tailwindcssの設定がかかれたCSSファイルを読み込み
const root = createRoot(document.getElementById("app")!);
root.render(<App />); // あとでApp.tsxを作成して呼び出します。
webpackの設定
webpack.config.js
の設定を以下のように変更します。
以下ではReactとTailwindcssを利用する設定を追記しています。
//@ts-check
'use strict';
const path = require('path');
const webpack = require('webpack');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const baseConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js', '.tsx']
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
],
};
/** @type WebpackConfig */
const extensionConfig = {
...baseConfig,
entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, "dist"),
filename: "extension.js",
libraryTarget: "commonjs2",
},
externals: {
vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
};
/** @type WebpackConfig */
const webviewConfig = {
...baseConfig,
target: ["web", "es2020"],
entry: "./src/index.tsx",
experiments: { outputModule: true },
output: {
path: path.resolve(__dirname, "dist"),
filename: "webview.js",
libraryTarget: "module",
chunkFormat: "module",
},
};
module.exports = [extensionConfig, webviewConfig];
WebViewのHTMLに当たるtsxファイルの作成
先ほど、文字列でHTMLを記述していたものをtsx
形式のファイルとして記述していきます。
今回はsrc
配下にApp.tsx
として作成します。
この時、Tailwindcss
のクラスが利用できるようになっています。
import React from 'react';
const vscode = (window as any).acquireVsCodeApi(); // vscodeのAPI
export function App() {
const handleDestroy = () => {
// ボタンをクリックした際にpostMessageで実際の処理を呼び出します。
vscode.postMessage({
command: 'destroy'
});
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Destroy</h1>
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={handleDestroy}
>
Destroy
</button>
</div>
);
};
拡張機能のコマンドの修正
最後にextension.ts
を修正すれば完了です。
参考にさせて頂いた記事の中でvscode.Uri.joinPath
が使われているところがありますが、これは仕様変更によりpath.join
を使うようになっているそうなので注意してください。
import * as vscode from 'vscode';
import * as childProcess from 'child_process';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
// Destroyコマンドを登録
const disposable = vscode.commands.registerCommand('my-extension.destroy', () => {
// WebViewを開く
const panel = vscode.window.createWebviewPanel(
'destroy',
'Destroy',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
// WebViewに表示するHTMLを生成
panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);
// WebViewからのメッセージを受信
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'destroy':
// consoleのコマンドを実行する
const command = "echo 'バルス'";
// コマンドを実行
childProcess.exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
if (stdout) {
console.log(`stdout: ${stdout}`);
return;
}
});
return;
}
},
undefined,
context.subscriptions
);
});
context.subscriptions.push(disposable);
}
// Webview側で使用できるようにuriに変換する関数
function getUri(
webview: vscode.Webview,
extensionUri: vscode.Uri,
pathList: string[]
) {
// vscode.Uri.joinPath は、利用できなくなったため、path.join を使用
return webview.asWebviewUri(vscode.Uri.file(path.join(extensionUri.fsPath, ...pathList)));
}
// Nonceを生成する関数
function getNonce() {
let text = "";
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
// WebViewに表示するHTMLを生成
function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) {
const webviewUri = getUri(webview, extensionUri, ["dist", "webview.js"]);
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<div id="app"></div>
<script type="module" nonce="${nonce}" src="${webviewUri}"></script>
</body>
</html>`;
}
// This method is called when your extension is deactivated
export function deactivate() { }
コンパイルと実行
上記の作業が全て完了すると、以下のようなディレクトリ構成になります。
実際に開発する場合はディレクトリを分割しておいたほうがキレイだと思います。
.
├── package.json
├── package-lock.json
├── postcss.config.js
├── src
│ ├── App.tsx
│ ├── extension.ts
│ ├── index.css
│ ├── index.tsx
│ └── test
│ └── extension.test.ts
├── tailwind.config.js
├── tsconfig.json
└── webpack.config.js
npm run compile
を実行してデバッグ実行すると、以下のように先ほどのApp.tsx
で記述したボタンが出てくるのがわかると思います。Tailwindcssのスタイルも適用されていますね。
本当はwatch
イベントにて自動でコンパイルされてホットリロードできるはずなのですが、筆者の環境だとうまく動いていなかったみたいなので何か設定が足りないのかもしれません。(Macで試したときはホットリロードきいていた気がします。)
気が向いたら、検証して追記したいと思います。
ちなみに今回のコマンドでは実害の無い破滅の呪文を唱えています。
まとめ
今回はReact
を利用した実装でしたが、Vue
を使った開発なども行えるようなので気が向いたら記事にしようかなーと思います。
自分が慣れているフレームワークを使って開発ができるのは嬉しいですよね。私は文字列でHTMLを書かなくてはならない状態には多分耐えられないので真っ先に探しました。
今回の記事で作成したリポジトリは以下で公開しています。
作成する際は参考にしてください。
次回は本題、とあるツールのGUI版を作って行く予定です。
それでは。
-
破滅の呪文。本番環境では絶対に実行しないでね。 ↩