refactor: Migrate to monorepo
BREAKING CHANGE: Complete structure overhaul
This commit is contained in:
496 files changed
+45388
-7531
No files matched your search
@@ -1,38 +0,0 @@
|
||||
# Testing Configuration
|
||||
|
||||
The project uses Jest with a clean configuration structure located in `.config/jest/`:
|
||||
|
||||
```
|
||||
.config/jest/
|
||||
├── setup.ts # Global test environment setup
|
||||
├── test-utils.tsx # Custom render utilities for React components
|
||||
└── tsconfig.json # TypeScript configuration for test files
|
||||
```
|
||||
|
||||
## Setup Files
|
||||
|
||||
### `setup.ts`
|
||||
Global test environment configuration including browser API mocks (IntersectionObserver, ResizeObserver, matchMedia) needed for testing React components in jsdom.
|
||||
|
||||
### `test-utils.tsx`
|
||||
Custom render function with project-specific providers and configurations. Import this instead of `@testing-library/react` directly:
|
||||
|
||||
```typescript
|
||||
import { render, screen } from '@test/test-utils';
|
||||
import { MyComponent } from './MyComponent';
|
||||
|
||||
// Use the custom render function
|
||||
render(<MyComponent />);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
```
|
||||
|
||||
## Path Aliases
|
||||
|
||||
The Jest configuration includes a `@test/*` alias that maps to `.config/jest/`:
|
||||
|
||||
```typescript
|
||||
// Import test utilities
|
||||
import { render, screen } from '@test/test-utils';
|
||||
```
|
||||
|
||||
This keeps test configuration separate from source code while maintaining clean imports.
|
||||
@@ -1,39 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
/**
|
||||
* Global test environment setup for Jest.
|
||||
*
|
||||
* This file configures browser APIs and global mocks needed for testing
|
||||
* React components in a jsdom environment.
|
||||
*/
|
||||
|
||||
// Mock matchMedia for components that use CSS media queries
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock IntersectionObserver for components using intersection detection
|
||||
(global as any).IntersectionObserver = class IntersectionObserver {
|
||||
constructor() {}
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
};
|
||||
|
||||
// Mock ResizeObserver for components using resize detection
|
||||
(global as any).ResizeObserver = class ResizeObserver {
|
||||
constructor() {}
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { render, type RenderOptions } from '@testing-library/react';
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
* Custom render function with default providers and configurations
|
||||
* @param ui - React element to render
|
||||
* @param options - Additional render options
|
||||
* @returns Render result from testing library
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { render, screen } from '.config/jest/test-utils';
|
||||
* import { MyComponent } from './MyComponent';
|
||||
*
|
||||
* render(<MyComponent />);
|
||||
* expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
* ```
|
||||
*/
|
||||
const customRender = (ui: ReactElement, options?: RenderOptions) => {
|
||||
return render(ui, {
|
||||
// Add any providers here if needed (e.g., ThemeProvider, Router, etc.)
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Re-export everything
|
||||
export * from '@testing-library/react';
|
||||
|
||||
// Override render method
|
||||
export { customRender as render };
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "@testing-library/jest-dom", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
name: next/checkout-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_version:
|
||||
description: 'Release version (e.g. v1.2.0)'
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
base_branch:
|
||||
description: 'Trunk branch to branch from'
|
||||
required: true
|
||||
default: 'next'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout selected main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.base_branch }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Create release branch
|
||||
run: |
|
||||
RELEASE="release/${{ github.event.inputs.release_version }}"
|
||||
echo "Creating branch $RELEASE off ${{ github.event.inputs.base_branch }}"
|
||||
git checkout -b "$RELEASE"
|
||||
git push -u origin "$RELEASE"
|
||||
|
||||
- name: Output branch URL
|
||||
run: |
|
||||
echo "Release branch created:"
|
||||
echo "https://github.com/${{ github.repository }}/tree/release/${{ github.event.inputs.release_version }}"
|
||||
@@ -1,150 +0,0 @@
|
||||
name: next/semantic-dispatch
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['next/required']
|
||||
branches: [next]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
packages: write
|
||||
outputs:
|
||||
new-release-published: ${{ steps.semantic.outputs.new-release-published }}
|
||||
new-release-version: ${{ steps.semantic.outputs.new-release-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Stage
|
||||
run: pnpm build
|
||||
- name: Build Storybook
|
||||
run: pnpm build-storybook
|
||||
- name: Pre-release
|
||||
id: semantic
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm exec semantic-release --prerelease
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
if: needs.release.outputs.new-release-published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Astro image
|
||||
id: meta-astro
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/deytenit/www.ermnvldmr.com
|
||||
tags: |
|
||||
type=raw,value=${{ needs.release.outputs.new-release-version }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-next,value=${{ needs.release.outputs.new-release-version }},match=(\d+\.\d+\.\d+),enable={{is_default_branch}}
|
||||
type=semver,pattern={{major}},suffix=-next,value=${{ needs.release.outputs.new-release-version }},match=(\d+\.\d+\.\d+),enable={{is_default_branch}}
|
||||
type=raw,value=latest-next
|
||||
type=sha,prefix={{branch}}-
|
||||
|
||||
- name: Extract metadata for Storybook image
|
||||
id: meta-storybook
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/deytenit/www.ermnvldmr.com/storybook
|
||||
tags: |
|
||||
type=raw,value=${{ needs.release.outputs.new-release-version }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-next,value=${{ needs.release.outputs.new-release-version }},match=(\d+\.\d+\.\d+),enable={{is_default_branch}}
|
||||
type=semver,pattern={{major}},suffix=-next,value=${{ needs.release.outputs.new-release-version }},match=(\d+\.\d+\.\d+),enable={{is_default_branch}}
|
||||
type=raw,value=latest-next
|
||||
type=sha,prefix={{branch}}-
|
||||
|
||||
- name: Build and push Astro image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/astro/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta-astro.outputs.tags }}
|
||||
labels: ${{ steps.meta-astro.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push Storybook image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/storybook/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta-storybook.outputs.tags }}
|
||||
labels: ${{ steps.meta-storybook.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+20
-51
@@ -1,11 +1,14 @@
|
||||
name: next/required
|
||||
name: packages/stl - next/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/stl/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
group: ci-stl-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -40,7 +43,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
name: Lint STL
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -55,11 +58,11 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run ESLint (CI mode)
|
||||
run: pnpm run lint --max-warnings=0
|
||||
- name: Lint STL package
|
||||
run: pnpm --filter @ermnvldmr/stl lint
|
||||
|
||||
type-check:
|
||||
name: Typecheck
|
||||
name: Typecheck STL
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -74,37 +77,12 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run TypeScript compiler
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
storybook-build:
|
||||
name: Build Storybook
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Storybook
|
||||
run: pnpm run build-storybook --quiet
|
||||
- name: Upload Storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static/
|
||||
retention-days: 1
|
||||
- name: Typecheck STL package
|
||||
run: pnpm --filter @ermnvldmr/stl typecheck
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: [setup, storybook-build]
|
||||
name: Test STL
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -118,27 +96,18 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Run unit tests with coverage
|
||||
run: pnpm run test:coverage
|
||||
- name: Download Storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static/
|
||||
- name: Serve Storybook and run tests with coverage
|
||||
run: pnpm run test:storybook:ci
|
||||
- name: Test STL package
|
||||
run: pnpm --filter @ermnvldmr/stl test:coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info,./coverage/storybook/coverage-storybook.json
|
||||
flags: unittests,storybook
|
||||
files: ./packages/stl/coverage/lcov.info
|
||||
flags: stl
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
name: Build
|
||||
name: Build STL Package
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -153,5 +122,5 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Astro build
|
||||
run: pnpm exec astro build
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
+62
-43
@@ -1,14 +1,15 @@
|
||||
name: pr/required
|
||||
name: packages/ui - next/required
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, next]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- 'packages/stl/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
group: ci-ui-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -43,7 +44,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
name: Lint UI & Dependencies
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -58,11 +59,13 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run ESLint (CI mode)
|
||||
run: pnpm run lint --max-warnings=0
|
||||
- name: Lint STL package
|
||||
run: pnpm --filter @ermnvldmr/stl lint
|
||||
- name: Lint UI package
|
||||
run: pnpm --filter @ermnvldmr/ui lint
|
||||
|
||||
type-check:
|
||||
name: Typecheck
|
||||
name: Typecheck UI & Dependencies
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -77,11 +80,13 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run TypeScript compiler
|
||||
run: pnpm exec tsc --noEmit
|
||||
- name: Typecheck STL package
|
||||
run: pnpm --filter @ermnvldmr/stl typecheck
|
||||
- name: Typecheck UI package
|
||||
run: pnpm --filter @ermnvldmr/ui typecheck
|
||||
|
||||
storybook-build:
|
||||
name: Build Storybook
|
||||
build-dependencies:
|
||||
name: Build Dependencies (STL)
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -96,18 +101,12 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Storybook
|
||||
run: pnpm run build-storybook --quiet
|
||||
- name: Upload Storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static/
|
||||
retention-days: 1
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: [setup, storybook-build]
|
||||
name: Test UI & Dependencies
|
||||
needs: [setup, build-dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -121,28 +120,23 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Run unit tests with coverage
|
||||
run: pnpm run test:coverage
|
||||
- name: Download Storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static/
|
||||
- name: Serve Storybook and run tests with coverage
|
||||
run: pnpm run test:storybook:ci
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Test STL package
|
||||
run: pnpm --filter @ermnvldmr/stl test:coverage
|
||||
- name: Test UI package
|
||||
run: pnpm --filter @ermnvldmr/ui test:coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info,./coverage/storybook/coverage-storybook.json
|
||||
flags: unittests,storybook
|
||||
files: ./packages/stl/coverage/lcov.info,./packages/ui/coverage/lcov.info
|
||||
flags: stl,ui
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: setup
|
||||
storybook-build:
|
||||
name: Build UI Storybook
|
||||
needs: [setup, build-dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -156,5 +150,30 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Astro build
|
||||
run: pnpm exec astro build
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Build UI Storybook
|
||||
run: pnpm --filter @ermnvldmr/ui build-storybook
|
||||
|
||||
build:
|
||||
name: Build UI Package
|
||||
needs: [setup, build-dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
@@ -1,89 +0,0 @@
|
||||
name: release/deploy-production
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
BUILD_PATH: '.'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ ! github.event.release.prerelease }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Stage
|
||||
run: pnpm build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy stage
|
||||
steps:
|
||||
- name: Deploy in production
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -1,145 +0,0 @@
|
||||
name: release/semantic-dispatch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
packages: write
|
||||
outputs:
|
||||
new-release-published: ${{ steps.semantic.outputs.new-release-published }}
|
||||
new-release-version: ${{ steps.semantic.outputs.new-release-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Stage
|
||||
run: pnpm build
|
||||
- name: Build Storybook
|
||||
run: pnpm build-storybook
|
||||
- name: Release
|
||||
id: semantic
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm exec semantic-release
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
if: needs.release.outputs.new-release-published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Astro image
|
||||
id: meta-astro
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/deytenit/www.ermnvldmr.com
|
||||
tags: |
|
||||
type=raw,value=${{ needs.release.outputs.new-release-version }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release.outputs.new-release-version }},enable={{is_default_branch}}
|
||||
type=semver,pattern={{major}},value=${{ needs.release.outputs.new-release-version }},enable={{is_default_branch}}
|
||||
type=raw,value=latest
|
||||
type=sha,prefix={{branch}}-
|
||||
|
||||
- name: Extract metadata for Storybook image
|
||||
id: meta-storybook
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/deytenit/www.ermnvldmr.com/storybook
|
||||
tags: |
|
||||
type=raw,value=${{ needs.release.outputs.new-release-version }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release.outputs.new-release-version }},enable={{is_default_branch}}
|
||||
type=semver,pattern={{major}},value=${{ needs.release.outputs.new-release-version }},enable={{is_default_branch}}
|
||||
type=raw,value=latest
|
||||
type=sha,prefix={{branch}}-
|
||||
|
||||
- name: Build and push Astro image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/astro/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta-astro.outputs.tags }}
|
||||
labels: ${{ steps.meta-astro.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push Storybook image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/storybook/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta-storybook.outputs.tags }}
|
||||
labels: ${{ steps.meta-storybook.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -0,0 +1,49 @@
|
||||
name: services/docs - next/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'services/docs/**'
|
||||
|
||||
concurrency:
|
||||
group: ci-docs-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup Hugo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: 'latest'
|
||||
extended: true
|
||||
|
||||
build:
|
||||
name: Build Hugo Site
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: 'latest'
|
||||
extended: true
|
||||
|
||||
- name: Build Hugo site
|
||||
working-directory: services/docs
|
||||
run: hugo --gc --minify
|
||||
@@ -0,0 +1,207 @@
|
||||
name: services/www - next/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'services/www/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/stl/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-www-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build-dependencies:
|
||||
name: Build Dependencies (STL → UI)
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
|
||||
lint:
|
||||
name: Lint WWW & Dependencies
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Lint STL package
|
||||
run: pnpm --filter @ermnvldmr/stl lint
|
||||
- name: Lint UI package
|
||||
run: pnpm --filter @ermnvldmr/ui lint
|
||||
- name: Lint WWW service
|
||||
run: pnpm --filter @ermnvldmr/www lint
|
||||
|
||||
type-check:
|
||||
name: Typecheck WWW & Dependencies
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Typecheck STL package
|
||||
run: pnpm --filter @ermnvldmr/stl typecheck
|
||||
- name: Typecheck UI package
|
||||
run: pnpm --filter @ermnvldmr/ui typecheck
|
||||
- name: Typecheck WWW service
|
||||
run: pnpm --filter @ermnvldmr/www typecheck
|
||||
|
||||
storybook-build:
|
||||
name: Build Storybook
|
||||
needs: [setup, build-dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Build WWW Storybook
|
||||
run: pnpm --filter @ermnvldmr/www build-storybook
|
||||
- name: Upload Storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: services/www/storybook-static/
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: [setup, storybook-build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Test STL package
|
||||
run: pnpm --filter @ermnvldmr/stl test:coverage
|
||||
- name: Test UI package
|
||||
run: pnpm --filter @ermnvldmr/ui test:coverage
|
||||
- name: Test WWW service
|
||||
run: pnpm --filter @ermnvldmr/www test:coverage
|
||||
- name: Download Storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: services/www/storybook-static/
|
||||
- name: Serve Storybook and run tests with coverage
|
||||
run: pnpm --filter @ermnvldmr/www test:storybook:ci
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./services/www/coverage/lcov.info,./services/www/coverage/storybook/coverage-storybook.json
|
||||
flags: unittests,storybook
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
name: Build WWW Service
|
||||
needs: [setup, build-dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build STL package
|
||||
run: pnpm --filter @ermnvldmr/stl build
|
||||
- name: Build UI package
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Build WWW service
|
||||
run: pnpm --filter @ermnvldmr/www build
|
||||
@@ -1,46 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { mergeConfig } from 'vite';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-themes',
|
||||
'@storybook/addon-coverage',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
const tailwindcss = (await import('@tailwindcss/vite')).default;
|
||||
const istanbul = (await import('vite-plugin-istanbul')).default;
|
||||
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
istanbul({
|
||||
include: 'src/**/*',
|
||||
exclude: ['node_modules', 'test/', '**/*.stories.tsx', '**/*.test.tsx'],
|
||||
extension: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
requireEnv: false,
|
||||
cypress: false,
|
||||
forceBuildInstrument: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../src'),
|
||||
'@shadcn': path.resolve(__dirname, '../src/shadcn'),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
# Codecov configuration for www.ermnvldmr.com
|
||||
# Documentation: https://docs.codecov.com/docs/codecovyml-reference
|
||||
|
||||
# Coverage reporting settings
|
||||
coverage:
|
||||
# Set precision for coverage percentage (0-5 decimal places)
|
||||
precision: 2
|
||||
|
||||
# Round coverage percentage (up, down, nearest)
|
||||
round: nearest
|
||||
|
||||
# Coverage range (red to green)
|
||||
range: 70..90
|
||||
|
||||
# Status checks configuration
|
||||
status:
|
||||
# Overall project coverage
|
||||
project:
|
||||
default:
|
||||
# Require coverage to be within 1% of base branch
|
||||
threshold: 1%
|
||||
# Target minimum coverage
|
||||
target: 80%
|
||||
# Allow coverage to drop by at most 1%
|
||||
if_ci_failed: error
|
||||
|
||||
# Patch coverage (for new code in PRs)
|
||||
patch:
|
||||
default:
|
||||
# Require 80% coverage on new code
|
||||
target: 80%
|
||||
# Only post status if coverage changes
|
||||
only_pulls: true
|
||||
|
||||
# Comment settings for PRs
|
||||
comment:
|
||||
# Customize the PR comment layout
|
||||
layout: "header, diff, flags, files"
|
||||
# Show coverage for files with changes
|
||||
behavior: default
|
||||
# Require changes to post comment
|
||||
require_changes: true
|
||||
|
||||
# Coverage collection
|
||||
codecov:
|
||||
# Require CI to pass before processing coverage
|
||||
require_ci_to_pass: true
|
||||
|
||||
# Maximum number of reports to wait for
|
||||
max_report_age: "24h"
|
||||
|
||||
# File path adjustments
|
||||
fixes:
|
||||
- "src/::" # Remove src/ prefix from file paths in reports
|
||||
|
||||
# Ignore certain files from coverage
|
||||
ignore:
|
||||
- "**/*.stories.tsx" # Storybook files
|
||||
- "**/*.test.tsx" # Test files themselves
|
||||
- "**/*.test.ts" # Test files themselves
|
||||
- "**/test-utils.*" # Test utilities
|
||||
- ".storybook/**" # Storybook config
|
||||
- ".config/**" # Configuration files
|
||||
- "dist/**" # Build output
|
||||
- "coverage/**" # Coverage reports
|
||||
- "**/*.d.ts" # Type definitions
|
||||
+26
-13
@@ -1,47 +1,60 @@
|
||||
# Docker Compose for local development and testing
|
||||
# Use: docker-compose up to start both services locally
|
||||
# Use: docker-compose up to start services locally
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Astro static site
|
||||
astro:
|
||||
# WWW service (Astro static site)
|
||||
www:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/astro/Dockerfile
|
||||
target: production
|
||||
ports:
|
||||
- "3000:8080"
|
||||
container_name: www-ermnvldmr-astro
|
||||
container_name: ermnvldmr-www
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Storybook component library
|
||||
storybook:
|
||||
# WWW Storybook
|
||||
www-storybook:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/storybook/Dockerfile
|
||||
target: production
|
||||
ports:
|
||||
- "6006:8080"
|
||||
container_name: www-ermnvldmr-storybook
|
||||
container_name: ermnvldmr-www-storybook
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Docs service (Hugo static site) - Future implementation
|
||||
# docs:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/hugo/Dockerfile
|
||||
# target: production
|
||||
# ports:
|
||||
# - "3001:8080"
|
||||
# container_name: ermnvldmr-docs
|
||||
# environment:
|
||||
# - NODE_ENV=production
|
||||
# restart: unless-stopped
|
||||
|
||||
# Optional: Add a reverse proxy for unified access
|
||||
# nginx-proxy:
|
||||
# image: nginx:alpine
|
||||
@@ -50,10 +63,10 @@ services:
|
||||
# volumes:
|
||||
# - ./docker/nginx/proxy.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
# depends_on:
|
||||
# - astro
|
||||
# - storybook
|
||||
# container_name: www-ermnvldmr-proxy
|
||||
# - www
|
||||
# - www-storybook
|
||||
# container_name: ermnvldmr-proxy
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: www-ermnvldmr-network
|
||||
name: ermnvldmr-infrastructure
|
||||
@@ -1,213 +0,0 @@
|
||||
# Docker Configuration
|
||||
|
||||
This directory contains Docker configurations for containerizing the www.ermnvldmr.com website and Storybook component library.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
docker/
|
||||
├── astro/
|
||||
│ └── Dockerfile # Astro static site container
|
||||
├── storybook/
|
||||
│ └── Dockerfile # Storybook component library container
|
||||
└── nginx/
|
||||
├── nginx.conf # Main nginx configuration
|
||||
├── astro.conf # Astro site server configuration
|
||||
└── storybook.conf # Storybook server configuration
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
### Main Website
|
||||
- **Image**: `ghcr.io/deytenit/www.ermnvldmr.com`
|
||||
- **Source**: `docker/astro/Dockerfile`
|
||||
- **Purpose**: Serves the static Astro website
|
||||
|
||||
### Component Library
|
||||
- **Image**: `ghcr.io/deytenit/www.ermnvldmr.com/storybook`
|
||||
- **Source**: `docker/storybook/Dockerfile`
|
||||
- **Purpose**: Serves the Storybook component documentation
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Start both containers
|
||||
docker-compose up
|
||||
|
||||
# Build and start
|
||||
docker-compose up --build
|
||||
|
||||
# Access services
|
||||
# Main site: http://localhost:3000
|
||||
# Storybook: http://localhost:6006
|
||||
```
|
||||
|
||||
### Manual Build
|
||||
|
||||
```bash
|
||||
# Build Astro site
|
||||
docker build -f docker/astro/Dockerfile -t www-astro .
|
||||
|
||||
# Build Storybook
|
||||
docker build -f docker/storybook/Dockerfile -t www-storybook .
|
||||
|
||||
# Run containers
|
||||
docker run -p 3000:8080 www-astro
|
||||
docker run -p 6006:8080 www-storybook
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Security
|
||||
- **Non-root execution**: Containers run as `nginx-user` (UID 1001)
|
||||
- **Minimal attack surface**: Alpine Linux base with minimal packages
|
||||
- **Security headers**: Comprehensive HTTP security headers configured
|
||||
- **Non-privileged ports**: Expose port 8080 instead of 80
|
||||
|
||||
### Performance
|
||||
- **Multi-stage builds**: Separate build and runtime stages for smaller images
|
||||
- **Static content optimization**: nginx tuned for static file serving
|
||||
- **Compression**: Gzip compression for all text-based assets
|
||||
- **Caching**: Proper cache headers for optimal performance
|
||||
|
||||
### Reliability
|
||||
- **Health checks**: Built-in health monitoring endpoints
|
||||
- **Signal handling**: Proper signal handling with dumb-init
|
||||
- **Graceful shutdown**: Clean container shutdown procedures
|
||||
- **Resource limits**: Configurable resource constraints
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The containers are designed to be stateless and don't require runtime environment variables. All configuration is baked into the build process.
|
||||
|
||||
### nginx Customization
|
||||
|
||||
Modify nginx configurations in the `nginx/` directory:
|
||||
|
||||
- **nginx.conf**: Global nginx settings (workers, gzip, security)
|
||||
- **astro.conf**: Astro-specific routing and caching
|
||||
- **storybook.conf**: Storybook-specific SPA routing and CSP
|
||||
|
||||
### Build Arguments
|
||||
|
||||
No build arguments are currently required, but can be added for customization:
|
||||
|
||||
```dockerfile
|
||||
ARG NODE_VERSION=20
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Images are automatically built and pushed via GitHub Actions when:
|
||||
- **Pre-release**: Push to `next` branch triggers semantic pre-release
|
||||
- **Release**: Manual workflow dispatch creates stable release
|
||||
|
||||
### Production Usage
|
||||
|
||||
```bash
|
||||
# Pull latest images
|
||||
docker pull ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
docker pull ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest
|
||||
|
||||
# Run with restart policy
|
||||
docker run -d \
|
||||
--name www-astro \
|
||||
-p 3000:8080 \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
|
||||
docker run -d \
|
||||
--name www-storybook \
|
||||
-p 6006:8080 \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest
|
||||
```
|
||||
|
||||
### Reverse Proxy Integration
|
||||
|
||||
Both containers are designed to work behind a reverse proxy:
|
||||
|
||||
- Expose port 8080 (non-privileged)
|
||||
- Include proper health check endpoints
|
||||
- Forward client IP and headers correctly
|
||||
- Support SSL termination at proxy level
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Container health
|
||||
curl http://localhost:3000/health
|
||||
curl http://localhost:6006/health
|
||||
|
||||
# Docker health status
|
||||
docker ps # Shows health status
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker logs container-name
|
||||
|
||||
# Follow logs
|
||||
docker logs -f container-name
|
||||
|
||||
# Docker Compose logs
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build failures**: Check Node.js version and pnpm compatibility
|
||||
2. **Permission errors**: Verify nginx user configuration
|
||||
3. **Port conflicts**: Ensure ports 3000 and 6006 are available
|
||||
4. **Health check failures**: Verify nginx configuration and file permissions
|
||||
|
||||
### Debug Access
|
||||
|
||||
```bash
|
||||
# Interactive shell
|
||||
docker exec -it container-name sh
|
||||
|
||||
# Debug container without starting services
|
||||
docker run -it --entrypoint=/bin/sh image-name
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Vulnerability Scanning
|
||||
|
||||
Images are automatically scanned for vulnerabilities in the CI/CD pipeline. For manual scanning:
|
||||
|
||||
```bash
|
||||
# Using Docker Scout
|
||||
docker scout cves ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
|
||||
# Using Trivy
|
||||
trivy image ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
```
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- Regular base image updates
|
||||
- Non-root user execution
|
||||
- Minimal package installation
|
||||
- Security header enforcement
|
||||
- Access logging enabled
|
||||
|
||||
---
|
||||
|
||||
For detailed deployment and development guides, see:
|
||||
- [Container Deployment Guide](../docs/container-deployment.md)
|
||||
- [Container Development Guide](../docs/container-development.md)
|
||||
@@ -1,60 +0,0 @@
|
||||
# Multi-stage build for Astro static site
|
||||
# Build stage - use Node.js with pnpm for building
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files for dependency installation
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies with frozen lockfile for reproducible builds
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the Astro application
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage - minimal nginx:alpine for serving static files
|
||||
FROM nginx:1.25-alpine AS production
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
# Copy custom nginx configuration
|
||||
COPY docker/nginx/astro.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy built static files from builder stage
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Create nginx user and set proper permissions
|
||||
RUN addgroup -g 1001 -S nginx-user && \
|
||||
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user nginx-user && \
|
||||
chown -R nginx-user:nginx-user /usr/share/nginx/html && \
|
||||
chown -R nginx-user:nginx-user /var/cache/nginx && \
|
||||
chown -R nginx-user:nginx-user /var/log/nginx && \
|
||||
chown -R nginx-user:nginx-user /etc/nginx/conf.d && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R nginx-user:nginx-user /var/run/nginx.pid
|
||||
|
||||
# Switch to non-root user
|
||||
USER nginx-user
|
||||
|
||||
# Expose port 8080 (non-privileged port)
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
|
||||
|
||||
# Use dumb-init as entrypoint for proper signal handling
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
# Start nginx in foreground
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,69 +0,0 @@
|
||||
# Nginx server configuration for Astro static site
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security headers specific to main site
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# Caching strategy for static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Caching for HTML files (shorter cache time)
|
||||
location ~* \.html$ {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, must-revalidate";
|
||||
add_header Vary "Accept-Encoding";
|
||||
}
|
||||
|
||||
# Main location block for Astro routes
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
|
||||
# Handle client-side routing if needed
|
||||
# Uncomment if using client-side routing
|
||||
# try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Specific handling for Astro's _astro directory
|
||||
location /_astro/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Custom error pages
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# Main nginx configuration optimized for static content delivery
|
||||
user nginx-user;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging format
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Performance optimizations
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/json
|
||||
application/xml
|
||||
image/svg+xml;
|
||||
|
||||
# Security headers (basic set)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Include server configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
# Nginx server configuration for Storybook static site
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security headers for Storybook
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# Storybook needs more permissive CSP for iframe and scripts
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-src 'self';" always;
|
||||
|
||||
# Caching strategy for static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Caching for HTML files (shorter cache time)
|
||||
location ~* \.html$ {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, must-revalidate";
|
||||
add_header Vary "Accept-Encoding";
|
||||
}
|
||||
|
||||
# Main location block for Storybook
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Handle Storybook's client-side routing
|
||||
# Always fall back to index.html for SPA behavior
|
||||
}
|
||||
|
||||
# Storybook static assets
|
||||
location /static/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Storybook specific directories
|
||||
location ~ ^/(sb-|assets/|fonts/) {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Custom error pages
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# Multi-stage build for Storybook static site
|
||||
# Build stage - use Node.js with pnpm for building
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files for dependency installation
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies with frozen lockfile for reproducible builds
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the Storybook application
|
||||
RUN pnpm build-storybook
|
||||
|
||||
# Production stage - minimal nginx:alpine for serving static files
|
||||
FROM nginx:1.25-alpine AS production
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
# Copy custom nginx configuration
|
||||
COPY docker/nginx/storybook.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy built static files from builder stage
|
||||
COPY --from=builder /app/storybook-static /usr/share/nginx/html
|
||||
|
||||
# Create nginx user and set proper permissions
|
||||
RUN addgroup -g 1001 -S nginx-user && \
|
||||
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user nginx-user && \
|
||||
chown -R nginx-user:nginx-user /usr/share/nginx/html && \
|
||||
chown -R nginx-user:nginx-user /var/cache/nginx && \
|
||||
chown -R nginx-user:nginx-user /var/log/nginx && \
|
||||
chown -R nginx-user:nginx-user /etc/nginx/conf.d && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R nginx-user:nginx-user /var/run/nginx.pid
|
||||
|
||||
# Switch to non-root user
|
||||
USER nginx-user
|
||||
|
||||
# Expose port 8080 (non-privileged port)
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
|
||||
|
||||
# Use dumb-init as entrypoint for proper signal handling
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
# Start nginx in foreground
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,394 +0,0 @@
|
||||
# Container Deployment Guide
|
||||
|
||||
This guide covers the containerized deployment setup for the www.ermnvldmr.com website, including both the main Astro site and the Storybook component library.
|
||||
|
||||
## Overview
|
||||
|
||||
The project uses Docker containers to package and deploy:
|
||||
- **Main Site**: Astro static website served via nginx
|
||||
- **Storybook**: Component library served via nginx
|
||||
- **Registry**: GitHub Container Registry (ghcr.io)
|
||||
- **CI/CD**: GitHub Actions with semantic versioning
|
||||
|
||||
## Container Architecture
|
||||
|
||||
### Multi-stage Build Strategy
|
||||
|
||||
Both containers follow a multi-stage build pattern:
|
||||
|
||||
1. **Builder Stage**: Node.js 20 Alpine with pnpm for building assets
|
||||
2. **Production Stage**: nginx:1.25-alpine for serving static content
|
||||
|
||||
### Security Features
|
||||
|
||||
- **Non-root user**: Containers run as `nginx-user` (UID 1001)
|
||||
- **Non-privileged ports**: Expose port 8080 instead of 80
|
||||
- **Security headers**: CSP, HSTS, and other security headers configured
|
||||
- **Signal handling**: dumb-init for proper signal handling
|
||||
|
||||
### Images
|
||||
|
||||
- **Main Site**: `ghcr.io/deytenit/www.ermnvldmr.com`
|
||||
- **Storybook**: `ghcr.io/deytenit/www.ermnvldmr.com/storybook`
|
||||
|
||||
## Tagging Strategy
|
||||
|
||||
### Release Tags (from `release/semantic-dispatch`)
|
||||
|
||||
For both images:
|
||||
- `vX.X.X` - Full version (e.g., `v1.2.3`)
|
||||
- `vX.X` - Minor version (e.g., `v1.2`)
|
||||
- `vX` - Major version (e.g., `v1`)
|
||||
- `latest` - Latest stable release
|
||||
- `{sha}` - Commit SHA for traceability
|
||||
|
||||
### Pre-release Tags (from `next/semantic-dispatch`)
|
||||
|
||||
For both images:
|
||||
- `vX.X.X-next.X` - Full prerelease version (e.g., `v1.2.3-next.1`)
|
||||
- `vX.X.X-next` - Latest prerelease for that version
|
||||
- `vX.X-next` - Latest prerelease for minor version
|
||||
- `vX-next` - Latest prerelease for major version
|
||||
- `latest-next` - Latest prerelease overall
|
||||
- `{sha}` - Commit SHA for traceability
|
||||
|
||||
## Local Development
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
Start both services locally:
|
||||
|
||||
```bash
|
||||
# Start both containers
|
||||
docker-compose up
|
||||
|
||||
# Start in background
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
Access points:
|
||||
- **Main Site**: http://localhost:3000
|
||||
- **Storybook**: http://localhost:6006
|
||||
|
||||
### Building Individual Images
|
||||
|
||||
```bash
|
||||
# Build Astro site
|
||||
docker build -f docker/astro/Dockerfile -t www-astro .
|
||||
|
||||
# Build Storybook
|
||||
docker build -f docker/storybook/Dockerfile -t www-storybook .
|
||||
|
||||
# Run containers
|
||||
docker run -p 3000:8080 www-astro
|
||||
docker run -p 6006:8080 www-storybook
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Manual Container Deployment
|
||||
|
||||
Pull and run the latest images:
|
||||
|
||||
```bash
|
||||
# Pull latest stable images
|
||||
docker pull ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
docker pull ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest
|
||||
|
||||
# Run main site
|
||||
docker run -d \
|
||||
--name www-ermnvldmr-astro \
|
||||
-p 3000:8080 \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
|
||||
# Run storybook
|
||||
docker run -d \
|
||||
--name www-ermnvldmr-storybook \
|
||||
-p 6006:8080 \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The containers are designed to be stateless and don't require runtime environment variables. Configuration is baked into the build.
|
||||
|
||||
### Health Checks
|
||||
|
||||
Both containers include health check endpoints:
|
||||
|
||||
```bash
|
||||
# Check container health
|
||||
curl http://localhost:3000/health # Astro site
|
||||
curl http://localhost:6006/health # Storybook
|
||||
```
|
||||
|
||||
### Resource Requirements
|
||||
|
||||
**Recommended minimums per container:**
|
||||
- **CPU**: 0.1 cores
|
||||
- **Memory**: 64MB
|
||||
- **Storage**: 100MB
|
||||
|
||||
**Production recommendations:**
|
||||
- **CPU**: 0.25 cores
|
||||
- **Memory**: 128MB
|
||||
- **Storage**: 500MB (for logs and cache)
|
||||
|
||||
## Reverse Proxy Configuration
|
||||
|
||||
### nginx Configuration Example
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.ermnvldmr.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://astro-container:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name storybook.ermnvldmr.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://storybook-container:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik Configuration Example
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml with Traefik
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
astro:
|
||||
image: ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.astro.rule=Host(`www.ermnvldmr.com`)"
|
||||
- "traefik.http.services.astro.loadbalancer.server.port=8080"
|
||||
|
||||
storybook:
|
||||
image: ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.storybook.rule=Host(`storybook.ermnvldmr.com`)"
|
||||
- "traefik.http.services.storybook.loadbalancer.server.port=8080"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Container Won't Start
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
docker logs container-name
|
||||
|
||||
# Common causes:
|
||||
# - Missing dependencies in build
|
||||
# - Permission issues
|
||||
# - Port conflicts
|
||||
```
|
||||
|
||||
#### Build Failures
|
||||
|
||||
```bash
|
||||
# Check build logs
|
||||
docker build -f docker/astro/Dockerfile -t test .
|
||||
|
||||
# Common causes:
|
||||
# - Node version mismatch
|
||||
# - pnpm cache issues
|
||||
# - Missing build dependencies
|
||||
```
|
||||
|
||||
#### Permission Denied
|
||||
|
||||
```bash
|
||||
# Check if running as correct user
|
||||
docker exec container-name whoami
|
||||
|
||||
# Should output: nginx-user
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug mode in nginx:
|
||||
|
||||
```bash
|
||||
# Create debug container
|
||||
docker run -it --entrypoint=/bin/sh ghcr.io/deytenit/www.ermnvldmr.com:latest
|
||||
|
||||
# Check nginx config
|
||||
nginx -t
|
||||
|
||||
# Check file permissions
|
||||
ls -la /usr/share/nginx/html/
|
||||
```
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
#### nginx Optimization
|
||||
|
||||
For high-traffic deployments, consider:
|
||||
|
||||
- Adjusting worker processes
|
||||
- Enabling gzip compression levels
|
||||
- Tuning keepalive settings
|
||||
- Adding rate limiting
|
||||
|
||||
#### Container Resources
|
||||
|
||||
Monitor and adjust:
|
||||
|
||||
```bash
|
||||
# Check resource usage
|
||||
docker stats
|
||||
|
||||
# Update container limits
|
||||
docker run --memory=256m --cpus=0.5 ...
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Container Security
|
||||
|
||||
- Containers run as non-root user
|
||||
- Minimal attack surface with Alpine base
|
||||
- Regular security updates via base image updates
|
||||
- No sensitive data in containers
|
||||
|
||||
### Network Security
|
||||
|
||||
- Expose only necessary ports
|
||||
- Use internal networks for service communication
|
||||
- Implement proper firewall rules
|
||||
- Enable HTTPS with reverse proxy
|
||||
|
||||
### Registry Security
|
||||
|
||||
- Images are signed and scanned
|
||||
- Access controlled via GitHub permissions
|
||||
- Automatic vulnerability scanning
|
||||
- Regular base image updates
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```bash
|
||||
# Simple health check script
|
||||
#!/bin/bash
|
||||
if curl -f http://localhost:3000/health >/dev/null 2>&1; then
|
||||
echo "Astro site: healthy"
|
||||
else
|
||||
echo "Astro site: unhealthy"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Log Management
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker-compose logs -f astro
|
||||
docker-compose logs -f storybook
|
||||
|
||||
# Log rotation (configure docker daemon)
|
||||
# /etc/docker/daemon.json
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics Collection
|
||||
|
||||
Consider integrating with:
|
||||
- Prometheus + Grafana
|
||||
- nginx prometheus exporter
|
||||
- Container metrics exporters
|
||||
- Application performance monitoring
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The containers are automatically built and pushed when:
|
||||
|
||||
1. **Pre-release**: Merge to `next` branch triggers prerelease
|
||||
2. **Release**: Manual workflow dispatch creates stable release
|
||||
|
||||
### Workflow Outputs
|
||||
|
||||
Each workflow provides:
|
||||
- `new_release_published`: Boolean indicating if release was created
|
||||
- `new_release_version`: The version number of the release
|
||||
|
||||
### Manual Triggers
|
||||
|
||||
```bash
|
||||
# Trigger release workflow via GitHub CLI
|
||||
gh workflow run "release/semantic-dispatch" --ref main
|
||||
|
||||
# Trigger prerelease via merge to next branch
|
||||
git checkout next
|
||||
git merge feature-branch
|
||||
git push origin next
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Updates
|
||||
|
||||
1. **Base Image Updates**: Rebuild monthly for security patches
|
||||
2. **Dependency Updates**: Follow project dependency update schedule
|
||||
3. **Configuration Review**: Quarterly review of nginx and security settings
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
Since containers are stateless:
|
||||
- Source code is backed up in Git repository
|
||||
- Container images are versioned in registry
|
||||
- No persistent data to backup
|
||||
|
||||
### Rollback Procedure
|
||||
|
||||
```bash
|
||||
# Rollback to previous version
|
||||
docker pull ghcr.io/deytenit/www.ermnvldmr.com:v1.2.2
|
||||
docker stop current-container
|
||||
docker run -d --name new-container ghcr.io/deytenit/www.ermnvldmr.com:v1.2.2
|
||||
|
||||
# Or use docker-compose with specific version
|
||||
docker-compose down
|
||||
# Edit docker-compose.yml to specify version
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For additional support or questions, refer to the project repository issues or documentation.
|
||||
@@ -1,303 +0,0 @@
|
||||
# Container Development Guide
|
||||
|
||||
This guide covers development workflow with the containerized setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- pnpm v9.12.3+
|
||||
- Node.js v20+
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Clone and setup**:
|
||||
```bash
|
||||
git clone https://github.com/deytenit/www.ermnvldmr.com.git
|
||||
cd www.ermnvldmr.com
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Start development servers** (non-containerized):
|
||||
```bash
|
||||
pnpm dev # Astro dev server on :4321
|
||||
pnpm storybook # Storybook dev server on :6006
|
||||
```
|
||||
|
||||
3. **Test containerized build**:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
### Container Development Workflow
|
||||
|
||||
#### Building Images Locally
|
||||
|
||||
```bash
|
||||
# Build both images
|
||||
docker-compose build
|
||||
|
||||
# Build specific image
|
||||
docker-compose build astro
|
||||
docker-compose build storybook
|
||||
|
||||
# Build with no cache
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
#### Testing Container Changes
|
||||
|
||||
```bash
|
||||
# Start containers
|
||||
docker-compose up -d
|
||||
|
||||
# Check logs
|
||||
docker-compose logs -f astro
|
||||
docker-compose logs -f storybook
|
||||
|
||||
# Test health endpoints
|
||||
curl http://localhost:3000/health
|
||||
curl http://localhost:6006/health
|
||||
|
||||
# Stop containers
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Debugging Containers
|
||||
|
||||
#### Interactive Shell Access
|
||||
|
||||
```bash
|
||||
# Access running container
|
||||
docker-compose exec astro sh
|
||||
docker-compose exec storybook sh
|
||||
|
||||
# Or start debug container
|
||||
docker run -it --entrypoint=/bin/sh $(docker-compose config | grep image: | head -1 | cut -d: -f2-)
|
||||
```
|
||||
|
||||
#### Common Debug Commands
|
||||
|
||||
```bash
|
||||
# Inside container:
|
||||
whoami # Check user
|
||||
nginx -t # Test nginx config
|
||||
ls -la /usr/share/nginx/html/ # Check files
|
||||
ps aux # Check processes
|
||||
netstat -tlnp # Check ports
|
||||
```
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
docker/
|
||||
├── astro/
|
||||
│ └── Dockerfile # Astro site container
|
||||
├── storybook/
|
||||
│ └── Dockerfile # Storybook container
|
||||
└── nginx/
|
||||
├── nginx.conf # Main nginx config
|
||||
├── astro.conf # Astro site config
|
||||
└── storybook.conf # Storybook config
|
||||
```
|
||||
|
||||
### Build Context
|
||||
|
||||
The `.dockerignore` file optimizes build context:
|
||||
- Excludes `node_modules/`, `dist/`, build artifacts
|
||||
- Includes source files, package files, configs
|
||||
- Reduces build time and image size
|
||||
|
||||
### Multi-stage Build Benefits
|
||||
|
||||
1. **Smaller Images**: Only runtime dependencies in final image
|
||||
2. **Security**: No build tools in production image
|
||||
3. **Caching**: Efficient layer caching for faster builds
|
||||
4. **Consistency**: Same build environment across all environments
|
||||
|
||||
## nginx Configuration
|
||||
|
||||
### Security Features
|
||||
|
||||
- Non-root user execution
|
||||
- Security headers (HSTS, CSP, etc.)
|
||||
- Rate limiting capabilities
|
||||
- Access controls for sensitive paths
|
||||
|
||||
### Performance Optimizations
|
||||
|
||||
- Gzip compression for assets
|
||||
- Caching headers for static files
|
||||
- Connection keep-alive
|
||||
- Efficient static file serving
|
||||
|
||||
### Configuration Customization
|
||||
|
||||
Edit files in `docker/nginx/`:
|
||||
- `nginx.conf`: Global nginx settings
|
||||
- `astro.conf`: Astro-specific server block
|
||||
- `storybook.conf`: Storybook-specific server block
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
The workflows are triggered by semantic versioning:
|
||||
|
||||
1. **Development**: Push to feature branches
|
||||
2. **Pre-release**: Merge to `next` branch
|
||||
3. **Release**: Manual dispatch or release branch
|
||||
|
||||
### Build Process
|
||||
|
||||
1. Install dependencies with pnpm
|
||||
2. Build Astro static site
|
||||
3. Build Storybook static site
|
||||
4. Create semantic release
|
||||
5. Build and push Docker images (if release created)
|
||||
|
||||
### Image Tagging
|
||||
|
||||
Images follow semantic versioning:
|
||||
- Release: `latest`, `v1.2.3`, `v1.2`, `v1`
|
||||
- Pre-release: `latest-next`, `v1.2.3-next.1`, etc.
|
||||
|
||||
## Testing
|
||||
|
||||
### Container Testing
|
||||
|
||||
```bash
|
||||
# Test image builds
|
||||
docker build -f docker/astro/Dockerfile -t test-astro .
|
||||
docker build -f docker/storybook/Dockerfile -t test-storybook .
|
||||
|
||||
# Test containers run
|
||||
docker run -p 3000:8080 test-astro &
|
||||
docker run -p 6006:8080 test-storybook &
|
||||
|
||||
# Test endpoints
|
||||
curl -f http://localhost:3000/
|
||||
curl -f http://localhost:6006/
|
||||
curl -f http://localhost:3000/health
|
||||
curl -f http://localhost:6006/health
|
||||
|
||||
# Cleanup
|
||||
docker stop $(docker ps -q)
|
||||
```
|
||||
|
||||
### Security Testing
|
||||
|
||||
```bash
|
||||
# Scan images for vulnerabilities
|
||||
docker scout cves test-astro
|
||||
docker scout cves test-storybook
|
||||
|
||||
# Check for non-root execution
|
||||
docker run --rm test-astro whoami # Should output: nginx-user
|
||||
|
||||
# Test file permissions
|
||||
docker run --rm test-astro ls -la /usr/share/nginx/html/
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Image Size Optimization
|
||||
|
||||
Current optimizations:
|
||||
- Multi-stage builds
|
||||
- Alpine Linux base images
|
||||
- Minimal runtime dependencies
|
||||
- Optimized layer ordering
|
||||
|
||||
### Build Performance
|
||||
|
||||
- Layer caching in GitHub Actions
|
||||
- Efficient `.dockerignore`
|
||||
- Parallel builds for multiple platforms
|
||||
- Registry cache utilization
|
||||
|
||||
### Runtime Performance
|
||||
|
||||
- nginx optimizations for static content
|
||||
- Gzip compression
|
||||
- Proper caching headers
|
||||
- Health check optimizations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
1. **pnpm version mismatch**:
|
||||
```bash
|
||||
# Update Dockerfile pnpm version
|
||||
RUN corepack prepare pnpm@9.12.3 --activate
|
||||
```
|
||||
|
||||
2. **Permission issues**:
|
||||
```bash
|
||||
# Check user creation in Dockerfile
|
||||
RUN adduser -S -D -H -u 1001 nginx-user
|
||||
```
|
||||
|
||||
3. **Missing dependencies**:
|
||||
```bash
|
||||
# Ensure all deps in package.json
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
### Common Runtime Issues
|
||||
|
||||
1. **Container won't start**:
|
||||
```bash
|
||||
docker logs container-name
|
||||
# Check nginx config: nginx -t
|
||||
```
|
||||
|
||||
2. **404 errors**:
|
||||
```bash
|
||||
# Check file permissions and paths
|
||||
docker exec container ls -la /usr/share/nginx/html/
|
||||
```
|
||||
|
||||
3. **Health check failures**:
|
||||
```bash
|
||||
# Test health endpoint manually
|
||||
curl -v http://localhost:8080/health
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
```bash
|
||||
# Monitor resource usage
|
||||
docker stats
|
||||
|
||||
# Check nginx access logs
|
||||
docker exec container tail -f /var/log/nginx/access.log
|
||||
|
||||
# Profile application
|
||||
docker exec container top
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
### Making Changes to Containers
|
||||
|
||||
1. Modify Docker configurations
|
||||
2. Test locally with `docker-compose up --build`
|
||||
3. Validate with security and performance tests
|
||||
4. Update documentation
|
||||
5. Create pull request
|
||||
|
||||
### Release Process
|
||||
|
||||
1. Changes merged to `next` create pre-releases
|
||||
2. Manual workflow dispatch creates stable releases
|
||||
3. Both trigger automated container builds
|
||||
4. Images are automatically tagged and pushed
|
||||
|
||||
---
|
||||
|
||||
For more detailed deployment information, see [container-deployment.md](./container-deployment.md).
|
||||
+23
-188
@@ -1,207 +1,36 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier/flat';
|
||||
import { baseConfig } from '@ermnvldmr/eslint-config';
|
||||
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import jsdoc from 'eslint-plugin-jsdoc';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import storybook from 'eslint-plugin-storybook';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
...baseConfig,
|
||||
|
||||
// Additional ignores for monorepo
|
||||
{
|
||||
ignores: [
|
||||
'**/dist',
|
||||
'**/node_modules',
|
||||
'**/.astro',
|
||||
'**/.github',
|
||||
'**/.changeset',
|
||||
'**/coverage',
|
||||
'**/storybook-static',
|
||||
'.config/**/*',
|
||||
'*.config.{js,ts,mjs,cjs}',
|
||||
'astro.config.mjs',
|
||||
'eslint.config.mjs',
|
||||
'jest.config.cjs',
|
||||
'release.config.mjs',
|
||||
'!.storybook', // Allow linting of .storybook directory
|
||||
'services/docs/**/*', // Hugo content, no linting needed
|
||||
],
|
||||
},
|
||||
|
||||
// JavaScript
|
||||
eslint.configs.recommended,
|
||||
|
||||
// TypeScript - Strict Configuration (only for .ts/.tsx files)
|
||||
{
|
||||
files: ['**/*.{ts,tsx,mts,cts}'],
|
||||
extends: [
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// TypeScript strict rules
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', disallowTypeAnnotations: false },
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{ allowExpressions: true, allowTypedFunctionExpressions: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
// Allow triple-slash references in `*.d.ts` files.
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Import/Export Rules
|
||||
{
|
||||
plugins: { import: importPlugin },
|
||||
rules: {
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling'],
|
||||
'index',
|
||||
'type',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-unused-modules': 'error',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-self-import': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// JSDoc Requirements - Only for main source code
|
||||
{
|
||||
files: ['src/components/**/*.{ts,tsx}', 'src/shared/**/*.{ts,tsx}', 'src/www/**/*.{ts,tsx}'],
|
||||
plugins: { jsdoc },
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': [
|
||||
'error',
|
||||
{
|
||||
contexts: [
|
||||
'ExportNamedDeclaration > FunctionDeclaration',
|
||||
'ExportDefaultDeclaration > FunctionDeclaration',
|
||||
// Exclude VariableDeclaration for React components (memo wrapped)
|
||||
// React components should document via Props interface instead
|
||||
'TSInterfaceDeclaration',
|
||||
'TSTypeAliasDeclaration',
|
||||
],
|
||||
},
|
||||
],
|
||||
'jsdoc/require-description': 'error',
|
||||
'jsdoc/require-param': 'error',
|
||||
'jsdoc/require-returns': 'error',
|
||||
'jsdoc/require-example': 'warn',
|
||||
'jsdoc/check-alignment': 'error',
|
||||
'jsdoc/check-indentation': 'error',
|
||||
'jsdoc/check-syntax': 'error',
|
||||
'jsdoc/check-tag-names': 'error',
|
||||
'jsdoc/check-types': 'error',
|
||||
'jsdoc/valid-types': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// Astro
|
||||
...eslintPluginAstro.configs.recommended,
|
||||
{
|
||||
files: ['services/www/**/*.astro'],
|
||||
rules: {
|
||||
// Astro specific rules can go here
|
||||
},
|
||||
},
|
||||
|
||||
// Storybook - recommended configuration for flat config
|
||||
...storybook.configs['flat/recommended'],
|
||||
|
||||
// Set globals for Node scripts.
|
||||
{
|
||||
files: ['scripts/**'],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
|
||||
// Test files
|
||||
{
|
||||
files: ['**/*.{test,spec}.{js,jsx,ts,tsx}', '.config/jest/**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Relax rules for test files and jest configuration
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'react-refresh/only-export-components': 'off', // Allow re-exports in test utils
|
||||
},
|
||||
},
|
||||
|
||||
// CommonJS files - disable all TypeScript rules
|
||||
{
|
||||
files: ['**/*.cjs'],
|
||||
...tseslint.configs.disableTypeChecked,
|
||||
},
|
||||
|
||||
// Config files and tooling
|
||||
{
|
||||
files: ['*.config.{js,ts,mjs,cjs}', '.storybook/**/*.{js,ts,cjs,mjs}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'no-undef': 'off', // For CommonJS files
|
||||
},
|
||||
},
|
||||
|
||||
// Exclude shadcn directory from strict rules
|
||||
{
|
||||
files: ['src/shadcn/**/*.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// React - Strict Configuration
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
@@ -223,7 +52,7 @@ export default tseslint.config(
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with React 17+
|
||||
'react/prop-types': 'off', // Using TypeScript instead
|
||||
'react/display-name': 'error',
|
||||
'react/no-array-index-key': 'warn', // Changed from error to warn
|
||||
'react/no-array-index-key': 'warn',
|
||||
'react/no-danger': 'error',
|
||||
'react/no-deprecated': 'error',
|
||||
'react/no-direct-mutation-state': 'error',
|
||||
@@ -261,13 +90,14 @@ export default tseslint.config(
|
||||
plugins: { 'react-hooks': reactHooks },
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error', // Changed from warn to error
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// React Refresh
|
||||
{
|
||||
files: ['**/*.{jsx,tsx}'],
|
||||
ignores: ['.config/jest/**/*.{jsx,tsx}'], // Ignore jest config files
|
||||
ignores: ['.config/jest/**/*.{jsx,tsx}'],
|
||||
plugins: { 'react-refresh': reactRefresh },
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
@@ -300,6 +130,11 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier
|
||||
prettier
|
||||
// Set globals for Node scripts.
|
||||
{
|
||||
files: ['scripts/**'],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,36 +0,0 @@
|
||||
const config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/.config/jest/setup.ts'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@components/(.*)$': '<rootDir>/src/components/$1',
|
||||
'^@www/(.*)$': '<rootDir>/src/www/$1',
|
||||
'^@shared/(.*)$': '<rootDir>/src/shared/$1',
|
||||
'^@test/(.*)$': '<rootDir>/.config/jest/$1',
|
||||
},
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
|
||||
'<rootDir>/src/**/*.(test|spec).{js,jsx,ts,tsx}',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx,ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.stories.{js,jsx,ts,tsx}',
|
||||
'!.config/**',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': ['ts-jest', {
|
||||
tsconfig: {
|
||||
jsx: 'react-jsx',
|
||||
},
|
||||
}],
|
||||
},
|
||||
modulePathIgnorePatterns: ['<rootDir>/dist'],
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
+36
-99
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "www.ermnvldmr.com",
|
||||
"name": "ermnvldmr.com",
|
||||
"version": "1.0.0",
|
||||
"description": "Thoughts and creations of mine!",
|
||||
"description": "ermnvldmr.com monorepo",
|
||||
"author": "Vladimir Eremin <personal@ermnvldmr.com> (https://ermnvldmr.com)",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -13,108 +13,45 @@
|
||||
"url": "https://github.com/deytenit/www.ermnvldmr.com/issues"
|
||||
},
|
||||
"homepage": "https://github.com/deytenit/www.ermnvldmr.com#readme",
|
||||
"files": [
|
||||
"src/",
|
||||
"public/"
|
||||
],
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"pnpm": ">=9.12.3"
|
||||
},
|
||||
"scripts": {
|
||||
"ui:dev": "pnpm --filter @ermnvldmr/ui dev",
|
||||
"ui:build": "pnpm --filter @ermnvldmr/ui build",
|
||||
"ui:test": "pnpm --filter @ermnvldmr/ui test",
|
||||
"ui:storybook": "pnpm --filter @ermnvldmr/ui storybook",
|
||||
"www:dev": "pnpm --filter @ermnvldmr/www dev",
|
||||
"www:build": "pnpm --filter @ermnvldmr/www build",
|
||||
"www:storybook": "pnpm --filter @ermnvldmr/www storybook",
|
||||
"docs:dev": "pnpm --filter @ermnvldmr/docs dev",
|
||||
"docs:build": "pnpm --filter @ermnvldmr/docs build",
|
||||
"dev:ui": "pnpm ui:dev",
|
||||
"dev:www": "pnpm ui:build && pnpm www:dev",
|
||||
"dev:docs": "pnpm docs:dev",
|
||||
"build": "pnpm ui:build && pnpm -r --filter='./services/*' build",
|
||||
"build:storybooks": "pnpm ui:build && pnpm --filter @ermnvldmr/ui build-storybook && pnpm --filter @ermnvldmr/www build-storybook",
|
||||
"lint": "pnpm -r lint",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test": "pnpm -r test",
|
||||
"test:coverage": "pnpm -r test:coverage",
|
||||
"clean": "pnpm -r clean && rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@storybook/react-vite>vite": "^7.0.0",
|
||||
"@storybook/builder-vite>vite": "^7.0.0",
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript>vite": "^7.0.0",
|
||||
"@tailwindcss/vite>vite": "^7.0.0"
|
||||
"@tailwindcss/vite>vite": "^7.0.0",
|
||||
"vite-plugin-istanbul>vite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"lint": "eslint --fix .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:storybook": "test-storybook --coverage",
|
||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"http-server storybook-static --port 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && pnpm run test:storybook\"",
|
||||
"check": "npm run lint && npm run typecheck && npm run test",
|
||||
"clean": "rm -rf dist .turbo node_modules coverage",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:preview": "http-server storybook-static --port 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.2.5",
|
||||
"@astrojs/react": "^4.2.5",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"astro": "^5.7.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-aria": "^3.45.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"suncalc": "^1.9.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@jest/globals": "^30.2.0",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
"@storybook/addon-themes": "^8.6.15",
|
||||
"@storybook/react": "^8.6.15",
|
||||
"@storybook/react-vite": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@storybook/test-runner": "^0.24.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/suncalc": "^1.9.2",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsdoc": "^61.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-storybook": "^0.10.2",
|
||||
"globals": "^16.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"playwright": "^1.57.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"semantic-release": "^24.2.3",
|
||||
"semantic-release-export-data": "^1.2.0",
|
||||
"storybook": "^8.6.15",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tw-animate-css": "^1.2.8",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.1",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-istanbul": "^7.2.1",
|
||||
"wait-on": "^8.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier/flat';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import jsdoc from 'eslint-plugin-jsdoc';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
/**
|
||||
* Base ESLint configuration
|
||||
*/
|
||||
export const baseConfig = tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
'**/dist',
|
||||
'**/node_modules',
|
||||
'**/.github',
|
||||
'**/.changeset',
|
||||
'**/coverage',
|
||||
'.config/**/*',
|
||||
'*.config.{js,ts,mjs,cjs}',
|
||||
'!.storybook', // Allow linting of .storybook directory
|
||||
],
|
||||
},
|
||||
|
||||
// JavaScript
|
||||
eslint.configs.recommended,
|
||||
|
||||
// TypeScript - Strict Configuration (only for .ts/.tsx files)
|
||||
{
|
||||
files: ['**/*.{ts,tsx,mts,cts}'],
|
||||
extends: [
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// TypeScript strict rules
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', disallowTypeAnnotations: false },
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{ allowExpressions: true, allowTypedFunctionExpressions: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Allow triple-slash references in `*.d.ts` files.
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Import/Export Rules
|
||||
{
|
||||
plugins: { import: importPlugin },
|
||||
rules: {
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling'],
|
||||
'index',
|
||||
'type',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-unused-modules': 'error',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-self-import': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// JSDoc Requirements - Only for main source code
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}', 'packages/**/*.{ts,tsx}', 'services/**/*.{ts,tsx}'],
|
||||
plugins: { jsdoc },
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': [
|
||||
'error',
|
||||
{
|
||||
contexts: [
|
||||
'ExportNamedDeclaration > FunctionDeclaration',
|
||||
'ExportDefaultDeclaration > FunctionDeclaration',
|
||||
'TSInterfaceDeclaration',
|
||||
'TSTypeAliasDeclaration',
|
||||
],
|
||||
},
|
||||
],
|
||||
'jsdoc/require-description': 'error',
|
||||
'jsdoc/require-param': 'error',
|
||||
'jsdoc/require-returns': 'error',
|
||||
'jsdoc/require-example': 'warn',
|
||||
'jsdoc/check-alignment': 'error',
|
||||
'jsdoc/check-indentation': 'error',
|
||||
'jsdoc/check-syntax': 'error',
|
||||
'jsdoc/check-tag-names': 'error',
|
||||
'jsdoc/check-types': 'error',
|
||||
'jsdoc/valid-types': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// Test files
|
||||
{
|
||||
files: ['**/*.{test,spec}.{js,jsx,ts,tsx}', '.config/jest/**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Relax rules for test files and jest configuration
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// CommonJS files - disable all TypeScript rules
|
||||
{
|
||||
files: ['**/*.cjs'],
|
||||
...tseslint.configs.disableTypeChecked,
|
||||
},
|
||||
|
||||
// Config files and tooling
|
||||
{
|
||||
files: ['*.config.{js,ts,mjs,cjs}', '.storybook/**/*.{js,ts,cjs,mjs}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'no-undef': 'off', // For CommonJS files
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier
|
||||
prettier
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@ermnvldmr/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared ESLint configuration",
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"files": [
|
||||
"index.js",
|
||||
"react.js",
|
||||
"astro.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsdoc": "^61.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-storybook": "^0.10.2",
|
||||
"globals": "^16.0.0",
|
||||
"typescript-eslint": "^8.31.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@ermnvldmr/jest-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared Jest configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./base": "./dist/base.js",
|
||||
"./react": "./dist/react.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/globals": "^30.2.0",
|
||||
"jest": "^30.2.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"@types/jest": "^30.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"jest-environment-jsdom": "^30.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
/**
|
||||
* Base Jest configuration for TypeScript projects.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { baseJestConfig } from '@ermnvldmr/jest-config/base';
|
||||
*
|
||||
* export default {
|
||||
* ...baseJestConfig,
|
||||
* // package-specific overrides
|
||||
* } satisfies Config;
|
||||
* ```
|
||||
*/
|
||||
export const baseJestConfig: Config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.test.{ts,tsx}',
|
||||
'!src/**/*.stories.{ts,tsx}',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.{ts,tsx}',
|
||||
'**/*.{test,spec}.{ts,tsx}',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Jest configuration exports for the monorepo.
|
||||
*/
|
||||
|
||||
export { baseJestConfig } from './base.js';
|
||||
export { reactJestConfig } from './react.js';
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Config } from 'jest';
|
||||
import { baseJestConfig } from './base.js';
|
||||
|
||||
/**
|
||||
* Jest configuration for React projects with jsdom environment.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { reactJestConfig } from '@ermnvldmr/jest-config/react';
|
||||
*
|
||||
* export default reactJestConfig;
|
||||
* ```
|
||||
*/
|
||||
export const reactJestConfig: Config = {
|
||||
...baseJestConfig,
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
||||
moduleNameMapper: {
|
||||
...baseJestConfig.moduleNameMapper,
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
},
|
||||
transform: {
|
||||
...baseJestConfig.transform,
|
||||
'^.+\\.(js|jsx|ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { baseJestConfig } from '@ermnvldmr/jest-config/base';
|
||||
|
||||
export default baseJestConfig;
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@ermnvldmr/stl",
|
||||
"version": "1.0.0",
|
||||
"description": "Common reusable dependencies",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/jest-config": "workspace:*",
|
||||
"@ermnvldmr/vite-config": "workspace:*",
|
||||
"jest": "^30.2.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^4.1.4"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,22 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Combines and merges CSS class names using clsx and tailwind-merge.
|
||||
*
|
||||
* This utility function combines the power of clsx for conditional class handling
|
||||
* with tailwind-merge for proper Tailwind CSS class deduplication and conflict resolution.
|
||||
*
|
||||
* @param inputs - Class values to combine (strings, objects, arrays, etc.)
|
||||
* @returns Merged class string with conflicts resolved
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* cn('px-4 py-2', 'bg-blue-500') // 'px-4 py-2 bg-blue-500'
|
||||
* cn('px-4', { 'px-8': true }) // 'px-8' (px-8 overrides px-4)
|
||||
* cn('text-red-500', undefined, 'text-blue-500') // 'text-blue-500'
|
||||
* ```
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './functions';
|
||||
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Shared type definitions for common patterns.
|
||||
*/
|
||||
|
||||
export type { ClassNameProps } from './classNameProps';
|
||||
export type { TestIdProps } from './testIdProps';
|
||||
File renamed without changes.
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "**/*.test.ts", "**/*.test.tsx", "**/*.stories.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createLibConfig } from '@ermnvldmr/vite-config/lib';
|
||||
|
||||
export default createLibConfig('ErmnvldmrSTL', 'src/index.ts');
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@ermnvldmr/storybook-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared Storybook configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/main.js",
|
||||
"exports": {
|
||||
".": "./dist/main.js",
|
||||
"./preview": "./dist/preview.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
"@storybook/addon-themes": "^8.6.15",
|
||||
"@storybook/react": "^8.6.15",
|
||||
"@storybook/react-vite": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"vite-plugin-istanbul": "^7.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { basePreviewConfig } from './preview.js';
|
||||
|
||||
export { baseStorybookConfig } from './main.js';
|
||||
export { basePreviewConfig };
|
||||
@@ -0,0 +1,48 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
export const baseStorybookConfig: Partial<StorybookConfig> = {
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-themes',
|
||||
'@storybook/addon-coverage',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
check: false,
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
|
||||
},
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
try {
|
||||
const tailwindcss = (await import('@tailwindcss/vite')).default;
|
||||
const istanbul = (await import('vite-plugin-istanbul')).default;
|
||||
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
istanbul({
|
||||
include: 'src/**/*',
|
||||
exclude: ['node_modules', 'test/', '**/*.stories.tsx', '**/*.test.tsx'],
|
||||
extension: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
requireEnv: false,
|
||||
cypress: false,
|
||||
forceBuildInstrument: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback if plugins can't be loaded
|
||||
console.warn('Failed to load Storybook plugins:', error);
|
||||
return config;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,20 @@
|
||||
import { withThemeByClassName } from '@storybook/addon-themes';
|
||||
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../src/global.css';
|
||||
|
||||
const preview: Preview = {
|
||||
/**
|
||||
* Base preview configuration for theme support.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { basePreviewConfig } from '@ermnvldmr/storybook-config/preview';
|
||||
*
|
||||
* export default {
|
||||
* ...basePreviewConfig,
|
||||
* // additional overrides
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export const basePreviewConfig: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
@@ -35,5 +46,3 @@ const preview: Preview = {
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-themes',
|
||||
'@storybook/addon-coverage',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
check: false,
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../src/styles/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { reactJestConfig } from '@ermnvldmr/jest-config/react';
|
||||
|
||||
export default reactJestConfig;
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "@ermnvldmr/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared UI components and design system",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./styles": "./dist/styles/index.css"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "tsc && vite build",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build",
|
||||
"test:storybook": "@storybook/test-runner",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist coverage storybook-static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ermnvldmr/stl": "workspace:*",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-aria": "^3.45.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"tw-animate-css": "^1.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/jest-config": "workspace:*",
|
||||
"@ermnvldmr/storybook-config": "workspace:*",
|
||||
"@ermnvldmr/vite-config": "workspace:*",
|
||||
"@storybook/react": "^8.6.15",
|
||||
"@storybook/react-vite": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"storybook": "^8.6.15",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-istanbul": "^7.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+2
-3
@@ -1,8 +1,7 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@/shared/types';
|
||||
|
||||
import { cn } from '@/shared/helpers';
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
|
||||
/**
|
||||
* Tailwind gap spacing scale values.
|
||||
File renamed without changes.
@@ -0,0 +1,2 @@
|
||||
export { HStack } from './HStack/HStack';
|
||||
export { VStack } from './VStack/VStack';
|
||||
File renamed without changes.
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@test/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { render, screen } from '../../../test-utils';
|
||||
|
||||
/**
|
||||
* Test suite for the newspaper-style Button component
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useButton } from 'react-aria';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@/shared/types';
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
import type { AriaButtonProps } from 'react-aria';
|
||||
|
||||
/**
|
||||
File renamed without changes.
+4
-3
@@ -1,9 +1,10 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@/shared/types';
|
||||
import { VStack } from '../../generic/VStack/VStack';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
|
||||
import { VStack } from '@/components/shared/VStack/VStack';
|
||||
import { cn } from '@/shared/helpers';
|
||||
|
||||
/**
|
||||
* Props for the newspaper-style Separator component.
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Button } from './PaperButton/Button'
|
||||
export { Separator } from './PaperSeparator/Separator'
|
||||
@@ -0,0 +1,6 @@
|
||||
// Export all components
|
||||
export * from './components/paper';
|
||||
export * from './components/generic';
|
||||
|
||||
// Import styles to ensure they're bundled
|
||||
import './styles/index.css';
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
File renamed without changes.
@@ -0,0 +1,31 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import type { RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
* Custom render function that sets up any required providers
|
||||
* @param ui - React component to render
|
||||
* @param options - Render options excluding wrapper
|
||||
* @returns RenderResult from testing library
|
||||
* @example
|
||||
* ```tsx
|
||||
* render(<Button>Click me</Button>);
|
||||
* ```
|
||||
*/
|
||||
function customRender(
|
||||
ui: ReactElement,
|
||||
options?: Omit<RenderOptions, 'wrapper'>
|
||||
): RenderResult {
|
||||
return render(ui, {
|
||||
// Add any providers here if needed in the future
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Re-export everything from testing library
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from '@testing-library/react';
|
||||
|
||||
// Override render method
|
||||
export { customRender as render };
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "**/*.test.ts", "**/*.test.tsx", "**/*.stories.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createLibConfig } from '@ermnvldmr/vite-config/lib';
|
||||
|
||||
export default createLibConfig('ErmnvldmrUI', 'src/index.ts');
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@ermnvldmr/vite-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared Vite configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./lib": "./dist/lib.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-dts": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
"@types/node": "^25.0.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Vite configuration utilities for the monorepo.
|
||||
*/
|
||||
|
||||
export { createLibConfig } from './lib.js';
|
||||
@@ -0,0 +1,65 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import type { UserConfig } from 'vite';
|
||||
|
||||
/**
|
||||
* Base Vite configuration for library packages.
|
||||
*
|
||||
* @param packageName - The name for the library build
|
||||
* @param entry - Entry point file path (relative to package root)
|
||||
* @param external - Array of external dependencies to exclude from bundle
|
||||
* @returns Vite configuration object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createLibConfig } from '@ermnvldmr/vite-config/lib';
|
||||
*
|
||||
* export default createLibConfig('MyLib', 'src/index.ts', ['react', 'react-dom']);
|
||||
* ```
|
||||
*/
|
||||
export function createLibConfig(
|
||||
packageName: string,
|
||||
entry: string = 'src/index.ts',
|
||||
external: string[] = []
|
||||
): UserConfig {
|
||||
return defineConfig({
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(process.cwd(), entry),
|
||||
name: packageName,
|
||||
formats: ['es'],
|
||||
fileName: 'index',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react/jsx-runtime',
|
||||
'react/jsx-dev-runtime',
|
||||
'tailwindcss',
|
||||
...external
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'react/jsx-runtime': 'React',
|
||||
'react/jsx-dev-runtime': 'React',
|
||||
},
|
||||
},
|
||||
},
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(process.cwd(), 'src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Generated
+4742
-5131
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
# packages
|
||||
- 'packages/*'
|
||||
# services
|
||||
- 'services/*'
|
||||
@@ -1,12 +0,0 @@
|
||||
export default {
|
||||
branches: [
|
||||
{ name: 'next', prerelease: true },
|
||||
{ name: 'release/*', channel: false },
|
||||
],
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/github',
|
||||
'semantic-release-export-data',
|
||||
],
|
||||
};
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "Ermnvldmr Root Documentation"
|
||||
description: "Documentation for the root.ermnvldmr.com infrastructure"
|
||||
type: docs
|
||||
---
|
||||
|
||||
Various notes made during the development of personal infrastructure.
|
||||
|
||||
## Sections
|
||||
|
||||
- **[Recipes](/recipes/)** -- Step-by-step instructions,
|
||||
that I don't want to remember each time I need one.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "Recipes"
|
||||
description: "Step-by-step instructions, that I don't want to remember each time I need one."
|
||||
weight: 50
|
||||
type: docs
|
||||
---
|
||||
|
||||
Step-by-step instructions,
|
||||
that I don't want to remember each time I need one.
|
||||
|
||||
## What's in here
|
||||
|
||||
### [Initial Host Setup](/recipes/initial-setup)
|
||||
|
||||
Setting up a new host on Debian from scratch.
|
||||
|
||||
### [Xray Client Setup](/ru/recipes/xray-setup) (RU only)
|
||||
|
||||
Setting up xray client without any further usage in mind.
|
||||
|
||||
## Why “Recipes”?
|
||||
|
||||
These are not abstract troubleshooting guides.
|
||||
These are specific actions you will need to perform.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
title: "Initial Host Setup"
|
||||
description: "Setting up a fresh Debian host from scratch"
|
||||
weight: 1
|
||||
type: docs
|
||||
---
|
||||
|
||||
How to set up a fresh Debian host from zero to deployment.
|
||||
|
||||
## Add Adam User and Wheel Group
|
||||
|
||||
Adam is a core user on each of the system.
|
||||
It does not has password,
|
||||
hence only connection via ssh is available,
|
||||
providing a password-less sudo.
|
||||
|
||||
```bash
|
||||
# Add adam user
|
||||
useradd -m -s '/bin/bash' adam
|
||||
|
||||
# Add wheel group for password-less sudo
|
||||
groupadd wheel
|
||||
vi /etc/sudoers.d/99-root-ermnvldmr
|
||||
```
|
||||
|
||||
Fill file with the content:
|
||||
|
||||
```visudo
|
||||
%wheel ALL = (ALL) NOPASSWD: ALL
|
||||
```
|
||||
|
||||
## Configure SSH
|
||||
|
||||
Hardening SSHD,
|
||||
even stupidly changing port.
|
||||
|
||||
```bash
|
||||
# Move public ssh key of user adam to the server
|
||||
vi /home/adam/.ssh/authorized_keys
|
||||
|
||||
# Create additional ssh rules
|
||||
vi /etc/ssh/sshd_config.d/99-root-ermnvldmr.conf
|
||||
```
|
||||
|
||||
With the following content:
|
||||
|
||||
```conf
|
||||
Port 2222
|
||||
PasswordAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
AllowUsers adam
|
||||
```
|
||||
|
||||
## Install Docker
|
||||
|
||||
Follow the official Docker installation guide for Debian --
|
||||
[docs.docker.com](https://docs.docker.com/engine/install/debian/)
|
||||
|
||||
Test Docker installation:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
## Clone the root.ermnvldmr.com Repository
|
||||
|
||||
```bash
|
||||
# Clone to your preferred location
|
||||
cd /srv
|
||||
sudo mkdir root.ermnvldmr.com
|
||||
sudo chown adam:adam root.ermnvldmr.com
|
||||
|
||||
# Clone the repository
|
||||
git clone git@github.com:deytenit/root.ermnvldmr.com.git root.ermnvldmr.com
|
||||
|
||||
# Run the repository initialization
|
||||
./init.sh
|
||||
```
|
||||
|
||||
## Setup node
|
||||
|
||||
> [!IMPORTANT]
|
||||
> From now on `$NODE` equals node name of the server you are setting up.
|
||||
|
||||
> [!NOTE]
|
||||
> More about root.ermnvldmr.com compositions repository -- [github.com](https://github.com/)
|
||||
|
||||
Run node-specific scripts in $NODE/.host/.scripts
|
||||
in order of their numbering:
|
||||
|
||||
```bash
|
||||
# For example for node daedalus
|
||||
|
||||
# Setting up the base components
|
||||
./daedalus/.host/.scripts/00-base
|
||||
# Setting up ufw with rules defined in ./daedalus/.host/ufw
|
||||
./daedalus/.host/.scripts/10-ufw
|
||||
# Setting up crowdsec with rules defined in ./daedalus/.host/crowd
|
||||
./daedalus/.host/.scripts/20-crowd
|
||||
# Setting up cron with tasks defined in ./daedalus/.host/cron
|
||||
./daedalus/.host/.scripts/30-cron
|
||||
```
|
||||
|
||||
## Setup Storage Tiers
|
||||
|
||||
You need to decide where to store your data tiers:
|
||||
|
||||
```bash
|
||||
# Create storage directories
|
||||
sudo mkdir -p /srv/com-ermnvldmr-root-$NODE-{tier1,tier2,tier3}
|
||||
|
||||
# Setup tiers for the node
|
||||
.scripts/ops/setup-tiers $NODE \
|
||||
/srv/com-ermnvldmr-root-$NODE-tier1 \
|
||||
/srv/com-ermnvldmr-root-$NODE-tier2 \
|
||||
/srv/com-ermnvldmr-root-$NODE-tier3
|
||||
```
|
||||
|
||||
## Setup noroot-users
|
||||
|
||||
To harden the containers running on the node,
|
||||
we create series of noroot-users and shared group
|
||||
for each project in the compose repository node directory.
|
||||
|
||||
```bash
|
||||
# Setup noroot-users for the node
|
||||
.scripts/ops/setup-noroot-users $NODE
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "root.ermnvldmr.com"
|
||||
description: "Документация к инфраструктуре root.ermnvldmr.com"
|
||||
type: docs
|
||||
---
|
||||
|
||||
Различные заметки, сделанные в процессе разработки этой инфраструктуры.
|
||||
|
||||
## Разделы
|
||||
|
||||
- **[Рецепты](/recipes/)** -- Пошаговые инструкции,
|
||||
которые мне не хочется изобретать каждый раз снова.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Рецепты"
|
||||
description: "Пошаговые инструкции, которые мне не хочется изобретать каждый раз снова."
|
||||
weight: 50
|
||||
type: docs
|
||||
---
|
||||
|
||||
Пошаговые инструкции,
|
||||
которые мне не хочется изобретать каждый раз снова.
|
||||
|
||||
## Что здесь
|
||||
|
||||
### [Настройка xray-клиента](/recipes/xray-setup)
|
||||
|
||||
Настройте xray-client без какого-либо дальнейшего предназначения.
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
title: "Настройка xray-клиента"
|
||||
description: "Дружелюбная инструкция по настройке xray-client без какого-либо предназначения"
|
||||
type: docs
|
||||
---
|
||||
|
||||
Данная инструкция поможет вам пошагово настроить клиент для работы через заданную конфигурацию соединения и особые правила маршрутизации (*routing*).
|
||||
|
||||
**Следуйте** каждому шагу внимательно.
|
||||
|
||||
> [!NOTE]
|
||||
> Инструкция предназначена для настройки xray-клиента без дальнейшего предназначения – мы не предоставляем услуги прокси.
|
||||
|
||||
## Необходимые данные
|
||||
|
||||
- Строка подключения формата:
|
||||
|
||||
```
|
||||
vless://...
|
||||
```
|
||||
|
||||
- Набор правил маршрутизации:
|
||||
|
||||
> [!WARNING]
|
||||
> Данный формат правил **подходит только** для **v2rayN/v2rayNG**!
|
||||
> **Для happ** правильная строка указана **в самой инструкции**.
|
||||
|
||||
```json
|
||||
[
|
||||
{"domain":["domain:ru","domain:su","domain:vk.com","domain:yandex.com"],"enabled":true,"locked":false,"outboundTag":"direct","remarks":"Domain RU - Direct"},
|
||||
{"domain":["geosite:private"],"enabled":true,"locked":false,"outboundTag":"direct","remarks":"Domain Private - Direct"},
|
||||
{"enabled":true,"ip":["geoip:ru"],"locked":false,"outboundTag":"direct","remarks":"IP RU - Direct"},
|
||||
{"enabled":true,"ip":["geoip:private"],"locked":false,"outboundTag":"direct","remarks":"IP Private - Direct"}
|
||||
]
|
||||
```
|
||||
|
||||
## Android (v2rayNG)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> NekoBox (MatsuriDayo) более не рекомендован к использованию из-за малой активности в разработке – **удалите** его.
|
||||
|
||||
> [!TIP]
|
||||
> Наглядный пример настройки — [на видео](/videos/recipes-xray-setup-v2rayng.webm)
|
||||
|
||||
### Шаг 1. Установите приложение
|
||||
|
||||
- **Скачайте** v2rayNG с [официального репозитория](https://github.com/2dust/v2rayNG/releases).
|
||||
Обычно нужен файл `v2rayNG_x.x.x_universal.apk`.
|
||||
- **Установите** скачанный `.apk` файл на Android-устройство.
|
||||
|
||||
### Шаг 2. Добавьте конфигурацию
|
||||
|
||||
- **Откройте** приложение v2rayNG.
|
||||
- **Нажмите** "+" в правом верхнем углу.
|
||||
- **Выберите** `Import config from Clipboard` (предварительно **скопируйте** ссылку конфигурации в буфер обмена).
|
||||
|
||||
### Шаг 3. Настройте правила маршрутизации
|
||||
|
||||
- **Вернитесь** на главный экран приложения.
|
||||
- **Нажмите** на "бургер-меню" (три полоски) в левом верхнем углу.
|
||||
- **Выберите** `Routing Settings`.
|
||||
- **Нажмите** на три точки в правом верхнем углу.
|
||||
- **Выберите** `Import ruleset from clipboard` (предварительно **скопируйте** [JSON с правилами](#необходимые-данные)).
|
||||
- **Нажмите** на Domain strategy и **выберите** `IPOnDemand`.
|
||||
|
||||
### Шаг 4. Запустите подключение
|
||||
|
||||
- **Вернитесь** на главный экран.
|
||||
- **Нажмите** кнопку ▶ для запуска соединения.
|
||||
|
||||
## Windows / MacOS / Linux (v2rayN)
|
||||
|
||||
### Шаг 1. Установите приложение
|
||||
|
||||
- **Скачайте** нужную версию приложения с [официального репозитория](https://github.com/2dust/v2rayN/releases):
|
||||
- Windows: `v2rayN-windows-64-desktop.zip`
|
||||
- Linux: `v2rayN-linux-64.zip`
|
||||
- MacOS: `v2rayN-macos-arm64.dmg`
|
||||
|
||||
> [!TIP]
|
||||
> **Чтобы увидеть** все файлы для скачивания **нажмите** на ссылку под файлами.
|
||||
>
|
||||
> 
|
||||
|
||||
#### Windows
|
||||
|
||||
- **Распакуйте** файл `.zip` в любое удобное место.
|
||||

|
||||
|
||||
- **Переместите** распакованную папку в более естественное место –
|
||||
**вырежте** папку.
|
||||

|
||||
|
||||
- Затем **вставьте** в папку `Документы`.
|
||||
|
||||

|
||||
|
||||
- В перемещённой папке **будет** лежать `v2rayN` приложение.
|
||||
|
||||
> [!TIP]
|
||||
> **Чтобы не потерять** приложение – **отправьте** ярлык на Рабочий Стол.
|
||||
>
|
||||
> 
|
||||
|
||||
#### Linux
|
||||
|
||||
Если вы **пользуетесь Linux**, я думаю, у вас достаточно знаний, чтобы **настроить всё самостоятельно**.
|
||||
|
||||
В этом разделе будут рассмотрены **частые проблемы** и **пути их решения**.
|
||||
|
||||
#### MacOS
|
||||
|
||||
- **Откройте** файл `.dmg` и **перетащите** приложение в папку "Программы".
|
||||
- **Откройте** терминал и **выполните** команду:
|
||||
|
||||
```
|
||||
sudo xattr -cr /Applications/v2rayN.app
|
||||
```
|
||||
|
||||
- После первого открытия, **Откройте** системные настройки:
|
||||
- **Выберете** пункт `Privacy & Security`
|
||||
- **Прокрутите** вниз до уведомления о блокировке "xray"
|
||||
- **Нажмите** `Allow Anyway`
|
||||
|
||||
### Шаг 2. Добавьте конфигурацию
|
||||
|
||||
- **Откройте** v2rayN.
|
||||
- **Нажмите** `Configurations` в левом верхнем углу окна.
|
||||
- **Выберите** `Import Share Links from clipboard (Ctrl+V)` (предварительно **скопируйте** конфигурационную ссылку).
|
||||

|
||||
|
||||
### Шаг 3. Настройте правила маршрутизации
|
||||
|
||||
- **Перейдите** в `Settings` → `Routing Settings`.
|
||||

|
||||
- В открывшемся окне:
|
||||
- **Нажмите** `Add` в верхнем левом углу.
|
||||
- В новом окне в поле Remarks **введите** произвольное имя, например: "RU - Direct".
|
||||
- В поле Domain strategy **выберите** `IPOnDemand`.
|
||||
- В вверху окна **нажмите** `Import Rules From Clipboard` (предварительно **скопируйте** [JSON с правилами](#необходимые-данные)).
|
||||
- В появившемся окне "Do you want to append rules? ..." – **нажмите** `No`.
|
||||

|
||||
|
||||
- **Нажимайте** `Confirm` до тех пор, пока не **вернётесь** в основной интерфейс.
|
||||
|
||||
### Шаг 4. Запустите подключение
|
||||
|
||||
- **Выберите** созданные правила маршрутизации из второго выпадающего списка снизу.
|
||||

|
||||
|
||||
- **Нажмите** `Enable Tun` для запуска соединения (**следуйте** запросам v2rayN).
|
||||
|
||||
## iOS / iPadOS (happ)
|
||||
|
||||
### Шаг 1. Установите приложение
|
||||
|
||||
- **Установите** _happ_ из [App Store](https://apps.apple.com/us/app/happ-proxy-utility/id6504287215).
|
||||
|
||||
### Шаг 2. Добавьте конфигурацию
|
||||
|
||||
- **Откройте** приложение.
|
||||
- **Нажмите** "+" в правом верхнем углу.
|
||||
- **Выберите** `Import from clipboard` (предварительно **скопируйте** ссылку конфигурации в буфер обмена).
|
||||
|
||||

|
||||
|
||||
### Шаг 3. Откройте настройки роутинга
|
||||
|
||||
- **Перейдите** по значку шестиренки в левом верхнем углу.
|
||||
- **Выберите** `Routing Rules`.
|
||||
|
||||

|
||||
|
||||
### Шаг 4. Импортируйте роутинг
|
||||
|
||||
- **Нажмите** `...` (три точки) в правом верхнем углу.
|
||||
- **Выберите** `Import from clipboard` (предварительно **скопируйте** правила, см. ниже).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Формат правил отличается от v2rayN/v2rayNG!
|
||||
> Эта строка - эквивалентна правилам сверху.
|
||||
>
|
||||
> ```
|
||||
> happ://routing/add/eyJEbnNIb3N0cyI6eyJkbnMuZ29vZ2xlIjoiOC44LjguOCIsImNsb3VkZmxhcmUtZG5zLmNvbSI6IjEuMS4xLjEifSwiQmxvY2tJcCI6W10sIkZha2VEbnMiOnRydWUsIlJlbW90ZUROU1R5cGUiOiJEb1UiLCJEb21lc3RpY0ROU0lwIjoiOC44LjguOCIsIkRpcmVjdFNpdGVzIjpbImRvbWFpbjpydSIsImRvbWFpbjpzdSIsImRvbWFpbjp2ay5jb20iLCJkb21haW46eWFuZGV4LmNvbSIsImdlb3NpdGU6cHJpdmF0ZSJdLCJHZW9pcFVybCI6Imh0dHBzOlwvXC9naXRodWIuY29tXC9Mb3lhbHNvbGRpZXJcL3YycmF5LXJ1bGVzLWRhdFwvcmVsZWFzZXNcL2xhdGVzdFwvZG93bmxvYWRcL2dlb2lwLmRhdCIsIkdsb2JhbFByb3h5Ijp0cnVlLCJEaXJlY3RJcCI6WyJnZW9pcDpydSIsImdlb2lwOnByaXZhdGUiXSwiUHJveHlTaXRlcyI6W10sIk5hbWUiOiJEZWZhdWx0IiwiR2Vvc2l0ZVVybCI6Imh0dHBzOlwvXC9naXRodWIuY29tXC9Mb3lhbHNvbGRpZXJcL3YycmF5LXJ1bGVzLWRhdFwvcmVsZWFzZXNcL2xhdGVzdFwvZG93bmxvYWRcL2dlb3NpdGUuZGF0IiwiQmxvY2tTaXRlcyI6W10sIkxhc3RVcGRhdGVkIjoxNzU5NDMxODk5LCJVc2VDaHVua0ZpbGVzIjp0cnVlLCJSZW1vdGVETlNEb21haW4iOiJodHRwczpcL1wvY2xvdWRmbGFyZS1kbnMuY29tXC9kbnMtcXVlcnkiLCJEb21haW5TdHJhdGVneSI6IklQSWZOb25NYXRjaCIsIkRvbWVzdGljRE5TRG9tYWluIjoiaHR0cHM6XC9cL2Rucy5nb29nbGVcL2Rucy1xdWVyeSIsIlJlbW90ZUROU0lwIjoiMS4xLjEuMSIsIlByb3h5SXAiOltdLCJEb21lc3RpY0ROU1R5cGUiOiJEb1UifQ==
|
||||
> ```
|
||||
|
||||

|
||||
|
||||
### Шаг 5. Роутинг установлен
|
||||
|
||||
- **После** настройки роутинга,
|
||||
содержимое экрана должно быть похоже на картинку снизу.
|
||||
- **Вернитесь** на главный экран.
|
||||
- **Нажмите** кнопку вкключения для запуска соединения.
|
||||
|
||||

|
||||
@@ -0,0 +1,134 @@
|
||||
baseURL: 'https://docs.root.ermnvldmr.com'
|
||||
defaultContentLanguage: 'en'
|
||||
defaultContentLanguageInSubdir: true
|
||||
title: 'root.ermnvldmr.com'
|
||||
description: 'Documentation for the root.ermnvldmr.com infrastructure'
|
||||
|
||||
# Multilingual configuration
|
||||
languages:
|
||||
en:
|
||||
languageCode: 'en-us'
|
||||
languageName: 'English'
|
||||
title: 'root.ermnvldmr.com'
|
||||
description: 'Documentation for the root.ermnvldmr.com infrastructure'
|
||||
weight: 1
|
||||
contentDir: 'content/en'
|
||||
menu:
|
||||
main:
|
||||
# - name: Architecture
|
||||
# url: /en/architecture/
|
||||
# weight: 10
|
||||
# - name: Operations
|
||||
# url: /en/operations/
|
||||
# weight: 20
|
||||
# - name: Concepts
|
||||
# url: /en/concepts/
|
||||
# weight: 30
|
||||
- name: Recipes
|
||||
url: /en/recipes/
|
||||
weight: 40
|
||||
- name: Search
|
||||
weight: 50
|
||||
params:
|
||||
type: search
|
||||
ru:
|
||||
languageCode: 'ru'
|
||||
languageName: 'Русский'
|
||||
title: 'root.ermnvldmr.com'
|
||||
description: 'Документация для инфраструктуры root.ermnvldmr.com'
|
||||
weight: 2
|
||||
contentDir: 'content/ru'
|
||||
menu:
|
||||
main:
|
||||
- name: Архитектура
|
||||
url: /ru/architecture/
|
||||
weight: 10
|
||||
- name: Операции
|
||||
url: /ru/operations/
|
||||
weight: 20
|
||||
- name: Концепции
|
||||
url: /ru/concepts/
|
||||
weight: 30
|
||||
- name: Рецепты
|
||||
url: /ru/recipes/
|
||||
weight: 40
|
||||
- name: Поиск
|
||||
weight: 50
|
||||
params:
|
||||
type: search
|
||||
|
||||
# Theme configuration
|
||||
theme: hextra
|
||||
|
||||
enableGitInfo: true
|
||||
|
||||
# Hextra theme parameters
|
||||
params:
|
||||
description: "Documentation for the root.ermnvldmr.com infrastructure"
|
||||
|
||||
# Search configuration
|
||||
search:
|
||||
enable: true
|
||||
type: flexsearch
|
||||
flexsearch:
|
||||
# index page by: content | summary | heading | title
|
||||
index: content
|
||||
# full | forward | reverse | strict
|
||||
# https://github.com/nextapps-de/flexsearch/#tokenizer-prefix-search
|
||||
tokenize: forward
|
||||
|
||||
displayUpdatedDate: true
|
||||
dateFormat: "January 2, 2006"
|
||||
|
||||
# Navigation configuration
|
||||
navbar:
|
||||
displayTitle: true
|
||||
width: wide
|
||||
displayLogo: false
|
||||
|
||||
# Footer configuration
|
||||
footer:
|
||||
displayCopyright: true
|
||||
displayPoweredBy: true
|
||||
|
||||
# Edit page configuration
|
||||
editURL:
|
||||
enable: true
|
||||
base: "https://github.com/deytenit/root.ermnvldmr.com/edit/trunk/docs/content"
|
||||
|
||||
page:
|
||||
width: wide
|
||||
|
||||
theme:
|
||||
default: system
|
||||
displayToggle: true
|
||||
|
||||
# Markdown configuration
|
||||
markup:
|
||||
highlight:
|
||||
style: github
|
||||
lineNos: true
|
||||
noClasses: false
|
||||
goldmark:
|
||||
renderer:
|
||||
unsafe: true
|
||||
|
||||
# Output formats
|
||||
outputs:
|
||||
home: [HTML]
|
||||
page: [HTML]
|
||||
section: [HTML]
|
||||
|
||||
# URL configuration to preserve existing structure
|
||||
permalinks:
|
||||
architecture: /architecture/:slug/
|
||||
operations: /operations/:slug/
|
||||
reference: /reference/:slug/
|
||||
troubleshooting: /troubleshooting/:slug/
|
||||
|
||||
# Deployment configuration
|
||||
deployment:
|
||||
targets:
|
||||
- name: production
|
||||
URL: docs.root.ermnvldmr.com
|
||||
cloudFrontDistributionID: # TBD during deployment
|
||||
@@ -0,0 +1 @@
|
||||
copyright: "© 2025 Eremin Vladimir. All rights reserved."
|
||||
@@ -0,0 +1,6 @@
|
||||
copyright: "© 2025 Ерёмин Владимир. Все права защищены."
|
||||
note: Кстати
|
||||
tip: Полезно
|
||||
important: Важно
|
||||
warning: Внимание
|
||||
caution: Осторожно
|
||||
@@ -0,0 +1,46 @@
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<h3>{{ .Get "name" }}</h3>
|
||||
<span class="container-name">{{ .Get "container" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="service-details">
|
||||
<div class="service-row">
|
||||
<strong>Image:</strong> <code>{{ .Get "image" }}</code>
|
||||
</div>
|
||||
|
||||
<div class="service-row">
|
||||
<strong>Purpose:</strong> {{ .Get "purpose" }}
|
||||
</div>
|
||||
|
||||
{{ with .Get "ports" }}
|
||||
<div class="service-row">
|
||||
<strong>Ports:</strong> <code>{{ . }}</code>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Get "networks" }}
|
||||
<div class="service-row">
|
||||
<strong>Networks:</strong> <code>{{ . }}</code>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Get "volumes" }}
|
||||
<div class="service-row">
|
||||
<strong>Volumes:</strong> <code>{{ . }}</code>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Get "features" }}
|
||||
<div class="service-row">
|
||||
<strong>Features:</strong> {{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Get "management_url" }}
|
||||
<div class="service-row">
|
||||
<strong>Management URL:</strong> <a href="{{ . }}" target="_blank">{{ . }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@ermnvldmr/docs",
|
||||
"version": "1.0.0",
|
||||
"description": "docs.root.ermnvldmr.com documentation site",
|
||||
"scripts": {
|
||||
"dev": "hugo server --buildDrafts --buildExpired",
|
||||
"build": "hugo --gc --minify",
|
||||
"clean": "rm -rf public resources",
|
||||
"lint": "echo 'No linting for Hugo site'",
|
||||
"typecheck": "echo 'No typecheck for Hugo site'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"hugo": ">=0.110.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
docs.root.ermnvldmr.com
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,2 @@
|
||||
/*! tailwindcss v4.1.8 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--hx-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--hx-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--hx-color-red-100:oklch(93.6% .032 17.717);--hx-color-red-200:oklch(88.5% .062 18.334);--hx-color-red-900:oklch(39.6% .141 25.723);--hx-color-orange-50:oklch(98% .016 73.684);--hx-color-orange-100:oklch(95.4% .038 75.164);--hx-color-orange-300:oklch(83.7% .128 66.29);--hx-color-orange-400:oklch(75% .183 55.934);--hx-color-orange-800:oklch(47% .157 37.304);--hx-color-amber-100:oklch(96.2% .059 95.617);--hx-color-amber-200:oklch(92.4% .12 95.746);--hx-color-amber-900:oklch(41.4% .112 45.904);--hx-color-yellow-50:oklch(98.7% .026 102.212);--hx-color-yellow-100:oklch(97.3% .071 103.193);--hx-color-yellow-200:oklch(94.5% .129 101.54);--hx-color-yellow-700:oklch(55.4% .135 66.442);--hx-color-yellow-900:oklch(42.1% .095 57.708);--hx-color-green-100:oklch(96.2% .044 156.743);--hx-color-green-200:oklch(92.5% .084 155.995);--hx-color-green-900:oklch(39.3% .095 152.535);--hx-color-blue-100:oklch(93.2% .032 255.585);--hx-color-blue-200:oklch(88.2% .059 254.128);--hx-color-blue-900:oklch(37.9% .146 265.522);--hx-color-indigo-100:oklch(93% .034 272.788);--hx-color-indigo-200:oklch(87% .065 274.039);--hx-color-indigo-900:oklch(35.9% .144 278.697);--hx-color-slate-50:oklch(98.4% .003 247.858);--hx-color-slate-100:oklch(96.8% .007 247.896);--hx-color-slate-900:oklch(20.8% .042 265.755);--hx-color-gray-50:oklch(98.5% .002 247.839);--hx-color-gray-100:oklch(96.7% .003 264.542);--hx-color-gray-200:oklch(92.8% .006 264.531);--hx-color-gray-300:oklch(87.2% .01 258.338);--hx-color-gray-400:oklch(70.7% .022 261.325);--hx-color-gray-500:oklch(55.1% .027 264.364);--hx-color-gray-600:oklch(44.6% .03 256.802);--hx-color-gray-700:oklch(37.3% .034 259.733);--hx-color-gray-800:oklch(27.8% .033 256.848);--hx-color-gray-900:oklch(21% .034 264.665);--hx-color-neutral-50:oklch(98.5% 0 0);--hx-color-neutral-200:oklch(92.2% 0 0);--hx-color-neutral-300:oklch(87% 0 0);--hx-color-neutral-400:oklch(70.8% 0 0);--hx-color-neutral-500:oklch(55.6% 0 0);--hx-color-neutral-600:oklch(43.9% 0 0);--hx-color-neutral-700:oklch(37.1% 0 0);--hx-color-neutral-800:oklch(26.9% 0 0);--hx-color-neutral-900:oklch(20.5% 0 0);--hx-color-black:#000;--hx-color-white:#fff;--hx-spacing:.25rem;--hx-breakpoint-xl:80rem;--hx-container-6xl:72rem;--hx-text-xs:.75rem;--hx-text-xs--line-height:calc(1/.75);--hx-text-sm:.875rem;--hx-text-sm--line-height:calc(1.25/.875);--hx-text-base:1rem;--hx-text-base--line-height:calc(1.5/1);--hx-text-lg:1.125rem;--hx-text-lg--line-height:calc(1.75/1.125);--hx-text-xl:1.25rem;--hx-text-xl--line-height:calc(1.75/1.25);--hx-text-2xl:1.5rem;--hx-text-2xl--line-height:calc(2/1.5);--hx-text-3xl:1.875rem;--hx-text-3xl--line-height:calc(2.25/1.875);--hx-text-4xl:2.25rem;--hx-text-4xl--line-height:calc(2.5/2.25);--hx-text-5xl:3rem;--hx-text-5xl--line-height:1;--hx-font-weight-normal:400;--hx-font-weight-medium:500;--hx-font-weight-semibold:600;--hx-font-weight-bold:700;--hx-font-weight-extrabold:800;--hx-tracking-tighter:-.05em;--hx-tracking-tight:-.025em;--hx-leading-tight:1.25;--hx-radius-xs:.125rem;--hx-radius-sm:.25rem;--hx-radius-md:.375rem;--hx-radius-lg:.5rem;--hx-radius-xl:.75rem;--hx-radius-3xl:1.5rem;--hx-ease-iLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
:root{--hextra-max-page-width:90rem;--hextra-max-navbar-width:90rem;--hextra-max-footer-width:80rem}.hextra-max-page-width{max-width:var(--hextra-max-page-width)}.hextra-max-navbar-width{max-width:var(--hextra-max-navbar-width)}.hextra-max-footer-width{max-width:var(--hextra-max-footer-width)}/*!tailwindcss v4.1.8 | MIT License | https://tailwindcss.com*/@layer properties{@supports(((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--hx-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--hx-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--hx-color-red-100:oklch(93.6% .032 17.717);--hx-color-red-200:oklch(88.5% .062 18.334);--hx-color-red-900:oklch(39.6% .141 25.723);--hx-color-orange-50:oklch(98% .016 73.684);--hx-color-orange-100:oklch(95.4% .038 75.164);--hx-color-orange-300:oklch(83.7% .128 66.29);--hx-color-orange-400:oklch(75% .183 55.934);--hx-color-orange-800:oklch(47% .157 37.304);--hx-color-amber-100:oklch(96.2% .059 95.617);--hx-color-amber-200:oklch(92.4% .12 95.746);--hx-color-amber-900:oklch(41.4% .112 45.904);--hx-color-yellow-50:oklch(98.7% .026 102.212);--hx-color-yellow-100:oklch(97.3% .071 103.193);--hx-color-yellow-200:oklch(94.5% .129 101.54);--hx-color-yellow-700:oklch(55.4% .135 66.442);--hx-color-yellow-900:oklch(42.1% .095 57.708);--hx-color-green-100:oklch(96.2% .044 156.743);--hx-color-green-200:oklch(92.5% .084 155.995);--hx-color-green-900:oklch(39.3% .095 152.535);--hx-color-blue-100:oklch(93.2% .032 255.585);--hx-color-blue-200:oklch(88.2% .059 254.128);--hx-color-blue-900:oklch(37.9% .146 265.522);--hx-color-indigo-100:oklch(93% .034 272.788);--hx-color-indigo-200:oklch(87% .065 274.039);--hx-color-indigo-900:oklch(35.9% .144 278.697);--hx-color-slate-50:oklch(98.4% .003 247.858);--hx-color-slate-100:oklch(96.8% .007 247.896);--hx-color-slate-900:oklch(20.8% .042 265.755);--hx-color-gray-50:oklch(98.5% .002 247.839);--hx-color-gray-100:oklch(96.7% .003 264.542);--hx-color-gray-200:oklch(92.8% .006 264.531);--hx-color-gray-300:oklch(87.2% .01 258.338);--hx-color-gray-400:oklch(70.7% .022 261.325);--hx-color-gray-500:oklch(55.1% .027 264.364);--hx-color-gray-600:oklch(44.6% .03 256.802);--hx-color-gray-700:oklch(37.3% .034 259.733);--hx-color-gray-800:oklch(27.8% .033 256.848);--hx-color-gray-900:oklch(21% .034 264.665);--hx-color-neutral-50:oklch(98.5% 0 0);--hx-color-neutral-200:oklch(92.2% 0 0);--hx-color-neutral-300:oklch(87% 0 0);--hx-color-neutral-400:oklch(70.8% 0 0);--hx-color-neutral-500:oklch(55.6% 0 0);--hx-color-neutral-600:oklch(43.9% 0 0);--hx-color-neutral-700:oklch(37.1% 0 0);--hx-color-neutral-800:oklch(26.9% 0 0);--hx-color-neutral-900:oklch(20.5% 0 0);--hx-color-black:#000;--hx-color-white:#fff;--hx-spacing:.25rem;--hx-breakpoint-xl:80rem;--hx-container-6xl:72rem;--hx-text-xs:.75rem;--hx-text-xs--line-height:calc(1/.75);--hx-text-sm:.875rem;--hx-text-sm--line-height:calc(1.25/.875);--hx-text-base:1rem;--hx-text-base--line-height:calc(1.5/1);--hx-text-lg:1.125rem;--hx-text-lg--line-height:calc(1.75/1.125);--hx-text-xl:1.25rem;--hx-text-xl--line-height:calc(1.75/1.25);--hx-text-2xl:1.5rem;--hx-text-2xl--line-height:calc(2/1.5);--hx-text-3xl:1.875rem;--hx-text-3xl--line-height:calc(2.25/1.875);--hx-text-4xl:2.25rem;--hx-text-4xl--line-height:calc(2.5/2.25);--hx-text-5xl:3rem;--hx-text-5xl--line-height:1;--hx-font-weiLine truncated
|
||||
Whitespace-only changes.
@@ -0,0 +1,21 @@
|
||||
/* Hugo template to derive CSS variables from site and page parameters */
|
||||
|
||||
/* Do not remove the following comment. It is used by Hugo to render CSS variables.*/
|
||||
|
||||
:root {
|
||||
--hextra-max-page-width: 90rem;
|
||||
--hextra-max-navbar-width: 90rem;
|
||||
--hextra-max-footer-width: 80rem;
|
||||
}
|
||||
|
||||
.hextra-max-page-width {
|
||||
max-width: var(--hextra-max-page-width);
|
||||
}
|
||||
|
||||
.hextra-max-navbar-width {
|
||||
max-width: var(--hextra-max-navbar-width);
|
||||
}
|
||||
|
||||
.hextra-max-footer-width {
|
||||
max-width: var(--hextra-max-footer-width);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"/en/recipes/":{"data":{"":"","whats-in-here#What\u0026rsquo;s in here":"Step-by-step instructions, that I don’t want to remember each time I need one.\nWhat’s in here Initial Host Setup Setting up a new host on Debian from scratch.\nXray Client Setup (RU only) Setting up xray client without any further usage in mind.","why-recipes#Why “Recipes”?":"These are not abstract troubleshooting guides.\nThese are specific actions you will need to perform."},"title":"Recipes"},"/en/recipes/initial-setup/":{"data":{"":"How to set up a fresh Debian host from zero to deployment.","add-adam-user-and-wheel-group#Add Adam User and Wheel Group":"Adam is a core user on each of the system. It does not has password, hence only connection via ssh is available, providing a password-less sudo.\n1 2 3 4 5 6 # Add adam user useradd -m -s '/bin/bash' adam # Add wheel group for password-less sudo groupadd wheel vi /etc/sudoers.d/99-root-ermnvldmr Fill file with the content:\n%wheel ALL = (ALL) NOPASSWD: ALL ","clone-the-rootermnvldmrcom-repository#Clone the root.ermnvldmr.com Repository":" 1 2 3 4 5 6 7 8 9 10 # Clone to your preferred location cd /srv sudo mkdir root.ermnvldmr.com sudo chown adam:adam root.ermnvldmr.com # Clone the repository git clone git@github.com:deytenit/root.ermnvldmr.com.git root.ermnvldmr.com # Run the repository initialization ./init.sh ","configure-ssh#Configure SSH":"Hardening SSHD, even stupidly changing port.\n1 2 3 4 5 # Move public ssh key of user adam to the server vi /home/adam/.ssh/authorized_keys # Create additional ssh rules vi /etc/ssh/sshd_config.d/99-root-ermnvldmr.conf With the following content:\nPort 2222 PasswordAuthentication no PermitEmptyPasswords no AllowUsers adam ","install-docker#Install Docker":"Follow the official Docker installation guide for Debian – docs.docker.com\nTest Docker installation:\n1 2 docker --version docker compose version ","setup-node#Setup node":" Important\nFrom now on $NODE equals node name of the server you are setting up.\nNote\nMore about root.ermnvldmr.com compositions repository – github.com\nRun node-specific scripts in $NODE/.host/.scripts in order of their numbering:\n1 2 3 4 5 6 7 8 9 10 # For example for node daedalus # Setting up the base components ./daedalus/.host/.scripts/00-base # Setting up ufw with rules defined in ./daedalus/.host/ufw ./daedalus/.host/.scripts/10-ufw # Setting up crowdsec with rules defined in ./daedalus/.host/crowd ./daedalus/.host/.scripts/20-crowd # Setting up cron with tasks defined in ./daedalus/.host/cron ./daedalus/.host/.scripts/30-cron ","setup-noroot-users#Setup noroot-users":"To harden the containers running on the node, we create series of noroot-users and shared group for each project in the compose repository node directory.\n1 2 # Setup noroot-users for the node .scripts/ops/setup-noroot-users $NODE ","setup-storage-tiers#Setup Storage Tiers":"You need to decide where to store your data tiers:\n1 2 3 4 5 6 7 8 # Create storage directories sudo mkdir -p /srv/com-ermnvldmr-root-$NODE-{tier1,tier2,tier3} # Setup tiers for the node .scripts/ops/setup-tiers $NODE \\ /srv/com-ermnvldmr-root-$NODE-tier1 \\ /srv/com-ermnvldmr-root-$NODE-tier2 \\ /srv/com-ermnvldmr-root-$NODE-tier3 "},"title":"Initial Host Setup"}}
|
||||
@@ -0,0 +1,437 @@
|
||||
// Search functionality using FlexSearch.
|
||||
|
||||
// Change shortcut key to cmd+k on Mac, iPad or iPhone.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (/iPad|iPhone|Macintosh/.test(navigator.userAgent)) {
|
||||
// select the kbd element under the .search-wrapper class
|
||||
const keys = document.querySelectorAll(".search-wrapper kbd");
|
||||
keys.forEach(key => {
|
||||
key.innerHTML = '<span class="hx:text-xs">⌘</span>K';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Render the search data as JSON.
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
(function () {
|
||||
const searchDataURL = '/en.search-data.json';
|
||||
|
||||
const inputElements = document.querySelectorAll('.search-input');
|
||||
for (const el of inputElements) {
|
||||
el.addEventListener('focus', init);
|
||||
el.addEventListener('keyup', search);
|
||||
el.addEventListener('keydown', handleKeyDown);
|
||||
el.addEventListener('input', handleInputChange);
|
||||
}
|
||||
|
||||
const shortcutElements = document.querySelectorAll('.search-wrapper kbd');
|
||||
|
||||
function setShortcutElementsOpacity(opacity) {
|
||||
shortcutElements.forEach(el => {
|
||||
el.style.opacity = opacity;
|
||||
});
|
||||
}
|
||||
|
||||
function handleInputChange(e) {
|
||||
const opacity = e.target.value.length > 0 ? 0 : 100;
|
||||
setShortcutElementsOpacity(opacity);
|
||||
}
|
||||
|
||||
// Get the search wrapper, input, and results elements.
|
||||
function getActiveSearchElement() {
|
||||
const inputs = Array.from(document.querySelectorAll('.search-wrapper')).filter(el => el.clientHeight > 0);
|
||||
if (inputs.length === 1) {
|
||||
return {
|
||||
wrapper: inputs[0],
|
||||
inputElement: inputs[0].querySelector('.search-input'),
|
||||
resultsElement: inputs[0].querySelector('.search-results')
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const INPUTS = ['input', 'select', 'button', 'textarea']
|
||||
|
||||
// Focus the search input when pressing ctrl+k/cmd+k or /.
|
||||
document.addEventListener('keydown', function (e) {
|
||||
const { inputElement } = getActiveSearchElement();
|
||||
if (!inputElement) return;
|
||||
|
||||
const activeElement = document.activeElement;
|
||||
const tagName = activeElement && activeElement.tagName;
|
||||
if (
|
||||
inputElement === activeElement ||
|
||||
!tagName ||
|
||||
INPUTS.includes(tagName) ||
|
||||
(activeElement && activeElement.isContentEditable))
|
||||
return;
|
||||
|
||||
if (
|
||||
e.key === '/' ||
|
||||
(e.key === 'k' &&
|
||||
(e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey))
|
||||
) {
|
||||
e.preventDefault();
|
||||
inputElement.focus();
|
||||
} else if (e.key === 'Escape' && inputElement.value) {
|
||||
inputElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Dismiss the search results when clicking outside the search box.
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
const { inputElement, resultsElement } = getActiveSearchElement();
|
||||
if (!inputElement || !resultsElement) return;
|
||||
if (
|
||||
e.target !== inputElement &&
|
||||
e.target !== resultsElement &&
|
||||
!resultsElement.contains(e.target)
|
||||
) {
|
||||
setShortcutElementsOpacity(100);
|
||||
hideSearchResults();
|
||||
}
|
||||
});
|
||||
|
||||
// Get the currently active result and its index.
|
||||
function getActiveResult() {
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
if (!resultsElement) return { result: undefined, index: -1 };
|
||||
|
||||
const result = resultsElement.querySelector('.active');
|
||||
if (!result) return { result: undefined, index: -1 };
|
||||
|
||||
const index = parseInt(result.dataset.index, 10);
|
||||
return { result, index };
|
||||
}
|
||||
|
||||
// Set the active result by index.
|
||||
function setActiveResult(index) {
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
if (!resultsElement) return;
|
||||
|
||||
const { result: activeResult } = getActiveResult();
|
||||
activeResult && activeResult.classList.remove('active');
|
||||
const result = resultsElement.querySelector(`[data-index="${index}"]`);
|
||||
if (result) {
|
||||
result.classList.add('active');
|
||||
result.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the number of search results from the DOM.
|
||||
function getResultsLength() {
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
if (!resultsElement) return 0;
|
||||
return resultsElement.dataset.count;
|
||||
}
|
||||
|
||||
// Finish the search by hiding the results and clearing the input.
|
||||
function finishSearch() {
|
||||
const { inputElement } = getActiveSearchElement();
|
||||
if (!inputElement) return;
|
||||
hideSearchResults();
|
||||
inputElement.value = '';
|
||||
inputElement.blur();
|
||||
}
|
||||
|
||||
function hideSearchResults() {
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
if (!resultsElement) return;
|
||||
resultsElement.classList.add('hx:hidden');
|
||||
}
|
||||
|
||||
// Handle keyboard events.
|
||||
function handleKeyDown(e) {
|
||||
const { inputElement } = getActiveSearchElement();
|
||||
if (!inputElement) return;
|
||||
|
||||
const resultsLength = getResultsLength();
|
||||
const { result: activeResult, index: activeIndex } = getActiveResult();
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (activeIndex > 0) setActiveResult(activeIndex - 1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (activeIndex + 1 < resultsLength) setActiveResult(activeIndex + 1);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (activeResult) {
|
||||
activeResult.click();
|
||||
}
|
||||
finishSearch();
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideSearchResults();
|
||||
// Clear the input when pressing escape
|
||||
inputElement.value = '';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
// Remove focus from the input
|
||||
inputElement.blur();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Initializes the search.
|
||||
function init(e) {
|
||||
e.target.removeEventListener('focus', init);
|
||||
if (!(window.pageIndex && window.sectionIndex)) {
|
||||
preloadIndex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads the search index by fetching data and adding it to the FlexSearch index.
|
||||
* @returns {Promise<void>} A promise that resolves when the index is preloaded.
|
||||
*/
|
||||
async function preloadIndex() {
|
||||
const tokenize = 'forward';
|
||||
|
||||
// https://github.com/TryGhost/Ghost/pull/21148
|
||||
const regex = new RegExp(
|
||||
`[\u{4E00}-\u{9FFF}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{AC00}-\u{D7A3}\u{3400}-\u{4DBF}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B73F}\u{2B740}-\u{2B81F}\u{2B820}-\u{2CEAF}\u{2CEB0}-\u{2EBEF}\u{30000}-\u{3134F}\u{31350}-\u{323AF}\u{2EBF0}-\u{2EE5F}\u{F900}-\u{FAFF}\u{2F800}-\u{2FA1F}]|[0-9A-Za-zа-я\u00C0-\u017F\u0400-\u04FF\u0600-\u06FF\u0980-\u09FF\u1E00-\u1EFF\u0590-\u05FF]+`,
|
||||
'mug'
|
||||
);
|
||||
const encode = (str) => { return ('' + str).toLowerCase().match(regex) ?? []; }
|
||||
|
||||
window.pageIndex = new FlexSearch.Document({
|
||||
tokenize,
|
||||
encode,
|
||||
cache: 100,
|
||||
document: {
|
||||
id: 'id',
|
||||
store: ['title', 'crumb'],
|
||||
index: "content"
|
||||
}
|
||||
});
|
||||
|
||||
window.sectionIndex = new FlexSearch.Document({
|
||||
tokenize,
|
||||
encode,
|
||||
cache: 100,
|
||||
document: {
|
||||
id: 'id',
|
||||
store: ['title', 'content', 'url', 'display', 'crumb'],
|
||||
index: "content",
|
||||
tag: [{
|
||||
field: "pageId"
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
const resp = await fetch(searchDataURL);
|
||||
const data = await resp.json();
|
||||
let pageId = 0;
|
||||
for (const route in data) {
|
||||
let pageContent = '';
|
||||
++pageId;
|
||||
const urlParts = route.split('/').filter(x => x != "" && !x.startsWith('#'));
|
||||
|
||||
let crumb = '';
|
||||
let searchUrl = '/';
|
||||
for (let i = 0; i < urlParts.length; i++) {
|
||||
const urlPart = urlParts[i];
|
||||
searchUrl += urlPart + '/'
|
||||
|
||||
const crumbData = data[searchUrl];
|
||||
if (!crumbData) {
|
||||
console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route);
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = data[searchUrl].title;
|
||||
if (title == "_index") {
|
||||
title = urlPart.split("-").map(x => x).join(" ");
|
||||
}
|
||||
crumb += title;
|
||||
|
||||
if (i < urlParts.length - 1) {
|
||||
crumb += ' > ';
|
||||
}
|
||||
}
|
||||
|
||||
for (const heading in data[route].data) {
|
||||
const [hash, text] = heading.split('#');
|
||||
const url = route.trimEnd('/') + (hash ? '#' + hash : '');
|
||||
const title = text || data[route].title;
|
||||
|
||||
const content = data[route].data[heading] || '';
|
||||
const paragraphs = content.split('\n').filter(Boolean);
|
||||
|
||||
sectionIndex.add({
|
||||
id: url,
|
||||
url,
|
||||
title,
|
||||
crumb,
|
||||
pageId: `page_${pageId}`,
|
||||
content: title,
|
||||
...(paragraphs[0] && { display: paragraphs[0] })
|
||||
});
|
||||
|
||||
for (let i = 0; i < paragraphs.length; i++) {
|
||||
sectionIndex.add({
|
||||
id: `${url}_${i}`,
|
||||
url,
|
||||
title,
|
||||
crumb,
|
||||
pageId: `page_${pageId}`,
|
||||
content: paragraphs[i]
|
||||
});
|
||||
}
|
||||
|
||||
pageContent += ` ${title} ${content}`;
|
||||
}
|
||||
|
||||
window.pageIndex.add({
|
||||
id: pageId,
|
||||
title: data[route].title,
|
||||
crumb,
|
||||
content: pageContent
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a search based on the provided query and displays the results.
|
||||
* @param {Event} e - The event object.
|
||||
*/
|
||||
function search(e) {
|
||||
const query = e.target.value;
|
||||
if (!e.target.value) {
|
||||
hideSearchResults();
|
||||
return;
|
||||
}
|
||||
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
while (resultsElement.firstChild) {
|
||||
resultsElement.removeChild(resultsElement.firstChild);
|
||||
}
|
||||
resultsElement.classList.remove('hx:hidden');
|
||||
|
||||
const pageResults = window.pageIndex.search(query, 5, { enrich: true, suggest: true })[0]?.result || [];
|
||||
|
||||
const results = [];
|
||||
const pageTitleMatches = {};
|
||||
|
||||
for (let i = 0; i < pageResults.length; i++) {
|
||||
const result = pageResults[i];
|
||||
pageTitleMatches[i] = 0;
|
||||
|
||||
// Show the top 5 results for each page
|
||||
const sectionResults = window.sectionIndex.search(query, 5, { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];
|
||||
let isFirstItemOfPage = true
|
||||
const occurred = {}
|
||||
|
||||
for (let j = 0; j < sectionResults.length; j++) {
|
||||
const { doc } = sectionResults[j]
|
||||
const isMatchingTitle = doc.display !== undefined
|
||||
if (isMatchingTitle) {
|
||||
pageTitleMatches[i]++
|
||||
}
|
||||
const { url, title } = doc
|
||||
const content = doc.display || doc.content
|
||||
|
||||
if (occurred[url + '@' + content]) continue
|
||||
occurred[url + '@' + content] = true
|
||||
results.push({
|
||||
_page_rk: i,
|
||||
_section_rk: j,
|
||||
route: url,
|
||||
prefix: isFirstItemOfPage ? result.doc.crumb : undefined,
|
||||
children: { title, content }
|
||||
})
|
||||
isFirstItemOfPage = false
|
||||
}
|
||||
}
|
||||
const sortedResults = results
|
||||
.sort((a, b) => {
|
||||
// Sort by number of matches in the title.
|
||||
if (a._page_rk === b._page_rk) {
|
||||
return a._section_rk - b._section_rk
|
||||
}
|
||||
if (pageTitleMatches[a._page_rk] !== pageTitleMatches[b._page_rk]) {
|
||||
return pageTitleMatches[b._page_rk] - pageTitleMatches[a._page_rk]
|
||||
}
|
||||
return a._page_rk - b._page_rk
|
||||
})
|
||||
.map(res => ({
|
||||
id: `${res._page_rk}_${res._section_rk}`,
|
||||
route: res.route,
|
||||
prefix: res.prefix,
|
||||
children: res.children
|
||||
}));
|
||||
displayResults(sortedResults, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the search results on the page.
|
||||
*
|
||||
* @param {Array} results - The array of search results.
|
||||
* @param {string} query - The search query.
|
||||
*/
|
||||
function displayResults(results, query) {
|
||||
const { resultsElement } = getActiveSearchElement();
|
||||
if (!resultsElement) return;
|
||||
|
||||
if (!results.length) {
|
||||
resultsElement.innerHTML = `<span class="no-result">No results found.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight the query in the result text.
|
||||
function highlightMatches(text, query) {
|
||||
const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const regex = new RegExp(escapedQuery, 'gi');
|
||||
return text.replace(regex, (match) => `<span class="match">${match}</span>`);
|
||||
}
|
||||
|
||||
// Create a DOM element from the HTML string.
|
||||
function createElement(str) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = str.trim();
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
function handleMouseMove(e) {
|
||||
const target = e.target.closest('a');
|
||||
if (target) {
|
||||
const active = resultsElement.querySelector('a.active');
|
||||
if (active) {
|
||||
active.classList.remove('active');
|
||||
}
|
||||
target.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.prefix) {
|
||||
fragment.appendChild(createElement(`
|
||||
<div class="prefix">${result.prefix}</div>`));
|
||||
}
|
||||
let li = createElement(`
|
||||
<li>
|
||||
<a data-index="${i}" href="${result.route}" class=${i === 0 ? "active" : ""}>
|
||||
<div class="title">`+ highlightMatches(result.children.title, query) + `</div>` +
|
||||
(result.children.content ?
|
||||
`<div class="excerpt">` + highlightMatches(result.children.content, query) + `</div>` : '') + `
|
||||
</a>
|
||||
</li>`);
|
||||
li.addEventListener('mousemove', handleMouseMove);
|
||||
li.addEventListener('keydown', handleKeyDown);
|
||||
li.querySelector('a').addEventListener('click', finishSearch);
|
||||
fragment.appendChild(li);
|
||||
}
|
||||
resultsElement.appendChild(fragment);
|
||||
resultsElement.dataset.count = results.length;
|
||||
}
|
||||
})();
|
||||
Loaded 100 of 496 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user