ci(pages): add ESLint and bundle size check to CI pipeline (#558)

- Add ESLint with typescript-eslint and react-hooks plugin
- Add size-limit to monitor initial bundle size (150 kB threshold)
- Rename .js configs to .cjs for ESM compatibility ("type": "module")
- Fix no-useless-escape and no-useless-assignment lint errors
- Update pages-ci.yml with lint and size check steps
This commit is contained in:
kite
2026-07-28 20:19:37 +08:00
committed by GitHub
parent f93d1a491b
commit 8022d4a5a3
10 changed files with 52 additions and 7 deletions
+8
View File
@@ -29,6 +29,10 @@ jobs:
working-directory: pages
run: npm install
- name: Lint
working-directory: pages
run: npm run lint
- name: Typecheck
working-directory: pages
run: npm run typecheck
@@ -36,3 +40,7 @@ jobs:
- name: Build
working-directory: pages
run: npm run build
- name: Check bundle size
working-directory: pages
run: npm run size
+22
View File
@@ -0,0 +1,22 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
plugins: { "react-hooks": reactHooks },
rules: {
...reactHooks.configs.recommended.rules,
"react-hooks/set-state-in-effect": "off",
"no-empty": ["error", { allowEmptyCatch: true }],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
{ ignores: ["dist/", "node_modules/", "*.cjs"] }
);
+16 -1
View File
@@ -2,10 +2,13 @@
"name": "open-code-review-landing",
"version": "1.0.0",
"description": "Landing page for Open Code Review",
"type": "module",
"scripts": {
"dev": "webpack serve",
"build": "NODE_ENV=production webpack --mode production",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"size": "size-limit"
},
"dependencies": {
"@agentscope-ai/icons": "^1.0.68",
@@ -20,11 +23,19 @@
"overrides": {
"fast-uri": "^3.1.2"
},
"size-limit": [
{
"path": "dist/*.bundle.js",
"limit": "150 kB"
}
],
"devDependencies": {
"@babel/core": "^7.23.5",
"@babel/preset-env": "^7.29.5",
"@babel/preset-react": "^7.23.5",
"@babel/preset-typescript": "^7.23.3",
"@eslint/js": "^10.0.1",
"@size-limit/file": "^13.0.1",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
@@ -33,12 +44,16 @@
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^6.8.1",
"eslint": "^10.8.0",
"eslint-plugin-react-hooks": "^7.1.1",
"html-webpack-plugin": "^5.5.3",
"postcss": "^8.5.15",
"postcss-loader": "^7.3.3",
"size-limit": "^13.0.1",
"style-loader": "^3.3.3",
"tailwindcss": "^3.3.5",
"typescript": "^5.3.2",
"typescript-eslint": "^8.65.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.2.4"
@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
tailwindcss: { config: './tailwind.config.cjs' },
autoprefixer: {}
}
};
+2 -2
View File
@@ -113,7 +113,7 @@ export function searchBlog(query: string, language: string): { slug: BlogSlug; t
const lowerTitle = meta.title.toLowerCase();
const lowerSummary = (meta.summary || '').toLowerCase();
let snippet = '';
let snippet: string;
const contentIdx = lowerContent.indexOf(lowerQuery);
if (lowerTitle.includes(lowerQuery)) {
snippet = meta.title;
@@ -122,7 +122,7 @@ export function searchBlog(query: string, language: string): { slug: BlogSlug; t
} else if (contentIdx !== -1) {
const start = Math.max(0, contentIdx - 30);
const end = Math.min(content.length, contentIdx + query.length + 60);
snippet = content.slice(start, end).replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ').trim();
snippet = content.slice(start, end).replace(/[#*_`[\]()]/g, '').replace(/\n/g, ' ').trim();
if (start > 0) snippet = '...' + snippet;
if (end < content.length) snippet = snippet + '...';
} else {
+1 -1
View File
@@ -198,7 +198,7 @@ export function searchDocs(query: string, language: string): { slug: DocSlug; ti
// Extract snippet around match
const start = Math.max(0, idx - 30);
const end = Math.min(content.length, idx + query.length + 60);
let snippet = content.slice(start, end).replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ').trim();
let snippet = content.slice(start, end).replace(/[#*_`[\]()]/g, '').replace(/\n/g, ' ').trim();
if (start > 0) snippet = '...' + snippet;
if (end < content.length) snippet = snippet + '...';
const title = getDocTitle(slug, language);
+1 -1
View File
@@ -17,7 +17,7 @@ export function extractHeadings(markdown: string): { id: string; text: string; l
// Strip markdown link syntax [text](url) → text, then strip other formatting
const text = match[2]
.replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
.replace(/[`*_\[\]()]/g, '')
.replace(/[`*_[\]()]/g, '')
.trim();
const id = generateHeadingId(text);
headings.push({ id, text, level });
+1 -1
View File
@@ -10,7 +10,7 @@ export function generateHeadingId(text: string): string {
// bypassed by nested tags). Keeps text content, decodes HTML entities so the
// TOC side (raw markdown) and renderer side (marked HTML output) agree.
const plain = DOMPurify.sanitize(text, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] })
.replace(/[`*_\[\]()]/g, '')
.replace(/[`*_[\]()]/g, '')
.trim();
return plain.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-').replace(/^-|-$/g, '');
}