Initial commit
This commit is contained in:
110
skills/testing-strategy/templates/.github-workflows-test.yml
Normal file
110
skills/testing-strategy/templates/.github-workflows-test.yml
Normal file
@@ -0,0 +1,110 @@
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
test-typescript:
|
||||
name: TypeScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "bun"
|
||||
|
||||
- name: Install Doppler CLI
|
||||
uses: dopplerhq/cli-action@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run tests with coverage
|
||||
env:
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }}
|
||||
run: doppler run --config test -- bun run test:coverage
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/coverage-final.json
|
||||
|
||||
test-python:
|
||||
name: Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install Doppler CLI
|
||||
uses: dopplerhq/cli-action@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
- name: Run tests with coverage
|
||||
env:
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_TEST }}
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
doppler run --config test -- pytest --cov=app --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
102
skills/testing-strategy/templates/conftest.py
Normal file
102
skills/testing-strategy/templates/conftest.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# tests/conftest.py
|
||||
"""Shared test fixtures for all tests."""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from httpx import AsyncClient
|
||||
from uuid import uuid4
|
||||
from app.main import app
|
||||
from app.db.models import Base
|
||||
|
||||
# Doppler provides DATABASE_URL_TEST at runtime
|
||||
DATABASE_URL_TEST = os.getenv(
|
||||
"DATABASE_URL_TEST",
|
||||
"postgresql+asyncpg://localhost/test_db"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def engine():
|
||||
"""Create test database engine."""
|
||||
engine = create_async_engine(DATABASE_URL_TEST, echo=False)
|
||||
|
||||
# Create all tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
yield engine
|
||||
|
||||
# Drop all tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def session(engine):
|
||||
"""Create test database session with automatic rollback."""
|
||||
async_session = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client():
|
||||
"""Create test HTTP client."""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_id():
|
||||
"""Provide test tenant ID."""
|
||||
return uuid4()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_user(session, tenant_id):
|
||||
"""Create test user."""
|
||||
from app.db.models.user import User
|
||||
|
||||
user = User(
|
||||
tenant_id=tenant_id,
|
||||
email_address="test@example.com",
|
||||
name="Test User",
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client(client, test_user, tenant_id):
|
||||
"""Create authenticated HTTP client."""
|
||||
# Login and get token
|
||||
response = await client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email_address": test_user.email_address,
|
||||
"password": "testpassword",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Add auth header to client
|
||||
client.headers["Authorization"] = f"Bearer {token}"
|
||||
client.headers["X-Tenant-ID"] = str(tenant_id)
|
||||
|
||||
return client
|
||||
113
skills/testing-strategy/templates/pytest-integration.py
Normal file
113
skills/testing-strategy/templates/pytest-integration.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# tests/integration/test_FEATURE_api.py
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestYourAPI:
|
||||
"""Integration tests for Your API endpoints."""
|
||||
|
||||
async def test_create_endpoint(self, client: AsyncClient, tenant_id):
|
||||
"""Test POST /api/YOUR_RESOURCE creates resource."""
|
||||
response = await client.post(
|
||||
"/api/YOUR_RESOURCE",
|
||||
json={
|
||||
"name": "Test Resource",
|
||||
"description": "Test description",
|
||||
},
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Resource"
|
||||
assert data["tenant_id"] == str(tenant_id)
|
||||
|
||||
async def test_get_endpoint(self, client: AsyncClient, tenant_id, test_resource):
|
||||
"""Test GET /api/YOUR_RESOURCE/{id} retrieves resource."""
|
||||
response = await client.get(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == str(test_resource.id)
|
||||
assert data["name"] == test_resource.name
|
||||
|
||||
async def test_get_enforces_tenant_isolation(
|
||||
self, client: AsyncClient, tenant_id, test_resource
|
||||
):
|
||||
"""Test GET enforces tenant isolation."""
|
||||
# Should succeed with correct tenant
|
||||
response = await client.get(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Should fail with different tenant
|
||||
different_tenant = str(uuid4())
|
||||
response = await client.get(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
headers={"X-Tenant-ID": different_tenant},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_list_endpoint(self, client: AsyncClient, tenant_id):
|
||||
"""Test GET /api/YOUR_RESOURCE lists resources."""
|
||||
response = await client.get(
|
||||
"/api/YOUR_RESOURCE",
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
async def test_update_endpoint(
|
||||
self, client: AsyncClient, tenant_id, test_resource
|
||||
):
|
||||
"""Test PATCH /api/YOUR_RESOURCE/{id} updates resource."""
|
||||
response = await client.patch(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
json={"name": "Updated Name"},
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Updated Name"
|
||||
|
||||
async def test_delete_endpoint(
|
||||
self, client: AsyncClient, tenant_id, test_resource
|
||||
):
|
||||
"""Test DELETE /api/YOUR_RESOURCE/{id} deletes resource."""
|
||||
response = await client.delete(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify deletion
|
||||
response = await client.get(
|
||||
f"/api/YOUR_RESOURCE/{test_resource.id}",
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_validation_errors(self, client: AsyncClient, tenant_id):
|
||||
"""Test endpoint validates input correctly."""
|
||||
response = await client.post(
|
||||
"/api/YOUR_RESOURCE",
|
||||
json={
|
||||
"name": "", # Invalid: empty name
|
||||
},
|
||||
headers={"X-Tenant-ID": str(tenant_id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
119
skills/testing-strategy/templates/pytest-unit.py
Normal file
119
skills/testing-strategy/templates/pytest-unit.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# tests/unit/repositories/test_FEATURE_repository.py
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from app.db.repositories.YOUR_repository import YourRepository
|
||||
from app.db.models.YOUR_model import YourModel
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestYourRepository:
|
||||
"""Unit tests for YourRepository."""
|
||||
|
||||
async def test_get_by_id_success(self, session, tenant_id):
|
||||
"""Test retrieving entity by ID."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
# Create test entity
|
||||
entity = YourModel(
|
||||
tenant_id=tenant_id,
|
||||
name="Test Entity",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
await session.refresh(entity)
|
||||
|
||||
# Retrieve entity
|
||||
result = await repo.get_by_id(entity.id, tenant_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.id == entity.id
|
||||
assert result.name == "Test Entity"
|
||||
|
||||
async def test_get_by_id_enforces_tenant_isolation(
|
||||
self, session, tenant_id
|
||||
):
|
||||
"""Test that get_by_id enforces tenant isolation."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
# Create entity
|
||||
entity = YourModel(tenant_id=tenant_id, name="Test")
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
|
||||
# Try to access with different tenant_id
|
||||
different_tenant = uuid4()
|
||||
result = await repo.get_by_id(entity.id, different_tenant)
|
||||
|
||||
assert result is None
|
||||
|
||||
async def test_list_with_pagination(self, session, tenant_id):
|
||||
"""Test list with pagination."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
# Create multiple entities
|
||||
entities = [
|
||||
YourModel(tenant_id=tenant_id, name=f"Entity {i}")
|
||||
for i in range(10)
|
||||
]
|
||||
session.add_all(entities)
|
||||
await session.commit()
|
||||
|
||||
# Get first page
|
||||
page1 = await repo.list(tenant_id, limit=5, offset=0)
|
||||
assert len(page1) == 5
|
||||
|
||||
# Get second page
|
||||
page2 = await repo.list(tenant_id, limit=5, offset=5)
|
||||
assert len(page2) == 5
|
||||
|
||||
# Verify no overlap
|
||||
page1_ids = {e.id for e in page1}
|
||||
page2_ids = {e.id for e in page2}
|
||||
assert page1_ids.isdisjoint(page2_ids)
|
||||
|
||||
async def test_create_success(self, session, tenant_id):
|
||||
"""Test creating new entity."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
entity = await repo.create(
|
||||
tenant_id=tenant_id,
|
||||
name="New Entity",
|
||||
)
|
||||
|
||||
assert entity.id is not None
|
||||
assert entity.tenant_id == tenant_id
|
||||
assert entity.name == "New Entity"
|
||||
|
||||
async def test_update_success(self, session, tenant_id):
|
||||
"""Test updating existing entity."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
# Create entity
|
||||
entity = YourModel(tenant_id=tenant_id, name="Original")
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
|
||||
# Update entity
|
||||
updated = await repo.update(
|
||||
entity.id,
|
||||
tenant_id,
|
||||
name="Updated",
|
||||
)
|
||||
|
||||
assert updated.name == "Updated"
|
||||
|
||||
async def test_delete_success(self, session, tenant_id):
|
||||
"""Test deleting entity."""
|
||||
repo = YourRepository(session)
|
||||
|
||||
# Create entity
|
||||
entity = YourModel(tenant_id=tenant_id, name="To Delete")
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
|
||||
# Delete entity
|
||||
await repo.delete(entity.id, tenant_id)
|
||||
|
||||
# Verify deletion
|
||||
result = await repo.get_by_id(entity.id, tenant_id)
|
||||
assert result is None
|
||||
54
skills/testing-strategy/templates/vitest-component.test.tsx
Normal file
54
skills/testing-strategy/templates/vitest-component.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// tests/unit/lib/components/COMPONENT.test.tsx
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import YourComponent from "~/lib/components/YourComponent";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("~/lib/server/functions/YOUR_MODULE");
|
||||
|
||||
describe("YourComponent", () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
it("renders correctly with initial state", () => {
|
||||
render(<YourComponent />, { wrapper });
|
||||
expect(screen.getByText("Expected Text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles user interaction", async () => {
|
||||
render(<YourComponent />, { wrapper });
|
||||
|
||||
const button = screen.getByRole("button", { name: /click me/i });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Updated Text")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays loading state", () => {
|
||||
render(<YourComponent isLoading={true} />, { wrapper });
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays error state", async () => {
|
||||
// Mock error
|
||||
vi.mocked(someFunction).mockRejectedValue(new Error("Test error"));
|
||||
|
||||
render(<YourComponent />, { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/error/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
skills/testing-strategy/templates/vitest-integration.test.ts
Normal file
87
skills/testing-strategy/templates/vitest-integration.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// tests/integration/FEATURE-flow.test.ts
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { db } from "~/lib/server/db";
|
||||
import { users } from "~/lib/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
describe("Feature Integration Tests", () => {
|
||||
const testTenantId = "550e8400-e29b-41d4-a716-446655440000";
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup test data
|
||||
await db.delete(users).where(eq(users.tenant_id, testTenantId));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Cleanup test data
|
||||
await db.delete(users).where(eq(users.tenant_id, testTenantId));
|
||||
});
|
||||
|
||||
it("completes full workflow successfully", async () => {
|
||||
// 1. Create resource
|
||||
const [created] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
tenant_id: testTenantId,
|
||||
email_address: "test@example.com",
|
||||
name: "Test User",
|
||||
})
|
||||
.returning();
|
||||
|
||||
expect(created).toBeDefined();
|
||||
expect(created.email_address).toBe("test@example.com");
|
||||
|
||||
// 2. Retrieve resource
|
||||
const [retrieved] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, created.id))
|
||||
.where(eq(users.tenant_id, testTenantId));
|
||||
|
||||
expect(retrieved).toBeDefined();
|
||||
expect(retrieved.id).toBe(created.id);
|
||||
|
||||
// 3. Update resource
|
||||
const [updated] = await db
|
||||
.update(users)
|
||||
.set({ name: "Updated Name" })
|
||||
.where(eq(users.id, created.id))
|
||||
.returning();
|
||||
|
||||
expect(updated.name).toBe("Updated Name");
|
||||
|
||||
// 4. Delete resource
|
||||
await db.delete(users).where(eq(users.id, created.id));
|
||||
|
||||
// 5. Verify deletion
|
||||
const [deleted] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, created.id));
|
||||
|
||||
expect(deleted).toBeUndefined();
|
||||
});
|
||||
|
||||
it("enforces tenant isolation", async () => {
|
||||
const differentTenantId = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
// Create user in tenant 1
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
tenant_id: testTenantId,
|
||||
email_address: "tenant1@example.com",
|
||||
name: "Tenant 1 User",
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Attempt to access with different tenant_id
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, user.id))
|
||||
.where(eq(users.tenant_id, differentTenantId));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
19
skills/testing-strategy/templates/vitest-unit.test.ts
Normal file
19
skills/testing-strategy/templates/vitest-unit.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// tests/unit/lib/utils/FEATURE.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { functionToTest } from "~/lib/utils/FEATURE";
|
||||
|
||||
describe("functionToTest", () => {
|
||||
it("handles valid input correctly", () => {
|
||||
const result = functionToTest("valid input");
|
||||
expect(result).toBe("expected output");
|
||||
});
|
||||
|
||||
it("handles edge cases", () => {
|
||||
expect(functionToTest("")).toBe("");
|
||||
expect(functionToTest(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("throws error for invalid input", () => {
|
||||
expect(() => functionToTest("invalid")).toThrow("Error message");
|
||||
});
|
||||
});
|
||||
49
skills/testing-strategy/templates/vitest.config.ts
Normal file
49
skills/testing-strategy/templates/vitest.config.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
// Enable global test APIs
|
||||
globals: true,
|
||||
|
||||
// Use jsdom for browser-like environment
|
||||
environment: "jsdom",
|
||||
|
||||
// Run setup file before tests
|
||||
setupFiles: ["./tests/setup.ts"],
|
||||
|
||||
// Coverage configuration
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: [
|
||||
"node_modules/",
|
||||
"tests/",
|
||||
"**/*.config.ts",
|
||||
"**/*.d.ts",
|
||||
],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
|
||||
// Environment variables
|
||||
env: {
|
||||
DATABASE_URL_ADMIN: process.env.DATABASE_URL_ADMIN || "postgresql://localhost/test",
|
||||
REDIS_URL: process.env.REDIS_URL || "redis://localhost:6379",
|
||||
},
|
||||
},
|
||||
|
||||
// Path aliases
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user