---
name: setup
description: >
  Getting-started journey and plugin configuration. Covers the full path from
  install to first working island. Shopify-first setup: shopifyThemeIslands()
  options include directories (string | string[]),
  tagSource ("registeredTag" default — Tag derived from static
  customElements.define("...", ...) call; "filename" for v1.x compatibility),
  resolveTag({ filePath, defaultTag }) with unique final-tag requirements
  (defaultTag is the Registered Tag in registeredTag mode, filename-derived in
  filename mode), debug, directives deep-merge
  (visible, idle, media, defer, interaction, custom), retry (retries, delay
  with exponential backoff), directiveTimeout for hung custom directives, and
  the curated interaction-event config policy
  (`mouseenter`, `touchstart`, `focusin`; empty arrays rejected). Per-element
  `client:interaction` values are runtime-validated against the same curated
  set: unsupported tokens warn and are ignored; if no supported tokens remain,
  the runtime falls back to the configured default events.
type: core
library: vite-plugin-shopify-theme-islands
library_version: "2.0.0"
sources:
  - Rees1993/vite-plugin-shopify-theme-islands:src/index.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/options.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/resolved-config.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/revive-compile.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/revive-module.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/interaction-events.ts
---

## Setup

This plugin is Shopify-first and built for Liquid themes using custom elements.
Most Shopify projects also use
[vite-plugin-shopify](https://github.com/barrel/vite-plugin-shopify) to handle
Shopify-specific asset serving — if the project uses it, add this plugin
alongside it in the existing `plugins` array.

The package targets **Node.js 22+** and declares **Vite 6+** as a peer dependency.

### 1. Add the plugin to `vite.config.ts`

```ts
// vite.config.ts
import { defineConfig } from "vite";
import shopifyThemeIslands from "vite-plugin-shopify-theme-islands";

export default defineConfig({
  plugins: [shopifyThemeIslands()],
});
```

All options are optional. The default islands directory is `/frontend/js/islands/`.

### 2. Import the virtual module in the theme JS entry point

```ts
// frontend/js/theme.ts
import "vite-plugin-shopify-theme-islands/revive";
```

This activates the runtime — islands are never loaded without this import.

The same `/revive` module also exports `scan()`, `observe()`, `unobserve()`,
and `disconnect()` for partial swaps and teardown. If `disconnect()` is called
before `DOMContentLoaded`, the runtime cancels its pending startup listener so
islands never initialize later against stale DOM.
`/revive` is a shared page-level singleton, so later named imports reuse the
same runtime instance instead of creating a second one.

### 3. Add directives to Liquid templates

```html
<!-- sections/product.liquid -->
<product-form client:visible></product-form>
```

That's a working setup. Any discovered Island whose effective Tag matches
`<product-form>` is loaded lazily when the directive condition is met. In the
default `registeredTag` mode, that Tag comes from the file's static
`customElements.define("...", ...)` call; `tagSource: "filename"` restores the
v1.x filename-based lookup.

## Core Patterns

### Configure multiple island directories

```ts
shopifyThemeIslands({
  directories: ["/frontend/js/islands/", "/frontend/js/components/"],
});
```

### Override the derived Tag

```ts
shopifyThemeIslands({
  resolveTag({ filePath, defaultTag }) {
    if (filePath.endsWith("/legacy/widget.ts")) return "legacy-widget";
    if (filePath.endsWith("/skip-me.ts")) return false;
    return defaultTag;
  },
});
```

Use `resolveTag()` to override Tag derivation for specific files. In the
default `registeredTag` mode, `defaultTag` is the Tag read from the file's
static `customElements.define("...", ...)` call. Returning `false` excludes
the file from the island map. Returning `defaultTag` keeps the default.

If two different source files resolve to the same Tag, plugin compilation fails.
Rename, adjust `resolveTag`, or return `false` to exclude one file.

### Override built-in directive defaults

```ts
shopifyThemeIslands({
  directives: {
    visible: { rootMargin: "0px", threshold: 0.5 },
    idle: { timeout: 2000 },
    defer: { delay: 5000 },
    interaction: { events: ["mouseenter"] },
  },
});
```

Per-directive options are deep-merged — overriding `visible.rootMargin` preserves `visible.threshold` at its default of `0`.
For config, `directives.interaction.events` is intentionally narrow and only accepts `mouseenter`, `touchstart`, and `focusin`.
Per-element `client:interaction="..."` values are checked at runtime against that same set. Unsupported tokens warn and are ignored; if all tokens are unsupported, the runtime warns and falls back to the configured default events.
Per-element `client:idle` and `client:defer` values now require strict integer strings. Invalid values warn and fall back to the configured default timeout or delay.

### Enable automatic retry with exponential backoff

```ts
shopifyThemeIslands({
  retry: { retries: 3, delay: 1000 },
});
```

`retries` is the number of attempts after the first failure. `delay` is the base ms — each subsequent retry doubles it (1000ms → 2000ms → 4000ms).

### Guard against hung custom directives

```ts
shopifyThemeIslands({
  directiveTimeout: 5000,
});
```

When a custom directive never calls `load()`, the runtime normally waits forever. `directiveTimeout` turns that into an `islands:error` event and abandons the activation attempt after the configured number of milliseconds.

### Enable console debug output

```ts
shopifyThemeIslands({ debug: true });
```

Logs discovered islands, active directives per element, and load/error events at startup.

## Common Mistakes

### CRITICAL Virtual module not imported — islands never activate

Wrong:

```ts
// vite.config.ts — plugin configured but virtual module never imported
shopifyThemeIslands({ directories: ["/frontend/js/islands/"] });
```

Correct:

```ts
// frontend/js/theme.ts
import "vite-plugin-shopify-theme-islands/revive";
```

The plugin generates the virtual module but has no effect until it is imported in the browser entry point. Islands are silently never activated.

Source: src/index.ts — VIRTUAL_ID / RESOLVED_ID

### HIGH Agent hardcodes default values — unnecessary noise

Wrong:

```ts
shopifyThemeIslands({
  directories: ["/frontend/js/islands/"],
  debug: false,
  directives: {
    visible: { attribute: "client:visible", rootMargin: "200px", threshold: 0 },
    idle: { attribute: "client:idle", timeout: 500 },
    media: { attribute: "client:media" },
    defer: { attribute: "client:defer", delay: 3000 },
    interaction: { attribute: "client:interaction", events: ["mouseenter", "touchstart", "focusin"] },
  },
});
```

Correct:

```ts
shopifyThemeIslands();
```

All options are optional and default to sensible values. Only include options that differ from the defaults.

### HIGH Agent overwrites existing `vite.config.ts` instead of appending

Before adding the plugin, read the existing `vite.config.ts`. Projects commonly
already have `vite-plugin-shopify` or other plugins — the island plugin must be
added to the existing `plugins` array, not replace it.

Wrong:

```ts
// Replaces existing plugins
export default defineConfig({
  plugins: [shopifyThemeIslands()],
});
```

Correct:

```ts
// Appends to existing plugins
export default defineConfig({
  plugins: [
    shopify(), // pre-existing plugin preserved
    shopifyThemeIslands(),
  ],
});
```

### HIGH `retry` nested inside `directives` — no retries happen

Wrong:

```ts
shopifyThemeIslands({
  directives: {
    retry: { retries: 2 }, // ← wrong nesting
  },
});
```

Correct:

```ts
shopifyThemeIslands({
  retry: { retries: 2 }, // ← top-level option
});
```

`directives` accepts only `visible`, `idle`, `media`, `defer`, `interaction`, and `custom`. `retry` at `directives.retry` is silently ignored.

Source: src/options.ts — ShopifyThemeIslandsOptions

### HIGH Wrong key name for retry count

Wrong:

```ts
shopifyThemeIslands({ retry: { count: 3 } });
shopifyThemeIslands({ retry: { attempts: 3 } });
```

Correct:

```ts
shopifyThemeIslands({ retry: { retries: 3 } });
```

Unknown keys are silently ignored. The correct field is `retries`.

Source: src/contract.ts — RetryConfig

### HIGH `directiveTimeout` nested inside `directives` — timeout guard never applies

Wrong:

```ts
shopifyThemeIslands({
  directives: {
    directiveTimeout: 5000,
  },
});
```

Correct:

```ts
shopifyThemeIslands({
  directiveTimeout: 5000,
});
```

`directiveTimeout` is a top-level plugin option, not part of the per-directive config object.

Source: src/options.ts — ShopifyThemeIslandsOptions

### HIGH Empty or unsupported `directives.interaction.events` values fail config resolution

Wrong:

```ts
shopifyThemeIslands({
  directives: {
    interaction: { events: [] },
  },
});

shopifyThemeIslands({
  directives: {
    interaction: { events: ["click"] as never[] },
  },
});
```

Correct:

```ts
shopifyThemeIslands({
  directives: {
    interaction: { events: ["mouseenter", "focusin"] },
  },
});
```

The typed config surface only supports the package-owned interaction events `mouseenter`, `touchstart`, and `focusin`. An empty array is rejected because it would otherwise create an interaction gate that never resolves.

Source: src/interaction-events.ts — validateInteractionEvents()

### HIGH `directories: []` fails plugin validation

Wrong:

```ts
shopifyThemeIslands({ directories: [] });
```

Correct:

```ts
shopifyThemeIslands();
// or at least one path:
shopifyThemeIslands({ directories: ["/frontend/js/islands/"] });
```

An empty `directories` array is rejected at config resolution.

Source: src/resolved-config.ts — validateOptions()

### HIGH `directives.visible.threshold` outside 0–1 fails plugin validation

Wrong:

```ts
shopifyThemeIslands({
  directives: {
    visible: { threshold: 1.5 },
  },
});
```

Correct:

```ts
shopifyThemeIslands({
  directives: {
    visible: { threshold: 0.5 },
  },
});
```

`threshold` must be between `0` and `1` inclusive.

Source: src/resolved-config.ts — validateOptions()
