プログラミング/svelte/svelte5手順覚書 の履歴(No.11)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- プログラミング/svelte/svelte5手順覚書 へ行く。
概要†
あとで使えるように、いろいろな手順を書いておく。
開発環境の整備†
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
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 がテストの対象になる。
[playwrite インストール]
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 を入れる
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 チートシート:
- https://www.prisma.io/docs/orm/prisma-client/queries/crud#example-schema
- https://qiita.com/ryskBonn92/items/c45e22ce5f37d82ec8de
prisma/schema.prisma を編集してマイグレートする
$ pnpm prisma migrate dev --name "name for migration"
ユーザー認証 Lucia†
- https://lucia-auth.com/getting-started/sveltekit/
- https://lucia-auth.com/basics/sessions
- User にプロパティを持たせるには getUserAttributes で明示する必要がある
https://lucia-auth.com/basics/users#define-user-attributes
を見ながら
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
使い方はこの辺り :
- https://superforms.rocks/get-started
- https://superforms.rocks/examples
- プログラミング/svelte/Prisma と Lucia を使った認証システム#ud9711bc
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' ? '✅' : '❌'; 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†
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; }; }
メモ†
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 が必要なくなるというのも頷ける??
困ったとき†
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 が正しく受け渡せていない可能性があるのでチェックする。
effect_update_depth_exceeded†
リアクションのループが生じているということらしい。
つまり、何らかの $state 変数なりが変更されたときに、それを表示しようとするコード内でその $state 変数が変更を受けてしまっている、というような。
$state 指定をしたものを $effect 内で書き換えたりすると起きる。
ページの「状態」をうまく svelte 内に完結させる管理の仕方がまだわかっていないので、複雑なことをやろうとすると踏んでしまいがち???
まだ場当たり的にしか解決できてない。。。
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)が可能。
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, ''); で消している。