
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
Zero-
npm install mvc-kit
mvc-kit ships with built-in context for AI coding assistants — stays in sync with your installed version automatically.
npx mvc-kit-setup # Set up all agents
npx mvc-kit-setup claude # Claude Code only
npx mvc-kit-setup cursor # Cursor only
npx mvc-kit-setup copilot # GitHub Copilot only
Claude Code — installs .claude/rules/, .claude/commands/, and .claude/agents/. Auto-updates on subsequent npm install/npm update.
Cursor — writes .cursorrules. Copilot — writes .github/copilot-instructions.md. Both use idempotent markers, safe to re-run.
import { ViewModel } from 'mvc-kit';
interface CounterState {
count: number;
}
class CounterViewModel extends ViewModel<CounterState> {
increment() {
this.set({ count: this.state.count + 1 });
}
}
const counter = new CounterViewModel({ count: 0 });
counter.subscribe((state, prev) => console.log(state.count));
counter.increment(); // logs: 1
Reactive state container. Extend and call set() to update state.
class TodosViewModel extends ViewModel<{ items: string[] }> {
addItem(item: string) {
this.set(prev => ({ items: [...prev.items, item] }));
}
// Called once after init() — use for subscriptions, data fetching, etc.
protected onInit() {
this.loadItems();
}
// Called after every state change
protected onSet(prev: Readonly<{ items: string[] }>, next: Readonly<{ items: string[] }>) {
console.log('Items changed:', prev.items.length, '→', next.items.length);
}
protected onDispose() {
// Cleanup logic
}
// After init(), async methods are automatically tracked:
// vm.async.loadItems → { loading: boolean, error: string | null }
async loadItems() {
// this.disposeSignal is automatically aborted on dispose — fetch() will throw AbortError
const res = await fetch('/api/items', { signal: this.disposeSignal });
const items = await res.json();
this.set({ items });
}
}
Reactive entity with validation and dirty tracking.
class UserModel extends Model<{ name: string; email: string }> {
setName(name: string) {
this.set({ name });
}
protected validate(state: { name: string; email: string }) {
const errors: Partial<Record<keyof typeof state, string>> = {};
if (!state.name) errors.name = 'Name is required';
if (!state.email.includes('@')) errors.email = 'Invalid email';
return errors;
}
}
const user = new UserModel({ name: '', email: '' });
console.log(user.valid); // false
console.log(user.errors); // { name: 'Name is required', email: 'Invalid email' }
user.setName('John');
console.log(user.dirty); // true (differs from committed state)
user.commit(); // Mark current state as baseline
user.rollback(); // Revert to committed state
Reactive typed array with CRUD and query methods.
interface Todo {
id: string;
text: string;
done: boolean;
}
const todos = new Collection<Todo>();
// CRUD (triggers re-renders)
todos.add({ id: '1', text: 'Learn mvc-kit', done: false });
todos.upsert({ id: '1', text: 'Updated', done: true }); // Add-or-replace by ID
todos.update('1', { done: true });
todos.remove('1');
todos.reset([...]); // Replace all
todos.clear();
// Optimistic update with rollback
const rollback = todos.optimistic(() => {
todos.update('1', { done: true });
});
// On failure: rollback() restores pre-update state
// Eviction & TTL (opt-in via static overrides)
class RecentMessages extends Collection<Message> {
static MAX_SIZE = 500; // FIFO eviction when exceeded
static TTL = 5 * 60_000; // Auto-expire after 5 minutes
}
// Properties
todos.items; // T[] (same as state)
todos.length; // number of items
// Query (pure, no notifications)
todos.get('1'); // Get by id (O(1) via internal index)
todos.has('1'); // Check existence
todos.find(t => t.done); // Find first match
todos.filter(t => !t.done);
todos.sorted((a, b) => a.text.localeCompare(b.text));
todos.map(t => t.text); // Map to new array
Collections that cache/repopulate from browser or device storage. Three adapters for different environments:
// Web — localStorage (auto-hydrates on first access)
import { WebStorageCollection } from 'mvc-kit/web';
class CartCollection extends WebStorageCollection<CartItem> {
protected readonly storageKey = 'cart';
}
// Web — IndexedDB (per-item storage, requires hydrate())
import { IndexedDBCollection } from 'mvc-kit/web';
class MessagesCollection extends IndexedDBCollection<Message> {
protected readonly storageKey = 'messages';
}
// React Native — configurable backend (requires hydrate())
import { NativeCollection } from 'mvc-kit/react-native';
NativeCollection.configure({
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
});
class TodosCollection extends NativeCollection<Todo> {
protected readonly storageKey = 'todos';
}
All adapters inherit Collection's full API (CRUD, query, optimistic updates, eviction, TTL). Mutations are automatically persisted via debounced writes. See src/PersistentCollection.md for details.
Collection + async tracking toolkit. Extends Collection with lifecycle and automatic async method tracking.
class UsersResource extends Resource<User> {
private api = singleton(UserService);
async loadAll() {
const data = await this.api.getAll(this.disposeSignal);
this.reset(data);
}
async loadById(id: number) {
const user = await this.api.getById(id, this.disposeSignal);
this.upsert(user);
}
}
const users = singleton(UsersResource);
await users.init();
users.loadAll();
users.async.loadAll.loading; // true while loading
users.async.loadAll.error; // error message, or null
users.async.loadAll.errorCode; // 'not_found', 'network', etc.
// Inherits all Collection methods
users.items; // User[]
users.get(1); // User | undefined
users.filter(u => u.active);
Supports external Collection injection for shared data scenarios:
class UsersResource extends Resource<User> {
constructor() {
super(singleton(SharedUsersCollection)); // All mutations go to the shared collection
}
}
Stateless orchestrator for complex logic. Component-scoped, auto-disposed.
class CheckoutController extends Controller {
constructor(
private cart: CartViewModel,
private api: ApiService
) {
super();
}
// Called once after init() — set up subscriptions, wire dependencies
protected onInit() {
// subscribeTo registers auto-cleanup — no manual tracking needed
this.subscribeTo(this.cart, () => this.onCartChanged());
}
async submit() {
const items = this.cart.state.items;
// this.disposeSignal auto-cancels the request if the controller is disposed mid-flight
await this.api.checkout(items, { signal: this.disposeSignal });
this.cart.clear();
}
}
Non-reactive infrastructure service. Singleton-scoped.
class ApiService extends Service {
async fetchUser(id: string) {
// this.disposeSignal auto-cancels if the service is disposed
const res = await fetch(`/api/users/${id}`, { signal: this.disposeSignal });
return res.json();
}
}
Typed pub/sub event bus.
interface AppEvents {
'user:login': { userId: string };
'user:logout': void;
'notification': { message: string };
}
const bus = new EventBus<AppEvents>();
// Subscribe
const unsubscribe = bus.on('user:login', ({ userId }) => {
console.log(`User ${userId} logged in`);
});
// One-time subscription
bus.once('notification', ({ message }) => alert(message));
// Emit
bus.emit('user:login', { userId: '123' });
// Cleanup
unsubscribe();
bus.dispose();
ViewModels support an optional second generic parameter for typed events — fire-and-forget signals for toasts, navigation, animations, etc.
interface SaveEvents {
saved: { id: string };
error: { message: string };
}
class TodoVM extends ViewModel<TodoState, SaveEvents> {
async save() {
try {
const result = await this.api.save(this.state);
this.emit('saved', { id: result.id }); // protected, type-safe
} catch {
this.emit('error', { message: 'Save failed' });
}
}
}
// React — subscribe directly on the ViewModel
const [state, vm] = useLocal(TodoVM);
useEvent(vm, 'saved', ({ id }) => toast.success(`Saved ${id}`));
events getter is lazy (zero cost if never accessed)emit() is protected — only the ViewModel can emitE is omitted (default {}), everything works as before (backward-compatible)After init(), ViewModel automatically tracks loading and error state for every async method — no manual boilerplate needed.
Before (manual tracking):
class UsersVM extends ViewModel<{ users: User[]; loading: boolean; error: string | null }> {
async fetchUsers() {
this.set({ loading: true, error: null });
try {
const res = await fetch('/api/users', { signal: this.disposeSignal });
this.set({ users: await res.json(), loading: false });
} catch (e) {
if (isAbortError(e)) return;
this.set({ loading: false, error: e.message });
throw e;
}
}
}
After (automatic tracking):
class UsersVM extends ViewModel<{ users: User[] }> {
async fetchUsers() {
const res = await fetch('/api/users', { signal: this.disposeSignal });
this.set({ users: await res.json() });
}
}
const vm = new UsersVM({ users: [] });
vm.init();
// Automatic — no manual loading/error state needed
vm.async.fetchUsers // → { loading: false, error: null }
vm.fetchUsers();
vm.async.fetchUsers // → { loading: true, error: null }
await vm.fetchUsers();
vm.async.fetchUsers // → { loading: false, error: null }
interface TaskState {
readonly loading: boolean;
readonly error: string | null;
}
Each method key in vm.async returns a frozen TaskState snapshot. Unknown keys return the default { loading: false, error: null }.
Loading state is counter-based. If you call the same method multiple times concurrently, loading stays true until all calls complete:
vm.fetchUsers(); // loading: true (count: 1)
vm.fetchUsers(); // loading: true (count: 2)
// first resolves → loading: true (count: 1)
// second resolves → loading: false (count: 0)
TaskState.error (as a string message) AND re-thrown — standard Promise rejection behavior is preservedTaskState.error, not re-thrown from the outer promiseFor methods without try/catch, AbortError handling is fully automatic. For methods with explicit try/catch, see Error Utilities for when isAbortError() is needed.
Sync methods are auto-detected on first call. If a method returns a non-thenable, its wrapper is replaced with a direct bind() — zero overhead after the first call.
subscribeAsync(listener)Low-level subscription for async state changes. Mirrors the subscribe() contract. Used internally by useInstance() to trigger React re-renders when async status changes.
const unsub = vm.subscribeAsync(() => {
console.log(vm.async.fetchUsers);
});
async and subscribeAsync are reserved property names on ViewModel. Subclasses that define these as properties, methods, or getters throw immediately.
After dispose(), if async methods had pending calls, a warning is logged after GHOST_TIMEOUT (default 3s, configurable via static GHOST_TIMEOUT):
[mvc-kit] Ghost async operation detected: "fetchUsers" had 1 pending call(s)
when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.
Composable error handling utilities for consistent error classification.
import { isAbortError, classifyError, HttpError } from 'mvc-kit';
// In services — throw typed HTTP errors
if (!res.ok) throw new HttpError(res.status, res.statusText);
// In ViewModel catch blocks — guard shared-state side effects on abort
if (!isAbortError(e)) rollback();
// Classify any error into a canonical shape
const appError = classifyError(error);
// appError.code: 'unauthorized' | 'network' | 'timeout' | 'abort' | ...
When to use isAbortError(): The async tracking wrapper swallows AbortErrors at the outer promise level, but your catch blocks do receive them. Use isAbortError() only when the catch block has side effects on shared state (like rolling back optimistic updates on a singleton Collection). You don't need it for set() or emit() (both are no-ops after dispose), and you never need it in methods without try/catch.
Every class in mvc-kit has a built-in AbortSignal and cleanup registration system. This eliminates the need to manually track timers, subscriptions, and in-flight requests.
disposeSignal (public)A lazily-created AbortSignal that is automatically aborted when dispose() is called. Zero overhead if never accessed.
class ChatViewModel extends ViewModel<{ messages: Message[] }> {
protected onInit() {
this.loadMessages();
}
private async loadMessages() {
// fetch() throws AbortError if disposeSignal is aborted — no need for defensive checks after await
const res = await fetch('/api/messages', { signal: this.disposeSignal });
const messages = await res.json();
this.set({ messages });
}
}
subscribeTo(source, listener) (protected)Subscribe to a Subscribable and auto-unsubscribe on dispose. Available on ViewModel, Model, and Controller. Use for imperative reactions — for deriving values from collections, use getters instead (auto-tracking handles reactivity).
class ChatViewModel extends ViewModel<State> {
protected onInit() {
// Imperative reaction: play sound on new messages
this.subscribeTo(this.messagesCollection, () => this.playNotificationSound());
}
}
listenTo(source, event, handler) (protected)Subscribe to a typed event on a Channel or EventBus with automatic cleanup on dispose (and reset, for ViewModels). The event counterpart to subscribeTo.
class ChatViewModel extends ViewModel<State> {
private channel = singleton(ChatChannel);
protected onInit() {
this.listenTo(this.channel, 'message', (msg) => {
this.set({ messages: [...this.state.messages, msg] });
});
}
}
Available on ViewModel, Controller, Channel, and Model.
addCleanup(fn) (protected)Register a teardown callback that fires on dispose(), after disposeSignal abort but before onDispose(). Use for timers and external subscriptions not covered by subscribeTo or listenTo.
class DashboardController extends Controller {
protected onInit() {
const id = setInterval(() => this.poll(), 5000);
this.addCleanup(() => clearInterval(id));
}
}
_disposed = true → disposeSignal.abort() → addCleanup callbacks → onDispose() → clear internal data
onDispose(), so this.disposeSignal.aborted === true inside onDispose()disposeSignal was never accessed, the abort step is skipped (zero cost)teardown() get a fresh, un-aborted disposeSignalFor per-call cancellation (e.g., rapid room switching), compose with AbortSignal.any():
class ChatService extends Service {
async loadRoom(roomId: string, callSignal: AbortSignal) {
// Cancelled if EITHER the service is disposed OR the caller aborts
const res = await fetch(`/api/rooms/${roomId}`, {
signal: AbortSignal.any([this.disposeSignal, callSignal]),
});
return res.json();
}
}
Manage shared instances globally.
import { singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
// Get or create singleton
const api = singleton(ApiService);
// Singleton ViewModel with DEFAULT_STATE — no args needed at call sites
class AuthViewModel extends ViewModel<AuthState> {
static DEFAULT_STATE: AuthState = { user: null, isAuthenticated: false };
}
const auth = singleton(AuthViewModel); // uses DEFAULT_STATE automatically
// Check if singleton exists
hasSingleton(ApiService); // true
// Dispose specific singleton
teardown(ApiService);
// Dispose all singletons (useful in tests)
teardownAll();
import { useInstance, useLocal, useSingleton } from 'mvc-kit/react';
useInstance(subscribable)Subscribe to an existing Subscribable. No ownership - you manage disposal.
function Counter({ vm }: { vm: CounterViewModel }) {
const state = useInstance(vm);
return <div>{state.count}</div>;
}
useLocal(Class, ...args) or useLocal(factory)Create component-scoped instance. Auto-disposed on unmount. If the instance has an onInit() hook, it is called automatically after mount.
// Class-based
function Counter() {
const [state, vm] = useLocal(CounterViewModel, { count: 0 });
// vm.onInit() called automatically after mount — no useEffect needed
return <button onClick={() => vm.increment()}>{state.count}</button>;
}
// Factory-based (for complex initialization)
function Checkout() {
const controller = useLocal(() => new CheckoutController(cart, api));
return <button onClick={() => controller.submit()}>Submit</button>;
}
Return type:
[state, instance] tupleinstanceuseSingleton(Class, ...args)Get singleton instance. Registry manages lifecycle. Calls init() automatically after mount.
// Subscribable singleton
function UserProfile() {
const [state, vm] = useSingleton(UserViewModel);
return <div>{state.name}</div>;
}
// Service singleton
function Dashboard() {
const api = useSingleton(ApiService);
// ...
}
useModel(factory)Create component-scoped Model with validation and dirty state. Calls init() automatically after mount.
function UserForm() {
const { state, errors, valid, dirty, model } = useModel(() =>
new UserModel({ name: '', email: '' })
);
return (
<form>
<input
value={state.name}
onChange={e => model.setName(e.target.value)}
/>
{errors.name && <span>{errors.name}</span>}
<button disabled={!valid}>Submit</button>
</form>
);
}
useModelRef(factory)Create component-scoped Model with lifecycle management (init + dispose) but no subscription. The parent never re-renders from model state changes. Use with useField for per-field isolation in large forms.
function UserForm() {
const model = useModelRef(() => new UserModel({ name: '', email: '' }));
return (
<form>
<NameField model={model} />
<FormActions model={model} />
</form>
);
}
useField(model, key)Subscribe to a single field with surgical re-renders. The returned set() calls the Model's set() directly — use custom setter methods on the Model for any logic beyond simple assignment.
function NameField({ model }: { model: UserModel }) {
const { value, error, set } = useField(model, 'name');
return (
<div>
<input value={value} onChange={e => set(e.target.value)} />
{error && <span>{error}</span>}
</div>
);
}
useEvent(bus, event, handler)Subscribe to event, auto-unsubscribes on unmount.
function NotificationToast() {
const [message, setMessage] = useState('');
useEvent(bus, 'notification', ({ message }) => {
setMessage(message);
});
return message ? <div>{message}</div> : null;
}
useEmit(bus)Get stable emit function.
function LoginButton() {
const emit = useEmit(bus);
return (
<button onClick={() => emit('user:login', { userId: '123' })}>
Login
</button>
);
}
Provider and useResolveDependency injection for testing and Storybook.
// In tests/stories
<Provider provide={[
[ApiService, mockApi],
[UserViewModel, mockUserVM]
]}>
<MyComponent />
</Provider>
// In components - falls back to singleton() if no Provider
function MyComponent() {
const api = useResolve(ApiService);
// ...
}
useTeardown(...Classes)Teardown singletons on unmount.
function App() {
// Clean up these singletons when App unmounts
useTeardown(UserViewModel, CartViewModel);
return <Main />;
}
| Class | Description |
|---|---|
ViewModel<S, E?> | Reactive state container with optional typed events |
Model<S> | Reactive entity with validation/dirty tracking |
Collection<T> | Reactive typed array with CRUD |
Resource<T> | Collection + async tracking + external Collection injection |
Controller | Stateless orchestrator (Disposable) |
Service | Non-reactive infrastructure service (Disposable) |
EventBus<E> | Typed pub/sub event bus |
Trackable | Base class for custom reactive objects (subscribable + disposable + auto-bind) |
All composable helpers extend Trackable — subscribable, disposable, and auto-bound.
| Class | Description |
|---|---|
Sorting<T> | Multi-column sort state with 3-click toggle cycle and apply() pipeline |
Pagination | Page/pageSize state with apply() slicing |
Selection<K> | Key-based selection set with toggle/select-all semantics |
Feed<T> | Cursor + hasMore + item accumulation for server-side pagination |
Pending<K, Meta?> | Per-item operation queue with retry + status tracking + optional typed metadata |
| Interface | Description |
|---|---|
Subscribable<S> | Has state, subscribe(), dispose(), disposeSignal |
Disposable | Has disposed, disposeSignal, dispose() |
Initializable | Has initialized, init() |
Listener<S> | (state: S, prev: S) => void |
Updater<S> | (prev: S) => Partial<S> |
ValidationErrors<S> | Partial<Record<keyof S, string>> |
TaskState | { loading: boolean; error: string | null } |
AsyncMethodKeys<T> | Union of method names on T that return Promise (ViewModel) |
ResourceAsyncMethodKeys<T> | Union of method names on T that return Promise (Resource) |
| Function | Description |
|---|---|
singleton(Class, ...args) | Get or create singleton |
hasSingleton(Class) | Check if singleton exists |
teardown(Class) | Dispose and remove singleton |
teardownAll() | Dispose all singletons |
| Export | Description |
|---|---|
AppError (type) | Canonical error shape with typed code field |
HttpError | Typed HTTP error class for services to throw |
isAbortError(error) | Guard for AbortError — use in catch blocks with shared-state side effects |
classifyError(error) | Maps raw errors → AppError |
| Hook | Description |
|---|---|
useInstance(subscribable) | Subscribe to existing instance |
useLocal(Class | factory, ...args) | Component-scoped, auto-disposed, auto-init |
useSingleton(Class, ...args) | Singleton, registry-managed, auto-init |
useModel(factory) | Model with validation/dirty state, auto-init |
useModelRef(factory) | Model lifecycle only (no subscription). For per-field forms. |
useField(model, key) | Single field subscription |
useEvent(source, event, handler) | Subscribe to EventBus or ViewModel event |
useEmit(bus) | Get stable emit function |
useResolve(Class, ...args) | Resolve from Provider or singleton |
useTeardown(...Classes) | Teardown singletons on unmount |
| Component | Description |
|---|---|
DataTable<T> | Unstyled table with sort headers, selection checkboxes, pagination slots; accepts helper instances directly |
CardList<T> | Unstyled list/grid with render-prop items |
InfiniteScroll | IntersectionObserver wrapper for infinite loading; direction="up" for chat UIs |
Object.freeze()dispose() is idempotent (safe to call multiple times)init() is idempotent (safe to call multiple times, only runs onInit() once)set() and emit() are no-ops after dispose (not throws) — allows in-flight async callbacks to resolve harmlessly and cleanup callbacks to emit final eventscommit(), add(), etc.) throw after disposesubscribe() / on() return a no-op unsubscriber after dispose (does not throw)construct → init → use → dispose
onInit() runs once after init() — supports sync and async (void | Promise<void>)onSet(prev, next) runs after every state change (ViewModel, Model)onDispose() runs once on dispose, after disposeSignal abort and cleanup callbacksdisposeSignal is lazily created — zero memory/GC overhead unless accessed_disposed = true → disposeSignal.abort() → addCleanup callbacks → onDispose() → clear internal datadisposeSignal, addCleanup(), and onDispose() (including EventBus and Collection)subscribeTo(source, listener) for auto-cleaned subscriptionsE — events getter, emit() methodinit(), all subclass methods are wrapped for automatic async tracking; vm.async.methodName returns TaskStateisAbortError() to guard shared-state side effects like Collection rollbacks)async and subscribeAsync are reserved property names on ViewModel and ResourceuseLocal, useModel, useSingleton) auto-call init() after mountsingleton() does not auto-call init() — call it manually outside React_initialized guard prevents double-init during React's double-mount cycle; disposeSignal is not aborted during StrictMode's fake unmount/remount cycle__MVC_KIT_DEV__ enables development-only safety checks (e.g., detecting set() inside getters). It defaults to false safely — no bundler config required. See Dev Mode.Each core class and React hook has a dedicated reference doc with full API details, usage patterns, and examples.
Core Classes & Utilities
| Doc | Description |
|---|---|
| ViewModel | State management, computed getters, async tracking, typed events, lifecycle hooks |
| Model | Validation, dirty tracking, commit/rollback for entity forms |
| Collection | Reactive typed array, CRUD, optimistic updates, shared data cache |
| Resource | Collection + async tracking toolkit with external Collection injection |
| Controller | Stateless orchestrator for multi-ViewModel coordination |
| Service | Non-reactive infrastructure adapters (HTTP, storage, SDKs) |
| EventBus | Typed pub/sub for cross-cutting event communication |
| Channel | Persistent connections (WebSocket, SSE) with auto-reconnect |
| Trackable | Base class for custom reactive objects (subscribable + disposable + auto-bind) |
| Singleton Registry | Global instance management: singleton(), teardown(), teardownAll() |
| Sorting | Multi-column sort state with 3-click toggle cycle and apply pipeline |
| Pagination | Page/pageSize state with array slicing |
| Selection | Key-based selection set with toggle/select-all |
| Feed | Cursor + hasMore + item accumulation for server-side pagination |
| Pending | Per-item operation queue with retry + status tracking |
React Hooks
| Doc | Description |
|---|---|
| useLocal | Component-scoped instance, auto-init/dispose, deps array for recreate |
| useInstance | Subscribe to an existing Subscribable (no lifecycle management) |
| useSingleton | Singleton resolution with auto-init and shared state |
| useModel, useModelRef & useField | Model binding with validation/dirty state; lifecycle-only ref; surgical per-field subscriptions |
| useEvent & useEmit | Subscribe to and emit typed events from EventBus or ViewModel |
| useTeardown | Dispose singleton instances on component unmount |
Headless Components
| Doc | Description |
|---|---|
| DataTable | Unstyled table with sort, selection, pagination; accepts helpers directly |
| CardList | Unstyled list/grid with render-prop items |
| InfiniteScroll | IntersectionObserver wrapper for infinite loading; direction="up" for chat UIs |
__MVC_KIT_DEV__)mvc-kit includes development-only safety checks guarded by the __MVC_KIT_DEV__ flag. When enabled, these checks catch common mistakes at development time with clear console.error messages instead of silent infinite loops or hard-to-debug failures.
The flag defaults to false safely — no bundler config is required. The library uses a typeof guard internally, so importing mvc-kit in Node, Deno, SSR, or any unbundled environment works without a ReferenceError.
set() inside a getter — After init(), ViewModel getters are auto-memoized and dependency-tracked. Calling set() from a getter creates an infinite loop (state change → getter recompute → set() → repeat). The dev guard detects this, logs an error, and prevents the set() call.dispose(), if async methods had pending calls, a warning is logged after GHOST_TIMEOUT (default 3s). Suggests using disposeSignal to cancel in-flight work.undefined.async or subscribeAsync as a property, method, or getter.init(). The method still executes, but async tracking is not yet active.With a bundler (recommended)
Define __MVC_KIT_DEV__ as true in your bundler's compile-time define config:
Vite
// vite.config.ts
export default defineConfig({
define: { __MVC_KIT_DEV__: true },
// ...
});
Without a bundler
Set the global before importing mvc-kit:
globalThis.__MVC_KIT_DEV__ = true;
import { ViewModel } from 'mvc-kit';
Set __MVC_KIT_DEV__ to false in your production config (or omit it entirely). The guarded code is dead-code-eliminated by minifiers, resulting in zero runtime cost.
// vite.config.ts — production
export default defineConfig({
define: {
__MVC_KIT_DEV__: process.env.NODE_ENV !== 'production',
},
});
Internally, mvc-kit resolves the flag once at module load:
const __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;
This is the same pattern used by Vue and Preact. Without a bundler, typeof returns 'undefined' and the constant is false — safe, no crash. With a bundler define, the raw reference is replaced at build time: const __DEV__ = true (or false), and minifiers eliminate dead branches entirely.
MIT
FAQs
Zero-magic, class-based reactive ViewModel library
The npm package mvc-kit receives a total of 57 weekly downloads. As such, mvc-kit popularity was classified as not popular.
We found that mvc-kit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.