Merge pull request #2 from AdenMGB/add-jsdoc-comments

added JSDoc comments to background, plugin core, and settings fi…
This commit is contained in:
Aden Lindsay
2025-05-29 21:58:45 +09:30
committed by GitHub
4 changed files with 347 additions and 7 deletions
+38
View File
@@ -1,5 +1,18 @@
import Parser from "rss-parser"; import Parser from "rss-parser";
/**
* Fetches news articles specifically for Australia from the NewsAPI.
*
* This function handles a specific case for fetching Australian news. It includes a
* mechanism to retry the fetch operation by appending "%00" to the URL if a
* rate limit error (`response.code == "rateLimited"`) is encountered. This is
* likely a workaround for cache-busting or bypassing certain rate-limiting measures.
*
* @param {string} url The NewsAPI URL to fetch Australian news from.
* @param {any} sendResponse A callback function (likely from a browser extension message listener)
* to send the fetched news data back to the caller.
* It's called with an object like `{ news: responseData }`.
*/
const fetchAustraliaNews = async (url: string, sendResponse: any) => { const fetchAustraliaNews = async (url: string, sendResponse: any) => {
fetch(url) fetch(url)
.then((result) => result.json()) .then((result) => result.json())
@@ -12,6 +25,12 @@ const fetchAustraliaNews = async (url: string, sendResponse: any) => {
}); });
}; };
/**
* A record mapping lowercase country codes (e.g., "usa", "canada") to an array
* of RSS feed URLs for news sources in that country.
*
* @type {Record<string, string[]>}
*/
const rssFeedsByCountry: Record<string, string[]> = { const rssFeedsByCountry: Record<string, string[]> = {
usa: [ usa: [
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml", "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
@@ -54,6 +73,25 @@ const rssFeedsByCountry: Record<string, string[]> = {
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"], netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
}; };
/**
* Fetches news articles based on a specified source.
*
* The source can be:
* 1. The string "australia": Fetches news from Australian sources via NewsAPI,
* handled by the `fetchAustraliaNews` function.
* 2. A lowercase country code (e.g., "usa", "canada"): Fetches news from a predefined
* list of RSS feeds for that country, as specified in `rssFeedsByCountry`.
* 3. A direct RSS feed URL (starting with "http"): Fetches news directly from this URL.
*
* The fetched articles are then sent back to the caller using the `sendResponse` callback.
*
* @param {string} source The news source identifier. This can be "australia", a
* lowercase country code, or a direct RSS feed URL.
* @param {any} sendResponse A callback function (typically from a browser extension
* message listener, like `chrome.runtime.onMessage`)
* used to send the fetched news data back to the caller.
* It's called with an object like `{ news: { articles: [...] } }`.
*/
export async function fetchNews(source: string, sendResponse: any) { export async function fetchNews(source: string, sendResponse: any) {
if (source === "australia") { if (source === "australia") {
const date = new Date(); const date = new Date();
+60
View File
@@ -48,6 +48,40 @@ function createSEQTAAPI(): SEQTAAPI {
}; };
} }
/**
* Creates a reactive and persistent settings store for a given plugin.
* This store is a Svelte-like store, providing reactivity, persistence
* via `browser.storage.local`, and default value handling.
*
* @template T - Represents the structure of the plugin's settings, extending `PluginSettings`.
* @param {Plugin<T, any>} plugin The plugin instance for which the settings store is being created.
* `plugin.id` is used for namespacing the settings in storage,
* and `plugin.settings` provides the definitions and default values for each setting.
* @returns {SettingsAPI<T> & { loaded: Promise<void> }} An object that functions as a Svelte store,
* enhanced with specific methods for settings management.
* The object includes:
* - Reactivity: Changes to settings can be subscribed to using Svelte's store subscription pattern
* (though not explicitly a Svelte store, it behaves similarly for direct property access and updates).
* The `onChange` method provides a more direct way to listen for specific key changes.
* - Persistence: Settings are automatically loaded from `browser.storage.local` when the store is created
* and saved back whenever a setting is changed via the proxy's setter.
* - Default Values: Uses default values from the `plugin.settings` definition if no stored value exists for a setting.
* - `loaded`: A Promise that resolves when the settings have been successfully loaded from storage,
* allowing operations to be deferred until settings are ready.
* - Direct property access for getting values (e.g., `settingsStore.mySettingKey`).
* - Direct property assignment for setting values (e.g., `settingsStore.mySettingKey = newValue`), which also persists the change.
* - `onChange(key, callback)`: Method to listen for changes to a specific setting. (Note: The prompt mentioned `listen`, this is `onChange`).
* Returns an object with an `unregister` method.
* - `offChange(key, callback)`: Method to stop listening for changes to a specific setting.
* The following methods are not explicitly present on the returned proxy from `createSettingsAPI` but are typically
* expected in a full "Svelte store" settings manager. The current implementation relies on direct property
* manipulation for get/set, and re-initialization for reset-like behavior or would require external implementation
* of reset logic if needed:
* - `get(key)`: (Achieved by direct property access: `settingsStore.key`)
* - `set(key, value)`: (Achieved by direct property assignment: `settingsStore.key = value`)
* - `reset(key)`: (Would require manual re-application of `plugin.settings[key].default` and then setting it)
* - `resetAll()`: (Would require iterating through all `plugin.settings` and applying defaults, then setting them)
*/
function createSettingsAPI<T extends PluginSettings>( function createSettingsAPI<T extends PluginSettings>(
plugin: Plugin<T>, plugin: Plugin<T>,
): SettingsAPI<T> & { loaded: Promise<void> } { ): SettingsAPI<T> & { loaded: Promise<void> } {
@@ -293,6 +327,32 @@ function createEventsAPI(pluginId: string): EventsAPI {
}; };
} }
/**
* Creates and returns a tailored API object for a specific plugin.
* This API object provides the plugin with various functionalities such as
* managing settings, accessing namespaced storage, interacting with SEQTA-specific features,
* and handling plugin-specific events.
*
* @template T - The type of the plugin's settings, extending `PluginSettings`.
* @template S - The type of the data the plugin will store in its namespaced storage.
* @param {Plugin<T, S>} plugin The plugin instance for which the API is being created.
* The plugin's `id` and `name` are used internally by the API
* for namespacing and identification but are accessed from the `plugin` object directly.
* @returns {PluginAPI<T, S>} An API object containing the following key properties:
* - `seqta`: An API for interacting with SEQTA-specific functionalities, created by `createSEQTAAPI()`.
* This includes methods like `onMount` for DOM element appearance, `getFiber` for React component inspection,
* `getCurrentPage` for getting the current SEQTA page, and `onPageChange` for listening to page navigations.
* - `settings`: An API for managing plugin-specific settings, created by `createSettingsAPI(plugin)`.
* It allows getting, setting, and listening to changes in the plugin's settings,
* which are stored persistently and namespaced to the plugin. Includes a `loaded` promise.
* - `storage`: An API for providing namespaced storage for the plugin, created by `createStorageAPI<S>(plugin.id)`.
* It allows the plugin to store and retrieve arbitrary data, namespaced to prevent conflicts
* with other plugins or parts of the extension. Includes a `loaded` promise and `onChange` listeners.
* - `events`: An API for allowing the plugin to dispatch and listen for custom events within its own scope,
* created by `createEventsAPI(plugin.id)`. It provides `on(event, callback)` to listen for
* plugin-specific events and `emit(event, ...args)` to dispatch them. These events are namespaced
* to the plugin.
*/
export function createPluginAPI<T extends PluginSettings, S = any>( export function createPluginAPI<T extends PluginSettings, S = any>(
plugin: Plugin<T, S>, plugin: Plugin<T, S>,
): PluginAPI<T, S> { ): PluginAPI<T, S> {
+151 -3
View File
@@ -21,6 +21,12 @@ interface StorageChange<T = any> {
newValue?: T; newValue?: T;
} }
/**
* Singleton class responsible for the entire lifecycle of plugins.
* This includes registration, starting, stopping, event dispatching,
* managing plugin-specific styles, and listening for plugin setting changes
* to automatically start or stop plugins.
*/
export class PluginManager { export class PluginManager {
private static instance: PluginManager; private static instance: PluginManager;
private plugins: Map<string, Plugin<any, any>> = new Map(); private plugins: Map<string, Plugin<any, any>> = new Map();
@@ -30,10 +36,18 @@ export class PluginManager {
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map(); private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
private styleElements: Map<string, HTMLStyleElement> = new Map(); private styleElements: Map<string, HTMLStyleElement> = new Map();
/**
* Private constructor to enforce singleton pattern.
* Initializes the listener for plugin state changes from storage.
*/
private constructor() { private constructor() {
this.setupPluginStateListener(); this.setupPluginStateListener();
} }
/**
* Gets the singleton instance of the PluginManager.
* @returns {PluginManager} The singleton instance.
*/
public static getInstance(): PluginManager { public static getInstance(): PluginManager {
if (!PluginManager.instance) { if (!PluginManager.instance) {
PluginManager.instance = new PluginManager(); PluginManager.instance = new PluginManager();
@@ -41,6 +55,15 @@ export class PluginManager {
return PluginManager.instance; return PluginManager.instance;
} }
/**
* Dispatches an event to a specific plugin.
* If the plugin is currently running, the event is dispatched immediately via a DOM CustomEvent.
* If the plugin is not running, the event is added to a backlog to be processed when the plugin starts.
*
* @param {string} pluginId The ID of the target plugin.
* @param {string} event The name of the event to dispatch (e.g., "update").
* @param {any} [args] Optional arguments to pass with the event.
*/
public dispatchPluginEvent(pluginId: string, event: string, args?: any) { public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
const fullEventName = `plugin.${pluginId}.${event}`; const fullEventName = `plugin.${pluginId}.${event}`;
@@ -56,6 +79,14 @@ export class PluginManager {
} }
} }
/**
* Processes and dispatches any events that were backlogged for a plugin.
* This is typically called after a plugin has successfully started.
*
* @private
* @param {string} pluginId The ID of the plugin for which to process backlogged events.
* @returns {Promise<void>}
*/
private async processBackloggedEvents(pluginId: string) { private async processBackloggedEvents(pluginId: string) {
for (const [key, argsList] of this.eventBacklog.entries()) { for (const [key, argsList] of this.eventBacklog.entries()) {
const [eventPluginId, event] = key.split(":"); const [eventPluginId, event] = key.split(":");
@@ -68,6 +99,15 @@ export class PluginManager {
} }
} }
/**
* Registers a plugin with the manager.
* Plugins must have a unique ID.
*
* @template T - The type of settings the plugin uses.
* @template S - The type of storage the plugin uses.
* @param {Plugin<T, S>} plugin The plugin object to register.
* @throws {Error} If a plugin with the same ID is already registered.
*/
public registerPlugin<T extends PluginSettings, S>( public registerPlugin<T extends PluginSettings, S>(
plugin: Plugin<T, S>, plugin: Plugin<T, S>,
): void { ): void {
@@ -77,6 +117,22 @@ export class PluginManager {
this.plugins.set(plugin.id, plugin); this.plugins.set(plugin.id, plugin);
} }
/**
* Starts a specific plugin by its ID.
* This involves:
* - Checking if the plugin exists and isn't already running.
* - Creating and providing the plugin API (settings, storage, etc.).
* - Checking if the plugin is enabled (if `disableToggle` is true), respecting its `defaultEnabled` status.
* - Injecting any CSS styles defined by the plugin into the document head.
* - Waiting for the plugin's settings and storage to be loaded.
* - Executing the plugin's `run` method.
* - Storing any cleanup function returned by `run` for later use in `stopPlugin`.
* - Marking the plugin as running and processing any backlogged events for it.
*
* @param {string} pluginId The ID of the plugin to start.
* @returns {Promise<void>} A promise that resolves when the plugin has started or is determined not to start (e.g., disabled).
* @throws {Error} If the plugin is not found, or if an error occurs during plugin initialization or execution.
*/
public async startPlugin(pluginId: string): Promise<void> { public async startPlugin(pluginId: string): Promise<void> {
const plugin = this.plugins.get(pluginId); const plugin = this.plugins.get(pluginId);
if (!plugin) { if (!plugin) {
@@ -138,17 +194,36 @@ export class PluginManager {
} }
} }
/**
* Attempts to start all registered plugins.
* Errors during the start of individual plugins are caught and logged,
* allowing other plugins to attempt to start.
*
* @returns {Promise<void>} A promise that resolves when all plugins have attempted to start.
* It uses `Promise.allSettled` to wait for all start operations.
*/
public async startAllPlugins(): Promise<void> { public async startAllPlugins(): Promise<void> {
const startPromises = Array.from(this.plugins.keys()).map((id) => const startPromises = Array.from(this.plugins.keys()).map((id) =>
this.startPlugin(id).catch((error) => { this.startPlugin(id).catch((error) => {
console.error(`Failed to start plugin "${id}":`, error); console.error(`Failed to start plugin "${id}":`, error);
return Promise.reject(error); return Promise.reject(error); // Still reject to indicate failure for this specific plugin if needed by caller
}), }),
); );
await Promise.allSettled(startPromises); await Promise.allSettled(startPromises);
} }
/**
* Stops a specific plugin by its ID.
* This involves:
* - Removing any CSS styles injected by the plugin.
* - Executing the cleanup function that was returned by the plugin's `run` method (if any).
* - Marking the plugin as not running.
* - Emitting a "plugin.stopped" event with the pluginId.
*
* @param {string} pluginId The ID of the plugin to stop.
* @returns {Promise<void>} A promise that resolves when the plugin has been stopped.
*/
public async stopPlugin(pluginId: string): Promise<void> { public async stopPlugin(pluginId: string): Promise<void> {
// Remove plugin styles // Remove plugin styles
const styleElement = this.styleElements.get(pluginId); const styleElement = this.styleElements.get(pluginId);
@@ -167,18 +242,47 @@ export class PluginManager {
this.emit("plugin.stopped", pluginId); this.emit("plugin.stopped", pluginId);
} }
/**
* Stops all currently running plugins.
* Iterates through all registered plugins and calls `stopPlugin` for each.
*/
public stopAllPlugins(): void { public stopAllPlugins(): void {
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id)); Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
} }
/**
* Retrieves a registered plugin by its ID.
*
* @param {string} pluginId The ID of the plugin to retrieve.
* @returns {Plugin | undefined} The plugin object if found, otherwise undefined.
*/
public getPlugin(pluginId: string): Plugin | undefined { public getPlugin(pluginId: string): Plugin | undefined {
return this.plugins.get(pluginId); return this.plugins.get(pluginId);
} }
/**
* Retrieves an array of all registered plugin objects.
*
* @returns {Plugin[]} An array containing all registered plugin objects.
*/
public getAllPlugins(): Plugin[] { public getAllPlugins(): Plugin[] {
return Array.from(this.plugins.values()); return Array.from(this.plugins.values());
} }
/**
* Retrieves a structured list of settings for all registered plugins.
* This is primarily used for building user interfaces for plugin configuration.
* It processes each plugin's defined settings, adding IDs, titles, descriptions,
* and default enabled states. For plugins with `disableToggle`, an "enabled"
* boolean setting is automatically included.
*
* @returns {Array<object>} An array of objects, where each object represents a plugin
* and contains its ID, name, description, beta status,
* and a processed `settings` object. The `settings` object
* maps setting keys to their detailed configuration (type, title, etc.).
* The specific structure of each setting object within `settings`
* depends on its type (boolean, string, number, select, button, hotkey).
*/
public getAllPluginSettings(): Array<{ public getAllPluginSettings(): Array<{
pluginId: string; pluginId: string;
name: string; name: string;
@@ -197,6 +301,8 @@ export class PluginManager {
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> }) | (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string }); | (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string });
}; };
// Actual type is more complex, see original code, but this gives the gist for the JSDoc.
// Array<{ pluginId: string; name: string; description: string; beta?: boolean; settings: Record<string, ProcessedSetting>; disableToggle?: boolean; }>
}> { }> {
return Array.from(this.plugins.entries()).map(([id, plugin]) => { return Array.from(this.plugins.entries()).map(([id, plugin]) => {
const settingsEntries = Object.entries(plugin.settings).map( const settingsEntries = Object.entries(plugin.settings).map(
@@ -245,10 +351,24 @@ export class PluginManager {
}); });
} }
/**
* Checks if a specific plugin is currently running.
*
* @param {string} pluginId The ID of the plugin to check.
* @returns {boolean} True if the plugin is running, false otherwise.
*/
public isPluginRunning(pluginId: string): boolean { public isPluginRunning(pluginId: string): boolean {
return this.runningPlugins.get(pluginId) || false; return this.runningPlugins.get(pluginId) || false;
} }
/**
* Emits an event to all registered listeners for that event.
* This is an internal event bus for the PluginManager itself.
*
* @private
* @param {string} event The name of the event to emit.
* @param {any[]} args Arguments to pass to the event listeners.
*/
private emit(event: string, ...args: any[]): void { private emit(event: string, ...args: any[]): void {
const listeners = this.listeners.get(event); const listeners = this.listeners.get(event);
if (listeners) { if (listeners) {
@@ -256,6 +376,12 @@ export class PluginManager {
} }
} }
/**
* Registers an event listener for PluginManager's internal events.
*
* @param {string} event The name of the event to listen for (e.g., "plugin.stopped").
* @param {(...args: any[]) => void} callback The function to call when the event is emitted.
*/
public on(event: string, callback: (...args: any[]) => void): void { public on(event: string, callback: (...args: any[]) => void): void {
if (!this.listeners.has(event)) { if (!this.listeners.has(event)) {
this.listeners.set(event, new Set()); this.listeners.set(event, new Set());
@@ -263,6 +389,12 @@ export class PluginManager {
this.listeners.get(event)!.add(callback); this.listeners.get(event)!.add(callback);
} }
/**
* Unregisters an event listener for PluginManager's internal events.
*
* @param {string} event The name of the event.
* @param {(...args: any[]) => void} callback The callback function to remove.
*/
public off(event: string, callback: (...args: any[]) => void): void { public off(event: string, callback: (...args: any[]) => void): void {
const listeners = this.listeners.get(event); const listeners = this.listeners.get(event);
if (listeners) { if (listeners) {
@@ -270,7 +402,16 @@ export class PluginManager {
} }
} }
// Add handler for plugin enable/disable state changes /**
* Handles the change in a plugin's enabled state.
* Starts or stops the plugin based on the new `enabled` value.
* This is typically called by `setupPluginStateListener` when a relevant storage change is detected.
*
* @private
* @param {string} pluginId The ID of the plugin whose state has changed.
* @param {boolean} enabled The new enabled state of the plugin.
* @returns {Promise<void>}
*/
private async handlePluginStateChange( private async handlePluginStateChange(
pluginId: string, pluginId: string,
enabled: boolean, enabled: boolean,
@@ -282,7 +423,14 @@ export class PluginManager {
} }
} }
// Add listener for plugin settings changes /**
* Sets up a listener for browser storage changes.
* This listener monitors changes to plugin settings (specifically the `enabled` property
* for plugins with `disableToggle: true`) and calls `handlePluginStateChange`
* to automatically start or stop plugins accordingly.
*
* @private
*/
private setupPluginStateListener(): void { private setupPluginStateListener(): void {
browser.storage.onChanged.addListener( browser.storage.onChanged.addListener(
(changes: { [key: string]: StorageChange }, area: string) => { (changes: { [key: string]: StorageChange }, area: string) => {
+98 -4
View File
@@ -5,8 +5,19 @@ import type {
SelectSetting, SelectSetting,
StringSetting, StringSetting,
HotkeySetting, HotkeySetting,
PluginSettings,
} from "./types"; } from "./types";
/**
* Creates a complete `NumberSetting` object from its options.
* This helper function ensures the `type` property is correctly set to "number".
* It's used for defining a numeric setting for a plugin.
* This function itself does not handle storage or persistence; it defines the setting's structure.
*
* @param {Omit<NumberSetting, "type">} options The configuration options for the number setting,
* excluding the `type` property (e.g., `title`, `default`, `min`, `max`).
* @returns {NumberSetting} A complete number setting object with `type: "number"`.
*/
export function numberSetting( export function numberSetting(
options: Omit<NumberSetting, "type">, options: Omit<NumberSetting, "type">,
): NumberSetting { ): NumberSetting {
@@ -16,6 +27,16 @@ export function numberSetting(
}; };
} }
/**
* Creates a complete `BooleanSetting` object from its options.
* This helper function ensures the `type` property is correctly set to "boolean".
* It's used for defining a boolean (true/false) setting for a plugin.
* This function itself does not handle storage or persistence; it defines the setting's structure.
*
* @param {Omit<BooleanSetting, "type">} options The configuration options for the boolean setting,
* excluding the `type` property (e.g., `title`, `default`).
* @returns {BooleanSetting} A complete boolean setting object with `type: "boolean"`.
*/
export function booleanSetting( export function booleanSetting(
options: Omit<BooleanSetting, "type">, options: Omit<BooleanSetting, "type">,
): BooleanSetting { ): BooleanSetting {
@@ -25,6 +46,16 @@ export function booleanSetting(
}; };
} }
/**
* Creates a complete `StringSetting` object from its options.
* This helper function ensures the `type` property is correctly set to "string".
* It's used for defining a text-based setting for a plugin.
* This function itself does not handle storage or persistence; it defines the setting's structure.
*
* @param {Omit<StringSetting, "type">} options The configuration options for the string setting,
* excluding the `type` property (e.g., `title`, `default`, `placeholder`).
* @returns {StringSetting} A complete string setting object with `type: "string"`.
*/
export function stringSetting( export function stringSetting(
options: Omit<StringSetting, "type">, options: Omit<StringSetting, "type">,
): StringSetting { ): StringSetting {
@@ -34,15 +65,36 @@ export function stringSetting(
}; };
} }
export function selectSetting<T extends string>( /**
options: Omit<SelectSetting<T>, "type">, * Creates a complete `SelectSetting` object from its options.
): SelectSetting<T> { * This helper function ensures the `type` property is correctly set to "select".
* It's used for defining a setting where the user can choose from a predefined list of options.
* This function itself does not handle storage or persistence; it defines the setting's structure.
*
* @template TValue - The type of the value for each option in the select list (extends string).
* @param {Omit<SelectSetting<TValue>, "type">} options The configuration options for the select setting,
* excluding the `type` property (e.g., `title`, `default`, `options` array).
* @returns {SelectSetting<TValue>} A complete select setting object with `type: "select"`.
*/
export function selectSetting<TValue extends string>(
options: Omit<SelectSetting<TValue>, "type">,
): SelectSetting<TValue> {
return { return {
type: "select", type: "select",
...options, ...options,
}; };
} }
/**
* Creates a complete `ButtonSetting` object from its options.
* This helper function ensures the `type` property is correctly set to "button".
* It's used for defining a button in the plugin's settings UI, which can trigger an action.
* This function itself does not handle storage or persistence; it defines the button's structure and action.
*
* @param {Omit<ButtonSetting, "type">} options The configuration options for the button setting,
* excluding the `type` property (e.g., `title`, `label`, `trigger` function).
* @returns {ButtonSetting} A complete button setting object with `type: "button"`.
*/
export function buttonSetting( export function buttonSetting(
options: Omit<ButtonSetting, "type">, options: Omit<ButtonSetting, "type">,
): ButtonSetting { ): ButtonSetting {
@@ -52,6 +104,16 @@ export function buttonSetting(
}; };
} }
/**
* Creates a complete `HotkeySetting` object from its options.
* This helper function ensures the `type` property is correctly set to "hotkey".
* It's used for defining a setting where the user can configure a keyboard shortcut.
* This function itself does not handle storage or persistence; it defines the hotkey setting's structure.
*
* @param {Omit<HotkeySetting, "type">} options The configuration options for the hotkey setting,
* excluding the `type` property (e.g., `title`, `default` hotkey string).
* @returns {HotkeySetting} A complete hotkey setting object with `type: "hotkey"`.
*/
export function hotkeySetting( export function hotkeySetting(
options: Omit<HotkeySetting, "type">, options: Omit<HotkeySetting, "type">,
): HotkeySetting { ): HotkeySetting {
@@ -61,10 +123,42 @@ export function hotkeySetting(
}; };
} }
export function defineSettings<T extends Record<string, any>>(settings: T): T { /**
* Defines a collection of settings for a plugin.
* This function currently acts as an identity function, returning the settings object as is.
* Its primary purpose is to provide type inference and a structured way to define
* the entire settings configuration for a plugin, ensuring it conforms to the expected type.
* This function itself does not handle storage or persistence; it's for structural definition.
*
* @template TSettings - A record type where keys are setting names and values are setting definition objects
* (e.g., `NumberSetting`, `BooleanSetting`).
* @param {TSettings} settings The complete settings configuration object for the plugin.
* @returns {TSettings} The same settings configuration object, primarily for type checking/inference.
*/
export function defineSettings<TSettings extends Record<string, any>>(settings: TSettings): TSettings {
return settings; return settings;
} }
/**
* A property decorator for declaratively defining a plugin setting on a class property.
* When a class property is decorated with `@Setting({...})`, this decorator adds the
* provided setting definition (`settingDef`) to a static `settings` object on the
* class's prototype. This allows settings to be defined alongside their related class logic.
* This decorator itself does not handle runtime storage or persistence of setting *values*;
* it is for defining the *structure* and *metadata* of a setting.
*
* Example:
* ```typescript
* class MyPlugin extends BasePlugin {
* @Setting(numberSetting({ title: "My Number", default: 10 }))
* myNumberSetting: number; // Type annotation for the setting's value
* }
* ```
*
* @param {any} settingDef The setting definition object, typically created by one of the
* helper functions like `numberSetting(...)`, `booleanSetting(...)`, etc.
* @returns {PropertyDecorator} A property decorator function.
*/
export function Setting(settingDef: any): PropertyDecorator { export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => { return (target, propertyKey) => {
const proto = target.constructor.prototype; const proto = target.constructor.prototype;