Opal Ruby Rake任务重编译时触发FrozenError错误求助
错误核心原因
Ruby 3.1.x开始默认对部分核心数组实例启用自动冻结特性,而你使用的Opal 1.7.3版本在编译流程中,会尝试修改被Ruby运行时冻结的全局路径数组(比如Opal.paths)。首次编译时数组处于可修改状态,任务执行正常;但guard-rake重复触发任务时,该数组已被冻结,Opal代码再次尝试修改就会抛出FrozenError: can't modify frozen Array。
另外,guard-rake默认会复用Ruby进程上下文,首次编译后的Opal内部状态没有被重置,也是导致重复执行时冲突的原因之一。
Rake任务可能存在的问题
如果你的编译任务是直接修改全局Opal.paths(比如用append_path),写法类似下面这样,就会触发问题:
require 'opal' task :compile_opal do Opal.append_path 'app/assets/opal' js_output = Opal.compile('application') File.write('public/assets/application.js', js_output) end
问题在于Opal.append_path会直接修改全局的Opal.paths数组,Ruby 3.1+中该数组可能在首次使用后被自动冻结,后续任务执行时无法再修改。
修复方案
1. 重置Opal路径数组,避免复用冻结实例
每次编译任务执行时,手动重置Opal.paths为初始状态,而不是追加路径:
require 'opal' desc 'Compile Opal assets' task :compile_opal do # 重置路径,覆盖原有数组而非追加,避免操作冻结实例 Opal.paths.replace([ 'app/assets/opal', File.join(Opal.gem_dir, 'lib') ]) js_output = Opal.compile('application') File.write('public/assets/application.js', js_output) end
2. 使用独立编译器实例,隔离全局状态
创建独立的Opal::Compiler实例,指定专属路径,完全不依赖全局Opal.paths:
require 'opal' desc 'Compile Opal assets' task :compile_opal do compiler = Opal::Compiler.new( nil, path: ['app/assets/opal', File.join(Opal.gem_dir, 'lib')] ) # 读取入口文件并编译 source = File.read('app/assets/opal/application.rb') js_output = compiler.compile(source) File.write('public/assets/application.js', js_output) end
3. 关闭Ruby自动冻结特性
在Rake任务开头添加环境变量,禁用Ruby对数组字面量的自动冻结:
ENV['RUBY_FREEZE_ARRAY_LITERALS'] = 'no' require 'opal' # 后续编译任务代码...
4. 升级Opal版本到兼容版
Opal 1.8.0及以上版本已经修复了Ruby 3.1+的冻结数组兼容性问题,直接升级到最新稳定版可以彻底解决这类问题:
bundle update opal
Guardfile优化
确保guard-rake每次触发任务时重新加载上下文,避免状态残留:
guard :rake, task: 'compile_opal', reload: true do watch(%r{app/assets/opal/.+\.rb$}) end
reload: true选项会让guard每次执行任务时重新加载Rakefile代码,防止复用之前的冻结状态。
内容的提问来源于stack exchange,提问作者Jignesh Gohel

