Neutralino.js + Vite + Solid でデスクトップアプリを始めてみた

## 環境

Windows 11, Node v18 でやりました

```
> node --version
v18.20.3
> npm --version
10.7.0
```

## Neutralino をインストール

```
npm install -g @neutralinojs/neu
```

`neu` というコマンドがインストールされます。

## テンプレートからプロジェクトを作成

```
PS D:\mitayuki67> neu create neut-example3 --template neutralinojs/neutralinojs-zero
neu: INFO Downloading neutralinojs/neutralinojs-zero template to neut-example3 directory...
neu: INFO Extracting template zip file...
neu: INFO Downloading Neutralinojs binaries..
neu: INFO Found the latest release tag v5.1.0 for neutralinojs...
neu: INFO Extracting binaries.zip file...
neu: INFO Finalizing and cleaning temp. files.
neu: INFO Downloading the Neutralinojs client..
neu: INFO Found the latest release tag v5.1.0 for neutralino.js...
neu: INFO Downloading the Neutralinojs types..
neu: INFO Finalizing and cleaning temp. files...
-------
neu: INFO Enter 'cd neut-example3 && neu run' to run your application.
  _   _            _             _ _             _
 | \ | | ___ _   _| |_ _ __ __ _| (_)_ __   ___ (_)___
 |  \| |/ _ \ | | | __| '__/ _` | | | '_ \ / _ \| / __|
 | |\  |  __/ |_| | |_| | | (_| | | | | | | (_) | \__ \
 |_| \_|\___|\__,_|\__|_|  \__,_|_|_|_| |_|\___// |___/
                                              |__/
```

## Vite + solid プロジェクトを作成

`www` は不要なので削除し、`solid-src` ディレクトリを作ります。

```
PS D:\mitayuki67> cd .\neut-example3\
PS D:\mitayuki67\neut-example3> rm -r www
PS D:\mitayuki67\neut-example3> pnpm create vite@latest solid-src --template solid
.../18fad834751-5f84                     |   +1 +
.../18fad834751-5f84                     | Progress: resolved 1, reused 0, downloaded 1, added 1, done

Scaffolding project in D:\mitayuki67\neut-example3\solid-src...

Done. Now run:

  cd solid-src
  pnpm install
  pnpm run dev

PS D:\mitayuki67\neut-example3> cd .\solid-src\
PS D:\mitayuki67\neut-example3\solid-src> pnpm install
Packages: +82
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 120, reused 3, downloaded 79, added 82, done
node_modules/.pnpm/esbuild@0.20.2/node_modules/esbuild: Running postinstall script, done in 575ms

dependencies:
+ solid-js 1.8.17

devDependencies:
+ vite 5.2.11
+ vite-plugin-solid 2.10.2

Done in 7.1s

PS D:\mitayuki67\neut-example3> cd .\solid-src\
PS D:\mitayuki67\neut-example3\solid-src> pnpm build

> solid-src@0.0.0 build D:\mitayuki67\neut-example3\solid-src
> vite build

vite v5.2.11 building for production...
✓ 10 modules transformed.
dist/index.html                  0.46 kB │ gzip: 0.30 kB
dist/assets/index-D61y85IL.css   1.21 kB │ gzip: 0.62 kB
dist/assets/index-CJZJvNTb.js   10.60 kB │ gzip: 4.26 kB
✓ built in 492ms
PS D:\mitayuki67\neut-example3\solid-src> cd ..
```

## `neutralino.config.json` を編集

1. `documentRoot`, `cli/resourcesPath` を `/solid-src/dist/` に変える
2. `cli/clientLibrary` を削除する
3. `modes/window` を調整する（任意）

```
diff --git a/neutralino.config.json b/neutralino.config.json
index 7325a21..6b5d786 100644
--- a/neutralino.config.json
+++ b/neutralino.config.json
@@ -2,7 +2,7 @@
   "applicationId": "js.neutralino.zero",
   "version": "1.0.0",
   "defaultMode": "window",
-  "documentRoot": "/www/",
+  "documentRoot": "/solid-src/dist/",
   "url": "/",
   "enableServer": true,
   "enableNativeAPI": true,
@@ -13,17 +13,15 @@
     "window": {
       "title": "neut-example3",
       "width": 800,
-      "height": 500,
+      "height": 600,
       "minWidth": 400,
-      "minHeight": 200,
-      "icon": "/www/icon.png"
+      "minHeight": 200
     }
   },
   "cli": {
     "binaryName": "neut-example3",
-    "resourcesPath": "/www/",
+    "resourcesPath": "/solid-src/dist/",
     "extensionsPath": "/extensions/",
-    "clientLibrary": "/www/neutralino.js",
     "binaryVersion": "5.1.0",
     "clientVersion": "5.1.0"
   }
```

## 実行

```
PS D:\mitayuki67\neut-example3> neu run
neu: INFO Starting process: neutralino-win_x64.exe  --load-dir-res --path=. --export-auth-info --neu-dev-extension --neu-dev-auto-reload
neu: INFO neu CLI connected with the application.
```

実行できました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1188794/9ab81a8d-e516-9532-43a0-4600c92d2d17.png)

## ネイティブAPIをインストール

```
solid-src> pnpm add @neutralinojs/lib
Packages: +1
+
Progress: resolved 121, reused 82, downloaded 1, added 1, done

dependencies:
+ @neutralinojs/lib 5.1.0

Done in 3.4s
```

`index.html` で `__neutralino_globals.js` をインポートするように追加します。

```index.html
    <title>Vite + Solid</title>
    <script src="__neutralino_globals.js"></script>
  </head>
```

`filesytem.readDirectory` を追加してみます。

```neutralino.config.json
  "nativeAllowList": [
    "app.*",
    "filesystem.readDirectory"
  ],
...
  "cli": {
    "binaryName": "neut-example3",
    "resourcesPath": "/solid-src/dist/",
    "extensionsPath": "/extensions/",
    "frontendLibrary": {
      "patchFile": "/solid-src/index.html",
      "devUrl": "http://localhost:5173",
      "projectPath": "/solid-src/",
      "initCommand": "pnpm install",
      "devCommand": "pnpm dev",
      "buildCommand": "pnpm run build"
    },
    "binaryVersion": "5.1.0",
    "clientVersion": "5.1.0"
  }
```

`frontendLibrary/patchFile` とは、デバッグ実行のためにソースコード中の `__neutralino_globals.js` を `http://localhost:ポート番号/__neutralino_globals.js` に置換する設定です。
これが置換されたままだとビルドしても実行できないので、`neu build` する前に戻す必要があります。

```index.jsx
/* @refresh reload */
import { render } from 'solid-js/web'

import './index.css'
import App from './App'

import { init } from "@neutralinojs/lib"

const root = document.getElementById('root')

render(() => <App />, root)

if (window.NL_PORT == undefined) {
    window.location.reload()
}
init()
```

前述した `__neutralino_global.js` で環境変数の `NL_PORT` などを設定しているのですが、デバッグ実行だと `patchfile` が書き換わる前に実行されてしまうせいか、サーバーかクライアントのどちらかをリロードしないと実行できません。そこで未定義なら `reload` しています。しかし、これはデバッグ実行にしか影響がないので、本番コードには含めないほうが良いかもしれません。

```App.jsx
import { createEffect, createSignal } from 'solid-js'
import solidLogo from './assets/solid.svg'
import viteLogo from '/vite.svg'
import './App.css'

import { filesystem } from "@neutralinojs/lib"

function App() {
  const [count, setCount] = createSignal(0)
  const [data, setData] = createSignal("")

  createEffect(() => {
    filesystem.readDirectory('./').then((data) => {
      setData(data.map((each)=>each.entry).join('|'))
    }).catch((err) => {
      console.log(err)
    })
  })

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} class="logo" alt="Vite logo" />
        </a>
        <a href="https://solidjs.com" target="_blank">
          <img src={solidLogo} class="logo solid" alt="Solid logo" />
        </a>
      </div>
      <h1>Vite + Solid</h1>
      <div class="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count()}
        </button>
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
      <p class="read-the-docs">
        Click on the Vite and Solid logos to learn more
      </p>
      <p>
        data: {data()}
      </p>
    </>
  )
}

export default App
```

`filesystem` から適当にカレントディレクトリのファイル一覧を表示させてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1188794/ac15f8d3-f101-8434-c28d-8d3a38a827b6.png)