vite-plugin-shopify-theme-islands 0.1.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 ADDED
@@ -0,0 +1,122 @@
1
+ # vite-plugin-shopify-theme-islands
2
+
3
+ Island architecture for Shopify themes. Lazily hydrate custom elements using loading directives — only load the JavaScript when it's actually needed.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add -d vite-plugin-shopify-theme-islands
9
+ npm install -D vite-plugin-shopify-theme-islands
10
+ pnpm add -D vite-plugin-shopify-theme-islands
11
+ yarn add -D vite-plugin-shopify-theme-islands
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ ### 1. Add the plugin to `vite.config.ts`
17
+
18
+ ```ts
19
+ import { defineConfig } from "vite";
20
+ import shopifyThemeIslands from "vite-plugin-shopify-theme-islands";
21
+
22
+ export default defineConfig({
23
+ plugins: [
24
+ shopifyThemeIslands({
25
+ pathPrefix: "/frontend/js/islands/",
26
+ }),
27
+ ],
28
+ });
29
+ ```
30
+
31
+ ### 2. Call `revive` in your entrypoint
32
+
33
+ ```ts
34
+ import revive from "vite-plugin-shopify-theme-islands/revive";
35
+
36
+ const islands = import.meta.glob("/frontend/js/islands/*.{ts,js}");
37
+ revive(islands);
38
+ ```
39
+
40
+ The glob pattern must match the `pathPrefix` option. Each file in that directory corresponds to a custom element — the filename (without extension) is the tag name.
41
+
42
+ ## Writing islands
43
+
44
+ Each island is a file in your islands directory that defines and registers a custom element. The filename must match the custom element tag name used in your Liquid templates.
45
+
46
+ ```
47
+ frontend/js/islands/
48
+ product-form.ts → <product-form>
49
+ cart-drawer.ts → <cart-drawer>
50
+ ```
51
+
52
+ ```ts
53
+ // frontend/js/islands/product-form.ts
54
+ class ProductForm extends HTMLElement {
55
+ connectedCallback() {
56
+ // ...
57
+ }
58
+
59
+ disconnectedCallback() {
60
+ // ...
61
+ }
62
+ }
63
+
64
+ if (!customElements.get("product-form")) {
65
+ customElements.define("product-form", ProductForm);
66
+ }
67
+ ```
68
+
69
+ ## Loading directives
70
+
71
+ Add these attributes to your custom elements in Liquid to control when the JavaScript loads.
72
+
73
+ ### `client:visible`
74
+
75
+ Loads the island when the element scrolls into view.
76
+
77
+ ```html
78
+ <product-recommendations client:visible>
79
+ <!-- ... -->
80
+ </product-recommendations>
81
+ ```
82
+
83
+ ### `client:media`
84
+
85
+ Loads the island when a CSS media query matches.
86
+
87
+ ```html
88
+ <mobile-menu client:media="(max-width: 768px)">
89
+ <!-- ... -->
90
+ </mobile-menu>
91
+ ```
92
+
93
+ ### `client:idle`
94
+
95
+ Loads the island once the browser is idle (uses `requestIdleCallback`, falls back to `setTimeout`).
96
+
97
+ ```html
98
+ <recently-viewed client:idle>
99
+ <!-- ... -->
100
+ </recently-viewed>
101
+ ```
102
+
103
+ Directives can be combined — the element will wait for all conditions to be met before loading:
104
+
105
+ ```html
106
+ <heavy-widget client:visible client:idle>
107
+ <!-- ... -->
108
+ </heavy-widget>
109
+ ```
110
+
111
+ ## Options
112
+
113
+ | Option | Type | Default | Description |
114
+ | ------------------ | -------- | ------------------------- | -------------------------------------------------------------- |
115
+ | `pathPrefix` | `string` | `'/frontend/js/islands/'` | Path prefix used to match `import.meta.glob` keys to tag names |
116
+ | `directiveVisible` | `string` | `'client:visible'` | Attribute name for the visible directive |
117
+ | `directiveMedia` | `string` | `'client:media'` | Attribute name for the media directive |
118
+ | `directiveIdle` | `string` | `'client:idle'` | Attribute name for the idle directive |
119
+
120
+ ## License
121
+
122
+ MIT
package/client.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // virtual: alias for Vite convention compatibility
4
+ declare module 'virtual:shopify-theme-islands/revive' {
5
+ export { default } from 'vite-plugin-shopify-theme-islands/revive';
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface ShopifyThemeIslandsOptions {
3
+ /** Path prefix used to match island glob keys. Default: `'/frontend/js/islands/'` */
4
+ pathPrefix?: string;
5
+ /** Attribute for "load when visible". Default: `'client:visible'` */
6
+ directiveVisible?: string;
7
+ /** Attribute for "load when media matches". Default: `'client:media'` */
8
+ directiveMedia?: string;
9
+ /** Attribute for "load when idle". Default: `'client:idle'` */
10
+ directiveIdle?: string;
11
+ }
12
+ export default function shopifyThemeIslands(pluginOptions?: ShopifyThemeIslandsOptions): Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ // src/index.ts
2
+ var {readFileSync} = (() => ({}));
3
+ var VIRTUAL_REVIVE = "virtual:shopify-theme-islands/revive";
4
+ var VIRTUAL_RUNTIME = "\x00virtual:shopify-theme-islands/runtime";
5
+ var runtimePath = new URL("./runtime.js", import.meta.url).pathname;
6
+ function shopifyThemeIslands(pluginOptions = {}) {
7
+ const pathPrefix = pluginOptions.pathPrefix ?? "/frontend/js/islands/";
8
+ const directiveVisible = pluginOptions.directiveVisible ?? "client:visible";
9
+ const directiveMedia = pluginOptions.directiveMedia ?? "client:media";
10
+ const directiveIdle = pluginOptions.directiveIdle ?? "client:idle";
11
+ const runtime = readFileSync(runtimePath, "utf-8");
12
+ return {
13
+ name: "vite-plugin-shopify-theme-islands",
14
+ enforce: "pre",
15
+ resolveId(id) {
16
+ if (id === VIRTUAL_REVIVE || id === "vite-plugin-shopify-theme-islands/revive")
17
+ return "\x00" + VIRTUAL_REVIVE;
18
+ if (id === VIRTUAL_RUNTIME)
19
+ return id;
20
+ return null;
21
+ },
22
+ load(id) {
23
+ if (id === VIRTUAL_RUNTIME)
24
+ return runtime;
25
+ if (id !== "\x00" + VIRTUAL_REVIVE)
26
+ return null;
27
+ return `
28
+ import { revive as _revive } from '${VIRTUAL_RUNTIME}';
29
+
30
+ export default function revive(islands) {
31
+ _revive(islands, {
32
+ pathPrefix: ${JSON.stringify(pathPrefix)},
33
+ directiveVisible: ${JSON.stringify(directiveVisible)},
34
+ directiveMedia: ${JSON.stringify(directiveMedia)},
35
+ directiveIdle: ${JSON.stringify(directiveIdle)},
36
+ });
37
+ }
38
+ `;
39
+ }
40
+ };
41
+ }
42
+ export {
43
+ shopifyThemeIslands as default
44
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Island architecture runtime for Shopify themes.
3
+ *
4
+ * Walks the DOM for custom elements that match island files, then loads them
5
+ * lazily based on client directives:
6
+ *
7
+ * client:visible — load when the element scrolls into view
8
+ * client:media — load when a CSS media query matches
9
+ * client:idle — load when the browser has idle time
10
+ *
11
+ * Directives can be combined; all conditions must be met before loading.
12
+ * A MutationObserver re-runs the same logic for elements added dynamically.
13
+ */
14
+ interface ReviveOptions {
15
+ pathPrefix?: string;
16
+ directiveVisible?: string;
17
+ directiveMedia?: string;
18
+ directiveIdle?: string;
19
+ }
20
+ export declare function revive(islands: Record<string, () => Promise<unknown>>, options?: ReviveOptions): void;
21
+ export {};
@@ -0,0 +1,70 @@
1
+ // src/runtime.ts
2
+ function media(query) {
3
+ const m = window.matchMedia(query);
4
+ return new Promise((resolve) => {
5
+ if (m.matches)
6
+ resolve();
7
+ else
8
+ m.addEventListener("change", () => resolve(), { once: true });
9
+ });
10
+ }
11
+ function visible(element) {
12
+ return new Promise((resolve) => {
13
+ const obs = new IntersectionObserver((entries) => {
14
+ for (const e of entries) {
15
+ if (e.isIntersecting) {
16
+ obs.disconnect();
17
+ resolve();
18
+ break;
19
+ }
20
+ }
21
+ });
22
+ obs.observe(element);
23
+ });
24
+ }
25
+ function idle() {
26
+ return new Promise((resolve) => {
27
+ if ("requestIdleCallback" in window)
28
+ window.requestIdleCallback(() => resolve());
29
+ else
30
+ setTimeout(resolve, 200);
31
+ });
32
+ }
33
+ function revive(islands, options) {
34
+ const pathPrefix = options?.pathPrefix ?? "/frontend/js/islands/";
35
+ const attrVisible = options?.directiveVisible ?? "client:visible";
36
+ const attrMedia = options?.directiveMedia ?? "client:media";
37
+ const attrIdle = options?.directiveIdle ?? "client:idle";
38
+ const observer = new MutationObserver((mutations) => {
39
+ for (const { addedNodes } of mutations) {
40
+ for (const node of addedNodes) {
41
+ if (node.nodeType === Node.ELEMENT_NODE)
42
+ dfs(node);
43
+ }
44
+ }
45
+ });
46
+ async function dfs(node) {
47
+ const tagName = node.tagName.toLowerCase();
48
+ const loader = islands[pathPrefix + tagName + ".ts"] ?? islands[pathPrefix + tagName + ".js"];
49
+ if (/-/.test(tagName) && loader) {
50
+ if (node.hasAttribute(attrVisible))
51
+ await visible(node);
52
+ const q = node.getAttribute(attrMedia);
53
+ if (q)
54
+ await media(q);
55
+ if (node.hasAttribute(attrIdle))
56
+ await idle();
57
+ loader().catch(console.error);
58
+ }
59
+ let child = node.firstElementChild;
60
+ while (child) {
61
+ dfs(child);
62
+ child = child.nextElementSibling;
63
+ }
64
+ }
65
+ dfs(document.body);
66
+ observer.observe(document.body, { childList: true, subtree: true });
67
+ }
68
+ export {
69
+ revive
70
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "vite-plugin-shopify-theme-islands",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin for island architecture in Shopify themes",
5
+ "type": "module",
6
+ "packageManager": "bun@1.3.10",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./revive": {
15
+ "types": "./revive.d.ts"
16
+ },
17
+ "./client": {
18
+ "types": "./client.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "client.d.ts",
24
+ "revive.d.ts"
25
+ ],
26
+ "sideEffects": false,
27
+ "keywords": [
28
+ "vite",
29
+ "shopify",
30
+ "islands",
31
+ "vite-plugin"
32
+ ],
33
+ "license": "MIT",
34
+ "author": {
35
+ "name": "Alex Rees",
36
+ "email": "alex@thirteenstudios.agency",
37
+ "url": "https://thirteenstudios.agency"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/Rees1993/vite-plugin-shopify-theme-islands"
42
+ },
43
+ "homepage": "https://github.com/Rees1993/vite-plugin-shopify-theme-islands#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/Rees1993/vite-plugin-shopify-theme-islands/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "scripts": {
51
+ "build:js": "bun build src/index.ts src/runtime.ts --outdir dist --format esm",
52
+ "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist",
53
+ "build": "bun run build:js && bun run build:types",
54
+ "check": "tsc --noEmit",
55
+ "prepublishOnly": "bun run build"
56
+ },
57
+ "peerDependencies": {
58
+ "vite": ">=6"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^25.3.3",
62
+ "typescript": "^5.0.0",
63
+ "vite": "^6.0.0"
64
+ }
65
+ }
package/revive.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare function revive(islands: Record<string, () => Promise<unknown>>): void;
2
+ export default revive;