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:
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }],
|
||||
],
|
||||
};
|
||||
@@ -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%',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user