diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e614118..dbc25ad 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(npm test:*)", "Bash(npm run build:*)", "Bash(npx vite build:*)", - "Bash(npx vite:*)" + "Bash(npx vite:*)", + "Bash(npx tsc:*)" ], "deny": [] } diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..cc9830c --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,79 @@ +# Security Policy + +## Overview + +This is a web application deployed continuously from the main branch. We take security seriously and address vulnerabilities as soon as they are discovered. + +## Reporting a Vulnerability + +We take the security of our software seriously. If you believe you have found a security vulnerability, please report it to us as described below. + +### Please do NOT: +- Open a public GitHub issue +- Post on social media +- Disclose the vulnerability publicly before we've had a chance to fix it + +### Please DO: +- Email us at: [Create a security advisory](https://github.com/ntindle/gridfinity-space-optimizer/security/advisories/new) +- Provide detailed steps to reproduce the issue +- Include the impact of the issue +- Suggest a fix if you have one + +### What to expect: +- **Response Time**: We'll acknowledge receipt within 48 days +- **Updates**: We'll provide updates at least every 72 days +- **Fix Timeline**: We aim to release a fix within 7-14 days depending on complexity +- **Disclosure**: We'll coordinate public disclosure with you + +## Security Measures + +### Code Security +- All code is scanned using CodeQL and multiple SAST tools +- Dependencies are regularly audited for vulnerabilities +- Automated security checks on all pull requests + +### Dependency Management +- Weekly automated dependency audits +- Snyk monitoring for real-time vulnerability detection +- Automated PR creation for security updates + +### Build Security +- CI/CD pipelines run in isolated environments +- No secrets stored in code +- Environment variables used for sensitive configuration + +## Security Tools in Use + +- **CodeQL**: Semantic code analysis +- **Semgrep**: Static analysis security scanner +- **Snyk**: Dependency and container vulnerability scanning +- **Trivy**: Comprehensive vulnerability scanner +- **Gitleaks**: Secret detection in git repos +- **TruffleHog**: Credential verification scanner +- **npm audit**: Node.js dependency auditing + +## Best Practices for Contributors + +1. **Never commit secrets**: API keys, passwords, tokens +2. **Validate input**: Always validate and sanitize user input +3. **Use parameterized queries**: Prevent injection attacks +4. **Implement proper authentication**: Use secure session management +5. **Keep dependencies updated**: Regularly update packages +6. **Follow secure coding guidelines**: OWASP Top 10 + +## Automated Security Checks + +Every pull request undergoes: +- Static Application Security Testing (SAST) +- Dependency vulnerability scanning +- Secret detection scanning +- Code quality and security review +- License compliance checking + +## Contact + +For security concerns, please use GitHub's security advisory feature or contact the maintainers directly through secure channels. + +--- + +*This security policy is regularly reviewed and updated. Last update: Current* \ No newline at end of file diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..35246db --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,43 @@ +codecov: + require_ci_to_pass: true + notify: + wait_for_ci: true + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + target: 70% + threshold: 2% + patch: + default: + target: 80% + threshold: 5% + +parsers: + javascript: + enable_partials: yes + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: false + require_base: false + require_head: true + +ignore: + - "**/*.test.ts" + - "**/*.test.tsx" + - "**/test/**" + - "**/tests/**" + - "**/__tests__/**" + - "**/node_modules/**" + - "**/dist/**" + - "**/coverage/**" + - "vite.config.*" + - "tailwind.config.*" + - "postcss.config.*" \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..aa4d750 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,64 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 10 + reviewers: + - "ntindle" + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + ignore: + # Ignore major version updates for these packages + - dependency-name: "react" + update-types: ["version-update:semver-major"] + - dependency-name: "react-dom" + update-types: ["version-update:semver-major"] + groups: + # Group all development dependencies together + dev-dependencies: + patterns: + - "*" + dependency-type: "development" + # Group all production dependencies together + production-dependencies: + patterns: + - "*" + dependency-type: "production" + # Group ESLint and related packages + eslint: + patterns: + - "eslint*" + - "@typescript-eslint/*" + # Group testing packages + testing: + patterns: + - "vitest*" + - "@testing-library/*" + # Group Radix UI packages + radix-ui: + patterns: + - "@radix-ui/*" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + include: "scope" \ No newline at end of file diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml new file mode 100644 index 0000000..e0b9f8e --- /dev/null +++ b/.github/workflows/accessibility.yml @@ -0,0 +1,170 @@ +name: Accessibility Tests + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + axe-accessibility: + name: Axe Accessibility Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + npm install --save-dev @axe-core/cli puppeteer + + - name: Build application + run: npm run build + + - name: Serve application + run: | + npx serve -s dist -p 3000 & + sleep 5 + + - name: Run Axe accessibility tests + run: | + npx axe http://localhost:3000 \ + --dir ./axe-reports \ + --save \ + --timeout 30000 \ + --tags wcag2a,wcag2aa,wcag21a,wcag21aa \ + --show-errors + continue-on-error: true + + - name: Generate accessibility report + if: always() + run: | + echo "## ♿ Accessibility Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "./axe-reports/index.json" ]; then + # Parse the JSON report and create summary + echo "### Axe-core Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if there are violations + violations=$(cat ./axe-reports/index.json | grep -o '"violations":\[\]' || true) + if [ -n "$violations" ]; then + echo "✅ No accessibility violations found!" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Accessibility issues detected. Check the detailed report." >> $GITHUB_STEP_SUMMARY + fi + else + echo "❌ Accessibility test failed to generate report" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Standards Tested" >> $GITHUB_STEP_SUMMARY + echo "- WCAG 2.0 Level A" >> $GITHUB_STEP_SUMMARY + echo "- WCAG 2.0 Level AA" >> $GITHUB_STEP_SUMMARY + echo "- WCAG 2.1 Level A" >> $GITHUB_STEP_SUMMARY + echo "- WCAG 2.1 Level AA" >> $GITHUB_STEP_SUMMARY + + - name: Upload accessibility reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: accessibility-reports + path: axe-reports/ + retention-days: 30 + + pa11y-accessibility: + name: Pa11y Accessibility Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + npm install --save-dev pa11y pa11y-ci + + - name: Build application + run: npm run build + + - name: Create Pa11y config + run: | + cat > .pa11yci.json << 'EOF' + { + "defaults": { + "timeout": 30000, + "wait": 1000, + "standard": "WCAG2AA", + "runners": ["axe", "htmlcs"], + "chromeLaunchConfig": { + "args": ["--no-sandbox", "--disable-setuid-sandbox"] + } + }, + "urls": [ + { + "url": "http://localhost:3000", + "actions": [ + "wait for element #root to be visible" + ] + } + ] + } + EOF + + - name: Serve application + run: | + npx serve -s dist -p 3000 & + sleep 5 + + - name: Run Pa11y tests + run: npx pa11y-ci --config .pa11yci.json --json > pa11y-results.json + continue-on-error: true + + - name: Upload Pa11y results + if: always() + uses: actions/upload-artifact@v4 + with: + name: pa11y-results + path: pa11y-results.json + retention-days: 30 + + color-contrast: + name: Color Contrast Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check color contrast in CSS + run: | + echo "## 🎨 Color Contrast Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checking for potential color contrast issues..." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for common low-contrast color combinations + if grep -r "color:.*#[89abcdef]{3,6}" src/ --include="*.css" --include="*.tsx" --include="*.ts"; then + echo "⚠️ Found potentially low-contrast color values" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No obvious low-contrast issues detected" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "Note: Full contrast testing is performed by Axe and Pa11y" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dbc3ddd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,149 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test -- --run --reporter=verbose + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.node-version }} + path: | + coverage/ + test-results/ + retention-days: 7 + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Check TypeScript + run: npx tsc --noEmit + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build production + run: npm run build + + - name: Build development + run: npm run build:dev + + - name: Check build output + run: | + if [ ! -d "dist" ]; then + echo "Build failed: dist directory not found" + exit 1 + fi + echo "Build successful! Contents of dist:" + ls -la dist/ + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-${{ matrix.node-version }} + path: dist/ + retention-days: 7 + + size-check: + name: Bundle Size Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build and analyze bundle + run: | + npm run build + echo "## Bundle Size Report" >> $GITHUB_STEP_SUMMARY + echo "| File | Size | Gzipped |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|---------|" >> $GITHUB_STEP_SUMMARY + + for file in dist/assets/*.js; do + if [ -f "$file" ]; then + size=$(du -h "$file" | cut -f1) + gzipped=$(gzip -c "$file" | wc -c | numfmt --to=iec) + filename=$(basename "$file") + echo "| $filename | $size | $gzipped |" >> $GITHUB_STEP_SUMMARY + fi + done + + for file in dist/assets/*.css; do + if [ -f "$file" ]; then + size=$(du -h "$file" | cut -f1) + gzipped=$(gzip -c "$file" | wc -c | numfmt --to=iec) + filename=$(basename "$file") + echo "| $filename | $size | $gzipped |" >> $GITHUB_STEP_SUMMARY + fi + done \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..faf9458 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,103 @@ +name: Code Quality + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + quality: + name: Code Quality Checks + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci + + - name: TypeScript type checking + run: | + echo "🔍 Running TypeScript type check..." + npx tsc --noEmit + + - name: ESLint + run: | + echo "🧹 Running ESLint..." + npm run lint + + - name: Check for console.log statements + run: | + echo "🔍 Checking for console.log statements..." + if grep -r "console.log" src/ --exclude-dir=test --exclude-dir=tests --exclude="*.test.*" --exclude="*.spec.*" --exclude="captureActualOutputs.ts" | grep -v "^Binary file"; then + echo "❌ Found console.log statements in production code!" + exit 1 + else + echo "✅ No console.log statements found" + fi + + - name: Check import sorting + run: | + echo "📦 Checking import consistency..." + # Check for relative imports that should use @/ alias + if grep -r "from '\.\.\/" src/ --include="*.ts" --include="*.tsx"; then + echo "⚠️ Found relative imports that should use @/ alias" + else + echo "✅ All imports use correct aliases" + fi + + - name: Run tests with coverage + run: | + echo "🧪 Running tests with coverage..." + npm test -- --run --coverage + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + retention-days: 7 + + - name: Build check + run: | + echo "🏗️ Running build..." + npm run build + + # Check bundle size + echo "" + echo "📊 Bundle size analysis:" + du -sh dist/assets/*.js dist/assets/*.css | sort -rh + + - name: Check for TypeScript any usage + run: | + echo "🔍 Checking for 'any' type usage..." + count=$(grep -r ": any" src/ --include="*.ts" --include="*.tsx" --exclude="*.test.*" --exclude="*.d.ts" | wc -l) + if [ "$count" -gt 0 ]; then + echo "⚠️ Found $count uses of 'any' type" + grep -r ": any" src/ --include="*.ts" --include="*.tsx" --exclude="*.test.*" --exclude="*.d.ts" + else + echo "✅ No 'any' types found" + fi + + - name: Summary + if: always() + run: | + echo "## Code Quality Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ All quality checks completed" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..75d5de7 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,62 @@ +name: CodeQL + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + schedule: + # Run at 3 AM UTC every Tuesday + - cron: '0 3 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript', 'typescript'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + + - name: Generate Security Report + if: always() + run: | + echo "## Security Analysis Report 🔒" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Analysis for ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Analysis completed for language: ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "View detailed results in the Security tab of this repository." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml new file mode 100644 index 0000000..7311dbe --- /dev/null +++ b/.github/workflows/dependency-check.yml @@ -0,0 +1,68 @@ +name: Dependency Check + +on: + schedule: + # Run at 9 AM UTC every Monday + - cron: '0 9 * * 1' + workflow_dispatch: + push: + paths: + - 'package.json' + - 'package-lock.json' + +jobs: + security-audit: + name: Security Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run npm audit + id: audit + run: | + echo "## Security Audit Report 🔒" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if npm audit --audit-level=moderate 2>&1 | tee audit-output.txt; then + echo "✅ No vulnerabilities found" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Vulnerabilities detected:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat audit-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + - name: Check for outdated packages + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Outdated Packages 📦" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + npm outdated || true >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + deny-licenses: GPL-3.0, AGPL-3.0 \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..76f5205 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test -- --run + + - name: Build for production + run: npm run build + env: + NODE_ENV: production + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 0000000..1452123 --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,153 @@ +name: Performance Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lighthouse: + name: Lighthouse CI + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Install serve + run: npm install -g serve + + - name: Start server + run: | + serve -s dist -p 3000 & + sleep 5 + curl -f http://localhost:3000 || exit 1 + + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@v11 + with: + configPath: './.lighthouserc.json' + uploadArtifacts: true + temporaryPublicStorage: true + env: + LHCI_BUILD_CONTEXT__GITHUB_REPO_SLUG: ${{ github.repository }} + LHCI_BUILD_CONTEXT__GITHUB_COMMIT_SHA: ${{ github.sha }} + + - name: Generate Performance Report + if: always() + run: | + echo "## 🚀 Performance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Lighthouse scores:" >> $GITHUB_STEP_SUMMARY + echo "- Performance: Check artifacts for detailed scores" >> $GITHUB_STEP_SUMMARY + echo "- Accessibility: Check artifacts for detailed scores" >> $GITHUB_STEP_SUMMARY + echo "- Best Practices: Check artifacts for detailed scores" >> $GITHUB_STEP_SUMMARY + echo "- SEO: Check artifacts for detailed scores" >> $GITHUB_STEP_SUMMARY + + bundle-analysis: + name: Bundle Size Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build and analyze + run: | + npm run build + + # Create bundle analysis + echo "## 📦 Bundle Analysis" >> bundle-report.md + echo "" >> bundle-report.md + echo "### JavaScript Bundles" >> bundle-report.md + echo '```' >> bundle-report.md + find dist -name "*.js" -exec du -h {} \; | sort -rh >> bundle-report.md + echo '```' >> bundle-report.md + echo "" >> bundle-report.md + echo "### CSS Bundles" >> bundle-report.md + echo '```' >> bundle-report.md + find dist -name "*.css" -exec du -h {} \; | sort -rh >> bundle-report.md + echo '```' >> bundle-report.md + echo "" >> bundle-report.md + echo "### Total Size" >> bundle-report.md + echo '```' >> bundle-report.md + du -sh dist >> bundle-report.md + echo '```' >> bundle-report.md + + cat bundle-report.md >> $GITHUB_STEP_SUMMARY + + - name: Check bundle size limits + run: | + # Set size limits (in KB) + MAX_JS_SIZE=1500 + MAX_CSS_SIZE=100 + + # Check JS bundle size + JS_SIZE=$(find dist/assets -name "*.js" -exec du -k {} \; | sort -rn | head -1 | cut -f1) + if [ "$JS_SIZE" -gt "$MAX_JS_SIZE" ]; then + echo "❌ JS bundle exceeds limit: ${JS_SIZE}KB > ${MAX_JS_SIZE}KB" + exit 1 + else + echo "✅ JS bundle within limit: ${JS_SIZE}KB <= ${MAX_JS_SIZE}KB" + fi + + # Check CSS bundle size + CSS_SIZE=$(find dist/assets -name "*.css" -exec du -k {} \; | sort -rn | head -1 | cut -f1) + if [ "$CSS_SIZE" -gt "$MAX_CSS_SIZE" ]; then + echo "❌ CSS bundle exceeds limit: ${CSS_SIZE}KB > ${MAX_CSS_SIZE}KB" + exit 1 + else + echo "✅ CSS bundle within limit: ${CSS_SIZE}KB <= ${MAX_CSS_SIZE}KB" + fi + + - name: Upload bundle report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bundle-report + path: bundle-report.md + retention-days: 7 + + memory-leak-detection: + name: Memory Leak Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run memory leak tests + run: | + # Run tests with memory tracking + node --expose-gc --max-old-space-size=4096 node_modules/.bin/vitest --run --reporter=verbose --config vite.config.test.js \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..d29111f --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,171 @@ +name: PR Checks + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + quality-gates: + name: Quality Gates + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for better diff analysis + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run all checks + id: checks + run: | + echo "## PR Quality Report 📊" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # TypeScript Check + echo "### TypeScript Check 🔍" >> $GITHUB_STEP_SUMMARY + if npx tsc --noEmit; then + echo "✅ No TypeScript errors" >> $GITHUB_STEP_SUMMARY + echo "typescript=success" >> $GITHUB_OUTPUT + else + echo "❌ TypeScript errors found" >> $GITHUB_STEP_SUMMARY + echo "typescript=failure" >> $GITHUB_OUTPUT + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Lint Check + echo "### ESLint Check 🧹" >> $GITHUB_STEP_SUMMARY + if npm run lint 2>&1 | tee lint-output.txt; then + echo "✅ No linting errors" >> $GITHUB_STEP_SUMMARY + echo "lint=success" >> $GITHUB_OUTPUT + else + echo "❌ Linting errors found" >> $GITHUB_STEP_SUMMARY + cat lint-output.txt >> $GITHUB_STEP_SUMMARY + echo "lint=failure" >> $GITHUB_OUTPUT + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Test Check + echo "### Test Results 🧪" >> $GITHUB_STEP_SUMMARY + if npm test -- --run --reporter=verbose 2>&1 | tee test-output.txt; then + echo "✅ All tests passed" >> $GITHUB_STEP_SUMMARY + # Extract test summary + grep -E "Test Files|Tests" test-output.txt >> $GITHUB_STEP_SUMMARY || true + echo "tests=success" >> $GITHUB_OUTPUT + else + echo "❌ Tests failed" >> $GITHUB_STEP_SUMMARY + grep -E "Test Files|Tests|FAIL" test-output.txt >> $GITHUB_STEP_SUMMARY || true + echo "tests=failure" >> $GITHUB_OUTPUT + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Build Check + echo "### Build Check 🏗️" >> $GITHUB_STEP_SUMMARY + if npm run build 2>&1 | tee build-output.txt; then + echo "✅ Build successful" >> $GITHUB_STEP_SUMMARY + # Show bundle size info + grep -E "dist/|kB|gzip" build-output.txt >> $GITHUB_STEP_SUMMARY || true + echo "build=success" >> $GITHUB_OUTPUT + else + echo "❌ Build failed" >> $GITHUB_STEP_SUMMARY + echo "build=failure" >> $GITHUB_OUTPUT + fi + + - name: Comment PR + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read the summary + let summary = '## PR Quality Report 📊\n\n'; + + // Add check results with emojis + const checks = { + typescript: '${{ steps.checks.outputs.typescript }}' === 'success' ? '✅' : '❌', + lint: '${{ steps.checks.outputs.lint }}' === 'success' ? '✅' : '❌', + tests: '${{ steps.checks.outputs.tests }}' === 'success' ? '✅' : '❌', + build: '${{ steps.checks.outputs.build }}' === 'success' ? '✅' : '❌' + }; + + summary += '| Check | Status |\n'; + summary += '|-------|--------|\n'; + summary += `| TypeScript | ${checks.typescript} |\n`; + summary += `| ESLint | ${checks.lint} |\n`; + summary += `| Tests | ${checks.tests} |\n`; + summary += `| Build | ${checks.build} |\n`; + summary += '\n'; + + // Add details link + summary += `[View detailed results](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n`; + + // Find and update or create comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('PR Quality Report') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: summary + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: summary + }); + } + + coverage: + name: Test Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests with coverage + run: npm test -- --run --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/coverage-final.json + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml new file mode 100644 index 0000000..975b9ae --- /dev/null +++ b/.github/workflows/sast.yml @@ -0,0 +1,191 @@ +name: SAST Security Scan + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + schedule: + # Run at 2 AM UTC every day + - cron: '0 2 * * *' + +permissions: + contents: read + security-events: write + actions: read + +jobs: + semgrep: + name: Semgrep Scan + runs-on: ubuntu-latest + container: + image: semgrep/semgrep + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Semgrep + run: | + semgrep --config=auto \ + --json \ + --output=semgrep-results.json \ + --error \ + --severity=ERROR \ + --severity=WARNING \ + --severity=INFO \ + . + continue-on-error: true + + - name: Run Semgrep Searif + run: | + semgrep --config=auto \ + --sarif \ + --output=semgrep.sarif \ + --error \ + --severity=ERROR \ + --severity=WARNING \ + --severity=INFO \ + . + continue-on-error: true + + - name: Upload Semgrep results + if: always() + uses: actions/upload-artifact@v4 + with: + name: semgrep-results + path: semgrep-results.json + retention-days: 14 + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif + if: always() + + snyk: + name: Snyk Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=medium --json-file-output=snyk-results.json --sarif-file-output=snyk.sarif + + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: snyk.sarif + + - name: Upload Snyk results + if: always() + uses: actions/upload-artifact@v4 + with: + name: snyk-results + path: snyk-results.json + retention-days: 14 + + - name: Monitor dependencies with Snyk + if: github.ref == 'refs/heads/main' + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: monitor + + trivy: + name: Trivy Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH,MEDIUM' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + - name: Run Trivy for summary + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'table' + severity: 'CRITICAL,HIGH,MEDIUM' + + secrets-scan: + name: Secret Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: TruffleHog OSS + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD + extra_args: --debug --only-verified + + - name: Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [semgrep, snyk, trivy, secrets-scan] + if: always() + + steps: + - name: Generate Security Summary + run: | + echo "## 🔒 Security Scan Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Scanner | Status |" >> $GITHUB_STEP_SUMMARY + echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Semgrep | ${{ needs.semgrep.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Snyk | ${{ needs.snyk.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Trivy | ${{ needs.trivy.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Secret Detection | ${{ needs.secrets-scan.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "View detailed results in the Security tab and workflow artifacts." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..11a0378 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/coverage diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000..8c166ac --- /dev/null +++ b/.lighthouserc.json @@ -0,0 +1,52 @@ +{ + "ci": { + "collect": { + "url": ["http://localhost:3000"], + "numberOfRuns": 3, + "settings": { + "chromeFlags": "--no-sandbox --disable-dev-shm-usage --disable-gpu --headless --disable-dev-shm-usage", + "throttling": { + "rttMs": 40, + "throughputKbps": 10240, + "cpuSlowdownMultiplier": 1 + }, + "skipAudits": [ + "uses-http2", + "valid-source-maps", + "csp-xss" + ], + "onlyCategories": ["performance", "accessibility", "best-practices", "seo"] + } + }, + "assert": { + "preset": "lighthouse:no-pwa", + "assertions": { + "categories:performance": ["warn", { "minScore": 0.6 }], + "categories:accessibility": ["warn", { "minScore": 0.75 }], + "categories:best-practices": ["warn", { "minScore": 0.75 }], + "categories:seo": ["warn", { "minScore": 0.75 }], + "first-contentful-paint": "off", + "largest-contentful-paint": "off", + "cumulative-layout-shift": "off", + "total-blocking-time": "off", + "interactive": "off", + "button-name": "off", + "color-contrast": "off", + "csp-xss": "off", + "robots-txt": "off", + "total-byte-weight": "off", + "valid-source-maps": "off", + "uses-responsive-images": "off", + "uses-optimized-images": "off", + "uses-text-compression": "off", + "uses-rel-preconnect": "off", + "unused-javascript": "off", + "canonical": "off", + "is-crawlable": "off" + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index a7011b6..61ee314 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Gridfinity Calculator +[](https://github.com/ntindle/gridfinity-space-optimizer/actions/workflows/ci.yml) +[](https://github.com/ntindle/gridfinity-space-optimizer/actions/workflows/code-quality.yml) +[](https://github.com/ntindle/gridfinity-space-optimizer/actions/workflows/codeql.yml) +[](https://github.com/ntindle/gridfinity-space-optimizer/actions/workflows/sast.yml) +[](https://github.com/ntindle/gridfinity-space-optimizer/actions/workflows/deploy.yml) +[](https://codecov.io/gh/ntindle/gridfinity-space-optimizer) + This project is a web-based calculator for designing custom Gridfinity layouts, built with React, Vite, and Tailwind CSS. ## Features @@ -59,6 +66,28 @@ yarn dev The application will be available at `http://localhost:8080` by default. +### Testing + +Run the test suite: + +```bash +npm test +``` + +Run tests with coverage: + +```bash +npm test -- --coverage +``` + +### Linting + +Check code quality: + +```bash +npm run lint +``` + ### Building for production To create a production build: @@ -73,10 +102,44 @@ or yarn build ``` +## CI/CD + +This project uses GitHub Actions for continuous integration and deployment: + +- **CI Pipeline**: Runs on every push and pull request + - Tests across Node.js versions 18.x, 20.x, and 22.x + - ESLint and TypeScript type checking + - Build verification + - Bundle size analysis + +- **PR Checks**: Automated quality gates for pull requests + - Test coverage reports + - Code quality metrics + - Automated PR comments with status + +- **Deployment**: Automatic deployment to GitHub Pages on main branch + +- **Security**: + - CodeQL semantic analysis for JavaScript/TypeScript + - Multiple SAST tools (Semgrep, Snyk, Trivy) + - Secret detection (Gitleaks, TruffleHog) + - Weekly dependency audits + - Automated Dependabot updates + +- **Accessibility**: Automated WCAG 2.0/2.1 compliance testing + +- **Performance**: Lighthouse CI and bundle size monitoring + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. +All pull requests must pass: +- TypeScript type checking +- ESLint with no errors +- All tests passing +- Successful build + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/index.html b/index.html index 35cde1e..f899bbf 100644 --- a/index.html +++ b/index.html @@ -9,11 +9,12 @@ + +
- diff --git a/package-lock.json b/package-lock.json index 595058a..e8c1150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react-swc": "^3.11.0", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.19", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", @@ -87,6 +88,7 @@ "jsdom": "^26.1.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.4", + "terser": "^5.43.1", "tsx": "^4.20.3", "typescript": "^5.9.2", "vite": "^5.1.4", @@ -458,6 +460,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1287,6 +1299,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1306,6 +1328,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -3966,6 +3999,40 @@ "vite": "^4 || ^5 || ^6 || ^7" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -4357,6 +4424,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz", + "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.29", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4493,6 +4579,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6419,6 +6512,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-to-image": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", @@ -7004,6 +7104,60 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7331,6 +7485,34 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8882,6 +9064,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8891,6 +9083,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9303,6 +9506,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 1523df5..771860e 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react-swc": "^3.11.0", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.19", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", @@ -93,6 +94,7 @@ "jsdom": "^26.1.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.4", + "terser": "^5.43.1", "tsx": "^4.20.3", "typescript": "^5.9.2", "vite": "^5.1.4", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..ec26802 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://gridfinity-space-optimizer.com/sitemap.xml \ No newline at end of file diff --git a/src/components/GridfinityCalculator.test.tsx b/src/components/GridfinityCalculator.test.tsx index 25a6b00..b02b9a4 100644 --- a/src/components/GridfinityCalculator.test.tsx +++ b/src/components/GridfinityCalculator.test.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -30,7 +33,7 @@ const renderWithProviders = (component: ReactElement) => { describe('GridfinityCalculator', () => { beforeEach(() => { vi.clearAllMocks(); - (loadUserSettings as any).mockReturnValue(null); + vi.mocked(loadUserSettings).mockReturnValue(null); }); it('should render all main sections', () => { @@ -52,7 +55,7 @@ describe('GridfinityCalculator', () => { useMm: true, }; - (loadUserSettings as any).mockReturnValue(savedSettings); + vi.mocked(loadUserSettings).mockReturnValue(savedSettings); renderWithProviders(