vite-plugin-shopify-theme-islands 1.3.0 → 1.3.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 +12 -2
- package/dist/discovery.d.ts +30 -0
- package/dist/index.js +121 -65
- package/dist/interaction-events.d.ts +6 -0
- package/dist/runtime.js +26 -4
- package/package.json +1 -1
- package/skills/custom-directives/SKILL.md +1 -1
- package/skills/directives/SKILL.md +40 -6
- package/skills/lifecycle/SKILL.md +5 -2
- package/skills/setup/SKILL.md +11 -3
- package/skills/writing-islands/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ import { disconnect } from "vite-plugin-shopify-theme-islands/revive";
|
|
|
44
44
|
disconnect();
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
If `disconnect()` is called before `DOMContentLoaded`, the runtime also cancels its pending startup listener so islands never initialize later against stale DOM.
|
|
48
|
+
|
|
47
49
|
## Writing islands
|
|
48
50
|
|
|
49
51
|
Two approaches — use either or both.
|
|
@@ -200,7 +202,7 @@ Loads the island when the user interacts with the element. Listens for `mouseent
|
|
|
200
202
|
</cart-flyout>
|
|
201
203
|
```
|
|
202
204
|
|
|
203
|
-
The attribute value overrides the events for that element only
|
|
205
|
+
The attribute value overrides the events for that element only:
|
|
204
206
|
|
|
205
207
|
```html
|
|
206
208
|
<!-- only mouseenter — touchstart and focusin are excluded -->
|
|
@@ -209,6 +211,10 @@ The attribute value overrides the events for that element only (space-separated
|
|
|
209
211
|
</cart-flyout>
|
|
210
212
|
```
|
|
211
213
|
|
|
214
|
+
In plugin config, `directives.interaction.events` is intentionally narrower than the raw HTML attribute surface. The typed config only accepts the curated package-owned set `mouseenter`, `touchstart`, and `focusin`, and rejects empty arrays.
|
|
215
|
+
|
|
216
|
+
Per-element `client:interaction="..."` values are also validated at runtime against that same curated set. Unsupported tokens log a warning and are ignored. If no supported tokens remain, the runtime logs a warning and falls back to the configured default interaction events instead of attaching unsupported listeners.
|
|
217
|
+
|
|
212
218
|
Combine with `client:visible` to avoid attaching listeners to off-screen elements. Because directives resolve sequentially, interaction listeners are only registered once the element has entered the viewport:
|
|
213
219
|
|
|
214
220
|
```html
|
|
@@ -362,7 +368,7 @@ shopifyThemeIslands({
|
|
|
362
368
|
},
|
|
363
369
|
interaction: {
|
|
364
370
|
attribute: "client:interaction", // HTML attribute name
|
|
365
|
-
events: ["mouseenter", "touchstart", "focusin"], //
|
|
371
|
+
events: ["mouseenter", "touchstart", "focusin"], // curated config events that trigger load
|
|
366
372
|
},
|
|
367
373
|
custom: [], // custom directives — see Custom directives above
|
|
368
374
|
},
|
|
@@ -380,6 +386,8 @@ shopifyThemeIslands({
|
|
|
380
386
|
});
|
|
381
387
|
```
|
|
382
388
|
|
|
389
|
+
For `directives.interaction.events`, supported config values are currently limited to `mouseenter`, `touchstart`, and `focusin`. Passing `[]` or unsupported names causes config resolution to fail.
|
|
390
|
+
|
|
383
391
|
### Multiple island directories
|
|
384
392
|
|
|
385
393
|
```ts
|
|
@@ -442,6 +450,8 @@ offLoad();
|
|
|
442
450
|
offError();
|
|
443
451
|
```
|
|
444
452
|
|
|
453
|
+
For SPA teardown, the virtual `/revive` module also exports `disconnect()`, which stops further lifecycle observation and cancels pending startup before init has run.
|
|
454
|
+
|
|
445
455
|
### Raw DOM events
|
|
446
456
|
|
|
447
457
|
The events are also available via the standard `document.addEventListener` API. Event types are fully typed via `DocumentEventMap` augmentation — available automatically when `vite-plugin-shopify-theme-islands` is present in your TypeScript compilation (e.g. via `vite.config.ts` or a directive type import).
|
package/dist/discovery.d.ts
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
export declare const TS_JS_RE: RegExp;
|
|
3
3
|
/** Matches the island mixin import. Exported for plugin transform/watch detection. */
|
|
4
4
|
export declare const ISLAND_IMPORT_RE: RegExp;
|
|
5
|
+
export interface AliasLike {
|
|
6
|
+
find: string | RegExp;
|
|
7
|
+
replacement: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IslandInventoryConfig {
|
|
10
|
+
root: string;
|
|
11
|
+
aliases: readonly AliasLike[];
|
|
12
|
+
}
|
|
13
|
+
export interface IslandInventorySnapshot {
|
|
14
|
+
resolvedDirectories: string[];
|
|
15
|
+
islandFiles: string[];
|
|
16
|
+
directoryTagNames: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface IslandInventoryChange {
|
|
19
|
+
type: "detected" | "removed";
|
|
20
|
+
file: string;
|
|
21
|
+
}
|
|
22
|
+
export interface IslandInventoryBootstrapState {
|
|
23
|
+
root: string;
|
|
24
|
+
directories: string[];
|
|
25
|
+
islandFiles: Set<string>;
|
|
26
|
+
}
|
|
5
27
|
/** True if file is under any of the given absolute directory paths. */
|
|
6
28
|
export declare function inDirectory(file: string, absDirs: string[]): boolean;
|
|
7
29
|
/** Paths for load() virtual module: "/relative/to/root" form, forward slashes. */
|
|
@@ -10,3 +32,11 @@ export declare function getIslandPathsForLoad(islandFiles: Set<string>, root: st
|
|
|
10
32
|
export declare function discoverIslandFiles(root: string, absDirs: string[]): Set<string>;
|
|
11
33
|
/** Tag names (filename without extension) for TS/JS files in a directory. Used for debug logging. */
|
|
12
34
|
export declare function collectTagNames(dir: string): string[];
|
|
35
|
+
export declare function createIslandInventory(rawDirectories: string[]): {
|
|
36
|
+
configure(config: IslandInventoryConfig): void;
|
|
37
|
+
scan(): IslandInventorySnapshot | null;
|
|
38
|
+
applyTransform(id: string, code: string): IslandInventoryChange | null;
|
|
39
|
+
applyWatchChange(id: string, event: string): IslandInventoryChange | null;
|
|
40
|
+
getBootstrapState(): IslandInventoryBootstrapState;
|
|
41
|
+
getRoot(): string;
|
|
42
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
import { join as join2, relative as relative2 } from "node:path";
|
|
2
|
+
import { relative as relative2 } from "node:path";
|
|
4
3
|
|
|
5
4
|
// src/discovery.ts
|
|
6
5
|
import { readFileSync, readdirSync } from "node:fs";
|
|
@@ -35,6 +34,21 @@ function walkDir(dir, visitor) {
|
|
|
35
34
|
visitor(entry.name, full);
|
|
36
35
|
}
|
|
37
36
|
}
|
|
37
|
+
function resolveAliases(dirs, aliasesInput) {
|
|
38
|
+
const aliases = [...aliasesInput].sort((a, b) => (typeof b.find === "string" ? b.find.length : 0) - (typeof a.find === "string" ? a.find.length : 0));
|
|
39
|
+
return dirs.map((dir) => {
|
|
40
|
+
for (const { find, replacement } of aliases) {
|
|
41
|
+
if (typeof find === "string" && dir.startsWith(find))
|
|
42
|
+
return dir.replace(find, replacement);
|
|
43
|
+
if (find instanceof RegExp && find.test(dir))
|
|
44
|
+
return dir.replace(find, replacement);
|
|
45
|
+
}
|
|
46
|
+
return dir;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function toAbsoluteDirs(root, resolvedDirs) {
|
|
50
|
+
return resolvedDirs.map((dir) => dir.startsWith(root) ? dir : join(root, dir.replace(/^\//, "")));
|
|
51
|
+
}
|
|
38
52
|
function discoverIslandFiles(root, absDirs) {
|
|
39
53
|
const found = new Set;
|
|
40
54
|
walkDir(root, (_, full) => {
|
|
@@ -53,10 +67,73 @@ function collectTagNames(dir) {
|
|
|
53
67
|
walkDir(dir, (name) => names.push(name.replace(TS_JS_RE, "")));
|
|
54
68
|
return names;
|
|
55
69
|
}
|
|
70
|
+
function createIslandInventory(rawDirectories) {
|
|
71
|
+
let root = process.cwd();
|
|
72
|
+
let resolvedDirs = [...rawDirectories];
|
|
73
|
+
let absDirs = [...rawDirectories];
|
|
74
|
+
const islandFiles = new Set;
|
|
75
|
+
let scanned = false;
|
|
76
|
+
const buildSnapshot = () => ({
|
|
77
|
+
resolvedDirectories: [...resolvedDirs],
|
|
78
|
+
islandFiles: [...islandFiles],
|
|
79
|
+
directoryTagNames: absDirs.flatMap((dir) => collectTagNames(dir))
|
|
80
|
+
});
|
|
81
|
+
const updateIslandFile = (id, code) => {
|
|
82
|
+
if (!TS_JS_RE.test(id))
|
|
83
|
+
return null;
|
|
84
|
+
if (code.includes("shopify-theme-islands/island") && ISLAND_IMPORT_RE.test(code) && !inDirectory(id, absDirs)) {
|
|
85
|
+
const sizeBefore = islandFiles.size;
|
|
86
|
+
islandFiles.add(id);
|
|
87
|
+
return islandFiles.size !== sizeBefore ? { type: "detected", file: id } : null;
|
|
88
|
+
}
|
|
89
|
+
return islandFiles.delete(id) ? { type: "removed", file: id } : null;
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
configure(config) {
|
|
93
|
+
root = config.root;
|
|
94
|
+
resolvedDirs = resolveAliases(rawDirectories, config.aliases);
|
|
95
|
+
absDirs = toAbsoluteDirs(root, resolvedDirs);
|
|
96
|
+
},
|
|
97
|
+
scan() {
|
|
98
|
+
if (scanned)
|
|
99
|
+
return null;
|
|
100
|
+
scanned = true;
|
|
101
|
+
islandFiles.clear();
|
|
102
|
+
discoverIslandFiles(root, absDirs).forEach((file) => islandFiles.add(file));
|
|
103
|
+
return buildSnapshot();
|
|
104
|
+
},
|
|
105
|
+
applyTransform(id, code) {
|
|
106
|
+
return updateIslandFile(id, code);
|
|
107
|
+
},
|
|
108
|
+
applyWatchChange(id, event) {
|
|
109
|
+
if (!TS_JS_RE.test(id))
|
|
110
|
+
return null;
|
|
111
|
+
if (event === "delete") {
|
|
112
|
+
return islandFiles.delete(id) ? { type: "removed", file: id } : null;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return updateIslandFile(id, readFileSync(id, "utf-8"));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
getBootstrapState() {
|
|
121
|
+
return {
|
|
122
|
+
root,
|
|
123
|
+
directories: [...resolvedDirs],
|
|
124
|
+
islandFiles: new Set(islandFiles)
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
getRoot() {
|
|
128
|
+
return root;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
56
132
|
|
|
57
133
|
// src/interaction-events.ts
|
|
58
134
|
var INTERACTION_EVENT_NAMES = ["mouseenter", "touchstart", "focusin"];
|
|
59
135
|
var DEFAULT_INTERACTION_EVENTS = [...INTERACTION_EVENT_NAMES];
|
|
136
|
+
var INTERACTION_EVENT_NAMES_LABEL = INTERACTION_EVENT_NAMES.join(", ");
|
|
60
137
|
var INTERACTION_EVENT_NAME_SET = new Set(INTERACTION_EVENT_NAMES);
|
|
61
138
|
var PREFIX = "[vite-plugin-shopify-theme-islands]";
|
|
62
139
|
function isInteractionEventName(value) {
|
|
@@ -68,11 +145,23 @@ function validateInteractionEvents(events) {
|
|
|
68
145
|
if (events.length === 0) {
|
|
69
146
|
throw new Error(`${PREFIX} "directives.interaction.events" must not be empty`);
|
|
70
147
|
}
|
|
71
|
-
const
|
|
148
|
+
const { invalid } = partitionInteractionEventTokens(events);
|
|
149
|
+
const invalidEvent = invalid[0];
|
|
72
150
|
if (invalidEvent) {
|
|
73
151
|
throw new Error(`${PREFIX} "directives.interaction.events" contains unsupported event "${invalidEvent}"`);
|
|
74
152
|
}
|
|
75
153
|
}
|
|
154
|
+
function partitionInteractionEventTokens(tokens) {
|
|
155
|
+
const valid = [];
|
|
156
|
+
const invalid = [];
|
|
157
|
+
for (const token of tokens) {
|
|
158
|
+
if (isInteractionEventName(token))
|
|
159
|
+
valid.push(token);
|
|
160
|
+
else
|
|
161
|
+
invalid.push(token);
|
|
162
|
+
}
|
|
163
|
+
return { valid, invalid };
|
|
164
|
+
}
|
|
76
165
|
|
|
77
166
|
// src/contract.ts
|
|
78
167
|
var DEFAULT_DIRECTIVES = {
|
|
@@ -234,9 +323,10 @@ function createReviveBootstrapCompiler(ports, runtimePath) {
|
|
|
234
323
|
name,
|
|
235
324
|
entrypoint: await ports.resolveEntrypoint(entrypoint)
|
|
236
325
|
}))) : null;
|
|
326
|
+
const directoryGlobs = input.directories.map((dir) => dir + "**/*.{ts,js}");
|
|
237
327
|
return {
|
|
238
328
|
runtimePath,
|
|
239
|
-
directoryGlobs
|
|
329
|
+
directoryGlobs,
|
|
240
330
|
islandPaths,
|
|
241
331
|
customDirectives,
|
|
242
332
|
reviveOptions: input.reviveOptions
|
|
@@ -265,89 +355,57 @@ var defaultDirectories = ["/frontend/js/islands/"];
|
|
|
265
355
|
function normalizeDir(dir) {
|
|
266
356
|
return dir.endsWith("/") ? dir : dir + "/";
|
|
267
357
|
}
|
|
268
|
-
function resolveAliases(dirs, config) {
|
|
269
|
-
const aliases = [...config.resolve.alias].sort((a, b) => (typeof b.find === "string" ? b.find.length : 0) - (typeof a.find === "string" ? a.find.length : 0));
|
|
270
|
-
return dirs.map((dir) => {
|
|
271
|
-
for (const { find, replacement } of aliases) {
|
|
272
|
-
if (typeof find === "string" && dir.startsWith(find))
|
|
273
|
-
return dir.replace(find, replacement);
|
|
274
|
-
if (find instanceof RegExp && find.test(dir))
|
|
275
|
-
return dir.replace(find, replacement);
|
|
276
|
-
}
|
|
277
|
-
return dir;
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
358
|
function shopifyThemeIslands(options = {}) {
|
|
281
359
|
const rawDirs = (Array.isArray(options.directories) ? options.directories : [options.directories ?? defaultDirectories[0]]).map(normalizeDir);
|
|
282
360
|
const policy = resolveThemeIslandsPolicy(options);
|
|
283
361
|
const { directives, customDirectives: clientDirectiveDefinitions, debug } = policy.plugin;
|
|
284
362
|
const { runtime: reviveOptions } = policy;
|
|
285
363
|
const log = debug ? (...args) => console.log("[islands]", ...args) : () => {};
|
|
286
|
-
|
|
287
|
-
let root = process.cwd();
|
|
288
|
-
let absDirs = rawDirs;
|
|
289
|
-
const islandFiles = new Set;
|
|
290
|
-
let scanned = false;
|
|
364
|
+
const inventory = createIslandInventory(rawDirs);
|
|
291
365
|
return {
|
|
292
366
|
name: "vite-plugin-shopify-theme-islands",
|
|
293
367
|
enforce: "pre",
|
|
294
368
|
configResolved(config) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
369
|
+
inventory.configure({
|
|
370
|
+
root: config.root,
|
|
371
|
+
aliases: config.resolve.alias
|
|
372
|
+
});
|
|
298
373
|
},
|
|
299
374
|
buildStart() {
|
|
300
|
-
if (scanned)
|
|
301
|
-
return;
|
|
302
|
-
scanned = true;
|
|
303
375
|
const t0 = performance.now();
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
376
|
+
const snapshot = inventory.scan();
|
|
377
|
+
if (!snapshot)
|
|
378
|
+
return;
|
|
307
379
|
if (debug) {
|
|
308
380
|
const scanMs = (performance.now() - t0).toFixed(1);
|
|
309
381
|
log(`Scanned in ${scanMs}ms`);
|
|
310
|
-
log("Scanning directories:",
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (islandFiles.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
382
|
+
log("Scanning directories:", snapshot.resolvedDirectories.map((dir) => dir + "**/*.{ts,js}").join(", "));
|
|
383
|
+
if (snapshot.directoryTagNames.length) {
|
|
384
|
+
log(`Found ${snapshot.directoryTagNames.length} directory island(s): [${snapshot.directoryTagNames.join(", ")}]`);
|
|
385
|
+
}
|
|
386
|
+
if (snapshot.islandFiles.length) {
|
|
387
|
+
const root = inventory.getRoot();
|
|
388
|
+
log(`Found ${snapshot.islandFiles.length} island file(s) via mixin import:`);
|
|
389
|
+
for (const file of snapshot.islandFiles)
|
|
390
|
+
log(" ", relative2(root, file));
|
|
318
391
|
}
|
|
319
392
|
log("Directives:", directives);
|
|
320
393
|
}
|
|
321
394
|
},
|
|
322
395
|
transform(code, id) {
|
|
323
|
-
|
|
396
|
+
const change = inventory.applyTransform(id, code);
|
|
397
|
+
if (!change)
|
|
324
398
|
return;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
log("Detected island:", relative2(root, id));
|
|
328
|
-
} else {
|
|
329
|
-
if (islandFiles.delete(id))
|
|
330
|
-
log("Removed island:", relative2(root, id));
|
|
331
|
-
}
|
|
399
|
+
const root = inventory.getRoot();
|
|
400
|
+
log(change.type === "detected" ? "Detected island:" : "Removed island:", relative2(root, change.file));
|
|
332
401
|
},
|
|
333
402
|
watchChange(id, { event }) {
|
|
334
|
-
|
|
403
|
+
const change = inventory.applyWatchChange(id, event);
|
|
404
|
+
if (!change)
|
|
335
405
|
return;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} else {
|
|
340
|
-
try {
|
|
341
|
-
const content = readFileSync2(id, "utf-8");
|
|
342
|
-
if (ISLAND_IMPORT_RE.test(content) && !inDirectory(id, absDirs)) {
|
|
343
|
-
islandFiles.add(id);
|
|
344
|
-
log("Detected island (watchChange):", relative2(root, id));
|
|
345
|
-
} else {
|
|
346
|
-
if (islandFiles.delete(id))
|
|
347
|
-
log("Removed island (watchChange):", relative2(root, id));
|
|
348
|
-
}
|
|
349
|
-
} catch {}
|
|
350
|
-
}
|
|
406
|
+
const root = inventory.getRoot();
|
|
407
|
+
const prefix = event === "delete" ? "Removed island (deleted):" : change.type === "detected" ? "Detected island (watchChange):" : "Removed island (watchChange):";
|
|
408
|
+
log(prefix, relative2(root, change.file));
|
|
351
409
|
},
|
|
352
410
|
resolveId(id) {
|
|
353
411
|
if (id === VIRTUAL_ID)
|
|
@@ -369,9 +427,7 @@ function shopifyThemeIslands(options = {}) {
|
|
|
369
427
|
toLoadPaths: getIslandPathsForLoad
|
|
370
428
|
}, runtimePath);
|
|
371
429
|
const plan = await compiler.plan({
|
|
372
|
-
|
|
373
|
-
directories: resolvedDirs,
|
|
374
|
-
islandFiles,
|
|
430
|
+
...inventory.getBootstrapState(),
|
|
375
431
|
customDirectives: clientDirectiveDefinitions,
|
|
376
432
|
reviveOptions
|
|
377
433
|
});
|
|
@@ -8,5 +8,11 @@
|
|
|
8
8
|
export declare const INTERACTION_EVENT_NAMES: readonly ["mouseenter", "touchstart", "focusin"];
|
|
9
9
|
export type InteractionEventName = (typeof INTERACTION_EVENT_NAMES)[number];
|
|
10
10
|
export declare const DEFAULT_INTERACTION_EVENTS: readonly ["mouseenter", "touchstart", "focusin"];
|
|
11
|
+
export declare const INTERACTION_EVENT_NAMES_LABEL: string;
|
|
12
|
+
export interface InteractionEventTokenPartition {
|
|
13
|
+
valid: InteractionEventName[];
|
|
14
|
+
invalid: string[];
|
|
15
|
+
}
|
|
11
16
|
export declare function isInteractionEventName(value: string): value is InteractionEventName;
|
|
12
17
|
export declare function validateInteractionEvents(events: readonly string[] | undefined): asserts events is readonly InteractionEventName[];
|
|
18
|
+
export declare function partitionInteractionEventTokens(tokens: readonly string[]): InteractionEventTokenPartition;
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/interaction-events.ts
|
|
2
2
|
var INTERACTION_EVENT_NAMES = ["mouseenter", "touchstart", "focusin"];
|
|
3
3
|
var DEFAULT_INTERACTION_EVENTS = [...INTERACTION_EVENT_NAMES];
|
|
4
|
+
var INTERACTION_EVENT_NAMES_LABEL = INTERACTION_EVENT_NAMES.join(", ");
|
|
4
5
|
var INTERACTION_EVENT_NAME_SET = new Set(INTERACTION_EVENT_NAMES);
|
|
5
6
|
var PREFIX = "[vite-plugin-shopify-theme-islands]";
|
|
6
7
|
function isInteractionEventName(value) {
|
|
@@ -12,11 +13,23 @@ function validateInteractionEvents(events) {
|
|
|
12
13
|
if (events.length === 0) {
|
|
13
14
|
throw new Error(`${PREFIX} "directives.interaction.events" must not be empty`);
|
|
14
15
|
}
|
|
15
|
-
const
|
|
16
|
+
const { invalid } = partitionInteractionEventTokens(events);
|
|
17
|
+
const invalidEvent = invalid[0];
|
|
16
18
|
if (invalidEvent) {
|
|
17
19
|
throw new Error(`${PREFIX} "directives.interaction.events" contains unsupported event "${invalidEvent}"`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
function partitionInteractionEventTokens(tokens) {
|
|
23
|
+
const valid = [];
|
|
24
|
+
const invalid = [];
|
|
25
|
+
for (const token of tokens) {
|
|
26
|
+
if (isInteractionEventName(token))
|
|
27
|
+
valid.push(token);
|
|
28
|
+
else
|
|
29
|
+
invalid.push(token);
|
|
30
|
+
}
|
|
31
|
+
return { valid, invalid };
|
|
32
|
+
}
|
|
20
33
|
|
|
21
34
|
// src/contract.ts
|
|
22
35
|
var DEFAULT_DIRECTIVES = {
|
|
@@ -184,10 +197,19 @@ function createDirectiveOrchestrator(waiters = {
|
|
|
184
197
|
let events = [...directives.interaction.events];
|
|
185
198
|
if (interactionAttr) {
|
|
186
199
|
const tokens = interactionAttr.split(/\s+/).filter(Boolean);
|
|
187
|
-
if (tokens.length
|
|
188
|
-
events = tokens;
|
|
189
|
-
else {
|
|
200
|
+
if (tokens.length === 0) {
|
|
190
201
|
console.warn(`[islands] <${tagName}> ${directives.interaction.attribute} has no valid event tokens — using default events`);
|
|
202
|
+
} else {
|
|
203
|
+
const { valid, invalid } = partitionInteractionEventTokens(tokens);
|
|
204
|
+
if (invalid.length > 0) {
|
|
205
|
+
if (valid.length > 0) {
|
|
206
|
+
console.warn(`[islands] <${tagName}> ${directives.interaction.attribute} contains unsupported event token${invalid.length === 1 ? "" : "s"} (${invalid.join(", ")}) — ignoring invalid token${invalid.length === 1 ? "" : "s"}; supported tokens: ${INTERACTION_EVENT_NAMES_LABEL}`);
|
|
207
|
+
} else {
|
|
208
|
+
console.warn(`[islands] <${tagName}> ${directives.interaction.attribute} contains no supported event tokens (${invalid.join(", ")}) — using default events; supported tokens: ${INTERACTION_EVENT_NAMES_LABEL}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (valid.length > 0)
|
|
212
|
+
events = valid;
|
|
191
213
|
}
|
|
192
214
|
}
|
|
193
215
|
log.note(`waiting for ${directives.interaction.attribute} (${events.join(", ")})`);
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ description: >
|
|
|
10
10
|
are owned by src/directive-orchestration.ts.
|
|
11
11
|
type: core
|
|
12
12
|
library: vite-plugin-shopify-theme-islands
|
|
13
|
-
library_version: "1.3.
|
|
13
|
+
library_version: "1.3.1"
|
|
14
14
|
sources:
|
|
15
15
|
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
16
16
|
- Rees1993/vite-plugin-shopify-theme-islands:src/directive-orchestration.ts
|
|
@@ -6,14 +6,16 @@ description: >
|
|
|
6
6
|
client:defer (setTimeout delay), client:interaction (mouseenter/touchstart/focusin).
|
|
7
7
|
Directives resolve sequentially — visible → media → idle → defer →
|
|
8
8
|
interaction → custom. Per-element value overrides. Empty client:media
|
|
9
|
-
warning.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
warning. `client:interaction` now validates per-element tokens at runtime:
|
|
10
|
+
whitespace-only values warn and fall back; mixed supported/unsupported values
|
|
11
|
+
warn and ignore the unsupported tokens; fully unsupported values warn and fall
|
|
12
|
+
back to default events. Global `directives.interaction.events` config is
|
|
13
|
+
intentionally narrowed to the curated set `mouseenter`, `touchstart`, and
|
|
14
|
+
`focusin`. Current directive sequencing and custom-directive latching are
|
|
15
|
+
owned by src/directive-orchestration.ts.
|
|
14
16
|
type: core
|
|
15
17
|
library: vite-plugin-shopify-theme-islands
|
|
16
|
-
library_version: "1.3.
|
|
18
|
+
library_version: "1.3.1"
|
|
17
19
|
sources:
|
|
18
20
|
- Rees1993/vite-plugin-shopify-theme-islands:src/directive-orchestration.ts
|
|
19
21
|
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts
|
|
@@ -79,6 +81,7 @@ Combined directives are AND-latched. The island loads only after every condition
|
|
|
79
81
|
|
|
80
82
|
The attribute value overrides the globally configured default for that element. Other elements are unaffected.
|
|
81
83
|
In config, `directives.interaction.events` is stricter and only accepts the curated package-owned list: `mouseenter`, `touchstart`, and `focusin`.
|
|
84
|
+
At runtime, per-element `client:interaction` values use that same curated set. Unsupported tokens are ignored with a warning; if no supported tokens remain, the runtime warns and falls back to the default interaction events.
|
|
82
85
|
|
|
83
86
|
### `client:defer` without a value uses the global default
|
|
84
87
|
|
|
@@ -106,6 +109,18 @@ An empty `client:interaction` attribute uses the configured default events with
|
|
|
106
109
|
|
|
107
110
|
Source: src/directive-orchestration.ts — interaction token parsing and fallback warning
|
|
108
111
|
|
|
112
|
+
### Mixed supported and unsupported interaction tokens
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<!-- "click" is ignored with a warning; "mouseenter" still triggers load -->
|
|
116
|
+
<cart-flyout client:interaction="mouseenter click"></cart-flyout>
|
|
117
|
+
|
|
118
|
+
<!-- No supported tokens remain; warns and falls back to default events -->
|
|
119
|
+
<cart-flyout client:interaction="click submit"></cart-flyout>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Per-element values are no longer treated as an unconstrained event surface. The runtime filters them against the curated package-owned set.
|
|
123
|
+
|
|
109
124
|
### Changing built-in directive defaults globally
|
|
110
125
|
|
|
111
126
|
```ts
|
|
@@ -169,6 +184,25 @@ Whitespace-only values are not treated the same as an empty attribute. The runti
|
|
|
169
184
|
|
|
170
185
|
Source: src/directive-orchestration.ts — `interactionAttr.split(/\s+/).filter(Boolean)`
|
|
171
186
|
|
|
187
|
+
### MEDIUM Unsupported per-element interaction tokens are warned and ignored
|
|
188
|
+
|
|
189
|
+
Wrong:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<cart-flyout client:interaction="mouseenter click"></cart-flyout>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Correct:
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<!-- Use only the curated supported tokens -->
|
|
199
|
+
<cart-flyout client:interaction="mouseenter focusin"></cart-flyout>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The runtime no longer attaches arbitrary listeners for unsupported per-element tokens. Supported tokens still work; unsupported ones are ignored with a warning. If no supported tokens remain, the runtime falls back to the configured default events.
|
|
203
|
+
|
|
204
|
+
Source: src/directive-orchestration.ts — `partitionInteractionEventTokens()` handling
|
|
205
|
+
|
|
172
206
|
### HIGH Multiple directives are AND, not OR
|
|
173
207
|
|
|
174
208
|
Wrong assumption:
|
|
@@ -10,10 +10,11 @@ description: >
|
|
|
10
10
|
expiry. disconnect() from the virtual module revive for SPA navigation
|
|
11
11
|
teardown, including before DOMContentLoaded — it now prevents init from ever
|
|
12
12
|
starting if called early. Startup, DOM walking, mutation observation, and
|
|
13
|
-
parent/child activation gating are now owned by src/lifecycle.ts
|
|
13
|
+
parent/child activation gating are now owned by src/lifecycle.ts, while
|
|
14
|
+
runtime observability and event dispatch are routed through src/runtime-surface.ts.
|
|
14
15
|
type: core
|
|
15
16
|
library: vite-plugin-shopify-theme-islands
|
|
16
|
-
library_version: "1.3.
|
|
17
|
+
library_version: "1.3.1"
|
|
17
18
|
sources:
|
|
18
19
|
- Rees1993/vite-plugin-shopify-theme-islands:src/events.ts
|
|
19
20
|
- Rees1993/vite-plugin-shopify-theme-islands:src/runtime-surface.ts
|
|
@@ -94,6 +95,8 @@ disconnect();
|
|
|
94
95
|
|
|
95
96
|
The startup walk itself is now lifecycle-owned. The runtime resolves the root lazily at init time, then the lifecycle coordinator performs the initial walk, begins observing subtree additions, and keeps child islands gated behind queued parents until the parent resolves.
|
|
96
97
|
|
|
98
|
+
Load/error events and debug-ready groups are dispatched through the runtime surface, but the user-facing lifecycle behavior remains the same: startup is lazy, activation is subtree-aware, and teardown prevents later observation.
|
|
99
|
+
|
|
97
100
|
### Raw DOM events (when type augmentation is in scope)
|
|
98
101
|
|
|
99
102
|
```ts
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -7,11 +7,14 @@ description: >
|
|
|
7
7
|
defer, interaction, custom), retry (retries, delay with exponential
|
|
8
8
|
backoff), directiveTimeout for hung custom directives, and the curated
|
|
9
9
|
interaction-event config policy (`mouseenter`, `touchstart`, `focusin`; empty
|
|
10
|
-
arrays rejected).
|
|
11
|
-
|
|
10
|
+
arrays rejected). Per-element `client:interaction` values are runtime-validated
|
|
11
|
+
against the same curated set: unsupported tokens warn and are ignored; if no
|
|
12
|
+
supported tokens remain, the runtime falls back to the configured default
|
|
13
|
+
events. Load when setting up the plugin, configuring island scan directories,
|
|
14
|
+
or enabling retry / directive timeout.
|
|
12
15
|
type: core
|
|
13
16
|
library: vite-plugin-shopify-theme-islands
|
|
14
|
-
library_version: "1.3.
|
|
17
|
+
library_version: "1.3.1"
|
|
15
18
|
sources:
|
|
16
19
|
- Rees1993/vite-plugin-shopify-theme-islands:src/index.ts
|
|
17
20
|
- Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts
|
|
@@ -51,6 +54,10 @@ import "vite-plugin-shopify-theme-islands/revive";
|
|
|
51
54
|
|
|
52
55
|
This activates the runtime — islands are never loaded without this import.
|
|
53
56
|
|
|
57
|
+
For SPA teardown, the virtual `/revive` module also exports `disconnect()`.
|
|
58
|
+
If it is called before `DOMContentLoaded`, the runtime cancels its pending
|
|
59
|
+
startup listener so islands never initialize later against stale DOM.
|
|
60
|
+
|
|
54
61
|
### 3. Add directives to Liquid templates
|
|
55
62
|
|
|
56
63
|
```html
|
|
@@ -86,6 +93,7 @@ shopifyThemeIslands({
|
|
|
86
93
|
|
|
87
94
|
Per-directive options are deep-merged — overriding `visible.rootMargin` preserves `visible.threshold` at its default of `0`.
|
|
88
95
|
For config, `directives.interaction.events` is intentionally narrow and only accepts `mouseenter`, `touchstart`, and `focusin`.
|
|
96
|
+
Per-element `client:interaction="..."` values are checked at runtime against that same set. Unsupported tokens warn and are ignored; if all tokens are unsupported, the runtime warns and falls back to the configured default events.
|
|
89
97
|
|
|
90
98
|
### Enable automatic retry with exponential backoff
|
|
91
99
|
|
|
@@ -9,7 +9,7 @@ description: >
|
|
|
9
9
|
src/lifecycle.ts.
|
|
10
10
|
type: core
|
|
11
11
|
library: vite-plugin-shopify-theme-islands
|
|
12
|
-
library_version: "1.3.
|
|
12
|
+
library_version: "1.3.1"
|
|
13
13
|
sources:
|
|
14
14
|
- Rees1993/vite-plugin-shopify-theme-islands:src/island.ts
|
|
15
15
|
- Rees1993/vite-plugin-shopify-theme-islands:src/discovery.ts
|