vite-plugin-shopify-theme-islands
Version:
Vite plugin for island architecture in Shopify themes
164 lines (163 loc) • 6.06 kB
TypeScript
/**
* Plugin ↔ Runtime contract (deep module).
*
* Single source of truth for the payload shape, key→tag derivation, and defaults.
* Plugin and runtime both depend on this module in-process.
*/
import type { InteractionEventName } from "./interaction-events.js";
/** Loader function for one island chunk. */
export type IslandLoader = () => Promise<unknown>;
/** Directive config for the runtime (built-in + no plugin-only `custom` entrypoints). */
export interface RuntimeDirectivesConfig {
visible?: {
attribute?: string;
rootMargin?: string;
threshold?: number;
};
idle?: {
attribute?: string;
timeout?: number;
};
media?: {
attribute?: string;
};
defer?: {
attribute?: string;
delay?: number;
};
interaction?: {
attribute?: string;
events?: readonly InteractionEventName[];
};
}
/** Retry configuration. */
export interface RetryConfig {
retries?: number;
delay?: number;
}
/** Options passed from plugin to runtime (subset of plugin options). */
export interface ReviveOptions {
directives?: RuntimeDirectivesConfig;
debug?: boolean;
retry?: RetryConfig;
/**
* Milliseconds before a custom directive that never calls `load()` is considered timed out.
* When exceeded, `islands:error` is dispatched and the island is abandoned.
* Default: `0` (disabled).
*/
directiveTimeout?: number;
}
/** 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;
}
export interface ClientDirectiveContext {
/** Aborted when the directive should stop waiting and clean up any side effects. */
signal: AbortSignal;
/** Registers cleanup work that should run when the directive is aborted or resolves. */
onCleanup(cleanup: () => void): void;
}
/** Custom directive function at runtime (load, opts, element, ctx). */
export type ClientDirective = (load: () => Promise<void>, options: ClientDirectiveOptions, el: HTMLElement, ctx: ClientDirectiveContext) => void | Promise<void>;
/** 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 {
"islands:load": CustomEvent<IslandLoadDetail>;
"islands:error": CustomEvent<IslandErrorDetail>;
}
}
/**
* Payload the plugin emits and the runtime consumes.
* Islands: glob key → loader (e.g. "/frontend/js/islands/product-form.ts" → loader).
* Custom directives: attribute name → directive implementation (resolved at build).
* Options may be partial; runtime uses normalizeReviveOptions() to fill defaults.
*/
export interface RevivePayload {
islands: Record<string, IslandLoader>;
options?: ReviveOptions;
customDirectives?: Map<string, ClientDirective>;
resolvedTags?: Record<string, string | false>;
}
/** Fully resolved options; all directive and retry fields have defaults applied. */
export interface NormalizedReviveOptions {
directives: {
visible: {
attribute: string;
rootMargin: string;
threshold: number;
};
idle: {
attribute: string;
timeout: number;
};
media: {
attribute: string;
};
defer: {
attribute: string;
delay: number;
};
interaction: {
attribute: string;
events: readonly InteractionEventName[];
};
};
debug: boolean;
retry: {
retries: number;
delay: number;
};
directiveTimeout: number;
}
/** Default directive config. Single source of truth for plugin merge and runtime normalization. */
export declare const DEFAULT_DIRECTIVES: NormalizedReviveOptions["directives"];
/**
* Applies default values for all runtime options.
* Single source of truth so plugin and runtime do not duplicate defaults.
*/
export declare function normalizeReviveOptions(options?: ReviveOptions): NormalizedReviveOptions;
export type KeyToTagResult = {
tag: string;
skip?: boolean;
};
export type ResolvedTagOverride = string | false;
export type ResolveTagInput = {
filePath: string;
defaultTag: string;
};
export type ResolveTagOverrideFn = (input: ResolveTagInput) => ResolvedTagOverride;
/**
* Maps a glob key (e.g. "/frontend/js/islands/product-form.ts") to a custom element tag.
* Return { tag, skip: true } to exclude this entry from the island map.
*/
export type KeyToTagFn = (key: string) => KeyToTagResult;
/** Derives the default tag name from a glob key without warning or skipping. */
export declare function deriveDefaultTag(key: string): string;
/** Default: last path segment, extension stripped; skip (and warn) when tag has no hyphen. */
export declare function defaultKeyToTag(key: string): KeyToTagResult;
export declare function compileResolvedTags(filePaths: Iterable<string>, resolveTag: ResolveTagOverrideFn): Record<string, ResolvedTagOverride> | null;
/**
* Builds tag → loader map from payload.
* Applies the default key→tag derivation and requires unique tag ownership.
*/
export declare function buildIslandMap(payload: RevivePayload): Map<string, IslandLoader>;