首页
/ Svelte 编译器错误全解:错误码目录、输出格式与底层生成机制

Svelte 编译器错误全解:错误码目录、输出格式与底层生成机制

2026-09-06 13:11:07作者:龚格成

在 Svelte 项目中,编译期错误(compiler errors)是 Svelte 编译器在解析与校验 .svelte 文件时抛出的一类“带错误码、带源码位置、可被工具链程序化消费”的异常。本文以官方参考文档 Compiler errors 为主体,梳理其完整的错误码目录结构、每类错误码的典型触发场景,并深入 消息生成脚本错误诊断类编译器错误函数文件 的源码,说明一个错误码是如何从 Markdown 文档一路变成 CompileError 异常与文档页面的,帮助你在构建工具中正确捕获、展示和排查编译错误。

文档页面的实现:一行 @include 背后

30-compiler-errors.md 这个参考文档本身极简,正文只有一行:

@include .generated/compile-errors.md

也就是说,页面上列出的全部错误码内容并非手写在文档里,而是由脚本生成到 documentation/docs/98-reference/.generated/compile-errors.md(约 1264 行)后被包含进来。该生成文件开头明确标注 “This file is generated by scripts/process-messages/index.js. Do not edit!”。理解这一点很重要:错误码的唯一事实来源(single source of truth)是 packages/svelte/messages/compile-errors/ 目录下的 Markdown 源文件,文档页面和编译器抛错的 JS 函数都是由它派生的。

编译错误的输出格式:码、消息与代码帧

当编译器遇到错误时,会抛出 CompileError。从 compile_diagnostic.js 的实现看,每个诊断对象包含如下字段:

  • code:错误码(如 block_unclosed),用于机器判断错误类型;
  • message:人类可读的错误消息,末尾附带 https://svelte.dev/e/<code> 形式的说明页链接(见 compile-errors.js 模板e(node, 'CODE', ...) 的拼接方式);
  • filenamestartendposition:出错位置的文件名与行列信息;
  • frame:代码帧(code frame)。

其中代码帧由 get_code_frame 生成:它取出错行的前 2 行与后 3 行,在出错位置下方用 ^ 指示具体列,并把行首 Tab 统一替换为两个空格以对齐指示符。因此终端中的典型输出形如(格式依据 get_code_frametoString 的实现):

block_unclosed: Block was left open
src/App.svelte:5:0
3: <div>
4:   <h1>hi</h1>
5: {#if show}
  ^

CompileDiagnostic 同时提供 toJSON(),返回 { code, message, filename, start, end, position, frame } 的纯数据对象。这对构建工具很有用:在 Vite 插件或自定义打包流程中,你可以 catchCompileError 后调用 toJSON() 拿到结构化数据,再转成 IDE 悬浮提示或 CI 报告,而不是解析字符串。

另外注意抛出路径的细节:src/compiler/errors.js 中的内部类 InternalCompileError 继承自 Error,并把 CompileDiagnostic 的属性复制到自身上——源码注释说明这是为了让各种 bundler 插件能正确识别普通 Error,同时保持与 warning 相同的对象形状。它的 name 被设为 'CompileError',且故意清空 stack 以避免噪音。

错误码目录的结构:四个文件,各自管辖一类问题

生成脚本遍历 messages 目录下的分类目录,编译错误对应 compile-errors,其源文件按错误来源拆分为四个 Markdown 文件:

源文件 管辖范围 错误码数量(约)
options.md 编译器选项 3
script.md <script> 块与 runes 53
template.md 模板语法、元素、绑定、svelte: 元元素 约 100
style.md <style> 块与 :global 选择器 14

源文件的书写规范(决定生成的文档与函数长什么样)是:

  • ## <code> 标题声明一个错误码,每个文件内按字母序排列,出现重复错误码会直接抛 Duplicate message code 错误;
  • 紧随其后的 > 引用块是错误消息本身;多个引用块表示消息重载(overload),后一条必须比前一条引入新的 %参数% 占位符,否则脚本报 Message overloads must have new parameters
  • %name% 形式的占位符会被转换为对应错误函数的参数,并在生成代码的 JSDoc 中体现为 @param
  • 引用块之后的普通文本(含代码示例)作为“details”附录,出现在生成文档中,解释错误的来龙去脉与修法。

这一套格式规范定义在 process-messages/index.js 的解析逻辑中,脚本还支持 -w 监听模式,在 messages 目录变化时自动重新生成。

options 类:编译器选项错误

options.md 中最具实践价值的是 options_unrecognised

Unrecognised compiler option %keypath%

配合 validate-options.js 中的校验逻辑可以确认:Svelte 对未知选项采取“宽容但不沉默”的策略——已移除的旧选项(如 loopGuardTimeoutenableSourcemaphydratable 等)会通过 warn_removed 走 warning 通道(options_removed),而完全无法识别的选项键则抛出 options_unrecognisedkeypath 支持嵌套路径(如 foo.bar)。取值不合法则触发 options_invalid_value(消息为 Invalid compiler option: %details%)。这意味着在升级 Svelte 大版本后,构建配置里残留的旧选项要么收到弃用警告、要么直接编译失败,是很好的版本迁移“哨兵”。

script 类:runes 模式与 legacy 模式的边界

script.md 是错误码最密集的文件,核心主题是在 runes 模式与 legacy 模式之间,哪些写法被禁止、必须如何改写

$props() 的形态约束

  • props_invalid_placement$props() 只能作为组件顶层的变量声明初始化器使用;
  • props_invalid_identifier$props() 只能配合对象解构模式使用(即 let { a, b } = $props(),不能给一个标识符赋值);
  • props_invalid_pattern:解构中不能出现嵌套属性或计算键;
  • props_duplicate$props() 不允许调用多次;
  • props_illegal_name:以 $$ 开头的 prop 名是保留的;
  • bindable_invalid_location$bindable() 只能在 $props() 声明内部使用;
  • props_id_invalid_placement$props.id() 只能出现在组件顶层的变量声明初始化器中。

legacy 写法进入 runes 模式

Svelte 5 起组件默认可用 runes,编译器会阻止两套语法混用:

  • legacy_export_invalid:runes 模式下不能使用 export let,改用 $props()
  • legacy_reactive_statement_invalid:runes 模式下不允许 $: 语句,改用 $derived / $effect
  • legacy_props_invalid / legacy_rest_props_invalid:runes 模式下不能使用 $$props$$restProps
  • legacy_await_invalid:非 runes 模式(或实验选项未开启)时,derived 与模板表达式内不能使用 await。反之,在 runes 中误用非 runes 语法会触发 rune_invalid_usageCannot use %rune% rune in non-runes mode)。

rune 误用与改名

  • rune_invalid_name%name% is not a valid rune——当你写了 $statex 这类形似 rune 的标识符时触发;
  • rune_missing_parentheses:rune 是关键字而非值,不能赋值或传参,只能调用。details 部分给出了正反示例:let count = $state 报错,应写为 let count = $state(0)
  • rune_renamed / rune_removed:针对已改名或已移除的 rune 的升级提示,%name% is now %replacement% 这类消息直接告诉你新名字;
  • rune_invalid_arguments / rune_invalid_arguments_length / rune_invalid_spread:约束每个 rune 的参数形态;
  • rune_invalid_computed_property:不允许访问 rune 的计算属性。

$state 声明位置与模块导出

  • state_invalid_placement%rune%(...) 只能用作变量声明初始化器、类字段声明,或构造函数顶层对类字段的首次赋值。这解释了为什么 count = $state(0)(裸赋值)不合法,而 this.count = $state(0) 在构造函数中合法;
  • state_field_duplicate:同一个类字段用 $state/$derived 声明只能发生一次(类体内或构造函数内各算一次机会);details 部分给出了 class Counter { count = $state(0); } 等示例;
  • state_invalid_export:模块中被重新赋值的 state 不能直接 export,应导出返回其值的函数,或只修改该 state 的属性;
  • derived_invalid_export:同理,derived state 不能从模块导出,应导出一个返回其当前值的函数。

对 each 块参数的赋值

each_item_invalid_assignment 是 runes 模式下最典型的行为变化:不允许对 each 块参数重新赋值或绑定,应改用数组与索引变量。错误消息直接内嵌了修法,而 details 部分给出了完整的 legacy 与 runes 对照示例:

<!-- legacy 允许、runes 下报错的写法 -->
{#each array as entry}
	<button on:click={() => entry = 4}>change</button>
	<input bind:value={entry}>
{/each}
<!-- runes 下的正确写法 -->
<script>
	let array = $state([1, 2, 3]);
</script>

{#each array as entry, i}
	<button onclick={() => array[i] = 4}>change</button>
	<input bind:value={array[i]}>
{/each}

store 订阅与作用域

  • store_invalid_subscription_module$ 前缀访问 store 值只能在 .svelte 文件中使用,因为 Svelte 只在组件挂载/卸载时自动管理订阅;details 建议迁移到 runes;
  • store_invalid_subscription:不能在 <script module> 内引用 store 值;
  • store_invalid_scoped_subscription:不能订阅未在组件顶层声明的 store。

其他高频项

  • import_svelte_internal_forbidden:禁止导入 svelte/internal/*,内部 API 随时可能变化,遇到 Svelte 本身的限制应去上游提 issue;
  • typescript_invalid_feature:TypeScript 特性(如类型标注)不被原生支持,<script> 内需要预处理器转换(如 vitePreprocess({ script: true })),<script> 外则完全不支持;
  • declaration_duplicate / duplicate_class_field:重复声明检测;
  • export_undefined:导出了未定义的名字;
  • reactive_declaration_cycle / const_tag_cycle:响应式声明之间的循环依赖检测,%cycle% 会列出依赖环;
  • experimental_async:在 derived、模板表达式或组件顶层使用 await 需要开启 experimental.async 编译选项;
  • snippet_invalid_export:导出的 snippet 只能引用 <script module> 中声明的东西或其他可导出 snippet。details 部分给出了完整反例——greeting snippet 引用了实例 <script> 中的 message,因此不能作为模块级导出。

template 类:解析、属性、绑定与 svelte: 元元素

template.md 覆盖了模板层面的全部硬性错误,可按主题分组理解。

解析与块结构

  • block_unclosed(Block was left open)、element_unclosed<%name%> was left open)、unexpected_eof(Unexpected end of input):未闭合块/元素与文件截断;
  • block_invalid_continuation_placement{:else} 等续接块位置不对,消息里直接提示“是不是忘了闭合前面的元素或块”;
  • block_invalid_elseif:拼写纠正提示,'elseif' should be 'else if'
  • expected_block_type / expected_tag{#...} 必须是 if/each/await/key/snippet 之一,{@...} 必须是 html/render/attach/const/debug 之一;
  • element_invalid_closing_tagelement_invalid_closing_tag_autoclosed:处理 HTML 自动闭合带来的结构推断冲突(如 <li> 未闭合又被下一个 <li> 顶掉);
  • void_element_invalid_content:void 元素不能有子节点或闭合标签;
  • node_invalid_placement:浏览器会“修复”违反 HTML 约束的结构从而破坏 Svelte 对组件结构的假设。details 部分举了三个经典例子:<p>hello <div>world</div></p><p> 被自动闭合、<option> 内的 <div> 被移除、<table> 内自动插入 <tbody>

属性与绑定

  • attribute_duplicate:同一元素上属性必须唯一;
  • attribute_invalid_sequence_expression:runes 模式下属性值不能是逗号序列表达式(如 class={size, color})。details 给出两条修法:用数组 class={[size, color]},或确需逗号运算符时加括号 (size, color)(注意其求值结果是 colorsize 被忽略);
  • attribute_invalid_event_handler:事件属性(如 onclick)必须是 JS 表达式而不能是字符串;
  • attribute_contenteditable_missing / attribute_contenteditable_dynamictextContent/innerHTML/innerText 双向绑定要求静态的 contenteditable 属性;
  • bind_invalid_expression:只能绑定到标识符、成员表达式或 {get, set} 对;bind_invalid_target 说明某些 bind: 只能用于特定元素(如 bind:height 之于 <iframe> 等);bind_group_invalid_expression 要求 bind:group 目标是标识符或成员表达式;
  • bind_invalid_name:无效绑定名,且有带 %explanation% 的重载消息提供更具体的解释。

事件

  • mixed_event_handler_syntaxes:同一组件上混用旧语法(on:click)与新语法(onclick)不被允许,统一使用 on%name% 语法;
  • event_handler_invalid_modifier / event_handler_invalid_modifier_combination:修饰符白名单与互斥组合校验;
  • event_handler_invalid_component_modifier:除 once 外的修饰符只能用于 DOM 元素。

svelte: 元元素与 svelte:options

  • svelte_component_missing_this / svelte_element_missing_this<svelte:component> 必须有 this 属性,<svelte:element> 必须有带值的 this 属性;
  • svelte_meta_invalid_tag:列出合法的 <svelte:*> 标签名清单;
  • svelte_meta_invalid_placement<svelte:window><svelte:body><svelte:head><svelte:document> 等不能出现在元素或块内部;
  • svelte_options_invalid_customelement 系列:校验 <svelte:options>customElement 的形态——必须是合法自定义元素名的字符串字面量或 { tag, shadow, props } 结构体,且 props 必须是静态可分析的 { [key: string]: { attribute?, reflect?, type? } } 对象字面量,shadow 只能是 "open"/"none"ShadowRootInit

snippet、render 标签与 slot 的共存规则

  • render_tag_invalid_expression{@render ...} 只能包含调用表达式;render_tag_invalid_call_expression 禁止用 apply/bind/call 调用 snippet 函数;render_tag_invalid_spread_argument 禁止 spread 实参;
  • slot_snippet_conflict:同一组件内不允许 <slot> 语法与 {@render ...} 标签共存,要求彻底迁移到 {@render}
  • snippet_conflict:显式 children snippet 与隐式 children 内容不能同时存在;
  • const_tag_invalid_reference{@const} 声明在 snippet 作用域中的可见性问题。details 部分给出了关键示例——<svelte:boundary>(或组件)顶层代码属于隐式 children snippet,其中的 {@const foo = ...} 在同级定义的 failed snippet 里不可见,并解释了这背后的 snippet 等价展开。

其余实用项

  • each_key_without_as:没有 as 子句的 {#each} 不能带 key;
  • animation_missing_key / animation_invalid_placementanimate: 指令要求元素是带 key 的 {#each} 的唯一子元素,消息里甚至贴心地问“你是不是忘了给 each 加 key”;
  • transition_conflict / transition_duplicate:同一元素上不同/相同类型的 transition 指令冲突;
  • script_duplicate / style_duplicate:组件至多一个顶层 <script>(和/或一个 <script module>)与一个 <style>
  • js_parse_error:内嵌 JS 解析失败时透传底层解析器消息。

style 类::global 选择器规则

style.md 的全部错误都围绕 scoped CSS 中 :global 的合法用法:

  • css_global_invalid_placement:global(...) 只能出现在选择器序列的开头或结尾,不能在中间;
  • css_global_invalid_selector:global(...) 必须恰好包含一个选择器;
  • css_global_invalid_selector_list:作为复合选择器的一部分时,内部不能含类型选择器或通配选择器;
  • css_type_selector_invalid_placement:global(...) 后面不能紧跟类型选择器;
  • css_global_block_invalid_modifier / css_global_block_invalid_modifier_start:global 块不能“修饰”已存在的选择器,除非它本身是其他选择器的后代;
  • css_global_block_invalid_combinator:global 选择器不能紧跟某个组合符;
  • css_global_block_invalid_list:global 块不能与普通选择器混排在同一个选择器列表里。details 给出了具体反例与拆法——:global, x { y { color: red; } } 必须拆成 :global { y { ... } }x y { ... } 两个独立规则,因为块内“全部非 scoped”与外层“scoped”两种语义无法合并转换;
  • css_global_block_invalid_declaration:顶层 :global {...} 块内只能有规则、不能有声明;
  • css_nesting_selector_invalid_placement:嵌套选择器只能用于规则内部,或独立 :global(...) 的第一个选择器位置;
  • css_selector_invalid / css_expected_identifier / css_empty_declaration:CSS 解析层的基础错误。

底层机制:从 Markdown 到 CompileError 的完整链路

理解 process-messages/index.js 的生成流程后,可以完整还原一条错误码的“一生”:

  1. 编写:维护者在 packages/svelte/messages/compile-errors/*.md 中按 ## 错误码 + > 消息 + 可选 details 的格式新增条目;
  2. 解析与排序:脚本用正则 ## ([\w]+)\n\n... 提取每个错误码,检测重复,按字母序回写源文件;
  3. 生成文档:把所有错误码渲染为 ### 错误码 + 消息代码块(+ details)的 Markdown,写入 documentation/docs/98-reference/.generated/compile-errors.md,即官方文档页面包含的内容;
  4. 生成代码:脚本读取 templates/compile-errors.js 中的模板函数 CODE(node, PARAMETER),为每个错误码克隆出一份具名导出函数——如 export function options_invalid_value(node, details)——函数体内部调用 e(node, 'options_invalid_value', Invalid compiler option: ${details}\nhttps://svelte.dev/e/options_invalid_value)%var% 占位符在此阶段被转换为模板字符串插值与函数参数,重载消息则被编译为按“是否传入更多参数”分支的三元表达式;
  5. 抛出:编译器各阶段(parse/analyze/transform)在检测出违规时调用对应函数,e() 构造 InternalCompileErrorname'CompileError'),并基于传入的 AST 节点起点/终点(或数字位置)挂上行列信息与代码帧。

这套机制保证了三处永远一致:编译器实际抛出的消息、官方文档中的说明、错误函数的类型签名(JSDoc),三者都由同一份 Markdown 派生。

测试保障:compiler-errors 测试套件

仓库中的 tests/compiler-errors/ 目录为错误检测本身提供回归保障:samples/ 下每个子目录是一个用例,包含一个故意触发错误的 .svelte 文件与一个 .js 期望文件(如 each-key-without-asrunes-wrong-state-argsconst-tag-cyclical 等上百个样例),由 test.ts 统一驱动编译并比对抛出的错误码与消息。如果你在上游看到某个错误码的样例目录被新增或修改,基本可以推断对应的错误检测逻辑同步发生了变化。

实践建议:如何在构建工具中消费编译错误

  • 捕获与展示:在 Vite 插件、esbuild loader 或自定义构建脚本中 try/catch Svelte 编译调用;判断 err.name === 'CompileError' 后调用 toJSON(),用 code 做程序化分支(例如对 each_item_invalid_assignment 给出“这是 runes 行为变化”的定制提示),用 frame/start/end 渲染 IDE 级别的错误定位;
  • 错误码即文档锚点:每个错误消息自带 https://svelte.dev/e/<code> 链接,而仓库内的 错误码文档生成文件 提供等价的离线查阅入口,排障时按错误码检索即可;
  • 区分四类诊断:Svelte 的诊断体系分为编译器错误(本文)、编译器警告(不中断编译)、运行时错误 与运行时警告,它们共用同一套消息生成管线,但分属 compile-errorscompile-warningsclient-errorsshared-errors 等不同类别目录,排障时先确认错误出现的阶段(编译期还是运行期)能避免查错手册;
  • 升级检查options_removed/options_unrecognisedrune_renamed 这类错误码是版本迁移的天然检查点,升级大版本后跑一遍构建,任何残留的旧选项或旧语法都会以确定性错误暴露出来。
登录后查看全文
热门项目推荐
相关项目推荐