Hone logo
Hone
Problems

Vue Store Plugin System

This challenge focuses on implementing a flexible plugin system for a Vue.js store (using Pinia). This is a common requirement for extending store functionality with features like logging, persistence, or data synchronization. You will build a system that allows custom "plugins" to interact with and modify the store's behavior.

Problem Description

You are tasked with creating a mechanism to register and use plugins within a Pinia store. A Pinia store plugin is a function that receives the store instance as an argument and can optionally add new state properties, actions, or getters to it.

Key Requirements:

  1. Plugin Registration: Implement a usePlugin function on the Pinia store instance that accepts a plugin function.
  2. Plugin Execution: When usePlugin is called, the provided plugin function should be executed, receiving the store's this context as an argument.
  3. Plugin Capabilities: Plugins should be able to:
    • Add new state properties.
    • Add new actions.
    • Add new getters.
    • Access existing state, actions, and getters.
  4. Type Safety: The solution must be type-safe using TypeScript. Ensure that when a plugin adds new properties, the store's type is correctly augmented.

Expected Behavior:

When a plugin is registered, its modifications should be immediately available on the store instance and correctly typed.

Edge Cases:

  • What happens if a plugin tries to overwrite an existing property on the store? (For this challenge, assume plugins will not overwrite existing properties to keep scope manageable. In a real-world scenario, you might want to add checks or specific behaviors.)
  • Ensure that type augmentation for plugins works seamlessly with existing store definitions.

Examples

Example 1: Basic State Augmentation

Let's assume you have a simple Pinia store:

// store.ts
import { defineStore } from 'pinia';

interface MyStoreState {
  count: number;
}

export const useMyStore = defineStore('myStore', {
  state: (): MyStoreState => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++;
    },
  },
});

Now, let's define a plugin to add a message state property:

// plugins.ts
import { PiniaCustomProperties, PiniaStore } from 'pinia';

interface MyStoreWithPluginState extends PiniaCustomProperties {
  message: string;
}

interface MyStoreWithPlugin extends PiniaStore<'myStore', MyStoreState, {}, {}, MyStoreWithPluginState> {}

type MyStorePlugin = (store: MyStoreWithPlugin) => void;

export const messagePlugin: MyStorePlugin = (store) => {
  // Augment the store with a new state property
  // Note: In a real-world scenario, you'd typically use defineStore with plugin types.
  // For this challenge, we'll focus on the plugin function itself.
  (store as any).message = 'Hello from plugin!';
};

And how you might register it:

// main.ts
import { createPinia } from 'pinia';
import { useMyStore } from './store';
import { messagePlugin } from './plugins';

const pinia = createPinia();
const app = createApp(App);

app.use(pinia);

const myStore = useMyStore();
myStore.usePlugin(messagePlugin); // Assuming usePlugin is added to the store instance

console.log(myStore.message); // Expected output: "Hello from plugin!"

Example 2: Action Augmentation

Consider the same store as Example 1. Now, a plugin to add a resetCount action:

// plugins.ts (continued)
import { PiniaCustomProperties, PiniaStore } from 'pinia';

interface MyStoreWithPluginActions extends PiniaCustomProperties {
  resetCount: () => void;
}

interface MyStoreWithPlugin extends PiniaStore<'myStore', MyStoreState, { increment: () => void }, {}, MyStoreWithPluginActions> {}

type MyStorePlugin = (store: MyStoreWithPlugin) => void;

export const resetCounterPlugin: MyStorePlugin = (store) => {
  // Augment the store with a new action
  (store as any).resetCount = () => {
    store.count = 0;
  };
};

Usage:

// main.ts (continued)
// ... after registering messagePlugin
myStore.usePlugin(resetCounterPlugin);

myStore.increment();
console.log(myStore.count); // Expected output: 1
myStore.resetCount();
console.log(myStore.count); // Expected output: 0

Example 3: Getter Augmentation

And a plugin to add a doubleCount getter:

// plugins.ts (continued)
import { PiniaCustomProperties, PiniaStore } from 'pinia';

interface MyStoreWithPluginGetters extends PiniaCustomProperties {
  doubleCount: number;
}

interface MyStoreWithPlugin extends PiniaStore<'myStore', MyStoreState, { increment: () => void }, { doubleCount: number }, MyStoreWithPluginGetters> {}

type MyStorePlugin = (store: MyStoreWithPlugin) => void;

export const doubleCountPlugin: MyStorePlugin = (store) => {
  // Augment the store with a new getter
  Object.defineProperty(store, 'doubleCount', {
    get() {
      return this.count * 2;
    },
  });
};

Usage:

// main.ts (continued)
// ... after registering previous plugins
myStore.usePlugin(doubleCountPlugin);

myStore.increment();
console.log(myStore.count);      // Expected output: 1
console.log(myStore.doubleCount); // Expected output: 2

Constraints

  • The solution must use Pinia as the Vue store.
  • All code must be written in TypeScript.
  • You need to implement the usePlugin method that will be available on the Pinia store instance.
  • The solution should not rely on external libraries beyond Pinia and Vue.

Notes

  • Pinia itself has a plugin system, but this challenge asks you to implement your own simplified version to understand the underlying principles. You will not be using pinia.use(). Instead, you'll add a usePlugin method directly to the store instances.
  • Consider how you will type-safely add new properties to the store. You'll likely need to explore TypeScript's utility types and declaration merging.
  • Think about how you'll access the store's internal state, actions, and getters within your plugin.
  • The defineStore function in Pinia returns a store function. When you call this function (e.g., useMyStore()), you get the actual store instance. Your usePlugin method will be added to this instance.
  • For getters, Object.defineProperty is a common way to add them in JavaScript, and it works well within the plugin context.
Loading editor...
typescript