vite-plugin-shopify-theme-islands 1.2.0 → 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/config-policy.d.ts +11 -0
- package/dist/directive-orchestration.d.ts +27 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +67 -6
- package/dist/index.d.ts +2 -65
- package/dist/index.js +117 -70
- package/dist/options.d.ts +64 -0
- package/dist/revive-bootstrap.d.ts +31 -0
- 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 +12 -9
- package/skills/directives/SKILL.md +20 -21
- package/skills/lifecycle/SKILL.md +8 -5
- package/skills/setup/SKILL.md +5 -3
- package/skills/writing-islands/SKILL.md +1 -1
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,12 +6,15 @@ 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
20
|
---
|
|
@@ -142,7 +145,7 @@ const myDirective: ClientDirective = (load, _opts, el) => {
|
|
|
142
145
|
|
|
143
146
|
No immediate error is thrown by default, so the island is silently never loaded unless you configure `directiveTimeout`.
|
|
144
147
|
|
|
145
|
-
Source: src/
|
|
148
|
+
Source: src/directive-orchestration.ts — matched custom directives own the `run()` call path
|
|
146
149
|
|
|
147
150
|
### HIGH Writing a custom directive for mouseenter/touchstart/focusin — use `client:interaction` instead
|
|
148
151
|
|
|
@@ -167,7 +170,7 @@ Correct:
|
|
|
167
170
|
|
|
168
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).
|
|
169
172
|
|
|
170
|
-
Source: src/
|
|
173
|
+
Source: src/directive-orchestration.ts — built-in interaction handling covers the hover/touch/focus pattern
|
|
171
174
|
|
|
172
175
|
### HIGH AND-latch: both matched directives must call `load()`
|
|
173
176
|
|
|
@@ -190,7 +193,7 @@ Correct:
|
|
|
190
193
|
|
|
191
194
|
With two matching custom directives, `remaining = 2`. Each `load()` call decrements it. The island activates only when `remaining === 0`.
|
|
192
195
|
|
|
193
|
-
Source: src/
|
|
196
|
+
Source: src/directive-orchestration.ts — `let remaining = matched.length`
|
|
194
197
|
|
|
195
198
|
### HIGH Duplicate custom directive names or collisions with built-ins fail plugin setup
|
|
196
199
|
|
|
@@ -222,7 +225,7 @@ shopifyThemeIslands({
|
|
|
222
225
|
|
|
223
226
|
Custom directive names must be unique and must not collide with any built-in directive name, including renamed built-ins.
|
|
224
227
|
|
|
225
|
-
Source: src/
|
|
228
|
+
Source: src/config-policy.ts — validateOptions() duplicate and built-in conflict checks
|
|
226
229
|
|
|
227
230
|
### HIGH Entrypoint path missing `./` prefix
|
|
228
231
|
|
|
@@ -246,7 +249,7 @@ Correct:
|
|
|
246
249
|
|
|
247
250
|
Custom directive entrypoints are resolved through Vite. Relative local files should usually use `./...`; unresolved entrypoints fail the build.
|
|
248
251
|
|
|
249
|
-
Source: src/index.ts — `this.resolve(
|
|
252
|
+
Source: src/index.ts — `this.resolve(entrypoint)` throws on null during revive bootstrap planning
|
|
250
253
|
|
|
251
254
|
### MEDIUM Custom directives run after all built-in directive awaits
|
|
252
255
|
|
|
@@ -259,7 +262,7 @@ Wrong expectation:
|
|
|
259
262
|
|
|
260
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.
|
|
261
264
|
|
|
262
|
-
Source: src/
|
|
265
|
+
Source: src/directive-orchestration.ts — runBuiltIns() completes before runCustomDirectives()
|
|
263
266
|
|
|
264
267
|
### MEDIUM Calling `load()` multiple times has no effect after the first
|
|
265
268
|
|
|
@@ -281,4 +284,4 @@ const retryDirective: ClientDirective = (load, _opts, el) => {
|
|
|
281
284
|
|
|
282
285
|
The `loadOnce` wrapper ignores all calls after the first (`fired` guard). Use `{ once: true }` on event listeners to avoid unnecessary calls.
|
|
283
286
|
|
|
284
|
-
Source: src/
|
|
287
|
+
Source: src/directive-orchestration.ts — `if (fired || aborted) return Promise.resolve()`
|