vite-plugin-shopify-theme-islands
Version:
Vite plugin for island architecture in Shopify themes
143 lines (142 loc) • 5.96 kB
TypeScript
import type { Plugin } from "vite";
/** A function that triggers the load of an island module. */
export type ClientDirectiveLoader = () => Promise<void>;
/** Options passed to a custom client directive function. */
export interface ClientDirectiveOptions {
/** The matched attribute name, e.g. `'client:on-click'` */
name: string;
/** The attribute value; empty string if no value was set */
value: string;
}
/**
* A custom client directive function.
*
* Called by the runtime when a matching attribute is found on an island element.
* The function is responsible for calling `load()` when the desired condition is met.
*
* @example
* ```ts
* // src/directives/hash.ts
* import type { ClientDirective } from 'vite-plugin-shopify-theme-islands';
*
* const hashDirective: ClientDirective = (load, opts) => {
* const target = opts.value;
* if (location.hash === target) { load(); return; }
* window.addEventListener('hashchange', () => {
* if (location.hash === target) load();
* });
* };
*
* export default hashDirective;
* ```
*
* Register it in `vite.config.ts`:
* ```ts
* shopifyThemeIslands({
* directives: {
* custom: [{ name: 'client:hash', entrypoint: './src/directives/hash.ts' }],
* },
* })
* ```
*/
export type ClientDirective = (load: ClientDirectiveLoader, options: ClientDirectiveOptions, el: HTMLElement) => void | Promise<void>;
/** Plugin option entry for registering a custom client directive. */
export interface ClientDirectiveDefinition {
/** HTML attribute name, e.g. `'client:on-click'` */
name: string;
/** Path to the directive module (supports Vite aliases) */
entrypoint: string;
}
/** Shared directive configuration shape used by both the plugin and the runtime. */
export interface DirectivesConfig {
/** Configuration for the `client:visible` directive (IntersectionObserver). */
visible?: {
/** HTML attribute name. Default: `'client:visible'` */
attribute?: string;
/** Passed to IntersectionObserver — loads islands before they scroll into view. Default: `'200px'` */
rootMargin?: string;
/** Passed to IntersectionObserver — ratio of element that must be visible. Default: `0` */
threshold?: number;
};
/** Configuration for the `client:idle` directive (requestIdleCallback). */
idle?: {
/** HTML attribute name. Default: `'client:idle'` */
attribute?: string;
/** Deadline (ms) passed to requestIdleCallback; also used as the setTimeout fallback delay. Default: `500` */
timeout?: number;
};
/** Configuration for the `client:media` directive (matchMedia). */
media?: {
/** HTML attribute name. Default: `'client:media'` */
attribute?: string;
};
/** Configuration for the `client:defer` directive (fixed setTimeout delay). */
defer?: {
/** HTML attribute name. Default: `'client:defer'` */
attribute?: string;
/** Fallback delay (ms) when the attribute has no value. Default: `3000` */
delay?: number;
};
/** Configuration for the `client:interaction` directive (mouseenter/touchstart/focusin). */
interaction?: {
/** HTML attribute name. Default: `'client:interaction'` */
attribute?: string;
/** DOM event names to listen for. Default: `['mouseenter', 'touchstart', 'focusin']` */
events?: string[];
};
/** Custom client directives to register. Each entry maps an attribute name to a module entrypoint. */
custom?: ClientDirectiveDefinition[];
}
/** Runtime-facing directive configuration — omits plugin-only `custom` directives. */
export type RuntimeDirectivesConfig = Omit<DirectivesConfig, "custom">;
/** Retry configuration for failed island loads. */
export interface RetryConfig {
/** Number of times to retry after the initial failure. Default: `0` (no auto-retry) */
retries?: number;
/** Base delay in ms between retries; doubles each attempt. Default: `1000` */
delay?: number;
}
/** Event detail for the `islands:load` DOM event. */
export interface IslandLoadDetail {
/** The custom element tag name, e.g. `'product-form'` */
tag: string;
/** Milliseconds from directive resolution to successful module load (chunk fetch time). */
duration: number;
/** Which attempt succeeded. 1 = first try, 2 = first retry, etc. */
attempt: number;
}
/** Event detail for the `islands:error` DOM event. */
export interface IslandErrorDetail {
/** The custom element tag name, e.g. `'product-form'` */
tag: string;
/** The error thrown by the loader or custom directive */
error: unknown;
/** Which attempt failed. 1 = initial attempt, 2 = first retry, etc. */
attempt: number;
}
declare global {
interface DocumentEventMap {
/** Fired after an island module resolves successfully. */
"islands:load": CustomEvent<IslandLoadDetail>;
/** Fired when an island load or custom directive fails. Fired on each retry attempt. */
"islands:error": CustomEvent<IslandErrorDetail>;
}
}
export interface ShopifyThemeIslandsOptions {
/** Directories to scan for island files. Accepts paths or Vite aliases. Default: `['/frontend/js/islands/']` */
directories?: string | string[];
/** Log discovered islands and generated virtual module. Default: `false` */
debug?: boolean;
/** Per-directive configuration. */
directives?: DirectivesConfig;
/** Automatic retry behaviour for failed island loads. */
retry?: RetryConfig;
}
export interface ReviveOptions {
directives?: RuntimeDirectivesConfig;
/** Log island activation and directive events to the console. Default: `false` */
debug?: boolean;
/** Automatic retry behaviour for failed island loads. */
retry?: RetryConfig;
}
export default function shopifyThemeIslands(options?: ShopifyThemeIslandsOptions): Plugin;