Angular SSR 在 cPanel 报 Unexpected token '??=' 怎么解决?
结论
优先检查处理网站请求的 Node.js 进程实际使用的版本。终端中的 node -v 只能证明当前 Shell 找到的版本,不能证明 cPanel/LiteSpeed 启动应用时使用的是同一个可执行文件。
Node.js 24 支持 ??=,无需通过降级解决这个语法错误。Node.js 15 的发布说明已列出逻辑赋值运算符 &&=、||= 和 ??=;Angular 21.2 的兼容范围也包含 Node.js ^24.0.0。题目提到的两个 Node.js 24 版本都不会因为缺少 ??= 语法支持而直接报错。Node.js 15 发布说明、Angular 版本兼容表
现有日志看起来像来自较旧的 Node.js 运行时,但其中没有实际进程版本和时间戳,暂时无法确定它对应哪次启动。先获取一次新请求产生的日志,再决定修改哪层配置。
为什么面板和终端都显示 24,应用仍可能使用旧版本?
本地构建进程、服务器终端和 Web 服务器启动的应用进程可以使用不同的 Node.js 路径。
日志中的路径表明,报错经过了 LiteSpeed 的启动脚本:
/usr/local/lsws/fcgi-bin/lsnodesm.js
如果主机使用 CloudLinux Node.js Selector,LiteSpeed 会读取相关应用配置,但其底层实现与 Apache 的 Passenger 不同。不能仅根据 cPanel 界面认定具体启动方式。LiteSpeed 的 CloudLinux 集成说明
503 Service Unavailable 表示应用当前无法正常提供服务,本身不能证明 Angular 编译失败。题目中的日志更直接地指向应用加载或启动阶段。
第一步:从 Web 应用进程记录真实版本
可以先在 cPanel 中使用相同的 Node.js 版本设置创建临时诊断应用。如果只能使用现有应用,先记录原启动文件,再短暂切换到诊断文件,完成后恢复 server.mjs。
创建 runtime-probe.cjs,内容如下:
console.error('[runtime-probe]', JSON.stringify({
node: process.version,
v8: process.versions.v8,
execPath: process.execPath,
pid: process.pid,
cwd: process.cwd()
}));
try {
var result = new Function('let value; value ??= 1; return value;')();
console.error('[syntax-probe]', result);
} catch (error) {
console.error('[syntax-probe]', error.stack);
}
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('runtime probe\n');
}).listen(process.env.PORT || 3000);
使用 .cjs 可以让诊断文件在项目声明 "type": "module" 时仍按 CommonJS 加载。将 ??= 放进字符串,可以先记录版本,再单独检测运行时是否支持该语法。
通过面板保存并重启诊断应用,访问一次绑定的网址,然后查看新产生的 stderr.log。重点对比 node、execPath 和 pid;不要只在终端执行这个文件。
支持该语法时,[syntax-probe] 应输出 1。在 LiteSpeed Selector 集成模式下,监听端口可能被替换为 Unix domain socket,因此不能仅凭无法直连 3000 端口判断诊断失败。LiteSpeed 的测试与监听说明
如果诊断标记没有出现,先确认启动文件路径和日志位置,再检查启动器是否在执行文件前就失败了。此时需要主机管理员协助查看对应进程及 LiteSpeed 日志。
第二步:根据诊断结果修复启动配置
Web 进程确实使用了旧版本
应修复应用进程的 Node.js 路径,继续调整终端的 PATH 无法保证改变应用进程使用的版本。
检查以下内容:
- 当前域名是否绑定到刚修改的应用,Application root 和 Application startup file 是否正确。
- 应用配置引用的 Node.js 可执行文件是否对应所选版本。
- 保存版本设置后,原应用进程是否真正停止并重新启动。
如果使用 CloudLinux Selector,可以检查其生成配置中的 PassengerNodejs 路径。LiteSpeed 官方列出了对该指令的支持,但实际配置位置取决于主机环境。应通过面板或由管理员修正,避免随意覆盖面板管理的配置。LiteSpeed 支持的配置指令
向主机商提供应用目录、新日志、process.version、process.execPath 和对应 PID,有助于定位具体启动链路。仅修改 .bashrc 或切换终端中的 Node.js,不足以保证 Web 应用进程随之改变。
Web 进程显示 Node.js 24,语法检测也通过
恢复 server.mjs,重新启动应用,再复现一次错误。如果仍出现相同的 ??= 报错,确认该记录是否来自另一个进程、另一份部署或历史日志。
也可以用已核实的 Node.js 绝对路径直接启动同一份 SSR 构建产物。下面两个路径都是占位符,需要替换:
/absolute/path/to/node24 --trace-warnings --unhandled-rejections=strict /absolute/path/to/server.mjs
结果可以帮助区分故障位置:
- 直接启动正常、通过 cPanel 启动失败:继续检查启动器、工作目录、环境变量及 ESM 加载方式。
- 直接启动也失败:根据新的错误堆栈检查构建产物或依赖,定位实际出错的文件。
LiteSpeed 官方也建议独立启动 Node.js 应用,辅助区分应用代码问题与服务器集成问题。LiteSpeed 应用故障排查
第三步:单独处理 Error: Not supported
不能直接将 Error: Not supported 和 Unexpected token '??=' 视为同一个错误。前者只显示了启动器中的调用位置,信息不足以确定具体原因。
确认运行时正确后,如果新日志仍报 Not supported,请让管理员核查当前 LiteSpeed 启动组件对 .mjs 和 ESM 的加载支持,并结合服务器实际安装的启动脚本定位失败位置。
项目使用了 "type": "module",启动文件也是 server.mjs。不要直接把它改名为 .cjs,或把 import 换成 require();这些修改可能改变模块解释方式,却无法修复启动器的问题。如果需要启动包装文件,还要确认 SSR 服务确实执行了监听逻辑,仅导入模块成功不代表网站已经启动。
哪些配置不必先改?
target: "ES2022" 与现代 Node.js 运行环境相符。降低 TypeScript 的 target 不能保证所有服务器端依赖都会被转换,也不会修复 Web 服务器调用了错误 Node.js 路径的问题。
@types/node 是类型声明包,packageManager 描述包管理器信息,它们都不会决定生产进程实际使用哪个 Node.js 可执行文件。
日志中的 UnhandledPromiseRejectionWarning 是加载失败之后的表现。增加 .catch() 或使用 --unhandled-rejections=strict 可以改善错误暴露方式,但不能让旧运行时识别 ??=。
修复后,通过网站请求再次核实实际进程版本,并确认新日志不再出现对应的启动错误。如果语法错误消失后仍有 503,再根据新的首个错误继续排查。现有材料不足以保证调整 Node.js 路径就能解决全部部署问题。
备注:内容仅供参考。
