vite-plugin-shopify-theme-islands 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -306,6 +306,7 @@ Built-in directives always run first. A custom directive is only invoked after a
306
306
  ```
307
307
 
308
308
  The custom directive owns the `load()` call — the built-in chain never calls it directly when a custom directive is matched.
309
+ If a custom directive throws or returns a rejected promise, the runtime dispatches `islands:error` and abandons that island activation attempt.
309
310
 
310
311
  Multiple custom directives on the same element use AND semantics — the island loads only once all matched directives have called `load()`. For example, given two registered custom directives `client:hash` and `client:network`:
311
312
 
@@ -316,14 +317,27 @@ Multiple custom directives on the same element use AND semantics — the island
316
317
  </product-reviews>
317
318
  ```
318
319
 
320
+ #### Timeout guard
321
+
322
+ By default, a custom directive that never calls `load()` silently keeps the island unloaded forever. Set `directiveTimeout` to fire `islands:error` and abandon the island if the directive hasn't resolved within the given window:
323
+
324
+ ```ts
325
+ shopifyThemeIslands({
326
+ directiveTimeout: 5000, // abandon after 5 seconds
327
+ });
328
+ ```
329
+
330
+ This is useful during development to surface directives that hang due to bugs, or in production to ensure broken directives don't silently degrade the experience.
331
+
319
332
  ## Configuration
320
333
 
321
- | Option | Type | Default | Description |
322
- | ------------- | -------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
323
- | `directories` | `string \| string[]` | `['/frontend/js/islands/']` | Directories to scan for island files. Accepts Vite aliases. |
324
- | `directives` | `object` | see below | Per-directive configuration — attribute names, timing options, and custom entries. |
325
- | `retry` | `object` | — | Automatic retry behaviour for failed island loads. See [Retries](#retries). |
326
- | `debug` | `boolean` | `false` | Log discovered islands at build time and directive events in the browser console. |
334
+ | Option | Type | Default | Description |
335
+ | ------------------ | -------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
336
+ | `directories` | `string \| string[]` | `['/frontend/js/islands/']` | Directories to scan for island files. Accepts Vite aliases. |
337
+ | `directives` | `object` | see below | Per-directive configuration — attribute names, timing options, and custom entries. |
338
+ | `retry` | `object` | — | Automatic retry behaviour for failed island loads. See [Retries](#retries). |
339
+ | `debug` | `boolean` | `false` | Log discovered islands at build time and directive events in the browser console. |
340
+ | `directiveTimeout` | `number` | `0` (disabled) | Milliseconds before a custom directive that never calls `load()` is considered timed out. Fires `islands:error` and abandons the island. |
327
341
 
328
342
  ### Directive defaults
329
343
 
@@ -441,7 +455,7 @@ document.addEventListener("islands:load", (e) => {
441
455
  | Event | Detail properties | When it fires |
442
456
  | --------------- | ------------------------------ | ---------------------------------------------------------- |
443
457
  | `islands:load` | `tag`, `duration`, `attempt` | Island module resolves successfully |
444
- | `islands:error` | `tag`, `error`, `attempt` | Load or custom directive fails (alongside `console.error`) |
458
+ | `islands:error` | `tag`, `error`, `attempt` | Load fails, custom directive throws or rejects, or `directiveTimeout` expires (alongside `console.error`) |
445
459
 
446
460
  `islands:error` fires on each retry attempt, not just the final failure. Multiple independent listeners are supported — each receives its own event.
447
461
 
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Plugin ↔ Runtime contract (deep module).
3
+ *
4
+ * Single source of truth for the payload shape, key→tag derivation, and defaults.
5
+ * Plugin and runtime both depend on this module in-process.
6
+ */
7
+ /** Loader function for one island chunk. */
8
+ export type IslandLoader = () => Promise<unknown>;
9
+ /** Directive config for the runtime (built-in + no plugin-only `custom` entrypoints). */
10
+ export interface RuntimeDirectivesConfig {
11
+ visible?: {
12
+ attribute?: string;
13
+ rootMargin?: string;
14
+ threshold?: number;
15
+ };
16
+ idle?: {
17
+ attribute?: string;
18
+ timeout?: number;
19
+ };
20
+ media?: {
21
+ attribute?: string;
22
+ };
23
+ defer?: {
24
+ attribute?: string;
25
+ delay?: number;
26
+ };
27
+ interaction?: {
28
+ attribute?: string;
29
+ events?: string[];
30
+ };
31
+ }
32
+ /** Retry configuration. */
33
+ export interface RetryConfig {
34
+ retries?: number;
35
+ delay?: number;
36
+ }
37
+ /** Options passed from plugin to runtime (subset of plugin options). */
38
+ export interface ReviveOptions {
39
+ directives?: RuntimeDirectivesConfig;
40
+ debug?: boolean;
41
+ retry?: RetryConfig;
42
+ /**
43
+ * Milliseconds before a custom directive that never calls `load()` is considered timed out.
44
+ * When exceeded, `islands:error` is dispatched and the island is abandoned.
45
+ * Default: `0` (disabled).
46
+ */
47
+ directiveTimeout?: number;
48
+ }
49
+ /** Options passed to a custom client directive function. */
50
+ export interface ClientDirectiveOptions {
51
+ /** The matched attribute name, e.g. `'client:on-click'` */
52
+ name: string;
53
+ /** The attribute value; empty string if no value was set */
54
+ value: string;
55
+ }
56
+ /** Custom directive function at runtime (load, opts, element). */
57
+ export type ClientDirective = (load: () => Promise<void>, options: ClientDirectiveOptions, el: HTMLElement) => void | Promise<void>;
58
+ /** Event detail for the `islands:load` DOM event. */
59
+ export interface IslandLoadDetail {
60
+ /** The custom element tag name, e.g. `'product-form'` */
61
+ tag: string;
62
+ /** Milliseconds from directive resolution to successful module load (chunk fetch time). */
63
+ duration: number;
64
+ /** Which attempt succeeded. 1 = first try, 2 = first retry, etc. */
65
+ attempt: number;
66
+ }
67
+ /** Event detail for the `islands:error` DOM event. */
68
+ export interface IslandErrorDetail {
69
+ /** The custom element tag name, e.g. `'product-form'` */
70
+ tag: string;
71
+ /** The error thrown by the loader or custom directive */
72
+ error: unknown;
73
+ /** Which attempt failed. 1 = initial attempt, 2 = first retry, etc. */
74
+ attempt: number;
75
+ }
76
+ declare global {
77
+ interface DocumentEventMap {
78
+ "islands:load": CustomEvent<IslandLoadDetail>;
79
+ "islands:error": CustomEvent<IslandErrorDetail>;
80
+ }
81
+ }
82
+ /**
83
+ * Payload the plugin emits and the runtime consumes.
84
+ * Islands: glob key → loader (e.g. "/frontend/js/islands/product-form.ts" → loader).
85
+ * Custom directives: attribute name → directive implementation (resolved at build).
86
+ * Options may be partial; runtime uses normalizeReviveOptions() to fill defaults.
87
+ */
88
+ export interface RevivePayload {
89
+ islands: Record<string, IslandLoader>;
90
+ options?: ReviveOptions;
91
+ customDirectives?: Map<string, ClientDirective>;
92
+ }
93
+ /** Fully resolved options; all directive and retry fields have defaults applied. */
94
+ export interface NormalizedReviveOptions {
95
+ directives: {
96
+ visible: {
97
+ attribute: string;
98
+ rootMargin: string;
99
+ threshold: number;
100
+ };
101
+ idle: {
102
+ attribute: string;
103
+ timeout: number;
104
+ };
105
+ media: {
106
+ attribute: string;
107
+ };
108
+ defer: {
109
+ attribute: string;
110
+ delay: number;
111
+ };
112
+ interaction: {
113
+ attribute: string;
114
+ events: string[];
115
+ };
116
+ };
117
+ debug: boolean;
118
+ retry: {
119
+ retries: number;
120
+ delay: number;
121
+ };
122
+ directiveTimeout: number;
123
+ }
124
+ /** Default directive config. Single source of truth for plugin merge and runtime normalization. */
125
+ export declare const DEFAULT_DIRECTIVES: NormalizedReviveOptions["directives"];
126
+ /**
127
+ * Applies default values for all runtime options.
128
+ * Single source of truth so plugin and runtime do not duplicate defaults.
129
+ */
130
+ export declare function normalizeReviveOptions(options?: ReviveOptions): NormalizedReviveOptions;
131
+ export type KeyToTagResult = {
132
+ tag: string;
133
+ skip?: boolean;
134
+ };
135
+ /**
136
+ * Maps a glob key (e.g. "/frontend/js/islands/product-form.ts") to a custom element tag.
137
+ * Return { tag, skip: true } to exclude this entry from the island map.
138
+ */
139
+ export type KeyToTagFn = (key: string) => KeyToTagResult;
140
+ /** Default: last path segment, extension stripped; skip (and warn) when tag has no hyphen. */
141
+ export declare function defaultKeyToTag(key: string): KeyToTagResult;
142
+ /**
143
+ * Builds tag → loader map from payload.
144
+ * Applies the default key→tag derivation and deduplicates by tag (first wins).
145
+ */
146
+ export declare function buildIslandMap(payload: RevivePayload): Map<string, IslandLoader>;
@@ -0,0 +1,12 @@
1
+ /** Matches .ts or .js extension. Exported for plugin transform/watch filters. */
2
+ export declare const TS_JS_RE: RegExp;
3
+ /** Matches the island mixin import. Exported for plugin transform/watch detection. */
4
+ export declare const ISLAND_IMPORT_RE: RegExp;
5
+ /** True if file is under any of the given absolute directory paths. */
6
+ export declare function inDirectory(file: string, absDirs: string[]): boolean;
7
+ /** Paths for load() virtual module: "/relative/to/root" form, forward slashes. */
8
+ export declare function getIslandPathsForLoad(islandFiles: Set<string>, root: string): string[];
9
+ /** Scan from root for files containing the island import; returns paths (not in absDirs). */
10
+ export declare function discoverIslandFiles(root: string, absDirs: string[]): Set<string>;
11
+ /** Tag names (filename without extension) for TS/JS files in a directory. Used for debug logging. */
12
+ export declare function collectTagNames(dir: string): string[];
package/dist/index.d.ts CHANGED
@@ -1,45 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  /** A function that triggers the load of an island module. */
3
3
  export type ClientDirectiveLoader = () => Promise<void>;
4
- /** Options passed to a custom client directive function. */
5
- export interface ClientDirectiveOptions {
6
- /** The matched attribute name, e.g. `'client:on-click'` */
7
- name: string;
8
- /** The attribute value; empty string if no value was set */
9
- value: string;
10
- }
11
- /**
12
- * A custom client directive function.
13
- *
14
- * Called by the runtime when a matching attribute is found on an island element.
15
- * The function is responsible for calling `load()` when the desired condition is met.
16
- *
17
- * @example
18
- * ```ts
19
- * // src/directives/hash.ts
20
- * import type { ClientDirective } from 'vite-plugin-shopify-theme-islands';
21
- *
22
- * const hashDirective: ClientDirective = (load, opts) => {
23
- * const target = opts.value;
24
- * if (location.hash === target) { load(); return; }
25
- * window.addEventListener('hashchange', () => {
26
- * if (location.hash === target) load();
27
- * });
28
- * };
29
- *
30
- * export default hashDirective;
31
- * ```
32
- *
33
- * Register it in `vite.config.ts`:
34
- * ```ts
35
- * shopifyThemeIslands({
36
- * directives: {
37
- * custom: [{ name: 'client:hash', entrypoint: './src/directives/hash.ts' }],
38
- * },
39
- * })
40
- * ```
41
- */
42
- export type ClientDirective = (load: ClientDirectiveLoader, options: ClientDirectiveOptions, el: HTMLElement) => void | Promise<void>;
4
+ export type { ClientDirective, ClientDirectiveOptions } from "./contract.js";
43
5
  /** Plugin option entry for registering a custom client directive. */
44
6
  export interface ClientDirectiveDefinition {
45
7
  /** HTML attribute name, e.g. `'client:on-click'` */
@@ -87,41 +49,9 @@ export interface DirectivesConfig {
87
49
  /** Custom client directives to register. Each entry maps an attribute name to a module entrypoint. */
88
50
  custom?: ClientDirectiveDefinition[];
89
51
  }
90
- /** Runtime-facing directive configuration omits plugin-only `custom` directives. */
91
- export type RuntimeDirectivesConfig = Omit<DirectivesConfig, "custom">;
92
- /** Retry configuration for failed island loads. */
93
- export interface RetryConfig {
94
- /** Number of times to retry after the initial failure. Default: `0` (no auto-retry) */
95
- retries?: number;
96
- /** Base delay in ms between retries; doubles each attempt. Default: `1000` */
97
- delay?: number;
98
- }
99
- /** Event detail for the `islands:load` DOM event. */
100
- export interface IslandLoadDetail {
101
- /** The custom element tag name, e.g. `'product-form'` */
102
- tag: string;
103
- /** Milliseconds from directive resolution to successful module load (chunk fetch time). */
104
- duration: number;
105
- /** Which attempt succeeded. 1 = first try, 2 = first retry, etc. */
106
- attempt: number;
107
- }
108
- /** Event detail for the `islands:error` DOM event. */
109
- export interface IslandErrorDetail {
110
- /** The custom element tag name, e.g. `'product-form'` */
111
- tag: string;
112
- /** The error thrown by the loader or custom directive */
113
- error: unknown;
114
- /** Which attempt failed. 1 = initial attempt, 2 = first retry, etc. */
115
- attempt: number;
116
- }
117
- declare global {
118
- interface DocumentEventMap {
119
- /** Fired after an island module resolves successfully. */
120
- "islands:load": CustomEvent<IslandLoadDetail>;
121
- /** Fired when an island load or custom directive fails. Fired on each retry attempt. */
122
- "islands:error": CustomEvent<IslandErrorDetail>;
123
- }
124
- }
52
+ /** Event detail and runtime options (single source of truth in contract). */
53
+ import type { RetryConfig } from "./contract.js";
54
+ export type { IslandLoadDetail, IslandErrorDetail, ReviveOptions, RetryConfig, RuntimeDirectivesConfig, } from "./contract.js";
125
55
  export interface ShopifyThemeIslandsOptions {
126
56
  /** Directories to scan for island files. Accepts paths or Vite aliases. Default: `['/frontend/js/islands/']` */
127
57
  directories?: string | string[];
@@ -131,12 +61,11 @@ export interface ShopifyThemeIslandsOptions {
131
61
  directives?: DirectivesConfig;
132
62
  /** Automatic retry behaviour for failed island loads. */
133
63
  retry?: RetryConfig;
134
- }
135
- export interface ReviveOptions {
136
- directives?: RuntimeDirectivesConfig;
137
- /** Log island activation and directive events to the console. Default: `false` */
138
- debug?: boolean;
139
- /** Automatic retry behaviour for failed island loads. */
140
- retry?: RetryConfig;
64
+ /**
65
+ * Milliseconds before a custom directive that never calls `load()` is considered timed out.
66
+ * When exceeded, `islands:error` is dispatched and the island is abandoned.
67
+ * Default: `0` (disabled).
68
+ */
69
+ directiveTimeout?: number;
141
70
  }
142
71
  export default function shopifyThemeIslands(options?: ShopifyThemeIslandsOptions): Plugin;