VSCodeでTypeScriptのHTML埋め込み開発

# 概要

HTMLへの<scrpt>タグへのTypeScript埋め込みは公式ではサポートされていないのでVSCodeのHTMLモードで埋め込んでもTypeScript部分でエラーが出ます。
当たり前ですが、TypeScriptモードにするとHTML部分でエラーがでます。
HTMLモードではTypeScript部分が、TypeScriptモードではHTML部分でエラーが出ないようにしてみた。

# 方針

- そのままブラウザで実行してもとりあえず動く(余計な表示はしない)ように
- VSCodeのHTMLモード・TypeScriptモードでそれぞれエラーが出ないように
- ~~PHP~~EJSで実行すると別フォルダにちゃんとしたHTMLファイルを生成できるように

# TypeScript埋め込み

TypeScriptコードの実行にはJavaScriptへのトランスパイルが必要です、
一応Webブラウザで実行するライブラリはあるのですが、TypeScript対応のライブラリは調べた限り相当古いのしかなかったので、
今回はちゃんとメンテナンスされてそうなBabel Standaloneを使うことにしました。
TypeScriptではなくBabelなんですが、TypeScript構文をサポートするプリセットがあるみたいです。

https://babeljs.io/docs/en/babel-standalone

ただし、そのままでは.tsファイルしかTypeScriptコードとして認識しない設定になっているようなので、事前にすべての拡張子で適用する設定をする必要があるようです。

```html
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>
<script type="text/javascript">
    Babel.registerPreset('ts-plus', {
        presets: [
            [Babel.availablePresets['typescript'], { 'allExtensions': true, 'loose': true }]
        ]
    });
</script>
<script type="text/babel" data-presets="ts-plus">
    //typescriptコード
</script>
```

# 結局TypeScriptのままじゃデバッグできない

しかし、直接TypeScriptのコードがHTMLに埋め込めたとしても結局そのままではデベロッパーツールでデバッグができないので、デベロッパーツールを使うためにブラウザ上でトランスパイルする必要があります。
しかもトランスパイルしたコードの入った`<script>`を動的に追加するだけではダメで、最後に「//#sourceURL='ファイル名'」という特殊コメントを埋め込んでデベロッパーツールのファイル一覧に表示させる必要があるようです。(Chrome、Edgeで確認)
`<script type="text/babel">`を使うのはやめてidを付けた`<textarea>`にコードを入れることにしてそこからトランスパイルする動作にしてみました。

```html
<textarea id="__typescriptcode" style="display:none;">
    //TypeScriptコード
</textarea>
<script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@latest/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>
<script>
    //ブラウザでトランスパイルする(そうしないとDevToolでデバッグ出来ない)
    //Babel Standaloneの設定(TypeScriptプリセット使用)
    Babel.registerPreset('ts-plus', {
        presets: [
            [Babel.availablePresets['typescript'], { 'allExtensions': true, 'loose': true }]
        ]
    });
    //JavaScriptにトランスパイル
    let __transcode = Babel.transform(document.getElementById("__typescriptcode").textContent, { presets: ["ts-plus"] }).code;
    //トランスパイルしたコードを動作させるためのScriptタグを設定
    var __scriptelem = document.createElement("script");
    __scriptelem.type = "text/javascript";
    //最後に「//#sourceURL='ファイル名'」というコメントを追加すると動的に追加したJavaScriptコードもデバッグの対象にできる。
    __scriptelem.textContent = __transcode + "\n" + '/' + '/' + '# sourceURL=' + location.pathname + '.js';
    //scriptタグを追加してトランスパイルしたJavaScriptを動作させる
    document.body.appendChild(__scriptelem);
</script>
```



# ~~HTMLモードの不具合？~~

~~HTMLモードでは`<script type="text/babel">`内にコードを埋め込むと**JavaScriptコード**としてパースしようとする仕様があります。
もちろんそこにTypeScriptを入れるのでエラーになります。
だったらBabelサポートしてくれればいいのに・・・
これは設定から「html validate scripts」で検索して「HTML > Validate : Scripts」のチェックを外すしかないようです・・・~~

結局トランスパイルしてJavaScriptコードを動的に追加する必要があり`<script>`ではなく`<textarea>`に切り替えたのでそもそもこんな配慮は不要になりました…

# TypeScriptモードでエラーを出さないよう悪あがき

なぜだかよくわかりませんが、VSCodeでは~~any型を設定した型パラメータ~~`<any>`の後に数字を付けるとnumber型プリミティブのany型へのキャストという有効な構文としてエラーを出しません。これをブロックコメント開始を挟んで`</any>`の閉じタグを入れれば、
TypeScriptから見れば`<any>0;`というany型にキャストした数字の0という有効なTypeScript構文の後にコメント開始、HTMLから見れば`<any>0/*;</any>`という謎のタグに囲まれた謎の文字列という形になってどちらもエラーにはなりません。
CSSでanyタグを非表示にすればVSCodeでエラーが出ず、ブラウザに何も表示せずにTypeScriptコメントを始めることができます。
コメント開始さえできればコメント終了はそれほどでもないので何とかTypeScriptでエラーになるHTML部分をコメントで囲むことが出来ました。

# ~~PHP~~EJSで最後にはちゃんとしたTypeScript埋め込みHTMLを吐き出せるように

TypeScriptの文法上やむを得ずとりあえず動くとはいえ、凄まじいHTML文法違反の数々なのでせめて最終的にはちゃんとしたHTMLを吐き出せるように~~PHP~~EJSで`?compile=html`等をつけて実行すれば別フォルダに同名のちゃんとしたHTMLやTypeScriptコードを吐き出すコードを埋め込みました。
`<!-- start -->`と`<!-- end -->`の間のHTMLやtypescriptだけ取り出す感じで作ってみた。

# 完成

これで、同じファイルをHTMLモードとTypeScriptモードでエラーを出さずにそれぞれコーディングすることができ、
そのままブラウザで実行してデバッグしながら最終的にちゃんとしたHTMLを出力してリリースするという工程を1ファイルで行うことができるようになりました。

# インストールしたパッケージ

Babelは結構前にパッケージ名を変えたらしく、babel-coreと@babel/coreはバージョンが違います(後者の方が新しい)
古いサイトをみてパッケージを間違えると面倒なので気を付けよう(1敗

```json
{
  "dependencies": {
    "@babel/core": "^7.18.10",
    "@babel/plugin-transform-typescript": "^7.18.12",
    "@babel/preset-env": "^7.18.10",
    "@babel/preset-typescript": "^7.18.6",
    "body-parser": "^1.20.0",
    "ejs": "^3.1.8",
    "express": "^4.18.1"
  }
}
```

# コード

```html
<any>0;/*</any>
<style>
    any {
        display: none;
    }
</style>
<!--
<%
//プリプロセッサ
let ejsdata = {};





%>
<% let echostr = ''; %>
<% if (false) { %>
-->
<!-- start -->
<!DOCTYPE html>
<html lang="ja">

<head>
    <!-- HTMLヘッダ -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
    </style>
    <title></title>
</head>

<body>
    <!-- HTMLボディ -->






    <!-- HTMLここまで -->
    <!-- end -->
    <textarea id="__typescriptcode" style="display:none;">
    /**///<!-- cstart -->

//TypeScriptコード






//TypeScriptコードここまで
//<!-- end -->//</textarea>
    <any>0;/*</any>
    <!-- fstart -->
    <script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@latest/dist/polyfill.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>
    <script>
        //クライアントトランスパイラ
        //ブラウザでトランスパイルする(そうしないとDevToolでデバッグ出来ない)
        //Babel Standaloneの設定(TypeScriptプリセット使用)
        Babel.registerPreset('ts-plus', {
            presets: [
                [Babel.availablePresets['typescript'], { 'allExtensions': true, 'loose': true }]
            ]
        });
        //JavaScriptにトランスパイル
        let __transcode = Babel.transform(document.getElementById("__typescriptcode").textContent, { presets: ["ts-plus"] }).code;
        //トランスパイルしたコードを動作させるためのScriptタグを設定
        var __scriptelem = document.createElement("script");
        __scriptelem.type = "text/javascript";
        //最後に「//#sourceURL='ファイル名'」というコメントを追加すると動的に追加したJavaScriptコードもデバッグの対象にできる。
        __scriptelem.textContent = __transcode + "\n" + '/' + '/' + '# sourceURL=' + location.pathname + '.js';
        //scriptタグを追加してトランスパイルしたJavaScriptを動作させる
        document.body.appendChild(__scriptelem);
    </script>
    <!-- end -->
    <!-- start -->
</body>

</html>
<!-- end -->
<!--
<%
} else {
    //サーバーサイドトランスパイラ
    let directory = 'compile';       //出力ディレクトリ
    let compile = false;             //ファイルに出力
    let translate = false;           //JavaScriptにトランスパイル
    let insertscripttag = false;     //別のJavaScriptを指すscriptタグを出力
    let suffix = '';                 //出力ファイルの拡張子
    let afpattern = [];              //削除パターン
    let usecode = false;             //TypeScript(JavaScript)コードを出力
    let usehtml = false;             //HTMLを出力
    let usefootscript = false;       //ブラウザでトランスパイルするコードを出力
    let scripttag = ['', ''];        //コード部分に適用するタグの設定

    if (request.query.compile === 'html') {
        //TypeScriptコード(Babel Standaloneトランスパイル用コードも)含むHTMLファイルを出力
        compile = true;
        usecode = true;
        usehtml = true;
        usefootscript = true;
        scripttag = ['<textarea id="__typescriptcode" style="display:none;">', '</textarea>'];
        afpattern = [/\/\*\*\//g];
        suffix = '.html';
    } else if (request.query.compile === 'htmljs') {
        //トランスパイルしたJavaScriptコードを含むHTMLファイルを出力
        compile = true;
        usecode = true;
        usehtml = true;
        translate = true;
        scripttag = ['<script type="text/javascript">', '</script>'];
        afpattern = [/\/\*\*\//g];
        suffix = '.html';
    } else if (request.query.compile === 'htmljstag') {
        //<script src="ファイル名.js">を含むHTMLファイルを出力(JavaScriptコードは別に要出力)
        compile = true;
        usehtml = true;
        insertscripttag = true;
        suffix = '.html';
    } else if (request.query.compile === 'htmlonly') {
        //HTML部分のみをファイルに出力
        compile = true;
        usehtml = true;
        suffix = '.html';
    } else if (request.query.compile === 'typescript') {
        //TypeScriptコード部分のみファイルに出力
        compile = true;
        usecode = true;
        afpattern = [/\/\*\*\//g];
        suffix = '.ts';
    } else if (request.query.compile === 'javascript') {
        //トランスパイルしたJavaScriptコードをファイルに出力
        compile = true;
        translate = true;
        usecode = true;
        afpattern = [/\/\*\*\//g];
        suffix = '.js';
    } else {
        //EJSコードを削除したHTMLをブラウザに表示
        usecode = true;
        usehtml = true;
        usefootscript = true;
        scripttag = ['<textarea id="__typescriptcode" style="display:none;">', '</textarea>'];
    }

    //自分自身のファイルを読み込み
    let source = fs.readFileSync(__FILE__, 'utf-8').toString();

    //HTMLコメントの前の//を除去
    source = source.replaceAll(/\/\/([<]!--)/g, '$1');

    //特殊コメントをEJSタグに置換
    source = source.replaceAll(/\/\*%/g, '<' + '%');
    source = source.replaceAll(/%\*\/([a-zA-Z0-9_]*)/g, '%' + '>');
    source = source.replaceAll(/[<]!--%/g, '<' + '%');
    source = source.replaceAll(/%--[>]/g, '%' + '>');

    let htmlcode = source;
    let output = '';

    if (usehtml) {
        //HTML相当のコードを抜き出す
        if (usecode || insertscripttag) {
            //あとでTypeScriptやJavaScriptコードを挿入できるようにマーク文字列を埋め込み
            htmlcode = htmlcode.replace(/[<]!-- cstart --[>]/, '<' + '!-- start --' + '>[*script*]<' + '!-- end --' + '>');
            htmlcode = htmlcode.replace(/[<]!-- fstart --[>]/, '<' + '!-- start --' + '>[*fscript*]<' + '!-- end --' + '>');
        }

        //HTMLコメントの前の//を除去
        htmlcode = htmlcode.replaceAll(/\/\/([<]!--)/g, '$1');

        //HTMLコードを抜き出す
        let matches = htmlcode.matchAll(/[<]!-- start --[>](.*?)[<]!-- end --[>]/sg);

        for (const value of matches) {
            output += value[1];
        }
    }

    if (insertscripttag) {
        //マークを付けた所にscriptタグを埋め込む
        output = output.replace(/\[\*(script.*?\/script)\*\]/, '<$1>');
        output = output.replace(/\[\*fscript\*\]/, '');
    } else if (usecode) {
        let tscode = '';

        //TypeScriptコードを抜き出す
        let matches = source.match(/[<]!-- cstart --[>](.*?)[<]!-- end --[>]/s);
        tscode = matches[1];

        //事前に設定した余分なコードの除去
        for (const pattern of afpattern) {
            tscode = tscode.replaceAll(pattern, '');
        }

        //必要ならTypeScriptからJavaScriptにトランスパイル
        if (translate) {
            tscode = babel.transform(tscode, { 'targets': 'defaults', 'plugins': [['@babel/plugin-transform-typescript', { 'allExtensions': true, 'loose': true }]], presets: ['@babel/env'] }).code
        }

        if (usehtml) {
            //TypeScriptコードまたはJavaScriptコードをHTMLに埋め込む
            if (!insertscripttag) {
                output = output.replace(/\[\*script\*\]/, scripttag[0] + "\n" + tscode + "\n" + scripttag[1]);
            }
            if (usefootscript) {
                //ブラウザでトランスパイルするコードを埋め込む
                let footer;
                let matches = source.match(/[<]!-- fstart --[>](.*?)[<]!-- end --[>]/s);
                footer = matches[1];
                output = output.replace(/\[\*fscript\*\]/, footer);
            } else {
                //コードを出力しないのでマークだけ除去
                output = output.replace(/\[\*fscript\*\]/, '');
            }
        } else {
            //HTMLは出力しないのでそのままTypeScriptまたはJavaScriptファイルを出力
            output = tscode;
        }
    }

    //前後の余分な空白を除去
    output = output.trim();

    //EJSコードの変換
    output = (ejs.compile(output, { client: true }))(ejsdata);

    if (compile) {
        //ファイルに出力
        //自身のファイル情報を取得
        let path_parts = path.parse(__FILE__);

        if (insertscripttag) {
            //別のJavaScriptファイルを参照するscriptタグを出力
            output = output.replace(/\[\*script\*\]/, '<script src="./' + path_parts.name + '.js"></script>');
        }

        //出力用ディレクトリが存在しないときは作成する
        if (!fs.existsSync(path_parts.dir + '/' + directory + '/')) {
            fs.mkdir(path_parts.dir + '/' + directory + '/');
        }

        //設定した出力先ディレクトリに同名のファイルを書き込み
        fs.writeFileSync(path_parts.dir + '/' + directory + '/' + path_parts.name + suffix, output, 'utf-8');

        //書き込んだファイルのパスと書き込んだ旨のメッセージを出力
        echostr = '-' + '-' + '>' + path_parts.dir + '/' + directory + '/' + path_parts.name + suffix + '　にファイルを作成しました。' + '<' + '!' + '-' + '-';
    } else {
        //そのままブラウザに出力
        echostr = '-' + '-' + '>' + output + '<' + '!' + '-' + '-';
    }
}
%>
<%- echostr %>
-->
<!--*/ //-->
```

# 結論：TypeScriptをHTMLに埋め込むのはやめましょう

TypeScriptを別ファイルにして埋め込まないならnodeもいらないし、上記のような苦労をする必要がありません。
(ただしローカルでは外部ファイルをロードできないのでwebサーバー上で実行する必要があります)

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@latest/dist/polyfill.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>
    <script type="text/babel" data-presets="typescript" src="./test.ts"></script>
    <title>TypeScript Test</title>
</head>
<body>
</body>
</html>
```