diff --git a/web/package.json b/web/package.json index 7371884..54ac2a4 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview" + "preview": "vite preview", + "smoke": "node scripts/smoke-admin.mjs" }, "dependencies": { "pinia": "^4.0.3", diff --git a/web/scripts/check-declaration-order.mjs b/web/scripts/check-declaration-order.mjs new file mode 100644 index 0000000..5ae40be --- /dev/null +++ b/web/scripts/check-declaration-order.mjs @@ -0,0 +1,167 @@ +/** + * 声明顺序守卫:抓「setup 期就会求值的表达式(含它的间接依赖)引用了后面才声明的 const」这一类 TDZ。 + * + * 背景:AthletesSection.vue 里 `filteredIds = computed(() => filtered.value...)` 引用了 + * 199 行之后才声明的 `filtered`,而 `watch(filteredIds, ...)` 在注册时会**同步求值一次源** —— + * setup 阶段就访问到 TDZ 中的 `filtered`,抛 + * `ReferenceError: Cannot access 'filtered' before initialization`,整个分区渲染失败、 + * 页面只剩标签栏。这个缺陷躲过了 vue-tsc 与全部单元测试(没有 jsdom,组件从不真正挂载), + * 在 dev_0.1.6 / 0.1.7 / 0.1.8 三个版本里都没被发现,所以单独做一个静态检查。 + * + * 规则(保守,只报确定会炸的): + * 1. `watch(...)` / `watchEffect(...)` 是**注册时同步求值**的(Vue 要取初始值); + * 2. 从它的第一个实参出发,沿「const 初始化式里引用了哪些 const」这条边做**传递闭包**; + * 3. 闭包里任何 const 的声明行落在 watch 调用行**之后**,就是一次必然的 TDZ。 + * 直接依赖与间接依赖都要查 —— 只查直接依赖会漏掉本次事故(引用的 filteredIds 声明在前面)。 + * + * 只扫 `` + const violations = findOrderViolations('Broken.vue', broken) + assert.ok(violations.length > 0, '应当报出 watch 求值到后声明的 filtered') + assert.equal(violations[0].name, 'filtered') + assert.ok(violations[0].declaredAt > violations[0].line, '声明行必须晚于求值行') +}) + +test('声明顺序正确时不报', () => { + const fixed = `` + assert.deepEqual(findOrderViolations('Fixed.vue', fixed), []) +}) + +test('函数局部的 const 不算违规(只看脚本块顶层的声明)', () => { + const localConst = `` + assert.deepEqual(findOrderViolations('Local.vue', localConst), []) +}) + +test('整个 web/src 目录当前没有这类写法', () => { + const root = new URL('../../', import.meta.url).pathname + const violations = scanWebSources(root) + assert.deepEqual( + violations, + [], + violations.map((v) => `${v.file}:${v.line} 会求值到 ${v.name}(声明于第 ${v.declaredAt} 行)`).join('\n'), + ) +})