vite-plugin-shopify-theme-islands 1.2.1 → 1.3.0
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/contract.d.ts +3 -2
- 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 -0
- package/dist/index.js +35 -9
- package/dist/interaction-events.d.ts +12 -0
- package/dist/lifecycle.d.ts +27 -0
- package/dist/options.d.ts +3 -2
- package/dist/runtime-surface.d.ts +20 -0
- package/dist/runtime.js +383 -271
- package/package.json +1 -1
- package/skills/custom-directives/SKILL.md +11 -9
- package/skills/directives/SKILL.md +50 -22
- package/skills/lifecycle/SKILL.md +13 -5
- package/skills/setup/SKILL.md +39 -4
- package/skills/writing-islands/SKILL.md +6 -3
package/dist/contract.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Single source of truth for the payload shape, key→tag derivation, and defaults.
|
|
5
5
|
* Plugin and runtime both depend on this module in-process.
|
|
6
6
|
*/
|
|
7
|
+
import type { InteractionEventName } from "./interaction-events.js";
|
|
7
8
|
/** Loader function for one island chunk. */
|
|
8
9
|
export type IslandLoader = () => Promise<unknown>;
|
|
9
10
|
/** Directive config for the runtime (built-in + no plugin-only `custom` entrypoints). */
|
|
@@ -26,7 +27,7 @@ export interface RuntimeDirectivesConfig {
|
|
|
26
27
|
};
|
|
27
28
|
interaction?: {
|
|
28
29
|
attribute?: string;
|
|
29
|
-
events?:
|
|
30
|
+
events?: readonly InteractionEventName[];
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
/** Retry configuration. */
|
|
@@ -111,7 +112,7 @@ export interface NormalizedReviveOptions {
|
|
|
111
112
|
};
|
|
112
113
|
interaction: {
|
|
113
114
|
attribute: string;
|
|
114
|
-
events:
|
|
115
|
+
events: readonly InteractionEventName[];
|
|
115
116
|
};
|
|
116
117
|
};
|
|
117
118
|
debug: boolean;
|
|
@@ -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,
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export type ClientDirectiveLoader = () => Promise<void>;
|
|
|
5
5
|
export type { ClientDirective, ClientDirectiveOptions } from "./contract.js";
|
|
6
6
|
export type { ClientDirectiveDefinition, DirectivesConfig, ShopifyThemeIslandsOptions, } from "./options.js";
|
|
7
7
|
export type { IslandLoadDetail, IslandErrorDetail, ReviveOptions, RetryConfig, RuntimeDirectivesConfig, } from "./contract.js";
|
|
8
|
+
export type { InteractionEventName } from "./interaction-events.js";
|
|
9
|
+
export { DEFAULT_INTERACTION_EVENTS, INTERACTION_EVENT_NAMES, isInteractionEventName, } from "./interaction-events.js";
|
|
8
10
|
export default function shopifyThemeIslands(options?: ShopifyThemeIslandsOptions): Plugin;
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,26 @@ function collectTagNames(dir) {
|
|
|
54
54
|
return names;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// src/interaction-events.ts
|
|
58
|
+
var INTERACTION_EVENT_NAMES = ["mouseenter", "touchstart", "focusin"];
|
|
59
|
+
var DEFAULT_INTERACTION_EVENTS = [...INTERACTION_EVENT_NAMES];
|
|
60
|
+
var INTERACTION_EVENT_NAME_SET = new Set(INTERACTION_EVENT_NAMES);
|
|
61
|
+
var PREFIX = "[vite-plugin-shopify-theme-islands]";
|
|
62
|
+
function isInteractionEventName(value) {
|
|
63
|
+
return INTERACTION_EVENT_NAME_SET.has(value);
|
|
64
|
+
}
|
|
65
|
+
function validateInteractionEvents(events) {
|
|
66
|
+
if (events === undefined)
|
|
67
|
+
return;
|
|
68
|
+
if (events.length === 0) {
|
|
69
|
+
throw new Error(`${PREFIX} "directives.interaction.events" must not be empty`);
|
|
70
|
+
}
|
|
71
|
+
const invalidEvent = events.find((eventName) => !isInteractionEventName(eventName));
|
|
72
|
+
if (invalidEvent) {
|
|
73
|
+
throw new Error(`${PREFIX} "directives.interaction.events" contains unsupported event "${invalidEvent}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
// src/contract.ts
|
|
58
78
|
var DEFAULT_DIRECTIVES = {
|
|
59
79
|
visible: { attribute: "client:visible", rootMargin: "200px", threshold: 0 },
|
|
@@ -62,7 +82,7 @@ var DEFAULT_DIRECTIVES = {
|
|
|
62
82
|
defer: { attribute: "client:defer", delay: 3000 },
|
|
63
83
|
interaction: {
|
|
64
84
|
attribute: "client:interaction",
|
|
65
|
-
events: [
|
|
85
|
+
events: [...DEFAULT_INTERACTION_EVENTS]
|
|
66
86
|
}
|
|
67
87
|
};
|
|
68
88
|
var DEFAULT_RETRY = { retries: 0, delay: 1000 };
|
|
@@ -70,6 +90,7 @@ function normalizeReviveOptions(options) {
|
|
|
70
90
|
const d = DEFAULT_DIRECTIVES;
|
|
71
91
|
const r = DEFAULT_RETRY;
|
|
72
92
|
const dir = options?.directives;
|
|
93
|
+
validateInteractionEvents(dir?.interaction?.events);
|
|
73
94
|
return {
|
|
74
95
|
directives: {
|
|
75
96
|
visible: { ...d.visible, ...dir?.visible },
|
|
@@ -105,7 +126,7 @@ function buildIslandMap(payload) {
|
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
// src/config-policy.ts
|
|
108
|
-
var
|
|
129
|
+
var PREFIX2 = "[vite-plugin-shopify-theme-islands]";
|
|
109
130
|
function mergeDirectives(directives) {
|
|
110
131
|
return {
|
|
111
132
|
visible: { ...DEFAULT_DIRECTIVES.visible, ...directives?.visible },
|
|
@@ -118,19 +139,21 @@ function mergeDirectives(directives) {
|
|
|
118
139
|
function validateOptions(options, directives) {
|
|
119
140
|
const customDefs = options.directives?.custom ?? [];
|
|
120
141
|
if (Array.isArray(options.directories) && options.directories.length === 0) {
|
|
121
|
-
throw new Error(`${
|
|
142
|
+
throw new Error(`${PREFIX2} "directories" must not be empty`);
|
|
122
143
|
}
|
|
123
144
|
const threshold = options.directives?.visible?.threshold;
|
|
124
145
|
if (threshold !== undefined && (threshold < 0 || threshold > 1)) {
|
|
125
|
-
throw new Error(`${
|
|
146
|
+
throw new Error(`${PREFIX2} "directives.visible.threshold" must be between 0 and 1, got ${threshold}`);
|
|
126
147
|
}
|
|
148
|
+
const interactionEvents = options.directives?.interaction?.events;
|
|
149
|
+
validateInteractionEvents(interactionEvents);
|
|
127
150
|
if (options.retry !== undefined) {
|
|
128
151
|
const { retries, delay } = options.retry;
|
|
129
152
|
if (retries !== undefined && retries < 0) {
|
|
130
|
-
throw new Error(`${
|
|
153
|
+
throw new Error(`${PREFIX2} "retry.retries" must be >= 0, got ${retries}`);
|
|
131
154
|
}
|
|
132
155
|
if (delay !== undefined && delay < 0) {
|
|
133
|
-
throw new Error(`${
|
|
156
|
+
throw new Error(`${PREFIX2} "retry.delay" must be >= 0, got ${delay}`);
|
|
134
157
|
}
|
|
135
158
|
}
|
|
136
159
|
const builtinAttributes = new Set([
|
|
@@ -143,10 +166,10 @@ function validateOptions(options, directives) {
|
|
|
143
166
|
const seen = new Set;
|
|
144
167
|
for (const def of customDefs) {
|
|
145
168
|
if (seen.has(def.name)) {
|
|
146
|
-
throw new Error(`${
|
|
169
|
+
throw new Error(`${PREFIX2} Duplicate custom directive name: "${def.name}"`);
|
|
147
170
|
}
|
|
148
171
|
if (builtinAttributes.has(def.name)) {
|
|
149
|
-
throw new Error(`${
|
|
172
|
+
throw new Error(`${PREFIX2} Custom directive "${def.name}" conflicts with a built-in directive`);
|
|
150
173
|
}
|
|
151
174
|
seen.add(def.name);
|
|
152
175
|
}
|
|
@@ -357,5 +380,8 @@ function shopifyThemeIslands(options = {}) {
|
|
|
357
380
|
};
|
|
358
381
|
}
|
|
359
382
|
export {
|
|
360
|
-
|
|
383
|
+
isInteractionEventName,
|
|
384
|
+
shopifyThemeIslands as default,
|
|
385
|
+
INTERACTION_EVENT_NAMES,
|
|
386
|
+
DEFAULT_INTERACTION_EVENTS
|
|
361
387
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated interaction events accepted by `client:interaction`.
|
|
3
|
+
*
|
|
4
|
+
* The list is intentionally narrow: these are the user-intent signals the
|
|
5
|
+
* plugin supports for now, and they are exposed as a package-owned union so
|
|
6
|
+
* config can be type-checked without relying on the DOM lib surface.
|
|
7
|
+
*/
|
|
8
|
+
export declare const INTERACTION_EVENT_NAMES: readonly ["mouseenter", "touchstart", "focusin"];
|
|
9
|
+
export type InteractionEventName = (typeof INTERACTION_EVENT_NAMES)[number];
|
|
10
|
+
export declare const DEFAULT_INTERACTION_EVENTS: readonly ["mouseenter", "touchstart", "focusin"];
|
|
11
|
+
export declare function isInteractionEventName(value: string): value is InteractionEventName;
|
|
12
|
+
export declare function validateInteractionEvents(events: readonly string[] | undefined): asserts events is readonly InteractionEventName[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IslandLoader } from "./contract.js";
|
|
2
|
+
export interface IslandLifecycleStartInput {
|
|
3
|
+
getRoot(): HTMLElement | null;
|
|
4
|
+
islandMap: Map<string, IslandLoader>;
|
|
5
|
+
onActivate(tagName: string, el: HTMLElement, loader: () => Promise<unknown>): void;
|
|
6
|
+
onBeforeInitialWalk?: () => void;
|
|
7
|
+
onInitialWalkComplete?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface IslandLifecycle {
|
|
10
|
+
settleSuccess(tag: string): number;
|
|
11
|
+
settleFailure(tag: string): {
|
|
12
|
+
retryDelayMs: number | null;
|
|
13
|
+
attempt: number;
|
|
14
|
+
};
|
|
15
|
+
evict(tag: string): void;
|
|
16
|
+
isQueued(tag: string): boolean;
|
|
17
|
+
readonly initialWalkComplete: boolean;
|
|
18
|
+
watchCancellable(el: Element, cancel: () => void): () => void;
|
|
19
|
+
walk(root: HTMLElement): void;
|
|
20
|
+
start(input: IslandLifecycleStartInput): {
|
|
21
|
+
disconnect: () => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function createIslandLifecycleCoordinator(opts: {
|
|
25
|
+
retries: number;
|
|
26
|
+
retryDelay: number;
|
|
27
|
+
}): IslandLifecycle;
|
package/dist/options.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RetryConfig } from "./contract.js";
|
|
2
|
+
import type { InteractionEventName } from "./interaction-events.js";
|
|
2
3
|
/** Plugin option entry for registering a custom client directive. */
|
|
3
4
|
export interface ClientDirectiveDefinition {
|
|
4
5
|
/** HTML attribute name, e.g. `'client:on-click'` */
|
|
@@ -40,8 +41,8 @@ export interface DirectivesConfig {
|
|
|
40
41
|
interaction?: {
|
|
41
42
|
/** HTML attribute name. Default: `'client:interaction'` */
|
|
42
43
|
attribute?: string;
|
|
43
|
-
/**
|
|
44
|
-
events?:
|
|
44
|
+
/** Curated intent events to listen for. Default: `['mouseenter', 'touchstart', 'focusin']` */
|
|
45
|
+
events?: readonly InteractionEventName[];
|
|
45
46
|
};
|
|
46
47
|
/** Custom client directives to register. Each entry maps an attribute name to a module entrypoint. */
|
|
47
48
|
custom?: ClientDirectiveDefinition[];
|
|
@@ -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 {};
|