diff --git a/.github/workflows/pages-ci.yml b/.github/workflows/pages-ci.yml index 4e25ba2e..1e4ffac3 100644 --- a/.github/workflows/pages-ci.yml +++ b/.github/workflows/pages-ci.yml @@ -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 diff --git a/pages/eslint.config.js b/pages/eslint.config.js new file mode 100644 index 00000000..4fd7ba2f --- /dev/null +++ b/pages/eslint.config.js @@ -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"] } +); diff --git a/pages/package.json b/pages/package.json index 6a058129..c9ffceb4 100644 --- a/pages/package.json +++ b/pages/package.json @@ -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" diff --git a/pages/postcss.config.js b/pages/postcss.config.cjs similarity index 52% rename from pages/postcss.config.js rename to pages/postcss.config.cjs index 5cbc2c7d..d9f53532 100644 --- a/pages/postcss.config.js +++ b/pages/postcss.config.cjs @@ -1,6 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, + tailwindcss: { config: './tailwind.config.cjs' }, autoprefixer: {} } }; diff --git a/pages/src/content/blog/index.ts b/pages/src/content/blog/index.ts index 41786035..6512cf08 100644 --- a/pages/src/content/blog/index.ts +++ b/pages/src/content/blog/index.ts @@ -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 { diff --git a/pages/src/content/docs/index.ts b/pages/src/content/docs/index.ts index 676c549a..a54bfdc8 100644 --- a/pages/src/content/docs/index.ts +++ b/pages/src/content/docs/index.ts @@ -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); diff --git a/pages/src/utils/extractHeadings.ts b/pages/src/utils/extractHeadings.ts index 567a2210..4b745708 100644 --- a/pages/src/utils/extractHeadings.ts +++ b/pages/src/utils/extractHeadings.ts @@ -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 }); diff --git a/pages/src/utils/headingId.ts b/pages/src/utils/headingId.ts index 779ffd7e..c28d601b 100644 --- a/pages/src/utils/headingId.ts +++ b/pages/src/utils/headingId.ts @@ -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, ''); } diff --git a/pages/tailwind.config.js b/pages/tailwind.config.cjs similarity index 100% rename from pages/tailwind.config.js rename to pages/tailwind.config.cjs diff --git a/pages/webpack.config.js b/pages/webpack.config.cjs similarity index 100% rename from pages/webpack.config.js rename to pages/webpack.config.cjs