HTA（HTML Application）をPreactとTypeScriptで開発する

HTAはHTML+CSS+VBScript/JScriptでGUIアプリケーションを構築するレガシースクリプト技術です。少なくとも執筆時点ではWindows 11にも標準で搭載されています。これを、いまをときめく技術で書けたらきっと愉快です。実用性はありません。

## HTAについて

https://learn.microsoft.com/en-us/previous-versions/ms536496(v=vs.85)

だいたいInternet Explorerにいろいろ生えたElectron風のものと考えてよいでしょう。基本的な中身はIE対応のWebページと同様です。

`<meta charset="utf-8">`の有無でフォントのレンダリングが変わりました。`title`は`hta:application`より前に記述しないとウィンドウタイトルに反映されません。`x-ua-compatible`によってIEのバージョンを指定できますが、`IE=10`以降では`HTA:APPLICATION`オブジェクトの機能が事実上利用できません。`IE=9`で実行できるようにトランスパイルするのがHTA開発の正道と言えます[^1]。

[^1]: `IE=9`で開いた直後に、自身を`IE=edge`に書き換えたページにリダイレクトするハックが存在します。`HTA:APPLICATION`オブジェクトのすべてのプロパティは読込専用の`string`値なので、リダイレクト時にクエリ文字列に詰め、遷移先で復元すればよいでしょう。`location.href`の値がリダイレクト先の値になるなど細かな問題は残ります。

```html:hta_allEX.hta
<HTML>
<HEAD>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=9">

  <!-- From https://learn.microsoft.com/en-us/previous-versions/ms536495(v=vs.85) -->
  <TITLE>HTA Demo</TITLE>
    <HTA:APPLICATION ID="oHTA"
     APPLICATIONNAME="myApp"
     BORDER="thin"
     BORDERSTYLE="normal"
     CAPTION="yes"
     ICON=""
     MAXIMIZEBUTTON="yes"
     MINIMIZEBUTTON="yes"
     SHOWINTASKBAR="no"
     SINGLEINSTANCE="no"
     SYSMENU="yes"
     VERSION="1.0"
     WINDOWSTATE="maximize"/>

  <SCRIPT>

  /* This function also retrieves the value of the commandLine property,
     which cannot be set as an attribute.  */
   
  window.onload = function()
  {
     sTempStr = "applicationName  = " + oHTA.applicationName + "\n" + 
                "border           = " + oHTA.border          + "\n" +
                "borderStyle      = " + oHTA.borderStyle     + "\n" + 
                "caption          = " + oHTA.caption         + "\n" +
                "commandLine      = " + oHTA.commandLine     + "\n" +
                "icon             = " + oHTA.icon            + "\n" +
                "maximizeButton   = " + oHTA.maximizeButton  + "\n" +
                "minimizeButton   = " + oHTA.minimizeButton  + "\n" + 
                "showInTaskBar    = " + oHTA.showInTaskbar   + "\n" +
                "singleInstance   = " + oHTA.singleInstance  + "\n" +  
                "sysMenu          = " + oHTA.sysMenu         + "\n" + 
                "version          = " + oHTA.version         + "\n" + 
                "windowState      = " + oHTA.windowState     + "\n" ;
          
     oPre.innerText = sTempStr;       
  }
  </SCRIPT>
</HEAD>
<BODY SCROLL="no">
  <PRE ID=oPre>  </PRE>
</BODY>
</HTML>
```

## 技術選定

今回はHTAを最新のTypeScript（TSX）で書くことを目指します。

Reactはv18でIE対応を終了しました[^2]。v17以前を使ったり、v18も何らか工夫したりしたら使えるかもしれませんが、あまり考えたくありません。PreactであればIE11対応を明言しています[^3]ので、IE9対応まで落とし込める可能性は高そうです。今回はこちらを使用します。

[^2]: https://ja.react.dev/blog/2022/03/08/react-18-upgrade-guide#dropping-support-for-internet-explorer
[^3]: https://preactjs.com/about/browser-support/

[create-preact](https://github.com/preactjs/create-preact)のようなスキャフォールディングツールもありますが、これらは当然モダンフロントエンド開発に使用されることを前提としているので、HTA対応には要修正のポイントが多くなります。手作業で依存ライブラリをインストールし、ファイルを配置するほうがマシです。

ES5までトランスパイルが必要です。`*.hta`単ファイルで実行できると取り回しがよいので、バンドル＋埋め込みまで行います。webpack＋Babelに慣れているのでこれらを使いますが、他のツールでも可能なはずです。

IE9にはfetchがありません。特別使いたいわけではありませんが、練習も兼ねて含めます。

<!-- `JSON.stringify`/`JSON.parse`の二番目以降の引数が壊れていると覚えていたけど、IE8までの話だったみたい。JScriptではJSON3が必要。 -->

## 開発

```
PS > mkdir my-hta-preact
PS > cd my-hta-preact
PS > npm init --yes
PS > npm install --save-dev `
        @babel/core `
        @babel/preset-env `
        @babel/preset-react `
        @babel/preset-typescript `
        babel-loader `
        html-inline-script-webpack-plugin `
        html-webpack-plugin `
        typescript `
        webpack `
        webpack-cli
PS > npm install --save `
        core-js `
        preact `
        regenerator-runtime `
        whatwg-fetch
PS > New-Item -Type File webpack.config.js,babel.config.js
PS > mkdir src
PS > New-Item -Type File src\app.hta,src\index.tsx
PS > echo "ie 9" > .browserslistrc
PS > npx tsc --init
```

<details>
<summary>webpack.config.js</summary>

```js
new HtmlWebpackPlugin({
    filename: "app.hta",
    template: "src/app.hta",
    inject: "body",
    scriptLoading: "blocking"
}),
```

がキモです。`defer`なんて大層なものは使えません。過去のベストプラクティスに従って`body`の最後に含めます。

```webpack.config.js
const path = require("path");
const entry = path.resolve(__dirname, "src/index.tsx");

const webpack = require("webpack");

const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');

module.exports = {
    mode: "development",
    entry: entry,
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "index.js"
    },
    target: "browserslist",
    module: {
        rules: [
            {
                test: /\.(jsx?|tsx?)$/,
                loader: "babel-loader",
            },
        ],
    },
    resolve: {
        extensions: [".js", ".jsx", ".ts", ".tsx"],

        // https://preactjs.com/guide/v10/getting-started#aliasing-in-webpack
        alias: {
            "react": "preact/compat",
            "react-dom/test-utils": "preact/test-utils",
            "react-dom": "preact/compat",
            "react/jsx-runtime": "preact/jsx-runtime"
        }
    },
    plugins: [
        new webpack.ProvidePlugin({
            fetch: ["whatwg-fetch", "fetch"],
            Headers: ["whatwg-fetch", "Headers"],
            Request: ["whatwg-fetch", "Request"],
            Response: ["whatwg-fetch", "Response"],

            "window.fetch": ["whatwg-fetch", "fetch"],
            "window.Headers": ["whatwg-fetch", "Headers"],
            "window.Request": ["whatwg-fetch", "Request"],
            "window.Response": ["whatwg-fetch", "Response"],
        }),

        new HtmlWebpackPlugin({
            filename: "app.hta",
            template: "src/app.hta",
            inject: "body",
            scriptLoading: "blocking"
        }),
        new HtmlInlineScriptPlugin({
            htmlMatchPattern: [/\.hta$/],
        }),
    ],
    devtool: false,
};
```

</details>
<details>
<summary>babel.config.js</summary>

```babel.config.js
module.exports = {
    exclude: [
        // https://github.com/zloirock/core-js/issues/912
        "./node_modules/core-js"
    ],
    presets: [
        "@babel/preset-typescript",
        [
            // https://github.com/preactjs/preact-compat/issues/541#issuecomment-750382448
            "@babel/preset-react",
            {
                runtime: "automatic",
                importSource: "preact",
            }
        ],
        [
            "@babel/preset-env",
            {
                // debug: true,
                useBuiltIns: "usage",
                corejs: 3,
            }
        ]
    ],
};
```

</details>
<details>
<summary>tsconfig.json</summary>

前後数行以外はデフォルト値です。

```jsonc:tsconfig.json
{
  "compilerOptions": {
    // https://preactjs.com/guide/v10/typescript/#typescript-configuration
    "jsx": "react-jsx",
    "jsxImportSource": "preact",

    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */

    /* Interop Constraints */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */

    /* Completeness */
    "skipLibCheck": true,                                 /* Skip type checking all .d.ts files. */

    // https://preactjs.com/guide/v10/typescript/#typescript-preactcompat-configuration
    "baseUrl": "./",
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
      "react-dom": ["./node_modules/preact/compat/"]
    }
  }
}
```

</details>
<details>
<summary>src/app.hta</summary>

`hta:application`は適宜編集してください。

```html:src/app.hta
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=9">

    <title>App</title>
    <hta:application id="oHTA" applicationname="App" version="1" />
</head>

<body>
    <div id="root"></div>
</body>

</html>
```

</details>

`new ActiveXObject("WScript.Shell")`に型をつけるには別途型定義が必要です。[DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)に含まれていたように記憶しています。

```src/index.tsx
import { render } from "preact";
import { useEffect, useState } from "preact/hooks";

const App = () => {
    const [winVer, setWinVer] = useState("");
    const [text, setText] = useState("");

    useEffect(() => {
        const process = new ActiveXObject("WScript.Shell").Exec("cmd /c ver");
        while (process.Status === 0)
            ;
        setWinVer(process.StdOut.ReadAll());

        const fetchTime = () => {
            fetch("http://worldtimeapi.org/api/timezone/Asia/Tokyo")
                .then(res => res.json())
                .then(json => setText(JSON.stringify(json)));
        };

        fetchTime();
        const id = setInterval(fetchTime, 2000);

        return () => clearInterval(id);
    }, []);

    return (
        <>
            <p>{oHTA.applicationName}</p>
            <p>{winVer}</p>
            <p>{text}</p>
        </>
    );
};

render(<App />, document.getElementById("root")!);
```

おまけで、`HTA:APPLICATION`の型定義[^4]も配置します。`hta:application`の`id`に応じて生成されるグローバル変数であり、コマンドライン引数の取得等が可能です。

```src/hta-application.d.ts
/** https://learn.microsoft.com/en-us/previous-versions/ms536495(v=vs.85) */
declare var oHTA: {
    /** https://web.archive.org/web/20120819100330/https://msdn.microsoft.com/en-us/library/ms533031(v=vs.85) */
    readonly applicationName: string;
    readonly border: "thick" | "dialog" | "none" | "thin";
    readonly borderStyle: "normal" | "complex" | "raised" | "static" | "sunken";
    readonly caption: "yes" | "no";
    readonly commandLine: string;
    readonly contextMenu: "yes" | "no";
    readonly icon: string;
    readonly innerBorder: "yes" | "no";
    readonly maximizeButton: "yes" | "no";
    readonly minimizeButton: "yes" | "no";
    readonly navigable: "yes" | "no";
    readonly scroll: "yes" | "no" | "auto";
    readonly scrollFlat: "yes" | "no";
    readonly selection: "yes" | "no";
    readonly showInTaskBar: "yes" | "no";
    readonly singleInstance: "yes" | "no";
    readonly sysMenu: "yes" | "no";
    readonly version: string;
    readonly windowState: "normal" | "minimize" | "maximize";
};
```

```
PS > npx webpack build
PS > .\dist\app.hta
```

サンプルに特に意味はないですが、以下を表示しています。

- HTAアプリケーション名を表示（`HTA:APPLICATION`オブジェクトのデモ）
- Windowsのバージョンを表示（ActiveXのデモ）
- `fetch`で2秒おきに時刻を取得してきて表示（`fetch`のデモ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550728/1827c453-cdea-4919-278b-17c1c6c178f2.png)

[^4]: https://learn.microsoft.com/en-us/previous-versions/ms536495(v=vs.85)

## TODO

デバッグが辛そう。[jeremyben/HTAConsole](https://github.com/jeremyben/HTAConsole)で下部にコンソールを出せる。

CSSを何も調べていないけど、JavaScriptほど柔軟にIE対応はできなさそう。普通に[runjuu/html-inline-css-webpack-plugin](https://github.com/runjuu/html-inline-css-webpack-plugin)で埋め込めそうではある。

BigIntについて、[GoogleChromeLabs/jsbi](https://github.com/GoogleChromeLabs/jsbi)を使えば用は足せそう。BabelプラグインによってJSBIからBigIntに変換することを想定されていますが、逆をやろうとしているプラグインもあるにはありそう[^5]。

WebAssemblyのpolyfillである[evanw/polywasm](html-inline-script-webpack-plugin)が楽しそう。

[^5]: [Yaffle/babel-plugin-transform-bigint](https://github.com/Yaffle/babel-plugin-transform-bigint)、[DYSDF/babel-plugin-transform-bigint-to-jsbi](https://github.com/DYSDF/babel-plugin-transform-bigint-to-jsbi)