2025年1月28日火曜日

Nuxt のアプリを nexe でシングルバイナリにする

Nuxt のアプリを nexe でシングルバイナリにしたいという方が居たので、 試しにやってみた。

Nuxt アプリケーションの作成

プロジェクトの初期化

vscode ➜ .../TIL/js/nuxtjs/firststep (master) $ npx nuxi@latest init firststep
Need to install the following packages:
nuxi@3.20.0
Ok to proceed? (y) y


 Which package manager would you like to use?
npm
 Installing dependencies...
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported

> postinstall
> nuxt prepare

 Types generated in .nuxt

added 671 packages, and audited 673 packages in 1m

130 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
 Installation completed.
 Initialize git repository?
No
 Nuxt project has been created with the v3 template. Next steps:
  cd firststep
  Start development server with npm run dev
npm notice
npm notice New major version of npm available! 10.9.2 -> 11.0.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.0.0
npm notice To update run: npm install -g npm@11.0.0
npm notice

開発サーバー起動

cd firststep
npm run dev -- -o

動作確認

ブラウザで、 http://localhost:3000 へアクセスする。

ウェルカムページが見える。 OK.

Next プロジェクトのビルド

npm run build

.output/server/index.mjs にビルド結果のエントリーポイントが出力される。

シングルバイナリ化

esbuild を使ってバンドル・ミニファイ

npx esbuild .output/server/index.mjs --bundle --minify --platform=node --target=node14.17.0 --outdir=dist

Node のバージョンは、ビルド時間短縮のため、 nexe が提供している最新のプレビルドバイナリのバージョンで動くものを指定

これで、 ./dist/index.js にバンドル・ミニファイされたソースコードが出力される。

nexe を使ってシングルバイナリ化

npx nexe -i dist/index.js -o firststep --target windows-x64-14.15.3

windows-x64-14.15.3 が「nexe が提供している最新のプレビルドバイナリのバージョン」。

これで、 ./firststep.exe に実行バイナリが出力される。

動作確認

firststep.exe を Windows へ持っていき、ダブルクリック。

http://localhost:3000 へアクセス。OK.

もっと複雑なアプリケーションの場合は分からないが、 Get Started レベルのアプリならこれで OK なようだ。

最新の Node.js でビルドする

-b オプションを追加すると、指定した Node.js のソースをダウンロードし、 Node.js のビルドから開始する。

そのため、より新しい Node.js を使用したい場合には -b オプションを追加したうえで、 Node のバージョンに所望のバージョンを指定する。

npx esbuild .output/server/index.mjs --bundle --minify --platform=node --target=node22.13.1 --outdir=dist
npx nexe -i dist/index.js -o firststep -b --target windows-x64-22.13.1

Node.js のビルドが走るので、最悪数時間のビルド時間がかかる。

ただ、ビルド後の Node.js は ~/.nexe にキャッシュされるので、 2 回目以降のビルドは早くなる。

以上。

参考資料

2025年1月24日金曜日

ddt.vim を試す

Vim 上で動くターミナルに興味があるので、 denops の環境構築から試していく。

前提

今回使った devcontainer.json:
{
  "name": "ddt.vim study",
  "image": "ubuntu:24.04",
  "features": {
    "ghcr.io/devcontainers-community/features/deno:1": {}
  }
}

必要パッケージのインストール

プラグインインストール簡単化のために、 git が必要なので、インストールする。

apt update
apt install -y git

denops のインストール

Vim のパッケージ機能と git submodule を組み合わせてプラグインのインストールを行う。

なので今回は git submodulepack/denops/start/denops に配置する。

~/.vim に移動して、git リポジトリを初期化・サブモジュールの追加を行う。

cd ~
mkdir .vim
cd .vim
git init
git branch -m main
git submodule add https://github.com/vim-denops/denops.vim.git pack/denops/start/denops

ddt.vim のインストール

同様に `git submodule でインストールする。

git submodule add https://github.com/Shougo/ddt.vim.git pack/b/start/ddt.vim

ddt.vim の ui プラグインをインストール

git submodule add https://github.com/Shougo/ddt-ui-terminal.git pack/b/start/ddt-ui-terminal
git submodule add https://github.com/Shougo/ddt-ui-shell.git pack/b/start/ddt-ui-shell

設定

vimrc を作成し、プラグインの読み込みなど、最低限の設定を行う。

shougo-s-github/vim/rc/ddt.vim - Shougo/shougo-s-github を参考に、 deno の設定も追加。

~/ddt.vimrc:

packadd denops
packadd ddt.vim
packadd ddt-ui-terminal
packadd ddt-ui-shell

let g:denops#server#deno_args = [
    \   '-q',
    \   '-A',
    \ ]
let g:denops#server#deno_args += ['--unstable-ffi']

call ddt#custom#patch_global(#{
      \   uiParams: #{
      \     shell: #{
      \       nvimServer: '~/.cache/nvim/server.pipe',
      \       prompt: '=\\\>',
      \       promptPattern: '\w*=\\\> \?',
      \     },
      \     terminal: #{
      \       nvimServer: '~/.cache/nvim/server.pipe',
      \       command: ['bash'],
      \       promptPattern: has('win32') ? '\f\+>' : '\w*% \?',
      \     },
      \   },
      \ })

vimrc の作成が終わったら Vim を再起動し、以下コマンドで設定を読み込ませる。

:source ~/ddt.vimrc

ddt-ui-terminal の起動

こちらも shougo-s-github/vim/rc/ddt.vim - Shougo/shougo-s-github を参考に terminal ui を起動。

:call ddt#start(#{ name: t:->get('ddt_ui_terminal_last_name', 'terminal-' .. win_getid()), ui: 'terminal', })

Vim の :terminal が起動し、いつもと同じように操作できる。

ddt-ui-shell の起動

おなじく shougo-s-github/vim/rc/ddt.vim - Shougo/shougo-s-github を参考に shell ui を起動。

:call ddt#start(#{name: t:->get('ddt_ui_shell_last_name', 'shell-' .. win_getid()), ui: 'shell', })

空のバッファが表示されるが、一度エンターキーを押下すると、(前述の設定だと)以下のプロンプトが表示される。

/workspaces/TIL/vim/ddt
=\\\> 

ここからは癖強なので細かく説明していく。

  1. ここで、 uname -a を入力し、次の状態に持っていく
/workspaces/TIL/vim/ddt
=\\\> uname -a
  1. :call ddt#ui#do_action('executeLine') を実行する
  2. エンターキーを押下すると、以下のように uname -a の結果が表示される
/workspaces/TIL/vim/ddt
=\\\> uname -a
Linux 84223c761d6e 5.15.167.4-microsoft-standard-WSL2 #1 SMP Tue Nov 5 00:21:55 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
/workspaces/TIL/vim/ddt
=\\\>

このように、コマンドの実行すら関数を呼び出さないと行けないので、使い勝手の良いシェルにするには自分でマッピング定義をする必要がある。

ddt-ui-shell の設定

以下のようなマッピングを行うと、「プロンプトの行でエンターを押すと、記載されたコマンドの実行と結果表示」ができるようになる。

""" {{{ for ddt-ui-shell
augroup ddt-shell
    autocmd!
    autocmd FileType ddt-shell nnoremap <buffer> <CR> <Cmd>call ddt#ui#do_action('executeLine')<CR>
    autocmd FileType ddt-shell inoremap <buffer> <CR> <Cmd>call ddt#ui#do_action('executeLine')<CR>
augroup END
""" }}} for ddt-ui-shell

あとは ddt や ddt-ui-shell 等のヘルプを観ながら自分の好きなようにマッピングを追加していけばよい。

以上。

参考資料

2025年1月21日火曜日

Prisma で many to many のリレーションシップを定義する

Prisma での many to many のリレーションシップ定義・操作の勉強を行う。

前提

プロジェクトの初期化

npm init

必要パッケージのインストール

npm i dotenv
npm i -D typescript tsx @types/node prisma
  • dependencies
    • dotenv: 接続先設定を env に記述するために利用
  • devDependencies
    • typescript: TypeScript トランスパイルに利用
    • tsx: ts を実行できるコマンド。チュートリアルに入っていたから入れたが、使いどころは謎。
    • @types/node: node の型情報

DB 接続設定の作成

.env ファイルに接続先設定を記述する。

DATABASE_URL=postgres://postgres:postgres@postgres/postgres

TypeScript 設定ファイル(tsconfig.json)の作成

tsconfig.json の作成

tsc コマンドで、 tsconfig.json を作成する

npx tsc --init

tsconfig.json のパラメーター編集

以下表のとおりパラメーターを設定。

パラメーター
outDir ./dist

ここまでの動作確認

ひとまず TypeScript をコンパイル・実行できるかを確認。

mkdir src
echo 'console.log("Hello, World!");' > ./src/index.ts
tsc
node ./dist/index.js

Hello, World! が表示される … ok!

Prisma の設定

Prisma の初期化

npx prisma init --datasource-provider postgresql

prisma/schema.prisma にスキーマファイルが生成される。

NOTE: ここで、 npx prisma db pull とすると、既存 DB を基にスキーマファイルを更新してくれる

DB スキーマの作成

Prisma の DSL でスキーマを書いていく。

model Book {
  id      Int      @id @default(autoincrement())
  title   String
  authors BooksOnAuthors[]
}

model Author {
  id    Int    @id @default(autoincrement())
  name  String
  books BooksOnAuthors[]
}

model BooksOnAuthors {
  id         Int    @id @default(autoincrement())
  // 本を削除する際には、著者へのリレーションシップは消えて欲しいのでカスケード設定
  book       Book   @relation(fields: [bookId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  bookId     Int // リレーションスカラーフィールド。 上記の `@relation``fields` で指定した名前にする。
  // 著者を消そうとしたときに、その著者の本がまだある場合はエラーにして火しいのでカスケードなし
  author     Author @relation(fields: [authorId], references: [id])
  authorId Int // リレーションスカラーフィールド
}

prisma client のインストールとクライアントコード生成

この時点で、モデルに対応した API が生成されるようだ。

npx prisma generate

マイグレーションの実行

npx prisma migrate dev --name init

prisma/migrations にマイグレーション用 SQL が生成され、それが実行される。

A5:SQL-Mk2 で確認してみると、確かにテーブルが作成されている … ok!

メインファイルの作成

作成した Prisma の設定で DB にアクセスし、 CRUD するアプリケーションを作成する。

src/index.ts:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  // Author を生成
  const createdAuthor = await prisma.author.create({
    data: {
      name: 'mikoto2000',
    },
  })
  console.dir(`createdAuthor: ${JSON.stringify(createdAuthor)}`);

  // Book と、 Author へのリレーションシップを生成
  const createdBook = await prisma.book.create({
    data: {
      title: 'mikoto2000\'s history',
      authors: {
        create: [
          {
            authorId: createdAuthor.id
          }
        ]
      }
    },
  })
  console.dir(`createdBook: ${JSON.stringify(createdBook)}`);

  // 作成した Book の検索(中間テーブル・Author を JOIN して、戻り値に含める)
  const foundBook = await prisma.book.findMany({
    include: {
      authors: {
        include: {
          author: {}
        }
      }
    }
  });
  console.dir(`foundBook: ${JSON.stringify(foundBook)}`);
  console.dir(`プロパティ取得の練習: { title: ${foundBook[0].title}, Author: ${foundBook[0].authors.map((e) => e.author.name)}}`);

  // Book を削除(中間テーブルの要素と Book 自体の削除。中間テーブルはカスケード設定をしているので自動で削除される)
  const deletedBook = await prisma.book.delete({
    where: {
      id: createdBook.id
    },
  });
  console.dir(`deletedBook: ${JSON.stringify(deletedBook)}`);

  // Author を削除
  const deletedAuthor = await prisma.author.delete({
    where: {
      id: createdAuthor.id
    }
  });
  console.dir(`deletedAuthor: ${JSON.stringify(deletedAuthor)}`);
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

動作確認

ビルド

npx tsc

実行

node ➜ ~/tmp $ node ./dist/index.js 
'createdAuthor: {"id":1,"name":"mikoto2000"}'
`createdBook: {"id":1,"title":"mikoto2000's history"}`
`foundBook: [{"id":1,"title":"mikoto2000's history","authors":[{"id":1,"bookId":1,"au
thorId":1,"author":{"id":1,"name":"mikoto2000"}}]}]`
"プロパティ取得の練習: { title: mikoto2000's history, Author: mikoto2000}"
`deletedBook: {"id":1,"title":"mikoto2000's history"}`
'deletedAuthor: {"id":1,"name":"mikoto2000"}'

ok! 以上。

参考資料

2025年1月20日月曜日

Prisma に TypeScript と PostgreSQL で入門する

Drizzle との違いを理解するため Prisma の Get Started をやってみる。

前提

プロジェクトの初期化

npm init

必要パッケージのインストール

npm i dotenv
npm i -D typescript tsx @types/node prisma
  • dependencies
    • dotenv: 接続先設定を env に記述するために利用
  • devDependencies
    • typescript: TypeScript トランスパイルに利用
    • tsx: ts を実行できるコマンド。チュートリアルに入っていたから入れたが、使いどころは謎。
    • @types/node: node の型情報

DB 接続設定の作成

.env ファイルに接続先設定を記述する。

DATABASE_URL=postgres://postgres:postgres@postgres/postgres

TypeScript 設定ファイル(tsconfig.json)の作成

tsconfig.json の作成

tsc コマンドで、 tsconfig.json を作成する

npx tsc --init

tsconfig.json のパラメーター編集

以下表のとおりパラメーターを設定。

パラメーター
outDir ./dist

ここまでの動作確認

ひとまず TypeScript をコンパイル・実行できるかを確認。

mkdir src
echo 'console.log("Hello, World!");' > ./src/index.ts
tsc
node ./dist/index.js

Hello, World! が表示される … ok!

Prisma の設定

Prisma の初期化

npx prisma init --datasource-provider postgresql

prisma/schema.prisma にスキーマファイルが生成される。

NOTE: ここで、 npx prisma db pull とすると、既存 DB を基にスキーマファイルを更新してくれる

DB スキーマの作成

Prisma の DSL でスキーマを書いていく。

以下コードを見れば、どんなスキーマを作りたいのかわかるはず…

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

prisma client のインストールとクライアントコード生成

この時点で、モデルに対応した API が生成されるようだ。

npx prisma generate

マイグレーションの実行

npx prisma migrate dev --name init

prisma/migrations にマイグレーション用 SQL が生成され、それが実行される。

A5:SQL-Mk2 で確認してみると、確かにテーブルが作成されている … ok!

メインファイルの作成

作成した Prisma の設定で DB にアクセスし、 CRUD するアプリケーションを作成する。

src/index.ts:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  // User を生成
  const createdUser = await prisma.user.create({
    data: {
      name: 'mikoto2000',
      email: 'mikoto2000@gmail.com',
    },
  })
  console.dir(`createdUser: ${JSON.stringify(createdUser)}`);

  // user を検索
  const foundUser = await prisma.user.findUnique({
    where: {
      email: 'mikoto2000@gmail.com',
    }
  });
  console.dir(`foundUser: ${JSON.stringify(foundUser)}`);

  // user を更新
  if (foundUser) {
    const updatedUser = await prisma.user.update({
      where: {
        email: foundUser.email,
      },
      data: {
        name: 'mikoto2000+prisma@gmail.com',
      },
    })
    console.dir(`updatedUser: ${JSON.stringify(updatedUser)}`);
  }

  // user を再び検索
  const foundUser2 = await prisma.user.findMany({
    where: {
      name: 'mikoto2000+prisma@gmail.com',
    }
  });
  console.dir(`foundUser2: ${JSON.stringify(foundUser2)}`);

  // user を削除
  const deletedUser = await prisma.user.delete({
    where: {
      id: createdUser.id
    }
  });
  console.dir(`deletedUser: ${JSON.stringify(deletedUser)}`);
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

動作確認

ビルド

npx tsc

実行

node ➜ /workspaces $ node ./dist/index.js
'createdUser: {"id":21,"email":"mikoto2000@gmail.com","name":"mikoto2000"}'
'foundUser: {"id":21,"email":"mikoto2000@gmail.com","name":"mikoto2000"}'
'updatedUser: {"id":21,"email":"mikoto2000@gmail.com","name":"mikoto2000+prisma@gmail.com"}'
'foundUser2: [{"id":21,"email":"mikoto2000@gmail.com","name":"mikoto2000+prisma@gmail.com"}]'
'deletedUser: {"id":21,"email":"mikoto2000@gmail.com","name":"mikoto2000+prisma@gmail.com"}'

ok! 以上。

参考資料

2025年1月11日土曜日

nix-shell で開発環境の構築をしてみる

Dev container の代替になると聞き、Nix に入門してみたので、とりあえず開発環境を構築してみる。

目標

Nix で、 Node.js + PostgreSQL の開発環境を整えてみる。

前提

  • OS: Ubuntu 24.04 LST on Windows 11 Pro
  • DB: PostgreSQL 17
  • Node.js: 20.18.1

Nix のインストールと初期設定

インストール

Download | Nix & NixOS に記載のコマンドを実行する。

sh <(curl -L https://nixos.org/nix/install) --daemonexperimental-features = nix-command flakes

実験的機能の有効化

echo 'experimental-features = nix-command flakes' | sudo tee -a /etc/nix/nix.conf

開発環境定義

shell.nix を作成し、そのディレクトリで nix-shell を実行すると、 shell.nix で定義した通りの開発環境を準備してくれる。

各定義の意味はコード内のコメントを参照。

shell.nix:

let
  # 変数定義
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
  USER = (builtins.getEnv "USER");
  DEV_INFRA_DIR = (builtins.getEnv "PWD") + "/infra/postgres";
in

pkgs.mkShellNoCC {
  # インストールパッケージ指定
  packages = with pkgs; [
    vim
    nodejs
    postgresql_17
  ];

  # 環境変数指定
  DATABASE_URL = "postgres://" + USER + ":postgres@localhost/postgres";
  POSTGRES_PASSWORD = "postgres";
  POSTGRES_USER = USER;
  POSTGRES_DB = "postgres";
  POSTGRES_HOSTNAME = "localhost";
  PGDATA = DEV_INFRA_DIR + "/data";

  # 起動時に実行するコマンドを指定
  # (今回は PostgreSQL の初期化と起動)
  shellHook = ''
    mkdir -p $PG_DATA
    pg_ctl init
    pg_ctl start -l ${DEV_INFRA_DIR}/pg.log
  '';

開発

nix-shell でインストールした vim を使ってソースコードを書く

DB 接続するプログラムを載せるのが面倒なので省略。

mikoto2000/LibrarySystem-React-Router: 架空の図書貸出管理システム に前述の shell.nix を入れて実行したらちゃんとつなげられた。

終了

自動終了は実装できないので、 pg_ctl で終了してから nix-shell を抜ける。

pg_ctl stop
exit

以上。

ただ、すでに某所で devShell というものの存在をご教示いただいたので、そっちでやる方が良さそう。

参考資料

2025年1月1日水曜日

Drizzle のチュートリアルを TypeScript でやる

npm プロジェクトの初期化

npm init

指示通り設定を埋めていくと、 package.json が生成される。

必要パッケージのインストール

npm i drizzle-orm pg dotenv
npm i -D drizzle-kit tsx @types/pg typescript
  • dependencies
    • drizzle-orm: ORM 本体
    • pg: PostgreSQL へ接続するための Driver
    • dotenv: 接続先設定を env に記述するために利用
  • devDependencies
    • drizzle-kit: スキーマファイル生成や、マイグレーション等を行うのに利用
    • tsx: 謎。チュートリアルに入っていたから入れた。
    • @types/pg: pg の型情報
    • typescript: TypeScript トランスパイルに利用

DB 接続設定の作成

.env ファイルに接続先設定を記述する。

echo 'DATABASE_URL=postgres://postgres:postgres@postgres/postgres' > .env

TypeScript 設定ファイル(tsconfig.json)の作成

tsconfig.json の作成

tsc コマンドで、 tsconfig.json を作成する

npx tsc --init

tsconfig.json のパラメーター編集

以下表のとおりパラメーターを設定。

パラメーター
outDir ./dist

ここまでの動作確認

ひとまず TypeScript をコンパイル・実行できるかを確認。

mkdir src
echo 'console.log("Hello, World!");' > ./src/index.ts
tsc
node ./dist/index.js

Hello, World! が表示される … ok!

Drizzle の設定

コネクション初期化処理の作成

以下の通りコネクション初期化処理を作成。

mkdir -p ./src/db
cat << EOF > ./src/db/index.ts
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';

const db = drizzle(process.env.DATABASE_URL!);
EOF

Drizzle の設定ファイル作成

以下の通り設定ファイルを作成。

cat << EOF > ./drizzle.config.ts
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
EOF

DB スキーマの作成

TypeScript でスキーマを書いていく。

以下コードを見れば、どんなスキーマを作りたいのかわかるはず…

cat << EOF > ./src/db/schema.ts
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";

export const usersTable = pgTable("users", {
  id: integer().primaryKey().generatedAlwaysAsIdentity(),
  name: varchar({ length: 255 }).notNull(),
  age: integer().notNull(),
  email: varchar({ length: 255 }).notNull().unique(),
});
EOF

マイグレーションファイルの生成と適用

マイグレーションファイルの生成

npx drizzle-kit generate

設定ファイルの out に指定したとおり、 ./drizzle にマイグレーションファイルが出力される。

マイグレーションファイルの適用

migration サブコマンドで生成したマイグレーションファイルを DB に反映させる。

npx drizzle-kit migrate

A5:SQL-Mk2 で確認してみると、確かにテーブルが作成されている … ok!

メインファイルの作成

作成した Drizzle の設定で、DB にアクセスし、 CRD するアプリケーションを作成する。

cat <<"EOF" > ./src/index.ts
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
import { usersTable } from './db/schema';
  
const db = drizzle(process.env.DATABASE_URL!);

async function main() {
  const user: typeof usersTable.$inferInsert = {
    name: 'John',
    age: 30,
    email: 'john@example.com',
  };

  await db.insert(usersTable).values(user);
  console.log('New user created!')

  const users = await db.select().from(usersTable);
  console.log('Getting all users from the database: ', users)
  /*
  const users: {
    id: number;
    name: string;
    age: number;
    email: string;
  }[]
  */

  await db
    .update(usersTable)
    .set({
      age: 31,
    })
    .where(eq(usersTable.email, user.email));
  console.log('User info updated!')

  await db.delete(usersTable).where(eq(usersTable.email, user.email));
  console.log('User deleted!')
}

main();
EOF

動作確認

ビルド

npx tsc
node ./dist/src/index.js

実行

node ➜ /workspaces $ node ./dist/src/index.js
New user created!
Getting all users from the database:  [ { id: 1, name: 'John', age: 30, email: 'john@example.com' } ]
User info updated!
User deleted!

ok! 以上。

参考資料