ข้ามไปยังเนื้อหา

Project & Tooling

วิธีเริ่มโปรเจกต์ Svelte หรือ SvelteKit ในปัจจุบันคือ sv CLI คำสั่งเดียว scaffold โปรเจกต์และเพิ่ม integration ได้ (TypeScript, Prettier, ESLint, Tailwind, testing) แบบ interactive:

Terminal window
npx sv create my-app
cd my-app
npm install
npm run dev

sv create มาแทน flow เดิม npm create svelte@latest โดยตั้งค่าโปรเจกต์ SvelteKit ให้เป็น default (วิธีที่แนะนำในการสร้างแอป Svelte), wire up Vite ให้ และให้คุณเลือก add-on ตั้งแต่ต้น มี npx sv add <name> สำหรับเพิ่ม integration เข้าโปรเจกต์ที่มีอยู่แล้วภายหลังด้วย

โปรเจกต์ Svelte รันบน Vite ตอน development Vite serve โมดูลของคุณผ่าน native ESM พร้อม hot module replacement — แก้ component แล้วเห็นการเปลี่ยนแปลงโดยไม่ต้อง reload ทั้งหน้า และรักษา state ไว้ได้เท่าที่ทำได้ ส่วน production vite build จะ bundle และ optimize output

{
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
}
}

Svelte integration ของ Vite (@sveltejs/vite-plugin-svelte ที่ sv create wire ให้แล้ว) คือสิ่งที่รัน compiler บนไฟล์ .svelte ตอน dev และ build คุณแทบไม่ต้องแตะ plugin ตัวนี้ตรง ๆ

flowchart LR
  code["your .svelte / .ts files"] --> vite["Vite + svelte plugin"]
  vite --> dev["dev: HMR server"]
  vite --> build["build: compiled, bundled output"]
pipeline ของ dev และ build

มีเครื่องมือสองตัวที่ช่วยให้ประสบการณ์การเขียนตรงไปตรงมา:

  • Svelte editor extension (ทางการ สำหรับ VS Code และตัวอื่น) ให้ syntax highlighting, autocompletion และ inline diagnostics สำหรับไฟล์ .svelte — รวมถึง type error ใน markup ไม่ใช่แค่ใน script
  • svelte-check คือเวอร์ชัน command-line: type-check และ lint ทั้งโปรเจกต์ รวมถึง expression ใน template คุณจึงจับ error ใน CI แบบเดียวกับที่ editor โชว์ให้ตอนอยู่ในเครื่อง
Terminal window
npm run check # runs svelte-check across the project

เพราะ svelte-check เข้าใจไฟล์ .svelte แบบครบวงจร จึงจับ error ที่ tsc ธรรมดามองไม่เห็น — prop type ที่ส่งผิดใน markup, typo ใน template expression, CSS selector ที่ไม่ได้ใช้

“จับ prop type ที่ส่งผิดใน markup” ไม่ใช่แค่ทฤษฎี — นี่คือ component ที่มี prop ระบุ type และ caller ที่ส่ง type ผิด:

src/lib/Greeter.svelte
<script lang="ts">
interface Props {
name: string;
}
let { name }: Props = $props();
</script>
<p>Hello {name.toUpperCase()}</p>
src/lib/BadUsage.svelte
<!-- @expect-error Type 'number' is not assignable to type 'string'. -->
<script lang="ts">
import Greeter from './Greeter.svelte';
</script>
<Greeter name={42} />

รัน svelte-check กับโปรเจกต์จริง (output จริง ตัด path ออก):

$ svelte-check --tsconfig ./tsconfig.json
src/lib/BadUsage.svelte:4:10
Type 'number' is not assignable to type 'string'.

รัน tsc ธรรมดากับโปรเจกต์นี้จะไม่จับ error นี้เลย — type error อยู่ข้างใน markup ของไฟล์ .svelte (<Greeter name={42} />) ซึ่งมีแค่เครื่องมือที่เข้าใจ format .svelte จริง ๆ ไม่ใช่แค่ .ts/.tsx เท่านั้นที่ type-check ได้

Type-checking เป็นแกนหนึ่ง linting เป็นอีกแกน sv create wire ESLint ให้พร้อม eslint-plugin-svelte ซึ่งเข้าใจ pattern เฉพาะของ Svelte ที่ linter JS/TS ทั่วไปไม่มี concept เลย — {@html} ที่ไม่ escape, {#each} ที่ไม่มี key, directive on: ที่เลิกใช้แล้ว มันรองรับแค่ flat config (eslint.config.js) ของ ESLint สมัยใหม่เท่านั้น ตรงกับ ESLint ปัจจุบัน:

eslint.config.js
import svelteConfig from './svelte.config.js';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
export default defineConfig([
js.configs.recommended,
svelte.configs.recommended,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node },
},
},
{
files: ['**/*.svelte', '**/*.svelte.js', '**/*.svelte.ts'],
languageOptions: { parserOptions: { svelteConfig } },
},
]);

รันกับ component ที่ inject HTML ที่ไม่ sanitize:

src/lib/LintViolation.svelte
<script>
let unsafeHtml = $state('<b>hi</b>');
let count = $state(0);
</script>
{@html unsafeHtml}
<button onclick={() => count++}>{count}</button>

output จริง (eslint-plugin-svelte 3.23.0, ESLint 10):

$ npx eslint src/lib/LintViolation.svelte
/.../src/lib/LintViolation.svelte
6:1 error `{@html}` can lead to XSS attack svelte/no-at-html-tags
✖ 1 problem (1 error, 0 warnings)

svelte.configs.recommended รวม rule แบบนี้ไว้ — บวก rule ที่รู้จัก deprecation ที่ flag on:click และ pattern ยุค Svelte 4 อื่น ๆ — ดังนั้นขั้น lint ใน CI จับพวกนี้ได้ก่อน PR merge ไม่ใช่แค่ตอนที่ svelte-check บังเอิญสังเกตเห็น

@sveltejs/vite-plugin-svelte (ที่ sv create wire ให้ พูดถึงไปแล้วก่อนหน้าในบทนี้) คือจุดที่ compiler option ทุกตัวถูกตั้งค่าสุดท้ายจริง ๆ สำหรับทั้งโปรเจกต์ ใน svelte.config.js:

svelte.config.js
export default {
compilerOptions: {
// e.g. { runes: true } to force runes mode project-wide (see the
// "Why Svelte, the Compiler" lesson for what this overrides)
},
vitePlugin: {
// dynamicCompileOptions lets you vary compiler options PER FILE —
// e.g. forcing runes mode only for files under one directory during
// an incremental migration.
experimental: {
dynamicCompileOptions({ filename, compileOptions }) {
if (filename.includes('/src/routes/legacy/') || compileOptions.runes) return;
return { runes: true };
},
},
},
};

flag experimental.async ที่บท Async Svelteของคอร์สนี้พิสูจน์ไว้ — ต้องเปิดก่อน await จะใช้ได้ข้างใน $derived หรือ template expression — ถูกตั้งแบบเดียวกันเป๊ะ เป็น entry ของ compilerOptions.experimental; compile error จริงของบทนั้น (Cannot use \await` in deriveds and template expressions… unless the `experimental.async` compiler option is `true“) คือสิ่งที่เกิดขึ้นเมื่อ switch ระดับโปรเจกต์นี้ยังไม่เปิด

สำหรับโปรเจกต์ Svelte 4 ที่มีอยู่แล้ว sv CLI ก็ขับ migration เองด้วย:

Terminal window
npx sv migrate svelte-5

ตามmigration guide: มัน “automates repetitive tasks such as updating dependencies, converting let declarations to runes, migrating event attributes like on:click to onclick, and transforming slot-based patterns into the new snippet and render tag syntax” มันจัดการการ rewrite เชิงกลไก; ตารางการจำโค้ด legacy ในบท events & attributes คือที่อ้างอิงว่าแต่ละการแปลงนั้นเล็งไปที่อะไร และสำหรับ compile error จริงที่คุณจะเจอกับสิ่งที่ script อัตโนมัติทำไม่ครบ

ทุกอย่างในบทนี้คือ tooling ที่สร้างขึ้น รอบ ข้อเท็จจริงเดียว: ไฟล์ .svelte ไม่ใช่ JavaScript ธรรมดา ดังนั้นเครื่องมือใดก็ตามที่เข้าใจแค่ JS/TS (tsc, config ESLint ทั่วไป) จะมองไม่เห็นความผิดพลาดระดับ markup โดยโครงสร้าง svelte-check และ eslint-plugin-svelte ต่างมีอยู่เพราะต้องมีใครสักคน parse format .svelte เองก่อนที่ type-checking หรือ linting จะมองเห็นข้างใน markup ได้ — นั่นคือเหตุผลทั้งหมดที่มันเป็นเครื่องมือแยกต่างหาก แทนที่ tsc/eslint จะทำงานได้เองตั้งแต่แรก

ได้อะไรประโยชน์ต้นทุน
svelte-check แทน tsc ธรรมดาจับ error ข้างใน markup ได้ — พิสูจน์แล้วด้านบนด้วย prop type ที่ผิด ที่ tsc เพียวมองไม่เห็นเป็นคำสั่งที่สองใน CI ที่ช้ากว่า; ต้องเรียก --tsconfig ของตัวเองแยก
eslint-plugin-svelte flat configrule ที่รู้จัก Svelte (ความเสี่ยง {@html}, on: ที่เลิกใช้, {#each} ไม่มี key) — พิสูจน์แล้วด้านบนรองรับแค่ flat config; โปรเจกต์ที่ยังใช้ .eslintrc.js ต้อง migrate ก่อน
dynamicCompileOptions สำหรับ compiler setting ต่อไฟล์codebase ใหญ่บังคับ runes mode ทีละไฟล์ตอน migrate ได้ แทนที่จะเป็น all-or-nothingconfig ของโปรเจกต์เพิ่มอีกชั้นที่ต้องคิดตาม นอกเหนือจาก object compilerOptions ธรรมดา
sv migrate svelte-5ทำส่วนที่เป็นกลไกของการ upgrade Svelte 4 → 5 ให้อัตโนมัติไม่ได้ครอบคลุมทุกเคส — อะไรที่มันพลาดจะโผล่เป็น compile error จริงตามตารางการจำโค้ด legacy
  • “tsc เพียวก็พอสำหรับ type-check โปรเจกต์ Svelte” พิสูจน์แล้วด้านบน: prop type ที่ส่งผิดใน markup ของไฟล์ .svelte มองไม่เห็นสำหรับ tsc ธรรมดาเลย — มีแค่ svelte-check ที่ parse format Svelte จริง ๆ เท่านั้นที่จับได้
  • “config ESLint ไหนก็ใช้กับไฟล์ .svelte ได้” eslint-plugin-svelte v3 ต้องการ flat config (eslint.config.js) เท่านั้น และต้องมี svelte-eslint-parser ถึงจะอ่านไฟล์ได้เลยด้วยซ้ำ — setup .eslintrc.js แบบเก่าไม่ lint markup ของไฟล์ .svelte เลย
  • “experimental.async เป็น setting ของ <svelte:options> หรือต่อ component” มันคือ compiler option ระดับโปรเจกต์ (หรือต่อไฟล์ผ่าน dynamicCompileOptions) ที่ตั้งผ่าน vite-plugin-svelte/svelte.config.js — ไม่ใช่อะไรที่คุณ toggle ต่อ component

เวลารีวิว project configuration และ tooling setup ให้เช็ค:

  • CI รันแค่ tsc --noEmit (หรือไม่รันอะไรเลย) สำหรับโปรเจกต์ SvelteKit โดยไม่มีขั้น svelte-check เลยหรือเปล่า — แปลว่า type error ระดับ markup แบบที่พิสูจน์แล้วด้านบน ship ออกไปแบบเงียบ ๆ?
  • มี .eslintrc.js/.eslintrc.json อยู่คู่กับ eslint-plugin-svelte หรือเปล่า — คู่นี้ใช้ด้วยกันไม่ได้ เพราะ major version ปัจจุบันของ plugin ต้องการ flat config เท่านั้น?
  • บทเรียน, README หรือ snippet ที่ generate มายังพูดถึง npm create svelte@latest หรืออ้าง svelte-migrate เป็น package แยกต่างหาก แทนที่จะเป็น npx sv create / npx sv migrate svelte-5 ปัจจุบันหรือเปล่า?
  • compiler option อย่าง experimental.async ถูกตั้งไว้ต่อ component ที่ไหนสักแห่ง (เป็น prop, comment) แทนที่จะอยู่ใน compilerOptions/vitePlugin ของ svelte.config.js ซึ่งเป็นที่ที่มันต้องอยู่จริง ๆ หรือเปล่า?

🐛 หา bug ในโค้ดนี้

eslint.config.js
import js from '@eslint/js';
export default [
js.configs.recommended,
// ... rest of the config, no Svelte-specific plugin at all
];
เฉลย

config นี้ lint ไฟล์ .js/.ts ธรรมดาได้ปกติ แต่ไม่พูดถึง eslint-plugin-svelte หรือ parser ของมันเลย — ดังนั้นทุกไฟล์ .svelte ในโปรเจกต์จะถูกข้ามไปเลย หรือถ้าถูกรวมผ่าน glob ก็จะถูก parse เหมือนเป็น JavaScript ธรรมดา (ซึ่งมันไม่ใช่: markup, {expressions} และ directive ไม่ใช่ JS syntax ที่ถูกต้อง) ไม่ว่าทางไหน rule เฉพาะของ Svelte ที่พิสูจน์แล้วด้านบน (อย่าง error จริงของ svelte/no-at-html-tags) ไม่เคย run เลย และ review ที่ดูแค่ว่า “ESLint ผ่านไหม” จะได้ความมั่นใจปลอม ๆ

วิธีแก้คือ flat config จากบทนี้: import eslint-plugin-svelte, extend svelte.configs.recommended และชี้ไฟล์ .svelte/.svelte.js/.svelte.ts ไปที่ parser ของ Svelte ผ่าน languageOptions.parserOptions.svelteConfig

คำสั่งปัจจุบันสำหรับ scaffold โปรเจกต์ Svelte/SvelteKit ใหม่คืออะไร?
Vite ให้อะไรตอน development?
ทำไมใช้ svelte-check แทน tsc ธรรมดา?
เครื่องมือจริงตัวไหน flag บรรทัดนี้ว่าเสี่ยง และด้วย rule ชื่ออะไร?
{@html unsafeHtml}
eslint-plugin-svelte (major version ปัจจุบัน) ต้องการอะไรถึงจะ lint ไฟล์ .svelte ได้เลย?
compiler option ระดับโปรเจกต์อย่าง experimental.async ถูกตั้งค่าจริง ๆ ที่ไหน?
npx sv migrate svelte-5 ทำอะไร ตาม migration guide ทางการ?
svelte-check รายงานอะไรสำหรับสิ่งนี้ และทำไม tsc เพียวถึงพลาด?
<!-- Greeter.svelte declares: interface Props { name: string } -->
<Greeter name={42} />