vite-plugin-shopify-theme-islands 1.2.1 → 1.2.2
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/dist/directive-orchestration.d.ts +27 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +67 -6
- package/dist/runtime-surface.d.ts +20 -0
- package/dist/runtime.js +226 -166
- package/package.json +1 -1
- package/skills/custom-directives/SKILL.md +11 -9
- package/skills/directives/SKILL.md +19 -21
- package/skills/lifecycle/SKILL.md +6 -4
- package/skills/setup/SKILL.md +1 -1
- package/skills/writing-islands/SKILL.md +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClientDirective, NormalizedReviveOptions } from "./contract.js";
|
|
2
|
+
import type { RuntimeLogger } from "./runtime-surface.js";
|
|
3
|
+
export interface DirectiveWaiters {
|
|
4
|
+
waitVisible(element: Element, rootMargin: string, threshold: number, watch: (el: Element, cancel: () => void) => () => void): Promise<void>;
|
|
5
|
+
waitMedia(query: string): Promise<void>;
|
|
6
|
+
waitIdle(timeout: number): Promise<void>;
|
|
7
|
+
waitDelay(ms: number): Promise<void>;
|
|
8
|
+
waitInteraction(element: Element, events: string[], watch: (el: Element, cancel: () => void) => () => void): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface DirectiveRunContext {
|
|
11
|
+
tagName: string;
|
|
12
|
+
element: HTMLElement;
|
|
13
|
+
directives: NormalizedReviveOptions["directives"];
|
|
14
|
+
customDirectives?: Map<string, ClientDirective>;
|
|
15
|
+
directiveTimeout: number;
|
|
16
|
+
watchCancellable: (el: Element, cancel: () => void) => () => void;
|
|
17
|
+
log: RuntimeLogger;
|
|
18
|
+
run: () => Promise<void>;
|
|
19
|
+
onError(attrName: string, err: unknown): void;
|
|
20
|
+
}
|
|
21
|
+
export interface DirectiveOrchestrator {
|
|
22
|
+
run(ctx: DirectiveRunContext): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export declare class DirectiveCancelledError extends Error {
|
|
25
|
+
constructor();
|
|
26
|
+
}
|
|
27
|
+
export declare function createDirectiveOrchestrator(waiters?: DirectiveWaiters): DirectiveOrchestrator;
|
package/dist/events.d.ts
CHANGED
package/dist/events.js
CHANGED
|
@@ -1,13 +1,74 @@
|
|
|
1
|
+
// src/runtime-surface.ts
|
|
2
|
+
var SILENT_LOGGER = {
|
|
3
|
+
note() {},
|
|
4
|
+
flush() {}
|
|
5
|
+
};
|
|
6
|
+
function addListener(target, name, handler) {
|
|
7
|
+
const listener = (event) => handler(event.detail);
|
|
8
|
+
target.addEventListener(name, listener);
|
|
9
|
+
return () => target.removeEventListener(name, listener);
|
|
10
|
+
}
|
|
11
|
+
function dispatch(target, name, detail) {
|
|
12
|
+
target.dispatchEvent(new CustomEvent(name, { detail }));
|
|
13
|
+
}
|
|
14
|
+
function createRuntimeSurface(deps) {
|
|
15
|
+
return {
|
|
16
|
+
dispatchLoad(detail) {
|
|
17
|
+
dispatch(deps.target, "islands:load", detail);
|
|
18
|
+
},
|
|
19
|
+
dispatchError(detail) {
|
|
20
|
+
dispatch(deps.target, "islands:error", detail);
|
|
21
|
+
},
|
|
22
|
+
onLoad(handler) {
|
|
23
|
+
return addListener(deps.target, "islands:load", handler);
|
|
24
|
+
},
|
|
25
|
+
onError(handler) {
|
|
26
|
+
return addListener(deps.target, "islands:error", handler);
|
|
27
|
+
},
|
|
28
|
+
createLogger(tagName, debug) {
|
|
29
|
+
if (!debug)
|
|
30
|
+
return SILENT_LOGGER;
|
|
31
|
+
const msgs = [];
|
|
32
|
+
return {
|
|
33
|
+
note(msg) {
|
|
34
|
+
msgs.push(msg);
|
|
35
|
+
},
|
|
36
|
+
flush(summary) {
|
|
37
|
+
if (msgs.length === 0) {
|
|
38
|
+
deps.console.log("[islands]", `<${tagName}> ${summary}`);
|
|
39
|
+
} else {
|
|
40
|
+
deps.console.groupCollapsed(`[islands] <${tagName}> ${summary}`);
|
|
41
|
+
for (const msg of msgs)
|
|
42
|
+
deps.console.log(msg);
|
|
43
|
+
deps.console.groupEnd();
|
|
44
|
+
}
|
|
45
|
+
msgs.length = 0;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
beginReadyLog(islandCount, debug) {
|
|
50
|
+
if (!debug)
|
|
51
|
+
return () => {};
|
|
52
|
+
deps.console.groupCollapsed(`[islands] ready — ${islandCount} island(s)`);
|
|
53
|
+
return () => deps.console.groupEnd();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var runtimeSurface;
|
|
58
|
+
function getRuntimeSurface() {
|
|
59
|
+
runtimeSurface ??= createRuntimeSurface({
|
|
60
|
+
target: document,
|
|
61
|
+
console
|
|
62
|
+
});
|
|
63
|
+
return runtimeSurface;
|
|
64
|
+
}
|
|
65
|
+
|
|
1
66
|
// src/events.ts
|
|
2
67
|
function onIslandLoad(handler) {
|
|
3
|
-
|
|
4
|
-
document.addEventListener("islands:load", listener);
|
|
5
|
-
return () => document.removeEventListener("islands:load", listener);
|
|
68
|
+
return getRuntimeSurface().onLoad(handler);
|
|
6
69
|
}
|
|
7
70
|
function onIslandError(handler) {
|
|
8
|
-
|
|
9
|
-
document.addEventListener("islands:error", listener);
|
|
10
|
-
return () => document.removeEventListener("islands:error", listener);
|
|
71
|
+
return getRuntimeSurface().onError(handler);
|
|
11
72
|
}
|
|
12
73
|
export {
|
|
13
74
|
onIslandLoad,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IslandErrorDetail, IslandLoadDetail } from "./contract.js";
|
|
2
|
+
export interface RuntimeLogger {
|
|
3
|
+
note(msg: string): void;
|
|
4
|
+
flush(summary: string): void;
|
|
5
|
+
}
|
|
6
|
+
export interface RuntimeSurface {
|
|
7
|
+
dispatchLoad(detail: IslandLoadDetail): void;
|
|
8
|
+
dispatchError(detail: IslandErrorDetail): void;
|
|
9
|
+
onLoad(handler: (detail: IslandLoadDetail) => void): () => void;
|
|
10
|
+
onError(handler: (detail: IslandErrorDetail) => void): () => void;
|
|
11
|
+
createLogger(tagName: string, debug: boolean): RuntimeLogger;
|
|
12
|
+
beginReadyLog(islandCount: number, debug: boolean): () => void;
|
|
13
|
+
}
|
|
14
|
+
interface RuntimeSurfaceDeps {
|
|
15
|
+
target: Document;
|
|
16
|
+
console: Pick<Console, "log" | "groupCollapsed" | "groupEnd">;
|
|
17
|
+
}
|
|
18
|
+
export declare function createRuntimeSurface(deps: RuntimeSurfaceDeps): RuntimeSurface;
|
|
19
|
+
export declare function getRuntimeSurface(): RuntimeSurface;
|
|
20
|
+
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -48,18 +48,14 @@ function buildIslandMap(payload) {
|
|
|
48
48
|
return map;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// src/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
resolve();
|
|
58
|
-
else
|
|
59
|
-
m.addEventListener("change", () => resolve(), { once: true });
|
|
60
|
-
});
|
|
51
|
+
// src/directive-orchestration.ts
|
|
52
|
+
class DirectiveCancelledError extends Error {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("[islands] directive cancelled: element removed from DOM");
|
|
55
|
+
this.name = "DirectiveCancelledError";
|
|
56
|
+
}
|
|
61
57
|
}
|
|
62
|
-
function
|
|
58
|
+
function waitVisible(element, rootMargin, threshold, watch) {
|
|
63
59
|
return new Promise((resolve, reject) => {
|
|
64
60
|
let settled = false;
|
|
65
61
|
let unwatch = () => {};
|
|
@@ -80,7 +76,7 @@ function visible(element, rootMargin, threshold, watch) {
|
|
|
80
76
|
unwatch = watch(element, () => finish(() => reject(new DirectiveCancelledError)));
|
|
81
77
|
});
|
|
82
78
|
}
|
|
83
|
-
function
|
|
79
|
+
function waitInteraction(element, events, watch) {
|
|
84
80
|
return new Promise((resolve, reject) => {
|
|
85
81
|
let settled = false;
|
|
86
82
|
let unwatch = () => {};
|
|
@@ -104,10 +100,10 @@ function interaction(element, events, watch) {
|
|
|
104
100
|
unwatch = watch(element, () => finish(() => reject(new DirectiveCancelledError)));
|
|
105
101
|
});
|
|
106
102
|
}
|
|
107
|
-
function
|
|
103
|
+
function waitDelay(ms) {
|
|
108
104
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
105
|
}
|
|
110
|
-
function
|
|
106
|
+
function waitIdle(timeout) {
|
|
111
107
|
return new Promise((resolve) => {
|
|
112
108
|
if ("requestIdleCallback" in window)
|
|
113
109
|
window.requestIdleCallback(() => resolve(), { timeout });
|
|
@@ -115,38 +111,192 @@ function idle(timeout) {
|
|
|
115
111
|
setTimeout(resolve, timeout);
|
|
116
112
|
});
|
|
117
113
|
}
|
|
114
|
+
function waitMedia(query) {
|
|
115
|
+
const m = window.matchMedia(query);
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
if (m.matches)
|
|
118
|
+
resolve();
|
|
119
|
+
else
|
|
120
|
+
m.addEventListener("change", () => resolve(), { once: true });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function createDirectiveOrchestrator(waiters = {
|
|
124
|
+
waitVisible,
|
|
125
|
+
waitMedia,
|
|
126
|
+
waitIdle,
|
|
127
|
+
waitDelay,
|
|
128
|
+
waitInteraction
|
|
129
|
+
}) {
|
|
130
|
+
async function runBuiltIns(ctx) {
|
|
131
|
+
const { tagName, element: el, directives, log, watchCancellable } = ctx;
|
|
132
|
+
const visibleAttr = directives.visible.attribute;
|
|
133
|
+
if (el.getAttribute(visibleAttr) !== null) {
|
|
134
|
+
log.note(`waiting for ${visibleAttr}`);
|
|
135
|
+
await waiters.waitVisible(el, el.getAttribute(visibleAttr) || directives.visible.rootMargin, directives.visible.threshold, watchCancellable);
|
|
136
|
+
}
|
|
137
|
+
const query = el.getAttribute(directives.media.attribute);
|
|
138
|
+
if (query === "") {
|
|
139
|
+
console.warn(`[islands] <${tagName}> ${directives.media.attribute} has no value — media check skipped, island will load immediately`);
|
|
140
|
+
} else if (query) {
|
|
141
|
+
log.note(`waiting for ${directives.media.attribute}="${query}"`);
|
|
142
|
+
await waiters.waitMedia(query);
|
|
143
|
+
}
|
|
144
|
+
const idleAttr = el.getAttribute(directives.idle.attribute);
|
|
145
|
+
if (idleAttr !== null) {
|
|
146
|
+
const raw = parseInt(idleAttr, 10);
|
|
147
|
+
const elTimeout = Number.isNaN(raw) ? directives.idle.timeout : raw;
|
|
148
|
+
log.note(`waiting for ${directives.idle.attribute} (${elTimeout}ms)`);
|
|
149
|
+
await waiters.waitIdle(elTimeout);
|
|
150
|
+
}
|
|
151
|
+
const deferAttr = el.getAttribute(directives.defer.attribute);
|
|
152
|
+
if (deferAttr !== null) {
|
|
153
|
+
const msParsed = parseInt(deferAttr, 10);
|
|
154
|
+
if (deferAttr !== "" && Number.isNaN(msParsed)) {
|
|
155
|
+
console.warn(`[islands] <${tagName}> invalid ${directives.defer.attribute} value "${deferAttr}" — using default ${directives.defer.delay}ms`);
|
|
156
|
+
}
|
|
157
|
+
const ms = Number.isNaN(msParsed) ? directives.defer.delay : msParsed;
|
|
158
|
+
log.note(`waiting for ${directives.defer.attribute} (${ms}ms)`);
|
|
159
|
+
await waiters.waitDelay(ms);
|
|
160
|
+
}
|
|
161
|
+
const interactionAttr = el.getAttribute(directives.interaction.attribute);
|
|
162
|
+
if (interactionAttr !== null) {
|
|
163
|
+
let events = directives.interaction.events;
|
|
164
|
+
if (interactionAttr) {
|
|
165
|
+
const tokens = interactionAttr.split(/\s+/).filter(Boolean);
|
|
166
|
+
if (tokens.length > 0)
|
|
167
|
+
events = tokens;
|
|
168
|
+
else {
|
|
169
|
+
console.warn(`[islands] <${tagName}> ${directives.interaction.attribute} has no valid event tokens — using default events`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
log.note(`waiting for ${directives.interaction.attribute} (${events.join(", ")})`);
|
|
173
|
+
await waiters.waitInteraction(el, events, watchCancellable);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function runCustomDirectives(ctx) {
|
|
177
|
+
const matched = [];
|
|
178
|
+
if (ctx.customDirectives) {
|
|
179
|
+
for (const [attrName, directiveFn] of ctx.customDirectives) {
|
|
180
|
+
const value = ctx.element.getAttribute(attrName);
|
|
181
|
+
if (value !== null)
|
|
182
|
+
matched.push([attrName, directiveFn, value]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (matched.length === 0)
|
|
186
|
+
return false;
|
|
187
|
+
const attrNames = matched.map(([attrName]) => attrName).join(", ");
|
|
188
|
+
ctx.log.flush(`dispatching to custom directive${matched.length === 1 ? "" : "s"} ${attrNames}`);
|
|
189
|
+
let remaining = matched.length;
|
|
190
|
+
let fired = false;
|
|
191
|
+
let aborted = false;
|
|
192
|
+
let timer;
|
|
193
|
+
const loadOnce = () => {
|
|
194
|
+
if (fired || aborted)
|
|
195
|
+
return Promise.resolve();
|
|
196
|
+
if (--remaining === 0) {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
fired = true;
|
|
199
|
+
return ctx.run();
|
|
200
|
+
}
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
};
|
|
203
|
+
if (ctx.directiveTimeout > 0) {
|
|
204
|
+
timer = setTimeout(() => {
|
|
205
|
+
if (fired || aborted)
|
|
206
|
+
return;
|
|
207
|
+
aborted = true;
|
|
208
|
+
ctx.onError(attrNames, new Error(`[islands] Custom directive timed out after ${ctx.directiveTimeout}ms for <${ctx.tagName}>`));
|
|
209
|
+
}, ctx.directiveTimeout);
|
|
210
|
+
}
|
|
211
|
+
for (const [attrName, directiveFn, value] of matched) {
|
|
212
|
+
try {
|
|
213
|
+
Promise.resolve(directiveFn(loadOnce, { name: attrName, value }, ctx.element)).catch((err) => {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
aborted = true;
|
|
216
|
+
ctx.onError(attrName, err);
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
aborted = true;
|
|
221
|
+
ctx.onError(attrName, err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
async run(ctx) {
|
|
228
|
+
await runBuiltIns(ctx);
|
|
229
|
+
return runCustomDirectives(ctx);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/runtime-surface.ts
|
|
118
235
|
var SILENT_LOGGER = {
|
|
119
236
|
note() {},
|
|
120
237
|
flush() {}
|
|
121
238
|
};
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
239
|
+
function addListener(target, name, handler) {
|
|
240
|
+
const listener = (event) => handler(event.detail);
|
|
241
|
+
target.addEventListener(name, listener);
|
|
242
|
+
return () => target.removeEventListener(name, listener);
|
|
243
|
+
}
|
|
244
|
+
function dispatch(target, name, detail) {
|
|
245
|
+
target.dispatchEvent(new CustomEvent(name, { detail }));
|
|
246
|
+
}
|
|
247
|
+
function createRuntimeSurface(deps) {
|
|
126
248
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
249
|
+
dispatchLoad(detail) {
|
|
250
|
+
dispatch(deps.target, "islands:load", detail);
|
|
129
251
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
252
|
+
dispatchError(detail) {
|
|
253
|
+
dispatch(deps.target, "islands:error", detail);
|
|
254
|
+
},
|
|
255
|
+
onLoad(handler) {
|
|
256
|
+
return addListener(deps.target, "islands:load", handler);
|
|
257
|
+
},
|
|
258
|
+
onError(handler) {
|
|
259
|
+
return addListener(deps.target, "islands:error", handler);
|
|
260
|
+
},
|
|
261
|
+
createLogger(tagName, debug) {
|
|
262
|
+
if (!debug)
|
|
263
|
+
return SILENT_LOGGER;
|
|
264
|
+
const msgs = [];
|
|
265
|
+
return {
|
|
266
|
+
note(msg) {
|
|
267
|
+
msgs.push(msg);
|
|
268
|
+
},
|
|
269
|
+
flush(summary) {
|
|
270
|
+
if (msgs.length === 0) {
|
|
271
|
+
deps.console.log("[islands]", `<${tagName}> ${summary}`);
|
|
272
|
+
} else {
|
|
273
|
+
deps.console.groupCollapsed(`[islands] <${tagName}> ${summary}`);
|
|
274
|
+
for (const msg of msgs)
|
|
275
|
+
deps.console.log(msg);
|
|
276
|
+
deps.console.groupEnd();
|
|
277
|
+
}
|
|
278
|
+
msgs.length = 0;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
beginReadyLog(islandCount, debug) {
|
|
283
|
+
if (!debug)
|
|
284
|
+
return () => {};
|
|
285
|
+
deps.console.groupCollapsed(`[islands] ready — ${islandCount} island(s)`);
|
|
286
|
+
return () => deps.console.groupEnd();
|
|
140
287
|
}
|
|
141
288
|
};
|
|
142
289
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
290
|
+
var runtimeSurface;
|
|
291
|
+
function getRuntimeSurface() {
|
|
292
|
+
runtimeSurface ??= createRuntimeSurface({
|
|
293
|
+
target: document,
|
|
294
|
+
console
|
|
295
|
+
});
|
|
296
|
+
return runtimeSurface;
|
|
149
297
|
}
|
|
298
|
+
|
|
299
|
+
// src/runtime.ts
|
|
150
300
|
function isRevivePayload(v) {
|
|
151
301
|
return typeof v === "object" && v !== null && "islands" in v && !Array.isArray(v);
|
|
152
302
|
}
|
|
@@ -213,6 +363,7 @@ function createIslandRegistry(opts) {
|
|
|
213
363
|
};
|
|
214
364
|
}
|
|
215
365
|
function revive(islandsOrPayload, options, customDirectives) {
|
|
366
|
+
const runtimeSurface2 = getRuntimeSurface();
|
|
216
367
|
const payload = isRevivePayload(islandsOrPayload) ? islandsOrPayload : { islands: islandsOrPayload, options, customDirectives };
|
|
217
368
|
const opts = normalizeReviveOptions(payload.options);
|
|
218
369
|
const islandMap = buildIslandMap(payload);
|
|
@@ -222,17 +373,13 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
222
373
|
const attrIdle = opts.directives.idle.attribute;
|
|
223
374
|
const attrDefer = opts.directives.defer.attribute;
|
|
224
375
|
const attrInteraction = opts.directives.interaction.attribute;
|
|
225
|
-
const interactionEvents = opts.directives.interaction.events;
|
|
226
|
-
const rootMargin = opts.directives.visible.rootMargin;
|
|
227
|
-
const threshold = opts.directives.visible.threshold;
|
|
228
|
-
const idleTimeout = opts.directives.idle.timeout;
|
|
229
|
-
const deferDelay = opts.directives.defer.delay;
|
|
230
376
|
const debug = opts.debug;
|
|
231
377
|
const directiveTimeout = opts.directiveTimeout;
|
|
232
378
|
const registry = createIslandRegistry({
|
|
233
379
|
retries: opts.retry.retries,
|
|
234
380
|
retryDelay: opts.retry.delay
|
|
235
381
|
});
|
|
382
|
+
const directiveOrchestrator = createDirectiveOrchestrator();
|
|
236
383
|
const customElementFilter = {
|
|
237
384
|
acceptNode: (node) => {
|
|
238
385
|
const tag = node.tagName;
|
|
@@ -244,107 +391,6 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
244
391
|
return NodeFilter.FILTER_ACCEPT;
|
|
245
392
|
}
|
|
246
393
|
};
|
|
247
|
-
function makeDirectiveOutcomeHandler(tagName) {
|
|
248
|
-
return (outcome) => {
|
|
249
|
-
if (outcome.kind === "builtin-catch" && outcome.err instanceof DirectiveCancelledError) {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const err = outcome.err;
|
|
253
|
-
if (outcome.kind === "directive-error") {
|
|
254
|
-
console.error(`[islands] Custom directive ${outcome.attrName} failed for <${tagName}>:`, err);
|
|
255
|
-
} else {
|
|
256
|
-
console.error(`[islands] Built-in directive failed for <${tagName}>:`, err);
|
|
257
|
-
}
|
|
258
|
-
dispatch("islands:error", { tag: tagName, error: err, attempt: 1 });
|
|
259
|
-
registry.evict(tagName);
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
async function applyBuiltInDirectives(tagName, el, log) {
|
|
263
|
-
const visibleAttr = el.getAttribute(attrVisible);
|
|
264
|
-
if (visibleAttr !== null) {
|
|
265
|
-
log.note(`waiting for ${attrVisible}`);
|
|
266
|
-
await visible(el, visibleAttr || rootMargin, threshold, registry.watchCancellable);
|
|
267
|
-
}
|
|
268
|
-
const query = el.getAttribute(attrMedia);
|
|
269
|
-
if (query === "") {
|
|
270
|
-
console.warn(`[islands] <${tagName}> ${attrMedia} has no value — media check skipped, island will load immediately`);
|
|
271
|
-
} else if (query) {
|
|
272
|
-
log.note(`waiting for ${attrMedia}="${query}"`);
|
|
273
|
-
await media(query);
|
|
274
|
-
}
|
|
275
|
-
const idleAttr = el.getAttribute(attrIdle);
|
|
276
|
-
if (idleAttr !== null) {
|
|
277
|
-
const raw = parseInt(idleAttr, 10);
|
|
278
|
-
const elTimeout = Number.isNaN(raw) ? idleTimeout : raw;
|
|
279
|
-
log.note(`waiting for ${attrIdle} (${elTimeout}ms)`);
|
|
280
|
-
await idle(elTimeout);
|
|
281
|
-
}
|
|
282
|
-
const d = el.getAttribute(attrDefer);
|
|
283
|
-
if (d !== null) {
|
|
284
|
-
const dMs = parseInt(d, 10);
|
|
285
|
-
if (d !== "" && Number.isNaN(dMs)) {
|
|
286
|
-
console.warn(`[islands] <${tagName}> invalid ${attrDefer} value "${d}" — using default ${deferDelay}ms`);
|
|
287
|
-
}
|
|
288
|
-
const ms = Number.isNaN(dMs) ? deferDelay : dMs;
|
|
289
|
-
log.note(`waiting for ${attrDefer} (${ms}ms)`);
|
|
290
|
-
await defer(ms);
|
|
291
|
-
}
|
|
292
|
-
const interactionAttr = el.getAttribute(attrInteraction);
|
|
293
|
-
if (interactionAttr !== null) {
|
|
294
|
-
let events = interactionEvents;
|
|
295
|
-
if (interactionAttr) {
|
|
296
|
-
const tokens = interactionAttr.split(/\s+/).filter(Boolean);
|
|
297
|
-
if (tokens.length > 0)
|
|
298
|
-
events = tokens;
|
|
299
|
-
else
|
|
300
|
-
console.warn(`[islands] <${tagName}> ${attrInteraction} has no valid event tokens — using default events`);
|
|
301
|
-
}
|
|
302
|
-
log.note(`waiting for ${attrInteraction} (${events.join(", ")})`);
|
|
303
|
-
await interaction(el, events, registry.watchCancellable);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function applyCustomDirectives(tagName, el, matched, run, handleDirectiveError, log) {
|
|
307
|
-
if (matched.length === 0)
|
|
308
|
-
return false;
|
|
309
|
-
const attrNames = matched.map(([a]) => a).join(", ");
|
|
310
|
-
log.flush(`dispatching to custom directive${matched.length === 1 ? "" : "s"} ${attrNames}`);
|
|
311
|
-
let remaining = matched.length;
|
|
312
|
-
let fired = false;
|
|
313
|
-
let aborted = false;
|
|
314
|
-
const loadOnce = () => {
|
|
315
|
-
if (fired || aborted)
|
|
316
|
-
return Promise.resolve();
|
|
317
|
-
if (--remaining === 0) {
|
|
318
|
-
clearTimeout(timer);
|
|
319
|
-
fired = true;
|
|
320
|
-
return run();
|
|
321
|
-
}
|
|
322
|
-
return Promise.resolve();
|
|
323
|
-
};
|
|
324
|
-
let timer;
|
|
325
|
-
if (directiveTimeout > 0) {
|
|
326
|
-
timer = setTimeout(() => {
|
|
327
|
-
if (fired || aborted)
|
|
328
|
-
return;
|
|
329
|
-
aborted = true;
|
|
330
|
-
handleDirectiveError(attrNames, new Error(`[islands] Custom directive timed out after ${directiveTimeout}ms for <${tagName}>`));
|
|
331
|
-
}, directiveTimeout);
|
|
332
|
-
}
|
|
333
|
-
for (const [attrName, directiveFn, value] of matched) {
|
|
334
|
-
try {
|
|
335
|
-
Promise.resolve(directiveFn(loadOnce, { name: attrName, value }, el)).catch((err) => {
|
|
336
|
-
clearTimeout(timer);
|
|
337
|
-
aborted = true;
|
|
338
|
-
handleDirectiveError(attrName, err);
|
|
339
|
-
});
|
|
340
|
-
} catch (err) {
|
|
341
|
-
clearTimeout(timer);
|
|
342
|
-
aborted = true;
|
|
343
|
-
handleDirectiveError(attrName, err);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
394
|
async function loadIsland(tagName, el, loader) {
|
|
349
395
|
if (debug && !registry.initialWalkComplete) {
|
|
350
396
|
const parts = [];
|
|
@@ -368,22 +414,14 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
368
414
|
if (parts.length > 0)
|
|
369
415
|
console.log("[islands]", `<${tagName}> waiting · ${parts.join(", ")}`);
|
|
370
416
|
}
|
|
371
|
-
const log =
|
|
372
|
-
const handleOutcome = makeDirectiveOutcomeHandler(tagName);
|
|
373
|
-
try {
|
|
374
|
-
await applyBuiltInDirectives(tagName, el, log);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
handleOutcome({ kind: "builtin-catch", err });
|
|
377
|
-
log.flush(err instanceof DirectiveCancelledError ? "aborted (element removed)" : "aborted (directive error)");
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
417
|
+
const log = runtimeSurface2.createLogger(tagName, debug);
|
|
380
418
|
const run = () => {
|
|
381
419
|
if (disconnected)
|
|
382
420
|
return Promise.resolve();
|
|
383
421
|
const t0 = performance.now();
|
|
384
422
|
return loader().then(() => {
|
|
385
423
|
const attempt = registry.settleSuccess(tagName);
|
|
386
|
-
|
|
424
|
+
runtimeSurface2.dispatchLoad({
|
|
387
425
|
tag: tagName,
|
|
388
426
|
duration: performance.now() - t0,
|
|
389
427
|
attempt
|
|
@@ -393,22 +431,41 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
393
431
|
}).catch((err) => {
|
|
394
432
|
console.error(`[islands] Failed to load <${tagName}>:`, err);
|
|
395
433
|
const { retryDelayMs, attempt } = registry.settleFailure(tagName);
|
|
396
|
-
|
|
434
|
+
runtimeSurface2.dispatchError({ tag: tagName, error: err, attempt });
|
|
397
435
|
if (retryDelayMs !== null) {
|
|
398
436
|
setTimeout(run, retryDelayMs);
|
|
399
437
|
}
|
|
400
438
|
});
|
|
401
439
|
};
|
|
402
|
-
const handleDirectiveError = (attrName, err) =>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
440
|
+
const handleDirectiveError = (attrName, err) => {
|
|
441
|
+
if (attrName === null && err instanceof DirectiveCancelledError)
|
|
442
|
+
return;
|
|
443
|
+
if (attrName !== null) {
|
|
444
|
+
console.error(`[islands] Custom directive ${attrName} failed for <${tagName}>:`, err);
|
|
445
|
+
} else {
|
|
446
|
+
console.error(`[islands] Built-in directive failed for <${tagName}>:`, err);
|
|
409
447
|
}
|
|
410
|
-
|
|
448
|
+
runtimeSurface2.dispatchError({ tag: tagName, error: err, attempt: 1 });
|
|
449
|
+
registry.evict(tagName);
|
|
450
|
+
};
|
|
451
|
+
try {
|
|
452
|
+
const matchedCustomDirectives = await directiveOrchestrator.run({
|
|
453
|
+
tagName,
|
|
454
|
+
element: el,
|
|
455
|
+
directives: opts.directives,
|
|
456
|
+
customDirectives: resolvedDirectives,
|
|
457
|
+
directiveTimeout,
|
|
458
|
+
watchCancellable: registry.watchCancellable,
|
|
459
|
+
log,
|
|
460
|
+
run,
|
|
461
|
+
onError: handleDirectiveError
|
|
462
|
+
});
|
|
463
|
+
if (matchedCustomDirectives)
|
|
411
464
|
return;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
handleDirectiveError(null, err);
|
|
467
|
+
log.flush(err instanceof DirectiveCancelledError ? "aborted (element removed)" : "aborted (directive error)");
|
|
468
|
+
return;
|
|
412
469
|
}
|
|
413
470
|
log.flush("triggered");
|
|
414
471
|
run();
|
|
@@ -447,16 +504,18 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
447
504
|
registry.cancelDetached();
|
|
448
505
|
handleAdditions(mutations);
|
|
449
506
|
});
|
|
507
|
+
let disconnected = false;
|
|
508
|
+
let initialized = false;
|
|
450
509
|
function init() {
|
|
451
|
-
if (
|
|
452
|
-
|
|
510
|
+
if (disconnected || initialized)
|
|
511
|
+
return;
|
|
512
|
+
initialized = true;
|
|
513
|
+
const endReadyLog = runtimeSurface2.beginReadyLog(islandMap.size, debug);
|
|
453
514
|
walk(document.body);
|
|
454
515
|
registry.markInitialWalkComplete();
|
|
455
|
-
|
|
456
|
-
console.groupEnd();
|
|
516
|
+
endReadyLog();
|
|
457
517
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
458
518
|
}
|
|
459
|
-
let disconnected = false;
|
|
460
519
|
if (document.readyState === "loading") {
|
|
461
520
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
|
462
521
|
} else {
|
|
@@ -464,6 +523,7 @@ function revive(islandsOrPayload, options, customDirectives) {
|
|
|
464
523
|
}
|
|
465
524
|
const disconnect = () => {
|
|
466
525
|
disconnected = true;
|
|
526
|
+
document.removeEventListener("DOMContentLoaded", init);
|
|
467
527
|
observer.disconnect();
|
|
468
528
|
};
|
|
469
529
|
return { disconnect };
|
package/package.json
CHANGED
|
@@ -6,15 +6,17 @@ description: >
|
|
|
6
6
|
multiple custom directives match the same element, all must call load() before
|
|
7
7
|
the island activates. Error handling — thrown errors, rejected promises, and
|
|
8
8
|
directiveTimeout expiry fire islands:error. Custom directives run after all
|
|
9
|
-
built-in conditions resolve.
|
|
9
|
+
built-in conditions resolve. Current matching, AND-latch, and timeout policy
|
|
10
|
+
are owned by src/directive-orchestration.ts.
|
|
10
11
|
type: core
|
|
11
12
|
library: vite-plugin-shopify-theme-islands
|
|
12
|
-
library_version: "1.2.
|
|
13
|
+
library_version: "1.2.2"
|
|
13
14
|
sources:
|
|
14
15
|
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
16
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/directive-orchestration.ts
|
|
17
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/config-policy.ts
|
|
15
18
|
- Rees1993/vite-plugin-shopify-theme-islands:src/index.ts
|
|
16
19
|
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts
|
|
17
|
-
- Rees1993/vite-plugin-shopify-theme-islands:src/config-policy.ts
|
|
18
20
|
---
|
|
19
21
|
|
|
20
22
|
## Setup
|
|
@@ -143,7 +145,7 @@ const myDirective: ClientDirective = (load, _opts, el) => {
|
|
|
143
145
|
|
|
144
146
|
No immediate error is thrown by default, so the island is silently never loaded unless you configure `directiveTimeout`.
|
|
145
147
|
|
|
146
|
-
Source: src/
|
|
148
|
+
Source: src/directive-orchestration.ts — matched custom directives own the `run()` call path
|
|
147
149
|
|
|
148
150
|
### HIGH Writing a custom directive for mouseenter/touchstart/focusin — use `client:interaction` instead
|
|
149
151
|
|
|
@@ -168,7 +170,7 @@ Correct:
|
|
|
168
170
|
|
|
169
171
|
`client:interaction` is a built-in directive that handles `mouseenter`, `touchstart`, and `focusin`. Custom directives are for conditions the built-ins cannot express (e.g. URL hash matching, network conditions, feature flags).
|
|
170
172
|
|
|
171
|
-
Source: src/
|
|
173
|
+
Source: src/directive-orchestration.ts — built-in interaction handling covers the hover/touch/focus pattern
|
|
172
174
|
|
|
173
175
|
### HIGH AND-latch: both matched directives must call `load()`
|
|
174
176
|
|
|
@@ -191,7 +193,7 @@ Correct:
|
|
|
191
193
|
|
|
192
194
|
With two matching custom directives, `remaining = 2`. Each `load()` call decrements it. The island activates only when `remaining === 0`.
|
|
193
195
|
|
|
194
|
-
Source: src/
|
|
196
|
+
Source: src/directive-orchestration.ts — `let remaining = matched.length`
|
|
195
197
|
|
|
196
198
|
### HIGH Duplicate custom directive names or collisions with built-ins fail plugin setup
|
|
197
199
|
|
|
@@ -247,7 +249,7 @@ Correct:
|
|
|
247
249
|
|
|
248
250
|
Custom directive entrypoints are resolved through Vite. Relative local files should usually use `./...`; unresolved entrypoints fail the build.
|
|
249
251
|
|
|
250
|
-
Source: src/index.ts — `this.resolve(
|
|
252
|
+
Source: src/index.ts — `this.resolve(entrypoint)` throws on null during revive bootstrap planning
|
|
251
253
|
|
|
252
254
|
### MEDIUM Custom directives run after all built-in directive awaits
|
|
253
255
|
|
|
@@ -260,7 +262,7 @@ Wrong expectation:
|
|
|
260
262
|
|
|
261
263
|
The runtime awaits built-ins in order (`visible → media → idle → defer → interaction`) first, then passes control to matched custom directives. Custom directives cannot short-circuit or replace built-in awaits.
|
|
262
264
|
|
|
263
|
-
Source: src/
|
|
265
|
+
Source: src/directive-orchestration.ts — runBuiltIns() completes before runCustomDirectives()
|
|
264
266
|
|
|
265
267
|
### MEDIUM Calling `load()` multiple times has no effect after the first
|
|
266
268
|
|
|
@@ -282,4 +284,4 @@ const retryDirective: ClientDirective = (load, _opts, el) => {
|
|
|
282
284
|
|
|
283
285
|
The `loadOnce` wrapper ignores all calls after the first (`fired` guard). Use `{ once: true }` on event listeners to avoid unnecessary calls.
|
|
284
286
|
|
|
285
|
-
Source: src/
|
|
287
|
+
Source: src/directive-orchestration.ts — `if (fired || aborted) return Promise.resolve()`
|
|
@@ -4,16 +4,19 @@ description: >
|
|
|
4
4
|
Built-in client directives: client:visible (IntersectionObserver, rootMargin),
|
|
5
5
|
client:media (matchMedia query), client:idle (requestIdleCallback),
|
|
6
6
|
client:defer (setTimeout delay), client:interaction (mouseenter/touchstart/focusin).
|
|
7
|
-
Directives resolve sequentially — visible → media → idle → defer →
|
|
8
|
-
Per-element value overrides. Empty client:media
|
|
9
|
-
client:interaction values warn and fall back to
|
|
7
|
+
Directives resolve sequentially — visible → media → idle → defer →
|
|
8
|
+
interaction → custom. Per-element value overrides. Empty client:media
|
|
9
|
+
warning. Whitespace-only client:interaction values warn and fall back to
|
|
10
|
+
default events. Current directive sequencing and custom-directive latching
|
|
11
|
+
are owned by src/directive-orchestration.ts.
|
|
10
12
|
type: core
|
|
11
13
|
library: vite-plugin-shopify-theme-islands
|
|
12
|
-
library_version: "1.2.
|
|
14
|
+
library_version: "1.2.2"
|
|
13
15
|
sources:
|
|
16
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/directive-orchestration.ts
|
|
14
17
|
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts
|
|
15
|
-
- Rees1993/vite-plugin-shopify-theme-islands:src/
|
|
16
|
-
- Rees1993/vite-plugin-shopify-theme-islands:src/
|
|
18
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
19
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/config-policy.ts
|
|
17
20
|
---
|
|
18
21
|
|
|
19
22
|
## Setup
|
|
@@ -50,10 +53,7 @@ Directives resolve in a fixed order: `visible → media → idle → defer → i
|
|
|
50
53
|
<mega-menu client:visible client:interaction></mega-menu>
|
|
51
54
|
|
|
52
55
|
<!-- Loads when visible AND the media query matches -->
|
|
53
|
-
<product-recommendations
|
|
54
|
-
client:visible
|
|
55
|
-
client:media="(min-width: 768px)"
|
|
56
|
-
></product-recommendations>
|
|
56
|
+
<product-recommendations client:visible client:media="(min-width: 768px)"></product-recommendations>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Combined directives are AND-latched. The island loads only after every condition resolves. There is no OR mode.
|
|
@@ -100,7 +100,7 @@ An empty `client:defer` attribute is NOT zero — it falls back to the configure
|
|
|
100
100
|
|
|
101
101
|
An empty `client:interaction` attribute uses the configured default events with no warning. A whitespace-only value such as `client:interaction=" "` emits a warning and still falls back to the default events.
|
|
102
102
|
|
|
103
|
-
Source: src/
|
|
103
|
+
Source: src/directive-orchestration.ts — interaction token parsing and fallback warning
|
|
104
104
|
|
|
105
105
|
### Changing built-in directive defaults globally
|
|
106
106
|
|
|
@@ -118,8 +118,7 @@ shopifyThemeIslands({
|
|
|
118
118
|
### Removed elements abort waiting directives silently
|
|
119
119
|
|
|
120
120
|
```html
|
|
121
|
-
<hero-banner client:visible></hero-banner>
|
|
122
|
-
<cart-flyout client:interaction></cart-flyout>
|
|
121
|
+
<hero-banner client:visible></hero-banner> <cart-flyout client:interaction></cart-flyout>
|
|
123
122
|
```
|
|
124
123
|
|
|
125
124
|
If either element is removed from the DOM before its directive resolves, the runtime cancels that activation attempt and does not dispatch `islands:error`. This is expected teardown behavior, not a load failure.
|
|
@@ -142,7 +141,7 @@ Correct:
|
|
|
142
141
|
|
|
143
142
|
An empty `client:media` value emits a console warning and skips the media check — the island loads immediately. Provide a valid media query string.
|
|
144
143
|
|
|
145
|
-
Source: src/
|
|
144
|
+
Source: src/directive-orchestration.ts — `if (query === "")` branch
|
|
146
145
|
|
|
147
146
|
### MEDIUM Whitespace-only `client:interaction` value warns and falls back
|
|
148
147
|
|
|
@@ -164,7 +163,7 @@ Correct:
|
|
|
164
163
|
|
|
165
164
|
Whitespace-only values are not treated the same as an empty attribute. The runtime warns and falls back to the configured default events.
|
|
166
165
|
|
|
167
|
-
Source: src/
|
|
166
|
+
Source: src/directive-orchestration.ts — `interactionAttr.split(/\s+/).filter(Boolean)`
|
|
168
167
|
|
|
169
168
|
### HIGH Multiple directives are AND, not OR
|
|
170
169
|
|
|
@@ -184,7 +183,7 @@ Correct understanding:
|
|
|
184
183
|
|
|
185
184
|
The runtime awaits each directive sequentially. There is no way to express OR semantics with built-in directives — use a custom directive for that.
|
|
186
185
|
|
|
187
|
-
Source: src/
|
|
186
|
+
Source: src/directive-orchestration.ts — runBuiltIns() sequential awaits
|
|
188
187
|
|
|
189
188
|
### MEDIUM `client:defer` without value ≠ immediate load
|
|
190
189
|
|
|
@@ -204,7 +203,7 @@ Correct:
|
|
|
204
203
|
|
|
205
204
|
`client:defer` with no value uses the global `defer.delay` default (3000ms). `parseInt("", 10)` produces `NaN`, which the runtime replaces with the configured default.
|
|
206
205
|
|
|
207
|
-
Source: src/
|
|
206
|
+
Source: src/directive-orchestration.ts — defer parsing and fallback to directives.defer.delay
|
|
208
207
|
|
|
209
208
|
### MEDIUM Per-element visible value replaces rootMargin, not adds to it
|
|
210
209
|
|
|
@@ -224,15 +223,14 @@ Correct:
|
|
|
224
223
|
|
|
225
224
|
The attribute value is passed directly to `IntersectionObserver` as `rootMargin`, fully replacing the global default.
|
|
226
225
|
|
|
227
|
-
Source: src/
|
|
226
|
+
Source: src/directive-orchestration.ts — visible attribute value replaces directives.visible.rootMargin
|
|
228
227
|
|
|
229
228
|
### HIGH Directive attribute typo — island loads without condition
|
|
230
229
|
|
|
231
230
|
Wrong:
|
|
232
231
|
|
|
233
232
|
```html
|
|
234
|
-
<product-form client:visibled></product-form>
|
|
235
|
-
<product-form client:Visible></product-form>
|
|
233
|
+
<product-form client:visibled></product-form> <product-form client:Visible></product-form>
|
|
236
234
|
```
|
|
237
235
|
|
|
238
236
|
Correct:
|
|
@@ -243,7 +241,7 @@ Correct:
|
|
|
243
241
|
|
|
244
242
|
Directive attributes are case-sensitive. An unrecognised attribute is silently ignored — the island loads immediately as if no directive were set. No warning is emitted. Check for typos if an island activates earlier than expected.
|
|
245
243
|
|
|
246
|
-
Source: src/
|
|
244
|
+
Source: src/directive-orchestration.ts — built-ins read exact configured attribute names
|
|
247
245
|
|
|
248
246
|
### HIGH Agent uses default attribute name when developer has configured a custom one
|
|
249
247
|
|
|
@@ -8,12 +8,14 @@ description: >
|
|
|
8
8
|
duration (ms), and attempt (1-based). islands:error detail includes tag,
|
|
9
9
|
error, and attempt, including custom directive failures and directiveTimeout
|
|
10
10
|
expiry. disconnect() from the virtual module revive for SPA navigation
|
|
11
|
-
teardown
|
|
11
|
+
teardown, including before DOMContentLoaded — it now prevents init from ever
|
|
12
|
+
starting if called early.
|
|
12
13
|
type: core
|
|
13
14
|
library: vite-plugin-shopify-theme-islands
|
|
14
|
-
library_version: "1.2.
|
|
15
|
+
library_version: "1.2.2"
|
|
15
16
|
sources:
|
|
16
17
|
- Rees1993/vite-plugin-shopify-theme-islands:src/events.ts
|
|
18
|
+
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime-surface.ts
|
|
17
19
|
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
18
20
|
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts
|
|
19
21
|
- Rees1993/vite-plugin-shopify-theme-islands:src/revive-module.ts
|
|
@@ -86,7 +88,7 @@ import { disconnect } from "vite-plugin-shopify-theme-islands/revive";
|
|
|
86
88
|
disconnect();
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
`disconnect()` stops the MutationObserver and prevents new islands from activating. Call it before SPA page transitions to avoid activating islands from the previous page's DOM.
|
|
91
|
+
`disconnect()` stops the MutationObserver and prevents new islands from activating. If the runtime has not initialized yet because the document is still loading, `disconnect()` also unregisters the pending DOMContentLoaded startup listener so init never runs later. Call it before SPA page transitions to avoid activating islands from the previous page's DOM.
|
|
90
92
|
|
|
91
93
|
### Raw DOM events (when type augmentation is in scope)
|
|
92
94
|
|
|
@@ -171,7 +173,7 @@ onIslandError(({ tag, error, attempt }) => {
|
|
|
171
173
|
|
|
172
174
|
With `retry: { retries: 3 }`, a single island can fire `islands:error` up to 4 times before exhausting retries. Use `attempt` to distinguish the initial failure from retries.
|
|
173
175
|
|
|
174
|
-
Source: src/runtime.ts —
|
|
176
|
+
Source: src/runtime.ts — runtimeSurface.dispatchError(...) inside the loader failure path before retry check
|
|
175
177
|
|
|
176
178
|
### MEDIUM `islands:error` fires for custom directive failures too
|
|
177
179
|
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -10,7 +10,7 @@ description: >
|
|
|
10
10
|
directive timeout.
|
|
11
11
|
type: core
|
|
12
12
|
library: vite-plugin-shopify-theme-islands
|
|
13
|
-
library_version: "1.2.
|
|
13
|
+
library_version: "1.2.2"
|
|
14
14
|
sources:
|
|
15
15
|
- Rees1993/vite-plugin-shopify-theme-islands:src/index.ts
|
|
16
16
|
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
base class, and child island cascade behaviour.
|
|
9
9
|
type: core
|
|
10
10
|
library: vite-plugin-shopify-theme-islands
|
|
11
|
-
library_version: "1.2.
|
|
11
|
+
library_version: "1.2.2"
|
|
12
12
|
sources:
|
|
13
13
|
- Rees1993/vite-plugin-shopify-theme-islands:src/island.ts
|
|
14
14
|
- Rees1993/vite-plugin-shopify-theme-islands:src/discovery.ts
|