@pilllesss/yorn 1.0.182 → 1.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,1064 @@
1
+ # Svelte / SvelteKit Code Review Guide
2
+
3
+ Svelte 5 / SvelteKit 审查重点:Runes 响应式系统、Server/Client 边界、Form Actions、Store 迁移、以及安全性。
4
+
5
+ ## 目录
6
+
7
+ - [Runes: $state / $derived / $effect](#runes-state--derived--effect)
8
+ - [Load 函数(Server vs Client)](#load-函数server-vs-client)
9
+ - [Form Actions](#form-actions)
10
+ - [Store 迁移(→ $state)](#store-迁移)
11
+ - [SSR vs CSR 边界](#ssr-vs-csr-边界)
12
+ - [响应式语句迁移($: → Runes)](#响应式语句迁移)
13
+ - [性能优化](#性能优化)
14
+ - [安全审查](#安全审查)
15
+ - [Review Checklist](#review-checklist)
16
+
17
+ ---
18
+
19
+ ## Runes: $state / $derived / $effect
20
+
21
+ ### $state 基础用法
22
+
23
+ ```svelte
24
+ <!-- ❌ $state 用于永远不会变化的值 -->
25
+ <script lang="ts">
26
+ let config = $state({ timeout: 5000 }); // 不需要响应式
27
+ const API_URL = $state('/api'); // 常量不需要 $state
28
+ </script>
29
+
30
+ <!-- ✅ 常量直接声明 -->
31
+ <script lang="ts">
32
+ const config = { timeout: 5000 };
33
+ const API_URL = '/api';
34
+
35
+ // $state 只用于会变化的值
36
+ let count = $state(0);
37
+ let user = $state<User | null>(null);
38
+ </script>
39
+ ```
40
+
41
+ ### $state.raw 与大型对象
42
+
43
+ ```svelte
44
+ <!-- ❌ 大型不可变数据使用深度响应式 -->
45
+ <script lang="ts">
46
+ // largeData 会被深度代理,性能开销大
47
+ let data = $state(hugeApiResponse);
48
+ </script>
49
+
50
+ <!-- ✅ $state.raw 避免深度代理 -->
51
+ <script lang="ts">
52
+ let data = $state.raw(hugeApiResponse);
53
+
54
+ // 整体替换时才触发更新
55
+ async function refresh() {
56
+ data = await fetchLatestData(); // ✅ triggers reactivity
57
+ }
58
+
59
+ // ❌ 修改嵌套属性不会触发更新
60
+ // data.items[0].name = 'new'; // will NOT re-render
61
+ </script>
62
+ ```
63
+
64
+ ### $state.snapshot 用于外部库
65
+
66
+ ```svelte
67
+ <!-- ❌ 直接将 $state 对象传给外部库 -->
68
+ <script lang="ts">
69
+ let state = $state({ x: 0, y: 0 });
70
+
71
+ onMount(() => {
72
+ // 外部库可能无法正确处理 Proxy 对象
73
+ chartLibrary.update(state); // state is a Proxy!
74
+ });
75
+ </script>
76
+
77
+ <!-- ✅ $state.snapshot 获取普通对象副本 -->
78
+ <script lang="ts">
79
+ let state = $state({ x: 0, y: 0 });
80
+
81
+ onMount(() => {
82
+ // $state.snapshot produces a plain object (Svelte 5)
83
+ chartLibrary.update($state.snapshot(state));
84
+ });
85
+ </script>
86
+ ```
87
+
88
+ ### 解构 $state 丢失响应性
89
+
90
+ ```svelte
91
+ <!-- ❌ 解构 $state 对象丢失响应性 -->
92
+ <script lang="ts">
93
+ let state = $state({ count: 0, name: 'Svelte' });
94
+ let { count, name } = state; // count and name are plain values!
95
+ </script>
96
+ <p>{count}</p> <!-- ❌ will NOT update when state.count changes -->
97
+
98
+ <!-- ✅ 直接访问 $state 属性 -->
99
+ <script lang="ts">
100
+ let state = $state({ count: 0, name: 'Svelte' });
101
+ </script>
102
+ <p>{state.count}</p> <!-- ✅ stays reactive -->
103
+
104
+ <!-- ✅ 或者单独声明每个 $state -->
105
+ <script lang="ts">
106
+ let count = $state(0);
107
+ let name = $state('Svelte');
108
+ </script>
109
+ ```
110
+
111
+ ---
112
+
113
+ ### $derived 正确用法
114
+
115
+ ```svelte
116
+ <!-- ❌ #1 反模式:用 $effect 做状态同步 -->
117
+ <script lang="ts">
118
+ let firstName = $state('John');
119
+ let lastName = $state('Doe');
120
+ let fullName = $state('');
121
+
122
+ // 不要用 $effect 来同步派生状态!
123
+ $effect(() => {
124
+ fullName = `${firstName} ${lastName}`; // unnecessary effect
125
+ });
126
+ </script>
127
+
128
+ <!-- ✅ 使用 $derived 计算派生值 -->
129
+ <script lang="ts">
130
+ let firstName = $state('John');
131
+ let lastName = $state('Doe');
132
+ let fullName = $derived(`${firstName} ${lastName}`);
133
+ </script>
134
+ ```
135
+
136
+ ### $derived 中不应有副作用
137
+
138
+ ```svelte
139
+ <!-- ❌ $derived 中产生副作用 -->
140
+ <script lang="ts">
141
+ let items = $state<Item[]>([]);
142
+ let count = $derived(() => {
143
+ console.log('recalculating'); // side effect!
144
+ analytics.track('count', items.length); // side effect!
145
+ return items.length;
146
+ });
147
+ </script>
148
+
149
+ <!-- ✅ $derived 只用于纯计算 -->
150
+ <script lang="ts">
151
+ let items = $state<Item[]>([]);
152
+ let count = $derived(items.length);
153
+
154
+ // side effects go in $effect
155
+ $effect(() => {
156
+ analytics.track('count', count);
157
+ });
158
+ </script>
159
+ ```
160
+
161
+ ---
162
+
163
+ ### $effect 正确用法
164
+
165
+ #### $effect vs $derived
166
+
167
+ ```svelte
168
+ <!-- ❌ $effect 用于同步状态(第一大反模式) -->
169
+ <script lang="ts">
170
+ let searchQuery = $state('');
171
+ let results = $state([]);
172
+
173
+ $effect(() => {
174
+ results = searchQuery ? items.filter(i => i.name.includes(searchQuery)) : items;
175
+ });
176
+ </script>
177
+
178
+ <!-- ✅ 使用 $derived -->
179
+ <script lang="ts">
180
+ let searchQuery = $state('');
181
+ let results = $derived(
182
+ searchQuery ? items.filter(i => i.name.includes(searchQuery)) : items
183
+ );
184
+ </script>
185
+ ```
186
+
187
+ #### 无限循环
188
+
189
+ ```svelte
190
+ <!-- ❌ $effect 中更新自身依赖 → 无限循环 -->
191
+ <script lang="ts">
192
+ let count = $state(0);
193
+
194
+ $effect(() => {
195
+ console.log(count);
196
+ count++; // modifying dependency inside effect → infinite loop!
197
+ });
198
+ </script>
199
+
200
+ <!-- ✅ 避免在 $effect 中修改被追踪的状态 -->
201
+ <script lang="ts">
202
+ let count = $state(0);
203
+ let log = $state<string[]>([]);
204
+
205
+ $effect(() => {
206
+ // read count, write to a different state
207
+ log = [...log, `count is ${count}`];
208
+ });
209
+ </script>
210
+ ```
211
+
212
+ #### 清理函数
213
+
214
+ ```svelte
215
+ <!-- ❌ 缺少清理函数 → 内存泄漏 -->
216
+ <script lang="ts">
217
+ let roomId = $state('');
218
+
219
+ $effect(() => {
220
+ const ws = new WebSocket(`ws://example.com/${roomId}`);
221
+ ws.onmessage = (e) => {
222
+ messages = [...messages, JSON.parse(e.data)];
223
+ };
224
+ // no cleanup! WebSocket leaks when roomId changes
225
+ });
226
+ </script>
227
+
228
+ <!-- ✅ 返回清理函数 -->
229
+ <script lang="ts">
230
+ let roomId = $state('');
231
+
232
+ $effect(() => {
233
+ const ws = new WebSocket(`ws://example.com/${roomId}`);
234
+ ws.onmessage = (e) => {
235
+ messages = [...messages, JSON.parse(e.data)];
236
+ };
237
+ return () => ws.close(); // cleanup on re-run
238
+ });
239
+ </script>
240
+
241
+ <!-- ✅ 定时器清理 -->
242
+ <script lang="ts">
243
+ $effect(() => {
244
+ const id = setInterval(() => {
245
+ console.log('tick');
246
+ }, 1000);
247
+ return () => clearInterval(id);
248
+ });
249
+ </script>
250
+ ```
251
+
252
+ #### async $effect 的追踪陷阱
253
+
254
+ ```svelte
255
+ <!-- ❌ await 后读取的状态不会被追踪 -->
256
+ <script lang="ts">
257
+ let userId = $state('1');
258
+ let preference = $state('dark');
259
+
260
+ $effect(async () => {
261
+ const user = await fetchUser(userId); // userId IS tracked
262
+ const theme = preference; // NOT tracked (read after await)!
263
+ applyTheme(user, theme);
264
+ });
265
+ </script>
266
+
267
+ <!-- ✅ 在 await 前读取所有依赖 -->
268
+ <script lang="ts">
269
+ let userId = $state('1');
270
+ let preference = $state('dark');
271
+
272
+ $effect(async () => {
273
+ const currentPref = preference; // read before await
274
+ const user = await fetchUser(userId);
275
+ applyTheme(user, currentPref);
276
+ });
277
+ </script>
278
+ ```
279
+
280
+ #### untrack 排除依赖
281
+
282
+ ```svelte
283
+ <!-- ❌ 不小心追踪了不必要的依赖 -->
284
+ <script lang="ts">
285
+ let data = $state<Data | null>(null);
286
+ let debugMode = $state(false);
287
+
288
+ $effect(() => {
289
+ if (debugMode) { // debugMode becomes a dependency!
290
+ console.log('data changed', data);
291
+ }
292
+ });
293
+ </script>
294
+
295
+ <!-- ✅ untrack 排除不相关的依赖 -->
296
+ <script lang="ts">
297
+ import { untrack } from 'svelte';
298
+
299
+ let data = $state<Data | null>(null);
300
+ let debugMode = $state(false);
301
+
302
+ $effect(() => {
303
+ if (untrack(() => debugMode)) { // debugMode is NOT tracked
304
+ console.log('data changed', data);
305
+ }
306
+ });
307
+ </script>
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Load 函数(Server vs Client)
313
+
314
+ ### +page.server.js vs +page.js
315
+
316
+ ```typescript
317
+ // ❌ 在 +page.js 中访问数据库或 secrets
318
+ // src/routes/admin/+page.js
319
+ export async function load({ fetch }) {
320
+ // universal load runs on both server and client
321
+ const data = await db.query('SELECT * FROM users'); // db not available in browser!
322
+ return { users: data };
323
+ }
324
+
325
+ // ✅ 服务端逻辑放在 +page.server.js
326
+ // src/routes/admin/+page.server.js
327
+ import { db } from '$lib/server/db';
328
+
329
+ export async function load() {
330
+ const users = await db.query('SELECT * FROM users');
331
+ return { users };
332
+ }
333
+ ```
334
+
335
+ ```typescript
336
+ // ✅ +page.js 用于客户端也可用的数据(如 fetch 聚合)
337
+ // src/routes/dashboard/+page.js
338
+ export async function load({ fetch, parent }) {
339
+ const [analytics, notifications] = await Promise.all([
340
+ fetch('/api/analytics').then(r => r.json()),
341
+ fetch('/api/notifications').then(r => r.json())
342
+ ]);
343
+ return { analytics, notifications };
344
+ }
345
+ ```
346
+
347
+ ### await parent() 瀑布流
348
+
349
+ ```typescript
350
+ // ❌ 顺序 await parent → 瀑布流
351
+ // src/routes/blog/[slug]/+page.js
352
+ export async function load({ parent, fetch }) {
353
+ const parentData = await parent(); // wait for parent
354
+ const post = await fetch(`/api/posts/${parentData.blogId}`);
355
+ return { post };
356
+ }
357
+
358
+ // ✅ 尽可能并行,避免不必要的 parent await
359
+ // src/routes/blog/[slug]/+page.js
360
+ export async function load({ parent, fetch }) {
361
+ // only await parent if you truly need its data
362
+ const post = await fetch('/api/posts/slug');
363
+ return { post };
364
+ }
365
+
366
+ // ✅ 如果确实需要 parent 数据,无法避免瀑布流,但要明确注释
367
+ // src/routes/blog/[slug]/+page.js
368
+ export async function load({ parent, fetch }) {
369
+ const { blogId } = await parent(); // required: need blogId for post URL
370
+ const post = await fetch(`/api/posts/${blogId}`);
371
+ return { post };
372
+ }
373
+ ```
374
+
375
+ ### 不可序列化的返回值
376
+
377
+ ```typescript
378
+ // ❌ 从 server load 返回不可序列化的值
379
+ // src/routes/api/+page.server.js
380
+ export async function load() {
381
+ return {
382
+ stream: fs.createReadStream('data.csv'), // not serializable!
383
+ callback: () => console.log('hi'), // functions not serializable!
384
+ date: new Date(), // OK — devalue serializes Date/Map/Set fine
385
+ };
386
+ }
387
+
388
+ // ✅ 只返回可序列化的数据
389
+ // src/routes/api/+page.server.js
390
+ export async function load() {
391
+ return {
392
+ data: await readFile('data.csv', 'utf-8'),
393
+ timestamp: Date.now(),
394
+ };
395
+ }
396
+ ```
397
+
398
+ ---
399
+
400
+ ## Form Actions
401
+
402
+ ### 使用 POST 处理副作用
403
+
404
+ ```svelte
405
+ <!-- ❌ 用 GET/load 函数处理副作用 -->
406
+ <script lang="ts">
407
+ import { goto } from '$app/navigation';
408
+
409
+ async function deleteUser(id: string) {
410
+ await fetch(`/api/users/${id}`, { method: 'DELETE' });
411
+ goto('/users'); // side effect via client navigation
412
+ }
413
+ </script>
414
+ <button onclick={() => deleteUser(user.id)}>Delete</button>
415
+
416
+ <!-- ✅ 使用 form actions -->
417
+ ```
418
+
419
+ ```typescript
420
+ // src/routes/users/+page.server.js
421
+ import { fail, redirect } from '@sveltejs/kit';
422
+
423
+ export const actions = {
424
+ delete: async ({ request, locals }) => {
425
+ const formData = await request.formData();
426
+ const id = formData.get('id');
427
+
428
+ if (!id) return fail(400, { message: 'Missing id' });
429
+
430
+ await locals.db.users.delete(id);
431
+ throw redirect(303, '/users');
432
+ }
433
+ };
434
+ ```
435
+
436
+ ```svelte
437
+ <!-- form with progressive enhancement -->
438
+ <script lang="ts">
439
+ import { enhance } from '$app/forms';
440
+ </script>
441
+
442
+ <form method="POST" action="?/delete" use:enhance>
443
+ <input type="hidden" name="id" value={user.id} />
444
+ <button type="submit">Delete</button>
445
+ </form>
446
+ ```
447
+
448
+ ### fail() 中不暴露敏感信息
449
+
450
+ ```typescript
451
+ // ❌ fail() 中返回敏感信息
452
+ // src/routes/login/+page.server.js
453
+ export const actions = {
454
+ default: async ({ request, locals }) => {
455
+ const formData = await request.formData();
456
+ const user = await locals.db.users.findByEmail(formData.get('email'));
457
+
458
+ return fail(401, {
459
+ password: formData.get('password'), // ❌ exposes password in page data!
460
+ hint: user.passwordHint, // ❌ leaks internal data!
461
+ });
462
+ }
463
+ };
464
+
465
+ // ✅ 只返回安全的错误信息
466
+ export const actions = {
467
+ default: async ({ request }) => {
468
+ const formData = await request.formData();
469
+ const email = formData.get('email');
470
+
471
+ return fail(401, {
472
+ email, // ✅ safe to echo back
473
+ incorrect: true, // ✅ generic error flag
474
+ });
475
+ }
476
+ };
477
+ ```
478
+
479
+ ### use:enhance 渐进增强
480
+
481
+ ```svelte
482
+ <!-- ❌ 表单不使用 use:enhance → 没有 JS 时才用原生行为 -->
483
+ <form method="POST" action="?/create">
484
+ <input name="title" />
485
+ <button type="submit">Create</button>
486
+ </form>
487
+
488
+ <!-- ✅ use:enhance 提供 SPA 体验 + progressive enhancement -->
489
+ <script lang="ts">
490
+ import { enhance } from '$app/forms';
491
+ </script>
492
+
493
+ <form method="POST" action="?/create" use:enhance={() => {
494
+ return ({ update }) => {
495
+ update({ reset: false }); // customize behavior
496
+ };
497
+ }}>
498
+ <input name="title" />
499
+ <button type="submit">Create</button>
500
+ </form>
501
+
502
+ <!-- ✅ 带加载状态 -->
503
+ <form
504
+ method="POST"
505
+ action="?/create"
506
+ use:enhance={() => {
507
+ submitting = true;
508
+ return ({ update }) => {
509
+ update();
510
+ submitting = false;
511
+ };
512
+ }}
513
+ >
514
+ <button type="submit" disabled={submitting}>
515
+ {submitting ? 'Creating...' : 'Create'}
516
+ </button>
517
+ </form>
518
+ ```
519
+
520
+ ---
521
+
522
+ ## Store 迁移(→ $state)
523
+
524
+ ### writable/readable → $state
525
+
526
+ ```typescript
527
+ // ❌ Legacy store pattern (Svelte 4)
528
+ // src/lib/stores/user.js
529
+ import { writable, derived } from 'svelte/store';
530
+
531
+ export const user = writable(null);
532
+ export const isLoggedIn = derived(user, $user => !!$user);
533
+
534
+ // usage with $ prefix
535
+ // $user = { name: 'John' };
536
+
537
+ // ✅ Svelte 5: shared state in .svelte.js files
538
+ // src/lib/stores/user.svelte.js
539
+ let currentUser = $state<User | null>(null);
540
+
541
+ export function getUser() {
542
+ return currentUser;
543
+ }
544
+
545
+ export function setUser(user: User | null) {
546
+ currentUser = user;
547
+ }
548
+
549
+ export function isLoggedIn() {
550
+ return currentUser !== null;
551
+ }
552
+ ```
553
+
554
+ ### $ 前缀 store 语法是遗留语法
555
+
556
+ ```svelte
557
+ <!-- ❌ $ 前缀 store 自动订阅是遗留模式 -->
558
+ <script lang="ts">
559
+ import { count } from '$lib/stores/count';
560
+ // $count is legacy syntax in Svelte 5
561
+ </script>
562
+ <p>{$count}</p>
563
+
564
+ <!-- ✅ Svelte 5 runes 模式 -->
565
+ <script lang="ts">
566
+ import { getCount } from '$lib/stores/count.svelte';
567
+
568
+ let count = $derived(getCount());
569
+ </script>
570
+ <p>{count}</p>
571
+
572
+ <!-- ✅ 或者直接用 export 的 $state 响应式 getter -->
573
+ <script lang="ts">
574
+ // count.svelte.js exports a reactive reference
575
+ import { counter } from '$lib/stores/count.svelte';
576
+ </script>
577
+ <p>{counter.value}</p>
578
+ ```
579
+
580
+ ### .svelte.js / .svelte.ts 扩展名
581
+
582
+ ```typescript
583
+ // ❌ 在普通 .js 文件中使用 runes → 编译错误
584
+ // src/lib/utils.js
585
+ let state = $state(0); // runes only work in .svelte.js files!
586
+
587
+ // ✅ 使用 .svelte.js 扩展名
588
+ // src/lib/utils.svelte.js
589
+ let state = $state(0);
590
+
591
+ export function getState() {
592
+ return state;
593
+ }
594
+
595
+ export function setState(val: number) {
596
+ state = val;
597
+ }
598
+ ```
599
+
600
+ ---
601
+
602
+ ## SSR vs CSR 边界
603
+
604
+ ### ssr=false SPA 模式
605
+
606
+ ```typescript
607
+ // ❌ 在根 layout 中禁用 SSR → 全部变成 CSR
608
+ // src/routes/+layout.js
609
+ export const ssr = false; // entire app becomes SPA
610
+
611
+ // ✅ 只在需要的页面禁用 SSR
612
+ // src/routes/admin/dashboard/+page.js
613
+ export const ssr = false; // only this page skips SSR
614
+
615
+ // ✅ 更好的做法:按路由配置
616
+ // src/routes/editor/+page.js
617
+ export const ssr = false; // editor needs browser APIs, skip SSR
618
+ ```
619
+
620
+ ### 浏览器全局变量在 SSR 中
621
+
622
+ ```svelte
623
+ <!-- ❌ 在模块顶层访问浏览器 API -->
624
+ <script lang="ts">
625
+ const height = window.innerHeight; // ReferenceError during SSR!
626
+ const prefersDark = matchMedia('(prefers-color-scheme: dark)'); // crash!
627
+ </script>
628
+
629
+ <!-- ✅ 在 onMount 或 browser guard 中访问 -->
630
+ <script lang="ts">
631
+ import { onMount } from 'svelte';
632
+ import { browser } from '$app/environment';
633
+
634
+ let height = $state(0);
635
+
636
+ onMount(() => {
637
+ height = window.innerHeight;
638
+ });
639
+
640
+ // or conditional check
641
+ const prefersDark = browser
642
+ ? matchMedia('(prefers-color-scheme: dark)').matches
643
+ : false;
644
+ </script>
645
+ ```
646
+
647
+ ### prerender 与 actions 冲突
648
+
649
+ ```typescript
650
+ // ❌ prerender 页面中定义 actions → 编译错误
651
+ // src/routes/contact/+page.server.js
652
+ export const prerender = true;
653
+
654
+ export const actions = {
655
+ // Error: prerendered pages cannot have server-side form actions
656
+ default: async ({ request }) => { /* ... */ }
657
+ };
658
+
659
+ // ✅ prerender 页面不使用 server actions
660
+ // src/routes/about/+page.server.js
661
+ export const prerender = true;
662
+ // no actions — static page
663
+
664
+ // ✅ 需要 actions 的页面不 prerender
665
+ // src/routes/contact/+page.server.js
666
+ export const actions = {
667
+ default: async ({ request }) => {
668
+ // handle form submission
669
+ }
670
+ };
671
+ ```
672
+
673
+ ---
674
+
675
+ ## 响应式语句迁移
676
+
677
+ ### $: → $derived / $effect
678
+
679
+ ```svelte
680
+ <!-- ❌ Svelte 4 响应式语句 -->
681
+ <script lang="ts">
682
+ let count = 0;
683
+ let doubled = 0;
684
+
685
+ $: doubled = count * 2; // reactive assignment
686
+ $: if (count > 10) console.log('big');
687
+ </script>
688
+
689
+ <!-- ✅ Svelte 5 runes -->
690
+ <script lang="ts">
691
+ let count = $state(0);
692
+ let doubled = $derived(count * 2); // derived value
693
+
694
+ $effect(() => {
695
+ if (count > 10) console.log('big');
696
+ });
697
+ </script>
698
+ ```
699
+
700
+ ### export let → $props()
701
+
702
+ ```svelte
703
+ <!-- ❌ Svelte 4 props -->
704
+ <script lang="ts">
705
+ export let title: string;
706
+ export let count = 0;
707
+ </script>
708
+
709
+ <!-- ✅ Svelte 5 $props() -->
710
+ <script lang="ts">
711
+ let { title, count = 0 }: { title: string; count?: number } = $props();
712
+ </script>
713
+ ```
714
+
715
+ ### on:click → onclick
716
+
717
+ ```svelte
718
+ <!-- ❌ Svelte 4 指令式事件 -->
719
+ <button on:click={handleClick}>Click</button>
720
+ <button on:click={() => count++}>Increment</button>
721
+
722
+ <!-- ✅ Svelte 5 HTML 属性式事件 -->
723
+ <button onclick={handleClick}>Click</button>
724
+ <button onclick={() => count++}>Increment</button>
725
+ ```
726
+
727
+ ### createEventDispatcher → 回调 props
728
+
729
+ ```svelte
730
+ <!-- ❌ Svelte 4 事件 dispatch -->
731
+ <script lang="ts">
732
+ import { createEventDispatcher } from 'svelte';
733
+ const dispatch = createEventDispatcher();
734
+
735
+ function handleDelete() {
736
+ dispatch('delete', { id: 42 });
737
+ }
738
+ </script>
739
+
740
+ <!-- ✅ Svelte 5 回调 props -->
741
+ <script lang="ts">
742
+ let { ondelete }: { ondelete?: (e: { id: number }) => void } = $props();
743
+
744
+ function handleDelete() {
745
+ ondelete?.({ id: 42 });
746
+ }
747
+ </script>
748
+
749
+ <!-- parent usage -->
750
+ <Child ondelete={(e) => removeItem(e.id)} />
751
+ ```
752
+
753
+ ### slot → @render children()
754
+
755
+ ```svelte
756
+ <!-- ❌ Svelte 4 slot -->
757
+ <!-- Card.svelte -->
758
+ <div class="card">
759
+ <slot />
760
+ </div>
761
+
762
+ <!-- ✅ Svelte 5 snippets -->
763
+ <!-- Card.svelte -->
764
+ <script lang="ts">
765
+ let { children } = $props();
766
+ </script>
767
+ <div class="card">
768
+ {@render children()}
769
+ </div>
770
+
771
+ <!-- with named slots → named snippets -->
772
+ <!-- Layout.svelte -->
773
+ <script lang="ts">
774
+ let { header, children, footer } = $props();
775
+ </script>
776
+ <div>
777
+ <header>{@render header?.()}</header>
778
+ <main>{@render children()}</main>
779
+ <footer>{@render footer?.()}</footer>
780
+ </div>
781
+
782
+ <!-- parent usage -->
783
+ <Layout>
784
+ {#snippet header()}<h1>Title</h1>{/snippet}
785
+ <p>Body content</p>
786
+ {#snippet footer()}<p>Footer</p>{/snippet}
787
+ </Layout>
788
+ ```
789
+
790
+ ### beforeUpdate / afterUpdate → $effect.pre
791
+
792
+ ```svelte
793
+ <!-- ❌ Svelte 4 lifecycle hooks -->
794
+ <script lang="ts">
795
+ import { beforeUpdate, afterUpdate } from 'svelte';
796
+
797
+ let count = 0;
798
+
799
+ beforeUpdate(() => {
800
+ console.log('about to update', count);
801
+ });
802
+
803
+ afterUpdate(() => {
804
+ console.log('updated', count);
805
+ document.title = `Count: ${count}`;
806
+ });
807
+ </script>
808
+
809
+ <!-- ✅ Svelte 5 $effect and $effect.pre -->
810
+ <script lang="ts">
811
+ let count = $state(0);
812
+
813
+ // $effect.pre runs before DOM updates (like beforeUpdate)
814
+ $effect.pre(() => {
815
+ console.log('about to update', count);
816
+ });
817
+
818
+ // $effect runs after DOM updates (like afterUpdate)
819
+ $effect(() => {
820
+ console.log('updated', count);
821
+ document.title = `Count: ${count}`;
822
+ });
823
+ </script>
824
+ ```
825
+
826
+ ---
827
+
828
+ ## 性能优化
829
+
830
+ ### $state.raw 用于大型不可变数据
831
+
832
+ ```svelte
833
+ <!-- ❌ 深度代理大型不可变数据 -->
834
+ <script lang="ts">
835
+ let searchResults = $state(largeResultArray); // deep proxy on every item
836
+ </script>
837
+
838
+ <!-- ✅ $state.raw 避免深度代理 -->
839
+ <script lang="ts">
840
+ let searchResults = $state.raw<SearchResult[]>([]);
841
+
842
+ async function search(query: string) {
843
+ searchResults = await fetchResults(query); // whole-array replacement
844
+ }
845
+ </script>
846
+ ```
847
+
848
+ ### Keyed {#each}
849
+
850
+ ```svelte
851
+ <!-- ❌ 无 key 的 each → 低效 DOM diff -->
852
+ {#each items as item}
853
+ <div>{item.name}</div>
854
+ {/each}
855
+
856
+ <!-- ✅ 带唯一 key 的 each -->
857
+ {#each items as item (item.id)}
858
+ <div>{item.name}</div>
859
+ {/each}
860
+
861
+ <!-- ✅ 复合 key -->
862
+ {#each items as item (`${item.category}-${item.id}`)}
863
+ <div>{item.name}</div>
864
+ {/each}
865
+ ```
866
+
867
+ ### Streaming 与 load 中的 Promise
868
+
869
+ ```typescript
870
+ // ❌ 串行等待所有数据 → 页面阻塞
871
+ // src/routes/+page.server.js
872
+ export async function load({ params }) {
873
+ const posts = await getPosts(); // slow
874
+ const comments = await getComments(); // slow
875
+ const tags = await getTags(); // slow
876
+ return { posts, comments, tags };
877
+ }
878
+
879
+ // ✅ 并行加载独立数据
880
+ export async function load({ params }) {
881
+ return {
882
+ posts: getPosts(), // return promises directly for streaming
883
+ comments: getComments(),
884
+ tags: getTags(),
885
+ };
886
+ }
887
+ ```
888
+
889
+ ```svelte
890
+ <!-- streaming in template with {#await} -->
891
+ {#await data.posts}
892
+ <p>Loading posts...</p>
893
+ {:then posts}
894
+ <ul>
895
+ {#each posts as post (post.id)}
896
+ <li>{post.title}</li>
897
+ {/each}
898
+ </ul>
899
+ {:catch error}
900
+ <p>Failed to load posts: {error.message}</p>
901
+ {/await}
902
+ ```
903
+
904
+ ---
905
+
906
+ ## 安全审查
907
+
908
+ Svelte/SvelteKit 默认自动转义模板表达式。审查重点:`{@html}` 的使用、`$env/static/private` 泄露、CSRF 内建防护。
909
+
910
+ > **跨框架 XSS 防护详见 [XSS Prevention Guide](cross-cutting/xss-prevention.md)**,含 React/Vue/Angular/Svelte 示例及 CSP 配置。
911
+
912
+ ### 不暴露私有环境变量
913
+
914
+ ```typescript
915
+ // ❌ 在 universal load 中暴露服务端 secrets
916
+ // src/routes/admin/+page.js (universal — runs on client too!)
917
+ export async function load() {
918
+ return {
919
+ apiKey: process.env.SECRET_API_KEY, // exposed to client bundle!
920
+ dbUrl: import.meta.env.DATABASE_URL, // leaks to browser!
921
+ };
922
+ }
923
+
924
+ // ✅ 私有环境变量只在 server load 中使用
925
+ // src/routes/admin/+page.server.js (server-only)
926
+ export async function load({ locals }) {
927
+ // secrets stay on server
928
+ const data = await fetch(process.env.SECRET_API_KEY + '/admin');
929
+ return { data }; // only derived data is sent to client
930
+ }
931
+
932
+ // ✅ 公开变量使用 PUBLIC_ 前缀
933
+ // .env
934
+ // PUBLIC_API_URL=https://api.example.com
935
+ // SECRET_API_KEY=xxx (no PUBLIC_ prefix = server-only)
936
+ ```
937
+
938
+ ### $lib/server/ 服务端隔离
939
+
940
+ ```typescript
941
+ // ❌ 服务端代码放在可被客户端导入的位置
942
+ // src/lib/db.js
943
+ import { SECRET_DB_URL } from '$env/static/private';
944
+ // any client component importing this gets the secret!
945
+
946
+ // ✅ 放在 $lib/server/ 目录 → 客户端导入会编译报错
947
+ // src/lib/server/db.js
948
+ import { SECRET_DB_URL } from '$env/static/private';
949
+
950
+ export async function query(sql: string) {
951
+ // safe: client cannot import from $lib/server/
952
+ }
953
+
954
+ // usage in server files only
955
+ // src/routes/api/users/+server.js
956
+ import { query } from '$lib/server/db';
957
+ ```
958
+
959
+ ### CSRF 内建防护
960
+
961
+ ```typescript
962
+ // ✅ SvelteKit 内建 CSRF 防护
963
+ // Origin header is checked automatically for POST/PUT/DELETE/PATCH
964
+ // No additional CSRF tokens needed for form actions
965
+
966
+ // ❌ 不要禁用 CSRF 检查(除非有充分理由)
967
+ // src/hooks.server.js
968
+ export const handle = sequence(
969
+ // do NOT do this without understanding the implications
970
+ // ({ event, resolve }) => resolve(event, { filterSerializedResponseHeaders: () => true })
971
+ );
972
+ ```
973
+
974
+ ### Cookie 安全设置
975
+
976
+ ```typescript
977
+ // ❌ 不安全的 Cookie 设置
978
+ // src/hooks.server.js
979
+ export async function handle({ event, resolve }) {
980
+ const token = event.cookies.get('session');
981
+ // cookie without httpOnly, secure, sameSite flags
982
+ event.cookies.set('session', token, {
983
+ path: '/',
984
+ // missing: httpOnly, secure, sameSite
985
+ });
986
+ }
987
+
988
+ // ✅ 安全的 Cookie 配置
989
+ import { dev } from '$app/environment';
990
+
991
+ event.cookies.set('session', token, {
992
+ path: '/',
993
+ httpOnly: true, // not accessible via JS
994
+ secure: !dev, // HTTPS only in production
995
+ sameSite: 'lax', // CSRF protection
996
+ maxAge: 60 * 60 * 24 * 7 // 1 week, explicit expiry
997
+ });
998
+ ```
999
+
1000
+ ---
1001
+
1002
+ ## Review Checklist
1003
+
1004
+ ### Runes: $state / $derived / $effect
1005
+
1006
+ - [ ] $state 只用于会变化的值,常量直接声明
1007
+ - [ ] 大型不可变数据使用 $state.raw
1008
+ - [ ] 没有解构 $state 对象(会丢失响应性)
1009
+ - [ ] 外部库使用 $state.snapshot 传入普通对象
1010
+ - [ ] $derived 中没有副作用
1011
+ - [ ] 没有用 $effect 替代 $derived 做状态同步
1012
+ - [ ] $effect 中不修改被追踪的状态(避免无限循环)
1013
+ - [ ] $effect 有清理函数(订阅、定时器、WebSocket)
1014
+ - [ ] async $effect 在 await 前读取所有需要追踪的状态
1015
+ - [ ] 使用 untrack 排除不相关的依赖
1016
+
1017
+ ### Load 函数
1018
+
1019
+ - [ ] 服务端逻辑放在 +page.server.js(不是 +page.js)
1020
+ - [ ] 避免不必要的 await parent() 瀑布流
1021
+ - [ ] 独立数据并行加载(Promise.all 或直接返回 Promise)
1022
+ - [ ] server load 只返回可序列化的数据
1023
+
1024
+ ### Form Actions
1025
+
1026
+ - [ ] 副作用操作(增删改)使用 form actions + POST
1027
+ - [ ] fail() 不返回敏感信息(密码、内部数据)
1028
+ - [ ] 使用 use:enhance 实现渐进增强
1029
+
1030
+ ### Store 迁移
1031
+
1032
+ - [ ] writable/readable → $state 在 .svelte.js 文件中
1033
+ - [ ] 不在普通 .js 文件中使用 runes
1034
+ - [ ] 不使用遗留的 $ 前缀 store 语法
1035
+
1036
+ ### SSR vs CSR 边界
1037
+
1038
+ - [ ] 不在根 layout 中全局禁用 SSR
1039
+ - [ ] 浏览器 API(window、document)在 onMount 或 browser guard 中使用
1040
+ - [ ] prerender 页面不包含 server actions
1041
+
1042
+ ### Svelte 4 → 5 迁移
1043
+
1044
+ - [ ] $: → $derived / $effect
1045
+ - [ ] export let → $props()
1046
+ - [ ] on:click → onclick
1047
+ - [ ] createEventDispatcher → 回调 props
1048
+ - [ ] slot → @render children()
1049
+ - [ ] beforeUpdate/afterUpdate → $effect.pre / $effect
1050
+
1051
+ ### 性能优化
1052
+
1053
+ - [ ] 大型不可变数据使用 $state.raw
1054
+ - [ ] {#each} 使用唯一 key
1055
+ - [ ] load 函数返回 Promise 实现流式传输
1056
+ - [ ] 独立数据并行加载
1057
+
1058
+ ### 安全审查
1059
+
1060
+ - [ ] 私有环境变量只在 server load 中使用
1061
+ - [ ] 服务端代码放在 $lib/server/ 目录
1062
+ - [ ] 不禁用内建 CSRF 防护
1063
+ - [ ] Cookie 设置 httpOnly、secure、sameSite
1064
+ - [ ] server load 不泄露密钥和内部数据