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

Add jsdoc comments
This commit is contained in:
Aden Lindsay
2025-05-29 22:27:08 +09:30
committed by GitHub
13 changed files with 686 additions and 61 deletions
+33
View File
@@ -1,8 +1,41 @@
import fs from "fs";
import mime from "mime-types";
/**
* A Vite plugin designed to load files as base64 encoded data URLs.
* This plugin intercepts module imports that have a `?base64` query parameter
* appended to the file path. It then reads the targeted file, converts its content
* to a base64 string, and constructs a data URL which is then exported as the
* default export of a new JavaScript module.
*
* @example
* // To use this loader, import a file with ?base64 query:
* // import myImageBase64 from './path/to/myimage.png?base64';
* // myImageBase64 will then be a string like "data:image/png;base64,..."
*/
export const base64Loader = {
/**
* The name of the Vite plugin.
* @type {string}
*/
name: "base64-loader",
/**
* The core transformation function of the Vite plugin.
* It is called by Vite for modules that might need transformation. This function
* checks if the module ID includes the `?base64` query. If so, it reads the
* specified file, converts it to a base64 data URL, and returns a new
* JavaScript module that default exports this data URL.
*
* @param {any} _ The original code of the file. This parameter is unused by this loader.
* @param {string} id The ID of the module being transformed. This string typically
* contains the absolute file path and any query parameters
* (e.g., "/path/to/file.png?base64").
* @returns {string | null} If the module ID does not contain `?base64` query,
* it returns `null` to indicate no transformation.
* Otherwise, it returns a string of JavaScript code
* that default exports the base64 data URL of the file.
* For example: `export default 'data:image/png;base64,xxxx';`
*/
transform(_: any, id: string) {
const [filePath, query] = id.split("?");
if (query !== "base64") return null;
+39 -6
View File
@@ -1,25 +1,58 @@
// ref: https://stackoverflow.com/a/76920975
import type { Plugin } from "vite";
/**
* Creates a Vite plugin designed to gracefully handle the conclusion of the build process.
* This plugin utilizes the `buildEnd` and `closeBundle` hooks provided by Vite.
* It checks for errors at the end of the build:
* - If an error occurred during the build (`buildEnd` hook receives an error), it logs the error
* and explicitly exits the Node.js process with a status code of 1 (indicating failure).
* - If the build completes without errors and the bundle is successfully generated
* (`closeBundle` hook is called), it logs a success message and exits the process
* with a status code of 0 (indicating success).
* This explicit process exiting can be useful in CI/CD environments or scripts that
* rely on the process status code to determine the build outcome.
* The core logic for using these hooks to exit the process is inspired by
* a solution found on StackOverflow (https://stackoverflow.com/a/76920975).
*
* @returns {Plugin} A Vite plugin object configured with `name`, `buildEnd`, and `closeBundle` hooks.
*/
export default function ClosePlugin(): Plugin {
return {
/**
* The unique name of this Vite plugin. This name is used by Vite for identification
* purposes and will appear in warnings, errors, and logs related to this plugin.
* @type {string}
*/
name: "ClosePlugin", // required, will show up in warnings and errors
// use this to catch errors when building
/**
* A Vite hook that is called when the build process has finished, regardless of
* whether it was successful or encountered an error.
*
* @param {Error} [error] An optional error object. If the build failed, this parameter
* will contain the error that occurred. If the build was successful,
* this parameter will be undefined or null.
*/
buildEnd(error) {
if (error) {
console.error("Error bundling");
console.error(error);
process.exit(1);
process.exit(1); // Exit with status 1 indicating an error
} else {
console.log("Build ended");
console.log("Build ended"); // Log successful completion of the build phase
}
},
// use this to catch the end of a build without errors
/**
* A Vite hook that is called after the `buildEnd` hook, but only if the build
* was successful (i.e., no errors were passed to `buildEnd`) and all output
* files have been generated and written to disk. This signifies the successful
* completion of the entire bundling process.
*/
closeBundle() {
console.log("Bundle closed");
process.exit(0);
console.log("Bundle closed"); // Log successful closure of the bundle
process.exit(0); // Exit with status 0 indicating a successful build
},
};
}
+23 -10
View File
@@ -1,12 +1,22 @@
import type { Browser, BuildTarget, Manifest } from "./types";
import type { AnyCase } from "./utils";
/**
*
* Packages a given manifest object with a specific target browser identifier.
* This function is typically used in multi-browser extension build processes
* to create a configuration object that pairs the manifest data with the browser
* it's intended for. The `AnyCase<Browser>` type for the browser parameter
* implies that browser names like 'chrome', 'firefox', etc., can be provided
* in various casings.
*
* @export
* @param {Manifest} manifest
* @param {AnyCase<Browser>} browser
* @return {*} {@link BuildTarget}
* @param {Manifest} manifest The core manifest data for the extension,
* compatible with `chrome.runtime.ManifestV3` as defined by the {@link Manifest} type.
* @param {AnyCase<Browser>} browser The target browser identifier (e.g., 'chrome', 'firefox', 'CHROME').
* Refers to the {@link Browser} type, allowing for flexible casing.
* @returns {BuildTarget} An object that pairs the `manifest` with its target `browser`.
* The structure is `{ manifest: Manifest; browser: AnyCase<Browser>; }`
* as defined by the {@link BuildTarget} type.
*/
export function createManifest(
manifest: Manifest,
@@ -19,14 +29,17 @@ export function createManifest(
}
/**
* create a base Manifest to inherit from
* type Manifest = chrome.runtime.ManifestV3
*
* use as shared base to extend inBrowser manifests
* Defines a base manifest object.
* This function is typically used to establish a common, shared foundation for an extension's manifest
* (compatible with `chrome.runtime.ManifestV3` as per the {@link Manifest} type).
* This base can then be extended or modified for different browsers or specific build configurations.
* For example, you might define core permissions and properties here, and then add
* browser-specific keys in subsequent steps.
*
* @export
* @param {Manifest} manifest
* @return {*} {@link Manifest}
* @param {Manifest} manifest The core manifest data to be used as a base.
* This should conform to the {@link Manifest} type structure.
* @returns {Manifest} The provided manifest object, intended to serve as a reusable base.
*/
export function createManifestBase(manifest: Manifest): Manifest {
return manifest;
+42 -9
View File
@@ -1,27 +1,60 @@
// vite-plugin-inline-worker-dev.ts
// vite-plugin-inline-worker-dev.ts
import { Plugin } from "vite";
import fs from "fs/promises";
import { build, transform } from "esbuild";
import { build } from "esbuild";
/**
* Creates a Vite plugin designed for bundling and inlining web worker scripts during development.
* This plugin specifically targets module imports that include a `?inlineWorker` query parameter.
* When such an import is encountered, the plugin bundles the worker script using `esbuild`
* and then generates JavaScript code that inlines this bundled worker as a Blob,
* creating the worker instance via `URL.createObjectURL()`.
* The name "vite:inline-worker-dev" suggests it's primarily intended for development builds.
*
* @returns {Plugin} A Vite plugin object with `name` and `load` properties.
*/
export default function InlineWorkerDevPlugin(): Plugin {
return {
/**
* The unique name of this Vite plugin.
* @type {string}
*/
name: "vite:inline-worker-dev",
/**
* The Vite hook responsible for loading and transforming modules.
* This function intercepts modules imported with `?inlineWorker`.
* For such modules, it bundles the worker script and returns JavaScript code
* that, when executed, will create an instance of this worker from an inlined Blob.
*
* @async
* @param {string} id The path or ID of the module Vite is attempting to load,
* potentially including query parameters (e.g., "/path/to/worker.ts?inlineWorker").
* @returns {Promise<string | null>} A promise that resolves to:
* - `null` if the module ID does not include `?inlineWorker`.
* - A string of JavaScript code if the module is an inline worker.
* This code will define a default export function (e.g., `InlineWorker`)
* that, when called, creates and returns a new `Worker` instance
* from the bundled and inlined worker script.
*/
async load(id) {
if (id.includes("?inlineWorker")) {
const [cleanPath] = id.split("?");
console.log("cleanPath", cleanPath);
const code = await fs.readFile(cleanPath, "utf-8");
// Note: Original code had `await fs.readFile(cleanPath, "utf-8");` but `code` wasn't used.
// `esbuild` directly takes `cleanPath` as an entry point.
const result = await build({
entryPoints: [cleanPath],
entryPoints: [cleanPath], // esbuild uses the file path directly
bundle: true,
write: false,
platform: "browser",
format: "iife",
target: "esnext",
write: false, // We want the output in memory, not written to disk
platform: "browser", // Target environment for the worker code
format: "iife", // Immediately Invoked Function Expression, suitable for workers
target: "esnext", // Transpile to modern JavaScript
});
const workerCode = result.outputFiles[0].text;
// Construct JavaScript code that will create the worker from a Blob.
// This code is what gets returned to Vite and replaces the original import.
const workerBlobCode = `
const code = ${JSON.stringify(workerCode)};
export default function InlineWorker() {
@@ -31,7 +64,7 @@ export default function InlineWorkerDevPlugin(): Plugin {
`;
return workerBlobCode;
}
return null;
return null; // Let Vite handle other modules normally
},
};
}
+101 -18
View File
@@ -1,8 +1,33 @@
/**
* @fileoverview
* This script is a command-line utility for publishing the BetterSEQTA+ extension.
* It automates the process of finding the latest built extension ZIP files for specified
* browsers, zipping the project source code (for Firefox), and then invoking the
* `publish-extension` tool with the appropriate arguments.
*
* To use this script, invoke it with Node.js followed by browser arguments:
* e.g., `node lib/publish.js --b chrome firefox`
* or `node lib/publish.js --b chrome`
* or `node lib/publish.js --b firefox`
*/
const glob = require("glob");
const semver = require("semver");
const { execSync } = require("child_process");
const path = require("path");
/**
* Determines the latest version string from a list of filenames that include version numbers.
* Filenames are expected to follow a pattern like `betterseqtaplus@3.4.5.1-chrome.zip`.
* This function handles potential 4-part versions (e.g., `3.4.5.1`) by trimming them
* to 3 parts (e.g., `3.4.5`) for comparison using the `semver` library. After identifying
* the latest semver-compatible version, it returns the original full version string
* (e.g., "3.4.5.1") that corresponds to this latest version.
*
* @param {string[]} files An array of filenames.
* @returns {string | null} The latest version string (e.g., "3.4.5.1") found among the files,
* or `null` if no valid version numbers are found or no files are provided.
*/
function getLatestVersion(files) {
console.log("Files passed to getLatestVersion:", files);
@@ -19,32 +44,56 @@ function getLatestVersion(files) {
if (!match) return null;
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
const semverVersion = fullVersion.split(".").slice(0, 3).join("."); // Trim to 3.4.5
// Trim to 3 parts for semver comparison, as semver typically handles X.Y.Z
const semverVersion = fullVersion.split(".").slice(0, 3).join(".");
return { fullVersion, semverVersion };
})
.filter(Boolean);
.filter(Boolean); // Remove null entries if any file didn't match
console.log(
"Extracted versions:",
versions.map((v) => v.semverVersion),
);
if (versions.length === 0) {
console.log("No versions extracted.");
return null;
}
// Find latest version using the trimmed semver format
const latestSemver = semver.maxSatisfying(
versions.map((v) => v.semverVersion),
"*",
"*", // Satisfy any version, effectively finding the max
);
console.log("Latest SemVer-compatible version:", latestSemver);
// Get the full version that matches the latest SemVer version
const latestVersion =
versions.find((v) => v.semverVersion === latestSemver)?.fullVersion || null;
console.log("Final selected latest version:", latestVersion);
return latestVersion;
if (!latestSemver) {
console.log("Could not determine latest semver version.");
return null;
}
// Get the original full version string that matches the identified latest SemVer version
const latestVersionData = versions.find(
(v) => v.semverVersion === latestSemver,
);
const latestFullVersion = latestVersionData ? latestVersionData.fullVersion : null;
console.log("Final selected latest version:", latestFullVersion);
return latestFullVersion;
}
/**
* Finds the path to the latest built ZIP file for a specific browser.
* It constructs a glob pattern based on the browser name (e.g., `dist/betterseqtaplus@*-*chrome.zip`),
* finds all matching files, and then uses `getLatestVersion` to identify the version string
* of the most recent file. Finally, it returns the full path to that specific file.
*
* @param {string} browser A string indicating the target browser (e.g., "chrome", "firefox").
* @returns {string | undefined} The filepath string to the latest ZIP file for the specified browser,
* or `undefined` if no matching file is found or if the latest version
* cannot be determined.
*/
function getLatestFiles(browser) {
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
console.log("Glob pattern:", pattern);
@@ -52,15 +101,32 @@ function getLatestFiles(browser) {
const files = glob.sync(pattern);
console.log("Files found for browser", browser, ":", files);
const latestVersion = getLatestVersion(files);
if (files.length === 0) {
console.log("No files found for browser", browser);
return undefined;
}
// Find the exact file by matching the original full version
const latestVersion = getLatestVersion(files);
if (!latestVersion) {
console.log("Could not determine latest version for browser", browser);
return undefined;
}
// Find the exact file by matching the original full version string
const latestFile = files.find((file) => file.includes(`@${latestVersion}-`));
console.log("Latest file for browser", browser, ":", latestFile);
return latestFile;
}
/**
* Creates a ZIP file of the project's source code, excluding specified development-related
* files and directories such as `node_modules`, `dist`, `.git`, etc.
* It uses the `7z` command-line tool to perform the archiving.
* The output filename is fixed as `dist/betterseqtaplus@latest-sources.zip`.
*
* @returns {string} The filename of the created ZIP file (e.g., `dist/betterseqtaplus@latest-sources.zip`).
*/
function zipSources() {
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
@@ -74,17 +140,31 @@ function zipSources() {
"LICENSE",
"package.json",
]
.map((pattern) => `-x!${pattern}`)
.map((pattern) => `-x!${pattern}`) // Format for 7z exclude syntax
.join(" ");
// Command to zip the current directory's contents into zipFileName, applying exclude patterns
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
console.log("Zipping project sources with command:", zipCommand);
execSync(zipCommand, { stdio: "inherit" });
execSync(zipCommand, { stdio: "inherit" }); // Execute synchronously and show output
return zipFileName;
}
/**
* Orchestrates the extension publishing process for the specified browsers.
* This function performs the following steps:
* 1. Calls `getLatestFiles` to find the latest built ZIP for Chrome if "chrome" is in `browsers`.
* 2. Calls `getLatestFiles` to find the latest built ZIP for Firefox if "firefox" is in `browsers`.
* 3. Calls `zipSources` to create a source code ZIP if "firefox" is in `browsers` (required for Mozilla Add-ons).
* 4. Validates that all required files were found and that at least one browser was specified. Exits if not.
* 5. Constructs the `publish-extension` command-line string with the appropriate arguments
* based on the found ZIP files for the specified browsers.
* 6. Executes the constructed `publish-extension` command.
*
* @param {string[]} browsers An array of browser strings (e.g., ["chrome", "firefox"]) for which to publish the extension.
*/
function runPublishCommand(browsers) {
const chromeZip = browsers.includes("chrome")
? getLatestFiles("chrome")
@@ -92,6 +172,7 @@ function runPublishCommand(browsers) {
const firefoxZip = browsers.includes("firefox")
? getLatestFiles("firefox")
: null;
// Sources are typically only needed for Firefox submissions
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
console.log("Chrome zip:", chromeZip);
@@ -100,15 +181,16 @@ function runPublishCommand(browsers) {
if (browsers.length === 0) {
console.log("No browsers specified. Exiting.");
process.exit(0);
process.exit(0); // Exit gracefully if no action is needed
}
// Check if required files are missing for the specified browsers
if (
(browsers.includes("chrome") && !chromeZip) ||
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
) {
console.error("Could not find required zip files for specified browsers.");
process.exit(1);
process.exit(1); // Exit with error status
}
let command = "publish-extension";
@@ -120,12 +202,13 @@ function runPublishCommand(browsers) {
}
console.log("Running command:", command);
execSync(command, { stdio: "inherit" });
execSync(command, { stdio: "inherit" }); // Execute and show output
}
// Parse command-line arguments
// Parse command-line arguments to determine which browsers to publish for
const args = process.argv.slice(2);
const browserIndex = args.indexOf("--b");
const browserIndex = args.indexOf("--b"); // Find the --b flag
// If --b is found, take all subsequent arguments as browser names
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
runPublishCommand(browsers);
+42 -4
View File
@@ -1,17 +1,55 @@
import fs from "fs";
/**
* Creates a Vite plugin designed to improve the reliability of Hot Module Replacement (HMR)
* for global CSS files.
*
* When a JavaScript/TypeScript module that imports a CSS file is updated, Vite's HMR
* might not always reliably update the styles injected by that global CSS. This plugin
* attempts to mitigate this by listening for hot updates. If an updated module
* has direct importers that are CSS files (e.g., a JS file imports a global CSS file),
* this plugin will "touch" those CSS files by updating their access and modification
* timestamps using `fs.utimesSync`. This action can help signal to Vite or the browser
* that the CSS file has changed, potentially triggering a more reliable style reload.
*
* @returns {import('vite').Plugin} A Vite plugin object configured with `name` and `handleHotUpdate` hooks.
*/
export default function touchGlobalCSSPlugin() {
return {
/**
* The unique name of this Vite plugin.
* This name is used by Vite for identification purposes and will appear in logs.
* @type {string}
*/
name: "touch-global-css",
/**
* A Vite hook that is called when a module is hot-updated.
* This function inspects the importers of the updated module. If any of these
* importers are CSS files, their filesystem timestamps are updated ("touched").
*
* @param {object} context The context object provided by Vite's `handleHotUpdate` hook.
* @param {Array<import('vite').ModuleNode>} context.modules An array of `ModuleNode` instances that have been updated.
* This plugin specifically accesses `modules[0]._clientModule.importers`
* to find CSS files that import the updated module.
*/
handleHotUpdate({ modules }) {
// log all of the staticImportedUrls
const importers = modules[0]._clientModule.importers;
// It's assumed `modules[0]` is the primary updated module of interest.
// `_clientModule` and `importers` might be internal or less stable Vite APIs.
const importers = modules[0]?._clientModule?.importers;
if (importers) {
importers.forEach((importer) => {
if (importer.file.includes(".css")) {
console.log("touching", importer.file);
// Check if the importer is a CSS file
if (importer.file && importer.file.includes(".css")) {
console.log("[touch-global-css] touching", importer.file);
try {
// Update the access and modification times of the CSS file to the current time
fs.utimesSync(importer.file, new Date(), new Date());
} catch (err) {
console.error(`[touch-global-css] Error touching file ${importer.file}:`, err);
}
}
});
}
},
};
}
+144 -8
View File
@@ -1,6 +1,9 @@
import type { ManifestV3Export } from "@crxjs/vite-plugin";
import { type AnyCase, createEnum } from "./utils";
import { type AnyCase, createEnum, ObjectValues } from "./utils";
/**
* Enumerates supported JavaScript frameworks for project generation or configuration.
*/
export const FrameworkEnum = {
React: "React",
Vanilla: "Vanilla",
@@ -10,6 +13,9 @@ export const FrameworkEnum = {
Vue: "Vue",
} as const;
/**
* Enumerates supported web browsers, typically for targeting builds or configurations.
*/
export const BrowserEnum = {
Chrome: "Chrome",
Brave: "Brave",
@@ -19,15 +25,26 @@ export const BrowserEnum = {
Safari: "Safari",
} as const;
/**
* @private
* Enumerates supported programming languages for project setup.
* This enum is not exported, suggesting it's for internal use within this module or related modules.
*/
const LanguageEnum = {
TypeScript: "TypeScript",
JavaScript: "JavaScript",
} as const;
/**
* Enumerates supported styling options or libraries.
*/
export const StyleEnum = {
Tailwind: "Tailwind",
} as const;
/**
* Enumerates supported package managers.
*/
export const PackageManagerEnum = {
Bun: "Bun",
PnPm: "PnPm",
@@ -35,7 +52,21 @@ export const PackageManagerEnum = {
Yarn: "Yarn",
} as const;
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
/**
* Defines the structure for browser-specific settings within a web extension manifest.
* This is particularly used for Firefox (gecko) extensions to specify properties like
* an extension ID, and minimum/maximum supported browser versions.
* The structure is based on common manifest extensions for Firefox.
* See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings
* The link in the original code (// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts)
* also points to type definitions that include this structure.
*
* @property {object} [browser_specific_settings] - Container for browser-specific settings.
* @property {object} [browser_specific_settings.gecko] - Settings specific to Gecko-based browsers (e.g., Firefox).
* @property {string} [browser_specific_settings.gecko.id] - The unique identifier for the extension in Firefox.
* @property {string} [browser_specific_settings.gecko.strict_min_version] - The minimum version of Firefox the extension is compatible with.
* @property {string} [browser_specific_settings.gecko.strict_max_version] - The maximum version of Firefox the extension is compatible with.
*/
export type BrowserSpecificSettings = {
browser_specific_settings?: {
gecko?: {
@@ -46,59 +77,164 @@ export type BrowserSpecificSettings = {
};
};
/**
* Represents the structure of a Chrome Manifest V3 file.
* This type is an alias for `ManifestV3Export` from the `@crxjs/vite-plugin`,
* which provides a comprehensive definition for Chrome extension manifests.
*/
export type Manifest = ManifestV3Export;
/** Alias for the `icons` property within a Chrome Manifest V3. */
export type ManifestIcons = chrome.runtime.ManifestIcons;
/** Alias for the `background` property within a Chrome Manifest V3. */
export type ManifestBackground = chrome.runtime.ManifestV3["background"];
/** Alias for the `content_scripts` property within a Chrome Manifest V3. */
export type ManifestContentScripts =
chrome.runtime.ManifestV3["content_scripts"];
/** Alias for the `web_accessible_resources` property within a Chrome Manifest V3. */
export type ManifestWebAccessibleResources =
chrome.runtime.ManifestV3["web_accessible_resources"];
/** Alias for the `commands` property within a Chrome Manifest V3. */
export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
/** Alias for the `action` property (or `browser_action`/`page_action`) within a Chrome Manifest V3. */
export type ManifestAction = chrome.runtime.ManifestV3["action"];
/** Alias for the `permissions` property within a Chrome Manifest V3. */
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
/** Alias for the `options_ui` property within a Chrome Manifest V3. */
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
/** Alias for the `chrome_url_overrides` property within a Chrome Manifest V3. */
export type ManifestURLOverrides =
chrome.runtime.ManifestV3["chrome_url_overrides"];
/**
* Creates a type that accepts a string literal `T` in either its capitalized or lowercase form.
* Useful for defining types that should be case-insensitive for specific known strings.
* @template T - A string literal type.
*/
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>;
/**
* Creates a record type where both keys and values are derived from a string literal `T`,
* specifically using `BrowserName<T>` which allows for capitalized or lowercase forms.
* This could be used to define an object where, for example, keys are 'Chrome' or 'chrome'
* and values are also 'Chrome' or 'chrome'.
* @template T - A string literal type, typically representing a browser name.
*/
export type BrowserEnumType<T extends string> = {
[browser in BrowserName<T>]: BrowserName<T>;
};
/**
* Represents the target browser for a build, allowing for various casings of browser names
* (e.g., "chrome", "Chrome", "CHROME") through the `AnyCase<Browser>` utility type.
* `Browser` itself is a union of specific browser name strings (e.g., "Chrome" | "Firefox").
*/
export type BuildMode = AnyCase<Browser>;
/**
* Defines an object structure that pairs a web extension `Manifest`
* with its target `browser` (represented as `AnyCase<Browser>`).
* This is commonly used in build processes to manage configurations for different browsers.
*/
export type BuildTarget = {
manifest: Manifest;
browser: AnyCase<Browser>;
};
/**
* Defines the configuration options for a build process.
* @property {"build" | "serve"} [command] - The type of build command (e.g., 'build' for production, 'serve' for development).
* @property {AnyCase<Browser> | string | undefined} [mode] - The target build mode, typically a browser name (allowing various casings)
* or potentially other custom mode strings.
*/
export type BuildConfig = {
command?: "build" | "serve";
mode?: AnyCase<Browser> | string | undefined;
};
/**
* Defines the structure for repository information, commonly found in `package.json`.
* @property {string} type - The type of the repository (e.g., "git").
* @property {string} [url] - The URL of the repository.
* @property {Bugs} [bugs] - An object containing information about where to report bugs.
*/
export interface Repository {
type: string;
url?: string;
bugs?: Bugs;
}
/**
* Defines the structure for bug reporting information, often part of the `Repository` interface.
* @property {string} [url] - The URL of the issue tracker.
* @property {string} [email] - The email address for reporting bugs.
*/
export interface Bugs {
url?: string;
email?: string;
}
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum];
/**
* A string literal union type representing supported browser names, derived from the values of `BrowserEnum`.
* e.g., "Chrome" | "Firefox" | ...
*/
export type Browser = ObjectValues<typeof BrowserEnum>;
/**
* A constant intended to provide access to browser names, potentially in various casings.
* Its type `AnyCase<Browser>` suggests it can be used where case-insensitivity for browser names is needed.
* The `createEnum(BrowserEnum)` call aims to produce a representation of browser names from `BrowserEnum`.
* Note: `createEnum` from `lib/utils.ts` has a declared return type of `ObjectValues<T>` (a union of values),
* while its implementation uses `Object.values()` which returns an array. This constant will hold the
* runtime array value, but its JSDoc type refers to the more restrictive `AnyCase<Browser>` union type.
*/
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
export type PackageManager =
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum];
/**
* A string literal union type representing supported package managers, derived from the values of `PackageManagerEnum`.
* e.g., "Bun" | "PnPm" | "Npm" | "Yarn"
*/
export type PackageManager = ObjectValues<typeof PackageManagerEnum>;
/**
* A constant intended to provide access to package manager names, potentially in various casings.
* Its type `AnyCase<PackageManager>` suggests it can be used where case-insensitivity for package manager names is needed.
* Utilizes `createEnum(PackageManagerEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
*/
export const PackageManager: AnyCase<PackageManager> =
createEnum(PackageManagerEnum);
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum];
/**
* A string literal union type representing supported JavaScript frameworks, derived from the values of `FrameworkEnum`.
* e.g., "React" | "Vanilla" | ...
*/
export type Framework = ObjectValues<typeof FrameworkEnum>;
/**
* A constant intended to provide access to framework names, potentially in various casings.
* Its type `AnyCase<Framework>` suggests it can be used where case-insensitivity for framework names is needed.
* Utilizes `createEnum(FrameworkEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
*/
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum];
/**
* A string literal union type representing supported styling options, derived from the values of `StyleEnum`.
* e.g., "Tailwind"
*/
export type Style = ObjectValues<typeof StyleEnum>;
/**
* A constant intended to provide access to style option names, potentially in various casings.
* Its type `AnyCase<Style>` suggests it can be used where case-insensitivity for style names is needed.
* Utilizes `createEnum(StyleEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
*/
export const Style: AnyCase<Style> = createEnum(StyleEnum);
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum];
/**
* A string literal union type representing supported programming languages, derived from the values of `LanguageEnum`.
* e.g., "TypeScript" | "JavaScript"
*/
export type Language = ObjectValues<typeof LanguageEnum>;
/**
* A constant intended to provide access to programming language names, potentially in various casings.
* Its type `AnyCase<Language>` suggests it can be used where case-insensitivity for language names is needed.
* Utilizes `createEnum(LanguageEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
*/
export const Language: AnyCase<Language> = createEnum(LanguageEnum);
+63
View File
@@ -1,21 +1,84 @@
/**
* Extracts a union type of all values from the properties of an object type `T`.
*
* @template T - An object type (typically a Record or an enum-like object).
* @example
* type MyObject = { a: "foo", b: "bar", c: 123 };
* type MyObjectValues = ObjectValues<MyObject>; // "foo" | "bar" | 123
*/
export type ObjectValues<T> = T[keyof T];
/**
* Creates a union of an object's string values, often used to represent the set of possible values for an enum-like object.
* Note: The implementation `Object.values(enumObj) as unknown as ObjectValues<T>` returns an array at runtime,
* but the declared return type `ObjectValues<T>` is a union of the object's property values.
* This type signature suggests it's intended to represent the set of possible string values from `enumObj`.
*
* @template T - An object type where keys are strings and values are strings (e.g., `const MyEnum = { VAL_A: "A", VAL_B: "B" }`).
* @param {T} enumObj - The object from which to extract values.
* @returns {ObjectValues<T>} A union type representing all possible string values of the `enumObj`.
* For example, if `enumObj` is `{ A: "valA", B: "valB" }`, the return type is `"valA" | "valB"`.
* (Runtime behavior of `Object.values()` is to return an array like `["valA", "valB"]`).
*/
export function createEnum<T extends Record<string, string>>(enumObj: T) {
return Object.values(enumObj) as unknown as ObjectValues<T>;
}
/**
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
* of a given string literal type `T`.
*
* @template T - A string literal type.
* @example
* type MyString = "example";
* type MyStringAnyCase = AnyCase<MyString>; // "EXAMPLE" | "example" | "Example" | "example" (Uncapitalize<"Example"> is "example")
*/
export type AnyCase<T extends string> =
| Uppercase<T>
| Lowercase<T>
| Capitalize<T>
| Uncapitalize<T>;
/**
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
* of the union of two given string literal types `T` and `K`.
* This is useful for representing a combined set of related string constants where case variations are permitted for each.
*
* @template T - A string literal type.
* @template K - Another string literal type.
* @example
* type Lang1 = "english";
* type Lang2 = "french";
* type CombinedLangsAnyCase = AnyCaseLanguage<Lang1, Lang2>;
* // Result includes: "ENGLISH" | "english" | "English" | "FRENCH" | "french" | "French" etc.
* // for all case variations of "english" and "french".
*/
export type AnyCaseLanguage<T extends string, K extends string> =
| Uppercase<T | K>
| Lowercase<T | K>
| Capitalize<T | K>
| Uncapitalize<T | K>;
/**
* Extracts a new object type containing only the keys of `T` whose properties are optional
* (i.e., their type includes `undefined`). The values associated with these keys retain their original types.
*
* @template T - An object type.
* @example
* type MyObject = {
* requiredProp: string;
* optionalProp?: number;
* anotherOptional?: boolean | undefined;
* nullProp: string | null;
* };
* type MyOptionalProps = OptionalKeys<MyObject>;
* // MyOptionalProps would be conceptually equivalent to:
* // {
* // optionalProp?: number;
* // anotherOptional?: boolean | undefined;
* // }
* // The actual resulting type is an object type with only these optional keys.
*/
export type OptionalKeys<T> = {
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
};
+76 -3
View File
@@ -1,8 +1,20 @@
import { type DBSchema, type IDBPDatabase, openDB } from "idb";
/**
* Defines the schema for the IndexedDB database used for storing background image data.
*
* @interface BackgroundDB
* @extends {DBSchema}
* @property {object} backgrounds - The object store for background images.
* @property {string} backgrounds.key - The type of the key for the object store (in this case, it's `id` as defined in `keyPath`).
* @property {object} backgrounds.value - The structure of the objects stored.
* @property {string} backgrounds.value.id - The unique identifier for the background image record.
* @property {string} backgrounds.value.type - The MIME type of the image (e.g., "image/png", "image/jpeg").
* @property {Blob} backgrounds.value.blob - The binary large object (Blob) containing the image data.
*/
interface BackgroundDB extends DBSchema {
backgrounds: {
key: string;
key: string; // Corresponds to the 'id' property due to keyPath: "id"
value: {
id: string;
type: string;
@@ -13,6 +25,14 @@ interface BackgroundDB extends DBSchema {
let db: IDBPDatabase<BackgroundDB> | null = null;
/**
* Initializes and opens an IndexedDB connection or returns an existing one.
* If the database doesn't exist or needs an upgrade, the `upgrade` callback
* creates the 'backgrounds' object store with 'id' as the keyPath.
*
* @async
* @returns {Promise<IDBPDatabase<BackgroundDB>>} A promise that resolves with the database instance.
*/
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db;
@@ -25,6 +45,12 @@ export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
return db;
}
/**
* Retrieves all background image records from the 'backgrounds' object store in IndexedDB.
*
* @async
* @returns {Promise<Array<{id: string, type: string, blob: Blob}>>} A promise that resolves with an array of all background image records.
*/
export async function readAllData(): Promise<
Array<{ id: string; type: string; blob: Blob }>
> {
@@ -32,6 +58,16 @@ export async function readAllData(): Promise<
return db.getAll("backgrounds");
}
/**
* Writes or updates a background image record in the 'backgrounds' object store.
* If a record with the given `id` already exists, it will be updated. Otherwise, a new record is created.
*
* @async
* @param {string} id - The unique identifier for the background image record.
* @param {string} type - The MIME type of the image (e.g., "image/png").
* @param {Blob} blob - The Blob object containing the image data.
* @returns {Promise<void>} A promise that resolves when the data has been successfully written.
*/
export async function writeData(
id: string,
type: string,
@@ -41,16 +77,37 @@ export async function writeData(
await db.put("backgrounds", { id, type, blob });
}
/**
* Deletes a background image record from the 'backgrounds' object store by its ID.
*
* @async
* @param {string} id - The unique identifier of the background image record to delete.
* @returns {Promise<void>} A promise that resolves when the data has been successfully deleted.
*/
export async function deleteData(id: string): Promise<void> {
const db = await openDatabase();
await db.delete("backgrounds", id);
}
/**
* Clears all records from the 'backgrounds' object store in IndexedDB.
*
* @async
* @returns {Promise<void>} A promise that resolves when all data has been successfully cleared.
*/
export async function clearAllData(): Promise<void> {
const db = await openDatabase();
await db.clear("backgrounds");
}
/**
* Retrieves a single background image record from the 'backgrounds' object store by its ID.
*
* @async
* @param {string} id - The unique identifier of the background image record to retrieve.
* @returns {Promise<{id: string, type: string, blob: Blob} | undefined>} A promise that resolves with the
* background image record if found, or undefined otherwise.
*/
export async function getDataById(
id: string,
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
@@ -58,6 +115,10 @@ export async function getDataById(
return db.get("backgrounds", id);
}
/**
* Closes the active IndexedDB connection and nullifies the global `db` variable.
* This is important to release resources and allow for proper database management.
*/
export function closeDatabase(): void {
if (db) {
db.close();
@@ -65,12 +126,24 @@ export function closeDatabase(): void {
}
}
// Helper function to check if IndexedDB is supported
/**
* Checks if IndexedDB is supported by the current browser environment.
*
* @returns {boolean} True if IndexedDB is supported, false otherwise.
*/
export function isIndexedDBSupported(): boolean {
return "indexedDB" in window;
}
// Helper function to check if there's enough storage space
/**
* Estimates available storage space and checks if it's sufficient for the specified `requiredSpace`.
* Uses the `navigator.storage.estimate()` API if available.
* If the API is not available or cannot determine space, it defaults to assuming enough space is available.
*
* @async
* @param {number} requiredSpace - The amount of storage space required, in bytes.
* @returns {Promise<boolean>} A promise that resolves with true if enough space is estimated to be available, false otherwise.
*/
export async function hasEnoughStorageSpace(
requiredSpace: number,
): Promise<boolean> {
+27
View File
@@ -1,11 +1,21 @@
type BackgroundUpdateCallback = () => void;
/**
* A singleton class used to notify listeners about generic background updates or events.
* These updates typically signify that UI components or other parts of the application
* might need to refresh or re-evaluate background-related data (e.g., after a new background
* image is added, removed, or changed).
*/
class BackgroundUpdates {
private static instance: BackgroundUpdates;
private listeners: Set<BackgroundUpdateCallback> = new Set();
private constructor() {}
/**
* Gets the singleton instance of the BackgroundUpdates class.
* @returns {BackgroundUpdates} The singleton instance.
*/
public static getInstance(): BackgroundUpdates {
if (!BackgroundUpdates.instance) {
BackgroundUpdates.instance = new BackgroundUpdates();
@@ -13,14 +23,31 @@ class BackgroundUpdates {
return BackgroundUpdates.instance;
}
/**
* Registers a callback function to be invoked when a background update is triggered.
*
* @param {BackgroundUpdateCallback} callback The function to call when a background update occurs.
* This callback takes no arguments and returns void.
*/
public addListener(callback: BackgroundUpdateCallback): void {
this.listeners.add(callback);
}
/**
* Unregisters a previously added callback function.
* After calling this method, the provided callback will no longer be invoked when a background update is triggered.
*
* @param {BackgroundUpdateCallback} callback The callback function to remove from the listeners.
*/
public removeListener(callback: BackgroundUpdateCallback): void {
this.listeners.delete(callback);
}
/**
* Invokes all registered listener callbacks, signifying that a background update has occurred.
* This method should be called whenever a change to background data happens that requires
* other parts of the application to be notified.
*/
public triggerUpdate(): void {
this.listeners.forEach((callback) => callback());
}
+16
View File
@@ -21,14 +21,30 @@ class SettingsPopup {
return SettingsPopup.instance;
}
/**
* Registers a callback function to be invoked when the settings popup is closed.
*
* @param {SettingsPopupCallback} callback The function to call when the settings popup closes.
* This callback takes no arguments and returns void.
*/
public addListener(callback: SettingsPopupCallback): void {
this.listeners.add(callback);
}
/**
* Unregisters a previously added callback function.
* After calling this method, the provided callback will no longer be invoked when the settings popup closes.
*
* @param {SettingsPopupCallback} callback The callback function to remove from the listeners.
*/
public removeListener(callback: SettingsPopupCallback): void {
this.listeners.delete(callback);
}
/**
* Invokes all registered listener callbacks.
* This method should be called when the settings popup is closed to notify all subscribed components or services.
*/
public triggerClose(): void {
this.listeners.forEach((callback) => callback());
}
+27
View File
@@ -1,11 +1,21 @@
type ThemeUpdateCallback = () => void;
/**
* A singleton class used to notify listeners about theme-related updates.
* These updates can include events like theme changes, custom theme modifications,
* or any other event that might require UI components to refresh their appearance
* or re-apply theme styles.
*/
class ThemeUpdates {
private static instance: ThemeUpdates;
private listeners: Set<ThemeUpdateCallback> = new Set();
private constructor() {}
/**
* Gets the singleton instance of the ThemeUpdates class.
* @returns {ThemeUpdates} The singleton instance.
*/
public static getInstance(): ThemeUpdates {
if (!ThemeUpdates.instance) {
ThemeUpdates.instance = new ThemeUpdates();
@@ -13,14 +23,31 @@ class ThemeUpdates {
return ThemeUpdates.instance;
}
/**
* Registers a callback function to be invoked when a theme update is triggered.
*
* @param {ThemeUpdateCallback} callback The function to call when a theme update occurs.
* This callback takes no arguments and returns void.
*/
public addListener(callback: ThemeUpdateCallback): void {
this.listeners.add(callback);
}
/**
* Unregisters a previously added callback function.
* After calling this method, the provided callback will no longer be invoked when a theme update is triggered.
*
* @param {ThemeUpdateCallback} callback The callback function to remove from the listeners.
*/
public removeListener(callback: ThemeUpdateCallback): void {
this.listeners.delete(callback);
}
/**
* Invokes all registered listener callbacks, signifying that a theme-related update has occurred.
* This method should be called whenever a change related to themes happens that requires
* other parts of the application to be notified.
*/
public triggerUpdate(): void {
this.listeners.forEach((callback) => callback());
}
+50
View File
@@ -1,9 +1,27 @@
import type { LoadedCustomTheme } from "@/types/CustomThemes";
/**
* Generates a random 9-character alphanumeric string to be used as a unique ID for images.
* This helps in identifying and managing custom images within a theme.
*
* @returns {string} A randomly generated unique ID string.
*/
export function generateImageId(): string {
return Math.random().toString(36).substr(2, 9);
}
/**
* Handles the upload of a new custom image from a file input event.
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
* generates a unique ID and a default variable name for it, and then adds this new image
* to the `CustomImages` array within the provided `theme` object.
*
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
* @param {LoadedCustomTheme} theme The current theme object to which the new image will be added.
* @returns {Promise<LoadedCustomTheme> | LoadedCustomTheme} A Promise that resolves with the updated theme object
* containing the new image if a file was processed.
* Returns the original theme object synchronously if no file was selected.
*/
export function handleImageUpload(
event: Event,
theme: LoadedCustomTheme,
@@ -34,6 +52,16 @@ export function handleImageUpload(
return theme;
}
/**
* Removes a custom image from the theme based on its ID.
* It filters out the image with the specified `imageId` from the `CustomImages` array
* in the `theme` object.
*
* @param {string} imageId The unique ID of the custom image to be removed.
* @param {LoadedCustomTheme} theme The current theme object from which the image will be removed.
* @returns {LoadedCustomTheme} A new theme object with the specified image removed from its `CustomImages` array.
* This function is synchronous.
*/
export function handleRemoveImage(
imageId: string,
theme: LoadedCustomTheme,
@@ -44,6 +72,17 @@ export function handleRemoveImage(
} as LoadedCustomTheme;
}
/**
* Updates the CSS variable name associated with a specific custom image in the theme.
* It finds the image by `imageId` in the `CustomImages` array of the `theme` object
* and updates its `variableName` property.
*
* @param {string} imageId The unique ID of the custom image whose variable name is to be updated.
* @param {string} variableName The new CSS variable name to assign to the image.
* @param {LoadedCustomTheme} theme The current theme object containing the image to be updated.
* @returns {LoadedCustomTheme} A new theme object with the updated image variable name.
* This function is synchronous.
*/
export function handleImageVariableChange(
imageId: string,
variableName: string,
@@ -57,6 +96,17 @@ export function handleImageVariableChange(
} as LoadedCustomTheme;
}
/**
* Handles the upload of a cover image for the theme from a file input event.
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
* and then updates the `coverImage` property of the provided `theme` object with this new blob.
*
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
* @param {LoadedCustomTheme} theme The current theme object whose cover image will be updated.
* @returns {Promise<LoadedCustomTheme>} A Promise that resolves with the updated theme object
* containing the new cover image if a file was processed.
* Returns a Promise resolving with the original theme object if no file was selected.
*/
export function handleCoverImageUpload(
event: Event,
theme: LoadedCustomTheme,