chore: 安全升级并完善 CI/测试
- 升级 js-yaml 修复生产依赖漏洞 - 新增 CI:lint/test/build - 增加书签处理单测与可测性导出"- 生成器补充 config/user 缺失提示 - 增加 lint/format/check 脚本与 Prettier 配置 - 统一行尾策略并支持书签确定性输出"
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
15
.gitattributes
vendored
15
.gitattributes
vendored
@@ -1,2 +1,15 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
|
||||
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
15
.github/workflows/deploy.yml
vendored
15
.github/workflows/deploy.yml
vendored
@@ -50,6 +50,8 @@ jobs:
|
||||
|
||||
- name: Process bookmark files
|
||||
if: steps.check_bookmark_files.outputs.found == 'true'
|
||||
env:
|
||||
MENAV_BOOKMARKS_DETERMINISTIC: '1'
|
||||
run: |
|
||||
echo "Processing bookmark files..."
|
||||
node src/bookmark-processor.js
|
||||
@@ -98,7 +100,16 @@ jobs:
|
||||
git commit -m "Add bookmarks configuration from imported bookmarks"
|
||||
fi
|
||||
|
||||
# Also check for navigation file changes
|
||||
# Also check for site.yml changes(导航已合并到 site.yml)
|
||||
if [ -f config/user/site.yml ]; then
|
||||
if ! git diff --quiet config/user/site.yml; then
|
||||
echo "config/user/site.yml has changes, committing..."
|
||||
git add config/user/site.yml
|
||||
git commit -m "Update site configuration for bookmarks"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also check for legacy navigation file changes
|
||||
if [ -f config/user/navigation.yml ]; then
|
||||
if ! git diff --quiet config/user/navigation.yml; then
|
||||
echo "config/user/navigation.yml has changes, committing..."
|
||||
@@ -194,4 +205,4 @@ jobs:
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
22
README.md
22
README.md
@@ -265,6 +265,10 @@ npm install
|
||||
```bash
|
||||
npm run import-bookmarks
|
||||
```
|
||||
- 若希望生成结果保持确定性(便于版本管理,减少时间戳导致的无意义 diff):
|
||||
```bash
|
||||
MENAV_BOOKMARKS_DETERMINISTIC=1 npm run import-bookmarks
|
||||
```
|
||||
- 系统会自动将书签转换为配置文件保存到`config/user/pages/bookmarks.yml`
|
||||
- **注意**:`npm run dev`命令不会自动处理书签文件,必须先手动运行上述命令
|
||||
|
||||
@@ -281,6 +285,19 @@ npm run build
|
||||
|
||||
构建后的文件位于`dist`目录
|
||||
|
||||
6. 提交前检查(推荐)
|
||||
|
||||
```bash
|
||||
# 一键检查(语法检查 + 单元测试 + 构建)
|
||||
npm run check
|
||||
```
|
||||
|
||||
(可选)格式化代码:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 部署方式
|
||||
@@ -413,7 +430,8 @@ MeNav使用模块化配置方式,将配置分散到多个文件中,更易于
|
||||
* **创建配置目录**:
|
||||
- 在`config/user/`目录下创建您的自定义配置文件
|
||||
- 可以参考项目结构中的`config/_default/`目录结构
|
||||
- 至少需要创建`site.yml`
|
||||
- 至少需要创建`site.yml`(缺失时构建会直接报错退出,避免生成空白站点)
|
||||
- 首次使用建议先完整复制 `config/_default/` 到 `config/user/`,再按需修改(因为配置采用“完全替换”策略,不会从默认配置补齐缺失项)
|
||||
|
||||
### 配置详解
|
||||
|
||||
@@ -745,6 +763,8 @@ MeNav支持从浏览器导入书签,快速批量添加网站链接,无需手
|
||||
|
||||
</details>
|
||||
|
||||
> 💡 *版本管理建议*:若你希望每次导入书签生成的 `bookmarks.yml` 内容保持确定性(避免因时间戳导致的无意义 diff),可在运行导入时设置环境变量:`MENAV_BOOKMARKS_DETERMINISTIC=1`。
|
||||
|
||||
> 生成的配置可在`config/user/pages/bookmarks.yml`中查看和编辑
|
||||
|
||||
## 常见问题
|
||||
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "menav",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "menav",
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
"version": "1.3.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1",
|
||||
"ansi-styles": "^6.2.1",
|
||||
@@ -15,11 +15,12 @@
|
||||
"color-name": "^2.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"has-flag": "^5.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"mime-db": "^1.52.0",
|
||||
"supports-color": "^9.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.4.2",
|
||||
"serve": "^14.2.1"
|
||||
}
|
||||
},
|
||||
@@ -646,9 +647,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -821,6 +822,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
10
package.json
10
package.json
@@ -9,7 +9,12 @@
|
||||
"dev": "node src/generator.js && serve dist -l 5173",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"build": "npm run clean && npm run generate",
|
||||
"import-bookmarks": "node src/bookmark-processor.js"
|
||||
"import-bookmarks": "node src/bookmark-processor.js",
|
||||
"test": "node --test test/*.js",
|
||||
"lint": "node --check \"src/generator.js\" && node --check \"src/bookmark-processor.js\" && node --check \"src/script.js\"",
|
||||
"format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"",
|
||||
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"",
|
||||
"check": "npm run lint && npm test && npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"navigation",
|
||||
@@ -19,7 +24,7 @@
|
||||
"author": "Your Name",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"ansi-regex": "^6.0.1",
|
||||
"ansi-styles": "^6.2.1",
|
||||
@@ -30,6 +35,7 @@
|
||||
"mime-db": "^1.52.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.4.2",
|
||||
"serve": "^14.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,11 +714,15 @@ function generateBookmarksYaml(bookmarks) {
|
||||
quotingType: '"'
|
||||
});
|
||||
|
||||
// 添加注释
|
||||
const yamlWithComment =
|
||||
// 添加注释(可选确定性输出,方便版本管理)
|
||||
const deterministic = process.env.MENAV_BOOKMARKS_DETERMINISTIC === '1';
|
||||
const timestampLine = deterministic
|
||||
? ''
|
||||
: `# 由bookmark-processor.js生成于 ${new Date().toISOString()}\n`;
|
||||
|
||||
const yamlWithComment =
|
||||
`# 自动生成的书签配置文件
|
||||
# 由bookmark-processor.js生成于 ${new Date().toISOString()}
|
||||
# 若要更新,请将新的书签HTML文件放入bookmarks/目录
|
||||
${timestampLine}# 若要更新,请将新的书签HTML文件放入bookmarks/目录
|
||||
# 此文件使用模块化配置格式,位于config/user/pages/目录下
|
||||
|
||||
${yamlString}`;
|
||||
@@ -914,4 +918,13 @@ if (require.main === module) {
|
||||
console.error('Unhandled error in bookmark processing:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureUserConfigInitialized,
|
||||
ensureUserSiteYmlExists,
|
||||
upsertBookmarksNavInSiteYml,
|
||||
parseBookmarks,
|
||||
generateBookmarksYaml,
|
||||
updateNavigationWithBookmarks,
|
||||
};
|
||||
|
||||
@@ -519,6 +519,19 @@ function loadConfig() {
|
||||
|
||||
// 根据优先级顺序选择最高优先级的配置
|
||||
if (hasUserModularConfig) {
|
||||
// 配置采用“完全替换”策略:一旦存在 config/user/,将不会回退到 config/_default/
|
||||
if (!fs.existsSync('config/user/site.yml')) {
|
||||
console.error('[ERROR] 检测到 config/user/ 目录,但缺少 config/user/site.yml。');
|
||||
console.error('[ERROR] 由于配置采用“完全替换”策略,系统不会从 config/_default/ 补齐缺失配置。');
|
||||
console.error('[ERROR] 解决方法:先完整复制 config/_default/ 到 config/user/,再按需修改。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync('config/user/pages')) {
|
||||
console.warn('[WARN] 检测到 config/user/ 目录,但缺少 config/user/pages/。部分页面内容可能为空。');
|
||||
console.warn('[WARN] 建议:复制 config/_default/pages/ 到 config/user/pages/,再按需修改。');
|
||||
}
|
||||
|
||||
// 1. 最高优先级: config/user/ 目录
|
||||
config = loadModularConfig('config/user');
|
||||
} else if (hasDefaultModularConfig) {
|
||||
|
||||
125
test/bookmark-processor.node-test.js
Normal file
125
test/bookmark-processor.node-test.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const {
|
||||
ensureUserConfigInitialized,
|
||||
ensureUserSiteYmlExists,
|
||||
upsertBookmarksNavInSiteYml,
|
||||
parseBookmarks,
|
||||
generateBookmarksYaml,
|
||||
} = require('../src/bookmark-processor.js');
|
||||
|
||||
function stripYamlComments(yamlText) {
|
||||
return yamlText
|
||||
.split(/\r?\n/)
|
||||
.filter(line => !/^\s*#/.test(line))
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
test('parseBookmarks:解析书签栏、根目录书签与图标映射', () => {
|
||||
const html = `
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><H3 PERSONAL_TOOLBAR_FOLDER="true">书签栏</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://github.com/">GitHub</A>
|
||||
<DT><H3>工具</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://www.google.com/">Google</A>
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
`;
|
||||
|
||||
const bookmarks = parseBookmarks(html);
|
||||
assert.ok(bookmarks);
|
||||
assert.ok(Array.isArray(bookmarks.categories));
|
||||
assert.ok(bookmarks.categories.length >= 2);
|
||||
|
||||
// 根目录书签应该插入到首位
|
||||
assert.equal(bookmarks.categories[0].name, '根目录书签');
|
||||
assert.ok(Array.isArray(bookmarks.categories[0].sites));
|
||||
assert.equal(bookmarks.categories[0].sites[0].name, 'GitHub');
|
||||
assert.equal(bookmarks.categories[0].sites[0].icon, 'fab fa-github');
|
||||
|
||||
const tools = bookmarks.categories.find(c => c.name === '工具');
|
||||
assert.ok(tools, '应解析出“工具”分类');
|
||||
assert.ok(Array.isArray(tools.sites));
|
||||
assert.equal(tools.sites[0].name, 'Google');
|
||||
});
|
||||
|
||||
test('generateBookmarksYaml:生成 YAML 且可被解析', () => {
|
||||
const bookmarks = {
|
||||
categories: [
|
||||
{
|
||||
name: '示例分类',
|
||||
icon: 'fas fa-folder',
|
||||
sites: [{ name: 'Example', url: 'https://example.com', icon: 'fas fa-link', description: '' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const yamlText = generateBookmarksYaml(bookmarks);
|
||||
assert.ok(typeof yamlText === 'string');
|
||||
assert.ok(yamlText.includes('# 自动生成的书签配置文件'));
|
||||
assert.ok(yamlText.includes('categories:'));
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const parsed = yaml.load(stripYamlComments(yamlText));
|
||||
assert.equal(parsed.title, '我的书签');
|
||||
assert.ok(Array.isArray(parsed.categories));
|
||||
assert.equal(parsed.categories[0].name, '示例分类');
|
||||
});
|
||||
|
||||
test('upsertBookmarksNavInSiteYml:无 navigation 时追加并幂等', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'menav-test-'));
|
||||
const filePath = path.join(tmp, 'site.yml');
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
`title: Test Site\n`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const r1 = upsertBookmarksNavInSiteYml(filePath);
|
||||
assert.equal(r1.updated, true);
|
||||
|
||||
const updated1 = fs.readFileSync(filePath, 'utf8');
|
||||
assert.ok(updated1.includes('navigation:'));
|
||||
assert.ok(updated1.includes('- name: 书签'));
|
||||
assert.ok(updated1.includes('id: bookmarks'));
|
||||
|
||||
const r2 = upsertBookmarksNavInSiteYml(filePath);
|
||||
assert.equal(r2.updated, false);
|
||||
assert.equal(r2.reason, 'already_present');
|
||||
});
|
||||
|
||||
test('ensureUserConfigInitialized/ensureUserSiteYmlExists:可在空目录初始化用户配置', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'menav-test-'));
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(tmp);
|
||||
|
||||
try {
|
||||
fs.mkdirSync('config/_default/pages', { recursive: true });
|
||||
fs.writeFileSync('config/_default/site.yml', 'title: Default\n', 'utf8');
|
||||
fs.writeFileSync('config/_default/pages/home.yml', 'categories: []\n', 'utf8');
|
||||
|
||||
const init = ensureUserConfigInitialized();
|
||||
assert.equal(init.initialized, true);
|
||||
assert.ok(fs.existsSync('config/user/site.yml'));
|
||||
assert.ok(fs.existsSync('config/user/pages/home.yml'));
|
||||
|
||||
// 若 site.yml 已存在,应直接返回 true
|
||||
assert.equal(ensureUserSiteYmlExists(), true);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user