vite-plugin-shopify-theme-islands 0.5.0 → 0.6.1
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 +106 -15
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -21
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +55 -19
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ The plugin detects the mixin import at build time and includes the file as a laz
|
|
|
100
100
|
|
|
101
101
|
Both can be used together — directory scanning for new islands, the mixin for existing components you want to adopt without moving.
|
|
102
102
|
|
|
103
|
-
##
|
|
103
|
+
## Directives
|
|
104
104
|
|
|
105
105
|
Add these attributes to your custom elements in Liquid to control when the JavaScript loads. Without a directive, the island loads immediately.
|
|
106
106
|
|
|
@@ -114,6 +114,15 @@ Loads the island when the element scrolls into view.
|
|
|
114
114
|
</product-recommendations>
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
The attribute value overrides the global `rootMargin` for that element only:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<!-- load only once fully visible (no pre-load margin) -->
|
|
121
|
+
<product-recommendations client:visible="0px">
|
|
122
|
+
<!-- ... -->
|
|
123
|
+
</product-recommendations>
|
|
124
|
+
```
|
|
125
|
+
|
|
117
126
|
### `client:media`
|
|
118
127
|
|
|
119
128
|
Loads the island when a CSS media query matches.
|
|
@@ -134,6 +143,15 @@ Loads the island once the browser is idle (uses `requestIdleCallback` with a 500
|
|
|
134
143
|
</recently-viewed>
|
|
135
144
|
```
|
|
136
145
|
|
|
146
|
+
The attribute value overrides the global `timeout` for that element only:
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<!-- wait up to 2 seconds for idle time before loading -->
|
|
150
|
+
<recently-viewed client:idle="2000">
|
|
151
|
+
<!-- ... -->
|
|
152
|
+
</recently-viewed>
|
|
153
|
+
```
|
|
154
|
+
|
|
137
155
|
### `client:defer`
|
|
138
156
|
|
|
139
157
|
Loads the island after a fixed delay. The delay in milliseconds is read from the attribute value. If no value is given, the configured default (3000ms) is used.
|
|
@@ -151,7 +169,9 @@ Loads the island after a fixed delay. The delay in milliseconds is read from the
|
|
|
151
169
|
|
|
152
170
|
Unlike `client:idle`, which waits for genuine browser idle time, `client:defer` always waits exactly the specified number of milliseconds.
|
|
153
171
|
|
|
154
|
-
|
|
172
|
+
### Combining directives
|
|
173
|
+
|
|
174
|
+
Directives can be combined — the element waits for all conditions to be met before loading:
|
|
155
175
|
|
|
156
176
|
```html
|
|
157
177
|
<heavy-widget client:visible client:idle>
|
|
@@ -159,13 +179,83 @@ Directives can be combined — the element will wait for all conditions to be me
|
|
|
159
179
|
</heavy-widget>
|
|
160
180
|
```
|
|
161
181
|
|
|
162
|
-
|
|
182
|
+
### Custom directives
|
|
183
|
+
|
|
184
|
+
Register your own loading conditions via `directives.custom`. A custom directive is a function that receives a `load` callback and decides when to call it.
|
|
185
|
+
|
|
186
|
+
#### 1. Write the directive
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// src/directives/hover.ts
|
|
190
|
+
import type { ClientDirective } from "vite-plugin-shopify-theme-islands";
|
|
191
|
+
|
|
192
|
+
const hoverDirective: ClientDirective = (load, _opts, el) => {
|
|
193
|
+
el.addEventListener("mouseenter", load, { once: true });
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default hoverDirective;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`mouseenter` fires before `click`, so the module starts downloading the moment the user moves their cursor toward the element — by the time they click it's already loaded.
|
|
200
|
+
|
|
201
|
+
The function signature is `(load, options, el) => void | Promise<void>`:
|
|
202
|
+
|
|
203
|
+
| Parameter | Type | Description |
|
|
204
|
+
| --------------- | ------------------------ | ----------------------------------------------------- |
|
|
205
|
+
| `load` | `() => Promise<unknown>` | Call this to trigger the island module load |
|
|
206
|
+
| `options.name` | `string` | The matched attribute name, e.g. `'client:hover'` |
|
|
207
|
+
| `options.value` | `string` | The attribute value; empty string if no value was set |
|
|
208
|
+
| `el` | `HTMLElement` | The island element |
|
|
209
|
+
|
|
210
|
+
#### 2. Register it in the plugin config
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// vite.config.ts
|
|
214
|
+
import shopifyThemeIslands from "vite-plugin-shopify-theme-islands";
|
|
215
|
+
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
plugins: [
|
|
218
|
+
shopifyThemeIslands({
|
|
219
|
+
directives: {
|
|
220
|
+
custom: [{ name: "client:hover", entrypoint: "./src/directives/hover.ts" }],
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The `entrypoint` supports Vite aliases.
|
|
228
|
+
|
|
229
|
+
#### 3. Use it in Liquid
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<quick-add client:hover>
|
|
233
|
+
<!-- ... -->
|
|
234
|
+
</quick-add>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Ordering
|
|
238
|
+
|
|
239
|
+
Built-in directives always run first. A custom directive is only invoked after all built-in conditions on the element have been met. This means you can gate a custom directive behind `client:visible` to avoid wiring event listeners for off-screen elements:
|
|
240
|
+
|
|
241
|
+
```html
|
|
242
|
+
<!-- element must enter the viewport before the hover handler is registered -->
|
|
243
|
+
<quick-add client:visible client:hover>
|
|
244
|
+
<!-- ... -->
|
|
245
|
+
</quick-add>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The custom directive owns the `load()` call — the built-in chain never calls it directly when a custom directive is matched.
|
|
249
|
+
|
|
250
|
+
> Only one custom directive can be active per element. If multiple custom directive attributes are present, the first registered one is used and a console warning is emitted. Combining multiple custom directives on one element is not yet supported.
|
|
251
|
+
|
|
252
|
+
## Configuration
|
|
163
253
|
|
|
164
|
-
| Option | Type | Default | Description
|
|
165
|
-
| ------------- | -------------------- | --------------------------- |
|
|
166
|
-
| `directories` | `string \| string[]` | `['/frontend/js/islands/']` | Directories to scan for island files. Accepts Vite aliases.
|
|
167
|
-
| `directives` | `object` | see below | Per-directive configuration
|
|
168
|
-
| `debug` | `boolean` | `false` | Log discovered islands at build time and directive events in the browser console.
|
|
254
|
+
| Option | Type | Default | Description |
|
|
255
|
+
| ------------- | -------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
|
|
256
|
+
| `directories` | `string \| string[]` | `['/frontend/js/islands/']` | Directories to scan for island files. Accepts Vite aliases. |
|
|
257
|
+
| `directives` | `object` | see below | Per-directive configuration — attribute names, timing options, and custom entries. |
|
|
258
|
+
| `debug` | `boolean` | `false` | Log discovered islands at build time and directive events in the browser console. |
|
|
169
259
|
|
|
170
260
|
### Directive defaults
|
|
171
261
|
|
|
@@ -174,20 +264,21 @@ shopifyThemeIslands({
|
|
|
174
264
|
directives: {
|
|
175
265
|
visible: {
|
|
176
266
|
attribute: "client:visible", // HTML attribute name
|
|
177
|
-
rootMargin: "200px",
|
|
178
|
-
threshold: 0,
|
|
267
|
+
rootMargin: "200px", // passed to IntersectionObserver — pre-loads before scrolling into view
|
|
268
|
+
threshold: 0, // passed to IntersectionObserver — ratio of element that must be visible
|
|
179
269
|
},
|
|
180
270
|
idle: {
|
|
181
|
-
attribute: "client:idle",
|
|
182
|
-
timeout: 500,
|
|
271
|
+
attribute: "client:idle", // HTML attribute name
|
|
272
|
+
timeout: 500, // deadline (ms) for requestIdleCallback; also the setTimeout fallback delay
|
|
183
273
|
},
|
|
184
274
|
media: {
|
|
185
|
-
attribute: "client:media",
|
|
275
|
+
attribute: "client:media", // HTML attribute name
|
|
186
276
|
},
|
|
187
277
|
defer: {
|
|
188
|
-
attribute: "client:defer",
|
|
189
|
-
delay: 3000,
|
|
278
|
+
attribute: "client:defer", // HTML attribute name
|
|
279
|
+
delay: 3000, // fallback delay (ms) when the attribute has no value
|
|
190
280
|
},
|
|
281
|
+
custom: [], // custom directives — see Custom directives above
|
|
191
282
|
},
|
|
192
283
|
});
|
|
193
284
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
/** A function that triggers the load of an island module. */
|
|
3
|
+
export type ClientDirectiveLoader = () => Promise<unknown>;
|
|
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/hover.ts
|
|
20
|
+
* import type { ClientDirective } from 'vite-plugin-shopify-theme-islands';
|
|
21
|
+
*
|
|
22
|
+
* const hoverDirective: ClientDirective = (load, _opts, el) => {
|
|
23
|
+
* el.addEventListener('mouseenter', load, { once: true });
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* export default hoverDirective;
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Register it in `vite.config.ts`:
|
|
30
|
+
* ```ts
|
|
31
|
+
* shopifyThemeIslands({
|
|
32
|
+
* directives: {
|
|
33
|
+
* custom: [{ name: 'client:hover', entrypoint: './src/directives/hover.ts' }],
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type ClientDirective = (load: ClientDirectiveLoader, options: ClientDirectiveOptions, el: HTMLElement) => void | Promise<void>;
|
|
39
|
+
/** Plugin option entry for registering a custom client directive. */
|
|
40
|
+
export interface ClientDirectiveDefinition {
|
|
41
|
+
/** HTML attribute name, e.g. `'client:on-click'` */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Path to the directive module (supports Vite aliases) */
|
|
44
|
+
entrypoint: string;
|
|
45
|
+
}
|
|
2
46
|
/** Shared directive configuration shape used by both the plugin and the runtime. */
|
|
3
47
|
export interface DirectivesConfig {
|
|
4
48
|
/** Configuration for the `client:visible` directive (IntersectionObserver). */
|
|
@@ -29,6 +73,8 @@ export interface DirectivesConfig {
|
|
|
29
73
|
/** Fallback delay (ms) when the attribute has no value. Default: `3000` */
|
|
30
74
|
delay?: number;
|
|
31
75
|
};
|
|
76
|
+
/** Custom client directives to register. Each entry maps an attribute name to a module entrypoint. */
|
|
77
|
+
custom?: ClientDirectiveDefinition[];
|
|
32
78
|
}
|
|
33
79
|
export interface ShopifyThemeIslandsOptions {
|
|
34
80
|
/** Directories to scan for island files. Accepts paths or Vite aliases. Default: `['/frontend/js/islands/']` */
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ var runtimePath = fileURLToPath(new URL("./runtime.js", import.meta.url));
|
|
|
9
9
|
var islandPath = fileURLToPath(new URL("./island.js", import.meta.url));
|
|
10
10
|
var ISLAND_IMPORT_RE = /from\s+['"]vite-plugin-shopify-theme-islands\/island['"]/;
|
|
11
11
|
var TS_JS_RE = /\.(ts|js)$/;
|
|
12
|
+
var SKIP_DIRS = new Set(["node_modules", "dist", "build", "public", "assets", ".cache"]);
|
|
12
13
|
var defaults = {
|
|
13
14
|
directories: ["/frontend/js/islands/"],
|
|
14
15
|
directives: {
|
|
@@ -33,6 +34,23 @@ function resolveAliases(dirs, config) {
|
|
|
33
34
|
return dir;
|
|
34
35
|
});
|
|
35
36
|
}
|
|
37
|
+
function collectTagNames(dir, names) {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
|
|
46
|
+
continue;
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
collectTagNames(join(dir, entry.name), names);
|
|
49
|
+
} else if (TS_JS_RE.test(entry.name)) {
|
|
50
|
+
names.push(entry.name.replace(/\.(ts|js)$/, ""));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
function scanForIslandFiles(dir, found) {
|
|
37
55
|
let entries;
|
|
38
56
|
try {
|
|
@@ -41,7 +59,7 @@ function scanForIslandFiles(dir, found) {
|
|
|
41
59
|
return;
|
|
42
60
|
}
|
|
43
61
|
for (const entry of entries) {
|
|
44
|
-
if (entry.name.startsWith(".") || entry.name
|
|
62
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
|
|
45
63
|
continue;
|
|
46
64
|
const full = join(dir, entry.name);
|
|
47
65
|
if (entry.isDirectory()) {
|
|
@@ -63,46 +81,51 @@ function shopifyThemeIslands(options = {}) {
|
|
|
63
81
|
media: { ...defaults.directives.media, ...options.directives?.media },
|
|
64
82
|
defer: { ...defaults.directives.defer, ...options.directives?.defer }
|
|
65
83
|
};
|
|
84
|
+
const clientDirectiveDefinitions = options.directives?.custom ?? [];
|
|
66
85
|
const debug = options.debug ?? false;
|
|
67
|
-
const log = (...args) => {
|
|
68
|
-
if (debug)
|
|
69
|
-
console.log("[islands]", ...args);
|
|
70
|
-
};
|
|
86
|
+
const log = debug ? (...args) => console.log("[islands]", ...args) : () => {};
|
|
71
87
|
let resolvedDirs = rawDirs;
|
|
72
88
|
let root = process.cwd();
|
|
89
|
+
let absDirs = rawDirs;
|
|
73
90
|
const islandFiles = new Set;
|
|
74
91
|
let scanned = false;
|
|
75
|
-
const inDirectory = (file) =>
|
|
76
|
-
const absDir = dir.startsWith(root) ? dir : join(root, dir.replace(/^\//, ""));
|
|
77
|
-
return file.startsWith(absDir);
|
|
78
|
-
});
|
|
92
|
+
const inDirectory = (file) => absDirs.some((dir) => file.startsWith(dir));
|
|
79
93
|
return {
|
|
80
94
|
name: "vite-plugin-shopify-theme-islands",
|
|
81
95
|
enforce: "pre",
|
|
82
96
|
configResolved(config) {
|
|
83
97
|
root = config.root;
|
|
84
98
|
resolvedDirs = resolveAliases(rawDirs, config);
|
|
99
|
+
absDirs = resolvedDirs.map((d) => d.startsWith(root) ? d : join(root, d.replace(/^\//, "")));
|
|
85
100
|
},
|
|
86
101
|
buildStart() {
|
|
87
102
|
if (scanned)
|
|
88
103
|
return;
|
|
89
104
|
scanned = true;
|
|
105
|
+
const t0 = performance.now();
|
|
90
106
|
scanForIslandFiles(root, islandFiles);
|
|
107
|
+
const scanMs = (performance.now() - t0).toFixed(1);
|
|
91
108
|
for (const f of islandFiles)
|
|
92
109
|
if (inDirectory(f))
|
|
93
110
|
islandFiles.delete(f);
|
|
94
111
|
if (debug) {
|
|
112
|
+
log(`Scanned in ${scanMs}ms`);
|
|
95
113
|
log("Scanning directories:", resolvedDirs.map((d) => d + "**/*.{ts,js}").join(", "));
|
|
96
|
-
|
|
114
|
+
const dirNames = [];
|
|
115
|
+
for (const dir of absDirs)
|
|
116
|
+
collectTagNames(dir, dirNames);
|
|
117
|
+
if (dirNames.length)
|
|
118
|
+
log(`Found ${dirNames.length} directory island(s): [${dirNames.join(", ")}]`);
|
|
97
119
|
if (islandFiles.size) {
|
|
98
120
|
log(`Found ${islandFiles.size} island file(s) via mixin import:`);
|
|
99
121
|
for (const f of islandFiles)
|
|
100
122
|
log(" ", relative(root, f));
|
|
101
123
|
}
|
|
124
|
+
log("Directives:", directives);
|
|
102
125
|
}
|
|
103
126
|
},
|
|
104
127
|
transform(code, id) {
|
|
105
|
-
if (!
|
|
128
|
+
if (!TS_JS_RE.test(id))
|
|
106
129
|
return;
|
|
107
130
|
if (code.includes("shopify-theme-islands/island") && ISLAND_IMPORT_RE.test(code) && !inDirectory(id)) {
|
|
108
131
|
islandFiles.add(id);
|
|
@@ -137,21 +160,40 @@ function shopifyThemeIslands(options = {}) {
|
|
|
137
160
|
if (id === ISLAND_ID)
|
|
138
161
|
return islandPath;
|
|
139
162
|
},
|
|
140
|
-
load(id) {
|
|
163
|
+
async load(id) {
|
|
141
164
|
if (id !== RESOLVED_ID)
|
|
142
165
|
return;
|
|
143
166
|
const globs = resolvedDirs.map((dir) => `...import.meta.glob(${JSON.stringify(dir + "**/*.{ts,js}")})`);
|
|
144
|
-
const islandPaths = [...islandFiles].map((file) => "/" + relative(root, file).replace(/\\/g, "/"));
|
|
145
|
-
const islandsEntries = [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
]
|
|
149
|
-
|
|
167
|
+
const islandPaths = islandFiles.size ? [...islandFiles].map((file) => "/" + relative(root, file).replace(/\\/g, "/")) : null;
|
|
168
|
+
const islandsEntries = [`{ ${globs.join(", ")} }`];
|
|
169
|
+
if (islandPaths)
|
|
170
|
+
islandsEntries.push(`import.meta.glob(${JSON.stringify(islandPaths)})`);
|
|
171
|
+
const directiveImports = [];
|
|
172
|
+
const mapEntries = [];
|
|
173
|
+
for (const [i, def] of clientDirectiveDefinitions.entries()) {
|
|
174
|
+
const resolved = await this.resolve(def.entrypoint);
|
|
175
|
+
if (!resolved) {
|
|
176
|
+
throw new Error(`[vite-plugin-shopify-theme-islands] Cannot resolve custom directive entrypoint: "${def.entrypoint}"`);
|
|
177
|
+
}
|
|
178
|
+
directiveImports.push(`import _directive${i} from ${JSON.stringify(resolved.id)};`);
|
|
179
|
+
mapEntries.push(` [${JSON.stringify(def.name)}, _directive${i}]`);
|
|
180
|
+
}
|
|
181
|
+
const lines = [
|
|
182
|
+
...directiveImports,
|
|
150
183
|
`import { revive as _islands } from ${JSON.stringify(runtimePath)};`,
|
|
151
184
|
`const islands = Object.assign({}, ${islandsEntries.join(", ")});`,
|
|
152
|
-
`const options = ${JSON.stringify({ directives, debug })}
|
|
153
|
-
|
|
154
|
-
|
|
185
|
+
`const options = ${JSON.stringify({ directives, debug })};`
|
|
186
|
+
];
|
|
187
|
+
if (mapEntries.length) {
|
|
188
|
+
lines.push(`const customDirectives = new Map([
|
|
189
|
+
${mapEntries.join(`,
|
|
190
|
+
`)}
|
|
191
|
+
]);`);
|
|
192
|
+
lines.push(`_islands(islands, options, customDirectives);`);
|
|
193
|
+
} else {
|
|
194
|
+
lines.push(`_islands(islands, options);`);
|
|
195
|
+
}
|
|
196
|
+
return lines.join(`
|
|
155
197
|
`);
|
|
156
198
|
}
|
|
157
199
|
};
|
package/dist/runtime.d.ts
CHANGED
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
* Directives can be combined; all conditions must be met before loading.
|
|
13
13
|
* A MutationObserver re-runs the same logic for elements added dynamically.
|
|
14
14
|
*/
|
|
15
|
-
import type { ReviveOptions } from "./index.js";
|
|
16
|
-
export declare function revive(islands: Record<string, () => Promise<unknown>>, options?: ReviveOptions): void;
|
|
15
|
+
import type { ClientDirective, ReviveOptions } from "./index.js";
|
|
16
|
+
export declare function revive(islands: Record<string, () => Promise<unknown>>, options?: ReviveOptions, customDirectives?: Map<string, ClientDirective>): void;
|
package/dist/runtime.js
CHANGED
|
@@ -41,7 +41,7 @@ function idle(timeout) {
|
|
|
41
41
|
var customElementFilter = {
|
|
42
42
|
acceptNode: (node) => node.tagName.includes("-") ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
|
|
43
43
|
};
|
|
44
|
-
function revive(islands, options) {
|
|
44
|
+
function revive(islands, options, customDirectives) {
|
|
45
45
|
const attrVisible = options?.directives?.visible?.attribute ?? "client:visible";
|
|
46
46
|
const attrMedia = options?.directives?.media?.attribute ?? "client:media";
|
|
47
47
|
const attrIdle = options?.directives?.idle?.attribute ?? "client:idle";
|
|
@@ -50,7 +50,8 @@ function revive(islands, options) {
|
|
|
50
50
|
const threshold = options?.directives?.visible?.threshold ?? 0;
|
|
51
51
|
const idleTimeout = options?.directives?.idle?.timeout ?? 500;
|
|
52
52
|
const deferDelay = options?.directives?.defer?.delay ?? 3000;
|
|
53
|
-
const
|
|
53
|
+
const debug = options?.debug ?? false;
|
|
54
|
+
const log = debug ? (...args) => console.log("[islands]", ...args) : () => {};
|
|
54
55
|
const islandMap = new Map;
|
|
55
56
|
for (const [key, loader] of Object.entries(islands)) {
|
|
56
57
|
const tagName = key.split("/").pop().replace(/\.(ts|js)$/, "");
|
|
@@ -61,44 +62,79 @@ function revive(islands, options) {
|
|
|
61
62
|
if (!islandMap.has(tagName))
|
|
62
63
|
islandMap.set(tagName, loader);
|
|
63
64
|
}
|
|
65
|
+
log(`revive() ready — ${islandMap.size} island(s)`);
|
|
64
66
|
const queued = new Set;
|
|
65
67
|
const pendingVisible = new Map;
|
|
66
68
|
async function loadIsland(tagName, el, loader) {
|
|
67
69
|
log(`<${tagName}> activating`);
|
|
70
|
+
const msgs = [];
|
|
71
|
+
const note = debug ? (msg) => {
|
|
72
|
+
msgs.push(msg);
|
|
73
|
+
} : () => {};
|
|
74
|
+
const flush = debug ? (final) => {
|
|
75
|
+
if (msgs.length === 0) {
|
|
76
|
+
console.log("[islands]", `<${tagName}> ${final}`);
|
|
77
|
+
} else {
|
|
78
|
+
console.groupCollapsed(`[islands] <${tagName}>`);
|
|
79
|
+
for (const m of msgs)
|
|
80
|
+
console.log(m);
|
|
81
|
+
console.log(final);
|
|
82
|
+
console.groupEnd();
|
|
83
|
+
}
|
|
84
|
+
} : () => {};
|
|
68
85
|
try {
|
|
69
86
|
if (el.hasAttribute(attrVisible)) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
const elRootMargin = el.getAttribute(attrVisible) || rootMargin;
|
|
88
|
+
note(`waiting for ${attrVisible}`);
|
|
89
|
+
await visible(el, elRootMargin, threshold, pendingVisible);
|
|
73
90
|
}
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
await media(
|
|
78
|
-
log(`<${tagName}> ${attrMedia} resolved`);
|
|
91
|
+
const query = el.getAttribute(attrMedia);
|
|
92
|
+
if (query) {
|
|
93
|
+
note(`waiting for ${attrMedia}="${query}"`);
|
|
94
|
+
await media(query);
|
|
79
95
|
}
|
|
80
96
|
if (el.hasAttribute(attrIdle)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
const rawIdle = parseInt(el.getAttribute(attrIdle), 10);
|
|
98
|
+
const elTimeout = Number.isNaN(rawIdle) ? idleTimeout : rawIdle;
|
|
99
|
+
note(`waiting for ${attrIdle} (timeout: ${elTimeout}ms)`);
|
|
100
|
+
await idle(elTimeout);
|
|
84
101
|
}
|
|
85
102
|
const d = el.getAttribute(attrDefer);
|
|
86
103
|
if (d !== null) {
|
|
87
104
|
const raw = parseInt(d, 10);
|
|
88
|
-
const ms =
|
|
105
|
+
const ms = Number.isNaN(raw) ? deferDelay : raw;
|
|
89
106
|
if (d !== "" && Number.isNaN(raw)) {
|
|
90
107
|
console.warn(`[islands] <${tagName}> invalid ${attrDefer} value "${d}" — using default ${deferDelay}ms`);
|
|
91
108
|
}
|
|
92
|
-
|
|
109
|
+
note(`waiting for ${attrDefer} (${ms}ms)`);
|
|
93
110
|
await defer(ms);
|
|
94
|
-
log(`<${tagName}> ${attrDefer} resolved`);
|
|
95
111
|
}
|
|
96
112
|
} catch {
|
|
97
|
-
|
|
113
|
+
flush("aborted (element removed)");
|
|
98
114
|
return;
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
const run = () => loader().catch((err) => {
|
|
117
|
+
console.error(`[islands] Failed to load <${tagName}>:`, err);
|
|
118
|
+
queued.delete(tagName);
|
|
119
|
+
});
|
|
120
|
+
if (customDirectives?.size) {
|
|
121
|
+
const matched = [];
|
|
122
|
+
for (const [attrName, directiveFn] of customDirectives) {
|
|
123
|
+
if (el.hasAttribute(attrName))
|
|
124
|
+
matched.push([attrName, directiveFn]);
|
|
125
|
+
}
|
|
126
|
+
if (matched.length > 1) {
|
|
127
|
+
console.warn(`[islands] <${tagName}> has multiple custom directives (${matched.map(([a]) => a).join(", ")}) — only "${matched[0][0]}" will be used. Combining custom directives is not yet supported.`);
|
|
128
|
+
}
|
|
129
|
+
if (matched.length > 0) {
|
|
130
|
+
const [attrName, directiveFn] = matched[0];
|
|
131
|
+
flush(`dispatching to custom directive ${attrName}`);
|
|
132
|
+
directiveFn(run, { name: attrName, value: el.getAttribute(attrName) }, el);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
flush("triggered");
|
|
137
|
+
run();
|
|
102
138
|
}
|
|
103
139
|
function activate(el) {
|
|
104
140
|
const tagName = el.tagName.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-shopify-theme-islands",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Vite plugin for island architecture in Shopify themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.10",
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"test:plugin": "bun test src/__tests__/plugin.test.ts",
|
|
59
59
|
"test:runtime": "bun test ./src/__tests__/runtime.test.ts",
|
|
60
60
|
"test": "bun test",
|
|
61
|
-
"test:watch": "bun test --watch"
|
|
61
|
+
"test:watch": "bun test --watch",
|
|
62
|
+
"lint": "oxlint src/",
|
|
63
|
+
"format": "oxfmt src/"
|
|
62
64
|
},
|
|
63
65
|
"peerDependencies": {
|
|
64
66
|
"vite": ">=6"
|
|
@@ -67,6 +69,8 @@
|
|
|
67
69
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
68
70
|
"@types/bun": "latest",
|
|
69
71
|
"@types/node": "^25.3.3",
|
|
72
|
+
"oxfmt": "^0.38.0",
|
|
73
|
+
"oxlint": "^1.53.0",
|
|
70
74
|
"typescript": "^5.0.0",
|
|
71
75
|
"vite": "^6.0.0"
|
|
72
76
|
}
|