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 +122 -0
- package/client.d.ts +6 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +44 -0
- package/dist/runtime.d.ts +21 -0
- package/dist/runtime.js +70 -0
- package/package.json +65 -0
- package/revive.d.ts +2 -0
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
package/dist/index.d.ts
ADDED
|
@@ -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 {};
|
package/dist/runtime.js
ADDED
|
@@ -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