Skip to content

Commit 27e2351

Browse files
feat(#58-66): framework adapters — next/nuxt/vue/react/svelte/sveltekit/remix/astro/angular/partytown
All adapters are framework-agnostic — no peer dependencies. Consumer brings their own React/Vue/Angular/etc, passing the framework primitives in via a factory or constructor. Tree-shakeable: each adapter ships independently. - ahize/react (#61): createUseAhize(React) → useAhize hook with isReady, identity, identify, show, hide, shutdown, pageView. Reads onIdentityChange so identity stays in sync. - ahize/vue (#60): createUseAhize(Vue) returning a Composition API style composable with reactive isReady + identity refs. - ahize/svelte (#62 partial): createAhizeStore returns a Svelte-store-shaped subscribe contract; auto-loads on first subscriber, unloads listener after last unsubscribe. - ahize/sveltekit (#62): setupAhize(opts, navApi) wires afterNavigate from $app/navigation for auto pageView. - ahize/next (#58): createAhizeComponent(React, nextNav) returns an <Ahize> component; auto-fires pageView on usePathname() + useSearchParams() changes. - ahize/nuxt (#59): createNuxtAhizePlugin(opts) — drop-in for `~/plugins/ahize.client.ts`, hooks $router.afterEach for pageView. - ahize/remix (#63): createRemixAhize(React, remix) hook reading useLocation(). - ahize/astro (#64): mountAhize() in island scripts; listens to astro:after-swap for view-transition pageView. - ahize/angular (#65): AhizeAngularService<T> standalone-compatible service class subscribing to Router events. - ahize/partytown (#66): partytownForward(...providers) returns the exact list of window globals each provider needs in @builder.io/partytown's forward config; partytownConfig() wraps it as a config fragment. Closes #58 #59 #60 #61 #62 #63 #64 #65 #66 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1c4e4b6 commit 27e2351

12 files changed

Lines changed: 649 additions & 0 deletions

File tree

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,46 @@
143143
"./diagnostics": {
144144
"types": "./dist/diagnostics.d.mts",
145145
"default": "./dist/diagnostics.mjs"
146+
},
147+
"./next": {
148+
"types": "./dist/adapters/next.d.mts",
149+
"default": "./dist/adapters/next.mjs"
150+
},
151+
"./nuxt": {
152+
"types": "./dist/adapters/nuxt.d.mts",
153+
"default": "./dist/adapters/nuxt.mjs"
154+
},
155+
"./vue": {
156+
"types": "./dist/adapters/vue.d.mts",
157+
"default": "./dist/adapters/vue.mjs"
158+
},
159+
"./react": {
160+
"types": "./dist/adapters/react.d.mts",
161+
"default": "./dist/adapters/react.mjs"
162+
},
163+
"./svelte": {
164+
"types": "./dist/adapters/svelte.d.mts",
165+
"default": "./dist/adapters/svelte.mjs"
166+
},
167+
"./sveltekit": {
168+
"types": "./dist/adapters/sveltekit.d.mts",
169+
"default": "./dist/adapters/sveltekit.mjs"
170+
},
171+
"./remix": {
172+
"types": "./dist/adapters/remix.d.mts",
173+
"default": "./dist/adapters/remix.mjs"
174+
},
175+
"./astro": {
176+
"types": "./dist/adapters/astro.d.mts",
177+
"default": "./dist/adapters/astro.mjs"
178+
},
179+
"./angular": {
180+
"types": "./dist/adapters/angular.d.mts",
181+
"default": "./dist/adapters/angular.mjs"
182+
},
183+
"./partytown": {
184+
"types": "./dist/adapters/partytown.d.mts",
185+
"default": "./dist/adapters/partytown.mjs"
146186
}
147187
},
148188
"scripts": {

src/adapters/angular.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Angular v16+ standalone-compatible service factory.
2+
// Consumer brings their Injectable + their Router for navigation events.
3+
4+
import type { Identity, IdentityState, LoadOptions } from "../_types.ts";
5+
6+
interface AhizeProvider {
7+
load(options: LoadOptions): Promise<void>;
8+
identify(identity: Identity): Promise<void>;
9+
pageView(info?: { path?: string; locale?: string }): Promise<void>;
10+
shutdown(): Promise<void>;
11+
isReady(): boolean;
12+
getIdentity(): IdentityState;
13+
}
14+
15+
interface RouterLike {
16+
events: {
17+
subscribe: (cb: (event: { url?: string; constructor: { name: string } }) => void) => {
18+
unsubscribe(): void;
19+
};
20+
};
21+
}
22+
23+
export class AhizeAngularService<T extends LoadOptions> {
24+
private subscription: { unsubscribe(): void } | undefined;
25+
26+
constructor(
27+
private readonly provider: AhizeProvider,
28+
private readonly options: T,
29+
private readonly router?: RouterLike,
30+
) {}
31+
32+
async init(identity?: Identity): Promise<void> {
33+
if (typeof window === "undefined") return;
34+
await this.provider.load(this.options);
35+
if (identity) await this.provider.identify(identity);
36+
37+
if (this.router) {
38+
this.subscription = this.router.events.subscribe((event) => {
39+
if (event.constructor.name === "NavigationEnd" && event.url) {
40+
void this.provider.pageView({ path: event.url });
41+
}
42+
});
43+
}
44+
}
45+
46+
identify(identity: Identity): Promise<void> {
47+
return this.provider.identify(identity);
48+
}
49+
50+
pageView(info?: { path?: string; locale?: string }): Promise<void> {
51+
return this.provider.pageView(info);
52+
}
53+
54+
isReady(): boolean {
55+
return this.provider.isReady();
56+
}
57+
58+
getIdentity(): IdentityState {
59+
return this.provider.getIdentity();
60+
}
61+
62+
destroy(): void {
63+
this.subscription?.unsubscribe();
64+
void this.provider.shutdown();
65+
}
66+
}

src/adapters/astro.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Astro island integration — `<AhizeIsland client:idle>` boots a provider on
2+
// the client side. Pure framework boundary helper since Astro just renders
3+
// strings server-side; the actual mount runs the provider's load() in the
4+
// browser.
5+
6+
import type { Identity, LoadOptions } from "../_types.ts";
7+
8+
interface AhizeProvider {
9+
load(options: LoadOptions): Promise<void>;
10+
identify(identity: Identity): Promise<void>;
11+
pageView(info?: { path?: string; locale?: string }): Promise<void>;
12+
}
13+
14+
export interface AstroAhizeOptions<T extends LoadOptions> {
15+
provider: AhizeProvider;
16+
options: T;
17+
identity?: Identity;
18+
/** Listen on the Astro view-transitions:after-swap event for pageView. */
19+
autoPageView?: boolean;
20+
}
21+
22+
export async function mountAhize<T extends LoadOptions>(opts: AstroAhizeOptions<T>): Promise<void> {
23+
if (typeof window === "undefined") return;
24+
await opts.provider.load(opts.options);
25+
if (opts.identity) await opts.provider.identify(opts.identity);
26+
27+
if (opts.autoPageView !== false) {
28+
document.addEventListener("astro:after-swap", () => {
29+
void opts.provider.pageView({ path: location.pathname + location.search });
30+
});
31+
}
32+
}

src/adapters/next.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Next.js adapters — both App Router and Pages Router.
2+
//
3+
// App Router usage:
4+
// "use client";
5+
// import { Ahize } from "ahize/next";
6+
// import * as intercom from "ahize/intercom";
7+
// <Ahize provider={intercom} options={{ appId: "..." }} />
8+
//
9+
// Pages Router usage:
10+
// import { Ahize } from "ahize/next";
11+
// inside _app.tsx return <Ahize ... />
12+
//
13+
// pageView() auto-fires on usePathname() change. Bring your own React.
14+
15+
import type { Identity, IdentityState, LoadOptions } from "../_types.ts";
16+
17+
interface AhizeProvider {
18+
load(options: LoadOptions): Promise<void>;
19+
identify(identity: Identity): Promise<void>;
20+
pageView(info?: { path?: string; locale?: string }): Promise<void>;
21+
shutdown(): Promise<void>;
22+
destroy(): Promise<void>;
23+
onIdentityChange(listener: (next: IdentityState, prev: IdentityState) => void): () => void;
24+
isReady(): boolean;
25+
}
26+
27+
interface ReactLike {
28+
useEffect(cb: () => void | (() => void), deps?: readonly unknown[]): void;
29+
createElement(
30+
tag: string | unknown,
31+
props?: Record<string, unknown>,
32+
...children: unknown[]
33+
): unknown;
34+
}
35+
36+
interface NextAppRouterHooks {
37+
/** next/navigation usePathname() */
38+
usePathname?: () => string;
39+
/** next/navigation useSearchParams() */
40+
useSearchParams?: () => { toString(): string };
41+
}
42+
43+
export interface NextAhizeOptions<T extends LoadOptions> {
44+
provider: AhizeProvider;
45+
options: T;
46+
/** Optional identity to apply after load(). */
47+
identity?: Identity;
48+
/** Auto-fire pageView on App Router path change. */
49+
autoPageView?: boolean;
50+
}
51+
52+
export function createAhizeComponent(React: ReactLike, nextNav?: NextAppRouterHooks) {
53+
return function Ahize<T extends LoadOptions>(props: NextAhizeOptions<T>) {
54+
React.useEffect(() => {
55+
let mounted = true;
56+
props.provider.load(props.options).then(() => {
57+
if (mounted && props.identity) void props.provider.identify(props.identity);
58+
});
59+
return () => {
60+
mounted = false;
61+
};
62+
}, [props.provider, JSON.stringify(props.options)]);
63+
64+
const pathname = nextNav?.usePathname?.();
65+
const search = nextNav?.useSearchParams?.()?.toString();
66+
React.useEffect(() => {
67+
if (!props.autoPageView || !pathname) return;
68+
void props.provider.pageView({ path: search ? `${pathname}?${search}` : pathname });
69+
}, [pathname, search, props.autoPageView]);
70+
71+
return null;
72+
};
73+
}

src/adapters/nuxt.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Nuxt 3 plugin factory. Use inside `~/plugins/ahize.client.ts`:
2+
//
3+
// import { defineNuxtPlugin } from "#app";
4+
// import { useNuxtApp } from "#imports";
5+
// import * as intercom from "ahize/intercom";
6+
// import { createNuxtAhizePlugin } from "ahize/nuxt";
7+
//
8+
// export default defineNuxtPlugin(createNuxtAhizePlugin({
9+
// provider: intercom,
10+
// options: { appId: "abc" },
11+
// }));
12+
13+
import type { Identity, IdentityState, LoadOptions } from "../_types.ts";
14+
15+
interface AhizeProvider {
16+
load(options: LoadOptions): Promise<void>;
17+
identify(identity: Identity): Promise<void>;
18+
shutdown(): Promise<void>;
19+
pageView(info?: { path?: string; locale?: string }): Promise<void>;
20+
isReady(): boolean;
21+
getIdentity(): IdentityState;
22+
}
23+
24+
interface NuxtPluginContext {
25+
provide?: (name: string, value: unknown) => void;
26+
$router?: { afterEach?: (cb: (to: { fullPath: string }) => void) => void };
27+
hook?: (name: string, cb: (...args: unknown[]) => void) => void;
28+
}
29+
30+
export interface NuxtAhizeOptions<T extends LoadOptions> {
31+
provider: AhizeProvider;
32+
options: T;
33+
identity?: Identity;
34+
autoPageView?: boolean;
35+
/** Inject under $ahize, default true. */
36+
provide?: boolean;
37+
}
38+
39+
export function createNuxtAhizePlugin<T extends LoadOptions>(opts: NuxtAhizeOptions<T>) {
40+
return async (nuxtApp: NuxtPluginContext) => {
41+
if (typeof window === "undefined") return;
42+
await opts.provider.load(opts.options);
43+
if (opts.identity) void opts.provider.identify(opts.identity);
44+
45+
if (opts.autoPageView !== false && nuxtApp.$router?.afterEach) {
46+
nuxtApp.$router.afterEach((to) => {
47+
void opts.provider.pageView({ path: to.fullPath });
48+
});
49+
}
50+
51+
if (opts.provide !== false) {
52+
nuxtApp.provide?.("ahize", opts.provider);
53+
}
54+
};
55+
}

src/adapters/partytown.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Partytown adapter — sugar around the existing `partytown` flag on every
2+
// provider's load(). Use this when you want the provider's CDN script to run
3+
// in a worker via Builder's Partytown library.
4+
//
5+
// Setup:
6+
// 1. Install @builder.io/partytown and add <PartytownScript /> to your <head>.
7+
// 2. Forward the provider URL via Partytown's forward config:
8+
// forwardSettings([
9+
// 'Intercom', 'intercomSettings',
10+
// 'Tawk_API', 'Tawk_LoadStart',
11+
// // …
12+
// ])
13+
// 3. Load with `partytown: true`.
14+
15+
import type { ProviderName } from "../_types.ts";
16+
17+
const FORWARD: Partial<Record<ProviderName, readonly string[]>> = {
18+
intercom: ["Intercom", "intercomSettings"],
19+
crisp: ["$crisp", "CRISP_WEBSITE_ID", "CRISP_TOKEN_ID", "CRISP_RUNTIME_CONFIG"],
20+
tawk: ["Tawk_API", "Tawk_LoadStart"],
21+
zendesk: ["zE", "zESettings"],
22+
hubspot: ["HubSpotConversations", "hsConversationsSettings", "hsConversationsOnReady", "_hsq"],
23+
chatwoot: ["chatwootSDK", "$chatwoot", "chatwootSettings"],
24+
livechat: ["__lc", "LiveChatWidget"],
25+
drift: ["drift", "driftt"],
26+
freshchat: ["fcWidget", "fcSettings"],
27+
olark: ["olark"],
28+
userlike: ["userlikeMessenger"],
29+
helpscout: ["Beacon"],
30+
smartsupp: ["smartsupp", "_smartsupp"],
31+
liveagent: ["LiveAgent"],
32+
gist: ["gist", "gistAppId"],
33+
jivochat: [
34+
"jivo_api",
35+
"jivo_onLoadCallback",
36+
"jivo_onOpen",
37+
"jivo_onClose",
38+
"jivo_onMessageSent",
39+
],
40+
tidio: ["tidioChatApi"],
41+
sendbird: ["__sb_widget_settings"],
42+
};
43+
44+
/**
45+
* Returns the list of `forwardSettings` Partytown needs for a given provider.
46+
* Pass these (flat-spread across providers) into the @builder.io/partytown
47+
* <PartytownScript forward={...} />.
48+
*/
49+
export function partytownForward(...providers: ProviderName[]): string[] {
50+
const out = new Set<string>();
51+
for (const p of providers) for (const k of FORWARD[p] ?? []) out.add(k);
52+
return [...out];
53+
}
54+
55+
/**
56+
* Convenience: returns a PartytownConfig fragment ready to merge into the
57+
* Partytown script tag's `data-config` attribute.
58+
*/
59+
export function partytownConfig(...providers: ProviderName[]): { forward: string[] } {
60+
return { forward: partytownForward(...providers) };
61+
}

0 commit comments

Comments
 (0)