PyScriptでESMモジュールを使う

## 概要

PyScriptからJavaScriptのESMモジュールをインポートするのは難しくはないですが、当たり前とはいえJavaScriptとは全然違うのでメモ

## 謎モジュールを作る

テストするにあたって今使いたいモジュールが特に思いつかなかったので、こんな感じのどうでもいいモジュールを自作してみた。

```testmod.js
export { test2 } from './testmod2.js';

export function test() {
    alert('test');
}

export default () => {
    alert('test default');
}
```

```testmod2.js
export function test2() {
    alert('test2');
}
```

## PyScriptのconfigについて

PyScriptでは外部PythonファイルやJavaScriptモジュールなどの様々な設定をJSONやTOMLフォーマットで記述できます。

設定を記述したJSONやTOMLファイルをscript要素のconfig属性に指定するのが一般的ですが、JSONやTOMLをpy-config(MicroPythonの場合はmpy-config)要素内やscriptタグのconfig要素に直接記述することもできるようです。

詳しくは[公式ドキュメント](https://docs.pyscript.net/2025.3.1/user-guide/configuration/)を見てください。


## 静的インポート

JavaScriptではコードのトップレベルに`import`を書く形のインポートをPyScriptで実現するには、configでインポートするJavaScriptファイルとモジュール名を設定すると、`pyscript.js_modules`モジュールに登録されているのでconfigで設定したモジュール名を`import`する形になります。

これも詳しくは[公式ドキュメント](https://docs.pyscript.net/2025.3.1/user-guide/configuration/#javascript-modules)を見てください。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://pyscript.net/releases/2025.3.1/core.css" />
    <script type="module" src="https://pyscript.net/releases/2025.3.1/core.js"></script>
    <mpy-config>
        {
            "js_modules": {
                "main": {
                    "./testmod.js": "testmod"
                }
            }
        }
    </mpy-config>
    <title>Document</title>
</head>
<body>
    <button id="btn">default export</button>
    <button id="btn2">named export</button>
    <button id="btn3">external module export</button>
    <script type="mpy">
import js
from pyscript.js_modules import testmod

btn = js.document.getElementById('btn')
btn.addEventListener('click', testmod.default);

btn2 = js.document.getElementById('btn2')
btn2.addEventListener('click', testmod.test);

btn3 = js.document.getElementById('btn3')
btn3.addEventListener('click', testmod.test2);
    </script>
</body>
</html>
```

## 動的インポート

JavaScriptでいうところのdynamic importというかimport呼び出し(関数ではないらしい)のようにスクリプト実行中に動的にインポートすることも可能です。

PyScriptでは、`pyscript`モジュールの`js_import`関数で動的インポートします。

多分バグだと思いますが、2025年5月現在のバージョンでは相対パスを渡すとPyScript本体のURLになってしまうようなので、JavaScriptの`location.href`の最後の`/`以降を削除したURLを文字列結合してモジュールのURLを生成しました。

`js_import`は常にタプルを返すので、戻り値の変数の後ろにカンマをつけるか、戻り値の変数の[0]を参照することでモジュール本体を取得できます。

こちらも詳しくは[公式ドキュメント](https://docs.pyscript.net/2025.3.1/api/#pyscriptjs_import)を見てください。


```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://pyscript.net/releases/2025.3.1/core.css" />
    <script type="module" src="https://pyscript.net/releases/2025.3.1/core.js"></script>
    <title>Document</title>
</head>
<body>
    <button id="btn">default export</button>
    <button id="btn2">named export</button>
    <button id="btn3">external module export</button>
    <script type="mpy">
import js
from pyscript import js_import, display

base = js.location.href.rsplit('/', 1)[0]

display(js.location.href)
display(base)

testmod, = await js_import(base + '/testmod.js')

btn = js.document.getElementById('btn')
btn.addEventListener('click', testmod.default)

btn2 = js.document.getElementById('btn2')
btn2.addEventListener('click', testmod.test)

btn3 = js.document.getElementById('btn3')
btn3.addEventListener('click', testmod.test2)
    </script>
</body>
</html>
```