import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';
const resolvePath = (relative: string): string =>
fileURLToPath(new URL(relative, import.meta.url));
export default defineConfig({
plugins: [react()],
resolve: {
alias: [
// Most specific prefix first so '@shared' wins over '@'.
{ find: '@shared', replacement: resolvePath('../shared/src') },
{ find: '@', replacement: resolvePath('./src') },
],
},
build: {
sourcemap: true,
outDir: 'dist',
},
});
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src', '<rootDir>/tests'],
moduleNameMapper: {
'^@shared/(.*)$': '<rootDir>/../shared/src/$1',
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
export default config;
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@shared/*": ["../shared/src/*"]
},
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src", "tests"]
}
import { httpClient } from '@/lib/http-client';
import type { User } from '@shared/types/user';
export async function fetchUser(id: string): Promise<User> {
const res = await httpClient.get(`/users/${id}`);
if (!res.ok) {
throw new Error(`fetchUser failed: ${res.status}`);
}
return (await res.json()) as User;
}
export async function fetchActiveUsers(): Promise<User[]> {
const res = await httpClient.get('/users?active=true');
const users = (await res.json()) as User[];
return users.filter((u) => u.status === 'active');
}
Path aliases let a codebase import from @/services/user instead of brittle relative chains like ../../../services/user. TypeScript understands these aliases at type-check time, but the compiler does not rewrite import specifiers, so every runtime and tooling layer that resolves modules must be taught the same mapping. This snippet shows the three places the same alias table has to be declared to stay consistent: the TypeScript config, the Vite bundler, and the Jest test runner.
In tsconfig.json, baseUrl anchors resolution at the project root and the paths map defines each alias. @/* points at src/* for the app's internal imports, while @shared/* reaches into a sibling package — a common monorepo setup. The paths entries only affect how tsc and editors resolve types; they are purely a compile-time hint and produce no output on their own. This is the pitfall that trips people up: type-checking passes, but the built bundle throws Cannot find module because the bundler never learned the mapping.
In vite.config.ts, resolve.alias mirrors the same table for the actual runtime resolver. Using fileURLToPath with import.meta.url produces absolute paths that work regardless of the working directory. The order matters when aliases share a prefix — more specific keys like @shared should resolve before broader ones. Rather than hand-maintaining two lists, teams often generate resolve.alias from tsconfig via a plugin, but the explicit form shown here makes the relationship obvious.
In jest.config.ts, moduleNameMapper performs the same job with regular expressions, since Jest resolves modules through Node without any bundler. The ^@/(.*)$ pattern captures the tail and rewrites it under <rootDir>/src. Each alias needs its own regex, and forgetting one produces test-only failures that never appear in the app build.
The broader lesson is that path aliases are not a single feature but a convention that every module resolver in the toolchain must agree on. Keeping the three tables synchronized — or generating them from one source — is what makes aliases reliable instead of a source of environment-specific breakage.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
use my_crate::add;
#[test]
fn test_public_api() {
assert_eq!(add(3, 4), 7);
}
Integration tests in tests/ directory
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
Share this code
Here's the card — post it anywhere.