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/runtime.js CHANGED
@@ -48,18 +48,14 @@ function buildIslandMap(payload) {
48
48
  return map;
49
49
  }
50
50
 
51
- // src/runtime.ts
52
- var dispatch = (name, detail) => document.dispatchEvent(new CustomEvent(name, { detail }));
53
- function media(query) {
54
- const m = window.matchMedia(query);
55
- return new Promise((resolve) => {
56
- if (m.matches)
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 visible(element, rootMargin, threshold, watch) {
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 interaction(element, events, watch) {
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 defer(ms) {
103
+ function waitDelay(ms) {
108
104
  return new Promise((resolve) => setTimeout(resolve, ms));
109
105
  }
110
- function idle(timeout) {
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 createIslandLogger(tagName, debug) {
123
- if (!debug)
124
- return SILENT_LOGGER;
125
- const msgs = [];
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
- note(msg) {
128
- msgs.push(msg);
249
+ dispatchLoad(detail) {
250
+ dispatch(deps.target, "islands:load", detail);
129
251
  },
130
- flush(summary) {
131
- if (msgs.length === 0) {
132
- console.log("[islands]", `<${tagName}> ${summary}`);
133
- } else {
134
- console.groupCollapsed(`[islands] <${tagName}> ${summary}`);
135
- for (const m of msgs)
136
- console.log(m);
137
- console.groupEnd();
138
- }
139
- msgs.length = 0;
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
- class DirectiveCancelledError extends Error {
145
- constructor() {
146
- super("[islands] directive cancelled: element removed from DOM");
147
- this.name = "DirectiveCancelledError";
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 = createIslandLogger(tagName, debug);
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
- dispatch("islands:load", {
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
- dispatch("islands:error", { tag: tagName, error: err, attempt });
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) => handleOutcome({ kind: "directive-error", attrName, err });
403
- if (resolvedDirectives?.size) {
404
- const matched = [];
405
- for (const [attrName, directiveFn] of resolvedDirectives) {
406
- const value = el.getAttribute(attrName);
407
- if (value !== null)
408
- matched.push([attrName, directiveFn, value]);
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
- if (applyCustomDirectives(tagName, el, matched, run, handleDirectiveError, log))
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 (debug)
452
- console.groupCollapsed(`[islands] ready — ${islandMap.size} island(s)`);
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
- if (debug)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-shopify-theme-islands",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Vite plugin for island architecture in Shopify themes",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.10",
@@ -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.0"
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/runtime.ts — directive owns the `run()` call path
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/runtime.ts — `interaction()` built-in handles the hover/touch/focus pattern
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/runtime.ts — `let remaining = matched.length`
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/index.ts — validateOptions duplicate and built-in conflict checks
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(def.entrypoint)` throws on null
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/runtime.ts — built-in awaits precede `if (customDirectives?.size)` block
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/runtime.ts — `if (fired || aborted) return Promise.resolve()`
287
+ Source: src/directive-orchestration.ts — `if (fired || aborted) return Promise.resolve()`