プログラミング/svelte/svelte5手順覚書 の履歴(No.21)

更新


プログラミング/svelte

概要

svelte5 の流儀は svelte4 とずいぶん違うようなので、

https://svelte-5-preview.vercel.app/docs/introduction

を見ながら進めつつ、あとで使えるよういろいろな手順をメモしておく。

開発環境の整備

nvm のアップデート

git でインストールしてある場合には

LANG:console
$ cd ~/.nvm
$ git fetch
$ git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags
 ...
 refs/tags/v0.40.0 Tue Jul 30 12:50:18 2024 -0700
$ git checkout v0.40.0

みたいにする。

node

最新版を入れるなら、

LANG:console
$ nvm install --lts
Now using node v20.16.0 (npm v10.8.1)

git alias

~/.gitconfig

[alias]
	ss = status
	br = branch
	co = checkout
	com = commit
	comm = commit -m
	log1 = log -1
	log2 = log -2
	log3 = log -3
	logo = log --oneline --pretty=format:'%C(Yellow)%h%Creset %ad %C(Green)%s%Creset' --date=short
	logn = log --name-status --oneline --pretty=format:'%C(Yellow)%h%Creset %ad %C(Green)%s%Creset' --date=short
       tags = for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags

peco

ソフトウェア/peco で紹介したもの。

インストールは https://github.com/peco/peco/releases から peco_windows_amd64.zip を落としてきて、中の peco.exe を ~/bin に置くだけ。

~/.bash_profile に以下を追加して

export PATH=$PATH:~/bin

ログインしなおして、

LANG:console
$ ls | peco

でファイル一覧からの選択画面が表示されたら成功。

~/.gitconfig

[alias]
  # SYNTAX: git d
  #
  # 履歴一覧を表示して選択した履歴との diff を取る
  # 初期状態のまま Enter を押せば単に git diff と打ったのと同様に HEAD と working copy との間の差分をとる
  # 履歴を1つ選んで Enter を押せば、その履歴と working copy との差分を取る
  # Ctrl+space で2つ選べば、その間の差分を取る
  #
  d = !git diff --minimal --ignore-all-space `git log --oneline --branches | peco | ruby -e 'STDIN.readlines.reverse.each{|s| puts s[0..6]}'`

  # SYNTAX: git addq
  #
  # git add するファイルを peco で選択する
  addq= "!if [ \"`git status --porcelain | grep -E '^.[^ ]'`\" != \"\" ]; then git add `git status --porcelain | grep -E '^.[^ ]' | peco | sed -e 's/.* //'`; fi"

  # SYNTAX: git rmq
  #
  # git rmするファイルを peco で選択する
  rmq= "!if [ \"`git status --porcelain | grep -E '^.[^ ]'`\" != \"\" ]; then git rm `git status --porcelain | grep -E '^.[^ ]' | peco | sed -e 's/.* //'`; fi"

  # SYNTAX: git resetq
  #
  # git reset するファイルを peco で選択する
  resetq = "!if [ \"`git status --porcelain | grep -E '^[^\\? ]'`\" != \"\" ]; then git reset `git status --porcelain | grep -E '^[^\\? ]' | peco | sed -e 's/.* //'`; fi"

VSCode

  • ターミナルから code <file名> とするとエディタ上にファイルを開ける

プロジェクトの作成

LANG: console
$ pnpm create svelte@latest <projectname>
$ cd <projectname>
$ pnpm install && git init && git add -A && git commit -m "Initial commit"
$ pnpm test
$ pnpm dev

git add -A と git add . の違いは、git add -Aはレポジトリ内のどこで実行してもレポジトリ全体を処理するが、git add .はカレントディレクトリ以下のみを処理する、とのこと。 ← https://note.nkmk.me/git-add-u-a-period/

コンポーネントのアップデート

LANG: console
$ pnpm update && \
  git add -A && git commit -m "pnpm update"

prettier

.prettierrc

  {
-   "useTabs": true,
+   "useTabs": false,
    "singleQuote": true,
-   "trailingComma": "none",
+   "trailingComma": "es5",
+   "quoteProps": "consistent",
    "printWidth": 100,
    "plugins": ["prettier-plugin-svelte"],
    "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
  }
LANG: console
$ pnpm format && \
  git add -A && \
  git commit -m "prettier を通した"

prettier の自動実行

LANG: console
$ cat > .git/hooks/pre-commit
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0

# Prettify all selected files
echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write

# Add back the modified/prettified files to staging
echo "$FILES" | xargs git add

exit 0
^D
$ chmod u+x .git/hooks/pre-commit

pnpm

  • run を付ける必要がない
  • npx の代わりに pnpm dlx とする

テスト環境

LANG: console
$ pnpm test:integration     # playwright による結合テスト
$ pnpm test:unit            # vitest でユニットテストを行い、自動実行用に待機する
$ pnpm test                 # 上の2つを続けて行い、vitest で待機する
$ pnpm playwright test --ui # playwright の gui を立ち上げる

[vitest]

LANG: console
$ cat vite.config.ts 
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
        plugins: [sveltekit()],
        test: {
                include: ['src/**/*.{test,spec}.{js,ts}']
        }
});

となっているので、src/ の下のすべての .test.ts および .spec.ts がテストの対象になる。

[playwrightインストール]

LANG: console
$ pnpm dlx playwright install
$ echo /test-results >> .gitignore
$ cat playwright.config.ts 
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
        webServer: {
                command: 'pnpm build && pnpm preview', 
                port: 4173
        },
        testDir: 'tests',
        testMatch: /(.+\.)?(test|spec)\.[jt]s/
};

tests/ フォルダの *test.ts のようなファイルがテスト対象になる(test の前に . はなくてもいい)。

pnpm dev していない状況では、ファイルを更新した場合にはテスト前に pnpm build し直さなければならないっぽいのでその点には注意が必要だ。

tsx

tsx を使うと Type Script で書かれたユーティリティスクリプトをトランススクリプトせずコマンドラインから直接実行することができる。

LANG: console
$ pnpm i -D tsx

とすると入れられて、プロジェクトの tsconfig.json を食わせて実行すれば大抵のものはそのまま動くみたい。

LANG: consle
$ ./node_modules/.bin/tsx --tsconfig tsconfig.json path/to/script.ts

プロジェクト

css

DaisyUI を入れる

https://daisyui.com/

LANG: console
$ pnpm dlx svelte-add@latest tailwindcss
◇  Do you want to use typography plugin?
│  Yes
$ pnpm add -D daisyui@latest

https://daisyui.com/docs/layout-and-typography/#-1 を参考に、

tailwind.config.ts

LANG: ts
+ import TailwindcssTypography from '@tailwindcss/typography';
+ import DaisyUI from 'daisyui';
   
  export default {
     //...
-   plugins: [],
+   plugins: [
+     TailwindcssTypography,
+     DaisyUI,
+   ],
+ 
+   daisyui: {
+     themes: ['light']
+   },
  } as Config;

データベース prisma

インストール

LANG: console
$ pnpm add -D prisma && pnpm add @prisma/client
$ pnpm dlx prisma init --datasource-provider sqlite && cat .env
$ echo "/prisma/*.db" >> .gitignore         # db がコミットされないようにする
$ echo "/prisma/*.db-journal" >> .gitignore
$ git add . && git commit -m "Prisma をインストールした"

サーバーで使うためのコードを追加

src/lib/server/db.ts

import { PrismaClient } from '@prisma/client';

export const db = new PrismaClient();

Prisma チートシート:

prisma/schema.prisma を編集してマイグレートする

$ pnpm prisma migrate dev --name "name for migration"

ユーザー認証 Lucia

を見ながら

LANG: console
$ pnpm add lucia @lucia-auth/adapter-prisma

app.d.ts

LANG: ts
declare global {
  namespace App {
    ...
    // Locals を使うことで +page.server.ts 内で load から action へデータを渡せる
    interface Locals {
      user: import('lucia').User | null;
      session: import('lucia').Session | null;
    }
    interface PageData {
      user: import('lucia').User | null;
      session: import('lucia').Session | null;

src/lib/server/lucia.ts

LANG: ts
import { Lucia } from "lucia";
import { dev } from "$app/environment";
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
import { db } from "$lib/server/db";
import type { RequestEvent } from '@sveltejs/kit';

const adapter = new PrismaAdapter(db.session, db.user);

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    attributes: {
      // set to `true` when using HTTPS
      secure: !dev
    }
  }
});

// ログインしてるなら cookie から session, user を返す
// src/hooks.server.ts で呼び出される
export async function findUserAndSession(event: RequestEvent) {
  const sessionId = event.cookies.get(lucia.sessionCookieName);
  if (!sessionId) {
    return { session: null, user: null };
  }
  const { session, user } = await lucia.validateSession(sessionId);
  if (session && session.fresh) {
    const sessionCookie = lucia.createSessionCookie(session.id);
    // sveltekit types deviates from the de-facto standard
    // you can use 'as any' too
    event.cookies.set(sessionCookie.name, sessionCookie.value, {
      path: ".",
      ...sessionCookie.attributes
    });
  }
  if (!session) {
    const sessionCookie = lucia.createBlankSessionCookie();
    event.cookies.set(sessionCookie.name, sessionCookie.value, {
      path: ".",
      ...sessionCookie.attributes
    });
  }
  return { session, user };
}

declare module "lucia" {
  interface Register {
    Lucia: typeof lucia;
    DatabaseUserAttributes: DatabaseUserAttributes;
  }
}

interface DatabaseUserAttributes {
  name: string,
  email: string,
}

フォームのバリデーション sveltekit-superforms

LANG: console
$ pnpm add -D sveltekit-superforms zod

使い方はこの辺り :

LANG: ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

export const load = (async (event) => {
  const form = await superValidate(zod(schema));
 
  return { form };
}) satisfies PageServerLoad;

export const actions = {
  default: async (event) => {
    // フォームデータのバリデーション
    const form = await superValidate(event, zod(schema));

src/lib/server/index.ts

// superforms の form コントロールにエラーメッセージを追加する
// @example ```addErrorsToSuperValidated(form, 'email', 'そのアドレスは使えません');```
export function addErrorsToSuperValidated<
  KEYS extends string,
  FORM extends {
    valid: boolean;
    errors: { [key in KEYS]?: string[] };
  },
>(form: FORM, item: KEYS, message: string) {
  form.errors[item] = [...(form.errors[item] || []), message];
  form.valid = false;
}

フラッシュメッセージ sveltekit-flash-message

https://github.com/ciscoheat/sveltekit-flash-message

LANG: console
$ pnpm add -D sveltekit-flash-message

app.d.ts

LANG: ts
declare global {
  namespace App {
    ...
    interface PageData {
      ...
      // https://github.com/ciscoheat/sveltekit-flash-message
      flash?: { type: 'success' | 'error'; message: string };

src\routes\+layout.server.ts

LANG: ts
import { loadFlash } from 'sveltekit-flash-message/server';

export const load = loadFlash(async (event) => {
  // その他の処理
 
  return { someData: 'data'  } // $page.data に受け渡すデータ
});

src\routes\+layout.svelte

LANG: ts
<script lang="ts">
  import { getFlash } from 'sveltekit-flash-message';
  import { page } from '$app/stores';

  const flash = getFlash(page);
  const { children } = $props();
</script>

{#if $flash}
  {@const bg = $flash.type == 'success' ? '#3D9970' : '#FF4136'}
  <div style:background-color={bg} class="flash">{$flash.message}</div>
{/if}

{@render children()}

もう少し凝りたければ、

flash-toast.svelte

LANG: html
<script lang="ts">
  /**
   * TODO: 複数のメッセージを同時に表示できるようにしたい
   */
  import { getFlash } from 'sveltekit-flash-message';
  import { page } from '$app/stores';

  let flashMessage = $state('' as string);
  let flashType = $state('success' as 'success' | 'error');
  let show = $state(false as boolean);

  // メッセージがセットされれば表示する
  const flash = getFlash(page);

  // メッセージがあれば初回表示時に表示する
  let firstTime = true;
  $effect(() => {
    if (firstTime) {
      firstTime = false;
      flash.subscribe(($flash) => {
        if ($flash) {
          let icon = $flash.type == 'success' ? '&#9989;' : '&#10060;';
          flashMessage = icon + ' ' + $flash.message;
          flashType = $flash.type;
          show = true;

          flash.set(undefined);
        }
      });
    }
  });

  function onanimationend() {
    show = false;
  }
</script>

<div class="flash-toast {flashType}" class:is-show={show} {onanimationend}>
  <!-- メッセージ部分 -->
  <div>{@html flashMessage}</div>
  <!-- 吹き出し部分 -->
  <div></div>
</div>

<!-- svelte-ignore css_unused_selector -->
<style type="postcss">
  .flash-toast {
    &.success > div {
      --color-back: #9de8af;
    }

    &.error > div {
      --color-back: #f1aeb5;
    }

    /* https://codepen.io/kandai/pen/qBEbgQv を参考にした */
    box-sizing: border-box;
    position: fixed;
    top: 10px;
    right: 0px;
    /* right: -220px; */
    transform: translateX(calc(100% + 10px));
    display: flex;
    align-items: start;

    /* メッセージ部分 */
    & > div:first-child {
      max-width: 25vw;
      color: #000;
      padding: 4px;
      border: 8px solid;
      border-radius: 8px 0px 8px 8px;
      background-color: var(--color-back);
      border-color: var(--color-back);
    }
    /* 吹き出しの三角形 */
    & > div:nth-child(2) {
      border: solid transparent;
      content: '';
      height: 0;
      width: 0;
      pointer-events: none;
      position: relative;
      border-top-width: 0px;
      border-bottom-width: 10px;
      border-left-width: 10px;
      border-right-width: 10px;
      border-radius: 0;
      border-left-color: var(--color-back);
    }

    &.is-show {
      animation: anime 4s ease 1 normal;
    }
  }

  @keyframes anime {
    /* 全部で 4s */
    12.5% {
      transform: translateX(0);
    } /* 0.5s で飛び出す */
    87.5% {
      transform: translateX(0);
    } /* 0.5s で引っ込む */
  }
</style>

のようなのを作っておいて、

src\routes\+layout.svelte

LANG: ts
<script lang="ts">
  import '../../app.css';
  import FlashToast from './flash-toast.svelte';

  const { children } = $props();
</script>

<FlashToast />
<div class="container">
  {@render children()}
</div>

のようにするとか。

メール送信 nodemailer

https://nodemailer.com/

LANG: console
$ pnpm add nodemailer && pnpm add -D @types/nodemailer

src/lib/server/transporter.ts

LANG:ts
import { createTransport, type SentMessageInfo, type SendMailOptions } from 'nodemailer';
import { env } from '$env/dynamic/private';
import * as fs from 'fs';

export const transporter = createTransport({
  host: 'localhost',
  port: 25,
});

// 開発環境やテスト環境なら実際にはメールを送らずに
// コンソールへ表示 & test-results/mail-sent.txt へ保存
if (env.DATABASE_URL.match(/\b(dev|test)\.db$/)) {
  transporter.sendMail = async (mailOptions: SendMailOptions) => {
    console.log(mailOptions);
    if (!fs.existsSync('test-results')) {
      fs.mkdirSync('test-results', { mode: 0o755 });
    }
    fs.writeFileSync('test-results/mail-sent.txt', JSON.stringify(mailOptions));
    return null as SentMessageInfo;
  };
}

node サーバーで動かす場合のデプロイ方法

https://kit.svelte.jp/docs/adapter-node https://zenn.dev/rabee/articles/sveltekit-nodejs-setup

pnpm build 時に使われるアダプターを adapter-node にすればよい。

まず

LANG:console
$ pnpm i -D @sveltejs/adapter-node@next

として adapter-node インストールしてから svelte.config.js の指定を書き換える。

svelte.config.js

LANG:ts
// import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-node';

そして、

LANG:console
$ pnpm build

これで build/ に実行に必要なファイルができる。

dev セクションに指定したパッケージは production 環境では必要ないので、

LANG:console
$ pnpm install --frozen-lockfile --prod

のように --prod を付けて pnpm install することで dev セクションのパッケージを削除しておく。

サーバーの起動は、https://kit.svelte.jp/docs/adapter-node にある環境変数を設定してから node build/index.js すればよい。

LANG:console
$ PORT=4174 node build

こうすれば 4174 ポートでサーバーが立ち上がる。

node サーバーのデーモン化

https://qiita.com/poruruba/items/10df0d94e9127797498f

pm2 を使うといいらしい。 (古い記事では forever が紹介されたりもしているのだけれどこのところメンテナンスされていないので)

LANG:console
$ pnpm install pm2

の後に

LANG:console
$ pnpm pm2 start build --name <appname>

みたいにすると起動できる。

ただこれだと環境変数を指定できないのかもしれなくて、以降も間違いなく起動できるようにするには pm2.config.cjs を作るとよい。

pm2.config.cjs

LANG:js
module.exports = {
  apps : [
    {
      name: "appname_port3001",
      script: "./build/index.js",
      cwd : '/path/to/the/app',
      env: {
        "HOST": "127.0.0.1",
        "PORT": 3001,
        "NODE_ENV": "production"
      }
    }
  ]
}
LANG:console
$ pnpm pm2 start pm2.config.cjs

として起動する。

es6 形式でない .js ファイルは .cjs としておかないと、

LANG:console
[PM2][ERROR] File pm2.config.js malformated
Error [ERR_REQUIRE_ESM]: require() of ES Module pm2.config.js not supported.
pm2.config.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
Instead either rename pm2.config.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /home/takeuchi/remese-check/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).

と言われてしまうので注意。

動作中のサーバーの一覧を確認するには

LANG:console
$ pnpm pm2 ls

0 番目のアプリを止めるには

LANG:console
$ pnpm pm2 delete 0

0 番目のアプリの状態を表示するには

LANG:console
$pnpm pm2 show 0

ログを表示するには

LANG:console
$ pnpm pm2 logs   # すべてのプロセスのログ
$ pnpm pm2 logs 0 # 0 番目のアプリのログ

pm2 自体を止めるには

LANG:console
$ pnpm pm2 kill

システム再起動時に自動実行

システムの再起動時に pm2 を自動実行するには、

LANG:console
$ crontab -e

で crontab 設定を開いて

@reboot /path/to/the/app/node_modules/.bin/pm2 start /path/to/the/app/pm2.config.cjs

を追加すればいい・・・のかな???

開いてるポートを確認

LANG: console
$ sudo lsof -i -P +c15 | grep LISTEN

のようにする。

sudo を付けないと自分が起動したプロセスについてしか調べられないみたい。

メモ

svelte 情報源

アクセスログを残す

https://www.reddit.com/r/sveltejs/comments/xtbkpb/how_are_you_logging_http_requests_in_sveltekit/

を元に少し色付けして、

src/hooks.server.ts

LANG:ts
import { sequence } from "@sveltejs/kit/hooks";
import type { Handle } from "@sveltejs/kit";

const logger: Handle = async ({ event, resolve }) => {
    const requestStartTime = Date.now();
    const response = await resolve(event);

    const time = new Date(requestStartTime).toISOString();
    let message = '';
    if(response.status < 300) {
      message += `\x1b[94m`; // 青
    } else
    if(response.status < 400) {
      message += `\x1b[93m`; // 黄色
    } else
    if(response.status < 500) {
      message += `\x1b[95m`; // オレンジ
    } else {
      message += `\x1b[91m`; // 赤
    }
    message += new Date(requestStartTime).toISOString() + `\x1b[49m `;
    message += `\x1b[30;106m${event.request.method}\x1b[49m `; // 反転
    message += `\x1b[97;49m${event.url.pathname+event.url.search}\x1b[49m`; // 白

    console.log(
	    message,
	    `(${Date.now() - requestStartTime}ms)`,
	    response.status
    );
    return response;
};

export const handle: Handle = sequence(logger);

開発時のログの取り方

LANG: console
$ pnpm dev

とする代わりに

LANG: console
$ pnpm dev | tee -a dev.log

のように起動すればログを dev.log に保存できる。

あるいは、既存のログに追加するなら

LANG: console
$ pnpm dev | tee dev.log

とする。

事前に、

LANG: console
$ echo /dev.log >> .gitignore

もしておくとよい。

ただ、これをすると画面上でカラー表示がなくなってしまうのが玉に瑕?

https://blog.ayakumo.net/entry/2022/09/10/192732

LANG: console
$ sudo apt install expect

しておいて、

LANG: console
$ unbuffer pnpm dev 2>&1 | tee dev.log

とすると、カラーのまま表示&保存可能。

カラーコードの残ったログは、VSCode の ANSI Colors プラグインでプレビューすればカラーのまま参照できる。

$effect/$effect.pre の実行順

こちらでテストした結果

   "$effect.pre of Outer Component"
   "$effect.pre of Inner Component"
   "$effect of Inner Component"
   "$effect of Outer Component"

の順に呼ばれることが分かった。

javascript でディープコピー

https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript

structuredClone(value)

とするだけでいいらしい。

サーバー側とクライアント側とで Rune は共有されない

サーバー側で設定した $state 変数をクライアント側で読み出せるわけではないため、

SSR をうまく効かせるためにはサーバー側でもクライアント側でも同じ $state が設定されるようにしないといけない。

  • .server.ts で PageData を設定
  • .svelte で data を参照して $state 変数を設定

としてあれば問題ないはず。

zod のスキーマからデータの型を得る

LANG: ts
type Data = typeof schema._type;

$state 変数をモジュールから export する

$state は .svelte あるいは .svelte.ts/.svelte.js の中でしか使えないので、 $state 変数を export するモジュールの拡張子は .svelte.ts とする。

$state 変数は単体で export できないため、class フィールドとして export するか、 あるいは $state 変数自体ではなくその getter や setter, modifier を export する。

test.svelte.ts

LANG: ts
// The `$state` rune is only available inside `.svelte` and `.svelte.js/ts` files at Module.rune_outside_svelte 

// $state(...)` can only be used as a variable declaration initializer or a class field

// Cannot export state from a module if it is reassigned. Either export a function 
// returning the state value or only mutate the state value's properties

// 書き換え可能な $state 変数を直接 export することはできないのでゲッターやセッターを export する
let counter = $state(0);
export const s = { 
  get count() { return counter; },
  increment() { counter += 1 },
};

// クラスフィールドが $state 変数なら直接 export 可能
class T {
  count = $state(0);
  increment() { this.count += 1 };
}
export const t = new T();

// $state 変数自体を const としてそのフィールドを書き換えるのは OK
export const u = $state({ count: 0 });

本当に黒魔法的に感じるけれど、なぜかこれで s.count や t.count、u.count への参照がリアクティブになる。

LANG: html
<script lang="ts">
  import { s, t, u } from './test.svelte';
</script>

{s.count} <br/> <!-- ← ここがリアクティブに変化する -->
<br/>
<button onclick={()=>s.increment()}>Increment</button><br/>
<br/>

{t.count} <br/> <!-- ← ここがリアクティブに変化する -->
<br/>
<button onclick={()=>t.increment()}>Increment</button><br/> 
<button onclick={()=>t.count -= 1}>Decrement</button><br/> <!-- 直接変更も可能 -->
<br/>

{u.count} <br/> <!-- ← ここがリアクティブに変化する -->
<br/>
<button onclick={()=>u.increment()}>Increment</button><br/> 
<button onclick={()=>u.count -= 1}>Decrement</button><br/> <!-- 直接変更も可能 -->
<br/>

まじかすげー。

store が必要なくなるというのも頷ける??

$state な配列の要素読み出しもリアクティブ

LANG:ts
  class TObj {
    array = $state([0]);
    get arrayTop() { return this.array[0] } // リアクティブになる
  }
  let obj = new TObj();

のようにしてあると obj.arrayTop へのアクセスは正しくリアクティブになるため $derived は必要ない。

postcss-color-mod-function を入れた

https://github.com/csstools/postcss-color-mod-function

css に

    background-color: color-mod(#f0ad4e lightness(90%));

みたいに書けるようになる。

$ pnpm i postcss-color-mod-function -D

はいいとして postcss.config.js を

LANG:ts
export default {
  plugins: {
    autoprefixer: {},
  },
};

から

LANG:ts
import postcssColorModFunction from 'postcss-color-mod-function';
import autoprefixer from 'autoprefixer';

export default {
  plugins: [autoprefixer(), postcssColorModFunction()],
};

のように変更しなければならなかったのだけれどそういうものなのかしら???

困ったとき

binding_property_non_reactive

ブラウザのコンソールにこのエラーが出る。

[svelte] binding_property_non_reactive
`bind:this={components[0]}` (.svelte-kit/generated/root.svelte:44:42) is binding to a non-reactive property

bind:this にバインドする変数を $state にしてみても解決しない

https://github.com/sveltejs/svelte/issues/12514

これだと思うのだけれど、待っていれば直るということなのかな?

Tagged Template 形式の関数呼び出しがリアクティブにならないバグがある

$state 変数により出力の変化する func(s: string) を func(str) として呼び出すならこの呼び出しはリアクティブになるのだけれど、現状では func`some string` の形で呼び出すとりアクティビティが得られない。

これはバグなので将来的には解消されるはず:
https://github.com/sveltejs/svelte/issues/12687

→ 解消した

style タグの直後で unknown word エラー

間違って // で始まる一行コメントを書いているとこのエラーが出るのだけれど、 一行コメントのある行じゃなくて style タグの直後にエラーが表示されるので 理由が分からず途方に暮れる。

Playwright による integration test で Error: Process from config.webServer was not able to start. Exit code: 1

TEST RESULT ウィンドウの結果が、

LANG:console
Running global setup if any…
Error: Process from config.webServer was not able to start. Exit code: 1

となってエラーの詳細が表示されない。

大抵は pnpm build で失敗しているということなので Terminal から

LANG:console
$ pnpm build

してエラーメッセージを確認する。

satisfies PageServerLoad がエラーになる

page.server.ts 内の記述が間違っていないのにここがエラーになるのとすれば、 その上の階層の layout.server.ts において必要な PageData が正しく受け渡せていない可能性があるのでチェックする。

https://github.com/nextauthjs/next-auth/issues/7543

effect_update_depth_exceeded

リアクションのループが生じているということらしい。

$effect/$effect.pre は参照した rune 変数の変更のたびに呼び出されるので、 「$effect 内で参照している rune 変数を同じ $effect 内で書き換える」を行ってしまうと無限ループになる。

$effect/$effect.pre は複数書いてよく、それぞれの $effect/$effect.pre は依存する rune 変数を別々に管理することに注意しつつ、

さらなるリアクティブ更新を生みたくない部分を

LANG:ts
 import { untrack } from 'svelte';

で括って隠してやれば避けられる。

$effect(()=>{
  depending1;  // untrack の外で読んだいずれかの $state 変数が変更されれば
  depending2;  // この $effect が呼び出される

  untrack(()=>{

    // この中で参照する変数が変更されてもこの $effect は呼び出されない

  });
});

typescript で addEventListener

普通に (e: Event)=>{ } 形式の関数を関数を渡すとエラーになる?

https://qiita.com/tobita0000/items/7341e11305eb25726dc0

LANG:ts
  a.addEventListener('click',   {handleEvent: (e: Event & { currentTarget: HTMLAnchorElement}) => {
    const anchor = e.currentTarget;
    const h = document.querySelector<HTMLHeadingElement>(anchor.href.replace(/^[^#]*/, ''));
    if (h) animateScroll(h);
  }});

svelte ファイルをテキストメールのテンプレートに使う

svelte5 の場合 svelte/server に含まれる render という関数を使うことで文字列化(hydrate)が可能。

https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes-server-api-changes

LANG: ts
import { render } from 'svelte/server';
import Template from './my-template.svelte';

const text = render(Template, {
  props: {
    prop1: value1,
    prop2: value2,
  },
}).body
  .replaceAll(/<!--[^>]*-->/g,'')
  .replaceAll(/^\n*<[^>]+>\n?|<\/[^<]+>\n*$/g, '');

dev モードでは自動生成されるコメントタグがたくさん入るため、後から replaceAll で消す。 pnpm build 後はコメントは入らないので無駄になるけどテストのためにもこのコードは必要。

my-template.svelte

LANG: html
<script lang="ts">
  let { title, body }: { title: string, body: string } = $props();
</script>

<!-- prettier-ignore -->
<pre>
* {title}
{body}
</pre>

出力結果を html ではなくプレーンテキストとして用いる場合、 テンプレートは例えば <pre> タグで全体を括って <!-- prettier-ignore --> を付けておく。

そうしないと prettier が勝手なところで改行したり、空白を入れたりしてしまう。

https://github.com/sveltejs/prettier-plugin-svelte/issues/59#issuecomment-683425785

prettier-ignore はタグ単位で効果を発揮するので、上記のように何らかのタグで括る必要がある。

邪魔なタグは後から .replaceAll(/^\n*<[^>]+>\n?|<\/[^<]+>\n*$/g, ''); で消している。

メンバー関数を変数に取り出すと this が消えてしまう

svelte じゃなく javascript の話だけれど、

LANG: ts
class TObj {
  value = 1
  get1 = () => this.value;
  get2() { return this.value }
}
const { get1, get2 } = obj;

console.log(get1()); // うまく動く
console.log(get2()); // this が undefined という TypeError

という結果になる。

ChatGPT に解説してもらった
Screenshot 2024-08-02 at 07-17-44 ChatGPT-trim.jpg

さすが ChatGPT 先生

get/set で作られたプロパティを変数に入れると混乱する

これは svelte の問題ではないのだけれど、

LANG:ts
const obj = {
  #count: 0;
  set count(c: number) { this.#count = c }
  get count() { return this.#count }
}

const { count } = obj;

このとき count 変数は get count() の結果得られたその時点での obj.#count の値を保持するに過ぎないので、obj.#count を変更してもその変化は反映されないし、count へ値を代入しても obj.#count は変化しない。

set/get の定義された属性を安全に使うには常に obj.count のように obj. をつけて呼び出さないとダメだ。

reload window で ssh の繋ぎなおし

https://stackoverflow.com/questions/60714159/is-there-a-way-to-reconnect-to-a-disconnected-vs-code-remote-ssh-connection

Ctrl+Shift+P からの reload window すると ssh remote での接続をやり直すことができる。

PC がスリープに入ったりで繋がらなくなったら試す。

VSC で ANSI Color でカラー化されたログを読む

ターミナルに表示した際にカラーになるようなログを読もうとして Esc[1mEsc[36m みたいのがたくさん出て残念な気分になるときは

https://qiita.com/YoshitakeHiroki/items/63db7b6722c031a441ed

にあるように ANSI Colors というプラグインを入れて、 Ctrl + Shift + P から ANSI Text: Open Preview を選ぶとよい。


Counter: 453 (from 2010/06/03), today: 4, yesterday: 5