feat: simplify app setup and make hooks available as top-level imports

This commit is contained in:
Guido D'Orsi
2025-01-07 11:57:19 +01:00
parent 6de44dd8d1
commit 9dd8d9510b
28 changed files with 839 additions and 442 deletions

View File

@@ -0,0 +1,7 @@
---
"jazz-svelte": minor
---
Change the way the JazzProvider is created and make the API available as top-level imports.
This is a breaking change.

View File

@@ -0,0 +1,7 @@
---
"jazz-vue": minor
---
Change the way the JazzProvider is created and make the composables available as top-level imports.
This is a breaking change.

View File

@@ -1,6 +1,5 @@
---
"jazz-react": minor
"jazz-svelte": minor
---
Change the way the JazzProvider is created and make the hooks available as top-level imports.

View File

@@ -9,9 +9,9 @@
</template>
<script setup lang="ts">
import { useAccount } from "jazz-vue";
import AppContainer from "./components/AppContainer.vue";
import TopBar from "./components/TopBar.vue";
import { useAccount } from "./main";
const { me, logOut } = useAccount();
</script>

View File

@@ -1,13 +1,9 @@
import { DemoAuthBasicUI, createJazzVueApp, useDemoAuth } from "jazz-vue";
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import "./index.css";
import router from "./router";
const Jazz = createJazzVueApp();
export const { useAccount, useCoState } = Jazz;
const { JazzProvider } = Jazz;
const RootComponent = defineComponent({
name: "RootComponent",
setup() {

View File

@@ -26,12 +26,12 @@
<script lang="ts">
import type { ID } from "jazz-tools";
import { useCoState } from "jazz-vue";
import { type PropType, computed, defineComponent, ref } from "vue";
import ChatBody from "../components/ChatBody.vue";
import ChatBubble from "../components/ChatBubble.vue";
import ChatInput from "../components/ChatInput.vue";
import EmptyChatMessage from "../components/EmptyChatMessage.vue";
import { useCoState } from "../main";
import { Chat, Message } from "../schema";
export default defineComponent({

View File

@@ -4,8 +4,8 @@
<script setup lang="ts">
import { Group } from "jazz-tools";
import { useAccount } from "jazz-vue";
import { useRouter } from "vue-router";
import { useAccount } from "../main";
import { Chat } from "../schema";
const router = useRouter();

View File

@@ -63,7 +63,7 @@ main {
</style>
<script setup lang="ts">
import { useAccount } from "./main";
import { useAccount } from "jazz-vue";
const { me, logOut } = useAccount();
</script>

View File

@@ -1,13 +1,15 @@
import { DemoAuthBasicUI, createJazzVueApp, useDemoAuth } from "jazz-vue";
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import router from "./router";
import { ToDoAccount } from "./schema";
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount });
export const { useAccount, useCoState } = Jazz;
const { JazzProvider } = Jazz;
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const RootComponent = defineComponent({
name: "RootComponent",
@@ -18,6 +20,7 @@ const RootComponent = defineComponent({
h(
JazzProvider,
{
AccountSchema: ToDoAccount,
auth: authMethod.value,
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},

View File

@@ -64,9 +64,9 @@
<script setup lang="ts">
import { Group, type ID } from "jazz-tools";
import { useAccount, useCoState } from "jazz-vue";
import { ref, toRaw, watch } from "vue";
import { computed } from "vue";
import { useAccount, useCoState } from "../main";
import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
const { me } = useAccount();

View File

@@ -82,48 +82,3 @@ useAcceptInvite({
});
```
</CodeGroup>
<ContentByFramework framework="react">
`useAcceptInvite` is imported from `jazz-react`.
<CodeGroup>
```ts
import { useAcceptInvite } from "jazz-react";
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native">
`useAcceptInvite` is exported from your Jazz app.
<CodeGroup>
```ts
const Jazz = createJazzReactNativeApp();
export const { useAcceptInvite } = Jazz;
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="vue">
`useAcceptInvite` is exported from your Jazz app.
<CodeGroup>
```ts
const Jazz = createJazzVueApp();
export const { useAcceptInvite } = Jazz;
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="svelte">
`useAcceptInvite` is exported from your Jazz app.
<CodeGroup>
```ts
const Jazz = createJazzApp();
export const { useAcceptInvite } = Jazz;
```
</CodeGroup>
</ContentByFramework>
...more docs coming soon

View File

@@ -108,15 +108,17 @@ Update the `src/main.ts` file to integrate Jazz:
<CodeGroup>
```typescript
import "./assets/main.css";
import { DemoAuthBasicUI, createJazzVueApp, useDemoAuth } from "jazz-vue";
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import router from "./router";
import { ToDoAccount } from "./schema";
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount });
export const { useAccount, useCoState } = Jazz;
const { JazzProvider } = Jazz;
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const RootComponent = defineComponent({
name: "RootComponent",
@@ -127,6 +129,7 @@ const RootComponent = defineComponent({
h(
JazzProvider,
{
AccountSchema: ToDoAccount,
auth: authMethod.value,
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
@@ -198,7 +201,7 @@ Update the `App.vue` file to include logout functionality:
</template>
<script setup lang="ts">
import { useAccount } from "./main";
import { useAccount } from "jazz-vue";
const { me, logOut } = useAccount();
</script>
@@ -215,7 +218,7 @@ Subscribe to a CoValue inside `src/views/HomeView.vue`:
import { Group, type ID } from "jazz-tools";
import { ref, toRaw, watch } from "vue";
import { computed } from "vue";
import { useAccount, useCoState } from "../main";
import { useAccount, useCoState } from "jazz-vue";
import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
const { me } = useAccount();

View File

@@ -1,6 +1,6 @@
pre-commit:
commands:
check:
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,vue,svelte}"
run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
stage_fixed: true

View File

@@ -2,7 +2,7 @@
"name": "jazz-monorepo",
"private": true,
"type": "module",
"workspaces": ["packages/*", "examples/*"],
"workspaces": ["packages/*", "examples/*", "starters/*"],
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
"engines": {
"node": ">=22.0.0"

View File

@@ -5,14 +5,24 @@
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
}
},
"dependencies": {
"@scure/bip39": "^1.3.0",
"cojson": "workspace:*",
"jazz-browser": "workspace:*",
"jazz-tools": "workspace:*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"rollup-plugin-node-externals": "^8.0.0",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.2.4",

View File

@@ -2,7 +2,7 @@ import { AgentSecret } from "cojson";
import { BrowserDemoAuth } from "jazz-browser";
import { Account, ID } from "jazz-tools";
import { onUnmounted, reactive, ref } from "vue";
import { logoutHandler } from "../createJazzVueApp.js";
import { logoutHandler } from "../provider.js";
export type DemoAuthState = (
| {

View File

@@ -0,0 +1,250 @@
import {
BrowserContext,
BrowserGuestContext,
consumeInviteLinkFromWindowLocation,
} from "jazz-browser";
import {
Account,
AnonymousJazzAgent,
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
subscribeToCoValue,
} from "jazz-tools";
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
ComputedRef,
MaybeRef,
Ref,
ShallowRef,
computed,
inject,
onMounted,
onUnmounted,
ref,
shallowRef,
toRaw,
unref,
watch,
} from "vue";
import { JazzContextSymbol, RegisteredAccount } from "./provider.js";
export const logoutHandler = ref<() => void>();
function useJazzContext() {
const context =
inject<Ref<BrowserContext<RegisteredAccount> | BrowserGuestContext>>(
JazzContextSymbol,
);
if (!context) {
throw new Error("useJazzContext must be used within a JazzProvider");
}
return context;
}
export function createUseAccountComposables<Acc extends Account>() {
function useAccount(): {
me: ComputedRef<Acc>;
logOut: () => void;
};
function useAccount<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined>;
logOut: () => void;
};
function useAccount<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<Acc | DeeplyLoaded<Acc, D> | undefined>;
logOut: () => void;
} {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAccount must be used within a JazzProvider");
}
if (!("me" in context.value)) {
throw new Error(
"useAccount can't be used in a JazzProvider with auth === 'guest' - consider using useAccountOrGuest()",
);
}
const contextMe = context.value.me as Acc;
const me = useCoState<Acc, D>(
contextMe.constructor as CoValueClass<Acc>,
contextMe.id,
depth,
);
return {
me: computed(() => {
const value =
depth === undefined
? me.value || toRaw((context.value as BrowserContext<Acc>).me)
: me.value;
return value ? toRaw(value) : value;
}),
logOut: context.value.logOut,
};
}
function useAccountOrGuest(): {
me: ComputedRef<Acc | AnonymousJazzAgent>;
};
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent>;
};
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<
Acc | DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent
>;
} {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAccountOrGuest must be used within a JazzProvider");
}
const contextMe = computed(() =>
"me" in context.value ? (context.value.me as Acc) : undefined,
);
const me = useCoState<Acc, D>(
contextMe.value?.constructor as CoValueClass<Acc>,
contextMe.value?.id,
depth,
);
if ("me" in context.value) {
return {
me: computed(() =>
depth === undefined
? me.value || toRaw((context.value as BrowserContext<Acc>).me)
: me.value,
),
};
} else {
return {
me: computed(() => toRaw((context.value as BrowserGuestContext).guest)),
};
}
}
return {
useAccount,
useAccountOrGuest,
};
}
const { useAccount, useAccountOrGuest } =
createUseAccountComposables<RegisteredAccount>();
export { useAccount, useAccountOrGuest };
export function useCoState<V extends CoValue, D>(
Schema: CoValueClass<V>,
id: MaybeRef<ID<V> | undefined>,
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
): Ref<DeeplyLoaded<V, D> | undefined> {
const state: ShallowRef<DeeplyLoaded<V, D> | undefined> =
shallowRef(undefined);
const context = useJazzContext();
if (!context.value) {
throw new Error("useCoState must be used within a JazzProvider");
}
let unsubscribe: (() => void) | undefined;
watch(
[() => unref(id), () => context, () => Schema, () => depth],
() => {
if (unsubscribe) unsubscribe();
const idValue = unref(id);
if (!idValue) return;
unsubscribe = subscribeToCoValue(
Schema,
idValue,
"me" in context.value
? toRaw(context.value.me)
: toRaw(context.value.guest),
depth,
(value) => {
state.value = value;
},
undefined,
true,
);
},
{ deep: true, immediate: true },
);
onUnmounted(() => {
if (unsubscribe) unsubscribe();
});
const computedState = computed(() => state.value);
return computedState;
}
export function useAcceptInvite<V extends CoValue>({
invitedObjectSchema,
onAccept,
forValueHint,
}: {
invitedObjectSchema: CoValueClass<V>;
onAccept: (projectID: ID<V>) => void;
forValueHint?: string;
}): void {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAcceptInvite must be used within a JazzProvider");
}
if (!("me" in context.value)) {
throw new Error(
"useAcceptInvite can't be used in a JazzProvider with auth === 'guest'.",
);
}
const runInviteAcceptance = () => {
const result = consumeInviteLinkFromWindowLocation({
as: toRaw((context.value as BrowserContext<RegisteredAccount>).me),
invitedObjectSchema,
forValueHint,
});
result
.then((res) => res && onAccept(res.valueID))
.catch((e) => {
console.error("Failed to accept invite", e);
});
};
onMounted(() => {
runInviteAcceptance();
});
watch(
() => onAccept,
(newOnAccept, oldOnAccept) => {
if (newOnAccept !== oldOnAccept) {
runInviteAcceptance();
}
},
);
}

View File

@@ -1,356 +0,0 @@
import {
BrowserContext,
BrowserGuestContext,
consumeInviteLinkFromWindowLocation,
createJazzBrowserContext,
} from "jazz-browser";
import {
Account,
AnonymousJazzAgent,
AuthMethod,
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
subscribeToCoValue,
} from "jazz-tools";
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Component,
ComputedRef,
MaybeRef,
PropType,
Ref,
ShallowRef,
computed,
defineComponent,
inject,
onMounted,
onUnmounted,
provide,
ref,
shallowRef,
toRaw,
unref,
watch,
} from "vue";
export const logoutHandler = ref<() => void>();
export interface JazzVueApp<Acc extends Account> {
JazzProvider: Component;
useAccount(): { me: ComputedRef<Acc>; logOut: () => void };
useAccount<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined>;
logOut: () => void;
};
useAccountOrGuest(): { me: ComputedRef<Acc | AnonymousJazzAgent> };
useAccountOrGuest<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent>;
};
useCoState<V extends CoValue, D>(
Schema: CoValueClass<V>,
id: MaybeRef<ID<V> | undefined>,
depth?: D & DepthsIn<V>,
): Ref<DeeplyLoaded<V, D> | undefined>;
useAcceptInvite<V extends CoValue>(args: {
invitedObjectSchema: CoValueClass<V>;
onAccept: (projectID: ID<V>) => void;
forValueHint?: string;
}): void;
}
const JazzContextSymbol = Symbol("JazzContext");
export function createJazzVueApp<Acc extends Account>({
AccountSchema = Account as any,
} = {}): JazzVueApp<Acc> {
const JazzProvider = defineComponent({
name: "JazzProvider",
props: {
auth: {
type: [String, Object] as PropType<AuthMethod | "guest">,
required: true,
},
peer: {
type: String as PropType<`wss://${string}` | `ws://${string}`>,
required: true,
},
storage: {
type: String as PropType<"indexedDB" | "singleTabOPFS">,
default: undefined,
},
},
setup(props, { slots }) {
const ctx = ref<BrowserContext<Acc> | BrowserGuestContext | undefined>(
undefined,
);
const key = ref(0);
provide(JazzContextSymbol, ctx);
const initializeContext = async () => {
if (ctx.value) {
ctx.value.done?.();
ctx.value = undefined;
}
try {
const context = await createJazzBrowserContext<Acc>(
props.auth === "guest"
? { peer: props.peer, storage: props.storage }
: {
AccountSchema,
auth: props.auth,
peer: props.peer,
storage: props.storage,
},
);
ctx.value = {
...context,
logOut: () => {
logoutHandler.value?.();
// context.logOut();
key.value += 1;
},
};
} catch (e) {
console.error("Error creating Jazz browser context:", e);
}
};
onMounted(() => {
void initializeContext();
});
watch(
() => key.value,
async () => {
await initializeContext();
},
);
onUnmounted(() => {
if (ctx.value) ctx.value.done?.();
});
return () => (ctx.value ? slots.default?.() : null);
},
});
function useJazzContext() {
const context =
inject<Ref<BrowserContext<Acc> | BrowserGuestContext>>(JazzContextSymbol);
if (!context) {
throw new Error("useJazzContext must be used within a JazzProvider");
}
return context;
}
function useAccount(): { me: ComputedRef<Acc>; logOut: () => void };
function useAccount<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined>;
logOut: () => void;
};
function useAccount<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<Acc | DeeplyLoaded<Acc, D> | undefined>;
logOut: () => void;
} {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAccount must be used within a JazzProvider");
}
if (!("me" in context.value)) {
throw new Error(
"useAccount can't be used in a JazzProvider with auth === 'guest' - consider using useAccountOrGuest()",
);
}
const contextMe = computed(() =>
"me" in context.value ? context.value.me : undefined,
);
const me = useCoState<Acc, D>(
contextMe.value?.constructor as CoValueClass<Acc>,
contextMe.value?.id,
depth,
);
return {
me: computed(() => {
const value =
depth === undefined
? me.value || toRaw((context.value as BrowserContext<Acc>).me)
: me.value;
return value ? toRaw(value) : value;
}),
logOut: context.value.logOut,
};
}
function useAccountOrGuest(): { me: ComputedRef<Acc | AnonymousJazzAgent> };
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth: D,
): {
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent>;
};
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<
Acc | DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent
>;
} {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAccountOrGuest must be used within a JazzProvider");
}
const contextMe = computed(() =>
"me" in context.value ? context.value.me : undefined,
);
const me = useCoState<Acc, D>(
contextMe.value?.constructor as CoValueClass<Acc>,
contextMe.value?.id,
depth,
);
if ("me" in context.value) {
return {
me: computed(() =>
depth === undefined
? me.value || toRaw((context.value as BrowserContext<Acc>).me)
: me.value,
),
};
} else {
return {
me: computed(() => toRaw((context.value as BrowserGuestContext).guest)),
};
}
}
function useCoState<V extends CoValue, D>(
Schema: CoValueClass<V>,
id: MaybeRef<ID<V> | undefined>,
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
): Ref<DeeplyLoaded<V, D> | undefined> {
const state: ShallowRef<DeeplyLoaded<V, D> | undefined> =
shallowRef(undefined);
const context = useJazzContext();
if (!context.value) {
throw new Error("useCoState must be used within a JazzProvider");
}
let unsubscribe: (() => void) | undefined;
watch(
[() => unref(id), () => context, () => Schema, () => depth],
() => {
if (unsubscribe) unsubscribe();
const idValue = unref(id);
if (!idValue) return;
unsubscribe = subscribeToCoValue(
Schema,
idValue,
"me" in context.value
? toRaw(context.value.me)
: toRaw(context.value.guest),
depth,
(value) => {
state.value = value;
},
);
},
{ deep: true, immediate: true },
);
onUnmounted(() => {
if (unsubscribe) unsubscribe();
});
const computedState = computed(() => state.value);
return computedState;
}
function useAcceptInvite<V extends CoValue>({
invitedObjectSchema,
onAccept,
forValueHint,
}: {
invitedObjectSchema: CoValueClass<V>;
onAccept: (projectID: ID<V>) => void;
forValueHint?: string;
}): void {
const context = useJazzContext();
if (!context.value) {
throw new Error("useAcceptInvite must be used within a JazzProvider");
}
if (!("me" in context.value)) {
throw new Error(
"useAcceptInvite can't be used in a JazzProvider with auth === 'guest'.",
);
}
const runInviteAcceptance = () => {
const result = consumeInviteLinkFromWindowLocation({
as: toRaw((context.value as BrowserContext<Acc>).me),
invitedObjectSchema,
forValueHint,
});
result
.then((res) => res && onAccept(res.valueID))
.catch((e) => {
console.error("Failed to accept invite", e);
});
};
onMounted(() => {
runInviteAcceptance();
});
watch(
() => onAccept,
(newOnAccept, oldOnAccept) => {
if (newOnAccept !== oldOnAccept) {
runInviteAcceptance();
}
},
);
}
return {
JazzProvider,
useAccount,
useAccountOrGuest,
useCoState,
useAcceptInvite,
};
}

View File

@@ -1,4 +1,7 @@
export * from "./createJazzVueApp.js";
export * from "./composables.js";
export { JazzProvider } from "./provider.js";
export * from "./auth/useDemoAuth.js";
export { default as DemoAuthBasicUI } from "./auth/DemoAuthBasicUI.vue";
export { default as ProgressiveImg } from "./ProgressiveImg.vue";
export { createInviteLink, parseInviteLink } from "jazz-browser";

View File

@@ -0,0 +1,106 @@
import {
BrowserContext,
BrowserGuestContext,
createJazzBrowserContext,
} from "jazz-browser";
import { Account, AccountClass, AuthMethod } from "jazz-tools";
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
PropType,
defineComponent,
onMounted,
onUnmounted,
provide,
ref,
watch,
} from "vue";
export const logoutHandler = ref<() => void>();
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface Register {}
export type RegisteredAccount = Register extends { Account: infer Acc }
? Acc
: Account;
export const JazzContextSymbol = Symbol("JazzContext");
export const JazzProvider = defineComponent({
name: "JazzProvider",
props: {
auth: {
type: [String, Object] as PropType<AuthMethod | "guest">,
required: true,
},
AccountSchema: {
type: Object as PropType<AccountClass<RegisteredAccount>>,
required: false,
},
peer: {
type: String as PropType<`wss://${string}` | `ws://${string}`>,
required: true,
},
storage: {
type: String as PropType<"indexedDB" | "singleTabOPFS">,
default: undefined,
},
},
setup(props, { slots }) {
const ctx = ref<
BrowserContext<RegisteredAccount> | BrowserGuestContext | undefined
>(undefined);
const key = ref(0);
provide(JazzContextSymbol, ctx);
const initializeContext = async () => {
if (ctx.value) {
ctx.value.done?.();
ctx.value = undefined;
}
try {
const context = await createJazzBrowserContext<RegisteredAccount>(
props.auth === "guest"
? { peer: props.peer, storage: props.storage }
: {
AccountSchema: props.AccountSchema,
auth: props.auth,
peer: props.peer,
storage: props.storage,
},
);
ctx.value = {
...context,
logOut: () => {
logoutHandler.value?.();
// context.logOut();
key.value += 1;
},
};
} catch (e) {
console.error("Error creating Jazz browser context:", e);
}
};
onMounted(() => {
void initializeContext();
});
watch(
() => key.value,
async () => {
await initializeContext();
},
);
onUnmounted(() => {
if (ctx.value) ctx.value.done?.();
});
return () => (ctx.value ? slots.default?.() : null);
},
});

View File

@@ -0,0 +1,27 @@
import { Account, AnonymousJazzAgent } from "jazz-tools";
import { getJazzContextShape } from "jazz-tools/testing";
import { provide } from "vue";
import { PropType, defineComponent, ref } from "vue";
import { JazzContextSymbol } from "./provider.js";
export const JazzTestProvider = defineComponent({
name: "JazzTestProvider",
props: {
account: {
type: Object as PropType<Account | { guest: AnonymousJazzAgent }>,
required: true,
},
},
setup(props, { slots }) {
const ctx = ref(getJazzContextShape(props.account));
provide(JazzContextSymbol, ctx);
return () => slots.default?.();
},
});
export {
createJazzTestAccount,
createJazzTestGuest,
linkAccounts,
} from "jazz-tools/testing";

View File

@@ -0,0 +1,66 @@
import { Account, AnonymousJazzAgent } from "jazz-tools";
import { createApp, defineComponent, h } from "vue";
import { JazzTestProvider } from "../testing";
export const withJazzTestSetup = <C extends (...args: any[]) => any>(
composable: C,
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
) => {
let result;
const wrapper = defineComponent({
setup() {
result = composable();
// suppress missing template warning
return () => {};
},
});
const app = createApp({
setup() {
return () =>
h(
JazzTestProvider,
{
account,
},
{
default: () => h(wrapper),
},
);
},
});
app.mount(document.createElement("div"));
// return the result and the app instance
// for testing provide/unmount
return [result, app] as [ReturnType<C>, ReturnType<typeof createApp>];
};
export function waitFor(callback: () => boolean | void) {
return new Promise<void>((resolve, reject) => {
const checkPassed = () => {
try {
return { ok: callback(), error: null };
} catch (error) {
return { ok: false, error };
}
};
let retries = 0;
const interval = setInterval(() => {
const { ok, error } = checkPassed();
if (ok !== false) {
clearInterval(interval);
resolve();
}
if (++retries > 10) {
clearInterval(interval);
reject(error);
}
}, 100);
});
}

View File

@@ -0,0 +1,53 @@
// @vitest-environment happy-dom
import { CoMap, Group, ID, co } from "jazz-tools";
import { describe, expect, it } from "vitest";
import { createInviteLink, useAcceptInvite } from "../index.js";
import { createJazzTestAccount, linkAccounts } from "../testing.js";
import { waitFor, withJazzTestSetup } from "./testUtils.js";
describe("useAcceptInvite", () => {
it("should accept the invite", async () => {
class TestMap extends CoMap {
value = co.string;
}
const account = await createJazzTestAccount();
const inviteSender = await createJazzTestAccount();
linkAccounts(account, inviteSender);
let acceptedId: ID<TestMap> | undefined;
const invitelink = createInviteLink(
TestMap.create(
{ value: "hello" },
{ owner: Group.create({ owner: inviteSender }) },
),
"reader",
);
location.href = invitelink;
withJazzTestSetup(
() =>
useAcceptInvite({
invitedObjectSchema: TestMap,
onAccept: (id) => {
acceptedId = id;
},
}),
{
account,
},
);
await waitFor(() => {
expect(acceptedId).toBeDefined();
});
const accepted = await TestMap.load(acceptedId!, account, {});
expect(accepted?.value).toEqual("hello");
});
});

View File

@@ -0,0 +1,51 @@
// @vitest-environment happy-dom
import { Account, CoMap, co } from "jazz-tools";
import { describe, expect, it } from "vitest";
import { createUseAccountComposables, useAccount } from "../composables.js";
import { createJazzTestAccount } from "../testing.js";
import { withJazzTestSetup } from "./testUtils.js";
describe("useAccount", () => {
it("should return the correct value", async () => {
const account = await createJazzTestAccount();
const [result] = withJazzTestSetup(() => useAccount(), {
account,
});
expect(result.me.value).toEqual(account);
});
it("should load nested values if requested", async () => {
class AccountRoot extends CoMap {
value = co.string;
}
class AccountSchema extends Account {
root = co.ref(AccountRoot);
migrate() {
if (!this._refs.root) {
this.root = AccountRoot.create({ value: "123" }, { owner: this });
}
}
}
const { useAccount } = createUseAccountComposables<AccountSchema>();
const account = await createJazzTestAccount({ AccountSchema });
const [result] = withJazzTestSetup(
() =>
useAccount({
root: {},
}),
{
account,
},
);
expect(result.me.value?.root?.value).toBe("123");
});
});

View File

@@ -0,0 +1,80 @@
// @vitest-environment happy-dom
import { Account, CoMap, co } from "jazz-tools";
import { describe, expect, it } from "vitest";
import {
createUseAccountComposables,
useAccountOrGuest,
} from "../composables.js";
import { createJazzTestAccount, createJazzTestGuest } from "../testing.js";
import { withJazzTestSetup } from "./testUtils.js";
describe("useAccountOrGuest", () => {
it("should return the correct me value", async () => {
const account = await createJazzTestAccount();
const [result] = withJazzTestSetup(() => useAccountOrGuest(), {
account,
});
expect(result.me.value).toEqual(account);
});
it("should return the guest agent if the account is a guest", async () => {
const account = await createJazzTestGuest();
const [result] = withJazzTestSetup(() => useAccountOrGuest(), {
account,
});
expect(result.me.value).toBe(account.guest);
});
it("should load nested values if requested", async () => {
class AccountRoot extends CoMap {
value = co.string;
}
class AccountSchema extends Account {
root = co.ref(AccountRoot);
migrate() {
if (!this._refs.root) {
this.root = AccountRoot.create({ value: "123" }, { owner: this });
}
}
}
const account = await createJazzTestAccount({ AccountSchema });
const { useAccountOrGuest } = createUseAccountComposables<AccountSchema>();
const [result] = withJazzTestSetup(
() =>
useAccountOrGuest({
root: {},
}),
{
account,
},
);
// @ts-expect-error
expect(result.me.value?.root?.value).toBe("123");
});
it("should not load nested values if the account is a guest", async () => {
const account = await createJazzTestGuest();
const [result] = withJazzTestSetup(
() =>
useAccountOrGuest({
root: {},
}),
{
account,
},
);
expect(result.me.value).toBe(account.guest);
});
});

View File

@@ -0,0 +1,127 @@
// @vitest-environment happy-dom
import { CoMap, co } from "jazz-tools";
import { createJazzTestAccount } from "jazz-tools/testing";
import { describe, expect, it } from "vitest";
import { useCoState } from "../index.js";
import { withJazzTestSetup } from "./testUtils.js";
describe("useCoState", () => {
it("should return the correct value", async () => {
class TestMap extends CoMap {
content = co.string;
}
const account = await createJazzTestAccount();
const map = TestMap.create(
{
content: "123",
},
{ owner: account },
);
const [result] = withJazzTestSetup(() => useCoState(TestMap, map.id, {}), {
account,
});
expect(result.value?.content).toBe("123");
});
it("should update the value when the coValue changes", async () => {
class TestMap extends CoMap {
content = co.string;
}
const account = await createJazzTestAccount();
const map = TestMap.create(
{
content: "123",
},
{ owner: account },
);
const [result] = withJazzTestSetup(() => useCoState(TestMap, map.id, {}), {
account,
});
expect(result.value?.content).toBe("123");
map.content = "456";
expect(result.value?.content).toBe("456");
});
it("should load nested values if requested", async () => {
class TestNestedMap extends CoMap {
content = co.string;
}
class TestMap extends CoMap {
content = co.string;
nested = co.ref(TestNestedMap);
}
const account = await createJazzTestAccount();
const map = TestMap.create(
{
content: "123",
nested: TestNestedMap.create(
{
content: "456",
},
{ owner: account },
),
},
{ owner: account },
);
const [result] = withJazzTestSetup(
() =>
useCoState(TestMap, map.id, {
nested: {},
}),
{
account,
},
);
expect(result.value?.content).toBe("123");
expect(result.value?.nested?.content).toBe("456");
});
it("should load nested values on access even if not requested", async () => {
class TestNestedMap extends CoMap {
content = co.string;
}
class TestMap extends CoMap {
content = co.string;
nested = co.ref(TestNestedMap);
}
const account = await createJazzTestAccount();
const map = TestMap.create(
{
content: "123",
nested: TestNestedMap.create(
{
content: "456",
},
{ owner: account },
),
},
{ owner: account },
);
const [result] = withJazzTestSetup(() => useCoState(TestMap, map.id, {}), {
account,
});
expect(result.value?.content).toBe("123");
expect(result.value?.nested?.content).toBe("456");
});
});

View File

@@ -1,27 +1,27 @@
import path from "path";
import vue from "@vitejs/plugin-vue";
import depsExternal from "rollup-plugin-node-externals";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
// @ts-expect-error types
plugins: [vue(), dts({ include: ["src/**/*.ts"], outDir: "dist" })],
plugins: [
vue(),
dts({ include: ["src/**/*.ts"], outDir: "dist" }),
depsExternal(),
],
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
entry: {
index: path.resolve(__dirname, "src/index.ts"),
testing: path.resolve(__dirname, "src/testing.ts"),
},
name: "JazzVue",
formats: ["es"],
fileName: (format) => `index.js`,
fileName: (_, entryName) => `${entryName}.js`,
},
rollupOptions: {
external: ["vue", "jazz-browser", "jazz-tools"],
output: {
globals: {
vue: "Vue",
"jazz-browser": "JazzBrowser",
"jazz-tools": "JazzTools",
},
},
external: ["vue"],
},
},
});

16
pnpm-lock.yaml generated
View File

@@ -1984,9 +1984,6 @@ importers:
packages/jazz-vue:
dependencies:
'@scure/bip39':
specifier: ^1.3.0
version: 1.5.0
cojson:
specifier: workspace:*
version: link:../cojson
@@ -2000,6 +1997,9 @@ importers:
'@vitejs/plugin-vue':
specifier: ^5.1.4
version: 5.2.1(vite@5.4.11(@types/node@22.10.2)(lightningcss@1.28.2)(terser@5.37.0))(vue@3.5.13(typescript@5.6.3))
rollup-plugin-node-externals:
specifier: ^8.0.0
version: 8.0.0(rollup@4.28.1)
typescript:
specifier: ~5.6.2
version: 5.6.3
@@ -9880,6 +9880,12 @@ packages:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
rollup-plugin-node-externals@8.0.0:
resolution: {integrity: sha512-2HIOpWsWn5DqBoYl6iCAmB4kd5GoGbF68PR4xKR1YBPvywiqjtYvDEjHFodyqRL51iAMDITP074Zxs0OKs6F+g==}
engines: {node: '>= 21 || ^20.6.0 || ^18.19.0'}
peerDependencies:
rollup: ^4.0.0
rollup@4.28.1:
resolution: {integrity: sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -20297,6 +20303,10 @@ snapshots:
dependencies:
glob: 10.4.5
rollup-plugin-node-externals@8.0.0(rollup@4.28.1):
dependencies:
rollup: 4.28.1
rollup@4.28.1:
dependencies:
'@types/estree': 1.0.6