VS Codeの拡張機能開発でReactを導入して開発体験を向上させる

## はじめに
初めまして。今年の1月からHRBrain入社した [yakiniku0220](https://twitter.com/yakiniku040220)です。

この時期はジメジメしていて雨も多いため、あまり外に出たくないものです。

実は今年の3月ごろから個人開発でVS Codeの拡張機能を開発しているのですが、開発をしている中で「Reactで書きたい」という感情が溢れ出てしまったので、実際にVS Codeの拡張機能開発でReactを導入してみました。

## [VS Codeの開発環境のSetup](#header1)
まず最初にYeomanとVS Code Extension Generatorを使ってプロジェクトのセットアップをします。
以下Installコマンドです。
```terminal
npx --package yo --package generator-code -- yo code
```
or
```terminal
npm install --global yo generator-code

yo code
```

上記コマンドを叩いた後に対話型で使用する言語や、プロダクト名、bundlerなどを選択します。
```terminal
npx --package yo --package generator-code -- yo code
Need to install the following packages:
yo@5.0.0
generator-code@1.10.1
Ok to proceed? (y) y
npm WARN deprecated npmlog@2.0.4: This package is no longer supported.
npm WARN deprecated are-we-there-yet@1.1.7: This package is no longer supported.
npm WARN deprecated gauge@1.2.7: This package is no longer supported.

     _-----_     ╭──────────────────────────╮
    |       |    │   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? sample-vscode-extension
? What's the identifier of your extension? sample-vscode-extension
? What's the description of your extension?
? Initialize a git repository? Yes
? Which bundler to use? webpack
? Which package manager to use? yarn
```

上記完了次第、VS Codeのセットアップは完了します。

## [ReactのSetup](#header2)
今回Reactを入れるきっかけになった部分です。
```js
export function activate(context: ExtensionContext) {
  …
  panel.webview.html = getWebviewContent();
  …
}

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>
　　　<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
　　</body>
　</html>`;
}
```
上記はWebview部分の公式のサンプルコードです。
基本的にVS CodeではWebviewを書く場合Template Literal形式で書く必要があります。
普段ReactやVueなどを書いている人からしたら書きづらいかと思います。

そこで開発体験を向上させるために普段使っているReactを入れてみました。

以下手順です。
### Reactをinstallする

以下公式のinstall手順です。
```terminal
yarn add react react-dom
```

上記のみだと型定義でエラーが発生してしまうので以下もinstallします。
```terminal
yarn add -D @types/react @types/react-dom @types/node
```

### index.tsxをsrc配下に作成
src配下にindex.tsxのファイルを作成します。
```js
import { createRoot } from "react-dom/client";
import { App } from "./App";

const root = createRoot(document.getElementById("app")!);
root.render(<App />);
```

### webpack.config.jsを修正
compileする際に、React側のコードもcompileする必要があるのでその設定をwebpack側に記載します。
```js
//@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"], <-- .tsxを追記
  },
  plugins: [
    new webpack.DefinePlugin({ <-- .webpack.DefinePluginを追記
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
    }),
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/, <-- .tsx?に変更
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader",
          },
        ],
      },
    ],
  },
  devtool: "nosources-source-map",
  infrastructureLogging: {
    level: "log", // enables logging required for problem matchers
  },
};

/** @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 = { <-- index.tsxをcompileするために処理を追加
  ...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];

```

### extension.tsの中身を修正
最後にextension.ts内のコードを修正します。
```js
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from "vscode";

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand("react.start", () => {
      // Create and show panel
      const panel = vscode.window.createWebviewPanel(
        "react",
        "React Sample",
        vscode.ViewColumn.One,
        {
          localResourceRoots: [
            vscode.Uri.joinPath(context.extensionUri, "dist"), <-- Webviewがローカルリソースにアクセスするために追記
          ],
          enableScripts: true, <-- Webview側のでscriptを有効化
        }
      );

      // And set its HTML content
      panel.webview.html = getWebviewContent(
        panel.webview,
        context.extensionUri
      );
    })
  );
}

// This method is called when your extension is deactivated
export function deactivate() {}

// Webview側で使用できるようにuriに変換する関数。
function getUri(
  webview: vscode.Webview,
  extensionUri: vscode.Uri,
  pathList: string[]
) {
  return webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, ...pathList));
}

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;
}

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>`;
}

```

その後 `yarn compile`をするとdist配下に`webview.js`と`webview.js.map`というファイルが生成されます。

## [動作確認](#header3)
デバックをして確認してみます。
![スクリーンショット 2024-06-12 14.12.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271450/5d84282c-18d7-6ad9-6d2d-737a0a4e8c06.png)

ちゃんとtsx側で書いたtextとgifが動いているのが確認できました。

## [まとめ](#header4)
今回VS Codeの拡張機能開発にReactを入れてみましたが、自分的には開発体験がかなり向上したと思います。
ぜひVS Codeの拡張機能を作る時は下記のリポジトリを参考に作ってもらえたら嬉しいです。
https://github.com/ebarakazuhiro/sample-vscode-extension

## [最後に](#header5)
株式会社HRBrainでは新しいメンバーを募集中です。
興味がある方は下記のリンクから宜しくお願い致します。

https://www.hrbrain.co.jp/recruit