refactor: reorganize project structure and improve code quality

- Move CI/CD configs to config/ci/ directory
- Reorganize scripts into categorized directories (deployment, monitoring, testing, utils)
- Consolidate documentation into docs/ directory with proper structure
- Update linting and testing configurations
- Remove obsolete test reports and performance summaries
- Add new documentation for code quality tools and contact form security
- Improve project organization and maintainability
- Fix lint-staged config to only lint JS/TS files
- Disable react/react-in-jsx-scope rule for Next.js compatibility
- Ignore scripts and test config directories in ESLint
This commit is contained in:
张翔
2026-03-24 13:38:58 +08:00
parent c06ac08510
commit 498bb3a3c8
62 changed files with 5473 additions and 6498 deletions
+28
View File
@@ -0,0 +1,28 @@
when:
branch: [main, develop]
event: [push, pull_request]
steps:
lint:
image: node:18-alpine
commands:
- npm ci
- npm run lint
- npm run type-check
test:
image: node:18-alpine
commands:
- npm ci
- npm run db:push
- npm run test:unit -- --coverage --coverageReporters=text --coverageReporters=lcov
- npx playwright install --with-deps
- npm run test:e2e
build:
image: node:18-alpine
commands:
- npm ci
- npm run build
when:
status: [success]
+12
View File
@@ -0,0 +1,12 @@
when:
branch: [main]
event: [push]
steps:
deploy:
image: node:18-alpine
commands:
- npm ci
- npm run build
- echo "Deploying to production..."
secrets: [deploy_key]
+71
View File
@@ -0,0 +1,71 @@
when:
event: [pull_request]
branch: [main, develop]
steps:
install-dependencies:
image: node:18-alpine
commands:
- npm ci
lint:
image: node:18-alpine
commands:
- echo "=== Running ESLint ==="
- npm run lint
- echo "✅ ESLint check passed"
type-check:
image: node:18-alpine
commands:
- echo "=== Running TypeScript type check ==="
- npm run type-check
- echo "✅ TypeScript type check passed"
unit-tests:
image: node:18-alpine
commands:
- echo "=== Running unit tests with coverage ==="
- npm run test:unit -- --coverage --coverageReporters=json
- |
COVERAGE=$(cat coverage/coverage-summary.json | grep -o '"lines":{"pct":[0-9.]*' | grep -o '[0-9.]*$')
echo "Current coverage: $COVERAGE%"
if [ $(echo "$COVERAGE < 42" | bc -l) -eq 1 ]; then
echo "❌ Coverage $COVERAGE% is below threshold 42%"
exit 1
fi
echo "✅ Coverage $COVERAGE% meets threshold 42%"
e2e-tests:
image: node:18-alpine
commands:
- echo "=== Running E2E tests ==="
- npx playwright install --with-deps
- npm run test:e2e
- echo "✅ E2E tests passed"
security-check:
image: node:18-alpine
commands:
- echo "=== Running security audit ==="
- npm audit --audit-level=moderate
- echo "✅ Security audit passed"
performance-check:
image: node:18-alpine
commands:
- echo "=== Running performance checks ==="
- npm run audit:performance
- echo "✅ Performance audit passed"
quality-summary:
image: node:18-alpine
commands:
- echo "=== Quality Gate Summary ==="
- echo "✅ All quality checks passed"
- echo " - ESLint: PASSED"
- echo " - TypeScript: PASSED"
- echo " - Unit Tests: PASSED (Coverage ≥ 42%)"
- echo " - E2E Tests: PASSED"
- echo " - Security: PASSED"
- echo " - Performance: PASSED"
+50
View File
@@ -0,0 +1,50 @@
when:
event:
- push
- pull_request
pipeline:
test-tier-fast:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: fast
CI: true
commands:
- cd e2e
- npm ci
- npx playwright install --with-deps
- npm run test:tier:fast
when:
branch:
- main
- develop
- feat-dynamic
test-tier-standard:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: standard
CI: true
commands:
- cd e2e
- npm ci
- npx playwright install --with-deps
- npm run test:tier:standard
when:
branch:
- main
- develop
test-tier-deep:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: deep
CI: true
commands:
- cd e2e
- npm ci
- npx playwright install --with-deps
- npm run test:tier:deep
when:
branch:
- main
+102
View File
@@ -0,0 +1,102 @@
when:
event:
- push
- pull_request
- tag
pipeline:
setup:
image: node:20-alpine
commands:
- node -v
- npm -v
- npm ci
- cd e2e && npm ci
test-tier-fast:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: fast
CI: true
commands:
- cd e2e
- npx playwright install --with-deps
- npm run test:tier:fast
depends_on:
- setup
test-tier-standard:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: standard
CI: true
commands:
- cd e2e
- npx playwright install --with-deps
- npm run test:tier:standard
depends_on:
- test-tier-fast
when:
status:
- success
test-tier-deep:
image: mcr.microsoft.com/playwright:v1.42.0-jammy
environment:
TEST_TIER: deep
CI: true
commands:
- cd e2e
- npx playwright install --with-deps
- npm run test:tier:deep
depends_on:
- test-tier-standard
when:
status:
- success
generate-report:
image: node:20-alpine
commands:
- cd e2e
- node scripts/generate-report.js
depends_on:
- test-tier-fast
- test-tier-standard
- test-tier-deep
upload-artifacts:
image: plugins/s3
settings:
bucket: test-reports
source: e2e/test-results/**
target: /${CI_REPO}/${CI_BUILD_NUMBER}/
path_style: true
depends_on:
- generate-report
when:
status:
- success
- failure
notify:
image: plugins/webhook
settings:
urls:
from_secret: webhook_url
content_type: application/json
template: |
{
"repo": "{{ repo.name }}",
"build": "{{ build.number }}",
"status": "{{ build.status }}",
"message": "{{ build.message }}",
"author": "{{ commit.author }}",
"link": "{{ build.link }}"
}
depends_on:
- upload-artifacts
when:
status:
- success
- failure
+59
View File
@@ -0,0 +1,59 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"settings": {
"react": {
"version": "detect"
}
},
"ignorePatterns": [
".next/**",
"out/**",
"build/**",
"dist/**",
"node_modules/**",
"coverage/**",
"scripts/**",
"config/test/**"
],
"globals": {
"jest": "readonly"
},
"rules": {
"react/react-in-jsx-scope": "off",
"react/no-unescaped-entities": "error",
"react/jsx-no-target-blank": "error",
"react/self-closing-comp": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"@typescript-eslint/no-explicit-any": "warn",
"no-console": ["warn", { "allow": ["warn", "error"] }],
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"],
"no-throw-literal": "error",
"prefer-promise-reject-errors": "error"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
],
};
+36
View File
@@ -0,0 +1,36 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.{ts,tsx}', '**/*.test.{ts,tsx}'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/__tests__/**',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
coverageReporters: ['text', 'lcov', 'html', 'json'],
coverageDirectory: 'coverage',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(nanoid|next-auth|@auth)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testTimeout: 10000,
verbose: true,
maxWorkers: '50%',
};
+194
View File
@@ -0,0 +1,194 @@
require('@testing-library/jest-dom');
const { TextEncoder, TextDecoder } = require('util');
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
jest.mock('next-auth', () => {
return {
__esModule: true,
default: jest.fn(() => ({
handlers: {
authOptions: {
providers: [],
callbacks: {},
pages: {},
session: {},
},
},
signIn: jest.fn(),
signOut: jest.fn(),
auth: jest.fn(),
})),
getServerSession: jest.fn(),
};
});
jest.mock('next-auth/providers/credentials', () =>
jest.fn(() => ({
name: '邮箱密码',
credentials: {
email: { label: '邮箱', type: 'email' },
password: { label: '密码', type: 'password' },
},
authorize: jest.fn(),
}))
);
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'test-id-123'),
}));
jest.mock('next/dynamic', () => ({
__esModule: true,
default: (importFn, options) => {
const MockComponent = (props) => null;
MockComponent.displayName = 'DynamicComponent';
MockComponent.preload = () => Promise.resolve();
return MockComponent;
},
}));
jest.mock('next/server', () => ({
NextRequest: class MockNextRequest {
constructor(input, init = {}) {
this.url = typeof input === 'string' ? input : input.url;
this.method = init.method || 'GET';
this.headers = new Headers(init.headers);
this.body = init.body;
}
async json() {
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
}
},
NextResponse: {
json: (body, init = {}) => ({
status: init.status || 200,
json: async () => body,
}),
},
}));
global.console = {
...console,
error: jest.fn(),
warn: jest.fn(),
log: jest.fn(),
};
class MockIntersectionObserver {
constructor(callback, options = {}) {
this.callback = callback;
this.options = options;
this.elements = new Set();
this.observationEntries = [];
}
observe(element) {
this.elements.add(element);
const entry = {
isIntersecting: true,
target: element,
boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {},
intersectionRatio: 1,
intersectionRect: {},
rootBounds: {},
time: Date.now(),
};
this.observationEntries.push(entry);
this.callback(this.observationEntries, this);
}
unobserve(element) {
this.elements.delete(element);
this.observationEntries = this.observationEntries.filter(
entry => entry.target !== element
);
}
disconnect() {
this.elements.clear();
this.observationEntries = [];
}
takeRecords() {
return this.observationEntries;
}
}
global.IntersectionObserver = MockIntersectionObserver;
global.IntersectionObserverEntry = class IntersectionObserverEntry {
constructor() {
this.isIntersecting = true;
this.target = {};
this.boundingClientRect = {};
this.intersectionRatio = 1;
this.intersectionRect = {};
this.rootBounds = {};
this.time = Date.now();
}
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
global.Request = class Request {
constructor(input, init = {}) {
this.url = typeof input === 'string' ? input : input.url;
this.method = init.method || 'GET';
this.headers = new Headers(init.headers);
this.body = init.body;
}
async json() {
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
}
};
global.Headers = class Headers {
constructor(init = {}) {
this.headers = {};
if (init) {
Object.entries(init).forEach(([key, value]) => {
this.headers[key.toLowerCase()] = value;
});
}
}
get(name) {
return this.headers[name.toLowerCase()];
}
set(name, value) {
this.headers[name.toLowerCase()] = value;
}
};
global.Response = class Response {
constructor(body, init = {}) {
this.body = body;
this.status = init.status || 200;
this.statusText = init.statusText || 'OK';
this.headers = new Headers(init.headers);
}
async json() {
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
}
async text() {
return String(this.body);
}
};
+57
View File
@@ -0,0 +1,57 @@
ci:
collect:
numberOfRuns: 3
startServerCommand: npm run start
startServerReadyPattern: 'Local:'
url:
- http://localhost:3000/
- http://localhost:3000/about
- http://localhost:3000/services
- http://localhost:3000/products
- http://localhost:3000/cases
- http://localhost:3000/news
- http://localhost:3000/contact
settings:
preset: desktop
onlyCategories:
- performance
- accessibility
- best-practices
- seo
assert:
assertions:
categories:performance:
- error
- minScore: 0.9
categories:accessibility:
- error
- minScore: 0.9
categories:best-practices:
- error
- minScore: 0.9
categories:seo:
- error
- minScore: 0.9
first-contentful-paint:
- error
- maxNumericValue: 2000
largest-contentful-paint:
- error
- maxNumericValue: 3000
cumulative-layout-shift:
- error
- maxNumericValue: 0.1
total-blocking-time:
- error
- maxNumericValue: 300
speed-index:
- error
- maxNumericValue: 3000
upload:
target: temporary-public-storage
settings:
output: html
outputPath: lighthouse-reports