@quvel-kit/core
v1.3.23
Published
Core utilities for Quvel UI
Downloads
1,951
Maintainers
Readme
@quvel-kit/core
Core utilities and infrastructure for Quasar + Laravel applications.
Features
Service Container System
- Isomorphic per-request container with Initialize → Boot → Register lifecycle
- SSR-aware services that can boot with request context
- Dependency injection with type-safe service resolution
- Service registration and lifecycle management
Core Services
All services are included and ready to use out of the box:
- LogService - Structured logging with trace correlation
- ApiService - Axios-based HTTP client with automatic trace headers
- I18nService - Internationalization with module support
- ValidationService - Zod-based validation with i18n integration
- WebSocketService - Laravel Echo + Pusher WebSocket support
- TaskService - Async task management with loading/error handling
- ThemeService - Light/dark mode management
Configuration is managed directly by the ServiceContainer as a first-class property, not as a service. All services receive AppConfig in their constructor.
Module System
- Module-based architecture for routes, i18n, services, and build config
- Resource path helpers for module assets
- Dynamic service registration per module
Utilities
- Asset loading - Dynamic CSS/JS injection for client and SSR
- Theme management - Light/dark mode with Quasar and Tailwind
- Script loading - Dynamic external script management
- Error handling - Laravel validation error processing
- Config utilities - Environment-based configuration merging
Composables
- useQuvel - Access Quvel service container in components
- useClient - Client-side detection for SSR-aware components
- useWindowEvent - Safe window event listener management
- useScript - Dynamic script loading with state tracking
- useMetaConfig - Page meta tags with SEO optimization
- useWebSockets - WebSocket channel subscription
- useScopedService - Component-scoped service instances
- useRecaptcha - Google reCAPTCHA integration
- useXsrf - XSRF token management
- useQueryMessageHandler - URL query parameter message handling
- useUrlQueryHandler - Generic URL query parameter processing
Installation
npm install @quvel-kit/core
# or
yarn add @quvel-kit/coreConfiguration
Environment Variables
See .env.example for all available configuration options.
Required Variables
# Application name
VITE_APP_NAME="My App"
# Backend API URL (Laravel)
VITE_API_URL=http://localhost:8000
# Frontend app URL (Quasar)
VITE_APP_URL=http://localhost:9000Optional Variables
# Application environment
VITE_APP_ENV=local
# Debug mode
VITE_DEBUG=true
# Application timezone
VITE_TIMEZONE=UTC
# Application locale
VITE_LOCALE=en
# Fallback locale
VITE_FALLBACK_LOCALE=en
# Session cookie name (must match Laravel)
VITE_SESSION_NAME=quvel_session
# Internal API URL for SSR server to backend communication
# Can be an internal network address for better performance
# Defaults to VITE_API_URL if not set
VITE_INTERNAL_API_URL=http://api-internal:8000
# Capacitor custom scheme for mobile apps
VITE_CAPACITOR_SCHEME=myappAppConfig Structure
The configuration follows a structured format matching the backend:
interface AppConfig {
app: {
name: string; // Application name
url: string; // Backend API URL
env?: string; // Environment (local, development, production)
debug?: boolean; // Debug mode
timezone?: string; // Timezone (UTC, America/New_York, etc.)
locale?: string; // Default locale (en, es, fr, etc.)
fallback_locale?: string; // Fallback locale
};
frontend: {
url: string; // Frontend app URL
custom_scheme?: string; // Capacitor custom scheme
};
assets?: AppAssets; // Dynamic CSS/JS injection
meta?: AppMeta; // Page meta configuration
headers?: HeadersConfig; // HTTP headers customization
api?: AppApiConfig; // API configuration (SSR key, etc.)
broadcasting?: BroadcastingConfig; // WebSocket/Pusher configuration
session?: SessionConfig; // Session cookie configuration
i18nCookie?: string; // I18n locale cookie name
userFactory?: UserFactory; // Custom user model factory
trace?: TraceInfo; // Request trace context (SSR-injected)
[key: string]: any; // Extensible for custom config
}Note: The VITE_INTERNAL_API_URL environment variable is used internally by @quvel-kit/ssr's SSRApiService and is not part of the AppConfig structure exposed to your application. It configures how the SSR server communicates with the backend.
Usage
Service Container
import { createContainer, LogService } from '@quvel-kit/core';
// Create container with services
const container = createContainer(
undefined, // SSR context (optional)
new Map([
['LogService', LogService],
])
);
// Access config (managed by container)
const config = container.config;
// Access services
const log = container.get(LogService);Using Services in Components
import { useQuvel } from '@quvel-kit/core';
export default defineComponent({
setup() {
const { config, log, api, i18n, task } = useQuvel();
const fetchData = async () => {
const data = await api.get('/users');
log.info('Fetched users', { count: data.length });
return data;
};
return { fetchData };
}
});TaskService Example
import { useQuvel } from '@quvel-kit/core';
export default defineComponent({
setup() {
const { task, api } = useQuvel();
const loginTask = task({
task: (credentials) => api.post('/login', credentials),
showLoading: true,
showNotification: {
success: 'Login successful!',
error: true, // Use default error message
},
handleLaravelError: true, // Auto-handle Laravel validation errors
});
const login = async () => {
const result = await loginTask.run();
if (result) {
router.push('/dashboard');
}
};
return { login, loginTask };
}
});Optional API Debug Interceptors
The core package provides optional debugging interceptors that can be enabled in development. Create a boot file to enable them:
// src/boot/api-debug.ts
import { boot } from 'quasar/wrappers';
import { useQuvel } from '@quvel-kit/core';
import { createDebugInterceptors } from '@quvel-kit/core/utils';
export default boot(({ ssrContext }) => {
if (import.meta.env.VITE_AXIOS_INTERCEPTORS === 'true') {
const quvel = useQuvel();
const interceptors = createDebugInterceptors(quvel.log, quvel.config, {
onRequest: (config) => {
// Custom request logging
},
onResponse: (response) => {
// Custom response logging
}
});
quvel.api.instance.interceptors.request.use(
interceptors.request.onFulfilled,
interceptors.request.onRejected
);
quvel.api.instance.interceptors.response.use(
interceptors.response.onFulfilled,
interceptors.response.onRejected
);
}
});Enable in .env:
VITE_AXIOS_INTERCEPTORS=trueDynamic Asset Loading
import { injectAssets } from '@quvel-kit/core';
// Client-side dynamic asset injection
injectAssets({
css: [
{
url: 'https://cdn.example.com/styles.css',
priority: 'critical',
position: 'head',
}
],
js: [
{
url: 'https://cdn.example.com/script.js',
loading: 'deferred',
position: 'body-end',
}
]
});Theme Management
import { useQuvel } from '@quvel-kit/core';
export default defineComponent({
setup() {
const { theme } = useQuvel();
// Get current theme
const currentTheme = theme.theme; // 'light' | 'dark'
// Set specific theme
theme.setTheme('dark');
// Toggle between light and dark
theme.toggleTheme();
return { currentTheme };
}
});Theme is automatically loaded from cookies or system preference during SSR boot. The ThemeService manages Quasar Dark mode and Tailwind classes.
I18n with Modules
import { I18nService } from '@quvel-kit/core';
import { getTranslations } from '@quvel-kit/core';
import { modules } from './modules';
// Aggregate translations from modules
const messages = {
'en-US': getTranslations(modules, 'en-US'),
'es-ES': getTranslations(modules, 'es-ES')
};
// Set translations before booting container
I18nService.setTranslations(messages);
// In component
const { i18n } = useQuvel();
i18n.t('common.welcome'); // Access translationsWebSocket Subscriptions
import { useWebSockets } from '@quvel-kit/core';
export default defineComponent({
setup() {
const { subscribe, unsubscribe } = useWebSockets();
onMounted(async () => {
const channel = await subscribe({
type: 'private',
channelName: 'user.1',
events: {
'notification': (data) => {
console.log('Notification received:', data);
}
}
});
onBeforeUnmount(() => {
unsubscribe(channel);
});
});
}
});Module System
The module system organizes features into self-contained units. Each module contributes routes, services, translations, and build configuration. Modules compose into applications without tight coupling.
Module Structure
A module is a plain object with optional properties:
export interface Module {
routes?: RouteRecordRaw[]; // Vue Router routes
services?: ServiceRegistry; // Service classes for DI
i18n?: TranslationRegistry; // Translations by locale
build?: ModuleBuildConfig; // Build-time configuration
}All properties are optional. Modules implement only what they need.
Creating a Module
Define a module by exporting an object:
// modules/Auth/index.ts
import type { Module } from '@quvel-kit/core';
import { AuthService } from './services/AuthService';
export const AuthModule: Module = {
routes: [
{
path: '/login',
component: () => import('./pages/LoginPage.vue')
},
{
path: '/register',
component: () => import('./pages/RegisterPage.vue')
}
],
services: {
AuthService: AuthService
},
i18n: {
'en-US': {
auth: {
login: 'Login',
register: 'Register',
logout: 'Logout'
}
},
'es-ES': {
auth: {
login: 'Iniciar sesión',
register: 'Registrarse',
logout: 'Cerrar sesión'
}
}
},
build: {
boot: ['auth'],
css: ['auth.css'],
plugins: ['Notify']
}
};Registering Modules
Collect modules in a registry:
// src/modules/index.ts
import { AuthModule } from './Auth';
import { DashboardModule } from './Dashboard';
import { ProfileModule } from './Profile';
export const modules = {
Auth: AuthModule,
Dashboard: DashboardModule,
Profile: ProfileModule
};Router Integration
Aggregate routes from all modules:
// src/router/routes.ts
import { getRoutes } from '@quvel-kit/core';
import { modules } from '../modules';
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: getRoutes(modules) // Inject module routes
}
];
export default routes;Service Container Integration
Register services from modules:
// src/boot/quvel.ts
import { defineQuvelBoot, getServices } from '@quvel-kit/core';
import { modules } from '../modules';
export default defineQuvelBoot(getServices(modules));I18n Integration
Load translations from modules:
// src/i18n/index.ts
import { getTranslations } from '@quvel-kit/core';
import { modules } from '../modules';
export default {
messages: {
'en-US': getTranslations(modules, 'en-US'),
'es-ES': getTranslations(modules, 'es-ES')
}
};Build Configuration
Apply module build configs in Quasar config:
// quasar.config.ts
import { createModuleTransformer } from '@quvel-kit/core/config/moduleTransformer';
import { modules } from './src/modules';
export default configure((ctx) => {
let config = {
boot: [],
css: [],
framework: {
plugins: []
}
};
// Apply module configs
config = createModuleTransformer(modules)(config, ctx);
return config;
});The transformer merges all module build configs into Quasar's configuration automatically.
Module Build Config
Modules declare build-time resources:
build: {
boot: ['auth', 'permissions'], // Boot files to load
css: ['module.css', 'theme.scss'], // CSS files to include
animations: ['fadeIn', 'slideUp'], // Quasar animations
plugins: ['Notify', 'Dialog'] // Quasar plugins
}Boot files support client/server targeting:
build: {
boot: [
{ path: 'auth', server: true, client: true },
{ path: 'analytics', server: false, client: true }
]
}Why Modules?
Without modules:
- Routes scattered across files
- Services manually wired together
- Translations duplicated
- Build config becomes unwieldy
With modules:
- Features are self-contained
- Adding a module is one line
- Removing a module is one line
- Zero manual wiring
Architecture
Service Lifecycle
Core uses a three-phase service lifecycle for dependency injection:
- Initialize - Service classes are instantiated with
configandssrContext - Register - Services receive the container and store references to dependencies
- Boot - Services use their dependencies for initialization (SSR-aware services only)
Important: Config is passed to all services in their constructor, so it's always available. Services like LogService and I18nService self-initialize in their constructors. Other services that initialize during register() may not be ready yet when accessed by other services during their register() phase.
All services complete their register() phase before any boot() phase begins. The boot() phase is where you can safely use ALL dependencies without ordering concerns.
Service Constructor Signature
Services receive config first, then ssrContext:
import type { QSsrContext } from '@quasar/app-vite';
import type { AppConfig } from '@quvel-kit/core';
import { Service } from '@quvel-kit/core';
export class MyService extends Service {
private readonly config: AppConfig;
constructor(config: AppConfig, ssrContext?: QSsrContext | null) {
super(config, ssrContext);
this.config = config;
// Config is always available in constructor
}
}Register Method Signature
Use destructuring in your register() method for clean dependency access:
import type { ServiceContainer } from '@quvel-kit/core';
import type { RegisterService } from '@quvel-kit/core';
export class MyService extends Service implements RegisterService {
private api!: ApiService;
// Use destructuring to access dependencies
register({ api, config, ssrContext }: ServiceContainer): void {
this.api = api;
// config and ssrContext are also available from container
this.client = createClient(ssrContext, config);
}
}Safety Guidelines
// ✅ ALWAYS SAFE: Config is passed in constructor
constructor(config: AppConfig, ssrContext?: QSsrContext | null) {
super(config, ssrContext);
this.config = config; // ✓ Always available
const debug = this.config.app.debug; // ✓ Safe to use
}
// ✅ ALWAYS SAFE: Config and Log via container
register({ config, log }: ServiceContainer): void {
const debug = config.app.debug; // ✓ Always ready
log.info('Service registered'); // ✓ Safe to use
}
// ⚠️ ORDERING ISSUE: Using services that initialize during register()
register({ i18n }: ServiceContainer): void {
this.i18n = i18n;
// This may fail if your service registers before I18nService!
const message = this.i18n.t('key'); // Race condition based on order
}
// ✅ CORRECT: Use all dependencies in boot()
boot(): void {
// All services are fully initialized - safe to use anything
const locale = this.config.app.locale;
const message = this.i18n.t('welcome');
this.api.setHeader('locale', locale);
}SSR Support
Services can be SSR-aware by implementing the SsrAwareService interface:
import type { QSsrContext } from '@quasar/app-vite';
export class MyService extends Service implements SsrAwareService {
boot(ssrContext?: QSsrContext | null): void {
// Access Quasar instance for SSR
if (ssrContext?.$q) {
ssrContext.$q.dark.set(true);
}
// Access request context
const config = ssrContext?.req?.requestContext?.appConfig;
}
}TypeScript Support
Full TypeScript support with comprehensive type definitions for:
- Service container and dependency injection
- Configuration structures
- API request/response types
- WebSocket channel types
- Task options and handlers
License
MIT
