@@ -0,0 +1,127 @@
// 用法:node scripts/check.js(在 pets-fe 目录下跑,或直接给全路径)
//
// 静态自查:class 有没有人定义、而且是不是「在能生效的地方」定义、pt-icon 的 name
// 是否存在、绑的事件方法是否存在、组件是否已注册。
//
// 防的是「漏改一处 → 元素完全裸奔」这种不报错、不在 diff 里、只在特定页面/弹层才暴露
// 的问题。这个项目已经踩过两次同一个坑:.post-card 留在 community.wxss 却被用户主页
// 用(帖子没样式)、.profile-row 留在 settings.wxss 却被「我的」用(入口整个散架)。
// 所以 class 检查是按作用域来的,不是全局并一起看——那样恰好检查不出这类问题。
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const FE = path . resolve ( _ _dirname , '..' ) ;
const walk = ( d , out = [ ] ) => {
for ( const e of fs . readdirSync ( d , { withFileTypes : true } ) ) {
const p = path . join ( d , e . name ) ;
if ( e . isDirectory ( ) ) { if ( ! [ 'node_modules' , '.git' ] . includes ( e . name ) ) walk ( p , out ) ; }
else out . push ( p ) ;
}
return out ;
} ;
const files = walk ( FE ) ;
const read = ( p ) => fs . readFileSync ( p , 'utf8' ) ;
const rel = ( p ) => path . relative ( FE , p ) ;
// ── 一个 wxss 里定义的 class,跟着 @import 一起收 ────────────────────────────
function classesOf ( wxss , seen = new Set ( ) ) {
const out = new Set ( ) ;
if ( ! fs . existsSync ( wxss ) || seen . has ( wxss ) ) return out ;
seen . add ( wxss ) ;
const src = read ( wxss ) ;
for ( const m of src . matchAll ( /\.([a-zA-Z][\w-]*)/g ) ) out . add ( m [ 1 ] ) ;
for ( const m of src . matchAll ( /@import\s+["']([^"']+)["']/g ) ) {
for ( const c of classesOf ( path . resolve ( path . dirname ( wxss ) , m [ 1 ] ) , seen ) ) out . add ( c ) ;
}
return out ;
}
// ── 作用域 ─────────────────────────────────────────────────────────────────
// app.wxss 是全局的;页面 wxss 只对自己那一页生效;组件 wxss 只对组件自己的节点生效。
// 页面之间互不可见——这正是前面两次事故的成因。
const GLOBAL = classesOf ( path . join ( FE , 'app.wxss' ) ) ;
// 谁定义了这个 class,报错时好指路
const whereDefined = new Map ( ) ;
for ( const f of files . filter ( ( f ) => f . endsWith ( '.wxss' ) ) ) {
for ( const c of classesOf ( f ) ) {
if ( ! whereDefined . has ( c ) ) whereDefined . set ( c , [ ] ) ;
whereDefined . get ( c ) . push ( rel ( f ) ) ;
}
}
const iconNames = new Set ( ) ;
for ( const m of read ( path . join ( FE , 'styles/iconfont.wxss' ) ) . matchAll ( /\.pt-i-([\w-]+)\{/g ) ) iconNames . add ( m [ 1 ] ) ;
const problems = [ ] ;
for ( const f of files . filter ( ( f ) => f . endsWith ( '.wxml' ) ) ) {
const src = read ( f ) ;
const r = rel ( f ) ;
const isComponent = /^(components|custom-tab-bar)\// . test ( r ) ;
// 这个 wxml 能看到的 class:自己那个同名 wxss,加上全局(组件要开了 addGlobalClass 才有)
const own = classesOf ( f . replace ( /\.wxml$/ , '.wxss' ) ) ;
const js = f . replace ( /\.wxml$/ , '.js' ) ;
const seesGlobal = ! isComponent ||
( fs . existsSync ( js ) && read ( js ) . includes ( 'addGlobalClass' ) ) ;
const scope = seesGlobal ? new Set ( [ ... own , ... GLOBAL ] ) : own ;
for ( const m of src . matchAll ( /class="([^"]*)"/g ) ) {
// 先把 {{...}} 整段挖掉(里面是表达式不是 class),再看剩下的静态部分;
// 表达式里的三元字面量 'active' 单独收集
const dyn = [ ... m [ 1 ] . matchAll ( /\{\{([^}]*)\}\}/g ) ] . map ( ( x ) => x [ 1 ] ) . join ( ' ' ) ;
const statics = m [ 1 ] . replace ( /\{\{[^}]*\}\}/g , ' ' ) ;
const names = statics . split ( /\s+/ ) . filter ( Boolean ) ;
// 只认三元表达式里的字面量(? 'x' : 'y');wxs 函数的字符串入参不是 class
for ( const q of dyn . matchAll ( /[?:]\s*'([\w-]+)'/g ) ) names . push ( q [ 1 ] ) ;
// 纯布局壳,没样式是故意的
const LAYOUT _ONLY = new Set ( [ 'greeting' , 'tab-text' , 'aiPlan' ] ) ;
for ( const raw of names ) {
if ( LAYOUT _ONLY . has ( raw ) ) continue ;
// class="row-{{item.role}}" 挖掉表达式后剩个 row-,是拼接前缀不是类名
if ( raw . endsWith ( '-' ) ) continue ;
if ( scope . has ( raw ) ) continue ;
const at = whereDefined . get ( raw ) ;
if ( at && at . length ) {
// 最坑的一种:class 有定义,但定义在别的页面/组件里,这一页拿不到,元素裸奔
problems . push ( ` ${ r } : class " ${ raw } " 只定义在 ${ at . join ( ' / ' ) } —— 页面级 wxss 跨不了页,这里是裸的。两页共用就提到 app.wxss ` ) ;
} else {
problems . push ( ` ${ r } : class " ${ raw } " 没有任何 wxss 定义 ` ) ;
}
}
}
// 事件绑定的方法必须真的存在——绑了个不存在的方法,点了没反应且不报错
if ( fs . existsSync ( js ) ) {
const code = read ( js ) ;
const handlers = new Set ( ) ;
for ( const m of src . matchAll ( /\b(?:bind|catch)(?::)?[a-z]+="([A-Za-z_$][\w$]*)"/g ) ) handlers . add ( m [ 1 ] ) ;
for ( const h of handlers ) {
if ( ! new RegExp ( ` (^|[ \\ s,{]) ${ h } \\ s*[(:] ` , 'm' ) . test ( code ) ) {
problems . push ( ` ${ r } : 绑了 ${ h } ,但 ${ path . basename ( js ) } 里没有这个方法 —— 点了没反应 ` ) ;
}
}
}
// pt-icon 的静态 name
for ( const m of src . matchAll ( /<pt-icon[^>]*name="([^"{]*)"/g ) ) {
if ( m [ 1 ] && ! iconNames . has ( m [ 1 ] ) ) problems . push ( ` ${ r } : pt-icon name=" ${ m [ 1 ] } " 图标不存在 ` ) ;
}
// 用到的自定义组件是否在同名 .json 里注册
const json = f . replace ( /\.wxml$/ , '.json' ) ;
const reg = fs . existsSync ( json ) ? Object . keys ( JSON . parse ( read ( json ) ) . usingComponents || { } ) : [ ] ;
const known = [ 'nav-bar' , 'pet-switch' , 'bottom-sheet' , 'fab' , 'pt-icon' , 'seg-tabs' , 'stat-ring' , 'profile-head' ] ;
for ( const tag of known ) {
if ( new RegExp ( '<' + tag + '[\\s>]' ) . test ( src ) && ! reg . includes ( tag ) ) {
problems . push ( ` ${ r } : 用了 < ${ tag } > 但 ${ path . basename ( json ) } 没有注册 ` ) ;
}
}
}
if ( problems . length ) {
console . log ( '发现 ' + problems . length + ' 处问题:' ) ;
problems . forEach ( ( p ) => console . log ( ' ✗ ' + p ) ) ;
process . exit ( 1 ) ;
}
console . log ( '✓ class 作用域 / 图标名 / 事件方法 / 组件注册 全部对得上(检查了 ' +
files . filter ( ( f ) => f . endsWith ( '.wxml' ) ) . length + ' 个 wxml, ' + iconNames . size + ' 个图标)' ) ;