Svelte 编译器<script>编译错误全解:52 个 script 错误码、触发条件与源码级排查指南
【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
本文基于 Svelte 仓库中packages/svelte/messages/compile-errors/script.md这份错误消息定义文档,完整梳理 Svelte 编译器在<script>(含<script module>)层面会抛出的全部编译错误:每个错误码的含义、触发场景、修复方式,以及它们在编译器2-analyze阶段的真实触发点。读完本文,你可以准确读懂每一条CompileError的报错信息,定位对应的源码校验逻辑,并通过测试样例目录复现和验证。
错误消息是如何生成与抛出的
理解这份错误清单前,先看它的生产管线,这决定了你在报错信息中看到的固定格式:
- 错误文案以 Markdown 形式维护在 messages/compile-errors/script.md 中,每个错误以
## 错误码小节定义消息模板,%name%形式的占位符在抛出时被替换为实际值(如%rune%、%name%、%cycle%)。 - 仓库通过 scripts/process-messages/templates/compile-errors.js 模板把每个小节转换成导出函数。该文件头部注明「This file is generated by scripts/process-messages/index.js. Do not edit!」——真正的可执行版本被生成到 src/compiler/errors.js。
- 每个生成函数形如
state_invalid_placement(node, rune),内部调用e(node, 'state_invalid_placement', 消息),构造一个继承自Error的InternalCompileError(name为'CompileError'),并携带[start, end]源码位置,最后以模板字符串形式抛出,且每条消息尾部都追加https://svelte.dev/e/<错误码>形式的帮助链接。从源码结构看,这意味着报错信息可以直接被 bundler 插件捕获(构造函数注释里明确说明了「extend from Error so that various bundler plugins properly handle it」)。
因此,实际开发中遇到CompileError: $props() can only be used at the top level...时,错误码即对应本文档中某个小节标题,帮助链接指向官方错误文档页。
下面按主题分组完整覆盖文档中的全部 52 个错误码。
分组速查表
1. Props 与$props()/$bindable()校验
| 错误码 | 消息 | 触发场景 |
|---|---|---|
props_invalid_placement | $props()can only be used at the top level of components as a variable declaration initializer | $props()没写在组件实例脚本顶层的变量声明初始化位置,例如写进<script module>或函数内 |
props_duplicate | Cannot use%rune%()more than once | 同一组件中多次调用$props()(或对$props.id()重复调用) |
props_invalid_identifier | $props()can only be used with an object destructuring pattern | let props = $props()而不是对象解构let { a } = $props() |
props_invalid_pattern | $props()assignment must not contain nested properties or computed keys | 解构模式里出现嵌套属性或计算键 |
props_id_invalid_placement | $props.id()can only be used at the top level of components as a variable declaration initializer | $props.id()未在顶层以简单标识符声明 |
props_illegal_name | Declaring or accessing a prop starting with$$is illegal (they are reserved for Svelte internals) | 声明或访问以$$开头的 prop |
bindable_invalid_location | $bindable()can only be used inside a$props()declaration | $bindable()出现在$props()声明之外 |
源码印证:phases/2-analyze/visitors/CallExpression.js 中对$props的三条校验与消息逐条对应——先用context.state.has_props_rune判重(props_duplicate),再校验父节点必须是VariableDeclarator、所在脚本类型是instance、且处于实例脚本作用域根(props_invalid_placement),最后要求$props()不接受任何实参(复用rune_invalid_arguments)。对$bindable(),同文件 L42-L52 沿 AST 路径向上回溯三层,确认它必须位于$props()解构模式内的AssignmentPattern中,否则抛bindable_invalid_location。
2. Runes 调用形态校验($state/$derived/$effect等)
| 错误码 | 消息 | 说明 |
|---|---|---|
state_invalid_placement | %rune%(...)can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor. | $state、$state.raw、$derived、$derived.by只能出现在三种位置 |
effect_invalid_placement | $effect()can only be used as an expression statement | $effect/$effect.pre不能作为表达式值传递,必须独立成句 |
rune_invalid_arguments | %rune%cannot be called with arguments | 对不接受参数的 rune(如$host、$effect.tracking)传了实参 |
rune_invalid_arguments_length | %rune%must be called with %args% | 实参数量不对。从源码看,$derived/$derived.by要求「exactly one argument」,其余 rune 要求「zero or one arguments」(CallExpression.js L128-L132) |
rune_invalid_computed_property | Cannot access a computed property of a rune | 形如$state['raw']的括号动态访问 |
rune_invalid_name | %name%is not a valid rune | 引用了不存在的 rune,如$states、$state.xyz |
rune_invalid_spread | %rune%cannot be called with a spread argument | 对 rune 使用展开调用 |
rune_invalid_usage | Cannot use%rune%rune in non-runes mode | 在非 runes 模式组件中使用 rune |
rune_missing_parentheses | Cannot use rune without parentheses | 裸引用 rune 而不带括号 |
rune_removed | The%name%rune has been removed | 已移除的 rune。Identifier.js L65-L67 中硬编码针对$state.is报此错 |
rune_renamed | %name%is now%replacement% | 已更名的 rune。源码中对$effect.active→$effect.tracking、$state.frozen→$state.raw专门给出替换建议(Identifier.js L57-L63) |
rune_missing_parentheses在原文档中有专门展开:Runes 是关键字而非值,不能被赋值或作为参数传递,只能被调用,因此不带括号引用即报错:
let count = $state;无论是 rune 本身($state)还是通过属性到达的 rune($derived.by),都要加上括号及所需参数:
let count = $state(0);触发点印证:visitors/Identifier.js L40-L76 中,当标识符名符合 rune 形态、且作用域中没有同名变量遮蔽时,编译器沿父链向上合并MemberExpression拼出完整 rune 名逐一校验;若最终父节点不是CallExpression,就抛rune_missing_parentheses。
一个特殊案例来自 visitors/EachBlock.js L21-L24:{#each items as $state}这种把 rune 名当作 each 块参数的写法,会直接复用state_invalid_placement报错,源码中还留有一条// TODO weird that this is necessary注释。
3. State 字段(class 中的 rune 声明)
| 错误码 | 消息 | 说明 |
|---|---|---|
state_field_duplicate | %name%has already been declared on this class | 同一个字段被两次声明为 state 字段 |
state_field_invalid_assignment | Cannot assign to a state field before its declaration | 在字段声明之前给它赋值(如构造函数内先赋值后声明) |
state_invalid_export | 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不能直接导出 |
derived_invalid_export | Cannot export derived state from a module. To expose the current derived value, export a function returning its value | 模块中的 derived 状态只能以函数形式对外暴露 |
state_field_duplicate原文档给出了完整示例。对使用了$state或$derived的 class 字段赋值被视为state 字段声明,可以写在类体内:
class Counter { count = $state(0); }也可以写在构造函数里:
class Counter { constructor() { this.count = $state(0); } }但同一字段只能声明一次,重复即报state_field_duplicate。触发逻辑见 visitors/PropertyDefinition.js L10-L18:按字段名在state_fields中查找已有记录,若当前节点不是原始声明节点且带值,还会比较start位置——先赋值后声明则抛state_field_invalid_assignment。
4. 命名与声明规则
| 错误码 | 消息 | 说明 |
|---|---|---|
declaration_duplicate | %name%has already been declared | 同一作用域重复声明同名变量 |
declaration_duplicate_module_import | Cannot declare a variable with the same name as an import from<script module> | 实例脚本变量与模块级 import 重名 |
duplicate_class_field | %name%has already been declared | 类字段重名 |
constant_assignment | Cannot assign to %thing% | 给常量(如const、import 等)赋值 |
constant_binding | Cannot bind to %thing% | 把bind:目标指向常量 |
dollar_binding_invalid | The $ name is reserved, and cannot be used for variables and imports | 变量或 import 恰好叫$ |
dollar_prefix_invalid | The $ prefix is reserved, and cannot be used for variables and imports | 变量或 import 以$开头(runes 模式下$前缀被保留) |
global_reference_invalid | %name%is an illegal variable name. To reference a global variable called%name%, useglobalThis.%name% | 想要引用形如$foo的全局变量 |
export_undefined | %name%is not defined | 导出了未定义的名称 |
invalid_arguments_usage | The arguments keyword cannot be used within the template or at the top level of a component | 在模板或组件顶层使用arguments |
module_illegal_default_export | A component cannot have a default export | .svelte组件中出现export default |
reactive_declaration_cycle | Cyclical dependency detected: %cycle% | 检测出循环依赖,%cycle% 会列出具体链条 |
global_reference_invalid有一条现成的测试样例 tests/compiler-errors/samples/store-global-disallowed,断言报错消息为`$foo` is an illegal variable name. To reference a global variable called `$foo`, use `globalThis.$foo`,可直接用于复现。
5.each块与 snippet 参数(不可变迭代目标)
each_item_invalid_assignment:Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. \array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)`
原文档对这条错误做了完整的历史对比。Legacy 模式下允许直接给 each 块参数重新赋值或双向绑定:
<script> let array = [1, 2, 3]; </script> {#each array as entry} <!-- reassignment --> <button on:click={() => entry = 4}>change</button> <!-- binding --> <input bind:value={entry}> {/each}这种做法后来被证明有缺陷、行为不可预测(尤其是在array.map(...)等派生值上遍历时),因此在 runes 模式中被禁止。等价写法是用下标代替:
<script> let array = $state([1, 2, 3]); </script> {#each array as entry, i} <!-- reassignment --> <button onclick={() => array[i] = 4}>change</button> <!-- binding --> <input bind:value={array[i]}> {/each}对应测试 tests/compiler-errors/samples/runes-invalid-each-binding 校验的正是这条消息原文。
snippet_parameter_assignment:Cannot reassign or bind to snippet parameter。snippet 参数与 each 块参数同理,是不可重新赋值、不可绑定的。
6. legacy 模式与 runes 模式的互斥规则
| 错误码 | 消息 | 说明 |
|---|---|---|
legacy_export_invalid | Cannot useexport letin runes mode — use$props()instead | runes 组件中使用了export let |
legacy_props_invalid | Cannot use$$propsin runes mode | runes 组件中引用$$props |
legacy_rest_props_invalid | Cannot use$$restPropsin runes mode | runes 组件中引用$$restProps |
legacy_reactive_statement_invalid | $:is not allowed in runes mode, use$derivedor$effectinstead | runes 组件中使用了$:响应式语句 |
legacy_await_invalid | Cannot useawaitin deriveds and template expressions, or at the top level of a component, unless in runes mode | 非 runes 模式下在 derived、模板表达式或组件顶层使用await |
experimental_async | Cannot useawaitin deriveds and template expressions, or at the top level of a component, unless theexperimental.asynccompiler option istrue | runes 模式下上述await用法需要显式开启experimental.async编译选项 |
runes_mode_invalid_import | %name% cannot be used in runes mode | 在 runes 模式导入了与之冲突的旧式 API(如 legacy 生命周期函数) |
从源码看,legacy 标识符在 Identifier.js L81-L89 中还有专门处理:非 runes 模式引用$$props/$$restProps时会设置analysis.uses_props/uses_rest_props标记,供后续 transform 阶段生成 props 对象使用。
7. Store 订阅($前缀自动订阅)
| 错误码 | 消息 |
|---|---|
store_invalid_scoped_subscription | Cannot subscribe to stores that are not declared at the top level of the component |
store_invalid_subscription | Cannot reference store value inside<script module> |
store_invalid_subscription_module | Cannot reference store value outside a.sveltefile |
store_invalid_subscription_module原文档有补充解释:用$前缀引用 store 值只在.svelte文件内有效,因为只有在那里 Svelte 才能自动在组件挂载时创建订阅、卸载时取消订阅;文档同时建议考虑迁移到 runes。三条错误的共同背景:自动订阅依赖组件生命周期管理,因此 store 必须声明在组件顶层、且不能在<script module>(无组件实例)或.svelte文件之外使用$store语法。
8. 其他专项
| 错误码 | 消息 | 说明 |
|---|---|---|
host_invalid_placement | $host()can only be used inside custom element component instances | $host()只能在customElement组件实例中使用,且不能带参数(CallExpression.js L59-L66 同时校验参数为空和custom_element标记) |
inspect_trace_generator | $inspect.trace(...)cannot be used inside a generator function | 生成器函数中无法使用 trace |
inspect_trace_invalid_placement | $inspect.trace(...)must be the first statement of a function body | trace 必须是函数体第一条语句 |
snippet_invalid_export | An exported snippet can only reference things declared in a<script module>, or other exportable snippets | 导出 snippet 的作用域限制 |
import_svelte_internal_forbidden | Imports ofsvelte/internal/*are forbidden... | 禁止导入内部运行时代码,因其属于私有实现且随时可能变更 |
snippet_invalid_export的原文档示例值得保留。你不能这样做:
<script module> export { greeting }; </script> <script> let message = 'hello'; </script> {#snippet greeting(name)} <p>{message} {name}!</p> {/snippet}原因是greeting引用了第二个<script>(实例作用域)里定义的message,而导出 snippet 只能引用<script module>中声明的东西或其他可导出 snippet。
import_svelte_internal_forbidden的消息原文还建议:如果是为了绕过 Svelte 的某个限制而导入svelte/internal/*,请到官方 Svelte 项目仓库提交 issue 说明使用场景。
9. TypeScript
typescript_invalid_feature:TypeScript language features like %feature% are not natively supported, and their use is generally discouraged. Outside of<script>tags, these features are not supported. For use within<script>tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are usingvitePreprocess, make sure to specifically enable preprocessing script tags (vitePreprocess({ script: true }))。
要点:<script>外的 TS 特性直接不支持;<script>内需要通过 preprocessor 先转成 JS 再交给编译器,使用vitePreprocess时务必显式开启script: true。
这些错误在哪里被触发:2-analyze 阶段的 visitor 机制
从源码结构看,以上错误全部在编译的第二阶段(分析阶段)由 AST visitor 抛出,核心文件与职责如下:
- phases/2-analyze/visitors/CallExpression.js:runes 调用形态校验中心——
$bindable/$host/$props/$props.id/$state系列/$effect系列的位置、参数、重复性检查都在这里完成(如 L115-L145 对$state/$derived的位置三条件与实参数量校验)。 - phases/2-analyze/visitors/Identifier.js:所有以
$开头的标识符统一入口,负责rune_invalid_name、rune_missing_parentheses、rune_renamed、rune_removed、rune_invalid_computed_property的判定。 - phases/2-analyze/visitors/EachBlock.js:each 块上下文校验,包括把 rune 名误当 each 参数的特判。
- phases/2-analyze/visitors/PropertyDefinition.js:类字段的 state 声明顺序校验(
state_field_invalid_assignment)。 - phases/2-analyze/visitors/VariableDeclarator.js:
$props()解构模式逐属性校验(props_invalid_pattern针对嵌套属性/计算键)。 - 统一出口为 src/compiler/errors.js 中按消息文件分组生成的导出函数,全部经
InternalCompileError携带位置信息抛出。
如何验证与复现:compiler-errors 测试套件
仓库内置了针对这些错误的完整回归测试,每个样例目录包含一个能触发错误的.svelte/.js输入文件和一份_config.js断言(错误码 + 消息原文):
- 样例总入口:tests/compiler-errors/test.ts,样例位于 tests/compiler-errors/samples/ 下,目录名与错误场景一一对应,如
runes-wrong-state-placement、runes-wrong-props-placement-instance、runes-invalid-each-binding、store-global-disallowed、snippet-invalid-export、runes-before-after-update等。 - 代表性断言示例:runes-wrong-state-placement/_config.js 断言错误码
state_invalid_placement及其完整消息;runes-invalid-each-binding/_config.js 断言each_item_invalid_assignment的索引替代写法提示。
排查建议:遇到某个 script 编译错误时,先记下错误码,在本文分组表中定位语义;再按上文的 visitor 文件路径找到对应校验分支;最后到tests/compiler-errors/samples/下找同名样例,对照输入文件即可看清该错误的最小复现形式与期望消息。
小结
- 这份 script.md 是 Svelte 编译器
<script>编译错误的唯一事实来源,52 个错误码覆盖了 runes 使用位置、参数形态、legacy/runes 模式互斥、命名保留字、store 自动订阅作用域、snippet 导出边界、state 字段声明与 TypeScript 预处理等全部<script>层面约束。 - 每条错误都经 process-messages 模板 生成带
svelte.dev/e/<错误码>链接的CompileError抛出,报错信息可直接作为检索与定位的索引。 - 所有校验集中在
2-analyze阶段的 AST visitors 中实现,配合tests/compiler-errors/samples/下的样例断言,形成了「消息定义 → 生成函数 → 校验触发 → 测试固化」的完整闭环。
【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考