* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs');
const path = require('path');
function getComponents() {
const componentsPath = path.join(
__dirname,
'packages/react-native-harmony/Libraries/Components',
);
if (fs.existsSync(componentsPath)) {
return fs
.readdirSync(componentsPath)
.filter((dir) =>
fs.statSync(path.join(componentsPath, dir)).isDirectory(),
);
}
return [];
}
const components = getComponents();
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'chore',
'deprecated',
'removed',
'security',
'revert',
],
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-enum': [
2,
'always',
[
...components,
'View',
'Text',
'Image',
'TextInput',
'ScrollView',
'FlatList',
'ListView',
'WebView',
'Modal',
'RefreshControl',
'ActivityIndicator',
'Flex',
'Absolute',
'SafeArea',
'KeyboardAvoidingView',
'Networking',
'Storage',
'AsyncStorage',
'Clipboard',
'Linking',
'Alert',
'Toast',
'Permissions',
'DeviceInfo',
'Animated',
'Reanimated',
'LayoutAnimation',
'GestureHandler',
'Navigation',
'ReactNavigation',
'StackNavigator',
'StyleSheet',
'StyleSheetOptimizer',
'Theme',
'Bridge',
'TurboModules',
'Fabric',
'LegacyComponents',
'Hermes',
'JSC',
'JSI',
'JSVM',
'BuildSystem',
'CMake',
'HAP',
'HAR',
'Symbols',
'DevTools',
'Debugger',
'Profiler',
'Flipper',
'Tests',
'E2E',
'UnitTests',
'Docs',
'Examples',
'Config',
'CLI',
'Scripts',
'Upgrade',
'Performance',
],
],
'scope-case': [2, 'always', 'pascal-case'],
'scope-empty': [0],
'subject-empty': [2, 'never'],
'subject-case': [0],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 100],
'body-max-line-length': [2, 'always', 100],
'footer-max-line-length': [2, 'always', 100],
'signed-off-by': [2, 'always'],
'references-empty': [0],
'rnoh-dco-email-match': [2, 'always'],
'rnoh-body-required': [2, 'always'],
'rnoh-symbols-change-warning': [1, 'always'],
'rnoh-native-change-warning': [1, 'always'],
'rnoh-build-change-warning': [1, 'always'],
'rnoh-component-change-warning': [1, 'always'],
'rnoh-breaking-change-format': [2, 'always'],
'rnoh-stability-ref-warning': [2, 'always'],
'rnoh-subject-english': [2, 'always'],
'rnoh-impact-required': [2, 'always'],
},
plugins: [
{
rules: {
'rnoh-dco-email-match': ({ raw }) => {
const signedOffByMatch = raw.match(/Signed-off-by:\s*(.+)/);
if (!signedOffByMatch) {
return [
false,
'commit 必须包含 Signed-off-by 签名行,格式: Signed-off-by: Name <your-email>\n使用 git commit --signoff 或手动添加\nDCO签署参考: docs/zh-cn/06-社区/社区规范/开发者共建指南.md - 第5节',
];
}
const email = signedOffByMatch[1].match(/<([^>]+)>/);
if (!email) {
return [
false,
'Signed-off-by 需包含邮箱地址,格式: Signed-off-by: Name <your-email>\nDCO签署参考: docs/zh-cn/06-社区/社区规范/开发者共建指南.md - 第5节',
];
}
return [true];
},
'rnoh-body-required': ({ type, body }) => {
const requiredTypes = [
'feat',
'fix',
'refactor',
'perf',
'deprecated',
'removed',
'security',
];
if (requiredTypes.includes(type) && !body) {
return [
false,
`${type} 类型commit必须提供body说明详细内容\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md`,
];
}
return [true];
},
'rnoh-symbols-change-warning': ({ scope, body }) => {
if (
scope === 'Symbols' &&
!body?.includes('符号') &&
!body?.includes('.sym') &&
!body?.includes('llvm-objcopy')
) {
return [
0,
'Symbols变更建议说明符号处理情况(如:符号分离、剥离、路径等)\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第3节',
];
}
return [true];
},
'rnoh-native-change-warning': ({ scope, body }) => {
const nativeScopes = [
'Hermes',
'JSI',
'JSVM',
'Bridge',
'TurboModules',
'Fabric',
];
if (
nativeScopes.includes(scope) &&
!body?.includes('符号') &&
!body?.includes('.so')
) {
return [
0,
'Native层变更建议说明符号处理和.so文件影响\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第3节',
];
}
return [true];
},
'rnoh-build-change-warning': ({ scope, body }) => {
const buildScopes = ['BuildSystem', 'CMake', 'HAP', 'HAR'];
if (
buildScopes.includes(scope) &&
!body?.includes('Debug') &&
!body?.includes('Release')
) {
return [
0,
'构建系统变更建议说明对Debug/Release/Release2模式的影响\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第3节',
];
}
return [true];
},
'rnoh-component-change-warning': ({ scope, body }) => {
const componentScopes = [
'View',
'Text',
'Image',
'ScrollView',
'FlatList',
'ListView',
'WebView',
'Modal',
];
if (
componentScopes.includes(scope) &&
!body?.includes('兼容性') &&
!body?.includes('API')
) {
return [
0,
'组件变更建议说明兼容性和API变化\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第3节',
];
}
return [true];
},
'rnoh-breaking-change-format': ({ body, footer }) => {
const content = (body || '') + '\n' + (footer || '');
if (
content.includes('BREAKING CHANGE') ||
content.includes('BREAKING-CHANGE')
) {
if (
!content.includes('BREAKING CHANGE:') &&
!content.includes('BREAKING-CHANGE:')
) {
return [
2,
'破坏性变更必须以 "BREAKING CHANGE:" 开头,并详细说明变更内容、影响范围、适配方案。\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md',
];
}
if (
!content.includes('影响范围') &&
!content.includes('适配方案') &&
!content.includes('适配指南')
) {
return [
1,
'破坏性变更建议补充影响范围和适配方案/适配指南。\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md',
];
}
}
return [true];
},
'rnoh-stability-ref-warning': ({ subject, body }) => {
const stabilityKeywords = [
'crash',
'freeze',
'frozen',
'hang',
'leak',
'OOM',
'oom',
'ANR',
'anr',
'memory',
'死机',
'冻屏',
'崩溃',
'闪退',
'内存泄漏',
'内存异常',
];
const content = (subject || '') + '\n' + (body || '');
const matched = stabilityKeywords.find((kw) =>
content.toLowerCase().includes(kw.toLowerCase()),
);
if (matched) {
return [
false,
`commit 内容包含稳定性关键词 "${matched}",建议在 body 中引用历史稳定性修复文档作为参考。\n参考: docs/zh-cn/05-运维/稳定性历史修复/`,
];
}
return [true];
},
'rnoh-subject-english': ({ subject }) => {
if (!subject) {
return [true];
}
const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf]/;
if (chineseRegex.test(subject)) {
return [
false,
'subject 必须使用英文,不允许中文。\n示例: feat(View): add pull-to-refresh support\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第1节',
];
}
return [true];
},
'rnoh-impact-required': ({ type, body }) => {
const impactRequiredTypes = [
'feat',
'fix',
'refactor',
'perf',
'security',
'deprecated',
'removed',
];
if (!impactRequiredTypes.includes(type) || !body) {
return [true];
}
const hasImpact =
body.includes('影响范围') ||
body.includes('影响') ||
body.toLowerCase().includes('impact') ||
body.toLowerCase().includes('affects') ||
body.toLowerCase().includes('affected') ||
body.toLowerCase().includes('scope of impact') ||
/\bscope\b/i.test(body);
if (!hasImpact) {
return [
false,
`${type} 类型commit建议在body中说明影响范围(如:影响哪些模块/功能/API/版本)。\n示例: 影响范围: 所有使用ScrollView的页面\n参考: docs/zh-cn/06-社区/社区规范/社区commit和pr规范.md - 第2节`,
];
}
return [true];
},
},
},
],
};